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,204 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "cgi"
5
+
6
+ module Liquid2
7
+ # Liquid filters and helper methods.
8
+ module Filters
9
+ # Return _left_ concatenated with _right_.
10
+ # Coerce _left_ and _right_ to strings if they aren't strings already.
11
+ def self.append(left, right)
12
+ Liquid2.to_s(left) + Liquid2.to_s(right)
13
+ end
14
+
15
+ # Return _left_ with the first character in uppercase and the rest lowercase.
16
+ # Coerce _left_ to a string if it is not one already.
17
+ def self.capitalize(left)
18
+ Liquid2.to_s(left).capitalize
19
+ end
20
+
21
+ # Return _left_ with all characters converted to lowercase.
22
+ # Coerce _left_ to a string if it is not one already.
23
+ def self.downcase(left)
24
+ Liquid2.to_s(left).downcase
25
+ end
26
+
27
+ # Return _left_ with all characters converted to uppercase.
28
+ # Coerce _left_ to a string if it is not one already.
29
+ def self.upcase(left)
30
+ Liquid2.to_s(left).upcase
31
+ end
32
+
33
+ # Return _left_ with special HTML characters replaced with their HTML-safe escape sequences.
34
+ # Coerce _left_ to a string if it is not one already.
35
+ def self.escape(left)
36
+ CGI.escape_html(Liquid2.to_s(left)) unless left.nil?
37
+ end
38
+
39
+ # Return _left_ with special HTML characters replaced with their HTML-safe escape sequences.
40
+ # Coerce _left_ to a string if it is not one already.
41
+ #
42
+ # It is safe to use `escape_once` on string values that already contain HTML-escape sequences.
43
+ def self.escape_once(left)
44
+ CGI.escape_html(CGI.unescape_html(Liquid2.to_s(left)))
45
+ end
46
+
47
+ # Return _left_ with leading whitespace removed.
48
+ # Coerce _left_ to a string if it is not one already.
49
+ def self.lstrip(left)
50
+ Liquid2.to_s(left).lstrip
51
+ end
52
+
53
+ # Return _left_ with trailing whitespace removed.
54
+ # Coerce _left_ to a string if it is not one already.
55
+ def self.rstrip(left)
56
+ Liquid2.to_s(left).rstrip
57
+ end
58
+
59
+ # Return _left_ with leading and trailing whitespace removed.
60
+ # Coerce _left_ to a string if it is not one already.
61
+ def self.strip(left)
62
+ Liquid2.to_s(left).strip
63
+ end
64
+
65
+ # Return _left_ with LF or CRLF replaced with `<br />\n`.
66
+ def self.newline_to_br(left)
67
+ Liquid2.to_s(left).gsub(/\r?\n/, "<br />\n")
68
+ end
69
+
70
+ # Return _right_ concatenated with _left_.
71
+ # Coerce _left_ and _right_ to strings if they aren't strings already.
72
+ def self.prepend(left, right)
73
+ Liquid2.to_s(right) + Liquid2.to_s(left)
74
+ end
75
+
76
+ # Return _left_ with all occurrences of _pattern_ replaced with _replacement_.
77
+ # All arguments are coerced to strings if they aren't strings already.
78
+ def self.replace(left, pattern, replacement = "")
79
+ Liquid2.to_s(left).gsub(Liquid2.to_s(pattern), Liquid2.to_s(replacement))
80
+ end
81
+
82
+ # Return _left_ with the first occurrence of _pattern_ replaced with _replacement_.
83
+ # All arguments are coerced to strings if they aren't strings already.
84
+ def self.replace_first(left, pattern, replacement = "")
85
+ Liquid2.to_s(left).sub(Liquid2.to_s(pattern), Liquid2.to_s(replacement))
86
+ end
87
+
88
+ # Return _left_ with the last occurrence of _pattern_ replaced with _replacement_.
89
+ # All arguments are coerced to strings if they aren't strings already.
90
+ def self.replace_last(left, pattern, replacement)
91
+ return left + replacement if Liquid2.undefined?(pattern)
92
+
93
+ head, match, tail = Liquid2.to_s(left).rpartition(Liquid2.to_s(pattern))
94
+ return left if match.empty?
95
+
96
+ head + Liquid2.to_s(replacement) + tail
97
+ end
98
+
99
+ # Return _left_ with all occurrences of _pattern_ removed.
100
+ # All arguments are coerced to strings if they aren't strings already.
101
+ def self.remove(left, pattern)
102
+ Liquid2.to_s(left).gsub(Liquid2.to_s(pattern), Liquid2.to_s(""))
103
+ end
104
+
105
+ # Return _left_ with the first occurrence of _pattern_ removed.
106
+ # All arguments are coerced to strings if they aren't strings already.
107
+ def self.remove_first(left, pattern)
108
+ Liquid2.to_s(left).sub(Liquid2.to_s(pattern), Liquid2.to_s(""))
109
+ end
110
+
111
+ # Return _left_ with the last occurrence of _pattern_ removed.
112
+ # All arguments are coerced to strings if they aren't strings already.
113
+ def self.remove_last(left, pattern)
114
+ return left if Liquid2.undefined?(pattern)
115
+
116
+ head, match, tail = Liquid2.to_s(left).rpartition(Liquid2.to_s(pattern))
117
+ return left if match.empty?
118
+
119
+ head + tail
120
+ end
121
+
122
+ # Split _left_ on every occurrence of _pattern_.
123
+ def self.split(left, pattern)
124
+ Liquid2.to_s(left).split(Liquid2.to_s(pattern))
125
+ end
126
+
127
+ RE_HTML_BLOCKS = Regexp.union(
128
+ %r{<script.*?</script>}m,
129
+ /<!--.*?-->/m,
130
+ %r{<style.*?</style>}m
131
+ )
132
+
133
+ RE_HTML_TAGS = /<.*?>/m
134
+
135
+ # Return _left_ with HTML tags removed.
136
+ def self.strip_html(left)
137
+ Liquid2.to_s(left).gsub(RE_HTML_BLOCKS, "").gsub(RE_HTML_TAGS, "")
138
+ end
139
+
140
+ # Return _left_ with CR and LF removed.
141
+ def self.strip_newlines(left)
142
+ Liquid2.to_s(left).gsub(/\r?\n/, "")
143
+ end
144
+
145
+ def self.truncate(left, max_length = 50, ellipsis = "...")
146
+ return if left.nil? || Liquid2.undefined?(left)
147
+
148
+ left = Liquid2.to_s(left)
149
+ max_length = to_integer(max_length)
150
+ return left if left.length <= max_length
151
+
152
+ ellipsis = Liquid2.to_s(ellipsis)
153
+ return ellipsis[0, max_length] if ellipsis.length >= max_length
154
+
155
+ "#{left[0...max_length - ellipsis.length]}#{ellipsis}"
156
+ end
157
+
158
+ def self.truncatewords(left, max_words = 15, ellipsis = "...")
159
+ return if left.nil? || Liquid2.undefined?(left)
160
+
161
+ left = Liquid2.to_s(left)
162
+ max_words = to_integer(max_words).clamp(1, 10_000)
163
+ words = left.split(" ", max_words + 1)
164
+ return left if words.length <= max_words
165
+
166
+ ellipsis = Liquid2.to_s(ellipsis)
167
+ words.pop
168
+ "#{words.join(" ")}#{ellipsis}"
169
+ end
170
+
171
+ def self.url_encode(left)
172
+ CGI.escape(Liquid2.to_s(left)) unless left.nil? || Liquid2.undefined?(left)
173
+ end
174
+
175
+ def self.url_decode(left)
176
+ return if left.nil? || Liquid2.undefined?(left)
177
+
178
+ decoded = CGI.unescape(Liquid2.to_s(left))
179
+ unless decoded.valid_encoding?
180
+ raise Liquid2::LiquidArgumentError.new("invalid byte sequence", nil)
181
+ end
182
+
183
+ decoded
184
+ end
185
+
186
+ def self.base64_encode(left)
187
+ Base64.strict_encode64(Liquid2.to_s(left)).force_encoding(Encoding::UTF_8)
188
+ end
189
+
190
+ def self.base64_decode(left)
191
+ decoded = Base64.strict_decode64(Liquid2.to_s(left)).force_encoding(Encoding::UTF_8)
192
+ decoded if decoded.valid_encoding?
193
+ end
194
+
195
+ def self.base64_url_safe_encode(left)
196
+ Base64.urlsafe_encode64(Liquid2.to_s(left)).force_encoding(Encoding::UTF_8)
197
+ end
198
+
199
+ def self.base64_url_safe_decode(left)
200
+ decoded = Base64.urlsafe_decode64(Liquid2.to_s(left)).force_encoding(Encoding::UTF_8)
201
+ decoded if decoded.valid_encoding?
202
+ end
203
+ end
204
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ module Liquid2
6
+ # Liquid template source text and meta data.
7
+ class TemplateSource
8
+ attr_reader :source, :name, :up_to_date, :matter
9
+
10
+ def initialize(source:, name:, up_to_date: nil, matter: nil)
11
+ @source = source
12
+ @name = name
13
+ @up_to_date = up_to_date
14
+ @matter = matter
15
+ end
16
+ end
17
+
18
+ # The base class for all template loaders.
19
+ class TemplateLoader
20
+ # Load and return template source text and any associated data.
21
+ # @param env [Environment] The current Liquid environment.
22
+ # @param name [String] A name or identifier for the target template source text.
23
+ # @param context [RenderContext?] The current render context, if one is available.
24
+ # @param *kwargs Arbitrary arguments that can be used to narrow the template source
25
+ # search space.
26
+ # @return [TemplateSource]
27
+ def get_source(env, name, context: nil, **kwargs) # rubocop:disable Lint/UnusedMethodArgument
28
+ raise "template loaders must implement `get_source`"
29
+ end
30
+
31
+ def load(env, name, globals: nil, context: nil, **kwargs)
32
+ data = get_source(env, name, context: context, **kwargs)
33
+ path = Pathname.new(data.name)
34
+ env.parse(data.source,
35
+ name: path.basename.to_s,
36
+ path: data.name,
37
+ globals: globals,
38
+ up_to_date: data.up_to_date,
39
+ overlay: data.matter)
40
+ end
41
+ end
42
+
43
+ # A template loader that reads templates from a hash.
44
+ class HashLoader < TemplateLoader
45
+ # @param templates [Hash<String, String>] A mapping of template names to template source text.
46
+ def initialize(templates)
47
+ super()
48
+ @templates = templates
49
+ end
50
+
51
+ def get_source(env, name, context: nil, **kwargs) # rubocop:disable Lint/UnusedMethodArgument
52
+ if (text = @templates[name])
53
+ TemplateSource.new(source: text, name: name)
54
+ else
55
+ raise LiquidTemplateNotFoundError.new("template not found #{name}", nil)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+ require_relative "../loader"
5
+ require_relative "mixins"
6
+
7
+ module Liquid2
8
+ # A template loader that reads template from a file system.
9
+ class FileSystemLoader < TemplateLoader
10
+ def initialize(search_path, default_extension: nil)
11
+ super()
12
+ @search_path = if search_path.is_a?(Array)
13
+ search_path.map { |p| Pathname.new(p) }
14
+ else
15
+ [Pathname.new(search_path)]
16
+ end
17
+
18
+ @default_extension = default_extension
19
+ end
20
+
21
+ def get_source(_env, name, context: nil, **_kwargs)
22
+ path = resolve_path(name)
23
+ mtime = path.mtime
24
+ up_to_date = -> { path.mtime == mtime }
25
+ TemplateSource.new(source: path.read, name: path.to_s, up_to_date: up_to_date)
26
+ end
27
+
28
+ def resolve_path(template_name)
29
+ template_path = Pathname.new(template_name)
30
+
31
+ # Append the default file extension if needed.
32
+ if @default_extension && template_path.extname.empty?
33
+ template_path = template_path.sub_ext(@default_extension || raise)
34
+ end
35
+
36
+ # Don't alow template names to escape the search path with "../".
37
+ template_path.each_filename do |part|
38
+ if part == ".."
39
+ raise LiquidTemplateNotFoundError.new("template not found #{template_name}",
40
+ nil)
41
+ end
42
+ end
43
+
44
+ # Search each path in turn.
45
+ @search_path.each do |path|
46
+ source_path = path.join(template_path)
47
+ return source_path if source_path.file?
48
+ end
49
+
50
+ raise LiquidTemplateNotFoundError.new("template not found #{template_name}", nil)
51
+ end
52
+ end
53
+
54
+ # A file system template loader that caches parsed templates.
55
+ class CachingFileSystemLoader < FileSystemLoader
56
+ include CachingLoaderMixin
57
+
58
+ def initialize(
59
+ search_path,
60
+ default_extension: nil,
61
+ auto_reload: true,
62
+ namespace_key: "",
63
+ capacity: 300,
64
+ thread_safe: false
65
+ )
66
+ super(search_path, default_extension: default_extension)
67
+
68
+ initialize_cache(
69
+ auto_reload: auto_reload,
70
+ namespace_key: namespace_key,
71
+ capacity: capacity,
72
+ thread_safe: thread_safe
73
+ )
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../utils/cache"
4
+
5
+ module Liquid2
6
+ # A mixin that adds caching to a template loader.
7
+ module CachingLoaderMixin
8
+ def initialize_cache(auto_reload: true, namespace_key: "", capacity: 300, thread_safe: false)
9
+ @auto_reload = auto_reload
10
+ @namespace_key = namespace_key
11
+ @cache = if thread_safe
12
+ ThreadSafeLRUCache.new(capacity)
13
+ else
14
+ LRUCache.new(capacity)
15
+ end
16
+ end
17
+
18
+ def load(env, name, globals: nil, context: nil, **kwargs)
19
+ key = cache_key(name, context: context, **kwargs)
20
+
21
+ # @type var template: Liquid2::Template
22
+ # @type var cached_template: Liquid2::Template
23
+ if (cached_template = @cache[key])
24
+ if @auto_reload && cached_template.up_to_date? == false
25
+ template = super
26
+ @cache[key] = template
27
+ template
28
+ else
29
+ cached_template
30
+ end
31
+ else
32
+ template = super
33
+ @cache[key] = template
34
+ template
35
+ end
36
+ end
37
+
38
+ def cache_key(name, context: nil, **kwargs)
39
+ return name unless @namespace_key
40
+
41
+ key = (@namespace_key || raise).to_sym
42
+ return "#{kwargs[key]}/#{name}" if kwargs.include?(key)
43
+ return name unless context
44
+
45
+ if (namespace = context.globals[@namespace_key || raise])
46
+ "#{namespace}/#{name}"
47
+ else
48
+ name
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquid2
4
+ # The base class for all nodes in a Liquid syntax tree.
5
+ class Node
6
+ attr_reader :token
7
+ attr_accessor :blank
8
+
9
+ # @param token [[Symbol, String?, Integer]]
10
+ def initialize(token)
11
+ @token = token
12
+ @blank = true
13
+ end
14
+
15
+ def render(_context, _buffer)
16
+ raise "nodes must implement `render: (RenderContext, String) -> void`"
17
+ end
18
+
19
+ def render_with_disabled_tag_check(context, buffer)
20
+ if context.disabled_tags.empty? ||
21
+ !is_a?(Tag) ||
22
+ !context.disabled_tags.include?(@token[1] || raise)
23
+ return render(context, buffer)
24
+ end
25
+
26
+ raise DisabledTagError.new("#{@token[1]} is not allowed in this context", @token)
27
+ end
28
+
29
+ # Return all children of this node.
30
+ def children(_static_context, include_partials: true) = []
31
+
32
+ # Return this node's expressions.
33
+ def expressions = []
34
+
35
+ # Return variables this node adds to the template local scope.
36
+ def template_scope = []
37
+
38
+ # Return variables this nodes adds to its block scope.
39
+ def block_scope = []
40
+
41
+ # Return information about a partial template loaded by this node.
42
+ def partial_scope = nil
43
+ end
44
+
45
+ # Partial template meta data.
46
+ class Partial
47
+ attr_reader :name, :scope, :in_scope
48
+
49
+ # @param name [Expression | String] The name of the partial template.
50
+ # @param scope [:shared | :isolated | :inherited] A symbol indicating the kind of
51
+ # scope the partial template should have when loaded.
52
+ # @param in_scope [Array[Identifier]] Names that will be added to the scope of the
53
+ # partial template.
54
+ def initialize(name, scope, in_scope)
55
+ @name = name
56
+ @scope = scope
57
+ @in_scope = in_scope
58
+ end
59
+ end
60
+
61
+ # An node representing a block of Liquid markup.
62
+ # Essentially an array of other nodes and strings.
63
+ # @param token [[Symbol, String?, Integer]]
64
+ # @param nodes [Array[Node | String]]
65
+ class Block < Node
66
+ def initialize(token, nodes)
67
+ super(token)
68
+ @nodes = nodes
69
+ @blank = nodes.all? do |n|
70
+ (n.is_a?(String) && n.match(/\A\s*\Z/)) || (n.is_a?(Node) && n.blank)
71
+ end
72
+ end
73
+
74
+ def render(context, buffer)
75
+ buffer = +"" if context.env.suppress_blank_control_flow_blocks && @blank
76
+ index = 0
77
+ while (node = @nodes[index])
78
+ index += 1
79
+ case node
80
+ when String
81
+ buffer << node
82
+ else
83
+ node.render_with_disabled_tag_check(context, buffer)
84
+ end
85
+
86
+ context.raise_for_output_limit(buffer.bytesize)
87
+ return unless context.interrupts.empty?
88
+ end
89
+ end
90
+
91
+ def children(_static_context, include_partials: true) = @nodes
92
+ end
93
+
94
+ # A Liquid block guarded by an expression.
95
+ # Only if the expression evaluates to a truthy value will the block be rendered.
96
+ class ConditionalBlock < Node
97
+ attr_reader :expression, :block
98
+
99
+ def initialize(token, expression, block)
100
+ super(token)
101
+ @expression = expression
102
+ @block = block
103
+ @blank = block.blank
104
+ end
105
+
106
+ def render(context, buffer)
107
+ @expression.evaluate(context) ? @block.render(context, buffer) : 0
108
+ end
109
+
110
+ def children(_static_context, include_partials: true) = [@block]
111
+ def expressions = [@expression]
112
+ end
113
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../node"
4
+
5
+ module Liquid2
6
+ # `{# comment #}` style comments.
7
+ class Comment < Node
8
+ attr_reader :text
9
+
10
+ # @param text [String]
11
+ def initialize(token, text)
12
+ super(token)
13
+ @text = text
14
+ end
15
+
16
+ def render(_context, _buffer) = 0
17
+ end
18
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../node"
4
+
5
+ module Liquid2
6
+ # The AST node representing output statements.
7
+ class Output < Node
8
+ attr_reader :expression
9
+
10
+ # @param token [[Symbol, String?, Integer]]
11
+ # @param expression [Expression]
12
+ def initialize(token, expression)
13
+ super(token)
14
+ @expression = expression
15
+ @blank = false
16
+ end
17
+
18
+ def render(context, buffer)
19
+ buffer << Liquid2.to_output_s(@expression.evaluate(context))
20
+ end
21
+
22
+ def expressions = [@expression]
23
+ end
24
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../tag"
4
+
5
+ module Liquid2
6
+ # The standard _assign_ tag.
7
+ class AssignTag < Tag
8
+ # @param parser [Parser]
9
+ # @return [AssignTag]
10
+ def self.parse(token, parser)
11
+ name = parser.parse_identifier(trailing_question: false)
12
+ parser.eat(:token_assign, "malformed identifier or missing assignment operator")
13
+ expression = parser.parse_filtered_expression
14
+ parser.carry_whitespace_control
15
+ parser.eat(:token_tag_end)
16
+ new(token, name, expression)
17
+ end
18
+
19
+ # @param token [[Symbol, String?, Integer]]
20
+ # @param name [Identifier]
21
+ # @param expression [Expression]
22
+ def initialize(token, name, expression)
23
+ super(token)
24
+ @name = name
25
+ @expression = expression
26
+ end
27
+
28
+ def render(context, _buffer)
29
+ context.assign(@name.name, context.evaluate(@expression))
30
+ end
31
+
32
+ def expressions = [@expression]
33
+ def template_scope = [@name]
34
+ end
35
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../tag"
4
+
5
+ module Liquid2
6
+ # The standard _comment_ tag.
7
+ class BlockComment < Tag
8
+ attr_reader :text
9
+
10
+ def self.parse(token, parser)
11
+ parser.carry_whitespace_control
12
+ parser.eat(:token_tag_end)
13
+ comment = parser.eat(:token_comment)
14
+ parser.eat_empty_tag("endcomment")
15
+ new(token, comment[1] || raise)
16
+ end
17
+
18
+ # @param text [String]
19
+ def initialize(token, text)
20
+ super(token)
21
+ @text = text
22
+ end
23
+
24
+ def render(_context, _buffer) = 0
25
+ end
26
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../tag"
4
+
5
+ module Liquid2
6
+ # The standard _capture_ tag.
7
+ class CaptureTag < Tag
8
+ END_BLOCK = Set["endcapture"]
9
+
10
+ # @param parser [Parser]
11
+ # @return [CaptureTag]
12
+ def self.parse(token, parser)
13
+ name = parser.parse_identifier(trailing_question: false)
14
+ parser.carry_whitespace_control
15
+ parser.eat(:token_tag_end)
16
+ block = parser.parse_block(END_BLOCK)
17
+ parser.eat_empty_tag("endcapture")
18
+ new(token, name, block)
19
+ end
20
+
21
+ # @param name [Identifier]
22
+ # @param block [Block]
23
+ def initialize(token, name, block)
24
+ super(token)
25
+ @name = name
26
+ @block = block
27
+ @block.blank = false
28
+ @blank = true
29
+ end
30
+
31
+ def render(context, _buffer)
32
+ buf = +""
33
+ @block.render(context, buf)
34
+ context.assign(@name.name, buf)
35
+ end
36
+
37
+ def children(_static_context, include_partials: true) = [@block]
38
+ def template_scope = [@name]
39
+ end
40
+ end