liquid 4.0.0 → 5.10.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 +5 -5
- data/History.md +235 -2
- data/README.md +58 -8
- data/lib/liquid/block.rb +51 -20
- data/lib/liquid/block_body.rb +216 -82
- data/lib/liquid/condition.rb +83 -32
- data/lib/liquid/const.rb +8 -0
- data/lib/liquid/context.rb +130 -59
- data/lib/liquid/deprecations.rb +22 -0
- data/lib/liquid/document.rb +47 -9
- data/lib/liquid/drop.rb +8 -2
- data/lib/liquid/environment.rb +159 -0
- data/lib/liquid/errors.rb +23 -20
- data/lib/liquid/expression.rb +114 -31
- data/lib/liquid/extensions.rb +8 -0
- data/lib/liquid/file_system.rb +6 -4
- data/lib/liquid/forloop_drop.rb +51 -4
- data/lib/liquid/i18n.rb +5 -3
- data/lib/liquid/interrupts.rb +3 -1
- data/lib/liquid/lexer.rb +165 -39
- data/lib/liquid/locales/en.yml +16 -6
- data/lib/liquid/parse_context.rb +62 -7
- data/lib/liquid/parse_tree_visitor.rb +42 -0
- data/lib/liquid/parser.rb +31 -19
- data/lib/liquid/parser_switching.rb +42 -3
- data/lib/liquid/partial_cache.rb +33 -0
- data/lib/liquid/profiler/hooks.rb +26 -14
- data/lib/liquid/profiler.rb +67 -86
- data/lib/liquid/range_lookup.rb +26 -6
- data/lib/liquid/registers.rb +51 -0
- data/lib/liquid/resource_limits.rb +47 -8
- data/lib/liquid/snippet_drop.rb +22 -0
- data/lib/liquid/standardfilters.rb +813 -137
- data/lib/liquid/strainer_template.rb +62 -0
- data/lib/liquid/tablerowloop_drop.rb +64 -5
- data/lib/liquid/tag/disableable.rb +22 -0
- data/lib/liquid/tag/disabler.rb +13 -0
- data/lib/liquid/tag.rb +42 -6
- data/lib/liquid/tags/assign.rb +46 -18
- data/lib/liquid/tags/break.rb +15 -4
- data/lib/liquid/tags/capture.rb +26 -18
- data/lib/liquid/tags/case.rb +108 -32
- data/lib/liquid/tags/comment.rb +76 -4
- data/lib/liquid/tags/continue.rb +15 -13
- data/lib/liquid/tags/cycle.rb +117 -34
- data/lib/liquid/tags/decrement.rb +30 -23
- data/lib/liquid/tags/doc.rb +81 -0
- data/lib/liquid/tags/echo.rb +39 -0
- data/lib/liquid/tags/for.rb +109 -96
- data/lib/liquid/tags/if.rb +72 -41
- data/lib/liquid/tags/ifchanged.rb +10 -11
- data/lib/liquid/tags/include.rb +89 -63
- data/lib/liquid/tags/increment.rb +31 -20
- data/lib/liquid/tags/inline_comment.rb +28 -0
- data/lib/liquid/tags/raw.rb +25 -13
- data/lib/liquid/tags/render.rb +151 -0
- data/lib/liquid/tags/snippet.rb +45 -0
- data/lib/liquid/tags/table_row.rb +104 -21
- data/lib/liquid/tags/unless.rb +37 -20
- data/lib/liquid/tags.rb +51 -0
- data/lib/liquid/template.rb +90 -106
- data/lib/liquid/template_factory.rb +9 -0
- data/lib/liquid/tokenizer.rb +143 -13
- data/lib/liquid/usage.rb +8 -0
- data/lib/liquid/utils.rb +114 -5
- data/lib/liquid/variable.rb +119 -45
- data/lib/liquid/variable_lookup.rb +35 -13
- data/lib/liquid/version.rb +3 -1
- data/lib/liquid.rb +31 -18
- metadata +56 -107
- data/lib/liquid/strainer.rb +0 -66
- data/test/fixtures/en_locale.yml +0 -9
- data/test/integration/assign_test.rb +0 -48
- data/test/integration/blank_test.rb +0 -106
- data/test/integration/capture_test.rb +0 -50
- data/test/integration/context_test.rb +0 -32
- data/test/integration/document_test.rb +0 -19
- data/test/integration/drop_test.rb +0 -273
- data/test/integration/error_handling_test.rb +0 -260
- data/test/integration/filter_test.rb +0 -178
- data/test/integration/hash_ordering_test.rb +0 -23
- data/test/integration/output_test.rb +0 -123
- data/test/integration/parsing_quirks_test.rb +0 -118
- data/test/integration/render_profiling_test.rb +0 -154
- data/test/integration/security_test.rb +0 -66
- data/test/integration/standard_filter_test.rb +0 -535
- data/test/integration/tags/break_tag_test.rb +0 -15
- data/test/integration/tags/continue_tag_test.rb +0 -15
- data/test/integration/tags/for_tag_test.rb +0 -410
- data/test/integration/tags/if_else_tag_test.rb +0 -188
- data/test/integration/tags/include_tag_test.rb +0 -238
- data/test/integration/tags/increment_tag_test.rb +0 -23
- data/test/integration/tags/raw_tag_test.rb +0 -31
- data/test/integration/tags/standard_tag_test.rb +0 -296
- data/test/integration/tags/statements_test.rb +0 -111
- data/test/integration/tags/table_row_test.rb +0 -64
- data/test/integration/tags/unless_else_tag_test.rb +0 -26
- data/test/integration/template_test.rb +0 -323
- data/test/integration/trim_mode_test.rb +0 -525
- data/test/integration/variable_test.rb +0 -92
- data/test/test_helper.rb +0 -117
- data/test/unit/block_unit_test.rb +0 -58
- data/test/unit/condition_unit_test.rb +0 -158
- data/test/unit/context_unit_test.rb +0 -483
- data/test/unit/file_system_unit_test.rb +0 -35
- data/test/unit/i18n_unit_test.rb +0 -37
- data/test/unit/lexer_unit_test.rb +0 -51
- data/test/unit/parser_unit_test.rb +0 -82
- data/test/unit/regexp_unit_test.rb +0 -44
- data/test/unit/strainer_unit_test.rb +0 -148
- data/test/unit/tag_unit_test.rb +0 -21
- data/test/unit/tags/case_tag_unit_test.rb +0 -10
- data/test/unit/tags/for_tag_unit_test.rb +0 -13
- data/test/unit/tags/if_tag_unit_test.rb +0 -8
- data/test/unit/template_unit_test.rb +0 -78
- data/test/unit/tokenizer_unit_test.rb +0 -55
- data/test/unit/variable_unit_test.rb +0 -162
data/lib/liquid/expression.rb
CHANGED
|
@@ -1,43 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Liquid
|
|
2
4
|
class Expression
|
|
3
|
-
|
|
4
|
-
|
|
5
|
+
LITERALS = {
|
|
6
|
+
nil => nil,
|
|
7
|
+
'nil' => nil,
|
|
8
|
+
'null' => nil,
|
|
9
|
+
'' => nil,
|
|
10
|
+
'true' => true,
|
|
11
|
+
'false' => false,
|
|
12
|
+
'blank' => '',
|
|
13
|
+
'empty' => '',
|
|
14
|
+
# in lax mode, minus sign can be a VariableLookup
|
|
15
|
+
# For simplicity and performace, we treat it like a literal
|
|
16
|
+
'-' => VariableLookup.parse("-", nil).freeze,
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
DOT = ".".ord
|
|
20
|
+
ZERO = "0".ord
|
|
21
|
+
NINE = "9".ord
|
|
22
|
+
DASH = "-".ord
|
|
23
|
+
|
|
24
|
+
# Use an atomic group (?>...) to avoid pathological backtracing from
|
|
25
|
+
# malicious input as described in https://github.com/Shopify/liquid/issues/1357
|
|
26
|
+
RANGES_REGEX = /\A\(\s*(?>(\S+)\s*\.\.)\s*(\S+)\s*\)\z/
|
|
27
|
+
INTEGER_REGEX = /\A(-?\d+)\z/
|
|
28
|
+
FLOAT_REGEX = /\A(-?\d+)\.\d+\z/
|
|
5
29
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
30
|
+
class << self
|
|
31
|
+
def safe_parse(parser, ss = StringScanner.new(""), cache = nil)
|
|
32
|
+
parse(parser.expression, ss, cache)
|
|
9
33
|
end
|
|
10
34
|
|
|
11
|
-
def
|
|
12
|
-
|
|
35
|
+
def parse(markup, ss = StringScanner.new(""), cache = nil)
|
|
36
|
+
return unless markup
|
|
37
|
+
|
|
38
|
+
markup = markup.strip # markup can be a frozen string
|
|
39
|
+
|
|
40
|
+
if (markup.start_with?('"') && markup.end_with?('"')) ||
|
|
41
|
+
(markup.start_with?("'") && markup.end_with?("'"))
|
|
42
|
+
return markup[1..-2]
|
|
43
|
+
elsif LITERALS.key?(markup)
|
|
44
|
+
return LITERALS[markup]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Cache only exists during parsing
|
|
48
|
+
if cache
|
|
49
|
+
return cache[markup] if cache.key?(markup)
|
|
50
|
+
|
|
51
|
+
cache[markup] = inner_parse(markup, ss, cache).freeze
|
|
52
|
+
else
|
|
53
|
+
inner_parse(markup, ss, nil).freeze
|
|
54
|
+
end
|
|
13
55
|
end
|
|
14
|
-
end
|
|
15
56
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
57
|
+
def inner_parse(markup, ss, cache)
|
|
58
|
+
if (markup.start_with?("(") && markup.end_with?(")")) && markup =~ RANGES_REGEX
|
|
59
|
+
return RangeLookup.parse(
|
|
60
|
+
Regexp.last_match(1),
|
|
61
|
+
Regexp.last_match(2),
|
|
62
|
+
ss,
|
|
63
|
+
cache,
|
|
64
|
+
)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
if (num = parse_number(markup, ss))
|
|
68
|
+
num
|
|
69
|
+
else
|
|
70
|
+
VariableLookup.parse(markup, ss, cache)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def parse_number(markup, ss)
|
|
75
|
+
# check if the markup is simple integer or float
|
|
28
76
|
case markup
|
|
29
|
-
when
|
|
30
|
-
|
|
31
|
-
when
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
77
|
+
when INTEGER_REGEX
|
|
78
|
+
return Integer(markup, 10)
|
|
79
|
+
when FLOAT_REGEX
|
|
80
|
+
return markup.to_f
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
ss.string = markup
|
|
84
|
+
# the first byte must be a digit or a dash
|
|
85
|
+
byte = ss.scan_byte
|
|
86
|
+
|
|
87
|
+
return false if byte != DASH && (byte < ZERO || byte > NINE)
|
|
88
|
+
|
|
89
|
+
if byte == DASH
|
|
90
|
+
peek_byte = ss.peek_byte
|
|
91
|
+
|
|
92
|
+
# if it starts with a dash, the next byte must be a digit
|
|
93
|
+
return false if peek_byte.nil? || !(peek_byte >= ZERO && peek_byte <= NINE)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# The markup could be a float with multiple dots
|
|
97
|
+
first_dot_pos = nil
|
|
98
|
+
num_end_pos = nil
|
|
99
|
+
|
|
100
|
+
while (byte = ss.scan_byte)
|
|
101
|
+
return false if byte != DOT && (byte < ZERO || byte > NINE)
|
|
102
|
+
|
|
103
|
+
# we found our number and now we are just scanning the rest of the string
|
|
104
|
+
next if num_end_pos
|
|
105
|
+
|
|
106
|
+
if byte == DOT
|
|
107
|
+
if first_dot_pos.nil?
|
|
108
|
+
first_dot_pos = ss.pos
|
|
109
|
+
else
|
|
110
|
+
# we found another dot, so we know that the number ends here
|
|
111
|
+
num_end_pos = ss.pos - 1
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
num_end_pos = markup.length if ss.eos?
|
|
117
|
+
|
|
118
|
+
if num_end_pos
|
|
119
|
+
# number ends with a number "123.123"
|
|
120
|
+
markup.byteslice(0, num_end_pos).to_f
|
|
39
121
|
else
|
|
40
|
-
|
|
122
|
+
# number ends with a dot "123."
|
|
123
|
+
markup.byteslice(0, first_dot_pos).to_f
|
|
41
124
|
end
|
|
42
125
|
end
|
|
43
126
|
end
|
data/lib/liquid/extensions.rb
CHANGED
data/lib/liquid/file_system.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Liquid
|
|
2
4
|
# A Liquid file system is a way to let your templates retrieve other templates for use with the include tag.
|
|
3
5
|
#
|
|
@@ -44,8 +46,8 @@ module Liquid
|
|
|
44
46
|
class LocalFileSystem
|
|
45
47
|
attr_accessor :root
|
|
46
48
|
|
|
47
|
-
def initialize(root, pattern = "_%s.liquid"
|
|
48
|
-
@root
|
|
49
|
+
def initialize(root, pattern = "_%s.liquid")
|
|
50
|
+
@root = root
|
|
49
51
|
@pattern = pattern
|
|
50
52
|
end
|
|
51
53
|
|
|
@@ -57,9 +59,9 @@ module Liquid
|
|
|
57
59
|
end
|
|
58
60
|
|
|
59
61
|
def full_path(template_path)
|
|
60
|
-
raise FileSystemError, "Illegal template name '#{template_path}'" unless
|
|
62
|
+
raise FileSystemError, "Illegal template name '#{template_path}'" unless %r{\A[^./][a-zA-Z0-9_/]+\z}.match?(template_path)
|
|
61
63
|
|
|
62
|
-
full_path = if template_path.include?('/'
|
|
64
|
+
full_path = if template_path.include?('/')
|
|
63
65
|
File.join(root, File.dirname(template_path), @pattern % File.basename(template_path))
|
|
64
66
|
else
|
|
65
67
|
File.join(root, @pattern % template_path)
|
data/lib/liquid/forloop_drop.rb
CHANGED
|
@@ -1,34 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Liquid
|
|
4
|
+
# @liquid_public_docs
|
|
5
|
+
# @liquid_type object
|
|
6
|
+
# @liquid_name forloop
|
|
7
|
+
# @liquid_summary
|
|
8
|
+
# Information about a parent [`for` loop](/docs/api/liquid/tags/for).
|
|
2
9
|
class ForloopDrop < Drop
|
|
3
10
|
def initialize(name, length, parentloop)
|
|
4
|
-
@name
|
|
5
|
-
@length
|
|
11
|
+
@name = name
|
|
12
|
+
@length = length
|
|
6
13
|
@parentloop = parentloop
|
|
7
|
-
@index
|
|
14
|
+
@index = 0
|
|
8
15
|
end
|
|
9
16
|
|
|
10
|
-
|
|
17
|
+
# @liquid_public_docs
|
|
18
|
+
# @liquid_name length
|
|
19
|
+
# @liquid_summary
|
|
20
|
+
# The total number of iterations in the loop.
|
|
21
|
+
# @liquid_return [number]
|
|
22
|
+
attr_reader :length
|
|
23
|
+
|
|
24
|
+
# @liquid_public_docs
|
|
25
|
+
# @liquid_name parentloop
|
|
26
|
+
# @liquid_summary
|
|
27
|
+
# The parent `forloop` object.
|
|
28
|
+
# @liquid_description
|
|
29
|
+
# If the current `for` loop isn't nested inside another `for` loop, then `nil` is returned.
|
|
30
|
+
# @liquid_return [forloop]
|
|
31
|
+
attr_reader :parentloop
|
|
32
|
+
|
|
33
|
+
attr_reader :name
|
|
11
34
|
|
|
35
|
+
# @liquid_public_docs
|
|
36
|
+
# @liquid_summary
|
|
37
|
+
# The 1-based index of the current iteration.
|
|
38
|
+
# @liquid_return [number]
|
|
12
39
|
def index
|
|
13
40
|
@index + 1
|
|
14
41
|
end
|
|
15
42
|
|
|
43
|
+
# @liquid_public_docs
|
|
44
|
+
# @liquid_summary
|
|
45
|
+
# The 0-based index of the current iteration.
|
|
46
|
+
# @liquid_return [number]
|
|
16
47
|
def index0
|
|
17
48
|
@index
|
|
18
49
|
end
|
|
19
50
|
|
|
51
|
+
# @liquid_public_docs
|
|
52
|
+
# @liquid_summary
|
|
53
|
+
# The 1-based index of the current iteration, in reverse order.
|
|
54
|
+
# @liquid_return [number]
|
|
20
55
|
def rindex
|
|
21
56
|
@length - @index
|
|
22
57
|
end
|
|
23
58
|
|
|
59
|
+
# @liquid_public_docs
|
|
60
|
+
# @liquid_summary
|
|
61
|
+
# The 0-based index of the current iteration, in reverse order.
|
|
62
|
+
# @liquid_return [number]
|
|
24
63
|
def rindex0
|
|
25
64
|
@length - @index - 1
|
|
26
65
|
end
|
|
27
66
|
|
|
67
|
+
# @liquid_public_docs
|
|
68
|
+
# @liquid_summary
|
|
69
|
+
# Returns `true` if the current iteration is the first. Returns `false` if not.
|
|
70
|
+
# @liquid_return [boolean]
|
|
28
71
|
def first
|
|
29
72
|
@index == 0
|
|
30
73
|
end
|
|
31
74
|
|
|
75
|
+
# @liquid_public_docs
|
|
76
|
+
# @liquid_summary
|
|
77
|
+
# Returns `true` if the current iteration is the last. Returns `false` if not.
|
|
78
|
+
# @liquid_return [boolean]
|
|
32
79
|
def last
|
|
33
80
|
@index == @length - 1
|
|
34
81
|
end
|
data/lib/liquid/i18n.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'yaml'
|
|
2
4
|
|
|
3
5
|
module Liquid
|
|
@@ -26,13 +28,13 @@ module Liquid
|
|
|
26
28
|
def interpolate(name, vars)
|
|
27
29
|
name.gsub(/%\{(\w+)\}/) do
|
|
28
30
|
# raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym]
|
|
29
|
-
|
|
31
|
+
(vars[Regexp.last_match(1).to_sym]).to_s
|
|
30
32
|
end
|
|
31
33
|
end
|
|
32
34
|
|
|
33
35
|
def deep_fetch_translation(name)
|
|
34
|
-
name.split('.'
|
|
35
|
-
level[cur]
|
|
36
|
+
name.split('.').reduce(locale) do |level, cur|
|
|
37
|
+
level[cur] || raise(TranslationError, "Translation for #{name} does not exist in locale #{path}")
|
|
36
38
|
end
|
|
37
39
|
end
|
|
38
40
|
end
|
data/lib/liquid/interrupts.rb
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Liquid
|
|
2
4
|
# An interrupt is any command that breaks processing of a block (ex: a for loop).
|
|
3
5
|
class Interrupt
|
|
4
6
|
attr_reader :message
|
|
5
7
|
|
|
6
8
|
def initialize(message = nil)
|
|
7
|
-
@message = message || "interrupt"
|
|
9
|
+
@message = message || "interrupt"
|
|
8
10
|
end
|
|
9
11
|
end
|
|
10
12
|
|
data/lib/liquid/lexer.rb
CHANGED
|
@@ -1,53 +1,179 @@
|
|
|
1
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module Liquid
|
|
3
4
|
class Lexer
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
5
|
+
CLOSE_ROUND = [:close_round, ")"].freeze
|
|
6
|
+
CLOSE_SQUARE = [:close_square, "]"].freeze
|
|
7
|
+
COLON = [:colon, ":"].freeze
|
|
8
|
+
COMMA = [:comma, ","].freeze
|
|
9
|
+
COMPARISION_NOT_EQUAL = [:comparison, "!="].freeze
|
|
10
|
+
COMPARISON_CONTAINS = [:comparison, "contains"].freeze
|
|
11
|
+
COMPARISON_EQUAL = [:comparison, "=="].freeze
|
|
12
|
+
COMPARISON_GREATER_THAN = [:comparison, ">"].freeze
|
|
13
|
+
COMPARISON_GREATER_THAN_OR_EQUAL = [:comparison, ">="].freeze
|
|
14
|
+
COMPARISON_LESS_THAN = [:comparison, "<"].freeze
|
|
15
|
+
COMPARISON_LESS_THAN_OR_EQUAL = [:comparison, "<="].freeze
|
|
16
|
+
COMPARISON_NOT_EQUAL_ALT = [:comparison, "<>"].freeze
|
|
17
|
+
DASH = [:dash, "-"].freeze
|
|
18
|
+
DOT = [:dot, "."].freeze
|
|
19
|
+
DOTDOT = [:dotdot, ".."].freeze
|
|
20
|
+
DOT_ORD = ".".ord
|
|
18
21
|
DOUBLE_STRING_LITERAL = /"[^\"]*"/
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
EOS = [:end_of_string].freeze
|
|
23
|
+
IDENTIFIER = /[a-zA-Z_][\w-]*\??/
|
|
24
|
+
NUMBER_LITERAL = /-?\d+(\.\d+)?/
|
|
25
|
+
OPEN_ROUND = [:open_round, "("].freeze
|
|
26
|
+
OPEN_SQUARE = [:open_square, "["].freeze
|
|
27
|
+
PIPE = [:pipe, "|"].freeze
|
|
28
|
+
QUESTION = [:question, "?"].freeze
|
|
29
|
+
RUBY_WHITESPACE = [" ", "\t", "\r", "\n", "\f"].freeze
|
|
30
|
+
SINGLE_STRING_LITERAL = /'[^\']*'/
|
|
31
|
+
WHITESPACE_OR_NOTHING = /\s*/
|
|
22
32
|
|
|
23
|
-
|
|
24
|
-
|
|
33
|
+
SINGLE_COMPARISON_TOKENS = [].tap do |table|
|
|
34
|
+
table["<".ord] = COMPARISON_LESS_THAN
|
|
35
|
+
table[">".ord] = COMPARISON_GREATER_THAN
|
|
36
|
+
table.freeze
|
|
25
37
|
end
|
|
26
38
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
TWO_CHARS_COMPARISON_JUMP_TABLE = [].tap do |table|
|
|
40
|
+
table["=".ord] = [].tap do |sub_table|
|
|
41
|
+
sub_table["=".ord] = COMPARISON_EQUAL
|
|
42
|
+
sub_table.freeze
|
|
43
|
+
end
|
|
44
|
+
table["!".ord] = [].tap do |sub_table|
|
|
45
|
+
sub_table["=".ord] = COMPARISION_NOT_EQUAL
|
|
46
|
+
sub_table.freeze
|
|
47
|
+
end
|
|
48
|
+
table.freeze
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
COMPARISON_JUMP_TABLE = [].tap do |table|
|
|
52
|
+
table["<".ord] = [].tap do |sub_table|
|
|
53
|
+
sub_table["=".ord] = COMPARISON_LESS_THAN_OR_EQUAL
|
|
54
|
+
sub_table[">".ord] = COMPARISON_NOT_EQUAL_ALT
|
|
55
|
+
sub_table.freeze
|
|
56
|
+
end
|
|
57
|
+
table[">".ord] = [].tap do |sub_table|
|
|
58
|
+
sub_table["=".ord] = COMPARISON_GREATER_THAN_OR_EQUAL
|
|
59
|
+
sub_table.freeze
|
|
60
|
+
end
|
|
61
|
+
table.freeze
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
NEXT_MATCHER_JUMP_TABLE = [].tap do |table|
|
|
65
|
+
"a".upto("z") do |c|
|
|
66
|
+
table[c.ord] = [:id, IDENTIFIER].freeze
|
|
67
|
+
table[c.upcase.ord] = [:id, IDENTIFIER].freeze
|
|
68
|
+
end
|
|
69
|
+
table["_".ord] = [:id, IDENTIFIER].freeze
|
|
70
|
+
|
|
71
|
+
"0".upto("9") do |c|
|
|
72
|
+
table[c.ord] = [:number, NUMBER_LITERAL].freeze
|
|
73
|
+
end
|
|
74
|
+
table["-".ord] = [:number, NUMBER_LITERAL].freeze
|
|
75
|
+
|
|
76
|
+
table["'".ord] = [:string, SINGLE_STRING_LITERAL].freeze
|
|
77
|
+
table["\"".ord] = [:string, DOUBLE_STRING_LITERAL].freeze
|
|
78
|
+
table.freeze
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
SPECIAL_TABLE = [].tap do |table|
|
|
82
|
+
table["|".ord] = PIPE
|
|
83
|
+
table[".".ord] = DOT
|
|
84
|
+
table[":".ord] = COLON
|
|
85
|
+
table[",".ord] = COMMA
|
|
86
|
+
table["[".ord] = OPEN_SQUARE
|
|
87
|
+
table["]".ord] = CLOSE_SQUARE
|
|
88
|
+
table["(".ord] = OPEN_ROUND
|
|
89
|
+
table[")".ord] = CLOSE_ROUND
|
|
90
|
+
table["?".ord] = QUESTION
|
|
91
|
+
table["-".ord] = DASH
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
NUMBER_TABLE = [].tap do |table|
|
|
95
|
+
"0".upto("9") do |c|
|
|
96
|
+
table[c.ord] = true
|
|
97
|
+
end
|
|
98
|
+
table.freeze
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# rubocop:disable Metrics/BlockNesting
|
|
102
|
+
class << self
|
|
103
|
+
def tokenize(ss)
|
|
104
|
+
output = []
|
|
105
|
+
|
|
106
|
+
until ss.eos?
|
|
107
|
+
ss.skip(WHITESPACE_OR_NOTHING)
|
|
108
|
+
|
|
109
|
+
break if ss.eos?
|
|
110
|
+
|
|
111
|
+
start_pos = ss.pos
|
|
112
|
+
peeked = ss.peek_byte
|
|
113
|
+
|
|
114
|
+
if (special = SPECIAL_TABLE[peeked])
|
|
115
|
+
ss.scan_byte
|
|
116
|
+
# Special case for ".."
|
|
117
|
+
if special == DOT && ss.peek_byte == DOT_ORD
|
|
118
|
+
ss.scan_byte
|
|
119
|
+
output << DOTDOT
|
|
120
|
+
elsif special == DASH
|
|
121
|
+
# Special case for negative numbers
|
|
122
|
+
if (peeked_byte = ss.peek_byte) && NUMBER_TABLE[peeked_byte]
|
|
123
|
+
ss.pos -= 1
|
|
124
|
+
output << [:number, ss.scan(NUMBER_LITERAL)]
|
|
125
|
+
else
|
|
126
|
+
output << special
|
|
127
|
+
end
|
|
128
|
+
else
|
|
129
|
+
output << special
|
|
130
|
+
end
|
|
131
|
+
elsif (sub_table = TWO_CHARS_COMPARISON_JUMP_TABLE[peeked])
|
|
132
|
+
ss.scan_byte
|
|
133
|
+
if (peeked_byte = ss.peek_byte) && (found = sub_table[peeked_byte])
|
|
134
|
+
output << found
|
|
135
|
+
ss.scan_byte
|
|
136
|
+
else
|
|
137
|
+
raise_syntax_error(start_pos, ss)
|
|
138
|
+
end
|
|
139
|
+
elsif (sub_table = COMPARISON_JUMP_TABLE[peeked])
|
|
140
|
+
ss.scan_byte
|
|
141
|
+
if (peeked_byte = ss.peek_byte) && (found = sub_table[peeked_byte])
|
|
142
|
+
output << found
|
|
143
|
+
ss.scan_byte
|
|
144
|
+
else
|
|
145
|
+
output << SINGLE_COMPARISON_TOKENS[peeked]
|
|
146
|
+
end
|
|
43
147
|
else
|
|
44
|
-
|
|
148
|
+
type, pattern = NEXT_MATCHER_JUMP_TABLE[peeked]
|
|
149
|
+
|
|
150
|
+
if type && (t = ss.scan(pattern))
|
|
151
|
+
# Special case for "contains"
|
|
152
|
+
output << if type == :id && t == "contains" && output.last&.first != :dot
|
|
153
|
+
COMPARISON_CONTAINS
|
|
154
|
+
else
|
|
155
|
+
[type, t]
|
|
156
|
+
end
|
|
157
|
+
else
|
|
158
|
+
raise_syntax_error(start_pos, ss)
|
|
159
|
+
end
|
|
45
160
|
end
|
|
46
161
|
end
|
|
47
|
-
|
|
162
|
+
# rubocop:enable Metrics/BlockNesting
|
|
163
|
+
output << EOS
|
|
164
|
+
rescue ::ArgumentError => e
|
|
165
|
+
if e.message == "invalid byte sequence in #{ss.string.encoding}"
|
|
166
|
+
raise SyntaxError, "Invalid byte sequence in #{ss.string.encoding}"
|
|
167
|
+
else
|
|
168
|
+
raise
|
|
169
|
+
end
|
|
48
170
|
end
|
|
49
171
|
|
|
50
|
-
|
|
172
|
+
def raise_syntax_error(start_pos, ss)
|
|
173
|
+
ss.pos = start_pos
|
|
174
|
+
# the character could be a UTF-8 character, use getch to get all the bytes
|
|
175
|
+
raise SyntaxError, "Unexpected character #{ss.getch}"
|
|
176
|
+
end
|
|
51
177
|
end
|
|
52
178
|
end
|
|
53
179
|
end
|
data/lib/liquid/locales/en.yml
CHANGED
|
@@ -2,25 +2,35 @@
|
|
|
2
2
|
errors:
|
|
3
3
|
syntax:
|
|
4
4
|
tag_unexpected_args: "Syntax Error in '%{tag}' - Valid syntax: %{tag}"
|
|
5
|
+
block_tag_unexpected_args: "Syntax Error in '%{tag}' - Valid syntax: {% %{tag} %}{% end%{tag} %}"
|
|
5
6
|
assign: "Syntax Error in 'assign' - Valid syntax: assign [var] = [source]"
|
|
6
7
|
capture: "Syntax Error in 'capture' - Valid syntax: capture [var]"
|
|
8
|
+
snippet: "Syntax Error in 'snippet' - Valid syntax: snippet [var]"
|
|
7
9
|
case: "Syntax Error in 'case' - Valid syntax: case [condition]"
|
|
8
10
|
case_invalid_when: "Syntax Error in tag 'case' - Valid when condition: {% when [condition] [or condition2...] %}"
|
|
9
11
|
case_invalid_else: "Syntax Error in tag 'case' - Valid else condition: {% else %} (no parameters) "
|
|
10
12
|
cycle: "Syntax Error in 'cycle' - Valid syntax: cycle [name :] var [, var2, var3 ...]"
|
|
13
|
+
doc_invalid_nested: "Syntax Error in 'doc' - Nested doc tags are not allowed"
|
|
11
14
|
for: "Syntax Error in 'for loop' - Valid syntax: for [item] in [collection]"
|
|
12
15
|
for_invalid_in: "For loops require an 'in' clause"
|
|
13
16
|
for_invalid_attribute: "Invalid attribute in for loop. Valid attributes are limit and offset"
|
|
14
17
|
if: "Syntax Error in tag 'if' - Valid syntax: if [expression]"
|
|
15
18
|
include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]"
|
|
16
|
-
|
|
17
|
-
invalid_delimiter: "'
|
|
19
|
+
inline_comment_invalid: "Syntax error in tag '#' - Each line of comments must be prefixed by the '#' character"
|
|
20
|
+
invalid_delimiter: "'%{tag}' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
|
|
21
|
+
invalid_template_encoding: "Invalid template encoding"
|
|
22
|
+
render: "Syntax error in tag 'render' - Template name must be a quoted string"
|
|
23
|
+
render_invalid_template_name: "Syntax error in tag 'render' - Expected a string or identifier, found %{found}"
|
|
24
|
+
table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
|
|
25
|
+
table_row_invalid_attribute: "Invalid attribute '%{attribute}' in tablerow loop. Valid attributes are cols, limit, offset, and range"
|
|
26
|
+
tag_never_closed: "'%{block_name}' tag was never closed"
|
|
27
|
+
tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
|
|
18
28
|
unexpected_else: "%{block_name} tag does not expect 'else' tag"
|
|
19
29
|
unexpected_outer_tag: "Unexpected outer '%{tag}' tag"
|
|
20
|
-
|
|
30
|
+
unknown_tag: "Unknown tag '%{tag}'"
|
|
21
31
|
variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}"
|
|
22
|
-
tag_never_closed: "'%{block_name}' tag was never closed"
|
|
23
|
-
meta_syntax_error: "Liquid syntax error: #{e.message}"
|
|
24
|
-
table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
|
|
25
32
|
argument:
|
|
26
33
|
include: "Argument error in tag 'include' - Illegal template name"
|
|
34
|
+
render: "Argument error in tag 'render' - Dynamically chosen templates are not allowed"
|
|
35
|
+
disabled:
|
|
36
|
+
tag: "usage is not allowed in this context"
|