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.
- checksums.yaml +7 -0
- data/lib/twig/auto_hash.rb +17 -0
- data/lib/twig/cache/base.rb +31 -0
- data/lib/twig/cache/filesystem.rb +47 -0
- data/lib/twig/cache/nil.rb +19 -0
- data/lib/twig/callable.rb +21 -0
- data/lib/twig/compiler.rb +123 -0
- data/lib/twig/context.rb +64 -0
- data/lib/twig/environment.rb +161 -0
- data/lib/twig/error/base.rb +37 -0
- data/lib/twig/error/syntax.rb +8 -0
- data/lib/twig/expression_parser.rb +517 -0
- data/lib/twig/extension/base.rb +23 -0
- data/lib/twig/extension/core.rb +89 -0
- data/lib/twig/extension/rails.rb +70 -0
- data/lib/twig/extension_set.rb +69 -0
- data/lib/twig/lexer.rb +372 -0
- data/lib/twig/loader/array.rb +39 -0
- data/lib/twig/loader/base.rb +32 -0
- data/lib/twig/loader/filesystem.rb +45 -0
- data/lib/twig/node/base.rb +61 -0
- data/lib/twig/node/block.rb +20 -0
- data/lib/twig/node/block_reference.rb +17 -0
- data/lib/twig/node/empty.rb +11 -0
- data/lib/twig/node/expression/array.rb +50 -0
- data/lib/twig/node/expression/assign_name.rb +28 -0
- data/lib/twig/node/expression/base.rb +20 -0
- data/lib/twig/node/expression/binary/base.rb +63 -0
- data/lib/twig/node/expression/call.rb +28 -0
- data/lib/twig/node/expression/constant.rb +17 -0
- data/lib/twig/node/expression/filter.rb +52 -0
- data/lib/twig/node/expression/get_attribute.rb +30 -0
- data/lib/twig/node/expression/helper_method.rb +31 -0
- data/lib/twig/node/expression/name.rb +37 -0
- data/lib/twig/node/expression/ternary.rb +28 -0
- data/lib/twig/node/expression/unary/base.rb +52 -0
- data/lib/twig/node/expression/variable/assign_context.rb +11 -0
- data/lib/twig/node/expression/variable/context.rb +11 -0
- data/lib/twig/node/for.rb +64 -0
- data/lib/twig/node/for_loop.rb +39 -0
- data/lib/twig/node/if.rb +50 -0
- data/lib/twig/node/include.rb +71 -0
- data/lib/twig/node/module.rb +74 -0
- data/lib/twig/node/nodes.rb +13 -0
- data/lib/twig/node/print.rb +18 -0
- data/lib/twig/node/text.rb +20 -0
- data/lib/twig/node/yield.rb +54 -0
- data/lib/twig/output_buffer.rb +29 -0
- data/lib/twig/parser.rb +131 -0
- data/lib/twig/railtie.rb +60 -0
- data/lib/twig/source.rb +13 -0
- data/lib/twig/template.rb +50 -0
- data/lib/twig/token.rb +48 -0
- data/lib/twig/token_parser/base.rb +20 -0
- data/lib/twig/token_parser/block.rb +54 -0
- data/lib/twig/token_parser/extends.rb +25 -0
- data/lib/twig/token_parser/for.rb +64 -0
- data/lib/twig/token_parser/if.rb +64 -0
- data/lib/twig/token_parser/include.rb +51 -0
- data/lib/twig/token_parser/yield.rb +44 -0
- data/lib/twig/token_stream.rb +73 -0
- data/lib/twig/twig_filter.rb +21 -0
- data/lib/twig_ruby.rb +36 -0
- 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
|