rux 1.1.1 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
data/spec/parser_spec.rb CHANGED
@@ -2,214 +2,7 @@ require 'spec_helper'
2
2
  require 'parser'
3
3
  require 'unparser'
4
4
 
5
- describe Rux::Parser do
6
- def compile(rux_code)
7
- Rux.to_ruby(rux_code)
8
- end
9
-
10
- it 'handles a single self-closing tag' do
11
- expect(compile("<Hello/>")).to eq("render(Hello.new)")
12
- end
13
-
14
- it 'handles a self-closing tag with spaces preceding the closing punctuation' do
15
- expect(compile("<Hello />")).to eq("render(Hello.new)")
16
- end
17
-
18
- it 'handles a single opening and closing tag' do
19
- expect(compile("<Hello></Hello>")).to eq('render(Hello.new)')
20
- end
21
-
22
- it 'handles a single tag with a text body' do
23
- expect(compile("<Hello>foo</Hello>")).to eq(<<~RUBY.strip)
24
- render(Hello.new) {
25
- "foo"
26
- }
27
- RUBY
28
- end
29
-
30
- it 'handles single-quoted rux attributes' do
31
- expect(compile("<Hello foo='bar' />")).to eq(
32
- 'render(Hello.new(foo: "bar"))'
33
- )
34
-
35
- expect(compile("<Hello foo='bar'></Hello>")).to eq(
36
- 'render(Hello.new(foo: "bar"))'
37
- )
38
- end
39
-
40
- it 'handles double-quoted rux attributes' do
41
- expect(compile('<Hello foo="bar" />')).to eq(
42
- 'render(Hello.new(foo: "bar"))'
43
- )
44
-
45
- expect(compile('<Hello foo="bar"></Hello>')).to eq(
46
- 'render(Hello.new(foo: "bar"))'
47
- )
48
- end
49
-
50
- it 'handles non-uniform spacing between attributes' do
51
- expect(compile('<Hello foo="bar" baz= "boo" bix ="bit" />')).to eq(
52
- 'render(Hello.new(foo: "bar", baz: "boo", bix: "bit"))'
53
- )
54
- end
55
-
56
- it 'handles boolean attributes' do
57
- expect(compile('<Hello disabled />')).to eq(
58
- 'render(Hello.new(disabled: "true"))'
59
- )
60
-
61
- expect(compile('<Hello disabled/>')).to eq(
62
- 'render(Hello.new(disabled: "true"))'
63
- )
64
-
65
- expect(compile('<Hello disabled></Hello>')).to eq(
66
- 'render(Hello.new(disabled: "true"))'
67
- )
68
- end
69
-
70
- it 'converts dashes to underscores in attribute keys' do
71
- expect(compile('<Hello foo-bar="baz" />')).to eq(
72
- 'render(Hello.new(foo_bar: "baz"))'
73
- )
74
- end
75
-
76
- it 'handles simple ruby statements in attributes' do
77
- expect(compile('<Hello foo={true} />')).to eq(
78
- 'render(Hello.new(foo: true))'
79
- )
80
- end
81
-
82
- it 'handles ruby hashes in attributes' do
83
- expect(compile('<Hello foo={{ foo: "bar", baz: "boo" }} />')).to eq(
84
- 'render(Hello.new(foo: { foo: "bar", baz: "boo" }))'
85
- )
86
- end
87
-
88
- it 'handles ruby code with curly braces in attributes' do
89
- expect(compile('<Hello foo={[1, 2, 3].map { |n| n * 2 }} />')).to eq(<<~RUBY.strip)
90
- render(Hello.new(foo: [1, 2, 3].map { |n|
91
- n * 2
92
- }))
93
- RUBY
94
- end
95
-
96
- it 'handles simple ruby statements in tag bodies' do
97
- expect(compile('<Hello>{"foo"}</Hello>')).to eq(<<~RUBY.strip)
98
- render(Hello.new) {
99
- "foo"
100
- }
101
- RUBY
102
- end
103
-
104
- it 'handles tag bodies containing ruby code with curly braces' do
105
- expect(compile('<Hello>{[1, 2, 3].map { |n| n * 2 }.join(", ")}</Hello>')).to eq(<<~RUBY.strip)
106
- render(Hello.new) {
107
- [1, 2, 3].map { |n|
108
- n * 2
109
- }.join(", ")
110
- }
111
- RUBY
112
- end
113
-
114
- it 'handles tag bodies with intermixed text and ruby code' do
115
- expect(compile('<Hello>abc {foo} def {bar} baz</Hello>')).to eq(<<~RUBY.strip)
116
- render(Hello.new) {
117
- Rux.create_buffer.tap { |_rux_buf_|
118
- _rux_buf_ << "abc "
119
- _rux_buf_ << foo
120
- _rux_buf_ << " def "
121
- _rux_buf_ << bar
122
- _rux_buf_ << " baz"
123
- }.to_s
124
- }
125
- RUBY
126
- end
127
-
128
- it 'handles rux tags inside ruby code' do
129
- rux_code = <<~RUX
130
- <Outer>
131
- {5.times.map do
132
- <Inner>What a {@thing}</Inner>
133
- end}
134
- </Outer>
135
- RUX
136
-
137
- expect(compile(rux_code)).to eq(<<~RUBY.strip)
138
- render(Outer.new) {
139
- 5.times.map {
140
- render(Inner.new) {
141
- Rux.create_buffer.tap { |_rux_buf_|
142
- _rux_buf_ << "What a "
143
- _rux_buf_ << @thing
144
- }.to_s
145
- }
146
- }
147
- }
148
- RUBY
149
- end
150
-
151
- it 'handles HTML tags inside ruby code' do
152
- rux_code = <<~RUX
153
- <div>
154
- {5.times.map do
155
- <p>What a {@thing}</p>
156
- end}
157
- </div>
158
- RUX
159
-
160
- expect(compile(rux_code)).to eq(<<~RUBY.strip)
161
- Rux.tag("div") {
162
- 5.times.map {
163
- Rux.tag("p") {
164
- Rux.create_buffer.tap { |_rux_buf_|
165
- _rux_buf_ << "What a "
166
- _rux_buf_ << @thing
167
- }.to_s
168
- }
169
- }
170
- }
171
- RUBY
172
- end
173
-
174
- it 'handles regular HTML tags' do
175
- expect(compile('<div>foo</div>')).to eq(<<~RUBY.strip)
176
- Rux.tag("div") {
177
- "foo"
178
- }
179
- RUBY
180
- end
181
-
182
- it 'handles regular HTML tags inside ruby code' do
183
- rux_code = <<~RUX
184
- <Outer>
185
- {5.times.map do
186
- <div>So {@cool}</div>
187
- end}
188
- </Outer>
189
- RUX
190
-
191
- expect(compile(rux_code)).to eq(<<~RUBY.strip)
192
- render(Outer.new) {
193
- 5.times.map {
194
- Rux.tag("div") {
195
- Rux.create_buffer.tap { |_rux_buf_|
196
- _rux_buf_ << "So "
197
- _rux_buf_ << @cool
198
- }.to_s
199
- }
200
- }
201
- }
202
- RUBY
203
- end
204
-
205
- it 'escapes HTML entities in strings' do
206
- expect(compile('<Hello>"foo"</Hello>')).to eq(<<~RUBY.strip)
207
- render(Hello.new) {
208
- "&quot;foo&quot;"
209
- }
210
- RUBY
211
- end
212
-
5
+ describe 'parsing', type: :parser do
213
6
  it 'raises an error on premature end of input' do
214
7
  expect { compile('<Hello') }.to raise_error(Rux::Lexer::EOFError)
215
8
  end
@@ -230,13 +23,13 @@ describe Rux::Parser do
230
23
  )
231
24
  end
232
25
 
233
- it 'emits handles spaces between adjacent ruby code snippets' do
26
+ it 'emits spaces between adjacent ruby code snippets' do
234
27
  expect(compile("<Hello>{first} {second}</Hello>")).to eq(<<~RUBY.strip)
235
- render(Hello.new) {
28
+ render(Hello.new) { |rux_block_arg0|
236
29
  Rux.create_buffer.tap { |_rux_buf_|
237
- _rux_buf_ << first
238
- _rux_buf_ << " "
239
- _rux_buf_ << second
30
+ _rux_buf_.append(first)
31
+ _rux_buf_.safe_append(" ")
32
+ _rux_buf_.append(second)
240
33
  }.to_s
241
34
  }
242
35
  RUBY
@@ -250,42 +43,24 @@ describe Rux::Parser do
250
43
  </Hello>
251
44
  RUX
252
45
  expect(compile(code)).to eq(<<~RUBY.strip)
253
- render(Hello.new) {
46
+ render(Hello.new) { |rux_block_arg0|
254
47
  Rux.create_buffer.tap { |_rux_buf_|
255
- _rux_buf_ << render(Hola.new) {
48
+ _rux_buf_.append(render(Hola.new) { |rux_block_arg1|
256
49
  Rux.create_buffer.tap { |_rux_buf_|
257
- _rux_buf_ << first
258
- _rux_buf_ << " "
259
- _rux_buf_ << second
50
+ _rux_buf_.append(first)
51
+ _rux_buf_.safe_append(" ")
52
+ _rux_buf_.append(second)
260
53
  }.to_s
261
- }
262
- _rux_buf_ << render(Hola.new) {
54
+ })
55
+ _rux_buf_.append(render(Hola.new) { |rux_block_arg1|
263
56
  Rux.create_buffer.tap { |_rux_buf_|
264
- _rux_buf_ << first
265
- _rux_buf_ << " "
266
- _rux_buf_ << second
57
+ _rux_buf_.append(first)
58
+ _rux_buf_.safe_append(" ")
59
+ _rux_buf_.append(second)
267
60
  }.to_s
268
- }
61
+ })
269
62
  }.to_s
270
63
  }
271
64
  RUBY
272
65
  end
273
-
274
- it 'slugifies ruby arguments' do
275
- code = <<~RUX
276
- <Hello data-foo="bar" />
277
- RUX
278
- expect(compile(code)).to eq(<<~RUBY.strip)
279
- render(Hello.new(data_foo: "bar"))
280
- RUBY
281
- end
282
-
283
- it 'does not slugify HTML attributes' do
284
- code = <<~RUX
285
- <div data-foo="bar" />
286
- RUX
287
- expect(compile(code)).to eq(<<~RUBY.strip)
288
- Rux.tag("div", { :"data-foo" => "bar" })
289
- RUBY
290
- end
291
66
  end
data/spec/render_spec.rb CHANGED
@@ -1,11 +1,6 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe Rux do
4
- def render(rux_code)
5
- ruby_code = Rux.to_ruby(rux_code)
6
- ViewComponent::Base.new.instance_eval(ruby_code)
7
- end
8
-
3
+ describe 'rendering', type: :render do
9
4
  it 'handles a HTML tags inside ruby code' do
10
5
  result = render(<<~RUBY)
11
6
  <div>
@@ -63,4 +58,92 @@ describe Rux do
63
58
  "<div><p>Hello World</p><p>Hello World</p><p>Hello World</p><p>Hello World</p></div>"
64
59
  )
65
60
  end
61
+
62
+ it 'slugifies ruby arguments' do
63
+ result = render(<<~RUBY)
64
+ <DataComponent data-foo="foo" />
65
+ RUBY
66
+
67
+ expect(result).to eq(
68
+ "<div data-foo=\"foo\"></div>"
69
+ )
70
+ end
71
+
72
+ it 'does not slugify HTML attributes' do
73
+ result = render(<<~RUBY)
74
+ <div data-foo="bar"></div>
75
+ RUBY
76
+
77
+ expect(result).to eq(
78
+ "<div data-foo=\"bar\"></div>"
79
+ )
80
+ end
81
+
82
+ it 'renders slots correctly' do
83
+ result = render(<<~RUBY)
84
+ <TableComponent>
85
+ <WithRow>
86
+ <WithColumn>Foo 1</WithColumn>
87
+ </WithRow>
88
+ <WithRow>
89
+ <WithColumn>Foo 2</WithColumn>
90
+ </WithRow>
91
+ </TableComponent>
92
+ RUBY
93
+
94
+ expect(result).to eq(
95
+ "<table><tr><td>Foo 1</td></tr><tr><td>Foo 2</td></tr></table>"
96
+ )
97
+ end
98
+
99
+ it 'calls .to_s on anything appended to a buffer' do
100
+ result = render(<<~RUBY)
101
+ <div>
102
+ {[["foo", "bar"], ["baz", "boo"]].map do |row|
103
+ <>{row}</>
104
+ end}
105
+ </div>
106
+ RUBY
107
+ expect(result).to eq(
108
+ '<div>foobarbazboo</div>'
109
+ )
110
+ end
111
+
112
+ it 'escapes arbitrary ruby expressions' do
113
+ result = render(<<~RUBY, value: "<p>Foo</p>")
114
+ <div>{kwargs[:value]}</div>
115
+ RUBY
116
+ expect(result).to eq(
117
+ "<div>&lt;p&gt;Foo&lt;/p&gt;</div>"
118
+ )
119
+ end
120
+
121
+ it 'escapes double quotes in HTML attributes' do
122
+ result = render(<<~RUBY)
123
+ <div class={'"foo"'}>foo</div>
124
+ RUBY
125
+ expect(result).to eq(
126
+ "<div class=\"&quot;foo&quot;\">foo</div>"
127
+ )
128
+ end
129
+
130
+ it 'handles keyword splats' do
131
+ kwargs = { a: "foo", b: "bar" }
132
+ result = render(<<~RUBY, **kwargs)
133
+ <ArgsComponent {**kwargs} />
134
+ RUBY
135
+ expect(result).to eq(
136
+ "<p>foo and bar</p>"
137
+ )
138
+ end
139
+
140
+ it 'handles keyword splats mixed with keyword args' do
141
+ kwargs = { a: "foo", b: "bar" }
142
+ result = render(<<~RUBY, **kwargs)
143
+ <ArgsComponent {**(kwargs.except(:b))} b={kwargs[:b]} />
144
+ RUBY
145
+ expect(result).to eq(
146
+ "<p>foo and bar</p>"
147
+ )
148
+ end
66
149
  end
data/spec/spec_helper.rb CHANGED
@@ -1,37 +1,34 @@
1
1
  require 'rspec'
2
2
  require 'rux'
3
- require 'pry-byebug'
4
3
 
5
- # a very basic implementation of the view_component lib
6
- module ViewComponent
7
- class Base
8
- attr_accessor :content
4
+ $:.push(File.join(__dir__, 'support'))
9
5
 
10
- def render(component, &block)
11
- component.content = block.call if block
12
- component.call
13
- end
14
- end
15
- end
6
+ require 'view_component/base'
16
7
 
17
- class TestComponent < ViewComponent::Base
18
- def call
19
- content
8
+ Dir.chdir(File.join(__dir__, 'support')) do
9
+ Dir.glob('components/*.rb').each do |component_file|
10
+ require component_file
20
11
  end
21
12
  end
22
13
 
23
- class ArgsComponent < ViewComponent::Base
24
- attr_reader :a, :b
25
-
26
- def initialize(a:, b:)
27
- @a = a
28
- @b = b
14
+ module Rux
15
+ module ParserTestHelpers
16
+ def compile(rux_code)
17
+ Rux.to_ruby(rux_code)
18
+ end
29
19
  end
30
20
 
31
- def call
32
- "<p>#{a} and #{b}</p>"
21
+ module RenderTestHelpers
22
+ def render(rux_code, **kwargs)
23
+ ruby_code = Rux.to_ruby(rux_code)
24
+ ViewComponent::Base.new.instance_exec(ruby_code, **kwargs) do |ruby_code, **kwargs|
25
+ eval(ruby_code)
26
+ end
27
+ end
33
28
  end
34
29
  end
35
30
 
36
31
  RSpec.configure do |config|
32
+ config.include(Rux::ParserTestHelpers, type: :parser)
33
+ config.include(Rux::RenderTestHelpers, type: :render)
37
34
  end
@@ -0,0 +1,14 @@
1
+ require 'view_component/base'
2
+
3
+ class ArgsComponent < ViewComponent::Base
4
+ attr_reader :a, :b
5
+
6
+ def initialize(a:, b:)
7
+ @a = a
8
+ @b = b
9
+ end
10
+
11
+ def call
12
+ "<p>#{a} and #{b}</p>"
13
+ end
14
+ end
@@ -0,0 +1,11 @@
1
+ require 'view_component/base'
2
+
3
+ class DataComponent < ViewComponent::Base
4
+ def initialize(data_foo:)
5
+ @data_foo = data_foo
6
+ end
7
+
8
+ def call
9
+ "<div data-foo=\"#{@data_foo}\"></div>"
10
+ end
11
+ end
@@ -0,0 +1,23 @@
1
+ require 'view_component/base'
2
+
3
+ class ColumnComponent < ViewComponent::Base
4
+ def call
5
+ "<td>#{content}</td>"
6
+ end
7
+ end
8
+
9
+ class RowComponent < ViewComponent::Base
10
+ renders_many :columns, ColumnComponent
11
+
12
+ def call
13
+ "<tr>#{columns.map(&:to_s).join}</tr>"
14
+ end
15
+ end
16
+
17
+ class TableComponent < ViewComponent::Base
18
+ renders_many :rows, RowComponent
19
+
20
+ def call
21
+ "<table>#{rows.map(&:to_s).join}</table>"
22
+ end
23
+ end
@@ -0,0 +1,7 @@
1
+ require 'view_component/base'
2
+
3
+ class TestComponent < ViewComponent::Base
4
+ def call
5
+ content
6
+ end
7
+ end