twig_ruby 0.0.1
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/lib/twig/auto_hash.rb +17 -0
- data/lib/twig/cache/base.rb +31 -0
- data/lib/twig/cache/filesystem.rb +47 -0
- data/lib/twig/cache/nil.rb +19 -0
- data/lib/twig/callable.rb +21 -0
- data/lib/twig/compiler.rb +123 -0
- data/lib/twig/context.rb +64 -0
- data/lib/twig/environment.rb +161 -0
- data/lib/twig/error/base.rb +37 -0
- data/lib/twig/error/syntax.rb +8 -0
- data/lib/twig/expression_parser.rb +517 -0
- data/lib/twig/extension/base.rb +23 -0
- data/lib/twig/extension/core.rb +89 -0
- data/lib/twig/extension/rails.rb +70 -0
- data/lib/twig/extension_set.rb +69 -0
- data/lib/twig/lexer.rb +372 -0
- data/lib/twig/loader/array.rb +39 -0
- data/lib/twig/loader/base.rb +32 -0
- data/lib/twig/loader/filesystem.rb +45 -0
- data/lib/twig/node/base.rb +61 -0
- data/lib/twig/node/block.rb +20 -0
- data/lib/twig/node/block_reference.rb +17 -0
- data/lib/twig/node/empty.rb +11 -0
- data/lib/twig/node/expression/array.rb +50 -0
- data/lib/twig/node/expression/assign_name.rb +28 -0
- data/lib/twig/node/expression/base.rb +20 -0
- data/lib/twig/node/expression/binary/base.rb +63 -0
- data/lib/twig/node/expression/call.rb +28 -0
- data/lib/twig/node/expression/constant.rb +17 -0
- data/lib/twig/node/expression/filter.rb +52 -0
- data/lib/twig/node/expression/get_attribute.rb +30 -0
- data/lib/twig/node/expression/helper_method.rb +31 -0
- data/lib/twig/node/expression/name.rb +37 -0
- data/lib/twig/node/expression/ternary.rb +28 -0
- data/lib/twig/node/expression/unary/base.rb +52 -0
- data/lib/twig/node/expression/variable/assign_context.rb +11 -0
- data/lib/twig/node/expression/variable/context.rb +11 -0
- data/lib/twig/node/for.rb +64 -0
- data/lib/twig/node/for_loop.rb +39 -0
- data/lib/twig/node/if.rb +50 -0
- data/lib/twig/node/include.rb +71 -0
- data/lib/twig/node/module.rb +74 -0
- data/lib/twig/node/nodes.rb +13 -0
- data/lib/twig/node/print.rb +18 -0
- data/lib/twig/node/text.rb +20 -0
- data/lib/twig/node/yield.rb +54 -0
- data/lib/twig/output_buffer.rb +29 -0
- data/lib/twig/parser.rb +131 -0
- data/lib/twig/railtie.rb +60 -0
- data/lib/twig/source.rb +13 -0
- data/lib/twig/template.rb +50 -0
- data/lib/twig/token.rb +48 -0
- data/lib/twig/token_parser/base.rb +20 -0
- data/lib/twig/token_parser/block.rb +54 -0
- data/lib/twig/token_parser/extends.rb +25 -0
- data/lib/twig/token_parser/for.rb +64 -0
- data/lib/twig/token_parser/if.rb +64 -0
- data/lib/twig/token_parser/include.rb +51 -0
- data/lib/twig/token_parser/yield.rb +44 -0
- data/lib/twig/token_stream.rb +73 -0
- data/lib/twig/twig_filter.rb +21 -0
- data/lib/twig_ruby.rb +36 -0
- metadata +103 -0
@@ -0,0 +1,517 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Twig
|
4
|
+
# @!attribute [r] environment
|
5
|
+
# @return [Environment]
|
6
|
+
class ExpressionParser
|
7
|
+
attr_reader :environment
|
8
|
+
|
9
|
+
OPERATOR_LEFT = 1
|
10
|
+
OPERATOR_RIGHT = 2
|
11
|
+
|
12
|
+
# @param [Parser] parser
|
13
|
+
# @param [Environment] environment
|
14
|
+
def initialize(parser, environment)
|
15
|
+
@parser = parser
|
16
|
+
@environment = environment
|
17
|
+
end
|
18
|
+
|
19
|
+
# @return [Node::Expression::Base]
|
20
|
+
def parse_expression(precedence = 0)
|
21
|
+
# @todo parse arrow
|
22
|
+
|
23
|
+
expr = primary
|
24
|
+
token = parser.current_token
|
25
|
+
|
26
|
+
while binary?(token) && binary_operators[token.value.to_sym][:precedence] >= precedence
|
27
|
+
operator = binary_operators[token.value.to_sym]
|
28
|
+
parser.stream.next
|
29
|
+
|
30
|
+
next_precedence = operator[:precedence]
|
31
|
+
next_precedence += 1 if operator[:associativity] == OPERATOR_LEFT
|
32
|
+
|
33
|
+
expr1 = parse_expression(next_precedence)
|
34
|
+
|
35
|
+
# @type [Node::Expression::Binary::Base]
|
36
|
+
expr = operator[:class].new(expr, expr1, token.lineno)
|
37
|
+
expr.attributes[:operator] = "binary_#{token.value}"
|
38
|
+
|
39
|
+
token = parser.current_token
|
40
|
+
end
|
41
|
+
|
42
|
+
return parse_ternary_expression(expr) if precedence.zero?
|
43
|
+
|
44
|
+
expr
|
45
|
+
end
|
46
|
+
|
47
|
+
# @return [Node::Expression::Base]
|
48
|
+
def parse_primary_expression
|
49
|
+
token = parser.current_token
|
50
|
+
|
51
|
+
case token.type
|
52
|
+
when Token::SYMBOL_TYPE
|
53
|
+
parser.stream.next
|
54
|
+
|
55
|
+
node = Node::Expression::Constant.new(token.value.to_sym, token.lineno)
|
56
|
+
when Token::NAME_TYPE
|
57
|
+
parser.stream.next
|
58
|
+
|
59
|
+
node = case token.value
|
60
|
+
when 'true', 'TRUE'
|
61
|
+
Node::Expression::Constant.new(true, token.lineno)
|
62
|
+
when 'false', 'FALSE'
|
63
|
+
Node::Expression::Constant.new(false, token.lineno)
|
64
|
+
when 'null', 'NULL', 'nil'
|
65
|
+
Node::Expression::Constant.new(nil, token.lineno)
|
66
|
+
else
|
67
|
+
if parser.current_token.value == '('
|
68
|
+
get_function_node(token.value, token.lineno)
|
69
|
+
else
|
70
|
+
# @todo should be a context variable
|
71
|
+
Node::Expression::Name.new(token.value, token.lineno)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
when Token::NUMBER_TYPE
|
75
|
+
parser.stream.next
|
76
|
+
|
77
|
+
node = Node::Expression::Constant.new(token.value, token.lineno)
|
78
|
+
when Token::STRING_TYPE, Token::INTERPOLATION_START_TYPE
|
79
|
+
node = parse_string_expression
|
80
|
+
when Token::PUNCTUATION_TYPE
|
81
|
+
case token.value
|
82
|
+
when '['
|
83
|
+
node = parse_sequence_expression
|
84
|
+
when '{'
|
85
|
+
node = parse_mapping_expression
|
86
|
+
else
|
87
|
+
Error::Syntax.new(
|
88
|
+
"Unexpected token #{token.type} of value #{token.value}",
|
89
|
+
token.lineno,
|
90
|
+
parser.stream.source
|
91
|
+
)
|
92
|
+
end
|
93
|
+
else
|
94
|
+
raise Error::Syntax.new(
|
95
|
+
"Unexpected token '#{token.type}' of value '#{token.value}'",
|
96
|
+
token.lineno,
|
97
|
+
parser.stream.source
|
98
|
+
)
|
99
|
+
end
|
100
|
+
|
101
|
+
parse_post_fix_expression(node)
|
102
|
+
end
|
103
|
+
|
104
|
+
# @param [Node::Expression::Base] node
|
105
|
+
# @return [Node::Expression::Base]
|
106
|
+
def parse_post_fix_expression(node)
|
107
|
+
loop do
|
108
|
+
token = parser.current_token
|
109
|
+
|
110
|
+
unless token.type == Token::PUNCTUATION_TYPE
|
111
|
+
break
|
112
|
+
end
|
113
|
+
|
114
|
+
case token.value
|
115
|
+
when '.', '['
|
116
|
+
node = parse_subscript_expression(node)
|
117
|
+
when '|'
|
118
|
+
node = parse_filter_expression(node)
|
119
|
+
else
|
120
|
+
break
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
node
|
125
|
+
end
|
126
|
+
|
127
|
+
def parse_subscript_expression(node)
|
128
|
+
if parser.stream.next.value == '.'
|
129
|
+
return parse_subscript_expression_dot(node)
|
130
|
+
end
|
131
|
+
|
132
|
+
parse_subscript_expression_array(node)
|
133
|
+
end
|
134
|
+
|
135
|
+
def parse_subscript_expression_dot(node)
|
136
|
+
stream = parser.stream
|
137
|
+
token = stream.current
|
138
|
+
lineno = token.lineno
|
139
|
+
arguments = Node::Expression::Array.new({}, lineno)
|
140
|
+
token = stream.next
|
141
|
+
|
142
|
+
if token.type == Token::NAME_TYPE
|
143
|
+
attribute = Node::Expression::Constant.new(token.value, token.lineno)
|
144
|
+
end
|
145
|
+
|
146
|
+
Node::Expression::GetAttribute.new(node, attribute, arguments, nil, token.lineno)
|
147
|
+
end
|
148
|
+
|
149
|
+
def parse_sequence_expression
|
150
|
+
stream = parser.stream
|
151
|
+
stream.expect(Token::PUNCTUATION_TYPE, '[', 'A sequence element was expected')
|
152
|
+
|
153
|
+
node = Node::Expression::Array.new({}, stream.current.lineno)
|
154
|
+
first = true
|
155
|
+
|
156
|
+
# raise stream.debug
|
157
|
+
|
158
|
+
until stream.test(Token::PUNCTUATION_TYPE, ']')
|
159
|
+
unless first
|
160
|
+
stream.expect(Token::PUNCTUATION_TYPE, ',', 'A sequence element must be followed by a comma')
|
161
|
+
|
162
|
+
# trailing comma
|
163
|
+
break if stream.test(Token::PUNCTUATION_TYPE, ']')
|
164
|
+
end
|
165
|
+
|
166
|
+
first = false
|
167
|
+
|
168
|
+
if stream.next_if(Token::SPREAD_TYPE)
|
169
|
+
expr = parse_expression
|
170
|
+
expr.attributes[:spread] = true
|
171
|
+
node.add_element(expr)
|
172
|
+
else
|
173
|
+
node.add_element(parse_expression)
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
stream.expect(Token::PUNCTUATION_TYPE, ']', 'An opened sequence is not properly closed')
|
178
|
+
|
179
|
+
node
|
180
|
+
end
|
181
|
+
|
182
|
+
def parse_mapping_expression
|
183
|
+
stream = parser.stream
|
184
|
+
stream.expect(Token::PUNCTUATION_TYPE, '{', 'A mapping element was expected')
|
185
|
+
|
186
|
+
node = Node::Expression::Array.new({}, stream.current.lineno)
|
187
|
+
first = true
|
188
|
+
|
189
|
+
until stream.test(Token::PUNCTUATION_TYPE, '}')
|
190
|
+
unless first
|
191
|
+
stream.expect(Token::PUNCTUATION_TYPE, ',', 'A mapping value must be followed by a comma')
|
192
|
+
|
193
|
+
# trailing comma
|
194
|
+
if stream.test(Token::PUNCTUATION_TYPE, '}')
|
195
|
+
break
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
first = false
|
200
|
+
|
201
|
+
if stream.next_if(Token::SPREAD_TYPE)
|
202
|
+
value = parse_expression
|
203
|
+
value.attributes[:spread] = true
|
204
|
+
node.add_element(value)
|
205
|
+
|
206
|
+
next
|
207
|
+
end
|
208
|
+
|
209
|
+
# a mapping key can be:
|
210
|
+
#
|
211
|
+
# * a number -- 12
|
212
|
+
# * a string -- 'a'
|
213
|
+
# * a name, which is equivalent to a symbol -- a
|
214
|
+
# * an expression, which must be enclosed in parentheses -- (1 + 2)
|
215
|
+
if (token = stream.next_if(Token::NAME_TYPE))
|
216
|
+
key = Node::Expression::Constant.new(token.value.to_sym, token.lineno)
|
217
|
+
|
218
|
+
# {a} is a shortcut for {a: a}
|
219
|
+
if stream.test(Token::PUNCTUATION_TYPE, %w[, }])
|
220
|
+
value = Node::Expression::Variable::Context.new(key.attributes[:value], key.lineno)
|
221
|
+
node.add_element(value, key.to_sym)
|
222
|
+
|
223
|
+
next
|
224
|
+
end
|
225
|
+
elsif (token = stream.next_if(Token::STRING_TYPE)) || (token = stream.next_if(Token::NUMBER_TYPE))
|
226
|
+
key = Node::Expression::Constant.new(token.value, token.lineno)
|
227
|
+
elsif stream.test(Token::PUNCTUATION_TYPE, '(')
|
228
|
+
key = parse_expression
|
229
|
+
else
|
230
|
+
current = stream.current
|
231
|
+
|
232
|
+
raise Error::Syntax.new(
|
233
|
+
'A mapping key must be a quoted string, number, name, or expression in parentheses ' \
|
234
|
+
"expected token '#{current.type}' of value '#{current.value}'",
|
235
|
+
current.lineno,
|
236
|
+
stream.source
|
237
|
+
)
|
238
|
+
end
|
239
|
+
|
240
|
+
stream.expect(Token::PUNCTUATION_TYPE, ':', 'A mapping key must be followed by a colon (:)')
|
241
|
+
value = parse_expression
|
242
|
+
|
243
|
+
node.add_element(value, key)
|
244
|
+
end
|
245
|
+
|
246
|
+
stream.expect(Token::PUNCTUATION_TYPE, '}', 'An opened mapping is not properly closed')
|
247
|
+
|
248
|
+
node
|
249
|
+
end
|
250
|
+
|
251
|
+
def get_function_node(name, line)
|
252
|
+
# @todo lots of stuff in this method
|
253
|
+
args = parse_only_arguments
|
254
|
+
|
255
|
+
if environment.helper_method?(name.to_sym)
|
256
|
+
Node::Expression::HelperMethod.new(name, args, line)
|
257
|
+
else
|
258
|
+
raise Error::Syntax.new("Unknown function '#{name}'", line, parser.stream.source)
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
def parse_only_arguments
|
263
|
+
args = AutoHash.new
|
264
|
+
stream = parser.stream
|
265
|
+
stream.expect(Token::PUNCTUATION_TYPE, '(', 'A list of arguments must begin with an opening parenthesis')
|
266
|
+
has_spread = false
|
267
|
+
|
268
|
+
until stream.test(Token::PUNCTUATION_TYPE, ')')
|
269
|
+
unless args.empty?
|
270
|
+
stream.expect(Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma')
|
271
|
+
|
272
|
+
# if above was trailing comma, exit early
|
273
|
+
break if stream.test(Token::PUNCTUATION_TYPE, ')')
|
274
|
+
end
|
275
|
+
|
276
|
+
if stream.next_if(Token::SPREAD_TYPE)
|
277
|
+
has_spread = true
|
278
|
+
value = Node::Expression::Unary::Spread.new(parse_expression, stream.current.lineno)
|
279
|
+
elsif has_spread
|
280
|
+
raise Error::Syntax.new(
|
281
|
+
'Normal arguments must be placed before argument unpacking.',
|
282
|
+
stream.current.lineno,
|
283
|
+
stream.source
|
284
|
+
)
|
285
|
+
else
|
286
|
+
value = parse_expression
|
287
|
+
end
|
288
|
+
|
289
|
+
name = nil
|
290
|
+
if (token = stream.next_if(Token::OPERATOR_TYPE, '=')) ||
|
291
|
+
(token = stream.next_if(Token::PUNCTUATION_TYPE, ':'))
|
292
|
+
unless value.class <= Node::Expression::Name
|
293
|
+
raise Error::Syntax.new(
|
294
|
+
"A parameter name must be a string, #{value.class.name} given.",
|
295
|
+
token.lineno,
|
296
|
+
stream.source
|
297
|
+
)
|
298
|
+
end
|
299
|
+
|
300
|
+
name = value.attributes[:name]
|
301
|
+
value = parse_expression
|
302
|
+
end
|
303
|
+
|
304
|
+
if name.nil?
|
305
|
+
args.add(value)
|
306
|
+
else
|
307
|
+
args[name] = value
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
stream.expect(Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis')
|
312
|
+
|
313
|
+
Node::Nodes.new(args)
|
314
|
+
end
|
315
|
+
|
316
|
+
# @return [Parser]
|
317
|
+
attr_reader :parser
|
318
|
+
|
319
|
+
# @return [Node::Expression::Base]
|
320
|
+
def primary
|
321
|
+
token = parser.current_token
|
322
|
+
|
323
|
+
if unary?(token)
|
324
|
+
operator = unary_operators[token.value.to_sym]
|
325
|
+
parser.stream.next
|
326
|
+
|
327
|
+
expr = parse_expression(operator[:precedence])
|
328
|
+
expr = operator[:class].new(expr, token.lineno)
|
329
|
+
expr.attributes[:operator] = "unary_#{token.value}"
|
330
|
+
|
331
|
+
return parse_post_fix_expression(expr)
|
332
|
+
elsif token.test(Token::PUNCTUATION_TYPE, '(')
|
333
|
+
parser.stream.next
|
334
|
+
expr = parse_expression.set_explicit_parentheses
|
335
|
+
|
336
|
+
parser.stream.expect(Token::PUNCTUATION_TYPE, ')', 'Open parenthesis not closed')
|
337
|
+
|
338
|
+
return parse_post_fix_expression(expr)
|
339
|
+
end
|
340
|
+
|
341
|
+
parse_primary_expression
|
342
|
+
end
|
343
|
+
|
344
|
+
# @param [Node::Expression] expr
|
345
|
+
# @return [Node::Expression]
|
346
|
+
def parse_ternary_expression(expr)
|
347
|
+
while parser.stream.next_if(Token::PUNCTUATION_TYPE, '?')
|
348
|
+
expr2 = parse_expression
|
349
|
+
expr3 = if parser.stream.next_if(Token::PUNCTUATION_TYPE, ':')
|
350
|
+
parse_expression
|
351
|
+
else
|
352
|
+
Node::Expression::Constant.new('', parser.current_token.lineno)
|
353
|
+
end
|
354
|
+
|
355
|
+
expr = Node::Expression::Ternary.new(expr, expr2, expr3, parser.current_token.lineno)
|
356
|
+
end
|
357
|
+
|
358
|
+
expr
|
359
|
+
end
|
360
|
+
|
361
|
+
def parse_filter_expression(node)
|
362
|
+
parser.stream.next
|
363
|
+
|
364
|
+
parse_filter_expression_raw(node)
|
365
|
+
end
|
366
|
+
|
367
|
+
def parse_filter_expression_raw(node)
|
368
|
+
loop do
|
369
|
+
token = parser.stream.expect(Token::NAME_TYPE)
|
370
|
+
|
371
|
+
arguments = if parser.stream.test(Token::PUNCTUATION_TYPE, '(')
|
372
|
+
parse_only_arguments
|
373
|
+
else
|
374
|
+
Node::Empty.new
|
375
|
+
end
|
376
|
+
|
377
|
+
filter = get_filter(token.value, token.lineno)
|
378
|
+
node = filter.node_class.new(node, filter, arguments, token.lineno)
|
379
|
+
|
380
|
+
unless parser.stream.test(Token::PUNCTUATION_TYPE, '|')
|
381
|
+
break
|
382
|
+
end
|
383
|
+
|
384
|
+
parser.stream.next
|
385
|
+
end
|
386
|
+
|
387
|
+
node
|
388
|
+
end
|
389
|
+
|
390
|
+
def parse_arguments
|
391
|
+
raise NotImplementedError
|
392
|
+
end
|
393
|
+
|
394
|
+
# @return [Node::Nodes]
|
395
|
+
def parse_assignment_expression
|
396
|
+
stream = parser.stream
|
397
|
+
targets = AutoHash.new
|
398
|
+
|
399
|
+
loop do
|
400
|
+
token = parser.current_token
|
401
|
+
|
402
|
+
if stream.test(Token::OPERATOR_TYPE) && token.value.match(Lexer::REGEX_NAME)
|
403
|
+
# in this context, string operators are variables names
|
404
|
+
parser.stream.next
|
405
|
+
else
|
406
|
+
stream.expect(Token::NAME_TYPE, nil, 'Only variables can be assigned to')
|
407
|
+
end
|
408
|
+
|
409
|
+
targets << Node::Expression::Variable::AssignContext.new(token.value, token.lineno)
|
410
|
+
|
411
|
+
unless stream.next_if(Token::PUNCTUATION_TYPE, ',')
|
412
|
+
break
|
413
|
+
end
|
414
|
+
end
|
415
|
+
|
416
|
+
Node::Nodes.new(targets)
|
417
|
+
end
|
418
|
+
|
419
|
+
def parse_string_expression
|
420
|
+
stream = parser.stream
|
421
|
+
nodes = []
|
422
|
+
|
423
|
+
# a string cannot be followed by another string in a single expression
|
424
|
+
next_can_be_string = true
|
425
|
+
|
426
|
+
loop do
|
427
|
+
if next_can_be_string && (token = stream.next_if(Token::STRING_TYPE))
|
428
|
+
nodes << Node::Expression::Constant.new(token.value, token.lineno)
|
429
|
+
next_can_be_string = false
|
430
|
+
elsif stream.next_if(Token::INTERPOLATION_START_TYPE)
|
431
|
+
nodes << parse_expression
|
432
|
+
stream.expect(Token::INTERPOLATION_END_TYPE)
|
433
|
+
next_can_be_string = true
|
434
|
+
else
|
435
|
+
break
|
436
|
+
end
|
437
|
+
end
|
438
|
+
|
439
|
+
expr = nodes.shift
|
440
|
+
nodes.each do |node|
|
441
|
+
expr = Node::Expression::Binary::Concat(expr, node, node.lineno)
|
442
|
+
end
|
443
|
+
|
444
|
+
expr
|
445
|
+
end
|
446
|
+
|
447
|
+
# @param [Node::Base]
|
448
|
+
# @return [Node::Expression::Base]
|
449
|
+
def parse_subscript_expression_array(node)
|
450
|
+
stream = parser.stream
|
451
|
+
token = stream.current
|
452
|
+
lineno = token.lineno
|
453
|
+
arguments = Node::Expression::Array.new({}, lineno)
|
454
|
+
|
455
|
+
slice = false
|
456
|
+
if stream.test(Token::PUNCTUATION_TYPE, ':')
|
457
|
+
slice = true
|
458
|
+
attribute = Node::Expression::Constant.new(0, token.lineno)
|
459
|
+
else
|
460
|
+
attribute = parse_expression
|
461
|
+
end
|
462
|
+
|
463
|
+
if stream.next_if(Token::PUNCTUATION_TYPE, ':')
|
464
|
+
slice = true
|
465
|
+
end
|
466
|
+
|
467
|
+
if slice
|
468
|
+
length = if stream.test(Token::PUNCTUATION_TYPE, ']')
|
469
|
+
Node::Expression::Constant.new(nil, token.lineno)
|
470
|
+
else
|
471
|
+
parse_expression
|
472
|
+
end
|
473
|
+
|
474
|
+
filter = get_filter('slice', token.lineno)
|
475
|
+
arguments = Node::Nodes.new(AutoHash.new.add(attribute, length))
|
476
|
+
filter = filter.node_class.new(node, filter, arguments, token.lineno)
|
477
|
+
|
478
|
+
stream.expect(Token::PUNCTUATION_TYPE, ']')
|
479
|
+
|
480
|
+
return filter
|
481
|
+
end
|
482
|
+
|
483
|
+
stream.expect(Token::PUNCTUATION_TYPE, ']')
|
484
|
+
|
485
|
+
Node::Expression::GetAttribute.new(node, attribute, arguments, Template::ARRAY_CALL, lineno)
|
486
|
+
end
|
487
|
+
|
488
|
+
# @return [Hash]
|
489
|
+
def unary_operators
|
490
|
+
@unary_operators ||= environment.operators[0]
|
491
|
+
end
|
492
|
+
|
493
|
+
# @return [Hash]
|
494
|
+
def binary_operators
|
495
|
+
@binary_operators ||= environment.operators[1]
|
496
|
+
end
|
497
|
+
|
498
|
+
# @param [Token] token
|
499
|
+
def unary?(token)
|
500
|
+
token.test(Token::OPERATOR_TYPE) && unary_operators.key?(token.value.to_sym)
|
501
|
+
end
|
502
|
+
|
503
|
+
# @param [Token] token
|
504
|
+
def binary?(token)
|
505
|
+
token.test(Token::OPERATOR_TYPE) && binary_operators.key?(token.value.to_sym)
|
506
|
+
end
|
507
|
+
|
508
|
+
# @return [Filter]
|
509
|
+
def get_filter(name, lineno)
|
510
|
+
unless (filter = environment.filter(name))
|
511
|
+
raise Error::Syntax.new("Unknown '#{name}' filter", lineno, parser.stream.source)
|
512
|
+
end
|
513
|
+
|
514
|
+
filter
|
515
|
+
end
|
516
|
+
end
|
517
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Twig
|
4
|
+
module Extension
|
5
|
+
class Base
|
6
|
+
def operators
|
7
|
+
[{}, {}]
|
8
|
+
end
|
9
|
+
|
10
|
+
def filters
|
11
|
+
{}
|
12
|
+
end
|
13
|
+
|
14
|
+
def token_parsers
|
15
|
+
[]
|
16
|
+
end
|
17
|
+
|
18
|
+
def helper_methods
|
19
|
+
[]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Twig
|
4
|
+
module Extension
|
5
|
+
class Core < Base
|
6
|
+
def operators
|
7
|
+
unary = Node::Expression::Unary
|
8
|
+
binary = Node::Expression::Binary
|
9
|
+
|
10
|
+
[
|
11
|
+
{
|
12
|
+
not: { precedence: 70, class: unary::Not },
|
13
|
+
'-': { precedence: 500, class: unary::Neg },
|
14
|
+
'+': { precedence: 500, class: unary::Pos },
|
15
|
+
},
|
16
|
+
{
|
17
|
+
or: { precedence: 10, class: binary::Or, associativity: ExpressionParser::OPERATOR_LEFT },
|
18
|
+
xor: { precedence: 12, class: binary::Xor, associativity: ExpressionParser::OPERATOR_LEFT },
|
19
|
+
and: { precedence: 15, class: binary::And, associativity: ExpressionParser::OPERATOR_LEFT },
|
20
|
+
|
21
|
+
'==': { precedence: 20, class: binary::Equal, associativity: ExpressionParser::OPERATOR_LEFT },
|
22
|
+
'!=': { precedence: 20, class: binary::NotEqual, associativity: ExpressionParser::OPERATOR_LEFT },
|
23
|
+
'<=>': { precedence: 20, class: binary::Spaceship, associativity: ExpressionParser::OPERATOR_LEFT },
|
24
|
+
'<': { precedence: 20, class: binary::Less, associativity: ExpressionParser::OPERATOR_LEFT },
|
25
|
+
'>': { precedence: 20, class: binary::Greater, associativity: ExpressionParser::OPERATOR_LEFT },
|
26
|
+
'>=': { precedence: 20, class: binary::GreaterEqual, associativity: ExpressionParser::OPERATOR_LEFT },
|
27
|
+
'<=': { precedence: 20, class: binary::LessEqual, associativity: ExpressionParser::OPERATOR_LEFT },
|
28
|
+
|
29
|
+
# @todo this needs a custom class but just needs to be parsed as operaor for for loops
|
30
|
+
in: { precedence: 20, class: binary::LessEqual, associativity: ExpressionParser::OPERATOR_LEFT },
|
31
|
+
|
32
|
+
'+': { precedence: 30, class: binary::Add, associativity: ExpressionParser::OPERATOR_LEFT },
|
33
|
+
'-': { precedence: 30, class: binary::Sub, associativity: ExpressionParser::OPERATOR_LEFT },
|
34
|
+
'~': { precedence: 40, class: binary::Concat, associativity: ExpressionParser::OPERATOR_LEFT },
|
35
|
+
'*': { precedence: 60, class: binary::Mul, associativity: ExpressionParser::OPERATOR_LEFT },
|
36
|
+
'/': { precedence: 60, class: binary::Div, associativity: ExpressionParser::OPERATOR_LEFT },
|
37
|
+
},
|
38
|
+
]
|
39
|
+
end
|
40
|
+
|
41
|
+
def filters
|
42
|
+
{
|
43
|
+
capitalize: TwigFilter.new('capitalize', [self, :capitalize]),
|
44
|
+
upper: TwigFilter.new('upper', [self, :upper]),
|
45
|
+
lower: TwigFilter.new('lower', [self, :lower]),
|
46
|
+
raw: TwigFilter.new('raw', [self, :raw]),
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
def token_parsers
|
51
|
+
[
|
52
|
+
TokenParser::Block.new,
|
53
|
+
TokenParser::Extends.new,
|
54
|
+
TokenParser::For.new,
|
55
|
+
TokenParser::If.new,
|
56
|
+
TokenParser::Include.new,
|
57
|
+
TokenParser::Yield.new,
|
58
|
+
]
|
59
|
+
end
|
60
|
+
|
61
|
+
def capitalize(string)
|
62
|
+
string.capitalize
|
63
|
+
end
|
64
|
+
|
65
|
+
def upper(string)
|
66
|
+
string.upcase
|
67
|
+
end
|
68
|
+
|
69
|
+
def lower(string)
|
70
|
+
string.downcase
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.ensure_hash(value)
|
74
|
+
return value if value.class < Hash
|
75
|
+
|
76
|
+
AutoHash.new.add(*value)
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.get_attribute(object, attribute, type)
|
80
|
+
case type
|
81
|
+
when Template::ARRAY_CALL
|
82
|
+
object[attribute] || (attribute.is_a?(String) ? object[attribute.to_sym] : object[attribute.to_s])
|
83
|
+
else
|
84
|
+
raise NotImplementedError, 'Need to implement other get_attribute calls'
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Twig
|
4
|
+
module Extension
|
5
|
+
class Rails < Extension::Base
|
6
|
+
def filters
|
7
|
+
{}
|
8
|
+
end
|
9
|
+
|
10
|
+
def helper_methods
|
11
|
+
%w[
|
12
|
+
render
|
13
|
+
|
14
|
+
distance_of_time_in_words
|
15
|
+
time_ago_in_words
|
16
|
+
|
17
|
+
number_to_currency
|
18
|
+
number_to_human
|
19
|
+
number_to_human_size
|
20
|
+
number_to_percentage
|
21
|
+
number_to_phone
|
22
|
+
number_with_delimiter
|
23
|
+
number_with_precision
|
24
|
+
|
25
|
+
excerpt
|
26
|
+
pluralize
|
27
|
+
truncate
|
28
|
+
word_wrap
|
29
|
+
|
30
|
+
button_to
|
31
|
+
current_page?
|
32
|
+
link_to
|
33
|
+
mail_to
|
34
|
+
url_for
|
35
|
+
|
36
|
+
raw
|
37
|
+
sanitize
|
38
|
+
sanitize_css
|
39
|
+
strip_links
|
40
|
+
strip_tags
|
41
|
+
|
42
|
+
audio_tag
|
43
|
+
auto_discovery_link_tag
|
44
|
+
favicon_link_tag
|
45
|
+
image_tag
|
46
|
+
javascript_include_tag
|
47
|
+
picture_tag
|
48
|
+
preload_link_tag
|
49
|
+
stylesheet_link_tag
|
50
|
+
video_tag
|
51
|
+
|
52
|
+
escape_javascript
|
53
|
+
javascript_tag
|
54
|
+
|
55
|
+
benchmark
|
56
|
+
cache
|
57
|
+
debug
|
58
|
+
|
59
|
+
tag
|
60
|
+
token_list
|
61
|
+
|
62
|
+
form_for
|
63
|
+
form_with
|
64
|
+
|
65
|
+
date_test
|
66
|
+
]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|