liquid 3.0.6 → 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 (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"