mirror of
https://github.com/kemko/liquid.git
synced 2026-01-02 00:05:42 +03:00
Compare commits
4 Commits
context_si
...
superfluid
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a179fd84a3 | ||
|
|
aa1ffb1fb8 | ||
|
|
050ee05583 | ||
|
|
84e789e2c1 |
@@ -6,21 +6,18 @@ rvm:
|
||||
- 2.3
|
||||
- 2.4
|
||||
- 2.5
|
||||
- &latest_ruby 2.6
|
||||
- ruby-head
|
||||
- jruby-head
|
||||
# - rbx-2
|
||||
|
||||
sudo: false
|
||||
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- libgmp3-dev
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- rvm: *latest_ruby
|
||||
script: bundle exec rake memory_profile:run
|
||||
name: Profiling Memory Usage
|
||||
allow_failures:
|
||||
- rvm: ruby-head
|
||||
- rvm: jruby-head
|
||||
|
||||
7
Gemfile
7
Gemfile
@@ -5,10 +5,10 @@ end
|
||||
|
||||
gemspec
|
||||
|
||||
|
||||
group :benchmark, :test do
|
||||
gem 'benchmark-ips'
|
||||
gem 'memory_profiler'
|
||||
gem 'terminal-table'
|
||||
|
||||
install_if -> { RUBY_PLATFORM !~ /mingw|mswin|java/ } do
|
||||
gem 'stackprof'
|
||||
@@ -17,8 +17,11 @@ end
|
||||
|
||||
group :test do
|
||||
gem 'rubocop', '~> 0.53.0'
|
||||
gem 'awesome_print'
|
||||
gem 'pry'
|
||||
gem 'byebug'
|
||||
|
||||
platform :mri do
|
||||
gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'liquid-tag'
|
||||
gem 'liquid-c', github: 'Shopify/liquid-c', ref: '9168659de45d6d576fce30c735f857e597fa26f6'
|
||||
end
|
||||
end
|
||||
|
||||
16
Rakefile
16
Rakefile
@@ -45,6 +45,17 @@ task :test do
|
||||
end
|
||||
end
|
||||
|
||||
desc 'runs the test suite using the superfluid compiler'
|
||||
Rake::TestTask.new(:test_superfluid) do |t|
|
||||
t.libs << '.' << 'lib' << 'test'
|
||||
t.test_files = FileList['test/integration/**/*_test.rb']
|
||||
t.verbose = false
|
||||
|
||||
ENV['LIQUID_PARSER_MODE'] = 'strict'
|
||||
ENV['LIQUID-C'] = '1'
|
||||
ENV['SUPERFLUID'] = '1'
|
||||
end
|
||||
|
||||
task gem: :build
|
||||
task :build do
|
||||
system "gem build liquid.gemspec"
|
||||
@@ -71,6 +82,11 @@ namespace :benchmark do
|
||||
task :strict do
|
||||
ruby "./performance/benchmark.rb strict"
|
||||
end
|
||||
|
||||
desc "Run the liquid benchmark with strict parsing"
|
||||
task :superfluid do
|
||||
ruby "./performance/benchmark.rb superfluid"
|
||||
end
|
||||
end
|
||||
|
||||
namespace :profile do
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
module Liquid
|
||||
class BlockBody
|
||||
LiquidTagToken = /\A\s*(\w+)\s*(.*?)\z/o
|
||||
FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(\w+)(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
|
||||
FullToken = /\A#{TagStart}#{WhitespaceControl}?\s*(\w+)\s*(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
|
||||
ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
|
||||
WhitespaceOrNothing = /\A\s*\z/
|
||||
TAGSTART = "{%".freeze
|
||||
@@ -14,42 +13,8 @@ module Liquid
|
||||
@blank = true
|
||||
end
|
||||
|
||||
def parse(tokenizer, parse_context, &block)
|
||||
def parse(tokenizer, parse_context)
|
||||
parse_context.line_number = tokenizer.line_number
|
||||
|
||||
if tokenizer.for_liquid_tag
|
||||
parse_for_liquid_tag(tokenizer, parse_context, &block)
|
||||
else
|
||||
parse_for_document(tokenizer, parse_context, &block)
|
||||
end
|
||||
end
|
||||
|
||||
private def parse_for_liquid_tag(tokenizer, parse_context)
|
||||
while token = tokenizer.shift
|
||||
unless token.empty? || token =~ WhitespaceOrNothing
|
||||
unless token =~ LiquidTagToken
|
||||
# line isn't empty but didn't match tag syntax, yield and let the
|
||||
# caller raise a syntax error
|
||||
return yield token, token
|
||||
end
|
||||
tag_name = $1
|
||||
markup = $2
|
||||
unless tag = registered_tags[tag_name]
|
||||
# end parsing if we reach an unknown tag and let the caller decide
|
||||
# determine how to proceed
|
||||
return yield tag_name, markup
|
||||
end
|
||||
new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
|
||||
@blank &&= new_tag.blank?
|
||||
@nodelist << new_tag
|
||||
end
|
||||
parse_context.line_number = tokenizer.line_number
|
||||
end
|
||||
|
||||
yield nil, nil
|
||||
end
|
||||
|
||||
private def parse_for_document(tokenizer, parse_context, &block)
|
||||
while token = tokenizer.shift
|
||||
next if token.empty?
|
||||
case
|
||||
@@ -58,20 +23,9 @@ module Liquid
|
||||
unless token =~ FullToken
|
||||
raise_missing_tag_terminator(token, parse_context)
|
||||
end
|
||||
tag_name = $2
|
||||
markup = $4
|
||||
|
||||
if parse_context.line_number
|
||||
# newlines inside the tag should increase the line number,
|
||||
# particularly important for multiline {% liquid %} tags
|
||||
parse_context.line_number += $1.count("\n".freeze) + $3.count("\n".freeze)
|
||||
end
|
||||
|
||||
if tag_name == 'liquid'.freeze
|
||||
liquid_tag_tokenizer = Tokenizer.new(markup, line_number: parse_context.line_number, for_liquid_tag: true)
|
||||
next parse_for_liquid_tag(liquid_tag_tokenizer, parse_context, &block)
|
||||
end
|
||||
|
||||
tag_name = $1
|
||||
markup = $2
|
||||
# fetch the tag from registered blocks
|
||||
unless tag = registered_tags[tag_name]
|
||||
# end parsing if we reach an unknown tag and let the caller decide
|
||||
# determine how to proceed
|
||||
@@ -143,7 +97,7 @@ module Liquid
|
||||
end
|
||||
idx += 1
|
||||
|
||||
raise_if_resource_limits_reached(context, output.bytesize - previous_output_size)
|
||||
context.raise_if_resource_limits_reached(output.bytesize - previous_output_size)
|
||||
end
|
||||
|
||||
output
|
||||
@@ -160,12 +114,6 @@ module Liquid
|
||||
output << context.handle_error(e, line_number)
|
||||
end
|
||||
|
||||
def raise_if_resource_limits_reached(context, length)
|
||||
context.resource_limits.render_length += length
|
||||
return unless context.resource_limits.reached?
|
||||
raise MemoryError.new("Memory limits exceeded".freeze)
|
||||
end
|
||||
|
||||
def create_variable(token, parse_context)
|
||||
token.scan(ContentOfVariable) do |content|
|
||||
markup = content.first
|
||||
|
||||
@@ -29,7 +29,7 @@ module Liquid
|
||||
@@operators
|
||||
end
|
||||
|
||||
attr_reader :attachment, :child_condition
|
||||
attr_reader :attachment, :child_condition, :child_relation
|
||||
attr_accessor :left, :operator, :right
|
||||
|
||||
def initialize(left = nil, operator = nil, right = nil)
|
||||
@@ -81,10 +81,6 @@ module Liquid
|
||||
"#<Condition #{[@left, @operator, @right].compact.join(' '.freeze)}>"
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_reader :child_relation
|
||||
|
||||
private
|
||||
|
||||
def equal_variables(left, right)
|
||||
|
||||
@@ -20,17 +20,31 @@ module Liquid
|
||||
@scopes = [(outer_scope || {})]
|
||||
@registers = registers
|
||||
@errors = []
|
||||
@interrupts = []
|
||||
@filters = []
|
||||
@global_filter = nil
|
||||
@partial = false
|
||||
@strict_variables = false
|
||||
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
|
||||
squash_instance_assigns_with_environments
|
||||
|
||||
@this_stack_used = false
|
||||
|
||||
self.exception_renderer = Template.default_exception_renderer
|
||||
self.exception_renderer = ->(e) { raise } if rethrow_errors
|
||||
if rethrow_errors
|
||||
self.exception_renderer = ->(e) { raise }
|
||||
end
|
||||
|
||||
squash_instance_assigns_with_environments
|
||||
@interrupts = []
|
||||
@filters = []
|
||||
@global_filter = nil
|
||||
end
|
||||
|
||||
def raise_argument_error(message)
|
||||
raise Liquid::ArgumentError, message
|
||||
end
|
||||
|
||||
def raise_if_resource_limits_reached(length)
|
||||
resource_limits.render_length += length
|
||||
return unless resource_limits.reached?
|
||||
raise MemoryError.new("Memory limits exceeded".freeze)
|
||||
end
|
||||
|
||||
def warnings
|
||||
@@ -83,9 +97,9 @@ module Liquid
|
||||
end
|
||||
|
||||
# Push new local scope on the stack. use <tt>Context#stack</tt> instead
|
||||
def push
|
||||
@scopes.unshift({})
|
||||
raise StackLevelError, "Nesting too deep".freeze if @scopes.length > (Block::MAX_DEPTH + 1)
|
||||
def push(new_scope = {})
|
||||
@scopes.unshift(new_scope)
|
||||
raise StackLevelError, "Nesting too deep".freeze if @scopes.length > Block::MAX_DEPTH
|
||||
end
|
||||
|
||||
# Merge a hash of variables in the current local scope
|
||||
@@ -107,15 +121,31 @@ module Liquid
|
||||
# end
|
||||
#
|
||||
# context['var] #=> nil
|
||||
def stack
|
||||
push
|
||||
def stack(new_scope = nil)
|
||||
old_stack_used = @this_stack_used
|
||||
if new_scope
|
||||
push(new_scope)
|
||||
@this_stack_used = true
|
||||
else
|
||||
@this_stack_used = false
|
||||
end
|
||||
|
||||
yield
|
||||
ensure
|
||||
pop
|
||||
pop if @this_stack_used
|
||||
@this_stack_used = old_stack_used
|
||||
end
|
||||
|
||||
def clear_instance_assigns
|
||||
@scopes[0] = {}
|
||||
end
|
||||
|
||||
# Only allow String, Numeric, Hash, Array, Proc, Boolean or <tt>Liquid::Drop</tt>
|
||||
def []=(key, value)
|
||||
unless @this_stack_used
|
||||
@this_stack_used = true
|
||||
push({})
|
||||
end
|
||||
@scopes[0][key] = value
|
||||
end
|
||||
|
||||
@@ -143,12 +173,27 @@ module Liquid
|
||||
def find_variable(key, raise_on_not_found: true)
|
||||
# This was changed from find() to find_index() because this is a very hot
|
||||
# path and find_index() is optimized in MRI to reduce object allocation
|
||||
index = @scopes.find_index { |s| s.key?(key) }
|
||||
scope = @scopes[index] if index
|
||||
|
||||
scope = (index = @scopes.find_index { |s| s.key?(key) }) && @scopes[index]
|
||||
scope ||= (index = @environments.find_index { |s| s.key?(key) }) && @environments[index]
|
||||
scope ||= {}
|
||||
variable = nil
|
||||
|
||||
variable = lookup_and_evaluate(scope, key, raise_on_not_found: raise_on_not_found).to_liquid
|
||||
if scope.nil?
|
||||
@environments.each do |e|
|
||||
variable = lookup_and_evaluate(e, key, raise_on_not_found: raise_on_not_found)
|
||||
# When lookup returned a value OR there is no value but the lookup also did not raise
|
||||
# then it is the value we are looking for.
|
||||
if !variable.nil? || @strict_variables && raise_on_not_found
|
||||
scope = e
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
scope ||= @environments.last || @scopes.last
|
||||
variable ||= lookup_and_evaluate(scope, key, raise_on_not_found: raise_on_not_found)
|
||||
|
||||
variable = variable.to_liquid
|
||||
variable.context = self if variable.respond_to?(:context=)
|
||||
|
||||
variable
|
||||
|
||||
@@ -15,6 +15,8 @@ module Liquid
|
||||
@end_obj = end_obj
|
||||
end
|
||||
|
||||
attr_reader :start_obj, :end_obj
|
||||
|
||||
def evaluate(context)
|
||||
start_int = to_integer(context.evaluate(@start_obj))
|
||||
end_int = to_integer(context.evaluate(@end_obj))
|
||||
|
||||
632
lib/liquid/superfluid.rb
Normal file
632
lib/liquid/superfluid.rb
Normal file
@@ -0,0 +1,632 @@
|
||||
require 'ap'
|
||||
|
||||
require 'pry'
|
||||
|
||||
require 'stackprof'
|
||||
|
||||
AwesomePrint.defaults = {
|
||||
raw: true
|
||||
}
|
||||
|
||||
module Liquid
|
||||
Liquid::BlockBody.class_eval do
|
||||
def render_to_output_buffer(context, output)
|
||||
|
||||
ruby = Compiler.compile(@nodelist)
|
||||
|
||||
if false
|
||||
puts
|
||||
puts "--------------------------- GENERATED RUBY -"
|
||||
line_number = 1
|
||||
puts(ruby.lines.map do |line|
|
||||
"#{line_number}\t#{line}".tap { line_number += 1}
|
||||
end)
|
||||
puts "--------------------------- /GENERATED RUBY -"
|
||||
end
|
||||
|
||||
instructions = RubyVM::InstructionSequence.compile(ruby)
|
||||
|
||||
output_io = StringIO.new
|
||||
instructions.eval.call(output_io, context, Condition)
|
||||
|
||||
output << output_io.string
|
||||
end
|
||||
end
|
||||
|
||||
class SuperfluidError < Exception
|
||||
end
|
||||
|
||||
class Output
|
||||
attr_reader :string, :indent_level
|
||||
|
||||
def initialize(initial_indent)
|
||||
@string = ''.dup
|
||||
@indent_level = initial_indent
|
||||
@indent_str = " " * initial_indent * 2
|
||||
end
|
||||
|
||||
def line(string)
|
||||
@string << @indent_str << string << "\n"
|
||||
end
|
||||
|
||||
def echo(string)
|
||||
output << "liquid_out.write(to_output(#{string}))"
|
||||
end
|
||||
|
||||
def indent(&block)
|
||||
output.indent(&block)
|
||||
end
|
||||
|
||||
def indent
|
||||
@indent_level += 1
|
||||
@indent_str = " " * @indent_level * 2
|
||||
yield
|
||||
@indent_level -= 1
|
||||
@indent_str = " " * @indent_level * 2
|
||||
end
|
||||
end
|
||||
|
||||
class Compiler
|
||||
class << self
|
||||
def compile(template)
|
||||
compiler = new
|
||||
compiler.compile(template)
|
||||
compiler.ruby
|
||||
end
|
||||
end
|
||||
|
||||
def initialize
|
||||
@variables = Set.new
|
||||
@output = Output.new(2)
|
||||
@blank = false
|
||||
end
|
||||
|
||||
def ruby
|
||||
[
|
||||
header,
|
||||
@output.string,
|
||||
trailer
|
||||
].join("\n")
|
||||
end
|
||||
|
||||
|
||||
def header
|
||||
<<~RUBY
|
||||
module Warning
|
||||
def warn(*)
|
||||
end
|
||||
end
|
||||
|
||||
class ForloopDrop
|
||||
def initialize(name, length, parentloop)
|
||||
@name = name
|
||||
@length = length
|
||||
@parentloop = parentloop
|
||||
@index = 0
|
||||
end
|
||||
|
||||
attr_accessor :parentloop
|
||||
|
||||
def [](value)
|
||||
case value
|
||||
when "length"
|
||||
@length
|
||||
when "name"
|
||||
@name
|
||||
when "index"
|
||||
@index + 1
|
||||
when "index0"
|
||||
@index
|
||||
when "rindex"
|
||||
@length - @index
|
||||
when "rindex0"
|
||||
@length - @index - 1
|
||||
when "first"
|
||||
@index == 0
|
||||
when "last"
|
||||
@index == @length - 1
|
||||
when "parentloop"
|
||||
@parentloop
|
||||
end
|
||||
end
|
||||
|
||||
def to_liquid
|
||||
self
|
||||
end
|
||||
|
||||
def key?(*)
|
||||
true
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def increment!
|
||||
@index += 1
|
||||
end
|
||||
end
|
||||
|
||||
def slice_collection(collection, from, limit)
|
||||
to = if limit.nil?
|
||||
nil
|
||||
else
|
||||
limit + from
|
||||
end
|
||||
|
||||
if (from != 0 || !to.nil?) && collection.respond_to?(:load_slice)
|
||||
collection.load_slice(from, to)
|
||||
else
|
||||
slice_collection_using_each(collection, from, to)
|
||||
end
|
||||
end
|
||||
|
||||
def slice_collection_using_each(collection, from, to)
|
||||
segments = []
|
||||
index = 0
|
||||
|
||||
if collection.is_a?(String)
|
||||
return collection.empty? ? [] : [collection]
|
||||
end
|
||||
return [] unless collection.respond_to?(:each)
|
||||
|
||||
collection.each do |item|
|
||||
if to && to <= index
|
||||
break
|
||||
end
|
||||
|
||||
if from <= index
|
||||
segments << item
|
||||
end
|
||||
|
||||
index += 1
|
||||
end
|
||||
|
||||
segments
|
||||
end
|
||||
|
||||
def apply_operator(left, operator, right)
|
||||
if left.respond_to?(operator) && right.respond_to?(operator) && !left.is_a?(Hash) && !right.is_a?(Hash)
|
||||
begin
|
||||
left.send(operator, right)
|
||||
rescue ::ArgumentError => e
|
||||
raise @context.raise_argument_error(e.message)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def contains?(left, right)
|
||||
if left && right && left.respond_to?(:include?)
|
||||
right = right.to_s if left.is_a?(String)
|
||||
left.include?(right)
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
class GlobalVariableLookup
|
||||
def method_missing(method_name, *)
|
||||
nil
|
||||
end
|
||||
|
||||
def to_output(value)
|
||||
output = if value.is_a?(Array)
|
||||
value.join
|
||||
elsif value == nil
|
||||
else
|
||||
value.to_s
|
||||
end
|
||||
@context.apply_global_filter(output)
|
||||
end
|
||||
|
||||
def run(liquid_out, context, condition)
|
||||
@condition = condition
|
||||
@context = context
|
||||
@for_offsets = {}
|
||||
@cycle_values = {}
|
||||
@context.registers[:for_stack] = []
|
||||
@context.registers[:cycle] ||= {}
|
||||
@if_changed_last = nil
|
||||
@prev_output_size = 0
|
||||
#{hoisted_variables}
|
||||
RUBY
|
||||
end
|
||||
|
||||
def trailer
|
||||
<<~RUBY
|
||||
end
|
||||
end
|
||||
GlobalVariableLookup.new.method(:run)
|
||||
RUBY
|
||||
end
|
||||
|
||||
def hoisted_variables
|
||||
@variables.map do |variable|
|
||||
normal_name = unvar(variable)
|
||||
"#{variable} = @context.find_variable(#{normal_name.inspect}, raise_on_not_found: false)"
|
||||
end.join("\n")
|
||||
end
|
||||
|
||||
def compile(node)
|
||||
old_blank = @blank
|
||||
@blank = if node.respond_to?(:blank?)
|
||||
node.blank?
|
||||
elsif !(node.is_a?(String) && node =~ /\A\s*\z/)
|
||||
false
|
||||
else
|
||||
@blank
|
||||
end
|
||||
|
||||
case node
|
||||
when Liquid::Document, Liquid::BlockBody
|
||||
node.nodelist.collect(&method(:compile))
|
||||
when Array
|
||||
node.collect(&method(:compile))
|
||||
when Liquid::Variable
|
||||
compile_variable(node)
|
||||
when Liquid::For
|
||||
compile_for(node)
|
||||
when Liquid::If
|
||||
compile_if(node)
|
||||
when Liquid::Ifchanged
|
||||
compile_if_changed(node)
|
||||
when Liquid::Template
|
||||
compile(node.root)
|
||||
when Liquid::Assign
|
||||
compile_assign(node)
|
||||
when Liquid::Case
|
||||
compile_case(node)
|
||||
when Liquid::Capture
|
||||
compile_capture(node)
|
||||
when String
|
||||
compile_echo_literal(node)
|
||||
when Liquid::Break
|
||||
line "break" if @in_loop
|
||||
when Liquid::Continue
|
||||
line "next" if @in_loop
|
||||
when Liquid::Cycle
|
||||
compile_cycle(node)
|
||||
when Liquid::Raw
|
||||
compile_raw(node)
|
||||
when Liquid::Increment
|
||||
compile_increment(node)
|
||||
when Liquid::Decrement
|
||||
compile_decrement(node)
|
||||
when Liquid::Comment
|
||||
else
|
||||
raise SuperfluidError, "Unknown node type #{node.inspect}"
|
||||
end
|
||||
|
||||
@blank = old_blank
|
||||
end
|
||||
|
||||
def compile_for(node)
|
||||
variable_name = node.variable_name
|
||||
|
||||
collection_name = node.collection_name
|
||||
|
||||
iter_target_expr = case collection_name
|
||||
when Liquid::VariableLookup
|
||||
make_variable_lookup_expr(collection_name)
|
||||
when Range
|
||||
collection_name
|
||||
when Liquid::RangeLookup
|
||||
start_expr = make_variable_expr(collection_name.start_obj)
|
||||
end_expr = make_variable_expr(collection_name.end_obj)
|
||||
line "start = #{start_expr}"
|
||||
line "@context.raise_argument_error('bad value for range') unless start.respond_to?(:to_i)"
|
||||
line "start = #{start_expr}.to_i"
|
||||
|
||||
line "finish = #{end_expr}"
|
||||
line "@context.raise_argument_error('bad value for range') unless finish.respond_to?(:to_i)"
|
||||
line "finish = #{end_expr}.to_i"
|
||||
|
||||
"start..finish"
|
||||
when Liquid::Expression::MethodLiteral
|
||||
'[]'
|
||||
else
|
||||
raise SuperfluidError, "Unknown iteration target: #{collection_name.inspect}"
|
||||
end
|
||||
|
||||
from_expr = if node.from == :continue
|
||||
"@for_offsets['#{node.name}'].to_i"
|
||||
elsif node.from
|
||||
make_variable_expr(node.from)
|
||||
else
|
||||
'0'
|
||||
end
|
||||
|
||||
limit_expr = node.limit ? make_variable_expr(node.limit) : 'nil'
|
||||
line "from = #{from_expr}"
|
||||
line "limit = #{limit_expr}"
|
||||
line "@context.raise_argument_error('invalid integer') unless from.is_a?(Integer)"
|
||||
line "@context.raise_argument_error('invalid integer') unless !limit || limit.is_a?(Integer)"
|
||||
|
||||
line "segment = slice_collection(#{iter_target_expr}, from, limit)"
|
||||
line "segment.reverse!" if node.reversed
|
||||
|
||||
forloop = var('forloop')
|
||||
hoist_var('forloop')
|
||||
line "#{forloop} = ForloopDrop.new('#{node.name}', segment.length, #{forloop})"
|
||||
|
||||
line "if segment.any?"
|
||||
indent do
|
||||
line "segment.each do |#{var(variable_name)}|"
|
||||
indent do
|
||||
line "@context['forloop'] = #{forloop}"
|
||||
|
||||
|
||||
old_in_loop = @in_loop
|
||||
compile(node.for_block)
|
||||
@in_loop = old_in_loop
|
||||
|
||||
line "#{forloop}.send(:increment!)"
|
||||
end
|
||||
line "end"
|
||||
end
|
||||
line "else"
|
||||
indent do
|
||||
compile(node.else_block) if node.else_block
|
||||
end
|
||||
line "end"
|
||||
|
||||
line "@for_offsets['#{node.name}'] = from + segment.length"
|
||||
line "#{forloop} = #{forloop}.parentloop"
|
||||
end
|
||||
|
||||
def compile_if(node)
|
||||
if_condition = node.blocks.first
|
||||
line "if #{make_condition_expr(if_condition)}"
|
||||
indent { if_condition.attachment.nodelist.each(&method(:compile)) }
|
||||
|
||||
node.blocks.drop(1).each do |condition|
|
||||
if condition.left != nil
|
||||
line "elsif #{make_condition_expr(condition)}"
|
||||
else
|
||||
line "else"
|
||||
end
|
||||
indent { condition.attachment.nodelist.each(&method(:compile)) }
|
||||
end
|
||||
|
||||
line "end"
|
||||
end
|
||||
|
||||
def compile_if_changed(node)
|
||||
line "if_changed = lambda do |; liquid_out|"
|
||||
indent do
|
||||
line "liquid_out = StringIO.new"
|
||||
node.nodelist.each(&method(:compile))
|
||||
line "liquid_out.string"
|
||||
end
|
||||
line "end.call"
|
||||
|
||||
line "if if_changed != @if_changed_last"
|
||||
indent { echo "if_changed" }
|
||||
line "end"
|
||||
|
||||
line "@if_changed_last = if_changed"
|
||||
end
|
||||
|
||||
def compile_capture(node)
|
||||
line "#{var(node.to)} = lambda do |; liquid_out|"
|
||||
hoist_var(node.to)
|
||||
indent do
|
||||
line "liquid_out = StringIO.new"
|
||||
node.nodelist.each(&method(:compile))
|
||||
line "liquid_out.string"
|
||||
end
|
||||
line "end.call"
|
||||
end
|
||||
|
||||
def make_condition_expr(node)
|
||||
condition = make_sub_condition_expr(node)
|
||||
if node.child_condition
|
||||
"(#{condition} #{node.child_relation} #{make_condition_expr(node.child_condition)})"
|
||||
else
|
||||
condition
|
||||
end
|
||||
end
|
||||
|
||||
def make_sub_condition_expr(node)
|
||||
return make_variable_expr(node.left) unless node.operator
|
||||
|
||||
operator = node.operator
|
||||
operator = "!=" if operator == "<>"
|
||||
|
||||
if operator == "=="
|
||||
if node.left.is_a?(Liquid::Expression::MethodLiteral) &&
|
||||
node.right.is_a?(Liquid::Expression::MethodLiteral)
|
||||
return "false"
|
||||
elsif node.right.is_a?(Liquid::Expression::MethodLiteral)
|
||||
target = make_variable_expr(node.left)
|
||||
message = node.right.method_name.inspect
|
||||
return "#{target}.respond_to?(#{message}) ? #{target}.send(#{message}) : nil"
|
||||
elsif node.left.is_a?(Liquid::Expression::MethodLiteral)
|
||||
target = make_variable_expr(node.right)
|
||||
message = node.left.method_name.inspect
|
||||
return "#{target}.respond_to?(#{message}) ? #{target}.send(#{message}) : nil"
|
||||
end
|
||||
end
|
||||
|
||||
left = make_variable_expr(node.left)
|
||||
right = make_variable_expr(node.right)
|
||||
|
||||
case operator
|
||||
when "contains"
|
||||
"contains?(#{left}, #{right})"
|
||||
else
|
||||
"apply_operator(#{left}, #{operator.inspect}, #{right})"
|
||||
end
|
||||
end
|
||||
|
||||
def compile_case(node)
|
||||
line 'if false' # HACK
|
||||
else_nodes, if_nodes = node.blocks.partition { |n| n.is_a?(Liquid::ElseCondition) }
|
||||
raise SuperfluidError, 'Too many else nodes' if else_nodes.count > 1
|
||||
else_node = else_nodes.first
|
||||
|
||||
if_nodes.each do |condition|
|
||||
left = make_variable_expr(condition.left)
|
||||
right = make_variable_expr(condition.right)
|
||||
line "elsif #{left} #{condition.operator} #{right}"
|
||||
indent { condition.attachment.nodelist.each(&method(:compile)) }
|
||||
end
|
||||
|
||||
if else_node
|
||||
line "else"
|
||||
indent { else_node.attachment.nodelist.each(&method(:compile)) }
|
||||
end
|
||||
|
||||
line "end"
|
||||
end
|
||||
|
||||
def compile_cycle(node)
|
||||
key = node.name
|
||||
key = key.name if key.is_a?(Liquid::VariableLookup)
|
||||
|
||||
line "key = #{key.inspect}"
|
||||
line "iteration = context.registers[:cycle][key].to_i"
|
||||
line "@cycle_values[key] ||= #{node.variables}"
|
||||
|
||||
line "val = @cycle_values[key][iteration]"
|
||||
echo 'val'
|
||||
line "context.registers[:cycle][key] = (iteration + 1) % #{node.variables.size}"
|
||||
end
|
||||
|
||||
def compile_raw(node)
|
||||
echo "#{node.body.inspect}"
|
||||
end
|
||||
|
||||
def compile_increment(node)
|
||||
line "value = context.environments.first[#{node.variable.inspect}] ||= 0"
|
||||
line "@context.environments.first[#{node.variable.inspect}] = value + 1"
|
||||
echo "value"
|
||||
end
|
||||
|
||||
def compile_decrement(node)
|
||||
line "value = context.environments.first[#{node.variable.inspect}] ||= 0"
|
||||
line "value -= 1"
|
||||
line "@context.environments.first[#{node.variable.inspect}] = value"
|
||||
echo "value"
|
||||
end
|
||||
|
||||
def compile_echo_literal(node)
|
||||
echo node.inspect
|
||||
end
|
||||
|
||||
def compile_variable(variable)
|
||||
echo make_variable_expr(variable)
|
||||
end
|
||||
|
||||
def compile_assign(node)
|
||||
from_expr = case node.from
|
||||
when Liquid::Variable
|
||||
make_variable_expr(node.from)
|
||||
else
|
||||
raise SuperfluidError, "Unknown assignment `from`: #{node.from.inspect}"
|
||||
end
|
||||
|
||||
line "#{var(node.to)} = #{from_expr}"
|
||||
hoist_var(node.to)
|
||||
end
|
||||
|
||||
def make_variable_expr(variable)
|
||||
case variable
|
||||
when Liquid::Variable
|
||||
base_expression = case variable.name
|
||||
when TrueClass, FalseClass, Numeric, String
|
||||
variable.name.inspect
|
||||
when Liquid::VariableLookup
|
||||
make_variable_lookup_expr(variable.name)
|
||||
when NilClass, Liquid::Expression::MethodLiteral
|
||||
'nil'
|
||||
else
|
||||
raise SuperfluidError, "Invalid variable name: #{variable.name.inspect}"
|
||||
derp "Bad var name", variable.name
|
||||
end
|
||||
variable.filters.inject(base_expression) do |inner, (filter_name, positional_args, keyword_args)|
|
||||
filter_args = positional_args.map(&method(:make_variable_expr))
|
||||
|
||||
if keyword_args
|
||||
filter_args << "{ " + keyword_args
|
||||
.transform_values(&method(:make_variable_expr))
|
||||
.collect { |(key, value)| "#{key.inspect} => #{value}" }
|
||||
.join(", ") + " }"
|
||||
end
|
||||
|
||||
"context.strainer.invoke(#{filter_name.inspect}, #{inner}, *[#{filter_args.join(", ")}])"
|
||||
end
|
||||
when Liquid::VariableLookup
|
||||
make_variable_lookup_expr(variable)
|
||||
when TrueClass, FalseClass, Numeric, String
|
||||
variable.inspect
|
||||
when NilClass
|
||||
'nil'
|
||||
else
|
||||
raise SuperfluidError, "Unknown expression type: #{variable.inspect}"
|
||||
end
|
||||
end
|
||||
|
||||
def make_variable_lookup_expr(variable_lookup)
|
||||
base_expr = var(variable_lookup.name)
|
||||
hoist_var(variable_lookup.name)
|
||||
return base_expr if variable_lookup.lookups.empty?
|
||||
|
||||
expr = Output.new(output.indent_level)
|
||||
expr.line "(begin"
|
||||
expr.indent do
|
||||
expr.line "inner = #{base_expr}"
|
||||
variable_lookup.lookups.each_with_index do |lookup, i|
|
||||
lookup_expr = lookup.inspect
|
||||
expr.line "inner = if inner.respond_to?(:[]) && ((inner.respond_to?(:key?) && inner.key?(#{lookup_expr})) || (inner.respond_to?(:fetch) && #{lookup_expr}.is_a?(Integer)))"
|
||||
expr.indent do
|
||||
expr.line "inner[#{lookup_expr}].to_liquid"
|
||||
end
|
||||
if variable_lookup.command_flags & (1 << i) != 0
|
||||
expr.line "elsif inner.respond_to?(#{lookup.inspect})"
|
||||
expr.indent do
|
||||
expr.line "inner.#{lookup}.to_liquid"
|
||||
end
|
||||
end
|
||||
expr.line "end"
|
||||
end
|
||||
end
|
||||
expr.line "end)"
|
||||
expr.string.strip
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def line(string)
|
||||
output.line(string)
|
||||
end
|
||||
|
||||
def echo(string)
|
||||
unless @blank
|
||||
line "liquid_out.write(to_output(#{string}))"
|
||||
end
|
||||
end
|
||||
|
||||
def indent(&block)
|
||||
output.indent(&block)
|
||||
end
|
||||
|
||||
def var(name)
|
||||
name = name
|
||||
.gsub('_', '__')
|
||||
.gsub('-', '_')
|
||||
"__liquid_#{name}"
|
||||
end
|
||||
|
||||
def unvar(name)
|
||||
name
|
||||
.delete_prefix('__liquid_')
|
||||
.gsub(/([^_])_([^_])/) { "#$1-#$2" }
|
||||
.gsub('__', '_')
|
||||
end
|
||||
|
||||
def hoist_var(name)
|
||||
@variables << var(name)
|
||||
end
|
||||
|
||||
attr_reader :output
|
||||
end
|
||||
end
|
||||
@@ -5,8 +5,8 @@ module Liquid
|
||||
include ParserSwitching
|
||||
|
||||
class << self
|
||||
def parse(tag_name, markup, tokenizer, parse_context)
|
||||
tag = new(tag_name, markup, parse_context)
|
||||
def parse(tag_name, markup, tokenizer, options)
|
||||
tag = new(tag_name, markup, options)
|
||||
tag.parse(tokenizer)
|
||||
tag
|
||||
end
|
||||
|
||||
@@ -10,10 +10,6 @@ module Liquid
|
||||
class Assign < Tag
|
||||
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
|
||||
|
||||
def self.syntax_error_translation_key
|
||||
"errors.syntax.assign".freeze
|
||||
end
|
||||
|
||||
attr_reader :to, :from
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
@@ -22,7 +18,7 @@ module Liquid
|
||||
@to = $1
|
||||
@from = Variable.new($2, options)
|
||||
else
|
||||
raise SyntaxError.new(options[:locale].t(self.class.syntax_error_translation_key))
|
||||
raise SyntaxError.new options[:locale].t("errors.syntax.assign".freeze)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ module Liquid
|
||||
class Capture < Block
|
||||
Syntax = /(#{VariableSignature}+)/o
|
||||
|
||||
attr_reader :to
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
if markup =~ Syntax
|
||||
@@ -33,6 +35,7 @@ module Liquid
|
||||
def blank?
|
||||
true
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
Template.register_tag('capture'.freeze, Capture)
|
||||
|
||||
@@ -15,7 +15,7 @@ module Liquid
|
||||
SimpleSyntax = /\A#{QuotedFragment}+/o
|
||||
NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om
|
||||
|
||||
attr_reader :variables
|
||||
attr_reader :variables, :name
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
|
||||
@@ -23,6 +23,8 @@ module Liquid
|
||||
@variable = markup.strip
|
||||
end
|
||||
|
||||
attr_reader :variable
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
value = context.environments.first[@variable] ||= 0
|
||||
value -= 1
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
module Liquid
|
||||
# Echo outputs an expression
|
||||
#
|
||||
# {% echo monkey %}
|
||||
# {% echo user.name %}
|
||||
#
|
||||
# This is identical to variable output syntax, like {{ foo }}, but works
|
||||
# inside {% liquid %} tags. The full syntax is supported, including filters:
|
||||
#
|
||||
# {% echo user | link %}
|
||||
#
|
||||
class Echo < Tag
|
||||
def initialize(tag_name, markup, parse_context)
|
||||
super
|
||||
@variable = Variable.new(markup, parse_context)
|
||||
end
|
||||
|
||||
def render(context)
|
||||
@variable.render(context)
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('echo'.freeze, Echo)
|
||||
end
|
||||
@@ -46,7 +46,7 @@ module Liquid
|
||||
class For < Block
|
||||
Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
|
||||
|
||||
attr_reader :collection_name, :variable_name, :limit, :from
|
||||
attr_reader :collection_name, :variable_name, :limit, :from, :for_block, :else_block, :name, :reversed
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
|
||||
@@ -20,6 +20,8 @@ module Liquid
|
||||
@variable = markup.strip
|
||||
end
|
||||
|
||||
attr_reader :variable
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
value = context.environments.first[@variable] ||= 0
|
||||
context.environments.first[@variable] = value + 1
|
||||
|
||||
@@ -3,6 +3,8 @@ module Liquid
|
||||
Syntax = /\A\s*\z/
|
||||
FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
|
||||
|
||||
attr_reader :body
|
||||
|
||||
def initialize(tag_name, markup, parse_context)
|
||||
super
|
||||
|
||||
|
||||
@@ -206,7 +206,7 @@ module Liquid
|
||||
|
||||
begin
|
||||
# render the nodelist.
|
||||
# for performance reasons we get an array back here. join will make a string out of it.
|
||||
|
||||
with_profiling(context) do
|
||||
@root.render_to_output_buffer(context, output || '')
|
||||
end
|
||||
|
||||
@@ -1,31 +1,25 @@
|
||||
module Liquid
|
||||
class Tokenizer
|
||||
attr_reader :line_number, :for_liquid_tag
|
||||
attr_reader :line_number
|
||||
|
||||
def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false)
|
||||
def initialize(source, line_numbers = false)
|
||||
@source = source
|
||||
@line_number = line_number || (line_numbers ? 1 : nil)
|
||||
@for_liquid_tag = for_liquid_tag
|
||||
@line_number = line_numbers ? 1 : nil
|
||||
@tokens = tokenize
|
||||
end
|
||||
|
||||
def shift
|
||||
token = @tokens.shift or return
|
||||
|
||||
if @line_number
|
||||
@line_number += @for_liquid_tag ? 1 : token.count("\n")
|
||||
end
|
||||
|
||||
token = @tokens.shift
|
||||
@line_number += token.count("\n") if @line_number && token
|
||||
token
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def tokenize
|
||||
@source = @source.source if @source.respond_to?(:source)
|
||||
return [] if @source.to_s.empty?
|
||||
|
||||
return @source.split("\n") if @for_liquid_tag
|
||||
|
||||
tokens = @source.split(TemplateParser)
|
||||
|
||||
# removes the rogue empty element at the beginning of the array
|
||||
|
||||
@@ -3,7 +3,7 @@ module Liquid
|
||||
SQUARE_BRACKETED = /\A\[(.*)\]\z/m
|
||||
COMMAND_METHODS = ['size'.freeze, 'first'.freeze, 'last'.freeze].freeze
|
||||
|
||||
attr_reader :name, :lookups
|
||||
attr_reader :name, :lookups, :command_flags
|
||||
|
||||
def self.parse(markup)
|
||||
new(markup)
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
require 'benchmark/ips'
|
||||
require_relative 'theme_runner'
|
||||
|
||||
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
|
||||
case ARGV.first.to_sym
|
||||
when :lax
|
||||
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
|
||||
when :strict
|
||||
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
|
||||
when :superfluid
|
||||
require 'liquid/superfluid'
|
||||
Liquid::Template.error_mode = :strict
|
||||
end
|
||||
|
||||
profiler = ThemeRunner.new
|
||||
|
||||
Benchmark.ips do |x|
|
||||
|
||||
@@ -1,62 +1,26 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'benchmark/ips'
|
||||
requirf 'benchmark/ips'
|
||||
require 'memory_profiler'
|
||||
require 'terminal-table'
|
||||
require_relative 'theme_runner'
|
||||
|
||||
class Profiler
|
||||
LOG_LABEL = "Profiling: ".rjust(14).freeze
|
||||
REPORTS_DIR = File.expand_path('.memprof', __dir__).freeze
|
||||
def profile(phase, &block)
|
||||
puts
|
||||
puts "#{phase}:"
|
||||
puts
|
||||
|
||||
def self.run
|
||||
puts
|
||||
yield new
|
||||
end
|
||||
report = MemoryProfiler.report(&block)
|
||||
|
||||
def initialize
|
||||
@allocated = []
|
||||
@retained = []
|
||||
@headings = []
|
||||
end
|
||||
|
||||
def profile(phase, &block)
|
||||
print LOG_LABEL
|
||||
print "#{phase}.. ".ljust(10)
|
||||
report = MemoryProfiler.report(&block)
|
||||
puts 'Done.'
|
||||
@headings << phase.capitalize
|
||||
@allocated << "#{report.scale_bytes(report.total_allocated_memsize)} (#{report.total_allocated} objects)"
|
||||
@retained << "#{report.scale_bytes(report.total_retained_memsize)} (#{report.total_retained} objects)"
|
||||
|
||||
return if ENV['CI']
|
||||
require 'fileutils'
|
||||
report_file = File.join(REPORTS_DIR, "#{sanitize(phase)}.txt")
|
||||
FileUtils.mkdir_p(REPORTS_DIR)
|
||||
report.pretty_print(to_file: report_file, scale_bytes: true)
|
||||
end
|
||||
|
||||
def tabulate
|
||||
table = Terminal::Table.new(headings: @headings.unshift('Phase')) do |t|
|
||||
t << @allocated.unshift('Total allocated')
|
||||
t << @retained.unshift('Total retained')
|
||||
end
|
||||
|
||||
puts
|
||||
puts table
|
||||
puts "\nDetailed report(s) saved to #{REPORTS_DIR}/" unless ENV['CI']
|
||||
end
|
||||
|
||||
def sanitize(string)
|
||||
string.downcase.gsub(/[\W]/, '-').squeeze('-')
|
||||
end
|
||||
report.pretty_print(
|
||||
color_output: true,
|
||||
scale_bytes: true,
|
||||
detailed_report: true
|
||||
)
|
||||
end
|
||||
|
||||
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
|
||||
|
||||
runner = ThemeRunner.new
|
||||
Profiler.run do |x|
|
||||
x.profile('parse') { runner.compile }
|
||||
x.profile('render') { runner.render }
|
||||
x.tabulate
|
||||
end
|
||||
profiler = ThemeRunner.new
|
||||
|
||||
profile("Parsing") { profiler.compile }
|
||||
profile("Rendering") { profiler.render }
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
require 'test_helper'
|
||||
|
||||
class EchoTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_echo_outputs_its_input
|
||||
assert_template_result('BAR', <<~LIQUID, { 'variable-name' => 'bar' })
|
||||
{%- echo variable-name | upcase -%}
|
||||
LIQUID
|
||||
end
|
||||
end
|
||||
@@ -1,104 +0,0 @@
|
||||
require 'test_helper'
|
||||
|
||||
class LiquidTagTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_liquid_tag
|
||||
assert_template_result('1 2 3', <<~LIQUID, 'array' => [1, 2, 3])
|
||||
{%- liquid
|
||||
echo array | join: " "
|
||||
-%}
|
||||
LIQUID
|
||||
|
||||
assert_template_result('1 2 3', <<~LIQUID, 'array' => [1, 2, 3])
|
||||
{%- liquid
|
||||
for value in array
|
||||
echo value
|
||||
unless forloop.last
|
||||
echo " "
|
||||
endunless
|
||||
endfor
|
||||
-%}
|
||||
LIQUID
|
||||
|
||||
assert_template_result('4 8 12 6', <<~LIQUID, 'array' => [1, 2, 3])
|
||||
{%- liquid
|
||||
for value in array
|
||||
assign double_value = value | times: 2
|
||||
echo double_value | times: 2
|
||||
unless forloop.last
|
||||
echo " "
|
||||
endunless
|
||||
endfor
|
||||
|
||||
echo " "
|
||||
echo double_value
|
||||
-%}
|
||||
LIQUID
|
||||
|
||||
assert_template_result('abc', <<~LIQUID)
|
||||
{%- liquid echo "a" -%}
|
||||
b
|
||||
{%- liquid echo "c" -%}
|
||||
LIQUID
|
||||
end
|
||||
|
||||
def test_liquid_tag_errors
|
||||
assert_match_syntax_error("syntax error (line 1): Unknown tag 'error'", <<~LIQUID)
|
||||
{%- liquid error no such tag -%}
|
||||
LIQUID
|
||||
|
||||
assert_match_syntax_error("syntax error (line 7): Unknown tag 'error'", <<~LIQUID)
|
||||
{{ test }}
|
||||
|
||||
{%-
|
||||
liquid
|
||||
for value in array
|
||||
|
||||
error no such tag
|
||||
endfor
|
||||
-%}
|
||||
LIQUID
|
||||
|
||||
assert_match_syntax_error("syntax error (line 2): Unknown tag '!!! the guards are vigilant'", <<~LIQUID)
|
||||
{%- liquid
|
||||
!!! the guards are vigilant
|
||||
-%}
|
||||
LIQUID
|
||||
|
||||
assert_match_syntax_error("syntax error (line 4): 'for' tag was never closed", <<~LIQUID)
|
||||
{%- liquid
|
||||
for value in array
|
||||
echo 'forgot to close the for tag'
|
||||
-%}
|
||||
LIQUID
|
||||
end
|
||||
|
||||
def test_line_number_is_correct_after_a_blank_token
|
||||
assert_match_syntax_error("syntax error (line 3): Unknown tag 'error'", "{% liquid echo ''\n\n error %}")
|
||||
assert_match_syntax_error("syntax error (line 3): Unknown tag 'error'", "{% liquid echo ''\n \n error %}")
|
||||
end
|
||||
|
||||
def test_cannot_open_blocks_living_past_a_liquid_tag
|
||||
assert_match_syntax_error("syntax error (line 3): 'if' tag was never closed", <<~LIQUID)
|
||||
{%- liquid
|
||||
if true
|
||||
-%}
|
||||
{%- endif -%}
|
||||
LIQUID
|
||||
end
|
||||
|
||||
def test_quirk_can_close_blocks_created_before_a_liquid_tag
|
||||
assert_template_result("42", <<~LIQUID)
|
||||
{%- if true -%}
|
||||
42
|
||||
{%- liquid endif -%}
|
||||
LIQUID
|
||||
end
|
||||
|
||||
def test_liquid_tag_in_raw
|
||||
assert_template_result("{% liquid echo 'test' %}\n", <<~LIQUID)
|
||||
{% raw %}{% liquid echo 'test' %}{% endraw %}
|
||||
LIQUID
|
||||
end
|
||||
end
|
||||
@@ -80,7 +80,10 @@ class VariableTest < Minitest::Test
|
||||
assigns['test'] = 'Tobi'
|
||||
assert_equal 'Hello Tobi', template.render!(assigns)
|
||||
assigns.delete('test')
|
||||
assert_equal "Hello ", template.render!(assigns)
|
||||
e = assert_raises(RuntimeError) do
|
||||
template.render!(assigns)
|
||||
end
|
||||
assert_equal "Unknown variable 'test'", e.message
|
||||
end
|
||||
|
||||
def test_multiline_variable
|
||||
|
||||
@@ -19,6 +19,11 @@ if ENV['LIQUID-C'] == '1'
|
||||
require 'liquid/c'
|
||||
end
|
||||
|
||||
if ENV['SUPERFLUID'] == '1'
|
||||
puts "-- SUPERFLUID"
|
||||
require 'liquid/superfluid'
|
||||
end
|
||||
|
||||
if Minitest.const_defined?('Test')
|
||||
# We're on Minitest 5+. Nothing to do here.
|
||||
else
|
||||
@@ -37,18 +42,18 @@ module Minitest
|
||||
include Liquid
|
||||
|
||||
def assert_template_result(expected, template, assigns = {}, message = nil)
|
||||
assert_equal expected, Template.parse(template, line_numbers: true).render!(assigns), message
|
||||
assert_equal expected, Template.parse(template).render!(assigns), message
|
||||
end
|
||||
|
||||
def assert_template_result_matches(expected, template, assigns = {}, message = nil)
|
||||
return assert_template_result(expected, template, assigns, message) unless expected.is_a? Regexp
|
||||
|
||||
assert_match expected, Template.parse(template, line_numbers: true).render!(assigns), message
|
||||
assert_match expected, Template.parse(template).render!(assigns), message
|
||||
end
|
||||
|
||||
def assert_match_syntax_error(match, template, assigns = {})
|
||||
exception = assert_raises(Liquid::SyntaxError) do
|
||||
Template.parse(template, line_numbers: true).render(assigns)
|
||||
Template.parse(template).render(assigns)
|
||||
end
|
||||
assert_match match, exception.message
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user