liquid 2.6.3 → 5.4.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 +272 -26
- data/README.md +67 -3
- data/lib/liquid/block.rb +62 -94
- data/lib/liquid/block_body.rb +255 -0
- data/lib/liquid/condition.rb +96 -38
- data/lib/liquid/context.rb +172 -154
- data/lib/liquid/document.rb +57 -9
- data/lib/liquid/drop.rb +33 -14
- data/lib/liquid/errors.rb +56 -10
- data/lib/liquid/expression.rb +45 -0
- data/lib/liquid/extensions.rb +21 -7
- data/lib/liquid/file_system.rb +27 -14
- data/lib/liquid/forloop_drop.rb +92 -0
- data/lib/liquid/i18n.rb +41 -0
- data/lib/liquid/interrupts.rb +3 -2
- data/lib/liquid/lexer.rb +62 -0
- data/lib/liquid/locales/en.yml +29 -0
- data/lib/liquid/parse_context.rb +54 -0
- data/lib/liquid/parse_tree_visitor.rb +42 -0
- data/lib/liquid/parser.rb +102 -0
- data/lib/liquid/parser_switching.rb +45 -0
- data/lib/liquid/partial_cache.rb +24 -0
- data/lib/liquid/profiler/hooks.rb +35 -0
- data/lib/liquid/profiler.rb +139 -0
- data/lib/liquid/range_lookup.rb +47 -0
- data/lib/liquid/registers.rb +51 -0
- data/lib/liquid/resource_limits.rb +62 -0
- data/lib/liquid/standardfilters.rb +789 -118
- data/lib/liquid/strainer_factory.rb +41 -0
- data/lib/liquid/strainer_template.rb +62 -0
- data/lib/liquid/tablerowloop_drop.rb +121 -0
- data/lib/liquid/tag/disableable.rb +22 -0
- data/lib/liquid/tag/disabler.rb +21 -0
- data/lib/liquid/tag.rb +49 -10
- data/lib/liquid/tags/assign.rb +61 -19
- data/lib/liquid/tags/break.rb +14 -4
- data/lib/liquid/tags/capture.rb +29 -21
- data/lib/liquid/tags/case.rb +80 -31
- data/lib/liquid/tags/comment.rb +24 -2
- data/lib/liquid/tags/continue.rb +14 -13
- data/lib/liquid/tags/cycle.rb +50 -32
- data/lib/liquid/tags/decrement.rb +24 -26
- data/lib/liquid/tags/echo.rb +41 -0
- data/lib/liquid/tags/for.rb +164 -100
- data/lib/liquid/tags/if.rb +105 -44
- data/lib/liquid/tags/ifchanged.rb +10 -11
- data/lib/liquid/tags/include.rb +85 -65
- data/lib/liquid/tags/increment.rb +24 -22
- data/lib/liquid/tags/inline_comment.rb +43 -0
- data/lib/liquid/tags/raw.rb +50 -11
- data/lib/liquid/tags/render.rb +109 -0
- data/lib/liquid/tags/table_row.rb +88 -0
- data/lib/liquid/tags/unless.rb +37 -21
- data/lib/liquid/template.rb +124 -46
- data/lib/liquid/template_factory.rb +9 -0
- data/lib/liquid/tokenizer.rb +39 -0
- data/lib/liquid/usage.rb +8 -0
- data/lib/liquid/utils.rb +68 -5
- data/lib/liquid/variable.rb +128 -32
- data/lib/liquid/variable_lookup.rb +96 -0
- data/lib/liquid/version.rb +3 -1
- data/lib/liquid.rb +36 -13
- metadata +69 -77
- data/lib/extras/liquid_view.rb +0 -51
- data/lib/liquid/htmltags.rb +0 -73
- data/lib/liquid/module_ex.rb +0 -62
- data/lib/liquid/strainer.rb +0 -53
- data/test/liquid/assign_test.rb +0 -21
- data/test/liquid/block_test.rb +0 -58
- data/test/liquid/capture_test.rb +0 -40
- data/test/liquid/condition_test.rb +0 -127
- data/test/liquid/context_test.rb +0 -478
- data/test/liquid/drop_test.rb +0 -180
- data/test/liquid/error_handling_test.rb +0 -81
- data/test/liquid/file_system_test.rb +0 -29
- data/test/liquid/filter_test.rb +0 -125
- data/test/liquid/hash_ordering_test.rb +0 -25
- data/test/liquid/module_ex_test.rb +0 -87
- data/test/liquid/output_test.rb +0 -116
- data/test/liquid/parsing_quirks_test.rb +0 -52
- data/test/liquid/regexp_test.rb +0 -44
- data/test/liquid/security_test.rb +0 -64
- data/test/liquid/standard_filter_test.rb +0 -263
- data/test/liquid/strainer_test.rb +0 -52
- data/test/liquid/tags/break_tag_test.rb +0 -16
- data/test/liquid/tags/continue_tag_test.rb +0 -16
- data/test/liquid/tags/for_tag_test.rb +0 -297
- data/test/liquid/tags/html_tag_test.rb +0 -63
- data/test/liquid/tags/if_else_tag_test.rb +0 -166
- data/test/liquid/tags/include_tag_test.rb +0 -166
- data/test/liquid/tags/increment_tag_test.rb +0 -24
- data/test/liquid/tags/raw_tag_test.rb +0 -24
- data/test/liquid/tags/standard_tag_test.rb +0 -295
- data/test/liquid/tags/statements_test.rb +0 -134
- data/test/liquid/tags/unless_else_tag_test.rb +0 -26
- data/test/liquid/template_test.rb +0 -146
- data/test/liquid/variable_test.rb +0 -186
- data/test/test_helper.rb +0 -29
- /data/{MIT-LICENSE → LICENSE} +0 -0
data/lib/liquid/extensions.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'time'
|
2
4
|
require 'date'
|
3
5
|
|
@@ -7,44 +9,56 @@ class String # :nodoc:
|
|
7
9
|
end
|
8
10
|
end
|
9
11
|
|
10
|
-
class
|
12
|
+
class Symbol # :nodoc:
|
13
|
+
def to_liquid
|
14
|
+
to_s
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class Array # :nodoc:
|
11
19
|
def to_liquid
|
12
20
|
self
|
13
21
|
end
|
14
22
|
end
|
15
23
|
|
16
|
-
class Hash
|
24
|
+
class Hash # :nodoc:
|
17
25
|
def to_liquid
|
18
26
|
self
|
19
27
|
end
|
20
28
|
end
|
21
29
|
|
22
|
-
class Numeric
|
30
|
+
class Numeric # :nodoc:
|
23
31
|
def to_liquid
|
24
32
|
self
|
25
33
|
end
|
26
34
|
end
|
27
35
|
|
28
|
-
class
|
36
|
+
class Range # :nodoc:
|
29
37
|
def to_liquid
|
30
38
|
self
|
31
39
|
end
|
32
40
|
end
|
33
41
|
|
34
|
-
class
|
42
|
+
class Time # :nodoc:
|
35
43
|
def to_liquid
|
36
44
|
self
|
37
45
|
end
|
38
46
|
end
|
39
47
|
|
40
|
-
class Date
|
48
|
+
class DateTime < Date # :nodoc:
|
49
|
+
def to_liquid
|
50
|
+
self
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class Date # :nodoc:
|
41
55
|
def to_liquid
|
42
56
|
self
|
43
57
|
end
|
44
58
|
end
|
45
59
|
|
46
60
|
class TrueClass
|
47
|
-
def to_liquid
|
61
|
+
def to_liquid # :nodoc:
|
48
62
|
self
|
49
63
|
end
|
50
64
|
end
|
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
|
#
|
@@ -8,13 +10,13 @@ module Liquid
|
|
8
10
|
#
|
9
11
|
# Example:
|
10
12
|
#
|
11
|
-
#
|
12
|
-
#
|
13
|
+
# Liquid::Template.file_system = Liquid::LocalFileSystem.new(template_path)
|
14
|
+
# liquid = Liquid::Template.parse(template)
|
13
15
|
#
|
14
16
|
# This will parse the template with a LocalFileSystem implementation rooted at 'template_path'.
|
15
17
|
class BlankFileSystem
|
16
18
|
# Called by Liquid to retrieve a template file
|
17
|
-
def read_template_file(
|
19
|
+
def read_template_file(_template_path)
|
18
20
|
raise FileSystemError, "This liquid context does not allow includes."
|
19
21
|
end
|
20
22
|
end
|
@@ -26,35 +28,46 @@ module Liquid
|
|
26
28
|
#
|
27
29
|
# Example:
|
28
30
|
#
|
29
|
-
#
|
31
|
+
# file_system = Liquid::LocalFileSystem.new("/some/path")
|
32
|
+
#
|
33
|
+
# file_system.full_path("mypartial") # => "/some/path/_mypartial.liquid"
|
34
|
+
# file_system.full_path("dir/mypartial") # => "/some/path/dir/_mypartial.liquid"
|
35
|
+
#
|
36
|
+
# Optionally in the second argument you can specify a custom pattern for template filenames.
|
37
|
+
# The Kernel::sprintf format specification is used.
|
38
|
+
# Default pattern is "_%s.liquid".
|
39
|
+
#
|
40
|
+
# Example:
|
41
|
+
#
|
42
|
+
# file_system = Liquid::LocalFileSystem.new("/some/path", "%s.html")
|
30
43
|
#
|
31
|
-
#
|
32
|
-
# file_system.full_path("dir/mypartial") # => "/some/path/dir/_mypartial.liquid"
|
44
|
+
# file_system.full_path("index") # => "/some/path/index.html"
|
33
45
|
#
|
34
46
|
class LocalFileSystem
|
35
47
|
attr_accessor :root
|
36
48
|
|
37
|
-
def initialize(root)
|
38
|
-
@root
|
49
|
+
def initialize(root, pattern = "_%s.liquid")
|
50
|
+
@root = root
|
51
|
+
@pattern = pattern
|
39
52
|
end
|
40
53
|
|
41
|
-
def read_template_file(template_path
|
54
|
+
def read_template_file(template_path)
|
42
55
|
full_path = full_path(template_path)
|
43
|
-
raise FileSystemError, "No such template '#{template_path}'" unless File.
|
56
|
+
raise FileSystemError, "No such template '#{template_path}'" unless File.exist?(full_path)
|
44
57
|
|
45
58
|
File.read(full_path)
|
46
59
|
end
|
47
60
|
|
48
61
|
def full_path(template_path)
|
49
|
-
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)
|
50
63
|
|
51
64
|
full_path = if template_path.include?('/')
|
52
|
-
File.join(root, File.dirname(template_path),
|
65
|
+
File.join(root, File.dirname(template_path), @pattern % File.basename(template_path))
|
53
66
|
else
|
54
|
-
File.join(root,
|
67
|
+
File.join(root, @pattern % template_path)
|
55
68
|
end
|
56
69
|
|
57
|
-
raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path)
|
70
|
+
raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path).start_with?(File.expand_path(root))
|
58
71
|
|
59
72
|
full_path
|
60
73
|
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
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](/api/liquid/tags#for).
|
9
|
+
class ForloopDrop < Drop
|
10
|
+
def initialize(name, length, parentloop)
|
11
|
+
@name = name
|
12
|
+
@length = length
|
13
|
+
@parentloop = parentloop
|
14
|
+
@index = 0
|
15
|
+
end
|
16
|
+
|
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
|
+
def name
|
34
|
+
Usage.increment('forloop_drop_name')
|
35
|
+
@name
|
36
|
+
end
|
37
|
+
|
38
|
+
# @liquid_public_docs
|
39
|
+
# @liquid_summary
|
40
|
+
# The 1-based index of the current iteration.
|
41
|
+
# @liquid_return [number]
|
42
|
+
def index
|
43
|
+
@index + 1
|
44
|
+
end
|
45
|
+
|
46
|
+
# @liquid_public_docs
|
47
|
+
# @liquid_summary
|
48
|
+
# The 0-based index of the current iteration.
|
49
|
+
# @liquid_return [number]
|
50
|
+
def index0
|
51
|
+
@index
|
52
|
+
end
|
53
|
+
|
54
|
+
# @liquid_public_docs
|
55
|
+
# @liquid_summary
|
56
|
+
# The 1-based index of the current iteration, in reverse order.
|
57
|
+
# @liquid_return [number]
|
58
|
+
def rindex
|
59
|
+
@length - @index
|
60
|
+
end
|
61
|
+
|
62
|
+
# @liquid_public_docs
|
63
|
+
# @liquid_summary
|
64
|
+
# The 0-based index of the current iteration, in reverse order.
|
65
|
+
# @liquid_return [number]
|
66
|
+
def rindex0
|
67
|
+
@length - @index - 1
|
68
|
+
end
|
69
|
+
|
70
|
+
# @liquid_public_docs
|
71
|
+
# @liquid_summary
|
72
|
+
# Returns `true` if the current iteration is the first. Returns `false` if not.
|
73
|
+
# @liquid_return [boolean]
|
74
|
+
def first
|
75
|
+
@index == 0
|
76
|
+
end
|
77
|
+
|
78
|
+
# @liquid_public_docs
|
79
|
+
# @liquid_summary
|
80
|
+
# Returns `true` if the current iteration is the last. Returns `false` if not.
|
81
|
+
# @liquid_return [boolean]
|
82
|
+
def last
|
83
|
+
@index == @length - 1
|
84
|
+
end
|
85
|
+
|
86
|
+
protected
|
87
|
+
|
88
|
+
def increment!
|
89
|
+
@index += 1
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
data/lib/liquid/i18n.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
module Liquid
|
6
|
+
class I18n
|
7
|
+
DEFAULT_LOCALE = File.join(File.expand_path(__dir__), "locales", "en.yml")
|
8
|
+
|
9
|
+
TranslationError = Class.new(StandardError)
|
10
|
+
|
11
|
+
attr_reader :path
|
12
|
+
|
13
|
+
def initialize(path = DEFAULT_LOCALE)
|
14
|
+
@path = path
|
15
|
+
end
|
16
|
+
|
17
|
+
def translate(name, vars = {})
|
18
|
+
interpolate(deep_fetch_translation(name), vars)
|
19
|
+
end
|
20
|
+
alias_method :t, :translate
|
21
|
+
|
22
|
+
def locale
|
23
|
+
@locale ||= YAML.load_file(@path)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def interpolate(name, vars)
|
29
|
+
name.gsub(/%\{(\w+)\}/) do
|
30
|
+
# raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym]
|
31
|
+
(vars[Regexp.last_match(1).to_sym]).to_s
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def deep_fetch_translation(name)
|
36
|
+
name.split('.').reduce(locale) do |level, cur|
|
37
|
+
level[cur] || raise(TranslationError, "Translation for #{name} does not exist in locale #{path}")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/liquid/interrupts.rb
CHANGED
@@ -1,10 +1,11 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
+
module Liquid
|
3
4
|
# An interrupt is any command that breaks processing of a block (ex: a for loop).
|
4
5
|
class Interrupt
|
5
6
|
attr_reader :message
|
6
7
|
|
7
|
-
def initialize(message=nil)
|
8
|
+
def initialize(message = nil)
|
8
9
|
@message = message || "interrupt"
|
9
10
|
end
|
10
11
|
end
|
data/lib/liquid/lexer.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "strscan"
|
4
|
+
module Liquid
|
5
|
+
class Lexer
|
6
|
+
SPECIALS = {
|
7
|
+
'|' => :pipe,
|
8
|
+
'.' => :dot,
|
9
|
+
':' => :colon,
|
10
|
+
',' => :comma,
|
11
|
+
'[' => :open_square,
|
12
|
+
']' => :close_square,
|
13
|
+
'(' => :open_round,
|
14
|
+
')' => :close_round,
|
15
|
+
'?' => :question,
|
16
|
+
'-' => :dash,
|
17
|
+
}.freeze
|
18
|
+
IDENTIFIER = /[a-zA-Z_][\w-]*\??/
|
19
|
+
SINGLE_STRING_LITERAL = /'[^\']*'/
|
20
|
+
DOUBLE_STRING_LITERAL = /"[^\"]*"/
|
21
|
+
NUMBER_LITERAL = /-?\d+(\.\d+)?/
|
22
|
+
DOTDOT = /\.\./
|
23
|
+
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/
|
24
|
+
WHITESPACE_OR_NOTHING = /\s*/
|
25
|
+
|
26
|
+
def initialize(input)
|
27
|
+
@ss = StringScanner.new(input)
|
28
|
+
end
|
29
|
+
|
30
|
+
def tokenize
|
31
|
+
@output = []
|
32
|
+
|
33
|
+
until @ss.eos?
|
34
|
+
@ss.skip(WHITESPACE_OR_NOTHING)
|
35
|
+
break if @ss.eos?
|
36
|
+
tok = if (t = @ss.scan(COMPARISON_OPERATOR))
|
37
|
+
[:comparison, t]
|
38
|
+
elsif (t = @ss.scan(SINGLE_STRING_LITERAL))
|
39
|
+
[:string, t]
|
40
|
+
elsif (t = @ss.scan(DOUBLE_STRING_LITERAL))
|
41
|
+
[:string, t]
|
42
|
+
elsif (t = @ss.scan(NUMBER_LITERAL))
|
43
|
+
[:number, t]
|
44
|
+
elsif (t = @ss.scan(IDENTIFIER))
|
45
|
+
[:id, t]
|
46
|
+
elsif (t = @ss.scan(DOTDOT))
|
47
|
+
[:dotdot, t]
|
48
|
+
else
|
49
|
+
c = @ss.getch
|
50
|
+
if (s = SPECIALS[c])
|
51
|
+
[s, c]
|
52
|
+
else
|
53
|
+
raise SyntaxError, "Unexpected character #{c}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
@output << tok
|
57
|
+
end
|
58
|
+
|
59
|
+
@output << [:end_of_string]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
---
|
2
|
+
errors:
|
3
|
+
syntax:
|
4
|
+
tag_unexpected_args: "Syntax Error in '%{tag}' - Valid syntax: %{tag}"
|
5
|
+
assign: "Syntax Error in 'assign' - Valid syntax: assign [var] = [source]"
|
6
|
+
capture: "Syntax Error in 'capture' - Valid syntax: capture [var]"
|
7
|
+
case: "Syntax Error in 'case' - Valid syntax: case [condition]"
|
8
|
+
case_invalid_when: "Syntax Error in tag 'case' - Valid when condition: {% when [condition] [or condition2...] %}"
|
9
|
+
case_invalid_else: "Syntax Error in tag 'case' - Valid else condition: {% else %} (no parameters) "
|
10
|
+
cycle: "Syntax Error in 'cycle' - Valid syntax: cycle [name :] var [, var2, var3 ...]"
|
11
|
+
for: "Syntax Error in 'for loop' - Valid syntax: for [item] in [collection]"
|
12
|
+
for_invalid_in: "For loops require an 'in' clause"
|
13
|
+
for_invalid_attribute: "Invalid attribute in for loop. Valid attributes are limit and offset"
|
14
|
+
if: "Syntax Error in tag 'if' - Valid syntax: if [expression]"
|
15
|
+
include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]"
|
16
|
+
inline_comment_invalid: "Syntax error in tag '#' - Each line of comments must be prefixed by the '#' character"
|
17
|
+
invalid_delimiter: "'%{tag}' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
|
18
|
+
render: "Syntax error in tag 'render' - Template name must be a quoted string"
|
19
|
+
table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
|
20
|
+
tag_never_closed: "'%{block_name}' tag was never closed"
|
21
|
+
tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
|
22
|
+
unexpected_else: "%{block_name} tag does not expect 'else' tag"
|
23
|
+
unexpected_outer_tag: "Unexpected outer '%{tag}' tag"
|
24
|
+
unknown_tag: "Unknown tag '%{tag}'"
|
25
|
+
variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}"
|
26
|
+
argument:
|
27
|
+
include: "Argument error in tag 'include' - Illegal template name"
|
28
|
+
disabled:
|
29
|
+
tag: "usage is not allowed in this context"
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Liquid
|
4
|
+
class ParseContext
|
5
|
+
attr_accessor :locale, :line_number, :trim_whitespace, :depth
|
6
|
+
attr_reader :partial, :warnings, :error_mode
|
7
|
+
|
8
|
+
def initialize(options = {})
|
9
|
+
@template_options = options ? options.dup : {}
|
10
|
+
|
11
|
+
@locale = @template_options[:locale] ||= I18n.new
|
12
|
+
@warnings = []
|
13
|
+
|
14
|
+
self.depth = 0
|
15
|
+
self.partial = false
|
16
|
+
end
|
17
|
+
|
18
|
+
def [](option_key)
|
19
|
+
@options[option_key]
|
20
|
+
end
|
21
|
+
|
22
|
+
def new_block_body
|
23
|
+
Liquid::BlockBody.new
|
24
|
+
end
|
25
|
+
|
26
|
+
def new_tokenizer(markup, start_line_number: nil, for_liquid_tag: false)
|
27
|
+
Tokenizer.new(markup, line_number: start_line_number, for_liquid_tag: for_liquid_tag)
|
28
|
+
end
|
29
|
+
|
30
|
+
def parse_expression(markup)
|
31
|
+
Expression.parse(markup)
|
32
|
+
end
|
33
|
+
|
34
|
+
def partial=(value)
|
35
|
+
@partial = value
|
36
|
+
@options = value ? partial_options : @template_options
|
37
|
+
|
38
|
+
@error_mode = @options[:error_mode] || Template.error_mode
|
39
|
+
end
|
40
|
+
|
41
|
+
def partial_options
|
42
|
+
@partial_options ||= begin
|
43
|
+
dont_pass = @template_options[:include_options_blacklist]
|
44
|
+
if dont_pass == true
|
45
|
+
{ locale: locale }
|
46
|
+
elsif dont_pass.is_a?(Array)
|
47
|
+
@template_options.reject { |k, _v| dont_pass.include?(k) }
|
48
|
+
else
|
49
|
+
@template_options
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Liquid
|
4
|
+
class ParseTreeVisitor
|
5
|
+
def self.for(node, callbacks = Hash.new(proc {}))
|
6
|
+
if defined?(node.class::ParseTreeVisitor)
|
7
|
+
node.class::ParseTreeVisitor
|
8
|
+
else
|
9
|
+
self
|
10
|
+
end.new(node, callbacks)
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(node, callbacks)
|
14
|
+
@node = node
|
15
|
+
@callbacks = callbacks
|
16
|
+
end
|
17
|
+
|
18
|
+
def add_callback_for(*classes, &block)
|
19
|
+
callback = block
|
20
|
+
callback = ->(node, _) { yield node } if block.arity.abs == 1
|
21
|
+
callback = ->(_, _) { yield } if block.arity.zero?
|
22
|
+
classes.each { |klass| @callbacks[klass] = callback }
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
def visit(context = nil)
|
27
|
+
children.map do |node|
|
28
|
+
item, new_context = @callbacks[node.class].call(node, context)
|
29
|
+
[
|
30
|
+
item,
|
31
|
+
ParseTreeVisitor.for(node, @callbacks).visit(new_context || context),
|
32
|
+
]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
def children
|
39
|
+
@node.respond_to?(:nodelist) ? Array(@node.nodelist) : []
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Liquid
|
4
|
+
class Parser
|
5
|
+
def initialize(input)
|
6
|
+
l = Lexer.new(input)
|
7
|
+
@tokens = l.tokenize
|
8
|
+
@p = 0 # pointer to current location
|
9
|
+
end
|
10
|
+
|
11
|
+
def jump(point)
|
12
|
+
@p = point
|
13
|
+
end
|
14
|
+
|
15
|
+
def consume(type = nil)
|
16
|
+
token = @tokens[@p]
|
17
|
+
if type && token[0] != type
|
18
|
+
raise SyntaxError, "Expected #{type} but found #{@tokens[@p].first}"
|
19
|
+
end
|
20
|
+
@p += 1
|
21
|
+
token[1]
|
22
|
+
end
|
23
|
+
|
24
|
+
# Only consumes the token if it matches the type
|
25
|
+
# Returns the token's contents if it was consumed
|
26
|
+
# or false otherwise.
|
27
|
+
def consume?(type)
|
28
|
+
token = @tokens[@p]
|
29
|
+
return false unless token && token[0] == type
|
30
|
+
@p += 1
|
31
|
+
token[1]
|
32
|
+
end
|
33
|
+
|
34
|
+
# Like consume? Except for an :id token of a certain name
|
35
|
+
def id?(str)
|
36
|
+
token = @tokens[@p]
|
37
|
+
return false unless token && token[0] == :id
|
38
|
+
return false unless token[1] == str
|
39
|
+
@p += 1
|
40
|
+
token[1]
|
41
|
+
end
|
42
|
+
|
43
|
+
def look(type, ahead = 0)
|
44
|
+
tok = @tokens[@p + ahead]
|
45
|
+
return false unless tok
|
46
|
+
tok[0] == type
|
47
|
+
end
|
48
|
+
|
49
|
+
def expression
|
50
|
+
token = @tokens[@p]
|
51
|
+
case token[0]
|
52
|
+
when :id
|
53
|
+
str = consume
|
54
|
+
str << variable_lookups
|
55
|
+
when :open_square
|
56
|
+
str = consume
|
57
|
+
str << expression
|
58
|
+
str << consume(:close_square)
|
59
|
+
str << variable_lookups
|
60
|
+
when :string, :number
|
61
|
+
consume
|
62
|
+
when :open_round
|
63
|
+
consume
|
64
|
+
first = expression
|
65
|
+
consume(:dotdot)
|
66
|
+
last = expression
|
67
|
+
consume(:close_round)
|
68
|
+
"(#{first}..#{last})"
|
69
|
+
else
|
70
|
+
raise SyntaxError, "#{token} is not a valid expression"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def argument
|
75
|
+
str = +""
|
76
|
+
# might be a keyword argument (identifier: expression)
|
77
|
+
if look(:id) && look(:colon, 1)
|
78
|
+
str << consume << consume << ' '
|
79
|
+
end
|
80
|
+
|
81
|
+
str << expression
|
82
|
+
str
|
83
|
+
end
|
84
|
+
|
85
|
+
def variable_lookups
|
86
|
+
str = +""
|
87
|
+
loop do
|
88
|
+
if look(:open_square)
|
89
|
+
str << consume
|
90
|
+
str << expression
|
91
|
+
str << consume(:close_square)
|
92
|
+
elsif look(:dot)
|
93
|
+
str << consume
|
94
|
+
str << consume(:id)
|
95
|
+
else
|
96
|
+
break
|
97
|
+
end
|
98
|
+
end
|
99
|
+
str
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Liquid
|
4
|
+
module ParserSwitching
|
5
|
+
def strict_parse_with_error_mode_fallback(markup)
|
6
|
+
strict_parse_with_error_context(markup)
|
7
|
+
rescue SyntaxError => e
|
8
|
+
case parse_context.error_mode
|
9
|
+
when :strict
|
10
|
+
raise
|
11
|
+
when :warn
|
12
|
+
parse_context.warnings << e
|
13
|
+
end
|
14
|
+
lax_parse(markup)
|
15
|
+
end
|
16
|
+
|
17
|
+
def parse_with_selected_parser(markup)
|
18
|
+
case parse_context.error_mode
|
19
|
+
when :strict then strict_parse_with_error_context(markup)
|
20
|
+
when :lax then lax_parse(markup)
|
21
|
+
when :warn
|
22
|
+
begin
|
23
|
+
strict_parse_with_error_context(markup)
|
24
|
+
rescue SyntaxError => e
|
25
|
+
parse_context.warnings << e
|
26
|
+
lax_parse(markup)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def strict_parse_with_error_context(markup)
|
34
|
+
strict_parse(markup)
|
35
|
+
rescue SyntaxError => e
|
36
|
+
e.line_number = line_number
|
37
|
+
e.markup_context = markup_context(markup)
|
38
|
+
raise e
|
39
|
+
end
|
40
|
+
|
41
|
+
def markup_context(markup)
|
42
|
+
"in \"#{markup.strip}\""
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Liquid
|
4
|
+
class PartialCache
|
5
|
+
def self.load(template_name, context:, parse_context:)
|
6
|
+
cached_partials = context.registers[:cached_partials]
|
7
|
+
cached = cached_partials[template_name]
|
8
|
+
return cached if cached
|
9
|
+
|
10
|
+
file_system = context.registers[:file_system]
|
11
|
+
source = file_system.read_template_file(template_name)
|
12
|
+
|
13
|
+
parse_context.partial = true
|
14
|
+
|
15
|
+
template_factory = context.registers[:template_factory]
|
16
|
+
template = template_factory.for(template_name)
|
17
|
+
|
18
|
+
partial = template.parse(source, parse_context)
|
19
|
+
cached_partials[template_name] = partial
|
20
|
+
ensure
|
21
|
+
parse_context.partial = false
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Liquid
|
4
|
+
module BlockBodyProfilingHook
|
5
|
+
def render_node(context, output, node)
|
6
|
+
if (profiler = context.profiler)
|
7
|
+
profiler.profile_node(context.template_name, code: node.raw, line_number: node.line_number) do
|
8
|
+
super
|
9
|
+
end
|
10
|
+
else
|
11
|
+
super
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
BlockBody.prepend(BlockBodyProfilingHook)
|
16
|
+
|
17
|
+
module DocumentProfilingHook
|
18
|
+
def render_to_output_buffer(context, output)
|
19
|
+
return super unless context.profiler
|
20
|
+
context.profiler.profile(context.template_name) { super }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
Document.prepend(DocumentProfilingHook)
|
24
|
+
|
25
|
+
module ContextProfilingHook
|
26
|
+
attr_accessor :profiler
|
27
|
+
|
28
|
+
def new_isolated_subcontext
|
29
|
+
new_context = super
|
30
|
+
new_context.profiler = profiler
|
31
|
+
new_context
|
32
|
+
end
|
33
|
+
end
|
34
|
+
Context.prepend(ContextProfilingHook)
|
35
|
+
end
|