hamlet 0.2.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +15 -5
- data/hamlet.gemspec +5 -2
- data/lib/hamlet/forked_slim_parser.rb +0 -6
- data/lib/hamlet/parser.rb +114 -66
- data/test/slim/test_chain_manipulation.rb +2 -2
- data/test/slim/test_code_blocks.rb +10 -10
- data/test/slim/test_code_escaping.rb +4 -4
- data/test/slim/test_code_evaluation.rb +36 -36
- data/test/slim/test_code_output.rb +22 -20
- data/test/slim/test_code_structure.rb +21 -21
- data/test/slim/test_embedded_engines.rb +16 -16
- data/test/slim/test_html_escaping.rb +6 -7
- data/test/slim/test_html_structure.rb +51 -52
- data/test/slim/test_parser_errors.rb +29 -27
- data/test/slim/test_pretty.rb +10 -8
- data/test/slim/test_ruby_errors.rb +13 -11
- data/test/slim/test_sections.rb +5 -5
- data/test/slim/test_slim_template.rb +3 -3
- data/test/slim/test_text_interpolation.rb +5 -5
- data/test/slim/test_wrapper.rb +2 -2
- metadata +34 -24
data/README.md
CHANGED
@@ -29,22 +29,32 @@ see, it is just HTML! Designers love Hamlet because it is just HTML! Closing tag
|
|
29
29
|
You can see the [original hamlet templating langauge](http://www.yesodweb.com/book/templates) and the
|
30
30
|
[javascript port](hamlet: https://github.com/gregwebs/hamlet.js).
|
31
31
|
|
32
|
-
This Hamlet works on top of [slim](https://github.com/stonean/slim/). Please
|
32
|
+
This Hamlet works on top of [slim](https://github.com/stonean/slim/). Please take a look at the [slim documentation](http://slim-lang.com) if you are looking to see if a more advanced feature is supported.
|
33
33
|
|
34
|
-
|
34
|
+
## Difference with Slim
|
35
|
+
|
36
|
+
The most important difference is that hamlet always uses angle brackets. Hamlet also does not require attributes to be quoted - unquoted is considered a normal html attribute value and quotes will be added. Hamlet also uses a '#' for code comments and the normal <!-- for HTML comments. Hamlet also uses different whitespace indicators - see the next section.
|
37
|
+
|
38
|
+
In Slim you have:
|
39
|
+
|
40
|
+
/! HTML comment
|
41
|
+
p data-attr="foo" Text
|
35
42
|
| More Text
|
43
|
+
/ Comment
|
36
44
|
|
37
45
|
In hamlet you have:
|
38
46
|
|
47
|
+
<!-- HTML comment
|
39
48
|
<p data-attr=foo>Text
|
40
49
|
More Text
|
50
|
+
# Comment
|
41
51
|
|
42
52
|
## Whitespace
|
43
53
|
|
44
|
-
|
54
|
+
I added the same syntax that hamlet.js uses to indicate whitespace: a closing bracket to the left and a code comment to the right. I will probably take out some of the slim white space techniques.
|
45
55
|
|
46
|
-
<p> White space to the left
|
47
|
-
> White space to the left again
|
56
|
+
<p> White space to the left # and to the right
|
57
|
+
> White space to the left again# None to the right
|
48
58
|
|
49
59
|
|
50
60
|
## Limitations
|
data/hamlet.gemspec
CHANGED
@@ -4,7 +4,7 @@ require 'date'
|
|
4
4
|
|
5
5
|
Gem::Specification.new do |s|
|
6
6
|
s.name = 'hamlet'
|
7
|
-
s.version = '0.
|
7
|
+
s.version = '0.3.0'
|
8
8
|
s.date = Date.today.to_s
|
9
9
|
s.authors = ['Greg Weber']
|
10
10
|
s.email = ['greg@gregweber.info']
|
@@ -16,7 +16,7 @@ Gem::Specification.new do |s|
|
|
16
16
|
s.rubyforge_project = s.name
|
17
17
|
|
18
18
|
s.files = `git ls-files`.split("\n")
|
19
|
-
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
#s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
20
|
s.require_paths = %w(lib)
|
21
21
|
|
22
22
|
s.add_dependency('slim', ['~> 1.0.0'])
|
@@ -29,6 +29,9 @@ Gem::Specification.new do |s|
|
|
29
29
|
s.add_development_dependency('creole', ['>= 0'])
|
30
30
|
s.add_development_dependency('builder', ['>= 0'])
|
31
31
|
s.add_development_dependency('pry', ['>= 0'])
|
32
|
+
if RUBY_VERSION =~ /1.9/
|
33
|
+
s.add_development_dependency('ruby-debug19', ['>= 0'])
|
34
|
+
end
|
32
35
|
|
33
36
|
unless defined?(RUBY_ENGINE) && RUBY_ENGINE == 'rbx'
|
34
37
|
s.add_development_dependency('rcov', ['>= 0'])
|
@@ -74,12 +74,6 @@ module ForkedSlim
|
|
74
74
|
DELIMITER_REGEX = /\A[\(\[\{]/
|
75
75
|
ATTR_NAME_REGEX = '\A\s*(\w[:\w-]*)'
|
76
76
|
|
77
|
-
if RUBY_VERSION > '1.9'
|
78
|
-
CLASS_ID_REGEX = /\A(#|\.)([\w\u00c0-\uFFFF][\w:\u00c0-\uFFFF-]*)/
|
79
|
-
else
|
80
|
-
CLASS_ID_REGEX = /\A(#|\.)(\w[\w:-]*)/
|
81
|
-
end
|
82
|
-
|
83
77
|
def reset(lines = nil, stacks = nil)
|
84
78
|
# Since you can indent however you like in Slim, we need to keep a list
|
85
79
|
# of how deeply indented you are. For instance, in a template like this:
|
data/lib/hamlet/parser.rb
CHANGED
@@ -3,77 +3,114 @@ require 'hamlet/forked_slim_parser'
|
|
3
3
|
# @api private
|
4
4
|
module Hamlet
|
5
5
|
class Parser < ForkedSlim::Parser
|
6
|
-
|
6
|
+
if RUBY_VERSION > '1.9'
|
7
|
+
CLASS_ID_REGEX = /\A\s*(#|\.)([\w\u00c0-\uFFFF][\w:\u00c0-\uFFFF-]*)/
|
8
|
+
else
|
9
|
+
CLASS_ID_REGEX = /\A\s*(#|\.)(\w[\w:-]*)/
|
10
|
+
end
|
11
|
+
|
12
|
+
# Compile string to Temple expression
|
13
|
+
#
|
14
|
+
# @param [String] str Slim code
|
15
|
+
# @return [Array] Temple expression representing the code]]
|
16
|
+
def call(str)
|
17
|
+
# Set string encoding if option is set
|
18
|
+
if options[:encoding] && str.respond_to?(:encoding)
|
19
|
+
old = str.encoding
|
20
|
+
str = str.dup if str.frozen?
|
21
|
+
str.force_encoding(options[:encoding])
|
22
|
+
# Fall back to old encoding if new encoding is invalid
|
23
|
+
str.force_encoding(old_enc) unless str.valid_encoding?
|
24
|
+
end
|
25
|
+
|
26
|
+
result = [:multi]
|
27
|
+
reset(str.split($/), [result])
|
28
|
+
|
29
|
+
while @lines.first && @lines.first =~ /\A\s*\Z/
|
30
|
+
@stacks.last << [:newline]
|
31
|
+
next_line
|
32
|
+
end
|
33
|
+
if @lines.first and @lines.first =~ /\A<doctype\s+([^>]*)/i
|
34
|
+
if !$'.empty? and $'[0] !~ /\s*#/
|
35
|
+
fail("did not expect content after doctype")
|
36
|
+
end
|
37
|
+
@stacks.last << [:html, :doctype, $1]
|
38
|
+
next_line
|
39
|
+
end
|
40
|
+
|
41
|
+
parse_line while next_line
|
42
|
+
|
43
|
+
reset
|
44
|
+
result
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
7
48
|
|
8
49
|
def parse_line_indicators
|
9
50
|
case @line
|
10
|
-
when /\A
|
11
|
-
# Found a comment block.
|
12
|
-
if @line =~ %r{\A/!( ?)(.*)\Z}
|
13
|
-
# HTML comment
|
14
|
-
block = [:multi]
|
15
|
-
@stacks.last << [:html, :comment, block]
|
16
|
-
@stacks << block
|
17
|
-
@stacks.last << [:slim, :interpolate, $2] unless $2.empty?
|
18
|
-
parse_text_block($2.empty? ? nil : @indents.last + $1.size + 2)
|
19
|
-
elsif @line =~ %r{\A/\[\s*(.*?)\s*\]\s*\Z}
|
20
|
-
# HTML conditional comment
|
21
|
-
block = [:multi]
|
22
|
-
@stacks.last << [:slim, :condcomment, $1, block]
|
23
|
-
@stacks << block
|
24
|
-
else
|
25
|
-
# Slim comment
|
26
|
-
parse_comment_block
|
27
|
-
end
|
28
|
-
when /\A-/
|
29
|
-
# Found a code block.
|
30
|
-
# We expect the line to be broken or the next line to be indented.
|
51
|
+
when /\A-/ # code block.
|
31
52
|
block = [:multi]
|
32
53
|
@line.slice!(0)
|
33
54
|
@stacks.last << [:slim, :control, parse_broken_line, block]
|
34
55
|
@stacks << block
|
35
|
-
when /\A=/
|
36
|
-
# Found an output block.
|
37
|
-
# We expect the line to be broken or the next line to be indented.
|
56
|
+
when /\A=/ # output block.
|
38
57
|
@line =~ /\A=(=?)('?)/
|
39
58
|
@line = $'
|
40
59
|
block = [:multi]
|
41
60
|
@stacks.last << [:slim, :output, $1.empty?, parse_broken_line, block]
|
42
61
|
@stacks.last << [:static, ' '] unless $2.empty?
|
43
62
|
@stacks << block
|
44
|
-
when /\A<(\w+):\s*\Z/
|
45
|
-
# Embedded template detected. It is treated as block.
|
63
|
+
when /\A<(\w+):\s*\Z/ # Embedded template. It is treated as block.
|
46
64
|
block = [:multi]
|
47
65
|
@stacks.last << [:newline] << [:slim, :embedded, $1, block]
|
48
66
|
@stacks << block
|
49
|
-
parse_text_block
|
67
|
+
parse_text_block(nil, true)
|
50
68
|
return # Don't append newline, this has already been done before
|
51
|
-
when /\
|
52
|
-
# Found doctype declaration
|
53
|
-
@stacks.last << [:html, :doctype, $'.strip]
|
54
|
-
when /\A<([#\.]|\w[:\w-]*)/
|
55
|
-
# Found a HTML tag.
|
69
|
+
when /\A<([#\.]|\w[:\w-]*)/ # HTML tag.
|
56
70
|
parse_tag($1)
|
57
|
-
when /\A(
|
58
|
-
|
59
|
-
@stacks.last <<
|
60
|
-
|
71
|
+
when /\A<!--( ?)(.*)\Z/ # HTML comment
|
72
|
+
block = [:multi]
|
73
|
+
@stacks.last << [:html, :comment, block]
|
74
|
+
@stacks << block
|
75
|
+
@stacks.last << [:slim, :interpolate, $2] unless $2.empty?
|
76
|
+
parse_text_block($2.empty? ? nil : @indents.last + $1.size + 2)
|
77
|
+
when %r{\A#\[\s*(.*?)\s*\]\s*\Z} # HTML conditional comment
|
78
|
+
block = [:multi]
|
79
|
+
@stacks.last << [:slim, :condcomment, $1, block]
|
80
|
+
@stacks << block
|
81
|
+
when /\A(?:\s*>( *))?/ # text block.
|
82
|
+
@stacks.last << [:slim, :interpolate, $1 ? $1 << $' : $']
|
83
|
+
parse_text_block($'.empty? ? nil : @indents.last + $1.to_s.size)
|
61
84
|
else
|
62
85
|
syntax_error! 'Unknown line indicator'
|
63
86
|
end
|
64
87
|
@stacks.last << [:newline]
|
65
88
|
end
|
66
89
|
|
67
|
-
def parse_text_block(text_indent = nil)
|
90
|
+
def parse_text_block(text_indent = nil, special = nil)
|
68
91
|
empty_lines = 0
|
92
|
+
multi_line = false
|
93
|
+
if special == :from_tag
|
94
|
+
multi_line = true
|
95
|
+
special = nil
|
96
|
+
end
|
97
|
+
|
98
|
+
first_line = true
|
99
|
+
close_bracket = false
|
69
100
|
until @lines.empty?
|
70
|
-
if @lines.first =~ /\A\s*\Z/
|
101
|
+
if @lines.first =~ /\A\s*>?\s*\Z/
|
71
102
|
next_line
|
72
103
|
@stacks.last << [:newline]
|
73
104
|
empty_lines += 1 if text_indent
|
74
105
|
else
|
75
106
|
indent = get_indent(@lines.first)
|
76
107
|
break if indent <= @indents.last
|
108
|
+
if @lines.first =~ /\A\s*>/
|
109
|
+
indent += 1 #$1.size if $1
|
110
|
+
close_bracket = true
|
111
|
+
else
|
112
|
+
close_bracket = false
|
113
|
+
end
|
77
114
|
|
78
115
|
if empty_lines > 0
|
79
116
|
@stacks.last << [:slim, :interpolate, "\n" * empty_lines]
|
@@ -85,16 +122,28 @@ module Hamlet
|
|
85
122
|
# The text block lines must be at least indented
|
86
123
|
# as deep as the first line.
|
87
124
|
if text_indent && indent < text_indent
|
88
|
-
|
89
|
-
|
125
|
+
# special case for a leading '>' being back 1 char
|
126
|
+
unless first_line && close_bracket && (text_indent - indent == 1)
|
127
|
+
@line.lstrip!
|
128
|
+
syntax_error!('Unexpected text indentation')
|
129
|
+
end
|
90
130
|
end
|
91
131
|
|
92
132
|
@line.slice!(0, text_indent || indent)
|
133
|
+
@line = $' if @line =~ /\A>/
|
134
|
+
# a code comment
|
135
|
+
if @line =~ /(\A|[^\\])#([^{]|\Z)/
|
136
|
+
@line = $` + $1
|
137
|
+
end
|
138
|
+
@stacks.last << [:newline] if multi_line && !special
|
93
139
|
@stacks.last << [:slim, :interpolate, (text_indent ? "\n" : '') + @line] << [:newline]
|
94
140
|
|
95
141
|
# The indentation of first line of the text block
|
96
142
|
# determines the text base indentation.
|
97
143
|
text_indent ||= indent
|
144
|
+
|
145
|
+
first_line = false
|
146
|
+
multi_line = true
|
98
147
|
end
|
99
148
|
end
|
100
149
|
end
|
@@ -111,27 +160,25 @@ module Hamlet
|
|
111
160
|
@stacks.last << tag
|
112
161
|
|
113
162
|
case @line
|
114
|
-
when /\A
|
115
|
-
# Handle output code
|
163
|
+
when /\A=(=?)('?)/ # Handle output code
|
116
164
|
block = [:multi]
|
117
165
|
@line = $'
|
118
166
|
content = [:slim, :output, $1 != '=', parse_broken_line, block]
|
119
167
|
tag << content
|
120
168
|
@stacks.last << [:static, ' '] unless $2.empty?
|
121
169
|
@stacks << block
|
122
|
-
when /\A\s
|
123
|
-
# Closed tag. Do nothing
|
124
|
-
when /\A\s*>?\s*\Z/
|
170
|
+
when /\A\s*\Z/
|
125
171
|
# Empty content
|
126
172
|
content = [:multi]
|
127
173
|
tag << content
|
128
174
|
@stacks << content
|
129
|
-
when
|
130
|
-
#
|
131
|
-
|
175
|
+
when %r!\A/>!
|
176
|
+
# Do nothing for closing tag
|
177
|
+
else # Text content
|
178
|
+
content = [:multi, [:slim, :interpolate, @line]]
|
132
179
|
tag << content
|
133
180
|
@stacks << content
|
134
|
-
parse_text_block(@orig_line.size - @line.size
|
181
|
+
parse_text_block(@orig_line.size - @line.size, :from_tag)
|
135
182
|
end
|
136
183
|
end
|
137
184
|
|
@@ -147,44 +194,45 @@ module Hamlet
|
|
147
194
|
end
|
148
195
|
|
149
196
|
# Check to see if there is a delimiter right after the tag name
|
150
|
-
delimiter =
|
151
|
-
if @line =~ DELIMITER_REGEX
|
152
|
-
delimiter = DELIMITERS[$&]
|
153
|
-
@line.slice!(0)
|
154
|
-
end
|
197
|
+
delimiter = '>'
|
155
198
|
|
156
199
|
orig_line = @orig_line
|
157
200
|
lineno = @lineno
|
158
201
|
while true
|
159
202
|
# Parse attributes
|
160
|
-
|
161
|
-
while @line =~ attr_regex
|
162
|
-
@line = $'
|
203
|
+
while @line =~ /#{ATTR_NAME_REGEX}\s*(=\s*)?/
|
163
204
|
name = $1
|
164
|
-
|
205
|
+
@line = $'
|
206
|
+
if !$2
|
165
207
|
attributes << [:slim, :attr, name, false, 'true']
|
166
208
|
elsif @line =~ /\A["']/
|
167
209
|
# Value is quoted (static)
|
168
210
|
@line = $'
|
169
211
|
attributes << [:html, :attr, name, [:slim, :interpolate, parse_quoted_attribute($&)]]
|
170
|
-
|
171
|
-
@line =~ /[^ >]+/
|
212
|
+
elsif @line =~ /\A[^ >]+/
|
172
213
|
@line = $'
|
173
214
|
attributes << [:html, :attr, name, [:slim, :interpolate, $&]]
|
174
215
|
end
|
175
216
|
end
|
176
217
|
|
177
|
-
|
178
|
-
break unless delimiter
|
218
|
+
@line.lstrip!
|
179
219
|
|
180
220
|
# Find ending delimiter
|
181
|
-
if @line =~ /\A
|
221
|
+
if @line =~ /\A(>|\Z)/
|
182
222
|
@line = $'
|
183
223
|
break
|
224
|
+
elsif @line =~ %r!\A/>!
|
225
|
+
# Do nothing for closing tag
|
226
|
+
# don't eat the line either, we check for it again
|
227
|
+
if not $'.empty? and $' !~ /\s*#/
|
228
|
+
syntax_error!("Did not expect any content after self closing tag",
|
229
|
+
:orig_line => orig_line,
|
230
|
+
:lineno => lineno,
|
231
|
+
:column => orig_line.size)
|
232
|
+
end
|
233
|
+
break
|
184
234
|
end
|
185
235
|
|
186
|
-
# Found something where an attribute should be
|
187
|
-
@line.lstrip!
|
188
236
|
syntax_error!('Expected attribute') unless @line.empty?
|
189
237
|
|
190
238
|
# Attributes span multiple lines
|
@@ -19,7 +19,7 @@ p Test
|
|
19
19
|
p Test
|
20
20
|
}
|
21
21
|
chain = proc do |engine|
|
22
|
-
engine.before(
|
22
|
+
engine.before(Hamlet::Parser, :WrapInput) do |input|
|
23
23
|
"p Header\n#{input}\np Footer"
|
24
24
|
end
|
25
25
|
end
|
@@ -32,7 +32,7 @@ p Test
|
|
32
32
|
p Test
|
33
33
|
}
|
34
34
|
chain = proc do |engine|
|
35
|
-
engine.after(
|
35
|
+
engine.after(Hamlet::Parser, :ReplaceParsedExp) do |exp|
|
36
36
|
[:slim, :output, false, '1+1', [:multi]]
|
37
37
|
end
|
38
38
|
end
|
@@ -3,9 +3,9 @@ require 'helper'
|
|
3
3
|
class TestSlimCodeBlocks < TestSlim
|
4
4
|
def test_render_with_output_code_block
|
5
5
|
source = %q{
|
6
|
-
p
|
6
|
+
<p
|
7
7
|
= hello_world "Hello Ruby!" do
|
8
|
-
|
8
|
+
Hello from within a block!
|
9
9
|
}
|
10
10
|
|
11
11
|
assert_html '<p>Hello Ruby! Hello from within a block! Hello Ruby!</p>', source
|
@@ -13,7 +13,7 @@ p
|
|
13
13
|
|
14
14
|
def test_render_with_output_code_within_block
|
15
15
|
source = %q{
|
16
|
-
p
|
16
|
+
<p
|
17
17
|
= hello_world "Hello Ruby!" do
|
18
18
|
= hello_world "Hello from within a block!"
|
19
19
|
}
|
@@ -23,7 +23,7 @@ p
|
|
23
23
|
|
24
24
|
def test_render_with_output_code_within_block_2
|
25
25
|
source = %q{
|
26
|
-
p
|
26
|
+
<p
|
27
27
|
= hello_world "Hello Ruby!" do
|
28
28
|
= hello_world "Hello from within a block!" do
|
29
29
|
= hello_world "And another one!"
|
@@ -34,10 +34,10 @@ p
|
|
34
34
|
|
35
35
|
def test_output_block_with_arguments
|
36
36
|
source = %q{
|
37
|
-
p
|
37
|
+
<p
|
38
38
|
= define_macro :person do |first_name, last_name|
|
39
|
-
|
40
|
-
|
39
|
+
<.first_name>= first_name
|
40
|
+
<.last_name>= last_name
|
41
41
|
== call_macro :person, 'John', 'Doe'
|
42
42
|
== call_macro :person, 'Max', 'Mustermann'
|
43
43
|
}
|
@@ -48,9 +48,9 @@ p
|
|
48
48
|
|
49
49
|
def test_render_with_control_code_loop
|
50
50
|
source = %q{
|
51
|
-
p
|
51
|
+
<p
|
52
52
|
- 3.times do
|
53
|
-
|
53
|
+
Hey!
|
54
54
|
}
|
55
55
|
|
56
56
|
assert_html '<p>Hey!Hey!Hey!</p>', source
|
@@ -60,7 +60,7 @@ p
|
|
60
60
|
source = %q{
|
61
61
|
= hello_world "Hello Ruby!" do
|
62
62
|
- if true
|
63
|
-
|
63
|
+
Hello from within a block!
|
64
64
|
}
|
65
65
|
|
66
66
|
assert_html 'Hello Ruby! Hello from within a block! Hello Ruby!', source
|
@@ -3,7 +3,7 @@ require 'helper'
|
|
3
3
|
class TestSlimCodeEscaping < TestSlim
|
4
4
|
def test_escaping_evil_method
|
5
5
|
source = %q{
|
6
|
-
p
|
6
|
+
<p>= evil_method
|
7
7
|
}
|
8
8
|
|
9
9
|
assert_html '<p><script>do_something_evil();</script></p>', source
|
@@ -11,7 +11,7 @@ p = evil_method
|
|
11
11
|
|
12
12
|
def test_render_without_html_safe
|
13
13
|
source = %q{
|
14
|
-
p
|
14
|
+
<p>= "<strong>Hello World\\n, meet \\"Slim\\"</strong>."
|
15
15
|
}
|
16
16
|
|
17
17
|
assert_html "<p><strong>Hello World\n, meet \"Slim\"</strong>.</p>", source
|
@@ -19,7 +19,7 @@ p = "<strong>Hello World\\n, meet \\"Slim\\"</strong>."
|
|
19
19
|
|
20
20
|
def test_render_with_html_safe_false
|
21
21
|
source = %q{
|
22
|
-
p
|
22
|
+
<p>= HtmlUnsafeString.new("<strong>Hello World\\n, meet \\"Slim\\"</strong>.")
|
23
23
|
}
|
24
24
|
|
25
25
|
assert_html "<p><strong>Hello World\n, meet \"Slim\"</strong>.</p>", source, :use_html_safe => true
|
@@ -27,7 +27,7 @@ p = HtmlUnsafeString.new("<strong>Hello World\\n, meet \\"Slim\\"</strong>.")
|
|
27
27
|
|
28
28
|
def test_render_with_html_safe_true
|
29
29
|
source = %q{
|
30
|
-
p
|
30
|
+
<p>= HtmlSafeString.new("<strong>Hello World\\n, meet \\"Slim\\"</strong>.")
|
31
31
|
}
|
32
32
|
|
33
33
|
assert_html "<p><strong>Hello World\n, meet \"Slim\"</strong>.</p>", source, :use_html_safe => true
|