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.
Files changed (84) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +0 -0
  3. data/.rubocop.yml +46 -0
  4. data/.ruby-version +1 -0
  5. data/.vscode/settings.json +32 -0
  6. data/CHANGELOG.md +5 -0
  7. data/LICENSE.txt +21 -0
  8. data/LICENSE_SHOPIFY.txt +20 -0
  9. data/README.md +219 -0
  10. data/Rakefile +23 -0
  11. data/Steepfile +26 -0
  12. data/lib/liquid2/context.rb +297 -0
  13. data/lib/liquid2/environment.rb +287 -0
  14. data/lib/liquid2/errors.rb +79 -0
  15. data/lib/liquid2/expression.rb +20 -0
  16. data/lib/liquid2/expressions/arguments.rb +25 -0
  17. data/lib/liquid2/expressions/array.rb +20 -0
  18. data/lib/liquid2/expressions/blank.rb +41 -0
  19. data/lib/liquid2/expressions/boolean.rb +20 -0
  20. data/lib/liquid2/expressions/filtered.rb +136 -0
  21. data/lib/liquid2/expressions/identifier.rb +43 -0
  22. data/lib/liquid2/expressions/lambda.rb +53 -0
  23. data/lib/liquid2/expressions/logical.rb +71 -0
  24. data/lib/liquid2/expressions/loop.rb +79 -0
  25. data/lib/liquid2/expressions/path.rb +33 -0
  26. data/lib/liquid2/expressions/range.rb +28 -0
  27. data/lib/liquid2/expressions/relational.rb +119 -0
  28. data/lib/liquid2/expressions/template_string.rb +20 -0
  29. data/lib/liquid2/filter.rb +95 -0
  30. data/lib/liquid2/filters/array.rb +202 -0
  31. data/lib/liquid2/filters/date.rb +20 -0
  32. data/lib/liquid2/filters/default.rb +16 -0
  33. data/lib/liquid2/filters/json.rb +15 -0
  34. data/lib/liquid2/filters/math.rb +87 -0
  35. data/lib/liquid2/filters/size.rb +11 -0
  36. data/lib/liquid2/filters/slice.rb +17 -0
  37. data/lib/liquid2/filters/sort.rb +96 -0
  38. data/lib/liquid2/filters/string.rb +204 -0
  39. data/lib/liquid2/loader.rb +59 -0
  40. data/lib/liquid2/loaders/file_system_loader.rb +76 -0
  41. data/lib/liquid2/loaders/mixins.rb +52 -0
  42. data/lib/liquid2/node.rb +113 -0
  43. data/lib/liquid2/nodes/comment.rb +18 -0
  44. data/lib/liquid2/nodes/output.rb +24 -0
  45. data/lib/liquid2/nodes/tags/assign.rb +35 -0
  46. data/lib/liquid2/nodes/tags/block_comment.rb +26 -0
  47. data/lib/liquid2/nodes/tags/capture.rb +40 -0
  48. data/lib/liquid2/nodes/tags/case.rb +111 -0
  49. data/lib/liquid2/nodes/tags/cycle.rb +63 -0
  50. data/lib/liquid2/nodes/tags/decrement.rb +29 -0
  51. data/lib/liquid2/nodes/tags/doc.rb +24 -0
  52. data/lib/liquid2/nodes/tags/echo.rb +31 -0
  53. data/lib/liquid2/nodes/tags/extends.rb +3 -0
  54. data/lib/liquid2/nodes/tags/for.rb +155 -0
  55. data/lib/liquid2/nodes/tags/if.rb +84 -0
  56. data/lib/liquid2/nodes/tags/include.rb +123 -0
  57. data/lib/liquid2/nodes/tags/increment.rb +29 -0
  58. data/lib/liquid2/nodes/tags/inline_comment.rb +28 -0
  59. data/lib/liquid2/nodes/tags/liquid.rb +29 -0
  60. data/lib/liquid2/nodes/tags/macro.rb +3 -0
  61. data/lib/liquid2/nodes/tags/raw.rb +30 -0
  62. data/lib/liquid2/nodes/tags/render.rb +137 -0
  63. data/lib/liquid2/nodes/tags/tablerow.rb +143 -0
  64. data/lib/liquid2/nodes/tags/translate.rb +3 -0
  65. data/lib/liquid2/nodes/tags/unless.rb +23 -0
  66. data/lib/liquid2/nodes/tags/with.rb +3 -0
  67. data/lib/liquid2/parser.rb +917 -0
  68. data/lib/liquid2/scanner.rb +595 -0
  69. data/lib/liquid2/static_analysis.rb +301 -0
  70. data/lib/liquid2/tag.rb +22 -0
  71. data/lib/liquid2/template.rb +182 -0
  72. data/lib/liquid2/undefined.rb +131 -0
  73. data/lib/liquid2/utils/cache.rb +80 -0
  74. data/lib/liquid2/utils/chain_hash.rb +40 -0
  75. data/lib/liquid2/utils/unescape.rb +119 -0
  76. data/lib/liquid2/version.rb +5 -0
  77. data/lib/liquid2.rb +90 -0
  78. data/performance/benchmark.rb +73 -0
  79. data/performance/memory_profile.rb +62 -0
  80. data/performance/profile.rb +71 -0
  81. data/sig/liquid2.rbs +2348 -0
  82. data.tar.gz.sig +0 -0
  83. metadata +164 -0
  84. 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