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,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
|
data/lib/liquid2/node.rb
ADDED
@@ -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
|