mirror of
https://github.com/kemko/liquid.git
synced 2026-01-02 00:05:42 +03:00
Compare commits
38 Commits
implicit-v
...
simplify_f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3c6a523b5 | ||
|
|
c34aba4d1a | ||
|
|
8933044cb5 | ||
|
|
ba0edc661d | ||
|
|
c06a325b3b | ||
|
|
34083c96d5 | ||
|
|
9672ed5285 | ||
|
|
f3112fc038 | ||
|
|
d338ccb9a6 | ||
|
|
6590815b00 | ||
|
|
d1deb89085 | ||
|
|
b3097f143c | ||
|
|
7b309dc75d | ||
|
|
8f68cffdf1 | ||
|
|
dd27d0fd1d | ||
|
|
7a26e6b3d8 | ||
|
|
cf4e77ab0c | ||
|
|
7bae55dd39 | ||
|
|
0ce8aef229 | ||
|
|
6eab595fae | ||
|
|
b16b109a80 | ||
|
|
831355dfbd | ||
|
|
00702d8e63 | ||
|
|
197c058208 | ||
|
|
98dfe198e1 | ||
|
|
c2c1497ca8 | ||
|
|
d19967a79d | ||
|
|
248c54a386 | ||
|
|
2c42447659 | ||
|
|
ab698191b9 | ||
|
|
9ef6f9b642 | ||
|
|
4684478e94 | ||
|
|
9640e77805 | ||
|
|
2a1ca3152d | ||
|
|
7dc488a73b | ||
|
|
e6ed804ca5 | ||
|
|
951abb67ee | ||
|
|
8d1cd41453 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,3 +7,4 @@ pkg
|
||||
.ruby-version
|
||||
Gemfile.lock
|
||||
.bundle
|
||||
.byebug_history
|
||||
|
||||
12
.rubocop.yml
12
.rubocop.yml
@@ -2,9 +2,15 @@ inherit_from:
|
||||
- .rubocop_todo.yml
|
||||
- ./.rubocop_todo.yml
|
||||
|
||||
require: rubocop-performance
|
||||
|
||||
Performance:
|
||||
Enabled: true
|
||||
|
||||
AllCops:
|
||||
Exclude:
|
||||
- 'performance/shopify/*'
|
||||
- 'vendor/bundle/**/*'
|
||||
- 'pkg/**'
|
||||
|
||||
Metrics/BlockNesting:
|
||||
@@ -79,9 +85,6 @@ Style/TrailingCommaInArrayLiteral:
|
||||
Style/TrailingCommaInHashLiteral:
|
||||
Enabled: false
|
||||
|
||||
Layout/IndentHash:
|
||||
EnforcedStyle: consistent
|
||||
|
||||
Style/FormatString:
|
||||
Enabled: false
|
||||
|
||||
@@ -106,9 +109,6 @@ Style/RegexpLiteral:
|
||||
Style/SymbolLiteral:
|
||||
Enabled: false
|
||||
|
||||
Performance/Count:
|
||||
Enabled: false
|
||||
|
||||
Naming/ConstantName:
|
||||
Enabled: false
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# This configuration was generated by
|
||||
# `rubocop --auto-gen-config`
|
||||
# on 2019-03-19 11:04:37 -0400 using RuboCop version 0.53.0.
|
||||
# on 2019-08-29 00:43:36 +1000 using RuboCop version 0.74.0.
|
||||
# The point is for the user to remove these configuration records
|
||||
# one by one as the offenses are removed from the code base.
|
||||
# Note that changes in the inspected code, or installation of new
|
||||
@@ -8,16 +8,67 @@
|
||||
|
||||
# Offense count: 1
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: Include, TreatCommentsAsGroupSeparators.
|
||||
# Configuration parameters: TreatCommentsAsGroupSeparators, Include.
|
||||
# Include: **/*.gemspec
|
||||
Gemspec/OrderedDependencies:
|
||||
Exclude:
|
||||
- 'liquid.gemspec'
|
||||
|
||||
# Offense count: 1
|
||||
# Configuration parameters: Include.
|
||||
# Include: **/*.gemspec,
|
||||
Gemspec/RequiredRubyVersion:
|
||||
Exclude:
|
||||
- 'liquid.gemspec'
|
||||
|
||||
# Offense count: 124
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle, IndentationWidth.
|
||||
# SupportedStyles: with_first_argument, with_fixed_indentation
|
||||
Layout/AlignArguments:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 7
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle.
|
||||
# SupportedHashRocketStyles: key, separator, table
|
||||
# SupportedColonStyles: key, separator, table
|
||||
# SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit
|
||||
Layout/AlignHash:
|
||||
Exclude:
|
||||
- 'lib/liquid/condition.rb'
|
||||
- 'lib/liquid/expression.rb'
|
||||
- 'test/unit/context_unit_test.rb'
|
||||
|
||||
# Offense count: 6
|
||||
# Cop supports --auto-correct.
|
||||
Layout/ClosingHeredocIndentation:
|
||||
Exclude:
|
||||
- 'test/integration/tags/for_tag_test.rb'
|
||||
|
||||
# Offense count: 27
|
||||
# Cop supports --auto-correct.
|
||||
Layout/EmptyLineAfterGuardClause:
|
||||
Exclude:
|
||||
- 'lib/liquid/block.rb'
|
||||
- 'lib/liquid/block_body.rb'
|
||||
- 'lib/liquid/context.rb'
|
||||
- 'lib/liquid/drop.rb'
|
||||
- 'lib/liquid/lexer.rb'
|
||||
- 'lib/liquid/parser.rb'
|
||||
- 'lib/liquid/standardfilters.rb'
|
||||
- 'lib/liquid/strainer.rb'
|
||||
- 'lib/liquid/tags/for.rb'
|
||||
- 'lib/liquid/tags/if.rb'
|
||||
- 'lib/liquid/tags/include.rb'
|
||||
- 'lib/liquid/utils.rb'
|
||||
- 'lib/liquid/variable.rb'
|
||||
- 'lib/liquid/variable_lookup.rb'
|
||||
|
||||
# Offense count: 5
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle.
|
||||
# SupportedStyles: auto_detection, squiggly, active_support, powerpack, unindent
|
||||
# SupportedStyles: squiggly, active_support, powerpack, unindent
|
||||
Layout/IndentHeredoc:
|
||||
Exclude:
|
||||
- 'test/integration/tags/for_tag_test.rb'
|
||||
@@ -32,6 +83,13 @@ Layout/MultilineMethodCallBraceLayout:
|
||||
- 'test/integration/error_handling_test.rb'
|
||||
- 'test/unit/strainer_unit_test.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: AllowForAlignment.
|
||||
Layout/SpaceAroundOperators:
|
||||
Exclude:
|
||||
- 'lib/liquid/condition.rb'
|
||||
|
||||
# Offense count: 2
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle.
|
||||
@@ -46,35 +104,33 @@ Lint/Void:
|
||||
Exclude:
|
||||
- 'lib/liquid/parse_context.rb'
|
||||
|
||||
# Offense count: 54
|
||||
# Offense count: 53
|
||||
Metrics/AbcSize:
|
||||
Max: 56
|
||||
|
||||
# Offense count: 12
|
||||
Metrics/CyclomaticComplexity:
|
||||
Max: 12
|
||||
Max: 13
|
||||
|
||||
# Offense count: 112
|
||||
# Configuration parameters: CountComments.
|
||||
# Offense count: 118
|
||||
# Configuration parameters: CountComments, ExcludedMethods.
|
||||
Metrics/MethodLength:
|
||||
Max: 37
|
||||
Max: 38
|
||||
|
||||
# Offense count: 8
|
||||
# Offense count: 9
|
||||
Metrics/PerceivedComplexity:
|
||||
Max: 11
|
||||
|
||||
# Offense count: 52
|
||||
# Configuration parameters: Blacklist.
|
||||
# Blacklist: END, (?-mix:EO[A-Z]{1})
|
||||
Naming/HeredocDelimiterNaming:
|
||||
# Offense count: 1
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: PreferredName.
|
||||
Naming/RescuedExceptionsVariableName:
|
||||
Exclude:
|
||||
- 'test/integration/assign_test.rb'
|
||||
- 'test/integration/capture_test.rb'
|
||||
- 'test/integration/trim_mode_test.rb'
|
||||
- 'lib/liquid/context.rb'
|
||||
|
||||
# Offense count: 23
|
||||
# Offense count: 20
|
||||
# Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames.
|
||||
# AllowedNames: io, id
|
||||
# AllowedNames: io, id, to, by, on, in, at, ip, db
|
||||
Naming/UncommunicativeMethodParamName:
|
||||
Exclude:
|
||||
- 'example/server/example_servlet.rb'
|
||||
@@ -82,14 +138,22 @@ Naming/UncommunicativeMethodParamName:
|
||||
- 'lib/liquid/context.rb'
|
||||
- 'lib/liquid/standardfilters.rb'
|
||||
- 'lib/liquid/tags/if.rb'
|
||||
- 'lib/liquid/utils.rb'
|
||||
- 'lib/liquid/variable.rb'
|
||||
- 'test/integration/filter_test.rb'
|
||||
- 'test/integration/standard_filter_test.rb'
|
||||
- 'test/integration/tags/for_tag_test.rb'
|
||||
- 'test/integration/template_test.rb'
|
||||
- 'test/unit/condition_unit_test.rb'
|
||||
|
||||
# Offense count: 5
|
||||
# Configuration parameters: EnforcedStyle.
|
||||
# SupportedStyles: inline, group
|
||||
Style/AccessModifierDeclarations:
|
||||
Exclude:
|
||||
- 'lib/liquid/block_body.rb'
|
||||
- 'lib/liquid/tag.rb'
|
||||
- 'lib/liquid/tags/include.rb'
|
||||
- 'test/unit/strainer_unit_test.rb'
|
||||
|
||||
# Offense count: 10
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle.
|
||||
@@ -117,15 +181,9 @@ Style/ConditionalAssignment:
|
||||
- 'lib/liquid/errors.rb'
|
||||
|
||||
# Offense count: 1
|
||||
Style/DateTime:
|
||||
Exclude:
|
||||
- 'test/unit/context_unit_test.rb'
|
||||
|
||||
# Offense count: 2
|
||||
# Cop supports --auto-correct.
|
||||
Style/EmptyCaseCondition:
|
||||
Exclude:
|
||||
- 'lib/liquid/block_body.rb'
|
||||
- 'lib/liquid/lexer.rb'
|
||||
|
||||
# Offense count: 5
|
||||
@@ -163,6 +221,13 @@ Style/FormatStringToken:
|
||||
- 'test/integration/filter_test.rb'
|
||||
- 'test/integration/hash_ordering_test.rb'
|
||||
|
||||
# Offense count: 106
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle.
|
||||
# SupportedStyles: always, never
|
||||
Style/FrozenStringLiteralComment:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 14
|
||||
# Configuration parameters: MinBodyLength.
|
||||
Style/GuardClause:
|
||||
@@ -180,6 +245,13 @@ Style/GuardClause:
|
||||
- 'lib/liquid/variable.rb'
|
||||
- 'test/unit/tokenizer_unit_test.rb'
|
||||
|
||||
# Offense count: 53
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle.
|
||||
# SupportedStyles: literals, strict
|
||||
Style/MutableConstant:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 1
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle, MinBodyLength.
|
||||
@@ -188,9 +260,9 @@ Style/Next:
|
||||
Exclude:
|
||||
- 'lib/liquid/tags/for.rb'
|
||||
|
||||
# Offense count: 4
|
||||
# Offense count: 13
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: AutoCorrect, EnforcedStyle.
|
||||
# Configuration parameters: AutoCorrect, EnforcedStyle, IgnoredMethods.
|
||||
# SupportedStyles: predicate, comparison
|
||||
Style/NumericPredicate:
|
||||
Exclude:
|
||||
@@ -199,6 +271,8 @@ Style/NumericPredicate:
|
||||
- 'lib/liquid/forloop_drop.rb'
|
||||
- 'lib/liquid/standardfilters.rb'
|
||||
- 'lib/liquid/tablerowloop_drop.rb'
|
||||
- 'test/integration/standard_filter_test.rb'
|
||||
- 'test/integration/template_test.rb'
|
||||
|
||||
# Offense count: 14
|
||||
# Cop supports --auto-correct.
|
||||
@@ -216,6 +290,16 @@ Style/RedundantSelf:
|
||||
Exclude:
|
||||
- 'lib/liquid/strainer.rb'
|
||||
|
||||
# Offense count: 3
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: ConvertCodeThatCanStartToReturnNil, Whitelist.
|
||||
# Whitelist: present?, blank?, presence, try, try!
|
||||
Style/SafeNavigation:
|
||||
Exclude:
|
||||
- 'lib/liquid/drop.rb'
|
||||
- 'lib/liquid/strainer.rb'
|
||||
- 'lib/liquid/tokenizer.rb'
|
||||
|
||||
# Offense count: 9
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: AllowAsExpressionSeparator.
|
||||
@@ -253,8 +337,9 @@ Style/WhileUntilModifier:
|
||||
Exclude:
|
||||
- 'lib/liquid/tags/case.rb'
|
||||
|
||||
# Offense count: 640
|
||||
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
|
||||
# Offense count: 665
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
|
||||
# URISchemes: http, https
|
||||
Metrics/LineLength:
|
||||
Max: 294
|
||||
|
||||
29
.travis.yml
29
.travis.yml
@@ -1,31 +1,30 @@
|
||||
language: ruby
|
||||
cache: bundler
|
||||
|
||||
rvm:
|
||||
- 2.1
|
||||
- 2.2
|
||||
- 2.3
|
||||
- 2.4
|
||||
- 2.5
|
||||
- &latest_ruby 2.6
|
||||
- 2.7
|
||||
- ruby-head
|
||||
- jruby-head
|
||||
# - rbx-2
|
||||
|
||||
sudo: false
|
||||
|
||||
addons:
|
||||
apt:
|
||||
packages:
|
||||
- libgmp3-dev
|
||||
- truffleruby
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- rvm: *latest_ruby
|
||||
script: bundle exec rake memory_profile:run
|
||||
name: Profiling Memory Usage
|
||||
allow_failures:
|
||||
- rvm: ruby-head
|
||||
- rvm: jruby-head
|
||||
- rvm: truffleruby
|
||||
|
||||
install:
|
||||
- bundle install
|
||||
|
||||
script: bundle exec rake
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- gh-pages
|
||||
- /.*-stable/
|
||||
|
||||
notifications:
|
||||
disable: true
|
||||
|
||||
10
Gemfile
10
Gemfile
@@ -8,16 +8,18 @@ gemspec
|
||||
group :benchmark, :test do
|
||||
gem 'benchmark-ips'
|
||||
gem 'memory_profiler'
|
||||
gem 'terminal-table'
|
||||
|
||||
install_if -> { RUBY_PLATFORM !~ /mingw|mswin|java/ } do
|
||||
install_if -> { RUBY_PLATFORM !~ /mingw|mswin|java/ && RUBY_ENGINE != 'truffleruby' } do
|
||||
gem 'stackprof'
|
||||
end
|
||||
end
|
||||
|
||||
group :test do
|
||||
gem 'rubocop', '~> 0.53.0'
|
||||
gem 'rubocop', '~> 0.74.0', require: false
|
||||
gem 'rubocop-performance', require: false
|
||||
|
||||
platform :mri do
|
||||
gem 'liquid-c', github: 'Shopify/liquid-c', ref: '9168659de45d6d576fce30c735f857e597fa26f6'
|
||||
platform :mri, :truffleruby do
|
||||
gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'liquid-tag'
|
||||
end
|
||||
end
|
||||
|
||||
10
Rakefile
10
Rakefile
@@ -19,8 +19,10 @@ task :warn_test do
|
||||
end
|
||||
|
||||
task :rubocop do
|
||||
require 'rubocop/rake_task'
|
||||
RuboCop::RakeTask.new
|
||||
if RUBY_ENGINE == 'ruby'
|
||||
require 'rubocop/rake_task'
|
||||
RuboCop::RakeTask.new
|
||||
end
|
||||
end
|
||||
|
||||
desc 'runs test suite with both strict and lax parsers'
|
||||
@@ -32,8 +34,8 @@ task :test do
|
||||
Rake::Task['base_test'].reenable
|
||||
Rake::Task['base_test'].invoke
|
||||
|
||||
if RUBY_ENGINE == 'ruby'
|
||||
ENV['LIQUID-C'] = '1'
|
||||
if RUBY_ENGINE == 'ruby' || RUBY_ENGINE == 'truffleruby'
|
||||
ENV['LIQUID_C'] = '1'
|
||||
|
||||
ENV['LIQUID_PARSER_MODE'] = 'lax'
|
||||
Rake::Task['base_test'].reenable
|
||||
|
||||
@@ -74,6 +74,7 @@ require 'liquid/condition'
|
||||
require 'liquid/utils'
|
||||
require 'liquid/tokenizer'
|
||||
require 'liquid/parse_context'
|
||||
require 'liquid/partial_cache'
|
||||
|
||||
# Load all the tags of the standard library
|
||||
#
|
||||
|
||||
@@ -13,6 +13,7 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
# For backwards compatibility
|
||||
def render(context)
|
||||
@body.render(context)
|
||||
end
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
module Liquid
|
||||
class BlockBody
|
||||
FullToken = /\A#{TagStart}#{WhitespaceControl}?\s*(\w+)\s*(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
|
||||
LiquidTagToken = /\A\s*(\w+)\s*(.*?)\z/o
|
||||
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
|
||||
@@ -13,8 +14,42 @@ module Liquid
|
||||
@blank = true
|
||||
end
|
||||
|
||||
def parse(tokenizer, parse_context)
|
||||
def parse(tokenizer, parse_context, &block)
|
||||
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
|
||||
@@ -23,9 +58,20 @@ module Liquid
|
||||
unless token =~ FullToken
|
||||
raise_missing_tag_terminator(token, parse_context)
|
||||
end
|
||||
tag_name = $1
|
||||
markup = $2
|
||||
# fetch the tag from registered blocks
|
||||
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
|
||||
|
||||
unless tag = registered_tags[tag_name]
|
||||
# end parsing if we reach an unknown tag and let the caller decide
|
||||
# determine how to proceed
|
||||
@@ -67,19 +113,23 @@ module Liquid
|
||||
end
|
||||
|
||||
def render(context)
|
||||
output = []
|
||||
render_to_output_buffer(context, '')
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
context.resource_limits.render_score += @nodelist.length
|
||||
|
||||
idx = 0
|
||||
while node = @nodelist[idx]
|
||||
previous_output_size = output.bytesize
|
||||
|
||||
case node
|
||||
when String
|
||||
check_resources(context, node)
|
||||
output << node
|
||||
when Variable
|
||||
render_node_to_output(node, output, context)
|
||||
render_node(context, output, node)
|
||||
when Block
|
||||
render_node_to_output(node, output, context, node.blank?)
|
||||
render_node(context, node.blank? ? '' : output, node)
|
||||
break if context.interrupt? # might have happened in a for-block
|
||||
when Continue, Break
|
||||
# If we get an Interrupt that means the block must stop processing. An
|
||||
@@ -88,34 +138,30 @@ module Liquid
|
||||
context.push_interrupt(node.interrupt)
|
||||
break
|
||||
else # Other non-Block tags
|
||||
render_node_to_output(node, output, context)
|
||||
render_node(context, output, node)
|
||||
break if context.interrupt? # might have happened through an include
|
||||
end
|
||||
idx += 1
|
||||
|
||||
raise_if_resource_limits_reached(context, output.bytesize - previous_output_size)
|
||||
end
|
||||
|
||||
output.join
|
||||
output
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def render_node_to_output(node, output, context, skip_output = false)
|
||||
node_output = node.render(context)
|
||||
node_output = node_output.is_a?(Array) ? node_output.join : node_output.to_s
|
||||
check_resources(context, node_output)
|
||||
output << node_output unless skip_output
|
||||
rescue MemoryError => e
|
||||
raise e
|
||||
def render_node(context, output, node)
|
||||
node.render_to_output_buffer(context, output)
|
||||
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
|
||||
context.handle_error(e, node.line_number)
|
||||
output << nil
|
||||
rescue ::StandardError => e
|
||||
line_number = node.is_a?(String) ? nil : node.line_number
|
||||
output << context.handle_error(e, line_number)
|
||||
end
|
||||
|
||||
def check_resources(context, node_output)
|
||||
context.resource_limits.render_length += node_output.bytesize
|
||||
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
|
||||
|
||||
@@ -12,19 +12,31 @@ module Liquid
|
||||
#
|
||||
# context['bob'] #=> nil class Context
|
||||
class Context
|
||||
attr_reader :scope, :errors, :registers, :environments, :resource_limits
|
||||
attr_reader :scopes, :errors, :registers, :environments, :resource_limits, :static_registers, :static_environments
|
||||
attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters
|
||||
|
||||
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil)
|
||||
@environments = [environments].flatten
|
||||
@scope = outer_scope || {}
|
||||
@registers = registers
|
||||
@errors = []
|
||||
@partial = false
|
||||
@strict_variables = false
|
||||
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
|
||||
BLANK_SCOPE = {}
|
||||
|
||||
# rubocop:disable Metrics/ParameterLists
|
||||
def self.build(environments: {}, outer_scope: {}, registers: {}, rethrow_errors: false, resource_limits: nil, static_registers: {}, static_environments: {})
|
||||
new(environments, outer_scope, registers, rethrow_errors, resource_limits, static_registers, static_environments)
|
||||
end
|
||||
|
||||
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil, static_registers = {}, static_environments = {})
|
||||
@environments = [environments].flatten
|
||||
@static_environments = [static_environments].flatten.map(&:freeze).freeze
|
||||
@scopes = [(outer_scope || {})]
|
||||
@registers = registers
|
||||
@static_registers = static_registers.freeze
|
||||
@errors = []
|
||||
@partial = false
|
||||
@strict_variables = false
|
||||
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
|
||||
@base_scope_depth = 0
|
||||
squash_instance_assigns_with_environments
|
||||
|
||||
@this_stack_used = false
|
||||
|
||||
self.exception_renderer = Template.default_exception_renderer
|
||||
if rethrow_errors
|
||||
self.exception_renderer = ->(e) { raise }
|
||||
@@ -33,9 +45,8 @@ module Liquid
|
||||
@interrupts = []
|
||||
@filters = []
|
||||
@global_filter = nil
|
||||
|
||||
@stack_level = 0
|
||||
end
|
||||
# rubocop:enable Metrics/ParameterLists
|
||||
|
||||
def warnings
|
||||
@warnings ||= []
|
||||
@@ -86,9 +97,21 @@ module Liquid
|
||||
strainer.invoke(method, *args).to_liquid
|
||||
end
|
||||
|
||||
# Push new local scope on the stack. use <tt>Context#stack</tt> instead
|
||||
def push(new_scope = {})
|
||||
@scopes.unshift(new_scope)
|
||||
check_overflow
|
||||
end
|
||||
|
||||
# Merge a hash of variables in the current local scope
|
||||
def merge(new_scopes)
|
||||
new_scopes.each { |k, v| self[k] = v }
|
||||
@scopes[0].merge!(new_scopes)
|
||||
end
|
||||
|
||||
# Pop from the stack. use <tt>Context#stack</tt> instead
|
||||
def pop
|
||||
raise ContextError if @scopes.size == 1
|
||||
@scopes.shift
|
||||
end
|
||||
|
||||
# Pushes a new local scope on the stack, pops it at the end of the block
|
||||
@@ -99,20 +122,51 @@ module Liquid
|
||||
# end
|
||||
#
|
||||
# context['var] #=> nil
|
||||
def stack(*variable_names)
|
||||
@stack_level += 1
|
||||
raise StackLevelError, "Nesting too deep".freeze if @stack_level > Block::MAX_DEPTH
|
||||
|
||||
begin
|
||||
yield
|
||||
ensure
|
||||
@stack_level -= 1
|
||||
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 if @this_stack_used
|
||||
@this_stack_used = old_stack_used
|
||||
end
|
||||
|
||||
# Creates a new context inheriting resource limits, filters, environment etc.,
|
||||
# but with an isolated scope.
|
||||
def new_isolated_subcontext
|
||||
check_overflow
|
||||
|
||||
Context.build(
|
||||
resource_limits: resource_limits,
|
||||
static_environments: static_environments,
|
||||
static_registers: static_registers
|
||||
).tap do |subcontext|
|
||||
subcontext.base_scope_depth = base_scope_depth + 1
|
||||
subcontext.exception_renderer = exception_renderer
|
||||
subcontext.filters = @filters
|
||||
subcontext.strainer = nil
|
||||
subcontext.errors = errors
|
||||
subcontext.warnings = warnings
|
||||
end
|
||||
end
|
||||
|
||||
def clear_instance_assigns
|
||||
@scopes[0] = {}
|
||||
end
|
||||
|
||||
# Only allow String, Numeric, Hash, Array, Proc, Boolean or <tt>Liquid::Drop</tt>
|
||||
def []=(key, value)
|
||||
(@scope[key] ||= [nil]) << value
|
||||
unless @this_stack_used
|
||||
@this_stack_used = true
|
||||
push({})
|
||||
end
|
||||
@scopes[0][key] = value
|
||||
end
|
||||
|
||||
# Look up variable, either resolve directly after considering the name. We can directly handle
|
||||
@@ -127,29 +181,6 @@ module Liquid
|
||||
evaluate(Expression.parse(expression))
|
||||
end
|
||||
|
||||
def unset(key)
|
||||
if @scope[key].size <= 1
|
||||
@scope.delete(key)
|
||||
else
|
||||
@scope[key].pop
|
||||
end
|
||||
end
|
||||
|
||||
def set_root(key, val)
|
||||
@scope[key] ||= []
|
||||
@scope[key][0] = val
|
||||
end
|
||||
|
||||
def set_level(key, val, int)
|
||||
@scope[key] ||= []
|
||||
@scope[key][int] = val
|
||||
end
|
||||
|
||||
def create_level(key)
|
||||
(@scope[key] ||= [nil]) << nil
|
||||
@scope[key].size - 1
|
||||
end
|
||||
|
||||
def key?(key)
|
||||
self[key] != nil
|
||||
end
|
||||
@@ -160,40 +191,25 @@ module Liquid
|
||||
|
||||
# Fetches an object starting at the local scope and then moving up the hierachy
|
||||
def find_variable(key, raise_on_not_found: true)
|
||||
trigger = false
|
||||
value = @scope[key]
|
||||
scope = @scope unless value.nil?
|
||||
trigger = true unless value.nil?
|
||||
# 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
|
||||
scope = (index = @scopes.find_index { |s| s.key?(key) }) && @scopes[index]
|
||||
scope ||= (index = @environments.find_index { |s| s.key?(key) || s.default_proc }) && @environments[index]
|
||||
scope ||= (index = @static_environments.find_index { |s| s.key?(key) }) && @static_environments[index]
|
||||
scope ||= BLANK_SCOPE
|
||||
|
||||
if scope.nil?
|
||||
index = @environments.find_index 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.
|
||||
!variable.nil? || @strict_variables && raise_on_not_found
|
||||
end
|
||||
|
||||
scope = @environments[index || -1]
|
||||
end
|
||||
|
||||
variable ||= lookup_and_evaluate(scope, key, trigger, raise_on_not_found: raise_on_not_found)
|
||||
|
||||
variable = variable.to_liquid
|
||||
variable = lookup_and_evaluate(scope, key, raise_on_not_found: raise_on_not_found).to_liquid
|
||||
variable.context = self if variable.respond_to?(:context=)
|
||||
|
||||
variable
|
||||
end
|
||||
|
||||
def lookup_and_evaluate(obj, key, trigger = false, raise_on_not_found: true)
|
||||
def lookup_and_evaluate(obj, key, raise_on_not_found: true)
|
||||
if @strict_variables && raise_on_not_found && obj.respond_to?(:key?) && !obj.key?(key)
|
||||
raise Liquid::UndefinedVariable, "undefined variable #{key}"
|
||||
end
|
||||
|
||||
value = if trigger == true
|
||||
obj[key][-1]
|
||||
else
|
||||
obj[key]
|
||||
end
|
||||
value = obj[key]
|
||||
|
||||
if value.is_a?(Proc) && obj.respond_to?(:[]=)
|
||||
obj[key] = (value.arity == 0) ? value.call : value.call(self)
|
||||
@@ -202,8 +218,22 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_writer :base_scope_depth, :warnings, :errors, :strainer, :filters
|
||||
|
||||
private
|
||||
|
||||
attr_reader :base_scope_depth
|
||||
|
||||
def check_overflow
|
||||
raise StackLevelError, "Nesting too deep".freeze if overflow?
|
||||
end
|
||||
|
||||
def overflow?
|
||||
base_scope_depth + @scopes.length > Block::MAX_DEPTH
|
||||
end
|
||||
|
||||
def internal_error
|
||||
# raise and catch to set backtrace and cause on exception
|
||||
raise Liquid::InternalError, 'internal'
|
||||
@@ -212,10 +242,10 @@ module Liquid
|
||||
end
|
||||
|
||||
def squash_instance_assigns_with_environments
|
||||
@scope.each_key do |k|
|
||||
@scopes.last.each_key do |k|
|
||||
@environments.each do |env|
|
||||
if env.key?(k)
|
||||
@scope[k] = [lookup_and_evaluate(env, k)]
|
||||
scopes.last[k] = lookup_and_evaluate(env, k)
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
@@ -22,5 +22,6 @@
|
||||
tag_never_closed: "'%{block_name}' tag was never closed"
|
||||
meta_syntax_error: "Liquid syntax error: #{e.message}"
|
||||
table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
|
||||
render: "Syntax error in tag 'render' - Template name must be a quoted string"
|
||||
argument:
|
||||
include: "Argument error in tag 'include' - Illegal template name"
|
||||
|
||||
@@ -44,11 +44,14 @@ module Liquid
|
||||
tok[0] == type
|
||||
end
|
||||
|
||||
SINGLE_TOKEN_EXPRESSION_TYPES = [:string, :number].freeze
|
||||
private_constant :SINGLE_TOKEN_EXPRESSION_TYPES
|
||||
|
||||
def expression
|
||||
token = @tokens[@p]
|
||||
if token[0] == :id
|
||||
variable_signature
|
||||
elsif [:string, :number].include? token[0]
|
||||
elsif SINGLE_TOKEN_EXPRESSION_TYPES.include? token[0]
|
||||
consume
|
||||
elsif token.first == :open_round
|
||||
consume
|
||||
|
||||
18
lib/liquid/partial_cache.rb
Normal file
18
lib/liquid/partial_cache.rb
Normal file
@@ -0,0 +1,18 @@
|
||||
module Liquid
|
||||
class PartialCache
|
||||
def self.load(template_name, context:, parse_context:)
|
||||
cached_partials = (context.registers[:cached_partials] ||= {})
|
||||
cached = cached_partials[template_name]
|
||||
return cached if cached
|
||||
|
||||
file_system = (context.registers[:file_system] ||= Liquid::Template.file_system)
|
||||
source = file_system.read_template_file(template_name)
|
||||
parse_context.partial = true
|
||||
|
||||
partial = Liquid::Template.parse(source, parse_context)
|
||||
cached_partials[template_name] = partial
|
||||
ensure
|
||||
parse_context.partial = false
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,23 +1,23 @@
|
||||
module Liquid
|
||||
class BlockBody
|
||||
def render_node_with_profiling(node, output, context, skip_output = false)
|
||||
def render_node_with_profiling(context, output, node)
|
||||
Profiler.profile_node_render(node) do
|
||||
render_node_without_profiling(node, output, context, skip_output)
|
||||
render_node_without_profiling(context, output, node)
|
||||
end
|
||||
end
|
||||
|
||||
alias_method :render_node_without_profiling, :render_node_to_output
|
||||
alias_method :render_node_to_output, :render_node_with_profiling
|
||||
alias_method :render_node_without_profiling, :render_node
|
||||
alias_method :render_node, :render_node_with_profiling
|
||||
end
|
||||
|
||||
class Include < Tag
|
||||
def render_with_profiling(context)
|
||||
def render_to_output_buffer_with_profiling(context, output)
|
||||
Profiler.profile_children(context.evaluate(@template_name_expr).to_s) do
|
||||
render_without_profiling(context)
|
||||
render_to_output_buffer_without_profiling(context, output)
|
||||
end
|
||||
end
|
||||
|
||||
alias_method :render_without_profiling, :render
|
||||
alias_method :render, :render_with_profiling
|
||||
alias_method :render_to_output_buffer_without_profiling, :render_to_output_buffer
|
||||
alias_method :render_to_output_buffer, :render_to_output_buffer_with_profiling
|
||||
end
|
||||
end
|
||||
|
||||
@@ -79,7 +79,7 @@ module Liquid
|
||||
truncate_string_str = truncate_string.to_s
|
||||
l = length - truncate_string_str.length
|
||||
l = 0 if l < 0
|
||||
input_str.length > length ? input_str[0...l] + truncate_string_str : input_str
|
||||
input_str.length > length ? input_str[0...l].concat(truncate_string_str) : input_str
|
||||
end
|
||||
|
||||
def truncatewords(input, words = 15, truncate_string = "...".freeze)
|
||||
@@ -88,7 +88,7 @@ module Liquid
|
||||
words = Utils.to_integer(words)
|
||||
l = words - 1
|
||||
l = 0 if l < 0
|
||||
wordlist.length > l ? wordlist[0..l].join(" ".freeze) + truncate_string.to_s : input
|
||||
wordlist.length > l ? wordlist[0..l].join(" ".freeze).concat(truncate_string.to_s) : input
|
||||
end
|
||||
|
||||
# Split input string into an array of substrings separated by given pattern.
|
||||
|
||||
@@ -5,8 +5,8 @@ module Liquid
|
||||
include ParserSwitching
|
||||
|
||||
class << self
|
||||
def parse(tag_name, markup, tokenizer, options)
|
||||
tag = new(tag_name, markup, options)
|
||||
def parse(tag_name, markup, tokenizer, parse_context)
|
||||
tag = new(tag_name, markup, parse_context)
|
||||
tag.parse(tokenizer)
|
||||
tag
|
||||
end
|
||||
@@ -36,6 +36,14 @@ module Liquid
|
||||
''.freeze
|
||||
end
|
||||
|
||||
# For backwards compatibility with custom tags. In a future release, the semantics
|
||||
# of the `render_to_output_buffer` method will become the default and the `render`
|
||||
# method will be removed.
|
||||
def render_to_output_buffer(context, output)
|
||||
output << render(context)
|
||||
output
|
||||
end
|
||||
|
||||
def blank?
|
||||
false
|
||||
end
|
||||
|
||||
@@ -10,6 +10,10 @@ 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)
|
||||
@@ -18,15 +22,15 @@ module Liquid
|
||||
@to = $1
|
||||
@from = Variable.new($2, options)
|
||||
else
|
||||
raise SyntaxError.new options[:locale].t("errors.syntax.assign".freeze)
|
||||
raise SyntaxError.new(options[:locale].t(self.class.syntax_error_translation_key))
|
||||
end
|
||||
end
|
||||
|
||||
def render(context)
|
||||
def render_to_output_buffer(context, output)
|
||||
val = @from.render(context)
|
||||
context.set_root(@to, val)
|
||||
context.scopes.last[@to] = val
|
||||
context.resource_limits.assign_score += assign_score_of(val)
|
||||
''.freeze
|
||||
output
|
||||
end
|
||||
|
||||
def blank?
|
||||
|
||||
@@ -22,11 +22,12 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
def render(context)
|
||||
output = super
|
||||
context.set_root(@to, output)
|
||||
context.resource_limits.assign_score += output.bytesize
|
||||
''.freeze
|
||||
def render_to_output_buffer(context, output)
|
||||
previous_output_size = output.bytesize
|
||||
super
|
||||
context.scopes.last[@to] = output
|
||||
context.resource_limits.assign_score += (output.bytesize - previous_output_size)
|
||||
output
|
||||
end
|
||||
|
||||
def blank?
|
||||
|
||||
@@ -38,16 +38,17 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
def render(context)
|
||||
execute_else_block = true
|
||||
def render_to_output_buffer(context, output)
|
||||
context.stack do
|
||||
execute_else_block = true
|
||||
|
||||
output = ''
|
||||
@blocks.each do |block|
|
||||
if block.else?
|
||||
return block.attachment.render(context) if execute_else_block
|
||||
elsif block.evaluate(context)
|
||||
execute_else_block = false
|
||||
output << block.attachment.render(context)
|
||||
@blocks.each do |block|
|
||||
if block.else?
|
||||
block.attachment.render_to_output_buffer(context, output) if execute_else_block
|
||||
elsif block.evaluate(context)
|
||||
execute_else_block = false
|
||||
block.attachment.render_to_output_buffer(context, output)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
module Liquid
|
||||
class Comment < Block
|
||||
def render(_context)
|
||||
''.freeze
|
||||
def render_to_output_buffer(_context, output)
|
||||
output
|
||||
end
|
||||
|
||||
def unknown_tag(_tag, _markup, _tokens)
|
||||
|
||||
@@ -31,16 +31,29 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
def render(context)
|
||||
def render_to_output_buffer(context, output)
|
||||
context.registers[:cycle] ||= {}
|
||||
|
||||
key = context.evaluate(@name)
|
||||
iteration = context.registers[:cycle][key].to_i
|
||||
result = context.evaluate(@variables[iteration])
|
||||
iteration += 1
|
||||
iteration = 0 if iteration >= @variables.size
|
||||
context.registers[:cycle][key] = iteration
|
||||
result
|
||||
context.stack do
|
||||
key = context.evaluate(@name)
|
||||
iteration = context.registers[:cycle][key].to_i
|
||||
|
||||
val = context.evaluate(@variables[iteration])
|
||||
|
||||
if val.is_a?(Array)
|
||||
val = val.join
|
||||
elsif !val.is_a?(String)
|
||||
val = val.to_s
|
||||
end
|
||||
|
||||
output << val
|
||||
|
||||
iteration += 1
|
||||
iteration = 0 if iteration >= @variables.size
|
||||
context.registers[:cycle][key] = iteration
|
||||
end
|
||||
|
||||
output
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -23,11 +23,12 @@ module Liquid
|
||||
@variable = markup.strip
|
||||
end
|
||||
|
||||
def render(context)
|
||||
def render_to_output_buffer(context, output)
|
||||
value = context.environments.first[@variable] ||= 0
|
||||
value -= 1
|
||||
context.environments.first[@variable] = value
|
||||
value.to_s
|
||||
output << value.to_s
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
24
lib/liquid/tags/echo.rb
Normal file
24
lib/liquid/tags/echo.rb
Normal file
@@ -0,0 +1,24 @@
|
||||
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_to_output_buffer(context, '')
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('echo'.freeze, Echo)
|
||||
end
|
||||
@@ -70,14 +70,16 @@ module Liquid
|
||||
@else_block = BlockBody.new
|
||||
end
|
||||
|
||||
def render(context)
|
||||
def render_to_output_buffer(context, output)
|
||||
segment = collection_segment(context)
|
||||
|
||||
if segment.empty?
|
||||
render_else(context)
|
||||
render_else(context, output)
|
||||
else
|
||||
render_segment(context, segment)
|
||||
render_segment(context, output, segment)
|
||||
end
|
||||
|
||||
output
|
||||
end
|
||||
|
||||
protected
|
||||
@@ -150,25 +152,23 @@ module Liquid
|
||||
segment
|
||||
end
|
||||
|
||||
def render_segment(context, segment)
|
||||
def render_segment(context, output, segment)
|
||||
for_stack = context.registers[:for_stack] ||= []
|
||||
length = segment.length
|
||||
|
||||
result = ''
|
||||
|
||||
context.stack('forloop', @variable_name) do
|
||||
context.stack do
|
||||
loop_vars = Liquid::ForloopDrop.new(@name, length, for_stack[-1])
|
||||
|
||||
for_stack.push(loop_vars)
|
||||
|
||||
begin
|
||||
context['forloop'.freeze] = loop_vars
|
||||
level = context.create_level(@variable_name)
|
||||
|
||||
segment.each do |item|
|
||||
context.set_level(@variable_name, item, level)
|
||||
result << @for_block.render(context)
|
||||
context[@variable_name] = item
|
||||
@for_block.render_to_output_buffer(context, output)
|
||||
loop_vars.send(:increment!)
|
||||
|
||||
# Handle any interrupts if they exist.
|
||||
if context.interrupt?
|
||||
interrupt = context.pop_interrupt
|
||||
@@ -176,15 +176,12 @@ module Liquid
|
||||
next if interrupt.is_a? ContinueInterrupt
|
||||
end
|
||||
end
|
||||
context.unset(@variable_name)
|
||||
context.unset('forloop'.freeze)
|
||||
ensure
|
||||
|
||||
for_stack.pop
|
||||
end
|
||||
end
|
||||
|
||||
result
|
||||
output
|
||||
end
|
||||
|
||||
def set_attribute(key, expr)
|
||||
@@ -200,8 +197,12 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
def render_else(context)
|
||||
@else_block ? @else_block.render(context) : ''.freeze
|
||||
def render_else(context, output)
|
||||
if @else_block
|
||||
@else_block.render_to_output_buffer(context, output)
|
||||
else
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
|
||||
@@ -39,13 +39,16 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
def render(context)
|
||||
@blocks.each do |block|
|
||||
if block.evaluate(context)
|
||||
return block.attachment.render(context)
|
||||
def render_to_output_buffer(context, output)
|
||||
context.stack do
|
||||
@blocks.each do |block|
|
||||
if block.evaluate(context)
|
||||
return block.attachment.render_to_output_buffer(context, output)
|
||||
end
|
||||
end
|
||||
end
|
||||
''.freeze
|
||||
|
||||
output
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
module Liquid
|
||||
class Ifchanged < Block
|
||||
def render(context)
|
||||
output = super
|
||||
def render_to_output_buffer(context, output)
|
||||
context.stack do
|
||||
block_output = ''
|
||||
super(context, block_output)
|
||||
|
||||
if output != context.registers[:ifchanged]
|
||||
context.registers[:ifchanged] = output
|
||||
output
|
||||
else
|
||||
''.freeze
|
||||
if block_output != context.registers[:ifchanged]
|
||||
context.registers[:ifchanged] = block_output
|
||||
output << block_output
|
||||
end
|
||||
end
|
||||
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -42,11 +42,16 @@ module Liquid
|
||||
def parse(_tokens)
|
||||
end
|
||||
|
||||
def render(context)
|
||||
def render_to_output_buffer(context, output)
|
||||
template_name = context.evaluate(@template_name_expr)
|
||||
raise ArgumentError.new(options[:locale].t("errors.argument.include")) unless template_name
|
||||
|
||||
partial = load_cached_partial(template_name, context)
|
||||
partial = PartialCache.load(
|
||||
template_name,
|
||||
context: context,
|
||||
parse_context: parse_context
|
||||
)
|
||||
|
||||
context_variable_name = template_name.split('/'.freeze).last
|
||||
|
||||
variable = if @variable_name_expr
|
||||
@@ -60,56 +65,32 @@ module Liquid
|
||||
begin
|
||||
context.template_name = template_name
|
||||
context.partial = true
|
||||
context.stack(context_variable_name, *@attributes.keys) do
|
||||
context.stack do
|
||||
@attributes.each do |key, value|
|
||||
context[key] = context.evaluate(value)
|
||||
end
|
||||
|
||||
if variable.is_a?(Array)
|
||||
variable.collect do |var|
|
||||
variable.each do |var|
|
||||
context[context_variable_name] = var
|
||||
partial.render(context)
|
||||
partial.render_to_output_buffer(context, output)
|
||||
end
|
||||
else
|
||||
context[context_variable_name] = variable
|
||||
partial.render(context)
|
||||
partial.render_to_output_buffer(context, output)
|
||||
end
|
||||
end
|
||||
ensure
|
||||
context.template_name = old_template_name
|
||||
context.partial = old_partial
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
output
|
||||
end
|
||||
|
||||
alias_method :parse_context, :options
|
||||
private :parse_context
|
||||
|
||||
def load_cached_partial(template_name, context)
|
||||
cached_partials = context.registers[:cached_partials] || {}
|
||||
|
||||
if cached = cached_partials[template_name]
|
||||
return cached
|
||||
end
|
||||
source = read_template_from_file_system(context)
|
||||
begin
|
||||
parse_context.partial = true
|
||||
partial = Liquid::Template.parse(source, parse_context)
|
||||
ensure
|
||||
parse_context.partial = false
|
||||
end
|
||||
cached_partials[template_name] = partial
|
||||
context.registers[:cached_partials] = cached_partials
|
||||
partial
|
||||
end
|
||||
|
||||
def read_template_from_file_system(context)
|
||||
file_system = context.registers[:file_system] || Liquid::Template.file_system
|
||||
|
||||
file_system.read_template_file(context.evaluate(@template_name_expr))
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
[
|
||||
|
||||
@@ -20,10 +20,12 @@ module Liquid
|
||||
@variable = markup.strip
|
||||
end
|
||||
|
||||
def render(context)
|
||||
def render_to_output_buffer(context, output)
|
||||
value = context.environments.first[@variable] ||= 0
|
||||
context.environments.first[@variable] = value + 1
|
||||
value.to_s
|
||||
|
||||
output << value.to_s
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -22,8 +22,9 @@ module Liquid
|
||||
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
|
||||
end
|
||||
|
||||
def render(_context)
|
||||
@body
|
||||
def render_to_output_buffer(_context, output)
|
||||
output << @body
|
||||
output
|
||||
end
|
||||
|
||||
def nodelist
|
||||
|
||||
54
lib/liquid/tags/render.rb
Normal file
54
lib/liquid/tags/render.rb
Normal file
@@ -0,0 +1,54 @@
|
||||
module Liquid
|
||||
class Render < Tag
|
||||
Syntax = /(#{QuotedString})#{QuotedFragment}*/o
|
||||
|
||||
attr_reader :template_name_expr, :attributes
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
|
||||
raise SyntaxError.new(options[:locale].t("errors.syntax.render".freeze)) unless markup =~ Syntax
|
||||
|
||||
template_name = $1
|
||||
|
||||
@template_name_expr = Expression.parse(template_name)
|
||||
|
||||
@attributes = {}
|
||||
markup.scan(TagAttributes) do |key, value|
|
||||
@attributes[key] = Expression.parse(value)
|
||||
end
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
# Though we evaluate this here we will only ever parse it as a string literal.
|
||||
template_name = context.evaluate(@template_name_expr)
|
||||
raise ArgumentError.new(options[:locale].t("errors.argument.include")) unless template_name
|
||||
|
||||
partial = PartialCache.load(
|
||||
template_name,
|
||||
context: context,
|
||||
parse_context: parse_context
|
||||
)
|
||||
|
||||
inner_context = context.new_isolated_subcontext
|
||||
inner_context.template_name = template_name
|
||||
inner_context.partial = true
|
||||
@attributes.each do |key, value|
|
||||
inner_context[key] = context.evaluate(value)
|
||||
end
|
||||
partial.render_to_output_buffer(inner_context, output)
|
||||
|
||||
output
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
def children
|
||||
[
|
||||
@node.template_name_expr,
|
||||
] + @node.attributes.values
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('render'.freeze, Render)
|
||||
end
|
||||
@@ -18,7 +18,7 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
def render(context)
|
||||
def render_to_output_buffer(context, output)
|
||||
collection = context.evaluate(@collection_name) or return ''.freeze
|
||||
|
||||
from = @attributes.key?('offset'.freeze) ? context.evaluate(@attributes['offset'.freeze]).to_i : 0
|
||||
@@ -30,25 +30,28 @@ module Liquid
|
||||
|
||||
cols = context.evaluate(@attributes['cols'.freeze]).to_i
|
||||
|
||||
result = "<tr class=\"row1\">\n"
|
||||
context.stack('tablerowloop', @variable_name) do
|
||||
output << "<tr class=\"row1\">\n"
|
||||
context.stack do
|
||||
tablerowloop = Liquid::TablerowloopDrop.new(length, cols)
|
||||
context['tablerowloop'.freeze] = tablerowloop
|
||||
|
||||
collection.each do |item|
|
||||
context[@variable_name] = item
|
||||
|
||||
result << "<td class=\"col#{tablerowloop.col}\">" << super << '</td>'
|
||||
output << "<td class=\"col#{tablerowloop.col}\">"
|
||||
super
|
||||
output << '</td>'
|
||||
|
||||
if tablerowloop.col_last && !tablerowloop.last
|
||||
result << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">"
|
||||
output << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">"
|
||||
end
|
||||
|
||||
tablerowloop.send(:increment!)
|
||||
end
|
||||
end
|
||||
result << "</tr>\n"
|
||||
result
|
||||
|
||||
output << "</tr>\n"
|
||||
output
|
||||
end
|
||||
|
||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
||||
|
||||
@@ -6,21 +6,23 @@ module Liquid
|
||||
# {% unless x < 0 %} x is greater than zero {% endunless %}
|
||||
#
|
||||
class Unless < If
|
||||
def render(context)
|
||||
# First condition is interpreted backwards ( if not )
|
||||
first_block = @blocks.first
|
||||
unless first_block.evaluate(context)
|
||||
return first_block.attachment.render(context)
|
||||
end
|
||||
def render_to_output_buffer(context, output)
|
||||
context.stack do
|
||||
# First condition is interpreted backwards ( if not )
|
||||
first_block = @blocks.first
|
||||
unless first_block.evaluate(context)
|
||||
return first_block.attachment.render_to_output_buffer(context, output)
|
||||
end
|
||||
|
||||
# After the first condition unless works just like if
|
||||
@blocks[1..-1].each do |block|
|
||||
if block.evaluate(context)
|
||||
return block.attachment.render(context)
|
||||
# After the first condition unless works just like if
|
||||
@blocks[1..-1].each do |block|
|
||||
if block.evaluate(context)
|
||||
return block.attachment.render_to_output_buffer(context, output)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
''.freeze
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ module Liquid
|
||||
private
|
||||
|
||||
def lookup_class(name)
|
||||
name.split("::").reject(&:empty?).reduce(Object) { |scope, const| scope.const_get(const) }
|
||||
Object.const_get(name)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -187,9 +187,12 @@ module Liquid
|
||||
raise ArgumentError, "Expected Hash or Liquid::Context as parameter"
|
||||
end
|
||||
|
||||
output = nil
|
||||
|
||||
case args.last
|
||||
when Hash
|
||||
options = args.pop
|
||||
output = options[:output] if options[:output]
|
||||
|
||||
registers.merge!(options[:registers]) if options[:registers].is_a?(Hash)
|
||||
|
||||
@@ -204,10 +207,9 @@ module Liquid
|
||||
begin
|
||||
# render the nodelist.
|
||||
# for performance reasons we get an array back here. join will make a string out of it.
|
||||
result = with_profiling(context) do
|
||||
@root.render(context)
|
||||
with_profiling(context) do
|
||||
@root.render_to_output_buffer(context, output || '')
|
||||
end
|
||||
result.respond_to?(:join) ? result.join : result
|
||||
rescue Liquid::MemoryError => e
|
||||
context.handle_error(e)
|
||||
ensure
|
||||
@@ -220,6 +222,10 @@ module Liquid
|
||||
render(*args)
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
render(context, output: output)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def tokenize(source)
|
||||
|
||||
@@ -1,25 +1,31 @@
|
||||
module Liquid
|
||||
class Tokenizer
|
||||
attr_reader :line_number
|
||||
attr_reader :line_number, :for_liquid_tag
|
||||
|
||||
def initialize(source, line_numbers = false)
|
||||
def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false)
|
||||
@source = source
|
||||
@line_number = line_numbers ? 1 : nil
|
||||
@line_number = line_number || (line_numbers ? 1 : nil)
|
||||
@for_liquid_tag = for_liquid_tag
|
||||
@tokens = tokenize
|
||||
end
|
||||
|
||||
def shift
|
||||
token = @tokens.shift
|
||||
@line_number += token.count("\n") if @line_number && token
|
||||
token = @tokens.shift or return
|
||||
|
||||
if @line_number
|
||||
@line_number += @for_liquid_tag ? 1 : token.count("\n")
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
@@ -85,12 +85,23 @@ module Liquid
|
||||
end
|
||||
|
||||
obj = context.apply_global_filter(obj)
|
||||
|
||||
taint_check(context, obj)
|
||||
|
||||
obj
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
obj = render(context)
|
||||
|
||||
if obj.is_a?(Array)
|
||||
output << obj.join
|
||||
elsif obj.nil?
|
||||
else
|
||||
output << obj.to_s
|
||||
end
|
||||
|
||||
output
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_filter_expressions(filter_name, unparsed_args)
|
||||
|
||||
@@ -16,7 +16,7 @@ Gem::Specification.new do |s|
|
||||
s.license = "MIT"
|
||||
# s.description = "A secure, non-evaling end user template engine with aesthetic markup."
|
||||
|
||||
s.required_ruby_version = ">= 2.1.0"
|
||||
s.required_ruby_version = ">= 2.4.0"
|
||||
s.required_rubygems_version = ">= 1.3.7"
|
||||
|
||||
s.test_files = Dir.glob("{test}/**/*")
|
||||
|
||||
@@ -2,25 +2,62 @@
|
||||
|
||||
require 'benchmark/ips'
|
||||
require 'memory_profiler'
|
||||
require 'terminal-table'
|
||||
require_relative 'theme_runner'
|
||||
|
||||
def profile(phase, &block)
|
||||
puts
|
||||
puts "#{phase}:"
|
||||
puts
|
||||
class Profiler
|
||||
LOG_LABEL = "Profiling: ".rjust(14).freeze
|
||||
REPORTS_DIR = File.expand_path('.memprof', __dir__).freeze
|
||||
|
||||
report = MemoryProfiler.report(&block)
|
||||
def self.run
|
||||
puts
|
||||
yield new
|
||||
end
|
||||
|
||||
report.pretty_print(
|
||||
color_output: true,
|
||||
scale_bytes: true,
|
||||
detailed_report: true
|
||||
)
|
||||
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
|
||||
end
|
||||
|
||||
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
|
||||
|
||||
profiler = ThemeRunner.new
|
||||
|
||||
profile("Parsing") { profiler.compile }
|
||||
profile("Rendering") { profiler.render }
|
||||
runner = ThemeRunner.new
|
||||
Profiler.run do |x|
|
||||
x.profile('parse') { runner.compile }
|
||||
x.profile('render') { runner.render }
|
||||
x.tabulate
|
||||
end
|
||||
|
||||
@@ -12,10 +12,10 @@ class CommentForm < Liquid::Block
|
||||
end
|
||||
end
|
||||
|
||||
def render(context)
|
||||
def render_to_output_buffer(context, output)
|
||||
article = context[@variable_name]
|
||||
|
||||
context.stack('form') do
|
||||
context.stack do
|
||||
context['form'] = {
|
||||
'posted_successfully?' => context.registers[:posted_successfully],
|
||||
'errors' => context['comment.errors'],
|
||||
@@ -23,7 +23,9 @@ class CommentForm < Liquid::Block
|
||||
'email' => context['comment.email'],
|
||||
'body' => context['comment.body']
|
||||
}
|
||||
wrap_in_form(article, render_all(@nodelist, context))
|
||||
|
||||
output << wrap_in_form(article, render_all(@nodelist, context, output))
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -21,10 +21,10 @@ class Paginate < Liquid::Block
|
||||
end
|
||||
end
|
||||
|
||||
def render(context)
|
||||
def render_to_output_buffer(context, output)
|
||||
@context = context
|
||||
|
||||
context.stack('paginate') do
|
||||
context.stack do
|
||||
current_page = context['current_page'].to_i
|
||||
|
||||
pagination = {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
require 'test_helper'
|
||||
|
||||
class FoobarTag < Liquid::Tag
|
||||
def render(*args)
|
||||
" "
|
||||
def render_to_output_buffer(context, output)
|
||||
output << ' '
|
||||
output
|
||||
end
|
||||
|
||||
Liquid::Template.register_tag('foobar', FoobarTag)
|
||||
end
|
||||
|
||||
class BlankTestFileSystem
|
||||
@@ -31,7 +30,9 @@ class BlankTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_new_tags_are_not_blank_by_default
|
||||
assert_template_result(" " * N, wrap_in_for("{% foobar %}"))
|
||||
with_custom_tag('foobar', FoobarTag) do
|
||||
assert_template_result(" " * N, wrap_in_for("{% foobar %}"))
|
||||
end
|
||||
end
|
||||
|
||||
def test_loops_are_blank
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
require 'test_helper'
|
||||
|
||||
class ContextDrop < Liquid::Drop
|
||||
def scopes
|
||||
@context.scopes.size
|
||||
end
|
||||
|
||||
def scopes_as_array
|
||||
(1..@context.scopes.size).to_a
|
||||
end
|
||||
|
||||
def loop_pos
|
||||
@context['forloop.index']
|
||||
end
|
||||
@@ -186,6 +194,31 @@ class DropsTest < Minitest::Test
|
||||
end
|
||||
end
|
||||
|
||||
def test_scope
|
||||
assert_equal '1', Liquid::Template.parse('{{ context.scopes }}').render!('context' => ContextDrop.new)
|
||||
assert_equal '2', Liquid::Template.parse('{%for i in dummy%}{{ context.scopes }}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1])
|
||||
assert_equal '3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ context.scopes }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1])
|
||||
end
|
||||
|
||||
def test_scope_though_proc
|
||||
assert_equal '1', Liquid::Template.parse('{{ s }}').render!('context' => ContextDrop.new, 's' => proc{ |c| c['context.scopes'] })
|
||||
assert_equal '2', Liquid::Template.parse('{%for i in dummy%}{{ s }}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc{ |c| c['context.scopes'] }, 'dummy' => [1])
|
||||
assert_equal '3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ s }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc{ |c| c['context.scopes'] }, 'dummy' => [1])
|
||||
end
|
||||
|
||||
def test_scope_with_assigns
|
||||
assert_equal 'variable', Liquid::Template.parse('{% assign a = "variable"%}{{a}}').render!('context' => ContextDrop.new)
|
||||
assert_equal 'variable', Liquid::Template.parse('{% assign a = "variable"%}{%for i in dummy%}{{a}}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1])
|
||||
assert_equal 'test', Liquid::Template.parse('{% assign header_gif = "test"%}{{header_gif}}').render!('context' => ContextDrop.new)
|
||||
assert_equal 'test', Liquid::Template.parse("{% assign header_gif = 'test'%}{{header_gif}}").render!('context' => ContextDrop.new)
|
||||
end
|
||||
|
||||
def test_scope_from_tags
|
||||
assert_equal '1', Liquid::Template.parse('{% for i in context.scopes_as_array %}{{i}}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1])
|
||||
assert_equal '12', Liquid::Template.parse('{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1])
|
||||
assert_equal '123', Liquid::Template.parse('{%for a in dummy%}{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1])
|
||||
end
|
||||
|
||||
def test_access_context_from_drop
|
||||
assert_equal '123', Liquid::Template.parse('{%for a in dummy%}{{ context.loop_pos }}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1, 2, 3])
|
||||
end
|
||||
|
||||
@@ -405,11 +405,11 @@ class StandardFiltersTest < Minitest::Test
|
||||
def test_map_over_drops_returning_procs
|
||||
drops = [
|
||||
{
|
||||
"proc" => ->{ "foo" }
|
||||
"proc" => ->{ "foo" },
|
||||
},
|
||||
{
|
||||
"proc" => ->{ "bar" }
|
||||
}
|
||||
"proc" => ->{ "bar" },
|
||||
},
|
||||
]
|
||||
templ = '{{ drops | map: "proc" }}'
|
||||
assert_template_result "foobar", templ, "drops" => drops
|
||||
|
||||
11
test/integration/tags/echo_test.rb
Normal file
11
test/integration/tags/echo_test.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
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
|
||||
@@ -368,23 +368,6 @@ HERE
|
||||
assert_template_result(expected, template, assigns)
|
||||
end
|
||||
|
||||
def test_overwriting_internal_variable
|
||||
template = <<-HEREDOC
|
||||
{% assign forloop = 'first' %}
|
||||
|
||||
{% for item in items %}
|
||||
{{ forloop }}
|
||||
{% assign forloop = 'second' %}
|
||||
{{ forloop }}
|
||||
{% endfor %}
|
||||
|
||||
{{ forloop }}
|
||||
HEREDOC
|
||||
|
||||
result = Liquid::Template.parse(template).render('items' => '1')
|
||||
assert_equal 'Liquid::ForloopDrop Liquid::ForloopDrop second', result.split.map(&:strip).join(' ')
|
||||
end
|
||||
|
||||
class LoaderDrop < Liquid::Drop
|
||||
attr_accessor :each_called, :load_slice_called
|
||||
|
||||
|
||||
@@ -176,7 +176,7 @@ class IfElseTagTest < Minitest::Test
|
||||
[false, true, true] => true,
|
||||
[false, true, false] => false,
|
||||
[false, false, true] => false,
|
||||
[false, false, false] => false
|
||||
[false, false, false] => false,
|
||||
}
|
||||
|
||||
tests.each do |vals, expected|
|
||||
|
||||
@@ -66,8 +66,9 @@ class CustomInclude < Liquid::Tag
|
||||
def parse(tokens)
|
||||
end
|
||||
|
||||
def render(context)
|
||||
@template_name[1..-2]
|
||||
def render_to_output_buffer(context, output)
|
||||
output << @template_name[1..-2]
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
104
test/integration/tags/liquid_tag_test.rb
Normal file
104
test/integration/tags/liquid_tag_test.rb
Normal file
@@ -0,0 +1,104 @@
|
||||
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
|
||||
149
test/integration/tags/render_tag_test.rb
Normal file
149
test/integration/tags/render_tag_test.rb
Normal file
@@ -0,0 +1,149 @@
|
||||
require 'test_helper'
|
||||
|
||||
class RenderTagTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_render_with_no_arguments
|
||||
Liquid::Template.file_system = StubFileSystem.new('source' => 'rendered content')
|
||||
assert_template_result 'rendered content', '{% render "source" %}'
|
||||
end
|
||||
|
||||
def test_render_tag_looks_for_file_system_in_registers_first
|
||||
file_system = StubFileSystem.new('pick_a_source' => 'from register file system')
|
||||
assert_equal 'from register file system',
|
||||
Template.parse('{% render "pick_a_source" %}').render!({}, registers: { file_system: file_system })
|
||||
end
|
||||
|
||||
def test_render_passes_named_arguments_into_inner_scope
|
||||
Liquid::Template.file_system = StubFileSystem.new('product' => '{{ inner_product.title }}')
|
||||
assert_template_result 'My Product', '{% render "product", inner_product: outer_product %}',
|
||||
'outer_product' => { 'title' => 'My Product' }
|
||||
end
|
||||
|
||||
def test_render_accepts_literals_as_arguments
|
||||
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ price }}')
|
||||
assert_template_result '123', '{% render "snippet", price: 123 %}'
|
||||
end
|
||||
|
||||
def test_render_accepts_multiple_named_arguments
|
||||
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ one }} {{ two }}')
|
||||
assert_template_result '1 2', '{% render "snippet", one: 1, two: 2 %}'
|
||||
end
|
||||
|
||||
def test_render_does_not_inherit_parent_scope_variables
|
||||
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ outer_variable }}')
|
||||
assert_template_result '', '{% assign outer_variable = "should not be visible" %}{% render "snippet" %}'
|
||||
end
|
||||
|
||||
def test_render_does_not_inherit_variable_with_same_name_as_snippet
|
||||
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ snippet }}')
|
||||
assert_template_result '', "{% assign snippet = 'should not be visible' %}{% render 'snippet' %}"
|
||||
end
|
||||
|
||||
def test_render_sets_the_correct_template_name_for_errors
|
||||
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ unsafe }}')
|
||||
|
||||
with_taint_mode :error do
|
||||
template = Liquid::Template.parse('{% render "snippet", unsafe: unsafe %}')
|
||||
context = Context.new('unsafe' => (+'unsafe').tap(&:taint))
|
||||
template.render(context)
|
||||
|
||||
assert_equal [Liquid::TaintedError], template.errors.map(&:class)
|
||||
assert_equal 'snippet', template.errors.first.template_name
|
||||
end
|
||||
end
|
||||
|
||||
def test_render_sets_the_correct_template_name_for_warnings
|
||||
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ unsafe }}')
|
||||
|
||||
with_taint_mode :warn do
|
||||
template = Liquid::Template.parse('{% render "snippet", unsafe: unsafe %}')
|
||||
context = Context.new('unsafe' => (+'unsafe').tap(&:taint))
|
||||
template.render(context)
|
||||
|
||||
assert_equal [Liquid::TaintedError], context.warnings.map(&:class)
|
||||
assert_equal 'snippet', context.warnings.first.template_name
|
||||
end
|
||||
end
|
||||
|
||||
def test_render_does_not_mutate_parent_scope
|
||||
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{% assign inner = 1 %}')
|
||||
assert_template_result '', "{% render 'snippet' %}{{ inner }}"
|
||||
end
|
||||
|
||||
def test_nested_render_tag
|
||||
Liquid::Template.file_system = StubFileSystem.new(
|
||||
'one' => "one {% render 'two' %}",
|
||||
'two' => 'two'
|
||||
)
|
||||
assert_template_result 'one two', "{% render 'one' %}"
|
||||
end
|
||||
|
||||
def test_recursively_rendered_template_does_not_produce_endless_loop
|
||||
Liquid::Template.file_system = StubFileSystem.new('loop' => '{% render "loop" %}')
|
||||
|
||||
assert_raises Liquid::StackLevelError do
|
||||
Template.parse('{% render "loop" %}').render!
|
||||
end
|
||||
end
|
||||
|
||||
def test_includes_and_renders_count_towards_the_same_recursion_limit
|
||||
Liquid::Template.file_system = StubFileSystem.new(
|
||||
'loop_render' => '{% render "loop_include" %}',
|
||||
'loop_include' => '{% include "loop_render" %}'
|
||||
)
|
||||
|
||||
assert_raises Liquid::StackLevelError do
|
||||
Template.parse('{% render "loop_include" %}').render!
|
||||
end
|
||||
end
|
||||
|
||||
def test_dynamically_choosen_templates_are_not_allowed
|
||||
Liquid::Template.file_system = StubFileSystem.new('snippet' => 'should not be rendered')
|
||||
|
||||
assert_raises Liquid::SyntaxError do
|
||||
Liquid::Template.parse("{% assign name = 'snippet' %}{% render name %}")
|
||||
end
|
||||
end
|
||||
|
||||
def test_include_tag_caches_second_read_of_same_partial
|
||||
file_system = StubFileSystem.new('snippet' => 'echo')
|
||||
assert_equal 'echoecho',
|
||||
Template.parse('{% render "snippet" %}{% render "snippet" %}')
|
||||
.render!({}, registers: { file_system: file_system })
|
||||
assert_equal 1, file_system.file_read_count
|
||||
end
|
||||
|
||||
def test_render_tag_doesnt_cache_partials_across_renders
|
||||
file_system = StubFileSystem.new('snippet' => 'my message')
|
||||
|
||||
assert_equal 'my message',
|
||||
Template.parse('{% include "snippet" %}').render!({}, registers: { file_system: file_system })
|
||||
assert_equal 1, file_system.file_read_count
|
||||
|
||||
assert_equal 'my message',
|
||||
Template.parse('{% include "snippet" %}').render!({}, registers: { file_system: file_system })
|
||||
assert_equal 2, file_system.file_read_count
|
||||
end
|
||||
|
||||
def test_render_tag_within_if_statement
|
||||
Liquid::Template.file_system = StubFileSystem.new('snippet' => 'my message')
|
||||
assert_template_result 'my message', '{% if true %}{% render "snippet" %}{% endif %}'
|
||||
end
|
||||
|
||||
def test_break_through_render
|
||||
Liquid::Template.file_system = StubFileSystem.new('break' => '{% break %}')
|
||||
assert_template_result '1', '{% for i in (1..3) %}{{ i }}{% break %}{{ i }}{% endfor %}'
|
||||
assert_template_result '112233', '{% for i in (1..3) %}{{ i }}{% render "break" %}{{ i }}{% endfor %}'
|
||||
end
|
||||
|
||||
def test_increment_is_isolated_between_renders
|
||||
Liquid::Template.file_system = StubFileSystem.new('incr' => '{% increment %}')
|
||||
assert_template_result '010', '{% increment %}{% increment %}{% render "incr" %}'
|
||||
end
|
||||
|
||||
def test_decrement_is_isolated_between_renders
|
||||
Liquid::Template.file_system = StubFileSystem.new('decr' => '{% decrement %}')
|
||||
assert_template_result '-1-2-1', '{% decrement %}{% decrement %}{% render "decr" %}'
|
||||
end
|
||||
end
|
||||
@@ -86,6 +86,14 @@ class VariableTest < Minitest::Test
|
||||
assert_equal "Unknown variable 'test'", e.message
|
||||
end
|
||||
|
||||
def test_environment_falsy
|
||||
template = Template.parse(%({{ test }}{% assign test = 'bar' %}{{ test }}))
|
||||
template.assigns['test'] = 'foo'
|
||||
assert_equal 'foobar', template.render!
|
||||
assert_equal 'bar', template.render!('test' => nil)
|
||||
assert_equal 'falsebar', template.render!('test' => false)
|
||||
end
|
||||
|
||||
def test_multiline_variable
|
||||
assert_equal 'worked', Template.parse("{{\ntest\n}}").render!('test' => 'worked')
|
||||
end
|
||||
|
||||
@@ -14,7 +14,7 @@ if env_mode = ENV['LIQUID_PARSER_MODE']
|
||||
end
|
||||
Liquid::Template.error_mode = mode
|
||||
|
||||
if ENV['LIQUID-C'] == '1'
|
||||
if ENV['LIQUID_C'] == '1'
|
||||
puts "-- LIQUID C"
|
||||
require 'liquid/c'
|
||||
end
|
||||
@@ -37,18 +37,18 @@ module Minitest
|
||||
include Liquid
|
||||
|
||||
def assert_template_result(expected, template, assigns = {}, message = nil)
|
||||
assert_equal expected, Template.parse(template).render!(assigns), message
|
||||
assert_equal expected, Template.parse(template, line_numbers: true).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).render!(assigns), message
|
||||
assert_match expected, Template.parse(template, line_numbers: true).render!(assigns), message
|
||||
end
|
||||
|
||||
def assert_match_syntax_error(match, template, assigns = {})
|
||||
exception = assert_raises(Liquid::SyntaxError) do
|
||||
Template.parse(template).render(assigns)
|
||||
Template.parse(template, line_numbers: true).render(assigns)
|
||||
end
|
||||
assert_match match, exception.message
|
||||
end
|
||||
@@ -84,6 +84,13 @@ module Minitest
|
||||
ensure
|
||||
Liquid::Template.error_mode = old_mode
|
||||
end
|
||||
|
||||
def with_custom_tag(tag_name, tag_class)
|
||||
Liquid::Template.register_tag(tag_name, tag_class)
|
||||
yield
|
||||
ensure
|
||||
Liquid::Template.tags.delete(tag_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -114,3 +121,17 @@ class ErrorDrop < Liquid::Drop
|
||||
raise Exception, 'exception'
|
||||
end
|
||||
end
|
||||
|
||||
class StubFileSystem
|
||||
attr_reader :file_read_count
|
||||
|
||||
def initialize(values)
|
||||
@file_read_count = 0
|
||||
@values = values
|
||||
end
|
||||
|
||||
def read_template_file(template_path)
|
||||
@file_read_count += 1
|
||||
@values.fetch(template_path)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -44,10 +44,47 @@ class BlockUnitTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_with_custom_tag
|
||||
Liquid::Template.register_tag("testtag", Block)
|
||||
assert Liquid::Template.parse("{% testtag %} {% endtesttag %}")
|
||||
ensure
|
||||
Liquid::Template.tags.delete('testtag')
|
||||
with_custom_tag('testtag', Block) do
|
||||
assert Liquid::Template.parse("{% testtag %} {% endtesttag %}")
|
||||
end
|
||||
end
|
||||
|
||||
def test_custom_block_tags_have_a_default_render_to_output_buffer_method_for_backwards_compatibility
|
||||
klass1 = Class.new(Block) do
|
||||
def render(*)
|
||||
'hello'
|
||||
end
|
||||
end
|
||||
|
||||
with_custom_tag('blabla', klass1) do
|
||||
template = Liquid::Template.parse("{% blabla %} bla {% endblabla %}")
|
||||
|
||||
assert_equal 'hello', template.render
|
||||
|
||||
buf = ''
|
||||
output = template.render({}, output: buf)
|
||||
assert_equal 'hello', output
|
||||
assert_equal 'hello', buf
|
||||
assert_equal buf.object_id, output.object_id
|
||||
end
|
||||
|
||||
klass2 = Class.new(klass1) do
|
||||
def render(*)
|
||||
'foo' + super + 'bar'
|
||||
end
|
||||
end
|
||||
|
||||
with_custom_tag('blabla', klass2) do
|
||||
template = Liquid::Template.parse("{% blabla %} foo {% endblabla %}")
|
||||
|
||||
assert_equal 'foohellobar', template.render
|
||||
|
||||
buf = ''
|
||||
output = template.render({}, output: buf)
|
||||
assert_equal 'foohellobar', output
|
||||
assert_equal 'foohellobar', buf
|
||||
assert_equal buf.object_id, output.object_id
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -102,6 +102,21 @@ class ContextUnitTest < Minitest::Test
|
||||
assert_nil @context['does_not_exist']
|
||||
end
|
||||
|
||||
def test_scoping
|
||||
@context.push
|
||||
@context.pop
|
||||
|
||||
assert_raises(Liquid::ContextError) do
|
||||
@context.pop
|
||||
end
|
||||
|
||||
assert_raises(Liquid::ContextError) do
|
||||
@context.push
|
||||
@context.pop
|
||||
@context.pop
|
||||
end
|
||||
end
|
||||
|
||||
def test_length_query
|
||||
@context['numbers'] = [1, 2, 3, 4]
|
||||
|
||||
@@ -155,12 +170,18 @@ class ContextUnitTest < Minitest::Test
|
||||
|
||||
def test_add_item_in_outer_scope
|
||||
@context['test'] = 'test'
|
||||
|
||||
@context.stack('test') do
|
||||
assert_equal 'test', @context['test']
|
||||
end
|
||||
|
||||
@context.push
|
||||
assert_equal 'test', @context['test']
|
||||
@context.pop
|
||||
assert_equal 'test', @context['test']
|
||||
end
|
||||
|
||||
def test_add_item_in_inner_scope
|
||||
@context.push
|
||||
@context['test'] = 'test'
|
||||
assert_equal 'test', @context['test']
|
||||
@context.pop
|
||||
assert_nil @context['test']
|
||||
end
|
||||
|
||||
def test_hierachical_data
|
||||
@@ -447,11 +468,79 @@ class ContextUnitTest < Minitest::Test
|
||||
assert_equal 'hi filtered', context.apply_global_filter('hi')
|
||||
end
|
||||
|
||||
def test_static_environments_are_read_with_lower_priority_than_environments
|
||||
context = Context.build(
|
||||
static_environments: { 'shadowed' => 'static', 'unshadowed' => 'static' },
|
||||
environments: { 'shadowed' => 'dynamic' }
|
||||
)
|
||||
|
||||
assert_equal 'dynamic', context['shadowed']
|
||||
assert_equal 'static', context['unshadowed']
|
||||
end
|
||||
|
||||
def test_apply_global_filter_when_no_global_filter_exist
|
||||
context = Context.new
|
||||
assert_equal 'hi', context.apply_global_filter('hi')
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_does_not_inherit_variables
|
||||
super_context = Context.new
|
||||
super_context['my_variable'] = 'some value'
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
|
||||
assert_nil subcontext['my_variable']
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_inherits_static_environment
|
||||
super_context = Context.build(static_environments: { 'my_environment_value' => 'my value' })
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
|
||||
assert_equal 'my value', subcontext['my_environment_value']
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_inherits_resource_limits
|
||||
resource_limits = ResourceLimits.new({})
|
||||
super_context = Context.new({}, {}, {}, false, resource_limits)
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
assert_equal resource_limits, subcontext.resource_limits
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_inherits_exception_renderer
|
||||
super_context = Context.new
|
||||
super_context.exception_renderer = ->(_e) { 'my exception message' }
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
assert_equal 'my exception message', subcontext.handle_error(Liquid::Error.new)
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_does_not_inherit_non_static_registers
|
||||
registers = {
|
||||
my_register: :my_value
|
||||
}
|
||||
super_context = Context.new({}, {}, registers)
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
assert_nil subcontext.registers[:my_register]
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_inherits_static_registers
|
||||
super_context = Context.build(static_registers: { my_register: :my_value })
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
assert_equal :my_value, subcontext.static_registers[:my_register]
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_inherits_filters
|
||||
my_filter = Module.new do
|
||||
def my_filter(*)
|
||||
'my filter result'
|
||||
end
|
||||
end
|
||||
|
||||
super_context = Context.new
|
||||
super_context.add_filters([my_filter])
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
template = Template.parse('{{ 123 | my_filter }}')
|
||||
assert_equal 'my filter result', template.render(subcontext)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def assert_no_object_allocations
|
||||
|
||||
91
test/unit/partial_cache_unit_test.rb
Normal file
91
test/unit/partial_cache_unit_test.rb
Normal file
@@ -0,0 +1,91 @@
|
||||
require 'test_helper'
|
||||
|
||||
class PartialCacheUnitTest < Minitest::Test
|
||||
def test_uses_the_file_system_register_if_present
|
||||
context = Liquid::Context.build(
|
||||
registers: {
|
||||
file_system: StubFileSystem.new('my_partial' => 'my partial body')
|
||||
}
|
||||
)
|
||||
|
||||
partial = Liquid::PartialCache.load(
|
||||
'my_partial',
|
||||
context: context,
|
||||
parse_context: Liquid::ParseContext.new
|
||||
)
|
||||
|
||||
assert_equal 'my partial body', partial.render
|
||||
end
|
||||
|
||||
def test_reads_from_the_file_system_only_once_per_file
|
||||
file_system = StubFileSystem.new('my_partial' => 'some partial body')
|
||||
context = Liquid::Context.build(
|
||||
registers: { file_system: file_system }
|
||||
)
|
||||
|
||||
2.times do
|
||||
Liquid::PartialCache.load(
|
||||
'my_partial',
|
||||
context: context,
|
||||
parse_context: Liquid::ParseContext.new
|
||||
)
|
||||
end
|
||||
|
||||
assert_equal 1, file_system.file_read_count
|
||||
end
|
||||
|
||||
def test_cache_state_is_stored_per_context
|
||||
parse_context = Liquid::ParseContext.new
|
||||
shared_file_system = StubFileSystem.new(
|
||||
'my_partial' => 'my shared value'
|
||||
)
|
||||
context_one = Liquid::Context.build(
|
||||
registers: {
|
||||
file_system: shared_file_system
|
||||
}
|
||||
)
|
||||
context_two = Liquid::Context.build(
|
||||
registers: {
|
||||
file_system: shared_file_system
|
||||
}
|
||||
)
|
||||
|
||||
2.times do
|
||||
Liquid::PartialCache.load(
|
||||
'my_partial',
|
||||
context: context_one,
|
||||
parse_context: parse_context
|
||||
)
|
||||
end
|
||||
|
||||
Liquid::PartialCache.load(
|
||||
'my_partial',
|
||||
context: context_two,
|
||||
parse_context: parse_context
|
||||
)
|
||||
|
||||
assert_equal 2, shared_file_system.file_read_count
|
||||
end
|
||||
|
||||
def test_cache_is_not_broken_when_a_different_parse_context_is_used
|
||||
file_system = StubFileSystem.new('my_partial' => 'some partial body')
|
||||
context = Liquid::Context.build(
|
||||
registers: { file_system: file_system }
|
||||
)
|
||||
|
||||
Liquid::PartialCache.load(
|
||||
'my_partial',
|
||||
context: context,
|
||||
parse_context: Liquid::ParseContext.new(my_key: 'value one')
|
||||
)
|
||||
Liquid::PartialCache.load(
|
||||
'my_partial',
|
||||
context: context,
|
||||
parse_context: Liquid::ParseContext.new(my_key: 'value two')
|
||||
)
|
||||
|
||||
# Technically what we care about is that the file was parsed twice,
|
||||
# but measuring file reads is an OK proxy for this.
|
||||
assert_equal 1, file_system.file_read_count
|
||||
end
|
||||
end
|
||||
@@ -18,4 +18,42 @@ class TagUnitTest < Minitest::Test
|
||||
tag = Tag.parse("some_tag", "", Tokenizer.new(""), ParseContext.new)
|
||||
assert_equal 'some_tag', tag.tag_name
|
||||
end
|
||||
|
||||
def test_custom_tags_have_a_default_render_to_output_buffer_method_for_backwards_compatibility
|
||||
klass1 = Class.new(Tag) do
|
||||
def render(*)
|
||||
'hello'
|
||||
end
|
||||
end
|
||||
|
||||
with_custom_tag('blabla', klass1) do
|
||||
template = Liquid::Template.parse("{% blabla %}")
|
||||
|
||||
assert_equal 'hello', template.render
|
||||
|
||||
buf = ''
|
||||
output = template.render({}, output: buf)
|
||||
assert_equal 'hello', output
|
||||
assert_equal 'hello', buf
|
||||
assert_equal buf.object_id, output.object_id
|
||||
end
|
||||
|
||||
klass2 = Class.new(klass1) do
|
||||
def render(*)
|
||||
'foo' + super + 'bar'
|
||||
end
|
||||
end
|
||||
|
||||
with_custom_tag('blabla', klass2) do
|
||||
template = Liquid::Template.parse("{% blabla %}")
|
||||
|
||||
assert_equal 'foohellobar', template.render
|
||||
|
||||
buf = ''
|
||||
output = template.render({}, output: buf)
|
||||
assert_equal 'foohellobar', output
|
||||
assert_equal 'foohellobar', buf
|
||||
assert_equal buf.object_id, output.object_id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user