twig_ruby 0.0.1

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 (64) hide show
  1. checksums.yaml +7 -0
  2. data/lib/twig/auto_hash.rb +17 -0
  3. data/lib/twig/cache/base.rb +31 -0
  4. data/lib/twig/cache/filesystem.rb +47 -0
  5. data/lib/twig/cache/nil.rb +19 -0
  6. data/lib/twig/callable.rb +21 -0
  7. data/lib/twig/compiler.rb +123 -0
  8. data/lib/twig/context.rb +64 -0
  9. data/lib/twig/environment.rb +161 -0
  10. data/lib/twig/error/base.rb +37 -0
  11. data/lib/twig/error/syntax.rb +8 -0
  12. data/lib/twig/expression_parser.rb +517 -0
  13. data/lib/twig/extension/base.rb +23 -0
  14. data/lib/twig/extension/core.rb +89 -0
  15. data/lib/twig/extension/rails.rb +70 -0
  16. data/lib/twig/extension_set.rb +69 -0
  17. data/lib/twig/lexer.rb +372 -0
  18. data/lib/twig/loader/array.rb +39 -0
  19. data/lib/twig/loader/base.rb +32 -0
  20. data/lib/twig/loader/filesystem.rb +45 -0
  21. data/lib/twig/node/base.rb +61 -0
  22. data/lib/twig/node/block.rb +20 -0
  23. data/lib/twig/node/block_reference.rb +17 -0
  24. data/lib/twig/node/empty.rb +11 -0
  25. data/lib/twig/node/expression/array.rb +50 -0
  26. data/lib/twig/node/expression/assign_name.rb +28 -0
  27. data/lib/twig/node/expression/base.rb +20 -0
  28. data/lib/twig/node/expression/binary/base.rb +63 -0
  29. data/lib/twig/node/expression/call.rb +28 -0
  30. data/lib/twig/node/expression/constant.rb +17 -0
  31. data/lib/twig/node/expression/filter.rb +52 -0
  32. data/lib/twig/node/expression/get_attribute.rb +30 -0
  33. data/lib/twig/node/expression/helper_method.rb +31 -0
  34. data/lib/twig/node/expression/name.rb +37 -0
  35. data/lib/twig/node/expression/ternary.rb +28 -0
  36. data/lib/twig/node/expression/unary/base.rb +52 -0
  37. data/lib/twig/node/expression/variable/assign_context.rb +11 -0
  38. data/lib/twig/node/expression/variable/context.rb +11 -0
  39. data/lib/twig/node/for.rb +64 -0
  40. data/lib/twig/node/for_loop.rb +39 -0
  41. data/lib/twig/node/if.rb +50 -0
  42. data/lib/twig/node/include.rb +71 -0
  43. data/lib/twig/node/module.rb +74 -0
  44. data/lib/twig/node/nodes.rb +13 -0
  45. data/lib/twig/node/print.rb +18 -0
  46. data/lib/twig/node/text.rb +20 -0
  47. data/lib/twig/node/yield.rb +54 -0
  48. data/lib/twig/output_buffer.rb +29 -0
  49. data/lib/twig/parser.rb +131 -0
  50. data/lib/twig/railtie.rb +60 -0
  51. data/lib/twig/source.rb +13 -0
  52. data/lib/twig/template.rb +50 -0
  53. data/lib/twig/token.rb +48 -0
  54. data/lib/twig/token_parser/base.rb +20 -0
  55. data/lib/twig/token_parser/block.rb +54 -0
  56. data/lib/twig/token_parser/extends.rb +25 -0
  57. data/lib/twig/token_parser/for.rb +64 -0
  58. data/lib/twig/token_parser/if.rb +64 -0
  59. data/lib/twig/token_parser/include.rb +51 -0
  60. data/lib/twig/token_parser/yield.rb +44 -0
  61. data/lib/twig/token_stream.rb +73 -0
  62. data/lib/twig/twig_filter.rb +21 -0
  63. data/lib/twig_ruby.rb +36 -0
  64. metadata +103 -0
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ # @!attribute [r] extensions
5
+ # @return [Hash<String, Extension::Base>]
6
+ class ExtensionSet
7
+ attr_reader :extensions
8
+
9
+ def initialize
10
+ @extensions = {}
11
+ @extensions.default_proc = lambda { |_hash, key|
12
+ raise "Extension '#{key}' does not exist"
13
+ }
14
+ end
15
+
16
+ # @param [Extension::Base] extension
17
+ def add(extension)
18
+ raise "Extension #{extension.class.name} already added" if has?(extension)
19
+
20
+ @extensions[extension.class.name] = extension
21
+ end
22
+
23
+ # @param [Object, String] extension
24
+ # @return [Boolean]
25
+ def has?(extension)
26
+ extension = extension.class.name unless extension.is_a?(String)
27
+ extensions.key?(extension.to_s)
28
+ end
29
+
30
+ def operators
31
+ all_unary = {}
32
+ all_binary = {}
33
+
34
+ extensions.values.map(&:operators).each do |unary, binary|
35
+ all_unary.merge!(unary)
36
+ all_binary.merge!(binary)
37
+ end
38
+
39
+ [all_unary, all_binary]
40
+ end
41
+
42
+ def helper_methods
43
+ @helper_methods ||= extensions.
44
+ values.
45
+ map(&:helper_methods).
46
+ reduce([], :concat).
47
+ map(&:to_sym)
48
+ end
49
+
50
+ def filters
51
+ @filters ||= extensions.values.map(&:filters).reduce({}, :merge)
52
+ end
53
+
54
+ def filter(name)
55
+ filters[name.to_sym]
56
+ end
57
+
58
+ def token_parsers
59
+ @token_parsers ||= extensions.
60
+ values.map(&:token_parsers).reduce([], :concat).
61
+ to_h { |token_parser| [token_parser.tag.to_sym, token_parser] }
62
+ end
63
+
64
+ # @return [TokenParser::Base|nil]
65
+ def token_parser(name)
66
+ token_parsers[name.to_sym]
67
+ end
68
+ end
69
+ end
data/lib/twig/lexer.rb ADDED
@@ -0,0 +1,372 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ class Lexer
5
+ TAG_COMMENT = %w[{# #}].freeze
6
+ TAG_BLOCK = %w[{% %}].freeze
7
+ TAG_VARIABLE = %w[{{ }}].freeze
8
+ WHITESPACE_TRIM = '-'
9
+ WHITESPACE_LINE_TRIM = '~'
10
+ WHITESPACE_LINE_CHARS = " \t\0\x0B"
11
+ INTERPOLATION = %w[#{ }].freeze
12
+ OPENING_BRACKET = '([{'.chars
13
+ CLOSING_BRACKET = ')]}'.chars
14
+ PUNCTUATION = OPENING_BRACKET + CLOSING_BRACKET + '?:.,|'.chars
15
+
16
+ REGEX_LNUM = /[0-9]+(_[0-9]+)*/
17
+ REGEX_FRAC = /\.#{REGEX_LNUM}/
18
+ REGEX_EXPONENT = /[eE][+-]?#{REGEX_LNUM}/
19
+ REGEX_DNUM = /#{REGEX_LNUM}(?:#{REGEX_FRAC})?/
20
+
21
+ REGEX_NAME = /[a-zA-Z_][a-zA-Z0-9_]*/
22
+ REGEX_SYMBOL = /:#{REGEX_NAME}/
23
+ REGEX_STRING = /\A"([^#"\\]*(?:\\\\.[^#"\\]*)*)"|'([^'\\]*(?:\\\\.[^'\\]*)*)'/s
24
+ REGEX_NUMBER = /\A(?:#{REGEX_DNUM}(?:#{REGEX_EXPONENT})?)/x
25
+
26
+ STATE_DATA = 0
27
+ STATE_BLOCK = 1
28
+ STATE_VAR = 2
29
+ STATE_STRING = 3
30
+ STATE_INTERPOLATION = 4
31
+
32
+ # @param [Environment] environment
33
+ def initialize(environment)
34
+ @environment = environment
35
+ end
36
+
37
+ # @param [Twig::Source] source
38
+ def tokenize(source)
39
+ @source = source
40
+ @code = source.code.tr("\r\n", "\n")
41
+ @cursor = 0
42
+ @lineno = 1
43
+ @end = @code.length
44
+ @tokens = []
45
+ @state = STATE_DATA
46
+ @states = []
47
+ @brackets = []
48
+ @position = -1
49
+ @positions = @code.to_enum(:scan, lex_tokens_start).map { Regexp.last_match }
50
+
51
+ while @cursor < @end
52
+ case @state
53
+ when STATE_DATA
54
+ lex_data
55
+ when STATE_BLOCK
56
+ lex_block
57
+ when STATE_VAR
58
+ lex_var
59
+ else
60
+ raise "Unknown state: #{@state}"
61
+ end
62
+ end
63
+
64
+ push_token(Token::EOF_TYPE)
65
+
66
+ TokenStream.new(@tokens, @source)
67
+ end
68
+
69
+ private
70
+
71
+ def lex_data
72
+ # If no matches are left we return the rest of the template as simple text token
73
+ if @position == @positions.length - 1
74
+ push_token(Token::TEXT_TYPE, @code[@cursor..])
75
+ @cursor = @end
76
+
77
+ return
78
+ end
79
+
80
+ # Find the first token after the current cursor
81
+ @position += 1
82
+ position = @positions[@position]
83
+
84
+ while position.begin(0) < @cursor
85
+ return if @position == @positions.length - 1
86
+
87
+ @position += 1
88
+ position = @positions[@position]
89
+ end
90
+
91
+ # Push the template text first
92
+ text = text_content = @code[@cursor, (position.begin(0) - @cursor)]
93
+
94
+ # TODO: Trim
95
+
96
+ push_token(Token::TEXT_TYPE, text)
97
+ move_cursor(text_content + position.to_s)
98
+
99
+ case @positions[@position][1]
100
+ when TAG_BLOCK[0]
101
+ if (match = @code[@cursor...].match(lex_block_raw_regex))
102
+ move_cursor(match.to_s)
103
+ lex_raw_data
104
+ elsif (match = @code[@cursor...].match(lex_block_line_regex))
105
+ move_cursor(match[0].to_s)
106
+ @lineno = match[1].to_i
107
+ else
108
+ push_token(Token::BLOCK_START_TYPE)
109
+ push_state(STATE_BLOCK)
110
+ @current_var_block_line = @lineno
111
+ end
112
+ when TAG_VARIABLE[0]
113
+ push_token(Token::VAR_START_TYPE)
114
+ push_state(STATE_VAR)
115
+ @current_var_block_line = @lineno
116
+ else
117
+ raise "Invalid start token #{@positions[@position]}"
118
+ end
119
+ end
120
+
121
+ def lex_raw_data
122
+ unless (match = @code[@cursor...].match(lex_raw_data_regex))
123
+ raise "Uexpected end of file. Unclosed 'verbatim' block"
124
+ end
125
+
126
+ text = @code[@cursor, match.begin(0)]
127
+ move_cursor(@code[@cursor, (match.begin(0) + match.to_s.length)])
128
+
129
+ # trim
130
+ if match[1]
131
+ text = if match[1] == WHITESPACE_TRIM
132
+ text.gsub(/ *$/, '') # space trim
133
+ else
134
+ text.rstrip # line trim
135
+ end
136
+ end
137
+
138
+ push_token(Token::TEXT_TYPE, text)
139
+ end
140
+
141
+ def lex_block
142
+ if @brackets.empty? && (match = @code[@cursor..].match(lex_block_regex))
143
+ push_token(Token::BLOCK_END_TYPE)
144
+ move_cursor(match.to_s)
145
+ pop_state
146
+ else
147
+ lex_expression
148
+ end
149
+ end
150
+
151
+ def lex_var
152
+ match = @code[@cursor...].match(lex_var_regex)
153
+
154
+ if @brackets.empty? && match
155
+ push_token(Token::VAR_END_TYPE)
156
+ move_cursor(match.to_s)
157
+ pop_state
158
+ else
159
+ lex_expression
160
+ end
161
+ end
162
+
163
+ def lex_expression
164
+ @code[@cursor..].match(/\A\s+/) do |match|
165
+ move_cursor(match.to_s)
166
+
167
+ if @cursor >= @end
168
+ raise "Unclosed #{@state == STATE_BLOCK ? 'block' : 'variable'}"
169
+ end
170
+ end
171
+
172
+ # Spread operator
173
+ if code_at?(0, '.') && (@cursor + 2 < @end) && code_at?(1, '.') && code_at?(2, '.')
174
+ push_token(Token::SPREAD_TYPE)
175
+ move_cursor('...')
176
+ # Arrow function
177
+ elsif code_at?(0, '=') && (@cursor + 1 < @end) && code_at?(1, '>')
178
+ push_token(Token::ARROW_TYPE)
179
+ move_cursor('=>')
180
+ elsif (match = @code[@cursor..].match(operator_regex))
181
+ push_token(Token::OPERATOR_TYPE, match.to_s.gsub('/\s+/', ' '))
182
+ move_cursor(match.to_s)
183
+ elsif (match = @code[@cursor..].match(/\A#{REGEX_NAME}/))
184
+ push_token(Token::NAME_TYPE, match.to_s)
185
+ move_cursor(match.to_s)
186
+ elsif (match = @code[@cursor..].match(/\A#{REGEX_SYMBOL}/))
187
+ push_token(Token::SYMBOL_TYPE, match.to_s[1..])
188
+ move_cursor(match.to_s)
189
+ elsif (match = @code[@cursor..].match(REGEX_NUMBER))
190
+ value = match.to_s.tr('_', '')
191
+ value = value.to_i.to_s == value ? value.to_i : value.to_f
192
+ push_token(Token::NUMBER_TYPE, value)
193
+ move_cursor(match.to_s)
194
+ elsif code_at?(0, PUNCTUATION)
195
+ # opening bracket
196
+ if code_at?(0, OPENING_BRACKET)
197
+ @brackets << [code_at, @lineno]
198
+ elsif code_at?(0, CLOSING_BRACKET)
199
+ if @brackets.empty?
200
+ raise Error::Syntax.new("Unexpected closing bracket: #{code_at}", @lineno, @source)
201
+ end
202
+
203
+ expect, lineno = @brackets.pop
204
+
205
+ unless code_at?(0, expect.tr(OPENING_BRACKET.join, CLOSING_BRACKET.join))
206
+ raise Error::Syntax.new("Unclosed bracket: #{code_at}", lineno, @source)
207
+ end
208
+ end
209
+
210
+ push_token(Token::PUNCTUATION_TYPE, code_at)
211
+ @cursor += 1
212
+ elsif (match = @code[@cursor..].match(REGEX_STRING))
213
+ push_token(Token::STRING_TYPE, match.to_s[1...-1])
214
+ move_cursor(match.to_s)
215
+ end
216
+
217
+ <<-TEMP
218
+ // opening double quoted string
219
+ elseif (preg_match(self::REGEX_DQ_STRING_DELIM, $this->code, $match, 0, $this->cursor)) {
220
+ $this->brackets[] = ['"', $this->lineno];
221
+ $this->pushState(self::STATE_STRING);
222
+ $this->moveCursor($match[0]);
223
+ }
224
+ // inline comment
225
+ elseif (preg_match(self::REGEX_INLINE_COMMENT, $this->code, $match, 0, $this->cursor)) {
226
+ $this->moveCursor($match[0]);
227
+ }
228
+ // unlexable
229
+ else {
230
+ throw new SyntaxError(\sprintf('Unexpected character "%s".', $this->code[$this->cursor]), $this->lineno, $this->source);
231
+ }
232
+ TEMP
233
+ end
234
+
235
+ def push_token(type, value = '')
236
+ return if type == Token::TEXT_TYPE && value.empty?
237
+
238
+ @tokens << Token.new(type, value, @lineno)
239
+ end
240
+
241
+ def push_state(state)
242
+ @states << @state
243
+ @state = state
244
+ end
245
+
246
+ def pop_state
247
+ if @states.empty?
248
+ raise 'Cannot pop state without a previous state.'
249
+ end
250
+
251
+ @state = @states.pop
252
+ end
253
+
254
+ def move_cursor(text)
255
+ @cursor += text.length
256
+ @lineno += text.scan("\n").count
257
+ end
258
+
259
+ # @param [Integer] seek
260
+ # @return [String]
261
+ def code_at(seek = 0)
262
+ @code[@cursor + seek]
263
+ end
264
+
265
+ # @param [Integer] seek
266
+ # @param [String | Array] char
267
+ def code_at?(seek, char)
268
+ dest = code_at(seek)
269
+
270
+ case char
271
+ when Array
272
+ char.include?(dest)
273
+ when String
274
+ dest == char
275
+ else
276
+ raise "Invalid char: #{char.inspect}"
277
+ end
278
+ end
279
+
280
+ def lex_tokens_start
281
+ @lex_tokens_start ||=
282
+ /
283
+ (#{Regexp.union([TAG_VARIABLE[0], TAG_BLOCK[0], TAG_COMMENT[0]])})
284
+ (#{Regexp.union([WHITESPACE_TRIM, WHITESPACE_LINE_TRIM])})?
285
+ /xm
286
+ end
287
+
288
+ def lex_var_regex
289
+ @lex_var_regex ||=
290
+ /\A\s*(?:
291
+ #{Regexp.union(
292
+ "#{WHITESPACE_TRIM}#{TAG_VARIABLE[1]}\\s*",
293
+ WHITESPACE_LINE_TRIM + TAG_VARIABLE[1] + "[#{WHITESPACE_LINE_CHARS}]*",
294
+ TAG_VARIABLE[1]
295
+ )}
296
+ )/x
297
+ end
298
+
299
+ def lex_block_raw_regex
300
+ @lex_block_raw_regex ||=
301
+ /\A\s*verbatim\s*(?:
302
+ #{Regexp.union(
303
+ "#{WHITESPACE_TRIM}#{TAG_BLOCK[1]}\\s*",
304
+ WHITESPACE_LINE_TRIM + TAG_BLOCK[1] + "[#{WHITESPACE_LINE_CHARS}]*",
305
+ TAG_BLOCK[1]
306
+ )}
307
+ )/sx
308
+ end
309
+
310
+ def lex_block_line_regex
311
+ @lex_block_line_regex ||= /\A\s*line\s+(\d+)\s*#{Regexp.escape(TAG_BLOCK[1])}/s
312
+ end
313
+
314
+ def lex_block_regex
315
+ @lex_block_regex ||=
316
+ /\A\s*(?:
317
+ #{Regexp.union(
318
+ /#{WHITESPACE_TRIM}#{TAG_BLOCK[1]}\s*\n?/,
319
+ WHITESPACE_LINE_TRIM + TAG_BLOCK[1] + "[#{WHITESPACE_LINE_CHARS}]*",
320
+ /#{TAG_BLOCK[1]}\n?/
321
+ )}
322
+ )/x
323
+ end
324
+
325
+ def lex_raw_data_regex
326
+ @lex_raw_data_regex ||=
327
+ /
328
+ #{TAG_BLOCK[0]}
329
+ (#{Regexp.union(WHITESPACE_TRIM, WHITESPACE_LINE_TRIM)})?\s*endverbatim\s*
330
+ (?:#{Regexp.union(
331
+ "#{WHITESPACE_TRIM}#{TAG_BLOCK[1]}\\s*",
332
+ WHITESPACE_LINE_TRIM + TAG_BLOCK[1] + "[#{WHITESPACE_LINE_CHARS}]*",
333
+ TAG_BLOCK[1]
334
+ )})
335
+ /sx
336
+ end
337
+
338
+ def operator_regex
339
+ return @operator_regex if defined?(@operator_regex)
340
+
341
+ unary, binary = @environment.operators
342
+ operators = ([:'='] + unary.keys + binary.keys).
343
+ to_h { |op| [op, op.length] }.
344
+ sort_by { |_, length| -length }.
345
+ to_h
346
+
347
+ chain = []
348
+
349
+ operators.each_key do |operator|
350
+ regex = Regexp.escape(operator)
351
+
352
+ # an operator that ends with a character must be followed by
353
+ # a whitespace, a parenthesis, an opening map [ or sequence {
354
+ if operator[-1].match(/\w/)
355
+ regex << '(?=[\s()\[{])'
356
+ end
357
+
358
+ # an operator that begins with a character must not have a dot or pipe before
359
+ if operator[0].match(/\w/)
360
+ regex = "(?<![\\.\\|])#{regex}"
361
+ end
362
+
363
+ # an operator with a space can be any amount of whitespaces
364
+ regex.gsub!(/\s+/, '\s+')
365
+
366
+ chain << regex
367
+ end
368
+
369
+ @operator_regex = Regexp.new("\\A(?:#{chain.join('|')})")
370
+ end
371
+ end
372
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ module Loader
5
+ class Array < Loader::Base
6
+ # @param [Hash<String>] templates
7
+ def initialize(templates)
8
+ super()
9
+
10
+ @templates = templates.transform_keys(&:to_sym)
11
+ end
12
+
13
+ def get_source_context(name)
14
+ name = name.to_sym
15
+ raise "LoaderError: Template #{name} is not defined" unless @templates[name]
16
+
17
+ ::Twig::Source.new(@templates[name], name)
18
+ end
19
+
20
+ def exists?(name)
21
+ @templates.key?(name.to_sym)
22
+ end
23
+
24
+ def get_cache_key(name)
25
+ name = name.to_sym
26
+ raise "LoaderError: Template #{name} is not defined" unless @templates[name]
27
+
28
+ "#{name}:#{@templates[name]}"
29
+ end
30
+
31
+ def fresh?(name, time)
32
+ name = name.to_sym
33
+ raise "LoaderError: Template #{name} is not defined" unless @templates[name]
34
+
35
+ true
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ module Loader
5
+ class Base
6
+ # @param [String] name
7
+ # @return [Twig::Source]
8
+ def get_source_context(name)
9
+ raise 'get_source_context not implemented'
10
+ end
11
+
12
+ # @param [String] name
13
+ # @return [String]
14
+ def get_cache_key(name)
15
+ raise 'get_cache_key not implemented'
16
+ end
17
+
18
+ # @param [String] name
19
+ # @param [Integer] time
20
+ # @return [Boolean]
21
+ def fresh?(name, time)
22
+ raise 'fresh? not implemented'
23
+ end
24
+
25
+ # @param [String] name
26
+ # @return [Boolean]
27
+ def exists?(name)
28
+ raise 'exists? not implemented'
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ module Loader
5
+ class Filesystem < Loader::Base
6
+ def initialize(root_path, paths = [])
7
+ super()
8
+
9
+ @root_path = root_path.to_s
10
+ @paths = paths.map(&:to_s)
11
+ end
12
+
13
+ def get_source_context(name)
14
+ if (file = find_template(name))
15
+ return Source.new(File.read(file), name)
16
+ end
17
+
18
+ raise "Unable to find '#{name}'"
19
+ end
20
+
21
+ def get_cache_key(name)
22
+ return unless (path = find_template(name))
23
+
24
+ path.delete_prefix(@root_path)
25
+ end
26
+
27
+ def fresh?(name, time)
28
+ if (file = find_template(name))
29
+ return File.mtime(file).to_i < time
30
+ end
31
+
32
+ false
33
+ end
34
+
35
+ private
36
+
37
+ def find_template(name)
38
+ @paths.each do |path|
39
+ absolute = File.join(@root_path, path, name)
40
+ return absolute if File.file?(absolute)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ module Node
5
+ class Base
6
+ attr_reader :tag, :lineno
7
+
8
+ # @return [Hash]
9
+ attr_reader :attributes
10
+
11
+ # @return [Source]
12
+ attr_reader :source_context
13
+
14
+ # @return [AutoHash<Node::Base>]
15
+ attr_reader :nodes
16
+
17
+ # @param [Hash<Node::Base>] nodes
18
+ # @param [Hash] attributes
19
+ # @param [Integer] lineno
20
+ def initialize(nodes = {}, attributes = {}, lineno = 0)
21
+ invalid = nodes.
22
+ values.
23
+ detect { |node| !node.class.ancestors.include?(Node::Base) }
24
+
25
+ raise "#{invalid.inspect} does not extend from #{Node::Base.name}" if invalid
26
+
27
+ @nodes = AutoHash[nodes]
28
+ @nodes.default_proc = ->(_hash, key) { raise "Node '#{key}' does not exist" }
29
+
30
+ @attributes = attributes
31
+ @attributes.default_proc = ->(_hash, key) { raise "Attribute '#{key}' does not exist" }
32
+
33
+ @lineno = lineno
34
+ @tag = nil
35
+ end
36
+
37
+ # @param [String] tag
38
+ def tag=(tag)
39
+ raise 'Cannot only set node tag once' if @tag
40
+
41
+ @tag = tag
42
+ end
43
+
44
+ # @param [Compiler] compiler
45
+ def compile(compiler)
46
+ @nodes.each_value { |node| compiler.subcompile(node) }
47
+ end
48
+
49
+ # @param [Source] source
50
+ def source_context=(source)
51
+ @source_context = source
52
+ @nodes.each_value { |node| node.source_context = source }
53
+ end
54
+
55
+ # @return [String]
56
+ def template_name
57
+ source_context.name
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ module Node
5
+ class Block < Node::Base
6
+ def initialize(name, body, lineno)
7
+ super({ body: }, { name: }, lineno)
8
+ end
9
+
10
+ def compile(compiler)
11
+ compiler.
12
+ write("def block_#{attributes[:name]}(context, blocks)\n").
13
+ indent.
14
+ subcompile(nodes[:body]).
15
+ outdent.
16
+ write("end\n\n")
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ module Node
5
+ class BlockReference < Node::Base
6
+ def initialize(name, lineno)
7
+ super({}, { name: }, lineno)
8
+ end
9
+
10
+ def compile(compiler)
11
+ compiler.
12
+ write("yield_block(:#{attributes[:name]}, context, block_list.merge(blocks));").
13
+ raw("\n")
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Twig
4
+ module Node
5
+ class Empty < Node::Base
6
+ def initialize(lineno = 0)
7
+ super({}, {}, lineno)
8
+ end
9
+ end
10
+ end
11
+ end