luoma 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.
Files changed (152) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +2 -0
  3. data/.rdoc_options +16 -0
  4. data/CHANGELOG.md +5 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +68 -0
  7. data/Rakefile +69 -0
  8. data/Steepfile +41 -0
  9. data/certs/jgrp.pem +27 -0
  10. data/docs/configuration.md +180 -0
  11. data/docs/custom_filters.md +3 -0
  12. data/docs/custom_tags.md +3 -0
  13. data/docs/expressions.md +3 -0
  14. data/docs/extension_types.md +41 -0
  15. data/docs/filter_reference.md +1702 -0
  16. data/docs/index.md +73 -0
  17. data/docs/luoma_for_template_authors.md +3 -0
  18. data/docs/markdown.md +111 -0
  19. data/docs/predicate_reference.md +3 -0
  20. data/docs/static_analysis.md +3 -0
  21. data/docs/tag_reference.md +352 -0
  22. data/docs/template_loaders.md +177 -0
  23. data/docs/undefined_variables.md +3 -0
  24. data/docs-requirements.txt +11 -0
  25. data/docs_/cycle.md +17 -0
  26. data/docs_/font_rendering_example.md +157 -0
  27. data/docs_/header_block_example.md +51 -0
  28. data/docs_/increment_and_decrement.md +49 -0
  29. data/docs_/logo_style_example.md +40 -0
  30. data/docs_/migration.md +11 -0
  31. data/docs_/notes.md +19 -0
  32. data/lib/luoma/cache.rb +80 -0
  33. data/lib/luoma/chain_hash.rb +54 -0
  34. data/lib/luoma/context.rb +227 -0
  35. data/lib/luoma/drop.rb +84 -0
  36. data/lib/luoma/drops/block.rb +33 -0
  37. data/lib/luoma/drops/expression.rb +29 -0
  38. data/lib/luoma/drops/html_safe.rb +36 -0
  39. data/lib/luoma/drops/range.rb +69 -0
  40. data/lib/luoma/drops/undefined.rb +119 -0
  41. data/lib/luoma/environment.rb +479 -0
  42. data/lib/luoma/errors.rb +79 -0
  43. data/lib/luoma/escape.rb +35 -0
  44. data/lib/luoma/expression.rb +1271 -0
  45. data/lib/luoma/filter.rb +97 -0
  46. data/lib/luoma/filters/array.rb +372 -0
  47. data/lib/luoma/filters/date.rb +19 -0
  48. data/lib/luoma/filters/default.rb +16 -0
  49. data/lib/luoma/filters/json.rb +14 -0
  50. data/lib/luoma/filters/math.rb +84 -0
  51. data/lib/luoma/filters/size.rb +13 -0
  52. data/lib/luoma/filters/slice.rb +52 -0
  53. data/lib/luoma/filters/sort.rb +113 -0
  54. data/lib/luoma/filters/string.rb +186 -0
  55. data/lib/luoma/fnv.rb +15 -0
  56. data/lib/luoma/lexer.rb +106 -0
  57. data/lib/luoma/lexer_legacy.rb +396 -0
  58. data/lib/luoma/lexer_unified.rb +279 -0
  59. data/lib/luoma/loader.rb +50 -0
  60. data/lib/luoma/loaders/choice_loader.rb +20 -0
  61. data/lib/luoma/loaders/file_system_loader.rb +75 -0
  62. data/lib/luoma/loaders/mixins.rb +52 -0
  63. data/lib/luoma/markup.rb +109 -0
  64. data/lib/luoma/parser.rb +252 -0
  65. data/lib/luoma/parser_unified.rb +979 -0
  66. data/lib/luoma/predicates/blank.rb +19 -0
  67. data/lib/luoma/predicates/defined.rb +10 -0
  68. data/lib/luoma/predicates/empty.rb +15 -0
  69. data/lib/luoma/predicates/type.rb +35 -0
  70. data/lib/luoma/static_analysis.rb +427 -0
  71. data/lib/luoma/tags/assign.rb +63 -0
  72. data/lib/luoma/tags/break.rb +23 -0
  73. data/lib/luoma/tags/capture.rb +43 -0
  74. data/lib/luoma/tags/case.rb +137 -0
  75. data/lib/luoma/tags/comment.rb +17 -0
  76. data/lib/luoma/tags/continue.rb +23 -0
  77. data/lib/luoma/tags/define.rb +42 -0
  78. data/lib/luoma/tags/else.rb +30 -0
  79. data/lib/luoma/tags/for.rb +108 -0
  80. data/lib/luoma/tags/if.rb +109 -0
  81. data/lib/luoma/tags/import.rb +91 -0
  82. data/lib/luoma/tags/include.rb +77 -0
  83. data/lib/luoma/tags/output.rb +20 -0
  84. data/lib/luoma/tags/raw.rb +26 -0
  85. data/lib/luoma/tags/render.rb +92 -0
  86. data/lib/luoma/tags/with.rb +55 -0
  87. data/lib/luoma/template.rb +178 -0
  88. data/lib/luoma/token.rb +84 -0
  89. data/lib/luoma/version.rb +5 -0
  90. data/lib/luoma.rb +75 -0
  91. data/sig/luoma/cache.rbs +44 -0
  92. data/sig/luoma/chain_hash.rbs +26 -0
  93. data/sig/luoma/context.rbs +115 -0
  94. data/sig/luoma/drop.rbs +44 -0
  95. data/sig/luoma/drops/block.rbs +10 -0
  96. data/sig/luoma/drops/expression.rbs +10 -0
  97. data/sig/luoma/drops/html_safe.rbs +14 -0
  98. data/sig/luoma/drops/range.rbs +15 -0
  99. data/sig/luoma/drops/undefined.rbs +27 -0
  100. data/sig/luoma/environment.rbs +212 -0
  101. data/sig/luoma/errors.rbs +67 -0
  102. data/sig/luoma/escape.rbs +15 -0
  103. data/sig/luoma/expression.rbs +514 -0
  104. data/sig/luoma/filter.rbs +66 -0
  105. data/sig/luoma/filters/array.rbs +74 -0
  106. data/sig/luoma/filters/date.rbs +9 -0
  107. data/sig/luoma/filters/default.rbs +8 -0
  108. data/sig/luoma/filters/json.rbs +7 -0
  109. data/sig/luoma/filters/math.rbs +37 -0
  110. data/sig/luoma/filters/size.rbs +6 -0
  111. data/sig/luoma/filters/slice.rbs +10 -0
  112. data/sig/luoma/filters/sort.rbs +17 -0
  113. data/sig/luoma/filters/string.rbs +103 -0
  114. data/sig/luoma/fnv.rbs +3 -0
  115. data/sig/luoma/lexer.rbs +42 -0
  116. data/sig/luoma/lexer_legacy.rbs +81 -0
  117. data/sig/luoma/lexer_unified.rbs +49 -0
  118. data/sig/luoma/loader.rbs +40 -0
  119. data/sig/luoma/loaders/choice_loader.rbs +9 -0
  120. data/sig/luoma/loaders/file_system_loader.rbs +19 -0
  121. data/sig/luoma/loaders/mixins.rbs +16 -0
  122. data/sig/luoma/markup.rbs +68 -0
  123. data/sig/luoma/parser.rbs +105 -0
  124. data/sig/luoma/parser_unified.rbs +150 -0
  125. data/sig/luoma/predicates/blank.rbs +6 -0
  126. data/sig/luoma/predicates/defined.rbs +6 -0
  127. data/sig/luoma/predicates/empty.rbs +6 -0
  128. data/sig/luoma/predicates/type.rbs +21 -0
  129. data/sig/luoma/static_analysis.rbs +141 -0
  130. data/sig/luoma/tags/assign.rbs +12 -0
  131. data/sig/luoma/tags/break.rbs +11 -0
  132. data/sig/luoma/tags/capture.rbs +14 -0
  133. data/sig/luoma/tags/case.rbs +36 -0
  134. data/sig/luoma/tags/comment.rbs +11 -0
  135. data/sig/luoma/tags/continue.rbs +11 -0
  136. data/sig/luoma/tags/define.rbs +16 -0
  137. data/sig/luoma/tags/else.rbs +17 -0
  138. data/sig/luoma/tags/for.rbs +26 -0
  139. data/sig/luoma/tags/if.rbs +45 -0
  140. data/sig/luoma/tags/import.rbs +13 -0
  141. data/sig/luoma/tags/include.rbs +13 -0
  142. data/sig/luoma/tags/output.rbs +13 -0
  143. data/sig/luoma/tags/raw.rbs +11 -0
  144. data/sig/luoma/tags/render.rbs +13 -0
  145. data/sig/luoma/tags/with.rbs +15 -0
  146. data/sig/luoma/template.rbs +117 -0
  147. data/sig/luoma/token.rbs +81 -0
  148. data/sig/luoma.rbs +11 -0
  149. data/zensical.toml +363 -0
  150. data.tar.gz.sig +0 -0
  151. metadata +233 -0
  152. metadata.gz.sig +0 -0
@@ -0,0 +1,979 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Luoma
4
+ class UnifiedParser < Parser
5
+ class Precedence
6
+ LOWEST = 1
7
+ COALESCE = 2
8
+ LOGICAL_OR = 3
9
+ LOGICAL_AND = 4
10
+ LOGICAL_NOT = 5
11
+ COMPARISON = 6
12
+ MEMBERSHIP = 6
13
+ PIPE = 7
14
+ FILTER_ARG = 8
15
+ ADD = 9
16
+ MUL = 10
17
+ NEG = 11
18
+ end
19
+
20
+ PRECEDENCES = {
21
+ token_or_else: Precedence::COALESCE,
22
+ token_or: Precedence::LOGICAL_OR,
23
+ token_and: Precedence::LOGICAL_AND,
24
+ token_not: Precedence::LOGICAL_NOT,
25
+ token_eq: Precedence::COMPARISON,
26
+ token_ne: Precedence::COMPARISON,
27
+ token_lt: Precedence::COMPARISON,
28
+ token_le: Precedence::COMPARISON,
29
+ token_gt: Precedence::COMPARISON,
30
+ token_ge: Precedence::COMPARISON,
31
+ token_in: Precedence::MEMBERSHIP,
32
+ token_contains: Precedence::MEMBERSHIP,
33
+ token_add: Precedence::ADD,
34
+ token_sub: Precedence::ADD,
35
+ token_mul: Precedence::MUL,
36
+ token_div: Precedence::MUL,
37
+ token_mod: Precedence::MUL,
38
+ token_pipe: Precedence::PIPE
39
+ }.freeze #: Hash[t_token_kind, Integer]
40
+
41
+ INFIX_OPERATORS = {
42
+ token_or_else: CoalesceExpression,
43
+ token_or: OrExpression,
44
+ token_and: AndExpression,
45
+ token_eq: EqExpression,
46
+ token_ne: NeExpression,
47
+ token_lt: LtExpression,
48
+ token_le: LeExpression,
49
+ token_gt: GtExpression,
50
+ token_ge: GeExpression,
51
+ token_in: InExpression,
52
+ token_contains: ContainsExpression,
53
+ token_add: AddExpression,
54
+ token_sub: SubExpression,
55
+ token_mul: MulExpression,
56
+ token_div: DivExpression,
57
+ token_mod: ModExpression,
58
+ token_pipe: nil
59
+ }.freeze #: Hash[t_token_kind, untyped]
60
+
61
+ TERMINATE_EXPRESSION = Set[
62
+ :token_wc,
63
+ :token_out_end,
64
+ :token_tag_end,
65
+ :token_text,
66
+ :token_if,
67
+ :token_else,
68
+ :token_rparen,
69
+ :token_eoi,
70
+ :token_interpolation_end,
71
+ :token_comma
72
+ ].freeze #: Set[t_token_kind]
73
+
74
+ TERMINATE_FILTER = Set[
75
+ :token_wc,
76
+ :token_out_end,
77
+ :token_tag_end,
78
+ :token_text,
79
+ :token_if,
80
+ :token_else,
81
+ :token_rparen,
82
+ :token_rbracket,
83
+ :token_rbrace,
84
+ :token_pipe,
85
+ :token_eoi,
86
+ :token_interpolation_end,
87
+ :token_or_else,
88
+ :token_or,
89
+ :token_and,
90
+ :token_eq,
91
+ :token_ne,
92
+ :token_lt,
93
+ :token_le,
94
+ :token_gt,
95
+ :token_ge,
96
+ :token_in,
97
+ :token_contains
98
+ ].freeze #: Set[t_token_kind]
99
+
100
+ PATH_PUNCTUATION = Set[
101
+ :token_dot,
102
+ :token_lbracket
103
+ ].freeze #: Set[t_token_kind]
104
+
105
+ STRING_LITERAL_KINDS = Set[
106
+ :token_single_escaped,
107
+ :token_single_quoted,
108
+ :token_double_escaped,
109
+ :token_double_quoted
110
+ ].freeze #: Set[t_token_kind]
111
+
112
+ PATH_SEGMENT_KINDS = Set[
113
+ :token_ident,
114
+ :token_false,
115
+ :token_true,
116
+ :token_null,
117
+ :token_nil,
118
+ :token_and,
119
+ :token_or,
120
+ :token_orElse,
121
+ :token_not,
122
+ :token_in,
123
+ :token_contains
124
+ ].freeze #: Set[t_token_kind]
125
+
126
+ KEYWORD_ARGUMENT_DELIMITERS = Set[
127
+ :token_colon,
128
+ :token_assign
129
+ ].freeze #: Set[t_token_kind]
130
+
131
+ RE_SLASH_U = /\\u([0-9a-fA-F]{4})/
132
+ RE_ESCAPE_INTERPOLATION = /\\\$\{/
133
+
134
+ #: (?require_commas: bool?) -> Array[Expression | KeywordArgument]
135
+ def parse_arguments(require_commas: nil)
136
+ args = [] #: Array[Expression | KeywordArgument]
137
+
138
+ loop do
139
+ kind_ = kind
140
+ break if TERMINATE_EXPRESSION.include?(kind_)
141
+
142
+ if kind_ == :token_ident && KEYWORD_ARGUMENT_DELIMITERS.include?(peek.first)
143
+ # A named argument
144
+ name = parse_ident
145
+ eat_one_of(KEYWORD_ARGUMENT_DELIMITERS)
146
+ args << KeywordArgument.new(name.token, name, parse_expression)
147
+ else
148
+ args << parse_expression
149
+ end
150
+
151
+ kind_ = kind
152
+ if require_commas && !TERMINATE_EXPRESSION.include?(kind_)
153
+ eat(:token_comma)
154
+ elsif kind_ == :token_comma
155
+ @pos += 1
156
+ end
157
+ end
158
+
159
+ args
160
+ end
161
+
162
+ #: (?stop: Set[String]?) -> t_block
163
+ def parse_block(stop: nil)
164
+ nodes = [] #: t_block
165
+
166
+ loop do
167
+ token = self.next
168
+
169
+ case token.first
170
+ when :token_text
171
+ nodes << @env.trim(
172
+ Luoma.get_token_value(token, @source),
173
+ @whitespace_control_carry,
174
+ peek_whitespace_control
175
+ )
176
+ when :token_out_start
177
+ nodes << parse_output
178
+ when :token_tag_start
179
+ if stop&.include?(peek_tag_name)
180
+ @pos -= 1
181
+ break
182
+ end
183
+
184
+ nodes << parse_tag
185
+ when :token_comment_start
186
+ nodes << parse_comment
187
+ when :token_eoi
188
+ break
189
+ else
190
+ raise TemplateSyntaxError.new(
191
+ "unexpected #{Luoma::TOKEN_KIND_MAP[token.first].inspect}",
192
+ token,
193
+ @source,
194
+ @template_name
195
+ )
196
+ end
197
+ end
198
+
199
+ nodes
200
+ end
201
+
202
+ #: (?precedence: Integer) -> Expression
203
+ def parse_expression(precedence: Precedence::LOWEST)
204
+ expr = parse_primary(precedence: precedence)
205
+ expr = parse_ternary(expr) if kind == :token_if
206
+ expr
207
+ end
208
+
209
+ #: () -> Name
210
+ def parse_ident
211
+ token = eat(:token_ident)
212
+
213
+ if PATH_PUNCTUATION.include?(kind)
214
+ raise TemplateSyntaxError.new(
215
+ "expected an identifier, found a path",
216
+ token,
217
+ @source,
218
+ @template_name
219
+ )
220
+ end
221
+
222
+ Name.new(token, Luoma.get_token_value(token, @source))
223
+ end
224
+
225
+ #: (?require_commas: bool?) -> Array[KeywordArgument]
226
+ def parse_keyword_arguments(require_commas: nil)
227
+ args = [] #: Array[KeywordArgument]
228
+
229
+ loop do
230
+ kind_ = kind
231
+ break if TERMINATE_EXPRESSION.include?(kind_)
232
+
233
+ name = parse_ident
234
+ eat_one_of(KEYWORD_ARGUMENT_DELIMITERS)
235
+ args << KeywordArgument.new(name.token, name, parse_expression)
236
+
237
+ kind_ = kind
238
+ if require_commas && !TERMINATE_EXPRESSION.include?(kind_)
239
+ eat(:token_comma)
240
+ elsif kind_ == :token_comma
241
+ @pos += 1
242
+ end
243
+ end
244
+
245
+ args
246
+ end
247
+
248
+ # Parse an identifier, possibly surrounded by quotes.
249
+ #: () -> Name
250
+ def parse_name
251
+ case kind
252
+ when :token_ident
253
+ parse_ident
254
+ when :token_single_quote, :token_double_quote
255
+ expr = parse_string_literal
256
+
257
+ if expr.segments.length > 1 || !expr.segments.first.is_a?(String)
258
+ raise TemplateSyntaxError.new(
259
+ "expected a string or identifier",
260
+ current,
261
+ @source,
262
+ @template_name
263
+ )
264
+ end
265
+
266
+ Name.new(expr.token, expr.segments[0]) # steep:ignore
267
+ else
268
+ raise TemplateSyntaxError.new(
269
+ "expected a string or identifier",
270
+ current,
271
+ @source,
272
+ @template_name
273
+ )
274
+ end
275
+ end
276
+
277
+ #: (?require_commas: bool?) -> Array[Expression]
278
+ def parse_positional_arguments(require_commas: nil)
279
+ args = [] #: Array[Expression]
280
+
281
+ loop do
282
+ break if TERMINATE_EXPRESSION.include?(kind)
283
+
284
+ args << parse_expression
285
+
286
+ kind_ = kind
287
+ if require_commas && !TERMINATE_EXPRESSION.include?(kind_)
288
+ eat(:token_comma)
289
+ elsif kind_ == :token_comma
290
+ @pos += 1
291
+ end
292
+ end
293
+
294
+ args
295
+ end
296
+
297
+ #: () -> StringLiteral
298
+ def parse_string_literal
299
+ token = self.next # quote
300
+ segments = [] #: Array[String|Expression]
301
+
302
+ loop do
303
+ case kind
304
+ when :token_single_quoted, :token_double_quoted
305
+ segments << Luoma.get_token_value(self.next, @source)
306
+ when :token_single_escaped, :token_double_escaped
307
+ segments << unescape(self.next)
308
+ when :token_interpolation_start
309
+ start_token = self.next
310
+ segments << parse_expression
311
+
312
+ unless kind == :token_interpolation_end
313
+ raise TemplateSyntaxError.new(
314
+ "unclosed template string expression",
315
+ Luoma.span(start_token, current),
316
+ @source,
317
+ @template_name
318
+ )
319
+ end
320
+
321
+ eat(:token_interpolation_end)
322
+ else
323
+ break
324
+ end
325
+ end
326
+
327
+ unless kind == token.first
328
+ raise TemplateSyntaxError.new(
329
+ "unclosed string literal",
330
+ Luoma.span(token, @tokens[@pos - 1] || current),
331
+ @source,
332
+ @template_name
333
+ )
334
+ end
335
+
336
+ StringLiteral.new(token, segments, eat(token.first))
337
+ end
338
+
339
+ protected
340
+
341
+ #: (?precedence: Integer) -> Expression
342
+ def parse_primary(precedence: Precedence::LOWEST)
343
+ left = case kind
344
+ when :token_single_quote, :token_double_quote
345
+ parse_string_literal
346
+ when :token_ident
347
+ peek.first == :token_arrow ? parse_lambda(precedence: precedence) : parse_path
348
+ when :token_lbracket
349
+ parse_array_or_path
350
+ when :token_lbrace
351
+ parse_object_literal
352
+ when :token_lparen
353
+ parse_lambda_range_or_group(precedence: precedence)
354
+ when :token_not
355
+ PATH_PUNCTUATION.include?(peek.first) ? parse_path : parse_prefix
356
+ when :token_add, :token_sub
357
+ parse_prefix
358
+ when :token_true
359
+ PATH_PUNCTUATION.include?(peek.first) ? parse_path : parse_true_literal
360
+ when :token_false
361
+ PATH_PUNCTUATION.include?(peek.first) ? parse_path : parse_false_literal
362
+ when :token_null, :token_nil
363
+ PATH_PUNCTUATION.include?(peek.first) ? parse_path : parse_null_literal
364
+ when :token_int
365
+ parse_int_literal
366
+ when :token_float
367
+ parse_float_literal
368
+ when :token_and, :token_or, :token_orElse, :token_contains, :token_in, :token_if, :token_else
369
+ if PATH_PUNCTUATION.include?(peek.first) || TERMINATE_EXPRESSION.include?(peek.first)
370
+ parse_path
371
+ else
372
+ token = current
373
+ raise TemplateSyntaxError.new(
374
+ "unexpected operator #{Luoma.get_token_value(token, @source).inspect}",
375
+ token,
376
+ @source,
377
+ @template_name
378
+ )
379
+ end
380
+ when :token_out_end, :token_wc, :token_tag_end
381
+ raise TemplateSyntaxError.new(
382
+ "unexpected empty expression",
383
+ current,
384
+ @source,
385
+ @template_name
386
+ )
387
+ else
388
+ token = current
389
+ raise TemplateSyntaxError.new(
390
+ "expected an expression, found #{Luoma::TOKEN_KIND_MAP[token.first].inspect}",
391
+ token,
392
+ @source,
393
+ @template_name
394
+ )
395
+ end
396
+
397
+ loop do
398
+ kind_ = kind
399
+ break if (PRECEDENCES[kind_] || Precedence::LOWEST) < precedence || !INFIX_OPERATORS.include?(kind_)
400
+
401
+ left = parse_infix(left)
402
+ end
403
+
404
+ left
405
+ end
406
+
407
+ #: (Expression) -> Expression
408
+ def parse_ternary(consequence)
409
+ eat(:token_if)
410
+ condition = parse_primary
411
+ alternative = if kind == :token_else
412
+ @pos += 1
413
+ parse_primary
414
+ end
415
+
416
+ TernaryExpression.new(consequence.token, consequence, condition, alternative)
417
+ end
418
+
419
+ #: () -> Markup
420
+ def parse_output
421
+ token = @tokens[@pos - 1]
422
+ skip_whitespace_control
423
+ expr = parse_expression
424
+ carry_whitespace_control
425
+
426
+ unless kind == :token_out_end
427
+ raise TemplateSyntaxError.new(
428
+ "bad expression or missing markup delimiter",
429
+ expr.span,
430
+ @source,
431
+ @template_name
432
+ )
433
+ end
434
+
435
+ eat(:token_out_end)
436
+ OutputStatement.new(token, expr)
437
+ end
438
+
439
+ #: () -> Markup
440
+ def parse_comment
441
+ skip_whitespace_control
442
+ token = eat(:token_comment)
443
+ carry_whitespace_control
444
+ eat(:token_comment_end)
445
+ Comment.new(token, Luoma.get_token_value(token, @source))
446
+ end
447
+
448
+ #: () -> Markup
449
+ def parse_tag
450
+ skip_whitespace_control
451
+ token = eat(:token_tag_name, message: "missing tag name")
452
+ tag_name = Luoma.get_token_value(token, @source)
453
+ tag = @env.tags[tag_name]
454
+
455
+ unless tag
456
+ raise TemplateSyntaxError.new(
457
+ "unexpected tag #{Luoma.get_token_value(token, @source).inspect}",
458
+ token,
459
+ @source,
460
+ @template_name
461
+ )
462
+ end
463
+
464
+ tag.parse(token, tag_name, self)
465
+ end
466
+
467
+ #: () -> Variable
468
+ def parse_path
469
+ token = current
470
+
471
+ root = if PATH_SEGMENT_KINDS.include?(token.first)
472
+ @pos += 1
473
+ Name.new(token, Luoma.get_token_value(token, @source))
474
+ else
475
+ eat(:token_lbracket)
476
+ if kind == :token_ident
477
+ path = parse_path
478
+ eat(:token_rbracket)
479
+ path
480
+ else
481
+ parse_string_literal.with(eat(:token_rbracket))
482
+ end
483
+ end
484
+
485
+ Variable.new(token, root, parse_path_segments)
486
+ end
487
+
488
+ #: () -> Array[t_path_segment]
489
+ def parse_path_segments
490
+ segments = [] #: Array[t_path_segment]
491
+
492
+ loop do
493
+ case kind
494
+ when :token_lbracket
495
+ segments << parse_bracketed_segment
496
+ when :token_dot
497
+ @pos += 1
498
+ token = eat_one_of(PATH_SEGMENT_KINDS)
499
+
500
+ if kind == :token_question
501
+ @pos += 1
502
+ segments << Predicate.new(
503
+ token,
504
+ Luoma.get_token_value(token, @source)
505
+ )
506
+
507
+ if PATH_PUNCTUATION.include?(kind)
508
+ raise TemplateSyntaxError.new(
509
+ "unexpected segment after predicate",
510
+ current,
511
+ @source,
512
+ @template_name
513
+ )
514
+ end
515
+
516
+ break
517
+ end
518
+
519
+ segments << Name.new(token, Luoma.get_token_value(token, @source))
520
+ else
521
+ break
522
+ end
523
+ end
524
+
525
+ segments
526
+ end
527
+
528
+ #: () -> t_path_segment
529
+ def parse_bracketed_segment
530
+ eat(:token_lbracket)
531
+ token = self.next
532
+
533
+ case token.first
534
+ when :token_int
535
+ IndexSelector.new(
536
+ token, Luoma.get_token_value(token, @source).to_i
537
+ ).with(eat(:token_rbracket))
538
+ when :token_ident, :token_false, :token_true, :token_null, :token_nil, :token_not, :token_and, :token_or
539
+ @pos -= 1
540
+ path = parse_path
541
+ eat(:token_rbracket)
542
+ path
543
+ when :token_double_quote, :token_single_quote
544
+ @pos -= 1
545
+ parse_string_literal.with(eat(:token_rbracket))
546
+ when :token_rbracket
547
+ raise TemplateSyntaxError.new(
548
+ "empty bracketed segment",
549
+ token,
550
+ @source,
551
+ @template_name
552
+ )
553
+ else
554
+ raise TemplateSyntaxError.new(
555
+ "expected an integer, identifier or string",
556
+ token,
557
+ @source,
558
+ @template_name
559
+ )
560
+ end
561
+ end
562
+
563
+ #: () -> Expression
564
+ def parse_array_or_path
565
+ start_pos = @pos
566
+ token = eat(:token_lbracket)
567
+
568
+ case kind
569
+ when :token_rbracket
570
+ # Empty array.
571
+ ArrayLiteral.new(token, []).with(eat(:token_rbracket))
572
+ when :token_triple_dot
573
+ # An array with a spread operator before the first item.
574
+ parse_partial_array(token, Spread.new(self.next, parse_expression))
575
+ else
576
+ expr = parse_expression
577
+
578
+ if kind != :token_comma && expr.is_a?(StringLiteral)
579
+ # A path, backtrack.
580
+ @pos = start_pos
581
+ parse_path
582
+ else
583
+ parse_partial_array(token, expr)
584
+ end
585
+ end
586
+ end
587
+
588
+ # Parse an array where we've consumed the opening bracket and first item.
589
+ #: (t_token, Expression|Spread) -> Expression
590
+ def parse_partial_array(token, first)
591
+ items = [first] #: Array[Expression|Spread]
592
+
593
+ loop do
594
+ break if kind == :token_rbracket
595
+
596
+ eat(:token_comma)
597
+
598
+ case kind
599
+ when :token_rbracket
600
+ # Trailing commas are OK.
601
+ break
602
+ when :token_triple_dot
603
+ items << Spread.new(self.next, parse_expression)
604
+ else
605
+ items << parse_expression
606
+ end
607
+ end
608
+
609
+ eat(:token_rbracket)
610
+ ArrayLiteral.new(token, items)
611
+ end
612
+
613
+ #: () -> ObjectLiteral
614
+ def parse_object_literal
615
+ token = eat(:token_lbrace)
616
+
617
+ return ObjectLiteral.new(token, []).with(eat(:token_rbrace)) if kind == :token_rbrace
618
+
619
+ items = [parse_object_item] #: Array[Item|Spread]
620
+
621
+ loop do
622
+ break if kind == :token_rbrace
623
+
624
+ eat(:token_comma)
625
+ break if kind == :token_rbrace # Trailing commas are OK.
626
+
627
+ items << parse_object_item
628
+ end
629
+
630
+ eat(:token_rbrace)
631
+ ObjectLiteral.new(token, items)
632
+ end
633
+
634
+ #: () -> Item | Spread
635
+ def parse_object_item
636
+ return Spread.new(self.next, parse_expression) if kind == :token_triple_dot
637
+
638
+ key = parse_name
639
+ eat(:token_colon)
640
+ value = parse_expression
641
+ Item.new(key.token, key, value)
642
+ end
643
+
644
+ #: () -> Expression
645
+ def parse_lambda_range_or_group(precedence:)
646
+ token = eat(:token_lparen)
647
+ expr = parse_expression
648
+
649
+ case kind
650
+ when :token_double_dot
651
+ # A range literal
652
+ @pos += 1
653
+ stop = parse_expression
654
+ eat(:token_rparen, message: "expected a closing bracket")
655
+ RangeLiteral.new(token, expr, stop)
656
+ when :token_triple_dot
657
+ raise TemplateSyntaxError.new(
658
+ "too many dots",
659
+ current,
660
+ @source,
661
+ @template_name
662
+ )
663
+ when :token_comma
664
+ # A lambda expression, but we've already consumed the first parameter.
665
+ parse_partial_lambda(expr, precedence: precedence)
666
+ else
667
+ if peek.first == :token_arrow && kind == :token_rparen
668
+ # A lambda expression with a single parameter surrounded by parens.
669
+ parse_partial_lambda(expr, precedence: precedence)
670
+ else
671
+ closing_token = eat(:token_rparen, message: "unbalanced brackets")
672
+ segments = PATH_PUNCTUATION.include?(kind) ? parse_path_segments : [] #: Array[t_path_segment]
673
+ GroupExpression.new(token, expr, segments).with(closing_token)
674
+ end
675
+ end
676
+ end
677
+
678
+ # Parse a lambda expression where we've already consumed the opening paren
679
+ # and first parameter.
680
+ #: (Expression) -> Lambda
681
+ def parse_partial_lambda(expr, precedence:)
682
+ unless expr.is_a?(Variable) && expr.segments.empty? && expr.root.is_a?(Name)
683
+ raise TemplateSyntaxError.new(
684
+ "expected an identifier",
685
+ expr.token,
686
+ @source,
687
+ @template_name
688
+ )
689
+ end
690
+
691
+ params = [expr.root] #: Array[Name]
692
+ @pos += 1 if kind == :token_comma
693
+
694
+ loop do
695
+ break if kind == :token_rparen
696
+
697
+ params << parse_ident
698
+ @pos += 1 if kind == :token_comma
699
+ end
700
+
701
+ eat(:token_rparen)
702
+ eat(:token_arrow)
703
+ Lambda.new(expr.token, params, parse_expression(precedence: precedence))
704
+ end
705
+
706
+ #: () -> Expression
707
+ def parse_prefix
708
+ token = self.next
709
+
710
+ case token.first
711
+ when :token_not
712
+ NotExpression.new(token, parse_expression(precedence: Precedence::LOGICAL_NOT))
713
+ when :token_add
714
+ PosExpression.new(token, parse_expression(precedence: Precedence::NEG))
715
+ when :token_sub
716
+ NegExpression.new(token, parse_expression(precedence: Precedence::NEG))
717
+ else
718
+ raise TemplateSyntaxError.new(
719
+ "unknown prefix operator",
720
+ token,
721
+ @source,
722
+ @template_name
723
+ )
724
+ end
725
+ end
726
+
727
+ #: () -> Expression
728
+ def parse_range_literal
729
+ token = eat(:token_lparen)
730
+ start = parse_expression
731
+ eat(:token_double_dot)
732
+ stop = parse_expression
733
+ eat(:token_rparen)
734
+ RangeLiteral.new(token, start, stop)
735
+ end
736
+
737
+ #: () -> Expression
738
+ def parse_true_literal
739
+ token = self.next
740
+ if PATH_PUNCTUATION.include?(kind)
741
+ @pos -= 1
742
+ parse_path
743
+ else
744
+ BooleanLiteral.new(token, true)
745
+ end
746
+ end
747
+
748
+ #: () -> Expression
749
+ def parse_false_literal
750
+ token = self.next
751
+ if PATH_PUNCTUATION.include?(kind)
752
+ @pos -= 1
753
+ parse_path
754
+ else
755
+ BooleanLiteral.new(token, false)
756
+ end
757
+ end
758
+
759
+ #: () -> Expression
760
+ def parse_null_literal
761
+ token = self.next
762
+ if PATH_PUNCTUATION.include?(kind)
763
+ @pos -= 1
764
+ parse_path
765
+ else
766
+ NullLiteral.new(token)
767
+ end
768
+ end
769
+
770
+ #: () -> Expression
771
+ def parse_int_literal
772
+ token = self.next
773
+ IntegerLiteral.new(token, Luoma.get_token_value(token, @source).to_i)
774
+ end
775
+
776
+ #: () -> Expression
777
+ def parse_float_literal
778
+ token = self.next
779
+ FloatLiteral.new(token, Float(Luoma.get_token_value(token, @source)))
780
+ end
781
+
782
+ #: (Expression) -> FilteredExpression
783
+ def parse_filter(token, left)
784
+ name = parse_path
785
+
786
+ if TERMINATE_FILTER.include?(kind) || kind == :token_comma
787
+ # No arguments
788
+ return FilteredExpression.new(
789
+ token,
790
+ left,
791
+ Filter.new(
792
+ name.token,
793
+ name,
794
+ [],
795
+ []
796
+ )
797
+ )
798
+ end
799
+
800
+ eat(:token_colon, message: "missing colon or pipe")
801
+ args = [] #: Array[Expression]
802
+ kwargs = [] #: Array[KeywordArgument]
803
+
804
+ loop do
805
+ kind_ = kind
806
+ break if TERMINATE_FILTER.include?(kind_)
807
+
808
+ if kind_ == :token_ident
809
+ peek_kind = peek.first
810
+ if KEYWORD_ARGUMENT_DELIMITERS.include?(peek_kind)
811
+ # A keyword argument
812
+ param = parse_ident
813
+ eat_one_of(KEYWORD_ARGUMENT_DELIMITERS)
814
+ value = parse_expression(precedence: Precedence::FILTER_ARG)
815
+ kwargs << KeywordArgument.new(param.token, param, value)
816
+ elsif peek_kind == :token_arrow
817
+ # A positional argument that is an arrow function with a single
818
+ # parameter.
819
+ args << parse_lambda(precedence: Precedence::FILTER_ARG)
820
+ else
821
+ # A positional argument that is a variable or path
822
+ args << parse_expression(precedence: Precedence::FILTER_ARG)
823
+ end
824
+ else
825
+ break if TERMINATE_FILTER.include?(kind_)
826
+
827
+ args << parse_expression(precedence: Precedence::FILTER_ARG)
828
+ end
829
+
830
+ break if TERMINATE_FILTER.include?(kind)
831
+
832
+ eat(:token_comma, message: "missing comma or pipe")
833
+ end
834
+
835
+ FilteredExpression.new(
836
+ token,
837
+ left,
838
+ Filter.new(
839
+ name.token,
840
+ name,
841
+ args,
842
+ kwargs
843
+ )
844
+ )
845
+ end
846
+
847
+ # Parse a lambda expression with a single parameter not enclosed in parentheses.
848
+ #: () -> Lambda
849
+ def parse_lambda(precedence:)
850
+ param = parse_ident
851
+ eat(:token_arrow)
852
+ Lambda.new(param.token, [param], parse_expression(precedence: precedence))
853
+ end
854
+
855
+ #: (Expression) -> Expression
856
+ def parse_infix(left)
857
+ op_token = self.next
858
+ kind_ = op_token.first
859
+
860
+ return parse_filter(op_token, left) if kind_ == :token_pipe
861
+
862
+ right = parse_expression(precedence: PRECEDENCES[kind_] || Precedence::LOWEST)
863
+ INFIX_OPERATORS[kind_].new(op_token, left, right)
864
+ end
865
+
866
+ #: (t_token) -> String
867
+ def unescape(token)
868
+ unescaped = [] # : Array[String]
869
+ scanner = StringScanner.new(Luoma.get_token_value(token, @source))
870
+
871
+ until scanner.eos?
872
+ if scanner.scan(RE_SLASH_U)
873
+ code_point = (scanner.captures&.first || raise).to_i(16)
874
+
875
+ if low_surrogate?(code_point)
876
+ raise TemplateSyntaxError.new(
877
+ "unexpected low surrogate",
878
+ token,
879
+ @source,
880
+ @template_name
881
+ )
882
+ end
883
+
884
+ if high_surrogate?(code_point)
885
+ unless scanner.scan(RE_SLASH_U)
886
+ raise TemplateSyntaxError.new(
887
+ "expected low surrogate",
888
+ token,
889
+ @source,
890
+ @template_name
891
+ )
892
+ end
893
+
894
+ low_surrogate = (scanner.captures&.first || raise).to_i(16)
895
+
896
+ unless low_surrogate?(low_surrogate)
897
+ raise TemplateSyntaxError.new(
898
+ "expected low surrogate",
899
+ token,
900
+ @source,
901
+ @template_name
902
+ )
903
+ end
904
+
905
+ code_point = 0x10000 + (
906
+ ((code_point & 0x03FF) << 10) | (low_surrogate & 0x03FF)
907
+ )
908
+ end
909
+
910
+ unescaped << code_point.chr(Encoding::UTF_8)
911
+ next
912
+ end
913
+
914
+ if scanner.scan(RE_ESCAPE_INTERPOLATION)
915
+ unescaped << "${"
916
+ next
917
+ end
918
+
919
+ ch = scanner.getch
920
+
921
+ break if ch.nil?
922
+
923
+ unless ch == "\\"
924
+ unescaped << ch
925
+ next
926
+ end
927
+
928
+ ch = scanner.getch
929
+
930
+ case ch
931
+ when "\""
932
+ unescaped << "\""
933
+ when "'"
934
+ unescaped << "'"
935
+ when "\\"
936
+ unescaped << "\\"
937
+ when "/"
938
+ unescaped << "/"
939
+ when "b"
940
+ unescaped << "\x08"
941
+ when "f"
942
+ unescaped << "\x0c"
943
+ when "n"
944
+ unescaped << "\n"
945
+ when "r"
946
+ unescaped << "\r"
947
+ when "t"
948
+ unescaped << "\t"
949
+ when nil
950
+ raise TemplateSyntaxError.new(
951
+ "incomplete escape sequence",
952
+ token,
953
+ @source,
954
+ @template_name
955
+ )
956
+ else
957
+ raise TemplateSyntaxError.new(
958
+ "unknown escape sequence",
959
+ token,
960
+ @source,
961
+ @template_name
962
+ )
963
+ end
964
+ end
965
+
966
+ unescaped.join
967
+ end
968
+
969
+ #: (Integer) -> bool
970
+ def high_surrogate?(code_point)
971
+ code_point.between?(0xD800, 0xDBFF)
972
+ end
973
+
974
+ #: (Integer) -> bool
975
+ def low_surrogate?(code_point)
976
+ code_point.between?(0xDC00, 0xDFFF)
977
+ end
978
+ end
979
+ end