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
@@ -3,85 +3,1113 @@
3
3
  module Twig
4
4
  module Extension
5
5
  class Core < Base
6
- def operators
7
- unary = Node::Expression::Unary
8
- binary = Node::Expression::Binary
6
+ DEFAULT_TRIM_CHARS = " \t\n\r\0\x0B"
7
+
8
+ class << self
9
+ include ActiveSupport::NumberHelper
10
+ end
11
+
12
+ def initialize
13
+ super
14
+
15
+ @date_format = '%B %-e, %Y %H:%M'
16
+ @number_format = [0, '.', ',']
17
+ end
18
+
19
+ attr_writer :date_format, :number_format, :timezone
20
+
21
+ def expression_parsers
22
+ unary = ExpressionParser::Prefix::Unary
23
+ binary = ExpressionParser::Infix::Binary
9
24
 
10
25
  [
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
- },
26
+ # Unary operators
27
+ unary.new(Node::Expression::Unary::Not, 'not', 70),
28
+ unary.new(Node::Expression::Unary::Spread, '...', 512, description: 'Spread Operator'),
29
+ unary.new(Node::Expression::Unary::Neg, '-', 500),
30
+ unary.new(Node::Expression::Unary::Pos, '+', 500),
31
+
32
+ # Binary operators
33
+ binary.new(
34
+ Node::Expression::Binary::Elvis, '?:', 5, binary::RIGHT,
35
+ description: 'Elvis operator (a ?: b)', aliases: ['? :']
36
+ ),
37
+ binary.new(
38
+ Node::Expression::Binary::NullCoalesce, '??', 5, binary::RIGHT,
39
+ description: 'Null coalescing operator (a ?? b)'
40
+ ),
41
+ binary.new(Node::Expression::Binary::Or, 'or', 10),
42
+ binary.new(Node::Expression::Binary::Xor, 'xor', 12),
43
+ binary.new(Node::Expression::Binary::And, 'and', 15),
44
+ binary.new(Node::Expression::Binary::BitwiseOr, 'b-or', 16),
45
+ binary.new(Node::Expression::Binary::BitwiseXor, 'b-xor', 17),
46
+ binary.new(Node::Expression::Binary::BitwiseAnd, 'b-and', 16),
47
+ binary.new(Node::Expression::Binary::Equal, '==', 20),
48
+ binary.new(Node::Expression::Binary::NotEqual, '!=', 20),
49
+ binary.new(Node::Expression::Binary::Spaceship, '<=>', 20),
50
+ binary.new(Node::Expression::Binary::Less, '<', 20),
51
+ binary.new(Node::Expression::Binary::Greater, '>', 20),
52
+ binary.new(Node::Expression::Binary::LessEqual, '<=', 20),
53
+ binary.new(Node::Expression::Binary::GreaterEqual, '>=', 20),
54
+ binary.new(Node::Expression::Binary::NotIn, 'not in', 20),
55
+ binary.new(Node::Expression::Binary::In, 'in', 20),
56
+ binary.new(Node::Expression::Binary::Matches, 'matches', 20),
57
+ binary.new(Node::Expression::Binary::StartsWith, 'starts with', 20),
58
+ binary.new(Node::Expression::Binary::EndsWith, 'ends with', 20),
59
+ binary.new(Node::Expression::Binary::HasSome, 'has some', 20),
60
+ binary.new(Node::Expression::Binary::HasEvery, 'has every', 20),
61
+ binary.new(Node::Expression::Binary::Range, '..', 25),
62
+ binary.new(Node::Expression::Binary::Add, '+', 30),
63
+ binary.new(Node::Expression::Binary::Sub, '-', 30),
64
+ binary.new(Node::Expression::Binary::Concat, '~', 27),
65
+ binary.new(Node::Expression::Binary::Mul, '*', 60),
66
+ binary.new(Node::Expression::Binary::Div, '/', 60),
67
+ binary.new(Node::Expression::Binary::FloorDiv, '//', 60, description: 'Floor division'),
68
+ binary.new(Node::Expression::Binary::Mod, '%', 60),
69
+ binary.new(Node::Expression::Binary::Power, '**', 200, binary::RIGHT, description: 'Exponentiation operator'),
70
+
71
+ # Ternary operator
72
+ ExpressionParser::Infix::ConditionalTernary.new,
73
+
74
+ # Twig callables
75
+ ExpressionParser::Infix::Is.new,
76
+ ExpressionParser::Infix::IsNot.new,
77
+ ExpressionParser::Infix::Filter.new,
78
+ ExpressionParser::Infix::Function.new,
79
+
80
+ # Get attribute operators
81
+ ExpressionParser::Infix::Dot.new,
82
+ ExpressionParser::Infix::SquareBracket.new,
83
+
84
+ # Group expression
85
+ ExpressionParser::Prefix::Grouping.new,
86
+
87
+ # Arrow function
88
+ ExpressionParser::Infix::Arrow.new,
89
+
90
+ # All literals
91
+ ExpressionParser::Prefix::Literal.new,
38
92
  ]
39
93
  end
40
94
 
41
95
  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
- }
96
+ [
97
+ # Formatting filters
98
+ TwigFilter.new('date', method(:format_date)),
99
+ TwigFilter.new('format', static(:sprintf)),
100
+ TwigFilter.new('replace', static(:replace)),
101
+ TwigFilter.new('number_format', method(:number_format)),
102
+ TwigFilter.new('abs', static(:abs)),
103
+ TwigFilter.new('round', static(:round)),
104
+
105
+ # Encoding
106
+ TwigFilter.new('url_encode', static(:url_encode)),
107
+ TwigFilter.new('json_encode', static(:json_encode)),
108
+ TwigFilter.new('convert_encoding', static(:convert_encoding)),
109
+
110
+ # Strings
111
+ TwigFilter.new('title', static(:title_case)),
112
+ TwigFilter.new('capitalize', static(:capitalize)),
113
+ TwigFilter.new('upper', static(:upper)),
114
+ TwigFilter.new('lower', static(:lower)),
115
+ TwigFilter.new('striptags', static(:strip_tags)),
116
+ TwigFilter.new('trim', static(:trim)),
117
+ TwigFilter.new('nl2br', static(:nl2br), { pre_escape: :html, is_safe: [:html] }),
118
+ TwigFilter.new('plural', static(:pluralize)),
119
+ TwigFilter.new('singular', static(:singularize)),
120
+ TwigFilter.new('slug', static(:slug)),
121
+
122
+ # array helpers
123
+ TwigFilter.new('join', static(:join)),
124
+ TwigFilter.new('split', static(:split), needs_charset: true),
125
+ TwigFilter.new('sort', static(:sort)),
126
+ TwigFilter.new('merge', static(:merge)),
127
+ TwigFilter.new('batch', static(:batch)),
128
+ TwigFilter.new('column', static(:column)),
129
+ TwigFilter.new('filter', static(:filter)),
130
+ TwigFilter.new('map', static(:map)),
131
+ TwigFilter.new('reduce', static(:reduce)),
132
+ TwigFilter.new('find', static(:find)),
133
+
134
+ # Arrays / Hashes filters
135
+ TwigFilter.new('reverse', static(:reverse)),
136
+ TwigFilter.new('shuffle', static(:shuffle)),
137
+ TwigFilter.new('length', static(:length)),
138
+ TwigFilter.new('slice', static(:slice)),
139
+ TwigFilter.new('first', static(:first)),
140
+ TwigFilter.new('last', static(:last)),
141
+
142
+ # iteration and runtime
143
+ TwigFilter.new('keys', static(:keys)),
144
+ TwigFilter.new('values', static(:values)),
145
+ TwigFilter.new('default', static(:default), {
146
+ node_class: Node::Expression::Filter::Default,
147
+ }),
148
+ TwigFilter.new('invoke', static(:invoke)),
149
+ ]
150
+ end
151
+
152
+ def functions
153
+ [
154
+ TwigFunction.new('parent', nil, {
155
+ parser_callable: static(:parse_parent_function),
156
+ }),
157
+ TwigFunction.new('block', nil, {
158
+ parser_callable: static(:parse_block_function),
159
+ }),
160
+ TwigFunction.new('loop', nil, {
161
+ parser_callable: static(:parse_loop_function),
162
+ }),
163
+ TwigFunction.new('max', static(:max)),
164
+ TwigFunction.new('min', static(:min)),
165
+ TwigFunction.new('range', static(:range)),
166
+ TwigFunction.new('constant', static(:constant)),
167
+ TwigFunction.new('cycle', static(:cycle)),
168
+ TwigFunction.new('random', static(:random), needs_charset: true),
169
+ TwigFunction.new('date', method(:convert_date)),
170
+ TwigFunction.new('include', static(:include), {
171
+ needs_environment: true, needs_context: true, is_safe: [:all]
172
+ }),
173
+ TwigFunction.new('source', static(:source), {
174
+ needs_environment: true, is_safe: [:all]
175
+ }),
176
+ ]
177
+ end
178
+
179
+ def tests
180
+ [
181
+ TwigTest.new('even', nil, { node_class: Node::Expression::Test::Even }),
182
+ TwigTest.new('odd', nil, { node_class: Node::Expression::Test::Odd }),
183
+ TwigTest.new('defined', nil, { node_class: Node::Expression::Test::Defined }),
184
+ TwigTest.new('same as', nil, {
185
+ node_class: Node::Expression::Test::SameAs, one_mandatory_argument: true
186
+ }),
187
+ TwigTest.new('null', nil, { node_class: Node::Expression::Test::Null }),
188
+ TwigTest.new('nil', nil, { node_class: Node::Expression::Test::Null }),
189
+ TwigTest.new('none', nil, { node_class: Node::Expression::Test::Null }),
190
+ TwigTest.new('divisible by', nil, {
191
+ node_class: Node::Expression::Test::DivisibleBy, one_mandatory_argument: true
192
+ }),
193
+ TwigTest.new('constant', nil, { node_class: Node::Expression::Test::Constant }),
194
+ TwigTest.new('empty', static(:test_empty?)),
195
+ TwigTest.new('iterable', nil, { node_class: Node::Expression::Test::Iterable }),
196
+ TwigTest.new('sequence', nil, { node_class: Node::Expression::Test::Sequence }),
197
+ TwigTest.new('mapping', nil, { node_class: Node::Expression::Test::Mapping }),
198
+ ]
48
199
  end
49
200
 
50
201
  def token_parsers
51
202
  [
203
+ TokenParser::Apply.new,
52
204
  TokenParser::Block.new,
205
+ TokenParser::Deprecated.new,
206
+ TokenParser::Do.new,
207
+ TokenParser::Embed.new,
53
208
  TokenParser::Extends.new,
54
209
  TokenParser::For.new,
210
+ TokenParser::From.new,
211
+ TokenParser::Guard.new,
212
+ TokenParser::Macro.new,
55
213
  TokenParser::If.new,
214
+ TokenParser::Import.new,
56
215
  TokenParser::Include.new,
216
+ TokenParser::Set.new,
217
+ TokenParser::Use.new,
218
+ TokenParser::With.new,
57
219
  TokenParser::Yield.new,
58
220
  ]
59
221
  end
60
222
 
61
- def capitalize(string)
62
- string.capitalize
223
+ def node_visitors
224
+ [
225
+ NodeVisitor::Spreader.new,
226
+ ]
227
+ end
228
+
229
+ def format_date(date, format: nil, timezone: self.timezone)
230
+ format ||= @date_format
231
+
232
+ convert_date(date, timezone:).strftime(format)
233
+ end
234
+
235
+ def convert_date(date = nil, timezone: self.timezone)
236
+ if date == 'now' || date.nil?
237
+ date = DateTime.now
238
+ elsif date.is_a?(Integer)
239
+ date = Time.then { |t| timezone == false ? t : t.zone }.at(date).to_datetime
240
+ elsif date.is_a?(String)
241
+ date = Time.then { |t| timezone == false ? t : t.zone }.parse(date)
242
+ end
243
+
244
+ timezone == false ? date : date.in_time_zone(timezone)
245
+ end
246
+
247
+ def timezone
248
+ @timezone ||= Time.zone
249
+ end
250
+
251
+ def self.sprintf(string, *values)
252
+ format(string || '', *values)
253
+ end
254
+
255
+ def self.replace(string, from)
256
+ return if string.nil?
257
+
258
+ unless from.is_a?(Hash) || from.is_a?(Array)
259
+ raise Error::Runtime, "The \"replace\" filter expects a sequence or a mapping, got \"#{from.class}\"."
260
+ end
261
+
262
+ from = ensure_hash(from)
263
+ regex = Regexp.union(
264
+ *from.keys.map(&:to_s)
265
+ )
266
+
267
+ string.gsub(regex, from.transform_keys(&:to_s))
268
+ end
269
+
270
+ def number_format(number, decimal: nil, decimal_point: nil, thousands_separator: nil)
271
+ number = 0 if number.nil? || number == ''
272
+ decimal ||= @number_format[0]
273
+ decimal_point ||= @number_format[1]
274
+ thousands_separator ||= @number_format[2]
275
+
276
+ options = {
277
+ precision: decimal,
278
+ delimiter: thousands_separator,
279
+ separator: decimal_point,
280
+ }.compact
281
+
282
+ self.class.number_to_delimited(
283
+ self.class.number_to_rounded(number, options),
284
+ options
285
+ )
286
+ end
287
+
288
+ def self.abs(number)
289
+ number.abs
290
+ end
291
+
292
+ def self.round(value, precision: 0, method: :common)
293
+ value = value.to_f
294
+ method = method.to_sym
295
+
296
+ return value.round(precision) if method == :common
297
+
298
+ unless %i[ceil floor].include?(method)
299
+ raise Error::Runtime, 'The "round" filter only supports the "common", "ceil", and "floor" methods'
300
+ end
301
+
302
+ rounded = (value * (10.0**precision)).public_send(method) / (10.0**precision)
303
+ rounded = rounded.to_i unless precision.positive?
304
+
305
+ rounded&.zero? ? 0 : rounded
306
+ end
307
+
308
+ def self.max(*args)
309
+ args = args[0] if args&.length&.== 1
310
+ args = args.values if args.is_a?(Hash)
311
+ args.max
312
+ end
313
+
314
+ def self.min(*args)
315
+ args = args[0] if args&.length&.== 1
316
+ args = args.values if args.is_a?(Hash)
317
+ args.min
318
+ end
319
+
320
+ def self.range(low, high, step: 1)
321
+ Range.new(low, high).step(step)
322
+ end
323
+
324
+ # @param [String] constant
325
+ # @param [Object, nil] object
326
+ # @param [Boolean] defined_test
327
+ def self.constant(constant, object = nil, defined_test: false)
328
+ unless object.nil?
329
+ if defined_test
330
+ return object.class.const_defined?(constant)
331
+ end
332
+
333
+ return object.class.const_get(constant)
334
+ end
335
+
336
+ if constant.include?('::')
337
+ class_name, _, constant = constant.rpartition('::')
338
+ else
339
+ class_name = Kernel.name
340
+ end
341
+
342
+ unless Object.const_defined?(class_name)
343
+ return false if defined_test
344
+
345
+ raise Error::Runtime, "Class #{class_name} does not exist."
346
+ end
347
+
348
+ klass = Object.const_get(class_name)
349
+
350
+ unless klass.const_defined?(constant)
351
+ return false if defined_test
352
+
353
+ raise Error::Runtime, "Class #{class_name} does not have a constant #{constant}."
354
+ end
355
+
356
+ return true if defined_test
357
+
358
+ klass.const_get(constant)
359
+ end
360
+
361
+ def self.cycle(values, position)
362
+ unless values.respond_to?(:[])
363
+ raise Error::Runtime, 'The "cycle" function only works with arrays'
364
+ end
365
+
366
+ unless values.respond_to?(:length)
367
+ raise Error::Runtime, 'The "cycle" function expects a countable sequence as first argument.'
368
+ end
369
+
370
+ unless values.length.positive?
371
+ raise Error::Runtime, 'The "cycle" function expects a non-empty sequence.'
372
+ end
373
+
374
+ values[position % values.length]
375
+ end
376
+
377
+ def self.url_encode(url)
378
+ if url.respond_to?(:map)
379
+ require 'uri'
380
+ URI.encode_www_form(url || {}).gsub('+', '%20')
381
+ else
382
+ require 'cgi'
383
+ CGI.escape(url || '').gsub('+', '%20')
384
+ end
385
+ end
386
+
387
+ def self.json_encode(object)
388
+ object.respond_to?(:to_json) ? object.to_json : '{}'
389
+ end
390
+
391
+ def self.convert_encoding(string, to, from)
392
+ (string || '').to_s.encode(to, from)
393
+ end
394
+
395
+ def self.title_case(string)
396
+ string&.titleize
397
+ end
398
+
399
+ def self.capitalize(string)
400
+ string&.capitalize
401
+ end
402
+
403
+ def self.upper(string)
404
+ string&.upcase
405
+ end
406
+
407
+ def self.lower(string)
408
+ string&.downcase
409
+ end
410
+
411
+ def self.strip_tags(string, tags: [])
412
+ Sanitize.fragment(string || '', elements: tags)
413
+ end
414
+
415
+ def self.trim(string, character_mask: DEFAULT_TRIM_CHARS, side: :both)
416
+ return string if character_mask.nil? || character_mask.empty?
417
+ return if string.nil?
418
+
419
+ side = side.to_sym
420
+ safe = string.html_safe?
421
+
422
+ unless %i[left right both].include?(side)
423
+ raise Error::Runtime, 'Trimming side must be "left", "right" or "both".'
424
+ end
425
+
426
+ if %i[left both].include?(side)
427
+ string = string.gsub(/\A[#{Regexp.escape(character_mask)}]*/, '')
428
+ end
429
+
430
+ if %i[right both].include?(side)
431
+ string = string.gsub(/[#{Regexp.escape(character_mask)}]*\z/, '')
432
+ end
433
+
434
+ safe && character_mask == DEFAULT_TRIM_CHARS ? string.html_safe : string
435
+ end
436
+
437
+ def self.nl2br(string)
438
+ string.gsub("\n", "<br>\n")
439
+ end
440
+
441
+ def self.singularize(string, count = nil)
442
+ string.singularize(count)
443
+ end
444
+
445
+ def self.pluralize(string, count = nil)
446
+ string.pluralize(count)
447
+ end
448
+
449
+ def self.slug(string, separator: '-', locale: 'en')
450
+ string.parameterize(separator:, locale:)
451
+ end
452
+
453
+ def self.join(value, glue: '', and_glue: nil)
454
+ return value unless value.respond_to?(:to_a)
455
+
456
+ value = value.values if value.respond_to?(:values)
457
+ value = value.to_a if value.respond_to?(:to_a)
458
+
459
+ return value.join(glue) if and_glue.nil? || and_glue == glue
460
+ return value[0] if value.length == 1
461
+
462
+ value[..-2].join(glue) + and_glue.to_s + value[-1].to_s
463
+ end
464
+
465
+ def self.split(charset, value, delimiter, limit: nil)
466
+ value ||= ''
467
+
468
+ unless delimiter == ''
469
+ return value.split(delimiter) if limit.nil?
470
+ return value.split(delimiter, limit) if limit >= 0
471
+
472
+ return value.split(delimiter)[0...limit]
473
+ end
474
+
475
+ if limit.nil? || limit <= 1
476
+ return value.chars
477
+ end
478
+
479
+ length = value.length
480
+
481
+ if limit.nil? || length < limit
482
+ return [value]
483
+ end
484
+
485
+ [*0..(length / limit).ceil].map do |i|
486
+ value[i * limit, limit]
487
+ end
488
+ end
489
+
490
+ # @param [Hash, Array, Enumerable] object
491
+ def self.sort(object, arrow = nil)
492
+ if arrow.nil?
493
+ object.is_a?(Hash) ? object.sort { |a, b| a[1] <=> b[1] }.to_h : object.sort
494
+ else
495
+ object.sort { |a, b| arrow.call(a, b) }.then do |sorted|
496
+ object.is_a?(Hash) ? sorted.to_h : sorted
497
+ end
498
+ end
499
+ end
500
+
501
+ def self.merge(first, *rest)
502
+ if first.is_a?(Hash)
503
+ [first, *rest].reduce(&:merge)
504
+ else
505
+ [first, *rest].reduce do |array, current|
506
+ array.concat(current.respond_to?(:values) ? current.values : current)
507
+ end
508
+ end
509
+ end
510
+
511
+ # @param [Array, Hash] object
512
+ # @param [Integer, Float] count
513
+ # @param [Object] fill
514
+ # @param [Boolean] preserve_keys
515
+ def self.batch(object, count, fill: nil, preserve_keys: true)
516
+ return if object.nil?
517
+
518
+ hash = object.is_a?(Array) ? object.each_with_index.to_h { |k, v| [v, k] } : object
519
+ size = count.ceil
520
+ last_key = 0
521
+
522
+ result = hash.each_slice(size).map do |slice|
523
+ unless preserve_keys
524
+ slice = [*0...size].zip(slice.to_h.values)
525
+ end
526
+
527
+ sliced_hash = slice.to_h
528
+ last_key = sliced_hash.keys[-1]
529
+
530
+ sliced_hash
531
+ end
532
+
533
+ if fill.nil? || result.empty?
534
+ return result
535
+ end
536
+
537
+ result[-1] = AutoHash.new.merge(result[-1])
538
+
539
+ [*0...(size - result[-1].length)].each do
540
+ result[-1].add(fill)
541
+ end
542
+
543
+ result
544
+ end
545
+
546
+ def self.column(object, column)
547
+ object.map { |o| o[column] }
63
548
  end
64
549
 
65
- def upper(string)
66
- string.upcase
550
+ def self.filter(object, proc)
551
+ enumerable_function(object, :filter, proc)
67
552
  end
68
553
 
69
- def lower(string)
70
- string.downcase
554
+ def self.map(object, proc)
555
+ result = enumerable_function(object, :map, proc)
556
+
557
+ if object.is_a?(Hash)
558
+ object.keys.zip(result).to_h
559
+ else
560
+ result
561
+ end
562
+ end
563
+
564
+ def self.reduce(object, proc, initial = nil)
565
+ accumulator = initial
566
+
567
+ case proc.arity
568
+ when 2
569
+ (object.is_a?(Hash) ? object.values : object).each do |value|
570
+ accumulator = proc.call(accumulator, value)
571
+ end
572
+ when 3
573
+ (object.is_a?(Hash) ? object : object.each_with_index).each do |key, value|
574
+ accumulator = proc.call(accumulator, value, key)
575
+ end
576
+ else
577
+ raise Error::Runtime, "Reduce takes 2 or 3 arguments, given #{proc.arity}."
578
+ end
579
+
580
+ accumulator
581
+ end
582
+
583
+ def self.array_every?(object, proc)
584
+ unless object.respond_to?(:all?)
585
+ raise Error::Runtime, "The \"has every\" test expects a sequence or a mapping, got \"#{object.class.name}\"."
586
+ end
587
+
588
+ if object.is_a?(Hash)
589
+ object.each do |k, v|
590
+ if proc.arity == 1
591
+ return false unless proc.call(v)
592
+ elsif !proc.call(v, k)
593
+ return false
594
+ end
595
+ end
596
+
597
+ true
598
+ else
599
+ object.all?(&proc)
600
+ end
601
+ end
602
+
603
+ def self.array_some?(object, proc)
604
+ unless object.respond_to?(:any?)
605
+ raise Error::Runtime, "The \"has some\" test expects a sequence or a mapping, got \"#{object.class.name}\"."
606
+ end
607
+
608
+ if object.is_a?(Hash)
609
+ object.each do |k, v|
610
+ if proc.arity == 1
611
+ return true if proc.call(v)
612
+ elsif proc.call(v, k)
613
+ return true
614
+ end
615
+ end
616
+
617
+ false
618
+ else
619
+ object.any?(&proc)
620
+ end
621
+ end
622
+
623
+ def self.find(object, proc)
624
+ enumerable_function(object, :find, proc)&.dig(1)
625
+ end
626
+
627
+ def self.reverse(object, preserve_keys: false)
628
+ return if object.nil?
629
+
630
+ object.is_a?(Hash) ? object.to_a.reverse.to_h : object.reverse
631
+ end
632
+
633
+ def self.shuffle(object)
634
+ return object.chars.shuffle.join if object.is_a?(String)
635
+
636
+ object.is_a?(Hash) ? object.values.shuffle : object.shuffle
637
+ end
638
+
639
+ def self.length(object)
640
+ return object.length if object.respond_to?(:length)
641
+ return object.count if object.respond_to?(:count)
642
+ return object.to_s.length unless object.method(:to_s).owner == Kernel
643
+
644
+ 1
645
+ end
646
+
647
+ def self.slice(object, start, length = nil, preserve_keys: false)
648
+ if object.is_a?(Hash)
649
+ object, keys = [object.values, object.keys]
650
+ preserve_keys = true
651
+ end
652
+
653
+ values = length.nil? ? object[start...] : object[start, length]
654
+
655
+ return values unless preserve_keys
656
+
657
+ keys ||= [*0...object.length]
658
+ keys = length.nil? ? keys[start...] : keys[start, length]
659
+ keys.zip(values).to_h
660
+ end
661
+
662
+ def self.first(object)
663
+ return if object.nil?
664
+ return object[0] if object.is_a?(String)
665
+
666
+ (object.is_a?(Hash) ? object.values : object).first
667
+ end
668
+
669
+ def self.last(object)
670
+ return if object.nil?
671
+ return object[-1] if object.is_a?(String)
672
+
673
+ (object.is_a?(Hash) ? object.values : object).last
674
+ end
675
+
676
+ def self.keys(object)
677
+ return object.keys if object.respond_to?(:keys)
678
+
679
+ (0...object.length).to_a
680
+ end
681
+
682
+ def self.values(object)
683
+ return object.values if object.respond_to?(:values)
684
+
685
+ object.to_a
686
+ end
687
+
688
+ def self.default(object, default = nil)
689
+ present = object.respond_to?(:empty?) ? !object.empty? : !!object
690
+
691
+ present ? object : default
692
+ end
693
+
694
+ def self.invoke(callable, *, **)
695
+ callable.call(*, **)
696
+ end
697
+
698
+ def self.random(charset, values = nil, max = nil)
699
+ if values.nil?
700
+ return max.nil? ? rand : rand(0..max.to_i)
701
+ end
702
+
703
+ if values.is_a?(Integer) || values.is_a?(Float)
704
+ if max.nil?
705
+ if values.negative?
706
+ max = 0
707
+ min = values
708
+ else
709
+ max = values
710
+ min = 0
711
+ end
712
+ else
713
+ min = values
714
+ end
715
+
716
+ return rand(min.to_i..max.to_i)
717
+ end
718
+
719
+ if values.is_a?(String)
720
+ return '' if values.empty?
721
+
722
+ if charset != 'UTF-8'
723
+ values = convert_encoding(values, 'UTF-8', charset)
724
+ end
725
+
726
+ # Unicode version of string split - split at all positions, but not after start and not before end
727
+ values = values.chars
728
+
729
+ if charset != 'UTF-8'
730
+ values = values.map { |value| convert_encoding(value, charset, 'UTF-8') }
731
+ end
732
+ end
733
+
734
+ # Check if values is iterable (responds to each and has length/size)
735
+ unless values.respond_to?(:each) && (values.respond_to?(:length) || values.respond_to?(:size))
736
+ return values
737
+ end
738
+
739
+ # Convert to array if it's a hash or other enumerable
740
+ values = values.is_a?(Hash) ? values.values : values.to_a
741
+
742
+ if values.empty?
743
+ raise Error::Runtime, 'The "random" function cannot pick from an empty sequence or mapping.'
744
+ end
745
+
746
+ values.sample
71
747
  end
72
748
 
73
749
  def self.ensure_hash(value)
74
- return value if value.class < Hash
750
+ return value.to_h if value.is_a?(Hash)
75
751
 
76
752
  AutoHash.new.add(*value)
77
753
  end
78
754
 
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])
755
+ def self.numeric?(value)
756
+ Float(value, exception: false)
757
+ end
758
+
759
+ def self.compare(a, b)
760
+ trim_var = ->(value) { trim(value, character_mask: " \t\n\r\v\f") }
761
+
762
+ if a.is_a?(Integer) && b.is_a?(String)
763
+ b_trim = trim_var.call(b)
764
+
765
+ unless numeric?(b_trim)
766
+ return a.to_s <=> b
767
+ end
768
+
769
+ if b_trim.to_i.to_s == b_trim
770
+ return a <=> b_trim.to_i
771
+ else
772
+ return a.to_f <=> b_trim.to_f
773
+ end
774
+ end
775
+
776
+ if a.is_a?(String) && b.is_a?(Integer)
777
+ a_trim = trim_var.call(a)
778
+
779
+ unless numeric?(a_trim)
780
+ return a <=> b.to_s
781
+ end
782
+
783
+ if a_trim.to_i.to_s == a_trim
784
+ return a_trim.to_i <=> b
785
+ else
786
+ return a_trim.to_f <=> b.to_f
787
+ end
788
+ end
789
+
790
+ if (a.is_a?(Float) || a.is_a?(Complex)) && b.is_a?(String)
791
+ if a.is_a?(Complex)
792
+ return 1
793
+ end
794
+
795
+ b_trim = trim_var.call(b)
796
+ unless numeric?(b_trim)
797
+ return a.to_s <=> b
798
+ end
799
+
800
+ return a <=> b_trim.to_f
801
+ end
802
+
803
+ if a.is_a?(String) && (b.is_a?(Float) || b.is_a?(Complex))
804
+ if b.is_a?(Complex)
805
+ return 1
806
+ end
807
+
808
+ a_trim = trim_var.call(a)
809
+ unless numeric?(a_trim)
810
+ return a <=> b.to_s
811
+ end
812
+
813
+ return a_trim.to_f <=> b
814
+ end
815
+
816
+ if a.is_a?(String) && b.is_a?(Symbol)
817
+ return a <=> b.to_s
818
+ end
819
+
820
+ if a.is_a?(Symbol) && b.is_a?(String)
821
+ return a.to_s <=> b
822
+ end
823
+
824
+ a <=> b
825
+ end
826
+
827
+ def self.in_filter(value, object)
828
+ return false unless object.respond_to?(:include?)
829
+
830
+ if object.is_a?(String)
831
+ return object.include?(value.to_s)
832
+ end
833
+
834
+ unless object.respond_to?(:any?)
835
+ return false
836
+ end
837
+
838
+ object.any? do |k, v|
839
+ (!v.nil? && compare(value, v)&.zero?) ||
840
+ compare(value, k)&.zero?
841
+ end ||
842
+ object.any? { |v| compare(value, v)&.zero? } ||
843
+ (value == false && in_filter(0, object)) ||
844
+ (value == [] && in_filter(false, object)) ||
845
+ (value == true && in_filter(1, object))
846
+ end
847
+
848
+ def self.matches(regexp, string)
849
+ return false if string.nil?
850
+
851
+ if regexp.respond_to?(:match) && (matches = regexp.match(%r{\A/([^/]*)/(.*)\z}))
852
+ modifiers = matches[2].empty? ? '' : "(?#{matches[2]})"
853
+ regex = /#{modifiers}#{matches[1]}/
854
+ else
855
+ regex = /#{regex}/
856
+ end
857
+
858
+ string.to_s.match?(regex)
859
+ rescue RegexpError => e
860
+ raise Error::Runtime, "Invalid regular expression passed to matches: #{e.message}"
861
+ end
862
+
863
+ # @param [Environment] environment
864
+ def self.get_attribute(
865
+ environment, source, object, attribute, type, arguments: {}, defined_test: false,
866
+ ignore_strict_check: false, lineno: -1, &
867
+ )
868
+ if type == Template::ARRAY_CALL || object.respond_to?(:[])
869
+ if object.respond_to?(:[]) && (
870
+ (object.is_a?(Array) && attribute.is_a?(Integer) && attribute < object.length) ||
871
+ (
872
+ object.is_a?(Hash) && (
873
+ object.key?(attribute) ||
874
+ (attribute.respond_to?(:to_sym) && object.key?(attribute.to_sym))
875
+ )
876
+ )
877
+ )
878
+ return true if defined_test
879
+
880
+ return object[attribute] || (attribute.is_a?(String) ? object[attribute.to_sym] : object[attribute.to_s])
881
+ end
882
+
883
+ if defined_test
884
+ return false
885
+ end
886
+
887
+ if type == Template::ARRAY_CALL
888
+ if ignore_strict_check || !environment.strict_variables?
889
+ return
890
+ end
891
+
892
+ raise Error::Runtime, "Can't find key #{attribute} in #{object.inspect}."
893
+ end
894
+ end
895
+
896
+ if object.respond_to?(attribute)
897
+ if defined_test
898
+ return true
899
+ end
900
+
901
+ positional = []
902
+ arguments.each do |k, v|
903
+ if !v.is_a?(Runtime::Spread) && k.is_a?(Integer)
904
+ positional << v
905
+ elsif v.is_a?(Runtime::Spread) && v.array?
906
+ positional = [*positional, *v.value]
907
+ end
908
+ end
909
+
910
+ kwargs = {}
911
+ arguments.each do |k, v|
912
+ if !v.is_a?(Runtime::Spread) && !k.is_a?(Integer)
913
+ kwargs[k] = v
914
+ elsif v.is_a?(Runtime::Spread) && v.hash?
915
+ kwargs = kwargs.merge(v.value)
916
+ end
917
+ end
918
+
919
+ kwargs = kwargs.transform_keys(&:to_sym)
920
+
921
+ if positional.length.positive? && kwargs.empty?
922
+ object.send(attribute, *positional, &)
923
+ elsif positional.empty? && kwargs.length.positive?
924
+ object.send(attribute, **kwargs, &)
925
+ elsif positional.length.positive? && kwargs.length.positive?
926
+ object.send(attribute, *positional, **kwargs, &)
927
+ else
928
+ case object
929
+ when Hash, Array
930
+ object[attribute]
931
+ else
932
+ object.send(attribute, &)
933
+ end
934
+ end
935
+ # Constant could be nil but we should return if we find it
936
+ elsif (constant = get_constant(object, attribute)) && constant[0] == :found
937
+ constant[1]
938
+ else
939
+ return if ignore_strict_check || !environment.strict_variables?
940
+
941
+ if defined_test
942
+ return false
943
+ end
944
+
945
+ message = if object.nil?
946
+ "Impossible to access an attribute (\"#{attribute}\") on a null variable."
947
+ elsif object.respond_to?(:[]) && !object.is_a?(String)
948
+ keys = object.respond_to?(:keys) ? "with keys \"#{object.keys.join(', ')}\"" : ''
949
+ "Key \"#{attribute}\" for sequence/mapping #{keys} does not exist."
950
+ else
951
+ "Impossible to access an attribute (\"#{attribute}\") on a #{object.class} " \
952
+ "variable (\"#{object}\")."
953
+ end
954
+
955
+ raise Error::Runtime.new(
956
+ message,
957
+ lineno,
958
+ source
959
+ )
960
+ end
961
+ end
962
+
963
+ def self.get_constant(object, attribute)
964
+ object = object.class unless object.respond_to?(:const_defined?)
965
+
966
+ if object.const_defined?(attribute)
967
+ [:found, object.const_get(attribute)]
968
+ else
969
+ [:not_found]
970
+ end
971
+ rescue NameError
972
+ [:not_found]
973
+ end
974
+
975
+ # Zeroes are false in Twig
976
+ def self.bool(value)
977
+ if !value ||
978
+ (value.respond_to?(:zero?) && value.zero?) ||
979
+ value == ''
980
+ false
981
+ else
982
+ true
983
+ end
984
+ end
985
+
986
+ # @todo How to post deprecations? Also check if Rails is loaded and deprecate that way
987
+ def self.deprecation_notice(message, template, line, package: nil, version: nil)
988
+ package = package ? " (Package: #{package})" : ''
989
+ version = version ? " (Version: #{version})" : ''
990
+
991
+ puts "Deprecation Notice: #{message} in #{template} on line #{line}#{package}#{version}"
992
+ end
993
+
994
+ def self.test_empty?(object)
995
+ object.nil? ||
996
+ (object == false) ||
997
+ (object.respond_to?(:empty?) && object.empty?) ||
998
+ (object.respond_to?(:length) && (object.length&.== 0)) || # rubocop:disable Style/ZeroLengthPredicate
999
+ (object.respond_to?(:size) && (object.size&.== 0)) || # rubocop:disable Style/ZeroLengthPredicate
1000
+ (object.respond_to?(:to_s) && (object.to_s&.== ''))
1001
+ end
1002
+
1003
+ # @param [Environment] environment
1004
+ # @param [Context] context
1005
+ def self.include(
1006
+ environment, context, template, variables = {}, with_context: true, ignore_missing: false, sandboxed: false
1007
+ )
1008
+ variables = if with_context
1009
+ context.merge(variables)
1010
+ else
1011
+ context.only(variables)
1012
+ end
1013
+
1014
+ # @todo: Missing sandbox
1015
+
1016
+ begin
1017
+ loaded = environment.resolve_template(template)
1018
+ rescue Error::Loader => e
1019
+ unless ignore_missing
1020
+ raise e
1021
+ end
1022
+
1023
+ return ''
1024
+ end
1025
+
1026
+ variables.buffer_and_return do
1027
+ loaded.render(variables)
1028
+ end
1029
+ end
1030
+
1031
+ # @param [Environment] environment
1032
+ # @param [String] name
1033
+ # @param [Boolean] ignore_missing
1034
+ def self.source(environment, name, ignore_missing: false)
1035
+ environment.loader.get_source_context(name).code
1036
+ rescue Error::Loader => e
1037
+ raise e unless ignore_missing
1038
+ end
1039
+
1040
+ # @param [Parser] parser
1041
+ # @param [Node::Base] fake_node
1042
+ def self.parse_parent_function(parser, fake_node, args, line)
1043
+ unless (block_name = parser.peek_block_stack)
1044
+ raise Error::Syntax.new(
1045
+ 'Calling the "parent" function outside of a block is forbidden.',
1046
+ line,
1047
+ parser.stream.source
1048
+ )
1049
+ end
1050
+
1051
+ unless parser.inheritance?
1052
+ raise Error::Syntax.new(
1053
+ 'Calling the "parent" function on a template that does not call "extends" or "use" is forbidden.',
1054
+ line,
1055
+ parser.stream.source
1056
+ )
1057
+ end
1058
+
1059
+ Node::Expression::Parent.new(block_name, line)
1060
+ end
1061
+
1062
+ # @param [Parser] parser
1063
+ # @param [Node::Base] fake_node
1064
+ def self.parse_block_function(parser, fake_node, args, line)
1065
+ fake_function = TwigFunction.new('block', ->(name, template = nil) {})
1066
+ positional, = Util::CallableArgumentsExtractor.
1067
+ new(fake_node, fake_function, parser.environment).
1068
+ extract_arguments(args)
1069
+
1070
+ Node::Expression::BlockReference.new(positional[0], positional[1], line)
1071
+ end
1072
+
1073
+ # @param [Parser] parser
1074
+ # @param [Node::Base] fake_node
1075
+ def self.parse_loop_function(parser, fake_node, args, line)
1076
+ fake_function = TwigFunction.new('loop', ->(iterator) {})
1077
+ positional, = Util::CallableArgumentsExtractor.
1078
+ new(fake_node, fake_function, parser.environment).
1079
+ extract_arguments(args)
1080
+
1081
+ recurse_args = Node::Expression::Hash.new(AutoHash.new.add(
1082
+ Node::Expression::Constant.new(0, line),
1083
+ positional[0]
1084
+ ), line)
1085
+ expr = Node::Expression::GetAttribute.new(
1086
+ Node::Expression::Variable::Context.new('loop', line),
1087
+ Node::Expression::Constant.new('call', line),
1088
+ recurse_args,
1089
+ Template::METHOD_CALL,
1090
+ line
1091
+ )
1092
+ expr.attributes[:is_generator] = true
1093
+ expr = Node::Expression::Filter::Raw.new(expr)
1094
+ expr.attributes[:is_generator] = true
1095
+
1096
+ expr
1097
+ end
1098
+
1099
+ def self.enumerable_function(object, function, proc)
1100
+ enumerable = Runtime::EnumerableHash.from(object)
1101
+
1102
+ case proc.arity
1103
+ when 1
1104
+ enumerable.public_send(function) do |_, value|
1105
+ proc.call(value)
1106
+ end
1107
+ when 2
1108
+ enumerable.public_send(function) do |key, value|
1109
+ proc.call(value, key)
1110
+ end
83
1111
  else
84
- raise NotImplementedError, 'Need to implement other get_attribute calls'
1112
+ raise Error::Runtime, "The #{function.to_s.capitalize} method takes 1 or 2 arguments, given #{proc.arity}."
85
1113
  end
86
1114
  end
87
1115
  end