mirror of
https://github.com/kemko/liquid.git
synced 2026-01-01 15:55:40 +03:00
Compare commits
103 Commits
prevent-sc
...
truncatewo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94df9a8f52 | ||
|
|
8bf0e7dfae | ||
|
|
077bf2a409 | ||
|
|
1a3e38c018 | ||
|
|
e495f75cc2 | ||
|
|
e781449c36 | ||
|
|
7eb03ea198 | ||
|
|
bd34cd5613 | ||
|
|
c28d455f7b | ||
|
|
d250a7f502 | ||
|
|
b0f46326ca | ||
|
|
7aed2f122c | ||
|
|
5199a34d9b | ||
|
|
4c2ab6f878 | ||
|
|
a818dd9d19 | ||
|
|
efef03d944 | ||
|
|
33760f083a | ||
|
|
013802c877 | ||
|
|
3dcad3b3cd | ||
|
|
db065315ba | ||
|
|
a03f02789b | ||
|
|
ca4b9b43af | ||
|
|
77084930e9 | ||
|
|
fb77921b15 | ||
|
|
0d02dea20b | ||
|
|
86b47ba28b | ||
|
|
95ff0595c6 | ||
|
|
bbc56f35ec | ||
|
|
dfbbf87ba9 | ||
|
|
037b603603 | ||
|
|
bd33df09de | ||
|
|
6ca5b62112 | ||
|
|
e1a2057a1b | ||
|
|
ae9dbe0ca7 | ||
|
|
3b486425b0 | ||
|
|
b08bcf00ac | ||
|
|
0740e8b431 | ||
|
|
5532df880f | ||
|
|
2b11efc3ae | ||
|
|
a1d982ca76 | ||
|
|
03be7f1ee3 | ||
|
|
1ced4eaf10 | ||
|
|
4970167726 | ||
|
|
065ccbc4aa | ||
|
|
1feaa63813 | ||
|
|
8541c6be35 | ||
|
|
18654526c8 | ||
|
|
bd1f7f9492 | ||
|
|
40d75dd283 | ||
|
|
f5011365f1 | ||
|
|
ebbd046c92 | ||
|
|
b9979088ec | ||
|
|
bd0e53bd2e | ||
|
|
4b586f4105 | ||
|
|
0410119d5f | ||
|
|
c2f67398d0 | ||
|
|
81149344a5 | ||
|
|
e9b649b345 | ||
|
|
9c538f4237 | ||
|
|
c08a358a2b | ||
|
|
dbaef5e79b | ||
|
|
48a155a213 | ||
|
|
c69a9a77c6 | ||
|
|
ef79fa3898 | ||
|
|
f7ad602bfc | ||
|
|
ffd6049ba2 | ||
|
|
b3ad54c0c2 | ||
|
|
67eca3f58d | ||
|
|
0847bf560f | ||
|
|
8074565c3e | ||
|
|
24e81267b9 | ||
|
|
c0ffee3ff9 | ||
|
|
c0ffeeef26 | ||
|
|
22dbf90b7d | ||
|
|
40c68c9c83 | ||
|
|
b7f0f158ab | ||
|
|
d8f31046a9 | ||
|
|
6c6382ed69 | ||
|
|
53ba1372f9 | ||
|
|
57c9cf64eb | ||
|
|
e83b1e4159 | ||
|
|
3784020a8d | ||
|
|
1223444738 | ||
|
|
2bfeed2b00 | ||
|
|
04b800d768 | ||
|
|
f1d62978ef | ||
|
|
ffadc64f28 | ||
|
|
5302f40342 | ||
|
|
b0f8c2c03e | ||
|
|
37e40673ff | ||
|
|
fefee4c675 | ||
|
|
1aa7d3d2ba | ||
|
|
0db9c56f34 | ||
|
|
f4d134cd5c | ||
|
|
b667bcb48b | ||
|
|
2c14e0b2ba | ||
|
|
ca207ed93f | ||
|
|
ef13343591 | ||
|
|
adb40c41b7 | ||
|
|
d8403af515 | ||
|
|
0d26f05bb8 | ||
|
|
1dcad34b06 | ||
|
|
c0ffee5919 |
40
.github/workflows/liquid.yml
vendored
Normal file
40
.github/workflows/liquid.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
name: Liquid
|
||||
on: [push]
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
entry:
|
||||
- { ruby: 2.5, allowed-failure: false }
|
||||
- { ruby: 2.6, allowed-failure: false }
|
||||
- { ruby: 2.7, allowed-failure: false }
|
||||
- { ruby: ruby-head, allowed-failure: true }
|
||||
name: test (${{ matrix.entry.ruby }})
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: ${{ matrix.entry.ruby }}
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: vendor/bundle
|
||||
key: ${{ runner.os }}-gems-${{ hashFiles('Gemfile') }}
|
||||
restore-keys: ${{ runner.os }}-gems-
|
||||
- run: bundle install --jobs=3 --retry=3 --path=vendor/bundle
|
||||
- run: bundle exec rake
|
||||
continue-on-error: ${{ matrix.entry.allowed-failure }}
|
||||
memory_profile:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 2.7
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: vendor/bundle
|
||||
key: ${{ runner.os }}-gems-${{ hashFiles('Gemfile') }}
|
||||
restore-keys: ${{ runner.os }}-gems-
|
||||
- run: bundle install --jobs=3 --retry=3 --path=vendor/bundle
|
||||
- run: bundle exec rake memory_profile:run
|
||||
@@ -1,3 +1,5 @@
|
||||
# Recommended rubocop version: ~> 0.78.0
|
||||
|
||||
AllCops:
|
||||
Exclude:
|
||||
- 'db/schema.rb'
|
||||
@@ -20,7 +22,7 @@ Style/Alias:
|
||||
- prefer_alias
|
||||
- prefer_alias_method
|
||||
|
||||
Layout/AlignHash:
|
||||
Layout/HashAlignment:
|
||||
EnforcedHashRocketStyle: key
|
||||
EnforcedColonStyle: key
|
||||
EnforcedLastArgumentHashStyle: ignore_implicit
|
||||
@@ -30,7 +32,7 @@ Layout/AlignHash:
|
||||
- ignore_implicit
|
||||
- ignore_explicit
|
||||
|
||||
Layout/AlignParameters:
|
||||
Layout/ParameterAlignment:
|
||||
EnforcedStyle: with_fixed_indentation
|
||||
SupportedStyles:
|
||||
- with_first_parameter
|
||||
@@ -172,7 +174,7 @@ Naming/FileName:
|
||||
Regex:
|
||||
IgnoreExecutableScripts: true
|
||||
|
||||
Layout/IndentFirstArgument:
|
||||
Layout/FirstArgumentIndentation:
|
||||
EnforcedStyle: consistent
|
||||
SupportedStyles:
|
||||
- consistent
|
||||
@@ -225,7 +227,7 @@ Layout/IndentationConsistency:
|
||||
Layout/IndentationWidth:
|
||||
Width: 2
|
||||
|
||||
Layout/IndentFirstArrayElement:
|
||||
Layout/FirstArrayElementIndentation:
|
||||
EnforcedStyle: consistent
|
||||
SupportedStyles:
|
||||
- special_inside_parentheses
|
||||
@@ -233,10 +235,10 @@ Layout/IndentFirstArrayElement:
|
||||
- align_brackets
|
||||
IndentationWidth:
|
||||
|
||||
Layout/IndentAssignment:
|
||||
Layout/AssignmentIndentation:
|
||||
IndentationWidth:
|
||||
|
||||
Layout/IndentFirstHashElement:
|
||||
Layout/FirstHashElementIndentation:
|
||||
EnforcedStyle: consistent
|
||||
SupportedStyles:
|
||||
- special_inside_parentheses
|
||||
@@ -340,9 +342,9 @@ Style/PercentQLiterals:
|
||||
Naming/PredicateName:
|
||||
NamePrefix:
|
||||
- is_
|
||||
NamePrefixBlacklist:
|
||||
ForbiddenPrefixes:
|
||||
- is_
|
||||
NameWhitelist:
|
||||
AllowedMethods:
|
||||
- is_a?
|
||||
Exclude:
|
||||
- 'spec/**/*'
|
||||
@@ -467,7 +469,7 @@ Style/TernaryParentheses:
|
||||
- require_no_parentheses
|
||||
AllowSafeAssignment: true
|
||||
|
||||
Layout/TrailingBlankLines:
|
||||
Layout/TrailingEmptyLines:
|
||||
EnforcedStyle: final_newline
|
||||
SupportedStyles:
|
||||
- final_newline
|
||||
@@ -478,7 +480,7 @@ Style/TrivialAccessors:
|
||||
AllowPredicates: true
|
||||
AllowDSLWriters: false
|
||||
IgnoreClassMethods: false
|
||||
Whitelist:
|
||||
AllowedMethods:
|
||||
- to_ary
|
||||
- to_a
|
||||
- to_c
|
||||
@@ -509,7 +511,7 @@ Style/WhileUntilModifier:
|
||||
Metrics/BlockNesting:
|
||||
Max: 3
|
||||
|
||||
Metrics/LineLength:
|
||||
Layout/LineLength:
|
||||
Max: 120
|
||||
AllowHeredoc: true
|
||||
AllowURI: true
|
||||
@@ -561,7 +563,7 @@ Lint/UnusedMethodArgument:
|
||||
Naming/AccessorMethodName:
|
||||
Enabled: true
|
||||
|
||||
Layout/AlignArray:
|
||||
Layout/ArrayAlignment:
|
||||
Enabled: true
|
||||
|
||||
Style/ArrayJoin:
|
||||
@@ -819,13 +821,13 @@ Layout/TrailingWhitespace:
|
||||
Style/UnlessElse:
|
||||
Enabled: true
|
||||
|
||||
Style/UnneededCapitalW:
|
||||
Style/RedundantCapitalW:
|
||||
Enabled: true
|
||||
|
||||
Style/UnneededInterpolation:
|
||||
Style/RedundantInterpolation:
|
||||
Enabled: true
|
||||
|
||||
Style/UnneededPercentQ:
|
||||
Style/RedundantPercentQ:
|
||||
Enabled: true
|
||||
|
||||
Style/VariableInterpolation:
|
||||
@@ -840,7 +842,7 @@ Style/WhileUntilDo:
|
||||
Style/ZeroLengthPredicate:
|
||||
Enabled: true
|
||||
|
||||
Layout/IndentHeredoc:
|
||||
Layout/HeredocIndentation:
|
||||
EnforcedStyle: squiggly
|
||||
|
||||
Lint/AmbiguousOperator:
|
||||
@@ -864,7 +866,7 @@ Lint/DeprecatedClassMethods:
|
||||
Lint/DuplicateMethods:
|
||||
Enabled: true
|
||||
|
||||
Lint/DuplicatedKey:
|
||||
Lint/DuplicateHashKey:
|
||||
Enabled: true
|
||||
|
||||
Lint/EachWithObjectArgument:
|
||||
@@ -891,8 +893,8 @@ Lint/FloatOutOfRange:
|
||||
Lint/FormatParameterMismatch:
|
||||
Enabled: true
|
||||
|
||||
Lint/HandleExceptions:
|
||||
Enabled: true
|
||||
Lint/SuppressedException:
|
||||
AllowComments: true
|
||||
|
||||
Lint/ImplicitStringConcatenation:
|
||||
Description: Checks for adjacent string literals on the same line, which could
|
||||
@@ -947,7 +949,7 @@ Lint/ShadowedException:
|
||||
Lint/ShadowingOuterLocalVariable:
|
||||
Enabled: true
|
||||
|
||||
Lint/StringConversionInInterpolation:
|
||||
Lint/RedundantStringCoercion:
|
||||
Enabled: true
|
||||
|
||||
Lint/UnderscorePrefixedVariableName:
|
||||
@@ -956,13 +958,13 @@ Lint/UnderscorePrefixedVariableName:
|
||||
Lint/UnifiedInteger:
|
||||
Enabled: true
|
||||
|
||||
Lint/UnneededCopDisableDirective:
|
||||
Lint/RedundantCopDisableDirective:
|
||||
Enabled: true
|
||||
|
||||
Lint/UnneededCopEnableDirective:
|
||||
Lint/RedundantCopEnableDirective:
|
||||
Enabled: true
|
||||
|
||||
Lint/UnneededSplatExpansion:
|
||||
Lint/RedundantSplatExpansion:
|
||||
Enabled: true
|
||||
|
||||
Lint/UnreachableCode:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
inherit_from:
|
||||
- https://shopify.github.io/ruby-style-guide/rubocop.yml
|
||||
- 'https://shopify.github.io/ruby-style-guide/rubocop.yml'
|
||||
- .rubocop_todo.yml
|
||||
|
||||
require: rubocop-performance
|
||||
@@ -8,9 +8,10 @@ Performance:
|
||||
Enabled: true
|
||||
|
||||
AllCops:
|
||||
TargetRubyVersion: 2.4
|
||||
Exclude:
|
||||
- 'vendor/bundle/**/*'
|
||||
|
||||
|
||||
Naming/MethodName:
|
||||
Exclude:
|
||||
- 'example/server/liquid_servlet.rb'
|
||||
- 'example/server/liquid_servlet.rb'
|
||||
|
||||
@@ -6,26 +6,6 @@
|
||||
# Note that changes in the inspected code, or installation of new
|
||||
# versions of RuboCop, may require this file to be generated again.
|
||||
|
||||
# Offense count: 2
|
||||
Lint/AmbiguousOperator:
|
||||
Exclude:
|
||||
- 'test/unit/condition_unit_test.rb'
|
||||
|
||||
# Offense count: 21
|
||||
# Configuration parameters: AllowSafeAssignment.
|
||||
Lint/AssignmentInCondition:
|
||||
Exclude:
|
||||
- 'lib/liquid/block_body.rb'
|
||||
- 'lib/liquid/lexer.rb'
|
||||
- 'lib/liquid/standardfilters.rb'
|
||||
- 'lib/liquid/tags/for.rb'
|
||||
- 'lib/liquid/tags/if.rb'
|
||||
- 'lib/liquid/tags/raw.rb'
|
||||
- 'lib/liquid/variable.rb'
|
||||
- 'performance/profile.rb'
|
||||
- 'test/test_helper.rb'
|
||||
- 'test/unit/tokenizer_unit_test.rb'
|
||||
|
||||
# Offense count: 2
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle.
|
||||
@@ -34,22 +14,11 @@ Lint/InheritException:
|
||||
Exclude:
|
||||
- 'lib/liquid/interrupts.rb'
|
||||
|
||||
# Offense count: 2
|
||||
Lint/UselessAssignment:
|
||||
Exclude:
|
||||
- 'performance/shopify/database.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Configuration parameters: CheckForMethodsWithNoSideEffects.
|
||||
Lint/Void:
|
||||
Exclude:
|
||||
- 'lib/liquid/parse_context.rb'
|
||||
|
||||
# Offense count: 98
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
|
||||
# URISchemes: http, https
|
||||
Metrics/LineLength:
|
||||
Layout/LineLength:
|
||||
Max: 294
|
||||
|
||||
# Offense count: 44
|
||||
@@ -76,26 +45,4 @@ Style/ClassVars:
|
||||
Exclude:
|
||||
- 'lib/liquid/condition.rb'
|
||||
- 'lib/liquid/strainer.rb'
|
||||
- 'lib/liquid/template.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Configuration parameters: AllowCoercion.
|
||||
Style/DateTime:
|
||||
Exclude:
|
||||
- 'test/unit/context_unit_test.rb'
|
||||
|
||||
# Offense count: 119
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: EnforcedStyle.
|
||||
# SupportedStyles: always, never
|
||||
Style/FrozenStringLiteralComment:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 9
|
||||
# Cop supports --auto-correct.
|
||||
# Configuration parameters: AllowAsExpressionSeparator.
|
||||
Style/Semicolon:
|
||||
Exclude:
|
||||
- 'test/integration/error_handling_test.rb'
|
||||
- 'test/integration/template_test.rb'
|
||||
- 'test/unit/context_unit_test.rb'
|
||||
- 'lib/liquid/template.rb'
|
||||
30
.travis.yml
30
.travis.yml
@@ -1,30 +0,0 @@
|
||||
language: ruby
|
||||
cache: bundler
|
||||
|
||||
rvm:
|
||||
- 2.4
|
||||
- 2.5
|
||||
- &latest_ruby 2.6
|
||||
- 2.7
|
||||
- ruby-head
|
||||
- jruby-head
|
||||
- truffleruby
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- rvm: *latest_ruby
|
||||
script: bundle exec rake memory_profile:run
|
||||
name: Profiling Memory Usage
|
||||
allow_failures:
|
||||
- rvm: ruby-head
|
||||
- rvm: jruby-head
|
||||
- rvm: truffleruby
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- gh-pages
|
||||
- /.*-stable/
|
||||
|
||||
notifications:
|
||||
disable: true
|
||||
4
Gemfile
4
Gemfile
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
source 'https://rubygems.org'
|
||||
git_source(:github) do |repo_name|
|
||||
"https://github.com/#{repo_name}.git"
|
||||
@@ -16,7 +18,7 @@ group :benchmark, :test do
|
||||
end
|
||||
|
||||
group :test do
|
||||
gem 'rubocop', '~> 0.74.0', require: false
|
||||
gem 'rubocop', '~> 0.78.0', require: false
|
||||
gem 'rubocop-performance', require: false
|
||||
|
||||
platform :mri, :truffleruby do
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# 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
|
||||
|
||||
### Fixed
|
||||
|
||||
20
Rakefile
20
Rakefile
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rake'
|
||||
require 'rake/testtask'
|
||||
$LOAD_PATH.unshift(File.expand_path("../lib", __FILE__))
|
||||
@@ -7,9 +9,15 @@ task(default: [:test, :rubocop])
|
||||
|
||||
desc('run test suite with default parser')
|
||||
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.verbose = false
|
||||
t.verbose = false
|
||||
end
|
||||
|
||||
Rake::TestTask.new(:integration_test) do |t|
|
||||
t.libs << 'lib' << 'test'
|
||||
t.test_files = FileList['test/integration/**/*_test.rb']
|
||||
t.verbose = false
|
||||
end
|
||||
|
||||
desc('run test suite with warn error mode')
|
||||
@@ -38,12 +46,12 @@ task :test do
|
||||
ENV['LIQUID_C'] = '1'
|
||||
|
||||
ENV['LIQUID_PARSER_MODE'] = 'lax'
|
||||
Rake::Task['base_test'].reenable
|
||||
Rake::Task['base_test'].invoke
|
||||
Rake::Task['integration_test'].reenable
|
||||
Rake::Task['integration_test'].invoke
|
||||
|
||||
ENV['LIQUID_PARSER_MODE'] = 'strict'
|
||||
Rake::Task['base_test'].reenable
|
||||
Rake::Task['base_test'].invoke
|
||||
Rake::Task['integration_test'].reenable
|
||||
Rake::Task['integration_test'].invoke
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ProductsFilter
|
||||
def price(integer)
|
||||
format("$%.2d USD", integer / 100.0)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet
|
||||
def do_GET(req, res)
|
||||
handle(:get, req, res)
|
||||
@@ -10,16 +12,16 @@ class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet
|
||||
private
|
||||
|
||||
def handle(_type, req, res)
|
||||
@request = req
|
||||
@request = req
|
||||
@response = res
|
||||
|
||||
@request.path_info =~ /(\w+)\z/
|
||||
@action = Regexp.last_match(1) || 'index'
|
||||
@action = Regexp.last_match(1) || 'index'
|
||||
@assigns = send(@action) if respond_to?(@action)
|
||||
|
||||
@response['Content-Type'] = "text/html"
|
||||
@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
|
||||
|
||||
def read_template(filename = @action)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'webrick'
|
||||
require 'rexml/document'
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Copyright (c) 2005 Tobias Luetke
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
@@ -21,10 +23,10 @@
|
||||
|
||||
module Liquid
|
||||
FilterSeparator = /\|/
|
||||
ArgumentSeparator = ','.freeze
|
||||
FilterArgumentSeparator = ':'.freeze
|
||||
VariableAttributeSeparator = '.'.freeze
|
||||
WhitespaceControl = '-'.freeze
|
||||
ArgumentSeparator = ','
|
||||
FilterArgumentSeparator = ':'
|
||||
VariableAttributeSeparator = '.'
|
||||
WhitespaceControl = '-'
|
||||
TagStart = /\{\%/
|
||||
TagEnd = /\%\}/
|
||||
VariableSignature = /\(?[\w\-\.\[\]]\)?/
|
||||
@@ -55,11 +57,14 @@ require 'liquid/forloop_drop'
|
||||
require 'liquid/extensions'
|
||||
require 'liquid/errors'
|
||||
require 'liquid/interrupts'
|
||||
require 'liquid/strainer'
|
||||
require 'liquid/strainer_factory'
|
||||
require 'liquid/strainer_template'
|
||||
require 'liquid/expression'
|
||||
require 'liquid/context'
|
||||
require 'liquid/parser_switching'
|
||||
require 'liquid/tag'
|
||||
require 'liquid/tag/disabler'
|
||||
require 'liquid/tag/disableable'
|
||||
require 'liquid/block'
|
||||
require 'liquid/block_body'
|
||||
require 'liquid/document'
|
||||
@@ -76,6 +81,9 @@ require 'liquid/tokenizer'
|
||||
require 'liquid/parse_context'
|
||||
require 'liquid/partial_cache'
|
||||
require 'liquid/usage'
|
||||
require 'liquid/register'
|
||||
require 'liquid/static_registers'
|
||||
require 'liquid/template_factory'
|
||||
|
||||
# Load all the tags of the standard library
|
||||
#
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Block < Tag
|
||||
MAX_DEPTH = 100
|
||||
@@ -8,7 +10,7 @@ module Liquid
|
||||
end
|
||||
|
||||
def parse(tokens)
|
||||
@body = BlockBody.new
|
||||
@body = new_body
|
||||
while parse_body(@body, tokens)
|
||||
end
|
||||
end
|
||||
@@ -26,20 +28,29 @@ module Liquid
|
||||
@body.nodelist
|
||||
end
|
||||
|
||||
def unknown_tag(tag, _params, _tokens)
|
||||
if tag == 'else'.freeze
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_else".freeze,
|
||||
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'
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_else",
|
||||
block_name: block_name)
|
||||
elsif tag.start_with?('end'.freeze)
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.invalid_delimiter".freeze,
|
||||
elsif tag.start_with?('end')
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.invalid_delimiter",
|
||||
tag: tag,
|
||||
block_name: block_name,
|
||||
block_delimiter: block_delimiter)
|
||||
else
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag".freeze, tag: tag)
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag)
|
||||
end
|
||||
end
|
||||
|
||||
def raise_tag_never_closed(block_name)
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_never_closed", block_name: block_name)
|
||||
end
|
||||
|
||||
def block_name
|
||||
@tag_name
|
||||
end
|
||||
@@ -48,11 +59,17 @@ module Liquid
|
||||
@block_delimiter ||= "end#{block_name}"
|
||||
end
|
||||
|
||||
protected
|
||||
private
|
||||
|
||||
# @api public
|
||||
def new_body
|
||||
parse_context.new_block_body
|
||||
end
|
||||
|
||||
# @api public
|
||||
def parse_body(body, tokens)
|
||||
if parse_context.depth >= MAX_DEPTH
|
||||
raise StackLevelError, "Nesting too deep".freeze
|
||||
raise StackLevelError, "Nesting too deep"
|
||||
end
|
||||
parse_context.depth += 1
|
||||
begin
|
||||
@@ -60,9 +77,7 @@ module Liquid
|
||||
@blank &&= body.blank?
|
||||
|
||||
return false if end_tag_name == block_delimiter
|
||||
unless end_tag_name
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name)
|
||||
end
|
||||
raise_tag_never_closed(block_name) unless end_tag_name
|
||||
|
||||
# this tag is not registered with the system
|
||||
# pass it to the current block for special handling or error reporting
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'English'
|
||||
|
||||
module Liquid
|
||||
class BlockBody
|
||||
LiquidTagToken = /\A\s*(\w+)\s*(.*?)\z/o
|
||||
FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(\w+)(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
|
||||
ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
|
||||
LiquidTagToken = /\A\s*(\w+)\s*(.*?)\z/o
|
||||
FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(\w+)(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
|
||||
ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
|
||||
WhitespaceOrNothing = /\A\s*\z/
|
||||
TAGSTART = "{%".freeze
|
||||
VARSTART = "{{".freeze
|
||||
TAGSTART = "{%"
|
||||
VARSTART = "{{"
|
||||
|
||||
attr_reader :nodelist
|
||||
|
||||
def initialize
|
||||
@nodelist = []
|
||||
@blank = true
|
||||
@blank = true
|
||||
end
|
||||
|
||||
def parse(tokenizer, parse_context, &block)
|
||||
@@ -25,7 +29,7 @@ module Liquid
|
||||
end
|
||||
|
||||
private def parse_for_liquid_tag(tokenizer, parse_context)
|
||||
while token = tokenizer.shift
|
||||
while (token = tokenizer.shift)
|
||||
unless token.empty? || token =~ WhitespaceOrNothing
|
||||
unless token =~ LiquidTagToken
|
||||
# line isn't empty but didn't match tag syntax, yield and let the
|
||||
@@ -33,8 +37,8 @@ module Liquid
|
||||
return yield token, token
|
||||
end
|
||||
tag_name = Regexp.last_match(1)
|
||||
markup = Regexp.last_match(2)
|
||||
unless tag = registered_tags[tag_name]
|
||||
markup = Regexp.last_match(2)
|
||||
unless (tag = registered_tags[tag_name])
|
||||
# end parsing if we reach an unknown tag and let the caller decide
|
||||
# determine how to proceed
|
||||
return yield tag_name, markup
|
||||
@@ -49,30 +53,77 @@ module Liquid
|
||||
yield nil, nil
|
||||
end
|
||||
|
||||
private def parse_for_document(tokenizer, parse_context, &block)
|
||||
while token = tokenizer.shift
|
||||
# @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)
|
||||
next if token.empty?
|
||||
case
|
||||
when token.start_with?(TAGSTART)
|
||||
whitespace_handler(token, parse_context)
|
||||
unless token =~ FullToken
|
||||
raise_missing_tag_terminator(token, parse_context)
|
||||
BlockBody.raise_missing_tag_terminator(token, parse_context)
|
||||
end
|
||||
tag_name = Regexp.last_match(2)
|
||||
markup = Regexp.last_match(4)
|
||||
markup = Regexp.last_match(4)
|
||||
|
||||
if parse_context.line_number
|
||||
# newlines inside the tag should increase the line number,
|
||||
# particularly important for multiline {% liquid %} tags
|
||||
parse_context.line_number += Regexp.last_match(1).count("\n".freeze) + Regexp.last_match(3).count("\n".freeze)
|
||||
parse_context.line_number += Regexp.last_match(1).count("\n") + Regexp.last_match(3).count("\n")
|
||||
end
|
||||
|
||||
if tag_name == 'liquid'.freeze
|
||||
liquid_tag_tokenizer = Tokenizer.new(markup, line_number: parse_context.line_number, for_liquid_tag: true)
|
||||
next parse_for_liquid_tag(liquid_tag_tokenizer, parse_context, &block)
|
||||
if tag_name == 'liquid'
|
||||
parse_liquid_tag(markup, parse_context)
|
||||
next
|
||||
end
|
||||
|
||||
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
|
||||
# determine how to proceed
|
||||
return yield tag_name, markup
|
||||
@@ -102,7 +153,11 @@ module Liquid
|
||||
if token[2] == WhitespaceControl
|
||||
previous_token = @nodelist.last
|
||||
if previous_token.is_a?(String)
|
||||
first_byte = previous_token.getbyte(0)
|
||||
previous_token.rstrip!
|
||||
if previous_token.empty? && parse_context[:bug_compatible_whitespace_trimming] && first_byte
|
||||
previous_token << first_byte
|
||||
end
|
||||
end
|
||||
end
|
||||
parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
|
||||
@@ -112,38 +167,47 @@ module Liquid
|
||||
@blank
|
||||
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)
|
||||
render_to_output_buffer(context, '')
|
||||
render_to_output_buffer(context, +'')
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
context.resource_limits.render_score += @nodelist.length
|
||||
context.resource_limits.increment_render_score(@nodelist.length)
|
||||
|
||||
idx = 0
|
||||
while node = @nodelist[idx]
|
||||
previous_output_size = output.bytesize
|
||||
|
||||
case node
|
||||
when String
|
||||
while (node = @nodelist[idx])
|
||||
if node.instance_of?(String)
|
||||
output << node
|
||||
when Variable
|
||||
else
|
||||
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
|
||||
# Interrupt is any command that stops block execution such as {% break %}
|
||||
# or {% continue %}
|
||||
context.push_interrupt(node.interrupt)
|
||||
break
|
||||
else # Other non-Block tags
|
||||
render_node(context, output, node)
|
||||
break if context.interrupt? # might have happened through an include
|
||||
# or {% continue %}. These tags may also occur through Block or Include tags.
|
||||
break if context.interrupt? # might have happened in a for-block
|
||||
end
|
||||
idx += 1
|
||||
|
||||
raise_if_resource_limits_reached(context, output.bytesize - previous_output_size)
|
||||
context.resource_limits.increment_write_score(output)
|
||||
end
|
||||
|
||||
output
|
||||
@@ -152,18 +216,7 @@ module Liquid
|
||||
private
|
||||
|
||||
def render_node(context, output, node)
|
||||
node.render_to_output_buffer(context, output)
|
||||
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
|
||||
context.handle_error(e, node.line_number)
|
||||
rescue ::StandardError => e
|
||||
line_number = node.is_a?(String) ? nil : node.line_number
|
||||
output << context.handle_error(e, line_number)
|
||||
end
|
||||
|
||||
def raise_if_resource_limits_reached(context, length)
|
||||
context.resource_limits.render_length += length
|
||||
return unless context.resource_limits.reached?
|
||||
raise MemoryError, "Memory limits exceeded".freeze
|
||||
BlockBody.render_node(context, output, node)
|
||||
end
|
||||
|
||||
def create_variable(token, parse_context)
|
||||
@@ -171,15 +224,17 @@ module Liquid
|
||||
markup = content.first
|
||||
return Variable.new(markup, parse_context)
|
||||
end
|
||||
raise_missing_variable_terminator(token, parse_context)
|
||||
BlockBody.raise_missing_variable_terminator(token, parse_context)
|
||||
end
|
||||
|
||||
# @deprecated Use {.raise_missing_tag_terminator} instead
|
||||
def raise_missing_tag_terminator(token, parse_context)
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_termination".freeze, token: token, tag_end: TagEnd.inspect)
|
||||
BlockBody.raise_missing_tag_terminator(token, parse_context)
|
||||
end
|
||||
|
||||
# @deprecated Use {.raise_missing_variable_terminator} instead
|
||||
def raise_missing_variable_terminator(token, parse_context)
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.variable_termination".freeze, token: token, tag_end: VariableEnd.inspect)
|
||||
BlockBody.raise_missing_variable_terminator(token, parse_context)
|
||||
end
|
||||
|
||||
def registered_tags
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# Container for liquid nodes which conveniently wraps decision making logic
|
||||
#
|
||||
@@ -8,14 +10,14 @@ module Liquid
|
||||
#
|
||||
class Condition #:nodoc:
|
||||
@@operators = {
|
||||
'=='.freeze => ->(cond, left, right) { cond.send(:equal_variables, left, right) },
|
||||
'!='.freeze => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
|
||||
'<>'.freeze => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
|
||||
'<'.freeze => :<,
|
||||
'>'.freeze => :>,
|
||||
'>='.freeze => :>=,
|
||||
'<='.freeze => :<=,
|
||||
'contains'.freeze => lambda do |_cond, left, right|
|
||||
'==' => ->(cond, left, right) { cond.send(:equal_variables, left, right) },
|
||||
'!=' => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
|
||||
'<>' => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
|
||||
'<' => :<,
|
||||
'>' => :>,
|
||||
'>=' => :>=,
|
||||
'<=' => :<=,
|
||||
'contains' => lambda do |_cond, left, right|
|
||||
if left && right && left.respond_to?(:include?)
|
||||
right = right.to_s if left.is_a?(String)
|
||||
left.include?(right)
|
||||
@@ -25,18 +27,37 @@ module Liquid
|
||||
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
|
||||
@@operators
|
||||
end
|
||||
|
||||
def self.parse_expression(markup)
|
||||
@@method_literals[markup] || Expression.parse(markup)
|
||||
end
|
||||
|
||||
attr_reader :attachment, :child_condition
|
||||
attr_accessor :left, :operator, :right
|
||||
|
||||
def initialize(left = nil, operator = nil, right = nil)
|
||||
@left = left
|
||||
@left = left
|
||||
@operator = operator
|
||||
@right = right
|
||||
@child_relation = nil
|
||||
@right = right
|
||||
|
||||
@child_relation = nil
|
||||
@child_condition = nil
|
||||
end
|
||||
|
||||
@@ -60,12 +81,12 @@ module Liquid
|
||||
end
|
||||
|
||||
def or(condition)
|
||||
@child_relation = :or
|
||||
@child_relation = :or
|
||||
@child_condition = condition
|
||||
end
|
||||
|
||||
def and(condition)
|
||||
@child_relation = :and
|
||||
@child_relation = :and
|
||||
@child_condition = condition
|
||||
end
|
||||
|
||||
@@ -78,7 +99,7 @@ module Liquid
|
||||
end
|
||||
|
||||
def inspect
|
||||
"#<Condition #{[@left, @operator, @right].compact.join(' '.freeze)}>"
|
||||
"#<Condition #{[@left, @operator, @right].compact.join(' ')}>"
|
||||
end
|
||||
|
||||
protected
|
||||
@@ -88,7 +109,7 @@ module Liquid
|
||||
private
|
||||
|
||||
def equal_variables(left, right)
|
||||
if left.is_a?(Liquid::Expression::MethodLiteral)
|
||||
if left.is_a?(MethodLiteral)
|
||||
if right.respond_to?(left.method_name)
|
||||
return right.send(left.method_name)
|
||||
else
|
||||
@@ -96,7 +117,7 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
if right.is_a?(Liquid::Expression::MethodLiteral)
|
||||
if right.is_a?(MethodLiteral)
|
||||
if left.respond_to?(right.method_name)
|
||||
return left.send(right.method_name)
|
||||
else
|
||||
@@ -113,7 +134,7 @@ module Liquid
|
||||
# return this as the result.
|
||||
return context.evaluate(left) if op.nil?
|
||||
|
||||
left = context.evaluate(left)
|
||||
left = context.evaluate(left)
|
||||
right = context.evaluate(right)
|
||||
|
||||
operation = self.class.operators[op] || raise(Liquid::ArgumentError, "Unknown operator #{op}")
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# Context keeps the variable stack and resolves variables, as well as keywords
|
||||
#
|
||||
@@ -16,33 +18,34 @@ module Liquid
|
||||
attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters
|
||||
|
||||
# rubocop:disable Metrics/ParameterLists
|
||||
def self.build(environments: {}, outer_scope: {}, registers: {}, rethrow_errors: false, resource_limits: nil, static_registers: {}, static_environments: {})
|
||||
new(environments, outer_scope, registers, rethrow_errors, resource_limits, static_registers, static_environments)
|
||||
def self.build(environments: {}, outer_scope: {}, registers: {}, rethrow_errors: false, resource_limits: nil, static_environments: {})
|
||||
new(environments, outer_scope, registers, rethrow_errors, resource_limits, static_environments)
|
||||
end
|
||||
|
||||
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil, static_registers = {}, static_environments = {})
|
||||
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil, static_environments = {})
|
||||
@environments = [environments]
|
||||
@environments.flatten!
|
||||
|
||||
@static_environments = [static_environments].flat_map(&:freeze).freeze
|
||||
@scopes = [(outer_scope || {})]
|
||||
@registers = registers
|
||||
@static_registers = static_registers.freeze
|
||||
@errors = []
|
||||
@partial = false
|
||||
@strict_variables = false
|
||||
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
|
||||
@base_scope_depth = 0
|
||||
squash_instance_assigns_with_environments
|
||||
@interrupts = []
|
||||
@filters = []
|
||||
@global_filter = nil
|
||||
@disabled_tags = {}
|
||||
|
||||
self.exception_renderer = Template.default_exception_renderer
|
||||
if rethrow_errors
|
||||
self.exception_renderer = ->(_e) { raise }
|
||||
end
|
||||
|
||||
@interrupts = []
|
||||
@filters = []
|
||||
@global_filter = nil
|
||||
# Do this last, since it could result in this object being passed to a Proc in the environment
|
||||
squash_instance_assigns_with_environments
|
||||
end
|
||||
# rubocop:enable Metrics/ParameterLists
|
||||
|
||||
@@ -51,7 +54,7 @@ module Liquid
|
||||
end
|
||||
|
||||
def strainer
|
||||
@strainer ||= Strainer.create(self, @filters)
|
||||
@strainer ||= StrainerFactory.create(self, @filters)
|
||||
end
|
||||
|
||||
# Adds filters to this context.
|
||||
@@ -86,7 +89,7 @@ module Liquid
|
||||
def handle_error(e, line_number = nil)
|
||||
e = internal_error unless e.is_a?(Liquid::Error)
|
||||
e.template_name ||= template_name
|
||||
e.line_number ||= line_number
|
||||
e.line_number ||= line_number
|
||||
errors.push(e)
|
||||
exception_renderer.call(e).to_s
|
||||
end
|
||||
@@ -135,14 +138,15 @@ module Liquid
|
||||
Context.build(
|
||||
resource_limits: resource_limits,
|
||||
static_environments: static_environments,
|
||||
static_registers: static_registers
|
||||
registers: StaticRegisters.new(registers)
|
||||
).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.filters = @filters
|
||||
subcontext.filters = @filters
|
||||
subcontext.strainer = nil
|
||||
subcontext.errors = errors
|
||||
subcontext.errors = errors
|
||||
subcontext.warnings = warnings
|
||||
subcontext.disabled_tags = @disabled_tags
|
||||
end
|
||||
end
|
||||
|
||||
@@ -187,7 +191,7 @@ module Liquid
|
||||
try_variable_find_in_environments(key, raise_on_not_found: raise_on_not_found)
|
||||
end
|
||||
|
||||
variable = variable.to_liquid
|
||||
variable = variable.to_liquid
|
||||
variable.context = self if variable.respond_to?(:context=)
|
||||
|
||||
variable
|
||||
@@ -207,9 +211,24 @@ module Liquid
|
||||
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
|
||||
|
||||
attr_writer :base_scope_depth, :warnings, :errors, :strainer, :filters
|
||||
attr_writer :base_scope_depth, :warnings, :errors, :strainer, :filters, :disabled_tags
|
||||
|
||||
private
|
||||
|
||||
@@ -232,7 +251,7 @@ module Liquid
|
||||
end
|
||||
|
||||
def check_overflow
|
||||
raise StackLevelError, "Nesting too deep".freeze if overflow?
|
||||
raise StackLevelError, "Nesting too deep" if overflow?
|
||||
end
|
||||
|
||||
def overflow?
|
||||
|
||||
@@ -1,26 +1,63 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Document < BlockBody
|
||||
class Document
|
||||
def self.parse(tokens, parse_context)
|
||||
doc = new
|
||||
doc = new(parse_context)
|
||||
doc.parse(tokens, parse_context)
|
||||
doc
|
||||
end
|
||||
|
||||
def parse(tokens, parse_context)
|
||||
super do |end_tag_name, _end_tag_params|
|
||||
unknown_tag(end_tag_name, parse_context) if end_tag_name
|
||||
attr_reader :parse_context, :body
|
||||
|
||||
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
|
||||
rescue SyntaxError => e
|
||||
e.line_number ||= parse_context.line_number
|
||||
raise
|
||||
end
|
||||
|
||||
def unknown_tag(tag, parse_context)
|
||||
def unknown_tag(tag, _markup, _tokenizer)
|
||||
case tag
|
||||
when 'else'.freeze, 'end'.freeze
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_outer_tag".freeze, tag: tag)
|
||||
when 'else', 'end'
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_outer_tag", tag: tag)
|
||||
else
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag".freeze, tag: tag)
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag)
|
||||
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
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'set'
|
||||
|
||||
module Liquid
|
||||
@@ -67,7 +69,7 @@ module Liquid
|
||||
|
||||
if include?(Enumerable)
|
||||
blacklist += Enumerable.public_instance_methods
|
||||
blacklist -= [:sort, :count, :first, :min, :max, :include?]
|
||||
blacklist -= [:sort, :count, :first, :min, :max]
|
||||
end
|
||||
|
||||
whitelist = [:to_liquid] + (public_instance_methods - blacklist)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Error < ::StandardError
|
||||
attr_accessor :line_number
|
||||
@@ -5,7 +7,7 @@ module Liquid
|
||||
attr_accessor :markup_context
|
||||
|
||||
def to_s(with_prefix = true)
|
||||
str = ""
|
||||
str = +""
|
||||
str << message_prefix if with_prefix
|
||||
str << super()
|
||||
|
||||
@@ -20,7 +22,7 @@ module Liquid
|
||||
private
|
||||
|
||||
def message_prefix
|
||||
str = ""
|
||||
str = +""
|
||||
str << if is_a?(SyntaxError)
|
||||
"Liquid syntax error"
|
||||
else
|
||||
@@ -38,19 +40,19 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
ArgumentError = Class.new(Error)
|
||||
ContextError = Class.new(Error)
|
||||
FileSystemError = Class.new(Error)
|
||||
StandardError = Class.new(Error)
|
||||
SyntaxError = Class.new(Error)
|
||||
StackLevelError = Class.new(Error)
|
||||
TaintedError = Class.new(Error)
|
||||
MemoryError = Class.new(Error)
|
||||
ZeroDivisionError = Class.new(Error)
|
||||
FloatDomainError = Class.new(Error)
|
||||
UndefinedVariable = Class.new(Error)
|
||||
ArgumentError = Class.new(Error)
|
||||
ContextError = Class.new(Error)
|
||||
FileSystemError = Class.new(Error)
|
||||
StandardError = Class.new(Error)
|
||||
SyntaxError = Class.new(Error)
|
||||
StackLevelError = Class.new(Error)
|
||||
MemoryError = Class.new(Error)
|
||||
ZeroDivisionError = Class.new(Error)
|
||||
FloatDomainError = Class.new(Error)
|
||||
UndefinedVariable = Class.new(Error)
|
||||
UndefinedDropMethod = Class.new(Error)
|
||||
UndefinedFilter = Class.new(Error)
|
||||
UndefinedFilter = Class.new(Error)
|
||||
MethodOverrideError = Class.new(Error)
|
||||
InternalError = Class.new(Error)
|
||||
DisabledError = Class.new(Error)
|
||||
InternalError = Class.new(Error)
|
||||
end
|
||||
|
||||
@@ -1,24 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
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 = {
|
||||
nil => nil, 'nil'.freeze => nil, 'null'.freeze => nil, ''.freeze => nil,
|
||||
'true'.freeze => true,
|
||||
'false'.freeze => false,
|
||||
'blank'.freeze => MethodLiteral.new(:blank?, '').freeze,
|
||||
'empty'.freeze => MethodLiteral.new(:empty?, '').freeze
|
||||
nil => nil, 'nil' => nil, 'null' => nil, '' => nil,
|
||||
'true' => true,
|
||||
'false' => false,
|
||||
'blank' => '',
|
||||
'empty' => ''
|
||||
}.freeze
|
||||
|
||||
SINGLE_QUOTED_STRING = /\A'(.*)'\z/m
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'time'
|
||||
require 'date'
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# A Liquid file system is a way to let your templates retrieve other templates for use with the include tag.
|
||||
#
|
||||
@@ -44,8 +46,8 @@ module Liquid
|
||||
class LocalFileSystem
|
||||
attr_accessor :root
|
||||
|
||||
def initialize(root, pattern = "_%s.liquid".freeze)
|
||||
@root = root
|
||||
def initialize(root, pattern = "_%s.liquid")
|
||||
@root = root
|
||||
@pattern = pattern
|
||||
end
|
||||
|
||||
@@ -57,9 +59,9 @@ module Liquid
|
||||
end
|
||||
|
||||
def full_path(template_path)
|
||||
raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ %r{\A[^./][a-zA-Z0-9_/]+\z}
|
||||
raise FileSystemError, "Illegal template name '#{template_path}'" unless %r{\A[^./][a-zA-Z0-9_/]+\z}.match?(template_path)
|
||||
|
||||
full_path = if template_path.include?('/'.freeze)
|
||||
full_path = if template_path.include?('/')
|
||||
File.join(root, File.dirname(template_path), @pattern % File.basename(template_path))
|
||||
else
|
||||
File.join(root, @pattern % template_path)
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class ForloopDrop < Drop
|
||||
def initialize(name, length, parentloop)
|
||||
@name = name
|
||||
@length = length
|
||||
@name = name
|
||||
@length = length
|
||||
@parentloop = parentloop
|
||||
@index = 0
|
||||
@index = 0
|
||||
end
|
||||
|
||||
attr_reader :name, :length, :parentloop
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'yaml'
|
||||
|
||||
module Liquid
|
||||
@@ -31,7 +33,7 @@ module Liquid
|
||||
end
|
||||
|
||||
def deep_fetch_translation(name)
|
||||
name.split('.'.freeze).reduce(locale) do |level, cur|
|
||||
name.split('.').reduce(locale) do |level, cur|
|
||||
level[cur] || raise(TranslationError, "Translation for #{name} does not exist in locale #{path}")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# An interrupt is any command that breaks processing of a block (ex: a for loop).
|
||||
class Interrupt
|
||||
attr_reader :message
|
||||
|
||||
def initialize(message = nil)
|
||||
@message = message || "interrupt".freeze
|
||||
@message = message || "interrupt"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "strscan"
|
||||
module Liquid
|
||||
class Lexer
|
||||
SPECIALS = {
|
||||
'|'.freeze => :pipe,
|
||||
'.'.freeze => :dot,
|
||||
':'.freeze => :colon,
|
||||
','.freeze => :comma,
|
||||
'['.freeze => :open_square,
|
||||
']'.freeze => :close_square,
|
||||
'('.freeze => :open_round,
|
||||
')'.freeze => :close_round,
|
||||
'?'.freeze => :question,
|
||||
'-'.freeze => :dash,
|
||||
'|' => :pipe,
|
||||
'.' => :dot,
|
||||
':' => :colon,
|
||||
',' => :comma,
|
||||
'[' => :open_square,
|
||||
']' => :close_square,
|
||||
'(' => :open_round,
|
||||
')' => :close_round,
|
||||
'?' => :question,
|
||||
'-' => :dash,
|
||||
}.freeze
|
||||
IDENTIFIER = /[a-zA-Z_][\w-]*\??/
|
||||
IDENTIFIER = /[a-zA-Z_][\w-]*\??/
|
||||
SINGLE_STRING_LITERAL = /'[^\']*'/
|
||||
DOUBLE_STRING_LITERAL = /"[^\"]*"/
|
||||
NUMBER_LITERAL = /-?\d+(\.\d+)?/
|
||||
DOTDOT = /\.\./
|
||||
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/
|
||||
NUMBER_LITERAL = /-?\d+(\.\d+)?/
|
||||
DOTDOT = /\.\./
|
||||
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/
|
||||
WHITESPACE_OR_NOTHING = /\s*/
|
||||
|
||||
def initialize(input)
|
||||
@@ -31,15 +33,21 @@ module Liquid
|
||||
until @ss.eos?
|
||||
@ss.skip(WHITESPACE_OR_NOTHING)
|
||||
break if @ss.eos?
|
||||
tok = if t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t]
|
||||
elsif t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t]
|
||||
elsif t = @ss.scan(DOUBLE_STRING_LITERAL) then [:string, t]
|
||||
elsif t = @ss.scan(NUMBER_LITERAL) then [:number, t]
|
||||
elsif t = @ss.scan(IDENTIFIER) then [:id, t]
|
||||
elsif t = @ss.scan(DOTDOT) then [:dotdot, t]
|
||||
tok = if (t = @ss.scan(COMPARISON_OPERATOR))
|
||||
[:comparison, t]
|
||||
elsif (t = @ss.scan(SINGLE_STRING_LITERAL))
|
||||
[:string, t]
|
||||
elsif (t = @ss.scan(DOUBLE_STRING_LITERAL))
|
||||
[:string, t]
|
||||
elsif (t = @ss.scan(NUMBER_LITERAL))
|
||||
[:number, t]
|
||||
elsif (t = @ss.scan(IDENTIFIER))
|
||||
[:id, t]
|
||||
elsif (t = @ss.scan(DOTDOT))
|
||||
[:dotdot, t]
|
||||
else
|
||||
c = @ss.getch
|
||||
if s = SPECIALS[c]
|
||||
c = @ss.getch
|
||||
if (s = SPECIALS[c])
|
||||
[s, c]
|
||||
else
|
||||
raise SyntaxError, "Unexpected character #{c}"
|
||||
|
||||
@@ -25,3 +25,5 @@
|
||||
render: "Syntax error in tag 'render' - Template name must be a quoted string"
|
||||
argument:
|
||||
include: "Argument error in tag 'include' - Illegal template name"
|
||||
disabled:
|
||||
tag: "usage is not allowed in this context"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class ParseContext
|
||||
attr_accessor :locale, :line_number, :trim_whitespace, :depth
|
||||
@@ -5,9 +7,11 @@ module Liquid
|
||||
|
||||
def initialize(options = {})
|
||||
@template_options = options ? options.dup : {}
|
||||
@locale = @template_options[:locale] ||= I18n.new
|
||||
|
||||
@locale = @template_options[:locale] ||= I18n.new
|
||||
@warnings = []
|
||||
self.depth = 0
|
||||
|
||||
self.depth = 0
|
||||
self.partial = false
|
||||
end
|
||||
|
||||
@@ -15,11 +19,15 @@ module Liquid
|
||||
@options[option_key]
|
||||
end
|
||||
|
||||
def new_block_body
|
||||
Liquid::BlockBody.new
|
||||
end
|
||||
|
||||
def partial=(value)
|
||||
@partial = value
|
||||
@options = value ? partial_options : @template_options
|
||||
|
||||
@error_mode = @options[:error_mode] || Template.error_mode
|
||||
value
|
||||
end
|
||||
|
||||
def partial_options
|
||||
|
||||
@@ -11,7 +11,7 @@ module Liquid
|
||||
end
|
||||
|
||||
def initialize(node, callbacks)
|
||||
@node = node
|
||||
@node = node
|
||||
@callbacks = callbacks
|
||||
end
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Parser
|
||||
def initialize(input)
|
||||
l = Lexer.new(input)
|
||||
l = Lexer.new(input)
|
||||
@tokens = l.tokenize
|
||||
@p = 0 # pointer to current location
|
||||
@p = 0 # pointer to current location
|
||||
end
|
||||
|
||||
def jump(point)
|
||||
@@ -66,10 +68,10 @@ module Liquid
|
||||
end
|
||||
|
||||
def argument
|
||||
str = ""
|
||||
str = +""
|
||||
# might be a keyword argument (identifier: expression)
|
||||
if look(:id) && look(:colon, 1)
|
||||
str << consume << consume << ' '.freeze
|
||||
str << consume << consume << ' '
|
||||
end
|
||||
|
||||
str << expression
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
module ParserSwitching
|
||||
def parse_with_selected_parser(markup)
|
||||
@@ -6,10 +8,10 @@ module Liquid
|
||||
when :lax then lax_parse(markup)
|
||||
when :warn
|
||||
begin
|
||||
return strict_parse_with_error_context(markup)
|
||||
strict_parse_with_error_context(markup)
|
||||
rescue SyntaxError => e
|
||||
parse_context.warnings << e
|
||||
return lax_parse(markup)
|
||||
lax_parse(markup)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -19,7 +21,7 @@ module Liquid
|
||||
def strict_parse_with_error_context(markup)
|
||||
strict_parse(markup)
|
||||
rescue SyntaxError => e
|
||||
e.line_number = line_number
|
||||
e.line_number = line_number
|
||||
e.markup_context = markup_context(markup)
|
||||
raise e
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class PartialCache
|
||||
def self.load(template_name, context:, parse_context:)
|
||||
@@ -6,10 +8,14 @@ module Liquid
|
||||
return cached if cached
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
ensure
|
||||
parse_context.partial = false
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'liquid/profiler/hooks'
|
||||
|
||||
module Liquid
|
||||
@@ -44,7 +46,7 @@ module Liquid
|
||||
include Enumerable
|
||||
|
||||
class Timing
|
||||
attr_reader :code, :partial, :line_number, :children
|
||||
attr_reader :code, :partial, :line_number, :children, :total_time, :self_time
|
||||
|
||||
def initialize(node, partial)
|
||||
@code = node.respond_to?(:raw) ? node.raw : node
|
||||
@@ -62,7 +64,18 @@ module Liquid
|
||||
end
|
||||
|
||||
def finish
|
||||
@end_time = Time.now
|
||||
@end_time = Time.now
|
||||
@total_time = @end_time - @start_time
|
||||
|
||||
if @children.empty?
|
||||
@self_time = @total_time
|
||||
else
|
||||
total_children_time = 0
|
||||
@children.each do |child|
|
||||
total_children_time += child.total_time
|
||||
end
|
||||
@self_time = @total_time - total_children_time
|
||||
end
|
||||
end
|
||||
|
||||
def render_time
|
||||
@@ -96,14 +109,14 @@ module Liquid
|
||||
Thread.current[:liquid_profiler]
|
||||
end
|
||||
|
||||
def initialize
|
||||
@partial_stack = ["<root>"]
|
||||
def initialize(partial_name = "<root>")
|
||||
@partial_stack = [partial_name]
|
||||
|
||||
@root_timing = Timing.new("", current_partial)
|
||||
@root_timing = Timing.new("", current_partial)
|
||||
@timing_stack = [@root_timing]
|
||||
|
||||
@render_start_at = Time.now
|
||||
@render_end_at = @render_start_at
|
||||
@render_end_at = @render_start_at
|
||||
end
|
||||
|
||||
def start
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class BlockBody
|
||||
def render_node_with_profiling(context, output, node)
|
||||
module BlockBodyProfilingHook
|
||||
def render_node(context, output, node)
|
||||
Profiler.profile_node_render(node) do
|
||||
render_node_without_profiling(context, output, node)
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
alias_method :render_node_without_profiling, :render_node
|
||||
alias_method :render_node, :render_node_with_profiling
|
||||
end
|
||||
BlockBody.prepend(BlockBodyProfilingHook)
|
||||
|
||||
class Include < Tag
|
||||
def render_to_output_buffer_with_profiling(context, output)
|
||||
module IncludeProfilingHook
|
||||
def render_to_output_buffer(context, output)
|
||||
Profiler.profile_children(context.evaluate(@template_name_expr).to_s) do
|
||||
render_to_output_buffer_without_profiling(context, output)
|
||||
super
|
||||
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
|
||||
Include.prepend(IncludeProfilingHook)
|
||||
end
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class RangeLookup
|
||||
def self.parse(start_markup, end_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)
|
||||
new(start_obj, end_obj)
|
||||
else
|
||||
@@ -12,12 +14,12 @@ module Liquid
|
||||
|
||||
def initialize(start_obj, end_obj)
|
||||
@start_obj = start_obj
|
||||
@end_obj = end_obj
|
||||
@end_obj = end_obj
|
||||
end
|
||||
|
||||
def evaluate(context)
|
||||
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
|
||||
end
|
||||
|
||||
|
||||
6
lib/liquid/register.rb
Normal file
6
lib/liquid/register.rb
Normal file
@@ -0,0 +1,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Register
|
||||
end
|
||||
end
|
||||
@@ -1,23 +1,62 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class ResourceLimits
|
||||
attr_accessor :render_length, :render_score, :assign_score,
|
||||
:render_length_limit, :render_score_limit, :assign_score_limit
|
||||
attr_accessor :render_length_limit, :render_score_limit, :assign_score_limit
|
||||
attr_reader :render_score, :assign_score
|
||||
|
||||
def initialize(limits)
|
||||
@render_length_limit = limits[:render_length_limit]
|
||||
@render_score_limit = limits[:render_score_limit]
|
||||
@assign_score_limit = limits[:assign_score_limit]
|
||||
@render_score_limit = limits[:render_score_limit]
|
||||
@assign_score_limit = limits[:assign_score_limit]
|
||||
reset
|
||||
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?
|
||||
(@render_length_limit && @render_length > @render_length_limit) ||
|
||||
(@render_score_limit && @render_score > @render_score_limit) ||
|
||||
(@assign_score_limit && @assign_score > @assign_score_limit)
|
||||
@reached_limit
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'cgi'
|
||||
require 'bigdecimal'
|
||||
|
||||
module Liquid
|
||||
module StandardFilters
|
||||
HTML_ESCAPE = {
|
||||
'&'.freeze => '&'.freeze,
|
||||
'>'.freeze => '>'.freeze,
|
||||
'<'.freeze => '<'.freeze,
|
||||
'"'.freeze => '"'.freeze,
|
||||
"'".freeze => '''.freeze,
|
||||
'&' => '&',
|
||||
'>' => '>',
|
||||
'<' => '<',
|
||||
'"' => '"',
|
||||
"'" => ''',
|
||||
}.freeze
|
||||
HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/
|
||||
STRIP_HTML_BLOCKS = Regexp.union(
|
||||
STRIP_HTML_BLOCKS = Regexp.union(
|
||||
%r{<script.*?</script>}m,
|
||||
/<!--.*?-->/m,
|
||||
%r{<style.*?</style>}m
|
||||
@@ -39,7 +41,7 @@ module Liquid
|
||||
end
|
||||
|
||||
def escape(input)
|
||||
CGI.escapeHTML(input.to_s).untaint unless input.nil?
|
||||
CGI.escapeHTML(input.to_s) unless input.nil?
|
||||
end
|
||||
alias_method :h, :escape
|
||||
|
||||
@@ -72,23 +74,36 @@ module Liquid
|
||||
end
|
||||
|
||||
# Truncate a string down to x characters
|
||||
def truncate(input, length = 50, truncate_string = "...".freeze)
|
||||
def truncate(input, length = 50, truncate_string = "...")
|
||||
return if input.nil?
|
||||
input_str = input.to_s
|
||||
length = Utils.to_integer(length)
|
||||
length = Utils.to_integer(length)
|
||||
|
||||
truncate_string_str = truncate_string.to_s
|
||||
|
||||
l = length - truncate_string_str.length
|
||||
l = 0 if l < 0
|
||||
|
||||
input_str.length > length ? input_str[0...l].concat(truncate_string_str) : input_str
|
||||
end
|
||||
|
||||
def truncatewords(input, words = 15, truncate_string = "...".freeze)
|
||||
def truncatewords(input, words = 15, truncate_string = "...")
|
||||
return if input.nil?
|
||||
wordlist = input.to_s.split
|
||||
words = Utils.to_integer(words)
|
||||
l = words - 1
|
||||
l = 0 if l < 0
|
||||
wordlist.length > l ? wordlist[0..l].join(" ".freeze).concat(truncate_string.to_s) : input
|
||||
|
||||
words = 1 if words <= 0
|
||||
|
||||
# Scan for non-space characters followed by one or more space characters
|
||||
# `words` times. Also ignore leading whitespace
|
||||
str = input[/\A[ ]*(?:[^ ]*[ ]+){#{words}}/]
|
||||
|
||||
if str
|
||||
str.strip! # Remove trailing space
|
||||
str.gsub!(/[ ]{2,}/, " ") # Shrink multiple spaces to one space
|
||||
str.concat(truncate_string.to_s)
|
||||
else
|
||||
input
|
||||
end
|
||||
end
|
||||
|
||||
# Split input string into an array of substrings separated by given pattern.
|
||||
@@ -113,7 +128,7 @@ module Liquid
|
||||
end
|
||||
|
||||
def strip_html(input)
|
||||
empty = ''.freeze
|
||||
empty = ''
|
||||
result = input.to_s.gsub(STRIP_HTML_BLOCKS, empty)
|
||||
result.gsub!(STRIP_HTML_TAGS, empty)
|
||||
result
|
||||
@@ -121,18 +136,18 @@ module Liquid
|
||||
|
||||
# Remove all newlines from the string
|
||||
def strip_newlines(input)
|
||||
input.to_s.gsub(/\r?\n/, ''.freeze)
|
||||
input.to_s.gsub(/\r?\n/, '')
|
||||
end
|
||||
|
||||
# Join elements of the array with certain character between them
|
||||
def join(input, glue = ' '.freeze)
|
||||
InputIterator.new(input).join(glue)
|
||||
def join(input, glue = ' ')
|
||||
InputIterator.new(input, context).join(glue)
|
||||
end
|
||||
|
||||
# Sort elements of the array
|
||||
# provide optional property with which to sort an array of hashes or drops
|
||||
def sort(input, property = nil)
|
||||
ary = InputIterator.new(input)
|
||||
ary = InputIterator.new(input, context)
|
||||
|
||||
return [] if ary.empty?
|
||||
|
||||
@@ -152,7 +167,7 @@ module Liquid
|
||||
# Sort elements of an array ignoring case if strings
|
||||
# provide optional property with which to sort an array of hashes or drops
|
||||
def sort_natural(input, property = nil)
|
||||
ary = InputIterator.new(input)
|
||||
ary = InputIterator.new(input, context)
|
||||
|
||||
return [] if ary.empty?
|
||||
|
||||
@@ -172,7 +187,7 @@ module Liquid
|
||||
# Filter the elements of an array to those with a certain property value.
|
||||
# By default the target is any truthy value.
|
||||
def where(input, property, target_value = nil)
|
||||
ary = InputIterator.new(input)
|
||||
ary = InputIterator.new(input, context)
|
||||
|
||||
if ary.empty?
|
||||
[]
|
||||
@@ -194,7 +209,7 @@ module Liquid
|
||||
# Remove duplicate elements from an array
|
||||
# provide optional property with which to determine uniqueness
|
||||
def uniq(input, property = nil)
|
||||
ary = InputIterator.new(input)
|
||||
ary = InputIterator.new(input, context)
|
||||
|
||||
if property.nil?
|
||||
ary.uniq
|
||||
@@ -211,16 +226,16 @@ module Liquid
|
||||
|
||||
# Reverse the elements of an array
|
||||
def reverse(input)
|
||||
ary = InputIterator.new(input)
|
||||
ary = InputIterator.new(input, context)
|
||||
ary.reverse
|
||||
end
|
||||
|
||||
# map/collect on a given 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)
|
||||
|
||||
if property == "to_liquid".freeze
|
||||
if property == "to_liquid"
|
||||
e
|
||||
elsif e.respond_to?(:[])
|
||||
r = e[property]
|
||||
@@ -234,7 +249,7 @@ module Liquid
|
||||
# Remove nils within an array
|
||||
# provide optional property with which to check for nil
|
||||
def compact(input, property = nil)
|
||||
ary = InputIterator.new(input)
|
||||
ary = InputIterator.new(input, context)
|
||||
|
||||
if property.nil?
|
||||
ary.compact
|
||||
@@ -250,23 +265,23 @@ module Liquid
|
||||
end
|
||||
|
||||
# Replace occurrences of a string with another
|
||||
def replace(input, string, replacement = ''.freeze)
|
||||
def replace(input, string, replacement = '')
|
||||
input.to_s.gsub(string.to_s, replacement.to_s)
|
||||
end
|
||||
|
||||
# Replace the first occurrences of a string with another
|
||||
def replace_first(input, string, replacement = ''.freeze)
|
||||
def replace_first(input, string, replacement = '')
|
||||
input.to_s.sub(string.to_s, replacement.to_s)
|
||||
end
|
||||
|
||||
# remove a substring
|
||||
def remove(input, string)
|
||||
input.to_s.gsub(string.to_s, ''.freeze)
|
||||
input.to_s.gsub(string.to_s, '')
|
||||
end
|
||||
|
||||
# remove the first occurrences of a substring
|
||||
def remove_first(input, string)
|
||||
input.to_s.sub(string.to_s, ''.freeze)
|
||||
input.to_s.sub(string.to_s, '')
|
||||
end
|
||||
|
||||
# add one string to another
|
||||
@@ -278,7 +293,7 @@ module Liquid
|
||||
unless array.respond_to?(:to_ary)
|
||||
raise ArgumentError, "concat filter requires an array argument"
|
||||
end
|
||||
InputIterator.new(input).concat(array)
|
||||
InputIterator.new(input, context).concat(array)
|
||||
end
|
||||
|
||||
# prepend a string to another
|
||||
@@ -288,7 +303,7 @@ module Liquid
|
||||
|
||||
# Add <br /> tags in front of all newlines in input string
|
||||
def newline_to_br(input)
|
||||
input.to_s.gsub(/\n/, "<br />\n".freeze)
|
||||
input.to_s.gsub(/\n/, "<br />\n")
|
||||
end
|
||||
|
||||
# Reformat a date using Ruby's core Time#strftime( string ) -> string
|
||||
@@ -325,7 +340,7 @@ module Liquid
|
||||
def date(input, format)
|
||||
return input if format.to_s.empty?
|
||||
|
||||
return input unless date = Utils.to_date(input)
|
||||
return input unless (date = Utils.to_date(input))
|
||||
|
||||
date.strftime(format.to_s)
|
||||
end
|
||||
@@ -419,17 +434,26 @@ module Liquid
|
||||
result.is_a?(BigDecimal) ? result.to_f : result
|
||||
end
|
||||
|
||||
def default(input, default_value = ''.freeze)
|
||||
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
|
||||
default_value
|
||||
else
|
||||
input
|
||||
end
|
||||
# Set a default value when the input is nil, false or empty
|
||||
#
|
||||
# Example:
|
||||
# {{ product.title | default: "No Title" }}
|
||||
#
|
||||
# Use `allow_false` when an input should only be tested against nil or empty and not false.
|
||||
#
|
||||
# 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
|
||||
|
||||
private
|
||||
|
||||
attr_reader :context
|
||||
|
||||
def raise_property_error(property)
|
||||
raise Liquid::ArgumentError, "cannot select the property '#{property}'"
|
||||
end
|
||||
@@ -458,8 +482,9 @@ module Liquid
|
||||
class InputIterator
|
||||
include Enumerable
|
||||
|
||||
def initialize(input)
|
||||
@input = if input.is_a?(Array)
|
||||
def initialize(input, context)
|
||||
@context = context
|
||||
@input = if input.is_a?(Array)
|
||||
input.flatten
|
||||
elsif input.is_a?(Hash)
|
||||
[input]
|
||||
@@ -497,6 +522,7 @@ module Liquid
|
||||
|
||||
def each
|
||||
@input.each do |e|
|
||||
e.context = @context if e.respond_to?(:context=)
|
||||
yield(e.respond_to?(:to_liquid) ? e.to_liquid : e)
|
||||
end
|
||||
end
|
||||
|
||||
44
lib/liquid/static_registers.rb
Normal file
44
lib/liquid/static_registers.rb
Normal file
@@ -0,0 +1,44 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class StaticRegisters
|
||||
attr_reader :static
|
||||
|
||||
def initialize(registers = {})
|
||||
@static = registers.is_a?(StaticRegisters) ? registers.static : registers
|
||||
@registers = {}
|
||||
end
|
||||
|
||||
def []=(key, value)
|
||||
@registers[key] = value
|
||||
end
|
||||
|
||||
def [](key)
|
||||
if @registers.key?(key)
|
||||
@registers[key]
|
||||
else
|
||||
@static[key]
|
||||
end
|
||||
end
|
||||
|
||||
def delete(key)
|
||||
@registers.delete(key)
|
||||
end
|
||||
|
||||
UNDEFINED = Object.new
|
||||
|
||||
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
|
||||
|
||||
def key?(key)
|
||||
@registers.key?(key) || @static.key?(key)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,66 +0,0 @@
|
||||
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
|
||||
36
lib/liquid/strainer_factory.rb
Normal file
36
lib/liquid/strainer_factory.rb
Normal 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
|
||||
53
lib/liquid/strainer_template.rb
Normal file
53
lib/liquid/strainer_template.rb
Normal 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
|
||||
@@ -1,11 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class TablerowloopDrop < Drop
|
||||
def initialize(length, cols)
|
||||
@length = length
|
||||
@row = 1
|
||||
@col = 1
|
||||
@cols = cols
|
||||
@index = 0
|
||||
@row = 1
|
||||
@col = 1
|
||||
@cols = cols
|
||||
@index = 0
|
||||
end
|
||||
|
||||
attr_reader :length, :col, :row
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Tag
|
||||
attr_reader :nodelist, :tag_name, :line_number, :parse_context
|
||||
@@ -11,14 +13,20 @@ module Liquid
|
||||
tag
|
||||
end
|
||||
|
||||
def disable_tags(*tag_names)
|
||||
@disabled_tags ||= []
|
||||
@disabled_tags.concat(tag_names)
|
||||
prepend(Disabler)
|
||||
end
|
||||
|
||||
private :new
|
||||
end
|
||||
|
||||
def initialize(tag_name, markup, parse_context)
|
||||
@tag_name = tag_name
|
||||
@markup = markup
|
||||
@tag_name = tag_name
|
||||
@markup = markup
|
||||
@parse_context = parse_context
|
||||
@line_number = parse_context.line_number
|
||||
@line_number = parse_context.line_number
|
||||
end
|
||||
|
||||
def parse(_tokens)
|
||||
@@ -33,7 +41,7 @@ module Liquid
|
||||
end
|
||||
|
||||
def render(_context)
|
||||
''.freeze
|
||||
''
|
||||
end
|
||||
|
||||
# For backwards compatibility with custom tags. In a future release, the semantics
|
||||
|
||||
22
lib/liquid/tag/disableable.rb
Normal file
22
lib/liquid/tag/disableable.rb
Normal 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
|
||||
21
lib/liquid/tag/disabler.rb
Normal file
21
lib/liquid/tag/disabler.rb
Normal 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
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# Assign sets a variable in your template.
|
||||
#
|
||||
@@ -10,26 +12,22 @@ module Liquid
|
||||
class Assign < Tag
|
||||
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
|
||||
|
||||
def self.syntax_error_translation_key
|
||||
"errors.syntax.assign".freeze
|
||||
end
|
||||
|
||||
attr_reader :to, :from
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
if markup =~ Syntax
|
||||
@to = Regexp.last_match(1)
|
||||
@to = Regexp.last_match(1)
|
||||
@from = Variable.new(Regexp.last_match(2), options)
|
||||
else
|
||||
raise SyntaxError, options[:locale].t(self.class.syntax_error_translation_key)
|
||||
raise SyntaxError, options[:locale].t('errors.syntax.assign')
|
||||
end
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
val = @from.render(context)
|
||||
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
|
||||
end
|
||||
|
||||
@@ -59,5 +57,5 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('assign'.freeze, Assign)
|
||||
Template.register_tag('assign', Assign)
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# Break tag to be used to break out of a for loop.
|
||||
#
|
||||
@@ -9,10 +11,13 @@ module Liquid
|
||||
# {% endfor %}
|
||||
#
|
||||
class Break < Tag
|
||||
def interrupt
|
||||
BreakInterrupt.new
|
||||
INTERRUPT = BreakInterrupt.new.freeze
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
context.push_interrupt(INTERRUPT)
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('break'.freeze, Break)
|
||||
Template.register_tag('break', Break)
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# Capture stores the result of a block into a variable without rendering it inplace.
|
||||
#
|
||||
@@ -23,10 +25,10 @@ module Liquid
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
previous_output_size = output.bytesize
|
||||
super
|
||||
context.scopes.last[@to] = output
|
||||
context.resource_limits.assign_score += (output.bytesize - previous_output_size)
|
||||
context.resource_limits.with_capture do
|
||||
capture_output = render(context)
|
||||
context.scopes.last[@to] = capture_output
|
||||
end
|
||||
output
|
||||
end
|
||||
|
||||
@@ -35,5 +37,5 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('capture'.freeze, Capture)
|
||||
Template.register_tag('capture', Capture)
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Case < Block
|
||||
Syntax = /(#{QuotedFragment})/o
|
||||
@@ -12,13 +14,16 @@ module Liquid
|
||||
if markup =~ Syntax
|
||||
@left = Expression.parse(Regexp.last_match(1))
|
||||
else
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.case".freeze)
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.case")
|
||||
end
|
||||
end
|
||||
|
||||
def parse(tokens)
|
||||
body = BlockBody.new
|
||||
body = new_body
|
||||
body = @blocks.last.attachment while parse_body(body, tokens)
|
||||
if blank?
|
||||
@blocks.each { |condition| condition.attachment.remove_blank_strings }
|
||||
end
|
||||
end
|
||||
|
||||
def nodelist
|
||||
@@ -27,9 +32,9 @@ module Liquid
|
||||
|
||||
def unknown_tag(tag, markup, tokens)
|
||||
case tag
|
||||
when 'when'.freeze
|
||||
when 'when'
|
||||
record_when_condition(markup)
|
||||
when 'else'.freeze
|
||||
when 'else'
|
||||
record_else_condition(markup)
|
||||
else
|
||||
super
|
||||
@@ -54,16 +59,16 @@ module Liquid
|
||||
private
|
||||
|
||||
def record_when_condition(markup)
|
||||
body = BlockBody.new
|
||||
body = new_body
|
||||
|
||||
while markup
|
||||
unless markup =~ WhenSyntax
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_when".freeze)
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_when")
|
||||
end
|
||||
|
||||
markup = Regexp.last_match(2)
|
||||
|
||||
block = Condition.new(@left, '=='.freeze, Expression.parse(Regexp.last_match(1)))
|
||||
block = Condition.new(@left, '==', Condition.parse_expression(Regexp.last_match(1)))
|
||||
block.attach(body)
|
||||
@blocks << block
|
||||
end
|
||||
@@ -71,11 +76,11 @@ module Liquid
|
||||
|
||||
def record_else_condition(markup)
|
||||
unless markup.strip.empty?
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_else".freeze)
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_else")
|
||||
end
|
||||
|
||||
block = ElseCondition.new
|
||||
block.attach(BlockBody.new)
|
||||
block.attach(new_body)
|
||||
@blocks << block
|
||||
end
|
||||
|
||||
@@ -86,5 +91,5 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('case'.freeze, Case)
|
||||
Template.register_tag('case', Case)
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Comment < Block
|
||||
def render_to_output_buffer(_context, output)
|
||||
@@ -12,5 +14,5 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('comment'.freeze, Comment)
|
||||
Template.register_tag('comment', Comment)
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# Continue tag to be used to break out of a for loop.
|
||||
#
|
||||
@@ -9,10 +11,13 @@ module Liquid
|
||||
# {% endfor %}
|
||||
#
|
||||
class Continue < Tag
|
||||
def interrupt
|
||||
ContinueInterrupt.new
|
||||
INTERRUPT = ContinueInterrupt.new.freeze
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
context.push_interrupt(INTERRUPT)
|
||||
output
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('continue'.freeze, Continue)
|
||||
Template.register_tag('continue', Continue)
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# Cycle is usually used within a loop to alternate between values, like colors or DOM classes.
|
||||
#
|
||||
@@ -22,19 +24,19 @@ module Liquid
|
||||
case markup
|
||||
when NamedSyntax
|
||||
@variables = variables_from_string(Regexp.last_match(2))
|
||||
@name = Expression.parse(Regexp.last_match(1))
|
||||
@name = Expression.parse(Regexp.last_match(1))
|
||||
when SimpleSyntax
|
||||
@variables = variables_from_string(markup)
|
||||
@name = @variables.to_s
|
||||
@name = @variables.to_s
|
||||
else
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.cycle".freeze)
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.cycle")
|
||||
end
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
context.registers[:cycle] ||= {}
|
||||
|
||||
key = context.evaluate(@name)
|
||||
key = context.evaluate(@name)
|
||||
iteration = context.registers[:cycle][key].to_i
|
||||
|
||||
val = context.evaluate(@variables[iteration])
|
||||
@@ -48,9 +50,9 @@ module Liquid
|
||||
output << val
|
||||
|
||||
iteration += 1
|
||||
iteration = 0 if iteration >= @variables.size
|
||||
context.registers[:cycle][key] = iteration
|
||||
iteration = 0 if iteration >= @variables.size
|
||||
|
||||
context.registers[:cycle][key] = iteration
|
||||
output
|
||||
end
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# decrement is used in a place where one needs to insert a counter
|
||||
# into a template, and needs the counter to survive across
|
||||
@@ -32,5 +34,5 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('decrement'.freeze, Decrement)
|
||||
Template.register_tag('decrement', Decrement)
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# Echo outputs an expression
|
||||
#
|
||||
@@ -16,9 +18,9 @@ module Liquid
|
||||
end
|
||||
|
||||
def render(context)
|
||||
@variable.render_to_output_buffer(context, '')
|
||||
@variable.render_to_output_buffer(context, +'')
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('echo'.freeze, Echo)
|
||||
Template.register_tag('echo', Echo)
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# "For" iterates over an array or collection.
|
||||
# Several useful variables are available to you within the loop.
|
||||
@@ -52,13 +54,18 @@ module Liquid
|
||||
super
|
||||
@from = @limit = nil
|
||||
parse_with_selected_parser(markup)
|
||||
@for_block = BlockBody.new
|
||||
@for_block = new_body
|
||||
@else_block = nil
|
||||
end
|
||||
|
||||
def parse(tokens)
|
||||
return unless parse_body(@for_block, tokens)
|
||||
parse_body(@else_block, tokens)
|
||||
if parse_body(@for_block, tokens)
|
||||
parse_body(@else_block, tokens)
|
||||
end
|
||||
if blank?
|
||||
@for_block.remove_blank_strings
|
||||
@else_block&.remove_blank_strings
|
||||
end
|
||||
end
|
||||
|
||||
def nodelist
|
||||
@@ -66,8 +73,8 @@ module Liquid
|
||||
end
|
||||
|
||||
def unknown_tag(tag, markup, tokens)
|
||||
return super unless tag == 'else'.freeze
|
||||
@else_block = BlockBody.new
|
||||
return super unless tag == 'else'
|
||||
@else_block = new_body
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
@@ -86,31 +93,33 @@ module Liquid
|
||||
|
||||
def lax_parse(markup)
|
||||
if markup =~ Syntax
|
||||
@variable_name = Regexp.last_match(1)
|
||||
collection_name = Regexp.last_match(2)
|
||||
@reversed = !!Regexp.last_match(3)
|
||||
@name = "#{@variable_name}-#{collection_name}"
|
||||
@variable_name = Regexp.last_match(1)
|
||||
collection_name = Regexp.last_match(2)
|
||||
@reversed = !!Regexp.last_match(3)
|
||||
@name = "#{@variable_name}-#{collection_name}"
|
||||
@collection_name = Expression.parse(collection_name)
|
||||
markup.scan(TagAttributes) do |key, value|
|
||||
set_attribute(key, value)
|
||||
end
|
||||
else
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.for".freeze)
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.for")
|
||||
end
|
||||
end
|
||||
|
||||
def strict_parse(markup)
|
||||
p = Parser.new(markup)
|
||||
@variable_name = p.consume(:id)
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in".freeze) unless p.id?('in'.freeze)
|
||||
collection_name = p.expression
|
||||
@name = "#{@variable_name}-#{collection_name}"
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in')
|
||||
|
||||
collection_name = p.expression
|
||||
@collection_name = Expression.parse(collection_name)
|
||||
@reversed = p.id?('reversed'.freeze)
|
||||
|
||||
@name = "#{@variable_name}-#{collection_name}"
|
||||
@reversed = p.id?('reversed')
|
||||
|
||||
while p.look(:id) && p.look(:colon, 1)
|
||||
unless attribute = p.id?('limit'.freeze) || p.id?('offset'.freeze)
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_attribute".freeze)
|
||||
unless (attribute = p.id?('limit') || p.id?('offset'))
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_attribute")
|
||||
end
|
||||
p.consume
|
||||
set_attribute(attribute, p.expression)
|
||||
@@ -154,7 +163,7 @@ module Liquid
|
||||
|
||||
def render_segment(context, output, segment)
|
||||
for_stack = context.registers[:for_stack] ||= []
|
||||
length = segment.length
|
||||
length = segment.length
|
||||
|
||||
context.stack do
|
||||
loop_vars = Liquid::ForloopDrop.new(@name, length, for_stack[-1])
|
||||
@@ -162,7 +171,7 @@ module Liquid
|
||||
for_stack.push(loop_vars)
|
||||
|
||||
begin
|
||||
context['forloop'.freeze] = loop_vars
|
||||
context['forloop'] = loop_vars
|
||||
|
||||
segment.each do |item|
|
||||
context[@variable_name] = item
|
||||
@@ -185,13 +194,13 @@ module Liquid
|
||||
|
||||
def set_attribute(key, expr)
|
||||
case key
|
||||
when 'offset'.freeze
|
||||
@from = if expr == 'continue'.freeze
|
||||
when 'offset'
|
||||
@from = if expr == 'continue'
|
||||
:continue
|
||||
else
|
||||
Expression.parse(expr)
|
||||
end
|
||||
when 'limit'.freeze
|
||||
when 'limit'
|
||||
@limit = Expression.parse(expr)
|
||||
end
|
||||
end
|
||||
@@ -211,5 +220,5 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('for'.freeze, For)
|
||||
Template.register_tag('for', For)
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# If is the conditional block
|
||||
#
|
||||
@@ -10,16 +12,16 @@ module Liquid
|
||||
# There are {% if count < 5 %} less {% else %} more {% endif %} items than you need.
|
||||
#
|
||||
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
|
||||
BOOLEAN_OPERATORS = %w(and or).freeze
|
||||
BOOLEAN_OPERATORS = %w(and or).freeze
|
||||
|
||||
attr_reader :blocks
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
@blocks = []
|
||||
push_block('if'.freeze, markup)
|
||||
push_block('if', markup)
|
||||
end
|
||||
|
||||
def nodelist
|
||||
@@ -29,10 +31,13 @@ module Liquid
|
||||
def parse(tokens)
|
||||
while parse_body(@blocks.last.attachment, tokens)
|
||||
end
|
||||
if blank?
|
||||
@blocks.each { |condition| condition.attachment.remove_blank_strings }
|
||||
end
|
||||
end
|
||||
|
||||
def unknown_tag(tag, markup, tokens)
|
||||
if ['elsif'.freeze, 'else'.freeze].include?(tag)
|
||||
if ['elsif', 'else'].include?(tag)
|
||||
push_block(tag, markup)
|
||||
else
|
||||
super
|
||||
@@ -52,29 +57,33 @@ module Liquid
|
||||
private
|
||||
|
||||
def push_block(tag, markup)
|
||||
block = if tag == 'else'.freeze
|
||||
block = if tag == 'else'
|
||||
ElseCondition.new
|
||||
else
|
||||
parse_with_selected_parser(markup)
|
||||
end
|
||||
|
||||
@blocks.push(block)
|
||||
block.attach(BlockBody.new)
|
||||
block.attach(new_body)
|
||||
end
|
||||
|
||||
def parse_expression(markup)
|
||||
Condition.parse_expression(markup)
|
||||
end
|
||||
|
||||
def lax_parse(markup)
|
||||
expressions = markup.scan(ExpressionsAndOperators)
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.if".freeze) 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?
|
||||
operator = expressions.pop.to_s.strip
|
||||
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.if".freeze) 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)))
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.if".freeze) unless BOOLEAN_OPERATORS.include?(operator)
|
||||
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)
|
||||
new_condition.send(operator, condition)
|
||||
condition = new_condition
|
||||
end
|
||||
@@ -92,7 +101,7 @@ module Liquid
|
||||
def parse_binary_comparisons(p)
|
||||
condition = parse_comparison(p)
|
||||
first_condition = condition
|
||||
while op = (p.id?('and'.freeze) || p.id?('or'.freeze))
|
||||
while (op = (p.id?('and') || p.id?('or')))
|
||||
child_condition = parse_comparison(p)
|
||||
condition.send(op, child_condition)
|
||||
condition = child_condition
|
||||
@@ -101,9 +110,9 @@ module Liquid
|
||||
end
|
||||
|
||||
def parse_comparison(p)
|
||||
a = Expression.parse(p.expression)
|
||||
if op = p.consume?(:comparison)
|
||||
b = Expression.parse(p.expression)
|
||||
a = parse_expression(p.expression)
|
||||
if (op = p.consume?(:comparison))
|
||||
b = parse_expression(p.expression)
|
||||
Condition.new(a, op, b)
|
||||
else
|
||||
Condition.new(a)
|
||||
@@ -117,5 +126,5 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('if'.freeze, If)
|
||||
Template.register_tag('if', If)
|
||||
end
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Ifchanged < Block
|
||||
def render_to_output_buffer(context, output)
|
||||
block_output = ''
|
||||
block_output = +''
|
||||
super(context, block_output)
|
||||
|
||||
if block_output != context.registers[:ifchanged]
|
||||
@@ -13,5 +15,5 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('ifchanged'.freeze, Ifchanged)
|
||||
Template.register_tag('ifchanged', Ifchanged)
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# Include allows templates to relate with other templates
|
||||
#
|
||||
@@ -14,28 +16,32 @@ module Liquid
|
||||
# {% include 'product' for products %}
|
||||
#
|
||||
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
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
|
||||
if markup =~ Syntax
|
||||
if markup =~ SYNTAX
|
||||
|
||||
template_name = Regexp.last_match(1)
|
||||
variable_name = Regexp.last_match(3)
|
||||
|
||||
@alias_name = Regexp.last_match(5)
|
||||
@variable_name_expr = variable_name ? Expression.parse(variable_name) : nil
|
||||
@template_name_expr = Expression.parse(template_name)
|
||||
@attributes = {}
|
||||
@attributes = {}
|
||||
|
||||
markup.scan(TagAttributes) do |key, value|
|
||||
@attributes[key] = Expression.parse(value)
|
||||
end
|
||||
|
||||
else
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.include".freeze)
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.include")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -52,7 +58,7 @@ module Liquid
|
||||
parse_context: parse_context
|
||||
)
|
||||
|
||||
context_variable_name = template_name.split('/'.freeze).last
|
||||
context_variable_name = @alias_name || template_name.split('/').last
|
||||
|
||||
variable = if @variable_name_expr
|
||||
context.evaluate(@variable_name_expr)
|
||||
@@ -61,10 +67,10 @@ module Liquid
|
||||
end
|
||||
|
||||
old_template_name = context.template_name
|
||||
old_partial = context.partial
|
||||
old_partial = context.partial
|
||||
begin
|
||||
context.template_name = template_name
|
||||
context.partial = true
|
||||
context.partial = true
|
||||
context.stack do
|
||||
@attributes.each do |key, value|
|
||||
context[key] = context.evaluate(value)
|
||||
@@ -82,7 +88,7 @@ module Liquid
|
||||
end
|
||||
ensure
|
||||
context.template_name = old_template_name
|
||||
context.partial = old_partial
|
||||
context.partial = old_partial
|
||||
end
|
||||
|
||||
output
|
||||
@@ -101,5 +107,5 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('include'.freeze, Include)
|
||||
Template.register_tag('include', Include)
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# increment is used in a place where one needs to insert a counter
|
||||
# into a template, and needs the counter to survive across
|
||||
@@ -29,5 +31,5 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('increment'.freeze, Increment)
|
||||
Template.register_tag('increment', Increment)
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Raw < Block
|
||||
Syntax = /\A\s*\z/
|
||||
@@ -10,16 +12,16 @@ module Liquid
|
||||
end
|
||||
|
||||
def parse(tokens)
|
||||
@body = ''
|
||||
while token = tokens.shift
|
||||
if token =~ FullTokenPossiblyInvalid
|
||||
@body << Regexp.last_match(1) if Regexp.last_match(1) != "".freeze
|
||||
return if block_delimiter == Regexp.last_match(2)
|
||||
@body = +''
|
||||
while (token = tokens.shift)
|
||||
if token =~ FullTokenPossiblyInvalid && block_delimiter == Regexp.last_match(2)
|
||||
@body << Regexp.last_match(1) if Regexp.last_match(1) != ""
|
||||
return
|
||||
end
|
||||
@body << token unless token.empty?
|
||||
end
|
||||
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name)
|
||||
raise_tag_never_closed(block_name)
|
||||
end
|
||||
|
||||
def render_to_output_buffer(_context, output)
|
||||
@@ -38,11 +40,11 @@ module Liquid
|
||||
protected
|
||||
|
||||
def ensure_valid_markup(tag_name, markup, parse_context)
|
||||
unless markup =~ Syntax
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_unexpected_args".freeze, tag: tag_name)
|
||||
unless Syntax.match?(markup)
|
||||
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_unexpected_args", tag: tag_name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('raw'.freeze, Raw)
|
||||
Template.register_tag('raw', Raw)
|
||||
end
|
||||
|
||||
@@ -1,17 +1,27 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Render < Tag
|
||||
SYNTAX = /(#{QuotedString})#{QuotedFragment}*/o
|
||||
FOR = 'for'
|
||||
SYNTAX = /(#{QuotedString}+)(\s+(with|#{FOR})\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
|
||||
|
||||
disable_tags "include"
|
||||
|
||||
attr_reader :template_name_expr, :attributes
|
||||
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.render".freeze) unless markup =~ SYNTAX
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.render") unless markup =~ SYNTAX
|
||||
|
||||
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)
|
||||
@for = (with_or_for == FOR)
|
||||
|
||||
@attributes = {}
|
||||
markup.scan(TagAttributes) do |key, value|
|
||||
@@ -20,6 +30,10 @@ module Liquid
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
render_tag(context, output)
|
||||
end
|
||||
|
||||
def render_tag(context, output)
|
||||
# Though we evaluate this here we will only ever parse it as a string literal.
|
||||
template_name = context.evaluate(@template_name_expr)
|
||||
raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name
|
||||
@@ -30,13 +44,29 @@ module Liquid
|
||||
parse_context: parse_context
|
||||
)
|
||||
|
||||
inner_context = context.new_isolated_subcontext
|
||||
inner_context.template_name = template_name
|
||||
inner_context.partial = true
|
||||
@attributes.each do |key, value|
|
||||
inner_context[key] = context.evaluate(value)
|
||||
context_variable_name = @alias_name || template_name.split('/').last
|
||||
|
||||
render_partial_func = ->(var, forloop) {
|
||||
inner_context = context.new_isolated_subcontext
|
||||
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
|
||||
partial.render_to_output_buffer(inner_context, output)
|
||||
|
||||
output
|
||||
end
|
||||
@@ -50,5 +80,5 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('render'.freeze, Render)
|
||||
Template.register_tag('render', Render)
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class TableRow < Block
|
||||
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
|
||||
@@ -7,33 +9,32 @@ module Liquid
|
||||
def initialize(tag_name, markup, options)
|
||||
super
|
||||
if markup =~ Syntax
|
||||
@variable_name = Regexp.last_match(1)
|
||||
@variable_name = Regexp.last_match(1)
|
||||
@collection_name = Expression.parse(Regexp.last_match(2))
|
||||
@attributes = {}
|
||||
@attributes = {}
|
||||
markup.scan(TagAttributes) do |key, value|
|
||||
@attributes[key] = Expression.parse(value)
|
||||
end
|
||||
else
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.table_row".freeze)
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.table_row")
|
||||
end
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
(collection = context.evaluate(@collection_name)) || (return ''.freeze)
|
||||
(collection = context.evaluate(@collection_name)) || (return '')
|
||||
|
||||
from = @attributes.key?('offset'.freeze) ? context.evaluate(@attributes['offset'.freeze]).to_i : 0
|
||||
to = @attributes.key?('limit'.freeze) ? from + context.evaluate(@attributes['limit'.freeze]).to_i : nil
|
||||
from = @attributes.key?('offset') ? context.evaluate(@attributes['offset']).to_i : 0
|
||||
to = @attributes.key?('limit') ? from + context.evaluate(@attributes['limit']).to_i : nil
|
||||
|
||||
collection = Utils.slice_collection(collection, from, to)
|
||||
length = collection.length
|
||||
|
||||
length = collection.length
|
||||
|
||||
cols = context.evaluate(@attributes['cols'.freeze]).to_i
|
||||
cols = context.evaluate(@attributes['cols']).to_i
|
||||
|
||||
output << "<tr class=\"row1\">\n"
|
||||
context.stack do
|
||||
tablerowloop = Liquid::TablerowloopDrop.new(length, cols)
|
||||
context['tablerowloop'.freeze] = tablerowloop
|
||||
context['tablerowloop'] = tablerowloop
|
||||
|
||||
collection.each do |item|
|
||||
context[@variable_name] = item
|
||||
@@ -61,5 +62,5 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('tablerow'.freeze, TableRow)
|
||||
Template.register_tag('tablerow', TableRow)
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require_relative 'if'
|
||||
|
||||
module Liquid
|
||||
@@ -24,5 +26,5 @@ module Liquid
|
||||
end
|
||||
end
|
||||
|
||||
Template.register_tag('unless'.freeze, Unless)
|
||||
Template.register_tag('unless', Unless)
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# Templates are central to liquid.
|
||||
# Interpretating templates is a two step process. First you compile the
|
||||
@@ -16,13 +18,11 @@ module Liquid
|
||||
attr_accessor :root
|
||||
attr_reader :resource_limits, :warnings
|
||||
|
||||
@@file_system = BlankFileSystem.new
|
||||
|
||||
class TagRegistry
|
||||
include Enumerable
|
||||
|
||||
def initialize
|
||||
@tags = {}
|
||||
@tags = {}
|
||||
@cache = {}
|
||||
end
|
||||
|
||||
@@ -61,76 +61,57 @@ module Liquid
|
||||
# :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.
|
||||
# :strict will enforce correct syntax.
|
||||
attr_writer :error_mode
|
||||
|
||||
# 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 :error_mode
|
||||
Template.error_mode = :lax
|
||||
|
||||
attr_accessor :default_exception_renderer
|
||||
Template.default_exception_renderer = lambda do |exception|
|
||||
exception
|
||||
end
|
||||
|
||||
def file_system
|
||||
@@file_system
|
||||
end
|
||||
attr_accessor :file_system
|
||||
Template.file_system = BlankFileSystem.new
|
||||
|
||||
def file_system=(obj)
|
||||
@@file_system = obj
|
||||
end
|
||||
attr_accessor :tags
|
||||
Template.tags = TagRegistry.new
|
||||
private :tags=
|
||||
|
||||
def register_tag(name, klass)
|
||||
tags[name.to_s] = klass
|
||||
end
|
||||
|
||||
def tags
|
||||
@tags ||= TagRegistry.new
|
||||
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
|
||||
# to all liquid views. Good for registering the standard library
|
||||
def register_filter(mod)
|
||||
Strainer.global_filter(mod)
|
||||
StrainerFactory.add_global_filter(mod)
|
||||
end
|
||||
|
||||
def default_resource_limits
|
||||
@default_resource_limits ||= {}
|
||||
end
|
||||
attr_accessor :default_resource_limits
|
||||
Template.default_resource_limits = {}
|
||||
private :default_resource_limits=
|
||||
|
||||
# creates a new <tt>Template</tt> object from liquid source code
|
||||
# To enable profiling, pass in <tt>profile: true</tt> as an option.
|
||||
# See Liquid::Profiler for more information
|
||||
def parse(source, options = {})
|
||||
template = Template.new
|
||||
template.parse(source, options)
|
||||
new.parse(source, options)
|
||||
end
|
||||
end
|
||||
|
||||
def initialize
|
||||
@rethrow_errors = false
|
||||
@resource_limits = ResourceLimits.new(self.class.default_resource_limits)
|
||||
@rethrow_errors = false
|
||||
@resource_limits = ResourceLimits.new(Template.default_resource_limits)
|
||||
end
|
||||
|
||||
# Parse source code.
|
||||
# Returns self for easy chaining
|
||||
def parse(source, options = {})
|
||||
@options = options
|
||||
@profiling = options[:profile]
|
||||
@options = options
|
||||
@profiling = options[:profile]
|
||||
@line_numbers = options[:line_numbers] || @profiling
|
||||
parse_context = options.is_a?(ParseContext) ? options : ParseContext.new(options)
|
||||
@root = Document.parse(tokenize(source), parse_context)
|
||||
@warnings = parse_context.warnings
|
||||
@root = Document.parse(tokenize(source), parse_context)
|
||||
@warnings = parse_context.warnings
|
||||
self
|
||||
end
|
||||
|
||||
@@ -165,7 +146,7 @@ module Liquid
|
||||
# filters and tags and might be useful to integrate liquid more with its host application
|
||||
#
|
||||
def render(*args)
|
||||
return ''.freeze if @root.nil?
|
||||
return '' if @root.nil?
|
||||
|
||||
context = case args.first
|
||||
when Liquid::Context
|
||||
@@ -177,7 +158,7 @@ module Liquid
|
||||
|
||||
c
|
||||
when Liquid::Drop
|
||||
drop = args.shift
|
||||
drop = args.shift
|
||||
drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
|
||||
when Hash
|
||||
Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
|
||||
@@ -189,12 +170,16 @@ module Liquid
|
||||
|
||||
output = nil
|
||||
|
||||
context_register = context.registers.is_a?(StaticRegisters) ? context.registers.static : context.registers
|
||||
|
||||
case args.last
|
||||
when Hash
|
||||
options = args.pop
|
||||
output = options[:output] if options[:output]
|
||||
output = options[:output] if options[:output]
|
||||
|
||||
registers.merge!(options[:registers]) if options[:registers].is_a?(Hash)
|
||||
options[:registers]&.each do |key, register|
|
||||
context_register[key] = register
|
||||
end
|
||||
|
||||
apply_options_to_context(context, options)
|
||||
when Module, Array
|
||||
@@ -208,7 +193,7 @@ module Liquid
|
||||
# render the nodelist.
|
||||
# for performance reasons we get an array back here. join will make a string out of it.
|
||||
with_profiling(context) do
|
||||
@root.render_to_output_buffer(context, output || '')
|
||||
@root.render_to_output_buffer(context, output || +'')
|
||||
end
|
||||
rescue Liquid::MemoryError => e
|
||||
context.handle_error(e)
|
||||
@@ -236,7 +221,7 @@ module Liquid
|
||||
if @profiling && !context.partial
|
||||
raise "Profiler not loaded, require 'liquid/profiler' first" unless defined?(Liquid::Profiler)
|
||||
|
||||
@profiler = Profiler.new
|
||||
@profiler = Profiler.new(context.template_name)
|
||||
@profiler.start
|
||||
|
||||
begin
|
||||
@@ -251,10 +236,10 @@ module Liquid
|
||||
|
||||
def apply_options_to_context(context, options)
|
||||
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.strict_variables = options[:strict_variables] if options[:strict_variables]
|
||||
context.strict_filters = options[:strict_filters] if options[:strict_filters]
|
||||
context.strict_variables = options[:strict_variables] if options[:strict_variables]
|
||||
context.strict_filters = options[:strict_filters] if options[:strict_filters]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
9
lib/liquid/template_factory.rb
Normal file
9
lib/liquid/template_factory.rb
Normal file
@@ -0,0 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class TemplateFactory
|
||||
def for(_template_name)
|
||||
Liquid::Template.new
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,12 +1,14 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class Tokenizer
|
||||
attr_reader :line_number, :for_liquid_tag
|
||||
|
||||
def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false)
|
||||
@source = source
|
||||
@line_number = line_number || (line_numbers ? 1 : nil)
|
||||
@source = source
|
||||
@line_number = line_number || (line_numbers ? 1 : nil)
|
||||
@for_liquid_tag = for_liquid_tag
|
||||
@tokens = tokenize
|
||||
@tokens = tokenize
|
||||
end
|
||||
|
||||
def shift
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
module Usage
|
||||
def self.increment(name)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
module Utils
|
||||
def self.slice_collection(collection, from, to)
|
||||
@@ -10,7 +12,7 @@ module Liquid
|
||||
|
||||
def self.slice_collection_using_each(collection, from, to)
|
||||
segments = []
|
||||
index = 0
|
||||
index = 0
|
||||
|
||||
# Maintains Ruby 1.8.7 String#each behaviour on 1.9
|
||||
if collection.is_a?(String)
|
||||
@@ -50,7 +52,7 @@ module Liquid
|
||||
when Numeric
|
||||
obj
|
||||
when String
|
||||
obj.strip =~ /\A-?\d+\.\d+\z/ ? BigDecimal(obj) : obj.to_i
|
||||
/\A-?\d+\.\d+\z/.match?(obj.strip) ? BigDecimal(obj) : obj.to_i
|
||||
else
|
||||
if obj.respond_to?(:to_number)
|
||||
obj.to_number
|
||||
@@ -69,7 +71,7 @@ module Liquid
|
||||
end
|
||||
|
||||
case obj
|
||||
when 'now'.freeze, 'today'.freeze
|
||||
when 'now', 'today'
|
||||
Time.now
|
||||
when /\A\d+\z/, Integer
|
||||
Time.at(obj.to_i)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# Holds variables. Variables are only loaded "just in time"
|
||||
# and are not evaluated as part of the render stage
|
||||
@@ -10,10 +12,10 @@ module Liquid
|
||||
# {{ user | link }}
|
||||
#
|
||||
class Variable
|
||||
FilterMarkupRegex = /#{FilterSeparator}\s*(.*)/om
|
||||
FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
|
||||
FilterArgsRegex = /(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o
|
||||
JustTagAttributes = /\A#{TagAttributes}\z/o
|
||||
FilterMarkupRegex = /#{FilterSeparator}\s*(.*)/om
|
||||
FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
|
||||
FilterArgsRegex = /(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o
|
||||
JustTagAttributes = /\A#{TagAttributes}\z/o
|
||||
MarkupWithQuotedFragment = /(#{QuotedFragment})(.*)/om
|
||||
|
||||
attr_accessor :filters, :name, :line_number
|
||||
@@ -23,10 +25,10 @@ module Liquid
|
||||
include ParserSwitching
|
||||
|
||||
def initialize(markup, parse_context)
|
||||
@markup = markup
|
||||
@name = nil
|
||||
@markup = markup
|
||||
@name = nil
|
||||
@parse_context = parse_context
|
||||
@line_number = parse_context.line_number
|
||||
@line_number = parse_context.line_number
|
||||
|
||||
parse_with_selected_parser(markup)
|
||||
end
|
||||
@@ -43,9 +45,9 @@ module Liquid
|
||||
@filters = []
|
||||
return unless markup =~ MarkupWithQuotedFragment
|
||||
|
||||
name_markup = Regexp.last_match(1)
|
||||
name_markup = Regexp.last_match(1)
|
||||
filter_markup = Regexp.last_match(2)
|
||||
@name = Expression.parse(name_markup)
|
||||
@name = Expression.parse(name_markup)
|
||||
if filter_markup =~ FilterMarkupRegex
|
||||
filters = Regexp.last_match(1).scan(FilterParser)
|
||||
filters.each do |f|
|
||||
@@ -84,9 +86,7 @@ module Liquid
|
||||
context.invoke(filter_name, output, *filter_args)
|
||||
end
|
||||
|
||||
obj = context.apply_global_filter(obj)
|
||||
taint_check(context, obj)
|
||||
obj
|
||||
context.apply_global_filter(obj)
|
||||
end
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
@@ -102,14 +102,22 @@ module Liquid
|
||||
output
|
||||
end
|
||||
|
||||
def disabled?(_context)
|
||||
false
|
||||
end
|
||||
|
||||
def disabled_tags
|
||||
[]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_filter_expressions(filter_name, unparsed_args)
|
||||
filter_args = []
|
||||
filter_args = []
|
||||
keyword_args = nil
|
||||
unparsed_args.each do |a|
|
||||
if matches = a.match(JustTagAttributes)
|
||||
keyword_args ||= {}
|
||||
if (matches = a.match(JustTagAttributes))
|
||||
keyword_args ||= {}
|
||||
keyword_args[matches[1]] = Expression.parse(matches[2])
|
||||
else
|
||||
filter_args << Expression.parse(a)
|
||||
@@ -132,25 +140,6 @@ module Liquid
|
||||
parsed_args
|
||||
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
|
||||
def children
|
||||
[@node.name] + @node.filters.flatten
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
class VariableLookup
|
||||
SQUARE_BRACKETED = /\A\[(.*)\]\z/m
|
||||
COMMAND_METHODS = ['size'.freeze, 'first'.freeze, 'last'.freeze].freeze
|
||||
COMMAND_METHODS = ['size', 'first', 'last'].freeze
|
||||
|
||||
attr_reader :name, :lookups
|
||||
|
||||
@@ -18,7 +20,7 @@ module Liquid
|
||||
end
|
||||
@name = name
|
||||
|
||||
@lookups = lookups
|
||||
@lookups = lookups
|
||||
@command_flags = 0
|
||||
|
||||
@lookups.each_index do |i|
|
||||
@@ -32,7 +34,7 @@ module Liquid
|
||||
end
|
||||
|
||||
def evaluate(context)
|
||||
name = context.evaluate(@name)
|
||||
name = context.evaluate(@name)
|
||||
object = context.find_variable(name)
|
||||
|
||||
@lookups.each_index do |i|
|
||||
@@ -45,7 +47,7 @@ module Liquid
|
||||
(object.respond_to?(:fetch) && key.is_a?(Integer)))
|
||||
|
||||
# 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
|
||||
|
||||
# Some special cases. If the part wasn't in square brackets and
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# encoding: utf-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
VERSION = "4.0.3".freeze
|
||||
VERSION = "4.0.3"
|
||||
end
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
# encoding: utf-8
|
||||
# frozen_string_literal: true
|
||||
|
||||
lib = File.expand_path('../lib/', __FILE__)
|
||||
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
||||
@@ -16,7 +17,7 @@ Gem::Specification.new do |s|
|
||||
s.license = "MIT"
|
||||
# s.description = "A secure, non-evaling end user template engine with aesthetic markup."
|
||||
|
||||
s.required_ruby_version = ">= 2.4.0"
|
||||
s.required_ruby_version = ">= 2.5.0"
|
||||
s.required_rubygems_version = ">= 1.3.7"
|
||||
|
||||
s.test_files = Dir.glob("{test}/**/*")
|
||||
@@ -26,6 +27,6 @@ Gem::Specification.new do |s|
|
||||
|
||||
s.require_path = "lib"
|
||||
|
||||
s.add_development_dependency('rake', '~> 11.3')
|
||||
s.add_development_dependency('rake', '~> 13.0')
|
||||
s.add_development_dependency('minitest')
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'benchmark/ips'
|
||||
require_relative 'theme_runner'
|
||||
|
||||
@@ -5,7 +7,7 @@ Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
|
||||
profiler = ThemeRunner.new
|
||||
|
||||
Benchmark.ips do |x|
|
||||
x.time = 10
|
||||
x.time = 10
|
||||
x.warmup = 5
|
||||
|
||||
puts
|
||||
|
||||
@@ -21,8 +21,8 @@ class Profiler
|
||||
end
|
||||
|
||||
def profile(phase, &block)
|
||||
print LOG_LABEL
|
||||
print "#{phase}.. ".ljust(10)
|
||||
print(LOG_LABEL)
|
||||
print("#{phase}.. ".ljust(10))
|
||||
report = MemoryProfiler.report(&block)
|
||||
puts 'Done.'
|
||||
@headings << phase.capitalize
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'stackprof'
|
||||
require_relative 'theme_runner'
|
||||
|
||||
@@ -13,7 +15,7 @@ profiler.run
|
||||
end
|
||||
end
|
||||
|
||||
if profile_type == :cpu && graph_filename = ENV['GRAPH_FILENAME']
|
||||
if profile_type == :cpu && (graph_filename = ENV['GRAPH_FILENAME'])
|
||||
File.open(graph_filename, 'w') do |f|
|
||||
StackProf::Report.new(results).print_graphviz(nil, f)
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class CommentForm < Liquid::Block
|
||||
Syntax = /(#{Liquid::VariableSignature}+)/
|
||||
|
||||
@@ -6,7 +8,7 @@ class CommentForm < Liquid::Block
|
||||
|
||||
if markup =~ Syntax
|
||||
@variable_name = Regexp.last_match(1)
|
||||
@attributes = {}
|
||||
@attributes = {}
|
||||
else
|
||||
raise SyntaxError, "Syntax Error in 'comment_form' - Valid syntax: comment_form [article]"
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'yaml'
|
||||
|
||||
module Database
|
||||
@@ -30,8 +32,8 @@ module Database
|
||||
db['article'] = db['blog']['articles'].first
|
||||
|
||||
db['cart'] = {
|
||||
'total_price' => db['line_items'].values.inject(0) { |sum, item| sum += item['line_price'] * item['quantity'] },
|
||||
'item_count' => db['line_items'].values.inject(0) { |sum, item| sum += item['quantity'] },
|
||||
'total_price' => db['line_items'].values.inject(0) { |sum, item| sum + item['line_price'] * item['quantity'] },
|
||||
'item_count' => db['line_items'].values.inject(0) { |sum, item| sum + item['quantity'] },
|
||||
'items' => db['line_items'].values,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'json'
|
||||
|
||||
module JsonFilter
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
$LOAD_PATH.unshift(__dir__ + '/../../lib')
|
||||
require_relative '../../lib/liquid'
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module MoneyFilter
|
||||
def money_with_currency(money)
|
||||
return '' if money.nil?
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class Paginate < Liquid::Block
|
||||
Syntax = /(#{Liquid::QuotedFragment})\s*(by\s*(\d+))?/
|
||||
|
||||
@@ -6,7 +8,7 @@ class Paginate < Liquid::Block
|
||||
|
||||
if markup =~ Syntax
|
||||
@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
|
||||
else
|
||||
20
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ShopFilter
|
||||
def asset_url(input)
|
||||
"/files/1/[shop_id]/[shop_id]/assets/#{input}"
|
||||
@@ -29,7 +31,7 @@ module ShopFilter
|
||||
|
||||
def link_to_vendor(vendor)
|
||||
if vendor
|
||||
link_to vendor, url_for_vendor(vendor), vendor
|
||||
link_to(vendor, url_for_vendor(vendor), vendor)
|
||||
else
|
||||
'Unknown Vendor'
|
||||
end
|
||||
@@ -37,7 +39,7 @@ module ShopFilter
|
||||
|
||||
def link_to_type(type)
|
||||
if type
|
||||
link_to type, url_for_type(type), type
|
||||
link_to(type, url_for_type(type), type)
|
||||
else
|
||||
'Unknown Vendor'
|
||||
end
|
||||
@@ -58,7 +60,7 @@ module ShopFilter
|
||||
|
||||
case style
|
||||
when 'original'
|
||||
return '/files/shops/random_number/' + url
|
||||
'/files/shops/random_number/' + url
|
||||
when 'grande', 'large', 'medium', 'compact', 'small', 'thumb', 'icon'
|
||||
"/files/shops/random_number/products/#{Regexp.last_match(1)}_#{style}.#{Regexp.last_match(2)}"
|
||||
else
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module TagFilter
|
||||
def link_to_tag(label, tag)
|
||||
"<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tag}\">#{label}</a>"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WeightFilter
|
||||
def weight(grams)
|
||||
format("%.2f", grams / 1000)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# This profiler run simulates Shopify.
|
||||
# We are looking in the tests directory for liquid files and render them within the designated layout file.
|
||||
# We will also export a substantial database to liquid which the templates can render values of.
|
||||
@@ -56,9 +58,9 @@ class ThemeRunner
|
||||
# `render` is called to benchmark just the render portion of liquid
|
||||
def render
|
||||
@compiled_tests.each do |test|
|
||||
tmpl = test[:tmpl]
|
||||
tmpl = test[:tmpl]
|
||||
assigns = test[:assigns]
|
||||
layout = test[:layout]
|
||||
layout = test[:layout]
|
||||
|
||||
if layout
|
||||
assigns['content_for_layout'] = tmpl.render!(assigns)
|
||||
@@ -72,7 +74,7 @@ class ThemeRunner
|
||||
private
|
||||
|
||||
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)
|
||||
compiled_test[:layout].render!(assigns) if layout
|
||||
end
|
||||
@@ -86,7 +88,7 @@ class ThemeRunner
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
if layout
|
||||
@@ -111,9 +113,9 @@ class ThemeRunner
|
||||
|
||||
# set up a new Liquid::Template object for use in `compile_and_render` and `compile_test`
|
||||
def init_template(page_template, template_file)
|
||||
tmpl = Liquid::Template.new
|
||||
tmpl.assigns['page_title'] = 'Page title'
|
||||
tmpl.assigns['template'] = page_template
|
||||
tmpl = Liquid::Template.new
|
||||
tmpl.assigns['page_title'] = 'Page title'
|
||||
tmpl.assigns['template'] = page_template
|
||||
tmpl.registers[:file_system] = ThemeRunner::FileSystem.new(File.dirname(template_file))
|
||||
tmpl
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class AssignTest < Minitest::Test
|
||||
@@ -8,9 +10,9 @@ class AssignTest < Minitest::Test
|
||||
{% assign this-thing = 'Print this-thing' %}
|
||||
{{ this-thing }}
|
||||
END_TEMPLATE
|
||||
template = Template.parse(template_source)
|
||||
rendered = template.render!
|
||||
assert_equal "Print this-thing", rendered.strip
|
||||
template = Template.parse(template_source)
|
||||
rendered = template.render!
|
||||
assert_equal("Print this-thing", rendered.strip)
|
||||
end
|
||||
|
||||
def test_assigned_variable
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class FoobarTag < Liquid::Tag
|
||||
@@ -94,9 +96,9 @@ class BlankTest < Minitest::Test
|
||||
|
||||
def test_include_is_blank
|
||||
Liquid::Template.file_system = BlankTestFileSystem.new
|
||||
assert_template_result "foobar" * (N + 1), wrap("{% include 'foobar' %}")
|
||||
assert_template_result " foobar " * (N + 1), wrap("{% include ' foobar ' %}")
|
||||
assert_template_result " " * (N + 1), wrap(" {% include ' ' %} ")
|
||||
assert_template_result("foobar" * (N + 1), wrap("{% include 'foobar' %}"))
|
||||
assert_template_result(" foobar " * (N + 1), wrap("{% include ' foobar ' %}"))
|
||||
assert_template_result(" " * (N + 1), wrap(" {% include ' ' %} "))
|
||||
end
|
||||
|
||||
def test_case_is_blank
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class BlockTest < Minitest::Test
|
||||
@@ -7,6 +9,50 @@ class BlockTest < Minitest::Test
|
||||
exc = assert_raises(SyntaxError) do
|
||||
Template.parse("{% if true %}{% endunless %}")
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class CaptureTest < Minitest::Test
|
||||
@@ -12,9 +14,9 @@ class CaptureTest < Minitest::Test
|
||||
{% capture this-thing %}Print this-thing{% endcapture %}
|
||||
{{ this-thing }}
|
||||
END_TEMPLATE
|
||||
template = Template.parse(template_source)
|
||||
rendered = template.render!
|
||||
assert_equal "Print this-thing", rendered.strip
|
||||
template = Template.parse(template_source)
|
||||
rendered = template.render!
|
||||
assert_equal("Print this-thing", rendered.strip)
|
||||
end
|
||||
|
||||
def test_capture_to_variable_from_outer_scope_if_existing
|
||||
@@ -28,9 +30,9 @@ class CaptureTest < Minitest::Test
|
||||
{% endif %}
|
||||
{{var}}
|
||||
END_TEMPLATE
|
||||
template = Template.parse(template_source)
|
||||
rendered = template.render!
|
||||
assert_equal "test-string", rendered.gsub(/\s/, '')
|
||||
template = Template.parse(template_source)
|
||||
rendered = template.render!
|
||||
assert_equal("test-string", rendered.gsub(/\s/, ''))
|
||||
end
|
||||
|
||||
def test_assigning_from_capture
|
||||
@@ -43,8 +45,8 @@ class CaptureTest < Minitest::Test
|
||||
{% endfor %}
|
||||
{{ first }}-{{ second }}
|
||||
END_TEMPLATE
|
||||
template = Template.parse(template_source)
|
||||
rendered = template.render!
|
||||
assert_equal "3-3", rendered.gsub(/\s/, '')
|
||||
template = Template.parse(template_source)
|
||||
rendered = template.render!
|
||||
assert_equal("3-3", rendered.gsub(/\s/, ''))
|
||||
end
|
||||
end # CaptureTest
|
||||
|
||||
608
test/integration/context_test
Normal file
608
test/integration/context_test
Normal file
@@ -0,0 +1,608 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class HundredCentes
|
||||
def to_liquid
|
||||
100
|
||||
end
|
||||
end
|
||||
|
||||
class CentsDrop < Liquid::Drop
|
||||
def amount
|
||||
HundredCentes.new
|
||||
end
|
||||
|
||||
def non_zero?
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
class ContextSensitiveDrop < Liquid::Drop
|
||||
def test
|
||||
@context['test']
|
||||
end
|
||||
end
|
||||
|
||||
class Category < Liquid::Drop
|
||||
attr_accessor :name
|
||||
|
||||
def initialize(name)
|
||||
@name = name
|
||||
end
|
||||
|
||||
def to_liquid
|
||||
CategoryDrop.new(self)
|
||||
end
|
||||
end
|
||||
|
||||
class CategoryDrop
|
||||
attr_accessor :category, :context
|
||||
def initialize(category)
|
||||
@category = category
|
||||
end
|
||||
end
|
||||
|
||||
class CounterDrop < Liquid::Drop
|
||||
def count
|
||||
@count ||= 0
|
||||
@count += 1
|
||||
end
|
||||
end
|
||||
|
||||
class ArrayLike
|
||||
def fetch(index)
|
||||
end
|
||||
|
||||
def [](index)
|
||||
@counts ||= []
|
||||
@counts[index] ||= 0
|
||||
@counts[index] += 1
|
||||
end
|
||||
|
||||
def to_liquid
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
class ContextTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def setup
|
||||
@context = Liquid::Context.new
|
||||
end
|
||||
|
||||
def test_variables
|
||||
@context['string'] = 'string'
|
||||
assert_equal('string', @context['string'])
|
||||
|
||||
@context['num'] = 5
|
||||
assert_equal(5, @context['num'])
|
||||
|
||||
@context['time'] = Time.parse('2006-06-06 12:00:00')
|
||||
assert_equal(Time.parse('2006-06-06 12:00:00'), @context['time'])
|
||||
|
||||
@context['date'] = Date.today
|
||||
assert_equal(Date.today, @context['date'])
|
||||
|
||||
now = Time.now
|
||||
@context['datetime'] = now
|
||||
assert_equal(now, @context['datetime'])
|
||||
|
||||
@context['bool'] = true
|
||||
assert_equal(true, @context['bool'])
|
||||
|
||||
@context['bool'] = false
|
||||
assert_equal(false, @context['bool'])
|
||||
|
||||
@context['nil'] = nil
|
||||
assert_nil(@context['nil'])
|
||||
assert_nil(@context['nil'])
|
||||
end
|
||||
|
||||
def test_variables_not_existing
|
||||
assert_nil(@context['does_not_exist'])
|
||||
end
|
||||
|
||||
def test_scoping
|
||||
@context.push
|
||||
@context.pop
|
||||
|
||||
assert_raises(Liquid::ContextError) do
|
||||
@context.pop
|
||||
end
|
||||
|
||||
assert_raises(Liquid::ContextError) do
|
||||
@context.push
|
||||
@context.pop
|
||||
@context.pop
|
||||
end
|
||||
end
|
||||
|
||||
def test_length_query
|
||||
@context['numbers'] = [1, 2, 3, 4]
|
||||
|
||||
assert_equal(4, @context['numbers.size'])
|
||||
|
||||
@context['numbers'] = { 1 => 1, 2 => 2, 3 => 3, 4 => 4 }
|
||||
|
||||
assert_equal(4, @context['numbers.size'])
|
||||
|
||||
@context['numbers'] = { 1 => 1, 2 => 2, 3 => 3, 4 => 4, 'size' => 1000 }
|
||||
|
||||
assert_equal(1000, @context['numbers.size'])
|
||||
end
|
||||
|
||||
def test_hyphenated_variable
|
||||
@context['oh-my'] = 'godz'
|
||||
assert_equal('godz', @context['oh-my'])
|
||||
end
|
||||
|
||||
def test_add_filter
|
||||
filter = Module.new do
|
||||
def hi(output)
|
||||
output + ' hi!'
|
||||
end
|
||||
end
|
||||
|
||||
context = Context.new
|
||||
context.add_filters(filter)
|
||||
assert_equal('hi? hi!', context.invoke(:hi, 'hi?'))
|
||||
|
||||
context = Context.new
|
||||
assert_equal('hi?', context.invoke(:hi, 'hi?'))
|
||||
|
||||
context.add_filters(filter)
|
||||
assert_equal('hi? hi!', context.invoke(:hi, 'hi?'))
|
||||
end
|
||||
|
||||
def test_only_intended_filters_make_it_there
|
||||
filter = Module.new do
|
||||
def hi(output)
|
||||
output + ' hi!'
|
||||
end
|
||||
end
|
||||
|
||||
context = Context.new
|
||||
assert_equal("Wookie", context.invoke("hi", "Wookie"))
|
||||
|
||||
context.add_filters(filter)
|
||||
assert_equal("Wookie hi!", context.invoke("hi", "Wookie"))
|
||||
end
|
||||
|
||||
def test_add_item_in_outer_scope
|
||||
@context['test'] = 'test'
|
||||
@context.push
|
||||
assert_equal('test', @context['test'])
|
||||
@context.pop
|
||||
assert_equal('test', @context['test'])
|
||||
end
|
||||
|
||||
def test_add_item_in_inner_scope
|
||||
@context.push
|
||||
@context['test'] = 'test'
|
||||
assert_equal('test', @context['test'])
|
||||
@context.pop
|
||||
assert_nil(@context['test'])
|
||||
end
|
||||
|
||||
def test_hierachical_data
|
||||
@context['hash'] = { "name" => 'tobi' }
|
||||
assert_equal('tobi', @context['hash.name'])
|
||||
assert_equal('tobi', @context['hash["name"]'])
|
||||
end
|
||||
|
||||
def test_keywords
|
||||
assert_equal(true, @context['true'])
|
||||
assert_equal(false, @context['false'])
|
||||
end
|
||||
|
||||
def test_digits
|
||||
assert_equal(100, @context['100'])
|
||||
assert_equal(100.00, @context['100.00'])
|
||||
end
|
||||
|
||||
def test_strings
|
||||
assert_equal("hello!", @context['"hello!"'])
|
||||
assert_equal("hello!", @context["'hello!'"])
|
||||
end
|
||||
|
||||
def test_merge
|
||||
@context.merge("test" => "test")
|
||||
assert_equal('test', @context['test'])
|
||||
@context.merge("test" => "newvalue", "foo" => "bar")
|
||||
assert_equal('newvalue', @context['test'])
|
||||
assert_equal('bar', @context['foo'])
|
||||
end
|
||||
|
||||
def test_array_notation
|
||||
@context['test'] = [1, 2, 3, 4, 5]
|
||||
|
||||
assert_equal(1, @context['test[0]'])
|
||||
assert_equal(2, @context['test[1]'])
|
||||
assert_equal(3, @context['test[2]'])
|
||||
assert_equal(4, @context['test[3]'])
|
||||
assert_equal(5, @context['test[4]'])
|
||||
end
|
||||
|
||||
def test_recoursive_array_notation
|
||||
@context['test'] = { 'test' => [1, 2, 3, 4, 5] }
|
||||
|
||||
assert_equal(1, @context['test.test[0]'])
|
||||
|
||||
@context['test'] = [{ 'test' => 'worked' }]
|
||||
|
||||
assert_equal('worked', @context['test[0].test'])
|
||||
end
|
||||
|
||||
def test_hash_to_array_transition
|
||||
@context['colors'] = {
|
||||
'Blue' => ['003366', '336699', '6699CC', '99CCFF'],
|
||||
'Green' => ['003300', '336633', '669966', '99CC99'],
|
||||
'Yellow' => ['CC9900', 'FFCC00', 'FFFF99', 'FFFFCC'],
|
||||
'Red' => ['660000', '993333', 'CC6666', 'FF9999'],
|
||||
}
|
||||
|
||||
assert_equal('003366', @context['colors.Blue[0]'])
|
||||
assert_equal('FF9999', @context['colors.Red[3]'])
|
||||
end
|
||||
|
||||
def test_try_first
|
||||
@context['test'] = [1, 2, 3, 4, 5]
|
||||
|
||||
assert_equal(1, @context['test.first'])
|
||||
assert_equal(5, @context['test.last'])
|
||||
|
||||
@context['test'] = { 'test' => [1, 2, 3, 4, 5] }
|
||||
|
||||
assert_equal(1, @context['test.test.first'])
|
||||
assert_equal(5, @context['test.test.last'])
|
||||
|
||||
@context['test'] = [1]
|
||||
assert_equal(1, @context['test.first'])
|
||||
assert_equal(1, @context['test.last'])
|
||||
end
|
||||
|
||||
def test_access_hashes_with_hash_notation
|
||||
@context['products'] = { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] }
|
||||
@context['product'] = { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] }
|
||||
|
||||
assert_equal(5, @context['products["count"]'])
|
||||
assert_equal('deepsnow', @context['products["tags"][0]'])
|
||||
assert_equal('deepsnow', @context['products["tags"].first'])
|
||||
assert_equal('draft151cm', @context['product["variants"][0]["title"]'])
|
||||
assert_equal('element151cm', @context['product["variants"][1]["title"]'])
|
||||
assert_equal('draft151cm', @context['product["variants"][0]["title"]'])
|
||||
assert_equal('element151cm', @context['product["variants"].last["title"]'])
|
||||
end
|
||||
|
||||
def test_access_variable_with_hash_notation
|
||||
@context['foo'] = 'baz'
|
||||
@context['bar'] = 'foo'
|
||||
|
||||
assert_equal('baz', @context['["foo"]'])
|
||||
assert_equal('baz', @context['[bar]'])
|
||||
end
|
||||
|
||||
def test_access_hashes_with_hash_access_variables
|
||||
@context['var'] = 'tags'
|
||||
@context['nested'] = { 'var' => 'tags' }
|
||||
@context['products'] = { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] }
|
||||
|
||||
assert_equal('deepsnow', @context['products[var].first'])
|
||||
assert_equal('freestyle', @context['products[nested.var].last'])
|
||||
end
|
||||
|
||||
def test_hash_notation_only_for_hash_access
|
||||
@context['array'] = [1, 2, 3, 4, 5]
|
||||
@context['hash'] = { 'first' => 'Hello' }
|
||||
|
||||
assert_equal(1, @context['array.first'])
|
||||
assert_nil(@context['array["first"]'])
|
||||
assert_equal('Hello', @context['hash["first"]'])
|
||||
end
|
||||
|
||||
def test_first_can_appear_in_middle_of_callchain
|
||||
@context['product'] = { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] }
|
||||
|
||||
assert_equal('draft151cm', @context['product.variants[0].title'])
|
||||
assert_equal('element151cm', @context['product.variants[1].title'])
|
||||
assert_equal('draft151cm', @context['product.variants.first.title'])
|
||||
assert_equal('element151cm', @context['product.variants.last.title'])
|
||||
end
|
||||
|
||||
def test_cents
|
||||
@context.merge("cents" => HundredCentes.new)
|
||||
assert_equal(100, @context['cents'])
|
||||
end
|
||||
|
||||
def test_nested_cents
|
||||
@context.merge("cents" => { 'amount' => HundredCentes.new })
|
||||
assert_equal(100, @context['cents.amount'])
|
||||
|
||||
@context.merge("cents" => { 'cents' => { 'amount' => HundredCentes.new } })
|
||||
assert_equal(100, @context['cents.cents.amount'])
|
||||
end
|
||||
|
||||
def test_cents_through_drop
|
||||
@context.merge("cents" => CentsDrop.new)
|
||||
assert_equal(100, @context['cents.amount'])
|
||||
end
|
||||
|
||||
def test_nested_cents_through_drop
|
||||
@context.merge("vars" => { "cents" => CentsDrop.new })
|
||||
assert_equal(100, @context['vars.cents.amount'])
|
||||
end
|
||||
|
||||
def test_drop_methods_with_question_marks
|
||||
@context.merge("cents" => CentsDrop.new)
|
||||
assert(@context['cents.non_zero?'])
|
||||
end
|
||||
|
||||
def test_context_from_within_drop
|
||||
@context.merge("test" => '123', "vars" => ContextSensitiveDrop.new)
|
||||
assert_equal('123', @context['vars.test'])
|
||||
end
|
||||
|
||||
def test_nested_context_from_within_drop
|
||||
@context.merge("test" => '123', "vars" => { "local" => ContextSensitiveDrop.new })
|
||||
assert_equal('123', @context['vars.local.test'])
|
||||
end
|
||||
|
||||
def test_ranges
|
||||
@context.merge("test" => '5')
|
||||
assert_equal((1..5), @context['(1..5)'])
|
||||
assert_equal((1..5), @context['(1..test)'])
|
||||
assert_equal((5..5), @context['(test..test)'])
|
||||
end
|
||||
|
||||
def test_cents_through_drop_nestedly
|
||||
@context.merge("cents" => { "cents" => CentsDrop.new })
|
||||
assert_equal(100, @context['cents.cents.amount'])
|
||||
|
||||
@context.merge("cents" => { "cents" => { "cents" => CentsDrop.new } })
|
||||
assert_equal(100, @context['cents.cents.cents.amount'])
|
||||
end
|
||||
|
||||
def test_drop_with_variable_called_only_once
|
||||
@context['counter'] = CounterDrop.new
|
||||
|
||||
assert_equal(1, @context['counter.count'])
|
||||
assert_equal(2, @context['counter.count'])
|
||||
assert_equal(3, @context['counter.count'])
|
||||
end
|
||||
|
||||
def test_drop_with_key_called_only_once
|
||||
@context['counter'] = CounterDrop.new
|
||||
|
||||
assert_equal(1, @context['counter["count"]'])
|
||||
assert_equal(2, @context['counter["count"]'])
|
||||
assert_equal(3, @context['counter["count"]'])
|
||||
end
|
||||
|
||||
def test_proc_as_variable
|
||||
@context['dynamic'] = proc { 'Hello' }
|
||||
|
||||
assert_equal('Hello', @context['dynamic'])
|
||||
end
|
||||
|
||||
def test_lambda_as_variable
|
||||
@context['dynamic'] = proc { 'Hello' }
|
||||
|
||||
assert_equal('Hello', @context['dynamic'])
|
||||
end
|
||||
|
||||
def test_nested_lambda_as_variable
|
||||
@context['dynamic'] = { "lambda" => proc { 'Hello' } }
|
||||
|
||||
assert_equal('Hello', @context['dynamic.lambda'])
|
||||
end
|
||||
|
||||
def test_array_containing_lambda_as_variable
|
||||
@context['dynamic'] = [1, 2, proc { 'Hello' }, 4, 5]
|
||||
|
||||
assert_equal('Hello', @context['dynamic[2]'])
|
||||
end
|
||||
|
||||
def test_lambda_is_called_once
|
||||
@context['callcount'] = proc {
|
||||
@global ||= 0
|
||||
@global += 1
|
||||
@global.to_s
|
||||
}
|
||||
|
||||
assert_equal('1', @context['callcount'])
|
||||
assert_equal('1', @context['callcount'])
|
||||
assert_equal('1', @context['callcount'])
|
||||
|
||||
@global = nil
|
||||
end
|
||||
|
||||
def test_nested_lambda_is_called_once
|
||||
@context['callcount'] = { "lambda" => proc {
|
||||
@global ||= 0
|
||||
@global += 1
|
||||
@global.to_s
|
||||
} }
|
||||
|
||||
assert_equal('1', @context['callcount.lambda'])
|
||||
assert_equal('1', @context['callcount.lambda'])
|
||||
assert_equal('1', @context['callcount.lambda'])
|
||||
|
||||
@global = nil
|
||||
end
|
||||
|
||||
def test_lambda_in_array_is_called_once
|
||||
@context['callcount'] = [1, 2, proc {
|
||||
@global ||= 0
|
||||
@global += 1
|
||||
@global.to_s
|
||||
}, 4, 5]
|
||||
|
||||
assert_equal('1', @context['callcount[2]'])
|
||||
assert_equal('1', @context['callcount[2]'])
|
||||
assert_equal('1', @context['callcount[2]'])
|
||||
|
||||
@global = nil
|
||||
end
|
||||
|
||||
def test_access_to_context_from_proc
|
||||
@context.registers[:magic] = 345392
|
||||
|
||||
@context['magic'] = proc { @context.registers[:magic] }
|
||||
|
||||
assert_equal(345392, @context['magic'])
|
||||
end
|
||||
|
||||
def test_to_liquid_and_context_at_first_level
|
||||
@context['category'] = Category.new("foobar")
|
||||
assert_kind_of(CategoryDrop, @context['category'])
|
||||
assert_equal(@context, @context['category'].context)
|
||||
end
|
||||
|
||||
def test_interrupt_avoids_object_allocations
|
||||
assert_no_object_allocations do
|
||||
@context.interrupt?
|
||||
end
|
||||
end
|
||||
|
||||
def test_context_initialization_with_a_proc_in_environment
|
||||
contx = Context.new([test: ->(c) { c['poutine'] }], test: :foo)
|
||||
|
||||
assert(contx)
|
||||
assert_nil(contx['poutine'])
|
||||
end
|
||||
|
||||
def test_apply_global_filter
|
||||
global_filter_proc = ->(output) { "#{output} filtered" }
|
||||
|
||||
context = Context.new
|
||||
context.global_filter = global_filter_proc
|
||||
|
||||
assert_equal('hi filtered', context.apply_global_filter('hi'))
|
||||
end
|
||||
|
||||
def test_static_environments_are_read_with_lower_priority_than_environments
|
||||
context = Context.build(
|
||||
static_environments: { 'shadowed' => 'static', 'unshadowed' => 'static' },
|
||||
environments: { 'shadowed' => 'dynamic' }
|
||||
)
|
||||
|
||||
assert_equal('dynamic', context['shadowed'])
|
||||
assert_equal('static', context['unshadowed'])
|
||||
end
|
||||
|
||||
def test_apply_global_filter_when_no_global_filter_exist
|
||||
context = Context.new
|
||||
assert_equal('hi', context.apply_global_filter('hi'))
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_does_not_inherit_variables
|
||||
super_context = Context.new
|
||||
super_context['my_variable'] = 'some value'
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
|
||||
assert_nil(subcontext['my_variable'])
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_inherits_static_environment
|
||||
super_context = Context.build(static_environments: { 'my_environment_value' => 'my value' })
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
|
||||
assert_equal('my value', subcontext['my_environment_value'])
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_inherits_resource_limits
|
||||
resource_limits = ResourceLimits.new({})
|
||||
super_context = Context.new({}, {}, {}, false, resource_limits)
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
assert_equal(resource_limits, subcontext.resource_limits)
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_inherits_exception_renderer
|
||||
super_context = Context.new
|
||||
super_context.exception_renderer = ->(_e) { 'my exception message' }
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
assert_equal('my exception message', subcontext.handle_error(Liquid::Error.new))
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_does_not_inherit_non_static_registers
|
||||
registers = {
|
||||
my_register: :my_value,
|
||||
}
|
||||
super_context = Context.new({}, {}, StaticRegisters.new(registers))
|
||||
super_context.registers[:my_register] = :my_alt_value
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
assert_equal(:my_value, subcontext.registers[:my_register])
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_inherits_static_registers
|
||||
super_context = Context.build(registers: { my_register: :my_value })
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
assert_equal(:my_value, subcontext.registers[:my_register])
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_registers_do_not_pollute_context
|
||||
super_context = Context.build(registers: { my_register: :my_value })
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
subcontext.registers[:my_register] = :my_alt_value
|
||||
assert_equal(:my_value, super_context.registers[:my_register])
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_inherits_filters
|
||||
my_filter = Module.new do
|
||||
def my_filter(*)
|
||||
'my filter result'
|
||||
end
|
||||
end
|
||||
|
||||
super_context = Context.new
|
||||
super_context.add_filters([my_filter])
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
template = Template.parse('{{ 123 | my_filter }}')
|
||||
assert_equal('my filter result', template.render(subcontext))
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def assert_no_object_allocations
|
||||
unless RUBY_ENGINE == 'ruby'
|
||||
skip("stackprof needed to count object allocations")
|
||||
end
|
||||
require 'stackprof'
|
||||
|
||||
profile = StackProf.run(mode: :object) do
|
||||
yield
|
||||
end
|
||||
assert_equal(0, profile[:samples])
|
||||
end
|
||||
end # ContextTest
|
||||
@@ -1,8 +1,597 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class HundredCentes
|
||||
def to_liquid
|
||||
100
|
||||
end
|
||||
end
|
||||
|
||||
class CentsDrop < Liquid::Drop
|
||||
def amount
|
||||
HundredCentes.new
|
||||
end
|
||||
|
||||
def non_zero?
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
class ContextSensitiveDrop < Liquid::Drop
|
||||
def test
|
||||
@context['test']
|
||||
end
|
||||
end
|
||||
|
||||
class Category < Liquid::Drop
|
||||
attr_accessor :name
|
||||
|
||||
def initialize(name)
|
||||
@name = name
|
||||
end
|
||||
|
||||
def to_liquid
|
||||
CategoryDrop.new(self)
|
||||
end
|
||||
end
|
||||
|
||||
class CategoryDrop
|
||||
attr_accessor :category, :context
|
||||
def initialize(category)
|
||||
@category = category
|
||||
end
|
||||
end
|
||||
|
||||
class CounterDrop < Liquid::Drop
|
||||
def count
|
||||
@count ||= 0
|
||||
@count += 1
|
||||
end
|
||||
end
|
||||
|
||||
class ArrayLike
|
||||
def fetch(index)
|
||||
end
|
||||
|
||||
def [](index)
|
||||
@counts ||= []
|
||||
@counts[index] ||= 0
|
||||
@counts[index] += 1
|
||||
end
|
||||
|
||||
def to_liquid
|
||||
self
|
||||
end
|
||||
end
|
||||
|
||||
class ContextTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def setup
|
||||
@context = Liquid::Context.new
|
||||
end
|
||||
|
||||
def test_variables
|
||||
@context['string'] = 'string'
|
||||
assert_equal('string', @context['string'])
|
||||
|
||||
@context['num'] = 5
|
||||
assert_equal(5, @context['num'])
|
||||
|
||||
@context['time'] = Time.parse('2006-06-06 12:00:00')
|
||||
assert_equal(Time.parse('2006-06-06 12:00:00'), @context['time'])
|
||||
|
||||
@context['date'] = Date.today
|
||||
assert_equal(Date.today, @context['date'])
|
||||
|
||||
now = Time.now
|
||||
@context['datetime'] = now
|
||||
assert_equal(now, @context['datetime'])
|
||||
|
||||
@context['bool'] = true
|
||||
assert_equal(true, @context['bool'])
|
||||
|
||||
@context['bool'] = false
|
||||
assert_equal(false, @context['bool'])
|
||||
|
||||
@context['nil'] = nil
|
||||
assert_nil(@context['nil'])
|
||||
assert_nil(@context['nil'])
|
||||
end
|
||||
|
||||
def test_variables_not_existing
|
||||
assert_nil(@context['does_not_exist'])
|
||||
end
|
||||
|
||||
def test_scoping
|
||||
@context.push
|
||||
@context.pop
|
||||
|
||||
assert_raises(Liquid::ContextError) do
|
||||
@context.pop
|
||||
end
|
||||
|
||||
assert_raises(Liquid::ContextError) do
|
||||
@context.push
|
||||
@context.pop
|
||||
@context.pop
|
||||
end
|
||||
end
|
||||
|
||||
def test_length_query
|
||||
@context['numbers'] = [1, 2, 3, 4]
|
||||
|
||||
assert_equal(4, @context['numbers.size'])
|
||||
|
||||
@context['numbers'] = { 1 => 1, 2 => 2, 3 => 3, 4 => 4 }
|
||||
|
||||
assert_equal(4, @context['numbers.size'])
|
||||
|
||||
@context['numbers'] = { 1 => 1, 2 => 2, 3 => 3, 4 => 4, 'size' => 1000 }
|
||||
|
||||
assert_equal(1000, @context['numbers.size'])
|
||||
end
|
||||
|
||||
def test_hyphenated_variable
|
||||
@context['oh-my'] = 'godz'
|
||||
assert_equal('godz', @context['oh-my'])
|
||||
end
|
||||
|
||||
def test_add_filter
|
||||
filter = Module.new do
|
||||
def hi(output)
|
||||
output + ' hi!'
|
||||
end
|
||||
end
|
||||
|
||||
context = Context.new
|
||||
context.add_filters(filter)
|
||||
assert_equal('hi? hi!', context.invoke(:hi, 'hi?'))
|
||||
|
||||
context = Context.new
|
||||
assert_equal('hi?', context.invoke(:hi, 'hi?'))
|
||||
|
||||
context.add_filters(filter)
|
||||
assert_equal('hi? hi!', context.invoke(:hi, 'hi?'))
|
||||
end
|
||||
|
||||
def test_only_intended_filters_make_it_there
|
||||
filter = Module.new do
|
||||
def hi(output)
|
||||
output + ' hi!'
|
||||
end
|
||||
end
|
||||
|
||||
context = Context.new
|
||||
assert_equal("Wookie", context.invoke("hi", "Wookie"))
|
||||
|
||||
context.add_filters(filter)
|
||||
assert_equal("Wookie hi!", context.invoke("hi", "Wookie"))
|
||||
end
|
||||
|
||||
def test_add_item_in_outer_scope
|
||||
@context['test'] = 'test'
|
||||
@context.push
|
||||
assert_equal('test', @context['test'])
|
||||
@context.pop
|
||||
assert_equal('test', @context['test'])
|
||||
end
|
||||
|
||||
def test_add_item_in_inner_scope
|
||||
@context.push
|
||||
@context['test'] = 'test'
|
||||
assert_equal('test', @context['test'])
|
||||
@context.pop
|
||||
assert_nil(@context['test'])
|
||||
end
|
||||
|
||||
def test_hierachical_data
|
||||
@context['hash'] = { "name" => 'tobi' }
|
||||
assert_equal('tobi', @context['hash.name'])
|
||||
assert_equal('tobi', @context['hash["name"]'])
|
||||
end
|
||||
|
||||
def test_keywords
|
||||
assert_equal(true, @context['true'])
|
||||
assert_equal(false, @context['false'])
|
||||
end
|
||||
|
||||
def test_digits
|
||||
assert_equal(100, @context['100'])
|
||||
assert_equal(100.00, @context['100.00'])
|
||||
end
|
||||
|
||||
def test_strings
|
||||
assert_equal("hello!", @context['"hello!"'])
|
||||
assert_equal("hello!", @context["'hello!'"])
|
||||
end
|
||||
|
||||
def test_merge
|
||||
@context.merge("test" => "test")
|
||||
assert_equal('test', @context['test'])
|
||||
@context.merge("test" => "newvalue", "foo" => "bar")
|
||||
assert_equal('newvalue', @context['test'])
|
||||
assert_equal('bar', @context['foo'])
|
||||
end
|
||||
|
||||
def test_array_notation
|
||||
@context['test'] = [1, 2, 3, 4, 5]
|
||||
|
||||
assert_equal(1, @context['test[0]'])
|
||||
assert_equal(2, @context['test[1]'])
|
||||
assert_equal(3, @context['test[2]'])
|
||||
assert_equal(4, @context['test[3]'])
|
||||
assert_equal(5, @context['test[4]'])
|
||||
end
|
||||
|
||||
def test_recoursive_array_notation
|
||||
@context['test'] = { 'test' => [1, 2, 3, 4, 5] }
|
||||
|
||||
assert_equal(1, @context['test.test[0]'])
|
||||
|
||||
@context['test'] = [{ 'test' => 'worked' }]
|
||||
|
||||
assert_equal('worked', @context['test[0].test'])
|
||||
end
|
||||
|
||||
def test_hash_to_array_transition
|
||||
@context['colors'] = {
|
||||
'Blue' => ['003366', '336699', '6699CC', '99CCFF'],
|
||||
'Green' => ['003300', '336633', '669966', '99CC99'],
|
||||
'Yellow' => ['CC9900', 'FFCC00', 'FFFF99', 'FFFFCC'],
|
||||
'Red' => ['660000', '993333', 'CC6666', 'FF9999'],
|
||||
}
|
||||
|
||||
assert_equal('003366', @context['colors.Blue[0]'])
|
||||
assert_equal('FF9999', @context['colors.Red[3]'])
|
||||
end
|
||||
|
||||
def test_try_first
|
||||
@context['test'] = [1, 2, 3, 4, 5]
|
||||
|
||||
assert_equal(1, @context['test.first'])
|
||||
assert_equal(5, @context['test.last'])
|
||||
|
||||
@context['test'] = { 'test' => [1, 2, 3, 4, 5] }
|
||||
|
||||
assert_equal(1, @context['test.test.first'])
|
||||
assert_equal(5, @context['test.test.last'])
|
||||
|
||||
@context['test'] = [1]
|
||||
assert_equal(1, @context['test.first'])
|
||||
assert_equal(1, @context['test.last'])
|
||||
end
|
||||
|
||||
def test_access_hashes_with_hash_notation
|
||||
@context['products'] = { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] }
|
||||
@context['product'] = { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] }
|
||||
|
||||
assert_equal(5, @context['products["count"]'])
|
||||
assert_equal('deepsnow', @context['products["tags"][0]'])
|
||||
assert_equal('deepsnow', @context['products["tags"].first'])
|
||||
assert_equal('draft151cm', @context['product["variants"][0]["title"]'])
|
||||
assert_equal('element151cm', @context['product["variants"][1]["title"]'])
|
||||
assert_equal('draft151cm', @context['product["variants"][0]["title"]'])
|
||||
assert_equal('element151cm', @context['product["variants"].last["title"]'])
|
||||
end
|
||||
|
||||
def test_access_variable_with_hash_notation
|
||||
@context['foo'] = 'baz'
|
||||
@context['bar'] = 'foo'
|
||||
|
||||
assert_equal('baz', @context['["foo"]'])
|
||||
assert_equal('baz', @context['[bar]'])
|
||||
end
|
||||
|
||||
def test_access_hashes_with_hash_access_variables
|
||||
@context['var'] = 'tags'
|
||||
@context['nested'] = { 'var' => 'tags' }
|
||||
@context['products'] = { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] }
|
||||
|
||||
assert_equal('deepsnow', @context['products[var].first'])
|
||||
assert_equal('freestyle', @context['products[nested.var].last'])
|
||||
end
|
||||
|
||||
def test_hash_notation_only_for_hash_access
|
||||
@context['array'] = [1, 2, 3, 4, 5]
|
||||
@context['hash'] = { 'first' => 'Hello' }
|
||||
|
||||
assert_equal(1, @context['array.first'])
|
||||
assert_nil(@context['array["first"]'])
|
||||
assert_equal('Hello', @context['hash["first"]'])
|
||||
end
|
||||
|
||||
def test_first_can_appear_in_middle_of_callchain
|
||||
@context['product'] = { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] }
|
||||
|
||||
assert_equal('draft151cm', @context['product.variants[0].title'])
|
||||
assert_equal('element151cm', @context['product.variants[1].title'])
|
||||
assert_equal('draft151cm', @context['product.variants.first.title'])
|
||||
assert_equal('element151cm', @context['product.variants.last.title'])
|
||||
end
|
||||
|
||||
def test_cents
|
||||
@context.merge("cents" => HundredCentes.new)
|
||||
assert_equal(100, @context['cents'])
|
||||
end
|
||||
|
||||
def test_nested_cents
|
||||
@context.merge("cents" => { 'amount' => HundredCentes.new })
|
||||
assert_equal(100, @context['cents.amount'])
|
||||
|
||||
@context.merge("cents" => { 'cents' => { 'amount' => HundredCentes.new } })
|
||||
assert_equal(100, @context['cents.cents.amount'])
|
||||
end
|
||||
|
||||
def test_cents_through_drop
|
||||
@context.merge("cents" => CentsDrop.new)
|
||||
assert_equal(100, @context['cents.amount'])
|
||||
end
|
||||
|
||||
def test_nested_cents_through_drop
|
||||
@context.merge("vars" => { "cents" => CentsDrop.new })
|
||||
assert_equal(100, @context['vars.cents.amount'])
|
||||
end
|
||||
|
||||
def test_drop_methods_with_question_marks
|
||||
@context.merge("cents" => CentsDrop.new)
|
||||
assert(@context['cents.non_zero?'])
|
||||
end
|
||||
|
||||
def test_context_from_within_drop
|
||||
@context.merge("test" => '123', "vars" => ContextSensitiveDrop.new)
|
||||
assert_equal('123', @context['vars.test'])
|
||||
end
|
||||
|
||||
def test_nested_context_from_within_drop
|
||||
@context.merge("test" => '123', "vars" => { "local" => ContextSensitiveDrop.new })
|
||||
assert_equal('123', @context['vars.local.test'])
|
||||
end
|
||||
|
||||
def test_ranges
|
||||
@context.merge("test" => '5')
|
||||
assert_equal((1..5), @context['(1..5)'])
|
||||
assert_equal((1..5), @context['(1..test)'])
|
||||
assert_equal((5..5), @context['(test..test)'])
|
||||
end
|
||||
|
||||
def test_cents_through_drop_nestedly
|
||||
@context.merge("cents" => { "cents" => CentsDrop.new })
|
||||
assert_equal(100, @context['cents.cents.amount'])
|
||||
|
||||
@context.merge("cents" => { "cents" => { "cents" => CentsDrop.new } })
|
||||
assert_equal(100, @context['cents.cents.cents.amount'])
|
||||
end
|
||||
|
||||
def test_drop_with_variable_called_only_once
|
||||
@context['counter'] = CounterDrop.new
|
||||
|
||||
assert_equal(1, @context['counter.count'])
|
||||
assert_equal(2, @context['counter.count'])
|
||||
assert_equal(3, @context['counter.count'])
|
||||
end
|
||||
|
||||
def test_drop_with_key_called_only_once
|
||||
@context['counter'] = CounterDrop.new
|
||||
|
||||
assert_equal(1, @context['counter["count"]'])
|
||||
assert_equal(2, @context['counter["count"]'])
|
||||
assert_equal(3, @context['counter["count"]'])
|
||||
end
|
||||
|
||||
def test_proc_as_variable
|
||||
@context['dynamic'] = proc { 'Hello' }
|
||||
|
||||
assert_equal('Hello', @context['dynamic'])
|
||||
end
|
||||
|
||||
def test_lambda_as_variable
|
||||
@context['dynamic'] = proc { 'Hello' }
|
||||
|
||||
assert_equal('Hello', @context['dynamic'])
|
||||
end
|
||||
|
||||
def test_nested_lambda_as_variable
|
||||
@context['dynamic'] = { "lambda" => proc { 'Hello' } }
|
||||
|
||||
assert_equal('Hello', @context['dynamic.lambda'])
|
||||
end
|
||||
|
||||
def test_array_containing_lambda_as_variable
|
||||
@context['dynamic'] = [1, 2, proc { 'Hello' }, 4, 5]
|
||||
|
||||
assert_equal('Hello', @context['dynamic[2]'])
|
||||
end
|
||||
|
||||
def test_lambda_is_called_once
|
||||
@context['callcount'] = proc {
|
||||
@global ||= 0
|
||||
@global += 1
|
||||
@global.to_s
|
||||
}
|
||||
|
||||
assert_equal('1', @context['callcount'])
|
||||
assert_equal('1', @context['callcount'])
|
||||
assert_equal('1', @context['callcount'])
|
||||
|
||||
@global = nil
|
||||
end
|
||||
|
||||
def test_nested_lambda_is_called_once
|
||||
@context['callcount'] = { "lambda" => proc {
|
||||
@global ||= 0
|
||||
@global += 1
|
||||
@global.to_s
|
||||
} }
|
||||
|
||||
assert_equal('1', @context['callcount.lambda'])
|
||||
assert_equal('1', @context['callcount.lambda'])
|
||||
assert_equal('1', @context['callcount.lambda'])
|
||||
|
||||
@global = nil
|
||||
end
|
||||
|
||||
def test_lambda_in_array_is_called_once
|
||||
@context['callcount'] = [1, 2, proc {
|
||||
@global ||= 0
|
||||
@global += 1
|
||||
@global.to_s
|
||||
}, 4, 5]
|
||||
|
||||
assert_equal('1', @context['callcount[2]'])
|
||||
assert_equal('1', @context['callcount[2]'])
|
||||
assert_equal('1', @context['callcount[2]'])
|
||||
|
||||
@global = nil
|
||||
end
|
||||
|
||||
def test_access_to_context_from_proc
|
||||
@context.registers[:magic] = 345392
|
||||
|
||||
@context['magic'] = proc { @context.registers[:magic] }
|
||||
|
||||
assert_equal(345392, @context['magic'])
|
||||
end
|
||||
|
||||
def test_to_liquid_and_context_at_first_level
|
||||
@context['category'] = Category.new("foobar")
|
||||
assert_kind_of(CategoryDrop, @context['category'])
|
||||
assert_equal(@context, @context['category'].context)
|
||||
end
|
||||
|
||||
def test_interrupt_avoids_object_allocations
|
||||
assert_no_object_allocations do
|
||||
@context.interrupt?
|
||||
end
|
||||
end
|
||||
|
||||
def test_context_initialization_with_a_proc_in_environment
|
||||
contx = Context.new([test: ->(c) { c['poutine'] }], test: :foo)
|
||||
|
||||
assert(contx)
|
||||
assert_nil(contx['poutine'])
|
||||
end
|
||||
|
||||
def test_apply_global_filter
|
||||
global_filter_proc = ->(output) { "#{output} filtered" }
|
||||
|
||||
context = Context.new
|
||||
context.global_filter = global_filter_proc
|
||||
|
||||
assert_equal('hi filtered', context.apply_global_filter('hi'))
|
||||
end
|
||||
|
||||
def test_static_environments_are_read_with_lower_priority_than_environments
|
||||
context = Context.build(
|
||||
static_environments: { 'shadowed' => 'static', 'unshadowed' => 'static' },
|
||||
environments: { 'shadowed' => 'dynamic' }
|
||||
)
|
||||
|
||||
assert_equal('dynamic', context['shadowed'])
|
||||
assert_equal('static', context['unshadowed'])
|
||||
end
|
||||
|
||||
def test_apply_global_filter_when_no_global_filter_exist
|
||||
context = Context.new
|
||||
assert_equal('hi', context.apply_global_filter('hi'))
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_does_not_inherit_variables
|
||||
super_context = Context.new
|
||||
super_context['my_variable'] = 'some value'
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
|
||||
assert_nil(subcontext['my_variable'])
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_inherits_static_environment
|
||||
super_context = Context.build(static_environments: { 'my_environment_value' => 'my value' })
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
|
||||
assert_equal('my value', subcontext['my_environment_value'])
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_inherits_resource_limits
|
||||
resource_limits = ResourceLimits.new({})
|
||||
super_context = Context.new({}, {}, {}, false, resource_limits)
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
assert_equal(resource_limits, subcontext.resource_limits)
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_inherits_exception_renderer
|
||||
super_context = Context.new
|
||||
super_context.exception_renderer = ->(_e) { 'my exception message' }
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
assert_equal('my exception message', subcontext.handle_error(Liquid::Error.new))
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_does_not_inherit_non_static_registers
|
||||
registers = {
|
||||
my_register: :my_value,
|
||||
}
|
||||
super_context = Context.new({}, {}, StaticRegisters.new(registers))
|
||||
super_context.registers[:my_register] = :my_alt_value
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
assert_equal(:my_value, subcontext.registers[:my_register])
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_inherits_static_registers
|
||||
super_context = Context.build(registers: { my_register: :my_value })
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
assert_equal(:my_value, subcontext.registers[:my_register])
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_registers_do_not_pollute_context
|
||||
super_context = Context.build(registers: { my_register: :my_value })
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
subcontext.registers[:my_register] = :my_alt_value
|
||||
assert_equal(:my_value, super_context.registers[:my_register])
|
||||
end
|
||||
|
||||
def test_new_isolated_subcontext_inherits_filters
|
||||
my_filter = Module.new do
|
||||
def my_filter(*)
|
||||
'my filter result'
|
||||
end
|
||||
end
|
||||
|
||||
super_context = Context.new
|
||||
super_context.add_filters([my_filter])
|
||||
subcontext = super_context.new_isolated_subcontext
|
||||
template = Template.parse('{{ 123 | my_filter }}')
|
||||
assert_equal('my filter result', template.render(subcontext))
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def test_override_global_filter
|
||||
global = Module.new do
|
||||
def notice(output)
|
||||
@@ -29,4 +618,18 @@ class ContextTest < Minitest::Test
|
||||
assert_empty context.errors
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def assert_no_object_allocations
|
||||
unless RUBY_ENGINE == 'ruby'
|
||||
skip("stackprof needed to count object allocations")
|
||||
end
|
||||
require 'stackprof'
|
||||
|
||||
profile = StackProf.run(mode: :object) do
|
||||
yield
|
||||
end
|
||||
assert_equal(0, profile[:samples])
|
||||
end
|
||||
end # ContextTest
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class DocumentTest < Minitest::Test
|
||||
@@ -7,13 +9,13 @@ class DocumentTest < Minitest::Test
|
||||
exc = assert_raises(SyntaxError) do
|
||||
Template.parse("{% else %}")
|
||||
end
|
||||
assert_equal exc.message, "Liquid syntax error: Unexpected outer 'else' tag"
|
||||
assert_equal(exc.message, "Liquid syntax error: Unexpected outer 'else' tag")
|
||||
end
|
||||
|
||||
def test_unknown_tag
|
||||
exc = assert_raises(SyntaxError) do
|
||||
Template.parse("{% foo %}")
|
||||
end
|
||||
assert_equal exc.message, "Liquid syntax error: Unknown tag 'foo'"
|
||||
assert_equal(exc.message, "Liquid syntax error: Unknown tag 'foo'")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class ContextDrop < Liquid::Drop
|
||||
@@ -31,7 +33,7 @@ class ProductDrop < Liquid::Drop
|
||||
|
||||
class CatchallDrop < Liquid::Drop
|
||||
def liquid_method_missing(method)
|
||||
'catchall_method: ' << method.to_s
|
||||
"catchall_method: #{method}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -47,10 +49,6 @@ class ProductDrop < Liquid::Drop
|
||||
ContextDrop.new
|
||||
end
|
||||
|
||||
def user_input
|
||||
"foo".taint
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def callmenot
|
||||
@@ -109,82 +107,61 @@ class DropsTest < Minitest::Test
|
||||
|
||||
def test_product_drop
|
||||
tpl = Liquid::Template.parse(' ')
|
||||
assert_equal ' ', tpl.render!('product' => ProductDrop.new)
|
||||
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
|
||||
assert_equal(' ', tpl.render!('product' => ProductDrop.new))
|
||||
end
|
||||
|
||||
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.pretty_inspect }}").render!('product' => ProductDrop.new)
|
||||
assert_equal "", Liquid::Template.parse("{{ product.whatever }}").render!('product' => ProductDrop.new)
|
||||
assert_equal "", Liquid::Template.parse('{{ product | map: "inspect" }}').render!('product' => ProductDrop.new)
|
||||
assert_equal "", Liquid::Template.parse('{{ product | map: "pretty_inspect" }}').render!('product' => ProductDrop.new)
|
||||
assert_equal "", Liquid::Template.parse('{{ product | map: "whatever" }}').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.whatever }}").render!('product' => ProductDrop.new))
|
||||
assert_equal("", Liquid::Template.parse('{{ product | map: "inspect" }}').render!('product' => ProductDrop.new))
|
||||
assert_equal("", Liquid::Template.parse('{{ product | map: "pretty_inspect" }}').render!('product' => ProductDrop.new))
|
||||
assert_equal("", Liquid::Template.parse('{{ product | map: "whatever" }}').render!('product' => ProductDrop.new))
|
||||
end
|
||||
|
||||
def test_drops_respond_to_to_liquid
|
||||
assert_equal "text1", Liquid::Template.parse("{{ product.to_liquid.texts.text }}").render!('product' => ProductDrop.new)
|
||||
assert_equal "text1", Liquid::Template.parse('{{ product | map: "to_liquid" | map: "texts" | map: "text" }}').render!('product' => ProductDrop.new)
|
||||
assert_equal("text1", Liquid::Template.parse("{{ product.to_liquid.texts.text }}").render!('product' => ProductDrop.new))
|
||||
assert_equal("text1", Liquid::Template.parse('{{ product | map: "to_liquid" | map: "texts" | map: "text" }}').render!('product' => ProductDrop.new))
|
||||
end
|
||||
|
||||
def test_text_drop
|
||||
output = Liquid::Template.parse(' {{ product.texts.text }} ').render!('product' => ProductDrop.new)
|
||||
assert_equal ' text1 ', output
|
||||
assert_equal(' text1 ', output)
|
||||
end
|
||||
|
||||
def test_catchall_unknown_method
|
||||
output = Liquid::Template.parse(' {{ product.catchall.unknown }} ').render!('product' => ProductDrop.new)
|
||||
assert_equal ' catchall_method: unknown ', output
|
||||
assert_equal(' catchall_method: unknown ', output)
|
||||
end
|
||||
|
||||
def test_catchall_integer_argument_drop
|
||||
output = Liquid::Template.parse(' {{ product.catchall[8] }} ').render!('product' => ProductDrop.new)
|
||||
assert_equal ' catchall_method: 8 ', output
|
||||
assert_equal(' catchall_method: 8 ', output)
|
||||
end
|
||||
|
||||
def test_text_array_drop
|
||||
output = Liquid::Template.parse('{% for text in product.texts.array %} {{text}} {% endfor %}').render!('product' => ProductDrop.new)
|
||||
assert_equal ' text1 text2 ', output
|
||||
assert_equal(' text1 text2 ', output)
|
||||
end
|
||||
|
||||
def test_context_drop
|
||||
output = Liquid::Template.parse(' {{ context.bar }} ').render!('context' => ContextDrop.new, 'bar' => "carrot")
|
||||
assert_equal ' carrot ', output
|
||||
assert_equal(' carrot ', output)
|
||||
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
|
||||
output = Liquid::Template.parse(' {{ product.context.foo }} ').render!('product' => ProductDrop.new, 'foo' => "monkey")
|
||||
assert_equal ' monkey ', output
|
||||
assert_equal(' monkey ', output)
|
||||
end
|
||||
|
||||
def test_protected
|
||||
output = Liquid::Template.parse(' {{ product.callmenot }} ').render!('product' => ProductDrop.new)
|
||||
assert_equal ' ', output
|
||||
assert_equal(' ', output)
|
||||
end
|
||||
|
||||
def test_object_methods_not_allowed
|
||||
@@ -195,40 +172,40 @@ class DropsTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_scope
|
||||
assert_equal '1', Liquid::Template.parse('{{ context.scopes }}').render!('context' => ContextDrop.new)
|
||||
assert_equal '2', Liquid::Template.parse('{%for i in dummy%}{{ context.scopes }}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1])
|
||||
assert_equal '3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ context.scopes }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1])
|
||||
assert_equal('1', Liquid::Template.parse('{{ context.scopes }}').render!('context' => ContextDrop.new))
|
||||
assert_equal('2', Liquid::Template.parse('{%for i in dummy%}{{ context.scopes }}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1]))
|
||||
assert_equal('3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ context.scopes }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1]))
|
||||
end
|
||||
|
||||
def test_scope_though_proc
|
||||
assert_equal '1', Liquid::Template.parse('{{ s }}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] })
|
||||
assert_equal '2', Liquid::Template.parse('{%for i in dummy%}{{ s }}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }, 'dummy' => [1])
|
||||
assert_equal '3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ s }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }, 'dummy' => [1])
|
||||
assert_equal('1', Liquid::Template.parse('{{ s }}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }))
|
||||
assert_equal('2', Liquid::Template.parse('{%for i in dummy%}{{ s }}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }, 'dummy' => [1]))
|
||||
assert_equal('3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ s }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }, 'dummy' => [1]))
|
||||
end
|
||||
|
||||
def test_scope_with_assigns
|
||||
assert_equal 'variable', Liquid::Template.parse('{% assign a = "variable"%}{{a}}').render!('context' => ContextDrop.new)
|
||||
assert_equal 'variable', Liquid::Template.parse('{% assign a = "variable"%}{%for i in dummy%}{{a}}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1])
|
||||
assert_equal 'test', Liquid::Template.parse('{% assign header_gif = "test"%}{{header_gif}}').render!('context' => ContextDrop.new)
|
||||
assert_equal 'test', Liquid::Template.parse("{% assign header_gif = 'test'%}{{header_gif}}").render!('context' => ContextDrop.new)
|
||||
assert_equal('variable', Liquid::Template.parse('{% assign a = "variable"%}{{a}}').render!('context' => ContextDrop.new))
|
||||
assert_equal('variable', Liquid::Template.parse('{% assign a = "variable"%}{%for i in dummy%}{{a}}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1]))
|
||||
assert_equal('test', Liquid::Template.parse('{% assign header_gif = "test"%}{{header_gif}}').render!('context' => ContextDrop.new))
|
||||
assert_equal('test', Liquid::Template.parse("{% assign header_gif = 'test'%}{{header_gif}}").render!('context' => ContextDrop.new))
|
||||
end
|
||||
|
||||
def test_scope_from_tags
|
||||
assert_equal '1', Liquid::Template.parse('{% for i in context.scopes_as_array %}{{i}}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1])
|
||||
assert_equal '12', Liquid::Template.parse('{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1])
|
||||
assert_equal '123', Liquid::Template.parse('{%for a in dummy%}{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1])
|
||||
assert_equal('1', Liquid::Template.parse('{% for i in context.scopes_as_array %}{{i}}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1]))
|
||||
assert_equal('12', Liquid::Template.parse('{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1]))
|
||||
assert_equal('123', Liquid::Template.parse('{%for a in dummy%}{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1]))
|
||||
end
|
||||
|
||||
def test_access_context_from_drop
|
||||
assert_equal '123', Liquid::Template.parse('{%for a in dummy%}{{ context.loop_pos }}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1, 2, 3])
|
||||
assert_equal('123', Liquid::Template.parse('{%for a in dummy%}{{ context.loop_pos }}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1, 2, 3]))
|
||||
end
|
||||
|
||||
def test_enumerable_drop
|
||||
assert_equal '123', Liquid::Template.parse('{% for c in collection %}{{c}}{% endfor %}').render!('collection' => EnumerableDrop.new)
|
||||
assert_equal('123', Liquid::Template.parse('{% for c in collection %}{{c}}{% endfor %}').render!('collection' => EnumerableDrop.new))
|
||||
end
|
||||
|
||||
def test_enumerable_drop_size
|
||||
assert_equal '3', Liquid::Template.parse('{{collection.size}}').render!('collection' => EnumerableDrop.new)
|
||||
assert_equal('3', Liquid::Template.parse('{{collection.size}}').render!('collection' => EnumerableDrop.new))
|
||||
end
|
||||
|
||||
def test_enumerable_drop_will_invoke_liquid_method_missing_for_clashing_method_names
|
||||
@@ -248,7 +225,7 @@ class DropsTest < Minitest::Test
|
||||
assert_equal "3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)
|
||||
end
|
||||
|
||||
assert_equal "yes", Liquid::Template.parse("{% if collection contains 3 %}yes{% endif %}").render!('collection' => RealEnumerableDrop.new)
|
||||
assert_equal("yes", Liquid::Template.parse("{% if collection contains 3 %}yes{% endif %}").render!('collection' => RealEnumerableDrop.new))
|
||||
|
||||
[:min, :first].each do |method|
|
||||
assert_equal "1", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new)
|
||||
@@ -259,15 +236,22 @@ class DropsTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_empty_string_value_access
|
||||
assert_equal '', Liquid::Template.parse('{{ product[value] }}').render!('product' => ProductDrop.new, 'value' => '')
|
||||
assert_equal('', Liquid::Template.parse('{{ product[value] }}').render!('product' => ProductDrop.new, 'value' => ''))
|
||||
end
|
||||
|
||||
def test_nil_value_access
|
||||
assert_equal '', Liquid::Template.parse('{{ product[value] }}').render!('product' => ProductDrop.new, 'value' => nil)
|
||||
assert_equal('', Liquid::Template.parse('{{ product[value] }}').render!('product' => ProductDrop.new, 'value' => nil))
|
||||
end
|
||||
|
||||
def test_default_to_s_on_drops
|
||||
assert_equal 'ProductDrop', Liquid::Template.parse("{{ product }}").render!('product' => ProductDrop.new)
|
||||
assert_equal 'EnumerableDrop', Liquid::Template.parse('{{ collection }}').render!('collection' => EnumerableDrop.new)
|
||||
assert_equal('ProductDrop', Liquid::Template.parse("{{ product }}").render!('product' => ProductDrop.new))
|
||||
assert_equal('EnumerableDrop', Liquid::Template.parse('{{ collection }}').render!('collection' => EnumerableDrop.new))
|
||||
end
|
||||
|
||||
def test_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 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)
|
||||
end
|
||||
end # DropsTest
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class ErrorHandlingTest < Minitest::Test
|
||||
@@ -33,31 +35,31 @@ class ErrorHandlingTest < Minitest::Test
|
||||
TEXT
|
||||
|
||||
output = Liquid::Template.parse(template, line_numbers: true).render('errors' => ErrorDrop.new)
|
||||
assert_equal expected, output
|
||||
assert_equal(expected, output)
|
||||
end
|
||||
|
||||
def test_standard_error
|
||||
template = Liquid::Template.parse(' {{ errors.standard_error }} ')
|
||||
assert_equal ' Liquid error: standard error ', template.render('errors' => ErrorDrop.new)
|
||||
assert_equal(' Liquid error: standard error ', template.render('errors' => ErrorDrop.new))
|
||||
|
||||
assert_equal 1, template.errors.size
|
||||
assert_equal StandardError, template.errors.first.class
|
||||
assert_equal(1, template.errors.size)
|
||||
assert_equal(StandardError, template.errors.first.class)
|
||||
end
|
||||
|
||||
def test_syntax
|
||||
template = Liquid::Template.parse(' {{ errors.syntax_error }} ')
|
||||
assert_equal ' Liquid syntax error: syntax error ', template.render('errors' => ErrorDrop.new)
|
||||
assert_equal(' Liquid syntax error: syntax error ', template.render('errors' => ErrorDrop.new))
|
||||
|
||||
assert_equal 1, template.errors.size
|
||||
assert_equal SyntaxError, template.errors.first.class
|
||||
assert_equal(1, template.errors.size)
|
||||
assert_equal(SyntaxError, template.errors.first.class)
|
||||
end
|
||||
|
||||
def test_argument
|
||||
template = Liquid::Template.parse(' {{ errors.argument_error }} ')
|
||||
assert_equal ' Liquid error: argument error ', template.render('errors' => ErrorDrop.new)
|
||||
assert_equal(' Liquid error: argument error ', template.render('errors' => ErrorDrop.new))
|
||||
|
||||
assert_equal 1, template.errors.size
|
||||
assert_equal ArgumentError, template.errors.first.class
|
||||
assert_equal(1, template.errors.size)
|
||||
assert_equal(ArgumentError, template.errors.first.class)
|
||||
end
|
||||
|
||||
def test_missing_endtag_parse_time_error
|
||||
@@ -76,9 +78,9 @@ class ErrorHandlingTest < Minitest::Test
|
||||
|
||||
def test_lax_unrecognized_operator
|
||||
template = Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', error_mode: :lax)
|
||||
assert_equal ' Liquid error: Unknown operator =! ', template.render
|
||||
assert_equal 1, template.errors.size
|
||||
assert_equal Liquid::ArgumentError, template.errors.first.class
|
||||
assert_equal(' Liquid error: Unknown operator =! ', template.render)
|
||||
assert_equal(1, template.errors.size)
|
||||
assert_equal(Liquid::ArgumentError, template.errors.first.class)
|
||||
end
|
||||
|
||||
def test_with_line_numbers_adds_numbers_to_parser_errors
|
||||
@@ -122,8 +124,8 @@ class ErrorHandlingTest < Minitest::Test
|
||||
error_mode: :warn,
|
||||
line_numbers: true)
|
||||
|
||||
assert_equal ['Liquid syntax error (line 4): Unexpected character = in "1 =! 2"'],
|
||||
template.warnings.map(&:message)
|
||||
assert_equal(['Liquid syntax error (line 4): Unexpected character = in "1 =! 2"'],
|
||||
template.warnings.map(&:message))
|
||||
end
|
||||
|
||||
def test_parsing_strict_with_line_numbers_adds_numbers_to_lexer_errors
|
||||
@@ -139,7 +141,7 @@ class ErrorHandlingTest < Minitest::Test
|
||||
line_numbers: true)
|
||||
end
|
||||
|
||||
assert_equal 'Liquid syntax error (line 4): Unexpected character = in "1 =! 2"', err.message
|
||||
assert_equal('Liquid syntax error (line 4): Unexpected character = in "1 =! 2"', err.message)
|
||||
end
|
||||
|
||||
def test_syntax_errors_in_nested_blocks_have_correct_line_number
|
||||
@@ -156,37 +158,37 @@ class ErrorHandlingTest < Minitest::Test
|
||||
line_numbers: true)
|
||||
end
|
||||
|
||||
assert_equal "Liquid syntax error (line 5): Unknown tag 'foo'", err.message
|
||||
assert_equal("Liquid syntax error (line 5): Unknown tag 'foo'", err.message)
|
||||
end
|
||||
|
||||
def test_strict_error_messages
|
||||
err = assert_raises(SyntaxError) do
|
||||
Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', error_mode: :strict)
|
||||
end
|
||||
assert_equal 'Liquid syntax error: Unexpected character = in "1 =! 2"', err.message
|
||||
assert_equal('Liquid syntax error: Unexpected character = in "1 =! 2"', err.message)
|
||||
|
||||
err = assert_raises(SyntaxError) do
|
||||
Liquid::Template.parse('{{%%%}}', error_mode: :strict)
|
||||
end
|
||||
assert_equal 'Liquid syntax error: Unexpected character % in "{{%%%}}"', err.message
|
||||
assert_equal('Liquid syntax error: Unexpected character % in "{{%%%}}"', err.message)
|
||||
end
|
||||
|
||||
def test_warnings
|
||||
template = Liquid::Template.parse('{% if ~~~ %}{{%%%}}{% else %}{{ hello. }}{% endif %}', error_mode: :warn)
|
||||
assert_equal 3, template.warnings.size
|
||||
assert_equal 'Unexpected character ~ in "~~~"', template.warnings[0].to_s(false)
|
||||
assert_equal 'Unexpected character % in "{{%%%}}"', template.warnings[1].to_s(false)
|
||||
assert_equal 'Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].to_s(false)
|
||||
assert_equal '', template.render
|
||||
assert_equal(3, template.warnings.size)
|
||||
assert_equal('Unexpected character ~ in "~~~"', template.warnings[0].to_s(false))
|
||||
assert_equal('Unexpected character % in "{{%%%}}"', template.warnings[1].to_s(false))
|
||||
assert_equal('Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].to_s(false))
|
||||
assert_equal('', template.render)
|
||||
end
|
||||
|
||||
def test_warning_line_numbers
|
||||
template = Liquid::Template.parse("{% if ~~~ %}\n{{%%%}}{% else %}\n{{ hello. }}{% endif %}", error_mode: :warn, line_numbers: true)
|
||||
assert_equal 'Liquid syntax error (line 1): Unexpected character ~ in "~~~"', template.warnings[0].message
|
||||
assert_equal 'Liquid syntax error (line 2): Unexpected character % in "{{%%%}}"', template.warnings[1].message
|
||||
assert_equal 'Liquid syntax error (line 3): Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].message
|
||||
assert_equal 3, template.warnings.size
|
||||
assert_equal [1, 2, 3], template.warnings.map(&:line_number)
|
||||
assert_equal('Liquid syntax error (line 1): Unexpected character ~ in "~~~"', template.warnings[0].message)
|
||||
assert_equal('Liquid syntax error (line 2): Unexpected character % in "{{%%%}}"', template.warnings[1].message)
|
||||
assert_equal('Liquid syntax error (line 3): Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].message)
|
||||
assert_equal(3, template.warnings.size)
|
||||
assert_equal([1, 2, 3], template.warnings.map(&:line_number))
|
||||
end
|
||||
|
||||
# Liquid should not catch Exceptions that are not subclasses of StandardError, like Interrupt and NoMemoryError
|
||||
@@ -202,35 +204,41 @@ class ErrorHandlingTest < Minitest::Test
|
||||
|
||||
output = template.render('errors' => ErrorDrop.new)
|
||||
|
||||
assert_equal 'This is a runtime error: Liquid error (line 1): internal', output
|
||||
assert_equal [Liquid::InternalError], template.errors.map(&:class)
|
||||
assert_equal('This is a runtime error: Liquid error (line 1): internal', output)
|
||||
assert_equal([Liquid::InternalError], template.errors.map(&:class))
|
||||
end
|
||||
|
||||
def test_setting_default_exception_renderer
|
||||
old_exception_renderer = Liquid::Template.default_exception_renderer
|
||||
exceptions = []
|
||||
Liquid::Template.default_exception_renderer = ->(e) { exceptions << e; '' }
|
||||
Liquid::Template.default_exception_renderer = ->(e) {
|
||||
exceptions << e
|
||||
''
|
||||
}
|
||||
template = Liquid::Template.parse('This is a runtime error: {{ errors.argument_error }}')
|
||||
|
||||
output = template.render('errors' => ErrorDrop.new)
|
||||
|
||||
assert_equal 'This is a runtime error: ', output
|
||||
assert_equal [Liquid::ArgumentError], template.errors.map(&:class)
|
||||
assert_equal('This is a runtime error: ', output)
|
||||
assert_equal([Liquid::ArgumentError], template.errors.map(&:class))
|
||||
ensure
|
||||
Liquid::Template.default_exception_renderer = old_exception_renderer if old_exception_renderer
|
||||
end
|
||||
|
||||
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 = []
|
||||
handler = ->(e) { exceptions << e; e.cause }
|
||||
handler = ->(e) {
|
||||
exceptions << e
|
||||
e.cause
|
||||
}
|
||||
|
||||
output = template.render({ 'errors' => ErrorDrop.new }, exception_renderer: handler)
|
||||
|
||||
assert_equal 'This is a runtime error: runtime error', output
|
||||
assert_equal [Liquid::InternalError], exceptions.map(&:class)
|
||||
assert_equal exceptions, template.errors
|
||||
assert_equal '#<RuntimeError: runtime error>', exceptions.first.cause.inspect
|
||||
assert_equal('This is a runtime error: runtime error', output)
|
||||
assert_equal([Liquid::InternalError], exceptions.map(&:class))
|
||||
assert_equal(exceptions, template.errors)
|
||||
assert_equal('#<RuntimeError: runtime error>', exceptions.first.cause.inspect)
|
||||
end
|
||||
|
||||
class TestFileSystem
|
||||
@@ -244,12 +252,21 @@ class ErrorHandlingTest < Minitest::Test
|
||||
|
||||
begin
|
||||
Liquid::Template.file_system = TestFileSystem.new
|
||||
|
||||
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
|
||||
Liquid::Template.file_system = old_file_system
|
||||
end
|
||||
assert_equal "Argument error:\nLiquid error (product line 1): argument error", page
|
||||
assert_equal "product", template.errors.first.template_name
|
||||
assert_equal("Argument error:\nLiquid error (product line 1): argument error", page)
|
||||
assert_equal("product", template.errors.first.template_name)
|
||||
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
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
module MoneyFilter
|
||||
@@ -39,13 +41,13 @@ class FiltersTest < Minitest::Test
|
||||
@context['var'] = 1000
|
||||
@context.add_filters(MoneyFilter)
|
||||
|
||||
assert_equal ' 1000$ ', Template.parse("{{var | money}}").render(@context)
|
||||
assert_equal(' 1000$ ', Template.parse("{{var | money}}").render(@context))
|
||||
end
|
||||
|
||||
def test_underscore_in_filter_name
|
||||
@context['var'] = 1000
|
||||
@context.add_filters(MoneyFilter)
|
||||
assert_equal ' 1000$ ', Template.parse("{{var | money_with_underscore}}").render(@context)
|
||||
assert_equal(' 1000$ ', Template.parse("{{var | money_with_underscore}}").render(@context))
|
||||
end
|
||||
|
||||
def test_second_filter_overwrites_first
|
||||
@@ -53,103 +55,112 @@ class FiltersTest < Minitest::Test
|
||||
@context.add_filters(MoneyFilter)
|
||||
@context.add_filters(CanadianMoneyFilter)
|
||||
|
||||
assert_equal ' 1000$ CAD ', Template.parse("{{var | money}}").render(@context)
|
||||
assert_equal(' 1000$ CAD ', Template.parse("{{var | money}}").render(@context))
|
||||
end
|
||||
|
||||
def test_size
|
||||
@context['var'] = 'abcd'
|
||||
@context.add_filters(MoneyFilter)
|
||||
|
||||
assert_equal '4', Template.parse("{{var | size}}").render(@context)
|
||||
assert_equal('4', Template.parse("{{var | size}}").render(@context))
|
||||
end
|
||||
|
||||
def test_join
|
||||
@context['var'] = [1, 2, 3, 4]
|
||||
|
||||
assert_equal "1 2 3 4", Template.parse("{{var | join}}").render(@context)
|
||||
assert_equal("1 2 3 4", Template.parse("{{var | join}}").render(@context))
|
||||
end
|
||||
|
||||
def test_sort
|
||||
@context['value'] = 3
|
||||
@context['value'] = 3
|
||||
@context['numbers'] = [2, 1, 4, 3]
|
||||
@context['words'] = ['expected', 'as', 'alphabetic']
|
||||
@context['arrays'] = ['flower', 'are']
|
||||
@context['words'] = ['expected', 'as', 'alphabetic']
|
||||
@context['arrays'] = ['flower', 'are']
|
||||
@context['case_sensitive'] = ['sensitive', 'Expected', 'case']
|
||||
|
||||
assert_equal '1 2 3 4', Template.parse("{{numbers | sort | join}}").render(@context)
|
||||
assert_equal 'alphabetic as expected', Template.parse("{{words | sort | join}}").render(@context)
|
||||
assert_equal '3', Template.parse("{{value | sort}}").render(@context)
|
||||
assert_equal 'are flower', Template.parse("{{arrays | sort | join}}").render(@context)
|
||||
assert_equal 'Expected case sensitive', Template.parse("{{case_sensitive | sort | join}}").render(@context)
|
||||
assert_equal('1 2 3 4', Template.parse("{{numbers | sort | join}}").render(@context))
|
||||
assert_equal('alphabetic as expected', Template.parse("{{words | sort | join}}").render(@context))
|
||||
assert_equal('3', Template.parse("{{value | sort}}").render(@context))
|
||||
assert_equal('are flower', Template.parse("{{arrays | sort | join}}").render(@context))
|
||||
assert_equal('Expected case sensitive', Template.parse("{{case_sensitive | sort | join}}").render(@context))
|
||||
end
|
||||
|
||||
def test_sort_natural
|
||||
@context['words'] = ['case', 'Assert', 'Insensitive']
|
||||
@context['hashes'] = [{ 'a' => 'A' }, { 'a' => 'b' }, { 'a' => 'C' }]
|
||||
@context['words'] = ['case', 'Assert', 'Insensitive']
|
||||
@context['hashes'] = [{ 'a' => 'A' }, { 'a' => 'b' }, { 'a' => 'C' }]
|
||||
@context['objects'] = [TestObject.new('A'), TestObject.new('b'), TestObject.new('C')]
|
||||
|
||||
# Test strings
|
||||
assert_equal 'Assert case Insensitive', Template.parse("{{words | sort_natural | join}}").render(@context)
|
||||
assert_equal('Assert case Insensitive', Template.parse("{{words | sort_natural | join}}").render(@context))
|
||||
|
||||
# Test hashes
|
||||
assert_equal 'A b C', Template.parse("{{hashes | sort_natural: 'a' | map: 'a' | join}}").render(@context)
|
||||
assert_equal('A b C', Template.parse("{{hashes | sort_natural: 'a' | map: 'a' | join}}").render(@context))
|
||||
|
||||
# Test objects
|
||||
assert_equal 'A b C', Template.parse("{{objects | sort_natural: 'a' | map: 'a' | join}}").render(@context)
|
||||
assert_equal('A b C', Template.parse("{{objects | sort_natural: 'a' | map: 'a' | join}}").render(@context))
|
||||
end
|
||||
|
||||
def test_compact
|
||||
@context['words'] = ['a', nil, 'b', nil, 'c']
|
||||
@context['hashes'] = [{ 'a' => 'A' }, { 'a' => nil }, { 'a' => 'C' }]
|
||||
@context['words'] = ['a', nil, 'b', nil, 'c']
|
||||
@context['hashes'] = [{ 'a' => 'A' }, { 'a' => nil }, { 'a' => 'C' }]
|
||||
@context['objects'] = [TestObject.new('A'), TestObject.new(nil), TestObject.new('C')]
|
||||
|
||||
# Test strings
|
||||
assert_equal 'a b c', Template.parse("{{words | compact | join}}").render(@context)
|
||||
assert_equal('a b c', Template.parse("{{words | compact | join}}").render(@context))
|
||||
|
||||
# Test hashes
|
||||
assert_equal 'A C', Template.parse("{{hashes | compact: 'a' | map: 'a' | join}}").render(@context)
|
||||
assert_equal('A C', Template.parse("{{hashes | compact: 'a' | map: 'a' | join}}").render(@context))
|
||||
|
||||
# Test objects
|
||||
assert_equal 'A C', Template.parse("{{objects | compact: 'a' | map: 'a' | join}}").render(@context)
|
||||
assert_equal('A C', Template.parse("{{objects | compact: 'a' | map: 'a' | join}}").render(@context))
|
||||
end
|
||||
|
||||
def test_strip_html
|
||||
@context['var'] = "<b>bla blub</a>"
|
||||
|
||||
assert_equal "bla blub", Template.parse("{{ var | strip_html }}").render(@context)
|
||||
assert_equal("bla blub", Template.parse("{{ var | strip_html }}").render(@context))
|
||||
end
|
||||
|
||||
def test_strip_html_ignore_comments_with_html
|
||||
@context['var'] = "<!-- split and some <ul> tag --><b>bla blub</a>"
|
||||
|
||||
assert_equal "bla blub", Template.parse("{{ var | strip_html }}").render(@context)
|
||||
assert_equal("bla blub", Template.parse("{{ var | strip_html }}").render(@context))
|
||||
end
|
||||
|
||||
def test_capitalize
|
||||
@context['var'] = "blub"
|
||||
|
||||
assert_equal "Blub", Template.parse("{{ var | capitalize }}").render(@context)
|
||||
assert_equal("Blub", Template.parse("{{ var | capitalize }}").render(@context))
|
||||
end
|
||||
|
||||
def test_nonexistent_filter_is_ignored
|
||||
@context['var'] = 1000
|
||||
|
||||
assert_equal '1000', Template.parse("{{ var | xyzzy }}").render(@context)
|
||||
assert_equal('1000', Template.parse("{{ var | xyzzy }}").render(@context))
|
||||
end
|
||||
|
||||
def test_filter_with_keyword_arguments
|
||||
@context['surname'] = 'john'
|
||||
@context['input'] = 'hello %{first_name}, %{last_name}'
|
||||
@context['input'] = 'hello %{first_name}, %{last_name}'
|
||||
@context.add_filters(SubstituteFilter)
|
||||
output = Template.parse(%({{ input | substitute: first_name: surname, last_name: 'doe' }})).render(@context)
|
||||
assert_equal 'hello john, doe', output
|
||||
output = Template.parse(%({{ input | substitute: first_name: surname, last_name: 'doe' }})).render(@context)
|
||||
assert_equal('hello john, doe', output)
|
||||
end
|
||||
|
||||
def test_override_object_method_in_filter
|
||||
assert_equal "tap overridden", Template.parse("{{var | tap}}").render!({ 'var' => 1000 }, filters: [OverrideObjectMethodFilter])
|
||||
assert_equal("tap overridden", Template.parse("{{var | tap}}").render!({ 'var' => 1000 }, filters: [OverrideObjectMethodFilter]))
|
||||
|
||||
# tap still treated as a non-existent filter
|
||||
assert_equal "1000", Template.parse("{{var | tap}}").render!('var' => 1000)
|
||||
assert_equal("1000", Template.parse("{{var | tap}}").render!('var' => 1000))
|
||||
end
|
||||
|
||||
def test_liquid_argument_error
|
||||
source = "{{ '' | size: 'too many args' }}"
|
||||
exc = assert_raises(Liquid::ArgumentError) do
|
||||
Template.parse(source).render!
|
||||
end
|
||||
assert_match(/\ALiquid error: wrong number of arguments /, exc.message)
|
||||
assert_equal(exc.message, Template.parse(source).render)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -165,8 +176,8 @@ class FiltersInTemplate < Minitest::Test
|
||||
end
|
||||
|
||||
def test_local_filter_with_deprecated_syntax
|
||||
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, CanadianMoneyFilter)
|
||||
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, [CanadianMoneyFilter])
|
||||
assert_equal(" 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, CanadianMoneyFilter))
|
||||
assert_equal(" 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, [CanadianMoneyFilter]))
|
||||
end
|
||||
end # FiltersTest
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class HashOrderingTest < Minitest::Test
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
module FunnyFilter
|
||||
@@ -40,84 +42,84 @@ class OutputTest < Minitest::Test
|
||||
text = %( {{best_cars}} )
|
||||
|
||||
expected = %( bmw )
|
||||
assert_equal expected, Template.parse(text).render!(@assigns)
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns))
|
||||
end
|
||||
|
||||
def test_variable_traversing_with_two_brackets
|
||||
text = %({{ site.data.menu[include.menu][include.locale] }})
|
||||
assert_equal "it works!", Template.parse(text).render!(
|
||||
assert_equal("it works!", Template.parse(text).render!(
|
||||
"site" => { "data" => { "menu" => { "foo" => { "bar" => "it works!" } } } },
|
||||
"include" => { "menu" => "foo", "locale" => "bar" }
|
||||
)
|
||||
))
|
||||
end
|
||||
|
||||
def test_variable_traversing
|
||||
text = %( {{car.bmw}} {{car.gm}} {{car.bmw}} )
|
||||
|
||||
expected = %( good bad good )
|
||||
assert_equal expected, Template.parse(text).render!(@assigns)
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns))
|
||||
end
|
||||
|
||||
def test_variable_piping
|
||||
text = %( {{ car.gm | make_funny }} )
|
||||
text = %( {{ car.gm | make_funny }} )
|
||||
expected = %( LOL )
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
|
||||
end
|
||||
|
||||
def test_variable_piping_with_input
|
||||
text = %( {{ car.gm | cite_funny }} )
|
||||
text = %( {{ car.gm | cite_funny }} )
|
||||
expected = %( LOL: bad )
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
|
||||
end
|
||||
|
||||
def test_variable_piping_with_args
|
||||
text = %! {{ car.gm | add_smiley : ':-(' }} !
|
||||
text = %! {{ car.gm | add_smiley : ':-(' }} !
|
||||
expected = %| bad :-( |
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
|
||||
end
|
||||
|
||||
def test_variable_piping_with_no_args
|
||||
text = %( {{ car.gm | add_smiley }} )
|
||||
text = %( {{ car.gm | add_smiley }} )
|
||||
expected = %| bad :-) |
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
|
||||
end
|
||||
|
||||
def test_multiple_variable_piping_with_args
|
||||
text = %! {{ car.gm | add_smiley : ':-(' | add_smiley : ':-('}} !
|
||||
text = %! {{ car.gm | add_smiley : ':-(' | add_smiley : ':-('}} !
|
||||
expected = %| bad :-( :-( |
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
|
||||
end
|
||||
|
||||
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> )
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
|
||||
end
|
||||
|
||||
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> )
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
|
||||
end
|
||||
|
||||
def test_multiple_pipings
|
||||
text = %( {{ best_cars | cite_funny | paragraph }} )
|
||||
text = %( {{ best_cars | cite_funny | paragraph }} )
|
||||
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
|
||||
|
||||
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> )
|
||||
|
||||
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])
|
||||
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
|
||||
end
|
||||
end # OutputTest
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class ParsingQuirksTest < Minitest::Test
|
||||
@@ -5,7 +7,7 @@ class ParsingQuirksTest < Minitest::Test
|
||||
|
||||
def test_parsing_css
|
||||
text = " div { font-weight: bold; } "
|
||||
assert_equal text, Template.parse(text).render!
|
||||
assert_equal(text, Template.parse(text).render!)
|
||||
end
|
||||
|
||||
def test_raise_on_single_close_bracet
|
||||
@@ -27,7 +29,7 @@ class ParsingQuirksTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_error_on_empty_filter
|
||||
assert Template.parse("{{test}}")
|
||||
assert(Template.parse("{{test}}"))
|
||||
|
||||
with_error_mode(:lax) do
|
||||
assert Template.parse("{{|test}}")
|
||||
@@ -62,15 +64,15 @@ class ParsingQuirksTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_no_error_on_lax_empty_filter
|
||||
assert Template.parse("{{test |a|b|}}", error_mode: :lax)
|
||||
assert Template.parse("{{test}}", error_mode: :lax)
|
||||
assert Template.parse("{{|test|}}", error_mode: :lax)
|
||||
assert(Template.parse("{{test |a|b|}}", error_mode: :lax))
|
||||
assert(Template.parse("{{test}}", error_mode: :lax))
|
||||
assert(Template.parse("{{|test|}}", error_mode: :lax))
|
||||
end
|
||||
|
||||
def test_meaningless_parens_lax
|
||||
with_error_mode(:lax) do
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class RenderProfilingTest < Minitest::Test
|
||||
@@ -17,35 +19,35 @@ class RenderProfilingTest < Minitest::Test
|
||||
t = Template.parse("{{ 'a string' | upcase }}")
|
||||
t.render!
|
||||
|
||||
assert_nil t.profiler
|
||||
assert_nil(t.profiler)
|
||||
end
|
||||
|
||||
def test_parse_makes_available_simple_profiling
|
||||
t = Template.parse("{{ 'a string' | upcase }}", profile: true)
|
||||
t.render!
|
||||
|
||||
assert_equal 1, t.profiler.length
|
||||
assert_equal(1, t.profiler.length)
|
||||
|
||||
node = t.profiler[0]
|
||||
assert_equal " 'a string' | upcase ", node.code
|
||||
assert_equal(" 'a string' | upcase ", node.code)
|
||||
end
|
||||
|
||||
def test_render_ignores_raw_strings_when_profiling
|
||||
t = Template.parse("This is raw string\nstuff\nNewline", profile: true)
|
||||
t.render!
|
||||
|
||||
assert_equal 0, t.profiler.length
|
||||
assert_equal(0, t.profiler.length)
|
||||
end
|
||||
|
||||
def test_profiling_includes_line_numbers_of_liquid_nodes
|
||||
t = Template.parse("{{ 'a string' | upcase }}\n{% increment test %}", profile: true)
|
||||
t.render!
|
||||
assert_equal 2, t.profiler.length
|
||||
assert_equal(2, t.profiler.length)
|
||||
|
||||
# {{ 'a string' | upcase }}
|
||||
assert_equal 1, t.profiler[0].line_number
|
||||
assert_equal(1, t.profiler[0].line_number)
|
||||
# {{ increment test }}
|
||||
assert_equal 2, t.profiler[1].line_number
|
||||
assert_equal(2, t.profiler[1].line_number)
|
||||
end
|
||||
|
||||
def test_profiling_includes_line_numbers_of_included_partials
|
||||
@@ -55,9 +57,9 @@ class RenderProfilingTest < Minitest::Test
|
||||
included_children = t.profiler[0].children
|
||||
|
||||
# {% assign template_name = 'a_template' %}
|
||||
assert_equal 1, included_children[0].line_number
|
||||
assert_equal(1, included_children[0].line_number)
|
||||
# {{ template_name }}
|
||||
assert_equal 2, included_children[1].line_number
|
||||
assert_equal(2, included_children[1].line_number)
|
||||
end
|
||||
|
||||
def test_profiling_times_the_rendering_of_tokens
|
||||
@@ -65,14 +67,14 @@ class RenderProfilingTest < Minitest::Test
|
||||
t.render!
|
||||
|
||||
node = t.profiler[0]
|
||||
refute_nil node.render_time
|
||||
refute_nil(node.render_time)
|
||||
end
|
||||
|
||||
def test_profiling_times_the_entire_render
|
||||
t = Template.parse("{% include 'a_template' %}", profile: true)
|
||||
t.render!
|
||||
|
||||
assert t.profiler.total_render_time >= 0, "Total render time was not calculated"
|
||||
assert(t.profiler.total_render_time >= 0, "Total render time was not calculated")
|
||||
end
|
||||
|
||||
def test_profiling_uses_include_to_mark_children
|
||||
@@ -80,7 +82,7 @@ class RenderProfilingTest < Minitest::Test
|
||||
t.render!
|
||||
|
||||
include_node = t.profiler[1]
|
||||
assert_equal 2, include_node.children.length
|
||||
assert_equal(2, include_node.children.length)
|
||||
end
|
||||
|
||||
def test_profiling_marks_children_with_the_name_of_included_partial
|
||||
@@ -132,23 +134,38 @@ class RenderProfilingTest < Minitest::Test
|
||||
timing_count += 1
|
||||
end
|
||||
|
||||
assert_equal 2, timing_count
|
||||
assert_equal(2, timing_count)
|
||||
end
|
||||
|
||||
def test_profiling_marks_children_of_if_blocks
|
||||
t = Template.parse("{% if true %} {% increment test %} {{ test }} {% endif %}", profile: true)
|
||||
t.render!
|
||||
|
||||
assert_equal 1, t.profiler.length
|
||||
assert_equal 2, t.profiler[0].children.length
|
||||
assert_equal(1, t.profiler.length)
|
||||
assert_equal(2, t.profiler[0].children.length)
|
||||
end
|
||||
|
||||
def test_profiling_marks_children_of_for_blocks
|
||||
t = Template.parse("{% for item in collection %} {{ item }} {% endfor %}", profile: true)
|
||||
t.render!("collection" => ["one", "two"])
|
||||
|
||||
assert_equal 1, t.profiler.length
|
||||
assert_equal(1, t.profiler.length)
|
||||
# Will profile each invocation of the for block
|
||||
assert_equal 2, t.profiler[0].children.length
|
||||
assert_equal(2, t.profiler[0].children.length)
|
||||
end
|
||||
|
||||
def test_profiling_supports_self_time
|
||||
t = Template.parse("{% for item in collection %} {{ item }} {% endfor %}", profile: true)
|
||||
t.render!("collection" => ["one", "two"])
|
||||
leaf = t.profiler[0].children[0]
|
||||
|
||||
assert_operator(leaf.self_time, :>, 0)
|
||||
end
|
||||
|
||||
def test_profiling_supports_total_time
|
||||
t = Template.parse("{% if true %} {% increment test %} {{ test }} {% endif %}", profile: true)
|
||||
t.render!
|
||||
|
||||
assert_operator(t.profiler[0].total_time, :>, 0)
|
||||
end
|
||||
end
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user