rux 1.1.2 → 1.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -0
- data/Gemfile +8 -1
- data/README.md +88 -0
- data/bin/ruxc +47 -5
- data/bin/ruxlex +13 -0
- data/lib/rux/ast/attr_node.rb +22 -0
- data/lib/rux/ast/attrs_node.rb +30 -0
- data/lib/rux/ast/fragment_node.rb +15 -0
- data/lib/rux/ast/root_node.rb +19 -0
- data/lib/rux/ast/ruby_attr_node.rb +28 -0
- data/lib/rux/ast/string_node.rb +4 -2
- data/lib/rux/ast/tag_node.rb +10 -1
- data/lib/rux/ast/text_node.rb +2 -1
- data/lib/rux/ast.rb +10 -5
- data/lib/rux/buffer.rb +21 -3
- data/lib/rux/default_tag_builder.rb +15 -6
- data/lib/rux/default_visitor.rb +100 -23
- data/lib/rux/lex/states.csv +51 -39
- data/lib/rux/lexer.rb +13 -0
- data/lib/rux/parser.rb +112 -28
- data/lib/rux/ruby_lexer.rb +32 -8
- data/lib/rux/rux_lexer.rb +16 -4
- data/lib/rux/version.rb +1 -1
- data/lib/rux/visitor.rb +16 -0
- data/lib/rux.rb +2 -3
- data/rux.gemspec +3 -1
- data/spec/parser/attributes_spec.rb +121 -0
- data/spec/parser/fragment_spec.rb +64 -0
- data/spec/parser/html_safety_spec.rb +13 -0
- data/spec/parser/slot_spec.rb +40 -0
- data/spec/parser/tag_spec.rb +174 -0
- data/spec/parser_spec.rb +56 -232
- data/spec/render_spec.rb +70 -7
- data/spec/spec_helper.rb +19 -32
- data/spec/support/components/args_component.rb +14 -0
- data/spec/support/components/data_component.rb +11 -0
- data/spec/support/components/table_component.rb +23 -0
- data/spec/support/components/test_component.rb +7 -0
- data/spec/support/view_component/base.rb +59 -0
- metadata +36 -8
|
@@ -0,0 +1,121 @@
|
|
|
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
|
+
|
|
98
|
+
it 'yields the component instance to the block using the as: argument for the variable name' do
|
|
99
|
+
code = <<~RUX
|
|
100
|
+
<Hello as={hello}>
|
|
101
|
+
{hello.foo}
|
|
102
|
+
</Hello>
|
|
103
|
+
RUX
|
|
104
|
+
expect(compile(code)).to eq(<<~RUBY.strip)
|
|
105
|
+
render(Hello.new) { |hello|
|
|
106
|
+
Rux.create_buffer.tap { |_rux_buf_|
|
|
107
|
+
_rux_buf_.append(hello.foo)
|
|
108
|
+
}.to_s
|
|
109
|
+
}
|
|
110
|
+
RUBY
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
it 'allows attributes to start with @' do
|
|
114
|
+
code = <<~RUX
|
|
115
|
+
<div @click="alert('foo')" />
|
|
116
|
+
RUX
|
|
117
|
+
expect(compile(code)).to eq(<<~RUBY.strip)
|
|
118
|
+
Rux.tag("div", { :@click => "alert('foo')" })
|
|
119
|
+
RUBY
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
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
|
+
|
|
48
|
+
it 'allows fragments nested inside other tags' do
|
|
49
|
+
code = <<~RUX
|
|
50
|
+
<div>
|
|
51
|
+
<>{"foo"}</>
|
|
52
|
+
</div>
|
|
53
|
+
RUX
|
|
54
|
+
expect(compile(code)).to eq(<<~RUBY.strip)
|
|
55
|
+
Rux.tag("div") {
|
|
56
|
+
Rux.create_buffer.tap { |_rux_buf_|
|
|
57
|
+
_rux_buf_.append(Rux.create_buffer.tap { |_rux_buf_|
|
|
58
|
+
_rux_buf_.append("foo")
|
|
59
|
+
}.to_s)
|
|
60
|
+
}.to_s
|
|
61
|
+
}
|
|
62
|
+
RUBY
|
|
63
|
+
end
|
|
64
|
+
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(""foo"")
|
|
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
|