rux 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.
- checksums.yaml +7 -0
- data/Gemfile +9 -0
- data/LICENSE +21 -0
- data/README.md +310 -0
- data/Rakefile +14 -0
- data/bin/ruxc +96 -0
- data/lib/rux.rb +73 -0
- data/lib/rux/ast.rb +9 -0
- data/lib/rux/ast/list_node.rb +15 -0
- data/lib/rux/ast/ruby_node.rb +15 -0
- data/lib/rux/ast/string_node.rb +15 -0
- data/lib/rux/ast/tag_node.rb +17 -0
- data/lib/rux/ast/text_node.rb +17 -0
- data/lib/rux/buffer.rb +15 -0
- data/lib/rux/default_tag_builder.rb +20 -0
- data/lib/rux/default_visitor.rb +67 -0
- data/lib/rux/file.rb +27 -0
- data/lib/rux/lex.rb +9 -0
- data/lib/rux/lex/patterns.rb +41 -0
- data/lib/rux/lex/state.rb +33 -0
- data/lib/rux/lex/states.csv +39 -0
- data/lib/rux/lex/transition.rb +22 -0
- data/lib/rux/lexer.rb +64 -0
- data/lib/rux/parser.rb +244 -0
- data/lib/rux/ruby_lexer.rb +143 -0
- data/lib/rux/rux_lexer.rb +157 -0
- data/lib/rux/utils.rb +15 -0
- data/lib/rux/version.rb +3 -0
- data/lib/rux/visitor.rb +33 -0
- data/rux.gemspec +20 -0
- data/spec/parser_spec.rb +229 -0
- data/spec/spec_helper.rb +6 -0
- metadata +102 -0
@@ -0,0 +1,157 @@
|
|
1
|
+
require 'csv'
|
2
|
+
|
3
|
+
module Rux
|
4
|
+
class RuxLexer
|
5
|
+
class << self
|
6
|
+
# See: https://docs.google.com/spreadsheets/d/11ikKuySIKoaj-kFIfhlzebUwH31cRt_1flGjWfk7RMg
|
7
|
+
def state_table
|
8
|
+
@state_table ||= {}.tap do |table|
|
9
|
+
state_table_data = CSV.read(state_table_path)
|
10
|
+
input_patterns = state_table_data[0][1..-1]
|
11
|
+
|
12
|
+
inputs = input_patterns.map do |pattern|
|
13
|
+
parse_pattern(pattern)
|
14
|
+
end
|
15
|
+
|
16
|
+
state_table_data[1..-1].each do |row|
|
17
|
+
next unless row[0] # allows blank lines in csv
|
18
|
+
|
19
|
+
state = Lex::State.parse(row[0], row[1..-1], inputs)
|
20
|
+
table[state.name] = state
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def state_table_path
|
26
|
+
@state_table_path ||=
|
27
|
+
::File.expand_path(::File.join('.', 'lex', 'states.csv'), __dir__)
|
28
|
+
end
|
29
|
+
|
30
|
+
def parse_pattern(pattern)
|
31
|
+
if pattern == "(space)"
|
32
|
+
Lex::CharsetPattern.new([' ', "\r", "\n"])
|
33
|
+
elsif pattern == "(default)"
|
34
|
+
Lex::DefaultPattern.new
|
35
|
+
elsif pattern.start_with?('[^')
|
36
|
+
Lex::NegatedCharsetPattern.parse(pattern[2..-2])
|
37
|
+
else
|
38
|
+
Lex::CharsetPattern.parse(pattern[1..-2])
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
def initialize(source_buffer, init_pos)
|
45
|
+
@p = init_pos
|
46
|
+
@source_buffer = source_buffer
|
47
|
+
@source_pts = @source_buffer.source.unpack('U*')
|
48
|
+
@generator = to_enum(:each_token)
|
49
|
+
end
|
50
|
+
|
51
|
+
def advance
|
52
|
+
@generator.next
|
53
|
+
end
|
54
|
+
|
55
|
+
def reset_to(pos)
|
56
|
+
@p = pos
|
57
|
+
@eof = false
|
58
|
+
end
|
59
|
+
|
60
|
+
def next_lexer(pos)
|
61
|
+
RubyLexer.new(@source_buffer, pos)
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def each_token
|
67
|
+
tag_stack = []
|
68
|
+
@eof = false
|
69
|
+
|
70
|
+
each_rux_token do |token|
|
71
|
+
state, (text, pos) = token
|
72
|
+
|
73
|
+
if ruby_code?(state)
|
74
|
+
@eof = true
|
75
|
+
|
76
|
+
# @eof is set to false by reset_to above, which is called after
|
77
|
+
# popping the previous lexer off the lexer stack (see lexer.rb)
|
78
|
+
while @eof
|
79
|
+
yield [nil, ['$eof', pos]]
|
80
|
+
end
|
81
|
+
|
82
|
+
next
|
83
|
+
end
|
84
|
+
|
85
|
+
yield token
|
86
|
+
|
87
|
+
case state
|
88
|
+
when :tRUX_TAG_OPEN, :tRUX_TAG_SELF_CLOSING
|
89
|
+
tag_stack.push(text)
|
90
|
+
when :tRUX_TAG_CLOSE
|
91
|
+
tag_stack.pop
|
92
|
+
when :tRUX_TAG_CLOSE_END
|
93
|
+
break if tag_stack.empty?
|
94
|
+
when :tRUX_TAG_SELF_CLOSING_END
|
95
|
+
tag_stack.pop
|
96
|
+
break if tag_stack.empty?
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def each_rux_token(&block)
|
102
|
+
cur_state = :tRUX_START
|
103
|
+
last_idx = @p
|
104
|
+
|
105
|
+
loop do
|
106
|
+
check_eof
|
107
|
+
|
108
|
+
chr = @source_pts[@p].chr
|
109
|
+
cur_trans = self.class.state_table[cur_state][chr]
|
110
|
+
|
111
|
+
unless cur_trans
|
112
|
+
raise Rux::Lexer::TransitionError,
|
113
|
+
"no transition found from #{cur_state} at position #{@p} while "\
|
114
|
+
'lexing rux code'
|
115
|
+
end
|
116
|
+
|
117
|
+
cur_state = cur_trans.to_state
|
118
|
+
@p += cur_trans.advance_count
|
119
|
+
|
120
|
+
if self.class.state_table[cur_state].terminal?
|
121
|
+
token_text = @source_buffer.source[last_idx...@p]
|
122
|
+
yield [cur_state, [token_text, make_range(last_idx, @p)]]
|
123
|
+
|
124
|
+
check_eof
|
125
|
+
|
126
|
+
next_chr = @source_pts[@p].chr
|
127
|
+
|
128
|
+
# no transition from the current state means we need to reset to the
|
129
|
+
# start state
|
130
|
+
unless self.class.state_table[cur_state][next_chr]
|
131
|
+
cur_state = :tRUX_START
|
132
|
+
end
|
133
|
+
|
134
|
+
last_idx = @p
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def check_eof
|
140
|
+
if @p >= @source_pts.length
|
141
|
+
raise Rux::Lexer::EOFError, 'unexpected end of rux input'
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def make_range(start, stop)
|
146
|
+
::Parser::Source::Range.new(@source_buffer, start, stop)
|
147
|
+
end
|
148
|
+
|
149
|
+
# Ruby code can only exist in two places: attribute values and inside tag
|
150
|
+
# bodies. Eventually I'd like to also allow passing a Ruby hash to
|
151
|
+
# dynamically specify attributes, but we're not there yet.
|
152
|
+
def ruby_code?(state)
|
153
|
+
state == :tRUX_ATTRIBUTE_VALUE_RUBY_CODE ||
|
154
|
+
state == :tRUX_LITERAL_RUBY_CODE
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
data/lib/rux/utils.rb
ADDED
data/lib/rux/version.rb
ADDED
data/lib/rux/visitor.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
module Rux
|
2
|
+
class Visitor
|
3
|
+
def visit(node)
|
4
|
+
node.accept(self)
|
5
|
+
end
|
6
|
+
|
7
|
+
def visit_list(node)
|
8
|
+
visit_children(node)
|
9
|
+
end
|
10
|
+
|
11
|
+
def visit_ruby(node)
|
12
|
+
visit_children(node)
|
13
|
+
end
|
14
|
+
|
15
|
+
def visit_string(node)
|
16
|
+
visit_children(node)
|
17
|
+
end
|
18
|
+
|
19
|
+
def visit_tag(node)
|
20
|
+
visit_children(node)
|
21
|
+
end
|
22
|
+
|
23
|
+
def visit_text(node)
|
24
|
+
visit_children(node)
|
25
|
+
end
|
26
|
+
|
27
|
+
def visit_children(node)
|
28
|
+
if node.respond_to?(:children)
|
29
|
+
node.children.each { |child| visit(child) }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
data/rux.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
$:.unshift File.join(File.dirname(__FILE__), 'lib')
|
2
|
+
require 'rux/version'
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = 'rux'
|
6
|
+
s.version = ::Rux::VERSION
|
7
|
+
s.authors = ['Cameron Dutro']
|
8
|
+
s.email = ['camertron@gmail.com']
|
9
|
+
s.homepage = 'http://github.com/camertron/rux'
|
10
|
+
s.description = s.summary = 'A jsx-inspired way to write view components.'
|
11
|
+
s.platform = Gem::Platform::RUBY
|
12
|
+
|
13
|
+
s.add_dependency 'parser', '~> 3.0'
|
14
|
+
s.add_dependency 'unparser', '~> 0.5'
|
15
|
+
|
16
|
+
s.require_path = 'lib'
|
17
|
+
s.executables << 'ruxc'
|
18
|
+
|
19
|
+
s.files = Dir['{lib,spec}/**/*', 'Gemfile', 'LICENSE', 'CHANGELOG.md', 'README.md', 'Rakefile', 'rux.gemspec']
|
20
|
+
end
|
data/spec/parser_spec.rb
ADDED
@@ -0,0 +1,229 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'parser'
|
3
|
+
require 'unparser'
|
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
|
+
Rux.create_buffer.tap { |_rux_buf_,|
|
140
|
+
_rux_buf_ << " "
|
141
|
+
_rux_buf_ << 5.times.map {
|
142
|
+
render(Inner.new) {
|
143
|
+
Rux.create_buffer.tap { |_rux_buf_,|
|
144
|
+
_rux_buf_ << "What a "
|
145
|
+
_rux_buf_ << @thing
|
146
|
+
}.to_s
|
147
|
+
}
|
148
|
+
}
|
149
|
+
_rux_buf_ << " "
|
150
|
+
}.to_s
|
151
|
+
}
|
152
|
+
RUBY
|
153
|
+
end
|
154
|
+
|
155
|
+
it 'handles regular HTML tags' do
|
156
|
+
expect(compile('<div>foo</div>')).to eq(<<~RUBY.strip)
|
157
|
+
Rux.tag("div") {
|
158
|
+
"foo"
|
159
|
+
}
|
160
|
+
RUBY
|
161
|
+
end
|
162
|
+
|
163
|
+
it 'handles regular HTML tags inside ruby code' do
|
164
|
+
rux_code = <<~RUX
|
165
|
+
<Outer>
|
166
|
+
{5.times.map do
|
167
|
+
<div>So {@cool}</div>
|
168
|
+
end}
|
169
|
+
</Outer>
|
170
|
+
RUX
|
171
|
+
|
172
|
+
expect(compile(rux_code)).to eq(<<~RUBY.strip)
|
173
|
+
render(Outer.new) {
|
174
|
+
Rux.create_buffer.tap { |_rux_buf_,|
|
175
|
+
_rux_buf_ << " "
|
176
|
+
_rux_buf_ << 5.times.map {
|
177
|
+
Rux.tag("div") {
|
178
|
+
Rux.create_buffer.tap { |_rux_buf_,|
|
179
|
+
_rux_buf_ << "So "
|
180
|
+
_rux_buf_ << @cool
|
181
|
+
}.to_s
|
182
|
+
}
|
183
|
+
}
|
184
|
+
_rux_buf_ << " "
|
185
|
+
}.to_s
|
186
|
+
}
|
187
|
+
RUBY
|
188
|
+
end
|
189
|
+
|
190
|
+
it 'escapes HTML entities in strings' do
|
191
|
+
expect(compile('<Hello>"foo"</Hello>')).to eq(<<~RUBY.strip)
|
192
|
+
render(Hello.new) {
|
193
|
+
""foo""
|
194
|
+
}
|
195
|
+
RUBY
|
196
|
+
end
|
197
|
+
|
198
|
+
it 'raises an error on premature end of input' do
|
199
|
+
expect { compile('<Hello') }.to raise_error(Rux::Lexer::EOFError)
|
200
|
+
end
|
201
|
+
|
202
|
+
it 'raises an error when no state transition can be found' do
|
203
|
+
expect { compile('<Hello <foo>') }.to(
|
204
|
+
raise_error(Rux::Lexer::TransitionError,
|
205
|
+
'no transition found from tRUX_ATTRIBUTE_SPACES_BODY at position 7 '\
|
206
|
+
'while lexing rux code')
|
207
|
+
)
|
208
|
+
end
|
209
|
+
|
210
|
+
it 'raises an error on tag mismatch' do
|
211
|
+
expect { compile('<Hello></Goodbye>') }.to(
|
212
|
+
raise_error(Rux::Parser::TagMismatchError,
|
213
|
+
"closing tag 'Goodbye' on line 1 did not match opening tag 'Hello' "\
|
214
|
+
'on line 1')
|
215
|
+
)
|
216
|
+
end
|
217
|
+
|
218
|
+
it 'emits handles spaces between adjacent ruby code snippets' do
|
219
|
+
expect(compile("<Hello>{first} {second}</Hello>")).to eq(<<~RUBY.strip)
|
220
|
+
render(Hello.new) {
|
221
|
+
Rux.create_buffer.tap { |_rux_buf_,|
|
222
|
+
_rux_buf_ << first
|
223
|
+
_rux_buf_ << " "
|
224
|
+
_rux_buf_ << second
|
225
|
+
}.to_s
|
226
|
+
}
|
227
|
+
RUBY
|
228
|
+
end
|
229
|
+
end
|