orb_template 0.2.0 → 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 +25 -0
- data/README.md +3 -3
- 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 +41 -3
- data/lib/orb/temple/compiler.rb +42 -54
- data/lib/orb/temple/engine.rb +4 -1
- data/lib/orb/temple/filters.rb +22 -15
- data/lib/orb/temple/identity.rb +5 -1
- data/lib/orb/token.rb +1 -1
- data/lib/orb/tokenizer2.rb +28 -28
- data/lib/orb/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0231005def8767c0efd2cb24b0efcb03b83b469f4760c5e3fcbaf262344c296e
|
|
4
|
+
data.tar.gz: c086ac14ba16300c36f7f0d39f02c426ca4ff568ef61cde48dfa91df3b433391
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 168996b1d44310ecffbf9ecb8e24b1a550b45b13b8110e80060b164d7175b2ecf623aa2e7d4b9da550378f7fb39f0862b85593844c9e8dff5936344dc376b9eb
|
|
7
|
+
data.tar.gz: 826c5078e0941e8874f1c70527b0ee967c900be19d1e520d4768601f5086b1d107b7655ec9d534868aa4e11abb488b5cfce7a2f9cc7b1506561849bca53ab19f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.2.2] - 2026-03-13
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Tuple destructuring in `:for` expressions now works correctly (e.g., `{#for name, spec in @tokens}`)
|
|
8
|
+
|
|
9
|
+
### Performance
|
|
10
|
+
|
|
11
|
+
- **33% faster** compilation pipeline for realistic templates, up to **52% faster** for expression-heavy templates
|
|
12
|
+
- Removed Temple `StaticAnalyzer` filter from engine pipeline -- ORB never emits static expressions as `:dynamic` nodes, so Ripper lexing/parsing on every dynamic node was pure overhead
|
|
13
|
+
- Boolean attributes now emit `[:static, ""]` directly instead of `[:dynamic, "nil"]`, avoiding unnecessary Ripper analysis
|
|
14
|
+
- Cached `block?`/`end?` regex results in expression node constructors (computed once instead of on every call)
|
|
15
|
+
- Optimized `Identity.generate` with direct string interpolation instead of array/compact/join
|
|
16
|
+
- Lazy-initialized `@errors` on AST nodes, saving one array allocation per node
|
|
17
|
+
- Removed unused `context={}` parameter from all compiler transform methods, eliminating hash allocation per recursive call
|
|
18
|
+
- Added single-pass `compile_captures_and_args` to `AttributesCompiler`, reducing double iteration over attributes
|
|
19
|
+
- Optimized `Token` constructor to avoid `method_missing` overhead and skip hash merge for common no-meta case
|
|
20
|
+
- Tokenizer: replaced per-call `StringScanner` allocation in `move_by` with `String#count`/`rindex`
|
|
21
|
+
- Tokenizer: switched from `StringIO` to `String` buffer with swap-on-consume pattern
|
|
22
|
+
- Tokenizer: added greedy multi-character scanning patterns for bulk text consumption in 9 tokenizer states
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- Benchmark test suite (`test/benchmark_test.rb`) with 8 template categories, per-template regression thresholds, stage-level profiling, and Temple IR node count tracking
|
|
27
|
+
|
|
3
28
|
## [0.2.0] - 2026-03-12
|
|
4
29
|
|
|
5
30
|
### Security
|
data/README.md
CHANGED
|
@@ -407,10 +407,10 @@ To enable `Tailwindcss` support for ORB, add this to your `settings.json`:
|
|
|
407
407
|
- [ ] full YARD-compatible documentation of the library
|
|
408
408
|
- [ ] **Step 3: Make it fast**
|
|
409
409
|
- [x] convert Lexer code to `StringScanner`
|
|
410
|
-
- [
|
|
410
|
+
- [x] create benchmark suite to establish baseline
|
|
411
411
|
- [ ] possibly merge lexer states through more intelligent look-ahead
|
|
412
|
-
- [
|
|
413
|
-
- [
|
|
412
|
+
- [x] optimize AST Parser
|
|
413
|
+
- [x] optimize Compiler
|
|
414
414
|
- [ ] **Step 4: Evolve**
|
|
415
415
|
- [ ] support additional directives, for instance, `Turbo` or `Stimulus` specific directives
|
|
416
416
|
- [ ] support additional block constructs
|
|
@@ -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,13 +81,14 @@ 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
93
|
[:html, :attr, attribute.name, [:escape, true, [:dynamic, attribute.value]]]
|
|
93
94
|
end
|
|
@@ -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,128 +68,116 @@ 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
|
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
|
@@ -16,6 +16,11 @@ module ORB
|
|
|
16
16
|
# Used to validate slot names before interpolation into generated Ruby code.
|
|
17
17
|
VALID_SLOT_NAME = /\A[a-z_]\w*\z/
|
|
18
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
|
+
|
|
19
24
|
def initialize(options = {})
|
|
20
25
|
@options = options
|
|
21
26
|
@attributes_compiler = AttributesCompiler.new
|
|
@@ -51,15 +56,13 @@ module ORB
|
|
|
51
56
|
|
|
52
57
|
block_name = "__orb__#{komponent_name.rpartition('::').last.underscore}"
|
|
53
58
|
block_name = node.directives.fetch(:with, block_name)
|
|
54
|
-
unless block_name.match?(
|
|
59
|
+
unless block_name.match?(VALID_SLOT_NAME)
|
|
55
60
|
raise ORB::SyntaxError.new("Invalid :with directive value: must be a valid Ruby identifier", 0)
|
|
56
61
|
end
|
|
57
62
|
|
|
58
|
-
#
|
|
59
|
-
#
|
|
60
|
-
|
|
61
|
-
arg_captures = @attributes_compiler.compile_captures(node.attributes, tmp)
|
|
62
|
-
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)
|
|
63
66
|
|
|
64
67
|
# Construct the render call for the view component
|
|
65
68
|
code = "render #{komponent_name}.new(#{args}) do |#{block_name}|"
|
|
@@ -87,20 +90,18 @@ module ORB
|
|
|
87
90
|
def on_orb_slot(node, content = [])
|
|
88
91
|
tmp = unique_name
|
|
89
92
|
|
|
90
|
-
#
|
|
91
|
-
|
|
92
|
-
# dynamic expressions in our template, and we need to first capture their results.
|
|
93
|
-
arg_captures = @attributes_compiler.compile_captures(node.attributes, tmp)
|
|
94
|
-
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)
|
|
95
95
|
|
|
96
96
|
# Prepare the slot name, parent name, and block name
|
|
97
97
|
slot_name = node.slot
|
|
98
98
|
unless slot_name.match?(VALID_SLOT_NAME)
|
|
99
99
|
raise ORB::SyntaxError.new("Invalid slot name: #{slot_name.inspect}", 0)
|
|
100
100
|
end
|
|
101
|
+
|
|
101
102
|
parent_name = "__orb__#{node.component.underscore}"
|
|
102
103
|
block_name = node.directives.fetch(:with, "__orb__#{slot_name}")
|
|
103
|
-
unless block_name.match?(
|
|
104
|
+
unless block_name.match?(VALID_SLOT_NAME)
|
|
104
105
|
raise ORB::SyntaxError.new("Invalid :with directive value: must be a valid Ruby identifier", 0)
|
|
105
106
|
end
|
|
106
107
|
|
|
@@ -132,10 +133,15 @@ module ORB
|
|
|
132
133
|
# @param [Array] content The content to be rendered for each iteration
|
|
133
134
|
# @return [Array] compiled Temple expression
|
|
134
135
|
def on_orb_for(expression, content)
|
|
135
|
-
|
|
136
|
-
|
|
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
|
|
137
142
|
|
|
138
|
-
enumerator
|
|
143
|
+
enumerator = match[1]
|
|
144
|
+
collection = match[2]
|
|
139
145
|
|
|
140
146
|
# Reject semicolons in the collection expression to prevent statement injection.
|
|
141
147
|
# Legitimate complex expressions should be assigned in a {%...%} block first.
|
|
@@ -163,6 +169,7 @@ module ORB
|
|
|
163
169
|
unless node.tag.match?(VALID_HTML_TAG_NAME)
|
|
164
170
|
raise ORB::SyntaxError.new("Invalid tag name: #{node.tag.inspect}", 0)
|
|
165
171
|
end
|
|
172
|
+
|
|
166
173
|
tmp = unique_name
|
|
167
174
|
splats = @attributes_compiler.compile_splat_attributes(node.splat_attributes)
|
|
168
175
|
code = "content_tag('#{node.tag}', #{splats}) do"
|
data/lib/orb/temple/identity.rb
CHANGED
data/lib/orb/token.rb
CHANGED
data/lib/orb/tokenizer2.rb
CHANGED
|
@@ -34,7 +34,6 @@ module ORB
|
|
|
34
34
|
@raise_errors = options.fetch(:raise_errors, true)
|
|
35
35
|
|
|
36
36
|
# Streaming Tokenizer State
|
|
37
|
-
@cursor = 0
|
|
38
37
|
@column = 1
|
|
39
38
|
@line = 1
|
|
40
39
|
@errors = []
|
|
@@ -42,7 +41,7 @@ module ORB
|
|
|
42
41
|
@attributes = []
|
|
43
42
|
@braces = []
|
|
44
43
|
@state = :initial
|
|
45
|
-
@buffer =
|
|
44
|
+
@buffer = +''
|
|
46
45
|
end
|
|
47
46
|
|
|
48
47
|
# Main Entry
|
|
@@ -66,11 +65,6 @@ module ORB
|
|
|
66
65
|
|
|
67
66
|
# Dispatcher based on current state
|
|
68
67
|
def next_token
|
|
69
|
-
# Detect infinite loop
|
|
70
|
-
# if @previous_cursor == @cursor && @previous_state == @state
|
|
71
|
-
# raise "Internal Error: detected infinite loop in :#{@state}"
|
|
72
|
-
# end
|
|
73
|
-
|
|
74
68
|
# Dispatch to state handler
|
|
75
69
|
send(:"next_in_#{@state}")
|
|
76
70
|
end
|
|
@@ -124,7 +118,7 @@ module ORB
|
|
|
124
118
|
add_token(:tag_open, nil)
|
|
125
119
|
move_by_matched
|
|
126
120
|
transition_to(:tag_open)
|
|
127
|
-
elsif @source.scan(OTHER)
|
|
121
|
+
elsif @source.scan(INITIAL_TEXT) || @source.scan(OTHER)
|
|
128
122
|
buffer_matched
|
|
129
123
|
move_by_matched
|
|
130
124
|
else
|
|
@@ -243,7 +237,7 @@ module ORB
|
|
|
243
237
|
current_attribute[2] = attribute_value
|
|
244
238
|
move_by_matched
|
|
245
239
|
transition_to(:tag_open_content)
|
|
246
|
-
elsif @source.scan(
|
|
240
|
+
elsif @source.scan(SINGLE_QUOTED_TEXT) || @source.scan(NEWLINE) || @source.scan(CRLF) || @source.scan(OTHER)
|
|
247
241
|
buffer_matched
|
|
248
242
|
move_by_matched
|
|
249
243
|
else
|
|
@@ -259,7 +253,7 @@ module ORB
|
|
|
259
253
|
current_attribute[2] = attribute_value
|
|
260
254
|
move_by_matched
|
|
261
255
|
transition_to(:tag_open_content)
|
|
262
|
-
elsif @source.scan(
|
|
256
|
+
elsif @source.scan(DOUBLE_QUOTED_TEXT) || @source.scan(NEWLINE) || @source.scan(CRLF) || @source.scan(OTHER)
|
|
263
257
|
buffer_matched
|
|
264
258
|
move_by_matched
|
|
265
259
|
else
|
|
@@ -285,7 +279,7 @@ module ORB
|
|
|
285
279
|
move_by_matched
|
|
286
280
|
transition_to(:tag_open_content)
|
|
287
281
|
end
|
|
288
|
-
elsif @source.scan(
|
|
282
|
+
elsif @source.scan(EXPRESSION_TEXT) || @source.scan(NEWLINE) || @source.scan(CRLF) || @source.scan(OTHER)
|
|
289
283
|
buffer_matched
|
|
290
284
|
move_by_matched
|
|
291
285
|
else
|
|
@@ -311,7 +305,7 @@ module ORB
|
|
|
311
305
|
clear_braces
|
|
312
306
|
transition_to(:tag_open_content)
|
|
313
307
|
end
|
|
314
|
-
elsif @source.scan(
|
|
308
|
+
elsif @source.scan(EXPRESSION_TEXT) || @source.scan(NEWLINE) || @source.scan(CRLF) || @source.scan(OTHER)
|
|
315
309
|
buffer_matched
|
|
316
310
|
move_by_matched
|
|
317
311
|
else
|
|
@@ -356,7 +350,7 @@ module ORB
|
|
|
356
350
|
update_current_token(text)
|
|
357
351
|
move_by_matched
|
|
358
352
|
transition_to(:initial)
|
|
359
|
-
elsif @source.scan(
|
|
353
|
+
elsif @source.scan(COMMENT_TEXT) || @source.scan(NEWLINE) || @source.scan(CRLF) || @source.scan(OTHER)
|
|
360
354
|
buffer_matched
|
|
361
355
|
move_by_matched
|
|
362
356
|
else
|
|
@@ -371,7 +365,7 @@ module ORB
|
|
|
371
365
|
update_current_token(text)
|
|
372
366
|
move_by_matched
|
|
373
367
|
transition_to(:initial)
|
|
374
|
-
elsif @source.scan(
|
|
368
|
+
elsif @source.scan(COMMENT_TEXT) || @source.scan(NEWLINE) || @source.scan(CRLF) || @source.scan(OTHER)
|
|
375
369
|
buffer_matched
|
|
376
370
|
move_by_matched
|
|
377
371
|
else
|
|
@@ -409,7 +403,7 @@ module ORB
|
|
|
409
403
|
buffer_matched
|
|
410
404
|
move_by_matched
|
|
411
405
|
end
|
|
412
|
-
elsif @source.scan(
|
|
406
|
+
elsif @source.scan(BLOCK_CONTENT_TEXT) || @source.scan(OTHER)
|
|
413
407
|
buffer_matched
|
|
414
408
|
move_by_matched
|
|
415
409
|
end
|
|
@@ -508,7 +502,7 @@ module ORB
|
|
|
508
502
|
buffer(tmp)
|
|
509
503
|
move_by(tmp)
|
|
510
504
|
end
|
|
511
|
-
elsif @source.scan(
|
|
505
|
+
elsif @source.scan(VERBATIM_TEXT) || @source.scan(NEWLINE) || @source.scan(CRLF) || @source.scan(OTHER)
|
|
512
506
|
buffer_matched
|
|
513
507
|
move_by_matched
|
|
514
508
|
end
|
|
@@ -551,6 +545,7 @@ module ORB
|
|
|
551
545
|
if @braces.length >= MAX_BRACE_DEPTH
|
|
552
546
|
raise ORB::SyntaxError.new("Maximum brace nesting depth (#{MAX_BRACE_DEPTH}) exceeded", @line)
|
|
553
547
|
end
|
|
548
|
+
|
|
554
549
|
@braces << "{"
|
|
555
550
|
end
|
|
556
551
|
|
|
@@ -560,14 +555,15 @@ module ORB
|
|
|
560
555
|
@column = column
|
|
561
556
|
end
|
|
562
557
|
|
|
558
|
+
# Update line/column tracking from a matched string.
|
|
559
|
+
# Uses String#count and String#rindex instead of re-scanning with StringScanner.
|
|
563
560
|
def move_by(str)
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
end
|
|
561
|
+
newlines = str.count("\n")
|
|
562
|
+
if newlines.positive?
|
|
563
|
+
@line += newlines
|
|
564
|
+
@column = str.length - str.rindex("\n")
|
|
565
|
+
else
|
|
566
|
+
@column += str.length
|
|
571
567
|
end
|
|
572
568
|
end
|
|
573
569
|
|
|
@@ -582,7 +578,11 @@ module ORB
|
|
|
582
578
|
|
|
583
579
|
# Create a new token
|
|
584
580
|
def create_token(type, value, meta = {})
|
|
585
|
-
|
|
581
|
+
if meta.empty?
|
|
582
|
+
Token.new(type, value, { line: @line, column: @column })
|
|
583
|
+
else
|
|
584
|
+
Token.new(type, value, { line: @line, column: @column }.merge!(meta))
|
|
585
|
+
end
|
|
586
586
|
end
|
|
587
587
|
|
|
588
588
|
# Create a token and add it to the token list
|
|
@@ -603,18 +603,18 @@ module ORB
|
|
|
603
603
|
|
|
604
604
|
# Read the buffer to a string
|
|
605
605
|
def read_buffer
|
|
606
|
-
@buffer.
|
|
606
|
+
@buffer.dup
|
|
607
607
|
end
|
|
608
608
|
|
|
609
609
|
# Clear the buffer
|
|
610
610
|
def clear_buffer
|
|
611
|
-
@buffer =
|
|
611
|
+
@buffer = +''
|
|
612
612
|
end
|
|
613
613
|
|
|
614
614
|
# Read the buffer to a string and clear it
|
|
615
615
|
def consume_buffer
|
|
616
|
-
str =
|
|
617
|
-
|
|
616
|
+
str = @buffer
|
|
617
|
+
@buffer = +''
|
|
618
618
|
str
|
|
619
619
|
end
|
|
620
620
|
|
data/lib/orb/version.rb
CHANGED