orb_template 0.1.0
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 +7 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/Makefile +45 -0
- data/README.md +429 -0
- data/Rakefile +15 -0
- data/lib/orb/ast/abstract_node.rb +27 -0
- data/lib/orb/ast/attribute.rb +51 -0
- data/lib/orb/ast/block_node.rb +26 -0
- data/lib/orb/ast/control_expression_node.rb +27 -0
- data/lib/orb/ast/newline_node.rb +22 -0
- data/lib/orb/ast/printing_expression_node.rb +29 -0
- data/lib/orb/ast/private_comment_node.rb +22 -0
- data/lib/orb/ast/public_comment_node.rb +22 -0
- data/lib/orb/ast/root_node.rb +11 -0
- data/lib/orb/ast/tag_node.rb +208 -0
- data/lib/orb/ast/text_node.rb +22 -0
- data/lib/orb/ast.rb +19 -0
- data/lib/orb/document.rb +19 -0
- data/lib/orb/errors.rb +40 -0
- data/lib/orb/parser.rb +182 -0
- data/lib/orb/patterns.rb +40 -0
- data/lib/orb/rails_derp.rb +138 -0
- data/lib/orb/rails_template.rb +101 -0
- data/lib/orb/railtie.rb +9 -0
- data/lib/orb/render_context.rb +36 -0
- data/lib/orb/template.rb +72 -0
- data/lib/orb/temple/attributes_compiler.rb +114 -0
- data/lib/orb/temple/compiler.rb +204 -0
- data/lib/orb/temple/engine.rb +40 -0
- data/lib/orb/temple/filters.rb +132 -0
- data/lib/orb/temple/generators.rb +108 -0
- data/lib/orb/temple/identity.rb +16 -0
- data/lib/orb/temple/parser.rb +46 -0
- data/lib/orb/temple.rb +16 -0
- data/lib/orb/token.rb +47 -0
- data/lib/orb/tokenizer.rb +757 -0
- data/lib/orb/tokenizer2.rb +591 -0
- data/lib/orb/utils/erb.rb +40 -0
- data/lib/orb/utils/orb.rb +12 -0
- data/lib/orb/version.rb +5 -0
- data/lib/orb.rb +50 -0
- metadata +89 -0
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'strscan'
|
|
4
|
+
require_relative 'patterns'
|
|
5
|
+
|
|
6
|
+
module ORB
|
|
7
|
+
# Tokenizer2 is a streaming, non-recursive tokenizer for ORB templates.
|
|
8
|
+
#
|
|
9
|
+
# It scans the source sequentially and emits tokens as it passes over the input.
|
|
10
|
+
# During scanning, it keeps track of the current state and the list of tokens.
|
|
11
|
+
# Any consumption of the source, either by buffering or skipping moves the cursor.
|
|
12
|
+
# The cursor position is used to keep track of the current line and column in the
|
|
13
|
+
# virtual source document. When tokens are generated, they are annotated with the
|
|
14
|
+
# position they were found in the virtual document.
|
|
15
|
+
class Tokenizer2
|
|
16
|
+
include ORB::Patterns
|
|
17
|
+
|
|
18
|
+
# Tags that should be ignored
|
|
19
|
+
IGNORED_BODY_TAGS = %w[script style].freeze
|
|
20
|
+
|
|
21
|
+
# Tags that are self-closing by HTML5 spec
|
|
22
|
+
VOID_ELEMENTS = %w[area base br col command embed hr img input keygen link meta param source track wbr].freeze
|
|
23
|
+
|
|
24
|
+
attr_reader :errors, :tokens
|
|
25
|
+
|
|
26
|
+
def initialize(source, options = {})
|
|
27
|
+
@source = StringScanner.new(source)
|
|
28
|
+
@raise_errors = options.fetch(:raise_errors, true)
|
|
29
|
+
|
|
30
|
+
# Streaming Tokenizer State
|
|
31
|
+
@cursor = 0
|
|
32
|
+
@column = 1
|
|
33
|
+
@line = 1
|
|
34
|
+
@errors = []
|
|
35
|
+
@tokens = []
|
|
36
|
+
@attributes = []
|
|
37
|
+
@braces = []
|
|
38
|
+
@state = :initial
|
|
39
|
+
@buffer = StringIO.new
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Main Entry
|
|
43
|
+
def tokenize
|
|
44
|
+
next_token until @source.eos?
|
|
45
|
+
|
|
46
|
+
# Consume remaining buffer
|
|
47
|
+
buffer_to_text_token
|
|
48
|
+
|
|
49
|
+
# Return the tokens
|
|
50
|
+
@tokens
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
alias_method :tokenize!, :tokenize
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
# Dispatcher based on current state
|
|
58
|
+
def next_token
|
|
59
|
+
# Detect infinite loop
|
|
60
|
+
# if @previous_cursor == @cursor && @previous_state == @state
|
|
61
|
+
# raise "Internal Error: detected infinite loop in :#{@state}"
|
|
62
|
+
# end
|
|
63
|
+
|
|
64
|
+
# Dispatch to state handler
|
|
65
|
+
send(:"next_in_#{@state}")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Read next token in :initial state
|
|
69
|
+
# rubocop:disable Metrics/AbcSize
|
|
70
|
+
def next_in_initial
|
|
71
|
+
if @source.scan(NEWLINE) || @source.scan(CRLF)
|
|
72
|
+
buffer_to_text_token
|
|
73
|
+
add_token(:newline, @source.matched)
|
|
74
|
+
move_by_matched
|
|
75
|
+
elsif @source.scan(PRIVATE_COMMENT_START)
|
|
76
|
+
buffer_to_text_token
|
|
77
|
+
add_token(:private_comment, nil)
|
|
78
|
+
move_by_matched
|
|
79
|
+
transition_to(:private_comment)
|
|
80
|
+
elsif @source.scan(PUBLIC_COMMENT_START)
|
|
81
|
+
buffer_to_text_token
|
|
82
|
+
add_token(:public_comment, nil)
|
|
83
|
+
move_by_matched
|
|
84
|
+
transition_to(:public_comment)
|
|
85
|
+
elsif @source.scan(BLOCK_OPEN)
|
|
86
|
+
buffer_to_text_token
|
|
87
|
+
add_token(:block_open, nil)
|
|
88
|
+
move_by_matched
|
|
89
|
+
transition_to(:block_open)
|
|
90
|
+
elsif @source.scan(BLOCK_CLOSE)
|
|
91
|
+
buffer_to_text_token
|
|
92
|
+
add_token(:block_close, nil)
|
|
93
|
+
move_by_matched
|
|
94
|
+
transition_to(:block_close)
|
|
95
|
+
elsif @source.scan(PRINTING_EXPRESSION_START)
|
|
96
|
+
buffer_to_text_token
|
|
97
|
+
add_token(:printing_expression, nil)
|
|
98
|
+
move_by_matched
|
|
99
|
+
clear_braces
|
|
100
|
+
transition_to(:printing_expression)
|
|
101
|
+
elsif @source.scan(CONTROL_EXPRESSION_START)
|
|
102
|
+
buffer_to_text_token
|
|
103
|
+
add_token(:control_expression, nil)
|
|
104
|
+
move_by_matched
|
|
105
|
+
clear_braces
|
|
106
|
+
transition_to(:control_expression)
|
|
107
|
+
elsif @source.scan(END_TAG_START)
|
|
108
|
+
buffer_to_text_token
|
|
109
|
+
add_token(:tag_close, nil)
|
|
110
|
+
move_by_matched
|
|
111
|
+
transition_to(:tag_close)
|
|
112
|
+
elsif @source.scan(START_TAG_START)
|
|
113
|
+
buffer_to_text_token
|
|
114
|
+
add_token(:tag_open, nil)
|
|
115
|
+
move_by_matched
|
|
116
|
+
transition_to(:tag_open)
|
|
117
|
+
elsif @source.scan(OTHER)
|
|
118
|
+
buffer_matched
|
|
119
|
+
move_by_matched
|
|
120
|
+
else
|
|
121
|
+
syntax_error!("Unexpected '#{@source.peek(1)}'")
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
# rubocop:enable Metrics/AbcSize
|
|
125
|
+
|
|
126
|
+
# Read next token in :tag_open state
|
|
127
|
+
def next_in_tag_open
|
|
128
|
+
if @source.scan(NEWLINE) || @source.scan(CRLF)
|
|
129
|
+
move_by_matched
|
|
130
|
+
elsif @source.scan(TAG_NAME)
|
|
131
|
+
tag = @source.matched
|
|
132
|
+
update_current_token(tag)
|
|
133
|
+
move_by_matched
|
|
134
|
+
clear_attributes
|
|
135
|
+
transition_to(:tag_open_content)
|
|
136
|
+
else
|
|
137
|
+
syntax_error!("Unexpected '#{@source.peek(1)}'")
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Read next token in :tag_open_content state
|
|
142
|
+
def next_in_tag_open_content
|
|
143
|
+
if @source.scan(NEWLINE) || @source.scan(CRLF) || @source.scan(BLANK)
|
|
144
|
+
move_by_matched
|
|
145
|
+
elsif @source.scan(START_TAG_END_VERBATIM)
|
|
146
|
+
current_token.set_meta(:self_closing, false)
|
|
147
|
+
current_token.set_meta(:verbatim, true)
|
|
148
|
+
current_token.set_meta(:attributes, @attributes) if @attributes.any?
|
|
149
|
+
clear_attributes
|
|
150
|
+
move_by_matched
|
|
151
|
+
transition_to(:verbatim)
|
|
152
|
+
elsif @source.scan(START_TAG_END_SELF_CLOSING)
|
|
153
|
+
current_token.set_meta(:self_closing, true)
|
|
154
|
+
current_token.set_meta(:attributes, @attributes) if @attributes.any?
|
|
155
|
+
clear_attributes
|
|
156
|
+
move_by_matched
|
|
157
|
+
transition_to(:initial)
|
|
158
|
+
elsif @source.scan(START_TAG_END)
|
|
159
|
+
current_token.set_meta(:self_closing, VOID_ELEMENTS.include?(current_token.value))
|
|
160
|
+
current_token.set_meta(:attributes, @attributes) if @attributes.any?
|
|
161
|
+
clear_attributes
|
|
162
|
+
move_by_matched
|
|
163
|
+
transition_to(:initial)
|
|
164
|
+
elsif @source.scan(START_TAG_START)
|
|
165
|
+
syntax_error!("Unexpected start of tag")
|
|
166
|
+
elsif @source.scan(%r{\*[^\s>/=]+})
|
|
167
|
+
splat = @source.matched
|
|
168
|
+
@attributes << [nil, :splat, splat]
|
|
169
|
+
move_by_matched
|
|
170
|
+
elsif @source.check(OTHER)
|
|
171
|
+
transition_to(:attribute_name)
|
|
172
|
+
else
|
|
173
|
+
syntax_error!("Unexpected '#{@source.peek(1)}'")
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Read next token in :attribute_name state
|
|
178
|
+
def next_in_attribute_name
|
|
179
|
+
if @source.scan(ATTRIBUTE_NAME)
|
|
180
|
+
@attributes << [@source.matched, :boolean, true]
|
|
181
|
+
move_by_matched
|
|
182
|
+
transition_to(:attribute_value?)
|
|
183
|
+
else
|
|
184
|
+
syntax_error!("Expected a valid attribute name")
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Read next token in :attribute_value? state
|
|
189
|
+
def next_in_attribute_value?
|
|
190
|
+
if @source.scan(NEWLINE) || @source.scan(CRLF) || @source.scan(BLANK)
|
|
191
|
+
move_by_matched
|
|
192
|
+
elsif @source.scan(ATTRIBUTE_ASSIGN)
|
|
193
|
+
move_by_matched
|
|
194
|
+
transition_to(:attribute_value!)
|
|
195
|
+
else
|
|
196
|
+
transition_to(:tag_open_content)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Read next token in :attribute_value! state
|
|
201
|
+
def next_in_attribute_value!
|
|
202
|
+
if @source.scan(NEWLINE) || @source.scan(CRLF) || @source.scan(BLANK)
|
|
203
|
+
move_by_matched
|
|
204
|
+
elsif @source.scan(SINGLE_QUOTE)
|
|
205
|
+
move_by_matched
|
|
206
|
+
transition_to(:attribute_value_single_quoted)
|
|
207
|
+
elsif @source.scan(DOUBLE_QUOTE)
|
|
208
|
+
move_by_matched
|
|
209
|
+
transition_to(:attribute_value_double_quoted)
|
|
210
|
+
elsif @source.scan(BRACE_OPEN)
|
|
211
|
+
move_by_matched
|
|
212
|
+
transition_to(:attribute_value_expression)
|
|
213
|
+
elsif @source.check(OTHER)
|
|
214
|
+
transition_to(:attribute_value_unquoted)
|
|
215
|
+
else
|
|
216
|
+
syntax_error!("Unexpected '#{@source.peek(1)}'")
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Read next token in :attribute_value_single_quoted state
|
|
221
|
+
def next_in_attribute_value_single_quoted
|
|
222
|
+
if @source.scan(SINGLE_QUOTE)
|
|
223
|
+
attribute_value = consume_buffer
|
|
224
|
+
current_attribute[1] = :string
|
|
225
|
+
current_attribute[2] = attribute_value
|
|
226
|
+
move_by_matched
|
|
227
|
+
transition_to(:tag_open_content)
|
|
228
|
+
elsif @source.scan(NEWLINE) || @source.scan(CRLF) || @source.scan(BLANK) || @source.scan(OTHER)
|
|
229
|
+
buffer_matched
|
|
230
|
+
move_by_matched
|
|
231
|
+
else
|
|
232
|
+
syntax_error!("Unexpected '#{@source.peek(1)}'")
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Read next token in :attribute_value_double_quoted state
|
|
237
|
+
def next_in_attribute_value_double_quoted
|
|
238
|
+
if @source.scan(DOUBLE_QUOTE)
|
|
239
|
+
attribute_value = consume_buffer
|
|
240
|
+
current_attribute[1] = :string
|
|
241
|
+
current_attribute[2] = attribute_value
|
|
242
|
+
move_by_matched
|
|
243
|
+
transition_to(:tag_open_content)
|
|
244
|
+
elsif @source.scan(NEWLINE) || @source.scan(CRLF) || @source.scan(BLANK) || @source.scan(OTHER)
|
|
245
|
+
buffer_matched
|
|
246
|
+
move_by_matched
|
|
247
|
+
else
|
|
248
|
+
syntax_error!("Unexpected '#{@source.peek(1)}'")
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
# Read next token in :attribute_value_expression state
|
|
253
|
+
def next_in_attribute_value_expression
|
|
254
|
+
if @source.scan(BRACE_OPEN)
|
|
255
|
+
@braces << "{"
|
|
256
|
+
buffer_matched
|
|
257
|
+
move_by_matched
|
|
258
|
+
elsif @source.scan(BRACE_CLOSE)
|
|
259
|
+
if @braces.any?
|
|
260
|
+
@braces.pop
|
|
261
|
+
buffer_matched
|
|
262
|
+
move_by_matched
|
|
263
|
+
else
|
|
264
|
+
attribute_expression = consume_buffer
|
|
265
|
+
current_attribute[1] = :expression
|
|
266
|
+
current_attribute[2] = attribute_expression.strip
|
|
267
|
+
move_by_matched
|
|
268
|
+
transition_to(:tag_open_content)
|
|
269
|
+
end
|
|
270
|
+
elsif @source.scan(NEWLINE) || @source.scan(CRLF) || @source.scan(BLANK) || @source.scan(OTHER)
|
|
271
|
+
buffer_matched
|
|
272
|
+
move_by_matched
|
|
273
|
+
else
|
|
274
|
+
syntax_error!("Unexpected end of input while reading expression attribute value")
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Read next token in :attribute_value_unquoted state
|
|
279
|
+
def next_in_attribute_value_unquoted
|
|
280
|
+
if @source.scan(UNQUOTED_VALUE)
|
|
281
|
+
attribute_value = @source.matched
|
|
282
|
+
current_attribute[1] = :string
|
|
283
|
+
current_attribute[2] = attribute_value
|
|
284
|
+
move_by_matched
|
|
285
|
+
transition_to(:tag_open_content)
|
|
286
|
+
elsif @source.scan(UNQUOTED_VALUE_INVALID_CHARS)
|
|
287
|
+
syntax_error!("Unexpected '#{@source.peek(1)}' in unquoted attribute value")
|
|
288
|
+
else
|
|
289
|
+
syntax_error!("Unexpected end of input while reading unquoted attribute value")
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Read next token in :tag_close state
|
|
294
|
+
def next_in_tag_close
|
|
295
|
+
if @source.scan(TAG_NAME)
|
|
296
|
+
buffer_matched
|
|
297
|
+
move_by_matched
|
|
298
|
+
elsif @source.scan(/[$?>]+/)
|
|
299
|
+
tag = consume_buffer
|
|
300
|
+
update_current_token(tag)
|
|
301
|
+
move_by(tag)
|
|
302
|
+
transition_to(:initial)
|
|
303
|
+
else
|
|
304
|
+
syntax_error!("Unexpected '#{@source.peek(1)}'")
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Read next token in :public_comment state
|
|
309
|
+
def next_in_public_comment
|
|
310
|
+
if @source.scan(PUBLIC_COMMENT_END)
|
|
311
|
+
text = consume_buffer
|
|
312
|
+
update_current_token(text)
|
|
313
|
+
move_by_matched
|
|
314
|
+
transition_to(:initial)
|
|
315
|
+
elsif @source.scan(NEWLINE) || @source.scan(CRLF) || @source.scan(BLANK) || @source.scan(OTHER)
|
|
316
|
+
buffer_matched
|
|
317
|
+
move_by_matched
|
|
318
|
+
else
|
|
319
|
+
syntax_error!("Unexpected input '#{@source.peek(1)}' while reading public comment")
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# Read tokens in :private_comment state
|
|
324
|
+
def next_in_private_comment
|
|
325
|
+
if @source.scan(PRIVATE_COMMENT_END)
|
|
326
|
+
text = consume_buffer
|
|
327
|
+
update_current_token(text)
|
|
328
|
+
move_by_matched
|
|
329
|
+
transition_to(:initial)
|
|
330
|
+
elsif @source.scan(NEWLINE) || @source.scan(CRLF) || @source.scan(BLANK) || @source.scan(OTHER)
|
|
331
|
+
buffer_matched
|
|
332
|
+
move_by_matched
|
|
333
|
+
else
|
|
334
|
+
syntax_error!("Unexpected input '#{@source.peek(1)}' while reading public comment")
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Read tokens in :block_open state
|
|
339
|
+
def next_in_block_open
|
|
340
|
+
if @source.scan(BLOCK_NAME_CHARS)
|
|
341
|
+
block_name = @source.matched
|
|
342
|
+
update_current_token(block_name)
|
|
343
|
+
move_by_matched
|
|
344
|
+
clear_braces
|
|
345
|
+
transition_to(:block_open_content)
|
|
346
|
+
else
|
|
347
|
+
syntax_error!("Exptected valid block name")
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# Read block expression until closing brace
|
|
352
|
+
def next_in_block_open_content
|
|
353
|
+
if @source.scan(BRACE_OPEN)
|
|
354
|
+
@braces << "{"
|
|
355
|
+
buffer_matched
|
|
356
|
+
move_by_matched
|
|
357
|
+
elsif @source.scan(BRACE_CLOSE)
|
|
358
|
+
if @braces.empty?
|
|
359
|
+
block_expression = consume_buffer
|
|
360
|
+
update_current_token(nil, expression: block_expression.strip)
|
|
361
|
+
move_by_matched
|
|
362
|
+
transition_to(:initial)
|
|
363
|
+
else
|
|
364
|
+
@braces.pop
|
|
365
|
+
buffer_matched
|
|
366
|
+
move_by_matched
|
|
367
|
+
end
|
|
368
|
+
elsif @source.scan(BLANK) || @source.scan(OTHER)
|
|
369
|
+
buffer_matched
|
|
370
|
+
move_by_matched
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Read tokens in :block_close state
|
|
375
|
+
def next_in_block_close
|
|
376
|
+
if @source.scan(BLOCK_NAME_CHARS)
|
|
377
|
+
block_name = @source.matched
|
|
378
|
+
update_current_token(block_name)
|
|
379
|
+
move_by_matched
|
|
380
|
+
transition_to(:block_close_content)
|
|
381
|
+
else
|
|
382
|
+
syntax_error!("Expected valid block name")
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
# Read block close name until closing brace
|
|
387
|
+
def next_in_block_close_content
|
|
388
|
+
if @source.scan(/\s/)
|
|
389
|
+
move_by_matched
|
|
390
|
+
elsif @source.scan(BRACE_CLOSE)
|
|
391
|
+
move_by_matched
|
|
392
|
+
transition_to(:initial)
|
|
393
|
+
else
|
|
394
|
+
syntax_error!("Expected closing brace '}'")
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# Read tokens in :printing_expression state
|
|
399
|
+
def next_in_printing_expression
|
|
400
|
+
if @source.scan(PRINTING_EXPRESSION_END)
|
|
401
|
+
expression = consume_buffer
|
|
402
|
+
update_current_token(expression)
|
|
403
|
+
move_by_matched
|
|
404
|
+
transition_to(:initial)
|
|
405
|
+
elsif @source.scan(BRACE_OPEN)
|
|
406
|
+
@braces << "{"
|
|
407
|
+
buffer_matched
|
|
408
|
+
move_by_matched
|
|
409
|
+
elsif @source.scan(BRACE_CLOSE)
|
|
410
|
+
if @braces.any?
|
|
411
|
+
@braces.pop
|
|
412
|
+
buffer_matched
|
|
413
|
+
move_by_matched
|
|
414
|
+
else
|
|
415
|
+
syntax_error!("Unexpected closing brace '}'")
|
|
416
|
+
end
|
|
417
|
+
elsif @source.scan(OTHER) || @source.scan(NEWLINE) || @source.scan(CRLF)
|
|
418
|
+
buffer_matched
|
|
419
|
+
move_by_matched
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
# Read tokens in :control_expression state
|
|
424
|
+
def next_in_control_expression
|
|
425
|
+
if @source.scan(CONTROL_EXPRESSION_END)
|
|
426
|
+
expression = consume_buffer
|
|
427
|
+
update_current_token(expression)
|
|
428
|
+
move_by_matched
|
|
429
|
+
transition_to(:initial)
|
|
430
|
+
elsif @source.scan(BRACE_OPEN)
|
|
431
|
+
@braces << "{"
|
|
432
|
+
buffer_matched
|
|
433
|
+
move_by_matched
|
|
434
|
+
elsif @source.scan(BRACE_CLOSE)
|
|
435
|
+
if @braces.any?
|
|
436
|
+
@braces.pop
|
|
437
|
+
buffer_matched
|
|
438
|
+
move_by_matched
|
|
439
|
+
else
|
|
440
|
+
syntax_error!("Unexpected closing brace '}'")
|
|
441
|
+
end
|
|
442
|
+
elsif @source.scan(OTHER) || @source.scan(NEWLINE) || @source.scan(CRLF)
|
|
443
|
+
buffer_matched
|
|
444
|
+
move_by_matched
|
|
445
|
+
end
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# Read tokens in :verbatim state
|
|
449
|
+
def next_in_verbatim
|
|
450
|
+
if @source.scan(END_TAG_START)
|
|
451
|
+
# store the match up to here in a temporary variable
|
|
452
|
+
tmp = @source.matched
|
|
453
|
+
# then find the next verbatim end tag end and
|
|
454
|
+
# check if the tag name matches the current verbatim tag name
|
|
455
|
+
lookahead = @source.check_until(END_TAG_END_VERBATIM)
|
|
456
|
+
|
|
457
|
+
# if the tag name matches, we have found the end of the verbatim tag
|
|
458
|
+
# and we can add a text token, as well as a tag_close token
|
|
459
|
+
if lookahead[0..-3] == current_token.value
|
|
460
|
+
buffer_to_text_token
|
|
461
|
+
add_token(:tag_close, nil)
|
|
462
|
+
transition_to(:tag_close)
|
|
463
|
+
else
|
|
464
|
+
buffer(tmp)
|
|
465
|
+
move_by(tmp)
|
|
466
|
+
end
|
|
467
|
+
elsif @source.scan(NEWLINE) || @source.scan(CRLF) || @source.scan(BLANK) || @source.scan(OTHER)
|
|
468
|
+
buffer_matched
|
|
469
|
+
move_by_matched
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
# --------------------------------------------------------------
|
|
474
|
+
# Helpers
|
|
475
|
+
|
|
476
|
+
# Terminates the tokenizer.
|
|
477
|
+
def terminate
|
|
478
|
+
@source.terminate
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
# Retrieve the current token
|
|
482
|
+
def current_token
|
|
483
|
+
raise "Invalid tokenizer state: no tokens present" if @tokens.empty?
|
|
484
|
+
|
|
485
|
+
@tokens.last
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
def current_attribute
|
|
489
|
+
raise "Invalid tokenizer state: no attributes present" if @attributes.empty?
|
|
490
|
+
|
|
491
|
+
@attributes.last
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
def add_attribute(name, type, value)
|
|
495
|
+
@attributes << [name, type, value]
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def clear_attributes
|
|
499
|
+
@attributes = []
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
def clear_braces
|
|
503
|
+
@braces = []
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
# Moves the cursor
|
|
507
|
+
def move(line, column)
|
|
508
|
+
@line = line
|
|
509
|
+
@column = column
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
def move_by(str)
|
|
513
|
+
scan = StringScanner.new(str)
|
|
514
|
+
until scan.eos?
|
|
515
|
+
if scan.scan(NEWLINE) || scan.scan(CRLF)
|
|
516
|
+
move(@line + 1, 1)
|
|
517
|
+
elsif scan.scan(OTHER)
|
|
518
|
+
move(@line, @column + scan.matched.size)
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
def move_by_matched
|
|
524
|
+
move_by(@source.matched)
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
# Changes the state
|
|
528
|
+
def transition_to(state)
|
|
529
|
+
@state = state
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
# Create a new token
|
|
533
|
+
def create_token(type, value, meta = {})
|
|
534
|
+
Token.new(type, value, meta.merge(line: @line, column: @column) { |_k, v1, _v2| v1 })
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
# Create a token and add it to the token list
|
|
538
|
+
def add_token(type, value, meta = {})
|
|
539
|
+
@tokens << create_token(type, value, meta)
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
# Update the current token
|
|
543
|
+
def update_current_token(value = nil, meta = {})
|
|
544
|
+
current_token.value = value if value
|
|
545
|
+
current_token.meta.merge!(meta)
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
# Write given str to buffer
|
|
549
|
+
def buffer(str)
|
|
550
|
+
@buffer << str
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
# Read the buffer to a string
|
|
554
|
+
def read_buffer
|
|
555
|
+
@buffer.string.clone
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
# Clear the buffer
|
|
559
|
+
def clear_buffer
|
|
560
|
+
@buffer = StringIO.new
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
# Read the buffer to a string and clear it
|
|
564
|
+
def consume_buffer
|
|
565
|
+
str = read_buffer
|
|
566
|
+
clear_buffer
|
|
567
|
+
str
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
def buffer_matched
|
|
571
|
+
buffer(@source.matched)
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
# Turn the buffer into a text token
|
|
575
|
+
def buffer_to_text_token
|
|
576
|
+
text = consume_buffer
|
|
577
|
+
add_token(:text, text, line: @line, column: @column - text.size) unless text.empty?
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
# Raise a syntax error
|
|
581
|
+
def syntax_error!(message)
|
|
582
|
+
if @raise_errors
|
|
583
|
+
raise ORB::SyntaxError.new("#{message} at line #{@line} and column #{@column} during :#{@state}", @line)
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
@errors << ORB::SyntaxError.new("#{message} at line #{@line} and column #{@column} during :#{@state}", @line)
|
|
587
|
+
|
|
588
|
+
terminate
|
|
589
|
+
end
|
|
590
|
+
end
|
|
591
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ORB
|
|
4
|
+
module Utils
|
|
5
|
+
class ERB
|
|
6
|
+
def self.tokenize(source) # :nodoc:
|
|
7
|
+
require "strscan"
|
|
8
|
+
source = StringScanner.new(source.chomp)
|
|
9
|
+
tokens = []
|
|
10
|
+
|
|
11
|
+
start_re = /<%(?:={1,2}|-|\#|%)?/m
|
|
12
|
+
finish_re = /(?:[-=])?%>/m
|
|
13
|
+
|
|
14
|
+
until source.eos?
|
|
15
|
+
pos = source.pos
|
|
16
|
+
source.scan_until(/(?:#{start_re}|#{finish_re})/)
|
|
17
|
+
len = source.pos - source.matched.bytesize - pos
|
|
18
|
+
|
|
19
|
+
case source.matched
|
|
20
|
+
when start_re
|
|
21
|
+
tokens << [:TEXT, source.string[pos, len]] if len.positive?
|
|
22
|
+
tokens << [:OPEN, source.matched]
|
|
23
|
+
raise NotImplemented unless source.scan(/(.*?)(?=#{finish_re}|\z)/m)
|
|
24
|
+
|
|
25
|
+
tokens << [:CODE, source.matched] unless source.matched.empty?
|
|
26
|
+
tokens << [:CLOSE, source.scan(finish_re)] unless source.eos?
|
|
27
|
+
|
|
28
|
+
when finish_re
|
|
29
|
+
tokens << [:CODE, source.string[pos, len]] if len.positive?
|
|
30
|
+
tokens << [:CLOSE, source.matched]
|
|
31
|
+
else
|
|
32
|
+
raise NotImplemented, source.matched
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
tokens
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/orb/version.rb
ADDED
data/lib/orb.rb
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'temple'
|
|
4
|
+
require 'cgi/util'
|
|
5
|
+
require "active_support/dependencies/autoload"
|
|
6
|
+
|
|
7
|
+
module ORB
|
|
8
|
+
extend ActiveSupport::Autoload
|
|
9
|
+
|
|
10
|
+
autoload :Error, 'orb/errors'
|
|
11
|
+
autoload :SyntaxError, 'orb/errors'
|
|
12
|
+
autoload :ParserError, 'orb/errors'
|
|
13
|
+
autoload :CompilerError, 'orb/errors'
|
|
14
|
+
autoload :Token
|
|
15
|
+
autoload :Tokenizer
|
|
16
|
+
autoload :RenderContext
|
|
17
|
+
autoload :AST
|
|
18
|
+
autoload :Parser
|
|
19
|
+
autoload :Document
|
|
20
|
+
autoload :Template
|
|
21
|
+
autoload :Temple
|
|
22
|
+
autoload :RailsTemplate
|
|
23
|
+
|
|
24
|
+
# Next-gen tokenizer built on top of strscan
|
|
25
|
+
autoload :Tokenizer2
|
|
26
|
+
|
|
27
|
+
# Configure class caching
|
|
28
|
+
singleton_class.send(:attr_accessor, :cache_classes)
|
|
29
|
+
self.cache_classes = true
|
|
30
|
+
|
|
31
|
+
# Configure order of component namespace lookups
|
|
32
|
+
singleton_class.send(:attr_accessor, :namespaces)
|
|
33
|
+
self.namespaces = []
|
|
34
|
+
|
|
35
|
+
def self.lookup_component(name)
|
|
36
|
+
namespaces.each do |namespace|
|
|
37
|
+
klass = "#{namespace}::#{name}"
|
|
38
|
+
return klass if Object.const_defined?(klass)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.html_escape(str)
|
|
45
|
+
CGI.escapeHTML(str.to_s)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Load the Railtie if we are in a Rails environment
|
|
50
|
+
require 'orb/railtie' if defined?(Rails)
|