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.
- checksums.yaml +4 -4
- data/README.md +116 -0
- data/lib/tasks/twig_parity.rake +278 -0
- data/lib/twig/auto_hash.rb +7 -1
- data/lib/twig/callable.rb +28 -1
- data/lib/twig/compiler.rb +35 -3
- data/lib/twig/environment.rb +198 -41
- data/lib/twig/error/base.rb +81 -16
- data/lib/twig/error/loader.rb +8 -0
- data/lib/twig/error/logic.rb +8 -0
- data/lib/twig/error/runtime.rb +8 -0
- data/lib/twig/expression_parser/base.rb +30 -0
- data/lib/twig/expression_parser/expression_parsers.rb +57 -0
- data/lib/twig/expression_parser/infix/arrow.rb +31 -0
- data/lib/twig/expression_parser/infix/binary.rb +34 -0
- data/lib/twig/expression_parser/infix/conditional_ternary.rb +39 -0
- data/lib/twig/expression_parser/infix/dot.rb +72 -0
- data/lib/twig/expression_parser/infix/filter.rb +43 -0
- data/lib/twig/expression_parser/infix/function.rb +67 -0
- data/lib/twig/expression_parser/infix/is.rb +53 -0
- data/lib/twig/expression_parser/infix/is_not.rb +19 -0
- data/lib/twig/expression_parser/infix/parses_arguments.rb +84 -0
- data/lib/twig/expression_parser/infix/square_bracket.rb +66 -0
- data/lib/twig/expression_parser/infix_expression_parser.rb +34 -0
- data/lib/twig/expression_parser/prefix/grouping.rb +60 -0
- data/lib/twig/expression_parser/prefix/literal.rb +244 -0
- data/lib/twig/expression_parser/prefix/unary.rb +29 -0
- data/lib/twig/expression_parser/prefix_expression_parser.rb +18 -0
- data/lib/twig/extension/base.rb +26 -4
- data/lib/twig/extension/core.rb +1076 -48
- data/lib/twig/extension/debug.rb +25 -0
- data/lib/twig/extension/escaper.rb +73 -0
- data/lib/twig/extension/rails.rb +10 -57
- data/lib/twig/extension/string_loader.rb +19 -0
- data/lib/twig/extension_set.rb +117 -20
- data/lib/twig/file_extension_escaping_strategy.rb +35 -0
- data/lib/twig/lexer.rb +225 -81
- data/lib/twig/loader/array.rb +25 -8
- data/lib/twig/loader/chain.rb +93 -0
- data/lib/twig/loader/filesystem.rb +106 -7
- data/lib/twig/node/auto_escape.rb +18 -0
- data/lib/twig/node/base.rb +58 -2
- data/lib/twig/node/block.rb +2 -0
- data/lib/twig/node/block_reference.rb +5 -1
- data/lib/twig/node/body.rb +7 -0
- data/lib/twig/node/cache.rb +50 -0
- data/lib/twig/node/capture.rb +22 -0
- data/lib/twig/node/deprecated.rb +53 -0
- data/lib/twig/node/do.rb +19 -0
- data/lib/twig/node/embed.rb +43 -0
- data/lib/twig/node/expression/array.rb +29 -20
- data/lib/twig/node/expression/arrow_function.rb +55 -0
- data/lib/twig/node/expression/assign_name.rb +1 -1
- data/lib/twig/node/expression/binary/and.rb +17 -0
- data/lib/twig/node/expression/binary/base.rb +6 -4
- data/lib/twig/node/expression/binary/boolean.rb +24 -0
- data/lib/twig/node/expression/binary/concat.rb +20 -0
- data/lib/twig/node/expression/binary/elvis.rb +35 -0
- data/lib/twig/node/expression/binary/ends_with.rb +24 -0
- data/lib/twig/node/expression/binary/floor_div.rb +21 -0
- data/lib/twig/node/expression/binary/has_every.rb +20 -0
- data/lib/twig/node/expression/binary/has_some.rb +20 -0
- data/lib/twig/node/expression/binary/in.rb +20 -0
- data/lib/twig/node/expression/binary/matches.rb +24 -0
- data/lib/twig/node/expression/binary/not_in.rb +20 -0
- data/lib/twig/node/expression/binary/null_coalesce.rb +49 -0
- data/lib/twig/node/expression/binary/or.rb +15 -0
- data/lib/twig/node/expression/binary/starts_with.rb +24 -0
- data/lib/twig/node/expression/binary/xor.rb +17 -0
- data/lib/twig/node/expression/block_reference.rb +62 -0
- data/lib/twig/node/expression/call.rb +126 -6
- data/lib/twig/node/expression/constant.rb +3 -1
- data/lib/twig/node/expression/filter/default.rb +37 -0
- data/lib/twig/node/expression/filter/raw.rb +31 -0
- data/lib/twig/node/expression/filter.rb +2 -2
- data/lib/twig/node/expression/function.rb +37 -0
- data/lib/twig/node/expression/get_attribute.rb +51 -7
- data/lib/twig/node/expression/hash.rb +75 -0
- data/lib/twig/node/expression/helper_method.rb +6 -18
- data/lib/twig/node/expression/macro_reference.rb +43 -0
- data/lib/twig/node/expression/name.rb +42 -8
- data/lib/twig/node/expression/operator_escape.rb +13 -0
- data/lib/twig/node/expression/parent.rb +28 -0
- data/lib/twig/node/expression/support_defined_test.rb +23 -0
- data/lib/twig/node/expression/ternary.rb +7 -1
- data/lib/twig/node/expression/test/base.rb +26 -0
- data/lib/twig/node/expression/test/constant.rb +35 -0
- data/lib/twig/node/expression/test/defined.rb +33 -0
- data/lib/twig/node/expression/test/divisible_by.rb +23 -0
- data/lib/twig/node/expression/test/even.rb +21 -0
- data/lib/twig/node/expression/test/iterable.rb +21 -0
- data/lib/twig/node/expression/test/mapping.rb +21 -0
- data/lib/twig/node/expression/test/null.rb +21 -0
- data/lib/twig/node/expression/test/odd.rb +21 -0
- data/lib/twig/node/expression/test/same_as.rb +23 -0
- data/lib/twig/node/expression/test/sequence.rb +21 -0
- data/lib/twig/node/expression/unary/base.rb +3 -1
- data/lib/twig/node/expression/unary/not.rb +18 -0
- data/lib/twig/node/expression/unary/spread.rb +18 -0
- data/lib/twig/node/expression/unary/string_cast.rb +18 -0
- data/lib/twig/node/expression/variable/assign_template.rb +35 -0
- data/lib/twig/node/expression/variable/local.rb +35 -0
- data/lib/twig/node/expression/variable/template.rb +54 -0
- data/lib/twig/node/for.rb +38 -8
- data/lib/twig/node/for_loop.rb +0 -22
- data/lib/twig/node/if.rb +4 -1
- data/lib/twig/node/import.rb +32 -0
- data/lib/twig/node/include.rb +38 -8
- data/lib/twig/node/macro.rb +79 -0
- data/lib/twig/node/module.rb +278 -23
- data/lib/twig/node/output.rb +7 -0
- data/lib/twig/node/print.rb +4 -1
- data/lib/twig/node/set.rb +72 -0
- data/lib/twig/node/text.rb +4 -1
- data/lib/twig/node/with.rb +50 -0
- data/lib/twig/node/yield.rb +6 -1
- data/lib/twig/node_traverser.rb +50 -0
- data/lib/twig/node_visitor/base.rb +30 -0
- data/lib/twig/node_visitor/escaper.rb +165 -0
- data/lib/twig/node_visitor/safe_analysis.rb +127 -0
- data/lib/twig/node_visitor/spreader.rb +39 -0
- data/lib/twig/output_buffer.rb +14 -12
- data/lib/twig/parser.rb +281 -8
- data/lib/twig/rails/config.rb +33 -0
- data/lib/twig/rails/engine.rb +44 -0
- data/lib/twig/rails/renderer.rb +41 -0
- data/lib/twig/runtime/argument_spreader.rb +46 -0
- data/lib/twig/runtime/context.rb +154 -0
- data/lib/twig/runtime/enumerable_hash.rb +51 -0
- data/lib/twig/runtime/escaper.rb +155 -0
- data/lib/twig/runtime/loop_context.rb +81 -0
- data/lib/twig/runtime/loop_iterator.rb +60 -0
- data/lib/twig/runtime/spread.rb +21 -0
- data/lib/twig/runtime_loader/base.rb +12 -0
- data/lib/twig/runtime_loader/factory.rb +23 -0
- data/lib/twig/template.rb +267 -14
- data/lib/twig/template_wrapper.rb +42 -0
- data/lib/twig/token.rb +28 -2
- data/lib/twig/token_parser/apply.rb +48 -0
- data/lib/twig/token_parser/auto_escape.rb +45 -0
- data/lib/twig/token_parser/base.rb +26 -0
- data/lib/twig/token_parser/block.rb +4 -4
- data/lib/twig/token_parser/cache.rb +31 -0
- data/lib/twig/token_parser/deprecated.rb +40 -0
- data/lib/twig/token_parser/do.rb +19 -0
- data/lib/twig/token_parser/embed.rb +62 -0
- data/lib/twig/token_parser/extends.rb +4 -3
- data/lib/twig/token_parser/for.rb +14 -9
- data/lib/twig/token_parser/from.rb +57 -0
- data/lib/twig/token_parser/guard.rb +65 -0
- data/lib/twig/token_parser/if.rb +9 -9
- data/lib/twig/token_parser/import.rb +29 -0
- data/lib/twig/token_parser/include.rb +2 -2
- data/lib/twig/token_parser/macro.rb +109 -0
- data/lib/twig/token_parser/set.rb +76 -0
- data/lib/twig/token_parser/use.rb +54 -0
- data/lib/twig/token_parser/with.rb +36 -0
- data/lib/twig/token_parser/yield.rb +7 -7
- data/lib/twig/token_stream.rb +23 -3
- data/lib/twig/twig_filter.rb +20 -0
- data/lib/twig/twig_function.rb +37 -0
- data/lib/twig/twig_test.rb +31 -0
- data/lib/twig/util/callable_arguments_extractor.rb +227 -0
- data/lib/twig_ruby.rb +21 -2
- metadata +148 -6
- data/lib/twig/context.rb +0 -64
- data/lib/twig/expression_parser.rb +0 -517
- 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
|
+
'�'
|
|
62
|
+
elsif char.bytesize == 1
|
|
63
|
+
# Use named entities for common characters
|
|
64
|
+
case ord
|
|
65
|
+
when 34 then '"' # quotation mark
|
|
66
|
+
when 38 then '&' # ampersand
|
|
67
|
+
when 60 then '<' # less-than sign
|
|
68
|
+
when 62 then '>' # 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,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
|
|
13
|
+
def initialize(environment)
|
|
12
14
|
@environment = environment
|
|
15
|
+
@parent = nil
|
|
13
16
|
@parents = {}
|
|
14
17
|
@blocks = {}
|
|
15
|
-
@
|
|
16
|
-
@
|
|
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
|
-
|
|
24
|
-
|
|
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
|
|
28
|
-
|
|
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
|
-
|
|
31
|
-
|
|
154
|
+
# @return [Template, false]
|
|
155
|
+
def get_parent(context)
|
|
156
|
+
if @parent
|
|
157
|
+
return @parent
|
|
32
158
|
end
|
|
33
159
|
|
|
34
|
-
|
|
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]
|
|
40
|
-
# @return
|
|
41
|
-
def
|
|
42
|
-
|
|
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]
|