liquid 4.0.0 → 5.10.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 (117) hide show
  1. checksums.yaml +5 -5
  2. data/History.md +235 -2
  3. data/README.md +58 -8
  4. data/lib/liquid/block.rb +51 -20
  5. data/lib/liquid/block_body.rb +216 -82
  6. data/lib/liquid/condition.rb +83 -32
  7. data/lib/liquid/const.rb +8 -0
  8. data/lib/liquid/context.rb +130 -59
  9. data/lib/liquid/deprecations.rb +22 -0
  10. data/lib/liquid/document.rb +47 -9
  11. data/lib/liquid/drop.rb +8 -2
  12. data/lib/liquid/environment.rb +159 -0
  13. data/lib/liquid/errors.rb +23 -20
  14. data/lib/liquid/expression.rb +114 -31
  15. data/lib/liquid/extensions.rb +8 -0
  16. data/lib/liquid/file_system.rb +6 -4
  17. data/lib/liquid/forloop_drop.rb +51 -4
  18. data/lib/liquid/i18n.rb +5 -3
  19. data/lib/liquid/interrupts.rb +3 -1
  20. data/lib/liquid/lexer.rb +165 -39
  21. data/lib/liquid/locales/en.yml +16 -6
  22. data/lib/liquid/parse_context.rb +62 -7
  23. data/lib/liquid/parse_tree_visitor.rb +42 -0
  24. data/lib/liquid/parser.rb +31 -19
  25. data/lib/liquid/parser_switching.rb +42 -3
  26. data/lib/liquid/partial_cache.rb +33 -0
  27. data/lib/liquid/profiler/hooks.rb +26 -14
  28. data/lib/liquid/profiler.rb +67 -86
  29. data/lib/liquid/range_lookup.rb +26 -6
  30. data/lib/liquid/registers.rb +51 -0
  31. data/lib/liquid/resource_limits.rb +47 -8
  32. data/lib/liquid/snippet_drop.rb +22 -0
  33. data/lib/liquid/standardfilters.rb +813 -137
  34. data/lib/liquid/strainer_template.rb +62 -0
  35. data/lib/liquid/tablerowloop_drop.rb +64 -5
  36. data/lib/liquid/tag/disableable.rb +22 -0
  37. data/lib/liquid/tag/disabler.rb +13 -0
  38. data/lib/liquid/tag.rb +42 -6
  39. data/lib/liquid/tags/assign.rb +46 -18
  40. data/lib/liquid/tags/break.rb +15 -4
  41. data/lib/liquid/tags/capture.rb +26 -18
  42. data/lib/liquid/tags/case.rb +108 -32
  43. data/lib/liquid/tags/comment.rb +76 -4
  44. data/lib/liquid/tags/continue.rb +15 -13
  45. data/lib/liquid/tags/cycle.rb +117 -34
  46. data/lib/liquid/tags/decrement.rb +30 -23
  47. data/lib/liquid/tags/doc.rb +81 -0
  48. data/lib/liquid/tags/echo.rb +39 -0
  49. data/lib/liquid/tags/for.rb +109 -96
  50. data/lib/liquid/tags/if.rb +72 -41
  51. data/lib/liquid/tags/ifchanged.rb +10 -11
  52. data/lib/liquid/tags/include.rb +89 -63
  53. data/lib/liquid/tags/increment.rb +31 -20
  54. data/lib/liquid/tags/inline_comment.rb +28 -0
  55. data/lib/liquid/tags/raw.rb +25 -13
  56. data/lib/liquid/tags/render.rb +151 -0
  57. data/lib/liquid/tags/snippet.rb +45 -0
  58. data/lib/liquid/tags/table_row.rb +104 -21
  59. data/lib/liquid/tags/unless.rb +37 -20
  60. data/lib/liquid/tags.rb +51 -0
  61. data/lib/liquid/template.rb +90 -106
  62. data/lib/liquid/template_factory.rb +9 -0
  63. data/lib/liquid/tokenizer.rb +143 -13
  64. data/lib/liquid/usage.rb +8 -0
  65. data/lib/liquid/utils.rb +114 -5
  66. data/lib/liquid/variable.rb +119 -45
  67. data/lib/liquid/variable_lookup.rb +35 -13
  68. data/lib/liquid/version.rb +3 -1
  69. data/lib/liquid.rb +31 -18
  70. metadata +56 -107
  71. data/lib/liquid/strainer.rb +0 -66
  72. data/test/fixtures/en_locale.yml +0 -9
  73. data/test/integration/assign_test.rb +0 -48
  74. data/test/integration/blank_test.rb +0 -106
  75. data/test/integration/capture_test.rb +0 -50
  76. data/test/integration/context_test.rb +0 -32
  77. data/test/integration/document_test.rb +0 -19
  78. data/test/integration/drop_test.rb +0 -273
  79. data/test/integration/error_handling_test.rb +0 -260
  80. data/test/integration/filter_test.rb +0 -178
  81. data/test/integration/hash_ordering_test.rb +0 -23
  82. data/test/integration/output_test.rb +0 -123
  83. data/test/integration/parsing_quirks_test.rb +0 -118
  84. data/test/integration/render_profiling_test.rb +0 -154
  85. data/test/integration/security_test.rb +0 -66
  86. data/test/integration/standard_filter_test.rb +0 -535
  87. data/test/integration/tags/break_tag_test.rb +0 -15
  88. data/test/integration/tags/continue_tag_test.rb +0 -15
  89. data/test/integration/tags/for_tag_test.rb +0 -410
  90. data/test/integration/tags/if_else_tag_test.rb +0 -188
  91. data/test/integration/tags/include_tag_test.rb +0 -238
  92. data/test/integration/tags/increment_tag_test.rb +0 -23
  93. data/test/integration/tags/raw_tag_test.rb +0 -31
  94. data/test/integration/tags/standard_tag_test.rb +0 -296
  95. data/test/integration/tags/statements_test.rb +0 -111
  96. data/test/integration/tags/table_row_test.rb +0 -64
  97. data/test/integration/tags/unless_else_tag_test.rb +0 -26
  98. data/test/integration/template_test.rb +0 -323
  99. data/test/integration/trim_mode_test.rb +0 -525
  100. data/test/integration/variable_test.rb +0 -92
  101. data/test/test_helper.rb +0 -117
  102. data/test/unit/block_unit_test.rb +0 -58
  103. data/test/unit/condition_unit_test.rb +0 -158
  104. data/test/unit/context_unit_test.rb +0 -483
  105. data/test/unit/file_system_unit_test.rb +0 -35
  106. data/test/unit/i18n_unit_test.rb +0 -37
  107. data/test/unit/lexer_unit_test.rb +0 -51
  108. data/test/unit/parser_unit_test.rb +0 -82
  109. data/test/unit/regexp_unit_test.rb +0 -44
  110. data/test/unit/strainer_unit_test.rb +0 -148
  111. data/test/unit/tag_unit_test.rb +0 -21
  112. data/test/unit/tags/case_tag_unit_test.rb +0 -10
  113. data/test/unit/tags/for_tag_unit_test.rb +0 -13
  114. data/test/unit/tags/if_tag_unit_test.rb +0 -8
  115. data/test/unit/template_unit_test.rb +0 -78
  116. data/test/unit/tokenizer_unit_test.rb +0 -55
  117. data/test/unit/variable_unit_test.rb +0 -162
@@ -1,43 +1,126 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  class Expression
3
- class MethodLiteral
4
- attr_reader :method_name, :to_s
5
+ LITERALS = {
6
+ nil => nil,
7
+ 'nil' => nil,
8
+ 'null' => nil,
9
+ '' => nil,
10
+ 'true' => true,
11
+ 'false' => false,
12
+ 'blank' => '',
13
+ 'empty' => '',
14
+ # in lax mode, minus sign can be a VariableLookup
15
+ # For simplicity and performace, we treat it like a literal
16
+ '-' => VariableLookup.parse("-", nil).freeze,
17
+ }.freeze
18
+
19
+ DOT = ".".ord
20
+ ZERO = "0".ord
21
+ NINE = "9".ord
22
+ DASH = "-".ord
23
+
24
+ # Use an atomic group (?>...) to avoid pathological backtracing from
25
+ # malicious input as described in https://github.com/Shopify/liquid/issues/1357
26
+ RANGES_REGEX = /\A\(\s*(?>(\S+)\s*\.\.)\s*(\S+)\s*\)\z/
27
+ INTEGER_REGEX = /\A(-?\d+)\z/
28
+ FLOAT_REGEX = /\A(-?\d+)\.\d+\z/
5
29
 
6
- def initialize(method_name, to_s)
7
- @method_name = method_name
8
- @to_s = to_s
30
+ class << self
31
+ def safe_parse(parser, ss = StringScanner.new(""), cache = nil)
32
+ parse(parser.expression, ss, cache)
9
33
  end
10
34
 
11
- def to_liquid
12
- to_s
35
+ def parse(markup, ss = StringScanner.new(""), cache = nil)
36
+ return unless markup
37
+
38
+ markup = markup.strip # markup can be a frozen string
39
+
40
+ if (markup.start_with?('"') && markup.end_with?('"')) ||
41
+ (markup.start_with?("'") && markup.end_with?("'"))
42
+ return markup[1..-2]
43
+ elsif LITERALS.key?(markup)
44
+ return LITERALS[markup]
45
+ end
46
+
47
+ # Cache only exists during parsing
48
+ if cache
49
+ return cache[markup] if cache.key?(markup)
50
+
51
+ cache[markup] = inner_parse(markup, ss, cache).freeze
52
+ else
53
+ inner_parse(markup, ss, nil).freeze
54
+ end
13
55
  end
14
- end
15
56
 
16
- LITERALS = {
17
- nil => nil, 'nil'.freeze => nil, 'null'.freeze => nil, ''.freeze => nil,
18
- 'true'.freeze => true,
19
- 'false'.freeze => false,
20
- 'blank'.freeze => MethodLiteral.new(:blank?, '').freeze,
21
- 'empty'.freeze => MethodLiteral.new(:empty?, '').freeze
22
- }
23
-
24
- def self.parse(markup)
25
- if LITERALS.key?(markup)
26
- LITERALS[markup]
27
- else
57
+ def inner_parse(markup, ss, cache)
58
+ if (markup.start_with?("(") && markup.end_with?(")")) && markup =~ RANGES_REGEX
59
+ return RangeLookup.parse(
60
+ Regexp.last_match(1),
61
+ Regexp.last_match(2),
62
+ ss,
63
+ cache,
64
+ )
65
+ end
66
+
67
+ if (num = parse_number(markup, ss))
68
+ num
69
+ else
70
+ VariableLookup.parse(markup, ss, cache)
71
+ end
72
+ end
73
+
74
+ def parse_number(markup, ss)
75
+ # check if the markup is simple integer or float
28
76
  case markup
29
- when /\A'(.*)'\z/m # Single quoted strings
30
- $1
31
- when /\A"(.*)"\z/m # Double quoted strings
32
- $1
33
- when /\A(-?\d+)\z/ # Integer and floats
34
- $1.to_i
35
- when /\A\((\S+)\.\.(\S+)\)\z/ # Ranges
36
- RangeLookup.parse($1, $2)
37
- when /\A(-?\d[\d\.]+)\z/ # Floats
38
- $1.to_f
77
+ when INTEGER_REGEX
78
+ return Integer(markup, 10)
79
+ when FLOAT_REGEX
80
+ return markup.to_f
81
+ end
82
+
83
+ ss.string = markup
84
+ # the first byte must be a digit or a dash
85
+ byte = ss.scan_byte
86
+
87
+ return false if byte != DASH && (byte < ZERO || byte > NINE)
88
+
89
+ if byte == DASH
90
+ peek_byte = ss.peek_byte
91
+
92
+ # if it starts with a dash, the next byte must be a digit
93
+ return false if peek_byte.nil? || !(peek_byte >= ZERO && peek_byte <= NINE)
94
+ end
95
+
96
+ # The markup could be a float with multiple dots
97
+ first_dot_pos = nil
98
+ num_end_pos = nil
99
+
100
+ while (byte = ss.scan_byte)
101
+ return false if byte != DOT && (byte < ZERO || byte > NINE)
102
+
103
+ # we found our number and now we are just scanning the rest of the string
104
+ next if num_end_pos
105
+
106
+ if byte == DOT
107
+ if first_dot_pos.nil?
108
+ first_dot_pos = ss.pos
109
+ else
110
+ # we found another dot, so we know that the number ends here
111
+ num_end_pos = ss.pos - 1
112
+ end
113
+ end
114
+ end
115
+
116
+ num_end_pos = markup.length if ss.eos?
117
+
118
+ if num_end_pos
119
+ # number ends with a number "123.123"
120
+ markup.byteslice(0, num_end_pos).to_f
39
121
  else
40
- VariableLookup.parse(markup)
122
+ # number ends with a dot "123."
123
+ markup.byteslice(0, first_dot_pos).to_f
41
124
  end
42
125
  end
43
126
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'time'
2
4
  require 'date'
3
5
 
@@ -7,6 +9,12 @@ class String # :nodoc:
7
9
  end
8
10
  end
9
11
 
12
+ class Symbol # :nodoc:
13
+ def to_liquid
14
+ to_s
15
+ end
16
+ end
17
+
10
18
  class Array # :nodoc:
11
19
  def to_liquid
12
20
  self
@@ -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
  #
@@ -44,8 +46,8 @@ module Liquid
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
 
@@ -57,9 +59,9 @@ module Liquid
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)
@@ -1,34 +1,81 @@
1
+ # frozen_string_literal: true
2
+
1
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](/docs/api/liquid/tags/for).
2
9
  class ForloopDrop < Drop
3
10
  def initialize(name, length, parentloop)
4
- @name = name
5
- @length = length
11
+ @name = name
12
+ @length = length
6
13
  @parentloop = parentloop
7
- @index = 0
14
+ @index = 0
8
15
  end
9
16
 
10
- attr_reader :name, :length, :parentloop
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
+ attr_reader :name
11
34
 
35
+ # @liquid_public_docs
36
+ # @liquid_summary
37
+ # The 1-based index of the current iteration.
38
+ # @liquid_return [number]
12
39
  def index
13
40
  @index + 1
14
41
  end
15
42
 
43
+ # @liquid_public_docs
44
+ # @liquid_summary
45
+ # The 0-based index of the current iteration.
46
+ # @liquid_return [number]
16
47
  def index0
17
48
  @index
18
49
  end
19
50
 
51
+ # @liquid_public_docs
52
+ # @liquid_summary
53
+ # The 1-based index of the current iteration, in reverse order.
54
+ # @liquid_return [number]
20
55
  def rindex
21
56
  @length - @index
22
57
  end
23
58
 
59
+ # @liquid_public_docs
60
+ # @liquid_summary
61
+ # The 0-based index of the current iteration, in reverse order.
62
+ # @liquid_return [number]
24
63
  def rindex0
25
64
  @length - @index - 1
26
65
  end
27
66
 
67
+ # @liquid_public_docs
68
+ # @liquid_summary
69
+ # Returns `true` if the current iteration is the first. Returns `false` if not.
70
+ # @liquid_return [boolean]
28
71
  def first
29
72
  @index == 0
30
73
  end
31
74
 
75
+ # @liquid_public_docs
76
+ # @liquid_summary
77
+ # Returns `true` if the current iteration is the last. Returns `false` if not.
78
+ # @liquid_return [boolean]
32
79
  def last
33
80
  @index == @length - 1
34
81
  end
data/lib/liquid/i18n.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'yaml'
2
4
 
3
5
  module Liquid
@@ -26,13 +28,13 @@ module Liquid
26
28
  def interpolate(name, vars)
27
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]}"
31
+ (vars[Regexp.last_match(1).to_sym]).to_s
30
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,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  # An interrupt is any command that breaks processing of a block (ex: a for loop).
3
5
  class Interrupt
4
6
  attr_reader :message
5
7
 
6
8
  def initialize(message = nil)
7
- @message = message || "interrupt".freeze
9
+ @message = message || "interrupt"
8
10
  end
9
11
  end
10
12
 
data/lib/liquid/lexer.rb CHANGED
@@ -1,53 +1,179 @@
1
- require "strscan"
1
+ # frozen_string_literal: true
2
+
2
3
  module Liquid
3
4
  class Lexer
4
- 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
- '?'.freeze => :question,
14
- '-'.freeze => :dash
15
- }
16
- IDENTIFIER = /[a-zA-Z_][\w-]*\??/
17
- SINGLE_STRING_LITERAL = /'[^\']*'/
5
+ CLOSE_ROUND = [:close_round, ")"].freeze
6
+ CLOSE_SQUARE = [:close_square, "]"].freeze
7
+ COLON = [:colon, ":"].freeze
8
+ COMMA = [:comma, ","].freeze
9
+ COMPARISION_NOT_EQUAL = [:comparison, "!="].freeze
10
+ COMPARISON_CONTAINS = [:comparison, "contains"].freeze
11
+ COMPARISON_EQUAL = [:comparison, "=="].freeze
12
+ COMPARISON_GREATER_THAN = [:comparison, ">"].freeze
13
+ COMPARISON_GREATER_THAN_OR_EQUAL = [:comparison, ">="].freeze
14
+ COMPARISON_LESS_THAN = [:comparison, "<"].freeze
15
+ COMPARISON_LESS_THAN_OR_EQUAL = [:comparison, "<="].freeze
16
+ COMPARISON_NOT_EQUAL_ALT = [:comparison, "<>"].freeze
17
+ DASH = [:dash, "-"].freeze
18
+ DOT = [:dot, "."].freeze
19
+ DOTDOT = [:dotdot, ".."].freeze
20
+ DOT_ORD = ".".ord
18
21
  DOUBLE_STRING_LITERAL = /"[^\"]*"/
19
- NUMBER_LITERAL = /-?\d+(\.\d+)?/
20
- DOTDOT = /\.\./
21
- COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains/
22
+ EOS = [:end_of_string].freeze
23
+ IDENTIFIER = /[a-zA-Z_][\w-]*\??/
24
+ NUMBER_LITERAL = /-?\d+(\.\d+)?/
25
+ OPEN_ROUND = [:open_round, "("].freeze
26
+ OPEN_SQUARE = [:open_square, "["].freeze
27
+ PIPE = [:pipe, "|"].freeze
28
+ QUESTION = [:question, "?"].freeze
29
+ RUBY_WHITESPACE = [" ", "\t", "\r", "\n", "\f"].freeze
30
+ SINGLE_STRING_LITERAL = /'[^\']*'/
31
+ WHITESPACE_OR_NOTHING = /\s*/
22
32
 
23
- def initialize(input)
24
- @ss = StringScanner.new(input.rstrip)
33
+ SINGLE_COMPARISON_TOKENS = [].tap do |table|
34
+ table["<".ord] = COMPARISON_LESS_THAN
35
+ table[">".ord] = COMPARISON_GREATER_THAN
36
+ table.freeze
25
37
  end
26
38
 
27
- def tokenize
28
- @output = []
29
-
30
- until @ss.eos?
31
- @ss.skip(/\s*/)
32
- tok = case
33
- when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t]
34
- when t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t]
35
- when t = @ss.scan(DOUBLE_STRING_LITERAL) then [:string, t]
36
- when t = @ss.scan(NUMBER_LITERAL) then [:number, t]
37
- when t = @ss.scan(IDENTIFIER) then [:id, t]
38
- when t = @ss.scan(DOTDOT) then [:dotdot, t]
39
- else
40
- c = @ss.getch
41
- if s = SPECIALS[c]
42
- [s, c]
39
+ TWO_CHARS_COMPARISON_JUMP_TABLE = [].tap do |table|
40
+ table["=".ord] = [].tap do |sub_table|
41
+ sub_table["=".ord] = COMPARISON_EQUAL
42
+ sub_table.freeze
43
+ end
44
+ table["!".ord] = [].tap do |sub_table|
45
+ sub_table["=".ord] = COMPARISION_NOT_EQUAL
46
+ sub_table.freeze
47
+ end
48
+ table.freeze
49
+ end
50
+
51
+ COMPARISON_JUMP_TABLE = [].tap do |table|
52
+ table["<".ord] = [].tap do |sub_table|
53
+ sub_table["=".ord] = COMPARISON_LESS_THAN_OR_EQUAL
54
+ sub_table[">".ord] = COMPARISON_NOT_EQUAL_ALT
55
+ sub_table.freeze
56
+ end
57
+ table[">".ord] = [].tap do |sub_table|
58
+ sub_table["=".ord] = COMPARISON_GREATER_THAN_OR_EQUAL
59
+ sub_table.freeze
60
+ end
61
+ table.freeze
62
+ end
63
+
64
+ NEXT_MATCHER_JUMP_TABLE = [].tap do |table|
65
+ "a".upto("z") do |c|
66
+ table[c.ord] = [:id, IDENTIFIER].freeze
67
+ table[c.upcase.ord] = [:id, IDENTIFIER].freeze
68
+ end
69
+ table["_".ord] = [:id, IDENTIFIER].freeze
70
+
71
+ "0".upto("9") do |c|
72
+ table[c.ord] = [:number, NUMBER_LITERAL].freeze
73
+ end
74
+ table["-".ord] = [:number, NUMBER_LITERAL].freeze
75
+
76
+ table["'".ord] = [:string, SINGLE_STRING_LITERAL].freeze
77
+ table["\"".ord] = [:string, DOUBLE_STRING_LITERAL].freeze
78
+ table.freeze
79
+ end
80
+
81
+ SPECIAL_TABLE = [].tap do |table|
82
+ table["|".ord] = PIPE
83
+ table[".".ord] = DOT
84
+ table[":".ord] = COLON
85
+ table[",".ord] = COMMA
86
+ table["[".ord] = OPEN_SQUARE
87
+ table["]".ord] = CLOSE_SQUARE
88
+ table["(".ord] = OPEN_ROUND
89
+ table[")".ord] = CLOSE_ROUND
90
+ table["?".ord] = QUESTION
91
+ table["-".ord] = DASH
92
+ end
93
+
94
+ NUMBER_TABLE = [].tap do |table|
95
+ "0".upto("9") do |c|
96
+ table[c.ord] = true
97
+ end
98
+ table.freeze
99
+ end
100
+
101
+ # rubocop:disable Metrics/BlockNesting
102
+ class << self
103
+ def tokenize(ss)
104
+ output = []
105
+
106
+ until ss.eos?
107
+ ss.skip(WHITESPACE_OR_NOTHING)
108
+
109
+ break if ss.eos?
110
+
111
+ start_pos = ss.pos
112
+ peeked = ss.peek_byte
113
+
114
+ if (special = SPECIAL_TABLE[peeked])
115
+ ss.scan_byte
116
+ # Special case for ".."
117
+ if special == DOT && ss.peek_byte == DOT_ORD
118
+ ss.scan_byte
119
+ output << DOTDOT
120
+ elsif special == DASH
121
+ # Special case for negative numbers
122
+ if (peeked_byte = ss.peek_byte) && NUMBER_TABLE[peeked_byte]
123
+ ss.pos -= 1
124
+ output << [:number, ss.scan(NUMBER_LITERAL)]
125
+ else
126
+ output << special
127
+ end
128
+ else
129
+ output << special
130
+ end
131
+ elsif (sub_table = TWO_CHARS_COMPARISON_JUMP_TABLE[peeked])
132
+ ss.scan_byte
133
+ if (peeked_byte = ss.peek_byte) && (found = sub_table[peeked_byte])
134
+ output << found
135
+ ss.scan_byte
136
+ else
137
+ raise_syntax_error(start_pos, ss)
138
+ end
139
+ elsif (sub_table = COMPARISON_JUMP_TABLE[peeked])
140
+ ss.scan_byte
141
+ if (peeked_byte = ss.peek_byte) && (found = sub_table[peeked_byte])
142
+ output << found
143
+ ss.scan_byte
144
+ else
145
+ output << SINGLE_COMPARISON_TOKENS[peeked]
146
+ end
43
147
  else
44
- raise SyntaxError, "Unexpected character #{c}"
148
+ type, pattern = NEXT_MATCHER_JUMP_TABLE[peeked]
149
+
150
+ if type && (t = ss.scan(pattern))
151
+ # Special case for "contains"
152
+ output << if type == :id && t == "contains" && output.last&.first != :dot
153
+ COMPARISON_CONTAINS
154
+ else
155
+ [type, t]
156
+ end
157
+ else
158
+ raise_syntax_error(start_pos, ss)
159
+ end
45
160
  end
46
161
  end
47
- @output << tok
162
+ # rubocop:enable Metrics/BlockNesting
163
+ output << EOS
164
+ rescue ::ArgumentError => e
165
+ if e.message == "invalid byte sequence in #{ss.string.encoding}"
166
+ raise SyntaxError, "Invalid byte sequence in #{ss.string.encoding}"
167
+ else
168
+ raise
169
+ end
48
170
  end
49
171
 
50
- @output << [:end_of_string]
172
+ def raise_syntax_error(start_pos, ss)
173
+ ss.pos = start_pos
174
+ # the character could be a UTF-8 character, use getch to get all the bytes
175
+ raise SyntaxError, "Unexpected character #{ss.getch}"
176
+ end
51
177
  end
52
178
  end
53
179
  end
@@ -2,25 +2,35 @@
2
2
  errors:
3
3
  syntax:
4
4
  tag_unexpected_args: "Syntax Error in '%{tag}' - Valid syntax: %{tag}"
5
+ block_tag_unexpected_args: "Syntax Error in '%{tag}' - Valid syntax: {% %{tag} %}{% end%{tag} %}"
5
6
  assign: "Syntax Error in 'assign' - Valid syntax: assign [var] = [source]"
6
7
  capture: "Syntax Error in 'capture' - Valid syntax: capture [var]"
8
+ snippet: "Syntax Error in 'snippet' - Valid syntax: snippet [var]"
7
9
  case: "Syntax Error in 'case' - Valid syntax: case [condition]"
8
10
  case_invalid_when: "Syntax Error in tag 'case' - Valid when condition: {% when [condition] [or condition2...] %}"
9
11
  case_invalid_else: "Syntax Error in tag 'case' - Valid else condition: {% else %} (no parameters) "
10
12
  cycle: "Syntax Error in 'cycle' - Valid syntax: cycle [name :] var [, var2, var3 ...]"
13
+ doc_invalid_nested: "Syntax Error in 'doc' - Nested doc tags are not allowed"
11
14
  for: "Syntax Error in 'for loop' - Valid syntax: for [item] in [collection]"
12
15
  for_invalid_in: "For loops require an 'in' clause"
13
16
  for_invalid_attribute: "Invalid attribute in for loop. Valid attributes are limit and offset"
14
17
  if: "Syntax Error in tag 'if' - Valid syntax: if [expression]"
15
18
  include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]"
16
- unknown_tag: "Unknown tag '%{tag}'"
17
- invalid_delimiter: "'end' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
19
+ inline_comment_invalid: "Syntax error in tag '#' - Each line of comments must be prefixed by the '#' character"
20
+ invalid_delimiter: "'%{tag}' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
21
+ invalid_template_encoding: "Invalid template encoding"
22
+ render: "Syntax error in tag 'render' - Template name must be a quoted string"
23
+ render_invalid_template_name: "Syntax error in tag 'render' - Expected a string or identifier, found %{found}"
24
+ table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
25
+ table_row_invalid_attribute: "Invalid attribute '%{attribute}' in tablerow loop. Valid attributes are cols, limit, offset, and range"
26
+ tag_never_closed: "'%{block_name}' tag was never closed"
27
+ tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
18
28
  unexpected_else: "%{block_name} tag does not expect 'else' tag"
19
29
  unexpected_outer_tag: "Unexpected outer '%{tag}' tag"
20
- tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
30
+ unknown_tag: "Unknown tag '%{tag}'"
21
31
  variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}"
22
- tag_never_closed: "'%{block_name}' tag was never closed"
23
- meta_syntax_error: "Liquid syntax error: #{e.message}"
24
- table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
25
32
  argument:
26
33
  include: "Argument error in tag 'include' - Illegal template name"
34
+ render: "Argument error in tag 'render' - Dynamically chosen templates are not allowed"
35
+ disabled:
36
+ tag: "usage is not allowed in this context"