orb_template 0.1.3 → 0.2.2

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.
@@ -3,17 +3,27 @@
3
3
  module ORB
4
4
  module AST
5
5
  class AbstractNode
6
- attr_accessor :children, :attributes, :errors
6
+ attr_accessor :children, :attributes
7
+ attr_writer :errors
8
+
9
+ EMPTY_ARRAY = [].freeze
7
10
 
8
11
  def initialize(*_args)
9
12
  @children = []
10
- @errors = []
13
+ end
14
+
15
+ def errors
16
+ @errors || EMPTY_ARRAY
11
17
  end
12
18
 
13
19
  def add_child(node)
14
20
  @children << node
15
21
  end
16
22
 
23
+ def add_error(error)
24
+ (@errors ||= []) << error
25
+ end
26
+
17
27
  def render(_context)
18
28
  raise "Not implemented - you must implement render in your subclass!"
19
29
  end
@@ -13,14 +13,16 @@ module ORB
13
13
  def initialize(token)
14
14
  super
15
15
  @expression = token.value
16
+ @is_block = BLOCK_RE.match?(@expression)
17
+ @is_end = @expression == 'end' || @expression.strip == 'end'
16
18
  end
17
19
 
18
20
  def block?
19
- @expression =~ BLOCK_RE
21
+ @is_block
20
22
  end
21
23
 
22
24
  def end?
23
- @expression.strip == 'end'
25
+ @is_end
24
26
  end
25
27
  end
26
28
  end
@@ -11,14 +11,16 @@ module ORB
11
11
  def initialize(token)
12
12
  super
13
13
  @expression = token.value
14
+ @is_block = BLOCK_RE.match?(@expression)
15
+ @is_end = @expression == 'end' || @expression.strip == 'end'
14
16
  end
15
17
 
16
18
  def block?
17
- @expression =~ BLOCK_RE
19
+ @is_block
18
20
  end
19
21
 
20
22
  def end?
21
- @expression.strip == 'end'
23
+ @is_end
22
24
  end
23
25
 
24
26
  def render(_context)
data/lib/orb/patterns.rb CHANGED
@@ -4,7 +4,7 @@ module ORB
4
4
  module Patterns
5
5
  SPACE_CHARS = /\s/
6
6
  TAG_NAME = %r{[^\s>/=$]+}
7
- ATTRIBUTE_NAME = %r{[^\s>/=]+}
7
+ ATTRIBUTE_NAME = /[a-zA-Z_:][-a-zA-Z0-9_:.]*/
8
8
  UNQUOTED_VALUE_INVALID_CHARS = /["'=<`]/
9
9
  UNQUOTED_VALUE = %r{[^\s/>]+}
10
10
  BLOCK_NAME_CHARS = /[^\s}]+/
@@ -37,5 +37,17 @@ module ORB
37
37
  CRLF = /\r\n/
38
38
  BLANK = /[[:blank:]]/
39
39
  OTHER = /./
40
+
41
+ # Greedy multi-character patterns for bulk scanning in each tokenizer state.
42
+ # Each pattern excludes only the characters that could start a delimiter in
43
+ # that state, so the tokenizer consumes runs of "boring" text in one match
44
+ # instead of character-by-character.
45
+ INITIAL_TEXT = /[^\n\r{<]+/
46
+ EXPRESSION_TEXT = /[^\n\r{}]+/
47
+ COMMENT_TEXT = /[^\n\r-]+/
48
+ VERBATIM_TEXT = /[^\n\r<]+/
49
+ SINGLE_QUOTED_TEXT = /[^\n\r']+/
50
+ DOUBLE_QUOTED_TEXT = /[^\n\r"]+/
51
+ BLOCK_CONTENT_TEXT = /[^{}\n\r]+/
40
52
  end
41
53
  end
@@ -81,15 +81,16 @@ module ORB
81
81
  # an attribute can be a static string, a dynamic expression,
82
82
  # or a boolean attribute (an attribute without a value, e.g. disabled, checked, etc.)
83
83
  #
84
- # For boolean attributes, we return a [:dynamic, "nil"] expression, so that the
85
- # final render for the attribute will be `attribute` instead of `attribute="true"`
84
+ # Compile a single attribute into Temple core abstraction.
85
+ # Boolean attributes emit [:static, ""] directly instead of [:dynamic, "nil"]
86
+ # to avoid unnecessary Ripper analysis in Temple's StaticAnalyzer filter.
86
87
  def compile_attribute(attribute)
87
88
  if attribute.string?
88
89
  [:html, :attr, attribute.name, [:static, attribute.value]]
89
90
  elsif attribute.bool?
90
- [:html, :attr, attribute.name, [:dynamic, "nil"]]
91
+ [:html, :attr, attribute.name, [:static, ""]]
91
92
  elsif attribute.expression?
92
- [:html, :attr, attribute.name, [:dynamic, attribute.value]]
93
+ [:html, :attr, attribute.name, [:escape, true, [:dynamic, attribute.value]]]
93
94
  end
94
95
  end
95
96
 
@@ -115,9 +116,46 @@ module ORB
115
116
  end
116
117
  end
117
118
 
119
+ # Pre-build the variable name string with a single interpolation
118
120
  def prefixed_variable_name(name, prefix)
119
121
  "#{prefix}_arg_#{name.underscore}"
120
122
  end
123
+
124
+ # Combined single-pass compilation of captures and komponent args
125
+ # Returns [captures_array, args_string]
126
+ def compile_captures_and_args(attributes, prefix)
127
+ captures = []
128
+ args = {}
129
+ splats = []
130
+
131
+ attributes.each do |attribute|
132
+ if attribute.splat?
133
+ splats << attribute.value
134
+ next
135
+ end
136
+
137
+ var_name = prefixed_variable_name(attribute.name, prefix)
138
+
139
+ # Build capture
140
+ if attribute.string?
141
+ captures << [:code, "#{var_name} = \"#{attribute.value}\""]
142
+ elsif attribute.bool?
143
+ captures << [:code, "#{var_name} = true"]
144
+ elsif attribute.expression?
145
+ captures << [:code, "#{var_name} = #{attribute.value}"]
146
+ end
147
+
148
+ # Build args hash
149
+ args = args.deep_merge(dash_to_hash(attribute.name, var_name))
150
+ end
151
+
152
+ # Build the argument list
153
+ result_parts = []
154
+ result_parts << hash_to_args_list(args) unless args.empty?
155
+ result_parts += splats
156
+
157
+ [captures, result_parts.join(', ')]
158
+ end
121
159
  end
122
160
  end
123
161
  end
@@ -31,33 +31,33 @@ module ORB
31
31
  #
32
32
  # rubocop:disable Metrics/CyclomaticComplexity
33
33
  # rubocop:disable Metrics/PerceivedComplexity
34
- def transform(node, context = {})
34
+ def transform(node)
35
35
  if node.is_a?(ORB::AST::RootNode)
36
- transform_children(node, context)
36
+ transform_children(node)
37
37
  elsif node.is_a?(ORB::AST::TextNode)
38
- transform_text_node(node, context)
38
+ transform_text_node(node)
39
39
  elsif node.is_a?(ORB::AST::PrintingExpressionNode)
40
- transform_printing_expression_node(node, context)
40
+ transform_printing_expression_node(node)
41
41
  elsif node.is_a?(ORB::AST::ControlExpressionNode)
42
- transform_control_expression_node(node, context)
42
+ transform_control_expression_node(node)
43
43
  elsif node.is_a?(ORB::AST::TagNode) && node.compiler_directives?
44
- transform_directives_for_tag_node(node, context)
44
+ transform_directives_for_tag_node(node)
45
45
  elsif node.is_a?(ORB::AST::TagNode) && node.dynamic?
46
- transform_dynamic_tag_node(node, context)
46
+ transform_dynamic_tag_node(node)
47
47
  elsif node.is_a?(ORB::AST::TagNode) && node.html_tag?
48
- transform_html_tag_node(node, context)
48
+ transform_html_tag_node(node)
49
49
  elsif node.is_a?(ORB::AST::TagNode) && node.component_tag?
50
- transform_component_tag_node(node, context)
50
+ transform_component_tag_node(node)
51
51
  elsif node.is_a?(ORB::AST::TagNode) && node.component_slot_tag?
52
- transform_component_slot_tag_node(node, context)
52
+ transform_component_slot_tag_node(node)
53
53
  elsif node.is_a?(ORB::AST::BlockNode)
54
- transform_block_node(node, context)
54
+ transform_block_node(node)
55
55
  elsif node.is_a?(ORB::AST::PublicCommentNode)
56
- transform_public_comment_node(node, context)
56
+ transform_public_comment_node(node)
57
57
  elsif node.is_a?(ORB::AST::PrivateCommentNode)
58
- transform_private_comment_node(node, context)
58
+ transform_private_comment_node(node)
59
59
  elsif node.is_a?(ORB::AST::NewlineNode)
60
- transform_newline_node(node, context)
60
+ transform_newline_node(node)
61
61
  elsif node.is_a?(ORB::Error)
62
62
  runtime_error(node)
63
63
  else
@@ -68,135 +68,123 @@ module ORB
68
68
  # rubocop:enable Metrics/PerceivedComplexity
69
69
 
70
70
  # Compile the children of a node and collect the result into a Temple expression
71
- def transform_children(node, context)
72
- [:multi, *node.children.map { |child| transform(child, context) }]
71
+ def transform_children(node)
72
+ [:multi, *node.children.map { |child| transform(child) }]
73
73
  end
74
74
 
75
75
  # Compile a TextNode into a Temple expression
76
- def transform_text_node(node, _context)
76
+ def transform_text_node(node)
77
77
  [:static, node.text]
78
78
  end
79
79
 
80
80
  # Compile an PExpressionNode into a Temple expression
81
- def transform_printing_expression_node(node, context)
81
+ def transform_printing_expression_node(node)
82
82
  if node.block?
83
83
  tmp = @identity.generate(:variable)
84
84
  [:multi,
85
- # Capture the result of the code in a variable. We can't do
86
- # `[:dynamic, code]` because it's probably not a complete
87
- # expression (which is a requirement for Temple).
88
85
  [
89
86
  :block, "#{tmp} = #{node.expression}",
90
-
91
- # Capture the content of a block in a separate buffer. This means
92
- # that `yield` will not output the content to the current buffer,
93
- # but rather return the output.
94
- #
95
- # The capturing can be disabled with the option :disable_capture.
96
- # Output code in the block writes directly to the output buffer then.
97
- # Rails handles this by replacing the output buffer for helpers.
98
87
  if @options.fetch(:disable_capture, false)
99
- transform_children(node, context)
88
+ transform_children(node)
100
89
  else
101
- [:capture, @identity.generate(:variable), transform_children(node, context)]
90
+ [:capture, @identity.generate(:variable), transform_children(node)]
102
91
  end
103
92
  ],
104
- # Output the content.
105
93
  [:escape, true, [:dynamic, tmp]]]
106
94
  elsif node.children.any?
107
- [:multi, [:escape, true, [:dynamic, node.expression]], transform_children(node, context)]
95
+ [:multi, [:escape, true, [:dynamic, node.expression]], transform_children(node)]
108
96
  else
109
97
  [:escape, true, [:dynamic, node.expression]]
110
98
  end
111
99
  end
112
100
 
113
101
  # Compile an NPExpressionNode into a Temple expression
114
- def transform_control_expression_node(node, context)
102
+ def transform_control_expression_node(node)
115
103
  if node.block?
116
104
  tmp = @identity.generate(:variable)
117
105
  [:multi,
118
- [:block, "#{tmp} = #{node.expression}", transform_children(node, context)]]
106
+ [:block, "#{tmp} = #{node.expression}", transform_children(node)]]
119
107
  elsif node.children.any?
120
- [:multi, [:code, node.expression], transform_children(node, context)]
108
+ [:multi, [:code, node.expression], transform_children(node)]
121
109
  else
122
110
  [:code, node.expression]
123
111
  end
124
112
  end
125
113
 
126
114
  # Compile an HTML TagNode into a Temple expression
127
- def transform_html_tag_node(node, context)
128
- [:orb, :tag, node.tag, node.attributes, transform_children(node, context)]
115
+ def transform_html_tag_node(node)
116
+ [:orb, :tag, node.tag, node.attributes, transform_children(node)]
129
117
  end
130
118
 
131
119
  # Compile a component TagNode into a Temple expression
132
- def transform_component_tag_node(node, context)
133
- [:orb, :component, node, transform_children(node, context)]
120
+ def transform_component_tag_node(node)
121
+ [:orb, :component, node, transform_children(node)]
134
122
  end
135
123
 
136
124
  # Compile a component slot TagNode into a Temple expression
137
- def transform_component_slot_tag_node(node, context)
138
- [:orb, :slot, node, transform_children(node, context)]
125
+ def transform_component_slot_tag_node(node)
126
+ [:orb, :slot, node, transform_children(node)]
139
127
  end
140
128
 
141
129
  # Compile a block node into a Temple expression
142
- def transform_block_node(node, context)
130
+ def transform_block_node(node)
143
131
  case node.name
144
132
  when :if
145
- [:orb, :if, node.expression, transform_children(node, context)]
133
+ [:orb, :if, node.expression, transform_children(node)]
146
134
  when :for
147
- [:orb, :for, node.expression, transform_children(node, context)]
135
+ [:orb, :for, node.expression, transform_children(node)]
148
136
  else
149
137
  [:static, 'Unknown block node']
150
138
  end
151
139
  end
152
140
 
153
141
  # Compile a comment node into a Temple expression
154
- def transform_public_comment_node(node, _context)
142
+ def transform_public_comment_node(node)
155
143
  [:html, :comment, [:static, node.text]]
156
144
  end
157
145
 
158
146
  # Compile a private_comment node into a Temple expression
159
- def transform_private_comment_node(_node, _context)
147
+ def transform_private_comment_node(_node)
160
148
  [:static, ""]
161
149
  end
162
150
 
163
151
  # Compile a newline node into a Temple expression
164
- def transform_newline_node(_node, _context)
152
+ def transform_newline_node(_node)
165
153
  [:newline]
166
154
  end
167
155
 
168
156
  # Compile a tag node with directives
169
- def transform_directives_for_tag_node(node, context)
157
+ def transform_directives_for_tag_node(node)
170
158
  # First, process any :if directives
171
159
  if_directive = node.directives.fetch(:if, false)
172
160
  if if_directive
173
161
  node.remove_directive(:if)
174
162
  return [:if,
175
163
  if_directive,
176
- transform(node, context)]
164
+ transform(node)]
177
165
  end
178
166
 
179
167
  # Second, process any :for directives
180
168
  for_directive = node.directives.fetch(:for, false)
181
169
  if for_directive
182
170
  node.remove_directive(:for)
183
- return [:orb, :for, for_directive, transform(node, context)]
171
+ return [:orb, :for, for_directive, transform(node)]
184
172
  end
185
173
 
186
174
  # Last, render as a dynamic node expression
187
- transform(node, context)
175
+ transform(node)
188
176
  end
189
177
 
190
178
  # Compile a dynamic tag node
191
- def transform_dynamic_tag_node(node, context)
192
- [:orb, :dynamic, node, transform_children(node, context)]
179
+ def transform_dynamic_tag_node(node)
180
+ [:orb, :dynamic, node, transform_children(node)]
193
181
  end
194
182
 
195
183
  # Helper or raising exceptions during compilation
196
184
  def runtime_error(error)
197
185
  [:multi].tap do |temple|
198
186
  (error.line - 1).times { temple << [:newline] } if error.line
199
- temple << [:code, %[raise ORB::Error.new(%q[#{error.message}], #{error.line.inspect})]]
187
+ temple << [:code, "raise ORB::Error.new(#{error.message.inspect}, #{error.line.inspect})"]
200
188
  end
201
189
  end
202
190
  end
@@ -30,7 +30,10 @@ module ORB
30
30
  html :Fast
31
31
  filter :Ambles
32
32
  filter :Escapable
33
- filter :StaticAnalyzer
33
+ # NOTE: StaticAnalyzer intentionally omitted. It calls Ripper.lex + Ripper.parse
34
+ # on every [:dynamic, ...] node to check if it's actually a static expression.
35
+ # ORB's compiler never emits static expressions as :dynamic nodes, so this
36
+ # filter always concludes "not static" -- pure overhead (Ripper is expensive).
34
37
  filter :ControlFlow
35
38
  filter :MultiFlattener
36
39
  filter :StaticMerger
@@ -3,6 +3,24 @@
3
3
  module ORB
4
4
  module Temple
5
5
  class Filters < ::Temple::Filter
6
+ # Valid HTML tag names: starts with a letter, followed by letters, digits, or hyphens.
7
+ # Used to validate dynamic tag names before interpolation into generated Ruby code.
8
+ VALID_HTML_TAG_NAME = /\A[a-zA-Z][a-zA-Z0-9-]*\z/
9
+
10
+ # Valid Ruby constant path: one or more segments like Foo, Foo::Bar, Foo::Bar::Baz.
11
+ # Each segment starts with an uppercase letter followed by word characters.
12
+ # Used to validate component names before interpolation into generated Ruby code.
13
+ VALID_COMPONENT_NAME = /\A[A-Z]\w*(::[A-Z]\w*)*\z/
14
+
15
+ # Valid Ruby identifier for use as a method name suffix in with_[name] slot calls.
16
+ # Used to validate slot names before interpolation into generated Ruby code.
17
+ VALID_SLOT_NAME = /\A[a-z_]\w*\z/
18
+
19
+ # Valid :for expression: single identifier or tuple-destructured identifiers
20
+ # (e.g., "item in items", "key, value in hash", "(key, value) in hash")
21
+ # followed by " in " and the collection expression.
22
+ VALID_FOR_EXPRESSION = /\A\s*(\(?[a-z_]\w*(?:\s*,\s*[a-z_]\w*)*\)?)\s+in\s+(.+)\z/m
23
+
6
24
  def initialize(options = {})
7
25
  @options = options
8
26
  @attributes_compiler = AttributesCompiler.new
@@ -32,15 +50,19 @@ module ORB
32
50
  name = node.tag.gsub('.', '::')
33
51
  komponent = ORB.lookup_component(name)
34
52
  komponent_name = komponent || name
53
+ unless komponent_name.match?(VALID_COMPONENT_NAME)
54
+ raise ORB::SyntaxError.new("Invalid component name: #{komponent_name.inspect}", 0)
55
+ end
35
56
 
36
57
  block_name = "__orb__#{komponent_name.rpartition('::').last.underscore}"
37
58
  block_name = node.directives.fetch(:with, block_name)
59
+ unless block_name.match?(VALID_SLOT_NAME)
60
+ raise ORB::SyntaxError.new("Invalid :with directive value: must be a valid Ruby identifier", 0)
61
+ end
38
62
 
39
- # We need to compile the attributes into a set of captures and a set of arguments
40
- # since arguments passed to the view component constructor may be defined as
41
- # dynamic expressions in our template, and we need to first capture their results.
42
- arg_captures = @attributes_compiler.compile_captures(node.attributes, tmp)
43
- args = @attributes_compiler.compile_komponent_args(node.attributes, tmp)
63
+ # Compile attributes in a single pass: captures for variable assignment +
64
+ # args string for the component constructor call.
65
+ arg_captures, args = @attributes_compiler.compile_captures_and_args(node.attributes, tmp)
44
66
 
45
67
  # Construct the render call for the view component
46
68
  code = "render #{komponent_name}.new(#{args}) do |#{block_name}|"
@@ -68,16 +90,20 @@ module ORB
68
90
  def on_orb_slot(node, content = [])
69
91
  tmp = unique_name
70
92
 
71
- # We need to compile the attributes into a set of captures and a set of arguments
72
- # since arguments passed to the view component constructor may be defined as
73
- # dynamic expressions in our template, and we need to first capture their results.
74
- arg_captures = @attributes_compiler.compile_captures(node.attributes, tmp)
75
- args = @attributes_compiler.compile_komponent_args(node.attributes, tmp)
93
+ # Compile attributes in a single pass
94
+ arg_captures, args = @attributes_compiler.compile_captures_and_args(node.attributes, tmp)
76
95
 
77
96
  # Prepare the slot name, parent name, and block name
78
97
  slot_name = node.slot
98
+ unless slot_name.match?(VALID_SLOT_NAME)
99
+ raise ORB::SyntaxError.new("Invalid slot name: #{slot_name.inspect}", 0)
100
+ end
101
+
79
102
  parent_name = "__orb__#{node.component.underscore}"
80
103
  block_name = node.directives.fetch(:with, "__orb__#{slot_name}")
104
+ unless block_name.match?(VALID_SLOT_NAME)
105
+ raise ORB::SyntaxError.new("Invalid :with directive value: must be a valid Ruby identifier", 0)
106
+ end
81
107
 
82
108
  # Construct the code to call the slot on the parent component
83
109
  code = "#{parent_name}.with_#{slot_name}(#{args}) do |#{block_name}|"
@@ -107,7 +133,22 @@ module ORB
107
133
  # @param [Array] content The content to be rendered for each iteration
108
134
  # @return [Array] compiled Temple expression
109
135
  def on_orb_for(expression, content)
110
- enumerator, collection = expression.split(' in ')
136
+ # Match single identifier or tuple-destructured identifiers (e.g., "key, value" or "(key, value)")
137
+ match = expression.match(VALID_FOR_EXPRESSION)
138
+ unless match
139
+ raise ORB::SyntaxError.new("Invalid :for expression: enumerator must be a valid Ruby identifier",
140
+ 0)
141
+ end
142
+
143
+ enumerator = match[1]
144
+ collection = match[2]
145
+
146
+ # Reject semicolons in the collection expression to prevent statement injection.
147
+ # Legitimate complex expressions should be assigned in a {%...%} block first.
148
+ if collection.include?(';')
149
+ raise ORB::SyntaxError.new("Invalid :for collection expression: semicolons are not allowed", 0)
150
+ end
151
+
111
152
  code = "#{collection}.each do |#{enumerator}|"
112
153
 
113
154
  [:multi,
@@ -125,6 +166,10 @@ module ORB
125
166
  on_orb_slot(node, content)
126
167
  else
127
168
  # It's a dynamic HTML tag
169
+ unless node.tag.match?(VALID_HTML_TAG_NAME)
170
+ raise ORB::SyntaxError.new("Invalid tag name: #{node.tag.inspect}", 0)
171
+ end
172
+
128
173
  tmp = unique_name
129
174
  splats = @attributes_compiler.compile_splat_attributes(node.splat_attributes)
130
175
  code = "content_tag('#{node.tag}', #{splats}) do"
@@ -9,7 +9,11 @@ module ORB
9
9
 
10
10
  def generate(prefix = nil)
11
11
  @unique_id += 1
12
- ["_orb_compiler", prefix, @unique_id].compact.join('_')
12
+ if prefix
13
+ "_orb_compiler_#{prefix}_#{@unique_id}"
14
+ else
15
+ "_orb_compiler_#{@unique_id}"
16
+ end
13
17
  end
14
18
  end
15
19
  end
data/lib/orb/token.rb CHANGED
@@ -9,7 +9,7 @@ module ORB
9
9
  @type = type
10
10
  @value = value
11
11
  @meta = meta
12
- @line = line || 0
12
+ @line = meta[:line] || 0
13
13
  end
14
14
 
15
15
  def set_meta(key, value)