liquid2 0.1.1 → 0.2.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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +16 -2
- data/LICENSE_SHOPIFY.txt +8 -0
- data/README.md +416 -13
- data/lib/liquid2/context.rb +29 -18
- data/lib/liquid2/environment.rb +53 -6
- data/lib/liquid2/errors.rb +4 -2
- data/lib/liquid2/expressions/arguments.rb +20 -0
- data/lib/liquid2/expressions/boolean.rb +2 -1
- data/lib/liquid2/expressions/filtered.rb +19 -25
- data/lib/liquid2/expressions/loop.rb +7 -5
- data/lib/liquid2/expressions/path.rb +19 -2
- data/lib/liquid2/filter.rb +1 -2
- data/lib/liquid2/filters/array.rb +0 -1
- data/lib/liquid2/filters/sort.rb +5 -4
- data/lib/liquid2/loader.rb +1 -0
- data/lib/liquid2/nodes/tags/doc.rb +2 -0
- data/lib/liquid2/nodes/tags/extends.rb +270 -1
- data/lib/liquid2/nodes/tags/include.rb +7 -7
- data/lib/liquid2/nodes/tags/macro.rb +145 -1
- data/lib/liquid2/nodes/tags/render.rb +8 -10
- data/lib/liquid2/nodes/tags/with.rb +42 -1
- data/lib/liquid2/parser.rb +84 -7
- data/lib/liquid2/scanner.rb +18 -42
- data/lib/liquid2/static_analysis.rb +1 -1
- data/lib/liquid2/template.rb +52 -5
- data/lib/liquid2/undefined.rb +22 -20
- data/lib/liquid2/version.rb +1 -1
- data/lib/liquid2.rb +2 -0
- data/sig/liquid2.rbs +234 -28
- data.tar.gz.sig +0 -0
- metadata +1 -2
- metadata.gz.sig +0 -0
- data/.vscode/settings.json +0 -32
data/lib/liquid2/environment.rb
CHANGED
@@ -21,16 +21,19 @@ require_relative "nodes/tags/cycle"
|
|
21
21
|
require_relative "nodes/tags/decrement"
|
22
22
|
require_relative "nodes/tags/doc"
|
23
23
|
require_relative "nodes/tags/echo"
|
24
|
+
require_relative "nodes/tags/extends"
|
24
25
|
require_relative "nodes/tags/for"
|
25
26
|
require_relative "nodes/tags/if"
|
26
27
|
require_relative "nodes/tags/include"
|
27
28
|
require_relative "nodes/tags/increment"
|
28
29
|
require_relative "nodes/tags/inline_comment"
|
29
30
|
require_relative "nodes/tags/liquid"
|
31
|
+
require_relative "nodes/tags/macro"
|
30
32
|
require_relative "nodes/tags/raw"
|
31
33
|
require_relative "nodes/tags/render"
|
32
34
|
require_relative "nodes/tags/tablerow"
|
33
35
|
require_relative "nodes/tags/unless"
|
36
|
+
require_relative "nodes/tags/with"
|
34
37
|
|
35
38
|
module Liquid2
|
36
39
|
# Template parsing and rendering configuration.
|
@@ -42,8 +45,28 @@ module Liquid2
|
|
42
45
|
class Environment
|
43
46
|
attr_reader :tags, :local_namespace_limit, :context_depth_limit, :loop_iteration_limit,
|
44
47
|
:output_stream_limit, :filters, :suppress_blank_control_flow_blocks,
|
45
|
-
:shorthand_indexes
|
48
|
+
:shorthand_indexes, :falsy_undefined
|
46
49
|
|
50
|
+
# @param context_depth_limit [Integer] The maximum number of times a render context can
|
51
|
+
# be extended or copied before a `Liquid2::LiquidResourceLimitError`` is raised.
|
52
|
+
# @param globals [Hash[String, untyped]?] Variables that are available to all templates
|
53
|
+
# rendered from this environment.
|
54
|
+
# @param loader [Liquid2::Loader] An instance of `Liquid2::Loader`. A template loader
|
55
|
+
# is responsible for finding and reading templates for `{% include %}` and
|
56
|
+
# `{% render %}` tags, or when calling `Liquid2::Environment.get_template(name)`.
|
57
|
+
# @param local_namespace_limit [Integer?] The maximum allowed "size" of the template
|
58
|
+
# local namespace (variables from `assign` and `capture` tags) before a
|
59
|
+
# `Liquid2::LiquidResourceLimitError`` is raised.
|
60
|
+
# @param loop_iteration_limit [Integer?] The maximum number of loop iterations allowed
|
61
|
+
# before a `LiquidResourceLimitError` is raised.
|
62
|
+
# @param output_stream_limit [Integer?] The maximum number of bytes that can be written
|
63
|
+
# to a template's output buffer before a `LiquidResourceLimitError` is raised.
|
64
|
+
# @param shorthand_indexes [bool] When `true`, allow shorthand dotted array indexes as
|
65
|
+
# well as bracketed indexes in variable paths. Defaults to `false`.
|
66
|
+
# @param suppress_blank_control_flow_blocks [bool] When `true`, suppress blank control
|
67
|
+
# flow block output, so as not to include unnecessary whitespace. Defaults to `true`.
|
68
|
+
# @param undefined [singleton(Liquid2::Undefined)] A singleton returning an instance of
|
69
|
+
# `Liquid2::Undefined`, which is used to represent template variables that don't exist.
|
47
70
|
def initialize(
|
48
71
|
context_depth_limit: 30,
|
49
72
|
globals: nil,
|
@@ -53,7 +76,8 @@ module Liquid2
|
|
53
76
|
output_stream_limit: nil,
|
54
77
|
shorthand_indexes: false,
|
55
78
|
suppress_blank_control_flow_blocks: true,
|
56
|
-
undefined: Undefined
|
79
|
+
undefined: Undefined,
|
80
|
+
falsy_undefined: true
|
57
81
|
)
|
58
82
|
# A mapping of tag names to objects responding to `parse(token, parser)`.
|
59
83
|
@tags = {}
|
@@ -99,10 +123,14 @@ module Liquid2
|
|
99
123
|
# unnecessary whitespace. Defaults to `true`.
|
100
124
|
@suppress_blank_control_flow_blocks = suppress_blank_control_flow_blocks
|
101
125
|
|
102
|
-
#
|
103
|
-
# don't exist.
|
126
|
+
# A singleton returning an instance of `Liquid2::Undefined`, which is used to
|
127
|
+
# represent template variables that don't exist.
|
104
128
|
@undefined = undefined
|
105
129
|
|
130
|
+
# When `true` (the default), undefined variables are considered falsy and do not
|
131
|
+
# raise an error when tested for truthiness.
|
132
|
+
@falsy_undefined = falsy_undefined
|
133
|
+
|
106
134
|
# Override `setup_tags_and_filters` in environment subclasses to configure custom
|
107
135
|
# tags and/or filters.
|
108
136
|
setup_tags_and_filters
|
@@ -118,8 +146,8 @@ module Liquid2
|
|
118
146
|
name: name, path: path, up_to_date: up_to_date,
|
119
147
|
globals: make_globals(globals), overlay: overlay)
|
120
148
|
rescue LiquidError => e
|
121
|
-
e.source = source
|
122
|
-
e.template_name = name unless name.empty?
|
149
|
+
e.source = source unless e.source
|
150
|
+
e.template_name = name unless e.template_name || name.empty?
|
123
151
|
raise
|
124
152
|
end
|
125
153
|
|
@@ -155,6 +183,20 @@ module Liquid2
|
|
155
183
|
@filters.delete(name)
|
156
184
|
end
|
157
185
|
|
186
|
+
# Add or replace a tag.
|
187
|
+
# @param name [String] The tag's name, as used by template authors.
|
188
|
+
# @param tag [responds to parse: ([Symbol, String?, Integer], Parser) -> Tag]
|
189
|
+
def register_tag(name, tag)
|
190
|
+
@tags[name] = tag
|
191
|
+
end
|
192
|
+
|
193
|
+
# Remove a tag from the tag register.
|
194
|
+
# @param name [String] The name of the tag.
|
195
|
+
# @return [_Tag | nil]
|
196
|
+
def delete_tag(name)
|
197
|
+
@tags.delete(name)
|
198
|
+
end
|
199
|
+
|
158
200
|
def setup_tags_and_filters
|
159
201
|
@tags["#"] = InlineComment
|
160
202
|
@tags["assign"] = AssignTag
|
@@ -167,15 +209,20 @@ module Liquid2
|
|
167
209
|
@tags["decrement"] = DecrementTag
|
168
210
|
@tags["doc"] = DocTag
|
169
211
|
@tags["echo"] = EchoTag
|
212
|
+
@tags["extends"] = ExtendsTag
|
213
|
+
@tags["block"] = BlockTag
|
170
214
|
@tags["for"] = ForTag
|
171
215
|
@tags["if"] = IfTag
|
172
216
|
@tags["include"] = IncludeTag
|
173
217
|
@tags["increment"] = IncrementTag
|
174
218
|
@tags["liquid"] = LiquidTag
|
219
|
+
@tags["macro"] = MacroTag
|
220
|
+
@tags["call"] = CallTag
|
175
221
|
@tags["raw"] = RawTag
|
176
222
|
@tags["render"] = RenderTag
|
177
223
|
@tags["tablerow"] = TableRowTag
|
178
224
|
@tags["unless"] = UnlessTag
|
225
|
+
@tags["with"] = WithTag
|
179
226
|
|
180
227
|
register_filter("abs", Liquid2::Filters.method(:abs))
|
181
228
|
register_filter("append", Liquid2::Filters.method(:append))
|
data/lib/liquid2/errors.rb
CHANGED
@@ -7,10 +7,10 @@ module Liquid2
|
|
7
7
|
|
8
8
|
FULL_MESSAGE = ((RUBY_VERSION.split(".")&.map(&:to_i) <=> [3, 2, 0]) || -1) < 1
|
9
9
|
|
10
|
-
def initialize(message, token = nil)
|
10
|
+
def initialize(message, token = nil, template_name: nil)
|
11
11
|
super(message)
|
12
12
|
@token = token
|
13
|
-
@template_name =
|
13
|
+
@template_name = template_name
|
14
14
|
@source = nil
|
15
15
|
end
|
16
16
|
|
@@ -76,4 +76,6 @@ module Liquid2
|
|
76
76
|
class LiquidResourceLimitError < LiquidError; end
|
77
77
|
class UndefinedError < LiquidError; end
|
78
78
|
class DisabledTagError < LiquidError; end
|
79
|
+
class TemplateInheritanceError < LiquidError; end
|
80
|
+
class RequiredBlockError < TemplateInheritanceError; end
|
79
81
|
end
|
@@ -22,4 +22,24 @@ module Liquid2
|
|
22
22
|
|
23
23
|
def children = [@value]
|
24
24
|
end
|
25
|
+
|
26
|
+
# A macro parameter with a name and optional default value.
|
27
|
+
class Parameter < Expression
|
28
|
+
attr_reader :value, :name, :sym
|
29
|
+
|
30
|
+
# @param name [String]
|
31
|
+
# @param value [Expression?]
|
32
|
+
def initialize(token, name, value)
|
33
|
+
super(token)
|
34
|
+
@name = name
|
35
|
+
@sym = name.to_sym
|
36
|
+
@value = value
|
37
|
+
end
|
38
|
+
|
39
|
+
def evaluate(context)
|
40
|
+
[@name, context.evaluate(@value)]
|
41
|
+
end
|
42
|
+
|
43
|
+
def children = [@value]
|
44
|
+
end
|
25
45
|
end
|
@@ -12,7 +12,8 @@ module Liquid2
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def evaluate(context)
|
15
|
-
|
15
|
+
value = context.evaluate(@expr)
|
16
|
+
Liquid2.truthy?(context, value.respond_to?(:to_liquid) ? value.to_liquid(context) : value)
|
16
17
|
end
|
17
18
|
|
18
19
|
def children = [@expr]
|
@@ -15,8 +15,10 @@ module Liquid2
|
|
15
15
|
|
16
16
|
def evaluate(context)
|
17
17
|
left = context.evaluate(@left)
|
18
|
+
return left if @filters.nil?
|
19
|
+
|
18
20
|
index = 0
|
19
|
-
while (filter = @filters[index])
|
21
|
+
while (filter = (@filters || raise)[index])
|
20
22
|
left = filter.evaluate(left, context)
|
21
23
|
index += 1
|
22
24
|
end
|
@@ -81,7 +83,7 @@ module Liquid2
|
|
81
83
|
attr_reader :name, :args
|
82
84
|
|
83
85
|
# @param name [String]
|
84
|
-
# @param args [Array[Expression]]
|
86
|
+
# @param args [Array[Expression]?]
|
85
87
|
def initialize(token, name, args)
|
86
88
|
super(token)
|
87
89
|
@name = name
|
@@ -92,36 +94,18 @@ module Liquid2
|
|
92
94
|
filter, with_context = context.env.filters[@name]
|
93
95
|
raise LiquidFilterNotFoundError.new("unknown filter #{@name.inspect}", @token) unless filter
|
94
96
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
if keyword_args.empty?
|
99
|
-
filter.call(left, *positional_args) # steep:ignore
|
100
|
-
else
|
101
|
-
filter.call(left, *positional_args, **keyword_args) # steep:ignore
|
102
|
-
end
|
103
|
-
rescue ArgumentError, TypeError => e
|
104
|
-
raise LiquidArgumentError.new(e.message, @token)
|
105
|
-
end
|
106
|
-
|
107
|
-
def children = @args
|
97
|
+
return filter.call(left) if @args.nil? && !with_context # steep:ignore
|
98
|
+
return filter.call(left, context: context) if @args.nil? && with_context # steep:ignore
|
108
99
|
|
109
|
-
private
|
110
|
-
|
111
|
-
# @param context [RenderContext]
|
112
|
-
# @return [positional arguments, keyword arguments] An array with two elements.
|
113
|
-
# The first is an array of evaluates positional arguments. The second is a hash
|
114
|
-
# of keyword names to evaluated keyword values.
|
115
|
-
def evaluate_args(context)
|
116
100
|
positional_args = [] # @type var positional_args: Array[untyped]
|
117
101
|
keyword_args = {} # @type var keyword_args: Hash[Symbol, untyped]
|
118
102
|
|
119
103
|
index = 0
|
120
104
|
loop do
|
121
105
|
# `@args[index]` could be `false` or `nil`
|
122
|
-
break if index >= @args.length
|
106
|
+
break if index >= @args.length # steep:ignore
|
123
107
|
|
124
|
-
arg = @args[index]
|
108
|
+
arg = @args[index] # steep:ignore
|
125
109
|
index += 1
|
126
110
|
if arg.respond_to?(:sym)
|
127
111
|
keyword_args[arg.sym] = context.evaluate(arg.value)
|
@@ -130,7 +114,17 @@ module Liquid2
|
|
130
114
|
end
|
131
115
|
end
|
132
116
|
|
133
|
-
[
|
117
|
+
keyword_args[:context] = context if with_context
|
118
|
+
|
119
|
+
if keyword_args.empty?
|
120
|
+
filter.call(left, *positional_args) # steep:ignore
|
121
|
+
else
|
122
|
+
filter.call(left, *positional_args, **keyword_args) # steep:ignore
|
123
|
+
end
|
124
|
+
rescue ArgumentError, TypeError => e
|
125
|
+
raise LiquidArgumentError.new(e.message, @token)
|
134
126
|
end
|
127
|
+
|
128
|
+
def children = @args || []
|
135
129
|
end
|
136
130
|
end
|
@@ -33,11 +33,13 @@ module Liquid2
|
|
33
33
|
elsif obj.is_a?(String)
|
34
34
|
# TODO: optionally enable/disable string iteration
|
35
35
|
obj.each_char.to_a
|
36
|
-
elsif obj.respond_to?(:
|
37
|
-
#
|
38
|
-
|
39
|
-
|
40
|
-
|
36
|
+
elsif obj.respond_to?(:slice)
|
37
|
+
# Special lazy drop slicing
|
38
|
+
return obj.slice(context.evaluate(@offset),
|
39
|
+
context.evaluate(@limit),
|
40
|
+
@reversed) || EMPTY_ENUM
|
41
|
+
elsif obj.is_a?(Enumerable) # rubocop:disable Lint/DuplicateBranch
|
42
|
+
obj.to_a
|
41
43
|
else
|
42
44
|
EMPTY_ENUM
|
43
45
|
end
|
@@ -8,6 +8,8 @@ module Liquid2
|
|
8
8
|
class Path < Expression
|
9
9
|
attr_reader :segments, :head
|
10
10
|
|
11
|
+
RE_PROPERTY = /\A[\u0080-\uFFFFa-zA-Z_][\u0080-\uFFFFa-zA-Z0-9_-]*\Z/
|
12
|
+
|
11
13
|
# @param segments [Array[String | Integer | Path]]
|
12
14
|
def initialize(token, segments)
|
13
15
|
super(token)
|
@@ -19,8 +21,9 @@ module Liquid2
|
|
19
21
|
context.fetch(@head, @segments, node: self)
|
20
22
|
end
|
21
23
|
|
22
|
-
|
23
|
-
|
24
|
+
def to_s
|
25
|
+
segment_to_s(@head, head: true) + @segments.map { |segment| segment_to_s(segment) }.join
|
26
|
+
end
|
24
27
|
|
25
28
|
def children
|
26
29
|
if @head.is_a?(Path)
|
@@ -29,5 +32,19 @@ module Liquid2
|
|
29
32
|
@segments.filter { |segment| segment.is_a?(Path) }
|
30
33
|
end
|
31
34
|
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def segment_to_s(segment, head: false)
|
39
|
+
if segment.is_a?(String)
|
40
|
+
if segment.match?(RE_PROPERTY)
|
41
|
+
"#{head ? "" : "."}#{segment}"
|
42
|
+
else
|
43
|
+
"[#{segment.inspect}]"
|
44
|
+
end
|
45
|
+
else
|
46
|
+
"[#{segment}]"
|
47
|
+
end
|
48
|
+
end
|
32
49
|
end
|
33
50
|
end
|
data/lib/liquid2/filter.rb
CHANGED
@@ -63,8 +63,7 @@ module Liquid2
|
|
63
63
|
end
|
64
64
|
|
65
65
|
# Cast _obj_ to a date and time. Return `nil` if casting fails.
|
66
|
-
#
|
67
|
-
# TODO: This was copied from Shopify/liquid. Include their license and copyright.
|
66
|
+
# NOTE: This was copied from Shopify/liquid.
|
68
67
|
def self.to_date(obj)
|
69
68
|
return obj if obj.respond_to?(:strftime)
|
70
69
|
|
data/lib/liquid2/filters/sort.rb
CHANGED
@@ -3,6 +3,8 @@
|
|
3
3
|
module Liquid2
|
4
4
|
# Liquid filters and helper methods.
|
5
5
|
module Filters
|
6
|
+
INFINITY_ARRAY = [Float::INFINITY].freeze # : [Float]
|
7
|
+
|
6
8
|
def self.sort(left, key = nil, context:)
|
7
9
|
left = Liquid2::Filters.to_enumerable(left)
|
8
10
|
|
@@ -82,12 +84,11 @@ module Liquid2
|
|
82
84
|
end
|
83
85
|
|
84
86
|
def self.ints(obj)
|
85
|
-
|
86
|
-
when Integer, Float, BigDecimal
|
87
|
+
if obj.is_a?(Integer) || obj.is_a?(Float) || obj.is_a?(BigDecimal)
|
87
88
|
[obj]
|
88
89
|
else
|
89
|
-
numeric = obj.to_s.scan(
|
90
|
-
return
|
90
|
+
numeric = obj.to_s.scan(/(?<=\.)0+|-?\d+/)
|
91
|
+
return INFINITY_ARRAY if numeric.empty?
|
91
92
|
|
92
93
|
numeric.map(&:to_i)
|
93
94
|
end
|
data/lib/liquid2/loader.rb
CHANGED
@@ -31,6 +31,7 @@ module Liquid2
|
|
31
31
|
def load(env, name, globals: nil, context: nil, **kwargs)
|
32
32
|
data = get_source(env, name, context: context, **kwargs)
|
33
33
|
path = Pathname.new(data.name)
|
34
|
+
# FIXME: name and path
|
34
35
|
env.parse(data.source,
|
35
36
|
name: path.basename.to_s,
|
36
37
|
path: data.name,
|
@@ -1,3 +1,272 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require_relative "../../tag"
|
4
|
+
|
5
|
+
module Liquid2
|
6
|
+
# The _extends_ tag.
|
7
|
+
class ExtendsTag < Tag
|
8
|
+
attr_reader :template_name
|
9
|
+
|
10
|
+
# @param token [[Symbol, String?, Integer]]
|
11
|
+
# @param parser [Parser]
|
12
|
+
# @return [ExtendsTag]
|
13
|
+
def self.parse(token, parser)
|
14
|
+
name = parser.parse_name
|
15
|
+
parser.carry_whitespace_control
|
16
|
+
parser.eat(:token_tag_end)
|
17
|
+
new(token, name)
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(token, name)
|
21
|
+
super(token)
|
22
|
+
@template_name = name
|
23
|
+
@blank = false
|
24
|
+
end
|
25
|
+
|
26
|
+
def render(context, buffer)
|
27
|
+
base_template = stack_blocks(context, context.template)
|
28
|
+
context.extend({}, template: base_template) do
|
29
|
+
base_template&.render_with_context(context, buffer)
|
30
|
+
end
|
31
|
+
context.tag_namespace[:extends].clear
|
32
|
+
context.interrupts << :stop_render
|
33
|
+
end
|
34
|
+
|
35
|
+
def children(static_context, include_partials: true)
|
36
|
+
return [] unless include_partials
|
37
|
+
|
38
|
+
begin
|
39
|
+
parent = static_context.env.get_template(
|
40
|
+
@template_name,
|
41
|
+
context: static_context,
|
42
|
+
tag: "extends"
|
43
|
+
)
|
44
|
+
parent.ast
|
45
|
+
rescue LiquidTemplateNotFoundError => e
|
46
|
+
e.token = @token
|
47
|
+
e.template_name = static_context.template.full_name
|
48
|
+
raise e
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def partial_scope
|
53
|
+
Partial.new(@template_name, :inherited, [])
|
54
|
+
end
|
55
|
+
|
56
|
+
protected
|
57
|
+
|
58
|
+
# Visit all templates in the inheritance chain and build a stack for each `block` tag.
|
59
|
+
def stack_blocks(context, template)
|
60
|
+
# @type var stacks: Hash[String, Array[untyped]]
|
61
|
+
stacks = context.tag_namespace[:extends]
|
62
|
+
|
63
|
+
# Guard against recursive `extends`.
|
64
|
+
seen_extends = Set[] # : Set[String]
|
65
|
+
|
66
|
+
# @type var stack_blocks_: ^(Template) -> Template?
|
67
|
+
stack_blocks_ = lambda do |template_|
|
68
|
+
extends_nodes, block_nodes = inheritance_nodes(context, template_)
|
69
|
+
template_name = template_.path || template_.name
|
70
|
+
|
71
|
+
if extends_nodes.length > 1
|
72
|
+
raise TemplateInheritanceError.new("too many 'extends' tags", extends_nodes[1].token,
|
73
|
+
template_name: template_name)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Identify duplicate blocks.
|
77
|
+
seen_blocks = Set[] # : Set[String]
|
78
|
+
|
79
|
+
block_nodes.each do |block|
|
80
|
+
if seen_blocks.include?(block.block_name)
|
81
|
+
raise TemplateInheritanceError.new("duplicate block #{block.block_name.inspect}",
|
82
|
+
block.token, template_name: template_name)
|
83
|
+
end
|
84
|
+
|
85
|
+
seen_blocks.add(block.block_name)
|
86
|
+
|
87
|
+
stack = stacks[block.block_name]
|
88
|
+
required = !stack.empty? && !block.required ? false : block.required
|
89
|
+
# [block, required, template, parent]
|
90
|
+
# [BlockTag, bool, Template, untyped?]
|
91
|
+
stack << [block, required, template_, nil]
|
92
|
+
# Populate parent block.
|
93
|
+
stack[-2][-1] = stack.last if stack.length > 1
|
94
|
+
end
|
95
|
+
|
96
|
+
return nil if extends_nodes.empty? # steep:ignore
|
97
|
+
|
98
|
+
extends_node = extends_nodes.first
|
99
|
+
|
100
|
+
if seen_extends.include?(extends_node.template_name)
|
101
|
+
raise TemplateInheritanceError.new(
|
102
|
+
"circular extends #{extends_node.template_name.inspect}",
|
103
|
+
extends_node.token,
|
104
|
+
template_name: template_name
|
105
|
+
)
|
106
|
+
end
|
107
|
+
|
108
|
+
seen_extends.add(extends_node.template_name)
|
109
|
+
|
110
|
+
begin
|
111
|
+
context.env.get_template(extends_node.template_name, context: context, tag: "extends")
|
112
|
+
rescue LiquidTemplateNotFoundError => e
|
113
|
+
e.token = extends_node.token
|
114
|
+
e.template_name = template_.full_name
|
115
|
+
raise e
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# @type var next_template: Template?
|
120
|
+
base = next_template = stack_blocks_.call(template)
|
121
|
+
|
122
|
+
while next_template
|
123
|
+
next_template = stack_blocks_.call(next_template)
|
124
|
+
base = next_template if next_template
|
125
|
+
end
|
126
|
+
|
127
|
+
base
|
128
|
+
end
|
129
|
+
|
130
|
+
# Traverse the template's syntax tree looking for `{% extends %}` and `{% block %}`.
|
131
|
+
# @return [[Array[ExtendsTag], Array[BlockTag]]]
|
132
|
+
def inheritance_nodes(context, template)
|
133
|
+
extends_nodes = [] # : Array[ExtendsTag]
|
134
|
+
block_nodes = [] # : Array[BlockTag]
|
135
|
+
|
136
|
+
# @type var visit: ^(Node) -> void
|
137
|
+
visit = lambda do |node|
|
138
|
+
extends_nodes << node if node.is_a?(ExtendsTag)
|
139
|
+
block_nodes << node if node.is_a?(BlockTag)
|
140
|
+
|
141
|
+
node.children(context, include_partials: false).each do |child|
|
142
|
+
visit.call(child) if child.is_a?(Node)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
template.ast.each { |node| visit.call(node) if node.is_a?(Node) }
|
147
|
+
|
148
|
+
[extends_nodes, block_nodes]
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# The _block_ tag.
|
153
|
+
class BlockTag < Tag
|
154
|
+
attr_reader :block_name, :required, :block
|
155
|
+
|
156
|
+
END_BLOCK = Set["endblock"]
|
157
|
+
|
158
|
+
# @param token [[Symbol, String?, Integer]]
|
159
|
+
# @param parser [Parser]
|
160
|
+
# @return [BlockTag]
|
161
|
+
def self.parse(token, parser)
|
162
|
+
block_name = parser.parse_name
|
163
|
+
required = if parser.current_kind == :token_required
|
164
|
+
parser.next
|
165
|
+
true
|
166
|
+
else
|
167
|
+
false
|
168
|
+
end
|
169
|
+
|
170
|
+
parser.carry_whitespace_control
|
171
|
+
parser.eat(:token_tag_end)
|
172
|
+
block = parser.parse_block(END_BLOCK)
|
173
|
+
parser.eat_empty_tag("endblock")
|
174
|
+
new(token, block_name, block, required: required)
|
175
|
+
end
|
176
|
+
|
177
|
+
def initialize(token, name, block, required:)
|
178
|
+
super(token)
|
179
|
+
@block_name = name
|
180
|
+
@block = block
|
181
|
+
@required = required
|
182
|
+
@blank = false
|
183
|
+
end
|
184
|
+
|
185
|
+
def render(context, buffer)
|
186
|
+
# @type var stack: Array[[BlockTag, bool, Template, untyped?]]
|
187
|
+
stack = context.tag_namespace[:extends][@block_name]
|
188
|
+
|
189
|
+
if stack.empty?
|
190
|
+
# This base block is being rendered directly.
|
191
|
+
if @required
|
192
|
+
raise RequiredBlockError.new("block #{@block_name.inspect} is required",
|
193
|
+
@token)
|
194
|
+
end
|
195
|
+
|
196
|
+
context.extend({ "block" => BlockDrop.new(token, context, @block_name, nil) }) do
|
197
|
+
@block.render(context, buffer)
|
198
|
+
end
|
199
|
+
|
200
|
+
return
|
201
|
+
end
|
202
|
+
|
203
|
+
block_tag, required, template, parent = stack.first
|
204
|
+
|
205
|
+
if required
|
206
|
+
raise RequiredBlockError.new("block #{@block_name.inspect} is required", @token,
|
207
|
+
template_name: template.path || template.name)
|
208
|
+
end
|
209
|
+
|
210
|
+
namespace = { "block" => BlockDrop.new(token, context, @block_name, parent) }
|
211
|
+
|
212
|
+
block_context = context.copy(namespace,
|
213
|
+
carry_loop_iterations: true,
|
214
|
+
block_scope: true,
|
215
|
+
template: template)
|
216
|
+
|
217
|
+
begin
|
218
|
+
block_tag.block.render(block_context, buffer)
|
219
|
+
rescue LiquidError => e
|
220
|
+
e.template_name = template.path || template.name
|
221
|
+
e.source = template.source
|
222
|
+
raise
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
def children(_static_context, include_partials: true)
|
227
|
+
[@block]
|
228
|
+
end
|
229
|
+
|
230
|
+
def block_scope
|
231
|
+
[Identifier.new([:token_word, "block", @token.last])]
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
# A `block` object available within `{% block %}` tags.
|
236
|
+
class BlockDrop
|
237
|
+
attr_reader :token
|
238
|
+
|
239
|
+
# @param token [[Symbol, String?, Integer]]
|
240
|
+
# @param context [RenderContext]
|
241
|
+
# @param name [String]
|
242
|
+
# @param parent [[BlockTag, bool, Template, Block?]?]
|
243
|
+
def initialize(token, context, name, parent)
|
244
|
+
@token = token
|
245
|
+
@context = context
|
246
|
+
@name = name
|
247
|
+
@parent = parent
|
248
|
+
end
|
249
|
+
|
250
|
+
def to_s = "BlockDrop(#{@name})"
|
251
|
+
|
252
|
+
def key?(key)
|
253
|
+
key == "super" && @parent
|
254
|
+
end
|
255
|
+
|
256
|
+
def [](key)
|
257
|
+
return nil if key != "super" || @parent.nil?
|
258
|
+
|
259
|
+
parent = @parent || raise
|
260
|
+
buf = +""
|
261
|
+
namespace = { "block" => BlockDrop.new(parent.first.token,
|
262
|
+
@context,
|
263
|
+
parent[2].path || parent[2].name,
|
264
|
+
parent.last) }
|
265
|
+
@context.extend(namespace) do
|
266
|
+
parent.first.block.render(@context, buf)
|
267
|
+
end
|
268
|
+
|
269
|
+
buf.freeze
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|