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,113 +1,139 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
- # Include allows templates to relate with other templates
3
- #
4
- # Simply include another template:
5
- #
6
- # {% include 'product' %}
7
- #
8
- # Include a template with a local variable:
9
- #
10
- # {% include 'product' with products[0] %}
11
- #
12
- # Include a template for a collection:
13
- #
14
- # {% include 'product' for products %}
4
+ # @liquid_public_docs
5
+ # @liquid_type tag
6
+ # @liquid_category theme
7
+ # @liquid_name include
8
+ # @liquid_summary
9
+ # Renders a [snippet](/themes/architecture/snippets).
10
+ # @liquid_description
11
+ # Inside the snippet, you can access and alter variables that are [created](/docs/api/liquid/tags/variable-tags) outside of the
12
+ # snippet.
13
+ # @liquid_syntax
14
+ # {% include 'filename' %}
15
+ # @liquid_syntax_keyword filename The name of the snippet to render, without the `.liquid` extension.
16
+ # @liquid_deprecated
17
+ # Deprecated because the way that variables are handled reduces performance and makes code harder to both read and maintain.
15
18
  #
19
+ # The `include` tag has been replaced by [`render`](/docs/api/liquid/tags/render).
16
20
  class Include < Tag
17
- Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/o
18
-
19
- def initialize(tag_name, markup, options)
20
- super
21
-
22
- if markup =~ Syntax
21
+ prepend Tag::Disableable
23
22
 
24
- template_name = $1
25
- variable_name = $3
23
+ SYNTAX = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
24
+ Syntax = SYNTAX
26
25
 
27
- @variable_name_expr = variable_name ? Expression.parse(variable_name) : nil
28
- @template_name_expr = Expression.parse(template_name)
29
- @attributes = {}
26
+ attr_reader :template_name_expr, :variable_name_expr, :attributes
30
27
 
31
- markup.scan(TagAttributes) do |key, value|
32
- @attributes[key] = Expression.parse(value)
33
- end
34
-
35
- else
36
- raise SyntaxError.new(options[:locale].t("errors.syntax.include".freeze))
37
- end
28
+ def initialize(tag_name, markup, options)
29
+ super
30
+ parse_with_selected_parser(markup)
38
31
  end
39
32
 
40
33
  def parse(_tokens)
41
34
  end
42
35
 
43
- def render(context)
36
+ def render_to_output_buffer(context, output)
44
37
  template_name = context.evaluate(@template_name_expr)
45
- raise ArgumentError.new(options[:locale].t("errors.argument.include")) unless template_name
38
+ raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name.is_a?(String)
39
+
40
+ partial = PartialCache.load(
41
+ template_name,
42
+ context: context,
43
+ parse_context: parse_context,
44
+ )
46
45
 
47
- partial = load_cached_partial(template_name, context)
48
- context_variable_name = template_name.split('/'.freeze).last
46
+ context_variable_name = @alias_name || template_name.split('/').last
49
47
 
50
48
  variable = if @variable_name_expr
51
49
  context.evaluate(@variable_name_expr)
52
50
  else
53
- context.find_variable(template_name)
51
+ context.find_variable(template_name, raise_on_not_found: false)
54
52
  end
55
53
 
56
54
  old_template_name = context.template_name
57
- old_partial = context.partial
55
+ old_partial = context.partial
56
+
58
57
  begin
59
- context.template_name = template_name
58
+ context.template_name = partial.name
60
59
  context.partial = true
60
+
61
61
  context.stack do
62
62
  @attributes.each do |key, value|
63
63
  context[key] = context.evaluate(value)
64
64
  end
65
65
 
66
66
  if variable.is_a?(Array)
67
- variable.collect do |var|
67
+ variable.each do |var|
68
68
  context[context_variable_name] = var
69
- partial.render(context)
69
+ partial.render_to_output_buffer(context, output)
70
70
  end
71
71
  else
72
72
  context[context_variable_name] = variable
73
- partial.render(context)
73
+ partial.render_to_output_buffer(context, output)
74
74
  end
75
75
  end
76
76
  ensure
77
77
  context.template_name = old_template_name
78
- context.partial = old_partial
78
+ context.partial = old_partial
79
79
  end
80
- end
81
80
 
82
- private
81
+ output
82
+ end
83
83
 
84
84
  alias_method :parse_context, :options
85
85
  private :parse_context
86
86
 
87
- def load_cached_partial(template_name, context)
88
- cached_partials = context.registers[:cached_partials] || {}
87
+ def rigid_parse(markup)
88
+ p = @parse_context.new_parser(markup)
89
89
 
90
- if cached = cached_partials[template_name]
91
- return cached
92
- end
93
- source = read_template_from_file_system(context)
94
- begin
95
- parse_context.partial = true
96
- partial = Liquid::Template.parse(source, parse_context)
97
- ensure
98
- parse_context.partial = false
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)
99
102
  end
100
- cached_partials[template_name] = partial
101
- context.registers[:cached_partials] = cached_partials
102
- partial
103
+
104
+ p.consume(:end_of_string)
105
+ end
106
+
107
+ def strict_parse(markup)
108
+ lax_parse(markup)
103
109
  end
104
110
 
105
- def read_template_from_file_system(context)
106
- file_system = context.registers[:file_system] || Liquid::Template.file_system
111
+ def lax_parse(markup)
112
+ if markup =~ SYNTAX
113
+ template_name = Regexp.last_match(1)
114
+ variable_name = Regexp.last_match(3)
107
115
 
108
- file_system.read_template_file(context.evaluate(@template_name_expr))
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
109
128
  end
110
- end
111
129
 
112
- Template.register_tag('include'.freeze, Include)
130
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
131
+ def children
132
+ [
133
+ @node.template_name_expr,
134
+ @node.variable_name_expr,
135
+ ] + @node.attributes.values
136
+ end
137
+ end
138
+ end
113
139
  end
@@ -1,31 +1,42 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
- # increment is used in a place where one needs to insert a counter
3
- # into a template, and needs the counter to survive across
4
- # multiple instantiations of the template.
5
- # (To achieve the survival, the application must keep the context)
6
- #
7
- # if the variable does not exist, it is created with value 0.
8
- #
9
- # Hello: {% increment variable %}
10
- #
11
- # gives you:
4
+ # @liquid_public_docs
5
+ # @liquid_type tag
6
+ # @liquid_category variable
7
+ # @liquid_name increment
8
+ # @liquid_summary
9
+ # Creates a new variable, with a default value of 0, that's increased by 1 with each subsequent call.
12
10
  #
13
- # Hello: 0
14
- # Hello: 1
15
- # Hello: 2
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.
14
+ # @liquid_description
15
+ # Variables that are declared with `increment` are unique to the [layout](/themes/architecture/layouts), [template](/themes/architecture/templates),
16
+ # or [section](/themes/architecture/sections) file that they're created in. However, the variable is shared across
17
+ # [snippets](/themes/architecture/snippets) included in the file.
16
18
  #
19
+ # Similarly, variables that are created with `increment` are independent from those created with [`assign`](/docs/api/liquid/tags/assign)
20
+ # and [`capture`](/docs/api/liquid/tags/capture). However, `increment` and [`decrement`](/docs/api/liquid/tags/decrement) share
21
+ # variables.
22
+ # @liquid_syntax
23
+ # {% increment variable_name %}
24
+ # @liquid_syntax_keyword variable_name The name of the variable being incremented.
17
25
  class Increment < Tag
26
+ attr_reader :variable_name
27
+
18
28
  def initialize(tag_name, markup, options)
19
29
  super
20
- @variable = markup.strip
30
+ @variable_name = markup.strip
21
31
  end
22
32
 
23
- def render(context)
24
- value = context.environments.first[@variable] ||= 0
25
- context.environments.first[@variable] = value + 1
26
- value.to_s
33
+ def render_to_output_buffer(context, output)
34
+ counter_environment = context.environments.first
35
+ value = counter_environment[@variable_name] || 0
36
+ counter_environment[@variable_name] = value + 1
37
+
38
+ output << value.to_s
39
+ output
27
40
  end
28
41
  end
29
-
30
- Template.register_tag('increment'.freeze, Increment)
31
42
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquid
4
+ class InlineComment < Tag
5
+ def initialize(tag_name, markup, options)
6
+ super
7
+
8
+ # Semantically, a comment should only ignore everything after it on the line.
9
+ # Currently, this implementation doesn't support mixing a comment with another tag
10
+ # but we need to reserve future support for this and prevent the introduction
11
+ # of inline comments from being backward incompatible change.
12
+ #
13
+ # As such, we're forcing users to put a # symbol on every line otherwise this
14
+ # tag will throw an error.
15
+ if markup.match?(/\n\s*[^#\s]/)
16
+ raise SyntaxError, options[:locale].t("errors.syntax.inline_comment_invalid")
17
+ end
18
+ end
19
+
20
+ def render_to_output_buffer(_context, output)
21
+ output
22
+ end
23
+
24
+ def blank?
25
+ true
26
+ end
27
+ end
28
+ end
@@ -1,7 +1,19 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
4
+ # @liquid_public_docs
5
+ # @liquid_type tag
6
+ # @liquid_category syntax
7
+ # @liquid_name raw
8
+ # @liquid_summary
9
+ # Outputs any Liquid code as text instead of rendering it.
10
+ # @liquid_syntax
11
+ # {% raw %}
12
+ # expression
13
+ # {% endraw %}
14
+ # @liquid_syntax_keyword expression The expression to be output without being rendered.
2
15
  class Raw < Block
3
16
  Syntax = /\A\s*\z/
4
- FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
5
17
 
6
18
  def initialize(tag_name, markup, parse_context)
7
19
  super
@@ -10,20 +22,22 @@ module Liquid
10
22
  end
11
23
 
12
24
  def parse(tokens)
13
- @body = ''
14
- while token = tokens.shift
15
- if token =~ FullTokenPossiblyInvalid
16
- @body << $1 if $1 != "".freeze
17
- return if block_delimiter == $2
25
+ @body = +''
26
+ while (token = tokens.shift)
27
+ if token =~ BlockBody::FullTokenPossiblyInvalid && block_delimiter == Regexp.last_match(2)
28
+ parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
29
+ @body << Regexp.last_match(1) if Regexp.last_match(1) != ""
30
+ return
18
31
  end
19
32
  @body << token unless token.empty?
20
33
  end
21
34
 
22
- raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
35
+ raise_tag_never_closed(block_name)
23
36
  end
24
37
 
25
- def render(_context)
26
- @body
38
+ def render_to_output_buffer(_context, output)
39
+ output << @body
40
+ output
27
41
  end
28
42
 
29
43
  def nodelist
@@ -37,11 +51,9 @@ module Liquid
37
51
  protected
38
52
 
39
53
  def ensure_valid_markup(tag_name, markup, parse_context)
40
- unless markup =~ Syntax
41
- raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_unexpected_args".freeze, tag: tag_name))
54
+ unless Syntax.match?(markup)
55
+ raise SyntaxError, parse_context.locale.t("errors.syntax.tag_unexpected_args", tag: tag_name)
42
56
  end
43
57
  end
44
58
  end
45
-
46
- Template.register_tag('raw'.freeze, Raw)
47
59
  end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquid
4
+ # @liquid_public_docs
5
+ # @liquid_type tag
6
+ # @liquid_category theme
7
+ # @liquid_name render
8
+ # @liquid_summary
9
+ # Renders a [snippet](/themes/architecture/snippets) or [app block](/themes/architecture/sections/section-schema#render-app-blocks).
10
+ # @liquid_description
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
+ # to pass outside variables to snippets.
14
+ #
15
+ # While you can't directly access created variables, you can access global objects, as well as any objects that are
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](/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
+ #
20
+ # Outside a snippet or app block, you can't access variables created inside the snippet or app block.
21
+ #
22
+ # > Note:
23
+ # > When you render a snippet using the `render` tag, you can't use the [`include` tag](/docs/api/liquid/tags/include)
24
+ # > inside the snippet.
25
+ # @liquid_syntax
26
+ # {% render 'filename' %}
27
+ # @liquid_syntax_keyword filename The name of the snippet to render, without the `.liquid` extension.
28
+ class Render < Tag
29
+ FOR = 'for'
30
+ SYNTAX = /(#{QuotedString}+|#{VariableSegment}+)(\s+(with|#{FOR})\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
31
+
32
+ disable_tags "include"
33
+
34
+ attr_reader :template_name_expr, :variable_name_expr, :attributes, :alias_name
35
+
36
+ def initialize(tag_name, markup, options)
37
+ super
38
+ parse_with_selected_parser(markup)
39
+ end
40
+
41
+ def for_loop?
42
+ @is_for_loop
43
+ end
44
+
45
+ def render_to_output_buffer(context, output)
46
+ render_tag(context, output)
47
+ end
48
+
49
+ def render_tag(context, output)
50
+ template = context.evaluate(@template_name_expr)
51
+
52
+ if template.respond_to?(:to_partial)
53
+ partial = template.to_partial
54
+ template_name = template.filename
55
+ context_variable_name = @alias_name || template.name
56
+ elsif @template_name_expr.is_a?(String)
57
+ partial = PartialCache.load(template, context: context, parse_context: parse_context)
58
+ template_name = partial.name
59
+ context_variable_name = @alias_name || template_name.split('/').last
60
+ else
61
+ raise ::ArgumentError
62
+ end
63
+
64
+ render_partial_func = ->(var, forloop) {
65
+ inner_context = context.new_isolated_subcontext
66
+ inner_context.template_name = template_name
67
+ inner_context.partial = true
68
+ inner_context['forloop'] = forloop if forloop
69
+
70
+ @attributes.each do |key, value|
71
+ inner_context[key] = context.evaluate(value)
72
+ end
73
+ inner_context[context_variable_name] = var unless var.nil?
74
+ partial.render_to_output_buffer(inner_context, output)
75
+ forloop&.send(:increment!)
76
+ }
77
+
78
+ variable = @variable_name_expr ? context.evaluate(@variable_name_expr) : nil
79
+ if @is_for_loop && variable.respond_to?(:each) && variable.respond_to?(:count)
80
+ forloop = Liquid::ForloopDrop.new(template_name, variable.count, nil)
81
+ variable.each { |var| render_partial_func.call(var, forloop) }
82
+ else
83
+ render_partial_func.call(variable, nil)
84
+ end
85
+
86
+ output
87
+ end
88
+
89
+ # render (string) (with|for expression)? (as id)? (key: value)*
90
+ def rigid_parse(markup)
91
+ p = @parse_context.new_parser(markup)
92
+
93
+ @template_name_expr = parse_expression(rigid_template_name(p), safe: true)
94
+ with_or_for = p.id?("for") || p.id?("with")
95
+ @variable_name_expr = safe_parse_expression(p) if with_or_for
96
+ @alias_name = p.consume(:id) if p.id?("as")
97
+ @is_for_loop = (with_or_for == FOR)
98
+
99
+ p.consume?(:comma)
100
+
101
+ @attributes = {}
102
+ while p.look(:id)
103
+ key = p.consume
104
+ p.consume(:colon)
105
+ @attributes[key] = safe_parse_expression(p)
106
+ p.consume?(:comma) # optional comma
107
+ end
108
+
109
+ p.consume(:end_of_string)
110
+ end
111
+
112
+ def rigid_template_name(p)
113
+ return p.consume(:string) if p.look(:string)
114
+ return p.consume(:id) if p.look(:id)
115
+
116
+ found = p.consume || "nothing"
117
+ raise SyntaxError, options[:locale].t("errors.syntax.render_invalid_template_name", found: found)
118
+ end
119
+
120
+ def strict_parse(markup)
121
+ lax_parse(markup)
122
+ end
123
+
124
+ def lax_parse(markup)
125
+ raise SyntaxError, options[:locale].t("errors.syntax.render") unless markup =~ SYNTAX
126
+
127
+ template_name = Regexp.last_match(1)
128
+ with_or_for = Regexp.last_match(3)
129
+ variable_name = Regexp.last_match(4)
130
+
131
+ @alias_name = Regexp.last_match(6)
132
+ @variable_name_expr = variable_name ? parse_expression(variable_name) : nil
133
+ @template_name_expr = parse_expression(template_name)
134
+ @is_for_loop = (with_or_for == FOR)
135
+
136
+ @attributes = {}
137
+ markup.scan(TagAttributes) do |key, value|
138
+ @attributes[key] = parse_expression(value)
139
+ end
140
+ end
141
+
142
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
143
+ def children
144
+ [
145
+ @node.template_name_expr,
146
+ @node.variable_name_expr,
147
+ ] + @node.attributes.values
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquid
4
+ # @liquid_public_docs
5
+ # @liquid_type tag
6
+ # @liquid_category variable
7
+ # @liquid_name snippet
8
+ # @liquid_summary
9
+ # Creates a new inline snippet.
10
+ # @liquid_description
11
+ # You can create inline snippets to make your Liquid code more modular.
12
+ # @liquid_syntax
13
+ # {% snippet snippet_name %}
14
+ # value
15
+ # {% endsnippet %}
16
+ class Snippet < Block
17
+ def initialize(tag_name, markup, options)
18
+ super
19
+ p = @parse_context.new_parser(markup)
20
+ if p.look(:id)
21
+ @to = p.consume(:id)
22
+ p.consume(:end_of_string)
23
+ else
24
+ raise SyntaxError, options[:locale].t("errors.syntax.snippet")
25
+ end
26
+ end
27
+
28
+ def render_to_output_buffer(context, output)
29
+ snippet_drop = SnippetDrop.new(@body, @to, context.template_name)
30
+ context.scopes.last[@to] = snippet_drop
31
+ context.resource_limits.increment_assign_score(assign_score_of(snippet_drop))
32
+ output
33
+ end
34
+
35
+ def blank?
36
+ true
37
+ end
38
+
39
+ private
40
+
41
+ def assign_score_of(snippet_drop)
42
+ snippet_drop.body.nodelist.sum { |node| node.to_s.bytesize }
43
+ end
44
+ end
45
+ end