liquid 4.0.1 → 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 (112) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +142 -0
  3. data/README.md +10 -4
  4. data/lib/liquid/block.rb +31 -14
  5. data/lib/liquid/block_body.rb +169 -56
  6. data/lib/liquid/condition.rb +59 -23
  7. data/lib/liquid/context.rb +111 -52
  8. data/lib/liquid/document.rb +47 -9
  9. data/lib/liquid/drop.rb +4 -2
  10. data/lib/liquid/errors.rb +20 -18
  11. data/lib/liquid/expression.rb +29 -33
  12. data/lib/liquid/extensions.rb +2 -0
  13. data/lib/liquid/file_system.rb +6 -4
  14. data/lib/liquid/forloop_drop.rb +54 -4
  15. data/lib/liquid/i18n.rb +5 -3
  16. data/lib/liquid/interrupts.rb +3 -1
  17. data/lib/liquid/lexer.rb +31 -24
  18. data/lib/liquid/locales/en.yml +8 -5
  19. data/lib/liquid/parse_context.rb +20 -4
  20. data/lib/liquid/parse_tree_visitor.rb +42 -0
  21. data/lib/liquid/parser.rb +30 -18
  22. data/lib/liquid/parser_switching.rb +17 -3
  23. data/lib/liquid/partial_cache.rb +24 -0
  24. data/lib/liquid/profiler/hooks.rb +26 -14
  25. data/lib/liquid/profiler.rb +67 -86
  26. data/lib/liquid/range_lookup.rb +13 -3
  27. data/lib/liquid/registers.rb +51 -0
  28. data/lib/liquid/resource_limits.rb +47 -8
  29. data/lib/liquid/standardfilters.rb +616 -129
  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 +64 -5
  33. data/lib/liquid/tag/disableable.rb +22 -0
  34. data/lib/liquid/tag/disabler.rb +21 -0
  35. data/lib/liquid/tag.rb +28 -6
  36. data/lib/liquid/tags/assign.rb +44 -18
  37. data/lib/liquid/tags/break.rb +16 -3
  38. data/lib/liquid/tags/capture.rb +24 -18
  39. data/lib/liquid/tags/case.rb +69 -27
  40. data/lib/liquid/tags/comment.rb +18 -3
  41. data/lib/liquid/tags/continue.rb +16 -12
  42. data/lib/liquid/tags/cycle.rb +45 -25
  43. data/lib/liquid/tags/decrement.rb +22 -20
  44. data/lib/liquid/tags/echo.rb +41 -0
  45. data/lib/liquid/tags/for.rb +97 -89
  46. data/lib/liquid/tags/if.rb +61 -35
  47. data/lib/liquid/tags/ifchanged.rb +11 -10
  48. data/lib/liquid/tags/include.rb +56 -56
  49. data/lib/liquid/tags/increment.rb +23 -17
  50. data/lib/liquid/tags/inline_comment.rb +43 -0
  51. data/lib/liquid/tags/raw.rb +25 -11
  52. data/lib/liquid/tags/render.rb +109 -0
  53. data/lib/liquid/tags/table_row.rb +53 -19
  54. data/lib/liquid/tags/unless.rb +38 -19
  55. data/lib/liquid/template.rb +52 -72
  56. data/lib/liquid/template_factory.rb +9 -0
  57. data/lib/liquid/tokenizer.rb +18 -10
  58. data/lib/liquid/usage.rb +8 -0
  59. data/lib/liquid/utils.rb +13 -3
  60. data/lib/liquid/variable.rb +52 -41
  61. data/lib/liquid/variable_lookup.rb +24 -10
  62. data/lib/liquid/version.rb +3 -1
  63. data/lib/liquid.rb +19 -6
  64. metadata +21 -104
  65. data/lib/liquid/strainer.rb +0 -66
  66. data/test/fixtures/en_locale.yml +0 -9
  67. data/test/integration/assign_test.rb +0 -48
  68. data/test/integration/blank_test.rb +0 -106
  69. data/test/integration/block_test.rb +0 -12
  70. data/test/integration/capture_test.rb +0 -50
  71. data/test/integration/context_test.rb +0 -32
  72. data/test/integration/document_test.rb +0 -19
  73. data/test/integration/drop_test.rb +0 -273
  74. data/test/integration/error_handling_test.rb +0 -260
  75. data/test/integration/filter_test.rb +0 -178
  76. data/test/integration/hash_ordering_test.rb +0 -23
  77. data/test/integration/output_test.rb +0 -123
  78. data/test/integration/parsing_quirks_test.rb +0 -122
  79. data/test/integration/render_profiling_test.rb +0 -154
  80. data/test/integration/security_test.rb +0 -80
  81. data/test/integration/standard_filter_test.rb +0 -626
  82. data/test/integration/tags/break_tag_test.rb +0 -15
  83. data/test/integration/tags/continue_tag_test.rb +0 -15
  84. data/test/integration/tags/for_tag_test.rb +0 -410
  85. data/test/integration/tags/if_else_tag_test.rb +0 -188
  86. data/test/integration/tags/include_tag_test.rb +0 -245
  87. data/test/integration/tags/increment_tag_test.rb +0 -23
  88. data/test/integration/tags/raw_tag_test.rb +0 -31
  89. data/test/integration/tags/standard_tag_test.rb +0 -296
  90. data/test/integration/tags/statements_test.rb +0 -111
  91. data/test/integration/tags/table_row_test.rb +0 -64
  92. data/test/integration/tags/unless_else_tag_test.rb +0 -26
  93. data/test/integration/template_test.rb +0 -332
  94. data/test/integration/trim_mode_test.rb +0 -529
  95. data/test/integration/variable_test.rb +0 -96
  96. data/test/test_helper.rb +0 -116
  97. data/test/unit/block_unit_test.rb +0 -58
  98. data/test/unit/condition_unit_test.rb +0 -166
  99. data/test/unit/context_unit_test.rb +0 -489
  100. data/test/unit/file_system_unit_test.rb +0 -35
  101. data/test/unit/i18n_unit_test.rb +0 -37
  102. data/test/unit/lexer_unit_test.rb +0 -51
  103. data/test/unit/parser_unit_test.rb +0 -82
  104. data/test/unit/regexp_unit_test.rb +0 -44
  105. data/test/unit/strainer_unit_test.rb +0 -164
  106. data/test/unit/tag_unit_test.rb +0 -21
  107. data/test/unit/tags/case_tag_unit_test.rb +0 -10
  108. data/test/unit/tags/for_tag_unit_test.rb +0 -13
  109. data/test/unit/tags/if_tag_unit_test.rb +0 -8
  110. data/test/unit/template_unit_test.rb +0 -78
  111. data/test/unit/tokenizer_unit_test.rb +0 -55
  112. data/test/unit/variable_unit_test.rb +0 -162
@@ -1,79 +1,99 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
- # If is the conditional block
3
- #
4
- # {% if user.admin %}
5
- # Admin user!
6
- # {% else %}
7
- # Not admin user
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
8
13
  # {% endif %}
9
- #
10
- # There are {% if count < 5 %} less {% else %} more {% endif %} items than you need.
11
- #
14
+ # @liquid_syntax_keyword condition The condition to evaluate.
15
+ # @liquid_syntax_keyword expression The expression to render if the condition is met.
12
16
  class If < Block
13
- Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o
17
+ Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o
14
18
  ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o
15
- BOOLEAN_OPERATORS = %w(and or)
19
+ BOOLEAN_OPERATORS = %w(and or).freeze
20
+
21
+ attr_reader :blocks
16
22
 
17
23
  def initialize(tag_name, markup, options)
18
24
  super
19
25
  @blocks = []
20
- push_block('if'.freeze, markup)
26
+ push_block('if', markup)
27
+ end
28
+
29
+ def nodelist
30
+ @blocks.map(&:attachment)
21
31
  end
22
32
 
23
33
  def parse(tokens)
24
34
  while parse_body(@blocks.last.attachment, tokens)
25
35
  end
36
+ @blocks.reverse_each do |block|
37
+ block.attachment.remove_blank_strings if blank?
38
+ block.attachment.freeze
39
+ end
26
40
  end
27
41
 
28
- def nodelist
29
- @blocks.map(&:attachment)
30
- end
42
+ ELSE_TAG_NAMES = ['elsif', 'else'].freeze
43
+ private_constant :ELSE_TAG_NAMES
31
44
 
32
45
  def unknown_tag(tag, markup, tokens)
33
- if ['elsif'.freeze, 'else'.freeze].include?(tag)
46
+ if ELSE_TAG_NAMES.include?(tag)
34
47
  push_block(tag, markup)
35
48
  else
36
49
  super
37
50
  end
38
51
  end
39
52
 
40
- def render(context)
41
- context.stack do
42
- @blocks.each do |block|
43
- if block.evaluate(context)
44
- return block.attachment.render(context)
45
- 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)
46
61
  end
47
- ''.freeze
48
62
  end
63
+
64
+ output
49
65
  end
50
66
 
51
67
  private
52
68
 
53
69
  def push_block(tag, markup)
54
- block = if tag == 'else'.freeze
70
+ block = if tag == 'else'
55
71
  ElseCondition.new
56
72
  else
57
73
  parse_with_selected_parser(markup)
58
74
  end
59
75
 
60
76
  @blocks.push(block)
61
- block.attach(BlockBody.new)
77
+ block.attach(new_body)
78
+ end
79
+
80
+ def parse_expression(markup)
81
+ Condition.parse_expression(parse_context, markup)
62
82
  end
63
83
 
64
84
  def lax_parse(markup)
65
85
  expressions = markup.scan(ExpressionsAndOperators)
66
- raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop =~ Syntax
86
+ raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop =~ Syntax
67
87
 
68
- condition = Condition.new(Expression.parse($1), $2, Expression.parse($3))
88
+ condition = Condition.new(parse_expression(Regexp.last_match(1)), Regexp.last_match(2), parse_expression(Regexp.last_match(3)))
69
89
 
70
90
  until expressions.empty?
71
91
  operator = expressions.pop.to_s.strip
72
92
 
73
- raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop.to_s =~ Syntax
93
+ raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop.to_s =~ Syntax
74
94
 
75
- new_condition = Condition.new(Expression.parse($1), $2, Expression.parse($3))
76
- raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless BOOLEAN_OPERATORS.include?(operator)
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)
77
97
  new_condition.send(operator, condition)
78
98
  condition = new_condition
79
99
  end
@@ -91,7 +111,7 @@ module Liquid
91
111
  def parse_binary_comparisons(p)
92
112
  condition = parse_comparison(p)
93
113
  first_condition = condition
94
- while op = (p.id?('and'.freeze) || p.id?('or'.freeze))
114
+ while (op = (p.id?('and') || p.id?('or')))
95
115
  child_condition = parse_comparison(p)
96
116
  condition.send(op, child_condition)
97
117
  condition = child_condition
@@ -100,15 +120,21 @@ module Liquid
100
120
  end
101
121
 
102
122
  def parse_comparison(p)
103
- a = Expression.parse(p.expression)
104
- if op = p.consume?(:comparison)
105
- b = Expression.parse(p.expression)
123
+ a = parse_expression(p.expression)
124
+ if (op = p.consume?(:comparison))
125
+ b = parse_expression(p.expression)
106
126
  Condition.new(a, op, b)
107
127
  else
108
128
  Condition.new(a)
109
129
  end
110
130
  end
131
+
132
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
133
+ def children
134
+ @node.blocks
135
+ end
136
+ end
111
137
  end
112
138
 
113
- Template.register_tag('if'.freeze, If)
139
+ Template.register_tag('if', If)
114
140
  end
@@ -1,18 +1,19 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  class Ifchanged < Block
3
- def render(context)
4
- context.stack do
5
- output = super
5
+ def render_to_output_buffer(context, output)
6
+ block_output = +''
7
+ super(context, block_output)
6
8
 
7
- if output != context.registers[:ifchanged]
8
- context.registers[:ifchanged] = output
9
- output
10
- else
11
- ''.freeze
12
- end
9
+ if block_output != context.registers[:ifchanged]
10
+ context.registers[:ifchanged] = block_output
11
+ output << block_output
13
12
  end
13
+
14
+ output
14
15
  end
15
16
  end
16
17
 
17
- Template.register_tag('ifchanged'.freeze, Ifchanged)
18
+ Template.register_tag('ifchanged', Ifchanged)
18
19
  end
@@ -1,51 +1,66 @@
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](/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`](/api/liquid/tags#render).
16
20
  class Include < Tag
17
- 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
25
+
26
+ attr_reader :template_name_expr, :variable_name_expr, :attributes
18
27
 
19
28
  def initialize(tag_name, markup, options)
20
29
  super
21
30
 
22
- if markup =~ Syntax
31
+ if markup =~ SYNTAX
23
32
 
24
- template_name = $1
25
- variable_name = $3
33
+ template_name = Regexp.last_match(1)
34
+ variable_name = Regexp.last_match(3)
26
35
 
27
- @variable_name_expr = variable_name ? Expression.parse(variable_name) : nil
28
- @template_name_expr = Expression.parse(template_name)
29
- @attributes = {}
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 = {}
30
40
 
31
41
  markup.scan(TagAttributes) do |key, value|
32
- @attributes[key] = Expression.parse(value)
42
+ @attributes[key] = parse_expression(value)
33
43
  end
34
44
 
35
45
  else
36
- raise SyntaxError.new(options[:locale].t("errors.syntax.include".freeze))
46
+ raise SyntaxError, options[:locale].t("errors.syntax.include")
37
47
  end
38
48
  end
39
49
 
40
50
  def parse(_tokens)
41
51
  end
42
52
 
43
- def render(context)
53
+ def render_to_output_buffer(context, output)
44
54
  template_name = context.evaluate(@template_name_expr)
45
- raise ArgumentError.new(options[:locale].t("errors.argument.include")) unless template_name
55
+ raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name
56
+
57
+ partial = PartialCache.load(
58
+ template_name,
59
+ context: context,
60
+ parse_context: parse_context
61
+ )
46
62
 
47
- partial = load_cached_partial(template_name, context)
48
- context_variable_name = template_name.split('/'.freeze).last
63
+ context_variable_name = @alias_name || template_name.split('/').last
49
64
 
50
65
  variable = if @variable_name_expr
51
66
  context.evaluate(@variable_name_expr)
@@ -54,60 +69,45 @@ module Liquid
54
69
  end
55
70
 
56
71
  old_template_name = context.template_name
57
- old_partial = context.partial
72
+ old_partial = context.partial
58
73
  begin
59
74
  context.template_name = template_name
60
- context.partial = true
75
+ context.partial = true
61
76
  context.stack do
62
77
  @attributes.each do |key, value|
63
78
  context[key] = context.evaluate(value)
64
79
  end
65
80
 
66
81
  if variable.is_a?(Array)
67
- variable.collect do |var|
82
+ variable.each do |var|
68
83
  context[context_variable_name] = var
69
- partial.render(context)
84
+ partial.render_to_output_buffer(context, output)
70
85
  end
71
86
  else
72
87
  context[context_variable_name] = variable
73
- partial.render(context)
88
+ partial.render_to_output_buffer(context, output)
74
89
  end
75
90
  end
76
91
  ensure
77
92
  context.template_name = old_template_name
78
- context.partial = old_partial
93
+ context.partial = old_partial
79
94
  end
80
- end
81
95
 
82
- private
96
+ output
97
+ end
83
98
 
84
99
  alias_method :parse_context, :options
85
100
  private :parse_context
86
101
 
87
- def load_cached_partial(template_name, context)
88
- cached_partials = context.registers[:cached_partials] || {}
89
-
90
- if cached = cached_partials[template_name]
91
- return cached
102
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
103
+ def children
104
+ [
105
+ @node.template_name_expr,
106
+ @node.variable_name_expr,
107
+ ] + @node.attributes.values
92
108
  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
99
- end
100
- cached_partials[template_name] = partial
101
- context.registers[:cached_partials] = cached_partials
102
- partial
103
- end
104
-
105
- def read_template_from_file_system(context)
106
- file_system = context.registers[:file_system] || Liquid::Template.file_system
107
-
108
- file_system.read_template_file(context.evaluate(@template_name_expr))
109
109
  end
110
110
  end
111
111
 
112
- Template.register_tag('include'.freeze, Include)
112
+ Template.register_tag('include', Include)
113
113
  end
@@ -1,31 +1,37 @@
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:
12
- #
13
- # Hello: 0
14
- # Hello: 1
15
- # Hello: 2
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.
16
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.
17
21
  class Increment < Tag
18
22
  def initialize(tag_name, markup, options)
19
23
  super
20
24
  @variable = markup.strip
21
25
  end
22
26
 
23
- def render(context)
27
+ def render_to_output_buffer(context, output)
24
28
  value = context.environments.first[@variable] ||= 0
25
29
  context.environments.first[@variable] = value + 1
26
- value.to_s
30
+
31
+ output << value.to_s
32
+ output
27
33
  end
28
34
  end
29
35
 
30
- Template.register_tag('increment'.freeze, Increment)
36
+ Template.register_tag('increment', Increment)
31
37
  end
@@ -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,4 +1,17 @@
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
17
  FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
@@ -10,20 +23,21 @@ module Liquid
10
23
  end
11
24
 
12
25
  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
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
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,11 @@ 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
59
 
46
- Template.register_tag('raw'.freeze, Raw)
60
+ Template.register_tag('raw', Raw)
47
61
  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