rux 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,15 @@
1
+ module Rux
2
+ module Utils
3
+ def attr_to_hash_elem(key, value)
4
+ key = key.gsub('-', '_')
5
+
6
+ if key =~ /\A[\w\d]+\z/
7
+ "#{key}: #{value}"
8
+ else
9
+ ":\"#{key}\" => #{value}"
10
+ end
11
+ end
12
+ end
13
+
14
+ Utils.extend(Utils)
15
+ end
@@ -0,0 +1,3 @@
1
+ module Rux
2
+ VERSION = '1.0.0'
3
+ end
@@ -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
@@ -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
+ "&quot;foo&quot;"
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