liquid 5.4.0 → 5.6.4

Sign up to get free protection for your applications and to get access to all the features.
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