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,7 +3,10 @@
3
3
  module Twig
4
4
  class Environment
5
5
  # @return [Cache::Base]
6
- attr_reader :cache
6
+ attr_reader :cache, :charset
7
+
8
+ # @return [Twig::Loader::Base]
9
+ attr_reader :loader
7
10
 
8
11
  # @param [::Twig::Loader::Base] loader
9
12
  def initialize(loader, options = {})
@@ -12,87 +15,210 @@ module Twig
12
15
  @options = {
13
16
  cache: false,
14
17
  debug: false,
18
+ charset: 'UTF-8',
19
+ strict_variables: false,
20
+ autoescape: :html,
15
21
  auto_reload: nil,
22
+ allow_helper_methods: false,
16
23
  }.merge(options)
17
24
 
18
- @auto_reload = options[:auto_reload].nil? ? options[:debug] : options[:auto_reload]
25
+ @auto_reload = @options[:auto_reload].nil? ? @options[:debug] : @options[:auto_reload]
26
+ @charset = @options[:charset]
27
+ @strict_variables = @options[:strict_variables]
19
28
 
20
29
  self.cache = @options[:cache]
21
30
 
31
+ @loaded_templates = {}
32
+ @globals = {}
33
+ @runtimes = {}
34
+ @runtime_loaders = []
35
+ @default_runtime_loader = RuntimeLoader::Factory.new({
36
+ Runtime::Escaper => -> { Runtime::Escaper.new(@charset) },
37
+ })
38
+
22
39
  add_extension(Extension::Core.new)
40
+ add_extension(Extension::Escaper.new(@options[:autoescape]))
23
41
  end
24
42
 
25
- def template_class(name)
43
+ def template_class(name, index = nil)
26
44
  key = loader.get_cache_key(name)
27
45
 
28
- "Compiled::Template_#{::Digest::SHA256.hexdigest(key)}"
46
+ "Compiled::Template_#{::Digest::SHA256.hexdigest(key)}#{"___#{index}" if index}"
47
+ end
48
+
49
+ # @param [String, Twig::TemplateWrapper] name
50
+ # @return [Twig::TemplateWrapper]
51
+ def load(name)
52
+ if name.is_a?(Twig::TemplateWrapper)
53
+ return name
54
+ end
55
+
56
+ cls = template_class(name)
57
+ TemplateWrapper.new(
58
+ self,
59
+ load_template(cls, name)
60
+ )
61
+ end
62
+
63
+ # @param [String, Twig::TemplateWrapper, Array<String>] name
64
+ def resolve_template(names)
65
+ unless names.is_a?(Array)
66
+ return load(names)
67
+ end
68
+
69
+ count = names.length
70
+
71
+ names.each do |name|
72
+ if name.is_a?(Twig::TemplateWrapper)
73
+ return name
74
+ end
75
+
76
+ unless count == 1 || loader.exists?(name)
77
+ next
78
+ end
79
+
80
+ return load(name)
81
+ end
82
+
83
+ raise Error::Loader, "Unable to find one of the following templates: \"#{names.join('", "')}\"."
29
84
  end
30
85
 
86
+ # @param [String] cls The class name to load
87
+ # @param [String] name The template name
88
+ # @param [Integer, nil] index Index for embedded templates
31
89
  # @return [Twig::Template]
32
- def load_template(name, **)
33
- class_name = template_class(name)
34
- cache_key = cache.generate_key(name, class_name)
90
+ def load_template(cls, name, index: nil)
91
+ main_cls = cls
35
92
 
36
- attempt_cache = !@auto_reload || template_fresh?(name, cache.timestamp(cache_key))
93
+ if index
94
+ cls += "___#{index}"
95
+ end
37
96
 
38
- if attempt_cache
39
- @cache.load(cache_key)
97
+ if !index.nil? && Twig.const_defined?(cls)
98
+ return @loaded_templates[cls] = Twig.const_get(cls).new(self)
40
99
  end
41
100
 
42
- # Cache didn't load a class or we should load fresh
43
- unless attempt_cache && Twig.const_defined?(class_name)
44
- code = render_ruby(name)
101
+ cache_key = cache.generate_key(name, main_cls)
102
+
103
+ # If not auto_reloading just use whatever is in the cache
104
+ if !@auto_reload || template_fresh?(name, cache.timestamp(cache_key))
105
+ if @loaded_templates.key?(cls)
106
+ return @loaded_templates[cls]
107
+ end
45
108
 
46
- # File cache loader won't rely on eval
47
- @cache.write(cache_key, code)
48
109
  @cache.load(cache_key)
49
110
 
50
- # Finally just eval the generated code if cache does
51
- # create the class
52
- Twig.module_eval(code) unless Twig.const_defined?(class_name)
111
+ if Twig.const_defined?(cls)
112
+ return @loaded_templates[cls] = Twig.const_get(cls).new(self)
113
+ end
53
114
  end
54
115
 
55
- Twig.const_get(template_class(name)).
56
- new(self, **)
57
- end
116
+ # No caches have fresh template at this point, so write it now
117
+ code = render_ruby(name)
58
118
 
59
- def render(name)
60
- loader.get_source_context(name).code
119
+ # File cache loader won't rely on eval
120
+ @cache.write(cache_key, code)
121
+ @cache.load(cache_key)
122
+
123
+ # Finally just eval the generated code if cache doesn't
124
+ # create the class
125
+ Twig.module_eval(code) unless Twig.const_defined?(cls)
126
+
127
+ @loaded_templates[cls] = Twig.const_get(cls).new(self)
61
128
  end
62
129
 
63
- def render_ruby(name)
64
- compile_source(
65
- loader.get_source_context(name)
66
- )
130
+ # @return [Twig::Template]
131
+ def create_template(template, name = nil)
132
+ hash = ::Digest::SHA256.hexdigest(template)
133
+ name = if name.nil?
134
+ "__string_template__#{hash}"
135
+ else
136
+ "#{name} (string template #{hash})"
137
+ end
138
+
139
+ chain_loader = Loader::Chain.new([
140
+ Loader::Array.new({ name => template }),
141
+ current = loader,
142
+ ])
143
+
144
+ @loader = chain_loader
145
+
146
+ cls = template_class(name)
147
+ TemplateWrapper.new(self, load_template(cls, name))
148
+ ensure
149
+ @loader = current
67
150
  end
68
151
 
69
152
  def extension(name)
70
- @extension_set.extensions[name]
153
+ extension_set.get(name)
154
+ end
155
+
156
+ def extension?(name)
157
+ extension_set.has?(name)
71
158
  end
72
159
 
73
160
  # @param [Extension::Base] extension
74
161
  def add_extension(extension)
75
- @extension_set.add(extension)
162
+ extension_set.add(extension)
76
163
  end
77
164
 
78
- # @return [Array]
79
- def operators
80
- @extension_set.operators
165
+ def runtime(klass)
166
+ return runtimes[klass] if runtimes.key?(klass)
167
+
168
+ runtime_loaders.each do |loader|
169
+ if (runtime = loader.load(klass))
170
+ return runtimes[klass] = runtime
171
+ end
172
+ end
173
+
174
+ if (runtime = default_runtime_loader.load(klass))
175
+ return runtimes[klass] = runtime
176
+ end
177
+
178
+ raise Error::Runtime, "Unable to load the \"#{klass}\" runtime."
81
179
  end
82
180
 
83
- # @return [TwigFilter]
181
+ # @param [RuntimeLoader::Base] loader
182
+ def add_runtime_loader(loader)
183
+ runtime_loaders << loader
184
+ end
185
+
186
+ # @return [ExpressionParser::ExpressionParsers]
187
+ def expression_parsers
188
+ extension_set.expression_parsers
189
+ end
190
+
191
+ # @return [TwigFilter, nil]
84
192
  def filter(name)
85
- @extension_set.filter(name)
193
+ extension_set.filter(name)
194
+ end
195
+
196
+ # @return [TwigFunction, nil]
197
+ def function(name)
198
+ extension_set.function(name)
86
199
  end
87
200
 
88
- # @return [TokenParser::Base]
201
+ # @return [TwigTest, nil]
202
+ def test(name)
203
+ extension_set.test(name)
204
+ end
205
+
206
+ # @return [TokenParser::Base, nil]
89
207
  def token_parser(name)
90
- @extension_set.token_parser(name)
208
+ extension_set.token_parser(name)
91
209
  end
92
210
 
93
- # @return [Boolean]
94
- def helper_method?(name)
95
- @extension_set.helper_methods.include?(name)
211
+ # @return [Array<NodeVisitor::Base>]
212
+ def node_visitors
213
+ extension_set.node_visitors
214
+ end
215
+
216
+ def add_global(name, value)
217
+ @globals[name] = value
218
+ end
219
+
220
+ def globals
221
+ @resolved_globals ||= extension_set.globals.merge(@globals)
96
222
  end
97
223
 
98
224
  # @param [Source] source
@@ -114,6 +240,9 @@ module Twig
114
240
  # @param [Source] source
115
241
  def compile_source(source)
116
242
  compile(parse(tokenize(source)))
243
+ rescue Error::Base => e
244
+ e.source_context = source
245
+ raise e
117
246
  end
118
247
 
119
248
  # @return [String]
@@ -141,10 +270,38 @@ module Twig
141
270
  end
142
271
  end
143
272
 
273
+ # @return [Boolean]
274
+ def allow_helper_methods?
275
+ @options[:allow_helper_methods]
276
+ end
277
+
278
+ def debug?
279
+ @options[:debug]
280
+ end
281
+
282
+ def strict_variables?
283
+ @strict_variables
284
+ end
285
+
144
286
  private
145
287
 
146
- # @return [Twig::Loader::Base]
147
- attr_reader :loader
288
+ # @return [ExtensionSet]
289
+ attr_reader :extension_set
290
+
291
+ # @return [RuntimeLoader::Base]
292
+ attr_reader :default_runtime_loader
293
+
294
+ # @return [Array<RuntimeLoader::Base>]
295
+ attr_reader :runtime_loaders
296
+
297
+ # @return [Hash{String => Object}]
298
+ attr_reader :runtimes
299
+
300
+ def render_ruby(name)
301
+ compile_source(
302
+ loader.get_source_context(name)
303
+ )
304
+ end
148
305
 
149
306
  def lexer
150
307
  @lexer ||= Lexer.new(self)
@@ -3,34 +3,99 @@
3
3
  module Twig
4
4
  module Error
5
5
  class Base < StandardError
6
+ # @return [Integer]
6
7
  attr_reader :lineno
7
8
 
8
9
  # @param [String] message
9
10
  # @param [Integer] lineno
10
11
  # @param [Source] source
11
- def initialize(message, lineno = -1, source = nil)
12
- super(message)
13
-
14
- if source
15
- name = source.name
16
- @source_code = source.code
17
- @source_path = source.path
18
- else
19
- name = nil
20
- end
12
+ def initialize(message, lineno = -1, source = nil, previous = nil)
13
+ super('')
21
14
 
22
15
  @lineno = lineno
23
- @name = name
16
+ @source = source
24
17
  @raw_message = message
18
+ @previous = previous
19
+
20
+ update_repr
21
+ end
22
+
23
+ # @return [Source, nil]
24
+ def source_context
25
+ @source
26
+ end
27
+
28
+ # @param [Source, nil] source
29
+ def source_context=(source)
30
+ @source = source
31
+ update_repr
25
32
  end
26
33
 
27
34
  def to_s
28
- parts = [@raw_message]
29
- parts << [" in #{@name}"] if @name
30
- parts << [":#{@lineno}"] if @name && @lineno
31
- parts << [" on line #{@lineno}"] if !@name && @lineno
35
+ @message
36
+ end
37
+
38
+ def lineno=(lineno)
39
+ @lineno = lineno
40
+ update_repr
41
+ end
42
+
43
+ def append_message(raw_message)
44
+ @raw_message += raw_message
45
+ update_repr
46
+ end
47
+
48
+ def guess
49
+ locations = [self, @previous].compact.map(&:backtrace_locations).flatten
50
+ locations.each do |location|
51
+ next if location.nil?
52
+ next unless location.label&.start_with?('Twig::Compiled::')
53
+
54
+ klass, _method = location.label.split('#')
55
+ klass = Twig::Compiled.const_get(klass)
56
+
57
+ next unless source.name == klass.source_context.name
58
+
59
+ klass.debug_info.each do |source_line, template_line|
60
+ next unless source_line <= location.lineno
32
61
 
33
- parts.join
62
+ self.lineno = template_line
63
+ update_repr
64
+ return # rubocop:disable Lint/NonLocalExitFromIterator
65
+ end
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ # @return [Source, nil]
72
+ attr_reader :source
73
+
74
+ def update_repr
75
+ if !source.nil? && source.path
76
+ # only update file and line together
77
+ @file = source.path
78
+ @line = lineno.positive? ? lineno : -1
79
+ end
80
+
81
+ @message = @raw_message.dup
82
+ last = @message[-1]
83
+
84
+ if (punctuation = %w[. ?].include?(last) ? last : '')
85
+ @message = @message[0...-1]
86
+ end
87
+
88
+ if !source.nil? && source.name
89
+ @message += " in \"#{source.name}\""
90
+ end
91
+
92
+ if lineno.positive?
93
+ @message += " at line #{lineno}"
94
+ end
95
+
96
+ unless punctuation.empty?
97
+ @message += punctuation
98
+ end
34
99
  end
35
100
  end
36
101
  end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ module Error
5
+ class Loader < Error::Base
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ module Error
5
+ class Logic < Error::Base
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ module Error
5
+ class Runtime < Error::Base
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'infix/parses_arguments'
4
+
5
+ module Twig
6
+ module ExpressionParser
7
+ class Base
8
+ def to_s
9
+ "Some kinda EP: #{name}"
10
+ end
11
+
12
+ def name
13
+ raise NotImplementedError
14
+ end
15
+
16
+ def precedence
17
+ raise NotImplementedError
18
+ end
19
+
20
+ # @return [Symbol]
21
+ def type
22
+ raise NotImplementedError
23
+ end
24
+
25
+ def aliases
26
+ []
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ module ExpressionParser
5
+ class ExpressionParsers
6
+ # @param [Array<Twig::ExpressionParser::Base>] parsers
7
+ def initialize(parsers = [])
8
+ @parsers_by_name = {}
9
+ @parsers_by_class = {}
10
+
11
+ add(parsers)
12
+ end
13
+
14
+ # @param [Array<Twig::ExpressionParser::Base>] parsers
15
+ def add(parsers)
16
+ parsers.each do |parser|
17
+ if parser.precedence > 512 || parser.precedence.negative?
18
+ raise(
19
+ ArgumentError,
20
+ "Precedence for \"#{parser.name}\" must be between 0 and 512, got #{parser.precedence}."
21
+ )
22
+ end
23
+
24
+ parsers_by_name[parser.type] ||= {}
25
+ parsers_by_name[parser.type][parser.name] = parser
26
+ parsers_by_class[parser.class.name] = parser
27
+
28
+ parser.aliases.each do |alias_name|
29
+ parsers_by_name[parser.type][alias_name] = parser
30
+ end
31
+ end
32
+ end
33
+
34
+ def each(&)
35
+ parsers_by_name.values.map(&:values).flatten.each(&)
36
+ end
37
+
38
+ # @return [ExpressionParser::Base, nil]
39
+ def by_class(klass)
40
+ parsers_by_class[klass]
41
+ end
42
+
43
+ # @return [ExpressionParser::Base, nil]
44
+ def by_name(type, name)
45
+ parsers_by_name[type][name]
46
+ end
47
+
48
+ private
49
+
50
+ # @return [Hash<Array<Twig::ExpressionParser::Base>>]]
51
+ attr_reader :parsers_by_name
52
+
53
+ # @return [Hash<Twig::ExpressionParser::Base>]
54
+ attr_reader :parsers_by_class
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ module ExpressionParser
5
+ module Infix
6
+ class Arrow < InfixExpressionParser
7
+ def parse(parser, left, token)
8
+ # As the expression of the arrow function is independent from the current precedence,
9
+ # we want a precedence of 0
10
+ Node::Expression::ArrowFunction.new(parser.parse_expression, left, token.lineno)
11
+ end
12
+
13
+ def name
14
+ '=>'
15
+ end
16
+
17
+ def description
18
+ 'Arrow function (x => expr)'
19
+ end
20
+
21
+ def precedence
22
+ 250
23
+ end
24
+
25
+ def associativity
26
+ LEFT
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ module ExpressionParser
5
+ module Infix
6
+ class Binary < InfixExpressionParser
7
+ attr_reader :associativity, :precedence, :aliases, :name
8
+
9
+ def initialize(node_class, name, precedence, associativity = LEFT, description: nil, aliases: [])
10
+ super()
11
+
12
+ @node_class = node_class
13
+ @name = name
14
+ @precedence = precedence
15
+ @associativity = associativity
16
+ @description = description
17
+ @aliases = aliases
18
+ end
19
+
20
+ def parse(parser, left, token)
21
+ right = parser.parse_expression(
22
+ left? ? precedence + 1 : precedence
23
+ )
24
+
25
+ @node_class.new(left, right, token.lineno)
26
+ end
27
+
28
+ def description
29
+ @description || ''
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ module ExpressionParser
5
+ module Infix
6
+ class ConditionalTernary < InfixExpressionParser
7
+ def parse(parser, left, token)
8
+ then_expr = parser.parse_expression(precedence)
9
+
10
+ else_expr = if parser.stream.next_if(Token::PUNCTUATION_TYPE, ':')
11
+ # Ternary operator (expr ? expr2 : expr3)
12
+ parser.parse_expression(precedence)
13
+ else
14
+ # Ternary without else (expr ? expr2)
15
+ Node::Expression::Constant.new('', token.lineno)
16
+ end
17
+
18
+ Node::Expression::Ternary.new(left, then_expr, else_expr, token.lineno)
19
+ end
20
+
21
+ def name
22
+ '?'
23
+ end
24
+
25
+ def description
26
+ 'Conditional operator (a ? b : c)'
27
+ end
28
+
29
+ def precedence
30
+ 0
31
+ end
32
+
33
+ def associativity
34
+ LEFT
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end