Skip to content

Commit

Permalink
Showing 10 changed files with 224 additions and 25 deletions.
3 changes: 3 additions & 0 deletions spec/std/data/test_template2.ecr
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<%= 1 -%>
<%= 2 -%>
<%= 3 -%>
1 change: 1 addition & 0 deletions spec/std/data/test_template3.ecr
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%- 2.times do |i| %><%= i %><% end -%>
2 changes: 2 additions & 0 deletions spec/std/data/test_template4.ecr
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
hi
<%- 2.times do |i| %><%= i %><% end -%>
4 changes: 4 additions & 0 deletions spec/std/data/test_template5.ecr
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
hi
<% 2.times do |i| -%>
<%= i %>
<% end -%>
1 change: 1 addition & 0 deletions spec/std/data/test_template6.ecr
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= "string with -%" %>
66 changes: 66 additions & 0 deletions spec/std/ecr/ecr_lexer_spec.cr
Original file line number Diff line number Diff line change
@@ -29,6 +29,8 @@ describe "ECR::Lexer" do
token.value.should eq(" foo ")
token.line_number.should eq(1)
token.column_number.should eq(9)
token.supress_leading?.should be_false
token.supress_trailing?.should be_false

token = lexer.next_token
token.type.should eq(:STRING)
@@ -40,6 +42,38 @@ describe "ECR::Lexer" do
token.type.should eq(:EOF)
end

it "lexes with <%- %>" do
lexer = ECR::Lexer.new("<%- foo %>")

token = lexer.next_token
token.type.should eq(:CONTROL)
token.value.should eq(" foo ")
token.line_number.should eq(1)
token.column_number.should eq(4)
token.supress_leading?.should be_true
token.supress_trailing?.should be_false
end

it "lexes with <% -%>" do
lexer = ECR::Lexer.new("<% foo -%>")

token = lexer.next_token
token.type.should eq(:CONTROL)
token.value.should eq(" foo ")
token.line_number.should eq(1)
token.column_number.should eq(3)
token.supress_leading?.should be_false
token.supress_trailing?.should be_true
end

it "lexes with -% inside string" do
lexer = ECR::Lexer.new("<% \"-%\" %>")

token = lexer.next_token
token.type.should eq(:CONTROL)
token.value.should eq(" \"-%\" ")
end

it "lexes with <%= %>" do
lexer = ECR::Lexer.new("hello <%= foo %> bar")

@@ -54,6 +88,8 @@ describe "ECR::Lexer" do
token.value.should eq(" foo ")
token.line_number.should eq(1)
token.column_number.should eq(10)
token.supress_leading?.should be_false
token.supress_trailing?.should be_false

token = lexer.next_token
token.type.should eq(:STRING)
@@ -65,6 +101,18 @@ describe "ECR::Lexer" do
token.type.should eq(:EOF)
end

it "lexes with <%= -%>" do
lexer = ECR::Lexer.new("<%= foo -%>")

token = lexer.next_token
token.type.should eq(:OUTPUT)
token.value.should eq(" foo ")
token.line_number.should eq(1)
token.column_number.should eq(4)
token.supress_leading?.should be_false
token.supress_trailing?.should be_true
end

it "lexes with <%# %>" do
lexer = ECR::Lexer.new("hello <%# foo %> bar")

@@ -79,6 +127,8 @@ describe "ECR::Lexer" do
token.value.should eq("# foo ")
token.line_number.should eq(1)
token.column_number.should eq(9)
token.supress_leading?.should be_false
token.supress_trailing?.should be_false

token = lexer.next_token
token.type.should eq(:STRING)
@@ -90,6 +140,18 @@ describe "ECR::Lexer" do
token.type.should eq(:EOF)
end

it "lexes with <%# -%>" do
lexer = ECR::Lexer.new("<%# foo -%>")

token = lexer.next_token
token.type.should eq(:CONTROL)
token.value.should eq("# foo ")
token.line_number.should eq(1)
token.column_number.should eq(3)
token.supress_leading?.should be_false
token.supress_trailing?.should be_true
end

it "lexes with <%% %>" do
lexer = ECR::Lexer.new("hello <%% foo %> bar")

@@ -104,6 +166,8 @@ describe "ECR::Lexer" do
token.value.should eq("<% foo %>")
token.line_number.should eq(1)
token.column_number.should eq(10)
token.supress_leading?.should be_false
token.supress_trailing?.should be_false

token = lexer.next_token
token.type.should eq(:STRING)
@@ -129,6 +193,8 @@ describe "ECR::Lexer" do
token.value.should eq("<%= foo %>")
token.line_number.should eq(1)
token.column_number.should eq(10)
token.supress_leading?.should be_false
token.supress_trailing?.should be_false

token = lexer.next_token
token.type.should eq(:STRING)
30 changes: 30 additions & 0 deletions spec/std/ecr/ecr_spec.cr
Original file line number Diff line number Diff line change
@@ -34,4 +34,34 @@ describe "ECR" do
view = ECRSpecHelloView.new("world!")
view.to_s.strip.should eq("Hello world! 012")
end

it "does with <%= -%>" do
io = MemoryIO.new
ECR.embed "#{__DIR__}/../data/test_template2.ecr", io
io.to_s.should eq("123")
end

it "does with <%- %> (1)" do
io = MemoryIO.new
ECR.embed "#{__DIR__}/../data/test_template3.ecr", io
io.to_s.should eq("01")
end

it "does with <%- %> (2)" do
io = MemoryIO.new
ECR.embed "#{__DIR__}/../data/test_template4.ecr", io
io.to_s.should eq("hi\n01")
end

it "does with <% -%>" do
io = MemoryIO.new
ECR.embed "#{__DIR__}/../data/test_template5.ecr", io
io.to_s.should eq("hi\n 0\n 1\n ")
end

it "does with -% inside string" do
io = MemoryIO.new
ECR.embed "#{__DIR__}/../data/test_template6.ecr", io
io.to_s.should eq("string with -%")
end
end
23 changes: 15 additions & 8 deletions src/ecr.cr
Original file line number Diff line number Diff line change
@@ -5,6 +5,13 @@
# There are `<%= %>` and `<% %>` syntax. The former will render returned values.
# The latter will not, but instead serve to control the structure as we do in Crystal.
#
# Using a dash inside `<...>` either eliminates previous indentation or removes the next newline:
#
# * `<%- ... %>`: removes previous indentation
# * `<% ... -%>`: removes next newline
# * `<%-= ... %>`: removes previous indentation
# * `<%= ... -%>`: removes next newline
#
# Quick Example:
#
# require "ecr"
@@ -25,11 +32,11 @@
# Using logical statements:
#
# # greeting.ecr
# <% if @name %>
# Greeting, <%= @name %>!
# <% else %>
# Greeting!
# <% end %>
# <%- if @name -%>
# Greeting, <%= @name %>!
# <%- else -%>
# Greeting!
# <%- end -%>
#
# Greeting.new(nil).to_s
# #=> Greeting!
@@ -49,9 +56,9 @@
# end
#
# # greeting.ecr
# <% @names.each do |name| %>
# Hi, <%= name %>!
# <% end %>
# <%- @names.each do |name| -%>
# Hi, <%= name %>!
# <%- end -%>
#
# Greeting.new("John", "Zoe", "Ben").to_s
# #=> Hi, John!
51 changes: 44 additions & 7 deletions src/ecr/lexer.cr
Original file line number Diff line number Diff line change
@@ -5,12 +5,16 @@ class ECR::Lexer
property value : String
property line_number : Int32
property column_number : Int32
property? supress_leading : Bool
property? supress_trailing : Bool

def initialize
@type = :EOF
@value = ""
@line_number = 0
@column_number = 0
@supress_leading = false
@supress_trailing = false
end
end

@@ -33,6 +37,13 @@ class ECR::Lexer
next_char
next_char

if current_char == '-'
@token.supress_leading = true
next_char
else
@token.supress_leading = false
end

case current_char
when '='
next_char
@@ -90,15 +101,31 @@ class ECR::Lexer
when '\n'
@line_number += 1
@column_number = 0
when '-'
if peek_next_char == '%'
# We need to peek another char, so we remember
# where we are, check that, and then go back
pos = @reader.pos
column_number = @column_number

next_char

is_end = peek_next_char == '>'
@reader.pos = pos
@column_number = column_number

if is_end
@token.supress_trailing = true
setup_control_token(start_pos, is_escape)
raise "expecting '>' after '-%'" if current_char != '>'
next_char
break
end
end
when '%'
if peek_next_char == '>'
@token.value = if is_escape
"<%#{string_range(start_pos, current_pos + 2)}"
else
string_range(start_pos)
end
next_char
next_char
@token.supress_trailing = false
setup_control_token(start_pos, is_escape)
break
end
end
@@ -109,6 +136,16 @@ class ECR::Lexer
@token
end

private def setup_control_token(start_pos, is_escape)
@token.value = if is_escape
"<%#{string_range(start_pos, current_pos + 2)}"
else
string_range(start_pos)
end
next_char
next_char
end

private def copy_location_info_to_token
@token.line_number = @line_number
@token.column_number = @column_number
68 changes: 58 additions & 10 deletions src/ecr/processor.cr
Original file line number Diff line number Diff line change
@@ -13,27 +13,48 @@ module ECR
# :nodoc:
def process_string(string, filename, buffer_name = DefaultBufferName)
lexer = Lexer.new string
token = lexer.next_token

String.build do |str|
while true
token = lexer.next_token
case token.type
when :STRING
string = token.value
token = lexer.next_token

string = supress_leading_indentation(token, string)

str << buffer_name
str << " << "
token.value.inspect(str)
string.inspect(str)
str << "\n"
when :OUTPUT
string = token.value
line_number = token.line_number
column_number = token.column_number
supress_trailing = token.supress_trailing?
token = lexer.next_token

supress_trailing_whitespace(token, supress_trailing)

str << "("
append_loc(str, filename, token)
str << token.value
append_loc(str, filename, line_number, column_number)
str << string
str << ").to_s "
str << buffer_name
str << "\n"
when :CONTROL
append_loc(str, filename, token)
str << " " unless token.value.starts_with?(' ')
str << token.value
string = token.value
line_number = token.line_number
column_number = token.column_number
supress_trailing = token.supress_trailing?
token = lexer.next_token

supress_trailing_whitespace(token, supress_trailing)

append_loc(str, filename, line_number, column_number)
str << " " unless string.starts_with?(' ')
str << string
str << "\n"
when :EOF
break
@@ -42,13 +63,40 @@ module ECR
end
end

private def append_loc(str, filename, token)
private def supress_leading_indentation(token, string)
# To supress leading indentation we find the last index of a newline and
# then check if all chars after that are whitespace.
# We use a Char::Reader for this for maximum efficiency.
if (token.type == :OUTPUT || token.type == :CONTROL) && token.supress_leading?
char_index = string.rindex('\n')
char_index = char_index ? char_index + 1 : 0
byte_index = string.char_index_to_byte_index(char_index).not_nil!
reader = Char::Reader.new(string)
reader.pos = byte_index
while reader.current_char.whitespace? && reader.has_next?
reader.next_char
end
if reader.pos == string.bytesize
string = string.byte_slice(0, byte_index)
end
end
string
end

private def supress_trailing_whitespace(token, supress_trailing)
if supress_trailing && token.type == :STRING
newline_index = token.value.index('\n')
token.value = token.value[newline_index + 1..-1] if newline_index
end
end

private def append_loc(str, filename, line_number, column_number)
str << %(#<loc:")
str << filename
str << %(",)
str << token.line_number
str << line_number
str << %(,)
str << token.column_number
str << column_number
str << %(>)
end
end

0 comments on commit 30fdda9

Please sign in to comment.