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,301 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Liquid2
|
4
|
+
# Template static analysis.
|
5
|
+
module StaticAnalysis
|
6
|
+
# The location of a variable, tag or filter.
|
7
|
+
class Span
|
8
|
+
attr_reader :template_name, :index
|
9
|
+
|
10
|
+
def initialize(template_name, index)
|
11
|
+
@template_name = template_name
|
12
|
+
@index = index
|
13
|
+
end
|
14
|
+
|
15
|
+
# @param source [String] Template source text.
|
16
|
+
# @return [[Integer, Integer]] The line and column number of this span in _source_.
|
17
|
+
def line_col(source)
|
18
|
+
lines = source.lines
|
19
|
+
cumulative_length = 0
|
20
|
+
target_line_index = -1
|
21
|
+
|
22
|
+
lines.each_with_index do |line, i|
|
23
|
+
cumulative_length += line.length
|
24
|
+
next unless @index < cumulative_length
|
25
|
+
|
26
|
+
target_line_index = i
|
27
|
+
line_number = target_line_index + 1
|
28
|
+
column_number = @index - (cumulative_length - lines[target_line_index].length)
|
29
|
+
return [line_number, column_number]
|
30
|
+
end
|
31
|
+
|
32
|
+
raise "index is out of bounds for span"
|
33
|
+
end
|
34
|
+
|
35
|
+
def ==(other)
|
36
|
+
self.class == other.class &&
|
37
|
+
@template_name == other.template_name &&
|
38
|
+
@index == other.index
|
39
|
+
end
|
40
|
+
|
41
|
+
alias eql? ==
|
42
|
+
|
43
|
+
def hash
|
44
|
+
[@template_name, @index].hash
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# A variable as a sequence of segments and its location.
|
49
|
+
class Variable
|
50
|
+
attr_reader :segments, :span
|
51
|
+
|
52
|
+
RE_PROPERTY = /\A[\u0080-\uFFFFa-zA-Z_][\u0080-\uFFFFa-zA-Z0-9_-]*\Z/
|
53
|
+
|
54
|
+
def initialize(segments, span)
|
55
|
+
@segments = segments
|
56
|
+
@span = span
|
57
|
+
end
|
58
|
+
|
59
|
+
def to_s
|
60
|
+
segments_to_s(@segments)
|
61
|
+
end
|
62
|
+
|
63
|
+
def ==(other)
|
64
|
+
self.class == other.class &&
|
65
|
+
@segments == other.segments &&
|
66
|
+
@span == other.span
|
67
|
+
end
|
68
|
+
|
69
|
+
alias eql? ==
|
70
|
+
|
71
|
+
def hash
|
72
|
+
@segments.hash
|
73
|
+
end
|
74
|
+
|
75
|
+
protected
|
76
|
+
|
77
|
+
def segments_to_s(segments)
|
78
|
+
head, *rest = segments
|
79
|
+
|
80
|
+
head.to_s + rest.map do |segment|
|
81
|
+
case segment
|
82
|
+
when Array
|
83
|
+
"[#{segments_to_s(segment)}]"
|
84
|
+
when String
|
85
|
+
if segment.match?(RE_PROPERTY)
|
86
|
+
".#{segment}"
|
87
|
+
else
|
88
|
+
"[#{segment.inspect}]"
|
89
|
+
end
|
90
|
+
else
|
91
|
+
"[#{segment}]"
|
92
|
+
end
|
93
|
+
end.join
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Helper to manage variable scope during static analysis.
|
98
|
+
class StaticScope
|
99
|
+
def initialize(globals)
|
100
|
+
@stack = [globals]
|
101
|
+
end
|
102
|
+
|
103
|
+
def include?(key)
|
104
|
+
@stack.any? { |scope| scope.include?(key) }
|
105
|
+
end
|
106
|
+
|
107
|
+
def push(scope)
|
108
|
+
@stack << scope
|
109
|
+
self
|
110
|
+
end
|
111
|
+
|
112
|
+
def pop
|
113
|
+
@stack.pop
|
114
|
+
end
|
115
|
+
|
116
|
+
def add(name)
|
117
|
+
@stack.first.add(name)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Helper to group variables by their root segment during static analysis.
|
122
|
+
class VariableMap
|
123
|
+
attr_reader :data
|
124
|
+
|
125
|
+
def initialize
|
126
|
+
@data = {}
|
127
|
+
end
|
128
|
+
|
129
|
+
def [](var)
|
130
|
+
key = var.segments.first.to_s
|
131
|
+
@data[key] = [] unless @data.include?(key)
|
132
|
+
@data[key]
|
133
|
+
end
|
134
|
+
|
135
|
+
def add(var)
|
136
|
+
send(:[], var) << var
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# The result of analyzing a template.
|
141
|
+
class Result
|
142
|
+
attr_reader :variables, :globals, :locals, :filters, :tags
|
143
|
+
|
144
|
+
def initialize(variables, globals, locals, filters, tags)
|
145
|
+
@variables = variables
|
146
|
+
@globals = globals
|
147
|
+
@locals = locals
|
148
|
+
@filters = filters
|
149
|
+
@tags = tags
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
def self.analyze(template, include_partials:)
|
154
|
+
variables = VariableMap.new
|
155
|
+
globals = VariableMap.new
|
156
|
+
locals = VariableMap.new
|
157
|
+
|
158
|
+
# @type var filters: Hash[String, Array[Span]]
|
159
|
+
filters = Hash.new { |hash, key| hash[key] = [] }
|
160
|
+
# @type var tags: Hash[String, Array[Span]]
|
161
|
+
tags = Hash.new { |hash, key| hash[key] = [] }
|
162
|
+
|
163
|
+
# @type var template_scope: Set[String]
|
164
|
+
template_scope = Set[]
|
165
|
+
root_scope = StaticScope.new(template_scope)
|
166
|
+
static_context = Liquid2::RenderContext.new(template)
|
167
|
+
|
168
|
+
# Names of partial templates that have already been analyzed.
|
169
|
+
# @type var seen: Set[String]
|
170
|
+
seen = Set[]
|
171
|
+
|
172
|
+
# @type var visit: ^(Node, String, StaticScope) -> void
|
173
|
+
visit = lambda do |node, template_name, scope|
|
174
|
+
seen.add(template_name) unless template_name.empty?
|
175
|
+
|
176
|
+
# Update tags
|
177
|
+
tags[node.name] << Span.new(template_name, node.token.last) if node.is_a?(Liquid2::Tag)
|
178
|
+
|
179
|
+
# Update variables from node.expressions
|
180
|
+
node.expressions.each do |expr|
|
181
|
+
if expr.is_a?(Liquid2::Expression)
|
182
|
+
analyze_variables(expr, template_name, scope, globals,
|
183
|
+
variables)
|
184
|
+
end
|
185
|
+
|
186
|
+
# Update filters from expr
|
187
|
+
extract_filters(expr, template_name).each do |name, span|
|
188
|
+
filters[name] << span
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
# Update template scope from node.template_scope
|
193
|
+
node.template_scope.each do |ident|
|
194
|
+
scope.add(ident.name)
|
195
|
+
locals.add(Variable.new([ident.name], Span.new(template_name, ident.token.last)))
|
196
|
+
end
|
197
|
+
|
198
|
+
if (partial = node.partial_scope)
|
199
|
+
partial_name = static_context.evaluate(partial.name).to_s
|
200
|
+
|
201
|
+
unless seen.include?(partial_name)
|
202
|
+
partial_scope = if partial.scope == :isolated
|
203
|
+
StaticScope.new(Set.new(partial.in_scope.map(&:name)))
|
204
|
+
else
|
205
|
+
root_scope.push(Set.new(partial.in_scope.map(&:name)))
|
206
|
+
end
|
207
|
+
|
208
|
+
node.children(static_context, include_partials: include_partials).each do |child|
|
209
|
+
seen.add(partial_name)
|
210
|
+
visit.call(child, partial_name, partial_scope) if child.is_a?(Liquid2::Node)
|
211
|
+
end
|
212
|
+
|
213
|
+
partial_scope.pop
|
214
|
+
end
|
215
|
+
else
|
216
|
+
scope.push(Set.new(node.block_scope.map(&:name)))
|
217
|
+
node.children(static_context, include_partials: include_partials).each do |child|
|
218
|
+
visit.call(child, template_name, scope) if child.is_a?(Liquid2::Node)
|
219
|
+
end
|
220
|
+
scope.pop
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
template.ast.each do |node|
|
225
|
+
visit.call(node, template.name, root_scope) if node.is_a?(Liquid2::Node)
|
226
|
+
end
|
227
|
+
|
228
|
+
Result.new(variables.data, globals.data, locals.data, filters, tags)
|
229
|
+
end
|
230
|
+
|
231
|
+
def self.extract_filters(expression, template_name)
|
232
|
+
filters = [] # : Array[[String, Span]]
|
233
|
+
|
234
|
+
if expression.is_a?(Liquid2::FilteredExpression)
|
235
|
+
expression.filters.each do |filter|
|
236
|
+
filters << [filter.name, Span.new(template_name, filter.token.last)]
|
237
|
+
end
|
238
|
+
elsif expression.is_a?(Liquid2::TernaryExpression)
|
239
|
+
expression.filters.each do |filter|
|
240
|
+
filters << [filter.name, Span.new(template_name, filter.token.last)]
|
241
|
+
end
|
242
|
+
|
243
|
+
expression.tail_filters.each do |filter|
|
244
|
+
filters << [filter.name, Span.new(template_name, filter.token.last)]
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
if expression.is_a?(Liquid2::Expression)
|
249
|
+
expression.children.each do |expr|
|
250
|
+
filters.concat(extract_filters(expr, template_name))
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
filters
|
255
|
+
end
|
256
|
+
|
257
|
+
def self.analyze_variables(expression, template_name, scope, globals, variables)
|
258
|
+
if expression.is_a?(Path)
|
259
|
+
var = Variable.new(segments(expression, template_name),
|
260
|
+
Span.new(template_name, expression.token.last))
|
261
|
+
variables.add(var)
|
262
|
+
|
263
|
+
root = var.segments.first.to_s
|
264
|
+
globals.add(var) unless scope.include?(root)
|
265
|
+
end
|
266
|
+
|
267
|
+
if (child_scope = expression.scope)
|
268
|
+
scope.push(Set.new(child_scope.map(&:name)))
|
269
|
+
expression.children.each do |expr|
|
270
|
+
if expr.is_a?(Expression)
|
271
|
+
analyze_variables(expr, template_name, scope, globals,
|
272
|
+
variables)
|
273
|
+
end
|
274
|
+
end
|
275
|
+
scope.pop
|
276
|
+
else
|
277
|
+
expression.children.each do |expr|
|
278
|
+
if expr.is_a?(Expression)
|
279
|
+
analyze_variables(expr, template_name, scope, globals,
|
280
|
+
variables)
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
def self.segments(path, template_name)
|
287
|
+
# @type var segments_: Array[untyped]
|
288
|
+
segments_ = [path.head.is_a?(Path) ? segments(path.head, template_name) : path.head]
|
289
|
+
|
290
|
+
path.segments.each do |segment|
|
291
|
+
segments_ << if segment.is_a?(Path)
|
292
|
+
segments(segment, template_name)
|
293
|
+
else
|
294
|
+
segment
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
segments_
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
data/lib/liquid2/tag.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Liquid2
|
4
|
+
# Base class for all Liquid tags.
|
5
|
+
class Tag < Node
|
6
|
+
def initialize(token)
|
7
|
+
super
|
8
|
+
return if token.first == :token_tag_name
|
9
|
+
|
10
|
+
raise "unexpected token kind for tag #{self.class} (#{token})"
|
11
|
+
end
|
12
|
+
|
13
|
+
# Render this tag to the output buffer.
|
14
|
+
# @param context [RenderContext]
|
15
|
+
# @param buffer [String]
|
16
|
+
def render(_context, _buffer)
|
17
|
+
raise "tags must implement `render: (RenderContext, String) -> void`."
|
18
|
+
end
|
19
|
+
|
20
|
+
def name = @token[1] || raise
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,182 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Liquid2
|
4
|
+
# A compiled template bound to a Liquid environment and ready to be rendered.
|
5
|
+
class Template
|
6
|
+
attr_reader :env, :ast, :name, :path, :globals, :overlay, :up_to_date, :source
|
7
|
+
|
8
|
+
# @param env [Environment]
|
9
|
+
# @param source [String]
|
10
|
+
# @param ast [Array[Node | String]]
|
11
|
+
# @param name [String] The template's name.
|
12
|
+
# @param path [String?] The path or other qualifying data to _name_.
|
13
|
+
# @param globals [_Namespace] Global template variables.
|
14
|
+
# @param overlay [_Namespace] Additional template variables. Could be from front matter
|
15
|
+
# or other meta data store, for example.
|
16
|
+
def initialize(env, source, ast, name: "", path: nil, up_to_date: nil, globals: nil,
|
17
|
+
overlay: nil)
|
18
|
+
@env = env
|
19
|
+
@source = source
|
20
|
+
@ast = ast
|
21
|
+
@name = name
|
22
|
+
@path = path
|
23
|
+
@globals = globals || {} # steep:ignore UnannotatedEmptyCollection
|
24
|
+
@overlay = overlay || {} # steep:ignore UnannotatedEmptyCollection
|
25
|
+
@up_to_date = up_to_date
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_s = @ast.to_s
|
29
|
+
|
30
|
+
# Return this template's path joined with its name, or just name if path is not available.
|
31
|
+
def full_name
|
32
|
+
@name + @path.to_s
|
33
|
+
end
|
34
|
+
|
35
|
+
# Render this template with data from _globals_ added to the render context.
|
36
|
+
# @param globals [Hash[::String, untyped]]
|
37
|
+
# @return [String]
|
38
|
+
def render(globals = nil)
|
39
|
+
buf = +""
|
40
|
+
context = RenderContext.new(self, globals: make_globals(globals))
|
41
|
+
render_with_context(context, buf)
|
42
|
+
buf
|
43
|
+
end
|
44
|
+
|
45
|
+
def render_with_context(context, buffer, partial: false, block_scope: false, namespace: nil)
|
46
|
+
# TODO: don't extend if namespace is nil
|
47
|
+
context.extend(namespace || {}) do
|
48
|
+
index = 0
|
49
|
+
while (node = @ast[index])
|
50
|
+
index += 1
|
51
|
+
case node
|
52
|
+
when String
|
53
|
+
buffer << node
|
54
|
+
else
|
55
|
+
node.render_with_disabled_tag_check(context, buffer)
|
56
|
+
end
|
57
|
+
|
58
|
+
context.raise_for_output_limit(buffer.bytesize)
|
59
|
+
|
60
|
+
next unless (interrupt = context.interrupts.pop)
|
61
|
+
|
62
|
+
if !partial || block_scope
|
63
|
+
raise LiquidSyntaxError.new("unexpected #{interrupt}",
|
64
|
+
node.token) # steep:ignore
|
65
|
+
end
|
66
|
+
|
67
|
+
context.interrupts << interrupt
|
68
|
+
break
|
69
|
+
end
|
70
|
+
end
|
71
|
+
rescue LiquidError => e
|
72
|
+
e.source = @source
|
73
|
+
e.template_name = @name unless @name.empty?
|
74
|
+
raise
|
75
|
+
end
|
76
|
+
|
77
|
+
# Merge template globals with another namespace.
|
78
|
+
def make_globals(namespace)
|
79
|
+
# TODO: optimize
|
80
|
+
@globals.merge(@overlay || {}, namespace || {})
|
81
|
+
end
|
82
|
+
|
83
|
+
# Return `false` if this template is stale and needs to be loaded again.
|
84
|
+
# `nil` is returned if an `up_to_date` proc is not available.
|
85
|
+
def up_to_date?
|
86
|
+
@up_to_date&.call
|
87
|
+
end
|
88
|
+
|
89
|
+
# Statically analyze this template and report variable, tag and filter usage.
|
90
|
+
# @param include_partials [bool]
|
91
|
+
# @return [Liquid2::StaticAnalysis::Result]
|
92
|
+
def analyze(include_partials: false)
|
93
|
+
Liquid2::StaticAnalysis.analyze(self, include_partials: include_partials)
|
94
|
+
end
|
95
|
+
|
96
|
+
# Return an array of comment nodes found in this template.
|
97
|
+
#
|
98
|
+
# Comment nodes have `token` and `text` attributes. Use `template.comments.map(&:text)`
|
99
|
+
# to get an array of comment strings. Each comment string includes leading and trailing
|
100
|
+
# whitespace.
|
101
|
+
#
|
102
|
+
# Note that this method does not try to load included or render templates when looking.
|
103
|
+
# for comment nodes.
|
104
|
+
#
|
105
|
+
# @return [Array[BlockComment | InlineComment | Comment]]
|
106
|
+
def comments
|
107
|
+
context = RenderContext.new(self)
|
108
|
+
nodes = [] # : Array[BlockComment | InlineComment | Comment]
|
109
|
+
|
110
|
+
# @type var visit: ^(Node) -> void
|
111
|
+
visit = lambda do |node|
|
112
|
+
if node.is_a?(BlockComment) || node.is_a?(InlineComment) || node.is_a?(Comment)
|
113
|
+
nodes << node
|
114
|
+
end
|
115
|
+
|
116
|
+
node.children(context, include_partials: false).each do |child|
|
117
|
+
visit.call(child) if child.is_a?(Node)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
@ast.each { |node| visit.call(node) if node.is_a?(Node) }
|
122
|
+
|
123
|
+
nodes
|
124
|
+
end
|
125
|
+
|
126
|
+
# Return an array of variables used in this template, without path segments.
|
127
|
+
# @param include_partials [bool]
|
128
|
+
# @return [Array[String]]
|
129
|
+
def variables(include_partials: false)
|
130
|
+
analyze(include_partials: include_partials).variables.keys
|
131
|
+
end
|
132
|
+
|
133
|
+
# Return an array of variables used in this template, including path segments.
|
134
|
+
# @param include_partials [bool]
|
135
|
+
# @return [Array[String]]
|
136
|
+
def variable_paths(include_partials: false)
|
137
|
+
analyze(include_partials: include_partials).variables.values.flatten.map(&:to_s).uniq
|
138
|
+
end
|
139
|
+
|
140
|
+
# Return an array of variables used in this template, each as an array of segments.
|
141
|
+
# @param include_partials [bool]
|
142
|
+
# @return [Array[Array[String | Integer | Segment]]]
|
143
|
+
def variable_segments(include_partials: false)
|
144
|
+
analyze(include_partials: include_partials).variables.values.flatten.map(&:segments).uniq
|
145
|
+
end
|
146
|
+
|
147
|
+
# Return an array of global variables used in this template, without path segments.
|
148
|
+
# @param include_partials [bool]
|
149
|
+
# @return [Array[String]]
|
150
|
+
def global_variables(include_partials: false)
|
151
|
+
analyze(include_partials: include_partials).globals.keys
|
152
|
+
end
|
153
|
+
|
154
|
+
# Return an array of global variables used in this template, including path segments.
|
155
|
+
# @param include_partials [bool]
|
156
|
+
# @return [Array[String]]
|
157
|
+
def global_variable_paths(include_partials: false)
|
158
|
+
analyze(include_partials: include_partials).globals.values.flatten.map(&:to_s).uniq
|
159
|
+
end
|
160
|
+
|
161
|
+
# Return an array of global variables used in this template, each as an array of segments.
|
162
|
+
# @param include_partials [bool]
|
163
|
+
# @return [Array[Array[String | Integer | Segment]]]
|
164
|
+
def global_variable_segments(include_partials: false)
|
165
|
+
analyze(include_partials: include_partials).globals.values.flatten.map(&:segments).uniq
|
166
|
+
end
|
167
|
+
|
168
|
+
# Return the names of all filters used in this template.
|
169
|
+
# @param include_partials [bool]
|
170
|
+
# @return [Array[String]]
|
171
|
+
def filter_names(include_partials: false)
|
172
|
+
analyze(include_partials: include_partials).filters.keys
|
173
|
+
end
|
174
|
+
|
175
|
+
# Return the names of all tags used in this template.
|
176
|
+
# @param include_partials [bool]
|
177
|
+
# @return [Array[String]]
|
178
|
+
def tag_names(include_partials: false)
|
179
|
+
analyze(include_partials: include_partials).tags.keys
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "errors"
|
4
|
+
|
5
|
+
module Liquid2
|
6
|
+
# The default undefined type. Can be iterated over an indexed without error.
|
7
|
+
class Undefined
|
8
|
+
attr_reader :force_default
|
9
|
+
|
10
|
+
def initialize(name, node: nil)
|
11
|
+
@name = name
|
12
|
+
@node = node
|
13
|
+
@force_default = false
|
14
|
+
end
|
15
|
+
|
16
|
+
def [](...) = self
|
17
|
+
def key?(...) = false
|
18
|
+
def include?(...) = false
|
19
|
+
def member?(...) = false
|
20
|
+
def fetch(...) = self
|
21
|
+
def ! = true
|
22
|
+
def ==(other) = other.nil? || other.is_a?(Undefined)
|
23
|
+
alias eql? ==
|
24
|
+
def size = 0
|
25
|
+
def length = 0
|
26
|
+
def to_s = ""
|
27
|
+
def to_i = 0
|
28
|
+
def to_f = 0.0
|
29
|
+
def each(...) = Enumerator.new {} # rubocop:disable Lint/EmptyBlock
|
30
|
+
def each_with_index(...) = Enumerator.new {} # rubocop:disable Lint/EmptyBlock
|
31
|
+
def join(...) = ""
|
32
|
+
def to_liquid(_context) = nil
|
33
|
+
def poke = true
|
34
|
+
end
|
35
|
+
|
36
|
+
# An undefined type that always raises an exception.
|
37
|
+
class StrictUndefined < Undefined
|
38
|
+
def initialize(name, node: nil)
|
39
|
+
super
|
40
|
+
@message = "#{name.inspect} is undefined"
|
41
|
+
end
|
42
|
+
|
43
|
+
def respond_to_missing? = true
|
44
|
+
|
45
|
+
def method_missing(...)
|
46
|
+
raise UndefinedError.new(@message, @node)
|
47
|
+
end
|
48
|
+
|
49
|
+
def [](...)
|
50
|
+
raise UndefinedError.new(@message, @node)
|
51
|
+
end
|
52
|
+
|
53
|
+
def key?(...)
|
54
|
+
raise UndefinedError.new(@message, @node)
|
55
|
+
end
|
56
|
+
|
57
|
+
def include?(...)
|
58
|
+
raise UndefinedError.new(@message, @node)
|
59
|
+
end
|
60
|
+
|
61
|
+
def member?(...)
|
62
|
+
raise UndefinedError.new(@message, @node)
|
63
|
+
end
|
64
|
+
|
65
|
+
def fetch(...)
|
66
|
+
raise UndefinedError.new(@message, @node)
|
67
|
+
end
|
68
|
+
|
69
|
+
def !
|
70
|
+
raise UndefinedError.new(@message, @node)
|
71
|
+
end
|
72
|
+
|
73
|
+
def ==(_other)
|
74
|
+
raise UndefinedError.new(@message, @node)
|
75
|
+
end
|
76
|
+
|
77
|
+
def !=(_other)
|
78
|
+
raise UndefinedError.new(@message, @node)
|
79
|
+
end
|
80
|
+
|
81
|
+
alias eql? ==
|
82
|
+
|
83
|
+
def size
|
84
|
+
raise UndefinedError.new(@message, @node)
|
85
|
+
end
|
86
|
+
|
87
|
+
def length
|
88
|
+
raise UndefinedError.new(@message, @node)
|
89
|
+
end
|
90
|
+
|
91
|
+
def to_s
|
92
|
+
raise UndefinedError.new(@message, @node)
|
93
|
+
end
|
94
|
+
|
95
|
+
def to_i
|
96
|
+
raise UndefinedError.new(@message, @node)
|
97
|
+
end
|
98
|
+
|
99
|
+
def to_f
|
100
|
+
raise UndefinedError.new(@message, @node)
|
101
|
+
end
|
102
|
+
|
103
|
+
def each(...)
|
104
|
+
raise UndefinedError.new(@message, @node)
|
105
|
+
end
|
106
|
+
|
107
|
+
def each_with_index(...)
|
108
|
+
raise UndefinedError.new(@message, @node)
|
109
|
+
end
|
110
|
+
|
111
|
+
def join(...)
|
112
|
+
raise UndefinedError.new(@message, @node)
|
113
|
+
end
|
114
|
+
|
115
|
+
def to_liquid(_context)
|
116
|
+
raise UndefinedError.new(@message, @node)
|
117
|
+
end
|
118
|
+
|
119
|
+
def poke
|
120
|
+
raise UndefinedError.new(@message, @node)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# A strict undefined type that plays nicely with the _default_ filter.
|
125
|
+
class StrictDefaultUndefined < StrictUndefined
|
126
|
+
def initialize(name, node: nil)
|
127
|
+
super
|
128
|
+
@force_default = true
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "monitor"
|
4
|
+
|
5
|
+
module Liquid2
|
6
|
+
# A least recently used cache relying on Ruby hash insertion order.
|
7
|
+
class LRUCache
|
8
|
+
attr_reader :max_size
|
9
|
+
|
10
|
+
def initialize(max_size = 128)
|
11
|
+
@data = {}
|
12
|
+
@max_size = max_size
|
13
|
+
end
|
14
|
+
|
15
|
+
# Return the cached value or nil if _key_ does not exist.
|
16
|
+
def [](key)
|
17
|
+
val = @data[key]
|
18
|
+
return nil if val.nil?
|
19
|
+
|
20
|
+
@data.delete(key)
|
21
|
+
@data[key] = val
|
22
|
+
val
|
23
|
+
end
|
24
|
+
|
25
|
+
def []=(key, value)
|
26
|
+
if @data.key?(key)
|
27
|
+
@data.delete(key)
|
28
|
+
elsif @data.length >= @max_size
|
29
|
+
@data.delete((@data.first || raise)[0])
|
30
|
+
end
|
31
|
+
@data[key] = value
|
32
|
+
end
|
33
|
+
|
34
|
+
def length
|
35
|
+
@data.length
|
36
|
+
end
|
37
|
+
|
38
|
+
def keys
|
39
|
+
@data.keys
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# A thread safe least recently used cache.
|
44
|
+
class ThreadSafeLRUCache < LRUCache
|
45
|
+
include MonitorMixin
|
46
|
+
|
47
|
+
alias unsafe_get []
|
48
|
+
alias unsafe_set []=
|
49
|
+
alias unsafe_length length
|
50
|
+
alias unsafe_keys keys
|
51
|
+
|
52
|
+
def initialize(max_size = 128)
|
53
|
+
super
|
54
|
+
end
|
55
|
+
|
56
|
+
def [](key)
|
57
|
+
synchronize do
|
58
|
+
unsafe_get(key)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def []=(key, value)
|
63
|
+
synchronize do
|
64
|
+
unsafe_set(key, value)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def length
|
69
|
+
synchronize do
|
70
|
+
unsafe_length
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def keys
|
75
|
+
synchronize do
|
76
|
+
unsafe_keys
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|