mirror of
https://github.com/Shopify/liquid.git
synced 2026-09-15 08:50:45 -07:00
154 lines
4.0 KiB
Ruby
154 lines
4.0 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Liquid
|
|
# Holds variables. Variables are only loaded "just in time"
|
|
# and are not evaluated as part of the render stage
|
|
#
|
|
# {{ monkey }}
|
|
# {{ user.name }}
|
|
#
|
|
# Variables can be combined with filters:
|
|
#
|
|
# {{ user | link }}
|
|
#
|
|
class Variable
|
|
FilterMarkupRegex = /#{FilterSeparator}\s*(.*)/om
|
|
FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
|
|
FilterArgsRegex = /(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o
|
|
JustTagAttributes = /\A#{TagAttributes}\z/o
|
|
MarkupWithQuotedFragment = /(#{QuotedFragment})(.*)/om
|
|
|
|
attr_accessor :filters, :name, :line_number
|
|
attr_reader :parse_context
|
|
alias_method :options, :parse_context
|
|
|
|
include ParserSwitching
|
|
|
|
def initialize(markup, parse_context)
|
|
@markup = markup
|
|
@name = nil
|
|
@parse_context = parse_context
|
|
@line_number = parse_context.line_number
|
|
|
|
parse_with_selected_parser(markup)
|
|
end
|
|
|
|
def raw
|
|
@markup
|
|
end
|
|
|
|
def markup_context(markup)
|
|
"in \"{{#{markup}}}\""
|
|
end
|
|
|
|
def parse_markup(markup)
|
|
@filters = []
|
|
p = @parse_context.new_parser(markup)
|
|
|
|
return if p.look(:end_of_string)
|
|
|
|
@name = p.expression
|
|
@filters << parse_filter_expressions(p) while p.consume?(:pipe)
|
|
p.consume(:end_of_string)
|
|
end
|
|
|
|
def render(context)
|
|
obj = context.evaluate(@name)
|
|
|
|
@filters.each do |filter_name, filter_args, filter_kwargs|
|
|
filter_args = evaluate_filter_expressions(context, filter_args, filter_kwargs)
|
|
obj = context.invoke(filter_name, obj, *filter_args)
|
|
end
|
|
|
|
context.apply_global_filter(obj)
|
|
end
|
|
|
|
def render_to_output_buffer(context, output)
|
|
obj = render(context)
|
|
render_obj_to_output(obj, output)
|
|
output
|
|
end
|
|
|
|
def render_obj_to_output(obj, output)
|
|
case obj
|
|
when NilClass
|
|
# Do nothing
|
|
when Array
|
|
obj.each do |o|
|
|
render_obj_to_output(o, output)
|
|
end
|
|
else
|
|
output << Liquid::Utils.to_s(obj)
|
|
end
|
|
end
|
|
|
|
def disabled?(_context)
|
|
false
|
|
end
|
|
|
|
def disabled_tags
|
|
[]
|
|
end
|
|
|
|
private
|
|
|
|
# Surprisingly, positional and keyword arguments can be mixed.
|
|
#
|
|
# filter = filtername [":" filterargs?]
|
|
# filterargs = argument ("," argument)*
|
|
# argument = (positional_argument | keyword_argument)
|
|
# positional_argument = expression
|
|
# keyword_argument = id ":" expression
|
|
def parse_filter_expressions(p)
|
|
filtername = p.consume(:id)
|
|
filter_args = []
|
|
keyword_args = {}
|
|
|
|
if p.consume?(:colon)
|
|
# Parse first argument (no leading comma)
|
|
argument(p, filter_args, keyword_args) unless end_of_arguments?(p)
|
|
|
|
# Parse remaining arguments (with leading commas) and optional trailing comma
|
|
argument(p, filter_args, keyword_args) while p.consume?(:comma) && !end_of_arguments?(p)
|
|
end
|
|
|
|
result = [filtername, filter_args]
|
|
result << keyword_args unless keyword_args.empty?
|
|
result
|
|
end
|
|
|
|
def argument(p, positional_arguments, keyword_arguments)
|
|
if p.look(:id) && p.look(:colon, 1)
|
|
key = p.consume(:id)
|
|
p.consume(:colon)
|
|
value = p.expression
|
|
keyword_arguments[key] = value
|
|
else
|
|
positional_arguments << p.expression
|
|
end
|
|
end
|
|
|
|
def end_of_arguments?(p)
|
|
p.look(:pipe) || p.look(:end_of_string)
|
|
end
|
|
|
|
def evaluate_filter_expressions(context, filter_args, filter_kwargs)
|
|
parsed_args = filter_args.map { |expr| context.evaluate(expr) }
|
|
if filter_kwargs
|
|
parsed_kwargs = {}
|
|
filter_kwargs.each do |key, expr|
|
|
parsed_kwargs[key] = context.evaluate(expr)
|
|
end
|
|
parsed_args << parsed_kwargs
|
|
end
|
|
parsed_args
|
|
end
|
|
|
|
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
|
def children
|
|
[@node.name] + @node.filters.flatten
|
|
end
|
|
end
|
|
end
|
|
end
|