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.
Files changed (100) hide show
  1. checksums.yaml +5 -5
  2. data/History.md +272 -26
  3. data/README.md +67 -3
  4. data/lib/liquid/block.rb +62 -94
  5. data/lib/liquid/block_body.rb +255 -0
  6. data/lib/liquid/condition.rb +96 -38
  7. data/lib/liquid/context.rb +172 -154
  8. data/lib/liquid/document.rb +57 -9
  9. data/lib/liquid/drop.rb +33 -14
  10. data/lib/liquid/errors.rb +56 -10
  11. data/lib/liquid/expression.rb +45 -0
  12. data/lib/liquid/extensions.rb +21 -7
  13. data/lib/liquid/file_system.rb +27 -14
  14. data/lib/liquid/forloop_drop.rb +92 -0
  15. data/lib/liquid/i18n.rb +41 -0
  16. data/lib/liquid/interrupts.rb +3 -2
  17. data/lib/liquid/lexer.rb +62 -0
  18. data/lib/liquid/locales/en.yml +29 -0
  19. data/lib/liquid/parse_context.rb +54 -0
  20. data/lib/liquid/parse_tree_visitor.rb +42 -0
  21. data/lib/liquid/parser.rb +102 -0
  22. data/lib/liquid/parser_switching.rb +45 -0
  23. data/lib/liquid/partial_cache.rb +24 -0
  24. data/lib/liquid/profiler/hooks.rb +35 -0
  25. data/lib/liquid/profiler.rb +139 -0
  26. data/lib/liquid/range_lookup.rb +47 -0
  27. data/lib/liquid/registers.rb +51 -0
  28. data/lib/liquid/resource_limits.rb +62 -0
  29. data/lib/liquid/standardfilters.rb +789 -118
  30. data/lib/liquid/strainer_factory.rb +41 -0
  31. data/lib/liquid/strainer_template.rb +62 -0
  32. data/lib/liquid/tablerowloop_drop.rb +121 -0
  33. data/lib/liquid/tag/disableable.rb +22 -0
  34. data/lib/liquid/tag/disabler.rb +21 -0
  35. data/lib/liquid/tag.rb +49 -10
  36. data/lib/liquid/tags/assign.rb +61 -19
  37. data/lib/liquid/tags/break.rb +14 -4
  38. data/lib/liquid/tags/capture.rb +29 -21
  39. data/lib/liquid/tags/case.rb +80 -31
  40. data/lib/liquid/tags/comment.rb +24 -2
  41. data/lib/liquid/tags/continue.rb +14 -13
  42. data/lib/liquid/tags/cycle.rb +50 -32
  43. data/lib/liquid/tags/decrement.rb +24 -26
  44. data/lib/liquid/tags/echo.rb +41 -0
  45. data/lib/liquid/tags/for.rb +164 -100
  46. data/lib/liquid/tags/if.rb +105 -44
  47. data/lib/liquid/tags/ifchanged.rb +10 -11
  48. data/lib/liquid/tags/include.rb +85 -65
  49. data/lib/liquid/tags/increment.rb +24 -22
  50. data/lib/liquid/tags/inline_comment.rb +43 -0
  51. data/lib/liquid/tags/raw.rb +50 -11
  52. data/lib/liquid/tags/render.rb +109 -0
  53. data/lib/liquid/tags/table_row.rb +88 -0
  54. data/lib/liquid/tags/unless.rb +37 -21
  55. data/lib/liquid/template.rb +124 -46
  56. data/lib/liquid/template_factory.rb +9 -0
  57. data/lib/liquid/tokenizer.rb +39 -0
  58. data/lib/liquid/usage.rb +8 -0
  59. data/lib/liquid/utils.rb +68 -5
  60. data/lib/liquid/variable.rb +128 -32
  61. data/lib/liquid/variable_lookup.rb +96 -0
  62. data/lib/liquid/version.rb +3 -1
  63. data/lib/liquid.rb +36 -13
  64. metadata +69 -77
  65. data/lib/extras/liquid_view.rb +0 -51
  66. data/lib/liquid/htmltags.rb +0 -73
  67. data/lib/liquid/module_ex.rb +0 -62
  68. data/lib/liquid/strainer.rb +0 -53
  69. data/test/liquid/assign_test.rb +0 -21
  70. data/test/liquid/block_test.rb +0 -58
  71. data/test/liquid/capture_test.rb +0 -40
  72. data/test/liquid/condition_test.rb +0 -127
  73. data/test/liquid/context_test.rb +0 -478
  74. data/test/liquid/drop_test.rb +0 -180
  75. data/test/liquid/error_handling_test.rb +0 -81
  76. data/test/liquid/file_system_test.rb +0 -29
  77. data/test/liquid/filter_test.rb +0 -125
  78. data/test/liquid/hash_ordering_test.rb +0 -25
  79. data/test/liquid/module_ex_test.rb +0 -87
  80. data/test/liquid/output_test.rb +0 -116
  81. data/test/liquid/parsing_quirks_test.rb +0 -52
  82. data/test/liquid/regexp_test.rb +0 -44
  83. data/test/liquid/security_test.rb +0 -64
  84. data/test/liquid/standard_filter_test.rb +0 -263
  85. data/test/liquid/strainer_test.rb +0 -52
  86. data/test/liquid/tags/break_tag_test.rb +0 -16
  87. data/test/liquid/tags/continue_tag_test.rb +0 -16
  88. data/test/liquid/tags/for_tag_test.rb +0 -297
  89. data/test/liquid/tags/html_tag_test.rb +0 -63
  90. data/test/liquid/tags/if_else_tag_test.rb +0 -166
  91. data/test/liquid/tags/include_tag_test.rb +0 -166
  92. data/test/liquid/tags/increment_tag_test.rb +0 -24
  93. data/test/liquid/tags/raw_tag_test.rb +0 -24
  94. data/test/liquid/tags/standard_tag_test.rb +0 -295
  95. data/test/liquid/tags/statements_test.rb +0 -134
  96. data/test/liquid/tags/unless_else_tag_test.rb +0 -26
  97. data/test/liquid/template_test.rb +0 -146
  98. data/test/liquid/variable_test.rb +0 -186
  99. data/test/test_helper.rb +0 -29
  100. /data/{MIT-LICENSE → LICENSE} +0 -0
@@ -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 Array # :nodoc:
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 # :nodoc:
24
+ class Hash # :nodoc:
17
25
  def to_liquid
18
26
  self
19
27
  end
20
28
  end
21
29
 
22
- class Numeric # :nodoc:
30
+ class Numeric # :nodoc:
23
31
  def to_liquid
24
32
  self
25
33
  end
26
34
  end
27
35
 
28
- class Time # :nodoc:
36
+ class Range # :nodoc:
29
37
  def to_liquid
30
38
  self
31
39
  end
32
40
  end
33
41
 
34
- class DateTime < Date # :nodoc:
42
+ class Time # :nodoc:
35
43
  def to_liquid
36
44
  self
37
45
  end
38
46
  end
39
47
 
40
- class Date # :nodoc:
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 # :nodoc:
61
+ def to_liquid # :nodoc:
48
62
  self
49
63
  end
50
64
  end
@@ -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
- # Liquid::Template.file_system = Liquid::LocalFileSystem.new(template_path)
12
- # liquid = Liquid::Template.parse(template)
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(template_path, context)
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
- # file_system = Liquid::LocalFileSystem.new("/some/path")
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
- # file_system.full_path("mypartial") # => "/some/path/_mypartial.liquid"
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 = 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, context)
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.exists?(full_path)
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 template_path =~ /^[^.\/][a-zA-Z0-9_\/]+$/
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), "_#{File.basename(template_path)}.liquid")
65
+ File.join(root, File.dirname(template_path), @pattern % File.basename(template_path))
53
66
  else
54
- File.join(root, "_#{template_path}.liquid")
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) =~ /^#{File.expand_path(root)}/
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
@@ -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
@@ -1,10 +1,11 @@
1
- module Liquid
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
@@ -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