better_html 0.0.12 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/lib/better_html.rb +0 -2
  3. data/lib/better_html/ast/iterator.rb +32 -0
  4. data/lib/better_html/ast/node.rb +14 -0
  5. data/lib/better_html/better_erb/runtime_checks.rb +3 -3
  6. data/lib/better_html/config.rb +12 -0
  7. data/lib/better_html/parser.rb +286 -0
  8. data/lib/better_html/test_helper/ruby_expr.rb +8 -5
  9. data/lib/better_html/test_helper/safe_erb_tester.rb +121 -108
  10. data/lib/better_html/test_helper/safe_lodash_tester.rb +44 -42
  11. data/lib/better_html/tokenizer/base_erb.rb +79 -0
  12. data/lib/better_html/tokenizer/html_erb.rb +31 -0
  13. data/lib/better_html/{node_iterator → tokenizer}/html_lodash.rb +30 -34
  14. data/lib/better_html/tokenizer/javascript_erb.rb +15 -0
  15. data/lib/better_html/{node_iterator → tokenizer}/location.rb +9 -3
  16. data/lib/better_html/tokenizer/token.rb +16 -0
  17. data/lib/better_html/tokenizer/token_array.rb +54 -0
  18. data/lib/better_html/tree/attribute.rb +31 -0
  19. data/lib/better_html/tree/attributes_list.rb +25 -0
  20. data/lib/better_html/tree/tag.rb +39 -0
  21. data/lib/better_html/version.rb +1 -1
  22. data/test/better_html/parser_test.rb +279 -0
  23. data/test/better_html/test_helper/safe_erb_tester_test.rb +11 -0
  24. data/test/better_html/test_helper/safe_lodash_tester_test.rb +11 -1
  25. data/test/better_html/tokenizer/html_erb_test.rb +158 -0
  26. data/test/better_html/tokenizer/html_lodash_test.rb +98 -0
  27. data/test/better_html/tokenizer/location_test.rb +57 -0
  28. data/test/better_html/tokenizer/token_array_test.rb +144 -0
  29. data/test/better_html/tokenizer/token_test.rb +15 -0
  30. metadata +45 -30
  31. data/lib/better_html/node_iterator.rb +0 -144
  32. data/lib/better_html/node_iterator/attribute.rb +0 -34
  33. data/lib/better_html/node_iterator/base.rb +0 -27
  34. data/lib/better_html/node_iterator/cdata.rb +0 -8
  35. data/lib/better_html/node_iterator/comment.rb +0 -8
  36. data/lib/better_html/node_iterator/content_node.rb +0 -13
  37. data/lib/better_html/node_iterator/element.rb +0 -26
  38. data/lib/better_html/node_iterator/html_erb.rb +0 -70
  39. data/lib/better_html/node_iterator/javascript_erb.rb +0 -55
  40. data/lib/better_html/node_iterator/text.rb +0 -8
  41. data/lib/better_html/node_iterator/token.rb +0 -8
  42. data/lib/better_html/tree.rb +0 -113
  43. data/test/better_html/node_iterator/html_erb_test.rb +0 -116
  44. data/test/better_html/node_iterator/html_lodash_test.rb +0 -132
  45. data/test/better_html/node_iterator/location_test.rb +0 -36
  46. data/test/better_html/node_iterator_test.rb +0 -221
  47. data/test/better_html/tree_test.rb +0 -110
@@ -0,0 +1,31 @@
1
+ require 'ast'
2
+
3
+ module BetterHtml
4
+ module Tree
5
+ class Attribute
6
+ attr_reader :node, :name_node, :equal_node, :value_node
7
+
8
+ def initialize(node)
9
+ @node = node
10
+ @name_node, @equal_node, @value_node = *node
11
+ end
12
+
13
+ def self.from_node(node)
14
+ new(node)
15
+ end
16
+
17
+ def loc
18
+ @node.loc
19
+ end
20
+
21
+ def name
22
+ @name_node&.loc&.source&.downcase
23
+ end
24
+
25
+ def value
26
+ parts = value_node.to_a.reject{ |node| node.is_a?(::AST::Node) && node.type == :quote }
27
+ parts.map { |s| s.is_a?(::AST::Node) ? s.loc.source : CGI.unescapeHTML(s) }.join
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,25 @@
1
+ require 'better_html/tree/attribute'
2
+
3
+ module BetterHtml
4
+ module Tree
5
+ class AttributesList
6
+ def initialize(list)
7
+ @list = list || []
8
+ end
9
+
10
+ def self.from_nodes(nodes)
11
+ new(nodes&.map { |node| Tree::Attribute.from_node(node) })
12
+ end
13
+
14
+ def [](name)
15
+ @list.find do |attribute|
16
+ attribute.name == name.downcase
17
+ end
18
+ end
19
+
20
+ def each(&block)
21
+ @list.each(&block)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,39 @@
1
+ require 'better_html/tree/attributes_list'
2
+ require 'better_html/ast/iterator'
3
+
4
+ module BetterHtml
5
+ module Tree
6
+ class Tag
7
+ attr_reader :node
8
+
9
+ def initialize(node)
10
+ @node = node
11
+ @start_solidus, @name_node, @attributes_node, @end_solidus = *node
12
+ end
13
+
14
+ def self.from_node(node)
15
+ new(node)
16
+ end
17
+
18
+ def loc
19
+ @node.loc
20
+ end
21
+
22
+ def name
23
+ @name_node&.loc&.source&.downcase
24
+ end
25
+
26
+ def closing?
27
+ @start_solidus&.type == :solidus
28
+ end
29
+
30
+ def self_closing?
31
+ @end_solidus&.type == :solidus
32
+ end
33
+
34
+ def attributes
35
+ @attributes ||= AttributesList.from_nodes(@attributes_node.to_a)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -1,3 +1,3 @@
1
1
  module BetterHtml
2
- VERSION = "0.0.12"
2
+ VERSION = "1.0.0"
3
3
  end
@@ -0,0 +1,279 @@
1
+ require 'test_helper'
2
+ require 'better_html/parser'
3
+ require 'ast'
4
+
5
+ module BetterHtml
6
+ class ParserTest < ActiveSupport::TestCase
7
+ include ::AST::Sexp
8
+
9
+ test "parse empty document" do
10
+ tree = Parser.new('')
11
+
12
+ assert_equal s(:document), tree.ast
13
+ end
14
+
15
+ test "consume cdata nodes" do
16
+ code = "<![CDATA[ foo ]]>"
17
+ tree = Parser.new(code)
18
+
19
+ assert_equal s(:document, s(:cdata, ' foo ')), tree.ast
20
+ assert_equal code, tree.ast.loc.source
21
+ end
22
+
23
+ test "unterminated cdata nodes are consumed until end" do
24
+ code = "<![CDATA[ foo"
25
+ tree = Parser.new(code)
26
+
27
+ assert_equal s(:document, s(:cdata, ' foo')), tree.ast
28
+ assert_equal code, tree.ast.loc.source
29
+ end
30
+
31
+ test "consume cdata with interpolation" do
32
+ code = "<![CDATA[ foo <%= bar %> baz ]]>"
33
+ tree = Parser.new(code)
34
+
35
+ assert_equal s(:document,
36
+ s(:cdata,
37
+ " foo ",
38
+ s(:erb, s(:indicator, '='), nil, s(:code, " bar "), nil),
39
+ " baz "
40
+ )),
41
+ tree.ast
42
+ assert_equal code, tree.ast.loc.source
43
+ end
44
+
45
+ test "consume comment nodes" do
46
+ tree = Parser.new("<!-- foo -->")
47
+
48
+ assert_equal s(:document, s(:comment, ' foo ')), tree.ast
49
+ end
50
+
51
+ test "unterminated comment nodes are consumed until end" do
52
+ tree = Parser.new("<!-- foo")
53
+
54
+ assert_equal s(:document, s(:comment, ' foo')), tree.ast
55
+ end
56
+
57
+ test "consume comment with interpolation" do
58
+ tree = Parser.new("<!-- foo <%= bar %> baz -->")
59
+
60
+ assert_equal s(:document,
61
+ s(:comment,
62
+ " foo ",
63
+ s(:erb, s(:indicator, "="), nil, s(:code, " bar "), nil),
64
+ " baz "
65
+ )),
66
+ tree.ast
67
+ end
68
+
69
+ test "consume tag nodes" do
70
+ tree = Parser.new("<div>")
71
+ assert_equal s(:document, s(:tag, nil, s(:tag_name, "div"), nil, nil)), tree.ast
72
+ end
73
+
74
+ test "tag without name" do
75
+ tree = Parser.new("foo < bar")
76
+ assert_equal s(:document,
77
+ s(:text, "foo "),
78
+ s(:tag, nil, nil,
79
+ s(:tag_attributes,
80
+ s(:attribute, s(:attribute_name, 'bar'), nil, nil)
81
+ ),
82
+ nil
83
+ )
84
+ ), tree.ast
85
+ end
86
+
87
+ test "consume tag nodes with solidus" do
88
+ tree = Parser.new("</div>")
89
+ assert_equal s(:document, s(:tag, s(:solidus), s(:tag_name, "div"), nil, nil)), tree.ast
90
+ end
91
+
92
+ test "sets self_closing when appropriate" do
93
+ tree = Parser.new("<div/>")
94
+ assert_equal s(:document, s(:tag, nil, s(:tag_name, "div"), nil, s(:solidus))), tree.ast
95
+ end
96
+
97
+ test "consume tag nodes until name ends" do
98
+ tree = Parser.new("<div/>")
99
+ assert_equal s(:document, s(:tag, nil, s(:tag_name, "div"), nil, s(:solidus))), tree.ast
100
+
101
+ tree = Parser.new("<div ")
102
+ assert_equal s(:document, s(:tag, nil, s(:tag_name, "div"), nil, nil)), tree.ast
103
+ end
104
+
105
+ test "consume tag nodes with interpolation" do
106
+ tree = Parser.new("<ns:<%= name %>-thing>")
107
+ assert_equal s(:document,
108
+ s(:tag,
109
+ nil,
110
+ s(:tag_name, "ns:", s(:erb, s(:indicator, "="), nil, s(:code, " name "), nil), "-thing"),
111
+ nil,
112
+ nil
113
+ )), tree.ast
114
+ end
115
+
116
+ test "consume tag attributes nodes unquoted value" do
117
+ tree = Parser.new("<div foo=bar>")
118
+ assert_equal s(:document,
119
+ s(:tag, nil,
120
+ s(:tag_name, "div"),
121
+ s(:tag_attributes,
122
+ s(:attribute,
123
+ s(:attribute_name, "foo"),
124
+ s(:equal),
125
+ s(:attribute_value, "bar")
126
+ )
127
+ ),
128
+ nil
129
+ )), tree.ast
130
+ end
131
+
132
+ test "consume attributes without name" do
133
+ tree = Parser.new("<div 'thing'>")
134
+ assert_equal s(:document,
135
+ s(:tag, nil,
136
+ s(:tag_name, "div"),
137
+ s(:tag_attributes,
138
+ s(:attribute,
139
+ nil,
140
+ nil,
141
+ s(:attribute_value, s(:quote, "'"), "thing", s(:quote, "'"))
142
+ )
143
+ ),
144
+ nil
145
+ )), tree.ast
146
+ end
147
+
148
+ test "consume tag attributes nodes quoted value" do
149
+ tree = Parser.new("<div foo=\"bar\">")
150
+ assert_equal s(:document,
151
+ s(:tag, nil,
152
+ s(:tag_name, "div"),
153
+ s(:tag_attributes,
154
+ s(:attribute,
155
+ s(:attribute_name, "foo"),
156
+ s(:equal),
157
+ s(:attribute_value, s(:quote, "\""), "bar", s(:quote, "\""))
158
+ )
159
+ ),
160
+ nil
161
+ )), tree.ast
162
+ end
163
+
164
+ test "consume tag attributes nodes interpolation in name and value" do
165
+ tree = Parser.new("<div data-<%= foo %>=\"some <%= value %> foo\">")
166
+ assert_equal s(:document,
167
+ s(:tag, nil,
168
+ s(:tag_name, "div"),
169
+ s(:tag_attributes,
170
+ s(:attribute,
171
+ s(:attribute_name, "data-", s(:erb, s(:indicator, "="), nil, s(:code, " foo "), nil)),
172
+ s(:equal),
173
+ s(:attribute_value,
174
+ s(:quote, "\""),
175
+ "some ",
176
+ s(:erb, s(:indicator, "="), nil, s(:code, " value "), nil),
177
+ " foo",
178
+ s(:quote, "\""),
179
+ ),
180
+ )
181
+ ),
182
+ nil
183
+ )), tree.ast
184
+ end
185
+
186
+ test "consume text nodes" do
187
+ tree = Parser.new("here is <%= some %> text")
188
+
189
+ assert_equal s(:document,
190
+ s(:text,
191
+ "here is ",
192
+ s(:erb, s(:indicator, "="), nil, s(:code, " some "), nil),
193
+ " text"
194
+ )), tree.ast
195
+ end
196
+
197
+ test "javascript template parsing works" do
198
+ tree = Parser.new("here is <%= some %> text", template_language: :javascript)
199
+
200
+ assert_equal s(:document,
201
+ s(:text,
202
+ "here is ",
203
+ s(:erb, s(:indicator, "="), nil, s(:code, " some "), nil),
204
+ " text"
205
+ )), tree.ast
206
+ end
207
+
208
+ test "javascript template does not consume html tags" do
209
+ tree = Parser.new("<div <%= some %> />", template_language: :javascript)
210
+
211
+ assert_equal s(:document,
212
+ s(:text,
213
+ "<div ",
214
+ s(:erb, s(:indicator, "="), nil, s(:code, " some "), nil),
215
+ " />"
216
+ )), tree.ast
217
+ end
218
+
219
+ test "lodash template parsing works" do
220
+ tree = Parser.new('<div class="[%= foo %]">', template_language: :lodash)
221
+
222
+ assert_equal s(:document,
223
+ s(:tag,
224
+ nil,
225
+ s(:tag_name, "div"),
226
+ s(:tag_attributes,
227
+ s(:attribute,
228
+ s(:attribute_name, "class"),
229
+ s(:equal),
230
+ s(:attribute_value,
231
+ s(:quote, "\""),
232
+ s(:lodash, s(:indicator, "="), s(:code, " foo ")),
233
+ s(:quote, "\"")
234
+ )
235
+ )
236
+ ),
237
+ nil
238
+ )
239
+ ), tree.ast
240
+ end
241
+
242
+ test "nodes are all nested under document" do
243
+ tree = Parser.new(<<~HTML)
244
+ some text
245
+ <!-- a comment -->
246
+ some more text
247
+ <%= an erb tag -%>
248
+ <div class="foo">
249
+ content
250
+ </div>
251
+ HTML
252
+
253
+ assert_equal s(:document,
254
+ s(:text, "some text\n"),
255
+ s(:comment, " a comment "),
256
+ s(:text,
257
+ "\nsome more text\n",
258
+ s(:erb, s(:indicator, '='), nil, s(:code, ' an erb tag '), s(:trim)),
259
+ "\n"
260
+ ),
261
+ s(:tag,
262
+ nil,
263
+ s(:tag_name, "div"),
264
+ s(:tag_attributes,
265
+ s(:attribute,
266
+ s(:attribute_name, "class"),
267
+ s(:equal),
268
+ s(:attribute_value, s(:quote, "\""), "foo", s(:quote, "\""))
269
+ )
270
+ ),
271
+ nil
272
+ ),
273
+ s(:text, "\n content\n"),
274
+ s(:tag, s(:solidus), s(:tag_name, 'div'), nil, nil),
275
+ s(:text, "\n"),
276
+ ), tree.ast
277
+ end
278
+ end
279
+ end
@@ -281,6 +281,17 @@ module BetterHtml
281
281
  assert_equal "erb statement not allowed here; did you mean '<%=' ?", errors.first.message
282
282
  end
283
283
 
284
+ test "disallowed script types" do
285
+ errors = parse(<<-EOF).errors
286
+ <script type="text/bogus">
287
+ </script>
288
+ EOF
289
+
290
+ assert_equal 1, errors.size
291
+ assert_equal 'type="text/bogus"', errors.first.location.source
292
+ assert_equal "text/bogus is not a valid type, valid types are text/javascript, text/template, text/html", errors.first.message
293
+ end
294
+
284
295
  test "statements not allowed in javascript template" do
285
296
  errors = parse(<<-JS, template_language: :javascript).errors
286
297
  <% if foo %>
@@ -46,7 +46,17 @@ module BetterHtml
46
46
  EOF
47
47
 
48
48
  assert_equal 1, errors.size
49
- assert_equal 'script', errors.first.location.source
49
+ assert_equal '<script type="text/javascript">', errors.first.location.source
50
+ assert_equal "No script tags allowed nested in lodash templates", errors.first.message
51
+ end
52
+
53
+ test "script tag names are unescaped" do
54
+ errors = parse(<<-EOF).errors
55
+ <script type="text/j&#x61;v&#x61;script"></script>
56
+ EOF
57
+
58
+ assert_equal 1, errors.size
59
+ assert_equal '<script type="text/j&#x61;v&#x61;script">', errors.first.location.source
50
60
  assert_equal "No script tags allowed nested in lodash templates", errors.first.message
51
61
  end
52
62
 
@@ -0,0 +1,158 @@
1
+ require 'test_helper'
2
+ require 'better_html/tokenizer/html_erb'
3
+
4
+ module BetterHtml
5
+ module Tokenizer
6
+ class HtmlErbTest < ActiveSupport::TestCase
7
+ test "text" do
8
+ scanner = HtmlErb.new("just some text")
9
+ assert_equal 1, scanner.tokens.size
10
+
11
+ assert_attributes ({
12
+ type: :text,
13
+ loc: { start: 0, stop: 13, source: 'just some text' }
14
+ }), scanner.tokens[0]
15
+ end
16
+
17
+ test "statement" do
18
+ scanner = HtmlErb.new("<% statement %>")
19
+ assert_equal 3, scanner.tokens.size
20
+
21
+ assert_attributes ({ type: :erb_begin, loc: { start: 0, stop: 1, source: '<%' } }), scanner.tokens[0]
22
+ assert_attributes ({ type: :code, loc: { start: 2, stop: 12, source: ' statement ' } }), scanner.tokens[1]
23
+ assert_attributes ({ type: :erb_end, loc: { start: 13, stop: 14, source: '%>' } }), scanner.tokens[2]
24
+ end
25
+
26
+ test "debug statement" do
27
+ scanner = HtmlErb.new("<%# statement %>")
28
+ assert_equal 4, scanner.tokens.size
29
+
30
+ assert_attributes ({ type: :erb_begin, loc: { start: 0, stop: 1, source: '<%' } }), scanner.tokens[0]
31
+ assert_attributes ({ type: :indicator, loc: { start: 2, stop: 2, source: '#' } }), scanner.tokens[1]
32
+ assert_attributes ({ type: :code, loc: { start: 3, stop: 13, source: ' statement ' } }), scanner.tokens[2]
33
+ assert_attributes ({ type: :erb_end, loc: { start: 14, stop: 15, source: '%>' } }), scanner.tokens[3]
34
+ end
35
+
36
+ test "when multi byte characters are present in erb" do
37
+ code = "<% ui_helper 'your store’s' %>"
38
+ scanner = HtmlErb.new(code)
39
+ assert_equal 3, scanner.tokens.size
40
+
41
+ assert_attributes ({ type: :erb_begin, loc: { start: 0, stop: 1, source: '<%' } }), scanner.tokens[0]
42
+ assert_attributes ({ type: :code, loc: { start: 2, stop: 27, source: " ui_helper 'your store’s' " } }), scanner.tokens[1]
43
+ assert_attributes ({ type: :erb_end, loc: { start: 28, stop: 29, source: '%>' } }), scanner.tokens[2]
44
+ assert_equal code.length, scanner.current_position
45
+ end
46
+
47
+ test "when multi byte characters are present in text" do
48
+ code = "your store’s"
49
+ scanner = HtmlErb.new(code)
50
+ assert_equal 1, scanner.tokens.size
51
+
52
+ assert_attributes ({ type: :text, loc: { start: 0, stop: 11, source: 'your store’s' } }), scanner.tokens[0]
53
+ assert_equal code.length, scanner.current_position
54
+ end
55
+
56
+ test "when multi byte characters are present in html" do
57
+ code = "<div title='your store’s'>foo</div>"
58
+ scanner = HtmlErb.new(code)
59
+ assert_equal 14, scanner.tokens.size
60
+
61
+ assert_attributes ({ type: :tag_start, loc: { start: 0, stop: 0, source: '<' } }), scanner.tokens[0]
62
+ assert_attributes ({ type: :tag_name, loc: { start: 1, stop: 3, source: "div" } }), scanner.tokens[1]
63
+ assert_attributes ({ type: :whitespace, loc: { start: 4, stop: 4, source: " " } }), scanner.tokens[2]
64
+ assert_attributes ({ type: :attribute_name, loc: { start: 5, stop: 9, source: "title" } }), scanner.tokens[3]
65
+ assert_attributes ({ type: :equal, loc: { start: 10, stop: 10, source: "=" } }), scanner.tokens[4]
66
+ assert_attributes ({ type: :attribute_quoted_value_start, loc: { start: 11, stop: 11, source: "'" } }), scanner.tokens[5]
67
+ assert_attributes ({ type: :attribute_quoted_value, loc: { start: 12, stop: 23, source: "your store’s" } }), scanner.tokens[6]
68
+ assert_attributes ({ type: :attribute_quoted_value_end, loc: { start: 24, stop: 24, source: "'" } }), scanner.tokens[7]
69
+ assert_equal code.length, scanner.current_position
70
+ end
71
+
72
+ test "expression literal" do
73
+ scanner = HtmlErb.new("<%= literal %>")
74
+ assert_equal 4, scanner.tokens.size
75
+
76
+ assert_attributes ({ type: :erb_begin, loc: { start: 0, stop: 1, source: '<%' } }), scanner.tokens[0]
77
+ assert_attributes ({ type: :indicator, loc: { start: 2, stop: 2, source: '=' } }), scanner.tokens[1]
78
+ assert_attributes ({ type: :code, loc: { start: 3, stop: 11, source: ' literal ' } }), scanner.tokens[2]
79
+ assert_attributes ({ type: :erb_end, loc: { start: 12, stop: 13, source: '%>' } }), scanner.tokens[3]
80
+ end
81
+
82
+ test "expression escaped" do
83
+ scanner = HtmlErb.new("<%== escaped %>")
84
+ assert_equal 4, scanner.tokens.size
85
+
86
+ assert_attributes ({ type: :erb_begin, loc: { start: 0, stop: 1, source: '<%' } }), scanner.tokens[0]
87
+ assert_attributes ({ type: :indicator, loc: { start: 2, stop: 3, source: '==' } }), scanner.tokens[1]
88
+ assert_attributes ({ type: :code, loc: { start: 4, stop: 12, source: ' escaped ' } }), scanner.tokens[2]
89
+ assert_attributes ({ type: :erb_end, loc: { start: 13, stop: 14, source: '%>' } }), scanner.tokens[3]
90
+ end
91
+
92
+ test "line number for multi-line statements" do
93
+ scanner = HtmlErb.new("before <% multi\nline %> after")
94
+ assert_equal 5, scanner.tokens.size
95
+
96
+ assert_attributes ({ type: :text, loc: { line: 1, source: 'before ' } }), scanner.tokens[0]
97
+ assert_attributes ({ type: :erb_begin, loc: { line: 1, source: '<%' } }), scanner.tokens[1]
98
+ assert_attributes ({ type: :code, loc: { line: 1, source: " multi\nline " } }), scanner.tokens[2]
99
+ assert_attributes ({ type: :erb_end, loc: { line: 2, source: "%>" } }), scanner.tokens[3]
100
+ assert_attributes ({ type: :text, loc: { line: 2, source: " after" } }), scanner.tokens[4]
101
+ end
102
+
103
+ test "multi-line statements with trim" do
104
+ scanner = HtmlErb.new("before\n<% multi\nline -%>\nafter")
105
+ assert_equal 7, scanner.tokens.size
106
+
107
+ assert_attributes ({ type: :text, loc: { line: 1, source: "before\n" } }), scanner.tokens[0]
108
+ assert_attributes ({ type: :erb_begin, loc: { line: 2, source: '<%' } }), scanner.tokens[1]
109
+ assert_attributes ({ type: :code, loc: { line: 2, source: " multi\nline " } }), scanner.tokens[2]
110
+ assert_attributes ({ type: :trim, loc: { line: 3, source: "-" } }), scanner.tokens[3]
111
+ assert_attributes ({ type: :erb_end, loc: { line: 3, source: "%>" } }), scanner.tokens[4]
112
+ assert_attributes ({ type: :text, loc: { line: 3, source: "\n" } }), scanner.tokens[5]
113
+ assert_attributes ({ type: :text, loc: { line: 4, source: "after" } }), scanner.tokens[6]
114
+ end
115
+
116
+ test "multi-line expression with trim" do
117
+ scanner = HtmlErb.new("before\n<%= multi\nline -%>\nafter")
118
+ assert_equal 8, scanner.tokens.size
119
+
120
+ assert_attributes ({ type: :text, loc: { line: 1, source: "before\n" } }), scanner.tokens[0]
121
+ assert_attributes ({ type: :erb_begin, loc: { line: 2, source: '<%' } }), scanner.tokens[1]
122
+ assert_attributes ({ type: :indicator, loc: { line: 2, source: '=' } }), scanner.tokens[2]
123
+ assert_attributes ({ type: :code, loc: { line: 2, source: " multi\nline " } }), scanner.tokens[3]
124
+ assert_attributes ({ type: :trim, loc: { line: 3, source: "-" } }), scanner.tokens[4]
125
+ assert_attributes ({ type: :erb_end, loc: { line: 3, source: "%>" } }), scanner.tokens[5]
126
+ assert_attributes ({ type: :text, loc: { line: 3, source: "\n" } }), scanner.tokens[6]
127
+ assert_attributes ({ type: :text, loc: { line: 4, source: "after" } }), scanner.tokens[7]
128
+ end
129
+
130
+ test "line counts with comments" do
131
+ scanner = HtmlErb.new("before\n<%# BO$$ Mode %>\nafter")
132
+ assert_equal 7, scanner.tokens.size
133
+
134
+ assert_attributes ({ type: :text, loc: { line: 1, source: "before\n" } }), scanner.tokens[0]
135
+ assert_attributes ({ type: :erb_begin, loc: { line: 2, source: '<%' } }), scanner.tokens[1]
136
+ assert_attributes ({ type: :indicator, loc: { line: 2, source: '#' } }), scanner.tokens[2]
137
+ assert_attributes ({ type: :code, loc: { line: 2, source: " BO$$ Mode " } }), scanner.tokens[3]
138
+ assert_attributes ({ type: :erb_end, loc: { line: 2, source: "%>" } }), scanner.tokens[4]
139
+ assert_attributes ({ type: :text, loc: { line: 2, source: "\n" } }), scanner.tokens[5]
140
+ assert_attributes ({ type: :text, loc: { line: 3, source: "after" } }), scanner.tokens[6]
141
+ end
142
+
143
+ private
144
+
145
+ def assert_attributes(attributes, token)
146
+ attributes.each do |key, value|
147
+ if value.nil?
148
+ assert_nil token.send(key)
149
+ elsif value.is_a?(Hash)
150
+ assert_attributes(value, token.send(key))
151
+ else
152
+ assert_equal value, token.send(key)
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end