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/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>
@@ -83,4 +78,72 @@ describe Rux do
83
78
  "<div data-foo=\"bar\"></div>"
84
79
  )
85
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
86
149
  end
data/spec/spec_helper.rb CHANGED
@@ -1,47 +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
29
- end
30
-
31
- def call
32
- "<p>#{a} and #{b}</p>"
33
- end
34
- end
35
-
36
- class DataComponent < ViewComponent::Base
37
- def initialize(data_foo:)
38
- @data_foo = data_foo
14
+ module Rux
15
+ module ParserTestHelpers
16
+ def compile(rux_code)
17
+ Rux.to_ruby(rux_code)
18
+ end
39
19
  end
40
20
 
41
- def call
42
- "<div data-foo=\"#{@data_foo}\"></div>"
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
43
28
  end
44
29
  end
45
30
 
46
31
  RSpec.configure do |config|
32
+ config.include(Rux::ParserTestHelpers, type: :parser)
33
+ config.include(Rux::RenderTestHelpers, type: :render)
47
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
@@ -0,0 +1,59 @@
1
+ # a very basic implementation of the view_component lib
2
+ module ViewComponent
3
+ class Slot
4
+ def initialize(component_instance, content_block)
5
+ @component_instance = component_instance
6
+ @content_block = content_block
7
+ end
8
+
9
+ def to_s
10
+ @component_instance.content = @content_block.call(@component_instance) if @content_block
11
+ @component_instance.call
12
+ end
13
+ end
14
+
15
+ class Base
16
+ class << self
17
+ def renders_many(name, component)
18
+ singular_name = name.to_s.chomp("s")
19
+
20
+ registered_slots[name] = {
21
+ renderable: component,
22
+ collection: true
23
+ }
24
+
25
+ define_method(:"with_#{singular_name}") do |*args, **kwargs, &block|
26
+ slots[name] ||= []
27
+ slots[name] << Slot.new(component.new(*args, **kwargs), block)
28
+ nil
29
+ end
30
+
31
+ define_method(name) do
32
+ slots[name] || []
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def registered_slots
39
+ @registered_slots ||= {}
40
+ end
41
+ end
42
+
43
+ attr_accessor :content
44
+
45
+ def render(component, &block)
46
+ if block
47
+ component.content = block.call(component)
48
+ end
49
+
50
+ component.call
51
+ end
52
+
53
+ private
54
+
55
+ def slots
56
+ @slots ||= {}
57
+ end
58
+ end
59
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rux
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.2
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cameron Dutro
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-05-29 00:00:00.000000000 Z
11
+ date: 2023-11-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: parser
@@ -54,7 +54,12 @@ files:
54
54
  - bin/ruxc
55
55
  - lib/rux.rb
56
56
  - lib/rux/ast.rb
57
+ - lib/rux/ast/attr_node.rb
58
+ - lib/rux/ast/attrs_node.rb
59
+ - lib/rux/ast/fragment_node.rb
57
60
  - lib/rux/ast/list_node.rb
61
+ - lib/rux/ast/root_node.rb
62
+ - lib/rux/ast/ruby_attr_node.rb
58
63
  - lib/rux/ast/ruby_node.rb
59
64
  - lib/rux/ast/string_node.rb
60
65
  - lib/rux/ast/tag_node.rb
@@ -76,9 +81,19 @@ files:
76
81
  - lib/rux/version.rb
77
82
  - lib/rux/visitor.rb
78
83
  - rux.gemspec
84
+ - spec/parser/attributes_spec.rb
85
+ - spec/parser/fragment_spec.rb
86
+ - spec/parser/html_safety_spec.rb
87
+ - spec/parser/slot_spec.rb
88
+ - spec/parser/tag_spec.rb
79
89
  - spec/parser_spec.rb
80
90
  - spec/render_spec.rb
81
91
  - spec/spec_helper.rb
92
+ - spec/support/components/args_component.rb
93
+ - spec/support/components/data_component.rb
94
+ - spec/support/components/table_component.rb
95
+ - spec/support/components/test_component.rb
96
+ - spec/support/view_component/base.rb
82
97
  homepage: http://github.com/camertron/rux
83
98
  licenses: []
84
99
  metadata: {}