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.
Files changed (168) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +116 -0
  3. data/lib/tasks/twig_parity.rake +278 -0
  4. data/lib/twig/auto_hash.rb +7 -1
  5. data/lib/twig/callable.rb +28 -1
  6. data/lib/twig/compiler.rb +35 -3
  7. data/lib/twig/environment.rb +198 -41
  8. data/lib/twig/error/base.rb +81 -16
  9. data/lib/twig/error/loader.rb +8 -0
  10. data/lib/twig/error/logic.rb +8 -0
  11. data/lib/twig/error/runtime.rb +8 -0
  12. data/lib/twig/expression_parser/base.rb +30 -0
  13. data/lib/twig/expression_parser/expression_parsers.rb +57 -0
  14. data/lib/twig/expression_parser/infix/arrow.rb +31 -0
  15. data/lib/twig/expression_parser/infix/binary.rb +34 -0
  16. data/lib/twig/expression_parser/infix/conditional_ternary.rb +39 -0
  17. data/lib/twig/expression_parser/infix/dot.rb +72 -0
  18. data/lib/twig/expression_parser/infix/filter.rb +43 -0
  19. data/lib/twig/expression_parser/infix/function.rb +67 -0
  20. data/lib/twig/expression_parser/infix/is.rb +53 -0
  21. data/lib/twig/expression_parser/infix/is_not.rb +19 -0
  22. data/lib/twig/expression_parser/infix/parses_arguments.rb +84 -0
  23. data/lib/twig/expression_parser/infix/square_bracket.rb +66 -0
  24. data/lib/twig/expression_parser/infix_expression_parser.rb +34 -0
  25. data/lib/twig/expression_parser/prefix/grouping.rb +60 -0
  26. data/lib/twig/expression_parser/prefix/literal.rb +244 -0
  27. data/lib/twig/expression_parser/prefix/unary.rb +29 -0
  28. data/lib/twig/expression_parser/prefix_expression_parser.rb +18 -0
  29. data/lib/twig/extension/base.rb +26 -4
  30. data/lib/twig/extension/core.rb +1076 -48
  31. data/lib/twig/extension/debug.rb +25 -0
  32. data/lib/twig/extension/escaper.rb +73 -0
  33. data/lib/twig/extension/rails.rb +10 -57
  34. data/lib/twig/extension/string_loader.rb +19 -0
  35. data/lib/twig/extension_set.rb +117 -20
  36. data/lib/twig/file_extension_escaping_strategy.rb +35 -0
  37. data/lib/twig/lexer.rb +225 -81
  38. data/lib/twig/loader/array.rb +25 -8
  39. data/lib/twig/loader/chain.rb +93 -0
  40. data/lib/twig/loader/filesystem.rb +106 -7
  41. data/lib/twig/node/auto_escape.rb +18 -0
  42. data/lib/twig/node/base.rb +58 -2
  43. data/lib/twig/node/block.rb +2 -0
  44. data/lib/twig/node/block_reference.rb +5 -1
  45. data/lib/twig/node/body.rb +7 -0
  46. data/lib/twig/node/cache.rb +50 -0
  47. data/lib/twig/node/capture.rb +22 -0
  48. data/lib/twig/node/deprecated.rb +53 -0
  49. data/lib/twig/node/do.rb +19 -0
  50. data/lib/twig/node/embed.rb +43 -0
  51. data/lib/twig/node/expression/array.rb +29 -20
  52. data/lib/twig/node/expression/arrow_function.rb +55 -0
  53. data/lib/twig/node/expression/assign_name.rb +1 -1
  54. data/lib/twig/node/expression/binary/and.rb +17 -0
  55. data/lib/twig/node/expression/binary/base.rb +6 -4
  56. data/lib/twig/node/expression/binary/boolean.rb +24 -0
  57. data/lib/twig/node/expression/binary/concat.rb +20 -0
  58. data/lib/twig/node/expression/binary/elvis.rb +35 -0
  59. data/lib/twig/node/expression/binary/ends_with.rb +24 -0
  60. data/lib/twig/node/expression/binary/floor_div.rb +21 -0
  61. data/lib/twig/node/expression/binary/has_every.rb +20 -0
  62. data/lib/twig/node/expression/binary/has_some.rb +20 -0
  63. data/lib/twig/node/expression/binary/in.rb +20 -0
  64. data/lib/twig/node/expression/binary/matches.rb +24 -0
  65. data/lib/twig/node/expression/binary/not_in.rb +20 -0
  66. data/lib/twig/node/expression/binary/null_coalesce.rb +49 -0
  67. data/lib/twig/node/expression/binary/or.rb +15 -0
  68. data/lib/twig/node/expression/binary/starts_with.rb +24 -0
  69. data/lib/twig/node/expression/binary/xor.rb +17 -0
  70. data/lib/twig/node/expression/block_reference.rb +62 -0
  71. data/lib/twig/node/expression/call.rb +126 -6
  72. data/lib/twig/node/expression/constant.rb +3 -1
  73. data/lib/twig/node/expression/filter/default.rb +37 -0
  74. data/lib/twig/node/expression/filter/raw.rb +31 -0
  75. data/lib/twig/node/expression/filter.rb +2 -2
  76. data/lib/twig/node/expression/function.rb +37 -0
  77. data/lib/twig/node/expression/get_attribute.rb +51 -7
  78. data/lib/twig/node/expression/hash.rb +75 -0
  79. data/lib/twig/node/expression/helper_method.rb +6 -18
  80. data/lib/twig/node/expression/macro_reference.rb +43 -0
  81. data/lib/twig/node/expression/name.rb +42 -8
  82. data/lib/twig/node/expression/operator_escape.rb +13 -0
  83. data/lib/twig/node/expression/parent.rb +28 -0
  84. data/lib/twig/node/expression/support_defined_test.rb +23 -0
  85. data/lib/twig/node/expression/ternary.rb +7 -1
  86. data/lib/twig/node/expression/test/base.rb +26 -0
  87. data/lib/twig/node/expression/test/constant.rb +35 -0
  88. data/lib/twig/node/expression/test/defined.rb +33 -0
  89. data/lib/twig/node/expression/test/divisible_by.rb +23 -0
  90. data/lib/twig/node/expression/test/even.rb +21 -0
  91. data/lib/twig/node/expression/test/iterable.rb +21 -0
  92. data/lib/twig/node/expression/test/mapping.rb +21 -0
  93. data/lib/twig/node/expression/test/null.rb +21 -0
  94. data/lib/twig/node/expression/test/odd.rb +21 -0
  95. data/lib/twig/node/expression/test/same_as.rb +23 -0
  96. data/lib/twig/node/expression/test/sequence.rb +21 -0
  97. data/lib/twig/node/expression/unary/base.rb +3 -1
  98. data/lib/twig/node/expression/unary/not.rb +18 -0
  99. data/lib/twig/node/expression/unary/spread.rb +18 -0
  100. data/lib/twig/node/expression/unary/string_cast.rb +18 -0
  101. data/lib/twig/node/expression/variable/assign_template.rb +35 -0
  102. data/lib/twig/node/expression/variable/local.rb +35 -0
  103. data/lib/twig/node/expression/variable/template.rb +54 -0
  104. data/lib/twig/node/for.rb +38 -8
  105. data/lib/twig/node/for_loop.rb +0 -22
  106. data/lib/twig/node/if.rb +4 -1
  107. data/lib/twig/node/import.rb +32 -0
  108. data/lib/twig/node/include.rb +38 -8
  109. data/lib/twig/node/macro.rb +79 -0
  110. data/lib/twig/node/module.rb +278 -23
  111. data/lib/twig/node/output.rb +7 -0
  112. data/lib/twig/node/print.rb +4 -1
  113. data/lib/twig/node/set.rb +72 -0
  114. data/lib/twig/node/text.rb +4 -1
  115. data/lib/twig/node/with.rb +50 -0
  116. data/lib/twig/node/yield.rb +6 -1
  117. data/lib/twig/node_traverser.rb +50 -0
  118. data/lib/twig/node_visitor/base.rb +30 -0
  119. data/lib/twig/node_visitor/escaper.rb +165 -0
  120. data/lib/twig/node_visitor/safe_analysis.rb +127 -0
  121. data/lib/twig/node_visitor/spreader.rb +39 -0
  122. data/lib/twig/output_buffer.rb +14 -12
  123. data/lib/twig/parser.rb +281 -8
  124. data/lib/twig/rails/config.rb +33 -0
  125. data/lib/twig/rails/engine.rb +44 -0
  126. data/lib/twig/rails/renderer.rb +41 -0
  127. data/lib/twig/runtime/argument_spreader.rb +46 -0
  128. data/lib/twig/runtime/context.rb +154 -0
  129. data/lib/twig/runtime/enumerable_hash.rb +51 -0
  130. data/lib/twig/runtime/escaper.rb +155 -0
  131. data/lib/twig/runtime/loop_context.rb +81 -0
  132. data/lib/twig/runtime/loop_iterator.rb +60 -0
  133. data/lib/twig/runtime/spread.rb +21 -0
  134. data/lib/twig/runtime_loader/base.rb +12 -0
  135. data/lib/twig/runtime_loader/factory.rb +23 -0
  136. data/lib/twig/template.rb +267 -14
  137. data/lib/twig/template_wrapper.rb +42 -0
  138. data/lib/twig/token.rb +28 -2
  139. data/lib/twig/token_parser/apply.rb +48 -0
  140. data/lib/twig/token_parser/auto_escape.rb +45 -0
  141. data/lib/twig/token_parser/base.rb +26 -0
  142. data/lib/twig/token_parser/block.rb +4 -4
  143. data/lib/twig/token_parser/cache.rb +31 -0
  144. data/lib/twig/token_parser/deprecated.rb +40 -0
  145. data/lib/twig/token_parser/do.rb +19 -0
  146. data/lib/twig/token_parser/embed.rb +62 -0
  147. data/lib/twig/token_parser/extends.rb +4 -3
  148. data/lib/twig/token_parser/for.rb +14 -9
  149. data/lib/twig/token_parser/from.rb +57 -0
  150. data/lib/twig/token_parser/guard.rb +65 -0
  151. data/lib/twig/token_parser/if.rb +9 -9
  152. data/lib/twig/token_parser/import.rb +29 -0
  153. data/lib/twig/token_parser/include.rb +2 -2
  154. data/lib/twig/token_parser/macro.rb +109 -0
  155. data/lib/twig/token_parser/set.rb +76 -0
  156. data/lib/twig/token_parser/use.rb +54 -0
  157. data/lib/twig/token_parser/with.rb +36 -0
  158. data/lib/twig/token_parser/yield.rb +7 -7
  159. data/lib/twig/token_stream.rb +23 -3
  160. data/lib/twig/twig_filter.rb +20 -0
  161. data/lib/twig/twig_function.rb +37 -0
  162. data/lib/twig/twig_test.rb +31 -0
  163. data/lib/twig/util/callable_arguments_extractor.rb +227 -0
  164. data/lib/twig_ruby.rb +21 -2
  165. metadata +148 -6
  166. data/lib/twig/context.rb +0 -64
  167. data/lib/twig/expression_parser.rb +0 -517
  168. 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.expression_parser.parse_expression
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::PUNCTUATION_TYPE, '|')
15
- until stream.test(Token::PUNCTUATION_TYPE, '|')
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::PUNCTUATION_TYPE, '|')
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
- ->(token) { token.test('endyield') }
39
+ def decide_yield_end(token)
40
+ token.test('endyield')
41
41
  end
42
42
  end
43
43
  end
@@ -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
- "Expected #{type}(#{value}) but got #{token.type}(#{token.value}) #{message}".rstrip,
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
- join("\n")
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
@@ -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