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,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
@@ -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