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.
@@ -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