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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +70 -0
- data/README.md +20 -4
- data/docs/2026-03-12-security-analysis.md +715 -0
- data/lib/orb/ast/abstract_node.rb +12 -2
- data/lib/orb/ast/control_expression_node.rb +4 -2
- data/lib/orb/ast/printing_expression_node.rb +4 -2
- data/lib/orb/patterns.rb +13 -1
- data/lib/orb/temple/attributes_compiler.rb +42 -4
- data/lib/orb/temple/compiler.rb +43 -55
- data/lib/orb/temple/engine.rb +4 -1
- data/lib/orb/temple/filters.rb +56 -11
- data/lib/orb/temple/identity.rb +5 -1
- data/lib/orb/token.rb +1 -1
- data/lib/orb/tokenizer2.rb +50 -33
- data/lib/orb/version.rb +1 -1
- metadata +3 -2
|
@@ -3,17 +3,27 @@
|
|
|
3
3
|
module ORB
|
|
4
4
|
module AST
|
|
5
5
|
class AbstractNode
|
|
6
|
-
attr_accessor :children, :attributes
|
|
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
|
-
|
|
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
|
-
@
|
|
21
|
+
@is_block
|
|
20
22
|
end
|
|
21
23
|
|
|
22
24
|
def end?
|
|
23
|
-
@
|
|
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
|
-
@
|
|
19
|
+
@is_block
|
|
18
20
|
end
|
|
19
21
|
|
|
20
22
|
def end?
|
|
21
|
-
@
|
|
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 =
|
|
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
|
-
#
|
|
85
|
-
#
|
|
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, [:
|
|
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
|
data/lib/orb/temple/compiler.rb
CHANGED
|
@@ -31,33 +31,33 @@ module ORB
|
|
|
31
31
|
#
|
|
32
32
|
# rubocop:disable Metrics/CyclomaticComplexity
|
|
33
33
|
# rubocop:disable Metrics/PerceivedComplexity
|
|
34
|
-
def transform(node
|
|
34
|
+
def transform(node)
|
|
35
35
|
if node.is_a?(ORB::AST::RootNode)
|
|
36
|
-
transform_children(node
|
|
36
|
+
transform_children(node)
|
|
37
37
|
elsif node.is_a?(ORB::AST::TextNode)
|
|
38
|
-
transform_text_node(node
|
|
38
|
+
transform_text_node(node)
|
|
39
39
|
elsif node.is_a?(ORB::AST::PrintingExpressionNode)
|
|
40
|
-
transform_printing_expression_node(node
|
|
40
|
+
transform_printing_expression_node(node)
|
|
41
41
|
elsif node.is_a?(ORB::AST::ControlExpressionNode)
|
|
42
|
-
transform_control_expression_node(node
|
|
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
|
|
44
|
+
transform_directives_for_tag_node(node)
|
|
45
45
|
elsif node.is_a?(ORB::AST::TagNode) && node.dynamic?
|
|
46
|
-
transform_dynamic_tag_node(node
|
|
46
|
+
transform_dynamic_tag_node(node)
|
|
47
47
|
elsif node.is_a?(ORB::AST::TagNode) && node.html_tag?
|
|
48
|
-
transform_html_tag_node(node
|
|
48
|
+
transform_html_tag_node(node)
|
|
49
49
|
elsif node.is_a?(ORB::AST::TagNode) && node.component_tag?
|
|
50
|
-
transform_component_tag_node(node
|
|
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
|
|
52
|
+
transform_component_slot_tag_node(node)
|
|
53
53
|
elsif node.is_a?(ORB::AST::BlockNode)
|
|
54
|
-
transform_block_node(node
|
|
54
|
+
transform_block_node(node)
|
|
55
55
|
elsif node.is_a?(ORB::AST::PublicCommentNode)
|
|
56
|
-
transform_public_comment_node(node
|
|
56
|
+
transform_public_comment_node(node)
|
|
57
57
|
elsif node.is_a?(ORB::AST::PrivateCommentNode)
|
|
58
|
-
transform_private_comment_node(node
|
|
58
|
+
transform_private_comment_node(node)
|
|
59
59
|
elsif node.is_a?(ORB::AST::NewlineNode)
|
|
60
|
-
transform_newline_node(node
|
|
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
|
|
72
|
-
[:multi, *node.children.map { |child| transform(child
|
|
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
|
|
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
|
|
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
|
|
88
|
+
transform_children(node)
|
|
100
89
|
else
|
|
101
|
-
[:capture, @identity.generate(:variable), transform_children(node
|
|
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
|
|
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
|
|
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
|
|
106
|
+
[:block, "#{tmp} = #{node.expression}", transform_children(node)]]
|
|
119
107
|
elsif node.children.any?
|
|
120
|
-
[:multi, [:code, node.expression], transform_children(node
|
|
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
|
|
128
|
-
[:orb, :tag, node.tag, node.attributes, transform_children(node
|
|
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
|
|
133
|
-
[:orb, :component, node, transform_children(node
|
|
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
|
|
138
|
-
[:orb, :slot, node, transform_children(node
|
|
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
|
|
130
|
+
def transform_block_node(node)
|
|
143
131
|
case node.name
|
|
144
132
|
when :if
|
|
145
|
-
[:orb, :if, node.expression, transform_children(node
|
|
133
|
+
[:orb, :if, node.expression, transform_children(node)]
|
|
146
134
|
when :for
|
|
147
|
-
[:orb, :for, node.expression, transform_children(node
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
175
|
+
transform(node)
|
|
188
176
|
end
|
|
189
177
|
|
|
190
178
|
# Compile a dynamic tag node
|
|
191
|
-
def transform_dynamic_tag_node(node
|
|
192
|
-
[:orb, :dynamic, node, transform_children(node
|
|
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,
|
|
187
|
+
temple << [:code, "raise ORB::Error.new(#{error.message.inspect}, #{error.line.inspect})"]
|
|
200
188
|
end
|
|
201
189
|
end
|
|
202
190
|
end
|
data/lib/orb/temple/engine.rb
CHANGED
|
@@ -30,7 +30,10 @@ module ORB
|
|
|
30
30
|
html :Fast
|
|
31
31
|
filter :Ambles
|
|
32
32
|
filter :Escapable
|
|
33
|
-
|
|
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
|
data/lib/orb/temple/filters.rb
CHANGED
|
@@ -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
|
-
#
|
|
40
|
-
#
|
|
41
|
-
|
|
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
|
-
#
|
|
72
|
-
|
|
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
|
-
|
|
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"
|
data/lib/orb/temple/identity.rb
CHANGED