liquid 5.8.7 → 5.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b3b75c321445b52cb025c14359defc6f1c5c38662f4cc49fdb25665578d7e0bd
4
- data.tar.gz: c8b444e6a3848fbe278c8208feca605b788417aba56d2091e39cc6cf427f703b
3
+ metadata.gz: 112da43191fdf32af9afb1ad6322ff2fab8b22b6d658073980ba236aa1834213
4
+ data.tar.gz: b7ab5c91bc2f65b91a782027eedd8374f1f46629c791d6b2c07a8ba2c07d7314
5
5
  SHA512:
6
- metadata.gz: 44360766b328bd38396bf8a0fa0a37f6808f955804fb0208d415d01ef5370ad3504adb8fa204e1df16fb28917e96588a0d51bcb21a46607a0c350b1434f7ea35
7
- data.tar.gz: 59de1d715d6a9dcff97600e160c0827bd83a6e4f36f92d546c8f6d7734103ffcbe0477d829a8a3c397db8bde5ca56fab13b981997e1222f734794c23303e29e4
6
+ metadata.gz: c941dc92d57cf97c30a6db6eb206e7f00b780070b8dc790def3a8be06a3714562813b0ca8da34d5745be4432c839cf2f5946557d103ae0e56ffe5822b4ebf1e6
7
+ data.tar.gz: 9491a34a69273c3cebe65208f935007e68dbd81a8b32913ebc7d0056863c9f3a30b6dff9d1da3b37af66e472aedf60c6979e22d5f500ba6d24bd4f8637449a3f
data/History.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # Liquid Change Log
2
2
 
3
+ ## 5.9.0
4
+ * Introduce `:rigid` error mode for stricter, safer parsing of all tags [CP Clermont, Guilherme Carreiro]
5
+
3
6
  ## 5.8.7
4
7
  * Expose body content in the `Doc` tag [James Meng]
5
8
 
data/README.md CHANGED
@@ -99,14 +99,14 @@ Setting the error mode of Liquid lets you specify how strictly you want your tem
99
99
  Normally the parser is very lax and will accept almost anything without error. Unfortunately this can make
100
100
  it very hard to debug and can lead to unexpected behaviour.
101
101
 
102
- Liquid also comes with a stricter parser that can be used when editing templates to give better error messages
102
+ Liquid also comes with different parsers that can be used when editing templates to give better error messages
103
103
  when templates are invalid. You can enable this new parser like this:
104
104
 
105
105
  ```ruby
106
- Liquid::Environment.default.error_mode = :strict
107
- Liquid::Environment.default.error_mode = :strict # Raises a SyntaxError when invalid syntax is used
108
- Liquid::Environment.default.error_mode = :warn # Adds strict errors to template.errors but continues as normal
109
- Liquid::Environment.default.error_mode = :lax # The default mode, accepts almost anything.
106
+ Liquid::Environment.default.error_mode = :rigid # Raises a SyntaxError when invalid syntax is used in all tags
107
+ Liquid::Environment.default.error_mode = :strict # Raises a SyntaxError when invalid syntax is used in some tags
108
+ Liquid::Environment.default.error_mode = :warn # Adds strict errors to template.errors but continues as normal
109
+ Liquid::Environment.default.error_mode = :lax # The default mode, accepts almost anything.
110
110
  ```
111
111
 
112
112
  If you want to set the error mode only on specific templates you can pass `:error_mode` as an option to `parse`:
@@ -48,8 +48,8 @@ module Liquid
48
48
  @@operators
49
49
  end
50
50
 
51
- def self.parse_expression(parse_context, markup)
52
- @@method_literals[markup] || parse_context.parse_expression(markup)
51
+ def self.parse_expression(parse_context, markup, safe: false)
52
+ @@method_literals[markup] || parse_context.parse_expression(markup, safe: safe)
53
53
  end
54
54
 
55
55
  attr_reader :attachment, :child_condition
@@ -34,7 +34,7 @@ module Liquid
34
34
  # @param file_system The default file system that is used
35
35
  # to load templates from.
36
36
  # @param error_mode [Symbol] The default error mode for all templates
37
- # (either :strict, :warn, or :lax).
37
+ # (either :rigid, :strict, :warn, or :lax).
38
38
  # @param exception_renderer [Proc] The exception renderer that is used to
39
39
  # render exceptions.
40
40
  # @yieldparam environment [Environment] The environment instance that is being built.
@@ -28,6 +28,10 @@ module Liquid
28
28
  FLOAT_REGEX = /\A(-?\d+)\.\d+\z/
29
29
 
30
30
  class << self
31
+ def safe_parse(parser, ss = StringScanner.new(""), cache = nil)
32
+ parse(parser.expression, ss, cache)
33
+ end
34
+
31
35
  def parse(markup, ss = StringScanner.new(""), cache = nil)
32
36
  return unless markup
33
37
 
@@ -20,6 +20,7 @@
20
20
  invalid_template_encoding: "Invalid template encoding"
21
21
  render: "Syntax error in tag 'render' - Template name must be a quoted string"
22
22
  table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
23
+ table_row_invalid_attribute: "Invalid attribute '%{attribute}' in tablerow loop. Valid attributes are cols, limit, offset, and range"
23
24
  tag_never_closed: "'%{block_name}' tag was never closed"
24
25
  tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
25
26
  unexpected_else: "%{block_name} tag does not expect 'else' tag"
@@ -50,7 +50,22 @@ module Liquid
50
50
  )
51
51
  end
52
52
 
53
- def parse_expression(markup)
53
+ def safe_parse_expression(parser)
54
+ Expression.safe_parse(parser, @string_scanner, @expression_cache)
55
+ end
56
+
57
+ def parse_expression(markup, safe: false)
58
+ if !safe && @error_mode == :rigid
59
+ # parse_expression is a widely used API. To maintain backward
60
+ # compatibility while raising awareness about rigid parser standards,
61
+ # the safe flag supports API users make a deliberate decision.
62
+ #
63
+ # In rigid mode, markup MUST come from a string returned by the parser
64
+ # (e.g., parser.expression). We're not calling the parser here to
65
+ # prevent redundant parser overhead.
66
+ raise Liquid::InternalError, "unsafe parse_expression cannot be used in rigid mode"
67
+ end
68
+
54
69
  Expression.parse(markup, @string_scanner, @expression_cache)
55
70
  end
56
71
 
@@ -2,10 +2,22 @@
2
2
 
3
3
  module Liquid
4
4
  module ParserSwitching
5
+ # Do not use this.
6
+ #
7
+ # It's basically doing the same thing the {#parse_with_selected_parser},
8
+ # except this will try the strict parser regardless of the error mode,
9
+ # and fall back to the lax parser if the error mode is lax or warn,
10
+ # except when in rigid mode where it uses the rigid parser.
11
+ #
12
+ # @deprecated Use {#parse_with_selected_parser} instead.
5
13
  def strict_parse_with_error_mode_fallback(markup)
14
+ return rigid_parse_with_error_context(markup) if rigid_mode?
15
+
6
16
  strict_parse_with_error_context(markup)
7
17
  rescue SyntaxError => e
8
18
  case parse_context.error_mode
19
+ when :rigid
20
+ raise
9
21
  when :strict
10
22
  raise
11
23
  when :warn
@@ -16,11 +28,12 @@ module Liquid
16
28
 
17
29
  def parse_with_selected_parser(markup)
18
30
  case parse_context.error_mode
31
+ when :rigid then rigid_parse_with_error_context(markup)
19
32
  when :strict then strict_parse_with_error_context(markup)
20
33
  when :lax then lax_parse(markup)
21
34
  when :warn
22
35
  begin
23
- strict_parse_with_error_context(markup)
36
+ rigid_parse_with_error_context(markup)
24
37
  rescue SyntaxError => e
25
38
  parse_context.warnings << e
26
39
  lax_parse(markup)
@@ -28,8 +41,20 @@ module Liquid
28
41
  end
29
42
  end
30
43
 
44
+ def rigid_mode?
45
+ parse_context.error_mode == :rigid
46
+ end
47
+
31
48
  private
32
49
 
50
+ def rigid_parse_with_error_context(markup)
51
+ rigid_parse(markup)
52
+ rescue SyntaxError => e
53
+ e.line_number = line_number
54
+ e.markup_context = markup_context(markup)
55
+ raise e
56
+ end
57
+
33
58
  def strict_parse_with_error_context(markup)
34
59
  strict_parse(markup)
35
60
  rescue SyntaxError => e
data/lib/liquid/tag.rb CHANGED
@@ -68,8 +68,12 @@ module Liquid
68
68
 
69
69
  private
70
70
 
71
- def parse_expression(markup)
72
- parse_context.parse_expression(markup)
71
+ def safe_parse_expression(parser)
72
+ parse_context.safe_parse_expression(parser)
73
+ end
74
+
75
+ def parse_expression(markup, safe: false)
76
+ parse_context.parse_expression(markup, safe: safe)
73
77
  end
74
78
  end
75
79
  end
@@ -9,6 +9,10 @@ module Liquid
9
9
  # Creates a new variable.
10
10
  # @liquid_description
11
11
  # You can create variables of any [basic type](/docs/api/liquid/basics#types), [object](/docs/api/liquid/objects), or object property.
12
+ #
13
+ # > Caution:
14
+ # > Predefined Liquid objects can be overridden by variables with the same name.
15
+ # > To make sure that you can access all Liquid objects, make sure that your variable name doesn't match a predefined object's name.
12
16
  # @liquid_syntax
13
17
  # {% assign variable_name = value %}
14
18
  # @liquid_syntax_keyword variable_name The name of the variable being created.
@@ -9,6 +9,10 @@ module Liquid
9
9
  # Creates a new variable with a string value.
10
10
  # @liquid_description
11
11
  # You can create complex strings with Liquid logic and variables.
12
+ #
13
+ # > Caution:
14
+ # > Predefined Liquid objects can be overridden by variables with the same name.
15
+ # > To make sure that you can access all Liquid objects, make sure that your variable name doesn't match a predefined object's name.
12
16
  # @liquid_syntax
13
17
  # {% capture variable %}
14
18
  # value
@@ -31,12 +31,7 @@ module Liquid
31
31
  def initialize(tag_name, markup, options)
32
32
  super
33
33
  @blocks = []
34
-
35
- if markup =~ Syntax
36
- @left = parse_expression(Regexp.last_match(1))
37
- else
38
- raise SyntaxError, options[:locale].t("errors.syntax.case")
39
- end
34
+ parse_with_selected_parser(markup)
40
35
  end
41
36
 
42
37
  def parse(tokens)
@@ -91,9 +86,50 @@ module Liquid
91
86
 
92
87
  private
93
88
 
89
+ def rigid_parse(markup)
90
+ parser = @parse_context.new_parser(markup)
91
+ @left = safe_parse_expression(parser)
92
+ parser.consume(:end_of_string)
93
+ end
94
+
95
+ def strict_parse(markup)
96
+ lax_parse(markup)
97
+ end
98
+
99
+ def lax_parse(markup)
100
+ if markup =~ Syntax
101
+ @left = parse_expression(Regexp.last_match(1))
102
+ else
103
+ raise SyntaxError, options[:locale].t("errors.syntax.case")
104
+ end
105
+ end
106
+
94
107
  def record_when_condition(markup)
95
108
  body = new_body
96
109
 
110
+ if rigid_mode?
111
+ parse_rigid_when(markup, body)
112
+ else
113
+ parse_lax_when(markup, body)
114
+ end
115
+ end
116
+
117
+ def parse_rigid_when(markup, body)
118
+ parser = @parse_context.new_parser(markup)
119
+
120
+ loop do
121
+ expr = safe_parse_expression(parser)
122
+ block = Condition.new(@left, '==', expr)
123
+ block.attach(body)
124
+ @blocks << block
125
+
126
+ break unless parser.id?('or') || parser.consume?(:comma)
127
+ end
128
+
129
+ parser.consume(:end_of_string)
130
+ end
131
+
132
+ def parse_lax_when(markup, body)
97
133
  while markup
98
134
  unless markup =~ WhenSyntax
99
135
  raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_when")
@@ -17,23 +17,13 @@ module Liquid
17
17
  class Cycle < Tag
18
18
  SimpleSyntax = /\A#{QuotedFragment}+/o
19
19
  NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om
20
+ UNNAMED_CYCLE_PATTERN = /\w+:0x\h{8}/
20
21
 
21
22
  attr_reader :variables
22
23
 
23
24
  def initialize(tag_name, markup, options)
24
25
  super
25
- case markup
26
- when NamedSyntax
27
- @variables = variables_from_string(Regexp.last_match(2))
28
- @name = parse_expression(Regexp.last_match(1))
29
- @is_named = true
30
- when SimpleSyntax
31
- @variables = variables_from_string(markup)
32
- @name = @variables.to_s
33
- @is_named = !@name.match?(/\w+:0x\h{8}/)
34
- else
35
- raise SyntaxError, options[:locale].t("errors.syntax.cycle")
36
- end
26
+ parse_with_selected_parser(markup)
37
27
  end
38
28
 
39
29
  def named?
@@ -65,19 +55,82 @@ module Liquid
65
55
 
66
56
  private
67
57
 
58
+ # cycle [name:] expression(, expression)*
59
+ def rigid_parse(markup)
60
+ p = @parse_context.new_parser(markup)
61
+
62
+ @variables = []
63
+
64
+ raise SyntaxError, options[:locale].t("errors.syntax.cycle") if p.look(:end_of_string)
65
+
66
+ first_expression = safe_parse_expression(p)
67
+ if p.look(:colon)
68
+ # cycle name: expr1, expr2, ...
69
+ @name = first_expression
70
+ @is_named = true
71
+ p.consume(:colon)
72
+ # After the colon, parse the first variable (required for named cycles)
73
+ @variables << maybe_dup_lookup(safe_parse_expression(p))
74
+ else
75
+ # cycle expr1, expr2, ...
76
+ @variables << maybe_dup_lookup(first_expression)
77
+ end
78
+
79
+ # Parse remaining comma-separated expressions
80
+ while p.consume?(:comma)
81
+ break if p.look(:end_of_string)
82
+
83
+ @variables << maybe_dup_lookup(safe_parse_expression(p))
84
+ end
85
+
86
+ p.consume(:end_of_string)
87
+
88
+ unless @is_named
89
+ @name = @variables.to_s
90
+ @is_named = !@name.match?(UNNAMED_CYCLE_PATTERN)
91
+ end
92
+ end
93
+
94
+ def strict_parse(markup)
95
+ lax_parse(markup)
96
+ end
97
+
98
+ def lax_parse(markup)
99
+ case markup
100
+ when NamedSyntax
101
+ @variables = variables_from_string(Regexp.last_match(2))
102
+ @name = parse_expression(Regexp.last_match(1))
103
+ @is_named = true
104
+ when SimpleSyntax
105
+ @variables = variables_from_string(markup)
106
+ @name = @variables.to_s
107
+ @is_named = !@name.match?(UNNAMED_CYCLE_PATTERN)
108
+ else
109
+ raise SyntaxError, options[:locale].t("errors.syntax.cycle")
110
+ end
111
+ end
112
+
68
113
  def variables_from_string(markup)
69
114
  markup.split(',').collect do |var|
70
115
  var =~ /\s*(#{QuotedFragment})\s*/o
71
116
  next unless Regexp.last_match(1)
72
117
 
73
- # Expression Parser returns cached objects, and we need to dup them to
74
- # start the cycle over for each new cycle call.
75
- # Liquid-C does not have a cache, so we don't need to dup the object.
76
118
  var = parse_expression(Regexp.last_match(1))
77
- var.is_a?(VariableLookup) ? var.dup : var
119
+ maybe_dup_lookup(var)
78
120
  end.compact
79
121
  end
80
122
 
123
+ # For backwards compatibility, whenever a lookup is used in an unnamed cycle,
124
+ # we make it so that the @variables.to_s produces different strings for cycles
125
+ # called with the same arguments (since @variables.to_s is used as the cycle counter key)
126
+ # This makes it so {% cycle a, b %} and {% cycle a, b %} have independent counters even if a and b share value.
127
+ # This is not true for literal values, {% cycle "a", "b" %} and {% cycle "a", "b" %} share the same counter.
128
+ # I was really scratching my head about this one, but migrating away from this would be more headache
129
+ # than it's worth. So we're keeping this quirk for now.
130
+ def maybe_dup_lookup(var)
131
+ var.is_a?(VariableLookup) ? var.dup : var
132
+ end
133
+
81
134
  class ParseTreeVisitor < Liquid::ParseTreeVisitor
82
135
  def children
83
136
  Array(@node.variables)
@@ -7,6 +7,10 @@ module Liquid
7
7
  # @liquid_name decrement
8
8
  # @liquid_summary
9
9
  # Creates a new variable, with a default value of -1, that's decreased by 1 with each subsequent call.
10
+ #
11
+ # > Caution:
12
+ # > Predefined Liquid objects can be overridden by variables with the same name.
13
+ # > To make sure that you can access all Liquid objects, make sure that your variable name doesn't match a predefined object's name.
10
14
  # @liquid_description
11
15
  # Variables that are declared with `decrement` are unique to the [layout](/themes/architecture/layouts), [template](/themes/architecture/templates),
12
16
  # or [section](/themes/architecture/sections) file that they're created in. However, the variable is shared across
@@ -20,8 +20,8 @@ module Liquid
20
20
  # @liquid_syntax_keyword variable The current item in the array.
21
21
  # @liquid_syntax_keyword array The array to iterate over.
22
22
  # @liquid_syntax_keyword expression The expression to render for each iteration.
23
- # @liquid_optional_param limit [number] The number of iterations to perform.
24
- # @liquid_optional_param offset [number] The 1-based index to start iterating at.
23
+ # @liquid_optional_param limit: [number] The number of iterations to perform.
24
+ # @liquid_optional_param offset: [number] The 1-based index to start iterating at.
25
25
  # @liquid_optional_param range [untyped] A custom numeric range to iterate over.
26
26
  # @liquid_optional_param reversed [untyped] Iterate in reverse order.
27
27
  class For < Block
@@ -93,7 +93,7 @@ module Liquid
93
93
  raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in')
94
94
 
95
95
  collection_name = p.expression
96
- @collection_name = parse_expression(collection_name)
96
+ @collection_name = parse_expression(collection_name, safe: true)
97
97
 
98
98
  @name = "#{@variable_name}-#{collection_name}"
99
99
  @reversed = p.id?('reversed')
@@ -104,13 +104,17 @@ module Liquid
104
104
  raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_attribute")
105
105
  end
106
106
  p.consume(:colon)
107
- set_attribute(attribute, p.expression)
107
+ set_attribute(attribute, p.expression, safe: true)
108
108
  end
109
109
  p.consume(:end_of_string)
110
110
  end
111
111
 
112
112
  private
113
113
 
114
+ def rigid_parse(markup)
115
+ strict_parse(markup)
116
+ end
117
+
114
118
  def collection_segment(context)
115
119
  offsets = context.registers[:for] ||= {}
116
120
 
@@ -174,16 +178,16 @@ module Liquid
174
178
  output
175
179
  end
176
180
 
177
- def set_attribute(key, expr)
181
+ def set_attribute(key, expr, safe: false)
178
182
  case key
179
183
  when 'offset'
180
184
  @from = if expr == 'continue'
181
185
  :continue
182
186
  else
183
- parse_expression(expr)
187
+ parse_expression(expr, safe: safe)
184
188
  end
185
189
  when 'limit'
186
- @limit = parse_expression(expr)
190
+ @limit = parse_expression(expr, safe: safe)
187
191
  end
188
192
  end
189
193
 
@@ -66,6 +66,10 @@ module Liquid
66
66
 
67
67
  private
68
68
 
69
+ def rigid_parse(markup)
70
+ strict_parse(markup)
71
+ end
72
+
69
73
  def push_block(tag, markup)
70
74
  block = if tag == 'else'
71
75
  ElseCondition.new
@@ -77,8 +81,8 @@ module Liquid
77
81
  block.attach(new_body)
78
82
  end
79
83
 
80
- def parse_expression(markup)
81
- Condition.parse_expression(parse_context, markup)
84
+ def parse_expression(markup, safe: false)
85
+ Condition.parse_expression(parse_context, markup, safe: safe)
82
86
  end
83
87
 
84
88
  def lax_parse(markup)
@@ -120,9 +124,9 @@ module Liquid
120
124
  end
121
125
 
122
126
  def parse_comparison(p)
123
- a = parse_expression(p.expression)
127
+ a = parse_expression(p.expression, safe: true)
124
128
  if (op = p.consume?(:comparison))
125
- b = parse_expression(p.expression)
129
+ b = parse_expression(p.expression, safe: true)
126
130
  Condition.new(a, op, b)
127
131
  else
128
132
  Condition.new(a)
@@ -27,24 +27,7 @@ module Liquid
27
27
 
28
28
  def initialize(tag_name, markup, options)
29
29
  super
30
-
31
- if markup =~ SYNTAX
32
-
33
- template_name = Regexp.last_match(1)
34
- variable_name = Regexp.last_match(3)
35
-
36
- @alias_name = Regexp.last_match(5)
37
- @variable_name_expr = variable_name ? parse_expression(variable_name) : nil
38
- @template_name_expr = parse_expression(template_name)
39
- @attributes = {}
40
-
41
- markup.scan(TagAttributes) do |key, value|
42
- @attributes[key] = parse_expression(value)
43
- end
44
-
45
- else
46
- raise SyntaxError, options[:locale].t("errors.syntax.include")
47
- end
30
+ parse_with_selected_parser(markup)
48
31
  end
49
32
 
50
33
  def parse(_tokens)
@@ -101,6 +84,49 @@ module Liquid
101
84
  alias_method :parse_context, :options
102
85
  private :parse_context
103
86
 
87
+ def rigid_parse(markup)
88
+ p = @parse_context.new_parser(markup)
89
+
90
+ @template_name_expr = safe_parse_expression(p)
91
+ @variable_name_expr = safe_parse_expression(p) if p.id?("for") || p.id?("with")
92
+ @alias_name = p.consume(:id) if p.id?("as")
93
+
94
+ p.consume?(:comma)
95
+
96
+ @attributes = {}
97
+ while p.look(:id)
98
+ key = p.consume
99
+ p.consume(:colon)
100
+ @attributes[key] = safe_parse_expression(p)
101
+ p.consume?(:comma)
102
+ end
103
+
104
+ p.consume(:end_of_string)
105
+ end
106
+
107
+ def strict_parse(markup)
108
+ lax_parse(markup)
109
+ end
110
+
111
+ def lax_parse(markup)
112
+ if markup =~ SYNTAX
113
+ template_name = Regexp.last_match(1)
114
+ variable_name = Regexp.last_match(3)
115
+
116
+ @alias_name = Regexp.last_match(5)
117
+ @variable_name_expr = variable_name ? parse_expression(variable_name) : nil
118
+ @template_name_expr = parse_expression(template_name)
119
+ @attributes = {}
120
+
121
+ markup.scan(TagAttributes) do |key, value|
122
+ @attributes[key] = parse_expression(value)
123
+ end
124
+
125
+ else
126
+ raise SyntaxError, options[:locale].t("errors.syntax.include")
127
+ end
128
+ end
129
+
104
130
  class ParseTreeVisitor < Liquid::ParseTreeVisitor
105
131
  def children
106
132
  [
@@ -7,6 +7,10 @@ module Liquid
7
7
  # @liquid_name increment
8
8
  # @liquid_summary
9
9
  # Creates a new variable, with a default value of 0, that's increased by 1 with each subsequent call.
10
+ #
11
+ # > Caution:
12
+ # > Predefined Liquid objects can be overridden by variables with the same name.
13
+ # > To make sure that you can access all Liquid objects, make sure that your variable name doesn't match a predefined object's name.
10
14
  # @liquid_description
11
15
  # Variables that are declared with `increment` are unique to the [layout](/themes/architecture/layouts), [template](/themes/architecture/templates),
12
16
  # or [section](/themes/architecture/sections) file that they're created in. However, the variable is shared across
@@ -35,22 +35,7 @@ module Liquid
35
35
 
36
36
  def initialize(tag_name, markup, options)
37
37
  super
38
-
39
- raise SyntaxError, options[:locale].t("errors.syntax.render") unless markup =~ SYNTAX
40
-
41
- template_name = Regexp.last_match(1)
42
- with_or_for = Regexp.last_match(3)
43
- variable_name = Regexp.last_match(4)
44
-
45
- @alias_name = Regexp.last_match(6)
46
- @variable_name_expr = variable_name ? parse_expression(variable_name) : nil
47
- @template_name_expr = parse_expression(template_name)
48
- @is_for_loop = (with_or_for == FOR)
49
-
50
- @attributes = {}
51
- markup.scan(TagAttributes) do |key, value|
52
- @attributes[key] = parse_expression(value)
53
- end
38
+ parse_with_selected_parser(markup)
54
39
  end
55
40
 
56
41
  def for_loop?
@@ -99,6 +84,55 @@ module Liquid
99
84
  output
100
85
  end
101
86
 
87
+ # render (string) (with|for expression)? (as id)? (key: value)*
88
+ def rigid_parse(markup)
89
+ p = @parse_context.new_parser(markup)
90
+
91
+ @template_name_expr = parse_expression(rigid_template_name(p), safe: true)
92
+ with_or_for = p.id?("for") || p.id?("with")
93
+ @variable_name_expr = safe_parse_expression(p) if with_or_for
94
+ @alias_name = p.consume(:id) if p.id?("as")
95
+ @is_for_loop = (with_or_for == FOR)
96
+
97
+ p.consume?(:comma)
98
+
99
+ @attributes = {}
100
+ while p.look(:id)
101
+ key = p.consume
102
+ p.consume(:colon)
103
+ @attributes[key] = safe_parse_expression(p)
104
+ p.consume?(:comma)
105
+ end
106
+
107
+ p.consume(:end_of_string)
108
+ end
109
+
110
+ def rigid_template_name(p)
111
+ p.consume(:string)
112
+ end
113
+
114
+ def strict_parse(markup)
115
+ lax_parse(markup)
116
+ end
117
+
118
+ def lax_parse(markup)
119
+ raise SyntaxError, options[:locale].t("errors.syntax.render") unless markup =~ SYNTAX
120
+
121
+ template_name = Regexp.last_match(1)
122
+ with_or_for = Regexp.last_match(3)
123
+ variable_name = Regexp.last_match(4)
124
+
125
+ @alias_name = Regexp.last_match(6)
126
+ @variable_name_expr = variable_name ? parse_expression(variable_name) : nil
127
+ @template_name_expr = parse_expression(template_name)
128
+ @is_for_loop = (with_or_for == FOR)
129
+
130
+ @attributes = {}
131
+ markup.scan(TagAttributes) do |key, value|
132
+ @attributes[key] = parse_expression(value)
133
+ end
134
+ end
135
+
102
136
  class ParseTreeVisitor < Liquid::ParseTreeVisitor
103
137
  def children
104
138
  [
@@ -19,17 +19,54 @@ module Liquid
19
19
  # @liquid_syntax_keyword variable The current item in the array.
20
20
  # @liquid_syntax_keyword array The array to iterate over.
21
21
  # @liquid_syntax_keyword expression The expression to render.
22
- # @liquid_optional_param cols [number] The number of columns that the table should have.
23
- # @liquid_optional_param limit [number] The number of iterations to perform.
24
- # @liquid_optional_param offset [number] The 1-based index to start iterating at.
22
+ # @liquid_optional_param cols: [number] The number of columns that the table should have.
23
+ # @liquid_optional_param limit: [number] The number of iterations to perform.
24
+ # @liquid_optional_param offset: [number] The 1-based index to start iterating at.
25
25
  # @liquid_optional_param range [untyped] A custom numeric range to iterate over.
26
26
  class TableRow < Block
27
27
  Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
28
+ ALLOWED_ATTRIBUTES = ['cols', 'limit', 'offset', 'range'].freeze
28
29
 
29
30
  attr_reader :variable_name, :collection_name, :attributes
30
31
 
31
32
  def initialize(tag_name, markup, options)
32
33
  super
34
+ parse_with_selected_parser(markup)
35
+ end
36
+
37
+ def rigid_parse(markup)
38
+ p = @parse_context.new_parser(markup)
39
+
40
+ @variable_name = p.consume(:id)
41
+
42
+ unless p.id?("in")
43
+ raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in")
44
+ end
45
+
46
+ @collection_name = safe_parse_expression(p)
47
+
48
+ p.consume?(:comma)
49
+
50
+ @attributes = {}
51
+ while p.look(:id)
52
+ key = p.consume
53
+ unless ALLOWED_ATTRIBUTES.include?(key)
54
+ raise SyntaxError, options[:locale].t("errors.syntax.table_row_invalid_attribute", attribute: key)
55
+ end
56
+
57
+ p.consume(:colon)
58
+ @attributes[key] = safe_parse_expression(p)
59
+ p.consume?(:comma)
60
+ end
61
+
62
+ p.consume(:end_of_string)
63
+ end
64
+
65
+ def strict_parse(markup)
66
+ lax_parse(markup)
67
+ end
68
+
69
+ def lax_parse(markup)
33
70
  if markup =~ Syntax
34
71
  @variable_name = Regexp.last_match(1)
35
72
  @collection_name = parse_expression(Regexp.last_match(2))
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Liquid
4
4
  # Templates are central to liquid.
5
- # Interpretating templates is a two step process. First you compile the
5
+ # Interpreting templates is a two step process. First you compile the
6
6
  # source code you got. During compile time some extensive error checking is performed.
7
7
  # your code should expect to get some SyntaxErrors.
8
8
  #
@@ -24,7 +24,8 @@ module Liquid
24
24
  # Sets how strict the parser should be.
25
25
  # :lax acts like liquid 2.5 and silently ignores malformed tags in most cases.
26
26
  # :warn is the default and will give deprecation warnings when invalid syntax is used.
27
- # :strict will enforce correct syntax.
27
+ # :strict enforces correct syntax for most tags
28
+ # :rigid enforces correct syntax for all tags
28
29
  def error_mode=(mode)
29
30
  Deprecations.warn("Template.error_mode=", "Environment#error_mode=")
30
31
  Environment.default.error_mode = mode
data/lib/liquid/utils.rb CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  module Liquid
4
4
  module Utils
5
+ DECIMAL_REGEX = /\A-?\d+\.\d+\z/
6
+ UNIX_TIMESTAMP_REGEX = /\A\d+\z/
7
+
5
8
  def self.slice_collection(collection, from, to)
6
9
  if (from != 0 || !to.nil?) && collection.respond_to?(:load_slice)
7
10
  collection.load_slice(from, to)
@@ -52,7 +55,7 @@ module Liquid
52
55
  when Numeric
53
56
  obj
54
57
  when String
55
- /\A-?\d+\.\d+\z/.match?(obj.strip) ? BigDecimal(obj) : obj.to_i
58
+ DECIMAL_REGEX.match?(obj.strip) ? BigDecimal(obj) : obj.to_i
56
59
  else
57
60
  if obj.respond_to?(:to_number)
58
61
  obj.to_number
@@ -73,7 +76,7 @@ module Liquid
73
76
  case obj
74
77
  when 'now', 'today'
75
78
  Time.now
76
- when /\A\d+\z/, Integer
79
+ when UNIX_TIMESTAMP_REGEX, Integer
77
80
  Time.at(obj.to_i)
78
81
  when String
79
82
  Time.parse(obj)
@@ -54,7 +54,7 @@ module Liquid
54
54
  next unless f =~ /\w+/
55
55
  filtername = Regexp.last_match(0)
56
56
  filterargs = f.scan(FilterArgsRegex).flatten
57
- @filters << parse_filter_expressions(filtername, filterargs)
57
+ @filters << lax_parse_filter_expressions(filtername, filterargs)
58
58
  end
59
59
  end
60
60
  end
@@ -65,15 +65,26 @@ module Liquid
65
65
 
66
66
  return if p.look(:end_of_string)
67
67
 
68
- @name = parse_context.parse_expression(p.expression)
68
+ @name = parse_context.safe_parse_expression(p)
69
69
  while p.consume?(:pipe)
70
70
  filtername = p.consume(:id)
71
71
  filterargs = p.consume?(:colon) ? parse_filterargs(p) : Const::EMPTY_ARRAY
72
- @filters << parse_filter_expressions(filtername, filterargs)
72
+ @filters << lax_parse_filter_expressions(filtername, filterargs)
73
73
  end
74
74
  p.consume(:end_of_string)
75
75
  end
76
76
 
77
+ def rigid_parse(markup)
78
+ @filters = []
79
+ p = @parse_context.new_parser(markup)
80
+
81
+ return if p.look(:end_of_string)
82
+
83
+ @name = parse_context.safe_parse_expression(p)
84
+ @filters << rigid_parse_filter_expressions(p) while p.consume?(:pipe)
85
+ p.consume(:end_of_string)
86
+ end
87
+
77
88
  def parse_filterargs(p)
78
89
  # first argument
79
90
  filterargs = [p.argument]
@@ -122,7 +133,7 @@ module Liquid
122
133
 
123
134
  private
124
135
 
125
- def parse_filter_expressions(filter_name, unparsed_args)
136
+ def lax_parse_filter_expressions(filter_name, unparsed_args)
126
137
  filter_args = []
127
138
  keyword_args = nil
128
139
  unparsed_args.each do |a|
@@ -138,6 +149,46 @@ module Liquid
138
149
  result
139
150
  end
140
151
 
152
+ # Surprisingly, positional and keyword arguments can be mixed.
153
+ #
154
+ # filter = filtername [":" filterargs?]
155
+ # filterargs = argument ("," argument)*
156
+ # argument = (positional_argument | keyword_argument)
157
+ # positional_argument = expression
158
+ # keyword_argument = id ":" expression
159
+ def rigid_parse_filter_expressions(p)
160
+ filtername = p.consume(:id)
161
+ filter_args = []
162
+ keyword_args = {}
163
+
164
+ if p.consume?(:colon)
165
+ # Parse first argument (no leading comma)
166
+ argument(p, filter_args, keyword_args) unless end_of_arguments?(p)
167
+
168
+ # Parse remaining arguments (with leading commas) and optional trailing comma
169
+ argument(p, filter_args, keyword_args) while p.consume?(:comma) && !end_of_arguments?(p)
170
+ end
171
+
172
+ result = [filtername, filter_args]
173
+ result << keyword_args unless keyword_args.empty?
174
+ result
175
+ end
176
+
177
+ def argument(p, positional_arguments, keyword_arguments)
178
+ if p.look(:id) && p.look(:colon, 1)
179
+ key = p.consume(:id)
180
+ p.consume(:colon)
181
+ value = parse_context.safe_parse_expression(p)
182
+ keyword_arguments[key] = value
183
+ else
184
+ positional_arguments << parse_context.safe_parse_expression(p)
185
+ end
186
+ end
187
+
188
+ def end_of_arguments?(p)
189
+ p.look(:pipe) || p.look(:end_of_string)
190
+ end
191
+
141
192
  def evaluate_filter_expressions(context, filter_args, filter_kwargs)
142
193
  parsed_args = filter_args.map { |expr| context.evaluate(expr) }
143
194
  if filter_kwargs
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Liquid
5
- VERSION = "5.8.7"
5
+ VERSION = "5.9.0"
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: liquid
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.8.7
4
+ version: 5.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tobias Lütke
@@ -159,7 +159,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
159
159
  - !ruby/object:Gem::Version
160
160
  version: 1.3.7
161
161
  requirements: []
162
- rubygems_version: 3.6.9
162
+ rubygems_version: 3.7.2
163
163
  specification_version: 4
164
164
  summary: A secure, non-evaling end user template engine with aesthetic markup.
165
165
  test_files: []