liquid2 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
- checksums.yaml.gz.sig +0 -0
- data/.rubocop.yml +46 -0
- data/.ruby-version +1 -0
- data/.vscode/settings.json +32 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/LICENSE_SHOPIFY.txt +20 -0
- data/README.md +219 -0
- data/Rakefile +23 -0
- data/Steepfile +26 -0
- data/lib/liquid2/context.rb +297 -0
- data/lib/liquid2/environment.rb +287 -0
- data/lib/liquid2/errors.rb +79 -0
- data/lib/liquid2/expression.rb +20 -0
- data/lib/liquid2/expressions/arguments.rb +25 -0
- data/lib/liquid2/expressions/array.rb +20 -0
- data/lib/liquid2/expressions/blank.rb +41 -0
- data/lib/liquid2/expressions/boolean.rb +20 -0
- data/lib/liquid2/expressions/filtered.rb +136 -0
- data/lib/liquid2/expressions/identifier.rb +43 -0
- data/lib/liquid2/expressions/lambda.rb +53 -0
- data/lib/liquid2/expressions/logical.rb +71 -0
- data/lib/liquid2/expressions/loop.rb +79 -0
- data/lib/liquid2/expressions/path.rb +33 -0
- data/lib/liquid2/expressions/range.rb +28 -0
- data/lib/liquid2/expressions/relational.rb +119 -0
- data/lib/liquid2/expressions/template_string.rb +20 -0
- data/lib/liquid2/filter.rb +95 -0
- data/lib/liquid2/filters/array.rb +202 -0
- data/lib/liquid2/filters/date.rb +20 -0
- data/lib/liquid2/filters/default.rb +16 -0
- data/lib/liquid2/filters/json.rb +15 -0
- data/lib/liquid2/filters/math.rb +87 -0
- data/lib/liquid2/filters/size.rb +11 -0
- data/lib/liquid2/filters/slice.rb +17 -0
- data/lib/liquid2/filters/sort.rb +96 -0
- data/lib/liquid2/filters/string.rb +204 -0
- data/lib/liquid2/loader.rb +59 -0
- data/lib/liquid2/loaders/file_system_loader.rb +76 -0
- data/lib/liquid2/loaders/mixins.rb +52 -0
- data/lib/liquid2/node.rb +113 -0
- data/lib/liquid2/nodes/comment.rb +18 -0
- data/lib/liquid2/nodes/output.rb +24 -0
- data/lib/liquid2/nodes/tags/assign.rb +35 -0
- data/lib/liquid2/nodes/tags/block_comment.rb +26 -0
- data/lib/liquid2/nodes/tags/capture.rb +40 -0
- data/lib/liquid2/nodes/tags/case.rb +111 -0
- data/lib/liquid2/nodes/tags/cycle.rb +63 -0
- data/lib/liquid2/nodes/tags/decrement.rb +29 -0
- data/lib/liquid2/nodes/tags/doc.rb +24 -0
- data/lib/liquid2/nodes/tags/echo.rb +31 -0
- data/lib/liquid2/nodes/tags/extends.rb +3 -0
- data/lib/liquid2/nodes/tags/for.rb +155 -0
- data/lib/liquid2/nodes/tags/if.rb +84 -0
- data/lib/liquid2/nodes/tags/include.rb +123 -0
- data/lib/liquid2/nodes/tags/increment.rb +29 -0
- data/lib/liquid2/nodes/tags/inline_comment.rb +28 -0
- data/lib/liquid2/nodes/tags/liquid.rb +29 -0
- data/lib/liquid2/nodes/tags/macro.rb +3 -0
- data/lib/liquid2/nodes/tags/raw.rb +30 -0
- data/lib/liquid2/nodes/tags/render.rb +137 -0
- data/lib/liquid2/nodes/tags/tablerow.rb +143 -0
- data/lib/liquid2/nodes/tags/translate.rb +3 -0
- data/lib/liquid2/nodes/tags/unless.rb +23 -0
- data/lib/liquid2/nodes/tags/with.rb +3 -0
- data/lib/liquid2/parser.rb +917 -0
- data/lib/liquid2/scanner.rb +595 -0
- data/lib/liquid2/static_analysis.rb +301 -0
- data/lib/liquid2/tag.rb +22 -0
- data/lib/liquid2/template.rb +182 -0
- data/lib/liquid2/undefined.rb +131 -0
- data/lib/liquid2/utils/cache.rb +80 -0
- data/lib/liquid2/utils/chain_hash.rb +40 -0
- data/lib/liquid2/utils/unescape.rb +119 -0
- data/lib/liquid2/version.rb +5 -0
- data/lib/liquid2.rb +90 -0
- data/performance/benchmark.rb +73 -0
- data/performance/memory_profile.rb +62 -0
- data/performance/profile.rb +71 -0
- data/sig/liquid2.rbs +2348 -0
- data.tar.gz.sig +0 -0
- metadata +164 -0
- metadata.gz.sig +0 -0
@@ -0,0 +1,595 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "utils/unescape"
|
4
|
+
|
5
|
+
module Liquid2
|
6
|
+
# Liquid template source text lexical scanner.
|
7
|
+
#
|
8
|
+
# This is a single pass tokenizer. We support tag and output delimiters inside string
|
9
|
+
# literals, so we must scan expressions as we go.
|
10
|
+
#
|
11
|
+
# We give comment and raw tags special consideration here.
|
12
|
+
class Scanner
|
13
|
+
attr_reader :tokens
|
14
|
+
|
15
|
+
RE_MARKUP_START = /\{[\{%#]/
|
16
|
+
RE_WHITESPACE = /[ \n\r\t]+/
|
17
|
+
RE_LINE_SPACE = /[ \t]+/
|
18
|
+
RE_WORD = /[\u0080-\uFFFFa-zA-Z_][\u0080-\uFFFFa-zA-Z0-9_-]*/
|
19
|
+
RE_INT = /-?\d+(?:[eE]\+?\d+)?/
|
20
|
+
RE_FLOAT = /((?:-?\d+\.\d+(?:[eE][+-]?\d+)?)|(-?\d+[eE]-\d+))/
|
21
|
+
RE_PUNCTUATION = /\?|\[|\]|\|{1,2}|\.{1,2}|,|:|\(|\)|[<>=!]+/
|
22
|
+
RE_SINGLE_QUOTE_STRING_SPECIAL = /[\\'\$]/
|
23
|
+
RE_DOUBLE_QUOTE_STRING_SPECIAL = /[\\"\$]/
|
24
|
+
|
25
|
+
# Keywords and symbols that get their own token kind.
|
26
|
+
TOKEN_MAP = {
|
27
|
+
"true" => :token_true,
|
28
|
+
"false" => :token_false,
|
29
|
+
"nil" => :token_nil,
|
30
|
+
"null" => :token_nil,
|
31
|
+
"and" => :token_and,
|
32
|
+
"or" => :token_or,
|
33
|
+
"not" => :token_not,
|
34
|
+
"in" => :token_in,
|
35
|
+
"contains" => :token_contains,
|
36
|
+
"if" => :token_if,
|
37
|
+
"else" => :token_else,
|
38
|
+
"with" => :token_with,
|
39
|
+
"required" => :token_required,
|
40
|
+
"as" => :token_as,
|
41
|
+
"for" => :token_for,
|
42
|
+
"blank" => :token_blank,
|
43
|
+
"empty" => :token_empty,
|
44
|
+
"?" => :token_question,
|
45
|
+
"[" => :token_lbracket,
|
46
|
+
"]" => :token_rbracket,
|
47
|
+
"|" => :token_pipe,
|
48
|
+
"||" => :token_double_pipe,
|
49
|
+
"." => :token_dot,
|
50
|
+
".." => :token_double_dot,
|
51
|
+
"," => :token_comma,
|
52
|
+
":" => :token_colon,
|
53
|
+
"(" => :token_lparen,
|
54
|
+
")" => :token_rparen,
|
55
|
+
"=" => :token_assign,
|
56
|
+
"<" => :token_lt,
|
57
|
+
"<=" => :token_le,
|
58
|
+
"<>" => :token_lg,
|
59
|
+
">" => :token_gt,
|
60
|
+
">=" => :token_ge,
|
61
|
+
"==" => :token_eq,
|
62
|
+
"!=" => :token_ne,
|
63
|
+
"=>" => :token_arrow
|
64
|
+
}.freeze
|
65
|
+
|
66
|
+
def self.tokenize(source, scanner)
|
67
|
+
lexer = new(source, scanner)
|
68
|
+
lexer.run
|
69
|
+
lexer.tokens
|
70
|
+
end
|
71
|
+
|
72
|
+
# @param source [String]
|
73
|
+
# @param scanner [StringScanner]
|
74
|
+
def initialize(source, scanner)
|
75
|
+
@source = source
|
76
|
+
@scanner = scanner
|
77
|
+
@scanner.string = @source
|
78
|
+
|
79
|
+
# A pointer to the start of the current token.
|
80
|
+
@start = 0
|
81
|
+
|
82
|
+
# Tokens are arrays of (kind, value, start index)
|
83
|
+
@tokens = [] # : Array[[Symbol, String?, Integer]]
|
84
|
+
end
|
85
|
+
|
86
|
+
def run
|
87
|
+
state = :lex_markup
|
88
|
+
state = send(state) until state.nil?
|
89
|
+
end
|
90
|
+
|
91
|
+
protected
|
92
|
+
|
93
|
+
# @param kind [Symbol]
|
94
|
+
# @param value [String?]
|
95
|
+
# @return void
|
96
|
+
def emit(kind, value)
|
97
|
+
# TODO: For debugging. Comment this out when benchmarking.
|
98
|
+
raise "empty span (#{kind}, #{value})" if @scanner.pos == @start
|
99
|
+
|
100
|
+
@tokens << [kind, value, @start]
|
101
|
+
@start = @scanner.pos
|
102
|
+
end
|
103
|
+
|
104
|
+
def skip_trivia
|
105
|
+
# TODO: For debugging. Comment this out when benchmarking.
|
106
|
+
raise "must emit before skipping trivia" if @scanner.pos != @start
|
107
|
+
|
108
|
+
@start = @scanner.pos if @scanner.skip(RE_WHITESPACE)
|
109
|
+
end
|
110
|
+
|
111
|
+
def skip_line_trivia
|
112
|
+
# TODO: For debugging. Comment this out when benchmarking.
|
113
|
+
raise "must emit before skipping line trivia" if @scanner.pos != @start
|
114
|
+
|
115
|
+
@start = @scanner.pos if @scanner.skip(RE_LINE_SPACE)
|
116
|
+
end
|
117
|
+
|
118
|
+
def accept_whitespace_control
|
119
|
+
# TODO: For debugging. Comment this out when benchmarking.
|
120
|
+
raise "must emit before accepting whitespace control" if @scanner.pos != @start
|
121
|
+
|
122
|
+
ch = @scanner.peek(1)
|
123
|
+
|
124
|
+
case ch
|
125
|
+
when "-", "+", "~"
|
126
|
+
@scanner.pos += 1
|
127
|
+
@tokens << [:token_whitespace_control, ch, @start]
|
128
|
+
@start = @scanner.pos
|
129
|
+
true
|
130
|
+
else
|
131
|
+
false
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def lex_markup
|
136
|
+
case @scanner.scan(RE_MARKUP_START)
|
137
|
+
when "{#"
|
138
|
+
:lex_comment
|
139
|
+
when "{{"
|
140
|
+
@tokens << [:token_output_start, nil, @start]
|
141
|
+
@start = @scanner.pos
|
142
|
+
accept_whitespace_control
|
143
|
+
skip_trivia
|
144
|
+
:lex_expression
|
145
|
+
when "{%"
|
146
|
+
@tokens << [:token_tag_start, nil, @start]
|
147
|
+
@start = @scanner.pos
|
148
|
+
accept_whitespace_control
|
149
|
+
skip_trivia
|
150
|
+
|
151
|
+
if (tag_name = @scanner.scan(/(?:[a-z][a-z_0-9]*|#)/))
|
152
|
+
@tokens << [:token_tag_name, tag_name, @start]
|
153
|
+
@start = @scanner.pos
|
154
|
+
|
155
|
+
case tag_name
|
156
|
+
when "#"
|
157
|
+
# Don't skip trivia for inline comments.
|
158
|
+
# This is for consistency with other types of comments that include
|
159
|
+
# leading whitespace.
|
160
|
+
:lex_inside_inline_comment
|
161
|
+
when "comment"
|
162
|
+
skip_trivia
|
163
|
+
:lex_block_comment
|
164
|
+
when "doc"
|
165
|
+
skip_trivia
|
166
|
+
:lex_doc
|
167
|
+
when "raw"
|
168
|
+
skip_trivia
|
169
|
+
:lex_raw
|
170
|
+
when "liquid"
|
171
|
+
skip_trivia
|
172
|
+
:lex_line_statements
|
173
|
+
else
|
174
|
+
skip_trivia
|
175
|
+
:lex_expression
|
176
|
+
end
|
177
|
+
else
|
178
|
+
# Missing or malformed tag name
|
179
|
+
# Try to parse expr anyway
|
180
|
+
:lex_expression
|
181
|
+
end
|
182
|
+
else
|
183
|
+
if @scanner.skip_until(/\{[\{%#]/)
|
184
|
+
@scanner.pos -= 2
|
185
|
+
@tokens << [:token_other, @source.byteslice(@start...@scanner.pos), @start]
|
186
|
+
@start = @scanner.pos
|
187
|
+
:lex_markup
|
188
|
+
else
|
189
|
+
@scanner.terminate
|
190
|
+
if @start != @scanner.pos
|
191
|
+
@tokens << [:token_other, @source.byteslice(@start...@scanner.pos), @start]
|
192
|
+
@start = @scanner.pos
|
193
|
+
end
|
194
|
+
nil
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
def lex_expression
|
200
|
+
# TODO: For debugging. Comment this out when benchmarking.
|
201
|
+
raise "must emit before accepting an expression token" if @scanner.pos != @start
|
202
|
+
|
203
|
+
loop do
|
204
|
+
skip_trivia
|
205
|
+
|
206
|
+
case @scanner.get_byte
|
207
|
+
when "'"
|
208
|
+
@start = @scanner.pos
|
209
|
+
scan_string("'", :token_single_quote_string, RE_SINGLE_QUOTE_STRING_SPECIAL)
|
210
|
+
when "\""
|
211
|
+
@start = @scanner.pos
|
212
|
+
scan_string("\"", :token_double_quote_string, RE_DOUBLE_QUOTE_STRING_SPECIAL)
|
213
|
+
when nil
|
214
|
+
# End of scanner. Unclosed expression or string literal.
|
215
|
+
break
|
216
|
+
else
|
217
|
+
@scanner.pos -= 1
|
218
|
+
if (value = @scanner.scan(RE_FLOAT))
|
219
|
+
@tokens << [:token_float, value, @start]
|
220
|
+
@start = @scanner.pos
|
221
|
+
elsif (value = @scanner.scan(RE_INT))
|
222
|
+
@tokens << [:token_int, value, @start]
|
223
|
+
@start = @scanner.pos
|
224
|
+
elsif (value = @scanner.scan(RE_PUNCTUATION))
|
225
|
+
@tokens << [TOKEN_MAP[value] || :token_unknown, value, @start]
|
226
|
+
@start = @scanner.pos
|
227
|
+
elsif (value = @scanner.scan(RE_WORD))
|
228
|
+
@tokens << [TOKEN_MAP[value] || :token_word, value, @start]
|
229
|
+
@start = @scanner.pos
|
230
|
+
else
|
231
|
+
break
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
accept_whitespace_control
|
237
|
+
|
238
|
+
# Miro benchmarks show no performance gain using scan_byte and peek_byte over scan here.
|
239
|
+
case @scanner.scan(/[\}%]\}/)
|
240
|
+
when "}}"
|
241
|
+
@tokens << [:token_output_end, nil, @start]
|
242
|
+
when "%}"
|
243
|
+
@tokens << [:token_tag_end, nil, @start]
|
244
|
+
else
|
245
|
+
# Unexpected token
|
246
|
+
return nil if @scanner.eos?
|
247
|
+
|
248
|
+
if (ch = @scanner.scan(/[\}%]/))
|
249
|
+
raise LiquidSyntaxError.new("missing \"}\" or \"%\" detected",
|
250
|
+
[:token_unknown, ch, @start])
|
251
|
+
end
|
252
|
+
|
253
|
+
@tokens << [:token_unknown, @scanner.getch, @start]
|
254
|
+
end
|
255
|
+
|
256
|
+
@start = @scanner.pos
|
257
|
+
:lex_markup
|
258
|
+
end
|
259
|
+
|
260
|
+
def lex_comment
|
261
|
+
hash_count = 1
|
262
|
+
|
263
|
+
if (hashes = @scanner.scan(/#+/))
|
264
|
+
hash_count += hashes.length
|
265
|
+
end
|
266
|
+
|
267
|
+
@tokens << [:token_comment_start, @source.byteslice(@start...@scanner.pos), @start]
|
268
|
+
@start = @scanner.pos
|
269
|
+
|
270
|
+
wc = accept_whitespace_control
|
271
|
+
|
272
|
+
if @scanner.skip_until(/([+\-~]?)(\#{#{hash_count}}\})/)
|
273
|
+
@scanner.pos -= @scanner[0]&.length || 0
|
274
|
+
@tokens << [:token_comment, @source.byteslice(@start...@scanner.pos), @start]
|
275
|
+
@start = @scanner.pos
|
276
|
+
|
277
|
+
if (ch = @scanner[1]) && !ch.empty?
|
278
|
+
@tokens << [:token_whitespace_control, ch, @start]
|
279
|
+
@start = @scanner.pos += 1
|
280
|
+
end
|
281
|
+
|
282
|
+
if (end_comment = @scanner[2])
|
283
|
+
@scanner.pos += end_comment.length
|
284
|
+
@tokens << [:token_comment_end, @source.byteslice(@start...@scanner.pos), @start]
|
285
|
+
@start = @scanner.pos
|
286
|
+
end
|
287
|
+
else
|
288
|
+
# Fix the last one or two emitted tokens. They are not the start of a comment.
|
289
|
+
@tokens.pop if wc
|
290
|
+
@tokens.pop
|
291
|
+
start = (@tokens.pop || raise).last
|
292
|
+
@tokens << [:token_other, @source.byteslice(start...@scanner.pos), start]
|
293
|
+
end
|
294
|
+
|
295
|
+
:lex_markup
|
296
|
+
end
|
297
|
+
|
298
|
+
def lex_inside_inline_comment
|
299
|
+
if @scanner.skip_until(/([+\-~])?%\}/)
|
300
|
+
@scanner.pos -= @scanner.captures&.first.nil? ? 2 : 3
|
301
|
+
@tokens << [:token_comment, @source.byteslice(@start...@scanner.pos), @start]
|
302
|
+
@start = @scanner.pos
|
303
|
+
end
|
304
|
+
|
305
|
+
accept_whitespace_control
|
306
|
+
|
307
|
+
case @scanner.scan(/[\}%]\}/)
|
308
|
+
when "}}"
|
309
|
+
@tokens << [:token_output_end, nil, @start]
|
310
|
+
when "%}"
|
311
|
+
@tokens << [:token_tag_end, nil, @start]
|
312
|
+
else
|
313
|
+
# Unexpected token
|
314
|
+
return nil if @scanner.eos?
|
315
|
+
|
316
|
+
@tokens << [:token_unknown, @scanner.getch, @start]
|
317
|
+
end
|
318
|
+
|
319
|
+
@start = @scanner.pos
|
320
|
+
:lex_markup
|
321
|
+
end
|
322
|
+
|
323
|
+
def lex_raw
|
324
|
+
skip_trivia
|
325
|
+
accept_whitespace_control
|
326
|
+
|
327
|
+
case @scanner.scan(/[\}%]\}/)
|
328
|
+
when "}}"
|
329
|
+
@tokens << [:token_output_end, nil, @start]
|
330
|
+
@start = @scanner.pos
|
331
|
+
when "%}"
|
332
|
+
@tokens << [:token_tag_end, nil, @start]
|
333
|
+
@start = @scanner.pos
|
334
|
+
end
|
335
|
+
|
336
|
+
if @scanner.skip_until(/(\{%[+\-~]?\s*endraw\s*[+\-~]?%\})/)
|
337
|
+
@scanner.pos -= @scanner.captures&.first&.length || raise
|
338
|
+
@tokens << [:token_raw, @source.byteslice(@start...@scanner.pos), @start]
|
339
|
+
@start = @scanner.pos
|
340
|
+
end
|
341
|
+
|
342
|
+
:lex_markup
|
343
|
+
end
|
344
|
+
|
345
|
+
def lex_block_comment
|
346
|
+
skip_trivia
|
347
|
+
accept_whitespace_control
|
348
|
+
|
349
|
+
case @scanner.scan(/[\}%]\}/)
|
350
|
+
when "}}"
|
351
|
+
@tokens << [:token_output_end, nil, @start]
|
352
|
+
@start = @scanner.pos
|
353
|
+
when "%}"
|
354
|
+
@tokens << [:token_tag_end, nil, @start]
|
355
|
+
@start = @scanner.pos
|
356
|
+
end
|
357
|
+
|
358
|
+
comment_depth = 1
|
359
|
+
raw_depth = 0
|
360
|
+
|
361
|
+
loop do
|
362
|
+
unless @scanner.skip_until(/(\{%[+\-~]?\s*(comment|raw|endcomment|endraw)\s*[+\-~]?%\})/)
|
363
|
+
break
|
364
|
+
end
|
365
|
+
|
366
|
+
tag_name = @scanner.captures&.last || raise
|
367
|
+
|
368
|
+
case tag_name
|
369
|
+
when "comment"
|
370
|
+
comment_depth += 1
|
371
|
+
when "raw"
|
372
|
+
raw_depth += 1
|
373
|
+
when "endraw"
|
374
|
+
raw_depth -= 1 if raw_depth.positive?
|
375
|
+
when "endcomment"
|
376
|
+
next if raw_depth.positive?
|
377
|
+
|
378
|
+
comment_depth -= 1
|
379
|
+
next if comment_depth.positive?
|
380
|
+
|
381
|
+
@scanner.pos -= @scanner.captures&.first&.length || raise
|
382
|
+
@tokens << [:token_comment, @source.byteslice(@start...@scanner.pos), @start]
|
383
|
+
@start = @scanner.pos
|
384
|
+
break
|
385
|
+
else
|
386
|
+
raise "unreachable"
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
:lex_markup
|
391
|
+
end
|
392
|
+
|
393
|
+
def lex_doc
|
394
|
+
skip_trivia
|
395
|
+
accept_whitespace_control
|
396
|
+
|
397
|
+
case @scanner.scan(/[\}%]\}/)
|
398
|
+
when "}}"
|
399
|
+
@tokens << [:token_output_end, nil, @start]
|
400
|
+
@start = @scanner.pos
|
401
|
+
when "%}"
|
402
|
+
@tokens << [:token_tag_end, nil, @start]
|
403
|
+
@start = @scanner.pos
|
404
|
+
end
|
405
|
+
|
406
|
+
if @scanner.skip_until(/(\{%[+\-~]?\s*enddoc\s*[+\-~]?%\})/)
|
407
|
+
@scanner.pos -= @scanner.captures&.first&.length || raise
|
408
|
+
@tokens << [:token_doc, @source.byteslice(@start...@scanner.pos), @start]
|
409
|
+
@start = @scanner.pos
|
410
|
+
end
|
411
|
+
|
412
|
+
:lex_markup
|
413
|
+
end
|
414
|
+
|
415
|
+
def lex_line_statements
|
416
|
+
# TODO: For debugging. Comment this out when benchmarking.
|
417
|
+
raise "must emit before accepting an expression token" if @scanner.pos != @start
|
418
|
+
|
419
|
+
skip_trivia # Leading newlines are OK
|
420
|
+
|
421
|
+
if (tag_name = @scanner.scan(/(?:[a-z][a-z_0-9]*|#)/))
|
422
|
+
@tokens << [:token_tag_start, nil, @start]
|
423
|
+
@tokens << [:token_tag_name, tag_name, @start]
|
424
|
+
@start = @scanner.pos
|
425
|
+
|
426
|
+
if tag_name == "#" && @scanner.scan_until(/([\r\n]+|-?%\})/)
|
427
|
+
@scanner.pos -= @scanner.captures&.first&.length || raise
|
428
|
+
@tokens << [:token_comment, @source.byteslice(@start...@scanner.pos), @start]
|
429
|
+
@start = @scanner.pos
|
430
|
+
@tokens << [:token_tag_end, nil, @start]
|
431
|
+
:lex_line_statements
|
432
|
+
|
433
|
+
elsif tag_name == "comment" && @scanner.scan_until(/(endcomment)/)
|
434
|
+
@tokens << [:token_tag_end, nil, @start]
|
435
|
+
@scanner.pos -= @scanner.captures&.first&.length || raise
|
436
|
+
@tokens << [:token_comment, @source.byteslice(@start...@scanner.pos), @start]
|
437
|
+
@start = @scanner.pos
|
438
|
+
:lex_line_statements
|
439
|
+
else
|
440
|
+
:lex_inside_line_statement
|
441
|
+
end
|
442
|
+
else
|
443
|
+
accept_whitespace_control
|
444
|
+
case @scanner.scan(/[\}%]\}/)
|
445
|
+
when "}}"
|
446
|
+
@tokens << [:token_output_end, nil, @start]
|
447
|
+
@start = @scanner.pos
|
448
|
+
when "%}"
|
449
|
+
@tokens << [:token_tag_end, nil, @start]
|
450
|
+
@start = @scanner.pos
|
451
|
+
end
|
452
|
+
|
453
|
+
:lex_markup
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
def lex_inside_line_statement
|
458
|
+
loop do
|
459
|
+
skip_line_trivia
|
460
|
+
|
461
|
+
case @scanner.get_byte
|
462
|
+
when "'"
|
463
|
+
@start = @scanner.pos
|
464
|
+
scan_string("'", :token_single_quote_string, RE_SINGLE_QUOTE_STRING_SPECIAL)
|
465
|
+
when "\""
|
466
|
+
@start = @scanner.pos
|
467
|
+
scan_string("\"", :token_double_quote_string, RE_DOUBLE_QUOTE_STRING_SPECIAL)
|
468
|
+
when nil
|
469
|
+
# End of scanner. Unclosed expression or string literal.
|
470
|
+
break
|
471
|
+
|
472
|
+
else
|
473
|
+
@scanner.pos -= 1
|
474
|
+
if (value = @scanner.scan(RE_FLOAT))
|
475
|
+
@tokens << [:token_float, value, @start]
|
476
|
+
@start = @scanner.pos
|
477
|
+
elsif (value = @scanner.scan(RE_INT))
|
478
|
+
@tokens << [:token_int, value, @start]
|
479
|
+
@start = @scanner.pos
|
480
|
+
elsif (value = @scanner.scan(RE_PUNCTUATION))
|
481
|
+
@tokens << [TOKEN_MAP[value] || raise, nil, @start]
|
482
|
+
@start = @scanner.pos
|
483
|
+
elsif (value = @scanner.scan(RE_WORD))
|
484
|
+
@tokens << [TOKEN_MAP[value] || :token_word, value, @start]
|
485
|
+
@start = @scanner.pos
|
486
|
+
elsif @scanner.scan(/(\r?\n)+/)
|
487
|
+
# End of the line statement
|
488
|
+
@tokens << [:token_tag_end, nil, @start]
|
489
|
+
@start = @scanner.pos
|
490
|
+
return :lex_line_statements
|
491
|
+
else
|
492
|
+
# End of the line statement and enclosing `liquid` tag.
|
493
|
+
@tokens << [:token_tag_end, nil, @start]
|
494
|
+
accept_whitespace_control
|
495
|
+
case @scanner.scan(/[\}%]\}/)
|
496
|
+
when "}}"
|
497
|
+
@tokens << [:token_output_end, nil, @start]
|
498
|
+
@start = @scanner.pos
|
499
|
+
when "%}"
|
500
|
+
@tokens << [:token_tag_end, nil, @start]
|
501
|
+
@start = @scanner.pos
|
502
|
+
end
|
503
|
+
|
504
|
+
return :lex_markup
|
505
|
+
end
|
506
|
+
end
|
507
|
+
end
|
508
|
+
end
|
509
|
+
|
510
|
+
# Scan a string literal surrounded by single quotes.
|
511
|
+
# Assumes the opening quote has already been consumed and emitted.
|
512
|
+
def scan_string(quote, symbol, pattern)
|
513
|
+
start_of_string = @start - 1
|
514
|
+
needs_unescaping = false
|
515
|
+
|
516
|
+
loop do
|
517
|
+
@scanner.pos -= 1 if @scanner.skip_until(pattern)
|
518
|
+
case @scanner.get_byte
|
519
|
+
when quote
|
520
|
+
# @type var token: [Symbol, String, Integer]
|
521
|
+
token = [symbol, @source.byteslice(@start...@scanner.pos - 1) || raise, @start]
|
522
|
+
token[1] = Liquid2.unescape_string(token[1], quote, token) if needs_unescaping
|
523
|
+
@tokens << token
|
524
|
+
@start = @scanner.pos
|
525
|
+
needs_unescaping = false
|
526
|
+
return
|
527
|
+
when "\\"
|
528
|
+
# An escape sequence. Move past the next character.
|
529
|
+
@scanner.get_byte
|
530
|
+
needs_unescaping = true
|
531
|
+
when "$"
|
532
|
+
next unless @scanner.peek(1) == "{"
|
533
|
+
|
534
|
+
# The start of a `${` expression.
|
535
|
+
# Emit what we have so far. This could be empty if the template string
|
536
|
+
# starts with `${`.
|
537
|
+
# @type var token: [Symbol, String, Integer]
|
538
|
+
token = [symbol, @source.byteslice(@start...@scanner.pos - 1) || raise, @start]
|
539
|
+
token[1] = Liquid2.unescape_string(token[1], quote, token) if needs_unescaping
|
540
|
+
@tokens << token
|
541
|
+
|
542
|
+
@start = @scanner.pos
|
543
|
+
needs_unescaping = false
|
544
|
+
|
545
|
+
# Emit and move past `${`
|
546
|
+
@tokens << [:token_string_interpol_start, nil, @start]
|
547
|
+
@scanner.pos += 1
|
548
|
+
@start = @scanner.pos
|
549
|
+
|
550
|
+
loop do
|
551
|
+
skip_trivia
|
552
|
+
|
553
|
+
case @scanner.get_byte
|
554
|
+
when "'"
|
555
|
+
@start = @scanner.pos
|
556
|
+
scan_string("'", :token_single_quote_string, RE_SINGLE_QUOTE_STRING_SPECIAL)
|
557
|
+
when "\""
|
558
|
+
@start = @scanner.pos
|
559
|
+
scan_string("\"", :token_double_quote_string, RE_DOUBLE_QUOTE_STRING_SPECIAL)
|
560
|
+
when "}"
|
561
|
+
@tokens << [:token_string_interpol_end, nil, @start]
|
562
|
+
@start = @scanner.pos
|
563
|
+
break
|
564
|
+
when nil
|
565
|
+
# End of scanner. Unclosed expression or string literal.
|
566
|
+
raise LiquidSyntaxError.new("unclosed string literal or template string expression",
|
567
|
+
[symbol, nil, start_of_string])
|
568
|
+
else
|
569
|
+
@scanner.pos -= 1
|
570
|
+
if (value = @scanner.scan(RE_FLOAT))
|
571
|
+
@tokens << [:token_float, value, @start]
|
572
|
+
@start = @scanner.pos
|
573
|
+
elsif (value = @scanner.scan(RE_INT))
|
574
|
+
@tokens << [:token_int, value, @start]
|
575
|
+
@start = @scanner.pos
|
576
|
+
elsif (value = @scanner.scan(RE_PUNCTUATION))
|
577
|
+
@tokens << [TOKEN_MAP[value] || raise, nil, @start]
|
578
|
+
@start = @scanner.pos
|
579
|
+
elsif (value = @scanner.scan(RE_WORD))
|
580
|
+
@tokens << [TOKEN_MAP[value] || :token_word, value, @start]
|
581
|
+
@start = @scanner.pos
|
582
|
+
else
|
583
|
+
break
|
584
|
+
end
|
585
|
+
end
|
586
|
+
end
|
587
|
+
when nil
|
588
|
+
# End of scanner. Unclosed string literal.
|
589
|
+
raise LiquidSyntaxError.new("unclosed string literal or template string expression",
|
590
|
+
[symbol, nil, start_of_string])
|
591
|
+
end
|
592
|
+
end
|
593
|
+
end
|
594
|
+
end
|
595
|
+
end
|