Compare commits

...

72 Commits

Author SHA1 Message Date
Dylan Thacker-Smith
33760f083a Extract rescue code from BlockBody#render_node for re-use in liquid-c 2020-09-25 11:24:39 -04:00
Dylan Thacker-Smith
013802c877 Move some unit tests without internal coupling to integration tests
since I would like to continue supporting these tests in liquid-c
in the foreseeable future.
2020-09-25 11:24:39 -04:00
Dylan Thacker-Smith
3dcad3b3cd Move test/integration/parse_tree_visitor_test.rb to test/unit
The ParseTreeVisitor exposes the liquid internals that won't be
kept compatible with liquid-c, so move it out of the integration
tests directory so that we can easily ignore it when testing liquid-c
2020-09-25 11:24:39 -04:00
Dylan Thacker-Smith
db065315ba Allow creating symbols that are garbage collected in a test 2020-09-25 11:24:39 -04:00
Dylan Thacker-Smith
a03f02789b Only use MethodLiteral in condition expressions (#1300) 2020-09-25 11:10:33 -04:00
Dylan Thacker-Smith
ca4b9b43af Port liquid-c bug compatible whitespace trimming (#1291) 2020-09-16 16:07:36 -04:00
Dylan Thacker-Smith
77084930e9 Bring back silencing of errors in blank nodes for backwards compatibility (#1292) 2020-09-15 10:35:18 -04:00
Dylan Thacker-Smith
fb77921b15 Merge pull request #1290 from Shopify/document-unknown-tag-refactor
Pass the tag markup and tokenizer to Document#unknown_tag
2020-09-11 09:34:16 -04:00
Dylan Thacker-Smith
0d02dea20b Rename Liquid::Block#unknown_tag parameters for clarity 2020-09-11 09:33:12 -04:00
Dylan Thacker-Smith
86b47ba28b Pass the tag markup and tokenizer to Document#unknown_tag
The parse_context no longer needs to be passed in because it is available
through through an attr_reader on the instance. However, the markup and
tokenizer weren't made available.  This refactor also makes the parameters
given to Document#unknown_tag consistent with Block#unknown_tag.
2020-09-11 09:33:12 -04:00
Dylan Thacker-Smith
95ff0595c6 Merge pull request #1289 from Shopify/refactor-for-c-block-body
Avoid direct coupling to BlockBody instances for liquid-c replacement
2020-09-11 09:15:58 -04:00
Dylan Thacker-Smith
bbc56f35ec Add ParseContext#new_block_body to centralize the liquid-c override point 2020-09-09 12:25:35 -04:00
Dylan Thacker-Smith
dfbbf87ba9 Use BlockBody from Document using composition rather than inheritence
This way liquid-c can more cleanly use a Liquid::C::BlockBody object
for the block body by overriding Liquid::Document#new_body.
2020-09-08 14:00:52 -04:00
Dylan Thacker-Smith
037b603603 Turn some Liquid::BlockBody methods into class methods for liquid-c
So they can be used from a Liquid::C::BlockBody
2020-09-08 14:00:48 -04:00
Dylan Thacker-Smith
bd33df09de Provide Block#new_body so that liquid-c can override it
This way liquid-c can return a body of a different class that wraps
a C implementation.
2020-09-08 13:59:48 -04:00
Dylan Thacker-Smith
6ca5b62112 Merge pull request #1285 from Shopify/fix-render-length-resource-limit
Fix render length resource limit so it doesn't multiply nested output
2020-09-08 13:57:30 -04:00
Dylan Thacker-Smith
e1a2057a1b Update assign_score during capturing
To stop long captures before they grow the heap more then they should.
2020-09-03 11:13:08 -04:00
Dylan Thacker-Smith
ae9dbe0ca7 Fix render length resource limit so it doesn't multiply nested output 2020-09-03 11:13:04 -04:00
Dylan Thacker-Smith
3b486425b0 Handle BlockBody#blank? at parse time (#1287) 2020-09-03 11:07:13 -04:00
Dylan Thacker-Smith
b08bcf00ac Push interrupts from Continue and Break tags rather than from BlockBody (#1286) 2020-09-03 06:55:24 -04:00
Dylan Thacker-Smith
0740e8b431 Remove unused quirk allowing liquid tags to close a block it is nested in (#1284) 2020-09-03 06:51:56 -04:00
Dylan Thacker-Smith
5532df880f Handle disabled tags errors like other liquid errors (#1275) 2020-08-18 11:39:54 -04:00
Dylan Thacker-Smith
2b11efc3ae Fix performance regression from introduction of Template#disable_tags (#1274) 2020-08-18 11:25:51 -04:00
Thierry Joyal
a1d982ca76 Merge pull request #1272 from Shopify/StaticRegisters/add-test-coverage
[StaticRegisters] Add test coverage
2020-08-04 08:31:45 -04:00
Thierry Joyal
03be7f1ee3 [StaticRegisters] Add test coverage 2020-07-28 10:23:51 -04:00
Dylan Thacker-Smith
1ced4eaf10 Merge pull request #1268 from Shopify/remove-taint-checking
Remove support for taint checking
2020-07-25 21:27:46 -04:00
Dylan Thacker-Smith
4970167726 Bump rake development dependency
Gets rid of a deprecation warning when running the tests.
2020-07-23 16:23:18 -04:00
Dylan Thacker-Smith
065ccbc4aa Remove support for taint checking 2020-07-23 16:22:46 -04:00
Feken Baboyan
1feaa63813 Merge pull request #1258 from Shopify/fix-context-overriding-in-templates
Fix how Template overrides static registers when #render is invoked
2020-05-28 09:32:31 -04:00
Feken Baboyan
8541c6be35 make Template override static registers only when the register key is not defined 2020-05-28 09:08:03 -04:00
Thierry Joyal
18654526c8 Merge pull request #1257 from Shopify/StaticRegisters/remove-registers-attr-reader
[StaticRegisters] Remove registers attr_reader
2020-05-22 14:01:37 -04:00
Thierry Joyal
bd1f7f9492 [StaticRegisters] Remove assertion for delete to not remove static content 2020-05-22 13:42:44 -04:00
Thierry Joyal
40d75dd283 Update test/unit/static_registers_unit_test.rb
Co-authored-by: Dylan Thacker-Smith <dylan.smith@shopify.com>
2020-05-22 12:15:52 -04:00
Thierry Joyal
f5011365f1 [StaticRegisters] Remove registers attr_reader 2020-05-22 10:51:07 -04:00
Thierry Joyal
ebbd046c92 Merge pull request #1250 from Shopify/static-registers/fetch-raise-on-missing
[StaticRegisters] Fetch raise on missing
2020-05-22 09:56:05 -04:00
Thierry Joyal
b9979088ec [StaticRegisters] Fetch raise on missing
Co-authored-by: Dylan Thacker-Smith <dylan.smith@shopify.com>
2020-05-22 09:35:47 -04:00
Dylan Thacker-Smith
bd0e53bd2e Merge pull request #1239 from Shopify/remove-bad-arity-assumption
Fix ParseTreeVisitorTest for ruby-head
2020-05-21 14:02:04 -04:00
Thierry Joyal
4b586f4105 Merge pull request #1251 from Shopify/travis/optional-head
[Travis] Optional head
2020-05-21 13:51:11 -04:00
Thierry Joyal
0410119d5f [Travis] Optional head 2020-05-21 12:45:14 -04:00
Dylan Thacker-Smith
c2f67398d0 Allow ruby-head failures
Ignore an object allocation test failure on ruby-head for now.
2020-03-31 10:53:49 -04:00
Dylan Thacker-Smith
81149344a5 Fix ParseTreeVisitorTest for ruby-head 2020-03-31 10:53:46 -04:00
Dylan Thacker-Smith
e9b649b345 Fix Liquid::Template inheritance (#1227)
self.class.default_resource_limits would return `nil` in a subclass, since
the attribute isn't set on subclasses.
2020-01-21 15:09:22 -05:00
Celso Dantas
9c538f4237 Merge pull request #1207 from Shopify/moving-const-to-const
Use String literal instead
2020-01-20 12:37:57 -05:00
Celso Dantas
c08a358a2b Use String literal instead of using a class method
The class method string definition is not needed here, so it can be removed.
2020-01-16 09:42:32 -05:00
Justin Li
dbaef5e79b Merge pull request #1180 from Shopify/test-all-filters
Test all filters against random data to detect exceptions
2020-01-13 15:52:44 -05:00
Dylan Thacker-Smith
48a155a213 Initialize Liquid::Template class attributes eagerly instead of lazily (#1223) 2020-01-10 17:42:01 -05:00
Dylan Thacker-Smith
c69a9a77c6 Merge pull request #1215 from Shopify/nested-liquid-tag
Fix liquid tag nested in outer block
2020-01-09 19:24:52 -05:00
Dylan Thacker-Smith
ef79fa3898 style: Avoid deep nesting for parsing the liquid tag in the block body 2020-01-09 19:13:13 -05:00
Dylan Thacker-Smith
f7ad602bfc Fix liquid tag nested in outer block 2020-01-09 19:13:13 -05:00
Dylan Thacker-Smith
ffd6049ba2 Merge pull request #1222 from Shopify/bump-ci-ruby
Test against the latest ruby in CI
2020-01-09 18:12:36 -05:00
Dylan Thacker-Smith
b3ad54c0c2 Test against the latest ruby in CI 2020-01-09 13:15:34 -05:00
Dylan Thacker-Smith
67eca3f58d Upgrade rubocop and style guide for ruby 2.7 compatibility 2020-01-09 13:15:34 -05:00
Thierry Joyal
0847bf560f Merge pull request #1218 from Shopify/strainer/remove-safe-navigation
Remove handling of a nil context in the Strainer class
2020-01-07 11:54:49 -05:00
Dylan Thacker-Smith
8074565c3e Merge pull request #1216 from Shopify/unsupported-taint-mode
Remove support for taint_mode on ruby versions that don't support it
2020-01-07 10:12:36 -05:00
Thierry Joyal
24e81267b9 Merge pull request #1208 from Shopify/strainer/revisit
[Strainer] Separate factory from template
2020-01-06 13:56:22 -05:00
Thierry Joyal
c0ffee3ff9 [Strainer] Remove safe navigation 2020-01-06 18:34:13 +00:00
Thierry Joyal
c0ffeeef26 [Strainer] Separate factory from template 2020-01-06 17:45:25 +00:00
Dylan Thacker-Smith
22dbf90b7d Try to stay compatible with ruby-head 2019-12-19 11:12:55 -05:00
Dylan Thacker-Smith
40c68c9c83 Remove support for taint_mode on ruby versions that don't support it 2019-12-19 11:12:51 -05:00
Martin Morissette
b7f0f158ab Merge pull request #1212 from Shopify/template-factory
Introduce template factory
2019-12-19 08:52:22 -05:00
Martin Morissette
d8f31046a9 Introduce template factory 2019-12-17 21:45:08 -05:00
Martin Morissette
6c6382ed69 Merge pull request #1213 from Shopify/allow-failures-ruby-27
Make ruby 2.7 optional in CI
2019-12-16 09:46:12 -05:00
Martin Morissette
53ba1372f9 Make ruby 2.7 optional in CI 2019-12-16 08:59:26 -05:00
Mike Angell
57c9cf64eb Allow render to handle with and for correctly (#1193)
* Allow render to handle with and for correctly

* code improvements
2019-10-23 04:12:46 +10:00
Alessandro Diogo Brückheimer
e83b1e4159 Add ForceEqualSignAlignment to .rubocop.yml (#1190)
* Add ForceEqualSignAlignment to .rubocop.yml

* Revert ForceEqualSignAlignment cop

* Update method alignment

* Undo addition of whitespace to improve readability

* Fix missing alignment
2019-10-21 21:18:48 +10:00
Mike Angell
3784020a8d [New Feature] Add forloop inside render tag when using for syntax (#1191)
* Add forloop to render for syntax

* Remove forloop guard
2019-10-17 23:06:13 +10:00
uchoudh
1223444738 Fix flaky tests (#1186) 2019-10-12 02:52:07 +11:00
Mike Angell
2bfeed2b00 Resolve InputIterator dropping context (#1184)
* Resolve InputIterator dropping context

* Prefer attr_reader
2019-10-09 08:00:16 +11:00
Mike Angell
04b800d768 Add support for as in Render and Include tags (#1181)
* Add support for alias

* Remove duplicate code

* Default to template name

* Improve variable matching

* Extract render_partial

* remove method
2019-10-09 07:59:52 +11:00
Mike Angell
f1d62978ef Allow default function to handle false as value (#1144)
* Allow default function to handle false as value

* Change to named parameter

* Remove redundant freeze

* add brackets to make intention clearer

* Use named param format from liquid

* Update syntax

* document default filter
2019-10-09 04:03:33 +11:00
Mike Angell
b0f8c2c03e Remove error logging 2019-10-05 01:13:35 +10:00
Mike Angell
37e40673ff Filter test 2019-10-04 17:00:54 +10:00
100 changed files with 1637 additions and 1262 deletions

View File

@@ -1,3 +1,5 @@
# Recommended rubocop version: ~> 0.78.0
AllCops: AllCops:
Exclude: Exclude:
- 'db/schema.rb' - 'db/schema.rb'
@@ -20,7 +22,7 @@ Style/Alias:
- prefer_alias - prefer_alias
- prefer_alias_method - prefer_alias_method
Layout/AlignHash: Layout/HashAlignment:
EnforcedHashRocketStyle: key EnforcedHashRocketStyle: key
EnforcedColonStyle: key EnforcedColonStyle: key
EnforcedLastArgumentHashStyle: ignore_implicit EnforcedLastArgumentHashStyle: ignore_implicit
@@ -30,7 +32,7 @@ Layout/AlignHash:
- ignore_implicit - ignore_implicit
- ignore_explicit - ignore_explicit
Layout/AlignParameters: Layout/ParameterAlignment:
EnforcedStyle: with_fixed_indentation EnforcedStyle: with_fixed_indentation
SupportedStyles: SupportedStyles:
- with_first_parameter - with_first_parameter
@@ -172,7 +174,7 @@ Naming/FileName:
Regex: Regex:
IgnoreExecutableScripts: true IgnoreExecutableScripts: true
Layout/IndentFirstArgument: Layout/FirstArgumentIndentation:
EnforcedStyle: consistent EnforcedStyle: consistent
SupportedStyles: SupportedStyles:
- consistent - consistent
@@ -225,7 +227,7 @@ Layout/IndentationConsistency:
Layout/IndentationWidth: Layout/IndentationWidth:
Width: 2 Width: 2
Layout/IndentFirstArrayElement: Layout/FirstArrayElementIndentation:
EnforcedStyle: consistent EnforcedStyle: consistent
SupportedStyles: SupportedStyles:
- special_inside_parentheses - special_inside_parentheses
@@ -233,10 +235,10 @@ Layout/IndentFirstArrayElement:
- align_brackets - align_brackets
IndentationWidth: IndentationWidth:
Layout/IndentAssignment: Layout/AssignmentIndentation:
IndentationWidth: IndentationWidth:
Layout/IndentFirstHashElement: Layout/FirstHashElementIndentation:
EnforcedStyle: consistent EnforcedStyle: consistent
SupportedStyles: SupportedStyles:
- special_inside_parentheses - special_inside_parentheses
@@ -340,9 +342,9 @@ Style/PercentQLiterals:
Naming/PredicateName: Naming/PredicateName:
NamePrefix: NamePrefix:
- is_ - is_
NamePrefixBlacklist: ForbiddenPrefixes:
- is_ - is_
NameWhitelist: AllowedMethods:
- is_a? - is_a?
Exclude: Exclude:
- 'spec/**/*' - 'spec/**/*'
@@ -467,7 +469,7 @@ Style/TernaryParentheses:
- require_no_parentheses - require_no_parentheses
AllowSafeAssignment: true AllowSafeAssignment: true
Layout/TrailingBlankLines: Layout/TrailingEmptyLines:
EnforcedStyle: final_newline EnforcedStyle: final_newline
SupportedStyles: SupportedStyles:
- final_newline - final_newline
@@ -478,7 +480,7 @@ Style/TrivialAccessors:
AllowPredicates: true AllowPredicates: true
AllowDSLWriters: false AllowDSLWriters: false
IgnoreClassMethods: false IgnoreClassMethods: false
Whitelist: AllowedMethods:
- to_ary - to_ary
- to_a - to_a
- to_c - to_c
@@ -509,7 +511,7 @@ Style/WhileUntilModifier:
Metrics/BlockNesting: Metrics/BlockNesting:
Max: 3 Max: 3
Metrics/LineLength: Layout/LineLength:
Max: 120 Max: 120
AllowHeredoc: true AllowHeredoc: true
AllowURI: true AllowURI: true
@@ -561,7 +563,7 @@ Lint/UnusedMethodArgument:
Naming/AccessorMethodName: Naming/AccessorMethodName:
Enabled: true Enabled: true
Layout/AlignArray: Layout/ArrayAlignment:
Enabled: true Enabled: true
Style/ArrayJoin: Style/ArrayJoin:
@@ -819,13 +821,13 @@ Layout/TrailingWhitespace:
Style/UnlessElse: Style/UnlessElse:
Enabled: true Enabled: true
Style/UnneededCapitalW: Style/RedundantCapitalW:
Enabled: true Enabled: true
Style/UnneededInterpolation: Style/RedundantInterpolation:
Enabled: true Enabled: true
Style/UnneededPercentQ: Style/RedundantPercentQ:
Enabled: true Enabled: true
Style/VariableInterpolation: Style/VariableInterpolation:
@@ -840,7 +842,7 @@ Style/WhileUntilDo:
Style/ZeroLengthPredicate: Style/ZeroLengthPredicate:
Enabled: true Enabled: true
Layout/IndentHeredoc: Layout/HeredocIndentation:
EnforcedStyle: squiggly EnforcedStyle: squiggly
Lint/AmbiguousOperator: Lint/AmbiguousOperator:
@@ -864,7 +866,7 @@ Lint/DeprecatedClassMethods:
Lint/DuplicateMethods: Lint/DuplicateMethods:
Enabled: true Enabled: true
Lint/DuplicatedKey: Lint/DuplicateHashKey:
Enabled: true Enabled: true
Lint/EachWithObjectArgument: Lint/EachWithObjectArgument:
@@ -891,8 +893,8 @@ Lint/FloatOutOfRange:
Lint/FormatParameterMismatch: Lint/FormatParameterMismatch:
Enabled: true Enabled: true
Lint/HandleExceptions: Lint/SuppressedException:
Enabled: true AllowComments: true
Lint/ImplicitStringConcatenation: Lint/ImplicitStringConcatenation:
Description: Checks for adjacent string literals on the same line, which could Description: Checks for adjacent string literals on the same line, which could
@@ -947,7 +949,7 @@ Lint/ShadowedException:
Lint/ShadowingOuterLocalVariable: Lint/ShadowingOuterLocalVariable:
Enabled: true Enabled: true
Lint/StringConversionInInterpolation: Lint/RedundantStringCoercion:
Enabled: true Enabled: true
Lint/UnderscorePrefixedVariableName: Lint/UnderscorePrefixedVariableName:
@@ -956,13 +958,13 @@ Lint/UnderscorePrefixedVariableName:
Lint/UnifiedInteger: Lint/UnifiedInteger:
Enabled: true Enabled: true
Lint/UnneededCopDisableDirective: Lint/RedundantCopDisableDirective:
Enabled: true Enabled: true
Lint/UnneededCopEnableDirective: Lint/RedundantCopEnableDirective:
Enabled: true Enabled: true
Lint/UnneededSplatExpansion: Lint/RedundantSplatExpansion:
Enabled: true Enabled: true
Lint/UnreachableCode: Lint/UnreachableCode:

View File

@@ -18,7 +18,7 @@ Lint/InheritException:
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. # Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
# URISchemes: http, https # URISchemes: http, https
Metrics/LineLength: Layout/LineLength:
Max: 294 Max: 294
# Offense count: 44 # Offense count: 44

View File

@@ -4,8 +4,8 @@ cache: bundler
rvm: rvm:
- 2.4 - 2.4
- 2.5 - 2.5
- &latest_ruby 2.6 - 2.6
- 2.7 - &latest_ruby 2.7
- ruby-head - ruby-head
matrix: matrix:

View File

@@ -18,7 +18,7 @@ group :benchmark, :test do
end end
group :test do group :test do
gem 'rubocop', '~> 0.74.0', require: false gem 'rubocop', '~> 0.78.0', require: false
gem 'rubocop-performance', require: false gem 'rubocop-performance', require: false
platform :mri, :truffleruby do platform :mri, :truffleruby do

View File

@@ -1,5 +1,11 @@
# Liquid Change Log # Liquid Change Log
### Unreleased
* Split Strainer class as a factory and a template (#1208) [Thierry Joyal]
* Remove handling of a nil context in the Strainer class (#1218) [Thierry Joyal]
* StaticRegisters#fetch to raise on missing key (#1250) [Thierry Joyal]
## 4.0.3 / 2019-03-12 ## 4.0.3 / 2019-03-12
### Fixed ### Fixed

View File

@@ -11,7 +11,7 @@ desc('run test suite with default parser')
Rake::TestTask.new(:base_test) do |t| Rake::TestTask.new(:base_test) do |t|
t.libs << '.' << 'lib' << 'test' t.libs << '.' << 'lib' << 'test'
t.test_files = FileList['test/{integration,unit}/**/*_test.rb'] t.test_files = FileList['test/{integration,unit}/**/*_test.rb']
t.verbose = false t.verbose = false
end end
desc('run test suite with warn error mode') desc('run test suite with warn error mode')

View File

@@ -12,16 +12,16 @@ class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet
private private
def handle(_type, req, res) def handle(_type, req, res)
@request = req @request = req
@response = res @response = res
@request.path_info =~ /(\w+)\z/ @request.path_info =~ /(\w+)\z/
@action = Regexp.last_match(1) || 'index' @action = Regexp.last_match(1) || 'index'
@assigns = send(@action) if respond_to?(@action) @assigns = send(@action) if respond_to?(@action)
@response['Content-Type'] = "text/html" @response['Content-Type'] = "text/html"
@response.status = 200 @response.status = 200
@response.body = Liquid::Template.parse(read_template).render(@assigns, filters: [ProductsFilter]) @response.body = Liquid::Template.parse(read_template).render(@assigns, filters: [ProductsFilter])
end end
def read_template(filename = @action) def read_template(filename = @action)

View File

@@ -57,11 +57,14 @@ require 'liquid/forloop_drop'
require 'liquid/extensions' require 'liquid/extensions'
require 'liquid/errors' require 'liquid/errors'
require 'liquid/interrupts' require 'liquid/interrupts'
require 'liquid/strainer' require 'liquid/strainer_factory'
require 'liquid/strainer_template'
require 'liquid/expression' require 'liquid/expression'
require 'liquid/context' require 'liquid/context'
require 'liquid/parser_switching' require 'liquid/parser_switching'
require 'liquid/tag' require 'liquid/tag'
require 'liquid/tag/disabler'
require 'liquid/tag/disableable'
require 'liquid/block' require 'liquid/block'
require 'liquid/block_body' require 'liquid/block_body'
require 'liquid/document' require 'liquid/document'
@@ -80,8 +83,8 @@ require 'liquid/partial_cache'
require 'liquid/usage' require 'liquid/usage'
require 'liquid/register' require 'liquid/register'
require 'liquid/static_registers' require 'liquid/static_registers'
require 'liquid/template_factory'
# Load all the tags of the standard library # Load all the tags of the standard library
# #
Dir["#{__dir__}/liquid/tags/*.rb"].each { |f| require f } Dir["#{__dir__}/liquid/tags/*.rb"].each { |f| require f }
Dir["#{__dir__}/liquid/registers/*.rb"].each { |f| require f }

View File

@@ -10,7 +10,7 @@ module Liquid
end end
def parse(tokens) def parse(tokens)
@body = BlockBody.new @body = new_body
while parse_body(@body, tokens) while parse_body(@body, tokens)
end end
end end
@@ -28,7 +28,12 @@ module Liquid
@body.nodelist @body.nodelist
end end
def unknown_tag(tag, _params, _tokens) def unknown_tag(tag_name, _markup, _tokenizer)
Block.raise_unknown_tag(tag_name, block_name, block_delimiter, parse_context)
end
# @api private
def self.raise_unknown_tag(tag, block_name, block_delimiter, parse_context)
if tag == 'else' if tag == 'else'
raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_else", raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_else",
block_name: block_name) block_name: block_name)
@@ -50,8 +55,14 @@ module Liquid
@block_delimiter ||= "end#{block_name}" @block_delimiter ||= "end#{block_name}"
end end
protected private
# @api public
def new_body
parse_context.new_block_body
end
# @api public
def parse_body(body, tokens) def parse_body(body, tokens)
if parse_context.depth >= MAX_DEPTH if parse_context.depth >= MAX_DEPTH
raise StackLevelError, "Nesting too deep" raise StackLevelError, "Nesting too deep"

View File

@@ -1,19 +1,21 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'English'
module Liquid module Liquid
class BlockBody class BlockBody
LiquidTagToken = /\A\s*(\w+)\s*(.*?)\z/o 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 ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
WhitespaceOrNothing = /\A\s*\z/ WhitespaceOrNothing = /\A\s*\z/
TAGSTART = "{%" TAGSTART = "{%"
VARSTART = "{{" VARSTART = "{{"
attr_reader :nodelist attr_reader :nodelist
def initialize def initialize
@nodelist = [] @nodelist = []
@blank = true @blank = true
end end
def parse(tokenizer, parse_context, &block) def parse(tokenizer, parse_context, &block)
@@ -35,7 +37,7 @@ module Liquid
return yield token, token return yield token, token
end end
tag_name = Regexp.last_match(1) tag_name = Regexp.last_match(1)
markup = Regexp.last_match(2) markup = Regexp.last_match(2)
unless (tag = registered_tags[tag_name]) unless (tag = registered_tags[tag_name])
# end parsing if we reach an unknown tag and let the caller decide # end parsing if we reach an unknown tag and let the caller decide
# determine how to proceed # determine how to proceed
@@ -51,17 +53,64 @@ module Liquid
yield nil, nil yield nil, nil
end end
private def parse_for_document(tokenizer, parse_context, &block) # @api private
def self.unknown_tag_in_liquid_tag(tag, parse_context)
Block.raise_unknown_tag(tag, 'liquid', '%}', parse_context)
end
# @api private
def self.raise_missing_tag_terminator(token, parse_context)
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_termination", token: token, tag_end: TagEnd.inspect)
end
# @api private
def self.raise_missing_variable_terminator(token, parse_context)
raise SyntaxError, parse_context.locale.t("errors.syntax.variable_termination", token: token, tag_end: VariableEnd.inspect)
end
# @api private
def self.render_node(context, output, node)
node.render_to_output_buffer(context, output)
rescue => exc
blank_tag = !node.instance_of?(Variable) && node.blank?
rescue_render_node(context, output, node.line_number, exc, blank_tag)
end
# @api private
def self.rescue_render_node(context, output, line_number, exc, blank_tag)
case exc
when MemoryError
raise
when UndefinedVariable, UndefinedDropMethod, UndefinedFilter
context.handle_error(exc, line_number)
else
error_message = context.handle_error(exc, line_number)
unless blank_tag # conditional for backwards compatibility
output << error_message
end
end
end
private def parse_liquid_tag(markup, parse_context)
liquid_tag_tokenizer = Tokenizer.new(markup, line_number: parse_context.line_number, for_liquid_tag: true)
parse_for_liquid_tag(liquid_tag_tokenizer, parse_context) do |end_tag_name, _end_tag_markup|
if end_tag_name
BlockBody.unknown_tag_in_liquid_tag(end_tag_name, parse_context)
end
end
end
private def parse_for_document(tokenizer, parse_context)
while (token = tokenizer.shift) while (token = tokenizer.shift)
next if token.empty? next if token.empty?
case case
when token.start_with?(TAGSTART) when token.start_with?(TAGSTART)
whitespace_handler(token, parse_context) whitespace_handler(token, parse_context)
unless token =~ FullToken unless token =~ FullToken
raise_missing_tag_terminator(token, parse_context) BlockBody.raise_missing_tag_terminator(token, parse_context)
end end
tag_name = Regexp.last_match(2) tag_name = Regexp.last_match(2)
markup = Regexp.last_match(4) markup = Regexp.last_match(4)
if parse_context.line_number if parse_context.line_number
# newlines inside the tag should increase the line number, # newlines inside the tag should increase the line number,
@@ -70,8 +119,8 @@ module Liquid
end end
if tag_name == 'liquid' if tag_name == 'liquid'
liquid_tag_tokenizer = Tokenizer.new(markup, line_number: parse_context.line_number, for_liquid_tag: true) parse_liquid_tag(markup, parse_context)
next parse_for_liquid_tag(liquid_tag_tokenizer, parse_context, &block) next
end end
unless (tag = registered_tags[tag_name]) unless (tag = registered_tags[tag_name])
@@ -104,7 +153,11 @@ module Liquid
if token[2] == WhitespaceControl if token[2] == WhitespaceControl
previous_token = @nodelist.last previous_token = @nodelist.last
if previous_token.is_a?(String) if previous_token.is_a?(String)
first_byte = previous_token.getbyte(0)
previous_token.rstrip! previous_token.rstrip!
if previous_token.empty? && parse_context[:bug_compatible_whitespace_trimming] && first_byte
previous_token << first_byte
end
end end
end end
parse_context.trim_whitespace = (token[-3] == WhitespaceControl) parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
@@ -114,38 +167,47 @@ module Liquid
@blank @blank
end end
# Remove blank strings in the block body for a control flow tag (e.g. `if`, `for`, `case`, `unless`)
# with a blank body.
#
# For example, in a conditional assignment like the following
#
# ```
# {% if size > max_size %}
# {% assign size = max_size %}
# {% endif %}
# ```
#
# we assume the intention wasn't to output the blank spaces in the `if` tag's block body, so this method
# will remove them to reduce the render output size.
#
# Note that it is now preferred to use the `liquid` tag for this use case.
def remove_blank_strings
raise "remove_blank_strings only support being called on a blank block body" unless @blank
@nodelist.reject! { |node| node.instance_of?(String) }
end
def render(context) def render(context)
render_to_output_buffer(context, +'') render_to_output_buffer(context, +'')
end end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
context.resource_limits.render_score += @nodelist.length context.resource_limits.increment_render_score(@nodelist.length)
idx = 0 idx = 0
while (node = @nodelist[idx]) while (node = @nodelist[idx])
previous_output_size = output.bytesize if node.instance_of?(String)
case node
when String
output << node output << node
when Variable else
render_node(context, output, node) render_node(context, output, node)
when Block
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 # If we get an Interrupt that means the block must stop processing. An
# Interrupt is any command that stops block execution such as {% break %} # Interrupt is any command that stops block execution such as {% break %}
# or {% continue %} # or {% continue %}. These tags may also occur through Block or Include tags.
context.push_interrupt(node.interrupt) break if context.interrupt? # might have happened in a for-block
break
else # Other non-Block tags
render_node(context, output, node)
break if context.interrupt? # might have happened through an include
end end
idx += 1 idx += 1
raise_if_resource_limits_reached(context, output.bytesize - previous_output_size) context.resource_limits.increment_write_score(output)
end end
output output
@@ -154,29 +216,7 @@ module Liquid
private private
def render_node(context, output, node) def render_node(context, output, node)
if node.disabled?(context) BlockBody.render_node(context, output, node)
output << node.disabled_error_message
return
end
disable_tags(context, node.disabled_tags) do
node.render_to_output_buffer(context, output)
end
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
context.handle_error(e, node.line_number)
rescue ::StandardError => e
line_number = node.is_a?(String) ? nil : node.line_number
output << context.handle_error(e, line_number)
end
def disable_tags(context, tags, &block)
return yield if tags.empty?
context.registers[:disabled_tags].disable(tags, &block)
end
def raise_if_resource_limits_reached(context, length)
context.resource_limits.render_length += length
return unless context.resource_limits.reached?
raise MemoryError, "Memory limits exceeded"
end end
def create_variable(token, parse_context) def create_variable(token, parse_context)
@@ -184,15 +224,17 @@ module Liquid
markup = content.first markup = content.first
return Variable.new(markup, parse_context) return Variable.new(markup, parse_context)
end end
raise_missing_variable_terminator(token, parse_context) BlockBody.raise_missing_variable_terminator(token, parse_context)
end end
# @deprecated Use {.raise_missing_tag_terminator} instead
def raise_missing_tag_terminator(token, parse_context) def raise_missing_tag_terminator(token, parse_context)
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_termination", token: token, tag_end: TagEnd.inspect) BlockBody.raise_missing_tag_terminator(token, parse_context)
end end
# @deprecated Use {.raise_missing_variable_terminator} instead
def raise_missing_variable_terminator(token, parse_context) def raise_missing_variable_terminator(token, parse_context)
raise SyntaxError, parse_context.locale.t("errors.syntax.variable_termination", token: token, tag_end: VariableEnd.inspect) BlockBody.raise_missing_variable_terminator(token, parse_context)
end end
def registered_tags def registered_tags

View File

@@ -27,18 +27,37 @@ module Liquid
end, end,
} }
class MethodLiteral
attr_reader :method_name, :to_s
def initialize(method_name, to_s)
@method_name = method_name
@to_s = to_s
end
end
@@method_literals = {
'blank' => MethodLiteral.new(:blank?, '').freeze,
'empty' => MethodLiteral.new(:empty?, '').freeze,
}
def self.operators def self.operators
@@operators @@operators
end end
def self.parse_expression(markup)
@@method_literals[markup] || Expression.parse(markup)
end
attr_reader :attachment, :child_condition attr_reader :attachment, :child_condition
attr_accessor :left, :operator, :right attr_accessor :left, :operator, :right
def initialize(left = nil, operator = nil, right = nil) def initialize(left = nil, operator = nil, right = nil)
@left = left @left = left
@operator = operator @operator = operator
@right = right @right = right
@child_relation = nil
@child_relation = nil
@child_condition = nil @child_condition = nil
end end
@@ -62,12 +81,12 @@ module Liquid
end end
def or(condition) def or(condition)
@child_relation = :or @child_relation = :or
@child_condition = condition @child_condition = condition
end end
def and(condition) def and(condition)
@child_relation = :and @child_relation = :and
@child_condition = condition @child_condition = condition
end end
@@ -90,7 +109,7 @@ module Liquid
private private
def equal_variables(left, right) def equal_variables(left, right)
if left.is_a?(Liquid::Expression::MethodLiteral) if left.is_a?(MethodLiteral)
if right.respond_to?(left.method_name) if right.respond_to?(left.method_name)
return right.send(left.method_name) return right.send(left.method_name)
else else
@@ -98,7 +117,7 @@ module Liquid
end end
end end
if right.is_a?(Liquid::Expression::MethodLiteral) if right.is_a?(MethodLiteral)
if left.respond_to?(right.method_name) if left.respond_to?(right.method_name)
return left.send(right.method_name) return left.send(right.method_name)
else else
@@ -115,7 +134,7 @@ module Liquid
# return this as the result. # return this as the result.
return context.evaluate(left) if op.nil? return context.evaluate(left) if op.nil?
left = context.evaluate(left) left = context.evaluate(left)
right = context.evaluate(right) right = context.evaluate(right)
operation = self.class.operators[op] || raise(Liquid::ArgumentError, "Unknown operator #{op}") operation = self.class.operators[op] || raise(Liquid::ArgumentError, "Unknown operator #{op}")

View File

@@ -41,9 +41,10 @@ module Liquid
self.exception_renderer = ->(_e) { raise } self.exception_renderer = ->(_e) { raise }
end end
@interrupts = [] @interrupts = []
@filters = [] @filters = []
@global_filter = nil @global_filter = nil
@disabled_tags = {}
end end
# rubocop:enable Metrics/ParameterLists # rubocop:enable Metrics/ParameterLists
@@ -52,7 +53,7 @@ module Liquid
end end
def strainer def strainer
@strainer ||= Strainer.create(self, @filters) @strainer ||= StrainerFactory.create(self, @filters)
end end
# Adds filters to this context. # Adds filters to this context.
@@ -87,7 +88,7 @@ module Liquid
def handle_error(e, line_number = nil) def handle_error(e, line_number = nil)
e = internal_error unless e.is_a?(Liquid::Error) e = internal_error unless e.is_a?(Liquid::Error)
e.template_name ||= template_name e.template_name ||= template_name
e.line_number ||= line_number e.line_number ||= line_number
errors.push(e) errors.push(e)
exception_renderer.call(e).to_s exception_renderer.call(e).to_s
end end
@@ -138,12 +139,13 @@ module Liquid
static_environments: static_environments, static_environments: static_environments,
registers: StaticRegisters.new(registers) registers: StaticRegisters.new(registers)
).tap do |subcontext| ).tap do |subcontext|
subcontext.base_scope_depth = base_scope_depth + 1 subcontext.base_scope_depth = base_scope_depth + 1
subcontext.exception_renderer = exception_renderer subcontext.exception_renderer = exception_renderer
subcontext.filters = @filters subcontext.filters = @filters
subcontext.strainer = nil subcontext.strainer = nil
subcontext.errors = errors subcontext.errors = errors
subcontext.warnings = warnings subcontext.warnings = warnings
subcontext.disabled_tags = @disabled_tags
end end
end end
@@ -188,7 +190,7 @@ module Liquid
try_variable_find_in_environments(key, raise_on_not_found: raise_on_not_found) try_variable_find_in_environments(key, raise_on_not_found: raise_on_not_found)
end end
variable = variable.to_liquid variable = variable.to_liquid
variable.context = self if variable.respond_to?(:context=) variable.context = self if variable.respond_to?(:context=)
variable variable
@@ -208,9 +210,24 @@ module Liquid
end end
end end
def with_disabled_tags(tag_names)
tag_names.each do |name|
@disabled_tags[name] = @disabled_tags.fetch(name, 0) + 1
end
yield
ensure
tag_names.each do |name|
@disabled_tags[name] -= 1
end
end
def tag_disabled?(tag_name)
@disabled_tags.fetch(tag_name, 0) > 0
end
protected protected
attr_writer :base_scope_depth, :warnings, :errors, :strainer, :filters attr_writer :base_scope_depth, :warnings, :errors, :strainer, :filters, :disabled_tags
private private

View File

@@ -1,23 +1,33 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
class Document < BlockBody class Document
def self.parse(tokens, parse_context) def self.parse(tokens, parse_context)
doc = new doc = new(parse_context)
doc.parse(tokens, parse_context) doc.parse(tokens, parse_context)
doc doc
end end
def parse(tokens, parse_context) attr_reader :parse_context, :body
super do |end_tag_name, _end_tag_params|
unknown_tag(end_tag_name, parse_context) if end_tag_name def initialize(parse_context)
@parse_context = parse_context
@body = new_body
end
def nodelist
@body.nodelist
end
def parse(tokenizer, parse_context)
while parse_body(tokenizer)
end end
rescue SyntaxError => e rescue SyntaxError => e
e.line_number ||= parse_context.line_number e.line_number ||= parse_context.line_number
raise raise
end end
def unknown_tag(tag, parse_context) def unknown_tag(tag, _markup, _tokenizer)
case tag case tag
when 'else', 'end' when 'else', 'end'
raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_outer_tag", tag: tag) raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_outer_tag", tag: tag)
@@ -25,5 +35,30 @@ module Liquid
raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag) raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag)
end end
end end
def render_to_output_buffer(context, output)
@body.render_to_output_buffer(context, output)
end
def render(context)
@body.render(context)
end
private
def new_body
parse_context.new_block_body
end
def parse_body(tokenizer)
@body.parse(tokenizer, parse_context) do |unknown_tag_name, unknown_tag_markup|
if unknown_tag_name
unknown_tag(unknown_tag_name, unknown_tag_markup, tokenizer)
true
else
false
end
end
end
end end
end end

View File

@@ -40,19 +40,19 @@ module Liquid
end end
end end
ArgumentError = Class.new(Error) ArgumentError = Class.new(Error)
ContextError = Class.new(Error) ContextError = Class.new(Error)
FileSystemError = Class.new(Error) FileSystemError = Class.new(Error)
StandardError = Class.new(Error) StandardError = Class.new(Error)
SyntaxError = Class.new(Error) SyntaxError = Class.new(Error)
StackLevelError = Class.new(Error) StackLevelError = Class.new(Error)
TaintedError = Class.new(Error) MemoryError = Class.new(Error)
MemoryError = Class.new(Error) ZeroDivisionError = Class.new(Error)
ZeroDivisionError = Class.new(Error) FloatDomainError = Class.new(Error)
FloatDomainError = Class.new(Error) UndefinedVariable = Class.new(Error)
UndefinedVariable = Class.new(Error)
UndefinedDropMethod = Class.new(Error) UndefinedDropMethod = Class.new(Error)
UndefinedFilter = Class.new(Error) UndefinedFilter = Class.new(Error)
MethodOverrideError = Class.new(Error) MethodOverrideError = Class.new(Error)
InternalError = Class.new(Error) DisabledError = Class.new(Error)
InternalError = Class.new(Error)
end end

View File

@@ -2,25 +2,12 @@
module Liquid module Liquid
class Expression class Expression
class MethodLiteral
attr_reader :method_name, :to_s
def initialize(method_name, to_s)
@method_name = method_name
@to_s = to_s
end
def to_liquid
to_s
end
end
LITERALS = { LITERALS = {
nil => nil, 'nil' => nil, 'null' => nil, '' => nil, nil => nil, 'nil' => nil, 'null' => nil, '' => nil,
'true' => true, 'true' => true,
'false' => false, 'false' => false,
'blank' => MethodLiteral.new(:blank?, '').freeze, 'blank' => '',
'empty' => MethodLiteral.new(:empty?, '').freeze 'empty' => ''
}.freeze }.freeze
SINGLE_QUOTED_STRING = /\A'(.*)'\z/m SINGLE_QUOTED_STRING = /\A'(.*)'\z/m

View File

@@ -47,7 +47,7 @@ module Liquid
attr_accessor :root attr_accessor :root
def initialize(root, pattern = "_%s.liquid") def initialize(root, pattern = "_%s.liquid")
@root = root @root = root
@pattern = pattern @pattern = pattern
end end

View File

@@ -3,10 +3,10 @@
module Liquid module Liquid
class ForloopDrop < Drop class ForloopDrop < Drop
def initialize(name, length, parentloop) def initialize(name, length, parentloop)
@name = name @name = name
@length = length @length = length
@parentloop = parentloop @parentloop = parentloop
@index = 0 @index = 0
end end
attr_reader :name, :length, :parentloop attr_reader :name, :length, :parentloop

View File

@@ -15,12 +15,12 @@ module Liquid
'?' => :question, '?' => :question,
'-' => :dash, '-' => :dash,
}.freeze }.freeze
IDENTIFIER = /[a-zA-Z_][\w-]*\??/ IDENTIFIER = /[a-zA-Z_][\w-]*\??/
SINGLE_STRING_LITERAL = /'[^\']*'/ SINGLE_STRING_LITERAL = /'[^\']*'/
DOUBLE_STRING_LITERAL = /"[^\"]*"/ DOUBLE_STRING_LITERAL = /"[^\"]*"/
NUMBER_LITERAL = /-?\d+(\.\d+)?/ NUMBER_LITERAL = /-?\d+(\.\d+)?/
DOTDOT = /\.\./ DOTDOT = /\.\./
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/ COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/
WHITESPACE_OR_NOTHING = /\s*/ WHITESPACE_OR_NOTHING = /\s*/
def initialize(input) def initialize(input)
@@ -33,7 +33,7 @@ module Liquid
until @ss.eos? until @ss.eos?
@ss.skip(WHITESPACE_OR_NOTHING) @ss.skip(WHITESPACE_OR_NOTHING)
break if @ss.eos? break if @ss.eos?
tok = if (t = @ss.scan(COMPARISON_OPERATOR)) tok = if (t = @ss.scan(COMPARISON_OPERATOR))
[:comparison, t] [:comparison, t]
elsif (t = @ss.scan(SINGLE_STRING_LITERAL)) elsif (t = @ss.scan(SINGLE_STRING_LITERAL))
[:string, t] [:string, t]
@@ -46,7 +46,7 @@ module Liquid
elsif (t = @ss.scan(DOTDOT)) elsif (t = @ss.scan(DOTDOT))
[:dotdot, t] [:dotdot, t]
else else
c = @ss.getch c = @ss.getch
if (s = SPECIALS[c]) if (s = SPECIALS[c])
[s, c] [s, c]
else else

View File

@@ -7,9 +7,11 @@ module Liquid
def initialize(options = {}) def initialize(options = {})
@template_options = options ? options.dup : {} @template_options = options ? options.dup : {}
@locale = @template_options[:locale] ||= I18n.new
@locale = @template_options[:locale] ||= I18n.new
@warnings = [] @warnings = []
self.depth = 0
self.depth = 0
self.partial = false self.partial = false
end end
@@ -17,9 +19,14 @@ module Liquid
@options[option_key] @options[option_key]
end end
def new_block_body
Liquid::BlockBody.new
end
def partial=(value) def partial=(value)
@partial = value @partial = value
@options = value ? partial_options : @template_options @options = value ? partial_options : @template_options
@error_mode = @options[:error_mode] || Template.error_mode @error_mode = @options[:error_mode] || Template.error_mode
end end

View File

@@ -11,7 +11,7 @@ module Liquid
end end
def initialize(node, callbacks) def initialize(node, callbacks)
@node = node @node = node
@callbacks = callbacks @callbacks = callbacks
end end

View File

@@ -3,9 +3,9 @@
module Liquid module Liquid
class Parser class Parser
def initialize(input) def initialize(input)
l = Lexer.new(input) l = Lexer.new(input)
@tokens = l.tokenize @tokens = l.tokenize
@p = 0 # pointer to current location @p = 0 # pointer to current location
end end
def jump(point) def jump(point)

View File

@@ -8,10 +8,10 @@ module Liquid
when :lax then lax_parse(markup) when :lax then lax_parse(markup)
when :warn when :warn
begin begin
return strict_parse_with_error_context(markup) strict_parse_with_error_context(markup)
rescue SyntaxError => e rescue SyntaxError => e
parse_context.warnings << e parse_context.warnings << e
return lax_parse(markup) lax_parse(markup)
end end
end end
end end
@@ -21,7 +21,7 @@ module Liquid
def strict_parse_with_error_context(markup) def strict_parse_with_error_context(markup)
strict_parse(markup) strict_parse(markup)
rescue SyntaxError => e rescue SyntaxError => e
e.line_number = line_number e.line_number = line_number
e.markup_context = markup_context(markup) e.markup_context = markup_context(markup)
raise e raise e
end end

View File

@@ -8,10 +8,14 @@ module Liquid
return cached if cached return cached if cached
file_system = (context.registers[:file_system] ||= Liquid::Template.file_system) file_system = (context.registers[:file_system] ||= Liquid::Template.file_system)
source = file_system.read_template_file(template_name) source = file_system.read_template_file(template_name)
parse_context.partial = true parse_context.partial = true
partial = Liquid::Template.parse(source, parse_context) template_factory = (context.registers[:template_factory] ||= Liquid::TemplateFactory.new)
template = template_factory.for(template_name)
partial = template.parse(source, parse_context)
cached_partials[template_name] = partial cached_partials[template_name] = partial
ensure ensure
parse_context.partial = false parse_context.partial = false

View File

@@ -64,7 +64,7 @@ module Liquid
end end
def finish def finish
@end_time = Time.now @end_time = Time.now
@total_time = @end_time - @start_time @total_time = @end_time - @start_time
if @children.empty? if @children.empty?
@@ -112,11 +112,11 @@ module Liquid
def initialize(partial_name = "<root>") def initialize(partial_name = "<root>")
@partial_stack = [partial_name] @partial_stack = [partial_name]
@root_timing = Timing.new("", current_partial) @root_timing = Timing.new("", current_partial)
@timing_stack = [@root_timing] @timing_stack = [@root_timing]
@render_start_at = Time.now @render_start_at = Time.now
@render_end_at = @render_start_at @render_end_at = @render_start_at
end end
def start def start

View File

@@ -1,25 +1,21 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
class BlockBody module BlockBodyProfilingHook
def render_node_with_profiling(context, output, node) def render_node(context, output, node)
Profiler.profile_node_render(node) do Profiler.profile_node_render(node) do
render_node_without_profiling(context, output, node) super
end end
end end
alias_method :render_node_without_profiling, :render_node
alias_method :render_node, :render_node_with_profiling
end end
BlockBody.prepend(BlockBodyProfilingHook)
class Include < Tag module IncludeProfilingHook
def render_to_output_buffer_with_profiling(context, output) def render_to_output_buffer(context, output)
Profiler.profile_children(context.evaluate(@template_name_expr).to_s) do Profiler.profile_children(context.evaluate(@template_name_expr).to_s) do
render_to_output_buffer_without_profiling(context, output) super
end end
end end
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
Include.prepend(IncludeProfilingHook)
end end

View File

@@ -4,7 +4,7 @@ module Liquid
class RangeLookup class RangeLookup
def self.parse(start_markup, end_markup) def self.parse(start_markup, end_markup)
start_obj = Expression.parse(start_markup) start_obj = Expression.parse(start_markup)
end_obj = Expression.parse(end_markup) end_obj = Expression.parse(end_markup)
if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate) if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate)
new(start_obj, end_obj) new(start_obj, end_obj)
else else
@@ -14,12 +14,12 @@ module Liquid
def initialize(start_obj, end_obj) def initialize(start_obj, end_obj)
@start_obj = start_obj @start_obj = start_obj
@end_obj = end_obj @end_obj = end_obj
end end
def evaluate(context) def evaluate(context)
start_int = to_integer(context.evaluate(@start_obj)) start_int = to_integer(context.evaluate(@start_obj))
end_int = to_integer(context.evaluate(@end_obj)) end_int = to_integer(context.evaluate(@end_obj))
start_int..end_int start_int..end_int
end end

View File

@@ -1,32 +0,0 @@
# frozen_string_literal: true
module Liquid
class DisabledTags < Register
def initialize
@disabled_tags = {}
end
def disabled?(tag)
@disabled_tags.key?(tag) && @disabled_tags[tag] > 0
end
def disable(tags)
tags.each(&method(:increment))
yield
ensure
tags.each(&method(:decrement))
end
private
def increment(tag)
@disabled_tags[tag] ||= 0
@disabled_tags[tag] += 1
end
def decrement(tag)
@disabled_tags[tag] -= 1
end
end
Template.add_register(:disabled_tags, DisabledTags.new)
end

View File

@@ -2,24 +2,61 @@
module Liquid module Liquid
class ResourceLimits class ResourceLimits
attr_accessor :render_length, :render_score, :assign_score, attr_accessor :render_length_limit, :render_score_limit, :assign_score_limit
:render_length_limit, :render_score_limit, :assign_score_limit attr_reader :render_score, :assign_score
def initialize(limits) def initialize(limits)
@render_length_limit = limits[:render_length_limit] @render_length_limit = limits[:render_length_limit]
@render_score_limit = limits[:render_score_limit] @render_score_limit = limits[:render_score_limit]
@assign_score_limit = limits[:assign_score_limit] @assign_score_limit = limits[:assign_score_limit]
reset reset
end end
def increment_render_score(amount)
@render_score += amount
raise_limits_reached if @render_score_limit && @render_score > @render_score_limit
end
def increment_assign_score(amount)
@assign_score += amount
raise_limits_reached if @assign_score_limit && @assign_score > @assign_score_limit
end
# update either render_length or assign_score based on whether or not the writes are captured
def increment_write_score(output)
if (last_captured = @last_capture_length)
captured = output.bytesize
increment = captured - last_captured
@last_capture_length = captured
increment_assign_score(increment)
elsif @render_length_limit && output.bytesize > @render_length_limit
raise_limits_reached
end
end
def raise_limits_reached
@reached_limit = true
raise MemoryError, "Memory limits exceeded"
end
def reached? def reached?
(@render_length_limit && @render_length > @render_length_limit) || @reached_limit
(@render_score_limit && @render_score > @render_score_limit) ||
(@assign_score_limit && @assign_score > @assign_score_limit)
end end
def reset def reset
@render_length = @render_score = @assign_score = 0 @reached_limit = false
@last_capture_length = nil
@render_score = @assign_score = 0
end
def with_capture
old_capture_length = @last_capture_length
begin
@last_capture_length = 0
yield
ensure
@last_capture_length = old_capture_length
end
end end
end end
end end

View File

@@ -13,7 +13,7 @@ module Liquid
"'" => '&#39;', "'" => '&#39;',
}.freeze }.freeze
HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/ HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/
STRIP_HTML_BLOCKS = Regexp.union( STRIP_HTML_BLOCKS = Regexp.union(
%r{<script.*?</script>}m, %r{<script.*?</script>}m,
/<!--.*?-->/m, /<!--.*?-->/m,
%r{<style.*?</style>}m %r{<style.*?</style>}m
@@ -41,7 +41,7 @@ module Liquid
end end
def escape(input) def escape(input)
CGI.escapeHTML(input.to_s).untaint unless input.nil? CGI.escapeHTML(input.to_s) unless input.nil?
end end
alias_method :h, :escape alias_method :h, :escape
@@ -77,19 +77,24 @@ module Liquid
def truncate(input, length = 50, truncate_string = "...") def truncate(input, length = 50, truncate_string = "...")
return if input.nil? return if input.nil?
input_str = input.to_s input_str = input.to_s
length = Utils.to_integer(length) length = Utils.to_integer(length)
truncate_string_str = truncate_string.to_s truncate_string_str = truncate_string.to_s
l = length - truncate_string_str.length l = length - truncate_string_str.length
l = 0 if l < 0 l = 0 if l < 0
input_str.length > length ? input_str[0...l].concat(truncate_string_str) : input_str input_str.length > length ? input_str[0...l].concat(truncate_string_str) : input_str
end end
def truncatewords(input, words = 15, truncate_string = "...") def truncatewords(input, words = 15, truncate_string = "...")
return if input.nil? return if input.nil?
wordlist = input.to_s.split wordlist = input.to_s.split
words = Utils.to_integer(words) words = Utils.to_integer(words)
l = words - 1 l = words - 1
l = 0 if l < 0 l = 0 if l < 0
wordlist.length > l ? wordlist[0..l].join(" ").concat(truncate_string.to_s) : input wordlist.length > l ? wordlist[0..l].join(" ").concat(truncate_string.to_s) : input
end end
@@ -115,7 +120,7 @@ module Liquid
end end
def strip_html(input) def strip_html(input)
empty = '' empty = ''
result = input.to_s.gsub(STRIP_HTML_BLOCKS, empty) result = input.to_s.gsub(STRIP_HTML_BLOCKS, empty)
result.gsub!(STRIP_HTML_TAGS, empty) result.gsub!(STRIP_HTML_TAGS, empty)
result result
@@ -128,13 +133,13 @@ module Liquid
# Join elements of the array with certain character between them # Join elements of the array with certain character between them
def join(input, glue = ' ') def join(input, glue = ' ')
InputIterator.new(input).join(glue) InputIterator.new(input, context).join(glue)
end end
# Sort elements of the array # Sort elements of the array
# provide optional property with which to sort an array of hashes or drops # provide optional property with which to sort an array of hashes or drops
def sort(input, property = nil) def sort(input, property = nil)
ary = InputIterator.new(input) ary = InputIterator.new(input, context)
return [] if ary.empty? return [] if ary.empty?
@@ -154,7 +159,7 @@ module Liquid
# Sort elements of an array ignoring case if strings # Sort elements of an array ignoring case if strings
# provide optional property with which to sort an array of hashes or drops # provide optional property with which to sort an array of hashes or drops
def sort_natural(input, property = nil) def sort_natural(input, property = nil)
ary = InputIterator.new(input) ary = InputIterator.new(input, context)
return [] if ary.empty? return [] if ary.empty?
@@ -174,7 +179,7 @@ module Liquid
# Filter the elements of an array to those with a certain property value. # Filter the elements of an array to those with a certain property value.
# By default the target is any truthy value. # By default the target is any truthy value.
def where(input, property, target_value = nil) def where(input, property, target_value = nil)
ary = InputIterator.new(input) ary = InputIterator.new(input, context)
if ary.empty? if ary.empty?
[] []
@@ -196,7 +201,7 @@ module Liquid
# Remove duplicate elements from an array # Remove duplicate elements from an array
# provide optional property with which to determine uniqueness # provide optional property with which to determine uniqueness
def uniq(input, property = nil) def uniq(input, property = nil)
ary = InputIterator.new(input) ary = InputIterator.new(input, context)
if property.nil? if property.nil?
ary.uniq ary.uniq
@@ -213,13 +218,13 @@ module Liquid
# Reverse the elements of an array # Reverse the elements of an array
def reverse(input) def reverse(input)
ary = InputIterator.new(input) ary = InputIterator.new(input, context)
ary.reverse ary.reverse
end end
# map/collect on a given property # map/collect on a given property
def map(input, property) def map(input, property)
InputIterator.new(input).map do |e| InputIterator.new(input, context).map do |e|
e = e.call if e.is_a?(Proc) e = e.call if e.is_a?(Proc)
if property == "to_liquid" if property == "to_liquid"
@@ -236,7 +241,7 @@ module Liquid
# Remove nils within an array # Remove nils within an array
# provide optional property with which to check for nil # provide optional property with which to check for nil
def compact(input, property = nil) def compact(input, property = nil)
ary = InputIterator.new(input) ary = InputIterator.new(input, context)
if property.nil? if property.nil?
ary.compact ary.compact
@@ -280,7 +285,7 @@ module Liquid
unless array.respond_to?(:to_ary) unless array.respond_to?(:to_ary)
raise ArgumentError, "concat filter requires an array argument" raise ArgumentError, "concat filter requires an array argument"
end end
InputIterator.new(input).concat(array) InputIterator.new(input, context).concat(array)
end end
# prepend a string to another # prepend a string to another
@@ -421,17 +426,26 @@ module Liquid
result.is_a?(BigDecimal) ? result.to_f : result result.is_a?(BigDecimal) ? result.to_f : result
end end
def default(input, default_value = '') # Set a default value when the input is nil, false or empty
if !input || input.respond_to?(:empty?) && input.empty? #
Usage.increment("default_filter_received_false_value") if input == false # See https://github.com/Shopify/liquid/issues/1127 # Example:
default_value # {{ product.title | default: "No Title" }}
else #
input # Use `allow_false` when an input should only be tested against nil or empty and not false.
end #
# Example:
# {{ product.title | default: "No Title", allow_false: true }}
#
def default(input, default_value = '', options = {})
options = {} unless options.is_a?(Hash)
false_check = options['allow_false'] ? input.nil? : !input
false_check || (input.respond_to?(:empty?) && input.empty?) ? default_value : input
end end
private private
attr_reader :context
def raise_property_error(property) def raise_property_error(property)
raise Liquid::ArgumentError, "cannot select the property '#{property}'" raise Liquid::ArgumentError, "cannot select the property '#{property}'"
end end
@@ -460,8 +474,9 @@ module Liquid
class InputIterator class InputIterator
include Enumerable include Enumerable
def initialize(input) def initialize(input, context)
@input = if input.is_a?(Array) @context = context
@input = if input.is_a?(Array)
input.flatten input.flatten
elsif input.is_a?(Hash) elsif input.is_a?(Hash)
[input] [input]
@@ -499,6 +514,7 @@ module Liquid
def each def each
@input.each do |e| @input.each do |e|
e.context = @context if e.respond_to?(:context=)
yield(e.respond_to?(:to_liquid) ? e.to_liquid : e) yield(e.respond_to?(:to_liquid) ? e.to_liquid : e)
end end
end end

View File

@@ -2,10 +2,10 @@
module Liquid module Liquid
class StaticRegisters class StaticRegisters
attr_reader :static, :registers attr_reader :static
def initialize(registers = {}) def initialize(registers = {})
@static = registers.is_a?(StaticRegisters) ? registers.static : registers @static = registers.is_a?(StaticRegisters) ? registers.static : registers
@registers = {} @registers = {}
end end
@@ -25,8 +25,16 @@ module Liquid
@registers.delete(key) @registers.delete(key)
end end
def fetch(key, default = nil) UNDEFINED = Object.new
key?(key) ? self[key] : default
def fetch(key, default = UNDEFINED, &block)
if @registers.key?(key)
@registers.fetch(key)
elsif default != UNDEFINED
@static.fetch(key, default, &block)
else
@static.fetch(key, &block)
end
end end
def key?(key) def key?(key)

View File

@@ -1,68 +0,0 @@
# frozen_string_literal: true
require 'set'
module Liquid
# Strainer is the parent class for the filters system.
# New filters are mixed into the strainer class which is then instantiated for each liquid template render run.
#
# The Strainer only allows method calls defined in filters given to it via Strainer.global_filter,
# Context#add_filters or Template.register_filter
class Strainer #:nodoc:
@@global_strainer = Class.new(Strainer) do
@filter_methods = Set.new
end
@@strainer_class_cache = Hash.new do |hash, filters|
hash[filters] = Class.new(@@global_strainer) do
@filter_methods = @@global_strainer.filter_methods.dup
filters.each { |f| add_filter(f) }
end
end
def initialize(context)
@context = context
end
class << self
attr_reader :filter_methods
end
def self.add_filter(filter)
raise ArgumentError, "Expected module but got: #{filter.class}" unless filter.is_a?(Module)
unless include?(filter)
invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }
if invokable_non_public_methods.any?
raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"
else
send(:include, filter)
@filter_methods.merge(filter.public_instance_methods.map(&:to_s))
end
end
end
def self.global_filter(filter)
@@strainer_class_cache.clear
@@global_strainer.add_filter(filter)
end
def self.invokable?(method)
@filter_methods.include?(method.to_s)
end
def self.create(context, filters = [])
@@strainer_class_cache[filters].new(context)
end
def invoke(method, *args)
if self.class.invokable?(method)
send(method, *args)
elsif @context&.strict_filters
raise Liquid::UndefinedFilter, "undefined filter #{method}"
else
args.first
end
rescue ::ArgumentError => e
raise Liquid::ArgumentError, e.message, e.backtrace
end
end
end

View File

@@ -0,0 +1,36 @@
# frozen_string_literal: true
module Liquid
# StrainerFactory is the factory for the filters system.
module StrainerFactory
extend self
def add_global_filter(filter)
strainer_class_cache.clear
global_filters << filter
end
def create(context, filters = [])
strainer_from_cache(filters).new(context)
end
private
def global_filters
@global_filters ||= []
end
def strainer_from_cache(filters)
strainer_class_cache[filters] ||= begin
klass = Class.new(StrainerTemplate)
global_filters.each { |f| klass.add_filter(f) }
filters.each { |f| klass.add_filter(f) }
klass
end
end
def strainer_class_cache
@strainer_class_cache ||= {}
end
end
end

View File

@@ -0,0 +1,53 @@
# frozen_string_literal: true
require 'set'
module Liquid
# StrainerTemplate is the computed class for the filters system.
# New filters are mixed into the strainer class which is then instantiated for each liquid template render run.
#
# The Strainer only allows method calls defined in filters given to it via StrainerFactory.add_global_filter,
# Context#add_filters or Template.register_filter
class StrainerTemplate
def initialize(context)
@context = context
end
class << self
def add_filter(filter)
return if include?(filter)
invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }
if invokable_non_public_methods.any?
raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"
end
include(filter)
filter_methods.merge(filter.public_instance_methods.map(&:to_s))
end
def invokable?(method)
filter_methods.include?(method.to_s)
end
private
def filter_methods
@filter_methods ||= Set.new
end
end
def invoke(method, *args)
if self.class.invokable?(method)
send(method, *args)
elsif @context.strict_filters
raise Liquid::UndefinedFilter, "undefined filter #{method}"
else
args.first
end
rescue ::ArgumentError => e
raise Liquid::ArgumentError, e.message, e.backtrace
end
end
end

View File

@@ -4,10 +4,10 @@ module Liquid
class TablerowloopDrop < Drop class TablerowloopDrop < Drop
def initialize(length, cols) def initialize(length, cols)
@length = length @length = length
@row = 1 @row = 1
@col = 1 @col = 1
@cols = cols @cols = cols
@index = 0 @index = 0
end end
attr_reader :length, :col, :row attr_reader :length, :col, :row

View File

@@ -13,22 +13,20 @@ module Liquid
tag tag
end end
def disable_tags(*tags) def disable_tags(*tag_names)
disabled_tags.push(*tags) @disabled_tags ||= []
@disabled_tags.concat(tag_names)
prepend(Disabler)
end end
private :new private :new
def disabled_tags
@disabled_tags ||= []
end
end end
def initialize(tag_name, markup, parse_context) def initialize(tag_name, markup, parse_context)
@tag_name = tag_name @tag_name = tag_name
@markup = markup @markup = markup
@parse_context = parse_context @parse_context = parse_context
@line_number = parse_context.line_number @line_number = parse_context.line_number
end end
def parse(_tokens) def parse(_tokens)
@@ -46,14 +44,6 @@ module Liquid
'' ''
end end
def disabled?(context)
context.registers[:disabled_tags].disabled?(tag_name)
end
def disabled_error_message
"#{tag_name} #{options[:locale].t('errors.disabled.tag')}"
end
# For backwards compatibility with custom tags. In a future release, the semantics # 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` # of the `render_to_output_buffer` method will become the default and the `render`
# method will be removed. # method will be removed.
@@ -65,9 +55,5 @@ module Liquid
def blank? def blank?
false false
end end
def disabled_tags
self.class.disabled_tags
end
end end
end end

View File

@@ -0,0 +1,22 @@
# frozen_string_literal: true
module Liquid
class Tag
module Disableable
def render_to_output_buffer(context, output)
if context.tag_disabled?(tag_name)
output << disabled_error(context)
return
end
super
end
def disabled_error(context)
# raise then rescue the exception so that the Context#exception_renderer can re-raise it
raise DisabledError, "#{tag_name} #{parse_context[:locale].t('errors.disabled.tag')}"
rescue DisabledError => exc
context.handle_error(exc, line_number)
end
end
end
end

View File

@@ -0,0 +1,21 @@
# frozen_string_literal: true
module Liquid
class Tag
module Disabler
module ClassMethods
attr_reader :disabled_tags
end
def self.prepended(base)
base.extend(ClassMethods)
end
def render_to_output_buffer(context, output)
context.with_disabled_tags(self.class.disabled_tags) do
super
end
end
end
end
end

View File

@@ -12,26 +12,22 @@ module Liquid
class Assign < Tag class Assign < Tag
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
def self.syntax_error_translation_key
"errors.syntax.assign"
end
attr_reader :to, :from attr_reader :to, :from
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
if markup =~ Syntax if markup =~ Syntax
@to = Regexp.last_match(1) @to = Regexp.last_match(1)
@from = Variable.new(Regexp.last_match(2), options) @from = Variable.new(Regexp.last_match(2), options)
else else
raise SyntaxError, options[:locale].t(self.class.syntax_error_translation_key) raise SyntaxError, options[:locale].t('errors.syntax.assign')
end end
end end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
val = @from.render(context) val = @from.render(context)
context.scopes.last[@to] = val context.scopes.last[@to] = val
context.resource_limits.assign_score += assign_score_of(val) context.resource_limits.increment_assign_score(assign_score_of(val))
output output
end end

View File

@@ -11,8 +11,11 @@ module Liquid
# {% endfor %} # {% endfor %}
# #
class Break < Tag class Break < Tag
def interrupt INTERRUPT = BreakInterrupt.new.freeze
BreakInterrupt.new
def render_to_output_buffer(context, output)
context.push_interrupt(INTERRUPT)
output
end end
end end

View File

@@ -25,10 +25,10 @@ module Liquid
end end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
previous_output_size = output.bytesize context.resource_limits.with_capture do
super capture_output = render(context)
context.scopes.last[@to] = output context.scopes.last[@to] = capture_output
context.resource_limits.assign_score += (output.bytesize - previous_output_size) end
output output
end end

View File

@@ -19,8 +19,11 @@ module Liquid
end end
def parse(tokens) def parse(tokens)
body = BlockBody.new body = new_body
body = @blocks.last.attachment while parse_body(body, tokens) body = @blocks.last.attachment while parse_body(body, tokens)
if blank?
@blocks.each { |condition| condition.attachment.remove_blank_strings }
end
end end
def nodelist def nodelist
@@ -56,7 +59,7 @@ module Liquid
private private
def record_when_condition(markup) def record_when_condition(markup)
body = BlockBody.new body = new_body
while markup while markup
unless markup =~ WhenSyntax unless markup =~ WhenSyntax
@@ -65,7 +68,7 @@ module Liquid
markup = Regexp.last_match(2) markup = Regexp.last_match(2)
block = Condition.new(@left, '==', Expression.parse(Regexp.last_match(1))) block = Condition.new(@left, '==', Condition.parse_expression(Regexp.last_match(1)))
block.attach(body) block.attach(body)
@blocks << block @blocks << block
end end
@@ -77,7 +80,7 @@ module Liquid
end end
block = ElseCondition.new block = ElseCondition.new
block.attach(BlockBody.new) block.attach(new_body)
@blocks << block @blocks << block
end end

View File

@@ -11,8 +11,11 @@ module Liquid
# {% endfor %} # {% endfor %}
# #
class Continue < Tag class Continue < Tag
def interrupt INTERRUPT = ContinueInterrupt.new.freeze
ContinueInterrupt.new
def render_to_output_buffer(context, output)
context.push_interrupt(INTERRUPT)
output
end end
end end

View File

@@ -24,10 +24,10 @@ module Liquid
case markup case markup
when NamedSyntax when NamedSyntax
@variables = variables_from_string(Regexp.last_match(2)) @variables = variables_from_string(Regexp.last_match(2))
@name = Expression.parse(Regexp.last_match(1)) @name = Expression.parse(Regexp.last_match(1))
when SimpleSyntax when SimpleSyntax
@variables = variables_from_string(markup) @variables = variables_from_string(markup)
@name = @variables.to_s @name = @variables.to_s
else else
raise SyntaxError, options[:locale].t("errors.syntax.cycle") raise SyntaxError, options[:locale].t("errors.syntax.cycle")
end end
@@ -36,7 +36,7 @@ module Liquid
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
context.registers[:cycle] ||= {} context.registers[:cycle] ||= {}
key = context.evaluate(@name) key = context.evaluate(@name)
iteration = context.registers[:cycle][key].to_i iteration = context.registers[:cycle][key].to_i
val = context.evaluate(@variables[iteration]) val = context.evaluate(@variables[iteration])
@@ -50,9 +50,9 @@ module Liquid
output << val output << val
iteration += 1 iteration += 1
iteration = 0 if iteration >= @variables.size iteration = 0 if iteration >= @variables.size
context.registers[:cycle][key] = iteration
context.registers[:cycle][key] = iteration
output output
end end

View File

@@ -54,13 +54,18 @@ module Liquid
super super
@from = @limit = nil @from = @limit = nil
parse_with_selected_parser(markup) parse_with_selected_parser(markup)
@for_block = BlockBody.new @for_block = new_body
@else_block = nil @else_block = nil
end end
def parse(tokens) def parse(tokens)
return unless parse_body(@for_block, tokens) if parse_body(@for_block, tokens)
parse_body(@else_block, tokens) parse_body(@else_block, tokens)
end
if blank?
@for_block.remove_blank_strings
@else_block&.remove_blank_strings
end
end end
def nodelist def nodelist
@@ -69,7 +74,7 @@ module Liquid
def unknown_tag(tag, markup, tokens) def unknown_tag(tag, markup, tokens)
return super unless tag == 'else' return super unless tag == 'else'
@else_block = BlockBody.new @else_block = new_body
end end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
@@ -88,10 +93,10 @@ module Liquid
def lax_parse(markup) def lax_parse(markup)
if markup =~ Syntax if markup =~ Syntax
@variable_name = Regexp.last_match(1) @variable_name = Regexp.last_match(1)
collection_name = Regexp.last_match(2) collection_name = Regexp.last_match(2)
@reversed = !!Regexp.last_match(3) @reversed = !!Regexp.last_match(3)
@name = "#{@variable_name}-#{collection_name}" @name = "#{@variable_name}-#{collection_name}"
@collection_name = Expression.parse(collection_name) @collection_name = Expression.parse(collection_name)
markup.scan(TagAttributes) do |key, value| markup.scan(TagAttributes) do |key, value|
set_attribute(key, value) set_attribute(key, value)
@@ -105,9 +110,11 @@ module Liquid
p = Parser.new(markup) p = Parser.new(markup)
@variable_name = p.consume(:id) @variable_name = p.consume(:id)
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in') raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in')
collection_name = p.expression
@name = "#{@variable_name}-#{collection_name}" collection_name = p.expression
@collection_name = Expression.parse(collection_name) @collection_name = Expression.parse(collection_name)
@name = "#{@variable_name}-#{collection_name}"
@reversed = p.id?('reversed') @reversed = p.id?('reversed')
while p.look(:id) && p.look(:colon, 1) while p.look(:id) && p.look(:colon, 1)
@@ -156,7 +163,7 @@ module Liquid
def render_segment(context, output, segment) def render_segment(context, output, segment)
for_stack = context.registers[:for_stack] ||= [] for_stack = context.registers[:for_stack] ||= []
length = segment.length length = segment.length
context.stack do context.stack do
loop_vars = Liquid::ForloopDrop.new(@name, length, for_stack[-1]) loop_vars = Liquid::ForloopDrop.new(@name, length, for_stack[-1])

View File

@@ -12,9 +12,9 @@ module Liquid
# There are {% if count < 5 %} less {% else %} more {% endif %} items than you need. # There are {% if count < 5 %} less {% else %} more {% endif %} items than you need.
# #
class If < Block class If < Block
Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o
ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o
BOOLEAN_OPERATORS = %w(and or).freeze BOOLEAN_OPERATORS = %w(and or).freeze
attr_reader :blocks attr_reader :blocks
@@ -31,6 +31,9 @@ module Liquid
def parse(tokens) def parse(tokens)
while parse_body(@blocks.last.attachment, tokens) while parse_body(@blocks.last.attachment, tokens)
end end
if blank?
@blocks.each { |condition| condition.attachment.remove_blank_strings }
end
end end
def unknown_tag(tag, markup, tokens) def unknown_tag(tag, markup, tokens)
@@ -61,21 +64,25 @@ module Liquid
end end
@blocks.push(block) @blocks.push(block)
block.attach(BlockBody.new) block.attach(new_body)
end
def parse_expression(markup)
Condition.parse_expression(markup)
end end
def lax_parse(markup) def lax_parse(markup)
expressions = markup.scan(ExpressionsAndOperators) expressions = markup.scan(ExpressionsAndOperators)
raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop =~ Syntax raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop =~ Syntax
condition = Condition.new(Expression.parse(Regexp.last_match(1)), Regexp.last_match(2), Expression.parse(Regexp.last_match(3))) condition = Condition.new(parse_expression(Regexp.last_match(1)), Regexp.last_match(2), parse_expression(Regexp.last_match(3)))
until expressions.empty? until expressions.empty?
operator = expressions.pop.to_s.strip operator = expressions.pop.to_s.strip
raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop.to_s =~ Syntax raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop.to_s =~ Syntax
new_condition = Condition.new(Expression.parse(Regexp.last_match(1)), Regexp.last_match(2), Expression.parse(Regexp.last_match(3))) new_condition = Condition.new(parse_expression(Regexp.last_match(1)), Regexp.last_match(2), parse_expression(Regexp.last_match(3)))
raise SyntaxError, options[:locale].t("errors.syntax.if") unless BOOLEAN_OPERATORS.include?(operator) raise SyntaxError, options[:locale].t("errors.syntax.if") unless BOOLEAN_OPERATORS.include?(operator)
new_condition.send(operator, condition) new_condition.send(operator, condition)
condition = new_condition condition = new_condition
@@ -103,9 +110,9 @@ module Liquid
end end
def parse_comparison(p) def parse_comparison(p)
a = Expression.parse(p.expression) a = parse_expression(p.expression)
if (op = p.consume?(:comparison)) if (op = p.consume?(:comparison))
b = Expression.parse(p.expression) b = parse_expression(p.expression)
Condition.new(a, op, b) Condition.new(a, op, b)
else else
Condition.new(a) Condition.new(a)

View File

@@ -16,21 +16,25 @@ module Liquid
# {% include 'product' for products %} # {% include 'product' for products %}
# #
class Include < Tag class Include < Tag
Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/o prepend Tag::Disableable
SYNTAX = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
Syntax = SYNTAX
attr_reader :template_name_expr, :variable_name_expr, :attributes attr_reader :template_name_expr, :variable_name_expr, :attributes
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
if markup =~ Syntax if markup =~ SYNTAX
template_name = Regexp.last_match(1) template_name = Regexp.last_match(1)
variable_name = Regexp.last_match(3) variable_name = Regexp.last_match(3)
@alias_name = Regexp.last_match(5)
@variable_name_expr = variable_name ? Expression.parse(variable_name) : nil @variable_name_expr = variable_name ? Expression.parse(variable_name) : nil
@template_name_expr = Expression.parse(template_name) @template_name_expr = Expression.parse(template_name)
@attributes = {} @attributes = {}
markup.scan(TagAttributes) do |key, value| markup.scan(TagAttributes) do |key, value|
@attributes[key] = Expression.parse(value) @attributes[key] = Expression.parse(value)
@@ -54,7 +58,7 @@ module Liquid
parse_context: parse_context parse_context: parse_context
) )
context_variable_name = template_name.split('/').last context_variable_name = @alias_name || template_name.split('/').last
variable = if @variable_name_expr variable = if @variable_name_expr
context.evaluate(@variable_name_expr) context.evaluate(@variable_name_expr)
@@ -63,10 +67,10 @@ module Liquid
end end
old_template_name = context.template_name old_template_name = context.template_name
old_partial = context.partial old_partial = context.partial
begin begin
context.template_name = template_name context.template_name = template_name
context.partial = true context.partial = true
context.stack do context.stack do
@attributes.each do |key, value| @attributes.each do |key, value|
context[key] = context.evaluate(value) context[key] = context.evaluate(value)
@@ -84,7 +88,7 @@ module Liquid
end end
ensure ensure
context.template_name = old_template_name context.template_name = old_template_name
context.partial = old_partial context.partial = old_partial
end end
output output

View File

@@ -2,7 +2,8 @@
module Liquid module Liquid
class Render < Tag class Render < Tag
SYNTAX = /(#{QuotedString})#{QuotedFragment}*/o FOR = 'for'
SYNTAX = /(#{QuotedString}+)(\s+(with|#{FOR})\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
disable_tags "include" disable_tags "include"
@@ -14,8 +15,13 @@ module Liquid
raise SyntaxError, options[:locale].t("errors.syntax.render") unless markup =~ SYNTAX raise SyntaxError, options[:locale].t("errors.syntax.render") unless markup =~ SYNTAX
template_name = Regexp.last_match(1) template_name = Regexp.last_match(1)
with_or_for = Regexp.last_match(3)
variable_name = Regexp.last_match(4)
@alias_name = Regexp.last_match(6)
@variable_name_expr = variable_name ? Expression.parse(variable_name) : nil
@template_name_expr = Expression.parse(template_name) @template_name_expr = Expression.parse(template_name)
@for = (with_or_for == FOR)
@attributes = {} @attributes = {}
markup.scan(TagAttributes) do |key, value| markup.scan(TagAttributes) do |key, value|
@@ -38,13 +44,29 @@ module Liquid
parse_context: parse_context parse_context: parse_context
) )
inner_context = context.new_isolated_subcontext context_variable_name = @alias_name || template_name.split('/').last
inner_context.template_name = template_name
inner_context.partial = true render_partial_func = ->(var, forloop) {
@attributes.each do |key, value| inner_context = context.new_isolated_subcontext
inner_context[key] = context.evaluate(value) inner_context.template_name = template_name
inner_context.partial = true
inner_context['forloop'] = forloop if forloop
@attributes.each do |key, value|
inner_context[key] = context.evaluate(value)
end
inner_context[context_variable_name] = var unless var.nil?
partial.render_to_output_buffer(inner_context, output)
forloop&.send(:increment!)
}
variable = @variable_name_expr ? context.evaluate(@variable_name_expr) : nil
if @for && variable.respond_to?(:each) && variable.respond_to?(:count)
forloop = Liquid::ForloopDrop.new(template_name, variable.count, nil)
variable.each { |var| render_partial_func.call(var, forloop) }
else
render_partial_func.call(variable, nil)
end end
partial.render_to_output_buffer(inner_context, output)
output output
end end

View File

@@ -9,9 +9,9 @@ module Liquid
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
if markup =~ Syntax if markup =~ Syntax
@variable_name = Regexp.last_match(1) @variable_name = Regexp.last_match(1)
@collection_name = Expression.parse(Regexp.last_match(2)) @collection_name = Expression.parse(Regexp.last_match(2))
@attributes = {} @attributes = {}
markup.scan(TagAttributes) do |key, value| markup.scan(TagAttributes) do |key, value|
@attributes[key] = Expression.parse(value) @attributes[key] = Expression.parse(value)
end end
@@ -24,11 +24,10 @@ module Liquid
(collection = context.evaluate(@collection_name)) || (return '') (collection = context.evaluate(@collection_name)) || (return '')
from = @attributes.key?('offset') ? context.evaluate(@attributes['offset']).to_i : 0 from = @attributes.key?('offset') ? context.evaluate(@attributes['offset']).to_i : 0
to = @attributes.key?('limit') ? from + context.evaluate(@attributes['limit']).to_i : nil to = @attributes.key?('limit') ? from + context.evaluate(@attributes['limit']).to_i : nil
collection = Utils.slice_collection(collection, from, to) collection = Utils.slice_collection(collection, from, to)
length = collection.length
length = collection.length
cols = context.evaluate(@attributes['cols']).to_i cols = context.evaluate(@attributes['cols']).to_i

View File

@@ -18,13 +18,11 @@ module Liquid
attr_accessor :root attr_accessor :root
attr_reader :resource_limits, :warnings attr_reader :resource_limits, :warnings
@@file_system = BlankFileSystem.new
class TagRegistry class TagRegistry
include Enumerable include Enumerable
def initialize def initialize
@tags = {} @tags = {}
@cache = {} @cache = {}
end end
@@ -63,84 +61,57 @@ module Liquid
# :lax acts like liquid 2.5 and silently ignores malformed tags in most cases. # :lax acts like liquid 2.5 and silently ignores malformed tags in most cases.
# :warn is the default and will give deprecation warnings when invalid syntax is used. # :warn is the default and will give deprecation warnings when invalid syntax is used.
# :strict will enforce correct syntax. # :strict will enforce correct syntax.
attr_writer :error_mode attr_accessor :error_mode
Template.error_mode = :lax
# Sets how strict the taint checker should be.
# :lax is the default, and ignores the taint flag completely
# :warn adds a warning, but does not interrupt the rendering
# :error raises an error when tainted output is used
attr_writer :taint_mode
attr_accessor :default_exception_renderer attr_accessor :default_exception_renderer
Template.default_exception_renderer = lambda do |exception| Template.default_exception_renderer = lambda do |exception|
exception exception
end end
def file_system attr_accessor :file_system
@@file_system Template.file_system = BlankFileSystem.new
end
def file_system=(obj) attr_accessor :tags
@@file_system = obj Template.tags = TagRegistry.new
end private :tags=
def register_tag(name, klass) def register_tag(name, klass)
tags[name.to_s] = klass tags[name.to_s] = klass
end end
def tags
@tags ||= TagRegistry.new
end
def add_register(name, klass)
registers[name.to_sym] = klass
end
def registers
@registers ||= {}
end
def error_mode
@error_mode ||= :lax
end
def taint_mode
@taint_mode ||= :lax
end
# Pass a module with filter methods which should be available # Pass a module with filter methods which should be available
# to all liquid views. Good for registering the standard library # to all liquid views. Good for registering the standard library
def register_filter(mod) def register_filter(mod)
Strainer.global_filter(mod) StrainerFactory.add_global_filter(mod)
end end
def default_resource_limits attr_accessor :default_resource_limits
@default_resource_limits ||= {} Template.default_resource_limits = {}
end private :default_resource_limits=
# creates a new <tt>Template</tt> object from liquid source code # creates a new <tt>Template</tt> object from liquid source code
# To enable profiling, pass in <tt>profile: true</tt> as an option. # To enable profiling, pass in <tt>profile: true</tt> as an option.
# See Liquid::Profiler for more information # See Liquid::Profiler for more information
def parse(source, options = {}) def parse(source, options = {})
template = Template.new new.parse(source, options)
template.parse(source, options)
end end
end end
def initialize def initialize
@rethrow_errors = false @rethrow_errors = false
@resource_limits = ResourceLimits.new(self.class.default_resource_limits) @resource_limits = ResourceLimits.new(Template.default_resource_limits)
end end
# Parse source code. # Parse source code.
# Returns self for easy chaining # Returns self for easy chaining
def parse(source, options = {}) def parse(source, options = {})
@options = options @options = options
@profiling = options[:profile] @profiling = options[:profile]
@line_numbers = options[:line_numbers] || @profiling @line_numbers = options[:line_numbers] || @profiling
parse_context = options.is_a?(ParseContext) ? options : ParseContext.new(options) parse_context = options.is_a?(ParseContext) ? options : ParseContext.new(options)
@root = Document.parse(tokenize(source), parse_context) @root = Document.parse(tokenize(source), parse_context)
@warnings = parse_context.warnings @warnings = parse_context.warnings
self self
end end
@@ -187,7 +158,7 @@ module Liquid
c c
when Liquid::Drop when Liquid::Drop
drop = args.shift drop = args.shift
drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits) drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
when Hash when Hash
Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits) Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
@@ -204,7 +175,7 @@ module Liquid
case args.last case args.last
when Hash when Hash
options = args.pop options = args.pop
output = options[:output] if options[:output] output = options[:output] if options[:output]
options[:registers]&.each do |key, register| options[:registers]&.each do |key, register|
context_register[key] = register context_register[key] = register
@@ -215,10 +186,6 @@ module Liquid
context.add_filters(args.pop) context.add_filters(args.pop)
end end
Template.registers.each do |key, register|
context_register[key] = register
end
# Retrying a render resets resource usage # Retrying a render resets resource usage
context.resource_limits.reset context.resource_limits.reset
@@ -269,10 +236,10 @@ module Liquid
def apply_options_to_context(context, options) def apply_options_to_context(context, options)
context.add_filters(options[:filters]) if options[:filters] context.add_filters(options[:filters]) if options[:filters]
context.global_filter = options[:global_filter] if options[:global_filter] context.global_filter = options[:global_filter] if options[:global_filter]
context.exception_renderer = options[:exception_renderer] if options[:exception_renderer] context.exception_renderer = options[:exception_renderer] if options[:exception_renderer]
context.strict_variables = options[:strict_variables] if options[:strict_variables] context.strict_variables = options[:strict_variables] if options[:strict_variables]
context.strict_filters = options[:strict_filters] if options[:strict_filters] context.strict_filters = options[:strict_filters] if options[:strict_filters]
end end
end end
end end

View File

@@ -0,0 +1,9 @@
# frozen_string_literal: true
module Liquid
class TemplateFactory
def for(_template_name)
Liquid::Template.new
end
end
end

View File

@@ -5,10 +5,10 @@ module Liquid
attr_reader :line_number, :for_liquid_tag attr_reader :line_number, :for_liquid_tag
def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false) def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false)
@source = source @source = source
@line_number = line_number || (line_numbers ? 1 : nil) @line_number = line_number || (line_numbers ? 1 : nil)
@for_liquid_tag = for_liquid_tag @for_liquid_tag = for_liquid_tag
@tokens = tokenize @tokens = tokenize
end end
def shift def shift

View File

@@ -12,7 +12,7 @@ module Liquid
def self.slice_collection_using_each(collection, from, to) def self.slice_collection_using_each(collection, from, to)
segments = [] segments = []
index = 0 index = 0
# Maintains Ruby 1.8.7 String#each behaviour on 1.9 # Maintains Ruby 1.8.7 String#each behaviour on 1.9
if collection.is_a?(String) if collection.is_a?(String)

View File

@@ -12,10 +12,10 @@ module Liquid
# {{ user | link }} # {{ user | link }}
# #
class Variable class Variable
FilterMarkupRegex = /#{FilterSeparator}\s*(.*)/om FilterMarkupRegex = /#{FilterSeparator}\s*(.*)/om
FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
FilterArgsRegex = /(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o FilterArgsRegex = /(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o
JustTagAttributes = /\A#{TagAttributes}\z/o JustTagAttributes = /\A#{TagAttributes}\z/o
MarkupWithQuotedFragment = /(#{QuotedFragment})(.*)/om MarkupWithQuotedFragment = /(#{QuotedFragment})(.*)/om
attr_accessor :filters, :name, :line_number attr_accessor :filters, :name, :line_number
@@ -25,10 +25,10 @@ module Liquid
include ParserSwitching include ParserSwitching
def initialize(markup, parse_context) def initialize(markup, parse_context)
@markup = markup @markup = markup
@name = nil @name = nil
@parse_context = parse_context @parse_context = parse_context
@line_number = parse_context.line_number @line_number = parse_context.line_number
parse_with_selected_parser(markup) parse_with_selected_parser(markup)
end end
@@ -45,9 +45,9 @@ module Liquid
@filters = [] @filters = []
return unless markup =~ MarkupWithQuotedFragment return unless markup =~ MarkupWithQuotedFragment
name_markup = Regexp.last_match(1) name_markup = Regexp.last_match(1)
filter_markup = Regexp.last_match(2) filter_markup = Regexp.last_match(2)
@name = Expression.parse(name_markup) @name = Expression.parse(name_markup)
if filter_markup =~ FilterMarkupRegex if filter_markup =~ FilterMarkupRegex
filters = Regexp.last_match(1).scan(FilterParser) filters = Regexp.last_match(1).scan(FilterParser)
filters.each do |f| filters.each do |f|
@@ -86,9 +86,7 @@ module Liquid
context.invoke(filter_name, output, *filter_args) context.invoke(filter_name, output, *filter_args)
end end
obj = context.apply_global_filter(obj) context.apply_global_filter(obj)
taint_check(context, obj)
obj
end end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
@@ -115,11 +113,11 @@ module Liquid
private private
def parse_filter_expressions(filter_name, unparsed_args) def parse_filter_expressions(filter_name, unparsed_args)
filter_args = [] filter_args = []
keyword_args = nil keyword_args = nil
unparsed_args.each do |a| unparsed_args.each do |a|
if (matches = a.match(JustTagAttributes)) if (matches = a.match(JustTagAttributes))
keyword_args ||= {} keyword_args ||= {}
keyword_args[matches[1]] = Expression.parse(matches[2]) keyword_args[matches[1]] = Expression.parse(matches[2])
else else
filter_args << Expression.parse(a) filter_args << Expression.parse(a)
@@ -142,25 +140,6 @@ module Liquid
parsed_args parsed_args
end end
def taint_check(context, obj)
return unless obj.tainted?
return if Template.taint_mode == :lax
@markup =~ QuotedFragment
name = Regexp.last_match(0)
error = TaintedError.new("variable '#{name}' is tainted and was not escaped")
error.line_number = line_number
error.template_name = context.template_name
case Template.taint_mode
when :warn
context.warnings << error
when :error
raise error
end
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children def children
[@node.name] + @node.filters.flatten [@node.name] + @node.filters.flatten

View File

@@ -3,7 +3,7 @@
module Liquid module Liquid
class VariableLookup class VariableLookup
SQUARE_BRACKETED = /\A\[(.*)\]\z/m SQUARE_BRACKETED = /\A\[(.*)\]\z/m
COMMAND_METHODS = ['size', 'first', 'last'].freeze COMMAND_METHODS = ['size', 'first', 'last'].freeze
attr_reader :name, :lookups attr_reader :name, :lookups
@@ -20,7 +20,7 @@ module Liquid
end end
@name = name @name = name
@lookups = lookups @lookups = lookups
@command_flags = 0 @command_flags = 0
@lookups.each_index do |i| @lookups.each_index do |i|
@@ -34,7 +34,7 @@ module Liquid
end end
def evaluate(context) def evaluate(context)
name = context.evaluate(@name) name = context.evaluate(@name)
object = context.find_variable(name) object = context.find_variable(name)
@lookups.each_index do |i| @lookups.each_index do |i|
@@ -47,7 +47,7 @@ module Liquid
(object.respond_to?(:fetch) && key.is_a?(Integer))) (object.respond_to?(:fetch) && key.is_a?(Integer)))
# if its a proc we will replace the entry with the proc # if its a proc we will replace the entry with the proc
res = context.lookup_and_evaluate(object, key) res = context.lookup_and_evaluate(object, key)
object = res.to_liquid object = res.to_liquid
# Some special cases. If the part wasn't in square brackets and # Some special cases. If the part wasn't in square brackets and

View File

@@ -27,6 +27,6 @@ Gem::Specification.new do |s|
s.require_path = "lib" s.require_path = "lib"
s.add_development_dependency('rake', '~> 11.3') s.add_development_dependency('rake', '~> 13.0')
s.add_development_dependency('minitest') s.add_development_dependency('minitest')
end end

View File

@@ -7,7 +7,7 @@ Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new profiler = ThemeRunner.new
Benchmark.ips do |x| Benchmark.ips do |x|
x.time = 10 x.time = 10
x.warmup = 5 x.warmup = 5
puts puts

View File

@@ -8,7 +8,7 @@ class CommentForm < Liquid::Block
if markup =~ Syntax if markup =~ Syntax
@variable_name = Regexp.last_match(1) @variable_name = Regexp.last_match(1)
@attributes = {} @attributes = {}
else else
raise SyntaxError, "Syntax Error in 'comment_form' - Valid syntax: comment_form [article]" raise SyntaxError, "Syntax Error in 'comment_form' - Valid syntax: comment_form [article]"
end end

View File

@@ -8,7 +8,7 @@ class Paginate < Liquid::Block
if markup =~ Syntax if markup =~ Syntax
@collection_name = Regexp.last_match(1) @collection_name = Regexp.last_match(1)
@page_size = if Regexp.last_match(2) @page_size = if Regexp.last_match(2)
Regexp.last_match(3).to_i Regexp.last_match(3).to_i
else else
20 20

View File

@@ -60,7 +60,7 @@ module ShopFilter
case style case style
when 'original' when 'original'
return '/files/shops/random_number/' + url '/files/shops/random_number/' + url
when 'grande', 'large', 'medium', 'compact', 'small', 'thumb', 'icon' when 'grande', 'large', 'medium', 'compact', 'small', 'thumb', 'icon'
"/files/shops/random_number/products/#{Regexp.last_match(1)}_#{style}.#{Regexp.last_match(2)}" "/files/shops/random_number/products/#{Regexp.last_match(1)}_#{style}.#{Regexp.last_match(2)}"
else else

View File

@@ -58,9 +58,9 @@ class ThemeRunner
# `render` is called to benchmark just the render portion of liquid # `render` is called to benchmark just the render portion of liquid
def render def render
@compiled_tests.each do |test| @compiled_tests.each do |test|
tmpl = test[:tmpl] tmpl = test[:tmpl]
assigns = test[:assigns] assigns = test[:assigns]
layout = test[:layout] layout = test[:layout]
if layout if layout
assigns['content_for_layout'] = tmpl.render!(assigns) assigns['content_for_layout'] = tmpl.render!(assigns)
@@ -74,7 +74,7 @@ class ThemeRunner
private private
def compile_and_render(template, layout, assigns, page_template, template_file) def compile_and_render(template, layout, assigns, page_template, template_file)
compiled_test = compile_test(template, layout, assigns, page_template, template_file) compiled_test = compile_test(template, layout, assigns, page_template, template_file)
assigns['content_for_layout'] = compiled_test[:tmpl].render!(assigns) assigns['content_for_layout'] = compiled_test[:tmpl].render!(assigns)
compiled_test[:layout].render!(assigns) if layout compiled_test[:layout].render!(assigns) if layout
end end
@@ -88,7 +88,7 @@ class ThemeRunner
end end
def compile_test(template, layout, assigns, page_template, template_file) def compile_test(template, layout, assigns, page_template, template_file)
tmpl = init_template(page_template, template_file) tmpl = init_template(page_template, template_file)
parsed_template = tmpl.parse(template).dup parsed_template = tmpl.parse(template).dup
if layout if layout
@@ -113,9 +113,9 @@ class ThemeRunner
# set up a new Liquid::Template object for use in `compile_and_render` and `compile_test` # set up a new Liquid::Template object for use in `compile_and_render` and `compile_test`
def init_template(page_template, template_file) def init_template(page_template, template_file)
tmpl = Liquid::Template.new tmpl = Liquid::Template.new
tmpl.assigns['page_title'] = 'Page title' tmpl.assigns['page_title'] = 'Page title'
tmpl.assigns['template'] = page_template tmpl.assigns['template'] = page_template
tmpl.registers[:file_system] = ThemeRunner::FileSystem.new(File.dirname(template_file)) tmpl.registers[:file_system] = ThemeRunner::FileSystem.new(File.dirname(template_file))
tmpl tmpl
end end

View File

@@ -10,8 +10,8 @@ class AssignTest < Minitest::Test
{% assign this-thing = 'Print this-thing' %} {% assign this-thing = 'Print this-thing' %}
{{ this-thing }} {{ this-thing }}
END_TEMPLATE END_TEMPLATE
template = Template.parse(template_source) template = Template.parse(template_source)
rendered = template.render! rendered = template.render!
assert_equal("Print this-thing", rendered.strip) assert_equal("Print this-thing", rendered.strip)
end end

View File

@@ -11,4 +11,48 @@ class BlockTest < Minitest::Test
end end
assert_equal(exc.message, "Liquid syntax error: 'endunless' is not a valid delimiter for if tags. use endif") assert_equal(exc.message, "Liquid syntax error: 'endunless' is not a valid delimiter for if tags. use endif")
end end
def test_with_custom_tag
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
end end

View File

@@ -14,8 +14,8 @@ class CaptureTest < Minitest::Test
{% capture this-thing %}Print this-thing{% endcapture %} {% capture this-thing %}Print this-thing{% endcapture %}
{{ this-thing }} {{ this-thing }}
END_TEMPLATE END_TEMPLATE
template = Template.parse(template_source) template = Template.parse(template_source)
rendered = template.render! rendered = template.render!
assert_equal("Print this-thing", rendered.strip) assert_equal("Print this-thing", rendered.strip)
end end
@@ -30,8 +30,8 @@ class CaptureTest < Minitest::Test
{% endif %} {% endif %}
{{var}} {{var}}
END_TEMPLATE END_TEMPLATE
template = Template.parse(template_source) template = Template.parse(template_source)
rendered = template.render! rendered = template.render!
assert_equal("test-string", rendered.gsub(/\s/, '')) assert_equal("test-string", rendered.gsub(/\s/, ''))
end end
@@ -45,8 +45,8 @@ class CaptureTest < Minitest::Test
{% endfor %} {% endfor %}
{{ first }}-{{ second }} {{ first }}-{{ second }}
END_TEMPLATE END_TEMPLATE
template = Template.parse(template_source) template = Template.parse(template_source)
rendered = template.render! rendered = template.render!
assert_equal("3-3", rendered.gsub(/\s/, '')) assert_equal("3-3", rendered.gsub(/\s/, ''))
end end
end # CaptureTest end # CaptureTest

View File

@@ -46,7 +46,7 @@ end
class CounterDrop < Liquid::Drop class CounterDrop < Liquid::Drop
def count def count
@count ||= 0 @count ||= 0
@count += 1 @count += 1
end end
end end
@@ -55,9 +55,9 @@ class ArrayLike
end end
def [](index) def [](index)
@counts ||= [] @counts ||= []
@counts[index] ||= 0 @counts[index] ||= 0
@counts[index] += 1 @counts[index] += 1
end end
def to_liquid def to_liquid
@@ -65,7 +65,7 @@ class ArrayLike
end end
end end
class ContextUnitTest < Minitest::Test class ContextTest < Minitest::Test
include Liquid include Liquid
def setup def setup
@@ -265,7 +265,7 @@ class ContextUnitTest < Minitest::Test
def test_access_hashes_with_hash_notation def test_access_hashes_with_hash_notation
@context['products'] = { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] } @context['products'] = { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] }
@context['product'] = { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] } @context['product'] = { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] }
assert_equal(5, @context['products["count"]']) assert_equal(5, @context['products["count"]'])
assert_equal('deepsnow', @context['products["tags"][0]']) assert_equal('deepsnow', @context['products["tags"][0]'])
@@ -285,8 +285,8 @@ class ContextUnitTest < Minitest::Test
end end
def test_access_hashes_with_hash_access_variables def test_access_hashes_with_hash_access_variables
@context['var'] = 'tags' @context['var'] = 'tags'
@context['nested'] = { 'var' => 'tags' } @context['nested'] = { 'var' => 'tags' }
@context['products'] = { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] } @context['products'] = { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] }
assert_equal('deepsnow', @context['products[var].first']) assert_equal('deepsnow', @context['products[var].first'])
@@ -295,7 +295,7 @@ class ContextUnitTest < Minitest::Test
def test_hash_notation_only_for_hash_access def test_hash_notation_only_for_hash_access
@context['array'] = [1, 2, 3, 4, 5] @context['array'] = [1, 2, 3, 4, 5]
@context['hash'] = { 'first' => 'Hello' } @context['hash'] = { 'first' => 'Hello' }
assert_equal(1, @context['array.first']) assert_equal(1, @context['array.first'])
assert_nil(@context['array["first"]']) assert_nil(@context['array["first"]'])
@@ -407,7 +407,7 @@ class ContextUnitTest < Minitest::Test
def test_lambda_is_called_once def test_lambda_is_called_once
@context['callcount'] = proc { @context['callcount'] = proc {
@global ||= 0 @global ||= 0
@global += 1 @global += 1
@global.to_s @global.to_s
} }
@@ -421,7 +421,7 @@ class ContextUnitTest < Minitest::Test
def test_nested_lambda_is_called_once def test_nested_lambda_is_called_once
@context['callcount'] = { "lambda" => proc { @context['callcount'] = { "lambda" => proc {
@global ||= 0 @global ||= 0
@global += 1 @global += 1
@global.to_s @global.to_s
} } } }
@@ -435,7 +435,7 @@ class ContextUnitTest < Minitest::Test
def test_lambda_in_array_is_called_once def test_lambda_in_array_is_called_once
@context['callcount'] = [1, 2, proc { @context['callcount'] = [1, 2, proc {
@global ||= 0 @global ||= 0
@global += 1 @global += 1
@global.to_s @global.to_s
}, 4, 5] }, 4, 5]
@@ -507,15 +507,15 @@ class ContextUnitTest < Minitest::Test
def test_new_isolated_subcontext_inherits_static_environment def test_new_isolated_subcontext_inherits_static_environment
super_context = Context.build(static_environments: { 'my_environment_value' => 'my value' }) super_context = Context.build(static_environments: { 'my_environment_value' => 'my value' })
subcontext = super_context.new_isolated_subcontext subcontext = super_context.new_isolated_subcontext
assert_equal('my value', subcontext['my_environment_value']) assert_equal('my value', subcontext['my_environment_value'])
end end
def test_new_isolated_subcontext_inherits_resource_limits def test_new_isolated_subcontext_inherits_resource_limits
resource_limits = ResourceLimits.new({}) resource_limits = ResourceLimits.new({})
super_context = Context.new({}, {}, {}, false, resource_limits) super_context = Context.new({}, {}, {}, false, resource_limits)
subcontext = super_context.new_isolated_subcontext subcontext = super_context.new_isolated_subcontext
assert_equal(resource_limits, subcontext.resource_limits) assert_equal(resource_limits, subcontext.resource_limits)
end end
@@ -532,19 +532,19 @@ class ContextUnitTest < Minitest::Test
} }
super_context = Context.new({}, {}, StaticRegisters.new(registers)) super_context = Context.new({}, {}, StaticRegisters.new(registers))
super_context.registers[:my_register] = :my_alt_value super_context.registers[:my_register] = :my_alt_value
subcontext = super_context.new_isolated_subcontext subcontext = super_context.new_isolated_subcontext
assert_equal(:my_value, subcontext.registers[:my_register]) assert_equal(:my_value, subcontext.registers[:my_register])
end end
def test_new_isolated_subcontext_inherits_static_registers def test_new_isolated_subcontext_inherits_static_registers
super_context = Context.build(registers: { my_register: :my_value }) super_context = Context.build(registers: { my_register: :my_value })
subcontext = super_context.new_isolated_subcontext subcontext = super_context.new_isolated_subcontext
assert_equal(:my_value, subcontext.registers[:my_register]) assert_equal(:my_value, subcontext.registers[:my_register])
end end
def test_new_isolated_subcontext_registers_do_not_pollute_context def test_new_isolated_subcontext_registers_do_not_pollute_context
super_context = Context.build(registers: { my_register: :my_value }) super_context = Context.build(registers: { my_register: :my_value })
subcontext = super_context.new_isolated_subcontext subcontext = super_context.new_isolated_subcontext
subcontext.registers[:my_register] = :my_alt_value subcontext.registers[:my_register] = :my_alt_value
assert_equal(:my_value, super_context.registers[:my_register]) assert_equal(:my_value, super_context.registers[:my_register])
end end
@@ -558,11 +558,40 @@ class ContextUnitTest < Minitest::Test
super_context = Context.new super_context = Context.new
super_context.add_filters([my_filter]) super_context.add_filters([my_filter])
subcontext = super_context.new_isolated_subcontext subcontext = super_context.new_isolated_subcontext
template = Template.parse('{{ 123 | my_filter }}') template = Template.parse('{{ 123 | my_filter }}')
assert_equal('my filter result', template.render(subcontext)) assert_equal('my filter result', template.render(subcontext))
end end
def test_disables_tag_specified
context = Context.new
context.with_disabled_tags(%w(foo bar)) do
assert_equal true, context.tag_disabled?("foo")
assert_equal true, context.tag_disabled?("bar")
assert_equal false, context.tag_disabled?("unknown")
end
end
def test_disables_nested_tags
context = Context.new
context.with_disabled_tags(["foo"]) do
context.with_disabled_tags(["foo"]) do
assert_equal true, context.tag_disabled?("foo")
assert_equal false, context.tag_disabled?("bar")
end
context.with_disabled_tags(["bar"]) do
assert_equal true, context.tag_disabled?("foo")
assert_equal true, context.tag_disabled?("bar")
context.with_disabled_tags(["foo"]) do
assert_equal true, context.tag_disabled?("foo")
assert_equal true, context.tag_disabled?("bar")
end
end
assert_equal true, context.tag_disabled?("foo")
assert_equal false, context.tag_disabled?("bar")
end
end
private private
def assert_no_object_allocations def assert_no_object_allocations

View File

@@ -49,10 +49,6 @@ class ProductDrop < Liquid::Drop
ContextDrop.new ContextDrop.new
end end
def user_input
(+"foo").taint
end
protected protected
def callmenot def callmenot
@@ -114,32 +110,6 @@ class DropsTest < Minitest::Test
assert_equal(' ', tpl.render!('product' => ProductDrop.new)) assert_equal(' ', tpl.render!('product' => ProductDrop.new))
end end
def test_rendering_raises_on_tainted_attr
with_taint_mode(:error) do
tpl = Liquid::Template.parse('{{ product.user_input }}')
assert_raises TaintedError do
tpl.render!('product' => ProductDrop.new)
end
end
end
def test_rendering_warns_on_tainted_attr
with_taint_mode(:warn) do
tpl = Liquid::Template.parse('{{ product.user_input }}')
context = Context.new('product' => ProductDrop.new)
tpl.render!(context)
assert_equal [Liquid::TaintedError], context.warnings.map(&:class)
assert_equal "variable 'product.user_input' is tainted and was not escaped", context.warnings.first.to_s(false)
end
end
def test_rendering_doesnt_raise_on_escaped_tainted_attr
with_taint_mode(:error) do
tpl = Liquid::Template.parse('{{ product.user_input | escape }}')
tpl.render!('product' => ProductDrop.new)
end
end
def test_drop_does_only_respond_to_whitelisted_methods def test_drop_does_only_respond_to_whitelisted_methods
assert_equal("", Liquid::Template.parse("{{ product.inspect }}").render!('product' => ProductDrop.new)) assert_equal("", Liquid::Template.parse("{{ product.inspect }}").render!('product' => ProductDrop.new))
assert_equal("", Liquid::Template.parse("{{ product.pretty_inspect }}").render!('product' => ProductDrop.new)) assert_equal("", Liquid::Template.parse("{{ product.pretty_inspect }}").render!('product' => ProductDrop.new))
@@ -179,6 +149,11 @@ class DropsTest < Minitest::Test
assert_equal(' carrot ', output) assert_equal(' carrot ', output)
end end
def test_context_drop_array_with_map
output = Liquid::Template.parse(' {{ contexts | map: "bar" }} ').render!('contexts' => [ContextDrop.new, ContextDrop.new], 'bar' => "carrot")
assert_equal(' carrotcarrot ', output)
end
def test_nested_context_drop def test_nested_context_drop
output = Liquid::Template.parse(' {{ product.context.foo }} ').render!('product' => ProductDrop.new, 'foo' => "monkey") output = Liquid::Template.parse(' {{ product.context.foo }} ').render!('product' => ProductDrop.new, 'foo' => "monkey")
assert_equal(' monkey ', output) assert_equal(' monkey ', output)
@@ -274,7 +249,7 @@ class DropsTest < Minitest::Test
end end
def test_invokable_methods def test_invokable_methods
assert_equal(%w(to_liquid catchall user_input context texts).to_set, ProductDrop.invokable_methods) assert_equal(%w(to_liquid catchall context texts).to_set, ProductDrop.invokable_methods)
assert_equal(%w(to_liquid scopes_as_array loop_pos scopes).to_set, ContextDrop.invokable_methods) assert_equal(%w(to_liquid scopes_as_array loop_pos scopes).to_set, ContextDrop.invokable_methods)
assert_equal(%w(to_liquid size max min first count).to_set, EnumerableDrop.invokable_methods) assert_equal(%w(to_liquid size max min first count).to_set, EnumerableDrop.invokable_methods)
assert_equal(%w(to_liquid max min sort count first).to_set, RealEnumerableDrop.invokable_methods) assert_equal(%w(to_liquid max min sort count first).to_set, RealEnumerableDrop.invokable_methods)

View File

@@ -226,9 +226,9 @@ class ErrorHandlingTest < Minitest::Test
end end
def test_exception_renderer_exposing_non_liquid_error def test_exception_renderer_exposing_non_liquid_error
template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true) template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true)
exceptions = [] exceptions = []
handler = ->(e) { handler = ->(e) {
exceptions << e exceptions << e
e.cause e.cause
} }
@@ -252,12 +252,21 @@ class ErrorHandlingTest < Minitest::Test
begin begin
Liquid::Template.file_system = TestFileSystem.new Liquid::Template.file_system = TestFileSystem.new
template = Liquid::Template.parse("Argument error:\n{% include 'product' %}", line_numbers: true) template = Liquid::Template.parse("Argument error:\n{% include 'product' %}", line_numbers: true)
page = template.render('errors' => ErrorDrop.new) page = template.render('errors' => ErrorDrop.new)
ensure ensure
Liquid::Template.file_system = old_file_system Liquid::Template.file_system = old_file_system
end end
assert_equal("Argument error:\nLiquid error (product line 1): argument error", page) assert_equal("Argument error:\nLiquid error (product line 1): argument error", page)
assert_equal("product", template.errors.first.template_name) assert_equal("product", template.errors.first.template_name)
end end
def test_bug_compatible_silencing_of_errors_in_blank_nodes
output = Liquid::Template.parse("{% assign x = 0 %}{% if 1 < '2' %}not blank{% assign x = 3 %}{% endif %}{{ x }}").render
assert_equal("Liquid error: comparison of Integer with String failed0", output)
output = Liquid::Template.parse("{% assign x = 0 %}{% if 1 < '2' %}{% assign x = 3 %}{% endif %}{{ x }}").render
assert_equal("0", output)
end
end end

View File

@@ -72,10 +72,10 @@ class FiltersTest < Minitest::Test
end end
def test_sort def test_sort
@context['value'] = 3 @context['value'] = 3
@context['numbers'] = [2, 1, 4, 3] @context['numbers'] = [2, 1, 4, 3]
@context['words'] = ['expected', 'as', 'alphabetic'] @context['words'] = ['expected', 'as', 'alphabetic']
@context['arrays'] = ['flower', 'are'] @context['arrays'] = ['flower', 'are']
@context['case_sensitive'] = ['sensitive', 'Expected', 'case'] @context['case_sensitive'] = ['sensitive', 'Expected', 'case']
assert_equal('1 2 3 4', Template.parse("{{numbers | sort | join}}").render(@context)) assert_equal('1 2 3 4', Template.parse("{{numbers | sort | join}}").render(@context))
@@ -86,8 +86,8 @@ class FiltersTest < Minitest::Test
end end
def test_sort_natural def test_sort_natural
@context['words'] = ['case', 'Assert', 'Insensitive'] @context['words'] = ['case', 'Assert', 'Insensitive']
@context['hashes'] = [{ 'a' => 'A' }, { 'a' => 'b' }, { 'a' => 'C' }] @context['hashes'] = [{ 'a' => 'A' }, { 'a' => 'b' }, { 'a' => 'C' }]
@context['objects'] = [TestObject.new('A'), TestObject.new('b'), TestObject.new('C')] @context['objects'] = [TestObject.new('A'), TestObject.new('b'), TestObject.new('C')]
# Test strings # Test strings
@@ -101,8 +101,8 @@ class FiltersTest < Minitest::Test
end end
def test_compact def test_compact
@context['words'] = ['a', nil, 'b', nil, 'c'] @context['words'] = ['a', nil, 'b', nil, 'c']
@context['hashes'] = [{ 'a' => 'A' }, { 'a' => nil }, { 'a' => 'C' }] @context['hashes'] = [{ 'a' => 'A' }, { 'a' => nil }, { 'a' => 'C' }]
@context['objects'] = [TestObject.new('A'), TestObject.new(nil), TestObject.new('C')] @context['objects'] = [TestObject.new('A'), TestObject.new(nil), TestObject.new('C')]
# Test strings # Test strings
@@ -141,9 +141,9 @@ class FiltersTest < Minitest::Test
def test_filter_with_keyword_arguments def test_filter_with_keyword_arguments
@context['surname'] = 'john' @context['surname'] = 'john'
@context['input'] = 'hello %{first_name}, %{last_name}' @context['input'] = 'hello %{first_name}, %{last_name}'
@context.add_filters(SubstituteFilter) @context.add_filters(SubstituteFilter)
output = Template.parse(%({{ input | substitute: first_name: surname, last_name: 'doe' }})).render(@context) output = Template.parse(%({{ input | substitute: first_name: surname, last_name: 'doe' }})).render(@context)
assert_equal('hello john, doe', output) assert_equal('hello john, doe', output)
end end

View File

@@ -61,63 +61,63 @@ class OutputTest < Minitest::Test
end end
def test_variable_piping def test_variable_piping
text = %( {{ car.gm | make_funny }} ) text = %( {{ car.gm | make_funny }} )
expected = %( LOL ) expected = %( LOL )
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])) assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
end end
def test_variable_piping_with_input def test_variable_piping_with_input
text = %( {{ car.gm | cite_funny }} ) text = %( {{ car.gm | cite_funny }} )
expected = %( LOL: bad ) expected = %( LOL: bad )
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])) assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
end end
def test_variable_piping_with_args def test_variable_piping_with_args
text = %! {{ car.gm | add_smiley : ':-(' }} ! text = %! {{ car.gm | add_smiley : ':-(' }} !
expected = %| bad :-( | expected = %| bad :-( |
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])) assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
end end
def test_variable_piping_with_no_args def test_variable_piping_with_no_args
text = %( {{ car.gm | add_smiley }} ) text = %( {{ car.gm | add_smiley }} )
expected = %| bad :-) | expected = %| bad :-) |
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])) assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
end end
def test_multiple_variable_piping_with_args def test_multiple_variable_piping_with_args
text = %! {{ car.gm | add_smiley : ':-(' | add_smiley : ':-('}} ! text = %! {{ car.gm | add_smiley : ':-(' | add_smiley : ':-('}} !
expected = %| bad :-( :-( | expected = %| bad :-( :-( |
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])) assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
end end
def test_variable_piping_with_multiple_args def test_variable_piping_with_multiple_args
text = %( {{ car.gm | add_tag : 'span', 'bar'}} ) text = %( {{ car.gm | add_tag : 'span', 'bar'}} )
expected = %( <span id="bar">bad</span> ) expected = %( <span id="bar">bad</span> )
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])) assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
end end
def test_variable_piping_with_variable_args def test_variable_piping_with_variable_args
text = %( {{ car.gm | add_tag : 'span', car.bmw}} ) text = %( {{ car.gm | add_tag : 'span', car.bmw}} )
expected = %( <span id="good">bad</span> ) expected = %( <span id="good">bad</span> )
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])) assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
end end
def test_multiple_pipings def test_multiple_pipings
text = %( {{ best_cars | cite_funny | paragraph }} ) text = %( {{ best_cars | cite_funny | paragraph }} )
expected = %( <p>LOL: bmw</p> ) expected = %( <p>LOL: bmw</p> )
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])) assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
end end
def test_link_to def test_link_to
text = %( {{ 'Typo' | link_to: 'http://typo.leetsoft.com' }} ) text = %( {{ 'Typo' | link_to: 'http://typo.leetsoft.com' }} )
expected = %( <a href="http://typo.leetsoft.com">Typo</a> ) expected = %( <a href="http://typo.leetsoft.com">Typo</a> )
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])) assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))

View File

@@ -72,7 +72,7 @@ class ParsingQuirksTest < Minitest::Test
def test_meaningless_parens_lax def test_meaningless_parens_lax
with_error_mode(:lax) do with_error_mode(:lax) do
assigns = { 'b' => 'bar', 'c' => 'baz' } assigns = { 'b' => 'bar', 'c' => 'baz' }
markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false" markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false"
assert_template_result(' YES ', "{% if #{markup} %} YES {% endif %}", assigns) assert_template_result(' YES ', "{% if #{markup} %} YES {% endif %}", assigns)
end end
end end

View File

@@ -1,27 +0,0 @@
# frozen_string_literal: true
require 'test_helper'
class DisabledTagsTest < Minitest::Test
include Liquid
class DisableRaw < Block
disable_tags "raw"
end
class DisableRawEcho < Block
disable_tags "raw", "echo"
end
def test_disables_raw
with_custom_tag('disable', DisableRaw) do
assert_template_result 'raw usage is not allowed in this contextfoo', '{% disable %}{% raw %}Foobar{% endraw %}{% echo "foo" %}{% enddisable %}'
end
end
def test_disables_echo_and_raw
with_custom_tag('disable', DisableRawEcho) do
assert_template_result 'raw usage is not allowed in this contextecho usage is not allowed in this context', '{% disable %}{% raw %}Foobar{% endraw %}{% echo "foo" %}{% enddisable %}'
end
end
end

View File

@@ -154,18 +154,18 @@ class RenderProfilingTest < Minitest::Test
assert_equal(2, t.profiler[0].children.length) assert_equal(2, t.profiler[0].children.length)
end end
def test_total_time_equals_self_time_in_leaf def test_profiling_supports_self_time
t = Template.parse("{% for item in collection %} {{ item }} {% endfor %}", profile: true) t = Template.parse("{% for item in collection %} {{ item }} {% endfor %}", profile: true)
t.render!("collection" => ["one", "two"]) t.render!("collection" => ["one", "two"])
leaf = t.profiler[0].children[0] leaf = t.profiler[0].children[0]
assert_equal leaf.total_time, leaf.self_time assert_operator(leaf.self_time, :>, 0)
end end
def test_total_time_greater_than_self_time_in_node def test_profiling_supports_total_time
t = Template.parse("{% if true %} {% increment test %} {{ test }} {% endif %}", profile: true) t = Template.parse("{% if true %} {% increment test %} {{ test }} {% endif %}", profile: true)
t.render! t.render!
assert_operator t.profiler[0].total_time, :>, t.profiler[0].self_time assert_operator(t.profiler[0].total_time, :>, 0)
end end
end end

View File

@@ -16,42 +16,49 @@ class SecurityTest < Minitest::Test
end end
def test_no_instance_eval def test_no_instance_eval
text = %( {{ '1+1' | instance_eval }} ) text = %( {{ '1+1' | instance_eval }} )
expected = %( 1+1 ) expected = %( 1+1 )
assert_equal(expected, Template.parse(text).render!(@assigns)) assert_equal(expected, Template.parse(text).render!(@assigns))
end end
def test_no_existing_instance_eval def test_no_existing_instance_eval
text = %( {{ '1+1' | __instance_eval__ }} ) text = %( {{ '1+1' | __instance_eval__ }} )
expected = %( 1+1 ) expected = %( 1+1 )
assert_equal(expected, Template.parse(text).render!(@assigns)) assert_equal(expected, Template.parse(text).render!(@assigns))
end end
def test_no_instance_eval_after_mixing_in_new_filter def test_no_instance_eval_after_mixing_in_new_filter
text = %( {{ '1+1' | instance_eval }} ) text = %( {{ '1+1' | instance_eval }} )
expected = %( 1+1 ) expected = %( 1+1 )
assert_equal(expected, Template.parse(text).render!(@assigns)) assert_equal(expected, Template.parse(text).render!(@assigns))
end end
def test_no_instance_eval_later_in_chain def test_no_instance_eval_later_in_chain
text = %( {{ '1+1' | add_one | instance_eval }} ) text = %( {{ '1+1' | add_one | instance_eval }} )
expected = %( 1+1 + 1 ) expected = %( 1+1 + 1 )
assert_equal(expected, Template.parse(text).render!(@assigns, filters: SecurityFilter)) assert_equal(expected, Template.parse(text).render!(@assigns, filters: SecurityFilter))
end end
def test_does_not_add_filters_to_symbol_table def test_does_not_permanently_add_filters_to_symbol_table
current_symbols = Symbol.all_symbols current_symbols = Symbol.all_symbols
test = %( {{ "some_string" | a_bad_filter }} ) # MRI imprecisely marks objects found on the C stack, which can result
# in uninitialized memory being marked. This can even result in the test failing
# deterministically for a given compilation of ruby. Using a separate thread will
# keep these writes of the symbol pointer on a separate stack that will be garbage
# collected after Thread#join.
Thread.new do
test = %( {{ "some_string" | a_bad_filter }} )
Template.parse(test).render!
nil
end.join
template = Template.parse(test) GC.start
assert_equal([], (Symbol.all_symbols - current_symbols))
template.render!
assert_equal([], (Symbol.all_symbols - current_symbols)) assert_equal([], (Symbol.all_symbols - current_symbols))
end end
@@ -68,13 +75,13 @@ class SecurityTest < Minitest::Test
def test_max_depth_nested_blocks_does_not_raise_exception def test_max_depth_nested_blocks_does_not_raise_exception
depth = Liquid::Block::MAX_DEPTH depth = Liquid::Block::MAX_DEPTH
code = "{% if true %}" * depth + "rendered" + "{% endif %}" * depth code = "{% if true %}" * depth + "rendered" + "{% endif %}" * depth
assert_equal("rendered", Template.parse(code).render!) assert_equal("rendered", Template.parse(code).render!)
end end
def test_more_than_max_depth_nested_blocks_raises_exception def test_more_than_max_depth_nested_blocks_raises_exception
depth = Liquid::Block::MAX_DEPTH + 1 depth = Liquid::Block::MAX_DEPTH + 1
code = "{% if true %}" * depth + "rendered" + "{% endif %}" * depth code = "{% if true %}" * depth + "rendered" + "{% endif %}" * depth
assert_raises(Liquid::StackLevelError) do assert_raises(Liquid::StackLevelError) do
Template.parse(code).render! Template.parse(code).render!
end end

View File

@@ -207,7 +207,7 @@ class StandardFiltersTest < Minitest::Test
end end
def test_sort_when_property_is_sometimes_missing_puts_nils_last def test_sort_when_property_is_sometimes_missing_puts_nils_last
input = [ input = [
{ "price" => 4, "handle" => "alpha" }, { "price" => 4, "handle" => "alpha" },
{ "handle" => "beta" }, { "handle" => "beta" },
{ "price" => 1, "handle" => "gamma" }, { "price" => 1, "handle" => "gamma" },
@@ -235,7 +235,7 @@ class StandardFiltersTest < Minitest::Test
end end
def test_sort_natural_when_property_is_sometimes_missing_puts_nils_last def test_sort_natural_when_property_is_sometimes_missing_puts_nils_last
input = [ input = [
{ "price" => "4", "handle" => "alpha" }, { "price" => "4", "handle" => "alpha" },
{ "handle" => "beta" }, { "handle" => "beta" },
{ "price" => "1", "handle" => "gamma" }, { "price" => "1", "handle" => "gamma" },
@@ -389,7 +389,7 @@ class StandardFiltersTest < Minitest::Test
def test_legacy_map_on_hashes_with_dynamic_key def test_legacy_map_on_hashes_with_dynamic_key
template = "{% assign key = 'foo' %}{{ thing | map: key | map: 'bar' }}" template = "{% assign key = 'foo' %}{{ thing | map: key | map: 'bar' }}"
hash = { "foo" => { "bar" => 42 } } hash = { "foo" => { "bar" => 42 } }
assert_template_result("42", template, "thing" => hash) assert_template_result("42", template, "thing" => hash)
end end
@@ -400,8 +400,8 @@ class StandardFiltersTest < Minitest::Test
end end
def test_map_over_proc def test_map_over_proc
drop = TestDrop.new drop = TestDrop.new
p = proc { drop } p = proc { drop }
templ = '{{ procs | map: "test" }}' templ = '{{ procs | map: "test" }}'
assert_template_result("testfoo", templ, "procs" => [p]) assert_template_result("testfoo", templ, "procs" => [p])
end end
@@ -685,6 +685,17 @@ class StandardFiltersTest < Minitest::Test
assert_equal("bar", @filters.default(false, "bar")) assert_equal("bar", @filters.default(false, "bar"))
assert_equal("bar", @filters.default([], "bar")) assert_equal("bar", @filters.default([], "bar"))
assert_equal("bar", @filters.default({}, "bar")) assert_equal("bar", @filters.default({}, "bar"))
assert_template_result('bar', "{{ false | default: 'bar' }}")
end
def test_default_handle_false
assert_equal("foo", @filters.default("foo", "bar", "allow_false" => true))
assert_equal("bar", @filters.default(nil, "bar", "allow_false" => true))
assert_equal("bar", @filters.default("", "bar", "allow_false" => true))
assert_equal(false, @filters.default(false, "bar", "allow_false" => true))
assert_equal("bar", @filters.default([], "bar", "allow_false" => true))
assert_equal("bar", @filters.default({}, "bar", "allow_false" => true))
assert_template_result('false', "{{ false | default: 'bar', allow_false: true }}")
end end
def test_cannot_access_private_methods def test_cannot_access_private_methods
@@ -757,6 +768,49 @@ class StandardFiltersTest < Minitest::Test
assert_nil(@filters.where([nil], "ok")) assert_nil(@filters.where([nil], "ok"))
end end
def test_all_filters_never_raise_non_liquid_exception
test_drop = TestDrop.new
test_drop.context = Context.new
test_enum = TestEnumerable.new
test_enum.context = Context.new
test_types = [
"foo",
123,
0,
0.0,
-1234.003030303,
-99999999,
1234.38383000383830003838300,
nil,
true,
false,
TestThing.new,
test_drop,
test_enum,
["foo", "bar"],
{ "foo" => "bar" },
{ foo: "bar" },
[{ "foo" => "bar" }, { "foo" => 123 }, { "foo" => nil }, { "foo" => true }, { "foo" => ["foo", "bar"] }],
{ 1 => "bar" },
["foo", 123, nil, true, false, Drop, ["foo"], { foo: "bar" }],
]
test_types.each do |first|
test_types.each do |other|
(@filters.methods - Object.methods).each do |method|
arg_count = @filters.method(method).arity
arg_count *= -1 if arg_count < 0
inputs = [first]
inputs << ([other] * (arg_count - 1)) if arg_count > 1
begin
@filters.send(method, *inputs)
rescue Liquid::ArgumentError, Liquid::ZeroDivisionError
nil
end
end
end
end
end
def test_where_no_target_value def test_where_no_target_value
input = [ input = [
{ "foo" => false }, { "foo" => false },
@@ -771,7 +825,7 @@ class StandardFiltersTest < Minitest::Test
private private
def with_timezone(tz) def with_timezone(tz)
old_tz = ENV['TZ'] old_tz = ENV['TZ']
ENV['TZ'] = tz ENV['TZ'] = tz
yield yield
ensure ensure

View File

@@ -0,0 +1,51 @@
# frozen_string_literal: true
require 'test_helper'
class TagDisableableTest < Minitest::Test
include Liquid
class DisableRaw < Block
disable_tags "raw"
end
class DisableRawEcho < Block
disable_tags "raw", "echo"
end
class DisableableRaw < Liquid::Raw
prepend Liquid::Tag::Disableable
end
class DisableableEcho < Liquid::Echo
prepend Liquid::Tag::Disableable
end
def test_disables_raw
with_disableable_tags do
with_custom_tag('disable', DisableRaw) do
output = Template.parse('{% disable %}{% raw %}Foobar{% endraw %}{% echo "foo" %}{% enddisable %}').render
assert_equal('Liquid error: raw usage is not allowed in this contextfoo', output)
end
end
end
def test_disables_echo_and_raw
with_disableable_tags do
with_custom_tag('disable', DisableRawEcho) do
output = Template.parse('{% disable %}{% raw %}Foobar{% endraw %}{% echo "foo" %}{% enddisable %}').render
assert_equal('Liquid error: raw usage is not allowed in this contextLiquid error: echo usage is not allowed in this context', output)
end
end
end
private
def with_disableable_tags
with_custom_tag('raw', DisableableRaw) do
with_custom_tag('echo', DisableableEcho) do
yield
end
end
end
end

View File

@@ -0,0 +1,45 @@
# frozen_string_literal: true
require 'test_helper'
class TagTest < Minitest::Test
include Liquid
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

View File

@@ -8,8 +8,8 @@ class BreakTagTest < Minitest::Test
# tests that no weird errors are raised if break is called outside of a # tests that no weird errors are raised if break is called outside of a
# block # block
def test_break_with_no_block def test_break_with_no_block
assigns = { 'i' => 1 } assigns = { 'i' => 1 }
markup = '{% break %}' markup = '{% break %}'
expected = '' expected = ''
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)

View File

@@ -8,8 +8,8 @@ class ContinueTagTest < Minitest::Test
# tests that no weird errors are raised if continue is called outside of a # tests that no weird errors are raised if continue is called outside of a
# block # block
def test_continue_with_no_block def test_continue_with_no_block
assigns = {} assigns = {}
markup = '{% continue %}' markup = '{% continue %}'
expected = '' expected = ''
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)

View File

@@ -106,7 +106,7 @@ HERE
end end
def test_limiting_with_invalid_limit def test_limiting_with_invalid_limit
assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] }
template = <<-MKUP template = <<-MKUP
{% for i in array limit: true offset: 1 %} {% for i in array limit: true offset: 1 %}
{{ i }} {{ i }}
@@ -120,7 +120,7 @@ HERE
end end
def test_limiting_with_invalid_offset def test_limiting_with_invalid_offset
assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] }
template = <<-MKUP template = <<-MKUP
{% for i in array limit: 1 offset: true %} {% for i in array limit: 1 offset: true %}
{{ i }} {{ i }}
@@ -134,8 +134,8 @@ HERE
end end
def test_dynamic_variable_limiting def test_dynamic_variable_limiting
assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] }
assigns['limit'] = 2 assigns['limit'] = 2
assigns['offset'] = 2 assigns['offset'] = 2
assert_template_result('34', '{%for i in array limit: limit offset: offset %}{{ i }}{%endfor%}', assigns) assert_template_result('34', '{%for i in array limit: limit offset: offset %}{{ i }}{%endfor%}', assigns)
@@ -152,8 +152,8 @@ HERE
end end
def test_pause_resume def test_pause_resume
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } } assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } }
markup = <<-MKUP markup = <<-MKUP
{%for i in array.items limit: 3 %}{{i}}{%endfor%} {%for i in array.items limit: 3 %}{{i}}{%endfor%}
next next
{%for i in array.items offset:continue limit: 3 %}{{i}}{%endfor%} {%for i in array.items offset:continue limit: 3 %}{{i}}{%endfor%}
@@ -171,8 +171,8 @@ HERE
end end
def test_pause_resume_limit def test_pause_resume_limit
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } } assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } }
markup = <<-MKUP markup = <<-MKUP
{%for i in array.items limit:3 %}{{i}}{%endfor%} {%for i in array.items limit:3 %}{{i}}{%endfor%}
next next
{%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%} {%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%}
@@ -190,8 +190,8 @@ HERE
end end
def test_pause_resume_big_limit def test_pause_resume_big_limit
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } } assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } }
markup = <<-MKUP markup = <<-MKUP
{%for i in array.items limit:3 %}{{i}}{%endfor%} {%for i in array.items limit:3 %}{{i}}{%endfor%}
next next
{%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%} {%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%}
@@ -209,8 +209,8 @@ HERE
end end
def test_pause_resume_big_offset def test_pause_resume_big_offset
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } } assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } }
markup = '{%for i in array.items limit:3 %}{{i}}{%endfor%} markup = '{%for i in array.items limit:3 %}{{i}}{%endfor%}
next next
{%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%} {%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%}
next next
@@ -226,26 +226,26 @@ HERE
def test_for_with_break def test_for_with_break
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] } } assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] } }
markup = '{% for i in array.items %}{% break %}{% endfor %}' markup = '{% for i in array.items %}{% break %}{% endfor %}'
expected = "" expected = ""
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
markup = '{% for i in array.items %}{{ i }}{% break %}{% endfor %}' markup = '{% for i in array.items %}{{ i }}{% break %}{% endfor %}'
expected = "1" expected = "1"
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
markup = '{% for i in array.items %}{% break %}{{ i }}{% endfor %}' markup = '{% for i in array.items %}{% break %}{{ i }}{% endfor %}'
expected = "" expected = ""
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
markup = '{% for i in array.items %}{{ i }}{% if i > 3 %}{% break %}{% endif %}{% endfor %}' markup = '{% for i in array.items %}{{ i }}{% if i > 3 %}{% break %}{% endif %}{% endfor %}'
expected = "1234" expected = "1234"
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
# tests to ensure it only breaks out of the local for loop # tests to ensure it only breaks out of the local for loop
# and not all of them. # and not all of them.
assigns = { 'array' => [[1, 2], [3, 4], [5, 6]] } assigns = { 'array' => [[1, 2], [3, 4], [5, 6]] }
markup = '{% for item in array %}' \ markup = '{% for item in array %}' \
'{% for i in item %}' \ '{% for i in item %}' \
'{% if i == 1 %}' \ '{% if i == 1 %}' \
'{% break %}' \ '{% break %}' \
@@ -257,8 +257,8 @@ HERE
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
# test break does nothing when unreached # test break does nothing when unreached
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5] } } assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5] } }
markup = '{% for i in array.items %}{% if i == 9999 %}{% break %}{% endif %}{{ i }}{% endfor %}' markup = '{% for i in array.items %}{% if i == 9999 %}{% break %}{% endif %}{{ i }}{% endfor %}'
expected = '12345' expected = '12345'
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
end end
@@ -266,29 +266,29 @@ HERE
def test_for_with_continue def test_for_with_continue
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5] } } assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5] } }
markup = '{% for i in array.items %}{% continue %}{% endfor %}' markup = '{% for i in array.items %}{% continue %}{% endfor %}'
expected = "" expected = ""
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
markup = '{% for i in array.items %}{{ i }}{% continue %}{% endfor %}' markup = '{% for i in array.items %}{{ i }}{% continue %}{% endfor %}'
expected = "12345" expected = "12345"
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
markup = '{% for i in array.items %}{% continue %}{{ i }}{% endfor %}' markup = '{% for i in array.items %}{% continue %}{{ i }}{% endfor %}'
expected = "" expected = ""
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
markup = '{% for i in array.items %}{% if i > 3 %}{% continue %}{% endif %}{{ i }}{% endfor %}' markup = '{% for i in array.items %}{% if i > 3 %}{% continue %}{% endif %}{{ i }}{% endfor %}'
expected = "123" expected = "123"
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
markup = '{% for i in array.items %}{% if i == 3 %}{% continue %}{% else %}{{ i }}{% endif %}{% endfor %}' markup = '{% for i in array.items %}{% if i == 3 %}{% continue %}{% else %}{{ i }}{% endif %}{% endfor %}'
expected = "1245" expected = "1245"
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
# tests to ensure it only continues the local for loop and not all of them. # tests to ensure it only continues the local for loop and not all of them.
assigns = { 'array' => [[1, 2], [3, 4], [5, 6]] } assigns = { 'array' => [[1, 2], [3, 4], [5, 6]] }
markup = '{% for item in array %}' \ markup = '{% for item in array %}' \
'{% for i in item %}' \ '{% for i in item %}' \
'{% if i == 1 %}' \ '{% if i == 1 %}' \
'{% continue %}' \ '{% continue %}' \
@@ -300,8 +300,8 @@ HERE
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
# test continue does nothing when unreached # test continue does nothing when unreached
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5] } } assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5] } }
markup = '{% for i in array.items %}{% if i == 9999 %}{% continue %}{% endif %}{{ i }}{% endfor %}' markup = '{% for i in array.items %}{% if i == 9999 %}{% continue %}{% endif %}{{ i }}{% endfor %}'
expected = '12345' expected = '12345'
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
end end
@@ -389,8 +389,8 @@ HERE
end end
def test_iterate_with_each_when_no_limit_applied def test_iterate_with_each_when_no_limit_applied
loader = LoaderDrop.new([1, 2, 3, 4, 5]) loader = LoaderDrop.new([1, 2, 3, 4, 5])
assigns = { 'items' => loader } assigns = { 'items' => loader }
expected = '12345' expected = '12345'
template = '{% for item in items %}{{item}}{% endfor %}' template = '{% for item in items %}{{item}}{% endfor %}'
assert_template_result(expected, template, assigns) assert_template_result(expected, template, assigns)
@@ -399,8 +399,8 @@ HERE
end end
def test_iterate_with_load_slice_when_limit_applied def test_iterate_with_load_slice_when_limit_applied
loader = LoaderDrop.new([1, 2, 3, 4, 5]) loader = LoaderDrop.new([1, 2, 3, 4, 5])
assigns = { 'items' => loader } assigns = { 'items' => loader }
expected = '1' expected = '1'
template = '{% for item in items limit:1 %}{{item}}{% endfor %}' template = '{% for item in items limit:1 %}{{item}}{% endfor %}'
assert_template_result(expected, template, assigns) assert_template_result(expected, template, assigns)
@@ -409,8 +409,8 @@ HERE
end end
def test_iterate_with_load_slice_when_limit_and_offset_applied def test_iterate_with_load_slice_when_limit_and_offset_applied
loader = LoaderDrop.new([1, 2, 3, 4, 5]) loader = LoaderDrop.new([1, 2, 3, 4, 5])
assigns = { 'items' => loader } assigns = { 'items' => loader }
expected = '34' expected = '34'
template = '{% for item in items offset:2 limit:2 %}{{item}}{% endfor %}' template = '{% for item in items offset:2 limit:2 %}{{item}}{% endfor %}'
assert_template_result(expected, template, assigns) assert_template_result(expected, template, assigns)
@@ -419,11 +419,11 @@ HERE
end end
def test_iterate_with_load_slice_returns_same_results_as_without def test_iterate_with_load_slice_returns_same_results_as_without
loader = LoaderDrop.new([1, 2, 3, 4, 5]) loader = LoaderDrop.new([1, 2, 3, 4, 5])
loader_assigns = { 'items' => loader } loader_assigns = { 'items' => loader }
array_assigns = { 'items' => [1, 2, 3, 4, 5] } array_assigns = { 'items' => [1, 2, 3, 4, 5] }
expected = '34' expected = '34'
template = '{% for item in items offset:2 limit:2 %}{{item}}{% endfor %}' template = '{% for item in items offset:2 limit:2 %}{{item}}{% endfor %}'
assert_template_result(expected, template, loader_assigns) assert_template_result(expected, template, loader_assigns)
assert_template_result(expected, template, array_assigns) assert_template_result(expected, template, array_assigns)
end end

View File

@@ -45,7 +45,7 @@ class IfElseTagTest < Minitest::Test
def test_comparison_of_strings_containing_and_or_or def test_comparison_of_strings_containing_and_or_or
awful_markup = "a == 'and' and b == 'or' and c == 'foo and bar' and d == 'bar or baz' and e == 'foo' and foo and bar" awful_markup = "a == 'and' and b == 'or' and c == 'foo and bar' and d == 'bar or baz' and e == 'foo' and foo and bar"
assigns = { 'a' => 'and', 'b' => 'or', 'c' => 'foo and bar', 'd' => 'bar or baz', 'e' => 'foo', 'foo' => true, 'bar' => true } assigns = { 'a' => 'and', 'b' => 'or', 'c' => 'foo and bar', 'd' => 'bar or baz', 'e' => 'foo', 'foo' => true, 'bar' => true }
assert_template_result(' YES ', "{% if #{awful_markup} %} YES {% endif %}", assigns) assert_template_result(' YES ', "{% if #{awful_markup} %} YES {% endif %}", assigns)
end end

View File

@@ -8,6 +8,9 @@ class TestFileSystem
when "product" when "product"
"Product: {{ product.title }} " "Product: {{ product.title }} "
when "product_alias"
"Product: {{ product.title }} "
when "locale_variables" when "locale_variables"
"Locale: {{echo1}} {{echo2}}" "Locale: {{echo1}} {{echo2}}"
@@ -51,7 +54,7 @@ class CountingFileSystem
attr_reader :count attr_reader :count
def read_template_file(_template_path) def read_template_file(_template_path)
@count ||= 0 @count ||= 0
@count += 1 @count += 1
'from CountingFileSystem' 'from CountingFileSystem'
end end
end end
@@ -91,6 +94,16 @@ class IncludeTagTest < Minitest::Test
"{% include 'product' with products[0] %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }]) "{% include 'product' with products[0] %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end end
def test_include_tag_with_alias
assert_template_result("Product: Draft 151cm ",
"{% include 'product_alias' with products[0] as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
def test_include_tag_for_alias
assert_template_result("Product: Draft 151cm Product: Element 155cm ",
"{% include 'product_alias' for products as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
def test_include_tag_with_default_name def test_include_tag_with_default_name
assert_template_result("Product: Draft 151cm ", assert_template_result("Product: Draft 151cm ",
"{% include 'product' %}", "product" => { 'title' => 'Draft 151cm' }) "{% include 'product' %}", "product" => { 'title' => 'Draft 151cm' })

View File

@@ -81,6 +81,16 @@ class LiquidTagTest < Minitest::Test
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 end
def test_nested_liquid_tag
assert_template_result('good', <<~LIQUID)
{%- if true %}
{%- liquid
echo "good"
%}
{%- endif -%}
LIQUID
end
def test_cannot_open_blocks_living_past_a_liquid_tag def test_cannot_open_blocks_living_past_a_liquid_tag
assert_match_syntax_error("syntax error (line 3): 'if' tag was never closed", <<~LIQUID) assert_match_syntax_error("syntax error (line 3): 'if' tag was never closed", <<~LIQUID)
{%- liquid {%- liquid
@@ -90,8 +100,8 @@ class LiquidTagTest < Minitest::Test
LIQUID LIQUID
end end
def test_quirk_can_close_blocks_created_before_a_liquid_tag def test_cannot_close_blocks_created_before_a_liquid_tag
assert_template_result("42", <<~LIQUID) assert_match_syntax_error("syntax error (line 3): 'endif' is not a valid delimiter for liquid tags. use %}", <<~LIQUID)
{%- if true -%} {%- if true -%}
42 42
{%- liquid endif -%} {%- liquid endif -%}

View File

@@ -42,32 +42,6 @@ class RenderTagTest < Minitest::Test
assert_template_result('', "{% assign snippet = 'should not be visible' %}{% render 'snippet' %}") assert_template_result('', "{% assign snippet = 'should not be visible' %}{% render 'snippet' %}")
end 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 def test_render_does_not_mutate_parent_scope
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{% assign inner = 1 %}') Liquid::Template.file_system = StubFileSystem.new('snippet' => '{% assign inner = 1 %}')
assert_template_result('', "{% render 'snippet' %}{{ inner }}") assert_template_result('', "{% render 'snippet' %}{{ inner }}")
@@ -153,7 +127,10 @@ class RenderTagTest < Minitest::Test
'test_include' => '{% include "foo" %}' 'test_include' => '{% include "foo" %}'
) )
assert_template_result('include usage is not allowed in this context', '{% render "test_include" %}') exc = assert_raises(Liquid::DisabledError) do
Liquid::Template.parse('{% render "test_include" %}').render!
end
assert_equal('Liquid error: include usage is not allowed in this context', exc.message)
end end
def test_includes_will_not_render_inside_nested_sibling_tags def test_includes_will_not_render_inside_nested_sibling_tags
@@ -163,6 +140,74 @@ class RenderTagTest < Minitest::Test
'test_include' => '{% include "foo" %}' 'test_include' => '{% include "foo" %}'
) )
assert_template_result('include usage is not allowed in this contextinclude usage is not allowed in this context', '{% render "nested_render_with_sibling_include" %}') output = Liquid::Template.parse('{% render "nested_render_with_sibling_include" %}').render
assert_equal('Liquid error: include usage is not allowed in this contextLiquid error: include usage is not allowed in this context', output)
end
def test_render_tag_with
Liquid::Template.file_system = StubFileSystem.new(
'product' => "Product: {{ product.title }} ",
'product_alias' => "Product: {{ product.title }} ",
)
assert_template_result("Product: Draft 151cm ",
"{% render 'product' with products[0] %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
def test_render_tag_with_alias
Liquid::Template.file_system = StubFileSystem.new(
'product' => "Product: {{ product.title }} ",
'product_alias' => "Product: {{ product.title }} ",
)
assert_template_result("Product: Draft 151cm ",
"{% render 'product_alias' with products[0] as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
def test_render_tag_for_alias
Liquid::Template.file_system = StubFileSystem.new(
'product' => "Product: {{ product.title }} ",
'product_alias' => "Product: {{ product.title }} ",
)
assert_template_result("Product: Draft 151cm Product: Element 155cm ",
"{% render 'product_alias' for products as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
def test_render_tag_for
Liquid::Template.file_system = StubFileSystem.new(
'product' => "Product: {{ product.title }} ",
'product_alias' => "Product: {{ product.title }} ",
)
assert_template_result("Product: Draft 151cm Product: Element 155cm ",
"{% render 'product' for products %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
def test_render_tag_forloop
Liquid::Template.file_system = StubFileSystem.new(
'product' => "Product: {{ product.title }} {% if forloop.first %}first{% endif %} {% if forloop.last %}last{% endif %} index:{{ forloop.index }} ",
)
assert_template_result("Product: Draft 151cm first index:1 Product: Element 155cm last index:2 ",
"{% render 'product' for products %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
def test_render_tag_for_drop
Liquid::Template.file_system = StubFileSystem.new(
'loop' => "{{ value.foo }}",
)
assert_template_result("123",
"{% render 'loop' for loop as value %}", "loop" => TestEnumerable.new)
end
def test_render_tag_with_drop
Liquid::Template.file_system = StubFileSystem.new(
'loop' => "{{ value }}",
)
assert_template_result("TestEnumerable",
"{% render 'loop' with loop as value %}", "loop" => TestEnumerable.new)
end end
end end

View File

@@ -174,7 +174,7 @@ class StandardTagTest < Minitest::Test
def test_assign_from_case def test_assign_from_case
# Example from the shopify forums # Example from the shopify forums
code = "{% case collection.handle %}{% when 'menswear-jackets' %}{% assign ptitle = 'menswear' %}{% when 'menswear-t-shirts' %}{% assign ptitle = 'menswear' %}{% else %}{% assign ptitle = 'womenswear' %}{% endcase %}{{ ptitle }}" code = "{% case collection.handle %}{% when 'menswear-jackets' %}{% assign ptitle = 'menswear' %}{% when 'menswear-t-shirts' %}{% assign ptitle = 'menswear' %}{% else %}{% assign ptitle = 'womenswear' %}{% endcase %}{{ ptitle }}"
template = Liquid::Template.parse(code) template = Liquid::Template.parse(code)
assert_equal("menswear", template.render!("collection" => { 'handle' => 'menswear-jackets' })) assert_equal("menswear", template.render!("collection" => { 'handle' => 'menswear-jackets' }))
assert_equal("menswear", template.render!("collection" => { 'handle' => 'menswear-t-shirts' })) assert_equal("menswear", template.render!("collection" => { 'handle' => 'menswear-t-shirts' }))

View File

@@ -83,7 +83,7 @@ class TemplateTest < Minitest::Test
t = Template.new t = Template.new
t.assigns['number'] = -> { t.assigns['number'] = -> {
@global ||= 0 @global ||= 0
@global += 1 @global += 1
} }
assert_equal('1', t.parse("{{number}}").render!) assert_equal('1', t.parse("{{number}}").render!)
assert_equal('1', t.parse("{{number}}").render!) assert_equal('1', t.parse("{{number}}").render!)
@@ -95,7 +95,7 @@ class TemplateTest < Minitest::Test
t = Template.new t = Template.new
assigns = { 'number' => -> { assigns = { 'number' => -> {
@global ||= 0 @global ||= 0
@global += 1 @global += 1
} } } }
assert_equal('1', t.parse("{{number}}").render!(assigns)) assert_equal('1', t.parse("{{number}}").render!(assigns))
assert_equal('1', t.parse("{{number}}").render!(assigns)) assert_equal('1', t.parse("{{number}}").render!(assigns))
@@ -111,13 +111,12 @@ class TemplateTest < Minitest::Test
def test_resource_limits_render_length def test_resource_limits_render_length
t = Template.parse("0123456789") t = Template.parse("0123456789")
t.resource_limits.render_length_limit = 5 t.resource_limits.render_length_limit = 9
assert_equal("Liquid error: Memory limits exceeded", t.render) assert_equal("Liquid error: Memory limits exceeded", t.render)
assert(t.resource_limits.reached?) assert(t.resource_limits.reached?)
t.resource_limits.render_length_limit = 10 t.resource_limits.render_length_limit = 10
assert_equal("0123456789", t.render!) assert_equal("0123456789", t.render!)
refute_nil(t.resource_limits.render_length)
end end
def test_resource_limits_render_score def test_resource_limits_render_score
@@ -176,50 +175,46 @@ class TemplateTest < Minitest::Test
end end
def test_resource_limits_hash_in_template_gets_updated_even_if_no_limits_are_set def test_resource_limits_hash_in_template_gets_updated_even_if_no_limits_are_set
t = Template.parse("{% for a in (1..100) %} {% assign foo = 1 %} {% endfor %}") t = Template.parse("{% for a in (1..100) %}x{% assign foo = 1 %} {% endfor %}")
t.render! t.render!
assert(t.resource_limits.assign_score > 0) assert(t.resource_limits.assign_score > 0)
assert(t.resource_limits.render_score > 0) assert(t.resource_limits.render_score > 0)
assert(t.resource_limits.render_length > 0)
end end
def test_render_length_persists_between_blocks def test_render_length_persists_between_blocks
t = Template.parse("{% if true %}aaaa{% endif %}") t = Template.parse("{% if true %}aaaa{% endif %}")
t.resource_limits.render_length_limit = 7 t.resource_limits.render_length_limit = 3
assert_equal("Liquid error: Memory limits exceeded", t.render) assert_equal("Liquid error: Memory limits exceeded", t.render)
t.resource_limits.render_length_limit = 8 t.resource_limits.render_length_limit = 4
assert_equal("aaaa", t.render) assert_equal("aaaa", t.render)
t = Template.parse("{% if true %}aaaa{% endif %}{% if true %}bbb{% endif %}") t = Template.parse("{% if true %}aaaa{% endif %}{% if true %}bbb{% endif %}")
t.resource_limits.render_length_limit = 13 t.resource_limits.render_length_limit = 6
assert_equal("Liquid error: Memory limits exceeded", t.render) assert_equal("Liquid error: Memory limits exceeded", t.render)
t.resource_limits.render_length_limit = 14 t.resource_limits.render_length_limit = 7
assert_equal("aaaabbb", t.render) assert_equal("aaaabbb", t.render)
t = Template.parse("{% if true %}a{% endif %}{% if true %}b{% endif %}{% if true %}a{% endif %}{% if true %}b{% endif %}{% if true %}a{% endif %}{% if true %}b{% endif %}") t = Template.parse("{% if true %}a{% endif %}{% if true %}b{% endif %}{% if true %}a{% endif %}{% if true %}b{% endif %}{% if true %}a{% endif %}{% if true %}b{% endif %}")
t.resource_limits.render_length_limit = 5 t.resource_limits.render_length_limit = 5
assert_equal("Liquid error: Memory limits exceeded", t.render) assert_equal("Liquid error: Memory limits exceeded", t.render)
t.resource_limits.render_length_limit = 11 t.resource_limits.render_length_limit = 6
assert_equal("Liquid error: Memory limits exceeded", t.render)
t.resource_limits.render_length_limit = 12
assert_equal("ababab", t.render) assert_equal("ababab", t.render)
end end
def test_render_length_uses_number_of_bytes_not_characters def test_render_length_uses_number_of_bytes_not_characters
t = Template.parse("{% if true %}すごい{% endif %}") t = Template.parse("{% if true %}すごい{% endif %}")
t.resource_limits.render_length_limit = 10 t.resource_limits.render_length_limit = 8
assert_equal("Liquid error: Memory limits exceeded", t.render) assert_equal("Liquid error: Memory limits exceeded", t.render)
t.resource_limits.render_length_limit = 18 t.resource_limits.render_length_limit = 9
assert_equal("すごい", t.render) assert_equal("すごい", t.render)
end end
def test_default_resource_limits_unaffected_by_render_with_context def test_default_resource_limits_unaffected_by_render_with_context
context = Context.new context = Context.new
t = Template.parse("{% for a in (1..100) %} {% assign foo = 1 %} {% endfor %}") t = Template.parse("{% for a in (1..100) %}x{% assign foo = 1 %} {% endfor %}")
t.render!(context) t.render!(context)
assert(context.resource_limits.assign_score > 0) assert(context.resource_limits.assign_score > 0)
assert(context.resource_limits.render_score > 0) assert(context.resource_limits.render_score > 0)
assert(context.resource_limits.render_length > 0)
end end
def test_can_use_drop_as_context def test_can_use_drop_as_context
@@ -243,7 +238,7 @@ class TemplateTest < Minitest::Test
def test_exception_renderer_that_returns_string def test_exception_renderer_that_returns_string
exception = nil exception = nil
handler = ->(e) { handler = ->(e) {
exception = e exception = e
'<!-- error -->' '<!-- error -->'
} }
@@ -267,20 +262,20 @@ class TemplateTest < Minitest::Test
def test_global_filter_option_on_render def test_global_filter_option_on_render
global_filter_proc = ->(output) { "#{output} filtered" } global_filter_proc = ->(output) { "#{output} filtered" }
rendered_template = Template.parse("{{name}}").render({ "name" => "bob" }, global_filter: global_filter_proc) rendered_template = Template.parse("{{name}}").render({ "name" => "bob" }, global_filter: global_filter_proc)
assert_equal('bob filtered', rendered_template) assert_equal('bob filtered', rendered_template)
end end
def test_global_filter_option_when_native_filters_exist def test_global_filter_option_when_native_filters_exist
global_filter_proc = ->(output) { "#{output} filtered" } global_filter_proc = ->(output) { "#{output} filtered" }
rendered_template = Template.parse("{{name | upcase}}").render({ "name" => "bob" }, global_filter: global_filter_proc) rendered_template = Template.parse("{{name | upcase}}").render({ "name" => "bob" }, global_filter: global_filter_proc)
assert_equal('BOB filtered', rendered_template) assert_equal('BOB filtered', rendered_template)
end end
def test_undefined_variables def test_undefined_variables
t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}") t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}")
result = t.render({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, strict_variables: true) result = t.render({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, strict_variables: true)
assert_equal('33 32 ', result) assert_equal('33 32 ', result)
@@ -295,8 +290,8 @@ class TemplateTest < Minitest::Test
def test_nil_value_does_not_raise def test_nil_value_does_not_raise
Liquid::Template.error_mode = :strict Liquid::Template.error_mode = :strict
t = Template.parse("some{{x}}thing") t = Template.parse("some{{x}}thing")
result = t.render!({ 'x' => nil }, strict_variables: true) result = t.render!({ 'x' => nil }, strict_variables: true)
assert_equal(0, t.errors.count) assert_equal(0, t.errors.count)
assert_equal('something', result) assert_equal('something', result)

View File

@@ -69,7 +69,7 @@ class TrimModeTest < Minitest::Test
# Make sure the trim isn't applied to standard tags # Make sure the trim isn't applied to standard tags
def test_standard_tags def test_standard_tags
whitespace = ' ' whitespace = ' '
text = <<-END_TEMPLATE text = <<-END_TEMPLATE
<div> <div>
<p> <p>
{% if true %} {% if true %}
@@ -110,58 +110,58 @@ class TrimModeTest < Minitest::Test
# Make sure the trim isn't too agressive # Make sure the trim isn't too agressive
def test_no_trim_output def test_no_trim_output
text = '<p>{{- \'John\' -}}</p>' text = '<p>{{- \'John\' -}}</p>'
expected = '<p>John</p>' expected = '<p>John</p>'
assert_template_result(expected, text) assert_template_result(expected, text)
end end
# Make sure the trim isn't too agressive # Make sure the trim isn't too agressive
def test_no_trim_tags def test_no_trim_tags
text = '<p>{%- if true -%}yes{%- endif -%}</p>' text = '<p>{%- if true -%}yes{%- endif -%}</p>'
expected = '<p>yes</p>' expected = '<p>yes</p>'
assert_template_result(expected, text) assert_template_result(expected, text)
text = '<p>{%- if false -%}no{%- endif -%}</p>' text = '<p>{%- if false -%}no{%- endif -%}</p>'
expected = '<p></p>' expected = '<p></p>'
assert_template_result(expected, text) assert_template_result(expected, text)
end end
def test_single_line_outer_tag def test_single_line_outer_tag
text = '<p> {%- if true %} yes {% endif -%} </p>' text = '<p> {%- if true %} yes {% endif -%} </p>'
expected = '<p> yes </p>' expected = '<p> yes </p>'
assert_template_result(expected, text) assert_template_result(expected, text)
text = '<p> {%- if false %} no {% endif -%} </p>' text = '<p> {%- if false %} no {% endif -%} </p>'
expected = '<p></p>' expected = '<p></p>'
assert_template_result(expected, text) assert_template_result(expected, text)
end end
def test_single_line_inner_tag def test_single_line_inner_tag
text = '<p> {% if true -%} yes {%- endif %} </p>' text = '<p> {% if true -%} yes {%- endif %} </p>'
expected = '<p> yes </p>' expected = '<p> yes </p>'
assert_template_result(expected, text) assert_template_result(expected, text)
text = '<p> {% if false -%} no {%- endif %} </p>' text = '<p> {% if false -%} no {%- endif %} </p>'
expected = '<p> </p>' expected = '<p> </p>'
assert_template_result(expected, text) assert_template_result(expected, text)
end end
def test_single_line_post_tag def test_single_line_post_tag
text = '<p> {% if true -%} yes {% endif -%} </p>' text = '<p> {% if true -%} yes {% endif -%} </p>'
expected = '<p> yes </p>' expected = '<p> yes </p>'
assert_template_result(expected, text) assert_template_result(expected, text)
text = '<p> {% if false -%} no {% endif -%} </p>' text = '<p> {% if false -%} no {% endif -%} </p>'
expected = '<p> </p>' expected = '<p> </p>'
assert_template_result(expected, text) assert_template_result(expected, text)
end end
def test_single_line_pre_tag def test_single_line_pre_tag
text = '<p> {%- if true %} yes {%- endif %} </p>' text = '<p> {%- if true %} yes {%- endif %} </p>'
expected = '<p> yes </p>' expected = '<p> yes </p>'
assert_template_result(expected, text) assert_template_result(expected, text)
text = '<p> {%- if false %} no {%- endif %} </p>' text = '<p> {%- if false %} no {%- endif %} </p>'
expected = '<p> </p>' expected = '<p> </p>'
assert_template_result(expected, text) assert_template_result(expected, text)
end end
@@ -330,7 +330,7 @@ class TrimModeTest < Minitest::Test
assert_template_result(expected, text) assert_template_result(expected, text)
whitespace = ' ' whitespace = ' '
text = <<-END_TEMPLATE text = <<-END_TEMPLATE
<div> <div>
<p> <p>
{% if false -%} {% if false -%}
@@ -504,7 +504,7 @@ class TrimModeTest < Minitest::Test
def test_raw_output def test_raw_output
whitespace = ' ' whitespace = ' '
text = <<-END_TEMPLATE text = <<-END_TEMPLATE
<div> <div>
{% raw %} {% raw %}
{%- if true -%} {%- if true -%}
@@ -528,4 +528,32 @@ class TrimModeTest < Minitest::Test
END_EXPECTED END_EXPECTED
assert_template_result(expected, text) assert_template_result(expected, text)
end end
def test_pre_trim_blank_preceding_text
template = Liquid::Template.parse("\n{%- raw %}{% endraw %}")
assert_equal("", template.render)
template = Liquid::Template.parse("\n{%- if true %}{% endif %}")
assert_equal("", template.render)
template = Liquid::Template.parse("{{ 'B' }} \n{%- if true %}C{% endif %}")
assert_equal("BC", template.render)
end
def test_bug_compatible_pre_trim
template = Liquid::Template.parse("\n {%- raw %}{% endraw %}", bug_compatible_whitespace_trimming: true)
assert_equal("\n", template.render)
template = Liquid::Template.parse("\n {%- if true %}{% endif %}", bug_compatible_whitespace_trimming: true)
assert_equal("\n", template.render)
template = Liquid::Template.parse("{{ 'B' }} \n{%- if true %}C{% endif %}", bug_compatible_whitespace_trimming: true)
assert_equal("B C", template.render)
template = Liquid::Template.parse("B\n {%- raw %}{% endraw %}", bug_compatible_whitespace_trimming: true)
assert_equal("B", template.render)
template = Liquid::Template.parse("B\n {%- if true %}{% endif %}", bug_compatible_whitespace_trimming: true)
assert_equal("B", template.render)
end
end # TrimModeTest end # TrimModeTest

View File

@@ -52,13 +52,13 @@ class VariableTest < Minitest::Test
end end
def test_preset_assigns def test_preset_assigns
template = Template.parse(%({{ test }})) template = Template.parse(%({{ test }}))
template.assigns['test'] = 'worked' template.assigns['test'] = 'worked'
assert_equal('worked', template.render!) assert_equal('worked', template.render!)
end end
def test_reuse_parsed_template def test_reuse_parsed_template
template = Template.parse(%({{ greeting }} {{ name }})) template = Template.parse(%({{ greeting }} {{ name }}))
template.assigns['greeting'] = 'Goodbye' template.assigns['greeting'] = 'Goodbye'
assert_equal('Hello Tobi', template.render!('greeting' => 'Hello', 'name' => 'Tobi')) assert_equal('Hello Tobi', template.render!('greeting' => 'Hello', 'name' => 'Tobi'))
assert_equal('Hello ', template.render!('greeting' => 'Hello', 'unknown' => 'Tobi')) assert_equal('Hello ', template.render!('greeting' => 'Hello', 'unknown' => 'Tobi'))
@@ -68,7 +68,7 @@ class VariableTest < Minitest::Test
end end
def test_assigns_not_polluted_from_template def test_assigns_not_polluted_from_template
template = Template.parse(%({{ test }}{% assign test = 'bar' %}{{ test }})) template = Template.parse(%({{ test }}{% assign test = 'bar' %}{{ test }}))
template.assigns['test'] = 'baz' template.assigns['test'] = 'baz'
assert_equal('bazbar', template.render!) assert_equal('bazbar', template.render!)
assert_equal('bazbar', template.render!) assert_equal('bazbar', template.render!)
@@ -77,8 +77,8 @@ class VariableTest < Minitest::Test
end end
def test_hash_with_default_proc def test_hash_with_default_proc
template = Template.parse(%(Hello {{ test }})) template = Template.parse(%(Hello {{ test }}))
assigns = Hash.new { |_h, k| raise "Unknown variable '#{k}'" } assigns = Hash.new { |_h, k| raise "Unknown variable '#{k}'" }
assigns['test'] = 'Tobi' assigns['test'] = 'Tobi'
assert_equal('Hello Tobi', template.render!(assigns)) assert_equal('Hello Tobi', template.render!(assigns))
assigns.delete('test') assigns.delete('test')

View File

@@ -54,28 +54,39 @@ module Minitest
assert_match(match, exception.message) assert_match(match, exception.message)
end end
def assert_usage_increment(name, times: 1)
old_method = Liquid::Usage.method(:increment)
calls = 0
begin
Liquid::Usage.singleton_class.send(:remove_method, :increment)
Liquid::Usage.define_singleton_method(:increment) do |got_name|
calls += 1 if got_name == name
old_method.call(got_name)
end
yield
ensure
Liquid::Usage.singleton_class.send(:remove_method, :increment)
Liquid::Usage.define_singleton_method(:increment, old_method)
end
assert_equal(times, calls, "Number of calls to Usage.increment with #{name.inspect}")
end
def with_global_filter(*globals) def with_global_filter(*globals)
original_global_strainer = Liquid::Strainer.class_variable_get(:@@global_strainer) original_global_filters = Liquid::StrainerFactory.instance_variable_get(:@global_filters)
Liquid::Strainer.class_variable_set(:@@global_strainer, Class.new(Liquid::Strainer) do Liquid::StrainerFactory.instance_variable_set(:@global_filters, [])
@filter_methods = Set.new globals.each do |global|
end) Liquid::StrainerFactory.add_global_filter(global)
Liquid::Strainer.class_variable_get(:@@strainer_class_cache).clear end
Liquid::StrainerFactory.send(:strainer_class_cache).clear
globals.each do |global| globals.each do |global|
Liquid::Template.register_filter(global) Liquid::Template.register_filter(global)
end end
yield yield
ensure ensure
Liquid::Strainer.class_variable_get(:@@strainer_class_cache).clear Liquid::StrainerFactory.send(:strainer_class_cache).clear
Liquid::Strainer.class_variable_set(:@@global_strainer, original_global_strainer) Liquid::StrainerFactory.instance_variable_set(:@global_filters, original_global_filters)
end
def with_taint_mode(mode)
old_mode = Liquid::Template.taint_mode
Liquid::Template.taint_mode = mode
yield
ensure
Liquid::Template.taint_mode = old_mode
end end
def with_error_mode(mode) def with_error_mode(mode)
@@ -87,10 +98,17 @@ module Minitest
end end
def with_custom_tag(tag_name, tag_class) def with_custom_tag(tag_name, tag_class)
Liquid::Template.register_tag(tag_name, tag_class) old_tag = Liquid::Template.tags[tag_name]
yield begin
ensure Liquid::Template.register_tag(tag_name, tag_class)
Liquid::Template.tags.delete(tag_name) yield
ensure
if old_tag
Liquid::Template.tags[tag_name] = old_tag
else
Liquid::Template.tags.delete(tag_name)
end
end
end end
end end
end end
@@ -128,7 +146,7 @@ class StubFileSystem
def initialize(values) def initialize(values)
@file_read_count = 0 @file_read_count = 0
@values = values @values = values
end end
def read_template_file(template_path) def read_template_file(template_path)
@@ -136,3 +154,16 @@ class StubFileSystem
@values.fetch(template_path) @values.fetch(template_path)
end end
end end
class StubTemplateFactory
attr_reader :count
def initialize
@count = 0
end
def for(_template_name)
@count += 1
Liquid::Template.new
end
end

View File

@@ -45,53 +45,9 @@ class BlockUnitTest < Minitest::Test
assert_equal(3, template.root.nodelist.size) assert_equal(3, template.root.nodelist.size)
end end
def test_with_custom_tag
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 private
def block_types(nodelist) def block_types(nodelist)
nodelist.collect(&:class) nodelist.collect(&:class)
end end
end # VariableTest end

View File

@@ -75,9 +75,9 @@ class ConditionUnitTest < Minitest::Test
end end
def test_contains_works_on_arrays def test_contains_works_on_arrays
@context = Liquid::Context.new @context = Liquid::Context.new
@context['array'] = [1, 2, 3, 4, 5] @context['array'] = [1, 2, 3, 4, 5]
array_expr = VariableLookup.new("array") array_expr = VariableLookup.new("array")
assert_evaluates_false(array_expr, 'contains', 0) assert_evaluates_false(array_expr, 'contains', 0)
assert_evaluates_true(array_expr, 'contains', 1) assert_evaluates_true(array_expr, 'contains', 1)
@@ -142,7 +142,7 @@ class ConditionUnitTest < Minitest::Test
end end
def test_left_or_right_may_contain_operators def test_left_or_right_may_contain_operators
@context = Liquid::Context.new @context = Liquid::Context.new
@context['one'] = @context['another'] = "gnomeslab-and-or-liquid" @context['one'] = @context['another'] = "gnomeslab-and-or-liquid"
assert_evaluates_true(VariableLookup.new("one"), '==', VariableLookup.new("another")) assert_evaluates_true(VariableLookup.new("one"), '==', VariableLookup.new("another"))

View File

@@ -238,7 +238,7 @@ class ParseTreeVisitorTest < Minitest::Test
def traversal(template) def traversal(template)
ParseTreeVisitor ParseTreeVisitor
.for(Template.parse(template).root) .for(Template.parse(template).root)
.add_callback_for(VariableLookup, &:name) .add_callback_for(VariableLookup) { |node| node.name } # rubocop:disable Style/SymbolProc
end end
def visit(template) def visit(template)

View File

@@ -21,7 +21,7 @@ class PartialCacheUnitTest < Minitest::Test
def test_reads_from_the_file_system_only_once_per_file def test_reads_from_the_file_system_only_once_per_file
file_system = StubFileSystem.new('my_partial' => 'some partial body') file_system = StubFileSystem.new('my_partial' => 'some partial body')
context = Liquid::Context.build( context = Liquid::Context.build(
registers: { file_system: file_system } registers: { file_system: file_system }
) )
@@ -37,7 +37,7 @@ class PartialCacheUnitTest < Minitest::Test
end end
def test_cache_state_is_stored_per_context def test_cache_state_is_stored_per_context
parse_context = Liquid::ParseContext.new parse_context = Liquid::ParseContext.new
shared_file_system = StubFileSystem.new( shared_file_system = StubFileSystem.new(
'my_partial' => 'my shared value' 'my_partial' => 'my shared value'
) )
@@ -71,7 +71,7 @@ class PartialCacheUnitTest < Minitest::Test
def test_cache_is_not_broken_when_a_different_parse_context_is_used def test_cache_is_not_broken_when_a_different_parse_context_is_used
file_system = StubFileSystem.new('my_partial' => 'some partial body') file_system = StubFileSystem.new('my_partial' => 'some partial body')
context = Liquid::Context.build( context = Liquid::Context.build(
registers: { file_system: file_system } registers: { file_system: file_system }
) )
@@ -90,4 +90,39 @@ class PartialCacheUnitTest < Minitest::Test
# but measuring file reads is an OK proxy for this. # but measuring file reads is an OK proxy for this.
assert_equal(1, file_system.file_read_count) assert_equal(1, file_system.file_read_count)
end end
def test_uses_default_template_factory_when_no_template_factory_found_in_register
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_uses_template_factory_register_if_present
template_factory = StubTemplateFactory.new
context = Liquid::Context.build(
registers: {
file_system: StubFileSystem.new('my_partial' => 'my partial body'),
template_factory: template_factory,
}
)
partial = Liquid::PartialCache.load(
'my_partial',
context: context,
parse_context: Liquid::ParseContext.new
)
assert_equal('my partial body', partial.render)
assert_equal(1, template_factory.count)
end
end end

View File

@@ -1,36 +0,0 @@
# frozen_string_literal: true
require 'test_helper'
class DisabledTagsUnitTest < Minitest::Test
include Liquid
def test_disables_tag_specified
register = DisabledTags.new
register.disable(%w(foo bar)) do
assert_equal true, register.disabled?("foo")
assert_equal true, register.disabled?("bar")
assert_equal false, register.disabled?("unknown")
end
end
def test_disables_nested_tags
register = DisabledTags.new
register.disable(["foo"]) do
register.disable(["foo"]) do
assert_equal true, register.disabled?("foo")
assert_equal false, register.disabled?("bar")
end
register.disable(["bar"]) do
assert_equal true, register.disabled?("foo")
assert_equal true, register.disabled?("bar")
register.disable(["foo"]) do
assert_equal true, register.disabled?("foo")
assert_equal true, register.disabled?("bar")
end
end
assert_equal true, register.disabled?("foo")
assert_equal false, register.disabled?("bar")
end
end
end

View File

@@ -5,244 +5,152 @@ require 'test_helper'
class StaticRegistersUnitTest < Minitest::Test class StaticRegistersUnitTest < Minitest::Test
include Liquid include Liquid
def set def test_set
static_register = StaticRegisters.new static_register = StaticRegisters.new(a: 1, b: 2)
static_register[nil] = true static_register[:b] = 22
static_register[1] = :one static_register[:c] = 33
static_register[:one] = "one"
static_register["two"] = "three"
static_register["two"] = 3
static_register[false] = nil
assert_equal({ nil => true, 1 => :one, :one => "one", "two" => 3, false => nil }, static_register.registers) assert_equal(1, static_register[:a])
assert_equal(22, static_register[:b])
static_register assert_equal(33, static_register[:c])
end end
def test_get def test_get_missing_key
static_register = set static_register = StaticRegisters.new
assert_equal(true, static_register[nil]) assert_nil(static_register[:missing])
assert_equal(:one, static_register[1])
assert_equal("one", static_register[:one])
assert_equal(3, static_register["two"])
assert_nil(static_register[false])
assert_nil(static_register["unknown"])
end end
def test_delete def test_delete
static_register = set static_register = StaticRegisters.new(a: 1, b: 2)
static_register[:b] = 22
static_register[:c] = 33
assert_equal(true, static_register.delete(nil)) assert_nil(static_register.delete(:a))
assert_equal(:one, static_register.delete(1))
assert_equal("one", static_register.delete(:one))
assert_equal(3, static_register.delete("two"))
assert_nil(static_register.delete(false))
assert_nil(static_register.delete("unknown"))
assert_equal({}, static_register.registers) assert_equal(22, static_register.delete(:b))
assert_equal(33, static_register.delete(:c))
assert_nil(static_register[:c])
assert_nil(static_register.delete(:d))
end end
def test_fetch def test_fetch
static_register = set static_register = StaticRegisters.new(a: 1, b: 2)
static_register[:b] = 22
static_register[:c] = 33
assert_equal(true, static_register.fetch(nil)) assert_equal(1, static_register.fetch(:a))
assert_equal(:one, static_register.fetch(1)) assert_equal(1, static_register.fetch(:a, "default"))
assert_equal("one", static_register.fetch(:one)) assert_equal(22, static_register.fetch(:b))
assert_equal(3, static_register.fetch("two")) assert_equal(22, static_register.fetch(:b, "default"))
assert_nil(static_register.fetch(false)) assert_equal(33, static_register.fetch(:c))
assert_nil(static_register.fetch("unknown")) assert_equal(33, static_register.fetch(:c, "default"))
end
def test_fetch_default assert_raises(KeyError) do
static_register = StaticRegisters.new static_register.fetch(:d)
end
assert_equal("default", static_register.fetch(:d, "default"))
assert_equal(true, static_register.fetch(nil, true)) result = static_register.fetch(:d) { "default" }
assert_equal(:one, static_register.fetch(1, :one)) assert_equal("default", result)
assert_equal("one", static_register.fetch(:one, "one"))
assert_equal(3, static_register.fetch("two", 3)) result = static_register.fetch(:d, "default 1") { "default 2" }
assert_nil(static_register.fetch(false, nil)) assert_equal("default 2", result)
end end
def test_key def test_key
static_register = set static_register = StaticRegisters.new(a: 1, b: 2)
static_register[:b] = 22
static_register[:c] = 33
assert_equal(true, static_register.key?(nil)) assert_equal(true, static_register.key?(:a))
assert_equal(true, static_register.key?(1)) assert_equal(true, static_register.key?(:b))
assert_equal(true, static_register.key?(:one)) assert_equal(true, static_register.key?(:c))
assert_equal(true, static_register.key?("two")) assert_equal(false, static_register.key?(:d))
assert_equal(true, static_register.key?(false))
assert_equal(false, static_register.key?("unknown"))
assert_equal(false, static_register.key?(true))
end
def set_with_static
static_register = StaticRegisters.new(nil => true, 1 => :one, :one => "one", "two" => 3, false => nil)
static_register[nil] = false
static_register["two"] = 4
static_register[true] = "foo"
assert_equal({ nil => true, 1 => :one, :one => "one", "two" => 3, false => nil }, static_register.static)
assert_equal({ nil => false, "two" => 4, true => "foo" }, static_register.registers)
static_register
end
def test_get_with_static
static_register = set_with_static
assert_equal(false, static_register[nil])
assert_equal(:one, static_register[1])
assert_equal("one", static_register[:one])
assert_equal(4, static_register["two"])
assert_equal("foo", static_register[true])
assert_nil(static_register[false])
end
def test_delete_with_static
static_register = set_with_static
assert_equal(false, static_register.delete(nil))
assert_equal(4, static_register.delete("two"))
assert_equal("foo", static_register.delete(true))
assert_nil(static_register.delete("unknown"))
assert_nil(static_register.delete(:one))
assert_equal({}, static_register.registers)
assert_equal({ nil => true, 1 => :one, :one => "one", "two" => 3, false => nil }, static_register.static)
end
def test_fetch_with_static
static_register = set_with_static
assert_equal(false, static_register.fetch(nil))
assert_equal(:one, static_register.fetch(1))
assert_equal("one", static_register.fetch(:one))
assert_equal(4, static_register.fetch("two"))
assert_equal("foo", static_register.fetch(true))
assert_nil(static_register.fetch(false))
end
def test_key_with_static
static_register = set_with_static
assert_equal(true, static_register.key?(nil))
assert_equal(true, static_register.key?(1))
assert_equal(true, static_register.key?(:one))
assert_equal(true, static_register.key?("two"))
assert_equal(true, static_register.key?(false))
assert_equal(false, static_register.key?("unknown"))
assert_equal(true, static_register.key?(true))
end end
def test_static_register_can_be_frozen def test_static_register_can_be_frozen
static_register = set_with_static static_register = StaticRegisters.new(a: 1)
static = static_register.static.freeze static_register.static.freeze
assert_raises(RuntimeError) do assert_raises(RuntimeError) do
static["two"] = "foo" static_register.static[:a] = "foo"
end end
assert_raises(RuntimeError) do assert_raises(RuntimeError) do
static["unknown"] = "foo" static_register.static[:b] = "foo"
end end
assert_raises(RuntimeError) do assert_raises(RuntimeError) do
static.delete("two") static_register.static.delete(:a)
end
assert_raises(RuntimeError) do
static_register.static.delete(:c)
end end
end end
def test_new_static_retains_static def test_new_static_retains_static
static_register = StaticRegisters.new(nil => true, 1 => :one, :one => "one", "two" => 3, false => nil) static_register = StaticRegisters.new(a: 1, b: 2)
static_register["one"] = 1 static_register[:b] = 22
static_register["two"] = 2 static_register[:c] = 33
static_register["three"] = 3
new_register = StaticRegisters.new(static_register) new_static_register = StaticRegisters.new(static_register)
assert_equal({}, new_register.registers) new_static_register[:b] = 222
new_register["one"] = 4 newest_static_register = StaticRegisters.new(new_static_register)
new_register["two"] = 5 newest_static_register[:c] = 333
new_register["three"] = 6
newest_register = StaticRegisters.new(new_register) assert_equal(1, static_register[:a])
assert_equal({}, newest_register.registers) assert_equal(22, static_register[:b])
assert_equal(33, static_register[:c])
newest_register["one"] = 7 assert_equal(1, new_static_register[:a])
newest_register["two"] = 8 assert_equal(222, new_static_register[:b])
newest_register["three"] = 9 assert_nil(new_static_register[:c])
assert_equal({ "one" => 1, "two" => 2, "three" => 3 }, static_register.registers) assert_equal(1, newest_static_register[:a])
assert_equal({ "one" => 4, "two" => 5, "three" => 6 }, new_register.registers) assert_equal(2, newest_static_register[:b])
assert_equal({ "one" => 7, "two" => 8, "three" => 9 }, newest_register.registers) assert_equal(333, newest_static_register[:c])
assert_equal({ nil => true, 1 => :one, :one => "one", "two" => 3, false => nil }, static_register.static)
assert_equal({ nil => true, 1 => :one, :one => "one", "two" => 3, false => nil }, new_register.static)
assert_equal({ nil => true, 1 => :one, :one => "one", "two" => 3, false => nil }, newest_register.static)
end end
def test_multiple_instances_are_unique def test_multiple_instances_are_unique
static_register = StaticRegisters.new(nil => true, 1 => :one, :one => "one", "two" => 3, false => nil) static_register_1 = StaticRegisters.new(a: 1, b: 2)
static_register["one"] = 1 static_register_1[:b] = 22
static_register["two"] = 2 static_register_1[:c] = 33
static_register["three"] = 3
new_register = StaticRegisters.new(foo: :bar) static_register_2 = StaticRegisters.new(a: 10, b: 20)
assert_equal({}, new_register.registers) static_register_2[:b] = 220
static_register_2[:c] = 330
new_register["one"] = 4 assert_equal({ a: 1, b: 2 }, static_register_1.static)
new_register["two"] = 5 assert_equal(1, static_register_1[:a])
new_register["three"] = 6 assert_equal(22, static_register_1[:b])
assert_equal(33, static_register_1[:c])
newest_register = StaticRegisters.new(bar: :foo) assert_equal({ a: 10, b: 20 }, static_register_2.static)
assert_equal({}, newest_register.registers) assert_equal(10, static_register_2[:a])
assert_equal(220, static_register_2[:b])
newest_register["one"] = 7 assert_equal(330, static_register_2[:c])
newest_register["two"] = 8
newest_register["three"] = 9
assert_equal({ "one" => 1, "two" => 2, "three" => 3 }, static_register.registers)
assert_equal({ "one" => 4, "two" => 5, "three" => 6 }, new_register.registers)
assert_equal({ "one" => 7, "two" => 8, "three" => 9 }, newest_register.registers)
assert_equal({ nil => true, 1 => :one, :one => "one", "two" => 3, false => nil }, static_register.static)
assert_equal({ foo: :bar }, new_register.static)
assert_equal({ bar: :foo }, newest_register.static)
end end
def test_can_update_static_directly_and_updates_all_instances def test_initialization_reused_static_same_memory_object
static_register = StaticRegisters.new(nil => true, 1 => :one, :one => "one", "two" => 3, false => nil) static_register_1 = StaticRegisters.new(a: 1, b: 2)
static_register["one"] = 1 static_register_1[:b] = 22
static_register["two"] = 2 static_register_1[:c] = 33
static_register["three"] = 3
new_register = StaticRegisters.new(static_register) static_register_2 = StaticRegisters.new(static_register_1)
assert_equal({}, new_register.registers)
assert_equal({ nil => true, 1 => :one, :one => "one", "two" => 3, false => nil }, static_register.static) assert_equal(1, static_register_2[:a])
assert_equal(2, static_register_2[:b])
assert_nil(static_register_2[:c])
new_register["one"] = 4 static_register_1.static[:b] = 222
new_register["two"] = 5 static_register_1.static[:c] = 333
new_register["three"] = 6
new_register.static["four"] = 10
newest_register = StaticRegisters.new(new_register) assert_same(static_register_1.static, static_register_2.static)
assert_equal({}, newest_register.registers)
assert_equal({ nil => true, 1 => :one, :one => "one", "two" => 3, false => nil, "four" => 10 }, new_register.static)
newest_register["one"] = 7
newest_register["two"] = 8
newest_register["three"] = 9
new_register.static["four"] = 5
new_register.static["five"] = 15
assert_equal({ "one" => 1, "two" => 2, "three" => 3 }, static_register.registers)
assert_equal({ "one" => 4, "two" => 5, "three" => 6 }, new_register.registers)
assert_equal({ "one" => 7, "two" => 8, "three" => 9 }, newest_register.registers)
assert_equal({ nil => true, 1 => :one, :one => "one", "two" => 3, false => nil, "four" => 5, "five" => 15 }, newest_register.static)
assert_equal({ nil => true, 1 => :one, :one => "one", "two" => 3, false => nil, "four" => 5, "five" => 15 }, static_register.static)
assert_equal({ nil => true, 1 => :one, :one => "one", "two" => 3, false => nil, "four" => 5, "five" => 15 }, new_register.static)
end end
end end

View File

@@ -0,0 +1,100 @@
# frozen_string_literal: true
require 'test_helper'
class StrainerFactoryUnitTest < Minitest::Test
include Liquid
module AccessScopeFilters
def public_filter
"public"
end
def private_filter
"private"
end
private :private_filter
end
StrainerFactory.add_global_filter(AccessScopeFilters)
module LateAddedFilter
def late_added_filter(_input)
"filtered"
end
end
def setup
@context = Context.build
end
def test_strainer
strainer = StrainerFactory.create(@context)
assert_equal(5, strainer.invoke('size', 'input'))
assert_equal("public", strainer.invoke("public_filter"))
end
def test_stainer_raises_argument_error
strainer = StrainerFactory.create(@context)
assert_raises(Liquid::ArgumentError) do
strainer.invoke("public_filter", 1)
end
end
def test_stainer_argument_error_contains_backtrace
strainer = StrainerFactory.create(@context)
exception = assert_raises(Liquid::ArgumentError) do
strainer.invoke("public_filter", 1)
end
assert_match(
/\ALiquid error: wrong number of arguments \((1 for 0|given 1, expected 0)\)\z/,
exception.message
)
assert_equal(exception.backtrace[0].split(':')[0], __FILE__)
end
def test_strainer_only_invokes_public_filter_methods
strainer = StrainerFactory.create(@context)
assert_equal(false, strainer.class.invokable?('__test__'))
assert_equal(false, strainer.class.invokable?('test'))
assert_equal(false, strainer.class.invokable?('instance_eval'))
assert_equal(false, strainer.class.invokable?('__send__'))
assert_equal(true, strainer.class.invokable?('size')) # from the standard lib
end
def test_strainer_returns_nil_if_no_filter_method_found
strainer = StrainerFactory.create(@context)
assert_nil(strainer.invoke("private_filter"))
assert_nil(strainer.invoke("undef_the_filter"))
end
def test_strainer_returns_first_argument_if_no_method_and_arguments_given
strainer = StrainerFactory.create(@context)
assert_equal("password", strainer.invoke("undef_the_method", "password"))
end
def test_strainer_only_allows_methods_defined_in_filters
strainer = StrainerFactory.create(@context)
assert_equal("1 + 1", strainer.invoke("instance_eval", "1 + 1"))
assert_equal("puts", strainer.invoke("__send__", "puts", "Hi Mom"))
assert_equal("has_method?", strainer.invoke("invoke", "has_method?", "invoke"))
end
def test_strainer_uses_a_class_cache_to_avoid_method_cache_invalidation
a = Module.new
b = Module.new
strainer = StrainerFactory.create(@context, [a, b])
assert_kind_of(StrainerTemplate, strainer)
assert_kind_of(a, strainer)
assert_kind_of(b, strainer)
assert_kind_of(Liquid::StandardFilters, strainer)
end
def test_add_global_filter_clears_cache
assert_equal('input', StrainerFactory.create(@context).invoke('late_added_filter', 'input'))
StrainerFactory.add_global_filter(LateAddedFilter)
assert_equal('filtered', StrainerFactory.create(nil).invoke('late_added_filter', 'input'))
end
end

View File

@@ -0,0 +1,82 @@
# frozen_string_literal: true
require 'test_helper'
class StrainerTemplateUnitTest < Minitest::Test
include Liquid
def test_add_filter_when_wrong_filter_class
c = Context.new
s = c.strainer
wrong_filter = ->(v) { v.reverse }
exception = assert_raises(TypeError) do
s.class.add_filter(wrong_filter)
end
assert_equal(exception.message, "wrong argument type Proc (expected Module)")
end
module PrivateMethodOverrideFilter
private
def public_filter
"overriden as private"
end
end
def test_add_filter_raises_when_module_privately_overrides_registered_public_methods
strainer = Context.new.strainer
error = assert_raises(Liquid::MethodOverrideError) do
strainer.class.add_filter(PrivateMethodOverrideFilter)
end
assert_equal('Liquid error: Filter overrides registered public methods as non public: public_filter', error.message)
end
module ProtectedMethodOverrideFilter
protected
def public_filter
"overriden as protected"
end
end
def test_add_filter_raises_when_module_overrides_registered_public_method_as_protected
strainer = Context.new.strainer
error = assert_raises(Liquid::MethodOverrideError) do
strainer.class.add_filter(ProtectedMethodOverrideFilter)
end
assert_equal('Liquid error: Filter overrides registered public methods as non public: public_filter', error.message)
end
module PublicMethodOverrideFilter
def public_filter
"public"
end
end
def test_add_filter_does_not_raise_when_module_overrides_previously_registered_method
strainer = Context.new.strainer
with_global_filter do
strainer.class.add_filter(PublicMethodOverrideFilter)
assert(strainer.class.send(:filter_methods).include?('public_filter'))
end
end
def test_add_filter_does_not_include_already_included_module
mod = Module.new do
class << self
attr_accessor :include_count
def included(_mod)
self.include_count += 1
end
end
self.include_count = 0
end
strainer = Context.new.strainer
strainer.class.add_filter(mod)
strainer.class.add_filter(mod)
assert_equal(1, mod.include_count)
end
end

View File

@@ -1,167 +0,0 @@
# frozen_string_literal: true
require 'test_helper'
class StrainerUnitTest < Minitest::Test
include Liquid
module AccessScopeFilters
def public_filter
"public"
end
def private_filter
"private"
end
private :private_filter
end
Strainer.global_filter(AccessScopeFilters)
def test_strainer
strainer = Strainer.create(nil)
assert_equal(5, strainer.invoke('size', 'input'))
assert_equal("public", strainer.invoke("public_filter"))
end
def test_stainer_raises_argument_error
strainer = Strainer.create(nil)
assert_raises(Liquid::ArgumentError) do
strainer.invoke("public_filter", 1)
end
end
def test_stainer_argument_error_contains_backtrace
strainer = Strainer.create(nil)
begin
strainer.invoke("public_filter", 1)
rescue Liquid::ArgumentError => e
assert_match(
/\ALiquid error: wrong number of arguments \((1 for 0|given 1, expected 0)\)\z/,
e.message
)
assert_equal(e.backtrace[0].split(':')[0], __FILE__)
end
end
def test_strainer_only_invokes_public_filter_methods
strainer = Strainer.create(nil)
assert_equal(false, strainer.class.invokable?('__test__'))
assert_equal(false, strainer.class.invokable?('test'))
assert_equal(false, strainer.class.invokable?('instance_eval'))
assert_equal(false, strainer.class.invokable?('__send__'))
assert_equal(true, strainer.class.invokable?('size')) # from the standard lib
end
def test_strainer_returns_nil_if_no_filter_method_found
strainer = Strainer.create(nil)
assert_nil(strainer.invoke("private_filter"))
assert_nil(strainer.invoke("undef_the_filter"))
end
def test_strainer_returns_first_argument_if_no_method_and_arguments_given
strainer = Strainer.create(nil)
assert_equal("password", strainer.invoke("undef_the_method", "password"))
end
def test_strainer_only_allows_methods_defined_in_filters
strainer = Strainer.create(nil)
assert_equal("1 + 1", strainer.invoke("instance_eval", "1 + 1"))
assert_equal("puts", strainer.invoke("__send__", "puts", "Hi Mom"))
assert_equal("has_method?", strainer.invoke("invoke", "has_method?", "invoke"))
end
def test_strainer_uses_a_class_cache_to_avoid_method_cache_invalidation
a = Module.new
b = Module.new
strainer = Strainer.create(nil, [a, b])
assert_kind_of(Strainer, strainer)
assert_kind_of(a, strainer)
assert_kind_of(b, strainer)
assert_kind_of(Liquid::StandardFilters, strainer)
end
def test_add_filter_when_wrong_filter_class
c = Context.new
s = c.strainer
wrong_filter = ->(v) { v.reverse }
assert_raises ArgumentError do
s.class.add_filter(wrong_filter)
end
end
module PrivateMethodOverrideFilter
private
def public_filter
"overriden as private"
end
end
def test_add_filter_raises_when_module_privately_overrides_registered_public_methods
strainer = Context.new.strainer
error = assert_raises(Liquid::MethodOverrideError) do
strainer.class.add_filter(PrivateMethodOverrideFilter)
end
assert_equal('Liquid error: Filter overrides registered public methods as non public: public_filter', error.message)
end
module ProtectedMethodOverrideFilter
protected
def public_filter
"overriden as protected"
end
end
def test_add_filter_raises_when_module_overrides_registered_public_method_as_protected
strainer = Context.new.strainer
error = assert_raises(Liquid::MethodOverrideError) do
strainer.class.add_filter(ProtectedMethodOverrideFilter)
end
assert_equal('Liquid error: Filter overrides registered public methods as non public: public_filter', error.message)
end
module PublicMethodOverrideFilter
def public_filter
"public"
end
end
def test_add_filter_does_not_raise_when_module_overrides_previously_registered_method
strainer = Context.new.strainer
strainer.class.add_filter(PublicMethodOverrideFilter)
assert(strainer.class.filter_methods.include?('public_filter'))
end
module LateAddedFilter
def late_added_filter(_input)
"filtered"
end
end
def test_global_filter_clears_cache
assert_equal('input', Strainer.create(nil).invoke('late_added_filter', 'input'))
Strainer.global_filter(LateAddedFilter)
assert_equal('filtered', Strainer.create(nil).invoke('late_added_filter', 'input'))
end
def test_add_filter_does_not_include_already_included_module
mod = Module.new do
class << self
attr_accessor :include_count
def included(_mod)
self.include_count += 1
end
end
self.include_count = 0
end
strainer = Context.new.strainer
strainer.class.add_filter(mod)
strainer.class.add_filter(mod)
assert_equal(1, mod.include_count)
end
end # StrainerTest

View File

@@ -20,42 +20,4 @@ class TagUnitTest < Minitest::Test
tag = Tag.parse("some_tag", "", Tokenizer.new(""), ParseContext.new) tag = Tag.parse("some_tag", "", Tokenizer.new(""), ParseContext.new)
assert_equal('some_tag', tag.tag_name) assert_equal('some_tag', tag.tag_name)
end 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 end

View File

@@ -0,0 +1,12 @@
# frozen_string_literal: true
require 'test_helper'
class TemplateFactoryUnitTest < Minitest::Test
include Liquid
def test_for_returns_liquid_template_instance
template = TemplateFactory.new.for("anything")
assert_instance_of(Liquid::Template, template)
end
end

View File

@@ -22,7 +22,7 @@ class TemplateUnitTest < Minitest::Test
def test_with_cache_classes_tags_returns_the_same_class def test_with_cache_classes_tags_returns_the_same_class
original_cache_setting = Liquid.cache_classes original_cache_setting = Liquid.cache_classes
Liquid.cache_classes = true Liquid.cache_classes = true
original_klass = Class.new original_klass = Class.new
Object.send(:const_set, :CustomTag, original_klass) Object.send(:const_set, :CustomTag, original_klass)
@@ -42,7 +42,7 @@ class TemplateUnitTest < Minitest::Test
def test_without_cache_classes_tags_reloads_the_class def test_without_cache_classes_tags_reloads_the_class
original_cache_setting = Liquid.cache_classes original_cache_setting = Liquid.cache_classes
Liquid.cache_classes = false Liquid.cache_classes = false
original_klass = Class.new original_klass = Class.new
Object.send(:const_set, :CustomTag, original_klass) Object.send(:const_set, :CustomTag, original_klass)
@@ -77,4 +77,11 @@ class TemplateUnitTest < Minitest::Test
ensure ensure
Template.tags.delete('fake') Template.tags.delete('fake')
end end
class TemplateSubclass < Liquid::Template
end
def test_template_inheritance
assert_equal("foo", TemplateSubclass.parse("foo").render)
end
end end

View File

@@ -34,7 +34,7 @@ class TokenizerTest < Minitest::Test
def tokenize(source) def tokenize(source)
tokenizer = Liquid::Tokenizer.new(source) tokenizer = Liquid::Tokenizer.new(source)
tokens = [] tokens = []
while (t = tokenizer.shift) while (t = tokenizer.shift)
tokens << t tokens << t
end end
@@ -42,7 +42,7 @@ class TokenizerTest < Minitest::Test
end end
def tokenize_line_numbers(source) def tokenize_line_numbers(source)
tokenizer = Liquid::Tokenizer.new(source, true) tokenizer = Liquid::Tokenizer.new(source, true)
line_numbers = [] line_numbers = []
loop do loop do
line_number = tokenizer.line_number line_number = tokenizer.line_number