rux 1.1.2 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/rux/parser.rb CHANGED
@@ -26,6 +26,12 @@ module Rux
26
26
  end
27
27
 
28
28
  def parse
29
+ AST::RootNode.new(list)
30
+ end
31
+
32
+ private
33
+
34
+ def list
29
35
  curlies = 1
30
36
  children = []
31
37
 
@@ -34,9 +40,9 @@ module Rux
34
40
  break unless type
35
41
 
36
42
  case type
37
- when :tLCURLY, :tLBRACE, :tRUX_LITERAL_RUBY_CODE_START
43
+ when :tLCURLY, :tLBRACE, :tRUX_LITERAL_RUBY_CODE_START, :tRUX_ATTRIBUTE_RUBY_CODE_START
38
44
  curlies += 1
39
- when :tRCURLY, :tRBRACE, :tRUX_LITERAL_RUBY_CODE_END
45
+ when :tRCURLY, :tRBRACE, :tRUX_LITERAL_RUBY_CODE_END, :tRUX_ATTRIBUTE_RUBY_CODE_END
40
46
  curlies -= 1
41
47
  end
42
48
 
@@ -46,6 +52,8 @@ module Rux
46
52
  children << rb
47
53
  elsif type_of(current) == :tRUX_TAG_OPEN_START
48
54
  children << tag
55
+ elsif type_of(current) == :tRUX_FRAGMENT_OPEN
56
+ children << fragment
49
57
  else
50
58
  raise UnexpectedTokenError,
51
59
  'expected ruby code or the start of a rux tag but found '\
@@ -56,8 +64,6 @@ module Rux
56
64
  AST::ListNode.new(children)
57
65
  end
58
66
 
59
- private
60
-
61
67
  def ruby
62
68
  ruby_start = pos_of(current).begin_pos
63
69
 
@@ -79,7 +85,7 @@ module Rux
79
85
 
80
86
  if pos_of(current).begin_pos != ruby_start
81
87
  AST::RubyNode.new(
82
- @lexer.source_buffer.source[ruby_start...(pos_of(current).end_pos - 1)]
88
+ @lexer.source_buffer.source[ruby_start...(pos_of(current).begin_pos)]
83
89
  )
84
90
  end
85
91
  end
@@ -93,7 +99,8 @@ module Rux
93
99
  attrs = attributes
94
100
  maybe_consume(:tRUX_ATTRIBUTE_SPACES)
95
101
  maybe_consume(:tRUX_TAG_OPEN_END)
96
- tag_node = AST::TagNode.new(tag_name, attrs)
102
+ tag_node = AST::TagNode.new(tag_name, attrs, tag_pos)
103
+ attrs.each { |attr_node| attr_node.tag_node = tag_node }
97
104
 
98
105
  if is?(:tRUX_TAG_SELF_CLOSING_END)
99
106
  consume(:tRUX_TAG_SELF_CLOSING_END)
@@ -103,12 +110,7 @@ module Rux
103
110
  @stack.push(tag_name)
104
111
 
105
112
  until is?(:tRUX_TAG_CLOSE_START)
106
- if is?(:tRUX_LITERAL, :tRUX_LITERAL_RUBY_CODE_START)
107
- lit = literal
108
- tag_node.children << lit if lit
109
- else
110
- tag_node.children << tag
111
- end
113
+ populate_next_child(tag_node)
112
114
  end
113
115
 
114
116
  consume(:tRUX_TAG_CLOSE_START)
@@ -131,44 +133,106 @@ module Rux
131
133
  tag_node
132
134
  end
133
135
 
136
+ def populate_next_child(node)
137
+ if is?(:tRUX_LITERAL, :tRUX_LITERAL_RUBY_CODE_START)
138
+ lit = literal
139
+ node.children << lit if lit
140
+ else
141
+ node.children << tag
142
+ end
143
+ end
144
+
134
145
  def attributes
135
- {}.tap do |attrs|
136
- while is?(:tRUX_ATTRIBUTE_NAME)
137
- key, value = attribute
138
- attrs[key] = value
146
+ pos = pos_of(current)
147
+
148
+ attrs = [].tap do |attrs|
149
+ loop do
150
+ attr = case type_of(current)
151
+ when :tRUX_ATTRIBUTE_NAME
152
+ attribute
153
+ when :tRUX_ATTRIBUTE_RUBY_CODE_START
154
+ ruby_attribute
155
+ end
139
156
 
140
157
  maybe_consume(:tRUX_ATTRIBUTE_SPACES)
158
+
159
+ if attr
160
+ attrs << attr
161
+ else
162
+ break
163
+ end
141
164
  end
142
165
  end
166
+
167
+ AST::AttrsNode.new(attrs, pos)
143
168
  end
144
169
 
145
170
  def attribute
146
- maybe_consume(:tRUX_ATTRIBUTE_SPACES)
147
171
  attr_name = text_of(current)
172
+ attr_pos = pos_of(current)
148
173
  consume(:tRUX_ATTRIBUTE_NAME)
149
174
  maybe_consume(:tRUX_ATTRIBUTE_EQUALS_SPACES)
150
175
 
151
176
  attr_value = if maybe_consume(:tRUX_ATTRIBUTE_EQUALS)
152
177
  maybe_consume(:tRUX_ATTRIBUTE_VALUE_SPACES)
153
- attribute_value
178
+ attribute_value.tap do
179
+ maybe_consume(:tRUX_ATTRIBUTE_VALUE_SPACES)
180
+ end
154
181
  else
155
182
  # if no equals sign, assume boolean attribute
156
- AST::StringNode.new("\"true\"")
183
+ AST::StringNode.new('true', :none, nil)
157
184
  end
158
185
 
159
- [attr_name, attr_value]
186
+ AST::AttrNode.new(attr_name, attr_value, attr_pos)
187
+ end
188
+
189
+ def ruby_attribute
190
+ consume(:tRUX_ATTRIBUTE_RUBY_CODE_START)
191
+
192
+ AST::RubyAttrNode.new(ruby).tap do
193
+ consume(:tRUX_ATTRIBUTE_RUBY_CODE_END)
194
+ end
160
195
  end
161
196
 
162
197
  def attribute_value
163
198
  if is?(:tRUX_ATTRIBUTE_VALUE_RUBY_CODE_START)
164
199
  attr_ruby_code
165
200
  else
166
- AST::StringNode.new(text_of(current)).tap do
167
- consume(:tRUX_ATTRIBUTE_VALUE)
201
+ case type_of(current)
202
+ when :tRUX_ATTRIBUTE_VALUE_DQ_START
203
+ attribute_value_dq
204
+ when :tRUX_ATTRIBUTE_VALUE_SQ_START
205
+ attribute_value_sq
206
+ when :tRUX_ATTRIBUTE_UQ_VALUE
207
+ attribute_value_uq
168
208
  end
169
209
  end
170
210
  end
171
211
 
212
+ def attribute_value_dq
213
+ consume(:tRUX_ATTRIBUTE_VALUE_DQ_START)
214
+
215
+ AST::StringNode.new(text_of(current), :double, pos_of(current)).tap do
216
+ consume(:tRUX_ATTRIBUTE_DQ_VALUE)
217
+ consume(:tRUX_ATTRIBUTE_VALUE_DQ_END)
218
+ end
219
+ end
220
+
221
+ def attribute_value_sq
222
+ consume(:tRUX_ATTRIBUTE_VALUE_SQ_START)
223
+
224
+ AST::StringNode.new(text_of(current), :single, pos_of(current)).tap do
225
+ consume(:tRUX_ATTRIBUTE_SQ_VALUE)
226
+ consume(:tRUX_ATTRIBUTE_VALUE_SQ_END)
227
+ end
228
+ end
229
+
230
+ def attribute_value_uq
231
+ AST::StringNode.new(text_of(current), :none, pos_of(current)).tap do
232
+ consume(:tRUX_ATTRIBUTE_UQ_VALUE)
233
+ end
234
+ end
235
+
172
236
  def attr_ruby_code
173
237
  consume(:tRUX_ATTRIBUTE_VALUE_RUBY_CODE_START)
174
238
 
@@ -177,13 +241,27 @@ module Rux
177
241
  end
178
242
  end
179
243
 
244
+ def fragment
245
+ consume(:tRUX_FRAGMENT_OPEN)
246
+
247
+ AST::FragmentNode.new.tap do |fragment_node|
248
+ until is?(:tRUX_TAG_CLOSE_START)
249
+ populate_next_child(fragment_node)
250
+ end
251
+
252
+ consume(:tRUX_TAG_CLOSE_START)
253
+ consume(:tRUX_FRAGMENT_CLOSE)
254
+ end
255
+ end
256
+
180
257
  def literal
181
258
  if is?(:tRUX_LITERAL_RUBY_CODE_START)
182
259
  literal_ruby_code
183
260
  else
184
261
  lit = squeeze_lit(text_of(current))
262
+ pos = pos_of(current)
185
263
  consume(:tRUX_LITERAL)
186
- AST::TextNode.new(lit) unless lit.empty?
264
+ AST::TextNode.new(lit, pos) unless lit.empty?
187
265
  end
188
266
  end
189
267
 
@@ -197,7 +275,7 @@ module Rux
197
275
  def literal_ruby_code
198
276
  consume(:tRUX_LITERAL_RUBY_CODE_START)
199
277
 
200
- parse.tap do |res|
278
+ list.tap do
201
279
  consume(:tRUX_LITERAL_RUBY_CODE_END)
202
280
  end
203
281
  end
@@ -117,6 +117,7 @@ module Rux
117
117
 
118
118
  def at_lt?
119
119
  is?(@rux_token_queue[1], :tLT) && (
120
+ is?(@rux_token_queue[2], :tGT) ||
120
121
  is?(@rux_token_queue[2], :tCONSTANT) ||
121
122
  is?(@rux_token_queue[2], :tIDENTIFIER)
122
123
  )
data/lib/rux/rux_lexer.rb CHANGED
@@ -2,6 +2,17 @@ require 'csv'
2
2
 
3
3
  module Rux
4
4
  class RuxLexer
5
+ RUBY_CODE_STATES = [
6
+ # ruby code in attributes, eg. <div {**kwargs}>
7
+ :tRUX_ATTRIBUTE_RUBY_CODE,
8
+
9
+ # ruby code in attribute values, eg. <div foo={bar}>
10
+ :tRUX_ATTRIBUTE_VALUE_RUBY_CODE,
11
+
12
+ # ruby code in tag bodies, eg. <div>{foo}</div>
13
+ :tRUX_LITERAL_RUBY_CODE
14
+ ].freeze
15
+
5
16
  class << self
6
17
  # See: https://docs.google.com/spreadsheets/d/11ikKuySIKoaj-kFIfhlzebUwH31cRt_1flGjWfk7RMg
7
18
  def state_table
@@ -87,11 +98,13 @@ module Rux
87
98
  case state
88
99
  when :tRUX_TAG_OPEN, :tRUX_TAG_SELF_CLOSING
89
100
  tag_stack.push(text)
101
+ when :tRUX_FRAGMENT_OPEN
102
+ tag_stack.push(:__fragment)
90
103
  when :tRUX_TAG_CLOSE
91
104
  tag_stack.pop
92
105
  when :tRUX_TAG_CLOSE_END
93
106
  break if tag_stack.empty?
94
- when :tRUX_TAG_SELF_CLOSING_END
107
+ when :tRUX_TAG_SELF_CLOSING_END, :tRUX_FRAGMENT_CLOSE
95
108
  tag_stack.pop
96
109
  break if tag_stack.empty?
97
110
  end
@@ -150,8 +163,7 @@ module Rux
150
163
  # bodies. Eventually I'd like to also allow passing a Ruby hash to
151
164
  # dynamically specify attributes, but we're not there yet.
152
165
  def ruby_code?(state)
153
- state == :tRUX_ATTRIBUTE_VALUE_RUBY_CODE ||
154
- state == :tRUX_LITERAL_RUBY_CODE
166
+ RUBY_CODE_STATES.include?(state)
155
167
  end
156
168
  end
157
169
  end
data/lib/rux/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Rux
2
- VERSION = '1.1.2'
2
+ VERSION = '1.2.0'
3
3
  end
data/lib/rux/visitor.rb CHANGED
@@ -4,6 +4,10 @@ module Rux
4
4
  node.accept(self)
5
5
  end
6
6
 
7
+ def visit_root(node)
8
+ visit_children(node)
9
+ end
10
+
7
11
  def visit_list(node)
8
12
  visit_children(node)
9
13
  end
@@ -20,6 +24,18 @@ module Rux
20
24
  visit_children(node)
21
25
  end
22
26
 
27
+ def visit_attrs(node)
28
+ visit_children(node)
29
+ end
30
+
31
+ def visit_attr(node)
32
+ visit_children(node)
33
+ end
34
+
35
+ def visit_fragment(node)
36
+ visit_children(node)
37
+ end
38
+
23
39
  def visit_text(node)
24
40
  visit_children(node)
25
41
  end
@@ -0,0 +1,97 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'attributes', type: :parser do
4
+ it 'handles single-quoted rux attributes' do
5
+ expect(compile("<Hello foo='bar' />")).to eq(
6
+ 'render(Hello.new(foo: "bar"))'
7
+ )
8
+
9
+ expect(compile("<Hello foo='bar'></Hello>")).to eq(
10
+ 'render(Hello.new(foo: "bar"))'
11
+ )
12
+ end
13
+
14
+ it 'handles double-quoted rux attributes' do
15
+ expect(compile('<Hello foo="bar" />')).to eq(
16
+ 'render(Hello.new(foo: "bar"))'
17
+ )
18
+
19
+ expect(compile('<Hello foo="bar"></Hello>')).to eq(
20
+ 'render(Hello.new(foo: "bar"))'
21
+ )
22
+ end
23
+
24
+ it 'handles unquoted rux attributes' do
25
+ expect(compile('<Hello foo=bar />')).to eq(
26
+ 'render(Hello.new(foo: "bar"))'
27
+ )
28
+
29
+ expect(compile('<Hello foo=bar></Hello>')).to eq(
30
+ 'render(Hello.new(foo: "bar"))'
31
+ )
32
+ end
33
+
34
+ it 'handles non-uniform spacing between attributes' do
35
+ expect(compile('<Hello foo="bar" baz= "boo" bix ="bit" />')).to eq(
36
+ 'render(Hello.new(foo: "bar", baz: "boo", bix: "bit"))'
37
+ )
38
+ end
39
+
40
+ it 'handles boolean attributes' do
41
+ expect(compile('<Hello disabled />')).to eq(
42
+ 'render(Hello.new(disabled: "true"))'
43
+ )
44
+
45
+ expect(compile('<Hello disabled/>')).to eq(
46
+ 'render(Hello.new(disabled: "true"))'
47
+ )
48
+
49
+ expect(compile('<Hello disabled></Hello>')).to eq(
50
+ 'render(Hello.new(disabled: "true"))'
51
+ )
52
+ end
53
+
54
+ it 'converts dashes to underscores in attribute keys' do
55
+ expect(compile('<Hello foo-bar="baz" />')).to eq(
56
+ 'render(Hello.new(foo_bar: "baz"))'
57
+ )
58
+ end
59
+
60
+ it 'handles simple ruby statements in attributes' do
61
+ expect(compile('<Hello foo={true} />')).to eq(
62
+ 'render(Hello.new(foo: true))'
63
+ )
64
+ end
65
+
66
+ it 'handles ruby hashes in attributes' do
67
+ expect(compile('<Hello foo={{ foo: "bar", baz: "boo" }} />')).to eq(
68
+ 'render(Hello.new(foo: { foo: "bar", baz: "boo" }))'
69
+ )
70
+ end
71
+
72
+ it 'handles ruby code with curly braces in attributes' do
73
+ expect(compile('<Hello foo={[1, 2, 3].map { |n| n * 2 }} />')).to eq(<<~RUBY.strip)
74
+ render(Hello.new(foo: [1, 2, 3].map { |n|
75
+ n * 2
76
+ }))
77
+ RUBY
78
+ end
79
+
80
+ it 'slugifies ruby arguments' do
81
+ code = <<~RUX
82
+ <Hello data-foo="bar" />
83
+ RUX
84
+ expect(compile(code)).to eq(<<~RUBY.strip)
85
+ render(Hello.new(data_foo: "bar"))
86
+ RUBY
87
+ end
88
+
89
+ it 'does not slugify HTML attributes' do
90
+ code = <<~RUX
91
+ <div data-foo="bar" />
92
+ RUX
93
+ expect(compile(code)).to eq(<<~RUBY.strip)
94
+ Rux.tag("div", { :"data-foo" => "bar" })
95
+ RUBY
96
+ end
97
+ end
@@ -0,0 +1,47 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'fragments', type: :parser do
4
+ it 'allows fragments' do
5
+ code = <<~RUX
6
+ <>
7
+ <div>Foo 1</div>
8
+ <div>Foo 2</div>
9
+ </>
10
+ RUX
11
+ expect(compile(code)).to eq(<<~RUBY.strip)
12
+ Rux.create_buffer.tap { |_rux_buf_|
13
+ _rux_buf_.append(Rux.tag("div") {
14
+ Rux.create_buffer.tap { |_rux_buf_|
15
+ _rux_buf_.safe_append("Foo 1")
16
+ }.to_s
17
+ })
18
+ _rux_buf_.append(Rux.tag("div") {
19
+ Rux.create_buffer.tap { |_rux_buf_|
20
+ _rux_buf_.safe_append("Foo 2")
21
+ }.to_s
22
+ })
23
+ }.to_s
24
+ RUBY
25
+ end
26
+
27
+ it 'allows fragments nested inside ruby code' do
28
+ code = <<~RUX
29
+ <table>
30
+ {rows.map do |row|
31
+ <>{row}</>
32
+ end}
33
+ </table>
34
+ RUX
35
+ expect(compile(code)).to eq(<<~RUBY.strip)
36
+ Rux.tag("table") {
37
+ Rux.create_buffer.tap { |_rux_buf_|
38
+ _rux_buf_.append(rows.map { |row|
39
+ Rux.create_buffer.tap { |_rux_buf_|
40
+ _rux_buf_.append(row)
41
+ }.to_s
42
+ })
43
+ }.to_s
44
+ }
45
+ RUBY
46
+ end
47
+ end
@@ -0,0 +1,13 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'html safety', type: :parser do
4
+ it 'escapes HTML entities in strings' do
5
+ expect(compile('<Hello>"foo"</Hello>')).to eq(<<~RUBY.strip)
6
+ render(Hello.new) { |rux_block_arg0|
7
+ Rux.create_buffer.tap { |_rux_buf_|
8
+ _rux_buf_.safe_append("&quot;foo&quot;")
9
+ }.to_s
10
+ }
11
+ RUBY
12
+ end
13
+ end
@@ -0,0 +1,40 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'slots', type: :parser do
4
+ it 'correctly transforms slot components into slot methods' do
5
+ code = <<~RUX
6
+ <TableComponent>
7
+ <WithRow>
8
+ <WithColumn>Foo 1</WithColumn>
9
+ </WithRow>
10
+ <WithRow>
11
+ <WithColumn>Foo 2</WithColumn>
12
+ </WithRow>
13
+ </TableComponent>
14
+ RUX
15
+ expect(compile(code)).to eq(<<~RUBY.strip)
16
+ render(TableComponent.new) { |rux_block_arg0|
17
+ Rux.create_buffer.tap { |_rux_buf_|
18
+ _rux_buf_.append((rux_block_arg0.with_row { |rux_block_arg1|
19
+ Rux.create_buffer.tap { |_rux_buf_|
20
+ _rux_buf_.append((rux_block_arg1.with_column { |rux_block_arg2|
21
+ Rux.create_buffer.tap { |_rux_buf_|
22
+ _rux_buf_.safe_append("Foo 1")
23
+ }.to_s
24
+ }; nil))
25
+ }.to_s
26
+ }; nil))
27
+ _rux_buf_.append((rux_block_arg0.with_row { |rux_block_arg1|
28
+ Rux.create_buffer.tap { |_rux_buf_|
29
+ _rux_buf_.append((rux_block_arg1.with_column { |rux_block_arg2|
30
+ Rux.create_buffer.tap { |_rux_buf_|
31
+ _rux_buf_.safe_append("Foo 2")
32
+ }.to_s
33
+ }; nil))
34
+ }.to_s
35
+ }; nil))
36
+ }.to_s
37
+ }
38
+ RUBY
39
+ end
40
+ end
@@ -0,0 +1,174 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'tags', type: :parser do
4
+ it 'handles a single self-closing tag' do
5
+ expect(compile("<Hello/>")).to eq("render(Hello.new)")
6
+ end
7
+
8
+ it 'handles a self-closing tag with spaces preceding the closing punctuation' do
9
+ expect(compile("<Hello />")).to eq("render(Hello.new)")
10
+ end
11
+
12
+ it 'handles a single opening and closing tag' do
13
+ expect(compile("<Hello></Hello>")).to eq('render(Hello.new)')
14
+ end
15
+
16
+ it 'handles a single tag with a text body' do
17
+ expect(compile("<Hello>foo</Hello>")).to eq(<<~RUBY.strip)
18
+ render(Hello.new) { |rux_block_arg0|
19
+ Rux.create_buffer.tap { |_rux_buf_|
20
+ _rux_buf_.safe_append("foo")
21
+ }.to_s
22
+ }
23
+ RUBY
24
+ end
25
+
26
+ it 'handles simple ruby statements in tag bodies' do
27
+ expect(compile('<Hello>{"foo"}</Hello>')).to eq(<<~RUBY.strip)
28
+ render(Hello.new) { |rux_block_arg0|
29
+ Rux.create_buffer.tap { |_rux_buf_|
30
+ _rux_buf_.append("foo")
31
+ }.to_s
32
+ }
33
+ RUBY
34
+ end
35
+
36
+ it 'handles tag bodies containing ruby code with curly braces' do
37
+ expect(compile('<Hello>{[1, 2, 3].map { |n| n * 2 }.join(", ")}</Hello>')).to eq(<<~RUBY.strip)
38
+ render(Hello.new) { |rux_block_arg0|
39
+ Rux.create_buffer.tap { |_rux_buf_|
40
+ _rux_buf_.append([1, 2, 3].map { |n|
41
+ n * 2
42
+ }.join(", "))
43
+ }.to_s
44
+ }
45
+ RUBY
46
+ end
47
+
48
+ it 'handles tag bodies with intermixed text and ruby code' do
49
+ expect(compile('<Hello>abc {foo} def {bar} baz</Hello>')).to eq(<<~RUBY.strip)
50
+ render(Hello.new) { |rux_block_arg0|
51
+ Rux.create_buffer.tap { |_rux_buf_|
52
+ _rux_buf_.safe_append("abc ")
53
+ _rux_buf_.append(foo)
54
+ _rux_buf_.safe_append(" def ")
55
+ _rux_buf_.append(bar)
56
+ _rux_buf_.safe_append(" baz")
57
+ }.to_s
58
+ }
59
+ RUBY
60
+ end
61
+
62
+ it 'handles rux tags inside ruby code' do
63
+ rux_code = <<~RUX
64
+ <Outer>
65
+ {5.times.map do
66
+ <Inner>What a {@thing}</Inner>
67
+ end}
68
+ </Outer>
69
+ RUX
70
+
71
+ expect(compile(rux_code)).to eq(<<~RUBY.strip)
72
+ render(Outer.new) { |rux_block_arg0|
73
+ Rux.create_buffer.tap { |_rux_buf_|
74
+ _rux_buf_.append(5.times.map {
75
+ render(Inner.new) { |rux_block_arg1|
76
+ Rux.create_buffer.tap { |_rux_buf_|
77
+ _rux_buf_.safe_append("What a ")
78
+ _rux_buf_.append(@thing)
79
+ }.to_s
80
+ }
81
+ })
82
+ }.to_s
83
+ }
84
+ RUBY
85
+ end
86
+
87
+ it 'handles HTML tags inside ruby code' do
88
+ rux_code = <<~RUX
89
+ <div>
90
+ {5.times.map do
91
+ <p>What a {@thing}</p>
92
+ end}
93
+ </div>
94
+ RUX
95
+
96
+ expect(compile(rux_code)).to eq(<<~RUBY.strip)
97
+ Rux.tag("div") {
98
+ Rux.create_buffer.tap { |_rux_buf_|
99
+ _rux_buf_.append(5.times.map {
100
+ Rux.tag("p") {
101
+ Rux.create_buffer.tap { |_rux_buf_|
102
+ _rux_buf_.safe_append("What a ")
103
+ _rux_buf_.append(@thing)
104
+ }.to_s
105
+ }
106
+ })
107
+ }.to_s
108
+ }
109
+ RUBY
110
+ end
111
+
112
+ it 'handles regular HTML tags' do
113
+ expect(compile('<div>foo</div>')).to eq(<<~RUBY.strip)
114
+ Rux.tag("div") {
115
+ Rux.create_buffer.tap { |_rux_buf_|
116
+ _rux_buf_.safe_append("foo")
117
+ }.to_s
118
+ }
119
+ RUBY
120
+ end
121
+
122
+ it 'handles regular HTML tags inside ruby code' do
123
+ rux_code = <<~RUX
124
+ <Outer>
125
+ {5.times.map do
126
+ <div>So {@cool}</div>
127
+ end}
128
+ </Outer>
129
+ RUX
130
+
131
+ expect(compile(rux_code)).to eq(<<~RUBY.strip)
132
+ render(Outer.new) { |rux_block_arg0|
133
+ Rux.create_buffer.tap { |_rux_buf_|
134
+ _rux_buf_.append(5.times.map {
135
+ Rux.tag("div") {
136
+ Rux.create_buffer.tap { |_rux_buf_|
137
+ _rux_buf_.safe_append("So ")
138
+ _rux_buf_.append(@cool)
139
+ }.to_s
140
+ }
141
+ })
142
+ }.to_s
143
+ }
144
+ RUBY
145
+ end
146
+
147
+ it 'demonstrates that tags support keyword splats' do
148
+ code = <<~RUX
149
+ <div {**args}>foo</div>
150
+ RUX
151
+
152
+ expect(compile(code)).to eq(<<~RUBY.strip)
153
+ Rux.tag("div", { **args }) {
154
+ Rux.create_buffer.tap { |_rux_buf_|
155
+ _rux_buf_.safe_append("foo")
156
+ }.to_s
157
+ }
158
+ RUBY
159
+ end
160
+
161
+ it 'demonstrates that tags support keyword arguments mixed with splats' do
162
+ code = <<~RUX
163
+ <div foo="bar" {**args} baz={"boo"}>foo</div>
164
+ RUX
165
+
166
+ expect(compile(code)).to eq(<<~RUBY.strip)
167
+ Rux.tag("div", { foo: "bar", **args, baz: "boo" }) {
168
+ Rux.create_buffer.tap { |_rux_buf_|
169
+ _rux_buf_.safe_append("foo")
170
+ }.to_s
171
+ }
172
+ RUBY
173
+ end
174
+ end