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
data/lib/twig/parser.rb CHANGED
@@ -6,25 +6,92 @@ module Twig
6
6
  class Parser
7
7
  attr_reader :stream, :block_stack
8
8
 
9
+ # @return [Environment]
10
+ attr_reader :environment
11
+
12
+ STACKABLE = %i[
13
+ stream
14
+ parent
15
+ blocks
16
+ block_stack
17
+ macros
18
+ imported_symbols
19
+ traits
20
+ embedded_templates
21
+ ignore_unknown_twig_callables
22
+ ].freeze
23
+
9
24
  # @param [Environment] environment
10
25
  def initialize(environment)
11
26
  @environment = environment
27
+ @parsers = environment.expression_parsers
28
+ @stack = []
12
29
  end
13
30
 
14
31
  # @param [TokenStream] stream
32
+ # @return [Node::Module]
15
33
  def parse(stream, test = nil, drop_needle: false)
34
+ # Save the current value to stack
35
+ frame = {}
36
+ STACKABLE.each do |attr|
37
+ frame[attr] = instance_variable_get("@#{attr}")
38
+ end
39
+
40
+ @stack << frame
16
41
  @stream = stream
17
42
  @parent = nil
43
+ @traits = AutoHash.new
44
+ @macros = {}
18
45
  @blocks = {}
46
+ @embedded_templates = AutoHash.new
19
47
  @block_stack = []
20
48
  @imported_symbols = [{}]
49
+ @ignore_unknown_twig_callables = false
50
+
51
+ begin
52
+ body = subparse(test, drop_needle:)
53
+
54
+ if !@parent.nil? && (body = filter_body_nodes(body)).nil?
55
+ body = Node::Empty.new
56
+ end
57
+ rescue Error::Syntax => e
58
+ unless e.source_context
59
+ e.source_context = stream.source
60
+ end
61
+
62
+ unless e.lineno
63
+ e.lineno = stream.current.lineno
64
+ end
65
+
66
+ raise e
67
+ end
21
68
 
22
- body = subparse(test, drop_needle:)
69
+ node = Node::Module.new(
70
+ Node::Body.new({ 0 => body }),
71
+ @parent,
72
+ @blocks.empty? ? Node::Empty.new : Node::Nodes.new(@blocks),
73
+ @macros.empty? ? Node::Empty.new : Node::Nodes.new(@macros),
74
+ @traits.empty? ? Node::Empty.new : Node::Nodes.new(@traits),
75
+ @embedded_templates.empty? ? Node::Empty.new : Node::Nodes.new(@embedded_templates),
76
+ stream.source
77
+ )
23
78
 
24
- Node::Module.new(body, @parent, Node::Nodes.new(@blocks), stream.source)
79
+ @visitors ||= environment.node_visitors
80
+
81
+ node = NodeTraverser.
82
+ new(environment, @visitors).
83
+ traverse(node)
84
+
85
+ # Restore stack
86
+ frame = @stack.pop
87
+ STACKABLE.each do |attr|
88
+ instance_variable_set("@#{attr}", frame[attr])
89
+ end
90
+
91
+ node
25
92
  end
26
93
 
27
- # @param [Proc] test
94
+ # @param [Method, nil] test
28
95
  # @return [Node::Base]
29
96
  def subparse(test, drop_needle: false)
30
97
  lineno = current_token.lineno
@@ -37,7 +104,7 @@ module Twig
37
104
  rv.add(Node::Text.new(token.value, token.lineno))
38
105
  when Token::VAR_START_TYPE
39
106
  token = stream.next
40
- expr = expression_parser.parse_expression
107
+ expr = parse_expression
41
108
  stream.expect(Token::VAR_END_TYPE)
42
109
 
43
110
  rv.add(Node::Print.new(expr, token.lineno))
@@ -56,11 +123,22 @@ module Twig
56
123
  return Node::Nodes.new(rv, lineno)
57
124
  end
58
125
 
59
- # @todo Check that there is a token parser for this token value
60
126
  subparser = @environment.token_parser(token.value)
61
127
 
62
128
  unless subparser
63
- raise Error::Syntax.new("Unexpected '#{token.value}' tag.", token.lineno, stream.source)
129
+ if test.nil?
130
+ raise Error::Syntax.new("Unknown \"#{token.value}\" tag.", token.lineno, stream.source)
131
+ else
132
+ e = Error::Syntax.new("Unexpected \"#{token.value}\" tag", token.lineno, stream.source)
133
+
134
+ if test.respond_to?(:receiver) && (receiver = test.receiver) && receiver.is_a?(Twig::TokenParser::Base)
135
+ e.append_message(
136
+ " (expecting closing tag for the \"#{receiver.tag}\" tag defined near line #{lineno})."
137
+ )
138
+ end
139
+
140
+ raise e
141
+ end
64
142
  end
65
143
 
66
144
  stream.next
@@ -80,16 +158,113 @@ module Twig
80
158
  Node::Nodes.new(rv)
81
159
  end
82
160
 
161
+ def subparse_ignore_unknown_twig_callables(test, drop_needle: false)
162
+ previous = @ignore_unknown_twig_callables
163
+ @ignore_unknown_twig_callables = true
164
+
165
+ begin
166
+ subparse(test, drop_needle:)
167
+ ensure
168
+ @ignore_unknown_twig_callables = previous
169
+ end
170
+ end
171
+
83
172
  # @return [Token]
84
173
  def current_token
85
174
  stream.current
86
175
  end
87
176
 
177
+ def parse_expression(precedence = 0)
178
+ token = current_token
179
+ if token.test(Token::OPERATOR_TYPE) && (parser = parsers.by_name(:prefix, token.value))
180
+ stream.next
181
+ expr = parser.parse(self, token)
182
+ else
183
+ expr = parsers.by_class(ExpressionParser::Prefix::Literal.name).parse(self, token)
184
+ end
185
+
186
+ token = current_token
187
+ while token.test(Token::OPERATOR_TYPE) &&
188
+ (parser = parsers.by_name(:infix, token.value)) &&
189
+ parser.precedence >= precedence
190
+
191
+ stream.next
192
+ expr = parser.parse(self, expr, token)
193
+ token = current_token
194
+ end
195
+
196
+ expr
197
+ end
198
+
199
+ # @return [TwigFilter]
200
+ def filter(name, lineno)
201
+ unless (filter = environment.filter(name))
202
+ unless ignore_unknown_twig_callables?
203
+ raise Error::Syntax.new("Unknown '#{name}' filter.", lineno, stream.source)
204
+ end
205
+
206
+ filter = TwigFilter.new(name, -> {})
207
+ end
208
+
209
+ filter
210
+ end
211
+
212
+ # @return [TwigFunction, Node::Expression::HelperMethod]
213
+ def function(name, args, lineno)
214
+ unless (function = environment.function(name))
215
+ if ignore_unknown_twig_callables?
216
+ return TwigFunction.new(name, -> {})
217
+ elsif environment.allow_helper_methods?
218
+ return Node::Expression::HelperMethod.new(name, args, lineno)
219
+ else
220
+ raise Error::Syntax.new("Unknown \"#{name}\" function.", lineno, stream.source)
221
+ end
222
+ end
223
+
224
+ function
225
+ end
226
+
227
+ # @param [Integer] line
228
+ # @return [TwigTest]
229
+ def test(line)
230
+ name = stream.expect(Token::NAME_TYPE).value
231
+
232
+ if stream.test(Token::NAME_TYPE)
233
+ # try 2 word tests
234
+ name = "#{name} #{current_token.value}"
235
+
236
+ if (test = environment.test(name))
237
+ stream.next
238
+ end
239
+ else
240
+ test = environment.test(name)
241
+ end
242
+
243
+ if test.nil? && ignore_unknown_twig_callables?
244
+ test = TwigTest.new(name, -> {})
245
+ end
246
+
247
+ unless test
248
+ raise Error::Syntax.new("Unknown #{name} test.", line, stream.source)
249
+ end
250
+
251
+ test
252
+ end
253
+
88
254
  # @return [ExpressionParser]
89
255
  def expression_parser
90
256
  @expression_parser ||= ExpressionParser.new(self, @environment)
91
257
  end
92
258
 
259
+ # @return [Boolean]
260
+ def inheritance?
261
+ @parent || @traits.length.positive?
262
+ end
263
+
264
+ def main_scope?
265
+ @imported_symbols.one?
266
+ end
267
+
93
268
  def push_local_scope
94
269
  @imported_symbols.unshift({})
95
270
  end
@@ -98,6 +273,26 @@ module Twig
98
273
  @imported_symbols.shift
99
274
  end
100
275
 
276
+ def add_imported_symbol(type, symbol_alias, name = nil, internal_ref = nil)
277
+ @imported_symbols[0][type] ||= {}
278
+ @imported_symbols[0][type][symbol_alias] = { name:, node: internal_ref }
279
+ end
280
+
281
+ def imported_symbol(type, symbol_alias)
282
+ if (symbol = @imported_symbols.dig(0, type, symbol_alias))
283
+ symbol
284
+ else
285
+ @imported_symbols.dig(-1, type, symbol_alias)
286
+ end
287
+ end
288
+
289
+ # @param [Node::Module] template
290
+ def embed_template(template)
291
+ template.index = rand(2**32)
292
+
293
+ @embedded_templates << template
294
+ end
295
+
101
296
  def peek_block_stack
102
297
  @block_stack[-1]
103
298
  end
@@ -114,9 +309,28 @@ module Twig
114
309
  @blocks.key?(name)
115
310
  end
116
311
 
117
- # @todo type value as BlockNode and also set it to a BodyNode
312
+ def add_trait(trait)
313
+ @traits << trait
314
+ end
315
+
316
+ # @param [String] name
317
+ # @param [Node::Macro] node
318
+ def set_macro(name, node)
319
+ @macros[name] = node
320
+ end
321
+
322
+ # @param [String] name
323
+ # @param [Node::Body] value
118
324
  def set_block(name, value)
119
- @blocks[name] = value
325
+ if @blocks.key?(name)
326
+ raise Error::Syntax.new(
327
+ "The block '#{name}' has already been defined line #{@blocks[name].lineno}.",
328
+ current_token.lineno,
329
+ @blocks[name].source_context
330
+ )
331
+ end
332
+
333
+ @blocks[name] = Node::Body.new({ 0 => value }, {}, value.lineno)
120
334
  end
121
335
 
122
336
  # @param [Node::Base] parent
@@ -127,5 +341,64 @@ module Twig
127
341
 
128
342
  @parent = parent
129
343
  end
344
+
345
+ def ignore_unknown_twig_callables?
346
+ @ignore_unknown_twig_callables
347
+ end
348
+
349
+ private
350
+
351
+ # @return [ExpressionParser::ExpressionParsers]
352
+ attr_reader :parsers
353
+
354
+ def filter_body_nodes(node, nested: false)
355
+ # check that the body does not contain non-empty output nodes
356
+ if (node.is_a?(Node::Text) && !node.attributes[:data].match?(/\A[[:space:]]*\z/)) ||
357
+ (!node.is_a?(Node::Text) && !node.is_a?(Node::BlockReference) && node.is_a?(Node::Output))
358
+ if node.to_s.include?("\xEF\xBB\xBF")
359
+ t = node.attributes[:data][3..]
360
+ # bypass empty nodes starting with a BOM
361
+ if t == '' || t.match?(/\A[[:space:]]*\z/)
362
+ return nil
363
+ end
364
+ end
365
+
366
+ raise Error::Syntax.new(
367
+ 'A template that extends another one cannot include content outside Twig blocks. Did you forget ' \
368
+ 'to put the content inside a {% block %} tag?',
369
+ node.lineno,
370
+ stream.source
371
+ )
372
+ end
373
+
374
+ # Bypass nodes that "capture" the output
375
+ if node.is_a?(Node::Set)
376
+ return node
377
+ end
378
+
379
+ # "block" tags that are not captured (see above) are only used for defining
380
+ # the content of the block. In such a case, nesting it does not work as
381
+ # expected as the definition is not part of the default template code flow.
382
+ if nested && node.is_a?(Node::BlockReference)
383
+ raise Error::Syntax.new(
384
+ 'A block definition cannot be nested under non-capturing nodes.',
385
+ node.lineno,
386
+ stream.source
387
+ )
388
+ end
389
+
390
+ if node.is_a?(Node::Output)
391
+ return nil
392
+ end
393
+
394
+ nested ||= !node.is_a?(Node::Nodes)
395
+ node.nodes.each do |k, n|
396
+ if filter_body_nodes(n, nested: nested).nil?
397
+ node.nodes.delete(k)
398
+ end
399
+ end
400
+
401
+ node
402
+ end
130
403
  end
131
404
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ module Rails
5
+ class Config
6
+ def self.defaults
7
+ ActiveSupport::OrderedOptions.new.merge!({
8
+ root: ::Rails.root,
9
+ paths: %w[./ app/views/],
10
+ debug: ::Rails.env.development?,
11
+ allow_helper_methods: true,
12
+ cache: ::Rails.root.join('tmp/cache/twig').to_s,
13
+ charset: 'UTF-8',
14
+ strict_variables: true,
15
+ autoescape: :name,
16
+ auto_reload: nil,
17
+ loader: lambda do
18
+ ::Twig::Loader::Filesystem.new(
19
+ current.root,
20
+ current.paths
21
+ )
22
+ end,
23
+ })
24
+ end
25
+
26
+ def self.current
27
+ self.configuration ||= defaults
28
+ end
29
+
30
+ class_attribute :configuration
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails'
4
+ require 'English'
5
+ require 'twig/rails/config'
6
+ require 'twig/rails/renderer'
7
+
8
+ module Twig
9
+ # @return [Environment]
10
+ def self.environment
11
+ @@environment ||= begin
12
+ options = ::Twig::Rails::Config.current.slice(
13
+ :autoescape,
14
+ :cache,
15
+ :debug,
16
+ :allow_helper_methods,
17
+ :charset,
18
+ :strict_variables,
19
+ :auto_reload
20
+ )
21
+
22
+ ::Twig::Environment.new(loader, options).tap do |env|
23
+ env.add_extension(::Twig::Extension::Rails.new)
24
+ env.add_extension(::Twig::Extension::Debug.new) if env.debug?
25
+ end
26
+ end
27
+ end
28
+
29
+ def self.loader
30
+ @@loader ||= ::Twig::Rails::Config.current.loader.call
31
+ end
32
+
33
+ module Rails
34
+ class Engine < ::Rails::Engine
35
+ config.before_configuration do |app|
36
+ app.config.twig = Config.current
37
+ end
38
+
39
+ initializer 'twig_ruby.configure_rails_initialization' do
40
+ ActionView::Template.register_template_handler :twig, Renderer.new
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ module Rails
5
+ class Renderer
6
+ def call(template, source)
7
+ <<~TEMPLATE
8
+ ::Twig.
9
+ environment.
10
+ load("#{template.short_identifier}").
11
+ render(
12
+ local_assigns,
13
+ call_context: self,
14
+ output_buffer: @output_buffer
15
+ )
16
+
17
+ @output_buffer
18
+ TEMPLATE
19
+ end
20
+
21
+ def translate_location(spot, _backtrace_location, source)
22
+ exception = $ERROR_INFO
23
+
24
+ return nil unless exception.is_a?(::ActionView::Template::Error)
25
+
26
+ twig_exception = exception.cause
27
+
28
+ return nil unless twig_exception.is_a?(::Twig::Error::Base)
29
+
30
+ lineno = twig_exception.lineno
31
+ lineno = 1 if lineno == -1
32
+
33
+ spot[:script_lines] = twig_exception.source_context&.code&.lines || source.lines
34
+ spot[:first_lineno] = spot[:last_lineno] = lineno
35
+ spot[:first_column] = spot[:last_column] = 0
36
+
37
+ spot
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ module Runtime
5
+ class ArgumentSpreader
6
+ # @param [Method] method
7
+ def initialize(method)
8
+ @method = method
9
+ end
10
+
11
+ # like {{ ["Hello", "World"] | join(glue: " ") }}
12
+ def call(*arguments, **kwargs)
13
+ positional = []
14
+
15
+ arguments.each do |v|
16
+ if v.is_a?(Spread)
17
+ if v.array?
18
+ positional = [*positional, *v.value]
19
+ else
20
+ kwargs = kwargs.merge(v.value)
21
+ end
22
+ else
23
+ positional << v
24
+ end
25
+ end
26
+
27
+ kwargs = kwargs.transform_keys(&:to_sym)
28
+
29
+ if positional.empty? && kwargs.empty?
30
+ method.call
31
+ elsif positional.length.positive? && kwargs.empty?
32
+ method.call(*positional)
33
+ elsif positional.empty? && kwargs.length.positive?
34
+ method.call(**kwargs)
35
+ elsif positional.length.positive? && kwargs.length.positive?
36
+ method.call(*positional, **kwargs)
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ # @return [Method]
43
+ attr_reader :method
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ module Runtime
5
+ class Context < Hash
6
+ attr_reader :call_context
7
+
8
+ def initialize(initial_context = {}, output_buffer: nil, call_context: nil)
9
+ super()
10
+
11
+ output_buffer ||= OutputBuffer.new
12
+ @output_buffer_stack = [output_buffer]
13
+ @call_context = call_context
14
+ @popping = false
15
+
16
+ merge!(initial_context)
17
+ end
18
+
19
+ def original_buffer
20
+ output_buffer_stack.first
21
+ end
22
+
23
+ def merge!(other, overwrite: true)
24
+ return if other == []
25
+
26
+ unless other.is_a?(Hash)
27
+ raise "Must merge! another Hash, given #{other.class.name}"
28
+ end
29
+
30
+ other.each do |k, v|
31
+ if overwrite || !key?(k)
32
+ self[k] = v
33
+ end
34
+ end
35
+
36
+ self
37
+ end
38
+
39
+ def keep!(keys)
40
+ (self.keys - keys).each { |k| delete(k) }
41
+ end
42
+
43
+ def remove!(*keys)
44
+ keys.each { |k| delete(k) }
45
+ end
46
+
47
+ def merge(other, overwrite: true)
48
+ self.class.new(self, call_context:, output_buffer:).merge!(other, overwrite:)
49
+ end
50
+
51
+ def only(other)
52
+ self.class.new(other, call_context:, output_buffer:)
53
+ end
54
+
55
+ def push_stack
56
+ stack.push({ remove: [], replace: {} })
57
+ end
58
+
59
+ def pop_stack(new_only = false)
60
+ return unless stack.last
61
+
62
+ @popping = true
63
+
64
+ frame = stack.pop
65
+ frame[:remove].each do |k|
66
+ delete(k)
67
+ end
68
+
69
+ unless new_only
70
+ frame[:replace].each { |k, v| self[k] = v }
71
+ end
72
+
73
+ @popping = false
74
+ end
75
+
76
+ def output_buffer
77
+ output_buffer_stack.last
78
+ end
79
+
80
+ def push_output_buffer
81
+ output_buffer_stack.push(OutputBuffer.new)
82
+ end
83
+
84
+ def pop_output_buffer
85
+ output_buffer_stack.pop
86
+ end
87
+
88
+ def buffer_and_return(&)
89
+ push_output_buffer
90
+ yield
91
+ pop_output_buffer
92
+ end
93
+
94
+ def clear
95
+ # Copy everything to the replace stack
96
+ merge!(self)
97
+
98
+ # Clear the hash
99
+ super
100
+ end
101
+
102
+ def dup
103
+ self.class.new(to_h, call_context:, output_buffer:)
104
+ end
105
+
106
+ def [](key)
107
+ super(key.to_sym)
108
+ end
109
+
110
+ def []=(key, value)
111
+ super if popping
112
+
113
+ key = key.to_sym
114
+
115
+ if (frame = stack.last)
116
+ super and return if frame[:replace].key?(key) || frame[:remove].include?(key)
117
+
118
+ if key?(key)
119
+ frame[:replace][key] = self[key]
120
+ else
121
+ frame[:remove].push(key)
122
+ end
123
+ end
124
+
125
+ super
126
+ end
127
+
128
+ # @return [Context]
129
+ def self.from(context = {}, output_buffer: nil, call_context: nil)
130
+ if context.is_a?(Context)
131
+ context
132
+ else
133
+ new(context, output_buffer:, call_context:)
134
+ end
135
+ end
136
+
137
+ def each(...)
138
+ to_h.each(...)
139
+ end
140
+
141
+ private
142
+
143
+ attr_reader :popping
144
+
145
+ def stack
146
+ @stack ||= []
147
+ end
148
+
149
+ def output_buffer_stack
150
+ @output_buffer_stack ||= []
151
+ end
152
+ end
153
+ end
154
+ end