twig_ruby 0.0.1 → 0.0.3
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 +4 -4
- data/README.md +116 -0
- data/lib/tasks/twig_parity.rake +278 -0
- data/lib/twig/auto_hash.rb +7 -1
- data/lib/twig/callable.rb +28 -1
- data/lib/twig/compiler.rb +35 -3
- data/lib/twig/environment.rb +198 -41
- data/lib/twig/error/base.rb +81 -16
- data/lib/twig/error/loader.rb +8 -0
- data/lib/twig/error/logic.rb +8 -0
- data/lib/twig/error/runtime.rb +8 -0
- data/lib/twig/expression_parser/base.rb +30 -0
- data/lib/twig/expression_parser/expression_parsers.rb +57 -0
- data/lib/twig/expression_parser/infix/arrow.rb +31 -0
- data/lib/twig/expression_parser/infix/binary.rb +34 -0
- data/lib/twig/expression_parser/infix/conditional_ternary.rb +39 -0
- data/lib/twig/expression_parser/infix/dot.rb +72 -0
- data/lib/twig/expression_parser/infix/filter.rb +43 -0
- data/lib/twig/expression_parser/infix/function.rb +67 -0
- data/lib/twig/expression_parser/infix/is.rb +53 -0
- data/lib/twig/expression_parser/infix/is_not.rb +19 -0
- data/lib/twig/expression_parser/infix/parses_arguments.rb +84 -0
- data/lib/twig/expression_parser/infix/square_bracket.rb +66 -0
- data/lib/twig/expression_parser/infix_expression_parser.rb +34 -0
- data/lib/twig/expression_parser/prefix/grouping.rb +60 -0
- data/lib/twig/expression_parser/prefix/literal.rb +244 -0
- data/lib/twig/expression_parser/prefix/unary.rb +29 -0
- data/lib/twig/expression_parser/prefix_expression_parser.rb +18 -0
- data/lib/twig/extension/base.rb +26 -4
- data/lib/twig/extension/core.rb +1076 -48
- data/lib/twig/extension/debug.rb +25 -0
- data/lib/twig/extension/escaper.rb +73 -0
- data/lib/twig/extension/rails.rb +10 -57
- data/lib/twig/extension/string_loader.rb +19 -0
- data/lib/twig/extension_set.rb +117 -20
- data/lib/twig/file_extension_escaping_strategy.rb +35 -0
- data/lib/twig/lexer.rb +225 -81
- data/lib/twig/loader/array.rb +25 -8
- data/lib/twig/loader/chain.rb +93 -0
- data/lib/twig/loader/filesystem.rb +106 -7
- data/lib/twig/node/auto_escape.rb +18 -0
- data/lib/twig/node/base.rb +58 -2
- data/lib/twig/node/block.rb +2 -0
- data/lib/twig/node/block_reference.rb +5 -1
- data/lib/twig/node/body.rb +7 -0
- data/lib/twig/node/cache.rb +50 -0
- data/lib/twig/node/capture.rb +22 -0
- data/lib/twig/node/deprecated.rb +53 -0
- data/lib/twig/node/do.rb +19 -0
- data/lib/twig/node/embed.rb +43 -0
- data/lib/twig/node/expression/array.rb +29 -20
- data/lib/twig/node/expression/arrow_function.rb +55 -0
- data/lib/twig/node/expression/assign_name.rb +1 -1
- data/lib/twig/node/expression/binary/and.rb +17 -0
- data/lib/twig/node/expression/binary/base.rb +6 -4
- data/lib/twig/node/expression/binary/boolean.rb +24 -0
- data/lib/twig/node/expression/binary/concat.rb +20 -0
- data/lib/twig/node/expression/binary/elvis.rb +35 -0
- data/lib/twig/node/expression/binary/ends_with.rb +24 -0
- data/lib/twig/node/expression/binary/floor_div.rb +21 -0
- data/lib/twig/node/expression/binary/has_every.rb +20 -0
- data/lib/twig/node/expression/binary/has_some.rb +20 -0
- data/lib/twig/node/expression/binary/in.rb +20 -0
- data/lib/twig/node/expression/binary/matches.rb +24 -0
- data/lib/twig/node/expression/binary/not_in.rb +20 -0
- data/lib/twig/node/expression/binary/null_coalesce.rb +49 -0
- data/lib/twig/node/expression/binary/or.rb +15 -0
- data/lib/twig/node/expression/binary/starts_with.rb +24 -0
- data/lib/twig/node/expression/binary/xor.rb +17 -0
- data/lib/twig/node/expression/block_reference.rb +62 -0
- data/lib/twig/node/expression/call.rb +126 -6
- data/lib/twig/node/expression/constant.rb +3 -1
- data/lib/twig/node/expression/filter/default.rb +37 -0
- data/lib/twig/node/expression/filter/raw.rb +31 -0
- data/lib/twig/node/expression/filter.rb +2 -2
- data/lib/twig/node/expression/function.rb +37 -0
- data/lib/twig/node/expression/get_attribute.rb +51 -7
- data/lib/twig/node/expression/hash.rb +75 -0
- data/lib/twig/node/expression/helper_method.rb +6 -18
- data/lib/twig/node/expression/macro_reference.rb +43 -0
- data/lib/twig/node/expression/name.rb +42 -8
- data/lib/twig/node/expression/operator_escape.rb +13 -0
- data/lib/twig/node/expression/parent.rb +28 -0
- data/lib/twig/node/expression/support_defined_test.rb +23 -0
- data/lib/twig/node/expression/ternary.rb +7 -1
- data/lib/twig/node/expression/test/base.rb +26 -0
- data/lib/twig/node/expression/test/constant.rb +35 -0
- data/lib/twig/node/expression/test/defined.rb +33 -0
- data/lib/twig/node/expression/test/divisible_by.rb +23 -0
- data/lib/twig/node/expression/test/even.rb +21 -0
- data/lib/twig/node/expression/test/iterable.rb +21 -0
- data/lib/twig/node/expression/test/mapping.rb +21 -0
- data/lib/twig/node/expression/test/null.rb +21 -0
- data/lib/twig/node/expression/test/odd.rb +21 -0
- data/lib/twig/node/expression/test/same_as.rb +23 -0
- data/lib/twig/node/expression/test/sequence.rb +21 -0
- data/lib/twig/node/expression/unary/base.rb +3 -1
- data/lib/twig/node/expression/unary/not.rb +18 -0
- data/lib/twig/node/expression/unary/spread.rb +18 -0
- data/lib/twig/node/expression/unary/string_cast.rb +18 -0
- data/lib/twig/node/expression/variable/assign_template.rb +35 -0
- data/lib/twig/node/expression/variable/local.rb +35 -0
- data/lib/twig/node/expression/variable/template.rb +54 -0
- data/lib/twig/node/for.rb +38 -8
- data/lib/twig/node/for_loop.rb +0 -22
- data/lib/twig/node/if.rb +4 -1
- data/lib/twig/node/import.rb +32 -0
- data/lib/twig/node/include.rb +38 -8
- data/lib/twig/node/macro.rb +79 -0
- data/lib/twig/node/module.rb +278 -23
- data/lib/twig/node/output.rb +7 -0
- data/lib/twig/node/print.rb +4 -1
- data/lib/twig/node/set.rb +72 -0
- data/lib/twig/node/text.rb +4 -1
- data/lib/twig/node/with.rb +50 -0
- data/lib/twig/node/yield.rb +6 -1
- data/lib/twig/node_traverser.rb +50 -0
- data/lib/twig/node_visitor/base.rb +30 -0
- data/lib/twig/node_visitor/escaper.rb +165 -0
- data/lib/twig/node_visitor/safe_analysis.rb +127 -0
- data/lib/twig/node_visitor/spreader.rb +39 -0
- data/lib/twig/output_buffer.rb +14 -12
- data/lib/twig/parser.rb +281 -8
- data/lib/twig/rails/config.rb +33 -0
- data/lib/twig/rails/engine.rb +44 -0
- data/lib/twig/rails/renderer.rb +41 -0
- data/lib/twig/runtime/argument_spreader.rb +46 -0
- data/lib/twig/runtime/context.rb +154 -0
- data/lib/twig/runtime/enumerable_hash.rb +51 -0
- data/lib/twig/runtime/escaper.rb +155 -0
- data/lib/twig/runtime/loop_context.rb +81 -0
- data/lib/twig/runtime/loop_iterator.rb +60 -0
- data/lib/twig/runtime/spread.rb +21 -0
- data/lib/twig/runtime_loader/base.rb +12 -0
- data/lib/twig/runtime_loader/factory.rb +23 -0
- data/lib/twig/template.rb +267 -14
- data/lib/twig/template_wrapper.rb +42 -0
- data/lib/twig/token.rb +28 -2
- data/lib/twig/token_parser/apply.rb +48 -0
- data/lib/twig/token_parser/auto_escape.rb +45 -0
- data/lib/twig/token_parser/base.rb +26 -0
- data/lib/twig/token_parser/block.rb +4 -4
- data/lib/twig/token_parser/cache.rb +31 -0
- data/lib/twig/token_parser/deprecated.rb +40 -0
- data/lib/twig/token_parser/do.rb +19 -0
- data/lib/twig/token_parser/embed.rb +62 -0
- data/lib/twig/token_parser/extends.rb +4 -3
- data/lib/twig/token_parser/for.rb +14 -9
- data/lib/twig/token_parser/from.rb +57 -0
- data/lib/twig/token_parser/guard.rb +65 -0
- data/lib/twig/token_parser/if.rb +9 -9
- data/lib/twig/token_parser/import.rb +29 -0
- data/lib/twig/token_parser/include.rb +2 -2
- data/lib/twig/token_parser/macro.rb +109 -0
- data/lib/twig/token_parser/set.rb +76 -0
- data/lib/twig/token_parser/use.rb +54 -0
- data/lib/twig/token_parser/with.rb +36 -0
- data/lib/twig/token_parser/yield.rb +7 -7
- data/lib/twig/token_stream.rb +23 -3
- data/lib/twig/twig_filter.rb +20 -0
- data/lib/twig/twig_function.rb +37 -0
- data/lib/twig/twig_test.rb +31 -0
- data/lib/twig/util/callable_arguments_extractor.rb +227 -0
- data/lib/twig_ruby.rb +21 -2
- metadata +148 -6
- data/lib/twig/context.rb +0 -64
- data/lib/twig/expression_parser.rb +0 -517
- data/lib/twig/railtie.rb +0 -60
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Twig
|
|
4
|
+
module TokenParser
|
|
5
|
+
# Defines a macro.
|
|
6
|
+
#
|
|
7
|
+
# {% macro input(name, value, type, size) %}
|
|
8
|
+
# <input type="{{ type|default('text') }}" name="{{ name }}" value="{{ value|e }}" size="{{ size|default(20) }}" />
|
|
9
|
+
# {% endmacro %}
|
|
10
|
+
#
|
|
11
|
+
class Macro < Base
|
|
12
|
+
def parse(token)
|
|
13
|
+
lineno = token.lineno
|
|
14
|
+
stream = parser.stream
|
|
15
|
+
name = stream.expect(Token::NAME_TYPE).value
|
|
16
|
+
arguments = parse_definition
|
|
17
|
+
|
|
18
|
+
stream.expect(Token::BLOCK_END_TYPE)
|
|
19
|
+
parser.push_local_scope
|
|
20
|
+
body = parser.subparse(method(:decide_block_end), drop_needle: true)
|
|
21
|
+
|
|
22
|
+
if (token = stream.next_if(Token::NAME_TYPE))
|
|
23
|
+
value = token.value
|
|
24
|
+
|
|
25
|
+
if value != name
|
|
26
|
+
raise Error::Syntax.new(
|
|
27
|
+
"Expected endmacro for macro \"#{name}\" (but \"#{value}\" given).",
|
|
28
|
+
stream.current.lineno,
|
|
29
|
+
stream.source_context
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
parser.pop_local_scope
|
|
35
|
+
stream.expect(Token::BLOCK_END_TYPE)
|
|
36
|
+
|
|
37
|
+
parser.set_macro(name, Node::Macro.new(name, Node::Body.new({ 0 => body }), arguments, lineno))
|
|
38
|
+
|
|
39
|
+
Node::Empty.new(lineno)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def decide_block_end(token)
|
|
43
|
+
token.test('endmacro')
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def tag
|
|
47
|
+
'macro'
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def parse_definition
|
|
53
|
+
arguments = Node::Expression::Hash.new({}, parser.current_token.lineno)
|
|
54
|
+
stream = parser.stream
|
|
55
|
+
stream.expect(Token::OPERATOR_TYPE, '(', 'A list of arguments must begin with an opening parenthesis')
|
|
56
|
+
|
|
57
|
+
until stream.test(Token::PUNCTUATION_TYPE, ')')
|
|
58
|
+
unless arguments.empty?
|
|
59
|
+
stream.expect(Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma')
|
|
60
|
+
|
|
61
|
+
# if the comma above was a trailing comma, early exit the argument parse loop
|
|
62
|
+
break if stream.test(Token::PUNCTUATION_TYPE, ')')
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
token = stream.expect(Token::NAME_TYPE, nil, 'An argument must be a name')
|
|
66
|
+
name = Node::Expression::Variable::Local.new(token.value, parser.current_token.lineno)
|
|
67
|
+
|
|
68
|
+
if (token = stream.next_if(Token::OPERATOR_TYPE, '=')) ||
|
|
69
|
+
(token = stream.next_if(Token::PUNCTUATION_TYPE, ':'))
|
|
70
|
+
default = parser.parse_expression
|
|
71
|
+
else
|
|
72
|
+
default = Node::Expression::Constant.new(nil, parser.current_token.lineno)
|
|
73
|
+
default.attributes[:is_implicit] = true
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
unless check_constant_expression(default)
|
|
77
|
+
raise Error::Syntax.new(
|
|
78
|
+
'A default value for an argument must be a constant (a boolean, a string, a number, ' \
|
|
79
|
+
'a sequence, or a mapping).',
|
|
80
|
+
token.lineno,
|
|
81
|
+
stream.source
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
arguments.add_element(default, name)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
stream.expect(Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis')
|
|
89
|
+
|
|
90
|
+
arguments
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# checks that the node only contains "constant" elements
|
|
94
|
+
def check_constant_expression(node)
|
|
95
|
+
return false unless node.is_a?(Node::Expression::Constant) ||
|
|
96
|
+
node.is_a?(Node::Expression::Array) ||
|
|
97
|
+
node.is_a?(Node::Expression::Hash) ||
|
|
98
|
+
node.is_a?(Node::Expression::Unary::Neg) ||
|
|
99
|
+
node.is_a?(Node::Expression::Unary::Pos)
|
|
100
|
+
|
|
101
|
+
node.nodes.each_value do |n|
|
|
102
|
+
return false unless check_constant_expression(n)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
true
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Twig
|
|
4
|
+
module TokenParser
|
|
5
|
+
# Defines a variable.
|
|
6
|
+
#
|
|
7
|
+
# {% set foo = 'foo' %}
|
|
8
|
+
# {% set foo = [1, 2] %}
|
|
9
|
+
# {% set foo = {'foo': 'bar'} %}
|
|
10
|
+
# {% set foo = 'foo' ~ 'bar' %}
|
|
11
|
+
# {% set foo, bar = 'foo', 'bar' %}
|
|
12
|
+
# {% set foo %}Some content{% endset %}
|
|
13
|
+
class Set < Base
|
|
14
|
+
def parse(token)
|
|
15
|
+
lineno = token.lineno
|
|
16
|
+
stream = parser.stream
|
|
17
|
+
names = parse_assignment_expression
|
|
18
|
+
capture = false
|
|
19
|
+
|
|
20
|
+
if stream.next_if(Token::OPERATOR_TYPE, '=')
|
|
21
|
+
values = parse_multi_target_expression
|
|
22
|
+
|
|
23
|
+
stream.expect(Token::BLOCK_END_TYPE)
|
|
24
|
+
|
|
25
|
+
if names.length != values.length
|
|
26
|
+
raise Error::Syntax.new(
|
|
27
|
+
'When using set, you must have the same number of variables and assignments',
|
|
28
|
+
stream.current.lineno,
|
|
29
|
+
stream.source
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
else
|
|
33
|
+
capture = true
|
|
34
|
+
|
|
35
|
+
if names.length > 1
|
|
36
|
+
raise Error::Syntax.new(
|
|
37
|
+
'When using set with a block, you cannot have a multi-target',
|
|
38
|
+
stream.current.lineno,
|
|
39
|
+
stream.source
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
stream.expect(Token::BLOCK_END_TYPE)
|
|
44
|
+
values = parser.subparse(method(:decide_block_end), drop_needle: true)
|
|
45
|
+
stream.expect(Token::BLOCK_END_TYPE)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
Node::Set.new(capture, names, values, lineno)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def tag
|
|
52
|
+
'set'
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def decide_block_end(token)
|
|
58
|
+
token.test('endset')
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def parse_multi_target_expression
|
|
62
|
+
targets = AutoHash.new
|
|
63
|
+
|
|
64
|
+
loop do
|
|
65
|
+
targets << parser.parse_expression
|
|
66
|
+
|
|
67
|
+
unless parser.stream.next_if(Token::PUNCTUATION_TYPE, ',')
|
|
68
|
+
break
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
Node::Nodes.new(targets)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Twig
|
|
4
|
+
module TokenParser
|
|
5
|
+
# Imports blocks defined in another template into the current template.
|
|
6
|
+
#
|
|
7
|
+
# {% extends "base.html" %}
|
|
8
|
+
# {% use "blocks.html" %}
|
|
9
|
+
#
|
|
10
|
+
# {% block title %}{% endblock %}
|
|
11
|
+
class Use < Base
|
|
12
|
+
def parse(token)
|
|
13
|
+
template = parser.parse_expression
|
|
14
|
+
stream = parser.stream
|
|
15
|
+
|
|
16
|
+
unless template.is_a?(Node::Expression::Constant)
|
|
17
|
+
raise Error::Syntax.new(
|
|
18
|
+
'The template references in a "use" statement must be a string.',
|
|
19
|
+
stream.current.lineno,
|
|
20
|
+
stream.source
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
targets = {}
|
|
25
|
+
|
|
26
|
+
if stream.next_if('with')
|
|
27
|
+
loop do
|
|
28
|
+
aliased = name = stream.expect(Token::NAME_TYPE).value
|
|
29
|
+
|
|
30
|
+
if stream.next_if('as')
|
|
31
|
+
aliased = stream.expect(Token::NAME_TYPE).value
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
targets[name] = Node::Expression::Constant.new(aliased, -1)
|
|
35
|
+
|
|
36
|
+
unless stream.next_if(Token::PUNCTUATION_TYPE, ',')
|
|
37
|
+
break
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
stream.expect(Token::BLOCK_END_TYPE)
|
|
43
|
+
|
|
44
|
+
parser.add_trait(Node::Nodes.new({ template:, targets: Node::Nodes.new(targets) }))
|
|
45
|
+
|
|
46
|
+
Node::Empty.new(token.lineno)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def tag
|
|
50
|
+
'use'
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Twig
|
|
4
|
+
module TokenParser
|
|
5
|
+
# Creates a nested scope
|
|
6
|
+
class With < Base
|
|
7
|
+
def parse(token)
|
|
8
|
+
stream = parser.stream
|
|
9
|
+
|
|
10
|
+
variables = nil
|
|
11
|
+
only = false
|
|
12
|
+
|
|
13
|
+
unless stream.test(Token::BLOCK_END_TYPE)
|
|
14
|
+
variables = parser.parse_expression
|
|
15
|
+
only = !stream.next_if(Token::NAME_TYPE, 'only').nil?
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
stream.expect(Token::BLOCK_END_TYPE)
|
|
19
|
+
body = parser.subparse(method(:decide_with_end), drop_needle: true)
|
|
20
|
+
stream.expect(Token::BLOCK_END_TYPE)
|
|
21
|
+
|
|
22
|
+
Node::With.new(body, variables, only, token.lineno)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def tag
|
|
26
|
+
'with'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def decide_with_end(token)
|
|
32
|
+
token.test('endwith')
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -6,13 +6,13 @@ module Twig
|
|
|
6
6
|
def parse(token)
|
|
7
7
|
stream = parser.stream
|
|
8
8
|
lineno = token.lineno
|
|
9
|
-
expr = parser.
|
|
9
|
+
expr = parser.parse_expression
|
|
10
10
|
arguments = []
|
|
11
11
|
|
|
12
12
|
stream.expect(Token::NAME_TYPE, 'do')
|
|
13
13
|
|
|
14
|
-
if stream.next_if(Token::
|
|
15
|
-
until stream.test(Token::
|
|
14
|
+
if stream.next_if(Token::OPERATOR_TYPE, '|')
|
|
15
|
+
until stream.test(Token::OPERATOR_TYPE, '|')
|
|
16
16
|
unless arguments.empty?
|
|
17
17
|
stream.expect(Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma')
|
|
18
18
|
end
|
|
@@ -20,11 +20,11 @@ module Twig
|
|
|
20
20
|
arguments.push(stream.expect(Token::NAME_TYPE).value)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
stream.expect(Token::
|
|
23
|
+
stream.expect(Token::OPERATOR_TYPE, '|')
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
stream.expect(Token::BLOCK_END_TYPE)
|
|
27
|
-
body = parser.subparse(decide_yield_end, drop_needle: true)
|
|
27
|
+
body = parser.subparse(method(:decide_yield_end), drop_needle: true)
|
|
28
28
|
stream.expect(Token::BLOCK_END_TYPE)
|
|
29
29
|
|
|
30
30
|
Node::Yield.new(expr, body, arguments, lineno)
|
|
@@ -36,8 +36,8 @@ module Twig
|
|
|
36
36
|
|
|
37
37
|
private
|
|
38
38
|
|
|
39
|
-
def decide_yield_end
|
|
40
|
-
|
|
39
|
+
def decide_yield_end(token)
|
|
40
|
+
token.test('endyield')
|
|
41
41
|
end
|
|
42
42
|
end
|
|
43
43
|
end
|
data/lib/twig/token_stream.rb
CHANGED
|
@@ -39,8 +39,14 @@ module Twig
|
|
|
39
39
|
token = current
|
|
40
40
|
|
|
41
41
|
unless token.test(type, value)
|
|
42
|
+
expected = Token.type_to_english(type)
|
|
43
|
+
unexpected = Token.type_to_english(token.type)
|
|
44
|
+
token_value = token.value.empty? ? '' : " of value \"#{token.value}\""
|
|
45
|
+
value = " with value \"#{value}\"" if value
|
|
46
|
+
message = "#{message} " if message
|
|
47
|
+
|
|
42
48
|
raise Error::Syntax.new(
|
|
43
|
-
"
|
|
49
|
+
"#{message}Unexpected token \"#{unexpected}\"#{token_value} (\"#{expected}\" expected#{value}).",
|
|
44
50
|
token.lineno,
|
|
45
51
|
source
|
|
46
52
|
)
|
|
@@ -51,6 +57,16 @@ module Twig
|
|
|
51
57
|
token
|
|
52
58
|
end
|
|
53
59
|
|
|
60
|
+
# @param [Integer] number
|
|
61
|
+
# @return [Token]
|
|
62
|
+
def look(number = 1)
|
|
63
|
+
unless tokens.length >= @current + number
|
|
64
|
+
raise Error::Syntax.new('Unexpected end of template.', tokens[@current].lineno, source)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
tokens[@current + number]
|
|
68
|
+
end
|
|
69
|
+
|
|
54
70
|
def test(primary, secondary = nil)
|
|
55
71
|
current.test(primary, secondary)
|
|
56
72
|
end
|
|
@@ -66,8 +82,12 @@ module Twig
|
|
|
66
82
|
def debug
|
|
67
83
|
tokens.
|
|
68
84
|
map(&:debug).
|
|
69
|
-
map { |type, value| "#{type}(#{value})" }
|
|
70
|
-
|
|
85
|
+
map { |type, value| "#{type}(#{value})" }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# @param [Array<Token>] tokens
|
|
89
|
+
def inject(tokens)
|
|
90
|
+
@tokens.insert(@current, *tokens)
|
|
71
91
|
end
|
|
72
92
|
end
|
|
73
93
|
end
|
data/lib/twig/twig_filter.rb
CHANGED
|
@@ -14,6 +14,26 @@ module Twig
|
|
|
14
14
|
}.merge(@options)
|
|
15
15
|
end
|
|
16
16
|
|
|
17
|
+
# @param [Node::Base] filter_args
|
|
18
|
+
def safe(filter_args)
|
|
19
|
+
return @options[:is_safe] unless @options[:is_safe].nil?
|
|
20
|
+
return @options[:is_safe_callback].call(filter_args) unless @options[:is_safe_callback].nil?
|
|
21
|
+
|
|
22
|
+
[]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def preserves_safety
|
|
26
|
+
@options[:preserves_safety] || []
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def pre_escape
|
|
30
|
+
@options[:pre_escape]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def type
|
|
34
|
+
:filter
|
|
35
|
+
end
|
|
36
|
+
|
|
17
37
|
def node_class
|
|
18
38
|
@options[:node_class]
|
|
19
39
|
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Twig
|
|
4
|
+
class TwigFunction < Callable
|
|
5
|
+
def initialize(name, callable = nil, options = {})
|
|
6
|
+
super
|
|
7
|
+
|
|
8
|
+
@options = {
|
|
9
|
+
is_safe: nil,
|
|
10
|
+
is_safe_callback: nil,
|
|
11
|
+
node_class: Node::Expression::Function,
|
|
12
|
+
parser_callable: nil,
|
|
13
|
+
}.merge(@options)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @param [Node::Base] function_args
|
|
17
|
+
def safe(function_args)
|
|
18
|
+
return @options[:is_safe] unless @options[:is_safe].nil?
|
|
19
|
+
return @options[:is_safe_callback].call(function_args) unless @options[:is_safe_callback].nil?
|
|
20
|
+
|
|
21
|
+
[]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def type
|
|
25
|
+
:function
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @return [Proc|nil]
|
|
29
|
+
def parser_callable
|
|
30
|
+
@options[:parser_callable]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def node_class
|
|
34
|
+
@options[:node_class]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Twig
|
|
4
|
+
class TwigTest < Callable
|
|
5
|
+
def initialize(name, callable = nil, options = {})
|
|
6
|
+
super
|
|
7
|
+
|
|
8
|
+
@options = {
|
|
9
|
+
node_class: Node::Expression::Test::Base,
|
|
10
|
+
one_mandatory_argument: false,
|
|
11
|
+
}.merge(@options)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def type
|
|
15
|
+
:test
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def node_class
|
|
19
|
+
@options[:node_class]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [Boolean]
|
|
23
|
+
def one_mandatory_argument?
|
|
24
|
+
@options[:one_mandatory_argument]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def needs_context?
|
|
28
|
+
false
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Twig
|
|
4
|
+
module Util
|
|
5
|
+
class CallableArgumentsExtractor
|
|
6
|
+
# @param [Node::Expression::Call] node
|
|
7
|
+
# @param [Callable] twig_callable
|
|
8
|
+
# @param [Environment] environment
|
|
9
|
+
def initialize(node, twig_callable, environment)
|
|
10
|
+
@node = node
|
|
11
|
+
@twig_callable = twig_callable
|
|
12
|
+
@environment = environment
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @param [Node::Nodes] arguments
|
|
16
|
+
def extract_arguments(arguments)
|
|
17
|
+
# Check argument order first
|
|
18
|
+
found_named = false
|
|
19
|
+
arguments.nodes.each_key do |key|
|
|
20
|
+
if key.is_a?(Integer)
|
|
21
|
+
if found_named
|
|
22
|
+
raise Error::Syntax.new(
|
|
23
|
+
"Positional arguments cannot be used after named arguments for #{@twig_callable.type} " \
|
|
24
|
+
"\"#{@twig_callable.name}\".",
|
|
25
|
+
@node.lineno,
|
|
26
|
+
@node.source_context
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
else
|
|
30
|
+
found_named = true
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
called_arguments = destination_arguments.keys.to_h { |k| [k, false] }
|
|
35
|
+
|
|
36
|
+
spreads, arguments = arguments.nodes.partition do |_, node|
|
|
37
|
+
node.is_a?(Node::Expression::Unary::Spread)
|
|
38
|
+
end.map(&:to_h)
|
|
39
|
+
|
|
40
|
+
positional, kwargs = arguments.partition do |key, _node|
|
|
41
|
+
key.is_a?(Integer)
|
|
42
|
+
end.map(&:to_h)
|
|
43
|
+
|
|
44
|
+
positional = positional.values
|
|
45
|
+
positional_count = positional.length
|
|
46
|
+
|
|
47
|
+
kwargs.transform_keys!(&:to_sym)
|
|
48
|
+
resolved_positional = []
|
|
49
|
+
resolved_kwargs = {}
|
|
50
|
+
rest = false
|
|
51
|
+
keyrest = false
|
|
52
|
+
|
|
53
|
+
destination_arguments.each do |name, type|
|
|
54
|
+
if positional.any?
|
|
55
|
+
case type
|
|
56
|
+
when :req, :opt
|
|
57
|
+
resolved_positional << positional.shift
|
|
58
|
+
when :keyreq, :key
|
|
59
|
+
arg = positional.shift
|
|
60
|
+
|
|
61
|
+
if arg.is_a?(Node::Expression::Unary::HashSpread)
|
|
62
|
+
keyrest = true
|
|
63
|
+
resolved_positional << arg
|
|
64
|
+
else
|
|
65
|
+
resolved_kwargs[name] = arg
|
|
66
|
+
end
|
|
67
|
+
when :rest
|
|
68
|
+
resolved_positional += positional
|
|
69
|
+
positional = []
|
|
70
|
+
rest = true
|
|
71
|
+
next
|
|
72
|
+
when :keyrest
|
|
73
|
+
arg = positional.shift
|
|
74
|
+
|
|
75
|
+
if arg.is_a?(Node::Expression::Unary::HashSpread)
|
|
76
|
+
keyrest = true
|
|
77
|
+
resolved_positional << arg
|
|
78
|
+
else
|
|
79
|
+
raise Error::Syntax.new(
|
|
80
|
+
"Expected a hash spread for argument \"#{name}\" " \
|
|
81
|
+
"for #{@twig_callable.type} \"#{@twig_callable.name}\".",
|
|
82
|
+
@node.lineno,
|
|
83
|
+
@node.source_context
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
next
|
|
88
|
+
else
|
|
89
|
+
raise "Unknown argument type: #{name} #{type}"
|
|
90
|
+
end
|
|
91
|
+
elsif kwargs.key?(name) || (found = kwargs.keys.detect { |key| key.to_s.underscore.to_sym == name })
|
|
92
|
+
found = name if found.nil?
|
|
93
|
+
|
|
94
|
+
if %i[opt req].include?(type)
|
|
95
|
+
resolved_positional << kwargs.delete(found)
|
|
96
|
+
else
|
|
97
|
+
resolved_kwargs[name] = kwargs.delete(found)
|
|
98
|
+
end
|
|
99
|
+
else
|
|
100
|
+
case type
|
|
101
|
+
when :opt, :key, :rest
|
|
102
|
+
next
|
|
103
|
+
when :keyrest
|
|
104
|
+
keyrest = true
|
|
105
|
+
resolved_kwargs.merge!(kwargs)
|
|
106
|
+
kwargs = {}
|
|
107
|
+
next
|
|
108
|
+
else
|
|
109
|
+
# If we have spreads, we just can't know until runtime since we don't know if it's a
|
|
110
|
+
# positional spread or kwarg spread because both use ...
|
|
111
|
+
unless spreads.any? || keyrest
|
|
112
|
+
raise Error::Syntax.new(
|
|
113
|
+
"Value for argument \"#{name}\" is required for #{@twig_callable.type} \"#{@twig_callable.name}\".",
|
|
114
|
+
@node.lineno,
|
|
115
|
+
@node.source_context
|
|
116
|
+
)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
called_arguments[name] = true
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# If any of our remaining kwargs intersect with called_arguments then we have a duplicate key
|
|
125
|
+
duplicated = called_arguments.select { |_k, v| v }.keys & kwargs.keys
|
|
126
|
+
duplicated = duplicated.map(&:to_s)
|
|
127
|
+
duplicated = duplicated[0] if duplicated.one?
|
|
128
|
+
|
|
129
|
+
unless duplicated.empty?
|
|
130
|
+
raise Error::Syntax.new(
|
|
131
|
+
"Argument #{duplicated.inspect} is defined twice for #{@twig_callable.type} " \
|
|
132
|
+
"\"#{@twig_callable.name}\".",
|
|
133
|
+
@node.lineno,
|
|
134
|
+
@node.source_context
|
|
135
|
+
)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
unexpected_arguments = []
|
|
139
|
+
unknown_argument = nil
|
|
140
|
+
|
|
141
|
+
# If there's no keyrest and any kwargs left, they are extraneous
|
|
142
|
+
if !keyrest && kwargs.any?
|
|
143
|
+
unexpected_arguments += kwargs.keys
|
|
144
|
+
unknown_argument = kwargs.values.first
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# If there's a rest and any positional left, they are extraneous
|
|
148
|
+
if !rest && positional.any?
|
|
149
|
+
unexpected_arguments += [
|
|
150
|
+
*((positional_count - positional.length)...positional_count),
|
|
151
|
+
]
|
|
152
|
+
unknown_argument = positional.first
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
if unexpected_arguments.any?
|
|
156
|
+
unknown_argument ||= @node
|
|
157
|
+
|
|
158
|
+
raise Error::Syntax.new(
|
|
159
|
+
"Unknown argument \"#{unexpected_arguments.join(', ')}\" " \
|
|
160
|
+
"for #{@twig_callable.type} \"#{@twig_callable.name}(#{destination_arguments.keys.join(', ')})\".",
|
|
161
|
+
unknown_argument.lineno,
|
|
162
|
+
unknown_argument.source_context
|
|
163
|
+
)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
[resolved_positional + spreads.values, resolved_kwargs]
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
private
|
|
170
|
+
|
|
171
|
+
# @return [Callable]
|
|
172
|
+
attr_reader :twig_callable
|
|
173
|
+
|
|
174
|
+
# @return [Environment]
|
|
175
|
+
attr_reader :environment
|
|
176
|
+
|
|
177
|
+
def destination_arguments
|
|
178
|
+
arguments = callable_method.parameters.to_h { |k, v| [v, k] }
|
|
179
|
+
|
|
180
|
+
if @node.nodes.key?(:node)
|
|
181
|
+
arguments.shift
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
if twig_callable.needs_charset?
|
|
185
|
+
arguments.shift
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
if twig_callable.needs_environment?
|
|
189
|
+
arguments.shift
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
if twig_callable.needs_context?
|
|
193
|
+
arguments.shift
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
twig_callable.arguments.each do
|
|
197
|
+
arguments.shift
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
arguments
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# @return [Method]
|
|
204
|
+
def callable_method
|
|
205
|
+
callable = twig_callable.callable
|
|
206
|
+
|
|
207
|
+
case callable
|
|
208
|
+
when ::Array
|
|
209
|
+
if callable[0] == :runtime
|
|
210
|
+
_, klass, method = callable
|
|
211
|
+
|
|
212
|
+
environment.runtime(klass).method(method.to_sym)
|
|
213
|
+
else
|
|
214
|
+
extension, method = callable[0, 2]
|
|
215
|
+
extension = extension.class.name if extension.is_a?(Extension::Base)
|
|
216
|
+
|
|
217
|
+
environment.extension(extension.delete_prefix('::')).method(method.to_sym)
|
|
218
|
+
end
|
|
219
|
+
when ::Method, ::Proc
|
|
220
|
+
callable
|
|
221
|
+
else
|
|
222
|
+
raise "Callable not supported: #{callable.inspect}"
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|