twig_ruby 0.0.1 → 0.0.2

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 (167) hide show
  1. checksums.yaml +4 -4
  2. data/lib/tasks/twig_parity.rake +278 -0
  3. data/lib/twig/auto_hash.rb +7 -1
  4. data/lib/twig/callable.rb +28 -1
  5. data/lib/twig/compiler.rb +35 -3
  6. data/lib/twig/environment.rb +198 -41
  7. data/lib/twig/error/base.rb +81 -16
  8. data/lib/twig/error/loader.rb +8 -0
  9. data/lib/twig/error/logic.rb +8 -0
  10. data/lib/twig/error/runtime.rb +8 -0
  11. data/lib/twig/expression_parser/base.rb +30 -0
  12. data/lib/twig/expression_parser/expression_parsers.rb +57 -0
  13. data/lib/twig/expression_parser/infix/arrow.rb +31 -0
  14. data/lib/twig/expression_parser/infix/binary.rb +34 -0
  15. data/lib/twig/expression_parser/infix/conditional_ternary.rb +39 -0
  16. data/lib/twig/expression_parser/infix/dot.rb +72 -0
  17. data/lib/twig/expression_parser/infix/filter.rb +43 -0
  18. data/lib/twig/expression_parser/infix/function.rb +67 -0
  19. data/lib/twig/expression_parser/infix/is.rb +53 -0
  20. data/lib/twig/expression_parser/infix/is_not.rb +19 -0
  21. data/lib/twig/expression_parser/infix/parses_arguments.rb +84 -0
  22. data/lib/twig/expression_parser/infix/square_bracket.rb +66 -0
  23. data/lib/twig/expression_parser/infix_expression_parser.rb +34 -0
  24. data/lib/twig/expression_parser/prefix/grouping.rb +60 -0
  25. data/lib/twig/expression_parser/prefix/literal.rb +244 -0
  26. data/lib/twig/expression_parser/prefix/unary.rb +29 -0
  27. data/lib/twig/expression_parser/prefix_expression_parser.rb +18 -0
  28. data/lib/twig/extension/base.rb +26 -4
  29. data/lib/twig/extension/core.rb +1076 -48
  30. data/lib/twig/extension/debug.rb +25 -0
  31. data/lib/twig/extension/escaper.rb +73 -0
  32. data/lib/twig/extension/rails.rb +10 -57
  33. data/lib/twig/extension/string_loader.rb +19 -0
  34. data/lib/twig/extension_set.rb +117 -20
  35. data/lib/twig/file_extension_escaping_strategy.rb +35 -0
  36. data/lib/twig/lexer.rb +225 -81
  37. data/lib/twig/loader/array.rb +25 -8
  38. data/lib/twig/loader/chain.rb +93 -0
  39. data/lib/twig/loader/filesystem.rb +106 -7
  40. data/lib/twig/node/auto_escape.rb +18 -0
  41. data/lib/twig/node/base.rb +58 -2
  42. data/lib/twig/node/block.rb +2 -0
  43. data/lib/twig/node/block_reference.rb +5 -1
  44. data/lib/twig/node/body.rb +7 -0
  45. data/lib/twig/node/cache.rb +50 -0
  46. data/lib/twig/node/capture.rb +22 -0
  47. data/lib/twig/node/deprecated.rb +53 -0
  48. data/lib/twig/node/do.rb +19 -0
  49. data/lib/twig/node/embed.rb +43 -0
  50. data/lib/twig/node/expression/array.rb +29 -20
  51. data/lib/twig/node/expression/arrow_function.rb +55 -0
  52. data/lib/twig/node/expression/assign_name.rb +1 -1
  53. data/lib/twig/node/expression/binary/and.rb +17 -0
  54. data/lib/twig/node/expression/binary/base.rb +6 -4
  55. data/lib/twig/node/expression/binary/boolean.rb +24 -0
  56. data/lib/twig/node/expression/binary/concat.rb +20 -0
  57. data/lib/twig/node/expression/binary/elvis.rb +35 -0
  58. data/lib/twig/node/expression/binary/ends_with.rb +24 -0
  59. data/lib/twig/node/expression/binary/floor_div.rb +21 -0
  60. data/lib/twig/node/expression/binary/has_every.rb +20 -0
  61. data/lib/twig/node/expression/binary/has_some.rb +20 -0
  62. data/lib/twig/node/expression/binary/in.rb +20 -0
  63. data/lib/twig/node/expression/binary/matches.rb +24 -0
  64. data/lib/twig/node/expression/binary/not_in.rb +20 -0
  65. data/lib/twig/node/expression/binary/null_coalesce.rb +49 -0
  66. data/lib/twig/node/expression/binary/or.rb +15 -0
  67. data/lib/twig/node/expression/binary/starts_with.rb +24 -0
  68. data/lib/twig/node/expression/binary/xor.rb +17 -0
  69. data/lib/twig/node/expression/block_reference.rb +62 -0
  70. data/lib/twig/node/expression/call.rb +126 -6
  71. data/lib/twig/node/expression/constant.rb +3 -1
  72. data/lib/twig/node/expression/filter/default.rb +37 -0
  73. data/lib/twig/node/expression/filter/raw.rb +31 -0
  74. data/lib/twig/node/expression/filter.rb +2 -2
  75. data/lib/twig/node/expression/function.rb +37 -0
  76. data/lib/twig/node/expression/get_attribute.rb +51 -7
  77. data/lib/twig/node/expression/hash.rb +75 -0
  78. data/lib/twig/node/expression/helper_method.rb +6 -18
  79. data/lib/twig/node/expression/macro_reference.rb +43 -0
  80. data/lib/twig/node/expression/name.rb +42 -8
  81. data/lib/twig/node/expression/operator_escape.rb +13 -0
  82. data/lib/twig/node/expression/parent.rb +28 -0
  83. data/lib/twig/node/expression/support_defined_test.rb +23 -0
  84. data/lib/twig/node/expression/ternary.rb +7 -1
  85. data/lib/twig/node/expression/test/base.rb +26 -0
  86. data/lib/twig/node/expression/test/constant.rb +35 -0
  87. data/lib/twig/node/expression/test/defined.rb +33 -0
  88. data/lib/twig/node/expression/test/divisible_by.rb +23 -0
  89. data/lib/twig/node/expression/test/even.rb +21 -0
  90. data/lib/twig/node/expression/test/iterable.rb +21 -0
  91. data/lib/twig/node/expression/test/mapping.rb +21 -0
  92. data/lib/twig/node/expression/test/null.rb +21 -0
  93. data/lib/twig/node/expression/test/odd.rb +21 -0
  94. data/lib/twig/node/expression/test/same_as.rb +23 -0
  95. data/lib/twig/node/expression/test/sequence.rb +21 -0
  96. data/lib/twig/node/expression/unary/base.rb +3 -1
  97. data/lib/twig/node/expression/unary/not.rb +18 -0
  98. data/lib/twig/node/expression/unary/spread.rb +18 -0
  99. data/lib/twig/node/expression/unary/string_cast.rb +18 -0
  100. data/lib/twig/node/expression/variable/assign_template.rb +35 -0
  101. data/lib/twig/node/expression/variable/local.rb +35 -0
  102. data/lib/twig/node/expression/variable/template.rb +54 -0
  103. data/lib/twig/node/for.rb +38 -8
  104. data/lib/twig/node/for_loop.rb +0 -22
  105. data/lib/twig/node/if.rb +4 -1
  106. data/lib/twig/node/import.rb +32 -0
  107. data/lib/twig/node/include.rb +38 -8
  108. data/lib/twig/node/macro.rb +79 -0
  109. data/lib/twig/node/module.rb +278 -23
  110. data/lib/twig/node/output.rb +7 -0
  111. data/lib/twig/node/print.rb +4 -1
  112. data/lib/twig/node/set.rb +72 -0
  113. data/lib/twig/node/text.rb +4 -1
  114. data/lib/twig/node/with.rb +50 -0
  115. data/lib/twig/node/yield.rb +6 -1
  116. data/lib/twig/node_traverser.rb +50 -0
  117. data/lib/twig/node_visitor/base.rb +30 -0
  118. data/lib/twig/node_visitor/escaper.rb +165 -0
  119. data/lib/twig/node_visitor/safe_analysis.rb +127 -0
  120. data/lib/twig/node_visitor/spreader.rb +39 -0
  121. data/lib/twig/output_buffer.rb +14 -12
  122. data/lib/twig/parser.rb +281 -8
  123. data/lib/twig/rails/config.rb +33 -0
  124. data/lib/twig/rails/engine.rb +44 -0
  125. data/lib/twig/rails/renderer.rb +41 -0
  126. data/lib/twig/runtime/argument_spreader.rb +46 -0
  127. data/lib/twig/runtime/context.rb +154 -0
  128. data/lib/twig/runtime/enumerable_hash.rb +51 -0
  129. data/lib/twig/runtime/escaper.rb +155 -0
  130. data/lib/twig/runtime/loop_context.rb +81 -0
  131. data/lib/twig/runtime/loop_iterator.rb +60 -0
  132. data/lib/twig/runtime/spread.rb +21 -0
  133. data/lib/twig/runtime_loader/base.rb +12 -0
  134. data/lib/twig/runtime_loader/factory.rb +23 -0
  135. data/lib/twig/template.rb +267 -14
  136. data/lib/twig/template_wrapper.rb +42 -0
  137. data/lib/twig/token.rb +28 -2
  138. data/lib/twig/token_parser/apply.rb +48 -0
  139. data/lib/twig/token_parser/auto_escape.rb +45 -0
  140. data/lib/twig/token_parser/base.rb +26 -0
  141. data/lib/twig/token_parser/block.rb +4 -4
  142. data/lib/twig/token_parser/cache.rb +31 -0
  143. data/lib/twig/token_parser/deprecated.rb +40 -0
  144. data/lib/twig/token_parser/do.rb +19 -0
  145. data/lib/twig/token_parser/embed.rb +62 -0
  146. data/lib/twig/token_parser/extends.rb +4 -3
  147. data/lib/twig/token_parser/for.rb +14 -9
  148. data/lib/twig/token_parser/from.rb +57 -0
  149. data/lib/twig/token_parser/guard.rb +65 -0
  150. data/lib/twig/token_parser/if.rb +9 -9
  151. data/lib/twig/token_parser/import.rb +29 -0
  152. data/lib/twig/token_parser/include.rb +2 -2
  153. data/lib/twig/token_parser/macro.rb +109 -0
  154. data/lib/twig/token_parser/set.rb +76 -0
  155. data/lib/twig/token_parser/use.rb +54 -0
  156. data/lib/twig/token_parser/with.rb +36 -0
  157. data/lib/twig/token_parser/yield.rb +7 -7
  158. data/lib/twig/token_stream.rb +23 -3
  159. data/lib/twig/twig_filter.rb +20 -0
  160. data/lib/twig/twig_function.rb +37 -0
  161. data/lib/twig/twig_test.rb +31 -0
  162. data/lib/twig/util/callable_arguments_extractor.rb +227 -0
  163. data/lib/twig_ruby.rb +21 -2
  164. metadata +145 -6
  165. data/lib/twig/context.rb +0 -64
  166. data/lib/twig/expression_parser.rb +0 -517
  167. data/lib/twig/railtie.rb +0 -60
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ module Runtime
5
+ class EnumerableHash
6
+ include Enumerable
7
+
8
+ MISSING_KEY = "__§__missing_key_#{rand}".freeze
9
+
10
+ private_constant :MISSING_KEY
11
+
12
+ def self.from(object)
13
+ if object.is_a?(Array)
14
+ new(AutoHash.new.add(*object))
15
+ elsif object.is_a?(Hash)
16
+ new(object.to_h)
17
+ else
18
+ new(object)
19
+ end
20
+ end
21
+
22
+ def initialize(wrapped)
23
+ @wrapped = wrapped
24
+ end
25
+
26
+ def each(...)
27
+ key = 0
28
+ @wrapped&.each do |k, v = MISSING_KEY| # rubocop:disable Style/HashEachMethods
29
+ if v == MISSING_KEY
30
+ yield(key, k)
31
+ key += 1
32
+ else
33
+ yield(k, v)
34
+ end
35
+ end
36
+ end
37
+
38
+ def values
39
+ collect { |_, v| v }
40
+ end
41
+
42
+ def keys
43
+ collect { |k, _| k }
44
+ end
45
+
46
+ def filter
47
+ self.class.new(super)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ module Runtime
5
+ class Escaper
6
+ def initialize(charset)
7
+ @charset = charset
8
+ end
9
+
10
+ JS_SHORT_MAP = {
11
+ '\\' => '\\\\',
12
+ '/' => '\\/',
13
+ "\x08" => '\b',
14
+ "\x0C" => '\f',
15
+ "\x0A" => '\n',
16
+ "\x0D" => '\r',
17
+ "\x09" => '\t',
18
+ }.freeze
19
+
20
+ def escape(string, strategy = :html, charset = nil, autoescape = false)
21
+ # Allow strings marked as html_safe to get through without escaping
22
+ if string.html_safe? && autoescape
23
+ return string
24
+ end
25
+
26
+ case strategy.to_sym
27
+ when :html
28
+ CGI.escapeHTML(string.to_s)
29
+ when :html_attr
30
+ escape_html_attr(string.to_s, charset || @charset)
31
+ when :js
32
+ escape_js(string.to_s, charset || @charset)
33
+ when :css
34
+ escape_css(string.to_s, charset || @charset)
35
+ when :url
36
+ CGI.escape(string.to_s)
37
+ else
38
+ string.to_s
39
+ end.html_safe
40
+ end
41
+
42
+ private
43
+
44
+ def escape_html_attr(string, charset)
45
+ # Convert encoding if needed
46
+ if charset != 'UTF-8'
47
+ string = convert_encoding(string, 'UTF-8', charset)
48
+ end
49
+
50
+ # Validate UTF-8
51
+ unless string.valid_encoding?
52
+ raise Error::Runtime, 'The string to escape is not a valid UTF-8 string.'
53
+ end
54
+
55
+ # Escape characters not safe for HTML attributes
56
+ string = string.gsub(/[^a-zA-Z0-9,.\-_]/u) do |char|
57
+ ord = char.ord
58
+
59
+ # Replace characters undefined in HTML with Unicode replacement character
60
+ if (ord <= 0x1F && char != "\t" && char != "\n" && char != "\r") || ord.between?(0x7F, 0x9F)
61
+ '&#xFFFD;'
62
+ elsif char.bytesize == 1
63
+ # Use named entities for common characters
64
+ case ord
65
+ when 34 then '&quot;' # quotation mark
66
+ when 38 then '&amp;' # ampersand
67
+ when 60 then '&lt;' # less-than sign
68
+ when 62 then '&gt;' # greater-than sign
69
+ else
70
+ format('&#x%02X;', ord)
71
+ end
72
+ else
73
+ # Use hex entities for multi-byte characters
74
+ format('&#x%04X;', char.codepoints.first)
75
+ end
76
+ end
77
+
78
+ # Convert back to original encoding if needed
79
+ if charset != 'UTF-8'
80
+ string = string.encode(charset, 'UTF-8')
81
+ end
82
+
83
+ string
84
+ end
85
+
86
+ def escape_js(string, charset)
87
+ # Convert encoding if needed
88
+ if charset != 'UTF-8'
89
+ string = convert_encoding(string, 'UTF-8', charset)
90
+ end
91
+
92
+ # Validate UTF-8
93
+ unless string.valid_encoding?
94
+ raise Error::Runtime, 'The string to escape is not a valid UTF-8 string.'
95
+ end
96
+
97
+ string = string.gsub(/[^a-zA-Z0-9,._]/) do |char|
98
+ codepoint = char.ord
99
+
100
+ if JS_SHORT_MAP.key?(char)
101
+ JS_SHORT_MAP[char]
102
+ elsif codepoint < 0x10000
103
+ format('\u%04X', codepoint)
104
+ else
105
+ # Split characters outside the BMP into surrogate pairs
106
+ # https://tools.ietf.org/html/rfc2781.html#section-2.1
107
+ u = codepoint - 0x10000
108
+ high = 0xD800 | (u >> 10)
109
+ low = 0xDC00 | (u & 0x3FF)
110
+
111
+ format('\u%04X\u%04X', high, low)
112
+ end
113
+ end
114
+
115
+ # Convert back to original encoding if needed
116
+ if charset != 'UTF-8'
117
+ string = string.encode(charset, 'UTF-8')
118
+ end
119
+
120
+ string
121
+ end
122
+
123
+ def escape_css(string, charset)
124
+ # Convert encoding if needed
125
+ if charset != 'UTF-8'
126
+ string = convert_encoding(string, 'UTF-8', charset)
127
+ end
128
+
129
+ # Validate UTF-8
130
+ unless string.valid_encoding?
131
+ raise Error::Runtime, 'The string to escape is not a valid UTF-8 string.'
132
+ end
133
+
134
+ string = string.gsub(/[^a-zA-Z0-9]/) do |char|
135
+ if char.bytesize == 1
136
+ format('\\%X ', char.ord)
137
+ else
138
+ format('\\%X ', char.codepoints.first)
139
+ end
140
+ end
141
+
142
+ # Convert back to original encoding if needed
143
+ if charset != 'UTF-8'
144
+ string = string.encode(charset, 'UTF-8')
145
+ end
146
+
147
+ string
148
+ end
149
+
150
+ def convert_encoding(string, to_encoding, from_encoding)
151
+ string.encode(to_encoding, from_encoding)
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # This file is part of Twig.
5
+ #
6
+ # (c) Fabien Potencier
7
+ #
8
+ # For the full copyright and license information, please view the LICENSE
9
+ # file that was distributed with this source code.
10
+
11
+ module Twig
12
+ module Runtime
13
+ class LoopContext
14
+ # @return [Object, nil] The parent context
15
+ attr_reader :parent
16
+
17
+ delegate :index0, :index, :revindex0, :revindex, :length, :first, :last, to: :loop
18
+
19
+ def initialize(loop, parent, blocks, recurse_func, depth)
20
+ @loop = loop
21
+ @parent = parent
22
+ @blocks = blocks
23
+ @recurse_func = recurse_func
24
+ @depth = depth
25
+ end
26
+
27
+ # @param value [Object] The first value in the cycle
28
+ # @param values [Array<Object>] The rest of the values in the cycle
29
+ # @return [Object] The current value in the cycle
30
+ def cycle(value, *values)
31
+ values.unshift(value)
32
+ values[index0 % values.length]
33
+ end
34
+
35
+ # Recursion function
36
+ # @param iterator [Enumerable] The iterator
37
+ # @return [Enumerator] An enumerator for recursive iteration
38
+ def call(iterator)
39
+ if @depth > 50
40
+ raise 'Nesting level too deep.'
41
+ end
42
+
43
+ @parent.buffer_and_return do
44
+ @recurse_func.call(LoopIterator.new(iterator), @parent, @blocks, @recurse_func, @depth + 1)
45
+ end
46
+ end
47
+
48
+ def changed(value)
49
+ if !defined?(@last_changed) || value != @last_changed
50
+ @last_changed = value
51
+
52
+ true
53
+ else
54
+ false
55
+ end
56
+ end
57
+
58
+ def previous
59
+ @loop.previous&.dig(1)
60
+ end
61
+
62
+ def next
63
+ @loop.next&.dig(1)
64
+ end
65
+
66
+ # @return [Integer] The depth starting from 0
67
+ def depth0
68
+ @depth
69
+ end
70
+
71
+ # @return [Integer] The depth starting from 1
72
+ def depth
73
+ @depth + 1
74
+ end
75
+
76
+ private
77
+
78
+ attr_reader :loop
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ module Runtime
5
+ class LoopIterator
6
+ attr_reader :index0, :previous, :next
7
+
8
+ def initialize(seq)
9
+ @seq = Runtime::EnumerableHash.from(seq).to_enum
10
+ @index0 = 0
11
+ @previous = nil
12
+ @next = nil
13
+ end
14
+
15
+ def each(&)
16
+ @index0 = 0
17
+
18
+ loop do
19
+ current = @seq.next
20
+
21
+ begin
22
+ @next = @seq.peek
23
+ rescue StopIteration
24
+ @next = nil
25
+ end
26
+
27
+ yield current[0], current[1]
28
+ @index0 += 1
29
+ @previous = current
30
+ rescue StopIteration
31
+ break
32
+ end
33
+ end
34
+
35
+ def first
36
+ @index0.zero?
37
+ end
38
+
39
+ def last
40
+ revindex0.zero? || length.zero?
41
+ end
42
+
43
+ def length
44
+ @length ||= @seq.count
45
+ end
46
+
47
+ def index
48
+ @index0 + 1
49
+ end
50
+
51
+ def revindex0
52
+ [0, length - index].max
53
+ end
54
+
55
+ def revindex
56
+ revindex0 + 1
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ module Runtime
5
+ class Spread
6
+ attr_reader :value
7
+
8
+ def initialize(value)
9
+ @value = value
10
+ end
11
+
12
+ def array?
13
+ value.is_a?(Array) || value.is_a?(Range)
14
+ end
15
+
16
+ def hash?
17
+ value.is_a?(Hash)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ module RuntimeLoader
5
+ class Base
6
+ # @return [Object, nil]
7
+ def load(klass)
8
+ raise NotImplementedError
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ module RuntimeLoader
5
+ class Factory < Base
6
+ def initialize(map)
7
+ super()
8
+
9
+ @map = map.transform_keys do |klass|
10
+ klass.is_a?(Class) ? klass.name : klass
11
+ end
12
+ end
13
+
14
+ def load(klass)
15
+ klass = klass.name if klass.is_a?(Class)
16
+
17
+ return nil unless @map.key?(klass)
18
+
19
+ @map[klass].call
20
+ end
21
+ end
22
+ end
23
+ end
data/lib/twig/template.rb CHANGED
@@ -7,39 +7,292 @@ module Twig
7
7
  METHOD_CALL = :method_call
8
8
  ANY_CALL = :any_call
9
9
 
10
+ attr_accessor :blocks
11
+
10
12
  # @param [Environment] environment
11
- def initialize(environment, call_context: nil, output_buffer: nil)
13
+ def initialize(environment)
12
14
  @environment = environment
15
+ @parent = nil
13
16
  @parents = {}
14
17
  @blocks = {}
15
- @call_context = call_context
16
- @output_buffer = output_buffer || OutputBuffer.new
18
+ @traits = {}
19
+ @macros = {}
20
+ @trait_aliases = {}
17
21
  end
18
22
 
19
23
  def call(context = {}, blocks = {})
20
24
  raise 'call is not implemented'
21
25
  end
22
26
 
23
- def render(context = {}, blocks = {})
24
- call(Context.new(context), blocks)
27
+ # @param [Runtime::Context] context
28
+ def render(context)
29
+ unless context.is_a?(Runtime::Context)
30
+ raise Error::Runtime, 'Render must implement Twig::Runtime::Context.'
31
+ end
32
+
33
+ render_with_blocks(context.merge(env.globals), blocks.merge(blocks))
34
+ end
35
+
36
+ def render_with_blocks(context = {}, blocks = {})
37
+ call(context, blocks)
38
+ rescue Error::Base => e
39
+ e.source_context = source_context unless e.source_context
40
+ e.guess if e.lineno == -1
41
+ raise e
42
+ rescue StandardError => e
43
+ exception = Error::Runtime.new(
44
+ "An exception has been thrown during the rendering of a template (\"#{e}\").",
45
+ -1,
46
+ source_context,
47
+ e
48
+ )
49
+ exception.guess
50
+
51
+ raise exception
52
+ end
53
+
54
+ def render_block(name, context, blocks = {}, use_blocks: true, template_context: self)
55
+ unless context.is_a?(Runtime::Context)
56
+ context = Runtime::Context.new(context)
57
+ end
58
+
59
+ name = name.to_sym
60
+ template = if use_blocks && blocks.key?(name)
61
+ blocks[name]
62
+ elsif self.blocks.key?(name)
63
+ self.blocks[name]
64
+ end
65
+
66
+ # avoid RCEs when sandbox is enabled
67
+ if !template.nil? && !template[0].is_a?(::Twig::Template)
68
+ raise Error::Logic, 'A block must be a method on a ::Twig::Template instance.'
69
+ end
70
+
71
+ if !template.nil?
72
+ begin
73
+ context.buffer_and_return do
74
+ template[0].public_send(template[1], context, blocks)
75
+ end.to_s.html_safe
76
+ rescue Error::Base => e
77
+ unless e.source_context
78
+ e.source_context = template[0].source_context
79
+ end
80
+
81
+ if e.lineno == -1
82
+ e.guess
83
+ end
84
+
85
+ raise e
86
+ rescue StandardError => e
87
+ # Rails wraps exceptions that happened using render
88
+ if e.respond_to?(:cause) && e.cause.is_a?(Error::Base)
89
+ e = e.cause
90
+ unless e.source_context
91
+ e.source_context = template[0].source_context
92
+ end
93
+ end
94
+
95
+ exception = Error::Runtime.new(
96
+ "An exception has been thrown during the rendering of a template (#{e})",
97
+ -1,
98
+ template[0].source_context,
99
+ e
100
+ )
101
+ exception.guess
102
+
103
+ raise exception
104
+ end
105
+ elsif (parent = get_parent(context))
106
+ parent.render_block(name, context, self.blocks.merge(blocks), use_blocks: false, template_context:)
107
+ elsif blocks.key?(name)
108
+ raise Error::Runtime.new(
109
+ "Block \"#{name}\" should not call parent() in \"#{blocks[name][0].template_name}\" " \
110
+ "as the block does not exist in the parent template \"#{template_name}\".",
111
+ -1,
112
+ blocks[name][0].source_context
113
+ )
114
+ else
115
+ raise Error::Runtime.new(
116
+ "Block \"#{name}\" on template \"#{template_name}\" does not exist.",
117
+ -1,
118
+ template_context.source_context
119
+ )
120
+ end
121
+ end
122
+
123
+ def block?(name, context, blocks = {})
124
+ name = name.to_sym
125
+ if blocks.key?(name) && blocks[name][0].is_a?(Template)
126
+ return true
127
+ end
128
+
129
+ if @blocks&.key?(name)
130
+ return true
131
+ end
132
+
133
+ if (parent = get_parent(context))
134
+ return parent.block?(name, context)
135
+ end
136
+
137
+ false
25
138
  end
26
139
 
27
- def yield_block(name, context = {}, blocks = {})
28
- object = self
140
+ def render_parent_block(name, context, blocks = {})
141
+ if @traits.key?(name.to_sym)
142
+ @traits[name.to_sym][0].render_block(@trait_aliases[name.to_sym] || name, context, blocks, use_blocks: false)
143
+ elsif (parent = get_parent(context))
144
+ parent.render_block(name, context, blocks, use_blocks: false)
145
+ else
146
+ raise Error::Runtime.new(
147
+ "The template has no parent and no traits defining the #{name} block.",
148
+ -1,
149
+ source_context
150
+ )
151
+ end
152
+ end
29
153
 
30
- if blocks.key?(name)
31
- object = blocks[name]
154
+ # @return [Template, false]
155
+ def get_parent(context)
156
+ if @parent
157
+ return @parent
32
158
  end
33
159
 
34
- object.public_send(:"block_#{name}", context, blocks)
160
+ unless (parent = do_get_parent(context))
161
+ return false
162
+ end
163
+
164
+ if parent.is_a?(Template)
165
+ return @parents[parent.source_context.name] = parent
166
+ end
167
+
168
+ unless @parents.key?(parent)
169
+ @parents[parent] = load(parent, -1)
170
+ end
171
+
172
+ @parents[parent]
173
+ end
174
+
175
+ def macro?(name, context)
176
+ if respond_to?(name.to_sym)
177
+ return true
178
+ end
179
+
180
+ unless (parent = get_parent(context))
181
+ return false
182
+ end
183
+
184
+ parent.macro?(name, context)
185
+ end
186
+
187
+ def render_macro(name, context, args, lineno, source)
188
+ macro_method = macro_template_reference(name, context, lineno, source)
189
+ macro_arguments = macro_method.parameters.select { |arg| arg[0] == :key }.map { |_, arg| arg }
190
+ mapped_arguments = AutoHash.new
191
+ kwarg = false
192
+
193
+ args.each do |key, value|
194
+ if !kwarg && key.is_a?(Integer)
195
+ if key >= 0 && key < macro_arguments.length
196
+ mapped_key = macro_arguments[key]
197
+ else
198
+ mapped_arguments.add(value)
199
+
200
+ next
201
+ end
202
+ elsif kwarg && key.is_a?(Integer)
203
+ raise Error::Runtime.new('Cannot place a positional argument after a keyword argument.', lineno, source)
204
+ else
205
+ kwarg = true
206
+ mapped_key = key.to_sym
207
+ end
208
+
209
+ if mapped_arguments.key?(mapped_key)
210
+ raise Error::Runtime.new("Argument \"#{mapped_key.inspect}\" passed twice.", lineno, source)
211
+ end
212
+
213
+ mapped_arguments[mapped_key] = value
214
+ end
215
+
216
+ macro_method.call(context.call_context, **mapped_arguments)
217
+ end
218
+
219
+ # @return [Method]
220
+ def macro_template_reference(name, context, lineno, source)
221
+ if respond_to?(name.to_sym)
222
+ return method(name.to_sym)
223
+ end
224
+
225
+ parent = self
226
+ while (parent = parent.get_parent(context))
227
+ if parent.respond_to?(name.to_sym)
228
+ return parent.method(name.to_sym)
229
+ end
230
+ end
231
+
232
+ raise Error::Runtime.new(
233
+ "Macro \"#{name.delete_prefix('macro_')}\" is not defined in template \"#{template_name}\".",
234
+ lineno,
235
+ source
236
+ )
237
+ end
238
+
239
+ def source_context
240
+ raise NotImplementedError
241
+ end
242
+
243
+ def traitable?
244
+ true
245
+ end
246
+
247
+ def unwrap
248
+ self
35
249
  end
36
250
 
37
251
  private
38
252
 
39
- # @param [String] name
40
- # @return ]Template]
41
- def load_template(name, template_name = '', template_line = nil)
42
- env.load_template(name, call_context: @call_context, output_buffer: @output_buffer)
253
+ # @param [String, TemplateWrapper] template
254
+ # @return [Template]
255
+ def load(template, line, index = nil)
256
+ if template.is_a?(Array)
257
+ return env.resolve_template(template).unwrap
258
+ end
259
+
260
+ if template.is_a?(TemplateWrapper)
261
+ return template.unwrap
262
+ end
263
+
264
+ if template == template_name
265
+ klass = self.class.name
266
+
267
+ if (pos = klass.rindex('___'))
268
+ klass = klass[0...pos]
269
+ end
270
+ else
271
+ klass = env.template_class(template)
272
+ end
273
+
274
+ env.load_template(klass, template, index:)
275
+ rescue Error::Base => e
276
+ unless e.source_context
277
+ e.source_context = source_context
278
+ end
279
+
280
+ if e.lineno.positive?
281
+ raise e
282
+ end
283
+
284
+ if line == -1
285
+ e.guess
286
+ else
287
+ e.lineno = line
288
+ end
289
+
290
+ raise e
291
+ end
292
+
293
+ # Overloaded by children
294
+ def do_get_parent(context)
295
+ false
43
296
  end
44
297
 
45
298
  # @return [Environment]