liquid 3.0.6 → 5.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (113) hide show
  1. checksums.yaml +5 -5
  2. data/History.md +243 -58
  3. data/README.md +43 -4
  4. data/lib/liquid/block.rb +57 -123
  5. data/lib/liquid/block_body.rb +217 -85
  6. data/lib/liquid/condition.rb +92 -45
  7. data/lib/liquid/context.rb +154 -89
  8. data/lib/liquid/document.rb +57 -9
  9. data/lib/liquid/drop.rb +20 -17
  10. data/lib/liquid/errors.rb +27 -29
  11. data/lib/liquid/expression.rb +32 -20
  12. data/lib/liquid/extensions.rb +21 -7
  13. data/lib/liquid/file_system.rb +17 -15
  14. data/lib/liquid/forloop_drop.rb +92 -0
  15. data/lib/liquid/i18n.rb +10 -8
  16. data/lib/liquid/interrupts.rb +4 -3
  17. data/lib/liquid/lexer.rb +37 -26
  18. data/lib/liquid/locales/en.yml +13 -6
  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 +30 -18
  22. data/lib/liquid/parser_switching.rb +20 -6
  23. data/lib/liquid/partial_cache.rb +24 -0
  24. data/lib/liquid/profiler/hooks.rb +26 -14
  25. data/lib/liquid/profiler.rb +72 -92
  26. data/lib/liquid/range_lookup.rb +28 -3
  27. data/lib/liquid/registers.rb +51 -0
  28. data/lib/liquid/resource_limits.rb +62 -0
  29. data/lib/liquid/standardfilters.rb +715 -132
  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 +35 -12
  36. data/lib/liquid/tags/assign.rb +57 -18
  37. data/lib/liquid/tags/break.rb +15 -5
  38. data/lib/liquid/tags/capture.rb +24 -18
  39. data/lib/liquid/tags/case.rb +79 -30
  40. data/lib/liquid/tags/comment.rb +19 -4
  41. data/lib/liquid/tags/continue.rb +16 -12
  42. data/lib/liquid/tags/cycle.rb +47 -27
  43. data/lib/liquid/tags/decrement.rb +23 -24
  44. data/lib/liquid/tags/echo.rb +41 -0
  45. data/lib/liquid/tags/for.rb +155 -124
  46. data/lib/liquid/tags/if.rb +97 -63
  47. data/lib/liquid/tags/ifchanged.rb +11 -12
  48. data/lib/liquid/tags/include.rb +82 -73
  49. data/lib/liquid/tags/increment.rb +23 -17
  50. data/lib/liquid/tags/inline_comment.rb +43 -0
  51. data/lib/liquid/tags/raw.rb +50 -8
  52. data/lib/liquid/tags/render.rb +109 -0
  53. data/lib/liquid/tags/table_row.rb +57 -41
  54. data/lib/liquid/tags/unless.rb +38 -20
  55. data/lib/liquid/template.rb +71 -103
  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 +63 -9
  60. data/lib/liquid/variable.rb +74 -56
  61. data/lib/liquid/variable_lookup.rb +31 -15
  62. data/lib/liquid/version.rb +3 -1
  63. data/lib/liquid.rb +27 -12
  64. metadata +30 -106
  65. data/lib/liquid/module_ex.rb +0 -62
  66. data/lib/liquid/strainer.rb +0 -59
  67. data/lib/liquid/token.rb +0 -18
  68. data/test/fixtures/en_locale.yml +0 -9
  69. data/test/integration/assign_test.rb +0 -48
  70. data/test/integration/blank_test.rb +0 -106
  71. data/test/integration/capture_test.rb +0 -50
  72. data/test/integration/context_test.rb +0 -32
  73. data/test/integration/drop_test.rb +0 -271
  74. data/test/integration/error_handling_test.rb +0 -207
  75. data/test/integration/filter_test.rb +0 -138
  76. data/test/integration/hash_ordering_test.rb +0 -23
  77. data/test/integration/output_test.rb +0 -124
  78. data/test/integration/parsing_quirks_test.rb +0 -116
  79. data/test/integration/render_profiling_test.rb +0 -154
  80. data/test/integration/security_test.rb +0 -64
  81. data/test/integration/standard_filter_test.rb +0 -396
  82. data/test/integration/tags/break_tag_test.rb +0 -16
  83. data/test/integration/tags/continue_tag_test.rb +0 -16
  84. data/test/integration/tags/for_tag_test.rb +0 -375
  85. data/test/integration/tags/if_else_tag_test.rb +0 -190
  86. data/test/integration/tags/include_tag_test.rb +0 -234
  87. data/test/integration/tags/increment_tag_test.rb +0 -24
  88. data/test/integration/tags/raw_tag_test.rb +0 -25
  89. data/test/integration/tags/standard_tag_test.rb +0 -297
  90. data/test/integration/tags/statements_test.rb +0 -113
  91. data/test/integration/tags/table_row_test.rb +0 -63
  92. data/test/integration/tags/unless_else_tag_test.rb +0 -26
  93. data/test/integration/template_test.rb +0 -182
  94. data/test/integration/variable_test.rb +0 -82
  95. data/test/test_helper.rb +0 -89
  96. data/test/unit/block_unit_test.rb +0 -55
  97. data/test/unit/condition_unit_test.rb +0 -149
  98. data/test/unit/context_unit_test.rb +0 -492
  99. data/test/unit/file_system_unit_test.rb +0 -35
  100. data/test/unit/i18n_unit_test.rb +0 -37
  101. data/test/unit/lexer_unit_test.rb +0 -48
  102. data/test/unit/module_ex_unit_test.rb +0 -87
  103. data/test/unit/parser_unit_test.rb +0 -82
  104. data/test/unit/regexp_unit_test.rb +0 -44
  105. data/test/unit/strainer_unit_test.rb +0 -69
  106. data/test/unit/tag_unit_test.rb +0 -16
  107. data/test/unit/tags/case_tag_unit_test.rb +0 -10
  108. data/test/unit/tags/for_tag_unit_test.rb +0 -13
  109. data/test/unit/tags/if_tag_unit_test.rb +0 -8
  110. data/test/unit/template_unit_test.rb +0 -69
  111. data/test/unit/tokenizer_unit_test.rb +0 -38
  112. data/test/unit/variable_unit_test.rb +0 -145
  113. /data/{MIT-LICENSE → LICENSE} +0 -0
data/lib/liquid/drop.rb CHANGED
@@ -1,7 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'set'
2
4
 
3
5
  module Liquid
4
-
5
6
  # A drop in liquid is a class which allows you to export DOM like things to liquid.
6
7
  # Methods of drops are callable.
7
8
  # The main use for liquid drops is to implement lazy loaded objects.
@@ -19,28 +20,27 @@ module Liquid
19
20
  # tmpl = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {%endfor%} ' )
20
21
  # tmpl.render('product' => ProductDrop.new ) # will invoke top_sales query.
21
22
  #
22
- # Your drop can either implement the methods sans any parameters or implement the before_method(name) method which is a
23
- # catch all.
23
+ # Your drop can either implement the methods sans any parameters
24
+ # or implement the liquid_method_missing(name) method which is a catch all.
24
25
  class Drop
25
26
  attr_writer :context
26
27
 
27
- EMPTY_STRING = ''.freeze
28
-
29
28
  # Catch all for the method
30
- def before_method(method)
31
- nil
29
+ def liquid_method_missing(method)
30
+ return nil unless @context&.strict_variables
31
+ raise Liquid::UndefinedDropMethod, "undefined method #{method}"
32
32
  end
33
33
 
34
34
  # called by liquid to invoke a drop
35
35
  def invoke_drop(method_or_key)
36
- if method_or_key && method_or_key != EMPTY_STRING && self.class.invokable?(method_or_key)
36
+ if self.class.invokable?(method_or_key)
37
37
  send(method_or_key)
38
38
  else
39
- before_method(method_or_key)
39
+ liquid_method_missing(method_or_key)
40
40
  end
41
41
  end
42
42
 
43
- def has_key?(name)
43
+ def key?(_name)
44
44
  true
45
45
  end
46
46
 
@@ -56,22 +56,25 @@ module Liquid
56
56
  self.class.name
57
57
  end
58
58
 
59
- alias :[] :invoke_drop
60
-
61
- private
59
+ alias_method :[], :invoke_drop
62
60
 
63
61
  # Check for method existence without invoking respond_to?, which creates symbols
64
62
  def self.invokable?(method_name)
65
- unless @invokable_methods
63
+ invokable_methods.include?(method_name.to_s)
64
+ end
65
+
66
+ def self.invokable_methods
67
+ @invokable_methods ||= begin
66
68
  blacklist = Liquid::Drop.public_instance_methods + [:each]
69
+
67
70
  if include?(Enumerable)
68
71
  blacklist += Enumerable.public_instance_methods
69
- blacklist -= [:sort, :count, :first, :min, :max, :include?]
72
+ blacklist -= [:sort, :count, :first, :min, :max]
70
73
  end
74
+
71
75
  whitelist = [:to_liquid] + (public_instance_methods - blacklist)
72
- @invokable_methods = Set.new(whitelist.map(&:to_s))
76
+ Set.new(whitelist.map(&:to_s))
73
77
  end
74
- @invokable_methods.include?(method_name.to_s)
75
78
  end
76
79
  end
77
80
  end
data/lib/liquid/errors.rb CHANGED
@@ -1,10 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  class Error < ::StandardError
3
5
  attr_accessor :line_number
6
+ attr_accessor :template_name
4
7
  attr_accessor :markup_context
5
8
 
6
- def to_s(with_prefix=true)
7
- str = ""
9
+ def to_s(with_prefix = true)
10
+ str = +""
8
11
  str << message_prefix if with_prefix
9
12
  str << super()
10
13
 
@@ -16,32 +19,20 @@ module Liquid
16
19
  str
17
20
  end
18
21
 
19
- def set_line_number_from_token(token)
20
- return unless token.respond_to?(:line_number)
21
- return if self.line_number
22
- self.line_number = token.line_number
23
- end
24
-
25
- def self.render(e)
26
- if e.is_a?(Liquid::Error)
27
- e.to_s
28
- else
29
- "Liquid error: #{e.to_s}"
30
- end
31
- end
32
-
33
22
  private
34
23
 
35
24
  def message_prefix
36
- str = ""
37
- if is_a?(SyntaxError)
38
- str << "Liquid syntax error"
25
+ str = +""
26
+ str << if is_a?(SyntaxError)
27
+ "Liquid syntax error"
39
28
  else
40
- str << "Liquid error"
29
+ "Liquid error"
41
30
  end
42
31
 
43
32
  if line_number
44
- str << " (line #{line_number})"
33
+ str << " ("
34
+ str << template_name << " " if template_name
35
+ str << "line " << line_number.to_s << ")"
45
36
  end
46
37
 
47
38
  str << ": "
@@ -49,12 +40,19 @@ module Liquid
49
40
  end
50
41
  end
51
42
 
52
- class ArgumentError < Error; end
53
- class ContextError < Error; end
54
- class FileSystemError < Error; end
55
- class StandardError < Error; end
56
- class SyntaxError < Error; end
57
- class StackLevelError < Error; end
58
- class TaintedError < Error; end
59
- class MemoryError < Error; end
43
+ ArgumentError = Class.new(Error)
44
+ ContextError = Class.new(Error)
45
+ FileSystemError = Class.new(Error)
46
+ StandardError = Class.new(Error)
47
+ SyntaxError = Class.new(Error)
48
+ StackLevelError = Class.new(Error)
49
+ MemoryError = Class.new(Error)
50
+ ZeroDivisionError = Class.new(Error)
51
+ FloatDomainError = Class.new(Error)
52
+ UndefinedVariable = Class.new(Error)
53
+ UndefinedDropMethod = Class.new(Error)
54
+ UndefinedFilter = Class.new(Error)
55
+ MethodOverrideError = Class.new(Error)
56
+ DisabledError = Class.new(Error)
57
+ InternalError = Class.new(Error)
60
58
  end
@@ -1,33 +1,45 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  class Expression
3
5
  LITERALS = {
4
- nil => nil, 'nil'.freeze => nil, 'null'.freeze => nil, ''.freeze => nil,
5
- 'true'.freeze => true,
6
- 'false'.freeze => false,
7
- 'blank'.freeze => :blank?,
8
- 'empty'.freeze => :empty?
9
- }
6
+ nil => nil, 'nil' => nil, 'null' => nil, '' => nil,
7
+ 'true' => true,
8
+ 'false' => false,
9
+ 'blank' => '',
10
+ 'empty' => ''
11
+ }.freeze
12
+
13
+ INTEGERS_REGEX = /\A(-?\d+)\z/
14
+ FLOATS_REGEX = /\A(-?\d[\d\.]+)\z/
15
+
16
+ # Use an atomic group (?>...) to avoid pathological backtracing from
17
+ # malicious input as described in https://github.com/Shopify/liquid/issues/1357
18
+ RANGES_REGEX = /\A\(\s*(?>(\S+)\s*\.\.)\s*(\S+)\s*\)\z/
10
19
 
11
20
  def self.parse(markup)
12
- if LITERALS.key?(markup)
13
- LITERALS[markup]
21
+ return nil unless markup
22
+
23
+ markup = markup.strip
24
+ if (markup.start_with?('"') && markup.end_with?('"')) ||
25
+ (markup.start_with?("'") && markup.end_with?("'"))
26
+ return markup[1..-2]
27
+ end
28
+
29
+ case markup
30
+ when INTEGERS_REGEX
31
+ Regexp.last_match(1).to_i
32
+ when RANGES_REGEX
33
+ RangeLookup.parse(Regexp.last_match(1), Regexp.last_match(2))
34
+ when FLOATS_REGEX
35
+ Regexp.last_match(1).to_f
14
36
  else
15
- case markup
16
- when /\A'(.*)'\z/m # Single quoted strings
17
- $1
18
- when /\A"(.*)"\z/m # Double quoted strings
19
- $1
20
- when /\A(-?\d+)\z/ # Integer and floats
21
- $1.to_i
22
- when /\A\((\S+)\.\.(\S+)\)\z/ # Ranges
23
- RangeLookup.parse($1, $2)
24
- when /\A(-?\d[\d\.]+)\z/ # Floats
25
- $1.to_f
37
+ if LITERALS.key?(markup)
38
+ LITERALS[markup]
26
39
  else
27
40
  VariableLookup.parse(markup)
28
41
  end
29
42
  end
30
43
  end
31
-
32
44
  end
33
45
  end
@@ -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,10 +28,10 @@ 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")
30
32
  #
31
- # file_system.full_path("mypartial") # => "/some/path/_mypartial.liquid"
32
- # file_system.full_path("dir/mypartial") # => "/some/path/dir/_mypartial.liquid"
33
+ # file_system.full_path("mypartial") # => "/some/path/_mypartial.liquid"
34
+ # file_system.full_path("dir/mypartial") # => "/some/path/dir/_mypartial.liquid"
33
35
  #
34
36
  # Optionally in the second argument you can specify a custom pattern for template filenames.
35
37
  # The Kernel::sprintf format specification is used.
@@ -37,35 +39,35 @@ module Liquid
37
39
  #
38
40
  # Example:
39
41
  #
40
- # file_system = Liquid::LocalFileSystem.new("/some/path", "%s.html")
42
+ # file_system = Liquid::LocalFileSystem.new("/some/path", "%s.html")
41
43
  #
42
- # file_system.full_path("index") # => "/some/path/index.html"
44
+ # file_system.full_path("index") # => "/some/path/index.html"
43
45
  #
44
46
  class LocalFileSystem
45
47
  attr_accessor :root
46
48
 
47
- def initialize(root, pattern = "_%s.liquid".freeze)
48
- @root = root
49
+ def initialize(root, pattern = "_%s.liquid")
50
+ @root = root
49
51
  @pattern = pattern
50
52
  end
51
53
 
52
- def read_template_file(template_path, context)
54
+ def read_template_file(template_path)
53
55
  full_path = full_path(template_path)
54
- 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)
55
57
 
56
58
  File.read(full_path)
57
59
  end
58
60
 
59
61
  def full_path(template_path)
60
- raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ /\A[^.\/][a-zA-Z0-9_\/]+\z/
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?('/'.freeze)
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)
66
68
  end
67
69
 
68
- raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path) =~ /\A#{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))
69
71
 
70
72
  full_path
71
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 CHANGED
@@ -1,11 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'yaml'
2
4
 
3
5
  module Liquid
4
6
  class I18n
5
- DEFAULT_LOCALE = File.join(File.expand_path(File.dirname(__FILE__)), "locales", "en.yml")
7
+ DEFAULT_LOCALE = File.join(File.expand_path(__dir__), "locales", "en.yml")
6
8
 
7
- class TranslationError < StandardError
8
- end
9
+ TranslationError = Class.new(StandardError)
9
10
 
10
11
  attr_reader :path
11
12
 
@@ -23,16 +24,17 @@ module Liquid
23
24
  end
24
25
 
25
26
  private
27
+
26
28
  def interpolate(name, vars)
27
- name.gsub(/%\{(\w+)\}/) {
29
+ name.gsub(/%\{(\w+)\}/) do
28
30
  # raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym]
29
- "#{vars[$1.to_sym]}"
30
- }
31
+ (vars[Regexp.last_match(1).to_sym]).to_s
32
+ end
31
33
  end
32
34
 
33
35
  def deep_fetch_translation(name)
34
- name.split('.'.freeze).reduce(locale) do |level, cur|
35
- level[cur] or raise TranslationError, "Translation for #{name} does not exist in locale #{path}"
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
@@ -1,11 +1,12 @@
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
- @message = message || "interrupt".freeze
8
+ def initialize(message = nil)
9
+ @message = message || "interrupt"
9
10
  end
10
11
  end
11
12
 
data/lib/liquid/lexer.rb CHANGED
@@ -1,43 +1,54 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "strscan"
2
4
  module Liquid
3
5
  class Lexer
4
6
  SPECIALS = {
5
- '|'.freeze => :pipe,
6
- '.'.freeze => :dot,
7
- ':'.freeze => :colon,
8
- ','.freeze => :comma,
9
- '['.freeze => :open_square,
10
- ']'.freeze => :close_square,
11
- '('.freeze => :open_round,
12
- ')'.freeze => :close_round
13
- }
14
- IDENTIFIER = /[\w\-?!]+/
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-]*\??/
15
19
  SINGLE_STRING_LITERAL = /'[^\']*'/
16
20
  DOUBLE_STRING_LITERAL = /"[^\"]*"/
17
- NUMBER_LITERAL = /-?\d+(\.\d+)?/
18
- DOTDOT = /\.\./
19
- COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains/
21
+ NUMBER_LITERAL = /-?\d+(\.\d+)?/
22
+ DOTDOT = /\.\./
23
+ COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/
24
+ WHITESPACE_OR_NOTHING = /\s*/
20
25
 
21
26
  def initialize(input)
22
- @ss = StringScanner.new(input.rstrip)
27
+ @ss = StringScanner.new(input)
23
28
  end
24
29
 
25
30
  def tokenize
26
31
  @output = []
27
32
 
28
- while !@ss.eos?
29
- @ss.skip(/\s*/)
30
- tok = case
31
- when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t]
32
- when t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t]
33
- when t = @ss.scan(DOUBLE_STRING_LITERAL) then [:string, t]
34
- when t = @ss.scan(NUMBER_LITERAL) then [:number, t]
35
- when t = @ss.scan(IDENTIFIER) then [:id, t]
36
- when t = @ss.scan(DOTDOT) then [:dotdot, t]
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]
37
48
  else
38
- c = @ss.getch
39
- if s = SPECIALS[c]
40
- [s,c]
49
+ c = @ss.getch
50
+ if (s = SPECIALS[c])
51
+ [s, c]
41
52
  else
42
53
  raise SyntaxError, "Unexpected character #{c}"
43
54
  end
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  errors:
3
3
  syntax:
4
+ tag_unexpected_args: "Syntax Error in '%{tag}' - Valid syntax: %{tag}"
4
5
  assign: "Syntax Error in 'assign' - Valid syntax: assign [var] = [source]"
5
6
  capture: "Syntax Error in 'capture' - Valid syntax: capture [var]"
6
7
  case: "Syntax Error in 'case' - Valid syntax: case [condition]"
@@ -12,11 +13,17 @@
12
13
  for_invalid_attribute: "Invalid attribute in for loop. Valid attributes are limit and offset"
13
14
  if: "Syntax Error in tag 'if' - Valid syntax: if [expression]"
14
15
  include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]"
15
- unknown_tag: "Unknown tag '%{tag}'"
16
- invalid_delimiter: "'end' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
17
- unexpected_else: "%{block_name} tag does not expect else tag"
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"
18
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}'"
19
25
  variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}"
20
- tag_never_closed: "'%{block_name}' tag was never closed"
21
- meta_syntax_error: "Liquid syntax error: #{e.message}"
22
- table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
26
+ argument:
27
+ include: "Argument error in tag 'include' - Illegal template name"
28
+ disabled:
29
+ tag: "usage is not allowed in this context"