liquid 5.4.0 → 5.6.4

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +11 -0
  3. data/README.md +48 -6
  4. data/lib/liquid/block.rb +8 -4
  5. data/lib/liquid/block_body.rb +28 -10
  6. data/lib/liquid/condition.rb +9 -4
  7. data/lib/liquid/const.rb +8 -0
  8. data/lib/liquid/context.rb +24 -14
  9. data/lib/liquid/deprecations.rb +22 -0
  10. data/lib/liquid/drop.rb +4 -0
  11. data/lib/liquid/environment.rb +159 -0
  12. data/lib/liquid/errors.rb +16 -15
  13. data/lib/liquid/expression.rb +101 -22
  14. data/lib/liquid/forloop_drop.rb +2 -5
  15. data/lib/liquid/lexer.rb +155 -44
  16. data/lib/liquid/locales/en.yml +1 -0
  17. data/lib/liquid/parse_context.rb +29 -6
  18. data/lib/liquid/parse_tree_visitor.rb +1 -1
  19. data/lib/liquid/parser.rb +3 -3
  20. data/lib/liquid/partial_cache.rb +12 -3
  21. data/lib/liquid/range_lookup.rb +14 -4
  22. data/lib/liquid/standardfilters.rb +82 -21
  23. data/lib/liquid/tablerowloop_drop.rb +1 -1
  24. data/lib/liquid/tag/disabler.rb +0 -8
  25. data/lib/liquid/tag.rb +13 -3
  26. data/lib/liquid/tags/assign.rb +1 -3
  27. data/lib/liquid/tags/break.rb +1 -3
  28. data/lib/liquid/tags/capture.rb +0 -2
  29. data/lib/liquid/tags/case.rb +1 -3
  30. data/lib/liquid/tags/comment.rb +60 -3
  31. data/lib/liquid/tags/continue.rb +1 -3
  32. data/lib/liquid/tags/cycle.rb +14 -4
  33. data/lib/liquid/tags/decrement.rb +8 -7
  34. data/lib/liquid/tags/echo.rb +2 -4
  35. data/lib/liquid/tags/for.rb +6 -8
  36. data/lib/liquid/tags/if.rb +3 -5
  37. data/lib/liquid/tags/ifchanged.rb +0 -2
  38. data/lib/liquid/tags/include.rb +8 -8
  39. data/lib/liquid/tags/increment.rb +8 -7
  40. data/lib/liquid/tags/inline_comment.rb +0 -15
  41. data/lib/liquid/tags/raw.rb +2 -4
  42. data/lib/liquid/tags/render.rb +14 -12
  43. data/lib/liquid/tags/table_row.rb +18 -6
  44. data/lib/liquid/tags/unless.rb +3 -5
  45. data/lib/liquid/tags.rb +47 -0
  46. data/lib/liquid/template.rb +60 -57
  47. data/lib/liquid/tokenizer.rb +127 -11
  48. data/lib/liquid/variable.rb +14 -8
  49. data/lib/liquid/variable_lookup.rb +13 -5
  50. data/lib/liquid/version.rb +1 -1
  51. data/lib/liquid.rb +15 -16
  52. metadata +37 -10
  53. data/lib/liquid/strainer_factory.rb +0 -41
@@ -12,26 +12,27 @@ module Liquid
12
12
  # or [section](/themes/architecture/sections) file that they're created in. However, the variable is shared across
13
13
  # [snippets](/themes/architecture#snippets) included in the file.
14
14
  #
15
- # Similarly, variables that are created with `increment` are independent from those created with [`assign`](/api/liquid/tags#assign)
16
- # and [`capture`](/api/liquid/tags#capture). However, `increment` and [`decrement`](/api/liquid/tags#decrement) share
15
+ # Similarly, variables that are created with `increment` are independent from those created with [`assign`](/docs/api/liquid/tags/assign)
16
+ # and [`capture`](/docs/api/liquid/tags/capture). However, `increment` and [`decrement`](/docs/api/liquid/tags/decrement) share
17
17
  # variables.
18
18
  # @liquid_syntax
19
19
  # {% increment variable_name %}
20
20
  # @liquid_syntax_keyword variable_name The name of the variable being incremented.
21
21
  class Increment < Tag
22
+ attr_reader :variable_name
23
+
22
24
  def initialize(tag_name, markup, options)
23
25
  super
24
- @variable = markup.strip
26
+ @variable_name = markup.strip
25
27
  end
26
28
 
27
29
  def render_to_output_buffer(context, output)
28
- value = context.environments.first[@variable] ||= 0
29
- context.environments.first[@variable] = value + 1
30
+ counter_environment = context.environments.first
31
+ value = counter_environment[@variable_name] || 0
32
+ counter_environment[@variable_name] = value + 1
30
33
 
31
34
  output << value.to_s
32
35
  output
33
36
  end
34
37
  end
35
-
36
- Template.register_tag('increment', Increment)
37
38
  end
@@ -1,19 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Liquid
4
- # @liquid_public_docs
5
- # @liquid_type tag
6
- # @liquid_category syntax
7
- # @liquid_name inline_comment
8
- # @liquid_summary
9
- # Prevents an expression from being rendered or output.
10
- # @liquid_description
11
- # Any text inside an `inline_comment` tag won't be rendered or output.
12
- #
13
- # You can create multi-line inline comments. However, each line must begin with a `#`.
14
- # @liquid_syntax
15
- # {% # content %}
16
- # @liquid_syntax_keyword content The content of the comment.
17
4
  class InlineComment < Tag
18
5
  def initialize(tag_name, markup, options)
19
6
  super
@@ -38,6 +25,4 @@ module Liquid
38
25
  true
39
26
  end
40
27
  end
41
-
42
- Template.register_tag('#', InlineComment)
43
28
  end
@@ -14,7 +14,6 @@ module Liquid
14
14
  # @liquid_syntax_keyword expression The expression to be output without being rendered.
15
15
  class Raw < Block
16
16
  Syntax = /\A\s*\z/
17
- FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
18
17
 
19
18
  def initialize(tag_name, markup, parse_context)
20
19
  super
@@ -25,7 +24,8 @@ module Liquid
25
24
  def parse(tokens)
26
25
  @body = +''
27
26
  while (token = tokens.shift)
28
- if token =~ FullTokenPossiblyInvalid && block_delimiter == Regexp.last_match(2)
27
+ if token =~ BlockBody::FullTokenPossiblyInvalid && block_delimiter == Regexp.last_match(2)
28
+ parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
29
29
  @body << Regexp.last_match(1) if Regexp.last_match(1) != ""
30
30
  return
31
31
  end
@@ -56,6 +56,4 @@ module Liquid
56
56
  end
57
57
  end
58
58
  end
59
-
60
- Template.register_tag('raw', Raw)
61
59
  end
@@ -8,19 +8,19 @@ module Liquid
8
8
  # @liquid_summary
9
9
  # Renders a [snippet](/themes/architecture#snippets) or [app block](/themes/architecture/sections/section-schema#render-app-blocks).
10
10
  # @liquid_description
11
- # Inside snippets and app blocks, you can't directly access variables that are [created](/api/liquid/tags#variable-tags) outside
12
- # of the snippet or app block. However, you can [specify variables as parameters](/api/liquid/tags#render-passing-variables-to-snippets)
11
+ # Inside snippets and app blocks, you can't directly access variables that are [created](/docs/api/liquid/tags/variable-tags) outside
12
+ # of the snippet or app block. However, you can [specify variables as parameters](/docs/api/liquid/tags/render#render-passing-variables-to-a-snippet)
13
13
  # to pass outside variables to snippets.
14
14
  #
15
15
  # While you can't directly access created variables, you can access global objects, as well as any objects that are
16
16
  # directly accessible outside the snippet or app block. For example, a snippet or app block inside the [product template](/themes/architecture/templates/product)
17
- # can access the [`product` object](/api/liquid/objects#product), and a snippet or app block inside a [section](/themes/architecture/sections)
18
- # can access the [`section` object](/api/liquid/objects#section).
17
+ # can access the [`product` object](/docs/api/liquid/objects/product), and a snippet or app block inside a [section](/themes/architecture/sections)
18
+ # can access the [`section` object](/docs/api/liquid/objects/section).
19
19
  #
20
20
  # Outside a snippet or app block, you can't access variables created inside the snippet or app block.
21
21
  #
22
22
  # > Note:
23
- # > When you render a snippet using the `render` tag, you can't use the [`include` tag](/api/liquid/tags#include)
23
+ # > When you render a snippet using the `render` tag, you can't use the [`include` tag](/docs/api/liquid/tags/include)
24
24
  # > inside the snippet.
25
25
  # @liquid_syntax
26
26
  # {% render 'filename' %}
@@ -31,7 +31,7 @@ module Liquid
31
31
 
32
32
  disable_tags "include"
33
33
 
34
- attr_reader :template_name_expr, :variable_name_expr, :attributes
34
+ attr_reader :template_name_expr, :variable_name_expr, :attributes, :alias_name
35
35
 
36
36
  def initialize(tag_name, markup, options)
37
37
  super
@@ -45,7 +45,7 @@ module Liquid
45
45
  @alias_name = Regexp.last_match(6)
46
46
  @variable_name_expr = variable_name ? parse_expression(variable_name) : nil
47
47
  @template_name_expr = parse_expression(template_name)
48
- @for = (with_or_for == FOR)
48
+ @is_for_loop = (with_or_for == FOR)
49
49
 
50
50
  @attributes = {}
51
51
  markup.scan(TagAttributes) do |key, value|
@@ -53,6 +53,10 @@ module Liquid
53
53
  end
54
54
  end
55
55
 
56
+ def for_loop?
57
+ @is_for_loop
58
+ end
59
+
56
60
  def render_to_output_buffer(context, output)
57
61
  render_tag(context, output)
58
62
  end
@@ -65,14 +69,14 @@ module Liquid
65
69
  partial = PartialCache.load(
66
70
  template_name,
67
71
  context: context,
68
- parse_context: parse_context
72
+ parse_context: parse_context,
69
73
  )
70
74
 
71
75
  context_variable_name = @alias_name || template_name.split('/').last
72
76
 
73
77
  render_partial_func = ->(var, forloop) {
74
78
  inner_context = context.new_isolated_subcontext
75
- inner_context.template_name = template_name
79
+ inner_context.template_name = partial.name
76
80
  inner_context.partial = true
77
81
  inner_context['forloop'] = forloop if forloop
78
82
 
@@ -85,7 +89,7 @@ module Liquid
85
89
  }
86
90
 
87
91
  variable = @variable_name_expr ? context.evaluate(@variable_name_expr) : nil
88
- if @for && variable.respond_to?(:each) && variable.respond_to?(:count)
92
+ if @is_for_loop && variable.respond_to?(:each) && variable.respond_to?(:count)
89
93
  forloop = Liquid::ForloopDrop.new(template_name, variable.count, nil)
90
94
  variable.each { |var| render_partial_func.call(var, forloop) }
91
95
  else
@@ -104,6 +108,4 @@ module Liquid
104
108
  end
105
109
  end
106
110
  end
107
-
108
- Template.register_tag('render', Render)
109
111
  end
@@ -11,7 +11,7 @@ module Liquid
11
11
  # The `tablerow` tag must be wrapped in HTML `<table>` and `</table>` tags.
12
12
  #
13
13
  # > Tip:
14
- # > Every `tablerow` loop has an associated [`tablerowloop` object](/api/liquid/objects#tablerowloop) with information about the loop.
14
+ # > Every `tablerow` loop has an associated [`tablerowloop` object](/docs/api/liquid/objects/tablerowloop) with information about the loop.
15
15
  # @liquid_syntax
16
16
  # {% tablerow variable in array %}
17
17
  # expression
@@ -45,13 +45,13 @@ module Liquid
45
45
  def render_to_output_buffer(context, output)
46
46
  (collection = context.evaluate(@collection_name)) || (return '')
47
47
 
48
- from = @attributes.key?('offset') ? context.evaluate(@attributes['offset']).to_i : 0
49
- to = @attributes.key?('limit') ? from + context.evaluate(@attributes['limit']).to_i : nil
48
+ from = @attributes.key?('offset') ? to_integer(context.evaluate(@attributes['offset'])) : 0
49
+ to = @attributes.key?('limit') ? from + to_integer(context.evaluate(@attributes['limit'])) : nil
50
50
 
51
51
  collection = Utils.slice_collection(collection, from, to)
52
52
  length = collection.length
53
53
 
54
- cols = context.evaluate(@attributes['cols']).to_i
54
+ cols = @attributes.key?('cols') ? to_integer(context.evaluate(@attributes['cols'])) : length
55
55
 
56
56
  output << "<tr class=\"row1\">\n"
57
57
  context.stack do
@@ -65,6 +65,12 @@ module Liquid
65
65
  super
66
66
  output << '</td>'
67
67
 
68
+ # Handle any interrupts if they exist.
69
+ if context.interrupt?
70
+ interrupt = context.pop_interrupt
71
+ break if interrupt.is_a?(BreakInterrupt)
72
+ end
73
+
68
74
  if tablerowloop.col_last && !tablerowloop.last
69
75
  output << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">"
70
76
  end
@@ -82,7 +88,13 @@ module Liquid
82
88
  super + @node.attributes.values + [@node.collection_name]
83
89
  end
84
90
  end
85
- end
86
91
 
87
- Template.register_tag('tablerow', TableRow)
92
+ private
93
+
94
+ def to_integer(value)
95
+ value.to_i
96
+ rescue NoMethodError
97
+ raise Liquid::ArgumentError, "invalid integer"
98
+ end
99
+ end
88
100
  end
@@ -11,7 +11,7 @@ module Liquid
11
11
  # Renders an expression unless a specific condition is `true`.
12
12
  # @liquid_description
13
13
  # > Tip:
14
- # > Similar to the [`if` tag](/api/liquid/tags#if), you can use `elsif` to add more conditions to an `unless` tag.
14
+ # > Similar to the [`if` tag](/docs/api/liquid/tags/if), you can use `elsif` to add more conditions to an `unless` tag.
15
15
  # @liquid_syntax
16
16
  # {% unless condition %}
17
17
  # expression
@@ -23,7 +23,7 @@ module Liquid
23
23
  # First condition is interpreted backwards ( if not )
24
24
  first_block = @blocks.first
25
25
  result = Liquid::Utils.to_liquid_value(
26
- first_block.evaluate(context)
26
+ first_block.evaluate(context),
27
27
  )
28
28
 
29
29
  unless result
@@ -33,7 +33,7 @@ module Liquid
33
33
  # After the first condition unless works just like if
34
34
  @blocks[1..-1].each do |block|
35
35
  result = Liquid::Utils.to_liquid_value(
36
- block.evaluate(context)
36
+ block.evaluate(context),
37
37
  )
38
38
 
39
39
  if result
@@ -44,6 +44,4 @@ module Liquid
44
44
  output
45
45
  end
46
46
  end
47
-
48
- Template.register_tag('unless', Unless)
49
47
  end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "tags/table_row"
4
+ require_relative "tags/echo"
5
+ require_relative "tags/if"
6
+ require_relative "tags/break"
7
+ require_relative "tags/inline_comment"
8
+ require_relative "tags/for"
9
+ require_relative "tags/assign"
10
+ require_relative "tags/ifchanged"
11
+ require_relative "tags/case"
12
+ require_relative "tags/include"
13
+ require_relative "tags/continue"
14
+ require_relative "tags/capture"
15
+ require_relative "tags/decrement"
16
+ require_relative "tags/unless"
17
+ require_relative "tags/increment"
18
+ require_relative "tags/comment"
19
+ require_relative "tags/raw"
20
+ require_relative "tags/render"
21
+ require_relative "tags/cycle"
22
+
23
+ module Liquid
24
+ module Tags
25
+ STANDARD_TAGS = {
26
+ 'cycle' => Cycle,
27
+ 'render' => Render,
28
+ 'raw' => Raw,
29
+ 'comment' => Comment,
30
+ 'increment' => Increment,
31
+ 'unless' => Unless,
32
+ 'decrement' => Decrement,
33
+ 'capture' => Capture,
34
+ 'continue' => Continue,
35
+ 'include' => Include,
36
+ 'case' => Case,
37
+ 'ifchanged' => Ifchanged,
38
+ 'assign' => Assign,
39
+ 'for' => For,
40
+ '#' => InlineComment,
41
+ 'break' => Break,
42
+ 'if' => If,
43
+ 'echo' => Echo,
44
+ 'tablerow' => TableRow,
45
+ }.freeze
46
+ end
47
+ end
@@ -15,98 +15,93 @@ module Liquid
15
15
  # template.render('user_name' => 'bob')
16
16
  #
17
17
  class Template
18
- attr_accessor :root
18
+ attr_accessor :root, :name
19
19
  attr_reader :resource_limits, :warnings
20
20
 
21
- class TagRegistry
22
- include Enumerable
21
+ attr_reader :profiler
23
22
 
24
- def initialize
25
- @tags = {}
26
- @cache = {}
23
+ class << self
24
+ # Sets how strict the parser should be.
25
+ # :lax acts like liquid 2.5 and silently ignores malformed tags in most cases.
26
+ # :warn is the default and will give deprecation warnings when invalid syntax is used.
27
+ # :strict will enforce correct syntax.
28
+ def error_mode=(mode)
29
+ Deprecations.warn("Template.error_mode=", "Environment#error_mode=")
30
+ Environment.default.error_mode = mode
27
31
  end
28
32
 
29
- def [](tag_name)
30
- return nil unless @tags.key?(tag_name)
31
- return @cache[tag_name] if Liquid.cache_classes
32
-
33
- lookup_class(@tags[tag_name]).tap { |o| @cache[tag_name] = o }
33
+ def error_mode
34
+ Environment.default.error_mode
34
35
  end
35
36
 
36
- def []=(tag_name, klass)
37
- @tags[tag_name] = klass.name
38
- @cache[tag_name] = klass
37
+ def default_exception_renderer=(renderer)
38
+ Deprecations.warn("Template.default_exception_renderer=", "Environment#exception_renderer=")
39
+ Environment.default.exception_renderer = renderer
39
40
  end
40
41
 
41
- def delete(tag_name)
42
- @tags.delete(tag_name)
43
- @cache.delete(tag_name)
42
+ def default_exception_renderer
43
+ Environment.default.exception_renderer
44
44
  end
45
45
 
46
- def each(&block)
47
- @tags.each(&block)
46
+ def file_system=(file_system)
47
+ Deprecations.warn("Template.file_system=", "Environment#file_system=")
48
+ Environment.default.file_system = file_system
48
49
  end
49
50
 
50
- private
51
-
52
- def lookup_class(name)
53
- Object.const_get(name)
51
+ def file_system
52
+ Environment.default.file_system
54
53
  end
55
- end
56
-
57
- attr_reader :profiler
58
54
 
59
- class << self
60
- # Sets how strict the parser should be.
61
- # :lax acts like liquid 2.5 and silently ignores malformed tags in most cases.
62
- # :warn is the default and will give deprecation warnings when invalid syntax is used.
63
- # :strict will enforce correct syntax.
64
- attr_accessor :error_mode
65
- Template.error_mode = :lax
66
-
67
- attr_accessor :default_exception_renderer
68
- Template.default_exception_renderer = lambda do |exception|
69
- exception
55
+ def tags
56
+ Environment.default.tags
70
57
  end
71
58
 
72
- attr_accessor :file_system
73
- Template.file_system = BlankFileSystem.new
74
-
75
- attr_accessor :tags
76
- Template.tags = TagRegistry.new
77
- private :tags=
78
-
79
59
  def register_tag(name, klass)
80
- tags[name.to_s] = klass
60
+ Deprecations.warn("Template.register_tag", "Environment#register_tag")
61
+ Environment.default.register_tag(name, klass)
81
62
  end
82
63
 
83
64
  # Pass a module with filter methods which should be available
84
65
  # to all liquid views. Good for registering the standard library
85
66
  def register_filter(mod)
86
- StrainerFactory.add_global_filter(mod)
67
+ Deprecations.warn("Template.register_filter", "Environment#register_filter")
68
+ Environment.default.register_filter(mod)
87
69
  end
88
70
 
89
- attr_accessor :default_resource_limits
90
- Template.default_resource_limits = {}
91
- private :default_resource_limits=
71
+ private def default_resource_limits=(limits)
72
+ Deprecations.warn("Template.default_resource_limits=", "Environment#default_resource_limits=")
73
+ Environment.default.default_resource_limits = limits
74
+ end
75
+
76
+ def default_resource_limits
77
+ Environment.default.default_resource_limits
78
+ end
92
79
 
93
80
  # creates a new <tt>Template</tt> object from liquid source code
94
81
  # To enable profiling, pass in <tt>profile: true</tt> as an option.
95
82
  # See Liquid::Profiler for more information
96
83
  def parse(source, options = {})
97
- new.parse(source, options)
84
+ environment = options[:environment] || Environment.default
85
+ new(environment: environment).parse(source, options)
98
86
  end
99
87
  end
100
88
 
101
- def initialize
89
+ def initialize(environment: Environment.default)
90
+ @environment = environment
102
91
  @rethrow_errors = false
103
- @resource_limits = ResourceLimits.new(Template.default_resource_limits)
92
+ @resource_limits = ResourceLimits.new(environment.default_resource_limits)
104
93
  end
105
94
 
106
95
  # Parse source code.
107
96
  # Returns self for easy chaining
108
97
  def parse(source, options = {})
109
98
  parse_context = configure_options(options)
99
+ source = source.to_s.to_str
100
+
101
+ unless source.valid_encoding?
102
+ raise TemplateEncodingError, parse_context.locale.t("errors.syntax.invalid_template_encoding")
103
+ end
104
+
110
105
  tokenizer = parse_context.new_tokenizer(source, start_line_number: @line_numbers && 1)
111
106
  @root = Document.parse(tokenizer, parse_context)
112
107
  self
@@ -156,11 +151,11 @@ module Liquid
156
151
  c
157
152
  when Liquid::Drop
158
153
  drop = args.shift
159
- drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
154
+ drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits, {}, @environment)
160
155
  when Hash
161
- Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
156
+ Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits, {}, @environment)
162
157
  when nil
163
- Context.new(assigns, instance_assigns, registers, @rethrow_errors, @resource_limits)
158
+ Context.new(assigns, instance_assigns, registers, @rethrow_errors, @resource_limits, {}, @environment)
164
159
  else
165
160
  raise ArgumentError, "Expected Hash or Liquid::Context as parameter"
166
161
  end
@@ -189,6 +184,8 @@ module Liquid
189
184
  @profiler = context.profiler = Liquid::Profiler.new
190
185
  end
191
186
 
187
+ context.template_name ||= name
188
+
192
189
  begin
193
190
  # render the nodelist.
194
191
  @root.render_to_output_buffer(context, output || +'')
@@ -218,8 +215,14 @@ module Liquid
218
215
  @options = options
219
216
  @profiling = profiling
220
217
  @line_numbers = options[:line_numbers] || @profiling
221
- parse_context = options.is_a?(ParseContext) ? options : ParseContext.new(options)
222
- @warnings = parse_context.warnings
218
+ parse_context = if options.is_a?(ParseContext)
219
+ options
220
+ else
221
+ opts = options.key?(:environment) ? options : options.merge(environment: @environment)
222
+ ParseContext.new(opts)
223
+ end
224
+
225
+ @warnings = parse_context.warnings
223
226
  parse_context
224
227
  end
225
228
 
@@ -1,18 +1,45 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "strscan"
4
+
3
5
  module Liquid
4
6
  class Tokenizer
5
7
  attr_reader :line_number, :for_liquid_tag
6
8
 
7
- def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false)
8
- @source = source.to_s.to_str
9
- @line_number = line_number || (line_numbers ? 1 : nil)
9
+ TAG_END = /%\}/
10
+ TAG_OR_VARIABLE_START = /\{[\{\%]/
11
+ NEWLINE = /\n/
12
+
13
+ OPEN_CURLEY = "{".ord
14
+ CLOSE_CURLEY = "}".ord
15
+ PERCENTAGE = "%".ord
16
+
17
+ def initialize(
18
+ source:,
19
+ string_scanner:,
20
+ line_numbers: false,
21
+ line_number: nil,
22
+ for_liquid_tag: false
23
+ )
24
+ @line_number = line_number || (line_numbers ? 1 : nil)
10
25
  @for_liquid_tag = for_liquid_tag
11
- @tokens = tokenize
26
+ @source = source.to_s.to_str
27
+ @offset = 0
28
+ @tokens = []
29
+
30
+ if @source
31
+ @ss = string_scanner
32
+ @ss.string = @source
33
+ tokenize
34
+ end
12
35
  end
13
36
 
14
37
  def shift
15
- (token = @tokens.shift) || return
38
+ token = @tokens[@offset]
39
+
40
+ return unless token
41
+
42
+ @offset += 1
16
43
 
17
44
  if @line_number
18
45
  @line_number += @for_liquid_tag ? 1 : token.count("\n")
@@ -24,16 +51,105 @@ module Liquid
24
51
  private
25
52
 
26
53
  def tokenize
27
- return [] if @source.empty?
54
+ if @for_liquid_tag
55
+ @tokens = @source.split("\n")
56
+ else
57
+ @tokens << shift_normal until @ss.eos?
58
+ end
59
+
60
+ @source = nil
61
+ @ss = nil
62
+ end
28
63
 
29
- return @source.split("\n") if @for_liquid_tag
64
+ def shift_normal
65
+ token = next_token
30
66
 
31
- tokens = @source.split(TemplateParser)
67
+ return unless token
32
68
 
33
- # removes the rogue empty element at the beginning of the array
34
- tokens.shift if tokens[0]&.empty?
69
+ token
70
+ end
71
+
72
+ def next_token
73
+ # possible states: :text, :tag, :variable
74
+ byte_a = @ss.peek_byte
75
+
76
+ if byte_a == OPEN_CURLEY
77
+ @ss.scan_byte
78
+
79
+ byte_b = @ss.peek_byte
80
+
81
+ if byte_b == PERCENTAGE
82
+ @ss.scan_byte
83
+ return next_tag_token
84
+ elsif byte_b == OPEN_CURLEY
85
+ @ss.scan_byte
86
+ return next_variable_token
87
+ end
88
+
89
+ @ss.pos -= 1
90
+ end
91
+
92
+ next_text_token
93
+ end
94
+
95
+ def next_text_token
96
+ start = @ss.pos
97
+
98
+ unless @ss.skip_until(TAG_OR_VARIABLE_START)
99
+ token = @ss.rest
100
+ @ss.terminate
101
+ return token
102
+ end
103
+
104
+ pos = @ss.pos -= 2
105
+ @source.byteslice(start, pos - start)
106
+ end
107
+
108
+ def next_variable_token
109
+ start = @ss.pos - 2
110
+
111
+ byte_a = byte_b = @ss.scan_byte
112
+
113
+ while byte_b
114
+ byte_a = @ss.scan_byte while byte_a && (byte_a != CLOSE_CURLEY && byte_a != OPEN_CURLEY)
115
+
116
+ break unless byte_a
117
+
118
+ if @ss.eos?
119
+ return byte_a == CLOSE_CURLEY ? @source.byteslice(start, @ss.pos - start) : "{{"
120
+ end
121
+
122
+ byte_b = @ss.scan_byte
123
+
124
+ if byte_a == CLOSE_CURLEY
125
+ if byte_b == CLOSE_CURLEY
126
+ return @source.byteslice(start, @ss.pos - start)
127
+ elsif byte_b != CLOSE_CURLEY
128
+ @ss.pos -= 1
129
+ return @source.byteslice(start, @ss.pos - start)
130
+ end
131
+ elsif byte_a == OPEN_CURLEY && byte_b == PERCENTAGE
132
+ return next_tag_token_with_start(start)
133
+ end
134
+
135
+ byte_a = byte_b
136
+ end
137
+
138
+ "{{"
139
+ end
140
+
141
+ def next_tag_token
142
+ start = @ss.pos - 2
143
+ if (len = @ss.skip_until(TAG_END))
144
+ @source.byteslice(start, len + 2)
145
+ else
146
+ "{%"
147
+ end
148
+ end
35
149
 
36
- tokens
150
+ def next_tag_token_with_start(start)
151
+ @ss.skip_until(TAG_END)
152
+ @source.byteslice(start, @ss.pos - start)
37
153
  end
38
154
  end
39
155
  end