Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/liquid.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ module Liquid
require 'liquid/drop'
require 'liquid/tablerowloop_drop'
require 'liquid/forloop_drop'
require 'liquid/snippet_drop'
require 'liquid/extensions'
require 'liquid/errors'
require 'liquid/interrupts'
Expand Down
3 changes: 3 additions & 0 deletions lib/liquid/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
block_tag_unexpected_args: "Syntax Error in '%{tag}' - Valid syntax: {% %{tag} %}{% end%{tag} %}"
assign: "Syntax Error in 'assign' - Valid syntax: assign [var] = [source]"
capture: "Syntax Error in 'capture' - Valid syntax: capture [var]"
snippet: "Syntax Error in 'snippet' - Valid syntax: snippet [var]"
case: "Syntax Error in 'case' - Valid syntax: case [condition]"
case_invalid_when: "Syntax Error in tag 'case' - Valid when condition: {% when [condition] [or condition2...] %}"
case_invalid_else: "Syntax Error in tag 'case' - Valid else condition: {% else %} (no parameters) "
Expand All @@ -19,6 +20,7 @@
invalid_delimiter: "'%{tag}' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
invalid_template_encoding: "Invalid template encoding"
render: "Syntax error in tag 'render' - Template name must be a quoted string"
render_invalid_template_name: "Syntax error in tag 'render' - Expected a string or identifier, found %{found}"
table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
table_row_invalid_attribute: "Invalid attribute '%{attribute}' in tablerow loop. Valid attributes are cols, limit, offset, and range"
tag_never_closed: "'%{block_name}' tag was never closed"
Expand All @@ -29,5 +31,6 @@
variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}"
argument:
include: "Argument error in tag 'include' - Illegal template name"
render: "Argument error in tag 'render' - Dynamically chosen templates are not allowed"
disabled:
tag: "usage is not allowed in this context"
22 changes: 22 additions & 0 deletions lib/liquid/snippet_drop.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

module Liquid
class SnippetDrop < Drop
attr_reader :body, :name, :filename

def initialize(body, name, filename)
super()
@body = body
@name = name
@filename = filename
end

def to_partial
@body
end

def to_s
'SnippetDrop'
end
end
end
2 changes: 2 additions & 0 deletions lib/liquid/tags.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
require_relative "tags/render"
require_relative "tags/cycle"
require_relative "tags/doc"
require_relative "tags/snippet"

module Liquid
module Tags
Expand All @@ -44,6 +45,7 @@ module Tags
'echo' => Echo,
'tablerow' => TableRow,
'doc' => Doc,
'snippet' => Snippet,
}.freeze
end
end
36 changes: 21 additions & 15 deletions lib/liquid/tags/render.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ module Liquid
# @liquid_syntax_keyword filename The name of the snippet to render, without the `.liquid` extension.
class Render < Tag
FOR = 'for'
SYNTAX = /(#{QuotedString}+)(\s+(with|#{FOR})\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
SYNTAX = /(#{QuotedString}+|#{VariableSegment}+)(\s+(with|#{FOR})\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o

disable_tags "include"

Expand All @@ -47,21 +47,23 @@ def render_to_output_buffer(context, output)
end

def render_tag(context, output)
# The expression should be a String literal, which parses to a String object
template_name = @template_name_expr
raise ::ArgumentError unless template_name.is_a?(String)

partial = PartialCache.load(
template_name,
context: context,
parse_context: parse_context,
)

context_variable_name = @alias_name || template_name.split('/').last
template = context.evaluate(@template_name_expr)

if template.respond_to?(:to_partial)
partial = template.to_partial
template_name = template.filename
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be good to respond to name as well for duck typing

context_variable_name = @alias_name || template.name
elsif @template_name_expr.is_a?(String)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: we should first check is_a?(String) since its the common/hot path

partial = PartialCache.load(template, context: context, parse_context: parse_context)
template_name = partial.name
context_variable_name = @alias_name || template_name.split('/').last
else
raise ::ArgumentError
end

render_partial_func = ->(var, forloop) {
inner_context = context.new_isolated_subcontext
inner_context.template_name = partial.name
inner_context.template_name = template_name
inner_context.partial = true
inner_context['forloop'] = forloop if forloop

Expand Down Expand Up @@ -101,14 +103,18 @@ def rigid_parse(markup)
key = p.consume
p.consume(:colon)
@attributes[key] = safe_parse_expression(p)
p.consume?(:comma)
p.consume?(:comma) # optional comma
end

p.consume(:end_of_string)
end

def rigid_template_name(p)
p.consume(:string)
return p.consume(:string) if p.look(:string)
return p.consume(:id) if p.look(:id)

found = p.consume || "nothing"
raise SyntaxError, options[:locale].t("errors.syntax.render_invalid_template_name", found: found)
end

def strict_parse(markup)
Expand Down
45 changes: 45 additions & 0 deletions lib/liquid/tags/snippet.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# frozen_string_literal: true

module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category variable
# @liquid_name snippet
# @liquid_summary
# Creates a new inline snippet.
# @liquid_description
# You can create inline snippets to make your Liquid code more modular.
# @liquid_syntax
# {% snippet snippet_name %}
# value
# {% endsnippet %}
class Snippet < Block
def initialize(tag_name, markup, options)
super
p = @parse_context.new_parser(markup)
if p.look(:id)
@to = p.consume(:id)
p.consume(:end_of_string)
else
raise SyntaxError, options[:locale].t("errors.syntax.snippet")
end
end

def render_to_output_buffer(context, output)
snippet_drop = SnippetDrop.new(@body, @to, context.template_name)
context.scopes.last[@to] = snippet_drop
context.resource_limits.increment_assign_score(assign_score_of(snippet_drop))
output
end

def blank?
true
end

private

def assign_score_of(snippet_drop)
snippet_drop.body.nodelist.sum { |node| node.to_s.bytesize }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this makes sense. Assigning a snippet drop shouldn't rely on parsed AST for this functionality.

end
end
end
11 changes: 7 additions & 4 deletions test/integration/tags/render_tag_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,6 @@ def test_sub_contexts_count_towards_the_same_recursion_limit
end
end

def test_dynamically_choosen_templates_are_not_allowed
assert_syntax_error("{% assign name = 'snippet' %}{% render name %}")
end

def test_rigid_parsing_errors
with_error_modes(:lax, :strict) do
assert_template_result(
Expand Down Expand Up @@ -294,6 +290,13 @@ def test_render_tag_with_drop
)
end

def test_render_tag_with_snippet_drop
assert_template_result(
"Hello from snippet",
"{% snippet my_snippet %}Hello from snippet{% endsnippet %}{% render my_snippet %}",
)
end

def test_render_tag_renders_error_with_template_name
assert_template_result(
'Liquid error (foo line 1): standard error',
Expand Down
Loading