rux 1.1.2 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 74fe5ea5efe72399854ca97874db7ae0f9aa59abcc9201fc31725991881148b5
4
- data.tar.gz: 5a07947618af42f689f84784ed6a4ce095ef4693cd6e6187008cd4a5cf774b4c
3
+ metadata.gz: fba8846934ea39d67f4705ce5a0ec7a477e65eccc5fc5b63a6ff1f6a2233e9ca
4
+ data.tar.gz: a27256e117f07fe67fe286bc0730b265bccd91e98e18c6944aa968862b8c0e4b
5
5
  SHA512:
6
- metadata.gz: fe10394671a5d56275289a84013f85ae13bf84c6690febf2906ed63d54005c13aee9417db3285855a75e3fc85484b51b84daa596223cd16844ecd4fbe008d728
7
- data.tar.gz: 368880ab3405473e61114d846ec3aa54ced6490a070abd358857fcba21b373b8dcfad5eeb8a4b1100b6429a97c24e642b2c4e60bb33384baa3b9da2194d251ab
6
+ metadata.gz: 62c36cfb6e05f6379f659e89c6bc3bea79144ff6eae9de80491597686b967daabe6c84b5e55dbf24ef104b9e7267aa186c370c9fae18ba8c1fcf038a3805db20
7
+ data.tar.gz: 3c7c4a4b5f7ac00a344358df1e02f7bf5850aa76b0974f466b6759973f6b8400700cfc2050e6c9e1debff42ac8ad56eba9e5b018261bfa4e6dfd4b344dbbca70
data/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ # 1.2.0
2
+ * Improve output safety.
3
+ - HTML tags are now automatically escaped when they come from Ruby code.
4
+ * Add fragment support.
5
+ - Analogous to JSX fragments, eg. `<>foo</>`.
6
+ * Add keyword argument support in HTML attributes.
7
+ - Eg. `<div {**kwargs} bar="baz">boo</div>`.
8
+ * Add ViewComponent slot support.
9
+ - Works via pseudo components that begin with `With`, eg. `<MySlotComponent><WithItem>Item</WithItem></MySlotComponent>`.
10
+ * Allow printing `ruxc` results to STDOUT.
11
+ * Support for unquoted attributes.
12
+ * Drop explicit support for Ruby versions < 3.
13
+
1
14
  # 1.1.2
2
15
  * Don't slugify HTML attributes in the tag builder either.
3
16
 
data/Gemfile CHANGED
@@ -3,7 +3,7 @@ source 'https://rubygems.org'
3
3
  gemspec
4
4
 
5
5
  group :development, :test do
6
- gem 'pry-byebug'
6
+ gem 'debug'
7
7
  gem 'rake'
8
8
  gem 'rspec'
9
9
  end
data/README.md CHANGED
@@ -148,6 +148,94 @@ end
148
148
 
149
149
  Notice we were able to embed Ruby within rux within Ruby within rux. Within Ruby. The rux parser supports unlimited levels of nesting, although you'll probably not want to go _too_ crazy.
150
150
 
151
+ ## Slots
152
+
153
+ Rux fully supports the view_component gem's [slots feature](https://viewcomponent.org/guide/slots.html), which allows a component to expose specific points in the rendered output where the caller can provide their own content. Let's look at a table component that exposes rows and columns via slots:
154
+
155
+ ```ruby
156
+ class TableComponent < ViewComponent::Base
157
+ renders_many :rows, RowComponent
158
+
159
+ def call
160
+ <table>
161
+ {rows.each do |row|
162
+ <>{row}</>
163
+ end}
164
+ </table>
165
+ end
166
+ end
167
+
168
+ class RowComponent < ViewComponent::Base
169
+ renders_many :columns, ColumnComponent
170
+
171
+ def call
172
+ <tr>
173
+ {columns.each do |column|
174
+ <>{column}</>
175
+ end}
176
+ </tr>
177
+ end
178
+ end
179
+
180
+ class ColumnComponent < ViewComponent::Base
181
+ def call
182
+ <td>{content}</td>
183
+ end
184
+ end
185
+ ```
186
+
187
+ Notice the use of rux fragments (analogous to JSX fragments) via the `<></>` syntax. This allows emitting a slot by dropping back to ruby via rux.
188
+
189
+ The `TableComponent` might be rendered in an ERB template like so:
190
+
191
+ ```erb
192
+ <%= render(TableComponent.new) do |table| %>
193
+ <% table.with_row do |row| %>
194
+ <% row.with_column { "Row 1, Col 1" } %>
195
+ <% row.with_column { "Row 1, Col 2" } %>
196
+ <% end %>
197
+ <% table.with_row do |row| %>
198
+ <% row.with_column { "Row 2, Col 1" } %>
199
+ <% row.with_column { "Row 2, Col 2" } %>
200
+ <% end %>
201
+ <% end %>
202
+ ```
203
+
204
+ Notice the slots are "filled in" using the `#with_row` and `#with_column` methods. In rux, these methods become components:
205
+
206
+ ```ruby
207
+ <TableComponent>
208
+ <WithRow>
209
+ <WithColumn>Row 1, Col 1</WithColumn>
210
+ <WithColumn>Row 1, Col 2</WithColumn>
211
+ </WithRow>
212
+ <WithRow>
213
+ <WithColumn>Row 2, Col 1</WithColumn>
214
+ <WithColumn>Row 2, Col 2</WithColumn>
215
+ </WithRow>
216
+ </TableComponent>
217
+ ```
218
+
219
+ ## The `as:` argument
220
+
221
+ In ViewComponent, component instances are yielded to the block on `#render`, eg:
222
+
223
+ ```erb
224
+ <%= render(MyComponent.new) do |component| %>
225
+ <%# 'component' is the instance of MyComponent passed to #render above %>
226
+ <% end %>
227
+ ```
228
+
229
+ Most of the time in rux, a reference to the component instance isn't necessary (see the section on slots above). Occasionally however it can be useful to, for example, call methods on the component instance to query its state, etc. Use the `as:` argument to assign the component instance to a local variable that's available inside the tag body:
230
+
231
+ ```ruby
232
+ <TableComponent something={value} as={table}>
233
+ {if table.something
234
+ # your code here
235
+ end}
236
+ </TableComponent>
237
+ ```
238
+
151
239
  ## Keyword Arguments Only
152
240
 
153
241
  Any view component that will be rendered by rux must _only_ accept keyword arguments in its constructor. For example:
data/bin/ruxc CHANGED
@@ -14,8 +14,8 @@ class RuxCLI
14
14
  end
15
15
 
16
16
  options = {
17
- recursive: false,
18
- pretty: true
17
+ pretty: true,
18
+ stdout: false
19
19
  }
20
20
 
21
21
  if argv.first != '-h' && argv.first != '--help'
@@ -33,6 +33,14 @@ class RuxCLI
33
33
  end
34
34
  end
35
35
 
36
+ oneline(<<~DESC).tap do |desc|
37
+ Print results to STDOUT instead of writing files to disk.
38
+ DESC
39
+ opts.on('-o', '--stdout', desc) do |stdout|
40
+ options[:stdout] = stdout
41
+ end
42
+ end
43
+
36
44
  opts.on('-h', '--help', 'Prints this help info') do
37
45
  puts opts
38
46
  exit
@@ -79,6 +87,10 @@ class RuxCLI
79
87
  @options[:pretty]
80
88
  end
81
89
 
90
+ def write_to_stdout?
91
+ @options[:stdout]
92
+ end
93
+
82
94
  private
83
95
 
84
96
  def directory?
@@ -91,6 +103,11 @@ cli.validate
91
103
 
92
104
  cli.each_file do |in_file, out_file, rbi_file|
93
105
  rux_file = Rux::File.new(in_file)
94
- rux_file.write(out_file, pretty: cli.pretty?)
95
- puts "Wrote #{out_file}"
106
+
107
+ if cli.write_to_stdout?
108
+ puts rux_file.to_ruby(pretty: cli.pretty?)
109
+ else
110
+ rux_file.write(out_file, pretty: cli.pretty?)
111
+ puts "Wrote #{out_file}"
112
+ end
96
113
  end
@@ -0,0 +1,22 @@
1
+ module Rux
2
+ module AST
3
+ class AttrNode
4
+ attr_reader :name, :value, :name_pos
5
+ attr_accessor :tag_node
6
+
7
+ def initialize(name, value, name_pos)
8
+ @name = name
9
+ @value = value
10
+ @name_pos = name_pos
11
+ end
12
+
13
+ def accept(visitor)
14
+ visitor.visit_attr(self)
15
+ end
16
+
17
+ def ruby_code?
18
+ false
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,30 @@
1
+ module Rux
2
+ module AST
3
+ class AttrsNode
4
+ include Enumerable
5
+
6
+ attr_reader :attrs, :pos
7
+
8
+ def initialize(attrs, pos)
9
+ @attrs = attrs
10
+ @pos = pos
11
+ end
12
+
13
+ def accept(visitor)
14
+ visitor.visit_attrs(self)
15
+ end
16
+
17
+ def each(&block)
18
+ attrs.each(&block)
19
+ end
20
+
21
+ def empty?
22
+ attrs.empty?
23
+ end
24
+
25
+ def get(name)
26
+ find { |attr| attr.name == name }
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,15 @@
1
+ module Rux
2
+ module AST
3
+ class FragmentNode
4
+ attr_reader :children
5
+
6
+ def initialize
7
+ @children = []
8
+ end
9
+
10
+ def accept(visitor)
11
+ visitor.visit_fragment(self)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,19 @@
1
+ module Rux
2
+ module AST
3
+ class RootNode
4
+ attr_reader :list
5
+
6
+ def initialize(list)
7
+ @list = list
8
+ end
9
+
10
+ def accept(visitor)
11
+ visitor.visit_root(self)
12
+ end
13
+
14
+ def children
15
+ @children ||= [list]
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,28 @@
1
+ module Rux
2
+ module AST
3
+ class RubyAttrNode
4
+ attr_reader :ruby_node
5
+ attr_accessor :tag_node
6
+
7
+ def initialize(ruby_node)
8
+ @ruby_node = ruby_node
9
+ end
10
+
11
+ def code
12
+ ruby_node.code
13
+ end
14
+
15
+ def accept(visitor)
16
+ visitor.visit_attr(self)
17
+ end
18
+
19
+ def ruby_code?
20
+ true
21
+ end
22
+
23
+ def name
24
+ nil
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,10 +1,12 @@
1
1
  module Rux
2
2
  module AST
3
3
  class StringNode
4
- attr_reader :str
4
+ attr_reader :str, :quote_type, :pos
5
5
 
6
- def initialize(str)
6
+ def initialize(str, quote_type, pos)
7
7
  @str = str
8
+ @quote_type = quote_type
9
+ @pos = pos
8
10
  end
9
11
 
10
12
  def accept(visitor)
@@ -3,9 +3,10 @@ module Rux
3
3
  class TagNode
4
4
  attr_reader :name, :attrs, :children
5
5
 
6
- def initialize(name, attrs)
6
+ def initialize(name, attrs, pos)
7
7
  @name = name
8
8
  @attrs = attrs
9
+ @pos = pos
9
10
  @children = []
10
11
  end
11
12
 
@@ -16,6 +17,14 @@ module Rux
16
17
  def component?
17
18
  name.start_with?(/[A-Z]/)
18
19
  end
20
+
21
+ def slot_component?
22
+ name.start_with?("With")
23
+ end
24
+
25
+ def slot_method
26
+ @slot_method ||= name.gsub(/(?<!^)([A-Z])/) { |x| "_#{x}" }.downcase
27
+ end
19
28
  end
20
29
  end
21
30
  end
@@ -5,8 +5,9 @@ module Rux
5
5
  class TextNode
6
6
  attr_reader :text
7
7
 
8
- def initialize(text)
8
+ def initialize(text, pos)
9
9
  @text = text
10
+ @pos = pos
10
11
  end
11
12
 
12
13
  def accept(visitor)
data/lib/rux/ast.rb CHANGED
@@ -1,9 +1,14 @@
1
1
  module Rux
2
2
  module AST
3
- autoload :ListNode, 'rux/ast/list_node'
4
- autoload :RubyNode, 'rux/ast/ruby_node'
5
- autoload :StringNode, 'rux/ast/string_node'
6
- autoload :TagNode, 'rux/ast/tag_node'
7
- autoload :TextNode, 'rux/ast/text_node'
3
+ autoload :AttrNode, 'rux/ast/attr_node'
4
+ autoload :AttrsNode, 'rux/ast/attrs_node'
5
+ autoload :FragmentNode, 'rux/ast/fragment_node'
6
+ autoload :ListNode, 'rux/ast/list_node'
7
+ autoload :RootNode, 'rux/ast/root_node'
8
+ autoload :RubyAttrNode, 'rux/ast/ruby_attr_node'
9
+ autoload :RubyNode, 'rux/ast/ruby_node'
10
+ autoload :StringNode, 'rux/ast/string_node'
11
+ autoload :TagNode, 'rux/ast/tag_node'
12
+ autoload :TextNode, 'rux/ast/text_node'
8
13
  end
9
14
  end
data/lib/rux/buffer.rb CHANGED
@@ -1,15 +1,33 @@
1
1
  module Rux
2
+ class SafeString < String
3
+ def html_safe?
4
+ true
5
+ end
6
+ end
7
+
2
8
  class Buffer
3
9
  def initialize(init_str = '')
4
10
  @string = init_str.dup
5
11
  end
6
12
 
7
- def <<(*obj)
8
- @string << obj.join
13
+ def append(obj)
14
+ Array(obj).each do |o|
15
+ @string << if o.respond_to?(:html_safe?) && o.html_safe?
16
+ o.to_s
17
+ else
18
+ CGI.escapeHTML(o.to_s)
19
+ end
20
+ end
21
+ end
22
+
23
+ def safe_append(obj)
24
+ Array(obj).each { |o| @string << o.to_s }
9
25
  end
10
26
 
11
27
  def to_s
12
- @string
28
+ SafeString.new(@string)
13
29
  end
30
+
31
+ alias html_safe to_s
14
32
  end
15
33
  end
@@ -1,19 +1,28 @@
1
1
  module Rux
2
2
  class DefaultTagBuilder
3
- def call(tag_name, attributes = {})
4
- attr_str = attributes.empty? ? '' : " #{serialize_attrs(attributes)}"
5
- "<#{tag_name}#{attr_str}>" <<
6
- (block_given? ? Array(yield) : []).join <<
7
- "</#{tag_name}>"
3
+ def call(tag_name, attributes = {}, &block)
4
+ SafeString.new(build(tag_name, attributes, &block))
8
5
  end
9
6
 
10
7
  private
11
8
 
9
+ def build(tag_name, attributes = {})
10
+ attr_str = attributes.empty? ? '' : " #{serialize_attrs(attributes)}"
11
+
12
+ "<#{tag_name}#{attr_str}>".tap do |result|
13
+ if block_given?
14
+ Array(yield).each { |body| result << body }
15
+ end
16
+
17
+ result << "</#{tag_name}>"
18
+ end
19
+ end
20
+
12
21
  def serialize_attrs(attributes)
13
22
  ''.tap do |result|
14
23
  attributes.each_pair.with_index do |(k, v), idx|
15
24
  result << ' ' unless idx == 0
16
- result << "#{k}=\"#{CGI.escape_html(v.to_s)}\""
25
+ result << "#{k}=\"#{v.to_s.gsub('"', "&quot;")}\""
17
26
  end
18
27
  end
19
28
  end
@@ -2,8 +2,18 @@ require 'cgi'
2
2
 
3
3
  module Rux
4
4
  class DefaultVisitor < Visitor
5
+ def initialize
6
+ @render_stack = []
7
+ end
8
+
9
+ def visit_root(node)
10
+ visit_list(node.list)
11
+ end
12
+
5
13
  def visit_list(node)
6
- node.children.map { |child| visit(child) }.join
14
+ ''.tap do |result|
15
+ node.children.each { |child| result << visit(child) }
16
+ end
7
17
  end
8
18
 
9
19
  def visit_ruby(node)
@@ -11,57 +21,121 @@ module Rux
11
21
  end
12
22
 
13
23
  def visit_string(node)
14
- node.str
24
+ case node.quote_type
25
+ when :single
26
+ "'#{node.str}'"
27
+ else
28
+ "\"#{node.str}\""
29
+ end
15
30
  end
16
31
 
17
32
  def visit_tag(node)
18
33
  ''.tap do |result|
19
- block_arg = if (as = node.attrs['as'])
34
+ block_arg = if (as = node.attrs.get('as'))
20
35
  visit(as)
21
36
  end
22
37
 
23
- at = node.attrs.each_with_object([]) do |(k, v), ret|
24
- next if k == 'as'
25
- ret << Utils.attr_to_hash_elem(k, visit(v), slugify: node.component?)
26
- end
38
+ block_arg ||= "rux_block_arg#{@render_stack.size}"
39
+
40
+ if node.slot_component?
41
+ result << "(#{parent_render[:block_arg]}.#{node.slot_method}"
27
42
 
28
- if node.component?
43
+ unless node.attrs.empty?
44
+ result << "(#{visit(node.attrs)})"
45
+ end
46
+ elsif node.component?
29
47
  result << "render(#{node.name}.new"
30
48
 
31
49
  unless node.attrs.empty?
32
- result << "(#{at.join(', ')})"
50
+ result << "(#{visit(node.attrs)})"
33
51
  end
52
+
53
+ result << ')'
34
54
  else
35
55
  result << "Rux.tag('#{node.name}'"
36
56
 
37
57
  unless node.attrs.empty?
38
- result << ", { #{at.join(', ')} }"
58
+ result << ", { #{visit(node.attrs)} }"
39
59
  end
60
+
61
+ result << ')'
40
62
  end
41
63
 
42
- result << ')'
64
+ @render_stack.push({
65
+ component_name: node.name,
66
+ block_arg: block_arg
67
+ })
43
68
 
44
- if node.children.size > 1
69
+ if node.children.size > 0
45
70
  result << " { "
46
- result << "|#{block_arg}| " if block_arg
71
+ result << "|#{block_arg}| " if block_arg && node.component?
47
72
  result << "Rux.create_buffer.tap { |_rux_buf_| "
48
73
 
49
74
  node.children.each do |child|
50
- result << "_rux_buf_ << #{visit(child).strip};"
75
+ result << append_statement_for(child)
51
76
  end
52
77
 
53
78
  result << " }.to_s }"
54
- elsif node.children.size == 1
55
- result << ' { '
56
- result << "|#{block_arg}| " if block_arg
57
- result << visit(node.children.first).strip
58
- result << ' }'
59
79
  end
80
+
81
+ # don't pass instances of ViewComponent::Slot to _rux_buf_#<< by wrapping
82
+ # the slot setter return value in (retval; nil)
83
+ if node.slot_component?
84
+ result << "; nil)"
85
+ end
86
+
87
+ @render_stack.pop
88
+ end
89
+ end
90
+
91
+ def append_statement_for(node)
92
+ if node.is_a?(AST::TextNode)
93
+ "_rux_buf_.safe_append(#{visit(node).strip});"
94
+ else
95
+ "_rux_buf_.append(#{visit(node).strip});"
96
+ end
97
+ end
98
+
99
+ def visit_attrs(node)
100
+ visited_attrs = node.attrs.each_with_object([]) do |attr, memo|
101
+ memo << visit(attr) unless attr.name == "as"
102
+ end
103
+
104
+ visited_attrs.join(", ")
105
+ end
106
+
107
+ def visit_attr(node)
108
+ if node.ruby_code?
109
+ node.code
110
+ else
111
+ Utils.attr_to_hash_elem(
112
+ node.name,
113
+ visit(node.value),
114
+ slugify: node.tag_node.component?
115
+ )
116
+ end
117
+ end
118
+
119
+ def visit_fragment(node)
120
+ ''.tap do |result|
121
+ result << "Rux.create_buffer.tap { |_rux_buf_| "
122
+
123
+ node.children.each do |child|
124
+ result << append_statement_for(child)
125
+ end
126
+
127
+ result << " }.to_s;"
60
128
  end
61
129
  end
62
130
 
63
131
  def visit_text(node)
64
132
  "\"#{CGI.escape_html(node.text)}\""
65
133
  end
134
+
135
+ private
136
+
137
+ def parent_render
138
+ @render_stack.last
139
+ end
66
140
  end
67
141
  end
@@ -1,11 +1,11 @@
1
1
  ,[<],[a-zA-Z0-9_-_---:-:],[>],[/],(space),[=],"[""]",['],"[^""]",[^'],[{],[}],(default)
2
2
  start,tag_open_test,,,,,,,,,,literal_ruby_code_start,,literal_body
3
- tag_open_test,,tag_open_start[0],,tag_close_start,,,,,,,,,
3
+ tag_open_test,,tag_open_start[0],fragment_open,tag_close_start,,,,,,,,,
4
4
  tag_open_start*,,tag_open_body,,,,,,,,,,,
5
5
  tag_open_body,,tag_open_body,tag_open[0],tag_self_closing[0],tag_open[0],,,,,,,,
6
6
  tag_open*,,,tag_open_end,,attribute_spaces_body,,,,,,,,
7
7
  tag_open_end*,,,,,,,,,,,,,
8
- tag_close_start*,,tag_close_body,,,,,,,,,,,
8
+ tag_close_start*,,tag_close_body,fragment_close,,,,,,,,,,
9
9
  tag_close_body,,tag_close_body,tag_close[0],,tag_close[0],,,,,,,,
10
10
  tag_close*,,,tag_close_end,tag_self_closing[0],tag_close_spaces_body,,,,,,,,
11
11
  tag_close_spaces_body,,,tag_close_spaces[0],,tag_close_spaces_body,,,,,,,,
@@ -14,23 +14,34 @@ tag_close_end*,,,,,,,,,,,,,
14
14
  tag_self_closing*,,,,tag_self_closing_start,,,,,,,,,
15
15
  tag_self_closing_start,,,tag_self_closing_end,,,,,,,,,,
16
16
  tag_self_closing_end*,,,,,,,,,,,,,
17
+ fragment_open*,,,,,,,,,,,,,
18
+ fragment_close*,,,,,,,,,,,,,
17
19
  ,,,,,,,,,,,,,
18
- attribute_spaces_body,,attribute_spaces[0],attribute_spaces[0],attribute_spaces[0],attribute_spaces_body,,,,,,,,
19
- attribute_spaces*,,attribute_name_body,tag_close_start,tag_self_closing_start,,,,,,,,,
20
+ attribute_spaces_body,,attribute_spaces[0],attribute_spaces[0],attribute_spaces[0],attribute_spaces_body,,,,,,attribute_spaces[0],,
21
+ attribute_spaces*,,attribute_name_body,tag_close_start,tag_self_closing_start,,,,,,,attribute_ruby_code_start,,
20
22
  attribute_name_body,,attribute_name_body,attribute_name[0],attribute_name[0],attribute_name[0],attribute_name[0],,,,,,,
21
23
  attribute_name*,,,tag_open_end,tag_self_closing_start,attribute_equals_spaces_body,attribute_equals,,,,,,,
22
24
  attribute_equals_spaces_body,,,attribute_equals_spaces[0],,attribute_equals_spaces_body,attribute_equals_spaces[0],,,,,,,attribute_equals_spaces[0]
23
25
  attribute_equals_spaces*,,,tag_open_end,tag_self_closing_start,,attribute_equals,,,,,,,attribute_name_body
24
- attribute_equals*,,attribute_uq_body,,,attribute_value_spaces_body[0],,attribute_dq_body,attribute_sq_body,,,attribute_value_ruby_code_start,,
25
- attribute_value_spaces_body,,attribute_value_spaces[0],,,attribute_value_spaces_body,,attribute_value_spaces[0],attribute_value_spaces[0],,,attribute_value_spaces[0],,
26
- attribute_value_spaces*,,attribute_uq_body,,,,,attribute_dq_body,attribute_sq_body,,,attribute_value_ruby_code_start,,
27
- attribute_dq_body,,,,,,,attribute_value,,attribute_dq_body,,,,
28
- attribute_sq_body,,,,,,,,attribute_value,,attribute_sq_body,,,
29
- attribute_uq_body,,attribute_uq_body,,,attribute_value,,,,,,,,
26
+ attribute_equals*,,attribute_value_uq_body,,,attribute_value_spaces_body[0],,attribute_value_dq_start,attribute_value_sq_start,,,attribute_value_ruby_code_start,,
27
+ attribute_value_spaces_body,,attribute_value_spaces[0],attribute_value_spaces[0],attribute_value_spaces[0],attribute_value_spaces_body,,attribute_value_spaces[0],attribute_value_spaces[0],,,attribute_value_spaces[0],,
28
+ attribute_value_spaces*,,attribute_value_uq_body,tag_open_end,tag_self_closing_start,,,attribute_value_dq_start,attribute_value_sq_start,,,attribute_value_ruby_code_start,,
29
+ attribute_value_dq_start*,,,,,,,,,attribute_value_dq_body,,,,
30
+ attribute_value_sq_start*,,,,,,,,,,attribute_value_sq_body,,,
31
+ attribute_value_dq_body,,,,,,,attribute_dq_value[0],,attribute_value_dq_body,,,,
32
+ attribute_value_sq_body,,,,,,,,attribute_sq_value[0],,attribute_value_sq_body,,,
33
+ attribute_value_uq_body,,attribute_value_uq_body,attribute_uq_value[0],attrIbute_uq_value[0],attribute_uq_value[0],,,,,,,,
34
+ attribute_value_dq_end*,,attribute_name,tag_open_end,tag_self_closing_start,attribute_spaces_body,,,,,,,,
35
+ attribute_value_sq_end*,,attribute_name,tag_open_end,tag_self_closing_start,attribute_spaces_body,,,,,,,,
30
36
  attribute_value_ruby_code_start*,,,,,,,,,,,,,attribute_value_ruby_code
31
37
  attribute_value_ruby_code*,,,,,,,,,,,,attribute_value_ruby_code_end,
32
38
  attribute_value_ruby_code_end*,,attribute_name,tag_open_end,tag_self_closing_start,attribute_spaces_body,,,,,,,,
33
- attribute_value*,,attribute_name,tag_open_end,tag_self_closing_start,attribute_spaces_body,,attribute_value_ending,attribute_value_ending,,,,,
39
+ attribute_dq_value*,,,,,,,attribute_value_dq_end,,,,,,
40
+ attribute_sq_value*,,,,,,,,attribute_value_sq_end,,,,,
41
+ attribute_uq_value*,,attribute_name,tag_open_end,tag_self_closing_start,attribute_spaces_body,,,,,,,,
42
+ attribute_ruby_code_start*,,,,,,,,,,,,,attribute_ruby_code[0]
43
+ attribute_ruby_code*,,,,,,,,,,,,attribute_ruby_code_end,
44
+ attribute_ruby_code_end*,,attribute_name,tag_open_end,tag_self_closing_start,attribute_spaces_body,,,,,,,,
34
45
  ,,,,,,,,,,,,,
35
46
  literal_body,literal[0],,,,,,,,,,literal[0],,literal_body
36
47
  literal*,,,,,,,,,,,literal_ruby_code_start,,