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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cce449d0ac0ab86e9a8bec2e22d3016cbd165bec8dc45f13208609acbc621639
4
- data.tar.gz: cf2eba65f10e960d939f7cc0f32a405a1c5faa29aec05eb73777b4c02f5ba81a
3
+ metadata.gz: 0231005def8767c0efd2cb24b0efcb03b83b469f4760c5e3fcbaf262344c296e
4
+ data.tar.gz: c086ac14ba16300c36f7f0d39f02c426ca4ff568ef61cde48dfa91df3b433391
5
5
  SHA512:
6
- metadata.gz: f2874e3e0763b2f00fef23dcc43e01b5b4013429cdf61fba6d82db80127cfce92bf9a755a64d56f4f6b355fa38890da58d5dc82ba54fd40d6b7effca47d63895
7
- data.tar.gz: 1536a650424e03821225cd9b74745457b620d1f539f56c5d0ad66215ada01e4b2b8068f06cc497771f85844fd33781194bf150f1bced6ee9e810f9dbf0c1d492
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
- - [ ] create benchmark suite to establish baseline
410
+ - [x] create benchmark suite to establish baseline
411
411
  - [ ] possibly merge lexer states through more intelligent look-ahead
412
- - [ ] optimize AST Parser
413
- - [ ] optimize Compiler
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, :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{[a-zA-Z_:][-a-zA-Z0-9_:.]*}
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
- # 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
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
@@ -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,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, 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
@@ -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
@@ -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?(/\A[a-z_]\w*\z/)
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
- # We need to compile the attributes into a set of captures and a set of arguments
59
- # since arguments passed to the view component constructor may be defined as
60
- # dynamic expressions in our template, and we need to first capture their results.
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
- # We need to compile the attributes into a set of captures and a set of arguments
91
- # since arguments passed to the view component constructor may be defined as
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?(/\A[a-z_]\w*\z/)
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
- match = expression.match(/\A\s*([a-z_]\w*)\s+in\s+(.+)\z/m)
136
- raise ORB::SyntaxError.new("Invalid :for expression: enumerator must be a valid Ruby identifier", 0) unless match
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, collection = match[1], match[2]
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"
@@ -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)
@@ -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 = StringIO.new
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(NEWLINE) || @source.scan(CRLF) || @source.scan(BLANK) || @source.scan(OTHER)
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(NEWLINE) || @source.scan(CRLF) || @source.scan(BLANK) || @source.scan(OTHER)
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(NEWLINE) || @source.scan(CRLF) || @source.scan(BLANK) || @source.scan(OTHER)
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(NEWLINE) || @source.scan(CRLF) || @source.scan(BLANK) || @source.scan(OTHER)
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(NEWLINE) || @source.scan(CRLF) || @source.scan(BLANK) || @source.scan(OTHER)
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(NEWLINE) || @source.scan(CRLF) || @source.scan(BLANK) || @source.scan(OTHER)
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(BLANK) || @source.scan(OTHER)
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(NEWLINE) || @source.scan(CRLF) || @source.scan(BLANK) || @source.scan(OTHER)
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
- scan = StringScanner.new(str)
565
- until scan.eos?
566
- if scan.scan(NEWLINE) || scan.scan(CRLF)
567
- move(@line + 1, 1)
568
- elsif scan.scan(OTHER)
569
- move(@line, @column + scan.matched.size)
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
- Token.new(type, value, meta.merge(line: @line, column: @column) { |_k, v1, _v2| v1 })
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.string.clone
606
+ @buffer.dup
607
607
  end
608
608
 
609
609
  # Clear the buffer
610
610
  def clear_buffer
611
- @buffer = StringIO.new
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 = read_buffer
617
- clear_buffer
616
+ str = @buffer
617
+ @buffer = +''
618
618
  str
619
619
  end
620
620
 
data/lib/orb/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ORB
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.2"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: orb_template
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - KUY.io Inc.