liquid 2.6.3 → 5.4.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 (100) hide show
  1. checksums.yaml +5 -5
  2. data/History.md +272 -26
  3. data/README.md +67 -3
  4. data/lib/liquid/block.rb +62 -94
  5. data/lib/liquid/block_body.rb +255 -0
  6. data/lib/liquid/condition.rb +96 -38
  7. data/lib/liquid/context.rb +172 -154
  8. data/lib/liquid/document.rb +57 -9
  9. data/lib/liquid/drop.rb +33 -14
  10. data/lib/liquid/errors.rb +56 -10
  11. data/lib/liquid/expression.rb +45 -0
  12. data/lib/liquid/extensions.rb +21 -7
  13. data/lib/liquid/file_system.rb +27 -14
  14. data/lib/liquid/forloop_drop.rb +92 -0
  15. data/lib/liquid/i18n.rb +41 -0
  16. data/lib/liquid/interrupts.rb +3 -2
  17. data/lib/liquid/lexer.rb +62 -0
  18. data/lib/liquid/locales/en.yml +29 -0
  19. data/lib/liquid/parse_context.rb +54 -0
  20. data/lib/liquid/parse_tree_visitor.rb +42 -0
  21. data/lib/liquid/parser.rb +102 -0
  22. data/lib/liquid/parser_switching.rb +45 -0
  23. data/lib/liquid/partial_cache.rb +24 -0
  24. data/lib/liquid/profiler/hooks.rb +35 -0
  25. data/lib/liquid/profiler.rb +139 -0
  26. data/lib/liquid/range_lookup.rb +47 -0
  27. data/lib/liquid/registers.rb +51 -0
  28. data/lib/liquid/resource_limits.rb +62 -0
  29. data/lib/liquid/standardfilters.rb +789 -118
  30. data/lib/liquid/strainer_factory.rb +41 -0
  31. data/lib/liquid/strainer_template.rb +62 -0
  32. data/lib/liquid/tablerowloop_drop.rb +121 -0
  33. data/lib/liquid/tag/disableable.rb +22 -0
  34. data/lib/liquid/tag/disabler.rb +21 -0
  35. data/lib/liquid/tag.rb +49 -10
  36. data/lib/liquid/tags/assign.rb +61 -19
  37. data/lib/liquid/tags/break.rb +14 -4
  38. data/lib/liquid/tags/capture.rb +29 -21
  39. data/lib/liquid/tags/case.rb +80 -31
  40. data/lib/liquid/tags/comment.rb +24 -2
  41. data/lib/liquid/tags/continue.rb +14 -13
  42. data/lib/liquid/tags/cycle.rb +50 -32
  43. data/lib/liquid/tags/decrement.rb +24 -26
  44. data/lib/liquid/tags/echo.rb +41 -0
  45. data/lib/liquid/tags/for.rb +164 -100
  46. data/lib/liquid/tags/if.rb +105 -44
  47. data/lib/liquid/tags/ifchanged.rb +10 -11
  48. data/lib/liquid/tags/include.rb +85 -65
  49. data/lib/liquid/tags/increment.rb +24 -22
  50. data/lib/liquid/tags/inline_comment.rb +43 -0
  51. data/lib/liquid/tags/raw.rb +50 -11
  52. data/lib/liquid/tags/render.rb +109 -0
  53. data/lib/liquid/tags/table_row.rb +88 -0
  54. data/lib/liquid/tags/unless.rb +37 -21
  55. data/lib/liquid/template.rb +124 -46
  56. data/lib/liquid/template_factory.rb +9 -0
  57. data/lib/liquid/tokenizer.rb +39 -0
  58. data/lib/liquid/usage.rb +8 -0
  59. data/lib/liquid/utils.rb +68 -5
  60. data/lib/liquid/variable.rb +128 -32
  61. data/lib/liquid/variable_lookup.rb +96 -0
  62. data/lib/liquid/version.rb +3 -1
  63. data/lib/liquid.rb +36 -13
  64. metadata +69 -77
  65. data/lib/extras/liquid_view.rb +0 -51
  66. data/lib/liquid/htmltags.rb +0 -73
  67. data/lib/liquid/module_ex.rb +0 -62
  68. data/lib/liquid/strainer.rb +0 -53
  69. data/test/liquid/assign_test.rb +0 -21
  70. data/test/liquid/block_test.rb +0 -58
  71. data/test/liquid/capture_test.rb +0 -40
  72. data/test/liquid/condition_test.rb +0 -127
  73. data/test/liquid/context_test.rb +0 -478
  74. data/test/liquid/drop_test.rb +0 -180
  75. data/test/liquid/error_handling_test.rb +0 -81
  76. data/test/liquid/file_system_test.rb +0 -29
  77. data/test/liquid/filter_test.rb +0 -125
  78. data/test/liquid/hash_ordering_test.rb +0 -25
  79. data/test/liquid/module_ex_test.rb +0 -87
  80. data/test/liquid/output_test.rb +0 -116
  81. data/test/liquid/parsing_quirks_test.rb +0 -52
  82. data/test/liquid/regexp_test.rb +0 -44
  83. data/test/liquid/security_test.rb +0 -64
  84. data/test/liquid/standard_filter_test.rb +0 -263
  85. data/test/liquid/strainer_test.rb +0 -52
  86. data/test/liquid/tags/break_tag_test.rb +0 -16
  87. data/test/liquid/tags/continue_tag_test.rb +0 -16
  88. data/test/liquid/tags/for_tag_test.rb +0 -297
  89. data/test/liquid/tags/html_tag_test.rb +0 -63
  90. data/test/liquid/tags/if_else_tag_test.rb +0 -166
  91. data/test/liquid/tags/include_tag_test.rb +0 -166
  92. data/test/liquid/tags/increment_tag_test.rb +0 -24
  93. data/test/liquid/tags/raw_tag_test.rb +0 -24
  94. data/test/liquid/tags/standard_tag_test.rb +0 -295
  95. data/test/liquid/tags/statements_test.rb +0 -134
  96. data/test/liquid/tags/unless_else_tag_test.rb +0 -26
  97. data/test/liquid/template_test.rb +0 -146
  98. data/test/liquid/variable_test.rb +0 -186
  99. data/test/test_helper.rb +0 -29
  100. /data/{MIT-LICENSE → LICENSE} +0 -0
@@ -1,78 +1,139 @@
1
- module Liquid
1
+ # frozen_string_literal: true
2
2
 
3
- # If is the conditional block
4
- #
5
- # {% if user.admin %}
6
- # Admin user!
7
- # {% else %}
8
- # Not admin user
3
+ module Liquid
4
+ # @liquid_public_docs
5
+ # @liquid_type tag
6
+ # @liquid_category conditional
7
+ # @liquid_name if
8
+ # @liquid_summary
9
+ # Renders an expression if a specific condition is `true`.
10
+ # @liquid_syntax
11
+ # {% if condition %}
12
+ # expression
9
13
  # {% endif %}
10
- #
11
- # There are {% if count < 5 %} less {% else %} more {% endif %} items than you need.
12
- #
13
- #
14
+ # @liquid_syntax_keyword condition The condition to evaluate.
15
+ # @liquid_syntax_keyword expression The expression to render if the condition is met.
14
16
  class If < Block
15
- SyntaxHelp = "Syntax Error in tag 'if' - Valid syntax: if [expression]"
16
- Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o
17
+ Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o
17
18
  ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o
18
- BOOLEAN_OPERATORS = %w(and or)
19
+ BOOLEAN_OPERATORS = %w(and or).freeze
19
20
 
20
- def initialize(tag_name, markup, tokens)
21
- @blocks = []
21
+ attr_reader :blocks
22
22
 
23
+ def initialize(tag_name, markup, options)
24
+ super
25
+ @blocks = []
23
26
  push_block('if', markup)
27
+ end
24
28
 
25
- super
29
+ def nodelist
30
+ @blocks.map(&:attachment)
26
31
  end
27
32
 
33
+ def parse(tokens)
34
+ while parse_body(@blocks.last.attachment, tokens)
35
+ end
36
+ @blocks.reverse_each do |block|
37
+ block.attachment.remove_blank_strings if blank?
38
+ block.attachment.freeze
39
+ end
40
+ end
41
+
42
+ ELSE_TAG_NAMES = ['elsif', 'else'].freeze
43
+ private_constant :ELSE_TAG_NAMES
44
+
28
45
  def unknown_tag(tag, markup, tokens)
29
- if ['elsif', 'else'].include?(tag)
46
+ if ELSE_TAG_NAMES.include?(tag)
30
47
  push_block(tag, markup)
31
48
  else
32
49
  super
33
50
  end
34
51
  end
35
52
 
36
- def render(context)
37
- context.stack do
38
- @blocks.each do |block|
39
- if block.evaluate(context)
40
- return render_all(block.attachment, context)
41
- end
53
+ def render_to_output_buffer(context, output)
54
+ @blocks.each do |block|
55
+ result = Liquid::Utils.to_liquid_value(
56
+ block.evaluate(context)
57
+ )
58
+
59
+ if result
60
+ return block.attachment.render_to_output_buffer(context, output)
42
61
  end
43
- ''
44
62
  end
63
+
64
+ output
45
65
  end
46
66
 
47
67
  private
48
68
 
49
- def push_block(tag, markup)
50
- block = if tag == 'else'
51
- ElseCondition.new
52
- else
69
+ def push_block(tag, markup)
70
+ block = if tag == 'else'
71
+ ElseCondition.new
72
+ else
73
+ parse_with_selected_parser(markup)
74
+ end
53
75
 
54
- expressions = markup.scan(ExpressionsAndOperators).reverse
55
- raise(SyntaxError, SyntaxHelp) unless expressions.shift =~ Syntax
76
+ @blocks.push(block)
77
+ block.attach(new_body)
78
+ end
56
79
 
57
- condition = Condition.new($1, $2, $3)
80
+ def parse_expression(markup)
81
+ Condition.parse_expression(parse_context, markup)
82
+ end
58
83
 
59
- while not expressions.empty?
60
- operator = (expressions.shift).to_s.strip
84
+ def lax_parse(markup)
85
+ expressions = markup.scan(ExpressionsAndOperators)
86
+ raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop =~ Syntax
61
87
 
62
- raise(SyntaxError, SyntaxHelp) unless expressions.shift.to_s =~ Syntax
88
+ condition = Condition.new(parse_expression(Regexp.last_match(1)), Regexp.last_match(2), parse_expression(Regexp.last_match(3)))
63
89
 
64
- new_condition = Condition.new($1, $2, $3)
65
- raise SyntaxError, "invalid boolean operator" unless BOOLEAN_OPERATORS.include?(operator)
66
- new_condition.send(operator, condition)
67
- condition = new_condition
68
- end
90
+ until expressions.empty?
91
+ operator = expressions.pop.to_s.strip
69
92
 
70
- condition
71
- end
93
+ raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop.to_s =~ Syntax
94
+
95
+ new_condition = Condition.new(parse_expression(Regexp.last_match(1)), Regexp.last_match(2), parse_expression(Regexp.last_match(3)))
96
+ raise SyntaxError, options[:locale].t("errors.syntax.if") unless BOOLEAN_OPERATORS.include?(operator)
97
+ new_condition.send(operator, condition)
98
+ condition = new_condition
99
+ end
100
+
101
+ condition
102
+ end
103
+
104
+ def strict_parse(markup)
105
+ p = Parser.new(markup)
106
+ condition = parse_binary_comparisons(p)
107
+ p.consume(:end_of_string)
108
+ condition
109
+ end
72
110
 
73
- @blocks.push(block)
74
- @nodelist = block.attach(Array.new)
111
+ def parse_binary_comparisons(p)
112
+ condition = parse_comparison(p)
113
+ first_condition = condition
114
+ while (op = (p.id?('and') || p.id?('or')))
115
+ child_condition = parse_comparison(p)
116
+ condition.send(op, child_condition)
117
+ condition = child_condition
75
118
  end
119
+ first_condition
120
+ end
121
+
122
+ def parse_comparison(p)
123
+ a = parse_expression(p.expression)
124
+ if (op = p.consume?(:comparison))
125
+ b = parse_expression(p.expression)
126
+ Condition.new(a, op, b)
127
+ else
128
+ Condition.new(a)
129
+ end
130
+ end
131
+
132
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
133
+ def children
134
+ @node.blocks
135
+ end
136
+ end
76
137
  end
77
138
 
78
139
  Template.register_tag('if', If)
@@ -1,18 +1,17 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  class Ifchanged < Block
5
+ def render_to_output_buffer(context, output)
6
+ block_output = +''
7
+ super(context, block_output)
3
8
 
4
- def render(context)
5
- context.stack do
6
-
7
- output = render_all(@nodelist, context)
8
-
9
- if output != context.registers[:ifchanged]
10
- context.registers[:ifchanged] = output
11
- output
12
- else
13
- ''
14
- end
9
+ if block_output != context.registers[:ifchanged]
10
+ context.registers[:ifchanged] = block_output
11
+ output << block_output
15
12
  end
13
+
14
+ output
16
15
  end
17
16
  end
18
17
 
@@ -1,92 +1,112 @@
1
- module Liquid
1
+ # frozen_string_literal: true
2
2
 
3
- # Include allows templates to relate with other templates
4
- #
5
- # Simply include another template:
6
- #
7
- # {% include 'product' %}
8
- #
9
- # Include a template with a local variable:
10
- #
11
- # {% include 'product' with products[0] %}
12
- #
13
- # Include a template for a collection:
14
- #
15
- # {% include 'product' for products %}
3
+ module Liquid
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](/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.
16
18
  #
19
+ # The `include` tag has been replaced by [`render`](/api/liquid/tags#render).
17
20
  class Include < Tag
18
- Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/o
21
+ prepend Tag::Disableable
22
+
23
+ SYNTAX = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
24
+ Syntax = SYNTAX
19
25
 
20
- def initialize(tag_name, markup, tokens)
21
- if markup =~ Syntax
26
+ attr_reader :template_name_expr, :variable_name_expr, :attributes
27
+
28
+ def initialize(tag_name, markup, options)
29
+ super
22
30
 
23
- @template_name = $1
24
- @variable_name = $3
25
- @attributes = {}
31
+ if markup =~ SYNTAX
32
+
33
+ template_name = Regexp.last_match(1)
34
+ variable_name = Regexp.last_match(3)
35
+
36
+ @alias_name = Regexp.last_match(5)
37
+ @variable_name_expr = variable_name ? parse_expression(variable_name) : nil
38
+ @template_name_expr = parse_expression(template_name)
39
+ @attributes = {}
26
40
 
27
41
  markup.scan(TagAttributes) do |key, value|
28
- @attributes[key] = value
42
+ @attributes[key] = parse_expression(value)
29
43
  end
30
44
 
31
45
  else
32
- raise SyntaxError.new("Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]")
46
+ raise SyntaxError, options[:locale].t("errors.syntax.include")
33
47
  end
34
-
35
- super
36
48
  end
37
49
 
38
- def parse(tokens)
50
+ def parse(_tokens)
39
51
  end
40
52
 
41
- def render(context)
42
- partial = load_cached_partial(context)
43
- variable = context[@variable_name || @template_name[1..-2]]
53
+ def render_to_output_buffer(context, output)
54
+ template_name = context.evaluate(@template_name_expr)
55
+ raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name
44
56
 
45
- context.stack do
46
- @attributes.each do |key, value|
47
- context[key] = context[value]
48
- end
57
+ partial = PartialCache.load(
58
+ template_name,
59
+ context: context,
60
+ parse_context: parse_context
61
+ )
49
62
 
50
- if variable.is_a?(Array)
51
- variable.collect do |var|
52
- context[@template_name[1..-2]] = var
53
- partial.render(context)
54
- end
55
- else
56
- context[@template_name[1..-2]] = variable
57
- partial.render(context)
58
- end
63
+ context_variable_name = @alias_name || template_name.split('/').last
64
+
65
+ variable = if @variable_name_expr
66
+ context.evaluate(@variable_name_expr)
67
+ else
68
+ context.find_variable(template_name, raise_on_not_found: false)
59
69
  end
60
- end
61
70
 
62
- private
63
- def load_cached_partial(context)
64
- cached_partials = context.registers[:cached_partials] || {}
65
- template_name = context[@template_name]
71
+ old_template_name = context.template_name
72
+ old_partial = context.partial
73
+ begin
74
+ context.template_name = template_name
75
+ context.partial = true
76
+ context.stack do
77
+ @attributes.each do |key, value|
78
+ context[key] = context.evaluate(value)
79
+ end
66
80
 
67
- if cached = cached_partials[template_name]
68
- return cached
81
+ if variable.is_a?(Array)
82
+ variable.each do |var|
83
+ context[context_variable_name] = var
84
+ partial.render_to_output_buffer(context, output)
85
+ end
86
+ else
87
+ context[context_variable_name] = variable
88
+ partial.render_to_output_buffer(context, output)
89
+ end
69
90
  end
70
- source = read_template_from_file_system(context)
71
- partial = Liquid::Template.parse(source)
72
- cached_partials[template_name] = partial
73
- context.registers[:cached_partials] = cached_partials
74
- partial
91
+ ensure
92
+ context.template_name = old_template_name
93
+ context.partial = old_partial
75
94
  end
76
95
 
77
- def read_template_from_file_system(context)
78
- file_system = context.registers[:file_system] || Liquid::Template.file_system
79
-
80
- # make read_template_file call backwards-compatible.
81
- case file_system.method(:read_template_file).arity
82
- when 1
83
- file_system.read_template_file(context[@template_name])
84
- when 2
85
- file_system.read_template_file(context[@template_name], context)
86
- else
87
- raise ArgumentError, "file_system.read_template_file expects two parameters: (template_name, context)"
88
- end
96
+ output
97
+ end
98
+
99
+ alias_method :parse_context, :options
100
+ private :parse_context
101
+
102
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
103
+ def children
104
+ [
105
+ @node.template_name_expr,
106
+ @node.variable_name_expr,
107
+ ] + @node.attributes.values
89
108
  end
109
+ end
90
110
  end
91
111
 
92
112
  Template.register_tag('include', Include)
@@ -1,34 +1,36 @@
1
- module Liquid
2
-
3
- # increment is used in a place where one needs to insert a counter
4
- # into a template, and needs the counter to survive across
5
- # multiple instantiations of the template.
6
- # (To achieve the survival, the application must keep the context)
7
- #
8
- # if the variable does not exist, it is created with value 0.
1
+ # frozen_string_literal: true
9
2
 
10
- # Hello: {% increment variable %}
11
- #
12
- # gives you:
13
- #
14
- # Hello: 0
15
- # Hello: 1
16
- # Hello: 2
3
+ module Liquid
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.
10
+ # @liquid_description
11
+ # Variables that are declared with `increment` are unique to the [layout](/themes/architecture/layouts), [template](/themes/architecture/templates),
12
+ # or [section](/themes/architecture/sections) file that they're created in. However, the variable is shared across
13
+ # [snippets](/themes/architecture#snippets) included in the file.
17
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
17
+ # variables.
18
+ # @liquid_syntax
19
+ # {% increment variable_name %}
20
+ # @liquid_syntax_keyword variable_name The name of the variable being incremented.
18
21
  class Increment < Tag
19
- def initialize(tag_name, markup, tokens)
20
- @variable = markup.strip
21
-
22
+ def initialize(tag_name, markup, options)
22
23
  super
24
+ @variable = markup.strip
23
25
  end
24
26
 
25
- def render(context)
27
+ def render_to_output_buffer(context, output)
26
28
  value = context.environments.first[@variable] ||= 0
27
29
  context.environments.first[@variable] = value + 1
28
- value.to_s
29
- end
30
30
 
31
- private
31
+ output << value.to_s
32
+ output
33
+ end
32
34
  end
33
35
 
34
36
  Template.register_tag('increment', Increment)
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
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
+ class InlineComment < Tag
18
+ def initialize(tag_name, markup, options)
19
+ super
20
+
21
+ # Semantically, a comment should only ignore everything after it on the line.
22
+ # Currently, this implementation doesn't support mixing a comment with another tag
23
+ # but we need to reserve future support for this and prevent the introduction
24
+ # of inline comments from being backward incompatible change.
25
+ #
26
+ # As such, we're forcing users to put a # symbol on every line otherwise this
27
+ # tag will throw an error.
28
+ if markup.match?(/\n\s*[^#\s]/)
29
+ raise SyntaxError, options[:locale].t("errors.syntax.inline_comment_invalid")
30
+ end
31
+ end
32
+
33
+ def render_to_output_buffer(_context, output)
34
+ output
35
+ end
36
+
37
+ def blank?
38
+ true
39
+ end
40
+ end
41
+
42
+ Template.register_tag('#', InlineComment)
43
+ end
@@ -1,19 +1,58 @@
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
- FullTokenPossiblyInvalid = /^(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}$/o
16
+ Syntax = /\A\s*\z/
17
+ FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
18
+
19
+ def initialize(tag_name, markup, parse_context)
20
+ super
21
+
22
+ ensure_valid_markup(tag_name, markup, parse_context)
23
+ end
4
24
 
5
25
  def parse(tokens)
6
- @nodelist ||= []
7
- @nodelist.clear
8
- while token = tokens.shift
9
- if token =~ FullTokenPossiblyInvalid
10
- @nodelist << $1 if $1 != ""
11
- if block_delimiter == $2
12
- end_tag
13
- return
14
- end
26
+ @body = +''
27
+ while (token = tokens.shift)
28
+ if token =~ FullTokenPossiblyInvalid && block_delimiter == Regexp.last_match(2)
29
+ @body << Regexp.last_match(1) if Regexp.last_match(1) != ""
30
+ return
15
31
  end
16
- @nodelist << token if not token.empty?
32
+ @body << token unless token.empty?
33
+ end
34
+
35
+ raise_tag_never_closed(block_name)
36
+ end
37
+
38
+ def render_to_output_buffer(_context, output)
39
+ output << @body
40
+ output
41
+ end
42
+
43
+ def nodelist
44
+ [@body]
45
+ end
46
+
47
+ def blank?
48
+ @body.empty?
49
+ end
50
+
51
+ protected
52
+
53
+ def ensure_valid_markup(tag_name, markup, parse_context)
54
+ unless Syntax.match?(markup)
55
+ raise SyntaxError, parse_context.locale.t("errors.syntax.tag_unexpected_args", tag: tag_name)
17
56
  end
18
57
  end
19
58
  end
@@ -0,0 +1,109 @@
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](/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)
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](/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).
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](/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}+)(\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
35
+
36
+ def initialize(tag_name, markup, options)
37
+ super
38
+
39
+ raise SyntaxError, options[:locale].t("errors.syntax.render") unless markup =~ SYNTAX
40
+
41
+ template_name = Regexp.last_match(1)
42
+ with_or_for = Regexp.last_match(3)
43
+ variable_name = Regexp.last_match(4)
44
+
45
+ @alias_name = Regexp.last_match(6)
46
+ @variable_name_expr = variable_name ? parse_expression(variable_name) : nil
47
+ @template_name_expr = parse_expression(template_name)
48
+ @for = (with_or_for == FOR)
49
+
50
+ @attributes = {}
51
+ markup.scan(TagAttributes) do |key, value|
52
+ @attributes[key] = parse_expression(value)
53
+ end
54
+ end
55
+
56
+ def render_to_output_buffer(context, output)
57
+ render_tag(context, output)
58
+ end
59
+
60
+ def render_tag(context, output)
61
+ # The expression should be a String literal, which parses to a String object
62
+ template_name = @template_name_expr
63
+ raise ::ArgumentError unless template_name.is_a?(String)
64
+
65
+ partial = PartialCache.load(
66
+ template_name,
67
+ context: context,
68
+ parse_context: parse_context
69
+ )
70
+
71
+ context_variable_name = @alias_name || template_name.split('/').last
72
+
73
+ render_partial_func = ->(var, forloop) {
74
+ inner_context = context.new_isolated_subcontext
75
+ inner_context.template_name = template_name
76
+ inner_context.partial = true
77
+ inner_context['forloop'] = forloop if forloop
78
+
79
+ @attributes.each do |key, value|
80
+ inner_context[key] = context.evaluate(value)
81
+ end
82
+ inner_context[context_variable_name] = var unless var.nil?
83
+ partial.render_to_output_buffer(inner_context, output)
84
+ forloop&.send(:increment!)
85
+ }
86
+
87
+ variable = @variable_name_expr ? context.evaluate(@variable_name_expr) : nil
88
+ if @for && variable.respond_to?(:each) && variable.respond_to?(:count)
89
+ forloop = Liquid::ForloopDrop.new(template_name, variable.count, nil)
90
+ variable.each { |var| render_partial_func.call(var, forloop) }
91
+ else
92
+ render_partial_func.call(variable, nil)
93
+ end
94
+
95
+ output
96
+ end
97
+
98
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
99
+ def children
100
+ [
101
+ @node.template_name_expr,
102
+ @node.variable_name_expr,
103
+ ] + @node.attributes.values
104
+ end
105
+ end
106
+ end
107
+
108
+ Template.register_tag('render', Render)
109
+ end