liquid2 0.1.0 → 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 +15 -2
- metadata.gz.sig +0 -0
- data/.vscode/settings.json +0 -32
@@ -47,7 +47,7 @@ module Liquid2
|
|
47
47
|
# @param args [Array<KeywordArgument> | nil]
|
48
48
|
def initialize(token, name, repeat, var, as, args)
|
49
49
|
super(token)
|
50
|
-
@
|
50
|
+
@template_name = name
|
51
51
|
@repeat = repeat
|
52
52
|
@var = var
|
53
53
|
@as = as
|
@@ -56,7 +56,7 @@ module Liquid2
|
|
56
56
|
end
|
57
57
|
|
58
58
|
def render(context, buffer)
|
59
|
-
name = context.evaluate(@
|
59
|
+
name = context.evaluate(@template_name)
|
60
60
|
template = context.env.get_template(name.to_s, context: context, tag: :include)
|
61
61
|
namespace = @args.to_h { |arg| [arg.name, context.evaluate(arg.value)] }
|
62
62
|
|
@@ -90,7 +90,7 @@ module Liquid2
|
|
90
90
|
def children(static_context, include_partials: true)
|
91
91
|
return [] unless include_partials
|
92
92
|
|
93
|
-
name = static_context.evaluate(@
|
93
|
+
name = static_context.evaluate(@template_name)
|
94
94
|
template = static_context.env.get_template(name.to_s, context: static_context, tag: :include)
|
95
95
|
template.ast
|
96
96
|
rescue LiquidTemplateNotFoundError => e
|
@@ -100,7 +100,7 @@ module Liquid2
|
|
100
100
|
end
|
101
101
|
|
102
102
|
def expressions
|
103
|
-
exprs = [@
|
103
|
+
exprs = [@template_name]
|
104
104
|
exprs << @var if @var
|
105
105
|
exprs.concat(@args.map(&:value))
|
106
106
|
exprs
|
@@ -112,12 +112,12 @@ module Liquid2
|
|
112
112
|
if @var
|
113
113
|
if @as
|
114
114
|
scope << @as # steep:ignore
|
115
|
-
elsif @
|
116
|
-
scope << Identifier.new([:token_word, @
|
115
|
+
elsif @template_name.is_a?(String)
|
116
|
+
scope << Identifier.new([:token_word, @template_name.split(".").first, @token.last])
|
117
117
|
end
|
118
118
|
end
|
119
119
|
|
120
|
-
Partial.new(@
|
120
|
+
Partial.new(@template_name, :shared, scope)
|
121
121
|
end
|
122
122
|
end
|
123
123
|
end
|
@@ -1,3 +1,147 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require_relative "../../tag"
|
4
|
+
|
5
|
+
module Liquid2
|
6
|
+
# The _macro_ tag.
|
7
|
+
class MacroTag < Tag
|
8
|
+
attr_reader :macro_name, :params, :block
|
9
|
+
|
10
|
+
END_BLOCK = Set["endmacro"]
|
11
|
+
|
12
|
+
# @param token [[Symbol, String?, Integer]]
|
13
|
+
# @param parser [Parser]
|
14
|
+
# @return [MacroTag]
|
15
|
+
def self.parse(token, parser)
|
16
|
+
name = parser.parse_name
|
17
|
+
parser.next if parser.current_kind == :token_comma
|
18
|
+
params = parser.parse_parameters
|
19
|
+
parser.next if parser.current_kind == :token_comma
|
20
|
+
parser.carry_whitespace_control
|
21
|
+
parser.eat(:token_tag_end)
|
22
|
+
block = parser.parse_block(END_BLOCK)
|
23
|
+
parser.eat_empty_tag("endmacro")
|
24
|
+
new(token, name, params, block)
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize(token, name, params, block)
|
28
|
+
super(token)
|
29
|
+
@macro_name = name
|
30
|
+
@params = params
|
31
|
+
@block = block
|
32
|
+
@blank = true
|
33
|
+
end
|
34
|
+
|
35
|
+
def render(context, _buffer)
|
36
|
+
# Macro tags don't render or evaluate anything, just store their parameter list
|
37
|
+
# and block on the render context so it can be called later by a `call` tag.
|
38
|
+
context.tag_namespace[:macros][@macro_name] = [@params, @block]
|
39
|
+
end
|
40
|
+
|
41
|
+
def children(_static_context, include_partials: true) = [@block]
|
42
|
+
def expressions = @params.values.filter_map(&:value)
|
43
|
+
|
44
|
+
def block_scope
|
45
|
+
[
|
46
|
+
Identifier.new([:token_word, "args", @token.last]),
|
47
|
+
Identifier.new([:token_word, "kwargs", @token.last]),
|
48
|
+
*@params.values.map { |param| Identifier.new([:token_word, param.name, param.token.last]) }
|
49
|
+
]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# The _call_ tag.
|
54
|
+
class CallTag < Tag
|
55
|
+
attr_reader :macro_name, :args, :kwargs
|
56
|
+
|
57
|
+
DISABLED_TAGS = Set["include", "block"]
|
58
|
+
|
59
|
+
# @param token [[Symbol, String?, Integer]]
|
60
|
+
# @param parser [Parser]
|
61
|
+
# @return [CallTag]
|
62
|
+
def self.parse(token, parser)
|
63
|
+
name = parser.parse_name
|
64
|
+
parser.next if parser.current_kind == :token_comma
|
65
|
+
args, kwargs = parser.parse_arguments
|
66
|
+
parser.carry_whitespace_control
|
67
|
+
parser.eat(:token_tag_end)
|
68
|
+
new(token, name, args, kwargs)
|
69
|
+
end
|
70
|
+
|
71
|
+
def initialize(token, name, args, kwargs)
|
72
|
+
super(token)
|
73
|
+
@macro_name = name
|
74
|
+
@args = args
|
75
|
+
@kwargs = kwargs
|
76
|
+
@blank = false
|
77
|
+
end
|
78
|
+
|
79
|
+
def render(context, buffer)
|
80
|
+
# @type var params: Hash[String, Parameter]?
|
81
|
+
# @type var block: Block
|
82
|
+
params, block = context.tag_namespace[:macros][@macro_name]
|
83
|
+
|
84
|
+
unless params
|
85
|
+
buffer << Liquid2.to_output_string(context.env.undefined(@macro_name, node: self))
|
86
|
+
return
|
87
|
+
end
|
88
|
+
|
89
|
+
# Parameter names mapped to default values. :undefined is used if there is no default.
|
90
|
+
args = params.values.to_h { |p| [p.name, p.value] }
|
91
|
+
excess_args = [] # : Array[untyped]
|
92
|
+
excess_kwargs = {} # : Hash[String, untyped]
|
93
|
+
|
94
|
+
# Update args with positional arguments.
|
95
|
+
# Keyword arguments are pushed to the end if they appear before positional arguments.
|
96
|
+
names = args.keys
|
97
|
+
length = @args.length
|
98
|
+
index = 0
|
99
|
+
while index < length
|
100
|
+
name = names[index]
|
101
|
+
expr = @args[index]
|
102
|
+
if name.nil?
|
103
|
+
excess_args << expr
|
104
|
+
else
|
105
|
+
args[name] = expr
|
106
|
+
end
|
107
|
+
index += 1
|
108
|
+
end
|
109
|
+
|
110
|
+
# Update args with keyword arguments.
|
111
|
+
@kwargs.each do |arg|
|
112
|
+
if params.include?(arg.name)
|
113
|
+
# This has the potential to override a positional argument.
|
114
|
+
args[arg.name] = arg.value
|
115
|
+
else
|
116
|
+
excess_kwargs[arg.name] = arg.value
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# @type var namespace: Hash[String, untyped]
|
121
|
+
namespace = {
|
122
|
+
"args" => excess_args.map { |arg| context.evaluate(arg) },
|
123
|
+
"kwargs" => excess_kwargs.transform_values! { |val| context.evaluate(val) }
|
124
|
+
}
|
125
|
+
|
126
|
+
args.each do |k, v|
|
127
|
+
namespace[k] = if v == :undefined
|
128
|
+
context.env.undefined(k, node: params[k])
|
129
|
+
else
|
130
|
+
context.evaluate(v)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
macro_context = context.copy(
|
135
|
+
namespace,
|
136
|
+
disabled_tags: DISABLED_TAGS,
|
137
|
+
carry_loop_iterations: true
|
138
|
+
)
|
139
|
+
|
140
|
+
block.render(macro_context, buffer)
|
141
|
+
end
|
142
|
+
|
143
|
+
def expressions
|
144
|
+
[*@args, *@kwargs.map(&:value)]
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
@@ -12,8 +12,6 @@ module Liquid2
|
|
12
12
|
# @return [RenderTag]
|
13
13
|
def self.parse(token, parser)
|
14
14
|
name = parser.parse_string
|
15
|
-
raise LiquidTypeError, "expected a string literal" unless name.is_a?(String)
|
16
|
-
|
17
15
|
repeat = false
|
18
16
|
var = nil # : Expression?
|
19
17
|
as = nil # : Identifier?
|
@@ -51,7 +49,7 @@ module Liquid2
|
|
51
49
|
# @param args [Array<KeywordArgument> | nil]
|
52
50
|
def initialize(token, name, repeat, var, as, args)
|
53
51
|
super(token)
|
54
|
-
@
|
52
|
+
@template_name = name
|
55
53
|
@repeat = repeat
|
56
54
|
@var = var
|
57
55
|
@as = as&.name
|
@@ -60,7 +58,7 @@ module Liquid2
|
|
60
58
|
end
|
61
59
|
|
62
60
|
def render(context, buffer)
|
63
|
-
template = context.env.get_template(@
|
61
|
+
template = context.env.get_template(@template_name, context: context, tag: :render)
|
64
62
|
namespace = @args.to_h { |arg| [arg.name, context.evaluate(arg.value)] }
|
65
63
|
|
66
64
|
ctx = context.copy(namespace,
|
@@ -96,7 +94,7 @@ module Liquid2
|
|
96
94
|
template.render_with_context(ctx, buffer, partial: true, block_scope: true)
|
97
95
|
end
|
98
96
|
rescue LiquidTemplateNotFoundError => e
|
99
|
-
e.token = @
|
97
|
+
e.token = @template_name
|
100
98
|
e.template_name = context.template.full_name
|
101
99
|
raise e
|
102
100
|
end
|
@@ -104,7 +102,7 @@ module Liquid2
|
|
104
102
|
def children(static_context, include_partials: true)
|
105
103
|
return [] unless include_partials
|
106
104
|
|
107
|
-
name = static_context.evaluate(@
|
105
|
+
name = static_context.evaluate(@template_name)
|
108
106
|
template = static_context.env.get_template(name.to_s, context: static_context, tag: :include)
|
109
107
|
template.ast
|
110
108
|
rescue LiquidTemplateNotFoundError => e
|
@@ -114,7 +112,7 @@ module Liquid2
|
|
114
112
|
end
|
115
113
|
|
116
114
|
def expressions
|
117
|
-
exprs = [@
|
115
|
+
exprs = [@template_name]
|
118
116
|
exprs << @var if @var
|
119
117
|
exprs.concat(@args.map(&:value))
|
120
118
|
exprs
|
@@ -126,12 +124,12 @@ module Liquid2
|
|
126
124
|
if @var
|
127
125
|
if @as
|
128
126
|
scope << @as # steep:ignore
|
129
|
-
elsif @
|
130
|
-
scope << Identifier.new([:token_word, @
|
127
|
+
elsif @template_name.is_a?(String)
|
128
|
+
scope << Identifier.new([:token_word, @template_name.split(".").first, @token.last])
|
131
129
|
end
|
132
130
|
end
|
133
131
|
|
134
|
-
Partial.new(@
|
132
|
+
Partial.new(@template_name, :isolated, scope)
|
135
133
|
end
|
136
134
|
end
|
137
135
|
end
|
@@ -1,3 +1,44 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
require_relative "../../tag"
|
4
|
+
|
5
|
+
module Liquid2
|
6
|
+
# The _with_ tag.
|
7
|
+
class WithTag < Tag
|
8
|
+
END_BLOCK = Set["endwith"]
|
9
|
+
|
10
|
+
# @param token [[Symbol, String?, Integer]]
|
11
|
+
# @param parser [Parser]
|
12
|
+
# @return [WithTag]
|
13
|
+
def self.parse(token, parser)
|
14
|
+
parser.next if parser.current_kind == :token_comma
|
15
|
+
args = parser.parse_keyword_arguments
|
16
|
+
parser.next if parser.current_kind == :token_comma
|
17
|
+
parser.carry_whitespace_control
|
18
|
+
parser.eat(:token_tag_end)
|
19
|
+
block = parser.parse_block(END_BLOCK)
|
20
|
+
parser.eat_empty_tag("endwith")
|
21
|
+
new(token, args, block)
|
22
|
+
end
|
23
|
+
|
24
|
+
# @param token [[Symbol, String?, Integer]]
|
25
|
+
# @param args [Array[KeywordArgument]]
|
26
|
+
# @param block [Block]
|
27
|
+
def initialize(token, args, block)
|
28
|
+
super(token)
|
29
|
+
@args = args
|
30
|
+
@block = block
|
31
|
+
@blank = block.blank
|
32
|
+
end
|
33
|
+
|
34
|
+
def render(context, buffer)
|
35
|
+
namespace = @args.to_h { |arg| [arg.name, context.evaluate(arg.value)] }
|
36
|
+
context.extend(namespace) do
|
37
|
+
@block.render(context, buffer)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def children(_static_context, include_partials: true) = [@block]
|
42
|
+
def block_scope = @args.map { |arg| Identifier.new(arg.token) }
|
43
|
+
end
|
44
|
+
end
|
data/lib/liquid2/parser.rb
CHANGED
@@ -233,7 +233,6 @@ module Liquid2
|
|
233
233
|
left = parse_primary
|
234
234
|
left = parse_array_literal(left) if current_kind == :token_comma
|
235
235
|
filters = parse_filters if current_kind == :token_pipe
|
236
|
-
filters ||= [] # : Array[Filter]
|
237
236
|
expr = FilteredExpression.new(token, left, filters)
|
238
237
|
|
239
238
|
if current_kind == :token_if
|
@@ -396,7 +395,7 @@ module Liquid2
|
|
396
395
|
# @raises [LiquidTypeError].
|
397
396
|
def parse_string
|
398
397
|
node = parse_primary
|
399
|
-
raise LiquidTypeError, "expected a string" unless node.is_a?(String)
|
398
|
+
raise LiquidTypeError, "expected a string literal" unless node.is_a?(String)
|
400
399
|
|
401
400
|
node
|
402
401
|
end
|
@@ -411,6 +410,23 @@ module Liquid2
|
|
411
410
|
Identifier.new(token)
|
412
411
|
end
|
413
412
|
|
413
|
+
# Parse a string literals or unquoted word.
|
414
|
+
def parse_name
|
415
|
+
case current_kind
|
416
|
+
when :token_word
|
417
|
+
parse_identifier.name
|
418
|
+
when :token_single_quote_string, :token_double_quote_string
|
419
|
+
node = parse_string_literal
|
420
|
+
unless node.is_a?(String)
|
421
|
+
raise LiquidSyntaxError.new("names can't be template strings", node.token)
|
422
|
+
end
|
423
|
+
|
424
|
+
node
|
425
|
+
else
|
426
|
+
raise LiquidSyntaxError.new("expected a string literal or unquoted word", current)
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
414
430
|
# Parse comma separated expression.
|
415
431
|
# Leading commas should be consumed by the caller.
|
416
432
|
# @return [Array<Expression>]
|
@@ -427,7 +443,7 @@ module Liquid2
|
|
427
443
|
args
|
428
444
|
end
|
429
445
|
|
430
|
-
# Parse comma name/value pairs.
|
446
|
+
# Parse comma separated name/value pairs.
|
431
447
|
# Leading commas should be consumed by the caller, if allowed.
|
432
448
|
# @return [Array<KeywordArgument>]
|
433
449
|
def parse_keyword_arguments
|
@@ -438,8 +454,7 @@ module Liquid2
|
|
438
454
|
|
439
455
|
word = eat(:token_word)
|
440
456
|
eat_one_of(:token_assign, :token_colon)
|
441
|
-
|
442
|
-
args << KeywordArgument.new(word, word[1] || raise, val)
|
457
|
+
args << KeywordArgument.new(word, word[1] || raise, parse_primary)
|
443
458
|
|
444
459
|
break unless current_kind == :token_comma
|
445
460
|
|
@@ -449,6 +464,68 @@ module Liquid2
|
|
449
464
|
args
|
450
465
|
end
|
451
466
|
|
467
|
+
# Parse comma separated parameter names with optional default expressions.
|
468
|
+
# Leading commas should be consumed by the caller, if allowed.
|
469
|
+
# @return [Hash[String, Parameter]]
|
470
|
+
def parse_parameters
|
471
|
+
args = {} # : Hash[String, Parameter]
|
472
|
+
|
473
|
+
loop do
|
474
|
+
break if TERMINATE_EXPRESSION.member?(current_kind)
|
475
|
+
|
476
|
+
word = eat(:token_word)
|
477
|
+
name = word[1] || raise
|
478
|
+
|
479
|
+
case current_kind
|
480
|
+
when :token_assign, :token_colon
|
481
|
+
@pos += 1
|
482
|
+
args[name] = Parameter.new(word, name, parse_primary)
|
483
|
+
@pos += 1 if current_kind == :token_comma
|
484
|
+
when :comma
|
485
|
+
args[name] = Parameter.new(word, name, :undefined)
|
486
|
+
@pos += 1
|
487
|
+
else
|
488
|
+
args[name] = Parameter.new(word, name, :undefined)
|
489
|
+
break
|
490
|
+
end
|
491
|
+
end
|
492
|
+
|
493
|
+
args
|
494
|
+
end
|
495
|
+
|
496
|
+
# Parse mixed positional and keyword arguments.
|
497
|
+
# Leading commas should be consumed by the caller, if allowed.
|
498
|
+
# @return [[Array[untyped], Array[KeywordArgument]]]
|
499
|
+
def parse_arguments
|
500
|
+
args = [] # : Array[untyped]
|
501
|
+
kwargs = [] # : Array[KeywordArgument]
|
502
|
+
|
503
|
+
loop do
|
504
|
+
break if TERMINATE_EXPRESSION.member?(current_kind)
|
505
|
+
|
506
|
+
case current_kind
|
507
|
+
when :token_word
|
508
|
+
if KEYWORD_ARGUMENT_DELIMITERS.include?(peek_kind)
|
509
|
+
token = self.next
|
510
|
+
@pos += 1 # = or :
|
511
|
+
kwargs << KeywordArgument.new(token, token[1] || raise, parse_primary)
|
512
|
+
else
|
513
|
+
# A positional argument
|
514
|
+
args << parse_primary
|
515
|
+
end
|
516
|
+
else
|
517
|
+
# A positional argument
|
518
|
+
args << parse_primary
|
519
|
+
end
|
520
|
+
|
521
|
+
break unless current_kind == :token_comma
|
522
|
+
|
523
|
+
@pos += 1
|
524
|
+
end
|
525
|
+
|
526
|
+
[args, kwargs]
|
527
|
+
end
|
528
|
+
|
452
529
|
protected
|
453
530
|
|
454
531
|
class Precedence
|
@@ -783,7 +860,7 @@ module Liquid2
|
|
783
860
|
|
784
861
|
unless current_kind == :token_colon || !TERMINATE_FILTER.member?(current_kind)
|
785
862
|
# No arguments
|
786
|
-
return Filter.new(name, name[1] || raise,
|
863
|
+
return Filter.new(name, name[1] || raise, nil)
|
787
864
|
end
|
788
865
|
|
789
866
|
@pos += 1 # token_colon
|
@@ -804,7 +881,7 @@ module Liquid2
|
|
804
881
|
args << parse_arrow_function
|
805
882
|
else
|
806
883
|
# A positional argument that is a path.
|
807
|
-
args <<
|
884
|
+
args << parse_primary
|
808
885
|
end
|
809
886
|
when :token_lparen
|
810
887
|
# A grouped expression or range or arrow function
|
data/lib/liquid2/scanner.rb
CHANGED
@@ -12,8 +12,6 @@ module Liquid2
|
|
12
12
|
class Scanner
|
13
13
|
attr_reader :tokens
|
14
14
|
|
15
|
-
RE_MARKUP_START = /\{[\{%#]/
|
16
|
-
RE_WHITESPACE = /[ \n\r\t]+/
|
17
15
|
RE_LINE_SPACE = /[ \t]+/
|
18
16
|
RE_WORD = /[\u0080-\uFFFFa-zA-Z_][\u0080-\uFFFFa-zA-Z0-9_-]*/
|
19
17
|
RE_INT = /-?\d+(?:[eE]\+?\d+)?/
|
@@ -94,31 +92,19 @@ module Liquid2
|
|
94
92
|
# @param value [String?]
|
95
93
|
# @return void
|
96
94
|
def emit(kind, value)
|
97
|
-
# TODO: For debugging. Comment this out when benchmarking.
|
98
|
-
raise "empty span (#{kind}, #{value})" if @scanner.pos == @start
|
99
|
-
|
100
95
|
@tokens << [kind, value, @start]
|
101
96
|
@start = @scanner.pos
|
102
97
|
end
|
103
98
|
|
104
99
|
def skip_trivia
|
105
|
-
|
106
|
-
raise "must emit before skipping trivia" if @scanner.pos != @start
|
107
|
-
|
108
|
-
@start = @scanner.pos if @scanner.skip(RE_WHITESPACE)
|
100
|
+
@start = @scanner.pos if @scanner.skip(/[ \n\r\t]+/)
|
109
101
|
end
|
110
102
|
|
111
103
|
def skip_line_trivia
|
112
|
-
# TODO: For debugging. Comment this out when benchmarking.
|
113
|
-
raise "must emit before skipping line trivia" if @scanner.pos != @start
|
114
|
-
|
115
104
|
@start = @scanner.pos if @scanner.skip(RE_LINE_SPACE)
|
116
105
|
end
|
117
106
|
|
118
107
|
def accept_whitespace_control
|
119
|
-
# TODO: For debugging. Comment this out when benchmarking.
|
120
|
-
raise "must emit before accepting whitespace control" if @scanner.pos != @start
|
121
|
-
|
122
108
|
ch = @scanner.peek(1)
|
123
109
|
|
124
110
|
case ch
|
@@ -133,7 +119,7 @@ module Liquid2
|
|
133
119
|
end
|
134
120
|
|
135
121
|
def lex_markup
|
136
|
-
case @scanner.scan(
|
122
|
+
case @scanner.scan(/\{[\{%#]/)
|
137
123
|
when "{#"
|
138
124
|
:lex_comment
|
139
125
|
when "{{"
|
@@ -197,37 +183,30 @@ module Liquid2
|
|
197
183
|
end
|
198
184
|
|
199
185
|
def lex_expression
|
200
|
-
# TODO: For debugging. Comment this out when benchmarking.
|
201
|
-
raise "must emit before accepting an expression token" if @scanner.pos != @start
|
202
|
-
|
203
186
|
loop do
|
204
187
|
skip_trivia
|
205
|
-
|
206
|
-
|
207
|
-
when "'"
|
188
|
+
if (value = @scanner.scan(RE_FLOAT))
|
189
|
+
@tokens << [:token_float, value, @start]
|
208
190
|
@start = @scanner.pos
|
209
|
-
|
210
|
-
|
191
|
+
elsif (value = @scanner.scan(RE_INT))
|
192
|
+
@tokens << [:token_int, value, @start]
|
193
|
+
@start = @scanner.pos
|
194
|
+
elsif (value = @scanner.scan(RE_PUNCTUATION))
|
195
|
+
@tokens << [TOKEN_MAP[value] || :token_unknown, value, @start]
|
196
|
+
@start = @scanner.pos
|
197
|
+
elsif (value = @scanner.scan(RE_WORD))
|
198
|
+
@tokens << [TOKEN_MAP[value] || :token_word, value, @start]
|
211
199
|
@start = @scanner.pos
|
212
|
-
scan_string("\"", :token_double_quote_string, RE_DOUBLE_QUOTE_STRING_SPECIAL)
|
213
|
-
when nil
|
214
|
-
# End of scanner. Unclosed expression or string literal.
|
215
|
-
break
|
216
200
|
else
|
217
|
-
@scanner.
|
218
|
-
|
219
|
-
@tokens << [:token_float, value, @start]
|
201
|
+
case @scanner.get_byte
|
202
|
+
when "'"
|
220
203
|
@start = @scanner.pos
|
221
|
-
|
222
|
-
|
223
|
-
@start = @scanner.pos
|
224
|
-
elsif (value = @scanner.scan(RE_PUNCTUATION))
|
225
|
-
@tokens << [TOKEN_MAP[value] || :token_unknown, value, @start]
|
226
|
-
@start = @scanner.pos
|
227
|
-
elsif (value = @scanner.scan(RE_WORD))
|
228
|
-
@tokens << [TOKEN_MAP[value] || :token_word, value, @start]
|
204
|
+
scan_string("'", :token_single_quote_string, RE_SINGLE_QUOTE_STRING_SPECIAL)
|
205
|
+
when "\""
|
229
206
|
@start = @scanner.pos
|
207
|
+
scan_string("\"", :token_double_quote_string, RE_DOUBLE_QUOTE_STRING_SPECIAL)
|
230
208
|
else
|
209
|
+
@scanner.pos -= 1
|
231
210
|
break
|
232
211
|
end
|
233
212
|
end
|
@@ -413,9 +392,6 @@ module Liquid2
|
|
413
392
|
end
|
414
393
|
|
415
394
|
def lex_line_statements
|
416
|
-
# TODO: For debugging. Comment this out when benchmarking.
|
417
|
-
raise "must emit before accepting an expression token" if @scanner.pos != @start
|
418
|
-
|
419
395
|
skip_trivia # Leading newlines are OK
|
420
396
|
|
421
397
|
if (tag_name = @scanner.scan(/(?:[a-z][a-z_0-9]*|#)/))
|
@@ -231,7 +231,7 @@ module Liquid2
|
|
231
231
|
def self.extract_filters(expression, template_name)
|
232
232
|
filters = [] # : Array[[String, Span]]
|
233
233
|
|
234
|
-
if expression.is_a?(Liquid2::FilteredExpression)
|
234
|
+
if expression.is_a?(Liquid2::FilteredExpression) && !expression.filters.nil?
|
235
235
|
expression.filters.each do |filter|
|
236
236
|
filters << [filter.name, Span.new(template_name, filter.token.last)]
|
237
237
|
end
|
data/lib/liquid2/template.rb
CHANGED
@@ -43,7 +43,6 @@ module Liquid2
|
|
43
43
|
end
|
44
44
|
|
45
45
|
def render_with_context(context, buffer, partial: false, block_scope: false, namespace: nil)
|
46
|
-
# TODO: don't extend if namespace is nil
|
47
46
|
context.extend(namespace || {}) do
|
48
47
|
index = 0
|
49
48
|
while (node = @ast[index])
|
@@ -59,6 +58,8 @@ module Liquid2
|
|
59
58
|
|
60
59
|
next unless (interrupt = context.interrupts.pop)
|
61
60
|
|
61
|
+
break if interrupt == :stop_render
|
62
|
+
|
62
63
|
if !partial || block_scope
|
63
64
|
raise LiquidSyntaxError.new("unexpected #{interrupt}",
|
64
65
|
node.token) # steep:ignore
|
@@ -69,15 +70,14 @@ module Liquid2
|
|
69
70
|
end
|
70
71
|
end
|
71
72
|
rescue LiquidError => e
|
72
|
-
e.source =
|
73
|
-
e.template_name = @name unless @name.empty?
|
73
|
+
e.source = context.template.source unless e.source
|
74
|
+
e.template_name = @name unless e.template_name || @name.empty?
|
74
75
|
raise
|
75
76
|
end
|
76
77
|
|
77
78
|
# Merge template globals with another namespace.
|
78
79
|
def make_globals(namespace)
|
79
|
-
|
80
|
-
@globals.merge(@overlay || {}, namespace || {})
|
80
|
+
@globals.merge(@overlay, namespace || {})
|
81
81
|
end
|
82
82
|
|
83
83
|
# Return `false` if this template is stale and needs to be loaded again.
|
@@ -123,6 +123,53 @@ module Liquid2
|
|
123
123
|
nodes
|
124
124
|
end
|
125
125
|
|
126
|
+
# Return an array of `{% doc %}` nodes found in this template.
|
127
|
+
#
|
128
|
+
# Each instance of `Liquid2::DocTag` has a `token` and `text` attribute. Use
|
129
|
+
# `Template#docs.map(&:text)` to get an array of doc strings.
|
130
|
+
#
|
131
|
+
# @return [Array[DocTag]]
|
132
|
+
def docs
|
133
|
+
context = RenderContext.new(self)
|
134
|
+
nodes = [] # : Array[DocTag]
|
135
|
+
|
136
|
+
# @type var visit: ^(Node) -> void
|
137
|
+
visit = lambda do |node|
|
138
|
+
nodes << node if node.is_a?(DocTag)
|
139
|
+
|
140
|
+
node.children(context, include_partials: false).each do |child|
|
141
|
+
visit.call(child) if child.is_a?(Node)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
@ast.each { |node| visit.call(node) if node.is_a?(Node) }
|
146
|
+
|
147
|
+
nodes
|
148
|
+
end
|
149
|
+
|
150
|
+
# Return arrays of `{% macro %}` and `{% call %}` tags found in this template.
|
151
|
+
# @param include_partials [bool]
|
152
|
+
# @return [Array[MacroTag], Array[CallTag]]
|
153
|
+
def macros(include_partials: false)
|
154
|
+
context = RenderContext.new(self)
|
155
|
+
macro_nodes = [] # : Array[MacroTag]
|
156
|
+
call_nodes = [] # : Array[CallTag]
|
157
|
+
|
158
|
+
# @type var visit: ^(Node) -> void
|
159
|
+
visit = lambda do |node|
|
160
|
+
macro_nodes << node if node.is_a?(MacroTag)
|
161
|
+
call_nodes << node if node.is_a?(CallTag)
|
162
|
+
|
163
|
+
node.children(context, include_partials: include_partials).each do |child|
|
164
|
+
visit.call(child) if child.is_a?(Node)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
@ast.each { |node| visit.call(node) if node.is_a?(Node) }
|
169
|
+
|
170
|
+
[macro_nodes, call_nodes]
|
171
|
+
end
|
172
|
+
|
126
173
|
# Return an array of variables used in this template, without path segments.
|
127
174
|
# @param include_partials [bool]
|
128
175
|
# @return [Array[String]]
|