liquid 2.6.3 → 5.4.0

Sign up to get free protection for your applications and to get access to all the features.
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