liquid2 0.1.0
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
- checksums.yaml.gz.sig +0 -0
- data/.rubocop.yml +46 -0
- data/.ruby-version +1 -0
- data/.vscode/settings.json +32 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/LICENSE_SHOPIFY.txt +20 -0
- data/README.md +219 -0
- data/Rakefile +23 -0
- data/Steepfile +26 -0
- data/lib/liquid2/context.rb +297 -0
- data/lib/liquid2/environment.rb +287 -0
- data/lib/liquid2/errors.rb +79 -0
- data/lib/liquid2/expression.rb +20 -0
- data/lib/liquid2/expressions/arguments.rb +25 -0
- data/lib/liquid2/expressions/array.rb +20 -0
- data/lib/liquid2/expressions/blank.rb +41 -0
- data/lib/liquid2/expressions/boolean.rb +20 -0
- data/lib/liquid2/expressions/filtered.rb +136 -0
- data/lib/liquid2/expressions/identifier.rb +43 -0
- data/lib/liquid2/expressions/lambda.rb +53 -0
- data/lib/liquid2/expressions/logical.rb +71 -0
- data/lib/liquid2/expressions/loop.rb +79 -0
- data/lib/liquid2/expressions/path.rb +33 -0
- data/lib/liquid2/expressions/range.rb +28 -0
- data/lib/liquid2/expressions/relational.rb +119 -0
- data/lib/liquid2/expressions/template_string.rb +20 -0
- data/lib/liquid2/filter.rb +95 -0
- data/lib/liquid2/filters/array.rb +202 -0
- data/lib/liquid2/filters/date.rb +20 -0
- data/lib/liquid2/filters/default.rb +16 -0
- data/lib/liquid2/filters/json.rb +15 -0
- data/lib/liquid2/filters/math.rb +87 -0
- data/lib/liquid2/filters/size.rb +11 -0
- data/lib/liquid2/filters/slice.rb +17 -0
- data/lib/liquid2/filters/sort.rb +96 -0
- data/lib/liquid2/filters/string.rb +204 -0
- data/lib/liquid2/loader.rb +59 -0
- data/lib/liquid2/loaders/file_system_loader.rb +76 -0
- data/lib/liquid2/loaders/mixins.rb +52 -0
- data/lib/liquid2/node.rb +113 -0
- data/lib/liquid2/nodes/comment.rb +18 -0
- data/lib/liquid2/nodes/output.rb +24 -0
- data/lib/liquid2/nodes/tags/assign.rb +35 -0
- data/lib/liquid2/nodes/tags/block_comment.rb +26 -0
- data/lib/liquid2/nodes/tags/capture.rb +40 -0
- data/lib/liquid2/nodes/tags/case.rb +111 -0
- data/lib/liquid2/nodes/tags/cycle.rb +63 -0
- data/lib/liquid2/nodes/tags/decrement.rb +29 -0
- data/lib/liquid2/nodes/tags/doc.rb +24 -0
- data/lib/liquid2/nodes/tags/echo.rb +31 -0
- data/lib/liquid2/nodes/tags/extends.rb +3 -0
- data/lib/liquid2/nodes/tags/for.rb +155 -0
- data/lib/liquid2/nodes/tags/if.rb +84 -0
- data/lib/liquid2/nodes/tags/include.rb +123 -0
- data/lib/liquid2/nodes/tags/increment.rb +29 -0
- data/lib/liquid2/nodes/tags/inline_comment.rb +28 -0
- data/lib/liquid2/nodes/tags/liquid.rb +29 -0
- data/lib/liquid2/nodes/tags/macro.rb +3 -0
- data/lib/liquid2/nodes/tags/raw.rb +30 -0
- data/lib/liquid2/nodes/tags/render.rb +137 -0
- data/lib/liquid2/nodes/tags/tablerow.rb +143 -0
- data/lib/liquid2/nodes/tags/translate.rb +3 -0
- data/lib/liquid2/nodes/tags/unless.rb +23 -0
- data/lib/liquid2/nodes/tags/with.rb +3 -0
- data/lib/liquid2/parser.rb +917 -0
- data/lib/liquid2/scanner.rb +595 -0
- data/lib/liquid2/static_analysis.rb +301 -0
- data/lib/liquid2/tag.rb +22 -0
- data/lib/liquid2/template.rb +182 -0
- data/lib/liquid2/undefined.rb +131 -0
- data/lib/liquid2/utils/cache.rb +80 -0
- data/lib/liquid2/utils/chain_hash.rb +40 -0
- data/lib/liquid2/utils/unescape.rb +119 -0
- data/lib/liquid2/version.rb +5 -0
- data/lib/liquid2.rb +90 -0
- data/performance/benchmark.rb +73 -0
- data/performance/memory_profile.rb +62 -0
- data/performance/profile.rb +71 -0
- data/sig/liquid2.rbs +2348 -0
- data.tar.gz.sig +0 -0
- metadata +164 -0
- metadata.gz.sig +0 -0
@@ -0,0 +1,297 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "utils/chain_hash"
|
4
|
+
|
5
|
+
module Liquid2
|
6
|
+
# Hash-like obj for resolving built-in dynamic objs.
|
7
|
+
class BuiltIn
|
8
|
+
def key?(key)
|
9
|
+
%w[now today].include?(key)
|
10
|
+
end
|
11
|
+
|
12
|
+
def fetch(key, default = :undefined)
|
13
|
+
case key
|
14
|
+
when "now", "today"
|
15
|
+
Time.now
|
16
|
+
else
|
17
|
+
default
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def [](key)
|
22
|
+
case key
|
23
|
+
when "now", "today"
|
24
|
+
Time.now
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Per render contextual information. A new RenderContext is created automatically
|
30
|
+
# every time `Template#render` is called.
|
31
|
+
class RenderContext
|
32
|
+
attr_reader :env, :template, :disabled_tags, :globals
|
33
|
+
attr_accessor :interrupts, :tag_namespace
|
34
|
+
|
35
|
+
BUILT_IN = BuiltIn.new
|
36
|
+
|
37
|
+
# @param template [Template]
|
38
|
+
# @param globals [Hash<String, Object>?]
|
39
|
+
# @param disabled_tags [Array<String>?]
|
40
|
+
# @param copy_depth [Integer?]
|
41
|
+
# @param parent [RenderContext?]
|
42
|
+
# @param parent_scope [Array[_Namespace]] Namespaces from a parent render context.
|
43
|
+
# @param loop_carry [Integer?]
|
44
|
+
# @param local_namespace_carry [Integer?]
|
45
|
+
def initialize(
|
46
|
+
template,
|
47
|
+
globals: nil,
|
48
|
+
disabled_tags: nil,
|
49
|
+
copy_depth: 0,
|
50
|
+
parent: nil,
|
51
|
+
loop_carry: 1,
|
52
|
+
local_namespace_carry: 0
|
53
|
+
)
|
54
|
+
@env = template.env
|
55
|
+
@template = template
|
56
|
+
@globals = globals || {} # steep:ignore UnannotatedEmptyCollection
|
57
|
+
@disabled_tags = disabled_tags || []
|
58
|
+
@copy_depth = copy_depth
|
59
|
+
@parent = parent
|
60
|
+
@loop_carry = loop_carry
|
61
|
+
|
62
|
+
# The current size of the local namespace. _size_ is a non-specific measure of the
|
63
|
+
# amount of memory used to store template local variables.
|
64
|
+
@assign_score = local_namespace_carry
|
65
|
+
|
66
|
+
# A namespace for template local variables (those bound with `assign` or `capture`).
|
67
|
+
@locals = {}
|
68
|
+
|
69
|
+
# A namespace for `increment` and `decrement` counters.
|
70
|
+
@counters = Hash.new(0)
|
71
|
+
|
72
|
+
# Namespaces are searched from right to left. When a RenderContext is extended, the
|
73
|
+
# temporary namespace is pushed to the end of this queue.
|
74
|
+
# TODO: exclude @globals if globals is empty
|
75
|
+
@scope = ReadOnlyChainHash.new(@counters, BUILT_IN, @globals, @locals)
|
76
|
+
|
77
|
+
# A namespace supporting stateful tags, such as `cycle` and `increment`.
|
78
|
+
# It's OK to use this hash for storing custom tag state.
|
79
|
+
@tag_namespace = {
|
80
|
+
cycles: Hash.new(0),
|
81
|
+
stop_index: {},
|
82
|
+
extends: Hash.new { |hash, key| hash[key] = [] },
|
83
|
+
macros: {}
|
84
|
+
}
|
85
|
+
|
86
|
+
# A stack of forloop objs used for populating forloop.parentloop.
|
87
|
+
@loops = [] # : Array[ForLoop]
|
88
|
+
|
89
|
+
# A stack of interrupts used to signal breaking and continuing `for` loops.
|
90
|
+
@interrupts = [] # : Array[Symbol]
|
91
|
+
end
|
92
|
+
|
93
|
+
# Evaluate _obj_ as an expression in the render current context.
|
94
|
+
def evaluate(obj)
|
95
|
+
obj.respond_to?(:evaluate) ? obj.evaluate(self) : obj
|
96
|
+
end
|
97
|
+
|
98
|
+
# Add _key_ to the local scope with value _value_.
|
99
|
+
# @param key [String]
|
100
|
+
# @param value [Object]
|
101
|
+
# @return [nil]
|
102
|
+
def assign(key, value)
|
103
|
+
@locals[key] = value
|
104
|
+
if (limit = @env.local_namespace_limit)
|
105
|
+
# Note that this approach does not account for overwriting keys/values
|
106
|
+
# in the local scope. The assign score is always incremented.
|
107
|
+
@assign_score += assign_score(value)
|
108
|
+
raise LiquidResourceLimitError, "local namespace limit reached" if @assign_score > limit
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
alias []= assign
|
113
|
+
|
114
|
+
# Resolve _path_ to variable/data in the current scope.
|
115
|
+
# @param head [String|Integer] First segment of the path.
|
116
|
+
# @param path [Array<String|Integer>] Remaining path segments.
|
117
|
+
# @param node [Node?] An associated token to use for error context.
|
118
|
+
# @param default [Object?] A default value to return if the path can no be resolved.
|
119
|
+
# @return [Object]
|
120
|
+
def fetch(head, path, node:, default: :undefined)
|
121
|
+
obj = @scope.fetch(evaluate(head))
|
122
|
+
|
123
|
+
if obj == :undefined
|
124
|
+
return @env.undefined(head, node: node) if default == :undefined
|
125
|
+
|
126
|
+
return default
|
127
|
+
end
|
128
|
+
|
129
|
+
index = 0
|
130
|
+
while (segment = path[index])
|
131
|
+
index += 1
|
132
|
+
segment = evaluate(segment)
|
133
|
+
segment = segment.to_liquid(self) if segment.respond_to?(:to_liquid)
|
134
|
+
|
135
|
+
if obj.respond_to?(:[]) &&
|
136
|
+
((obj.respond_to?(:key?) && obj.key?(segment)) ||
|
137
|
+
(obj.respond_to?(:fetch) && segment.is_a?(Integer)))
|
138
|
+
obj = obj[segment]
|
139
|
+
next
|
140
|
+
end
|
141
|
+
|
142
|
+
obj = if segment == "size" && obj.respond_to?(:size)
|
143
|
+
obj.size
|
144
|
+
elsif segment == "first" && obj.respond_to?(:first)
|
145
|
+
obj.first
|
146
|
+
elsif segment == "last" && obj.respond_to?(:last)
|
147
|
+
obj.last
|
148
|
+
else
|
149
|
+
return default == :undefined ? @env.undefined(head, node: node) : default
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
obj
|
154
|
+
end
|
155
|
+
|
156
|
+
# Resolve variable _name_ in the current scope.
|
157
|
+
# @param name [String]
|
158
|
+
# @return [Object?]
|
159
|
+
def resolve(name) = @scope.fetch(name)
|
160
|
+
|
161
|
+
alias [] resolve
|
162
|
+
|
163
|
+
# Extend the scope of this context with the given namespace. Expects a block.
|
164
|
+
# @param namespace [Hash<String, Object>]
|
165
|
+
# @param template [Template?] Replace the current template for the duration of the block.
|
166
|
+
def extend(namespace, template: nil)
|
167
|
+
if @scope.size > @env.context_depth_limit
|
168
|
+
raise LiquidResourceLimitError, "context depth limit reached"
|
169
|
+
end
|
170
|
+
|
171
|
+
template_ = @template
|
172
|
+
@template = template if template
|
173
|
+
@scope << namespace
|
174
|
+
yield
|
175
|
+
ensure
|
176
|
+
@template = template_
|
177
|
+
@scope.pop
|
178
|
+
end
|
179
|
+
|
180
|
+
# Copy this render context and add _namespace_ to the new scope.
|
181
|
+
# @param namespace [Hash<String, Object>]
|
182
|
+
# @param template [Template?] The template obj bound to the new context.
|
183
|
+
# @param disabled_tags [Set<String>] Names of tags to disallow in the new context.
|
184
|
+
# @param carry_loop_iterations [bool] If true, pass the current loop iteration count to the
|
185
|
+
# new context.
|
186
|
+
# @param block_scope [bool] It true, retain the current scope in the new context. Otherwise
|
187
|
+
# only global variables will be included in the new context's scope.
|
188
|
+
def copy(namespace,
|
189
|
+
template: nil,
|
190
|
+
disabled_tags: nil,
|
191
|
+
carry_loop_iterations: false,
|
192
|
+
block_scope: false)
|
193
|
+
if @copy_depth > @env.context_depth_limit
|
194
|
+
raise LiquidResourceLimitError, "context depth limit reached"
|
195
|
+
end
|
196
|
+
|
197
|
+
loop_carry = if carry_loop_iterations
|
198
|
+
@loops.map(&:length).reduce(@loop_carry) { |acc, value| acc * value }
|
199
|
+
else
|
200
|
+
1
|
201
|
+
end
|
202
|
+
|
203
|
+
scope = if block_scope
|
204
|
+
ReadOnlyChainHash.new(@scope, namespace)
|
205
|
+
else
|
206
|
+
ReadOnlyChainHash.new(@globals, namespace)
|
207
|
+
end
|
208
|
+
|
209
|
+
self.class.new(template || @template,
|
210
|
+
globals: scope,
|
211
|
+
disabled_tags: disabled_tags,
|
212
|
+
copy_depth: @copy_depth + 1,
|
213
|
+
parent: self,
|
214
|
+
loop_carry: loop_carry,
|
215
|
+
local_namespace_carry: @assign_score)
|
216
|
+
end
|
217
|
+
|
218
|
+
# Push a new namespace and forloop for the duration of a block.
|
219
|
+
# @param namespace [Hash<String, Object>]
|
220
|
+
# @param forloop [ForLoop]
|
221
|
+
def loop(namespace, forloop)
|
222
|
+
raise_for_loop_limit(length: forloop.length)
|
223
|
+
@loops << forloop
|
224
|
+
@scope << namespace
|
225
|
+
yield
|
226
|
+
ensure
|
227
|
+
@scope.pop
|
228
|
+
@loops.pop
|
229
|
+
end
|
230
|
+
|
231
|
+
# Return the last ForLoop obj if one is available, or an instance of Undefined otherwise.
|
232
|
+
def parent_loop(node)
|
233
|
+
return @env.undefined("parentloop", node: node) if @loops.empty?
|
234
|
+
|
235
|
+
@loops.last
|
236
|
+
end
|
237
|
+
|
238
|
+
# Get or set the stop index of a for loop.
|
239
|
+
def stop_index(key, index: nil)
|
240
|
+
if index
|
241
|
+
@tag_namespace[:stop_index][key] = index
|
242
|
+
else
|
243
|
+
@tag_namespace[:stop_index].fetch(key, 0)
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
def raise_for_loop_limit(length: 1)
|
248
|
+
return nil unless @env.loop_iteration_limit
|
249
|
+
|
250
|
+
loop_count = @loops.map(&:length).reduce(length * @loop_carry) { |acc, value| acc * value }
|
251
|
+
|
252
|
+
return unless loop_count > (@env.loop_iteration_limit || raise)
|
253
|
+
|
254
|
+
raise LiquidResourceLimitError, "loop iteration limit reached"
|
255
|
+
end
|
256
|
+
|
257
|
+
def raise_for_output_limit(length)
|
258
|
+
return unless @env.output_stream_limit && (@env.output_stream_limit || raise) < length
|
259
|
+
|
260
|
+
raise LiquidResourceLimitError, "output limit reached"
|
261
|
+
end
|
262
|
+
|
263
|
+
def get_output_buffer(parent_buffer)
|
264
|
+
return StringIO.new unless @env.output_stream_limit
|
265
|
+
|
266
|
+
carry = parent_buffer.is_a?(LimitedStringIO) ? parent_buffer.size : 0
|
267
|
+
LimitedStringIO.new((@env.output_stream_limit || raise) - carry)
|
268
|
+
end
|
269
|
+
|
270
|
+
def increment(name)
|
271
|
+
val = @counters[name]
|
272
|
+
@counters[name] = val + 1
|
273
|
+
val
|
274
|
+
end
|
275
|
+
|
276
|
+
def decrement(name)
|
277
|
+
val = @counters[name] - 1
|
278
|
+
@counters[name] = val
|
279
|
+
val
|
280
|
+
end
|
281
|
+
|
282
|
+
protected
|
283
|
+
|
284
|
+
def assign_score(value)
|
285
|
+
case value
|
286
|
+
when String
|
287
|
+
value.bytesize
|
288
|
+
when Array
|
289
|
+
value.sum(1) { |item| assign_score(item) } + 1
|
290
|
+
when Hash
|
291
|
+
value.sum(1) { |k, v| assign_score(k) + assign_score(v) }
|
292
|
+
else
|
293
|
+
1
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
@@ -0,0 +1,287 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "loader"
|
4
|
+
require_relative "parser"
|
5
|
+
require_relative "template"
|
6
|
+
require_relative "undefined"
|
7
|
+
require_relative "filters/array"
|
8
|
+
require_relative "filters/date"
|
9
|
+
require_relative "filters/default"
|
10
|
+
require_relative "filters/math"
|
11
|
+
require_relative "filters/json"
|
12
|
+
require_relative "filters/size"
|
13
|
+
require_relative "filters/slice"
|
14
|
+
require_relative "filters/sort"
|
15
|
+
require_relative "filters/string"
|
16
|
+
require_relative "nodes/tags/assign"
|
17
|
+
require_relative "nodes/tags/block_comment"
|
18
|
+
require_relative "nodes/tags/capture"
|
19
|
+
require_relative "nodes/tags/case"
|
20
|
+
require_relative "nodes/tags/cycle"
|
21
|
+
require_relative "nodes/tags/decrement"
|
22
|
+
require_relative "nodes/tags/doc"
|
23
|
+
require_relative "nodes/tags/echo"
|
24
|
+
require_relative "nodes/tags/for"
|
25
|
+
require_relative "nodes/tags/if"
|
26
|
+
require_relative "nodes/tags/include"
|
27
|
+
require_relative "nodes/tags/increment"
|
28
|
+
require_relative "nodes/tags/inline_comment"
|
29
|
+
require_relative "nodes/tags/liquid"
|
30
|
+
require_relative "nodes/tags/raw"
|
31
|
+
require_relative "nodes/tags/render"
|
32
|
+
require_relative "nodes/tags/tablerow"
|
33
|
+
require_relative "nodes/tags/unless"
|
34
|
+
|
35
|
+
module Liquid2
|
36
|
+
# Template parsing and rendering configuration.
|
37
|
+
#
|
38
|
+
# A Liquid::Environment is where you might register custom tags and filters,
|
39
|
+
# or store global context data that should be available to all templates.
|
40
|
+
#
|
41
|
+
# `Liquid2.parse(source)` is equivalent to `Liquid2::Environment.new.parse(source)`.
|
42
|
+
class Environment
|
43
|
+
attr_reader :tags, :local_namespace_limit, :context_depth_limit, :loop_iteration_limit,
|
44
|
+
:output_stream_limit, :filters, :suppress_blank_control_flow_blocks,
|
45
|
+
:shorthand_indexes
|
46
|
+
|
47
|
+
def initialize(
|
48
|
+
context_depth_limit: 30,
|
49
|
+
globals: nil,
|
50
|
+
loader: nil,
|
51
|
+
local_namespace_limit: nil,
|
52
|
+
loop_iteration_limit: nil,
|
53
|
+
output_stream_limit: nil,
|
54
|
+
shorthand_indexes: false,
|
55
|
+
suppress_blank_control_flow_blocks: true,
|
56
|
+
undefined: Undefined
|
57
|
+
)
|
58
|
+
# A mapping of tag names to objects responding to `parse(token, parser)`.
|
59
|
+
@tags = {}
|
60
|
+
|
61
|
+
# A mapping of filter names to objects responding to `#call(left, ...)`,
|
62
|
+
# along with a flag to indicate if the callable accepts a `context`
|
63
|
+
# keyword argument.
|
64
|
+
@filters = {}
|
65
|
+
|
66
|
+
# The maximum number of times a render context can be extended or copied before
|
67
|
+
# a Liquid2::LiquidResourceLimitError is raised.
|
68
|
+
@context_depth_limit = context_depth_limit
|
69
|
+
|
70
|
+
# Variables that are available to all templates rendered from this environment.
|
71
|
+
@globals = globals
|
72
|
+
|
73
|
+
# An instance of `Liquid2::Loader`. A template loader is responsible for finding and
|
74
|
+
# reading templates for `{% include %}` and `{% render %}` tags, or when calling
|
75
|
+
# `Liquid2::Environment.get_template(name)`.
|
76
|
+
@loader = loader || HashLoader.new({})
|
77
|
+
|
78
|
+
# The maximum allowed "size" of the template local namespace (variables from `assign`
|
79
|
+
# and `capture` tags) before a Liquid2::LiquidResourceLimitError is raised.
|
80
|
+
@local_namespace_limit = local_namespace_limit
|
81
|
+
|
82
|
+
# The maximum number of loop iterations allowed before a `LiquidResourceLimitError`
|
83
|
+
# is raised.
|
84
|
+
@loop_iteration_limit = loop_iteration_limit
|
85
|
+
|
86
|
+
# The maximum number of bytes that can be written to a template's output buffer
|
87
|
+
# before a `LiquidResourceLimitError` is raised.
|
88
|
+
@output_stream_limit = output_stream_limit
|
89
|
+
|
90
|
+
# We reuse the same string scanner when parsing templates for improved performance.
|
91
|
+
# TODO: Is this going to cause issues in multi threaded environments?
|
92
|
+
@scanner = StringScanner.new("")
|
93
|
+
|
94
|
+
# When `true`, allow shorthand dotted array indexes as well as bracketed indexes
|
95
|
+
# in variable paths. Defaults to `false`.
|
96
|
+
@shorthand_indexes = shorthand_indexes
|
97
|
+
|
98
|
+
# When `true`, suppress blank control flow block output, so as not to include
|
99
|
+
# unnecessary whitespace. Defaults to `true`.
|
100
|
+
@suppress_blank_control_flow_blocks = suppress_blank_control_flow_blocks
|
101
|
+
|
102
|
+
# An instance of `Liquid2::Undefined` used to represent template variables that
|
103
|
+
# don't exist.
|
104
|
+
@undefined = undefined
|
105
|
+
|
106
|
+
# Override `setup_tags_and_filters` in environment subclasses to configure custom
|
107
|
+
# tags and/or filters.
|
108
|
+
setup_tags_and_filters
|
109
|
+
end
|
110
|
+
|
111
|
+
# Parse _source_ text as a template.
|
112
|
+
# @param source [String] template source text.
|
113
|
+
# @return [Template]
|
114
|
+
def parse(source, name: "", path: nil, up_to_date: nil, globals: nil, overlay: nil)
|
115
|
+
Template.new(self,
|
116
|
+
source,
|
117
|
+
Parser.parse(self, source, scanner: @scanner),
|
118
|
+
name: name, path: path, up_to_date: up_to_date,
|
119
|
+
globals: make_globals(globals), overlay: overlay)
|
120
|
+
rescue LiquidError => e
|
121
|
+
e.source = source
|
122
|
+
e.template_name = name unless name.empty?
|
123
|
+
raise
|
124
|
+
end
|
125
|
+
|
126
|
+
# Parse and render template source text with _data_ as template variables.
|
127
|
+
# @param source [String]
|
128
|
+
# @param data [Hash[String, untyped]?]
|
129
|
+
# @return [String]
|
130
|
+
def render(source, data = nil)
|
131
|
+
parse(source).render(data)
|
132
|
+
end
|
133
|
+
|
134
|
+
# Add or replace a filter. The same callable can be registered multiple times with
|
135
|
+
# different names.
|
136
|
+
#
|
137
|
+
# If _callable_ accepts a keyword parameter called `context`, the active render
|
138
|
+
# context will be passed to `#call`.
|
139
|
+
#
|
140
|
+
# @param name [String] The name of the filter, as used by template authors.
|
141
|
+
# @param callable [responds to call] An object that responds to `#call(left, ...)`
|
142
|
+
# and `#parameters`. Like a Proc or Method.
|
143
|
+
def register_filter(name, callable)
|
144
|
+
with_context = callable.parameters.index do |(kind, param)|
|
145
|
+
kind == :keyreq && param == :context
|
146
|
+
end
|
147
|
+
@filters[name] = [callable, with_context]
|
148
|
+
end
|
149
|
+
|
150
|
+
# Remove a filter from the filter register.
|
151
|
+
# @param name [String] The name of the filter.
|
152
|
+
# @return [callable | nil] The callable implementing the removed filter, or nil
|
153
|
+
# if _name_ did not exist in the filter register.
|
154
|
+
def delete_filter(name)
|
155
|
+
@filters.delete(name)
|
156
|
+
end
|
157
|
+
|
158
|
+
def setup_tags_and_filters
|
159
|
+
@tags["#"] = InlineComment
|
160
|
+
@tags["assign"] = AssignTag
|
161
|
+
@tags["break"] = BreakTag
|
162
|
+
@tags["capture"] = CaptureTag
|
163
|
+
@tags["case"] = CaseTag
|
164
|
+
@tags["comment"] = BlockComment
|
165
|
+
@tags["continue"] = ContinueTag
|
166
|
+
@tags["cycle"] = CycleTag
|
167
|
+
@tags["decrement"] = DecrementTag
|
168
|
+
@tags["doc"] = DocTag
|
169
|
+
@tags["echo"] = EchoTag
|
170
|
+
@tags["for"] = ForTag
|
171
|
+
@tags["if"] = IfTag
|
172
|
+
@tags["include"] = IncludeTag
|
173
|
+
@tags["increment"] = IncrementTag
|
174
|
+
@tags["liquid"] = LiquidTag
|
175
|
+
@tags["raw"] = RawTag
|
176
|
+
@tags["render"] = RenderTag
|
177
|
+
@tags["tablerow"] = TableRowTag
|
178
|
+
@tags["unless"] = UnlessTag
|
179
|
+
|
180
|
+
register_filter("abs", Liquid2::Filters.method(:abs))
|
181
|
+
register_filter("append", Liquid2::Filters.method(:append))
|
182
|
+
register_filter("at_least", Liquid2::Filters.method(:at_least))
|
183
|
+
register_filter("at_most", Liquid2::Filters.method(:at_most))
|
184
|
+
register_filter("base64_decode", Liquid2::Filters.method(:base64_decode))
|
185
|
+
register_filter("base64_encode", Liquid2::Filters.method(:base64_encode))
|
186
|
+
register_filter("base64_url_safe_decode", Liquid2::Filters.method(:base64_url_safe_decode))
|
187
|
+
register_filter("base64_url_safe_encode", Liquid2::Filters.method(:base64_url_safe_encode))
|
188
|
+
register_filter("capitalize", Liquid2::Filters.method(:capitalize))
|
189
|
+
register_filter("ceil", Liquid2::Filters.method(:ceil))
|
190
|
+
register_filter("compact", Liquid2::Filters.method(:compact))
|
191
|
+
register_filter("concat", Liquid2::Filters.method(:concat))
|
192
|
+
register_filter("date", Liquid2::Filters.method(:date))
|
193
|
+
register_filter("default", Liquid2::Filters.method(:default))
|
194
|
+
register_filter("divided_by", Liquid2::Filters.method(:divided_by))
|
195
|
+
register_filter("downcase", Liquid2::Filters.method(:downcase))
|
196
|
+
register_filter("escape_once", Liquid2::Filters.method(:escape_once))
|
197
|
+
register_filter("escape", Liquid2::Filters.method(:escape))
|
198
|
+
register_filter("find_index", Liquid2::Filters.method(:find_index))
|
199
|
+
register_filter("find", Liquid2::Filters.method(:find))
|
200
|
+
register_filter("first", Liquid2::Filters.method(:first))
|
201
|
+
register_filter("floor", Liquid2::Filters.method(:floor))
|
202
|
+
register_filter("has", Liquid2::Filters.method(:has))
|
203
|
+
register_filter("join", Liquid2::Filters.method(:join))
|
204
|
+
register_filter("json", Liquid2::Filters.method(:json))
|
205
|
+
register_filter("last", Liquid2::Filters.method(:last))
|
206
|
+
register_filter("lstrip", Liquid2::Filters.method(:lstrip))
|
207
|
+
register_filter("map", Liquid2::Filters.method(:map))
|
208
|
+
register_filter("minus", Liquid2::Filters.method(:minus))
|
209
|
+
register_filter("modulo", Liquid2::Filters.method(:modulo))
|
210
|
+
register_filter("newline_to_br", Liquid2::Filters.method(:newline_to_br))
|
211
|
+
register_filter("plus", Liquid2::Filters.method(:plus))
|
212
|
+
register_filter("prepend", Liquid2::Filters.method(:prepend))
|
213
|
+
register_filter("reject", Liquid2::Filters.method(:reject))
|
214
|
+
register_filter("remove_first", Liquid2::Filters.method(:remove_first))
|
215
|
+
register_filter("remove_last", Liquid2::Filters.method(:remove_last))
|
216
|
+
register_filter("remove", Liquid2::Filters.method(:remove))
|
217
|
+
register_filter("replace_first", Liquid2::Filters.method(:replace_first))
|
218
|
+
register_filter("replace_last", Liquid2::Filters.method(:replace_last))
|
219
|
+
register_filter("replace", Liquid2::Filters.method(:replace))
|
220
|
+
register_filter("reverse", Liquid2::Filters.method(:reverse))
|
221
|
+
register_filter("round", Liquid2::Filters.method(:round))
|
222
|
+
register_filter("rstrip", Liquid2::Filters.method(:rstrip))
|
223
|
+
register_filter("size", Liquid2::Filters.method(:size))
|
224
|
+
register_filter("slice", Liquid2::Filters.method(:slice))
|
225
|
+
register_filter("sort_natural", Liquid2::Filters.method(:sort_natural))
|
226
|
+
register_filter("sort_numeric", Liquid2::Filters.method(:sort_numeric))
|
227
|
+
register_filter("sort", Liquid2::Filters.method(:sort))
|
228
|
+
register_filter("split", Liquid2::Filters.method(:split))
|
229
|
+
register_filter("strip_html", Liquid2::Filters.method(:strip_html))
|
230
|
+
register_filter("strip_newlines", Liquid2::Filters.method(:strip_newlines))
|
231
|
+
register_filter("strip", Liquid2::Filters.method(:strip))
|
232
|
+
register_filter("sum", Liquid2::Filters.method(:sum))
|
233
|
+
register_filter("times", Liquid2::Filters.method(:times))
|
234
|
+
register_filter("truncate", Liquid2::Filters.method(:truncate))
|
235
|
+
register_filter("truncatewords", Liquid2::Filters.method(:truncatewords))
|
236
|
+
register_filter("uniq", Liquid2::Filters.method(:uniq))
|
237
|
+
register_filter("url_encode", Liquid2::Filters.method(:url_encode))
|
238
|
+
register_filter("url_decode", Liquid2::Filters.method(:url_decode))
|
239
|
+
register_filter("upcase", Liquid2::Filters.method(:upcase))
|
240
|
+
register_filter("where", Liquid2::Filters.method(:where))
|
241
|
+
end
|
242
|
+
|
243
|
+
def undefined(name, node: nil)
|
244
|
+
@undefined.new(name, node: node)
|
245
|
+
end
|
246
|
+
|
247
|
+
# Trim _text_.
|
248
|
+
def trim(text, left_trim, right_trim)
|
249
|
+
case left_trim
|
250
|
+
when "-"
|
251
|
+
text.lstrip!
|
252
|
+
when "~"
|
253
|
+
text.sub!(/\A[\r\n]+/, "")
|
254
|
+
end
|
255
|
+
|
256
|
+
case right_trim
|
257
|
+
when "-"
|
258
|
+
text.rstrip!
|
259
|
+
when "~"
|
260
|
+
text.sub!(/[\r\n]+\Z/, "")
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
# Load and parse a template using the configured template loader.
|
265
|
+
# @param name [String] The template's name.
|
266
|
+
# @param globals [_Namespace?] Render context variables to attach to the template.
|
267
|
+
# @param context [RenderContext?] An optional render context that can be used to
|
268
|
+
# narrow the template search space.
|
269
|
+
# @param kwargs Arbitrary arguments that can be used to narrow the template search
|
270
|
+
# space.
|
271
|
+
# @return [Template]
|
272
|
+
def get_template(name, globals: nil, context: nil, **kwargs)
|
273
|
+
@loader.load(self, name, globals: globals, context: context, **kwargs)
|
274
|
+
rescue LiquidError => e
|
275
|
+
e.template_name = name unless e.template_name || e.is_a?(LiquidTemplateNotFoundError)
|
276
|
+
raise e
|
277
|
+
end
|
278
|
+
|
279
|
+
# Merge environment globals with another namespace.
|
280
|
+
def make_globals(namespace)
|
281
|
+
return @globals if namespace.nil?
|
282
|
+
return namespace if @globals.nil?
|
283
|
+
|
284
|
+
(@globals || raise).merge(namespace)
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Liquid2
|
4
|
+
# The base class for all Liquid errors.
|
5
|
+
class LiquidError < StandardError
|
6
|
+
attr_accessor :token, :template_name, :source
|
7
|
+
|
8
|
+
FULL_MESSAGE = ((RUBY_VERSION.split(".")&.map(&:to_i) <=> [3, 2, 0]) || -1) < 1
|
9
|
+
|
10
|
+
def initialize(message, token = nil)
|
11
|
+
super(message)
|
12
|
+
@token = token
|
13
|
+
@template_name = nil
|
14
|
+
@source = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
def detailed_message(highlight: true, **kwargs)
|
18
|
+
return super unless @source.is_a?(String) && @token
|
19
|
+
|
20
|
+
_kind, value, index = @token || raise
|
21
|
+
line, col, current_line = error_context(@source || raise, index)
|
22
|
+
|
23
|
+
name_and_position = if @template_name
|
24
|
+
"#{@template_name}:#{line}:#{col}"
|
25
|
+
else
|
26
|
+
"#{current_line.inspect}:#{line}:#{col}"
|
27
|
+
end
|
28
|
+
|
29
|
+
pad = " " * line.to_s.length
|
30
|
+
pointer = (" " * col) + ("^" * (value&.length || 1))
|
31
|
+
|
32
|
+
<<~MESSAGE.strip
|
33
|
+
#{self.class}: #{message}
|
34
|
+
#{pad} -> #{name_and_position}
|
35
|
+
#{pad} |
|
36
|
+
#{line} | #{current_line}
|
37
|
+
#{pad} | #{pointer} #{highlight ? "\e[1m#{message}\e[0m" : message}
|
38
|
+
MESSAGE
|
39
|
+
end
|
40
|
+
|
41
|
+
def full_message(highlight: true, order: :top)
|
42
|
+
if FULL_MESSAGE
|
43
|
+
# For Ruby < 3.2.0
|
44
|
+
"#{super}\n#{detailed_message(highlight: highlight, order: order)}"
|
45
|
+
else
|
46
|
+
super
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
protected
|
51
|
+
|
52
|
+
def error_context(source, index)
|
53
|
+
lines = source.lines
|
54
|
+
cumulative_length = 0
|
55
|
+
target_line_index = -1
|
56
|
+
|
57
|
+
lines.each_with_index do |line, i|
|
58
|
+
cumulative_length += line.length
|
59
|
+
next unless index < cumulative_length
|
60
|
+
|
61
|
+
target_line_index = i
|
62
|
+
line_number = target_line_index + 1
|
63
|
+
column_number = index - (cumulative_length - lines[target_line_index].length)
|
64
|
+
return [line_number, column_number, lines[target_line_index].rstrip]
|
65
|
+
end
|
66
|
+
|
67
|
+
raise "index is out of bounds for span"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
class LiquidSyntaxError < LiquidError; end
|
72
|
+
class LiquidArgumentError < LiquidError; end
|
73
|
+
class LiquidTypeError < LiquidError; end
|
74
|
+
class LiquidTemplateNotFoundError < LiquidError; end
|
75
|
+
class LiquidFilterNotFoundError < LiquidError; end
|
76
|
+
class LiquidResourceLimitError < LiquidError; end
|
77
|
+
class UndefinedError < LiquidError; end
|
78
|
+
class DisabledTagError < LiquidError; end
|
79
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Liquid2
|
4
|
+
# Base class for all expressions.
|
5
|
+
class Expression
|
6
|
+
attr_reader :token
|
7
|
+
|
8
|
+
# @param token [[Symbol, String?, Integer]]
|
9
|
+
def initialize(token)
|
10
|
+
@token = token
|
11
|
+
end
|
12
|
+
|
13
|
+
# Return children of this expression.
|
14
|
+
def children = []
|
15
|
+
|
16
|
+
# Return variables this expression adds to the scope of any child expressions.
|
17
|
+
# Currently used by lambda expressions only.
|
18
|
+
def scope = nil
|
19
|
+
end
|
20
|
+
end
|