Compare commits

..

57 Commits

Author SHA1 Message Date
uchoudh
5e86c35cbf Fix flaky tests 2019-10-09 14:29:44 -04:00
Mike Angell
2bfeed2b00 Resolve InputIterator dropping context (#1184)
* Resolve InputIterator dropping context

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

* Remove duplicate code

* Default to template name

* Improve variable matching

* Extract render_partial

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

* Change to named parameter

* Remove redundant freeze

* add brackets to make intention clearer

* Use named param format from liquid

* Update syntax

* document default filter
2019-10-09 04:03:33 +11:00
uchoudh
ffadc64f28 Merge pull request #1172 from Shopify/add-liquid-profiling-attr
Add liquid profile attributes
2019-10-08 10:49:54 -04:00
Mike Angell
5302f40342 Rubocop fixes (#1182) 2019-10-07 17:06:47 +11:00
uchoudh
fefee4c675 Add liquid profile attributes
Attribute testing

Add partial name support
2019-10-03 10:12:39 -04:00
Mike Angell
1aa7d3d2ba Change registers to by symbols (#1178) 2019-09-27 04:32:24 +10:00
Mike Angell
0db9c56f34 Disable rendering of tag based on register (#1162)
* Disable rendering of tag based on register

* Improvements to disable tag

* Resolve disbale tag tests

* Test disable_tags register

* disabled_tags is now always avaiable

* Allow multiple tags to be disabled at once

* Move disabled check to block_body

* Code improvements

* Remove redundant nil check

* Improve disabled tag error output

* Improve disable tag API

* Code improvements

* Switch disabled? to not mutate output

* Fix array handling shortcut in disable_tags
2019-09-26 00:18:30 +10:00
Mike Angell
f4d134cd5c Remove jruby and truffleruby testing (#1167) 2019-09-20 02:28:43 +10:00
Mike Angell
b667bcb48b Shopify stye guide fixes (#1160) 2019-09-20 02:08:11 +10:00
Ashwin Maroli
2c14e0b2ba Use Regexp#match? when MatchData is not used (#1165)
* Use `Regexp#match?` when `MatchData` is not used

* Add `TargetRubyVersion: 2.4` to RuboCop config
2019-09-20 02:07:52 +10:00
Ashwin Maroli
ca207ed93f Cleanup RuboCop configuration file (#1161) 2019-09-20 00:55:01 +10:00
Mike Angell
ef13343591 Changes static registers to not be frozen (#1163)
* Changes static registers to not be frozen

* Add frozen test to static registers
2019-09-20 00:24:48 +10:00
Mike Angell
adb40c41b7 Enable frozen_string_literal 2019-09-18 13:40:07 +10:00
Mike Angell
d8403af515 Reimplementation of Static Registers (#1157) 2019-09-18 13:25:55 +10:00
Mike Angell
0d26f05bb8 Enabled frozen string literals (#1154)
* Enabled frozen string literals

* Update rubocop config

* Prefer string interpolation in simple cases

Co-Authored-By: Dylan Thacker-Smith <dylan.smith@shopify.com>
2019-09-18 13:19:45 +10:00
Thierry Joyal
1dcad34b06 Merge pull request #1151 from Shopify/invokable-methods-for-enumerable-reject-include
Invokable methods for enumerable reject include?
2019-09-16 09:49:40 -04:00
Mike Angell
9a42c8c8b2 Merge pull request #1149 from Shopify/liquid-usage
Add usage tracking
2019-09-16 12:14:50 +10:00
Mike Angell
1fcef2133f Merge pull request #1143 from Shopify/styling-fixes-1
Apply simple rubocop fixes
2019-09-16 12:14:32 +10:00
Mike Angell
d7514b1305 Merge pull request #1137 from Shopify/remove-lazy-stacks
Remove lazy load stacks
2019-09-16 12:14:14 +10:00
Thierry Joyal
c0ffee5919 Invokable methods for enumerable reject include? 2019-09-12 12:58:51 +00:00
Mike Angell
724d02e9b3 Disable interrupt fix in this round 2019-09-11 06:35:08 +10:00
Mike Angell
a5b387cdd4 Remove reserved word Interrupt to avoid confusion
Also resolves rubocop conflicts
2019-09-11 06:32:31 +10:00
Mike Angell
8318be2edc Update readme 2019-09-11 05:20:05 +10:00
Mike Angell
b6547f322e Simplify usage 2019-09-11 04:56:25 +10:00
Mike Angell
b316ff8413 Add usage tracking 2019-09-11 04:20:34 +10:00
Justin Li
806b2622da Switch back to Liquid-C master, since https://github.com/Shopify/liquid-c/pull/50 is merged 2019-09-04 15:12:51 -04:00
Mike Angell
1f90a37b63 Merge branch 'master' into styling-fixes-1 2019-09-04 14:26:27 +10:00
Mike Angell
c34f7c9b2c Merge pull request #1145 from Shopify/master-fixes
Render tag styling fixes
2019-09-04 14:25:38 +10:00
Mike Angell
604d899496 Render tag styling fixes 2019-08-31 22:48:25 +10:00
Mike Angell
799da202df Apply simple rubocop fixes 2019-08-31 21:58:33 +10:00
Mike Angell
ddb45cd658 Merge pull request #1139 from Shopify/shopify_ruby_style
Follow Shopify ruby style guide
2019-08-31 21:43:45 +10:00
Mike Angell
dafbb4ae90 Remove hasnling false scopes 2019-08-31 20:03:54 +10:00
Justin Li
9876096cf4 Merge pull request #1141 from ashmaroli/reduce-context-constructor-allocations
Reduce allocations from `Liquid::Context.new`
2019-08-30 12:53:50 -04:00
Ashwin Maroli
8750b4b006 Reduce allocations from Liquid::Context.new 2019-08-30 09:01:47 +05:30
Samuel Doiron
34083c96d5 Merge pull request #1122 from Shopify/render-tag
Add new `{% render %}` tag
2019-08-29 16:49:56 -04:00
Samuel
9672ed5285 Add a new {% render %} tag
Example:

```
// the_count.liquid
{{ number }}! Ah ah ah.

// my_template.liquid
{% for number in range (1..3) %}
  {% render "the_count", number: number %}
{% endfor %}

Output:
1! Ah ah ah.
2! Ah ah ah.
3! Ah ah ah.
```

The `render` tag is a more strict version of the `include` tag. It is
designed to isolate itself from the parent rendering context both by
creating a new scope (which does not inherit the parent scope) and by
only inheriting "static" registers.

Static registers are those that do not hold mutable state which could
affect rendering. This again helps `render`ed templates remain entirely
separate from their calling context.

Unlike `include`, `render` does not permit specifying the target
template using a variable, only a string literal. For example, this
means that `{% render my_dynamic_template %}` is invalid syntax. This
will make it possible to statically analyze the dependencies between
templates without making Turing angry.

Note that the `static_environment` of a rendered template is inherited, unlike
the scope and regular environment. This environment is immutable from within the
template.

An alternate syntax, which mimics the `{% include ... for %}` tag is
currently in design discussion.
2019-08-29 16:32:05 -04:00
Justin Li
f3112fc038 Merge pull request #1136 from ashmaroli/travis-selected-branches
Build only pushes to certain branches on Travis CI
2019-08-29 13:47:59 -04:00
Samuel
d338ccb9a6 Add isolated subcontexts
An isolated subcontext inherits the environment, filters,
and static registers of its supercontext, but with a fresh
(isolated) scope.

This will pave the way for adding the `render` tag, which renders
templates in such a subcontext.
2019-08-29 10:27:15 -04:00
Mike Angell
d67de1c9b2 Follow Shopify ruby style
This is the first step in bringing Liquid style inline with Shopify ruby style
2019-08-29 13:39:57 +10:00
Mike Angell
2324564743 Remove lazy load stacks
Remove lazy load stacks and instead only create a new scope when a tag is known to need one
2019-08-29 09:09:32 +10:00
Ashwin Maroli
b3097f143c Build only pushes to certain branches on Travis CI 2019-08-28 21:28:49 +05:30
Mike Angell
7b309dc75d Merge pull request #1135 from Shopify/fix-failing-rubocop
Resolve failing rubocop issues
2019-08-29 01:11:25 +10:00
Mike Angell
8f68cffdf1 Resolve failing rubocop issues 2019-08-29 00:45:38 +10:00
Mike Angell
dd27d0fd1d Merge pull request #1133 from Shopify/liquid-tag-fixes
Bugfix for new Liquid tag
2019-08-29 00:36:13 +10:00
Mike Angell
7a26e6b3d8 Merge pull request #1131 from Shopify/bump-ruby-2-4
Rubocop upgrade, Ruby 2.4 minimum and TruffleRuby
2019-08-29 00:33:42 +10:00
Mike Angell
cf4e77ab0c Merge branch 'master' into bump-ruby-2-4 2019-08-29 00:24:45 +10:00
Mike Angell
7bae55dd39 Bugfix for new Liquid tag 2019-08-28 23:39:19 +10:00
Tobias Lütke
0ce8aef229 Merge pull request #1103 from ashmaroli/ci-profile-memory
Add a CI job to profile memory usage of commit
2019-08-27 15:11:55 -04:00
Tobias Lütke
6eab595fae Merge pull request #1086 from Shopify/liquid-tag
Add {% liquid %} and {% echo %} tags
2019-08-27 15:10:20 -04:00
Mike Angell
b16b109a80 Bump Minimum version to 2.4 and bump Rubocop 2019-08-28 00:31:44 +10:00
Ashwin Maroli
ab698191b9 Add a CI job to profile memory usage of commit 2019-05-17 22:47:05 +05:30
Justin Li
7dc488a73b Simplifications from review 2019-04-09 15:19:47 -04:00
Justin Li
e6ed804ca5 Fix line number tracking after a non-empty blank token 2019-04-08 18:43:09 -04:00
Justin Li
951abb67ee Remove {% local %} tag 2019-04-08 18:34:39 -04:00
Justin Li
8d1cd41453 Add {% liquid %}, {% echo %}, and {% local %} tags 2019-04-01 20:08:38 -04:00
136 changed files with 4184 additions and 2650 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,131 +1,16 @@
inherit_from:
- 'https://shopify.github.io/ruby-style-guide/rubocop.yml'
- .rubocop_todo.yml
- ./.rubocop_todo.yml
require: rubocop-performance
Performance:
Enabled: true
AllCops:
TargetRubyVersion: 2.4
Exclude:
- 'performance/shopify/*'
- 'pkg/**'
Metrics/BlockNesting:
Max: 3
Metrics/ModuleLength:
Enabled: false
Metrics/ClassLength:
Enabled: false
Lint/AssignmentInCondition:
Enabled: false
Lint/AmbiguousOperator:
Enabled: false
Lint/AmbiguousRegexpLiteral:
Enabled: false
Lint/ParenthesesAsGroupedExpression:
Enabled: false
Lint/UnusedBlockArgument:
Enabled: false
Layout/EndAlignment:
EnforcedStyleAlignWith: variable
Lint/UnusedMethodArgument:
Enabled: false
Style/SingleLineBlockParams:
Enabled: false
Style/DoubleNegation:
Enabled: false
Style/StringLiteralsInInterpolation:
Enabled: false
Style/AndOr:
Enabled: false
Style/SignalException:
Enabled: false
Style/StringLiterals:
Enabled: false
Style/BracesAroundHashParameters:
Enabled: false
Style/NumericLiterals:
Enabled: false
Layout/SpaceInsideArrayLiteralBrackets:
Enabled: false
Layout/SpaceBeforeBlockBraces:
Enabled: false
Style/Documentation:
Enabled: false
Style/ClassAndModuleChildren:
Enabled: false
Style/TrailingCommaInArrayLiteral:
Enabled: false
Style/TrailingCommaInHashLiteral:
Enabled: false
Layout/IndentHash:
EnforcedStyle: consistent
Style/FormatString:
Enabled: false
Layout/AlignParameters:
EnforcedStyle: with_fixed_indentation
Layout/MultilineOperationIndentation:
EnforcedStyle: indented
Style/IfUnlessModifier:
Enabled: false
Style/RaiseArgs:
Enabled: false
Style/PreferredHashMethods:
Enabled: false
Style/RegexpLiteral:
Enabled: false
Style/SymbolLiteral:
Enabled: false
Performance/Count:
Enabled: false
Naming/ConstantName:
Enabled: false
Layout/CaseIndentation:
Enabled: false
Style/ClassVars:
Enabled: false
Style/PerlBackrefs:
Enabled: false
Style/TrivialAccessors:
AllowPredicates: true
Style/WordArray:
Enabled: false
- 'vendor/bundle/**/*'
Naming/MethodName:
Exclude:

View File

@@ -1,37 +1,11 @@
# This configuration was generated by
# `rubocop --auto-gen-config`
# on 2019-04-22 19:11:24 -0400 using RuboCop version 0.53.0.
# on 2019-09-11 06:34:25 +1000 using RuboCop version 0.74.0.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again.
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: Include, TreatCommentsAsGroupSeparators.
# Include: **/*.gemspec
Gemspec/OrderedDependencies:
Exclude:
- 'liquid.gemspec'
# Offense count: 5
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: auto_detection, squiggly, active_support, powerpack, unindent
Layout/IndentHeredoc:
Exclude:
- 'test/integration/tags/for_tag_test.rb'
- 'test/integration/trim_mode_test.rb'
# Offense count: 6
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: symmetrical, new_line, same_line
Layout/MultilineMethodCallBraceLayout:
Exclude:
- 'test/integration/error_handling_test.rb'
- 'test/unit/strainer_unit_test.rb'
# Offense count: 2
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
@@ -40,221 +14,35 @@ Lint/InheritException:
Exclude:
- 'lib/liquid/interrupts.rb'
# Offense count: 1
# Configuration parameters: CheckForMethodsWithNoSideEffects.
Lint/Void:
Exclude:
- 'lib/liquid/parse_context.rb'
# Offense count: 53
Metrics/AbcSize:
Max: 56
# Offense count: 12
Metrics/CyclomaticComplexity:
Max: 13
# Offense count: 112
# Configuration parameters: CountComments.
Metrics/MethodLength:
Max: 38
# Offense count: 8
Metrics/PerceivedComplexity:
Max: 11
# Offense count: 52
# Configuration parameters: Blacklist.
# Blacklist: END, (?-mix:EO[A-Z]{1})
Naming/HeredocDelimiterNaming:
Exclude:
- 'test/integration/assign_test.rb'
- 'test/integration/capture_test.rb'
- 'test/integration/trim_mode_test.rb'
# Offense count: 23
# Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames.
# AllowedNames: io, id
Naming/UncommunicativeMethodParamName:
Exclude:
- 'example/server/example_servlet.rb'
- 'lib/liquid/condition.rb'
- 'lib/liquid/context.rb'
- 'lib/liquid/standardfilters.rb'
- 'lib/liquid/tags/if.rb'
- 'lib/liquid/utils.rb'
- 'lib/liquid/variable.rb'
- 'test/integration/filter_test.rb'
- 'test/integration/standard_filter_test.rb'
- 'test/integration/tags/for_tag_test.rb'
- 'test/integration/template_test.rb'
- 'test/unit/condition_unit_test.rb'
# Offense count: 12
# Offense count: 98
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: prefer_alias, prefer_alias_method
Style/Alias:
Exclude:
- 'lib/liquid/drop.rb'
- 'lib/liquid/i18n.rb'
- 'lib/liquid/profiler/hooks.rb'
- 'lib/liquid/standardfilters.rb'
- 'lib/liquid/tag.rb'
- 'lib/liquid/tags/include.rb'
- 'lib/liquid/variable.rb'
# Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
# URISchemes: http, https
Metrics/LineLength:
Max: 294
# Offense count: 22
Style/CommentedKeyword:
Enabled: false
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SingleLineConditionsOnly, IncludeTernaryExpressions.
# SupportedStyles: assign_to_condition, assign_inside_condition
Style/ConditionalAssignment:
Exclude:
- 'lib/liquid/errors.rb'
# Offense count: 1
Style/DateTime:
Exclude:
- 'test/unit/context_unit_test.rb'
# Offense count: 2
# Cop supports --auto-correct.
Style/EmptyCaseCondition:
# Offense count: 44
Naming/ConstantName:
Exclude:
- 'lib/liquid.rb'
- 'lib/liquid/block_body.rb'
- 'lib/liquid/lexer.rb'
# Offense count: 5
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: compact, expanded
Style/EmptyMethod:
Exclude:
- 'lib/liquid/tag.rb'
- 'lib/liquid/tags/comment.rb'
- 'lib/liquid/tags/include.rb'
- 'test/integration/tags/include_tag_test.rb'
- 'test/unit/context_unit_test.rb'
# Offense count: 3
# Cop supports --auto-correct.
Style/Encoding:
Exclude:
- 'lib/liquid/version.rb'
- 'liquid.gemspec'
- 'test/integration/standard_filter_test.rb'
# Offense count: 2
# Cop supports --auto-correct.
Style/ExpandPathArguments:
Exclude:
- 'Rakefile'
- 'liquid.gemspec'
# Offense count: 7
# Configuration parameters: EnforcedStyle.
# SupportedStyles: annotated, template, unannotated
Style/FormatStringToken:
Exclude:
- 'test/integration/filter_test.rb'
- 'test/integration/hash_ordering_test.rb'
# Offense count: 14
# Configuration parameters: MinBodyLength.
Style/GuardClause:
Exclude:
- 'lib/liquid/condition.rb'
- 'lib/liquid/lexer.rb'
- 'lib/liquid/strainer.rb'
- 'lib/liquid/tags/assign.rb'
- 'lib/liquid/tags/capture.rb'
- 'lib/liquid/tags/case.rb'
- 'lib/liquid/tags/cycle.rb'
- 'lib/liquid/tags/for.rb'
- 'lib/liquid/tags/if.rb'
- 'lib/liquid/tags/include.rb'
- 'lib/liquid/tags/raw.rb'
- 'lib/liquid/tags/table_row.rb'
- 'lib/liquid/variable.rb'
- 'test/unit/tokenizer_unit_test.rb'
- 'performance/shopify/comment_form.rb'
- 'performance/shopify/paginate.rb'
- 'test/integration/tags/include_tag_test.rb'
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, MinBodyLength.
# SupportedStyles: skip_modifier_ifs, always
Style/Next:
Exclude:
- 'lib/liquid/tags/for.rb'
# Offense count: 4
# Cop supports --auto-correct.
# Configuration parameters: AutoCorrect, EnforcedStyle.
# SupportedStyles: predicate, comparison
Style/NumericPredicate:
Exclude:
- 'spec/**/*'
- 'lib/liquid/context.rb'
- 'lib/liquid/forloop_drop.rb'
- 'lib/liquid/standardfilters.rb'
- 'lib/liquid/tablerowloop_drop.rb'
# Offense count: 14
# Cop supports --auto-correct.
# Configuration parameters: PreferredDelimiters.
Style/PercentLiteralDelimiters:
Exclude:
- 'lib/liquid/tags/if.rb'
- 'liquid.gemspec'
- 'test/integration/assign_test.rb'
- 'test/integration/standard_filter_test.rb'
# Offense count: 1
# Cop supports --auto-correct.
Style/RedundantSelf:
# Offense count: 5
Style/ClassVars:
Exclude:
- 'lib/liquid/condition.rb'
- 'lib/liquid/strainer.rb'
# 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'
# Offense count: 7
# Cop supports --auto-correct.
# Configuration parameters: MinSize.
# SupportedStyles: percent, brackets
Style/SymbolArray:
EnforcedStyle: brackets
# Offense count: 2
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, AllowSafeAssignment.
# SupportedStyles: require_parentheses, require_no_parentheses, require_parentheses_when_complex
Style/TernaryParentheses:
Exclude:
- 'lib/liquid/context.rb'
- 'lib/liquid/utils.rb'
# Offense count: 2
# Cop supports --auto-correct.
Style/UnneededPercentQ:
Exclude:
- 'test/integration/error_handling_test.rb'
# Offense count: 1
# Cop supports --auto-correct.
Style/WhileUntilModifier:
Exclude:
- 'lib/liquid/tags/case.rb'
# Offense count: 648
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
# URISchemes: http, https
Metrics/LineLength:
Max: 294
- 'lib/liquid/template.rb'

View File

@@ -1,31 +1,26 @@
language: ruby
cache: bundler
rvm:
- 2.1
- 2.2
- 2.3
- 2.4
- 2.5
- &latest_ruby 2.6
- 2.7
- ruby-head
- jruby-head
# - rbx-2
sudo: false
addons:
apt:
packages:
- libgmp3-dev
matrix:
include:
- rvm: *latest_ruby
script: bundle exec rake memory_profile:run
name: Profiling Memory Usage
allow_failures:
- rvm: ruby-head
- rvm: jruby-head
install:
- bundle install
script: bundle exec rake
branches:
only:
- master
- gh-pages
- /.*-stable/
notifications:
disable: true

16
Gemfile
View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
source 'https://rubygems.org'
git_source(:github) do |repo_name|
"https://github.com/#{repo_name}.git"
@@ -5,23 +7,21 @@ end
gemspec
group :benchmark, :test do
gem 'benchmark-ips'
gem 'memory_profiler'
gem 'terminal-table'
install_if -> { RUBY_PLATFORM !~ /mingw|mswin|java/ } do
install_if -> { RUBY_PLATFORM !~ /mingw|mswin|java/ && RUBY_ENGINE != 'truffleruby' } do
gem 'stackprof'
end
end
group :test do
gem 'rubocop', '~> 0.53.0'
gem 'awesome_print'
gem 'pry'
gem 'byebug'
gem 'rubocop', '~> 0.74.0', require: false
gem 'rubocop-performance', require: false
platform :mri do
gem 'liquid-c', github: 'Shopify/liquid-c', ref: '9168659de45d6d576fce30c735f857e597fa26f6'
platform :mri, :truffleruby do
gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'master'
end
end

View File

@@ -106,3 +106,9 @@ template = Liquid::Template.parse("{{x}} {{y}}")
template.render!({ 'x' => 1}, { strict_variables: true })
#=> Liquid::UndefinedVariable: Liquid error: undefined variable y
```
### Usage tracking
To help track usages of a feature or code path in production, we have released opt-in usage tracking. To enable this, we provide an empty `Liquid:: Usage.increment` method which you can customize to your needs. The feature is well suited to https://github.com/Shopify/statsd-instrument. However, the choice of implementation is up to you.
Once you have enabled usage tracking, we recommend reporting any events through Github Issues that your system may be logging. It is highly likely this event has been added to consider deprecating or improving code specific to this event, so please raise any concerns.

View File

@@ -1,29 +1,33 @@
# frozen_string_literal: true
require 'rake'
require 'rake/testtask'
$LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
$LOAD_PATH.unshift(File.expand_path("../lib", __FILE__))
require "liquid/version"
task default: [:test, :rubocop]
task(default: [:test, :rubocop])
desc 'run test suite with default parser'
desc('run test suite with default parser')
Rake::TestTask.new(:base_test) do |t|
t.libs << '.' << 'lib' << 'test'
t.test_files = FileList['test/{integration,unit}/**/*_test.rb']
t.verbose = false
end
desc 'run test suite with warn error mode'
desc('run test suite with warn error mode')
task :warn_test do
ENV['LIQUID_PARSER_MODE'] = 'warn'
Rake::Task['base_test'].invoke
end
task :rubocop do
require 'rubocop/rake_task'
RuboCop::RakeTask.new
if RUBY_ENGINE == 'ruby'
require 'rubocop/rake_task'
RuboCop::RakeTask.new
end
end
desc 'runs test suite with both strict and lax parsers'
desc('runs test suite with both strict and lax parsers')
task :test do
ENV['LIQUID_PARSER_MODE'] = 'lax'
Rake::Task['base_test'].invoke
@@ -32,8 +36,8 @@ task :test do
Rake::Task['base_test'].reenable
Rake::Task['base_test'].invoke
if RUBY_ENGINE == 'ruby'
ENV['LIQUID-C'] = '1'
if RUBY_ENGINE == 'ruby' || RUBY_ENGINE == 'truffleruby'
ENV['LIQUID_C'] = '1'
ENV['LIQUID_PARSER_MODE'] = 'lax'
Rake::Task['base_test'].reenable
@@ -45,18 +49,7 @@ task :test do
end
end
desc 'runs the test suite using the superfluid compiler'
Rake::TestTask.new(:test_superfluid) do |t|
t.libs << '.' << 'lib' << 'test'
t.test_files = FileList['test/integration/**/*_test.rb']
t.verbose = false
ENV['LIQUID_PARSER_MODE'] = 'strict'
ENV['LIQUID-C'] = '1'
ENV['SUPERFLUID'] = '1'
end
task gem: :build
task(gem: :build)
task :build do
system "gem build liquid.gemspec"
end
@@ -82,11 +75,6 @@ namespace :benchmark do
task :strict do
ruby "./performance/benchmark.rb strict"
end
desc "Run the liquid benchmark with strict parsing"
task :superfluid do
ruby "./performance/benchmark.rb superfluid"
end
end
namespace :profile do
@@ -108,7 +96,7 @@ namespace :memory_profile do
end
end
desc "Run example"
desc("Run example")
task :example do
ruby "-w -d -Ilib example/server/server.rb"
end

View File

@@ -1,6 +1,8 @@
# frozen_string_literal: true
module ProductsFilter
def price(integer)
sprintf("$%.2d USD", integer / 100.0)
format("$%.2d USD", integer / 100.0)
end
def prettyprint(text)

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet
def do_GET(req, res)
handle(:get, req, res)
@@ -9,12 +11,12 @@ class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet
private
def handle(type, req, res)
def handle(_type, req, res)
@request = req
@response = res
@request.path_info =~ /(\w+)\z/
@action = $1 || 'index'
@action = Regexp.last_match(1) || 'index'
@assigns = send(@action) if respond_to?(@action)
@response['Content-Type'] = "text/html"

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'webrick'
require 'rexml/document'
@@ -8,5 +10,5 @@ require_relative 'example_servlet'
# Setup webrick
server = WEBrick::HTTPServer.new(Port: ARGV[1] || 3000)
server.mount('/', Servlet)
trap("INT"){ server.shutdown }
trap("INT") { server.shutdown }
server.start

View File

@@ -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\-\.\[\]]\)?/
@@ -74,7 +76,12 @@ require 'liquid/condition'
require 'liquid/utils'
require 'liquid/tokenizer'
require 'liquid/parse_context'
require 'liquid/partial_cache'
require 'liquid/usage'
require 'liquid/register'
require 'liquid/static_registers'
# Load all the tags of the standard library
#
Dir["#{__dir__}/liquid/tags/*.rb"].each { |f| require f }
Dir["#{__dir__}/liquid/registers/*.rb"].each { |f| require f }

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid
class Block < Tag
MAX_DEPTH = 100
@@ -27,16 +29,16 @@ module Liquid
end
def unknown_tag(tag, _params, _tokens)
if tag == 'else'.freeze
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_else".freeze,
block_name: block_name))
elsif tag.start_with?('end'.freeze)
raise SyntaxError.new(parse_context.locale.t("errors.syntax.invalid_delimiter".freeze,
if tag == 'else'
raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_else",
block_name: block_name)
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))
block_delimiter: block_delimiter)
else
raise SyntaxError.new(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
@@ -52,7 +54,7 @@ module Liquid
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
@@ -61,7 +63,7 @@ module Liquid
return false if end_tag_name == block_delimiter
unless end_tag_name
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_never_closed", block_name: block_name)
end
# this tag is not registered with the system

View File

@@ -1,10 +1,13 @@
# frozen_string_literal: true
module Liquid
class BlockBody
FullToken = /\A#{TagStart}#{WhitespaceControl}?\s*(\w+)\s*(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
LiquidTagToken = /\A\s*(\w+)\s*(.*?)\z/o
FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(\w+)(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
WhitespaceOrNothing = /\A\s*\z/
TAGSTART = "{%".freeze
VARSTART = "{{".freeze
TAGSTART = "{%"
VARSTART = "{{"
attr_reader :nodelist
@@ -13,9 +16,43 @@ module Liquid
@blank = true
end
def parse(tokenizer, parse_context)
def parse(tokenizer, parse_context, &block)
parse_context.line_number = tokenizer.line_number
while token = tokenizer.shift
if tokenizer.for_liquid_tag
parse_for_liquid_tag(tokenizer, parse_context, &block)
else
parse_for_document(tokenizer, parse_context, &block)
end
end
private def parse_for_liquid_tag(tokenizer, parse_context)
while (token = tokenizer.shift)
unless token.empty? || token =~ WhitespaceOrNothing
unless token =~ LiquidTagToken
# line isn't empty but didn't match tag syntax, yield and let the
# caller raise a syntax error
return yield token, token
end
tag_name = Regexp.last_match(1)
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
end
new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
@blank &&= new_tag.blank?
@nodelist << new_tag
end
parse_context.line_number = tokenizer.line_number
end
yield nil, nil
end
private def parse_for_document(tokenizer, parse_context, &block)
while (token = tokenizer.shift)
next if token.empty?
case
when token.start_with?(TAGSTART)
@@ -23,10 +60,21 @@ module Liquid
unless token =~ FullToken
raise_missing_tag_terminator(token, parse_context)
end
tag_name = $1
markup = $2
# fetch the tag from registered blocks
unless tag = registered_tags[tag_name]
tag_name = Regexp.last_match(2)
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") + Regexp.last_match(3).count("\n")
end
if tag_name == 'liquid'
liquid_tag_tokenizer = Tokenizer.new(markup, line_number: parse_context.line_number, for_liquid_tag: true)
next parse_for_liquid_tag(liquid_tag_tokenizer, parse_context, &block)
end
unless (tag = registered_tags[tag_name])
# end parsing if we reach an unknown tag and let the caller decide
# determine how to proceed
return yield tag_name, markup
@@ -55,7 +103,7 @@ module Liquid
def whitespace_handler(token, parse_context)
if token[2] == WhitespaceControl
previous_token = @nodelist.last
if previous_token.is_a? String
if previous_token.is_a?(String)
previous_token.rstrip!
end
end
@@ -67,14 +115,14 @@ module Liquid
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
idx = 0
while node = @nodelist[idx]
while (node = @nodelist[idx])
previous_output_size = output.bytesize
case node
@@ -83,7 +131,7 @@ module Liquid
when Variable
render_node(context, output, node)
when Block
render_node(context, node.blank? ? '' : output, node)
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
@@ -97,7 +145,7 @@ module Liquid
end
idx += 1
context.raise_if_resource_limits_reached(output.bytesize - previous_output_size)
raise_if_resource_limits_reached(context, output.bytesize - previous_output_size)
end
output
@@ -106,7 +154,13 @@ module Liquid
private
def render_node(context, output, node)
node.render_to_output_buffer(context, output)
if node.disabled?(context)
output << node.disabled_error_message
return
end
disable_tags(context, node.disabled_tags) do
node.render_to_output_buffer(context, output)
end
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
context.handle_error(e, node.line_number)
rescue ::StandardError => e
@@ -114,6 +168,17 @@ module Liquid
output << context.handle_error(e, line_number)
end
def disable_tags(context, tags, &block)
return yield if tags.empty?
context.registers[:disabled_tags].disable(tags, &block)
end
def raise_if_resource_limits_reached(context, length)
context.resource_limits.render_length += length
return unless context.resource_limits.reached?
raise MemoryError, "Memory limits exceeded"
end
def create_variable(token, parse_context)
token.scan(ContentOfVariable) do |content|
markup = content.first
@@ -123,11 +188,11 @@ module Liquid
end
def raise_missing_tag_terminator(token, parse_context)
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_termination".freeze, token: token, tag_end: TagEnd.inspect))
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_termination", token: token, tag_end: TagEnd.inspect)
end
def raise_missing_variable_terminator(token, parse_context)
raise SyntaxError.new(parse_context.locale.t("errors.syntax.variable_termination".freeze, token: token, tag_end: VariableEnd.inspect))
raise SyntaxError, parse_context.locale.t("errors.syntax.variable_termination", token: token, tag_end: VariableEnd.inspect)
end
def registered_tags

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid
# Container for liquid nodes which conveniently wraps decision making logic
#
@@ -8,35 +10,35 @@ 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)
else
false
end
end
end,
}
def self.operators
@@operators
end
attr_reader :attachment, :child_condition, :child_relation
attr_reader :attachment, :child_condition
attr_accessor :left, :operator, :right
def initialize(left = nil, operator = nil, right = nil)
@left = left
@operator = operator
@right = right
@child_relation = nil
@child_relation = nil
@child_condition = nil
end
@@ -78,9 +80,13 @@ module Liquid
end
def inspect
"#<Condition #{[@left, @operator, @right].compact.join(' '.freeze)}>"
"#<Condition #{[@left, @operator, @right].compact.join(' ')}>"
end
protected
attr_reader :child_relation
private
def equal_variables(left, right)
@@ -112,7 +118,7 @@ module Liquid
left = context.evaluate(left)
right = context.evaluate(right)
operation = self.class.operators[op] || raise(Liquid::ArgumentError.new("Unknown operator #{op}"))
operation = self.class.operators[op] || raise(Liquid::ArgumentError, "Unknown operator #{op}")
if operation.respond_to?(:call)
operation.call(self, left, right)
@@ -120,7 +126,7 @@ module Liquid
begin
left.send(operation, right)
rescue ::ArgumentError => e
raise Liquid::ArgumentError.new(e.message)
raise Liquid::ArgumentError, e.message
end
end
end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid
# Context keeps the variable stack and resolves variables, as well as keywords
#
@@ -12,40 +14,38 @@ module Liquid
#
# context['bob'] #=> nil class Context
class Context
attr_reader :scopes, :errors, :registers, :environments, :resource_limits
attr_reader :scopes, :errors, :registers, :environments, :resource_limits, :static_registers, :static_environments
attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil)
@environments = [environments].flatten
@scopes = [(outer_scope || {})]
@registers = registers
@errors = []
@partial = false
@strict_variables = false
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
squash_instance_assigns_with_environments
# rubocop:disable Metrics/ParameterLists
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
@this_stack_used = false
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
@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
self.exception_renderer = Template.default_exception_renderer
if rethrow_errors
self.exception_renderer = ->(e) { raise }
self.exception_renderer = ->(_e) { raise }
end
@interrupts = []
@filters = []
@global_filter = nil
end
def raise_argument_error(message)
raise Liquid::ArgumentError, message
end
def raise_if_resource_limits_reached(length)
resource_limits.render_length += length
return unless resource_limits.reached?
raise MemoryError.new("Memory limits exceeded".freeze)
end
# rubocop:enable Metrics/ParameterLists
def warnings
@warnings ||= []
@@ -99,7 +99,7 @@ module Liquid
# Push new local scope on the stack. use <tt>Context#stack</tt> instead
def push(new_scope = {})
@scopes.unshift(new_scope)
raise StackLevelError, "Nesting too deep".freeze if @scopes.length > Block::MAX_DEPTH
check_overflow
end
# Merge a hash of variables in the current local scope
@@ -121,19 +121,30 @@ module Liquid
# end
#
# context['var] #=> nil
def stack(new_scope = nil)
old_stack_used = @this_stack_used
if new_scope
push(new_scope)
@this_stack_used = true
else
@this_stack_used = false
end
def stack(new_scope = {})
push(new_scope)
yield
ensure
pop if @this_stack_used
@this_stack_used = old_stack_used
pop
end
# Creates a new context inheriting resource limits, filters, environment etc.,
# but with an isolated scope.
def new_isolated_subcontext
check_overflow
Context.build(
resource_limits: resource_limits,
static_environments: static_environments,
registers: StaticRegisters.new(registers)
).tap do |subcontext|
subcontext.base_scope_depth = base_scope_depth + 1
subcontext.exception_renderer = exception_renderer
subcontext.filters = @filters
subcontext.strainer = nil
subcontext.errors = errors
subcontext.warnings = warnings
end
end
def clear_instance_assigns
@@ -142,10 +153,6 @@ module Liquid
# Only allow String, Numeric, Hash, Array, Proc, Boolean or <tt>Liquid::Drop</tt>
def []=(key, value)
unless @this_stack_used
@this_stack_used = true
push({})
end
@scopes[0][key] = value
end
@@ -174,25 +181,13 @@ module Liquid
# This was changed from find() to find_index() because this is a very hot
# path and find_index() is optimized in MRI to reduce object allocation
index = @scopes.find_index { |s| s.key?(key) }
scope = @scopes[index] if index
variable = nil
if scope.nil?
@environments.each do |e|
variable = lookup_and_evaluate(e, key, raise_on_not_found: raise_on_not_found)
# When lookup returned a value OR there is no value but the lookup also did not raise
# then it is the value we are looking for.
if !variable.nil? || @strict_variables && raise_on_not_found
scope = e
break
end
end
variable = if index
lookup_and_evaluate(@scopes[index], key, raise_on_not_found: raise_on_not_found)
else
try_variable_find_in_environments(key, raise_on_not_found: raise_on_not_found)
end
scope ||= @environments.last || @scopes.last
variable ||= lookup_and_evaluate(scope, key, raise_on_not_found: raise_on_not_found)
variable = variable.to_liquid
variable.context = self if variable.respond_to?(:context=)
@@ -207,14 +202,44 @@ module Liquid
value = obj[key]
if value.is_a?(Proc) && obj.respond_to?(:[]=)
obj[key] = (value.arity == 0) ? value.call : value.call(self)
obj[key] = value.arity == 0 ? value.call : value.call(self)
else
value
end
end
protected
attr_writer :base_scope_depth, :warnings, :errors, :strainer, :filters
private
attr_reader :base_scope_depth
def try_variable_find_in_environments(key, raise_on_not_found:)
@environments.each do |environment|
found_variable = lookup_and_evaluate(environment, key, raise_on_not_found: raise_on_not_found)
if !found_variable.nil? || @strict_variables && raise_on_not_found
return found_variable
end
end
@static_environments.each do |environment|
found_variable = lookup_and_evaluate(environment, key, raise_on_not_found: raise_on_not_found)
if !found_variable.nil? || @strict_variables && raise_on_not_found
return found_variable
end
end
nil
end
def check_overflow
raise StackLevelError, "Nesting too deep" if overflow?
end
def overflow?
base_scope_depth + @scopes.length > Block::MAX_DEPTH
end
def internal_error
# raise and catch to set backtrace and cause on exception
raise Liquid::InternalError, 'internal'

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid
class Document < BlockBody
def self.parse(tokens, parse_context)
@@ -7,7 +9,7 @@ module Liquid
end
def parse(tokens, parse_context)
super do |end_tag_name, end_tag_params|
super do |end_tag_name, _end_tag_params|
unknown_tag(end_tag_name, parse_context) if end_tag_name
end
rescue SyntaxError => e
@@ -17,10 +19,10 @@ module Liquid
def unknown_tag(tag, parse_context)
case tag
when 'else'.freeze, 'end'.freeze
raise SyntaxError.new(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.new(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
end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'set'
module Liquid
@@ -25,7 +27,7 @@ module Liquid
# Catch all for the method
def liquid_method_missing(method)
return nil unless @context && @context.strict_variables
return nil unless @context&.strict_variables
raise Liquid::UndefinedDropMethod, "undefined method #{method}"
end
@@ -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)

View File

@@ -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,11 +22,11 @@ module Liquid
private
def message_prefix
str = ""
if is_a?(SyntaxError)
str << "Liquid syntax error"
str = +""
str << if is_a?(SyntaxError)
"Liquid syntax error"
else
str << "Liquid error"
"Liquid error"
end
if line_number

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid
class Expression
class MethodLiteral
@@ -14,11 +16,11 @@ module Liquid
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' => MethodLiteral.new(:blank?, '').freeze,
'empty' => MethodLiteral.new(:empty?, '').freeze
}.freeze
SINGLE_QUOTED_STRING = /\A'(.*)'\z/m
@@ -33,13 +35,13 @@ module Liquid
else
case markup
when SINGLE_QUOTED_STRING, DOUBLE_QUOTED_STRING
$1
Regexp.last_match(1)
when INTEGERS_REGEX
$1.to_i
Regexp.last_match(1).to_i
when RANGES_REGEX
RangeLookup.parse($1, $2)
RangeLookup.parse(Regexp.last_match(1), Regexp.last_match(2))
when FLOATS_REGEX
$1.to_f
Regexp.last_match(1).to_f
else
VariableLookup.parse(markup)
end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'time'
require 'date'

View File

@@ -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,7 +46,7 @@ module Liquid
class LocalFileSystem
attr_accessor :root
def initialize(root, pattern = "_%s.liquid".freeze)
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 =~ /\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)

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid
class ForloopDrop < Drop
def initialize(name, length, parentloop)

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'yaml'
module Liquid
@@ -26,13 +28,13 @@ module Liquid
def interpolate(name, vars)
name.gsub(/%\{(\w+)\}/) do
# raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym]
(vars[$1.to_sym]).to_s
(vars[Regexp.last_match(1).to_sym]).to_s
end
end
def deep_fetch_translation(name)
name.split('.'.freeze).reduce(locale) do |level, cur|
level[cur] or raise TranslationError, "Translation for #{name} does not exist in locale #{path}"
name.split('.').reduce(locale) do |level, cur|
level[cur] || raise(TranslationError, "Translation for #{name} does not exist in locale #{path}")
end
end
end

View File

@@ -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

View File

@@ -1,17 +1,19 @@
# 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-]*\??/
SINGLE_STRING_LITERAL = /'[^\']*'/
@@ -31,16 +33,21 @@ module Liquid
until @ss.eos?
@ss.skip(WHITESPACE_OR_NOTHING)
break if @ss.eos?
tok = case
when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t]
when t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t]
when t = @ss.scan(DOUBLE_STRING_LITERAL) then [:string, t]
when t = @ss.scan(NUMBER_LITERAL) then [:number, t]
when t = @ss.scan(IDENTIFIER) then [:id, t]
when 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]
if (s = SPECIALS[c])
[s, c]
else
raise SyntaxError, "Unexpected character #{c}"

View File

@@ -22,5 +22,8 @@
tag_never_closed: "'%{block_name}' tag was never closed"
meta_syntax_error: "Liquid syntax error: #{e.message}"
table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
render: "Syntax error in tag 'render' - Template name must be a quoted string"
argument:
include: "Argument error in tag 'include' - Illegal template name"
disabled:
tag: "usage is not allowed in this context"

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid
class ParseContext
attr_accessor :locale, :line_number, :trim_whitespace, :depth
@@ -19,7 +21,6 @@ module Liquid
@partial = value
@options = value ? partial_options : @template_options
@error_mode = @options[:error_mode] || Template.error_mode
value
end
def partial_options
@@ -28,7 +29,7 @@ module Liquid
if dont_pass == true
{ locale: locale }
elsif dont_pass.is_a?(Array)
@template_options.reject { |k, v| dont_pass.include?(k) }
@template_options.reject { |k, _v| dont_pass.include?(k) }
else
@template_options
end

View File

@@ -28,7 +28,7 @@ module Liquid
item, new_context = @callbacks[node.class].call(node, context)
[
item,
ParseTreeVisitor.for(node, @callbacks).visit(new_context || context)
ParseTreeVisitor.for(node, @callbacks).visit(new_context || context),
]
end
end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid
class Parser
def initialize(input)
@@ -51,7 +53,7 @@ module Liquid
token = @tokens[@p]
if token[0] == :id
variable_signature
elsif SINGLE_TOKEN_EXPRESSION_TYPES.include? token[0]
elsif SINGLE_TOKEN_EXPRESSION_TYPES.include?(token[0])
consume
elsif token.first == :open_round
consume
@@ -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

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid
module ParserSwitching
def parse_with_selected_parser(markup)

View File

@@ -0,0 +1,20 @@
# frozen_string_literal: true
module Liquid
class PartialCache
def self.load(template_name, context:, parse_context:)
cached_partials = (context.registers[:cached_partials] ||= {})
cached = cached_partials[template_name]
return cached if cached
file_system = (context.registers[:file_system] ||= Liquid::Template.file_system)
source = file_system.read_template_file(template_name)
parse_context.partial = true
partial = Liquid::Template.parse(source, parse_context)
cached_partials[template_name] = partial
ensure
parse_context.partial = false
end
end
end

View File

@@ -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
@@ -63,6 +65,17 @@ module Liquid
def finish
@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,8 +109,8 @@ 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)
@timing_stack = [@root_timing]

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid
class BlockBody
def render_node_with_profiling(context, output, node)

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid
class RangeLookup
def self.parse(start_markup, end_markup)
@@ -15,8 +17,6 @@ module Liquid
@end_obj = end_obj
end
attr_reader :start_obj, :end_obj
def evaluate(context)
start_int = to_integer(context.evaluate(@start_obj))
end_int = to_integer(context.evaluate(@end_obj))

6
lib/liquid/register.rb Normal file
View File

@@ -0,0 +1,6 @@
# frozen_string_literal: true
module Liquid
class Register
end
end

View File

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

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid
class ResourceLimits
attr_accessor :render_length, :render_score, :assign_score,

View File

@@ -1,20 +1,22 @@
# frozen_string_literal: true
require 'cgi'
require 'bigdecimal'
module Liquid
module StandardFilters
HTML_ESCAPE = {
'&'.freeze => '&amp;'.freeze,
'>'.freeze => '&gt;'.freeze,
'<'.freeze => '&lt;'.freeze,
'"'.freeze => '&quot;'.freeze,
"'".freeze => '&#39;'.freeze
'&' => '&amp;',
'>' => '&gt;',
'<' => '&lt;',
'"' => '&quot;',
"'" => '&#39;',
}.freeze
HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/
STRIP_HTML_BLOCKS = Regexp.union(
/<script.*?<\/script>/m,
%r{<script.*?</script>}m,
/<!--.*?-->/m,
/<style.*?<\/style>/m
%r{<style.*?</style>}m
)
STRIP_HTML_TAGS = /<.*?>/m
@@ -72,7 +74,7 @@ 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)
@@ -82,13 +84,13 @@ module Liquid
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
wordlist.length > l ? wordlist[0..l].join(" ").concat(truncate_string.to_s) : input
end
# Split input string into an array of substrings separated by given pattern.
@@ -113,7 +115,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 +123,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 +154,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 +174,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 +196,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 +213,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 +236,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 +252,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
@@ -276,9 +278,9 @@ module Liquid
def concat(input, array)
unless array.respond_to?(:to_ary)
raise ArgumentError.new("concat filter requires an array argument")
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 +290,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 +327,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,18 +421,28 @@ 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?
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.new("cannot select the property '#{property}'")
raise Liquid::ArgumentError, "cannot select the property '#{property}'"
end
def apply_operation(input, operand, operation)
@@ -457,7 +469,8 @@ module Liquid
class InputIterator
include Enumerable
def initialize(input)
def initialize(input, context)
@context = context
@input = if input.is_a?(Array)
input.flatten
elsif input.is_a?(Hash)
@@ -496,6 +509,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

View File

@@ -0,0 +1,36 @@
# frozen_string_literal: true
module Liquid
class StaticRegisters
attr_reader :static, :registers
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
def fetch(key, default = nil)
key?(key) ? self[key] : default
end
def key?(key)
@registers.key?(key) || @static.key?(key)
end
end
end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'set'
module Liquid
@@ -27,7 +29,7 @@ module Liquid
def self.add_filter(filter)
raise ArgumentError, "Expected module but got: #{filter.class}" unless filter.is_a?(Module)
unless self.include?(filter)
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(', ')}"
@@ -54,7 +56,7 @@ module Liquid
def invoke(method, *args)
if self.class.invokable?(method)
send(method, *args)
elsif @context && @context.strict_filters
elsif @context&.strict_filters
raise Liquid::UndefinedFilter, "undefined filter #{method}"
else
args.first

View File

@@ -1,632 +0,0 @@
require 'ap'
require 'pry'
require 'stackprof'
AwesomePrint.defaults = {
raw: true
}
module Liquid
Liquid::BlockBody.class_eval do
def render_to_output_buffer(context, output)
ruby = Compiler.compile(@nodelist)
if false
puts
puts "--------------------------- GENERATED RUBY -"
line_number = 1
puts(ruby.lines.map do |line|
"#{line_number}\t#{line}".tap { line_number += 1}
end)
puts "--------------------------- /GENERATED RUBY -"
end
instructions = RubyVM::InstructionSequence.compile(ruby)
output_io = StringIO.new
instructions.eval.call(output_io, context, Condition)
output << output_io.string
end
end
class SuperfluidError < Exception
end
class Output
attr_reader :string, :indent_level
def initialize(initial_indent)
@string = ''.dup
@indent_level = initial_indent
@indent_str = " " * initial_indent * 2
end
def line(string)
@string << @indent_str << string << "\n"
end
def echo(string)
output << "liquid_out.write(to_output(#{string}))"
end
def indent(&block)
output.indent(&block)
end
def indent
@indent_level += 1
@indent_str = " " * @indent_level * 2
yield
@indent_level -= 1
@indent_str = " " * @indent_level * 2
end
end
class Compiler
class << self
def compile(template)
compiler = new
compiler.compile(template)
compiler.ruby
end
end
def initialize
@variables = Set.new
@output = Output.new(2)
@blank = false
end
def ruby
[
header,
@output.string,
trailer
].join("\n")
end
def header
<<~RUBY
module Warning
def warn(*)
end
end
class ForloopDrop
def initialize(name, length, parentloop)
@name = name
@length = length
@parentloop = parentloop
@index = 0
end
attr_accessor :parentloop
def [](value)
case value
when "length"
@length
when "name"
@name
when "index"
@index + 1
when "index0"
@index
when "rindex"
@length - @index
when "rindex0"
@length - @index - 1
when "first"
@index == 0
when "last"
@index == @length - 1
when "parentloop"
@parentloop
end
end
def to_liquid
self
end
def key?(*)
true
end
private
def increment!
@index += 1
end
end
def slice_collection(collection, from, limit)
to = if limit.nil?
nil
else
limit + from
end
if (from != 0 || !to.nil?) && collection.respond_to?(:load_slice)
collection.load_slice(from, to)
else
slice_collection_using_each(collection, from, to)
end
end
def slice_collection_using_each(collection, from, to)
segments = []
index = 0
if collection.is_a?(String)
return collection.empty? ? [] : [collection]
end
return [] unless collection.respond_to?(:each)
collection.each do |item|
if to && to <= index
break
end
if from <= index
segments << item
end
index += 1
end
segments
end
def apply_operator(left, operator, right)
if left.respond_to?(operator) && right.respond_to?(operator) && !left.is_a?(Hash) && !right.is_a?(Hash)
begin
left.send(operator, right)
rescue ::ArgumentError => e
raise @context.raise_argument_error(e.message)
end
end
end
def contains?(left, right)
if left && right && left.respond_to?(:include?)
right = right.to_s if left.is_a?(String)
left.include?(right)
else
false
end
end
class GlobalVariableLookup
def method_missing(method_name, *)
nil
end
def to_output(value)
output = if value.is_a?(Array)
value.join
elsif value == nil
else
value.to_s
end
@context.apply_global_filter(output)
end
def run(liquid_out, context, condition)
@condition = condition
@context = context
@for_offsets = {}
@cycle_values = {}
@context.registers[:for_stack] = []
@context.registers[:cycle] ||= {}
@if_changed_last = nil
@prev_output_size = 0
#{hoisted_variables}
RUBY
end
def trailer
<<~RUBY
end
end
GlobalVariableLookup.new.method(:run)
RUBY
end
def hoisted_variables
@variables.map do |variable|
normal_name = unvar(variable)
"#{variable} = @context.find_variable(#{normal_name.inspect}, raise_on_not_found: false)"
end.join("\n")
end
def compile(node)
old_blank = @blank
@blank = if node.respond_to?(:blank?)
node.blank?
elsif !(node.is_a?(String) && node =~ /\A\s*\z/)
false
else
@blank
end
case node
when Liquid::Document, Liquid::BlockBody
node.nodelist.collect(&method(:compile))
when Array
node.collect(&method(:compile))
when Liquid::Variable
compile_variable(node)
when Liquid::For
compile_for(node)
when Liquid::If
compile_if(node)
when Liquid::Ifchanged
compile_if_changed(node)
when Liquid::Template
compile(node.root)
when Liquid::Assign
compile_assign(node)
when Liquid::Case
compile_case(node)
when Liquid::Capture
compile_capture(node)
when String
compile_echo_literal(node)
when Liquid::Break
line "break" if @in_loop
when Liquid::Continue
line "next" if @in_loop
when Liquid::Cycle
compile_cycle(node)
when Liquid::Raw
compile_raw(node)
when Liquid::Increment
compile_increment(node)
when Liquid::Decrement
compile_decrement(node)
when Liquid::Comment
else
raise SuperfluidError, "Unknown node type #{node.inspect}"
end
@blank = old_blank
end
def compile_for(node)
variable_name = node.variable_name
collection_name = node.collection_name
iter_target_expr = case collection_name
when Liquid::VariableLookup
make_variable_lookup_expr(collection_name)
when Range
collection_name
when Liquid::RangeLookup
start_expr = make_variable_expr(collection_name.start_obj)
end_expr = make_variable_expr(collection_name.end_obj)
line "start = #{start_expr}"
line "@context.raise_argument_error('bad value for range') unless start.respond_to?(:to_i)"
line "start = #{start_expr}.to_i"
line "finish = #{end_expr}"
line "@context.raise_argument_error('bad value for range') unless finish.respond_to?(:to_i)"
line "finish = #{end_expr}.to_i"
"start..finish"
when Liquid::Expression::MethodLiteral
'[]'
else
raise SuperfluidError, "Unknown iteration target: #{collection_name.inspect}"
end
from_expr = if node.from == :continue
"@for_offsets['#{node.name}'].to_i"
elsif node.from
make_variable_expr(node.from)
else
'0'
end
limit_expr = node.limit ? make_variable_expr(node.limit) : 'nil'
line "from = #{from_expr}"
line "limit = #{limit_expr}"
line "@context.raise_argument_error('invalid integer') unless from.is_a?(Integer)"
line "@context.raise_argument_error('invalid integer') unless !limit || limit.is_a?(Integer)"
line "segment = slice_collection(#{iter_target_expr}, from, limit)"
line "segment.reverse!" if node.reversed
forloop = var('forloop')
hoist_var('forloop')
line "#{forloop} = ForloopDrop.new('#{node.name}', segment.length, #{forloop})"
line "if segment.any?"
indent do
line "segment.each do |#{var(variable_name)}|"
indent do
line "@context['forloop'] = #{forloop}"
old_in_loop = @in_loop
compile(node.for_block)
@in_loop = old_in_loop
line "#{forloop}.send(:increment!)"
end
line "end"
end
line "else"
indent do
compile(node.else_block) if node.else_block
end
line "end"
line "@for_offsets['#{node.name}'] = from + segment.length"
line "#{forloop} = #{forloop}.parentloop"
end
def compile_if(node)
if_condition = node.blocks.first
line "if #{make_condition_expr(if_condition)}"
indent { if_condition.attachment.nodelist.each(&method(:compile)) }
node.blocks.drop(1).each do |condition|
if condition.left != nil
line "elsif #{make_condition_expr(condition)}"
else
line "else"
end
indent { condition.attachment.nodelist.each(&method(:compile)) }
end
line "end"
end
def compile_if_changed(node)
line "if_changed = lambda do |; liquid_out|"
indent do
line "liquid_out = StringIO.new"
node.nodelist.each(&method(:compile))
line "liquid_out.string"
end
line "end.call"
line "if if_changed != @if_changed_last"
indent { echo "if_changed" }
line "end"
line "@if_changed_last = if_changed"
end
def compile_capture(node)
line "#{var(node.to)} = lambda do |; liquid_out|"
hoist_var(node.to)
indent do
line "liquid_out = StringIO.new"
node.nodelist.each(&method(:compile))
line "liquid_out.string"
end
line "end.call"
end
def make_condition_expr(node)
condition = make_sub_condition_expr(node)
if node.child_condition
"(#{condition} #{node.child_relation} #{make_condition_expr(node.child_condition)})"
else
condition
end
end
def make_sub_condition_expr(node)
return make_variable_expr(node.left) unless node.operator
operator = node.operator
operator = "!=" if operator == "<>"
if operator == "=="
if node.left.is_a?(Liquid::Expression::MethodLiteral) &&
node.right.is_a?(Liquid::Expression::MethodLiteral)
return "false"
elsif node.right.is_a?(Liquid::Expression::MethodLiteral)
target = make_variable_expr(node.left)
message = node.right.method_name.inspect
return "#{target}.respond_to?(#{message}) ? #{target}.send(#{message}) : nil"
elsif node.left.is_a?(Liquid::Expression::MethodLiteral)
target = make_variable_expr(node.right)
message = node.left.method_name.inspect
return "#{target}.respond_to?(#{message}) ? #{target}.send(#{message}) : nil"
end
end
left = make_variable_expr(node.left)
right = make_variable_expr(node.right)
case operator
when "contains"
"contains?(#{left}, #{right})"
else
"apply_operator(#{left}, #{operator.inspect}, #{right})"
end
end
def compile_case(node)
line 'if false' # HACK
else_nodes, if_nodes = node.blocks.partition { |n| n.is_a?(Liquid::ElseCondition) }
raise SuperfluidError, 'Too many else nodes' if else_nodes.count > 1
else_node = else_nodes.first
if_nodes.each do |condition|
left = make_variable_expr(condition.left)
right = make_variable_expr(condition.right)
line "elsif #{left} #{condition.operator} #{right}"
indent { condition.attachment.nodelist.each(&method(:compile)) }
end
if else_node
line "else"
indent { else_node.attachment.nodelist.each(&method(:compile)) }
end
line "end"
end
def compile_cycle(node)
key = node.name
key = key.name if key.is_a?(Liquid::VariableLookup)
line "key = #{key.inspect}"
line "iteration = context.registers[:cycle][key].to_i"
line "@cycle_values[key] ||= #{node.variables}"
line "val = @cycle_values[key][iteration]"
echo 'val'
line "context.registers[:cycle][key] = (iteration + 1) % #{node.variables.size}"
end
def compile_raw(node)
echo "#{node.body.inspect}"
end
def compile_increment(node)
line "value = context.environments.first[#{node.variable.inspect}] ||= 0"
line "@context.environments.first[#{node.variable.inspect}] = value + 1"
echo "value"
end
def compile_decrement(node)
line "value = context.environments.first[#{node.variable.inspect}] ||= 0"
line "value -= 1"
line "@context.environments.first[#{node.variable.inspect}] = value"
echo "value"
end
def compile_echo_literal(node)
echo node.inspect
end
def compile_variable(variable)
echo make_variable_expr(variable)
end
def compile_assign(node)
from_expr = case node.from
when Liquid::Variable
make_variable_expr(node.from)
else
raise SuperfluidError, "Unknown assignment `from`: #{node.from.inspect}"
end
line "#{var(node.to)} = #{from_expr}"
hoist_var(node.to)
end
def make_variable_expr(variable)
case variable
when Liquid::Variable
base_expression = case variable.name
when TrueClass, FalseClass, Numeric, String
variable.name.inspect
when Liquid::VariableLookup
make_variable_lookup_expr(variable.name)
when NilClass, Liquid::Expression::MethodLiteral
'nil'
else
raise SuperfluidError, "Invalid variable name: #{variable.name.inspect}"
derp "Bad var name", variable.name
end
variable.filters.inject(base_expression) do |inner, (filter_name, positional_args, keyword_args)|
filter_args = positional_args.map(&method(:make_variable_expr))
if keyword_args
filter_args << "{ " + keyword_args
.transform_values(&method(:make_variable_expr))
.collect { |(key, value)| "#{key.inspect} => #{value}" }
.join(", ") + " }"
end
"context.strainer.invoke(#{filter_name.inspect}, #{inner}, *[#{filter_args.join(", ")}])"
end
when Liquid::VariableLookup
make_variable_lookup_expr(variable)
when TrueClass, FalseClass, Numeric, String
variable.inspect
when NilClass
'nil'
else
raise SuperfluidError, "Unknown expression type: #{variable.inspect}"
end
end
def make_variable_lookup_expr(variable_lookup)
base_expr = var(variable_lookup.name)
hoist_var(variable_lookup.name)
return base_expr if variable_lookup.lookups.empty?
expr = Output.new(output.indent_level)
expr.line "(begin"
expr.indent do
expr.line "inner = #{base_expr}"
variable_lookup.lookups.each_with_index do |lookup, i|
lookup_expr = lookup.inspect
expr.line "inner = if inner.respond_to?(:[]) && ((inner.respond_to?(:key?) && inner.key?(#{lookup_expr})) || (inner.respond_to?(:fetch) && #{lookup_expr}.is_a?(Integer)))"
expr.indent do
expr.line "inner[#{lookup_expr}].to_liquid"
end
if variable_lookup.command_flags & (1 << i) != 0
expr.line "elsif inner.respond_to?(#{lookup.inspect})"
expr.indent do
expr.line "inner.#{lookup}.to_liquid"
end
end
expr.line "end"
end
end
expr.line "end)"
expr.string.strip
end
private
def line(string)
output.line(string)
end
def echo(string)
unless @blank
line "liquid_out.write(to_output(#{string}))"
end
end
def indent(&block)
output.indent(&block)
end
def var(name)
name = name
.gsub('_', '__')
.gsub('-', '_')
"__liquid_#{name}"
end
def unvar(name)
name
.delete_prefix('__liquid_')
.gsub(/([^_])_([^_])/) { "#$1-#$2" }
.gsub('__', '_')
end
def hoist_var(name)
@variables << var(name)
end
attr_reader :output
end
end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid
class TablerowloopDrop < Drop
def initialize(length, cols)

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid
class Tag
attr_reader :nodelist, :tag_name, :line_number, :parse_context
@@ -5,13 +7,21 @@ module Liquid
include ParserSwitching
class << self
def parse(tag_name, markup, tokenizer, options)
tag = new(tag_name, markup, options)
def parse(tag_name, markup, tokenizer, parse_context)
tag = new(tag_name, markup, parse_context)
tag.parse(tokenizer)
tag
end
def disable_tags(*tags)
disabled_tags.push(*tags)
end
private :new
def disabled_tags
@disabled_tags ||= []
end
end
def initialize(tag_name, markup, parse_context)
@@ -33,7 +43,15 @@ module Liquid
end
def render(_context)
''.freeze
''
end
def disabled?(context)
context.registers[:disabled_tags].disabled?(tag_name)
end
def disabled_error_message
"#{tag_name} #{options[:locale].t('errors.disabled.tag')}"
end
# For backwards compatibility with custom tags. In a future release, the semantics
@@ -47,5 +65,9 @@ module Liquid
def blank?
false
end
def disabled_tags
self.class.disabled_tags
end
end
end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid
# Assign sets a variable in your template.
#
@@ -10,15 +12,19 @@ module Liquid
class Assign < Tag
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
def self.syntax_error_translation_key
"errors.syntax.assign"
end
attr_reader :to, :from
def initialize(tag_name, markup, options)
super
if markup =~ Syntax
@to = $1
@from = Variable.new($2, options)
@to = Regexp.last_match(1)
@from = Variable.new(Regexp.last_match(2), options)
else
raise SyntaxError.new options[:locale].t("errors.syntax.assign".freeze)
raise SyntaxError, options[:locale].t(self.class.syntax_error_translation_key)
end
end
@@ -55,5 +61,5 @@ module Liquid
end
end
Template.register_tag('assign'.freeze, Assign)
Template.register_tag('assign', Assign)
end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid
# Break tag to be used to break out of a for loop.
#
@@ -14,5 +16,5 @@ module Liquid
end
end
Template.register_tag('break'.freeze, Break)
Template.register_tag('break', Break)
end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid
# Capture stores the result of a block into a variable without rendering it inplace.
#
@@ -13,14 +15,12 @@ module Liquid
class Capture < Block
Syntax = /(#{VariableSignature}+)/o
attr_reader :to
def initialize(tag_name, markup, options)
super
if markup =~ Syntax
@to = $1
@to = Regexp.last_match(1)
else
raise SyntaxError.new(options[:locale].t("errors.syntax.capture"))
raise SyntaxError, options[:locale].t("errors.syntax.capture")
end
end
@@ -35,8 +35,7 @@ module Liquid
def blank?
true
end
end
Template.register_tag('capture'.freeze, Capture)
Template.register_tag('capture', Capture)
end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid
class Case < Block
Syntax = /(#{QuotedFragment})/o
@@ -10,17 +12,15 @@ module Liquid
@blocks = []
if markup =~ Syntax
@left = Expression.parse($1)
@left = Expression.parse(Regexp.last_match(1))
else
raise SyntaxError.new(options[:locale].t("errors.syntax.case".freeze))
raise SyntaxError, options[:locale].t("errors.syntax.case")
end
end
def parse(tokens)
body = BlockBody.new
while parse_body(body, tokens)
body = @blocks.last.attachment
end
body = @blocks.last.attachment while parse_body(body, tokens)
end
def nodelist
@@ -29,9 +29,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
@@ -39,16 +39,14 @@ module Liquid
end
def render_to_output_buffer(context, output)
context.stack do
execute_else_block = true
execute_else_block = true
@blocks.each do |block|
if block.else?
block.attachment.render_to_output_buffer(context, output) if execute_else_block
elsif block.evaluate(context)
execute_else_block = false
block.attachment.render_to_output_buffer(context, output)
end
@blocks.each do |block|
if block.else?
block.attachment.render_to_output_buffer(context, output) if execute_else_block
elsif block.evaluate(context)
execute_else_block = false
block.attachment.render_to_output_buffer(context, output)
end
end
@@ -62,12 +60,12 @@ module Liquid
while markup
unless markup =~ WhenSyntax
raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_when".freeze))
raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_when")
end
markup = $2
markup = Regexp.last_match(2)
block = Condition.new(@left, '=='.freeze, Expression.parse($1))
block = Condition.new(@left, '==', Expression.parse(Regexp.last_match(1)))
block.attach(body)
@blocks << block
end
@@ -75,7 +73,7 @@ module Liquid
def record_else_condition(markup)
unless markup.strip.empty?
raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_else".freeze))
raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_else")
end
block = ElseCondition.new
@@ -90,5 +88,5 @@ module Liquid
end
end
Template.register_tag('case'.freeze, Case)
Template.register_tag('case', Case)
end

View File

@@ -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

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid
# Continue tag to be used to break out of a for loop.
#
@@ -14,5 +16,5 @@ module Liquid
end
end
Template.register_tag('continue'.freeze, Continue)
Template.register_tag('continue', Continue)
end

View File

@@ -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.
#
@@ -15,44 +17,42 @@ module Liquid
SimpleSyntax = /\A#{QuotedFragment}+/o
NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om
attr_reader :variables, :name
attr_reader :variables
def initialize(tag_name, markup, options)
super
case markup
when NamedSyntax
@variables = variables_from_string($2)
@name = Expression.parse($1)
@variables = variables_from_string(Regexp.last_match(2))
@name = Expression.parse(Regexp.last_match(1))
when SimpleSyntax
@variables = variables_from_string(markup)
@name = @variables.to_s
else
raise SyntaxError.new(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] ||= {}
context.stack do
key = context.evaluate(@name)
iteration = context.registers[:cycle][key].to_i
key = context.evaluate(@name)
iteration = context.registers[:cycle][key].to_i
val = context.evaluate(@variables[iteration])
val = context.evaluate(@variables[iteration])
if val.is_a?(Array)
val = val.join
elsif !val.is_a?(String)
val = val.to_s
end
output << val
iteration += 1
iteration = 0 if iteration >= @variables.size
context.registers[:cycle][key] = iteration
if val.is_a?(Array)
val = val.join
elsif !val.is_a?(String)
val = val.to_s
end
output << val
iteration += 1
iteration = 0 if iteration >= @variables.size
context.registers[:cycle][key] = iteration
output
end
@@ -61,7 +61,7 @@ module Liquid
def variables_from_string(markup)
markup.split(',').collect do |var|
var =~ /\s*(#{QuotedFragment})\s*/o
$1 ? Expression.parse($1) : nil
Regexp.last_match(1) ? Expression.parse(Regexp.last_match(1)) : nil
end.compact
end

View File

@@ -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
@@ -23,8 +25,6 @@ module Liquid
@variable = markup.strip
end
attr_reader :variable
def render_to_output_buffer(context, output)
value = context.environments.first[@variable] ||= 0
value -= 1
@@ -34,5 +34,5 @@ module Liquid
end
end
Template.register_tag('decrement'.freeze, Decrement)
Template.register_tag('decrement', Decrement)
end

26
lib/liquid/tags/echo.rb Normal file
View File

@@ -0,0 +1,26 @@
# frozen_string_literal: true
module Liquid
# Echo outputs an expression
#
# {% echo monkey %}
# {% echo user.name %}
#
# This is identical to variable output syntax, like {{ foo }}, but works
# inside {% liquid %} tags. The full syntax is supported, including filters:
#
# {% echo user | link %}
#
class Echo < Tag
def initialize(tag_name, markup, parse_context)
super
@variable = Variable.new(markup, parse_context)
end
def render(context)
@variable.render_to_output_buffer(context, +'')
end
end
Template.register_tag('echo', Echo)
end

View File

@@ -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.
@@ -46,7 +48,7 @@ module Liquid
class For < Block
Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
attr_reader :collection_name, :variable_name, :limit, :from, :for_block, :else_block, :name, :reversed
attr_reader :collection_name, :variable_name, :limit, :from
def initialize(tag_name, markup, options)
super
@@ -66,7 +68,7 @@ module Liquid
end
def unknown_tag(tag, markup, tokens)
return super unless tag == 'else'.freeze
return super unless tag == 'else'
@else_block = BlockBody.new
end
@@ -86,31 +88,31 @@ module Liquid
def lax_parse(markup)
if markup =~ Syntax
@variable_name = $1
collection_name = $2
@reversed = !!$3
@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.new(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.new(options[:locale].t("errors.syntax.for_invalid_in".freeze)) unless p.id?('in'.freeze)
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in')
collection_name = p.expression
@name = "#{@variable_name}-#{collection_name}"
@collection_name = Expression.parse(collection_name)
@reversed = p.id?('reversed'.freeze)
@reversed = p.id?('reversed')
while p.look(:id) && p.look(:colon, 1)
unless attribute = p.id?('limit'.freeze) || p.id?('offset'.freeze)
raise SyntaxError.new(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)
@@ -162,7 +164,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
@@ -170,11 +172,10 @@ module Liquid
loop_vars.send(:increment!)
# Handle any interrupts if they exist.
if context.interrupt?
interrupt = context.pop_interrupt
break if interrupt.is_a? BreakInterrupt
next if interrupt.is_a? ContinueInterrupt
end
next unless context.interrupt?
interrupt = context.pop_interrupt
break if interrupt.is_a?(BreakInterrupt)
next if interrupt.is_a?(ContinueInterrupt)
end
ensure
for_stack.pop
@@ -186,13 +187,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
@@ -212,5 +213,5 @@ module Liquid
end
end
Template.register_tag('for'.freeze, For)
Template.register_tag('for', For)
end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid
# If is the conditional block
#
@@ -19,7 +21,7 @@ module Liquid
def initialize(tag_name, markup, options)
super
@blocks = []
push_block('if'.freeze, markup)
push_block('if', markup)
end
def nodelist
@@ -32,7 +34,7 @@ module Liquid
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
@@ -40,11 +42,9 @@ module Liquid
end
def render_to_output_buffer(context, output)
context.stack do
@blocks.each do |block|
if block.evaluate(context)
return block.attachment.render_to_output_buffer(context, output)
end
@blocks.each do |block|
if block.evaluate(context)
return block.attachment.render_to_output_buffer(context, output)
end
end
@@ -54,7 +54,7 @@ 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)
@@ -66,17 +66,17 @@ module Liquid
def lax_parse(markup)
expressions = markup.scan(ExpressionsAndOperators)
raise(SyntaxError.new(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($1), $2, Expression.parse($3))
condition = Condition.new(Expression.parse(Regexp.last_match(1)), Regexp.last_match(2), Expression.parse(Regexp.last_match(3)))
until expressions.empty?
operator = expressions.pop.to_s.strip
raise(SyntaxError.new(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($1), $2, Expression.parse($3))
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless BOOLEAN_OPERATORS.include?(operator)
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") unless BOOLEAN_OPERATORS.include?(operator)
new_condition.send(operator, condition)
condition = new_condition
end
@@ -94,7 +94,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
@@ -104,7 +104,7 @@ module Liquid
def parse_comparison(p)
a = Expression.parse(p.expression)
if op = p.consume?(:comparison)
if (op = p.consume?(:comparison))
b = Expression.parse(p.expression)
Condition.new(a, op, b)
else
@@ -119,5 +119,5 @@ module Liquid
end
end
Template.register_tag('if'.freeze, If)
Template.register_tag('if', If)
end

View File

@@ -1,19 +1,19 @@
# frozen_string_literal: true
module Liquid
class Ifchanged < Block
def render_to_output_buffer(context, output)
context.stack do
block_output = ''
super(context, block_output)
block_output = +''
super(context, block_output)
if block_output != context.registers[:ifchanged]
context.registers[:ifchanged] = block_output
output << block_output
end
if block_output != context.registers[:ifchanged]
context.registers[:ifchanged] = block_output
output << block_output
end
output
end
end
Template.register_tag('ifchanged'.freeze, Ifchanged)
Template.register_tag('ifchanged', Ifchanged)
end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid
# Include allows templates to relate with other templates
#
@@ -14,18 +16,20 @@ module Liquid
# {% include 'product' for products %}
#
class Include < Tag
Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/o
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 = $1
variable_name = $3
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 = {}
@@ -35,7 +39,7 @@ module Liquid
end
else
raise SyntaxError.new(options[:locale].t("errors.syntax.include".freeze))
raise SyntaxError, options[:locale].t("errors.syntax.include")
end
end
@@ -44,10 +48,15 @@ module Liquid
def render_to_output_buffer(context, output)
template_name = context.evaluate(@template_name_expr)
raise ArgumentError.new(options[:locale].t("errors.argument.include")) unless template_name
raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name
partial = load_cached_partial(template_name, context)
context_variable_name = template_name.split('/'.freeze).last
partial = PartialCache.load(
template_name,
context: context,
parse_context: parse_context
)
context_variable_name = @alias_name || template_name.split('/').last
variable = if @variable_name_expr
context.evaluate(@variable_name_expr)
@@ -83,44 +92,18 @@ module Liquid
output
end
private
alias_method :parse_context, :options
private :parse_context
def load_cached_partial(template_name, context)
cached_partials = context.registers[:cached_partials] || {}
if cached = cached_partials[template_name]
return cached
end
source = read_template_from_file_system(context)
begin
parse_context.partial = true
partial = Liquid::Template.parse(source, parse_context)
ensure
parse_context.partial = false
end
cached_partials[template_name] = partial
context.registers[:cached_partials] = cached_partials
partial
end
def read_template_from_file_system(context)
file_system = context.registers[:file_system] || Liquid::Template.file_system
file_system.read_template_file(context.evaluate(@template_name_expr))
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[
@node.template_name_expr,
@node.variable_name_expr
@node.variable_name_expr,
] + @node.attributes.values
end
end
end
Template.register_tag('include'.freeze, Include)
Template.register_tag('include', Include)
end

View File

@@ -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
@@ -20,15 +22,14 @@ module Liquid
@variable = markup.strip
end
attr_reader :variable
def render_to_output_buffer(context, output)
value = context.environments.first[@variable] ||= 0
context.environments.first[@variable] = value + 1
output << value.to_s
output
end
end
Template.register_tag('increment'.freeze, Increment)
Template.register_tag('increment', Increment)
end

View File

@@ -1,10 +1,10 @@
# frozen_string_literal: true
module Liquid
class Raw < Block
Syntax = /\A\s*\z/
FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
attr_reader :body
def initialize(tag_name, markup, parse_context)
super
@@ -12,16 +12,16 @@ module Liquid
end
def parse(tokens)
@body = ''
while token = tokens.shift
@body = +''
while (token = tokens.shift)
if token =~ FullTokenPossiblyInvalid
@body << $1 if $1 != "".freeze
return if block_delimiter == $2
@body << Regexp.last_match(1) if Regexp.last_match(1) != ""
return if block_delimiter == Regexp.last_match(2)
end
@body << token unless token.empty?
end
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_never_closed", block_name: block_name)
end
def render_to_output_buffer(_context, output)
@@ -40,11 +40,11 @@ module Liquid
protected
def ensure_valid_markup(tag_name, markup, parse_context)
unless markup =~ Syntax
raise SyntaxError.new(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

73
lib/liquid/tags/render.rb Normal file
View File

@@ -0,0 +1,73 @@
# frozen_string_literal: true
module Liquid
class Render < Tag
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") unless 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 = {}
markup.scan(TagAttributes) do |key, value|
@attributes[key] = Expression.parse(value)
end
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
partial = PartialCache.load(
template_name,
context: context,
parse_context: parse_context
)
context_variable_name = @alias_name || template_name.split('/').last
render_partial_func = ->(var) {
inner_context = context.new_isolated_subcontext
inner_context.template_name = template_name
inner_context.partial = true
@attributes.each do |key, value|
inner_context[key] = context.evaluate(value)
end
inner_context[context_variable_name] = var unless var.nil?
partial.render_to_output_buffer(inner_context, output)
}
variable = @variable_name_expr ? context.evaluate(@variable_name_expr) : nil
variable.is_a?(Array) ? variable.each(&render_partial_func) : render_partial_func.call(variable)
output
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[
@node.template_name_expr,
] + @node.attributes.values
end
end
end
Template.register_tag('render', Render)
end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid
class TableRow < Block
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
@@ -7,33 +9,33 @@ module Liquid
def initialize(tag_name, markup, options)
super
if markup =~ Syntax
@variable_name = $1
@collection_name = Expression.parse($2)
@variable_name = Regexp.last_match(1)
@collection_name = Expression.parse(Regexp.last_match(2))
@attributes = {}
markup.scan(TagAttributes) do |key, value|
@attributes[key] = Expression.parse(value)
end
else
raise SyntaxError.new(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) or 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
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 +63,5 @@ module Liquid
end
end
Template.register_tag('tablerow'.freeze, TableRow)
Template.register_tag('tablerow', TableRow)
end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require_relative 'if'
module Liquid
@@ -7,18 +9,16 @@ module Liquid
#
class Unless < If
def render_to_output_buffer(context, output)
context.stack do
# First condition is interpreted backwards ( if not )
first_block = @blocks.first
unless first_block.evaluate(context)
return first_block.attachment.render_to_output_buffer(context, output)
end
# First condition is interpreted backwards ( if not )
first_block = @blocks.first
unless first_block.evaluate(context)
return first_block.attachment.render_to_output_buffer(context, output)
end
# After the first condition unless works just like if
@blocks[1..-1].each do |block|
if block.evaluate(context)
return block.attachment.render_to_output_buffer(context, output)
end
# After the first condition unless works just like if
@blocks[1..-1].each do |block|
if block.evaluate(context)
return block.attachment.render_to_output_buffer(context, output)
end
end
@@ -26,5 +26,5 @@ module Liquid
end
end
Template.register_tag('unless'.freeze, Unless)
Template.register_tag('unless', Unless)
end

View File

@@ -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
@@ -90,6 +92,14 @@ module Liquid
@tags ||= TagRegistry.new
end
def add_register(name, klass)
registers[name.to_sym] = klass
end
def registers
@registers ||= {}
end
def error_mode
@error_mode ||= :lax
end
@@ -165,14 +175,14 @@ 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
c = args.shift
if @rethrow_errors
c.exception_renderer = ->(e) { raise }
c.exception_renderer = ->(_e) { raise }
end
c
@@ -189,26 +199,34 @@ 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]
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
context.add_filters(args.pop)
end
Template.registers.each do |key, register|
context_register[key] = register
end
# Retrying a render resets resource usage
context.resource_limits.reset
begin
# render the nodelist.
# for performance reasons we get an array back here. join will make a string out of it.
with_profiling(context) do
@root.render_to_output_buffer(context, output || '')
@root.render_to_output_buffer(context, output || +'')
end
rescue Liquid::MemoryError => e
context.handle_error(e)
@@ -236,7 +254,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

View File

@@ -1,29 +1,37 @@
# frozen_string_literal: true
module Liquid
class Tokenizer
attr_reader :line_number
attr_reader :line_number, :for_liquid_tag
def initialize(source, line_numbers = false)
def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false)
@source = source
@line_number = line_numbers ? 1 : nil
@line_number = line_number || (line_numbers ? 1 : nil)
@for_liquid_tag = for_liquid_tag
@tokens = tokenize
end
def shift
token = @tokens.shift
@line_number += token.count("\n") if @line_number && token
(token = @tokens.shift) || return
if @line_number
@line_number += @for_liquid_tag ? 1 : token.count("\n")
end
token
end
private
def tokenize
@source = @source.source if @source.respond_to?(:source)
return [] if @source.to_s.empty?
return @source.split("\n") if @for_liquid_tag
tokens = @source.split(TemplateParser)
# removes the rogue empty element at the beginning of the array
tokens.shift if tokens[0] && tokens[0].empty?
tokens.shift if tokens[0]&.empty?
tokens
end

8
lib/liquid/usage.rb Normal file
View File

@@ -0,0 +1,8 @@
# frozen_string_literal: true
module Liquid
module Usage
def self.increment(name)
end
end
end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid
module Utils
def self.slice_collection(collection, from, to)
@@ -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)

View File

@@ -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
@@ -43,11 +45,11 @@ module Liquid
@filters = []
return unless markup =~ MarkupWithQuotedFragment
name_markup = $1
filter_markup = $2
name_markup = Regexp.last_match(1)
filter_markup = Regexp.last_match(2)
@name = Expression.parse(name_markup)
if filter_markup =~ FilterMarkupRegex
filters = $1.scan(FilterParser)
filters = Regexp.last_match(1).scan(FilterParser)
filters.each do |f|
next unless f =~ /\w+/
filtername = Regexp.last_match(0)
@@ -102,13 +104,21 @@ module Liquid
output
end
def disabled?(_context)
false
end
def disabled_tags
[]
end
private
def parse_filter_expressions(filter_name, unparsed_args)
filter_args = []
keyword_args = nil
unparsed_args.each do |a|
if matches = a.match(JustTagAttributes)
if (matches = a.match(JustTagAttributes))
keyword_args ||= {}
keyword_args[matches[1]] = Expression.parse(matches[2])
else
@@ -121,7 +131,7 @@ module Liquid
end
def evaluate_filter_expressions(context, filter_args, filter_kwargs)
parsed_args = filter_args.map{ |expr| context.evaluate(expr) }
parsed_args = filter_args.map { |expr| context.evaluate(expr) }
if filter_kwargs
parsed_kwargs = {}
filter_kwargs.each do |key, expr|

View File

@@ -1,9 +1,11 @@
# 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, :command_flags
attr_reader :name, :lookups
def self.parse(markup)
new(markup)
@@ -14,7 +16,7 @@ module Liquid
name = lookups.shift
if name =~ SQUARE_BRACKETED
name = Expression.parse($1)
name = Expression.parse(Regexp.last_match(1))
end
@name = name
@@ -24,7 +26,7 @@ module Liquid
@lookups.each_index do |i|
lookup = lookups[i]
if lookup =~ SQUARE_BRACKETED
lookups[i] = Expression.parse($1)
lookups[i] = Expression.parse(Regexp.last_match(1))
elsif COMMAND_METHODS.include?(lookup)
@command_flags |= 1 << i
end

View File

@@ -1,5 +1,6 @@
# encoding: utf-8
# frozen_string_literal: true
module Liquid
VERSION = "4.0.3".freeze
VERSION = "4.0.3"
end

View File

@@ -1,7 +1,8 @@
# encoding: utf-8
# frozen_string_literal: true
lib = File.expand_path('../lib/', __FILE__)
$LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require "liquid/version"
@@ -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.1.0"
s.required_ruby_version = ">= 2.4.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 'minitest'
s.add_development_dependency('rake', '~> 11.3')
s.add_development_dependency('minitest')
end

View File

@@ -1,16 +1,9 @@
# frozen_string_literal: true
require 'benchmark/ips'
require_relative 'theme_runner'
case ARGV.first.to_sym
when :lax
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
when :strict
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
when :superfluid
require 'liquid/superfluid'
Liquid::Template.error_mode = :strict
end
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new
Benchmark.ips do |x|

View File

@@ -1,26 +1,63 @@
# frozen_string_literal: true
requirf 'benchmark/ips'
require 'benchmark/ips'
require 'memory_profiler'
require 'terminal-table'
require_relative 'theme_runner'
def profile(phase, &block)
puts
puts "#{phase}:"
puts
class Profiler
LOG_LABEL = "Profiling: ".rjust(14).freeze
REPORTS_DIR = File.expand_path('.memprof', __dir__).freeze
report = MemoryProfiler.report(&block)
def self.run
puts
yield new
end
report.pretty_print(
color_output: true,
scale_bytes: true,
detailed_report: true
)
def initialize
@allocated = []
@retained = []
@headings = []
end
def profile(phase, &block)
print(LOG_LABEL)
print("#{phase}.. ".ljust(10))
report = MemoryProfiler.report(&block)
puts 'Done.'
@headings << phase.capitalize
@allocated << "#{report.scale_bytes(report.total_allocated_memsize)} (#{report.total_allocated} objects)"
@retained << "#{report.scale_bytes(report.total_retained_memsize)} (#{report.total_retained} objects)"
return if ENV['CI']
require 'fileutils'
report_file = File.join(REPORTS_DIR, "#{sanitize(phase)}.txt")
FileUtils.mkdir_p(REPORTS_DIR)
report.pretty_print(to_file: report_file, scale_bytes: true)
end
def tabulate
table = Terminal::Table.new(headings: @headings.unshift('Phase')) do |t|
t << @allocated.unshift('Total allocated')
t << @retained.unshift('Total retained')
end
puts
puts table
puts "\nDetailed report(s) saved to #{REPORTS_DIR}/" unless ENV['CI']
end
def sanitize(string)
string.downcase.gsub(/[\W]/, '-').squeeze('-')
end
end
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new
profile("Parsing") { profiler.compile }
profile("Rendering") { profiler.render }
runner = ThemeRunner.new
Profiler.run do |x|
x.profile('parse') { runner.compile }
x.profile('render') { runner.render }
x.tabulate
end

View File

@@ -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

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
class CommentForm < Liquid::Block
Syntax = /(#{Liquid::VariableSignature}+)/
@@ -5,10 +7,10 @@ class CommentForm < Liquid::Block
super
if markup =~ Syntax
@variable_name = $1
@variable_name = Regexp.last_match(1)
@attributes = {}
else
raise SyntaxError.new("Syntax Error in 'comment_form' - Valid syntax: comment_form [article]")
raise SyntaxError, "Syntax Error in 'comment_form' - Valid syntax: comment_form [article]"
end
end
@@ -20,8 +22,8 @@ class CommentForm < Liquid::Block
'posted_successfully?' => context.registers[:posted_successfully],
'errors' => context['comment.errors'],
'author' => context['comment.author'],
'email' => context['comment.email'],
'body' => context['comment.body']
'email' => context['comment.email'],
'body' => context['comment.body'],
}
output << wrap_in_form(article, render_all(@nodelist, context, output))

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'yaml'
module Database
@@ -16,9 +18,10 @@ module Database
end
# key the tables by handles, as this is how liquid expects it.
db = db.inject({}) do |assigns, (key, values)|
assigns[key] = values.inject({}) { |h, v| h[v['handle']] = v; h; }
assigns
db = db.each_with_object({}) do |(key, values), assigns|
assigns[key] = values.each_with_object({}) do |v, h|
h[v['handle']] = v
end
end
# Some standard direct accessors so that the specialized templates
@@ -29,9 +32,9 @@ 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'] },
'items' => db['line_items'].values
'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,
}
db
@@ -40,6 +43,6 @@ module Database
end
if __FILE__ == $PROGRAM_NAME
p Database.tables['collections']['frontpage'].keys
p(Database.tables['collections']['frontpage'].keys)
# p Database.tables['blog']['articles']
end

View File

@@ -1,7 +1,9 @@
# frozen_string_literal: true
require 'json'
module JsonFilter
def json(object)
JSON.dump(object.reject { |k, v| k == "collections" })
JSON.dump(object.reject { |k, _v| k == "collections" })
end
end

View File

@@ -1,4 +1,6 @@
$:.unshift __dir__ + '/../../lib'
# frozen_string_literal: true
$LOAD_PATH.unshift(__dir__ + '/../../lib')
require_relative '../../lib/liquid'
require_relative 'comment_form'
@@ -9,11 +11,11 @@ require_relative 'shop_filter'
require_relative 'tag_filter'
require_relative 'weight_filter'
Liquid::Template.register_tag 'paginate', Paginate
Liquid::Template.register_tag 'form', CommentForm
Liquid::Template.register_tag('paginate', Paginate)
Liquid::Template.register_tag('form', CommentForm)
Liquid::Template.register_filter JsonFilter
Liquid::Template.register_filter MoneyFilter
Liquid::Template.register_filter WeightFilter
Liquid::Template.register_filter ShopFilter
Liquid::Template.register_filter TagFilter
Liquid::Template.register_filter(JsonFilter)
Liquid::Template.register_filter(MoneyFilter)
Liquid::Template.register_filter(WeightFilter)
Liquid::Template.register_filter(ShopFilter)
Liquid::Template.register_filter(TagFilter)

View File

@@ -1,12 +1,14 @@
# frozen_string_literal: true
module MoneyFilter
def money_with_currency(money)
return '' if money.nil?
sprintf("$ %.2f USD", money / 100.0)
format("$ %.2f USD", money / 100.0)
end
def money(money)
return '' if money.nil?
sprintf("$ %.2f", money / 100.0)
format("$ %.2f", money / 100.0)
end
private

View File

@@ -1,13 +1,15 @@
# frozen_string_literal: true
class Paginate < Liquid::Block
Syntax = /(#{Liquid::QuotedFragment})\s*(by\s*(\d+))?/
Syntax = /(#{Liquid::QuotedFragment})\s*(by\s*(\d+))?/
def initialize(tag_name, markup, options)
super
if markup =~ Syntax
@collection_name = $1
@page_size = if $2
$3.to_i
@collection_name = Regexp.last_match(1)
@page_size = if Regexp.last_match(2)
Regexp.last_match(3).to_i
else
20
end
@@ -17,7 +19,7 @@ class Paginate < Liquid::Block
@attributes[key] = value
end
else
raise SyntaxError.new("Syntax Error in tag 'paginate' - Valid syntax: paginate [collection] by number")
raise SyntaxError, "Syntax Error in tag 'paginate' - Valid syntax: paginate [collection] by number"
end
end
@@ -25,19 +27,19 @@ class Paginate < Liquid::Block
@context = context
context.stack do
current_page = context['current_page'].to_i
current_page = context['current_page'].to_i
pagination = {
'page_size' => @page_size,
'current_page' => 5,
'current_offset' => @page_size * 5
'page_size' => @page_size,
'current_page' => 5,
'current_offset' => @page_size * 5,
}
context['paginate'] = pagination
collection_size = context[@collection_name].size
collection_size = context[@collection_name].size
raise ArgumentError.new("Cannot paginate array '#{@collection_name}'. Not found.") if collection_size.nil?
raise ArgumentError, "Cannot paginate array '#{@collection_name}'. Not found." if collection_size.nil?
page_count = (collection_size.to_f / @page_size.to_f).to_f.ceil + 1

View File

@@ -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
@@ -52,7 +54,7 @@ module ShopFilter
end
def product_img_url(url, style = 'small')
unless url =~ /\Aproducts\/([\w\-\_]+)\.(\w{2,4})/
unless url =~ %r{\Aproducts/([\w\-\_]+)\.(\w{2,4})}
raise ArgumentError, 'filter "size" can only be called on product images'
end
@@ -60,7 +62,7 @@ module ShopFilter
when 'original'
return '/files/shops/random_number/' + url
when 'grande', 'large', 'medium', 'compact', 'small', 'thumb', 'icon'
"/files/shops/random_number/products/#{$1}_#{style}.#{$2}"
"/files/shops/random_number/products/#{Regexp.last_match(1)}_#{style}.#{Regexp.last_match(2)}"
else
raise ArgumentError, 'valid parameters for filter "size" are: original, grande, large, medium, compact, small, thumb and icon '
end
@@ -70,16 +72,14 @@ module ShopFilter
html = []
html << %(<span class="prev">#{link_to(paginate['previous']['title'], paginate['previous']['url'])}</span>) if paginate['previous']
for part in paginate['parts']
if part['is_link']
html << %(<span class="page">#{link_to(part['title'], part['url'])}</span>)
paginate['parts'].each do |part|
html << if part['is_link']
%(<span class="page">#{link_to(part['title'], part['url'])}</span>)
elsif part['title'].to_i == paginate['current_page'].to_i
html << %(<span class="page current">#{part['title']}</span>)
%(<span class="page current">#{part['title']}</span>)
else
html << %(<span class="deco">#{part['title']}</span>)
%(<span class="deco">#{part['title']}</span>)
end
end
html << %(<span class="next">#{link_to(paginate['next']['title'], paginate['next']['url'])}</span>) if paginate['next']

View File

@@ -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>"
@@ -13,11 +15,11 @@ module TagFilter
def link_to_add_tag(label, tag)
tags = (@context['current_tags'] + [tag]).uniq
"<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tags.join("+")}\">#{label}</a>"
"<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tags.join('+')}\">#{label}</a>"
end
def link_to_remove_tag(label, tag)
tags = (@context['current_tags'] - [tag]).uniq
"<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tags.join("+")}\">#{label}</a>"
"<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tags.join('+')}\">#{label}</a>"
end
end

View File

@@ -1,6 +1,8 @@
# frozen_string_literal: true
module WeightFilter
def weight(grams)
sprintf("%.2f", grams / 1000)
format("%.2f", grams / 1000)
end
def weight_with_unit(grams)

View File

@@ -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.
@@ -31,7 +33,7 @@ class ThemeRunner
{
liquid: File.read(test),
layout: (File.file?(theme_path) ? File.read(theme_path) : nil),
template_name: test
template_name: test,
}
end.compact

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper'
class AssignTest < Minitest::Test
@@ -10,7 +12,7 @@ class AssignTest < Minitest::Test
END_TEMPLATE
template = Template.parse(template_source)
rendered = template.render!
assert_equal "Print this-thing", rendered.strip
assert_equal("Print this-thing", rendered.strip)
end
def test_assigned_variable

View File

@@ -1,7 +1,9 @@
# frozen_string_literal: true
require 'test_helper'
class FoobarTag < Liquid::Tag
def render_to_output_buffer(context, output)
def render_to_output_buffer(_context, output)
output << ' '
output
end
@@ -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

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper'
class BlockTest < Minitest::Test
@@ -7,6 +9,6 @@ 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
end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper'
class CaptureTest < Minitest::Test
@@ -14,7 +16,7 @@ class CaptureTest < Minitest::Test
END_TEMPLATE
template = Template.parse(template_source)
rendered = template.render!
assert_equal "Print this-thing", rendered.strip
assert_equal("Print this-thing", rendered.strip)
end
def test_capture_to_variable_from_outer_scope_if_existing
@@ -30,7 +32,7 @@ class CaptureTest < Minitest::Test
END_TEMPLATE
template = Template.parse(template_source)
rendered = template.render!
assert_equal "test-string", rendered.gsub(/\s/, '')
assert_equal("test-string", rendered.gsub(/\s/, ''))
end
def test_assigning_from_capture
@@ -45,6 +47,6 @@ class CaptureTest < Minitest::Test
END_TEMPLATE
template = Template.parse(template_source)
rendered = template.render!
assert_equal "3-3", rendered.gsub(/\s/, '')
assert_equal("3-3", rendered.gsub(/\s/, ''))
end
end # CaptureTest

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper'
class ContextTest < Minitest::Test

View File

@@ -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

View File

@@ -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
@@ -48,7 +50,7 @@ class ProductDrop < Liquid::Drop
end
def user_input
"foo".taint
(+"foo").taint
end
protected
@@ -109,7 +111,7 @@ class DropsTest < Minitest::Test
def test_product_drop
tpl = Liquid::Template.parse(' ')
assert_equal ' ', tpl.render!('product' => ProductDrop.new)
assert_equal(' ', tpl.render!('product' => ProductDrop.new))
end
def test_rendering_raises_on_tainted_attr
@@ -139,52 +141,57 @@ class DropsTest < Minitest::Test
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 +202,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
@@ -241,16 +248,16 @@ class DropsTest < Minitest::Test
end
def test_some_enumerable_methods_still_get_invoked
[ :count, :max ].each do |method|
[:count, :max].each do |method|
assert_equal "3", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new)
assert_equal "3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new)
assert_equal "3", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)
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|
[:min, :first].each do |method|
assert_equal "1", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new)
assert_equal "1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new)
assert_equal "1", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)
@@ -259,15 +266,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 user_input 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

View File

@@ -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,22 +78,21 @@ 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
err = assert_raises(SyntaxError) do
Liquid::Template.parse(%q(
Liquid::Template.parse('
foobar
{% "cat" | foobar %}
bla
),
line_numbers: true
)
',
line_numbers: true)
end
assert_match(/Liquid syntax error \(line 4\)/, err.message)
@@ -99,15 +100,14 @@ class ErrorHandlingTest < Minitest::Test
def test_with_line_numbers_adds_numbers_to_parser_errors_with_whitespace_trim
err = assert_raises(SyntaxError) do
Liquid::Template.parse(%q(
Liquid::Template.parse('
foobar
{%- "cat" | foobar -%}
bla
),
line_numbers: true
)
',
line_numbers: true)
end
assert_match(/Liquid syntax error \(line 4\)/, err.message)
@@ -122,11 +122,10 @@ class ErrorHandlingTest < Minitest::Test
bla
',
error_mode: :warn,
line_numbers: true
)
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,11 +138,10 @@ class ErrorHandlingTest < Minitest::Test
bla
',
error_mode: :strict,
line_numbers: true
)
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
@@ -157,41 +155,40 @@ class ErrorHandlingTest < Minitest::Test
bla
',
line_numbers: true
)
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
@@ -205,22 +202,25 @@ class ErrorHandlingTest < Minitest::Test
def test_default_exception_renderer_with_internal_error
template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true)
output = template.render({ 'errors' => ErrorDrop.new })
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 })
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
@@ -228,18 +228,21 @@ class ErrorHandlingTest < Minitest::Test
def test_exception_renderer_exposing_non_liquid_error
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
def read_template_file(template_path)
def read_template_file(_template_path)
"{{ errors.argument_error }}"
end
end
@@ -254,7 +257,7 @@ class ErrorHandlingTest < Minitest::Test
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
end

View File

@@ -1,24 +1,26 @@
# frozen_string_literal: true
require 'test_helper'
module MoneyFilter
def money(input)
sprintf(' %d$ ', input)
format(' %d$ ', input)
end
def money_with_underscore(input)
sprintf(' %d$ ', input)
format(' %d$ ', input)
end
end
module CanadianMoneyFilter
def money(input)
sprintf(' %d$ CAD ', input)
format(' %d$ CAD ', input)
end
end
module SubstituteFilter
def substitute(input, params = {})
input.gsub(/%\{(\w+)\}/) { |match| params[$1] }
input.gsub(/%\{(\w+)\}/) { |_match| params[Regexp.last_match(1)] }
end
end
@@ -26,7 +28,7 @@ class FiltersTest < Minitest::Test
include Liquid
module OverrideObjectMethodFilter
def tap(input)
def tap(_input)
"tap overridden"
end
end
@@ -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,20 +55,20 @@ 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
@@ -76,11 +78,11 @@ class FiltersTest < Minitest::Test
@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
@@ -89,13 +91,13 @@ class FiltersTest < Minitest::Test
@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
@@ -104,37 +106,37 @@ class FiltersTest < Minitest::Test
@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
@@ -142,14 +144,14 @@ class FiltersTest < Minitest::Test
@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
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
end
@@ -165,8 +167,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

View File

@@ -1,15 +1,17 @@
# frozen_string_literal: true
require 'test_helper'
class HashOrderingTest < Minitest::Test
module MoneyFilter
def money(input)
sprintf(' %d$ ', input)
format(' %d$ ', input)
end
end
module CanadianMoneyFilter
def money(input)
sprintf(' %d$ CAD ', input)
format(' %d$ CAD ', input)
end
end

View File

@@ -1,7 +1,9 @@
# frozen_string_literal: true
require 'test_helper'
module FunnyFilter
def make_funny(input)
def make_funny(_input)
'LOL'
end
@@ -32,7 +34,7 @@ class OutputTest < Minitest::Test
def setup
@assigns = {
'best_cars' => 'bmw',
'car' => { 'bmw' => 'good', 'gm' => 'bad' }
'car' => { 'bmw' => 'good', 'gm' => 'bad' },
}
end
@@ -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 }} )
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 }} )
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 : ':-(' }} !
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 }} )
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 : ':-('}} !
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'}} )
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}} )
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 }} )
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' }} )
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

View File

@@ -227,7 +227,7 @@ class ParseTreeVisitorTest < Minitest::Test
[[nil, [
[nil, [[nil, [["other", []]]]]],
["test", []],
["xs", []]
["xs", []],
]]],
traversal(%({% for x in xs offset: test %}{{ other }}{% endfor %})).visit
)

View File

@@ -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,9 +64,9 @@ 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

View File

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

View File

@@ -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
@@ -128,27 +130,42 @@ class RenderProfilingTest < Minitest::Test
t.render!
timing_count = 0
t.profiler.each do |timing|
t.profiler.each do |_timing|
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"] })
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

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper'
module SecurityFilter
@@ -17,28 +19,28 @@ class SecurityTest < Minitest::Test
text = %( {{ '1+1' | instance_eval }} )
expected = %( 1+1 )
assert_equal expected, Template.parse(text).render!(@assigns)
assert_equal(expected, Template.parse(text).render!(@assigns))
end
def test_no_existing_instance_eval
text = %( {{ '1+1' | __instance_eval__ }} )
expected = %( 1+1 )
assert_equal expected, Template.parse(text).render!(@assigns)
assert_equal(expected, Template.parse(text).render!(@assigns))
end
def test_no_instance_eval_after_mixing_in_new_filter
text = %( {{ '1+1' | instance_eval }} )
expected = %( 1+1 )
assert_equal expected, Template.parse(text).render!(@assigns)
assert_equal(expected, Template.parse(text).render!(@assigns))
end
def test_no_instance_eval_later_in_chain
text = %( {{ '1+1' | add_one | instance_eval }} )
expected = %( 1+1 + 1 )
assert_equal expected, Template.parse(text).render!(@assigns, filters: SecurityFilter)
assert_equal(expected, Template.parse(text).render!(@assigns, filters: SecurityFilter))
end
def test_does_not_add_filters_to_symbol_table
@@ -47,27 +49,27 @@ class SecurityTest < Minitest::Test
test = %( {{ "some_string" | a_bad_filter }} )
template = Template.parse(test)
assert_equal [], (Symbol.all_symbols - current_symbols)
assert_equal([], (Symbol.all_symbols - current_symbols))
template.render!
assert_equal [], (Symbol.all_symbols - current_symbols)
assert_equal([], (Symbol.all_symbols - current_symbols))
end
def test_does_not_add_drop_methods_to_symbol_table
current_symbols = Symbol.all_symbols
assigns = { 'drop' => Drop.new }
assert_equal "", Template.parse("{{ drop.custom_method_1 }}", assigns).render!
assert_equal "", Template.parse("{{ drop.custom_method_2 }}", assigns).render!
assert_equal "", Template.parse("{{ drop.custom_method_3 }}", assigns).render!
assert_equal("", Template.parse("{{ drop.custom_method_1 }}", assigns).render!)
assert_equal("", Template.parse("{{ drop.custom_method_2 }}", assigns).render!)
assert_equal("", Template.parse("{{ drop.custom_method_3 }}", assigns).render!)
assert_equal [], (Symbol.all_symbols - current_symbols)
assert_equal([], (Symbol.all_symbols - current_symbols))
end
def test_max_depth_nested_blocks_does_not_raise_exception
depth = Liquid::Block::MAX_DEPTH
code = "{% if true %}" * depth + "rendered" + "{% endif %}" * depth
assert_equal "rendered", Template.parse(code).render!
assert_equal("rendered", Template.parse(code).render!)
end
def test_more_than_max_depth_nested_blocks_raises_exception

View File

@@ -1,4 +1,5 @@
# encoding: utf-8
# frozen_string_literal: true
require 'test_helper'
@@ -17,7 +18,7 @@ class TestThing
"woot: #{@foo}"
end
def [](whatever)
def [](_whatever)
to_s
end
@@ -37,7 +38,7 @@ class TestEnumerable < Liquid::Drop
include Enumerable
def each(&block)
[ { "foo" => 1, "bar" => 2 }, { "foo" => 2, "bar" => 1 }, { "foo" => 3, "bar" => 3 } ].each(&block)
[{ "foo" => 1, "bar" => 2 }, { "foo" => 2, "bar" => 1 }, { "foo" => 3, "bar" => 3 }].each(&block)
end
end
@@ -59,34 +60,34 @@ class StandardFiltersTest < Minitest::Test
end
def test_size
assert_equal 3, @filters.size([1, 2, 3])
assert_equal 0, @filters.size([])
assert_equal 0, @filters.size(nil)
assert_equal(3, @filters.size([1, 2, 3]))
assert_equal(0, @filters.size([]))
assert_equal(0, @filters.size(nil))
end
def test_downcase
assert_equal 'testing', @filters.downcase("Testing")
assert_equal '', @filters.downcase(nil)
assert_equal('testing', @filters.downcase("Testing"))
assert_equal('', @filters.downcase(nil))
end
def test_upcase
assert_equal 'TESTING', @filters.upcase("Testing")
assert_equal '', @filters.upcase(nil)
assert_equal('TESTING', @filters.upcase("Testing"))
assert_equal('', @filters.upcase(nil))
end
def test_slice
assert_equal 'oob', @filters.slice('foobar', 1, 3)
assert_equal 'oobar', @filters.slice('foobar', 1, 1000)
assert_equal '', @filters.slice('foobar', 1, 0)
assert_equal 'o', @filters.slice('foobar', 1, 1)
assert_equal 'bar', @filters.slice('foobar', 3, 3)
assert_equal 'ar', @filters.slice('foobar', -2, 2)
assert_equal 'ar', @filters.slice('foobar', -2, 1000)
assert_equal 'r', @filters.slice('foobar', -1)
assert_equal '', @filters.slice(nil, 0)
assert_equal '', @filters.slice('foobar', 100, 10)
assert_equal '', @filters.slice('foobar', -100, 10)
assert_equal 'oob', @filters.slice('foobar', '1', '3')
assert_equal('oob', @filters.slice('foobar', 1, 3))
assert_equal('oobar', @filters.slice('foobar', 1, 1000))
assert_equal('', @filters.slice('foobar', 1, 0))
assert_equal('o', @filters.slice('foobar', 1, 1))
assert_equal('bar', @filters.slice('foobar', 3, 3))
assert_equal('ar', @filters.slice('foobar', -2, 2))
assert_equal('ar', @filters.slice('foobar', -2, 1000))
assert_equal('r', @filters.slice('foobar', -1))
assert_equal('', @filters.slice(nil, 0))
assert_equal('', @filters.slice('foobar', 100, 10))
assert_equal('', @filters.slice('foobar', -100, 10))
assert_equal('oob', @filters.slice('foobar', '1', '3'))
assert_raises(Liquid::ArgumentError) do
@filters.slice('foobar', nil)
end
@@ -97,109 +98,112 @@ class StandardFiltersTest < Minitest::Test
def test_slice_on_arrays
input = 'foobar'.split(//)
assert_equal %w(o o b), @filters.slice(input, 1, 3)
assert_equal %w(o o b a r), @filters.slice(input, 1, 1000)
assert_equal %w(), @filters.slice(input, 1, 0)
assert_equal %w(o), @filters.slice(input, 1, 1)
assert_equal %w(b a r), @filters.slice(input, 3, 3)
assert_equal %w(a r), @filters.slice(input, -2, 2)
assert_equal %w(a r), @filters.slice(input, -2, 1000)
assert_equal %w(r), @filters.slice(input, -1)
assert_equal %w(), @filters.slice(input, 100, 10)
assert_equal %w(), @filters.slice(input, -100, 10)
assert_equal(%w(o o b), @filters.slice(input, 1, 3))
assert_equal(%w(o o b a r), @filters.slice(input, 1, 1000))
assert_equal(%w(), @filters.slice(input, 1, 0))
assert_equal(%w(o), @filters.slice(input, 1, 1))
assert_equal(%w(b a r), @filters.slice(input, 3, 3))
assert_equal(%w(a r), @filters.slice(input, -2, 2))
assert_equal(%w(a r), @filters.slice(input, -2, 1000))
assert_equal(%w(r), @filters.slice(input, -1))
assert_equal(%w(), @filters.slice(input, 100, 10))
assert_equal(%w(), @filters.slice(input, -100, 10))
end
def test_truncate
assert_equal '1234...', @filters.truncate('1234567890', 7)
assert_equal '1234567890', @filters.truncate('1234567890', 20)
assert_equal '...', @filters.truncate('1234567890', 0)
assert_equal '1234567890', @filters.truncate('1234567890')
assert_equal "测试...", @filters.truncate("测试测试测试测试", 5)
assert_equal '12341', @filters.truncate("1234567890", 5, 1)
assert_equal('1234...', @filters.truncate('1234567890', 7))
assert_equal('1234567890', @filters.truncate('1234567890', 20))
assert_equal('...', @filters.truncate('1234567890', 0))
assert_equal('1234567890', @filters.truncate('1234567890'))
assert_equal("测试...", @filters.truncate("测试测试测试测试", 5))
assert_equal('12341', @filters.truncate("1234567890", 5, 1))
end
def test_split
assert_equal ['12', '34'], @filters.split('12~34', '~')
assert_equal ['A? ', ' ,Z'], @filters.split('A? ~ ~ ~ ,Z', '~ ~ ~')
assert_equal ['A?Z'], @filters.split('A?Z', '~')
assert_equal [], @filters.split(nil, ' ')
assert_equal ['A', 'Z'], @filters.split('A1Z', 1)
assert_equal(['12', '34'], @filters.split('12~34', '~'))
assert_equal(['A? ', ' ,Z'], @filters.split('A? ~ ~ ~ ,Z', '~ ~ ~'))
assert_equal(['A?Z'], @filters.split('A?Z', '~'))
assert_equal([], @filters.split(nil, ' '))
assert_equal(['A', 'Z'], @filters.split('A1Z', 1))
end
def test_escape
assert_equal '&lt;strong&gt;', @filters.escape('<strong>')
assert_equal '1', @filters.escape(1)
assert_equal '2001-02-03', @filters.escape(Date.new(2001, 2, 3))
assert_nil @filters.escape(nil)
assert_equal('&lt;strong&gt;', @filters.escape('<strong>'))
assert_equal('1', @filters.escape(1))
assert_equal('2001-02-03', @filters.escape(Date.new(2001, 2, 3)))
assert_nil(@filters.escape(nil))
end
def test_h
assert_equal '&lt;strong&gt;', @filters.h('<strong>')
assert_equal '1', @filters.h(1)
assert_equal '2001-02-03', @filters.h(Date.new(2001, 2, 3))
assert_nil @filters.h(nil)
assert_equal('&lt;strong&gt;', @filters.h('<strong>'))
assert_equal('1', @filters.h(1))
assert_equal('2001-02-03', @filters.h(Date.new(2001, 2, 3)))
assert_nil(@filters.h(nil))
end
def test_escape_once
assert_equal '&lt;strong&gt;Hulk&lt;/strong&gt;', @filters.escape_once('&lt;strong&gt;Hulk</strong>')
assert_equal('&lt;strong&gt;Hulk&lt;/strong&gt;', @filters.escape_once('&lt;strong&gt;Hulk</strong>'))
end
def test_url_encode
assert_equal 'foo%2B1%40example.com', @filters.url_encode('foo+1@example.com')
assert_equal '1', @filters.url_encode(1)
assert_equal '2001-02-03', @filters.url_encode(Date.new(2001, 2, 3))
assert_nil @filters.url_encode(nil)
assert_equal('foo%2B1%40example.com', @filters.url_encode('foo+1@example.com'))
assert_equal('1', @filters.url_encode(1))
assert_equal('2001-02-03', @filters.url_encode(Date.new(2001, 2, 3)))
assert_nil(@filters.url_encode(nil))
end
def test_url_decode
assert_equal 'foo bar', @filters.url_decode('foo+bar')
assert_equal 'foo bar', @filters.url_decode('foo%20bar')
assert_equal 'foo+1@example.com', @filters.url_decode('foo%2B1%40example.com')
assert_equal '1', @filters.url_decode(1)
assert_equal '2001-02-03', @filters.url_decode(Date.new(2001, 2, 3))
assert_nil @filters.url_decode(nil)
assert_equal('foo bar', @filters.url_decode('foo+bar'))
assert_equal('foo bar', @filters.url_decode('foo%20bar'))
assert_equal('foo+1@example.com', @filters.url_decode('foo%2B1%40example.com'))
assert_equal('1', @filters.url_decode(1))
assert_equal('2001-02-03', @filters.url_decode(Date.new(2001, 2, 3)))
assert_nil(@filters.url_decode(nil))
exception = assert_raises Liquid::ArgumentError do
@filters.url_decode('%ff')
end
assert_equal 'Liquid error: invalid byte sequence in UTF-8', exception.message
assert_equal('Liquid error: invalid byte sequence in UTF-8', exception.message)
end
def test_truncatewords
assert_equal 'one two three', @filters.truncatewords('one two three', 4)
assert_equal 'one two...', @filters.truncatewords('one two three', 2)
assert_equal 'one two three', @filters.truncatewords('one two three')
assert_equal 'Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221;...', @filters.truncatewords('Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221; x 16&#8221; x 10.5&#8221; high) with cover.', 15)
assert_equal "测试测试测试测试", @filters.truncatewords('测试测试测试测试', 5)
assert_equal 'one two1', @filters.truncatewords("one two three", 2, 1)
assert_equal('one two three', @filters.truncatewords('one two three', 4))
assert_equal('one two...', @filters.truncatewords('one two three', 2))
assert_equal('one two three', @filters.truncatewords('one two three'))
assert_equal(
'Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221;...',
@filters.truncatewords('Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221; x 16&#8221; x 10.5&#8221; high) with cover.', 15)
)
assert_equal("测试测试测试测试", @filters.truncatewords('测试测试测试测试', 5))
assert_equal('one two1', @filters.truncatewords("one two three", 2, 1))
end
def test_strip_html
assert_equal 'test', @filters.strip_html("<div>test</div>")
assert_equal 'test', @filters.strip_html("<div id='test'>test</div>")
assert_equal '', @filters.strip_html("<script type='text/javascript'>document.write('some stuff');</script>")
assert_equal '', @filters.strip_html("<style type='text/css'>foo bar</style>")
assert_equal 'test', @filters.strip_html("<div\nclass='multiline'>test</div>")
assert_equal 'test', @filters.strip_html("<!-- foo bar \n test -->test")
assert_equal '', @filters.strip_html(nil)
assert_equal('test', @filters.strip_html("<div>test</div>"))
assert_equal('test', @filters.strip_html("<div id='test'>test</div>"))
assert_equal('', @filters.strip_html("<script type='text/javascript'>document.write('some stuff');</script>"))
assert_equal('', @filters.strip_html("<style type='text/css'>foo bar</style>"))
assert_equal('test', @filters.strip_html("<div\nclass='multiline'>test</div>"))
assert_equal('test', @filters.strip_html("<!-- foo bar \n test -->test"))
assert_equal('', @filters.strip_html(nil))
# Quirk of the existing implementation
assert_equal 'foo;', @filters.strip_html("<<<script </script>script>foo;</script>")
assert_equal('foo;', @filters.strip_html("<<<script </script>script>foo;</script>"))
end
def test_join
assert_equal '1 2 3 4', @filters.join([1, 2, 3, 4])
assert_equal '1 - 2 - 3 - 4', @filters.join([1, 2, 3, 4], ' - ')
assert_equal '1121314', @filters.join([1, 2, 3, 4], 1)
assert_equal('1 2 3 4', @filters.join([1, 2, 3, 4]))
assert_equal('1 - 2 - 3 - 4', @filters.join([1, 2, 3, 4], ' - '))
assert_equal('1121314', @filters.join([1, 2, 3, 4], 1))
end
def test_sort
assert_equal [1, 2, 3, 4], @filters.sort([4, 3, 2, 1])
assert_equal [{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }], @filters.sort([{ "a" => 4 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a")
assert_equal([1, 2, 3, 4], @filters.sort([4, 3, 2, 1]))
assert_equal([{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }], @filters.sort([{ "a" => 4 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a"))
end
def test_sort_with_nils
assert_equal [1, 2, 3, 4, nil], @filters.sort([nil, 4, 3, 2, 1])
assert_equal [{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }, {}], @filters.sort([{ "a" => 4 }, { "a" => 3 }, {}, { "a" => 1 }, { "a" => 2 }], "a")
assert_equal([1, 2, 3, 4, nil], @filters.sort([nil, 4, 3, 2, 1]))
assert_equal([{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }, {}], @filters.sort([{ "a" => 4 }, { "a" => 3 }, {}, { "a" => 1 }, { "a" => 2 }], "a"))
end
def test_sort_when_property_is_sometimes_missing_puts_nils_last
@@ -208,26 +212,26 @@ class StandardFiltersTest < Minitest::Test
{ "handle" => "beta" },
{ "price" => 1, "handle" => "gamma" },
{ "handle" => "delta" },
{ "price" => 2, "handle" => "epsilon" }
{ "price" => 2, "handle" => "epsilon" },
]
expectation = [
{ "price" => 1, "handle" => "gamma" },
{ "price" => 2, "handle" => "epsilon" },
{ "price" => 4, "handle" => "alpha" },
{ "handle" => "delta" },
{ "handle" => "beta" }
{ "handle" => "beta" },
]
assert_equal expectation, @filters.sort(input, "price")
assert_equal(expectation, @filters.sort(input, "price"))
end
def test_sort_natural
assert_equal ["a", "B", "c", "D"], @filters.sort_natural(["c", "D", "a", "B"])
assert_equal [{ "a" => "a" }, { "a" => "B" }, { "a" => "c" }, { "a" => "D" }], @filters.sort_natural([{ "a" => "D" }, { "a" => "c" }, { "a" => "a" }, { "a" => "B" }], "a")
assert_equal(["a", "B", "c", "D"], @filters.sort_natural(["c", "D", "a", "B"]))
assert_equal([{ "a" => "a" }, { "a" => "B" }, { "a" => "c" }, { "a" => "D" }], @filters.sort_natural([{ "a" => "D" }, { "a" => "c" }, { "a" => "a" }, { "a" => "B" }], "a"))
end
def test_sort_natural_with_nils
assert_equal ["a", "B", "c", "D", nil], @filters.sort_natural([nil, "c", "D", "a", "B"])
assert_equal [{ "a" => "a" }, { "a" => "B" }, { "a" => "c" }, { "a" => "D" }, {}], @filters.sort_natural([{ "a" => "D" }, { "a" => "c" }, {}, { "a" => "a" }, { "a" => "B" }], "a")
assert_equal(["a", "B", "c", "D", nil], @filters.sort_natural([nil, "c", "D", "a", "B"]))
assert_equal([{ "a" => "a" }, { "a" => "B" }, { "a" => "c" }, { "a" => "D" }, {}], @filters.sort_natural([{ "a" => "D" }, { "a" => "c" }, {}, { "a" => "a" }, { "a" => "B" }], "a"))
end
def test_sort_natural_when_property_is_sometimes_missing_puts_nils_last
@@ -236,16 +240,16 @@ class StandardFiltersTest < Minitest::Test
{ "handle" => "beta" },
{ "price" => "1", "handle" => "gamma" },
{ "handle" => "delta" },
{ "price" => 2, "handle" => "epsilon" }
{ "price" => 2, "handle" => "epsilon" },
]
expectation = [
{ "price" => "1", "handle" => "gamma" },
{ "price" => 2, "handle" => "epsilon" },
{ "price" => "4", "handle" => "alpha" },
{ "handle" => "delta" },
{ "handle" => "beta" }
{ "handle" => "beta" },
]
assert_equal expectation, @filters.sort_natural(input, "price")
assert_equal(expectation, @filters.sort_natural(input, "price"))
end
def test_sort_natural_case_check
@@ -256,7 +260,7 @@ class StandardFiltersTest < Minitest::Test
{ "fake" => "t" },
{ "key" => "a" },
{ "key" => "b" },
{ "key" => "c" }
{ "key" => "c" },
]
expectation = [
{ "key" => "a" },
@@ -265,21 +269,21 @@ class StandardFiltersTest < Minitest::Test
{ "key" => "X" },
{ "key" => "Y" },
{ "key" => "Z" },
{ "fake" => "t" }
{ "fake" => "t" },
]
assert_equal expectation, @filters.sort_natural(input, "key")
assert_equal ["a", "b", "c", "X", "Y", "Z"], @filters.sort_natural(["X", "Y", "Z", "a", "b", "c"])
assert_equal(expectation, @filters.sort_natural(input, "key"))
assert_equal(["a", "b", "c", "X", "Y", "Z"], @filters.sort_natural(["X", "Y", "Z", "a", "b", "c"]))
end
def test_sort_empty_array
assert_equal [], @filters.sort([], "a")
assert_equal([], @filters.sort([], "a"))
end
def test_sort_invalid_property
foo = [
[1],
[2],
[3]
[3],
]
assert_raises Liquid::ArgumentError do
@@ -288,14 +292,14 @@ class StandardFiltersTest < Minitest::Test
end
def test_sort_natural_empty_array
assert_equal [], @filters.sort_natural([], "a")
assert_equal([], @filters.sort_natural([], "a"))
end
def test_sort_natural_invalid_property
foo = [
[1],
[2],
[3]
[3],
]
assert_raises Liquid::ArgumentError do
@@ -304,33 +308,33 @@ class StandardFiltersTest < Minitest::Test
end
def test_legacy_sort_hash
assert_equal [{ a: 1, b: 2 }], @filters.sort({ a: 1, b: 2 })
assert_equal([{ a: 1, b: 2 }], @filters.sort(a: 1, b: 2))
end
def test_numerical_vs_lexicographical_sort
assert_equal [2, 10], @filters.sort([10, 2])
assert_equal [{ "a" => 2 }, { "a" => 10 }], @filters.sort([{ "a" => 10 }, { "a" => 2 }], "a")
assert_equal ["10", "2"], @filters.sort(["10", "2"])
assert_equal [{ "a" => "10" }, { "a" => "2" }], @filters.sort([{ "a" => "10" }, { "a" => "2" }], "a")
assert_equal([2, 10], @filters.sort([10, 2]))
assert_equal([{ "a" => 2 }, { "a" => 10 }], @filters.sort([{ "a" => 10 }, { "a" => 2 }], "a"))
assert_equal(["10", "2"], @filters.sort(["10", "2"]))
assert_equal([{ "a" => "10" }, { "a" => "2" }], @filters.sort([{ "a" => "10" }, { "a" => "2" }], "a"))
end
def test_uniq
assert_equal ["foo"], @filters.uniq("foo")
assert_equal [1, 3, 2, 4], @filters.uniq([1, 1, 3, 2, 3, 1, 4, 3, 2, 1])
assert_equal [{ "a" => 1 }, { "a" => 3 }, { "a" => 2 }], @filters.uniq([{ "a" => 1 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a")
assert_equal(["foo"], @filters.uniq("foo"))
assert_equal([1, 3, 2, 4], @filters.uniq([1, 1, 3, 2, 3, 1, 4, 3, 2, 1]))
assert_equal([{ "a" => 1 }, { "a" => 3 }, { "a" => 2 }], @filters.uniq([{ "a" => 1 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a"))
testdrop = TestDrop.new
assert_equal [testdrop], @filters.uniq([testdrop, TestDrop.new], 'test')
assert_equal([testdrop], @filters.uniq([testdrop, TestDrop.new], 'test'))
end
def test_uniq_empty_array
assert_equal [], @filters.uniq([], "a")
assert_equal([], @filters.uniq([], "a"))
end
def test_uniq_invalid_property
foo = [
[1],
[2],
[3]
[3],
]
assert_raises Liquid::ArgumentError do
@@ -339,14 +343,14 @@ class StandardFiltersTest < Minitest::Test
end
def test_compact_empty_array
assert_equal [], @filters.compact([], "a")
assert_equal([], @filters.compact([], "a"))
end
def test_compact_invalid_property
foo = [
[1],
[2],
[3]
[3],
]
assert_raises Liquid::ArgumentError do
@@ -355,75 +359,75 @@ class StandardFiltersTest < Minitest::Test
end
def test_reverse
assert_equal [4, 3, 2, 1], @filters.reverse([1, 2, 3, 4])
assert_equal([4, 3, 2, 1], @filters.reverse([1, 2, 3, 4]))
end
def test_legacy_reverse_hash
assert_equal [{ a: 1, b: 2 }], @filters.reverse(a: 1, b: 2)
assert_equal([{ a: 1, b: 2 }], @filters.reverse(a: 1, b: 2))
end
def test_map
assert_equal [1, 2, 3, 4], @filters.map([{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }], 'a')
assert_template_result 'abc', "{{ ary | map:'foo' | map:'bar' }}",
'ary' => [{ 'foo' => { 'bar' => 'a' } }, { 'foo' => { 'bar' => 'b' } }, { 'foo' => { 'bar' => 'c' } }]
assert_equal([1, 2, 3, 4], @filters.map([{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }], 'a'))
assert_template_result('abc', "{{ ary | map:'foo' | map:'bar' }}",
'ary' => [{ 'foo' => { 'bar' => 'a' } }, { 'foo' => { 'bar' => 'b' } }, { 'foo' => { 'bar' => 'c' } }])
end
def test_map_doesnt_call_arbitrary_stuff
assert_template_result "", '{{ "foo" | map: "__id__" }}'
assert_template_result "", '{{ "foo" | map: "inspect" }}'
assert_template_result("", '{{ "foo" | map: "__id__" }}')
assert_template_result("", '{{ "foo" | map: "inspect" }}')
end
def test_map_calls_to_liquid
t = TestThing.new
assert_template_result "woot: 1", '{{ foo | map: "whatever" }}', "foo" => [t]
assert_template_result("woot: 1", '{{ foo | map: "whatever" }}', "foo" => [t])
end
def test_map_on_hashes
assert_template_result "4217", '{{ thing | map: "foo" | map: "bar" }}',
"thing" => { "foo" => [ { "bar" => 42 }, { "bar" => 17 } ] }
assert_template_result("4217", '{{ thing | map: "foo" | map: "bar" }}',
"thing" => { "foo" => [{ "bar" => 42 }, { "bar" => 17 }] })
end
def test_legacy_map_on_hashes_with_dynamic_key
template = "{% assign key = 'foo' %}{{ thing | map: key | map: 'bar' }}"
hash = { "foo" => { "bar" => 42 } }
assert_template_result "42", template, "thing" => hash
assert_template_result("42", template, "thing" => hash)
end
def test_sort_calls_to_liquid
t = TestThing.new
Liquid::Template.parse('{{ foo | sort: "whatever" }}').render("foo" => [t])
assert t.foo > 0
assert(t.foo > 0)
end
def test_map_over_proc
drop = TestDrop.new
p = proc{ drop }
p = proc { drop }
templ = '{{ procs | map: "test" }}'
assert_template_result "testfoo", templ, "procs" => [p]
assert_template_result("testfoo", templ, "procs" => [p])
end
def test_map_over_drops_returning_procs
drops = [
{
"proc" => ->{ "foo" },
"proc" => -> { "foo" },
},
{
"proc" => ->{ "bar" },
"proc" => -> { "bar" },
},
]
templ = '{{ drops | map: "proc" }}'
assert_template_result "foobar", templ, "drops" => drops
assert_template_result("foobar", templ, "drops" => drops)
end
def test_map_works_on_enumerables
assert_template_result "123", '{{ foo | map: "foo" }}', "foo" => TestEnumerable.new
assert_template_result("123", '{{ foo | map: "foo" }}', "foo" => TestEnumerable.new)
end
def test_map_returns_empty_on_2d_input_array
foo = [
[1],
[2],
[3]
[3],
]
assert_raises Liquid::ArgumentError do
@@ -435,7 +439,7 @@ class StandardFiltersTest < Minitest::Test
foo = [
[1],
[2],
[3]
[3],
]
assert_raises Liquid::ArgumentError do
@filters.map(foo, nil)
@@ -443,42 +447,42 @@ class StandardFiltersTest < Minitest::Test
end
def test_sort_works_on_enumerables
assert_template_result "213", '{{ foo | sort: "bar" | map: "foo" }}', "foo" => TestEnumerable.new
assert_template_result("213", '{{ foo | sort: "bar" | map: "foo" }}', "foo" => TestEnumerable.new)
end
def test_first_and_last_call_to_liquid
assert_template_result 'foobar', '{{ foo | first }}', 'foo' => [ThingWithToLiquid.new]
assert_template_result 'foobar', '{{ foo | last }}', 'foo' => [ThingWithToLiquid.new]
assert_template_result('foobar', '{{ foo | first }}', 'foo' => [ThingWithToLiquid.new])
assert_template_result('foobar', '{{ foo | last }}', 'foo' => [ThingWithToLiquid.new])
end
def test_truncate_calls_to_liquid
assert_template_result "wo...", '{{ foo | truncate: 5 }}', "foo" => TestThing.new
assert_template_result("wo...", '{{ foo | truncate: 5 }}', "foo" => TestThing.new)
end
def test_date
assert_equal 'May', @filters.date(Time.parse("2006-05-05 10:00:00"), "%B")
assert_equal 'June', @filters.date(Time.parse("2006-06-05 10:00:00"), "%B")
assert_equal 'July', @filters.date(Time.parse("2006-07-05 10:00:00"), "%B")
assert_equal('May', @filters.date(Time.parse("2006-05-05 10:00:00"), "%B"))
assert_equal('June', @filters.date(Time.parse("2006-06-05 10:00:00"), "%B"))
assert_equal('July', @filters.date(Time.parse("2006-07-05 10:00:00"), "%B"))
assert_equal 'May', @filters.date("2006-05-05 10:00:00", "%B")
assert_equal 'June', @filters.date("2006-06-05 10:00:00", "%B")
assert_equal 'July', @filters.date("2006-07-05 10:00:00", "%B")
assert_equal('May', @filters.date("2006-05-05 10:00:00", "%B"))
assert_equal('June', @filters.date("2006-06-05 10:00:00", "%B"))
assert_equal('July', @filters.date("2006-07-05 10:00:00", "%B"))
assert_equal '2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", "")
assert_equal '2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", "")
assert_equal '2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", "")
assert_equal '2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", nil)
assert_equal('2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", ""))
assert_equal('2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", ""))
assert_equal('2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", ""))
assert_equal('2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", nil))
assert_equal '07/05/2006', @filters.date("2006-07-05 10:00:00", "%m/%d/%Y")
assert_equal('07/05/2006', @filters.date("2006-07-05 10:00:00", "%m/%d/%Y"))
assert_equal "07/16/2004", @filters.date("Fri Jul 16 01:00:00 2004", "%m/%d/%Y")
assert_equal Date.today.year.to_s, @filters.date('now', '%Y')
assert_equal Date.today.year.to_s, @filters.date('today', '%Y')
assert_equal Date.today.year.to_s, @filters.date('Today', '%Y')
assert_equal("07/16/2004", @filters.date("Fri Jul 16 01:00:00 2004", "%m/%d/%Y"))
assert_equal(Date.today.year.to_s, @filters.date('now', '%Y'))
assert_equal(Date.today.year.to_s, @filters.date('today', '%Y'))
assert_equal(Date.today.year.to_s, @filters.date('Today', '%Y'))
assert_nil @filters.date(nil, "%B")
assert_nil(@filters.date(nil, "%B"))
assert_equal '', @filters.date('', "%B")
assert_equal('', @filters.date('', "%B"))
with_timezone("UTC") do
assert_equal "07/05/2006", @filters.date(1152098955, "%m/%d/%Y")
@@ -487,169 +491,169 @@ class StandardFiltersTest < Minitest::Test
end
def test_first_last
assert_equal 1, @filters.first([1, 2, 3])
assert_equal 3, @filters.last([1, 2, 3])
assert_nil @filters.first([])
assert_nil @filters.last([])
assert_equal(1, @filters.first([1, 2, 3]))
assert_equal(3, @filters.last([1, 2, 3]))
assert_nil(@filters.first([]))
assert_nil(@filters.last([]))
end
def test_replace
assert_equal '2 2 2 2', @filters.replace('1 1 1 1', '1', 2)
assert_equal '2 2 2 2', @filters.replace('1 1 1 1', 1, 2)
assert_equal '2 1 1 1', @filters.replace_first('1 1 1 1', '1', 2)
assert_equal '2 1 1 1', @filters.replace_first('1 1 1 1', 1, 2)
assert_template_result '2 1 1 1', "{{ '1 1 1 1' | replace_first: '1', 2 }}"
assert_equal('2 2 2 2', @filters.replace('1 1 1 1', '1', 2))
assert_equal('2 2 2 2', @filters.replace('1 1 1 1', 1, 2))
assert_equal('2 1 1 1', @filters.replace_first('1 1 1 1', '1', 2))
assert_equal('2 1 1 1', @filters.replace_first('1 1 1 1', 1, 2))
assert_template_result('2 1 1 1', "{{ '1 1 1 1' | replace_first: '1', 2 }}")
end
def test_remove
assert_equal ' ', @filters.remove("a a a a", 'a')
assert_equal ' ', @filters.remove("1 1 1 1", 1)
assert_equal 'a a a', @filters.remove_first("a a a a", 'a ')
assert_equal ' 1 1 1', @filters.remove_first("1 1 1 1", 1)
assert_template_result 'a a a', "{{ 'a a a a' | remove_first: 'a ' }}"
assert_equal(' ', @filters.remove("a a a a", 'a'))
assert_equal(' ', @filters.remove("1 1 1 1", 1))
assert_equal('a a a', @filters.remove_first("a a a a", 'a '))
assert_equal(' 1 1 1', @filters.remove_first("1 1 1 1", 1))
assert_template_result('a a a', "{{ 'a a a a' | remove_first: 'a ' }}")
end
def test_pipes_in_string_arguments
assert_template_result 'foobar', "{{ 'foo|bar' | remove: '|' }}"
assert_template_result('foobar', "{{ 'foo|bar' | remove: '|' }}")
end
def test_strip
assert_template_result 'ab c', "{{ source | strip }}", 'source' => " ab c "
assert_template_result 'ab c', "{{ source | strip }}", 'source' => " \tab c \n \t"
assert_template_result('ab c', "{{ source | strip }}", 'source' => " ab c ")
assert_template_result('ab c', "{{ source | strip }}", 'source' => " \tab c \n \t")
end
def test_lstrip
assert_template_result 'ab c ', "{{ source | lstrip }}", 'source' => " ab c "
assert_template_result "ab c \n \t", "{{ source | lstrip }}", 'source' => " \tab c \n \t"
assert_template_result('ab c ', "{{ source | lstrip }}", 'source' => " ab c ")
assert_template_result("ab c \n \t", "{{ source | lstrip }}", 'source' => " \tab c \n \t")
end
def test_rstrip
assert_template_result " ab c", "{{ source | rstrip }}", 'source' => " ab c "
assert_template_result " \tab c", "{{ source | rstrip }}", 'source' => " \tab c \n \t"
assert_template_result(" ab c", "{{ source | rstrip }}", 'source' => " ab c ")
assert_template_result(" \tab c", "{{ source | rstrip }}", 'source' => " \tab c \n \t")
end
def test_strip_newlines
assert_template_result 'abc', "{{ source | strip_newlines }}", 'source' => "a\nb\nc"
assert_template_result 'abc', "{{ source | strip_newlines }}", 'source' => "a\r\nb\nc"
assert_template_result('abc', "{{ source | strip_newlines }}", 'source' => "a\nb\nc")
assert_template_result('abc', "{{ source | strip_newlines }}", 'source' => "a\r\nb\nc")
end
def test_newlines_to_br
assert_template_result "a<br />\nb<br />\nc", "{{ source | newline_to_br }}", 'source' => "a\nb\nc"
assert_template_result("a<br />\nb<br />\nc", "{{ source | newline_to_br }}", 'source' => "a\nb\nc")
end
def test_plus
assert_template_result "2", "{{ 1 | plus:1 }}"
assert_template_result "2.0", "{{ '1' | plus:'1.0' }}"
assert_template_result("2", "{{ 1 | plus:1 }}")
assert_template_result("2.0", "{{ '1' | plus:'1.0' }}")
assert_template_result "5", "{{ price | plus:'2' }}", 'price' => NumberLikeThing.new(3)
assert_template_result("5", "{{ price | plus:'2' }}", 'price' => NumberLikeThing.new(3))
end
def test_minus
assert_template_result "4", "{{ input | minus:operand }}", 'input' => 5, 'operand' => 1
assert_template_result "2.3", "{{ '4.3' | minus:'2' }}"
assert_template_result("4", "{{ input | minus:operand }}", 'input' => 5, 'operand' => 1)
assert_template_result("2.3", "{{ '4.3' | minus:'2' }}")
assert_template_result "5", "{{ price | minus:'2' }}", 'price' => NumberLikeThing.new(7)
assert_template_result("5", "{{ price | minus:'2' }}", 'price' => NumberLikeThing.new(7))
end
def test_abs
assert_template_result "17", "{{ 17 | abs }}"
assert_template_result "17", "{{ -17 | abs }}"
assert_template_result "17", "{{ '17' | abs }}"
assert_template_result "17", "{{ '-17' | abs }}"
assert_template_result "0", "{{ 0 | abs }}"
assert_template_result "0", "{{ '0' | abs }}"
assert_template_result "17.42", "{{ 17.42 | abs }}"
assert_template_result "17.42", "{{ -17.42 | abs }}"
assert_template_result "17.42", "{{ '17.42' | abs }}"
assert_template_result "17.42", "{{ '-17.42' | abs }}"
assert_template_result("17", "{{ 17 | abs }}")
assert_template_result("17", "{{ -17 | abs }}")
assert_template_result("17", "{{ '17' | abs }}")
assert_template_result("17", "{{ '-17' | abs }}")
assert_template_result("0", "{{ 0 | abs }}")
assert_template_result("0", "{{ '0' | abs }}")
assert_template_result("17.42", "{{ 17.42 | abs }}")
assert_template_result("17.42", "{{ -17.42 | abs }}")
assert_template_result("17.42", "{{ '17.42' | abs }}")
assert_template_result("17.42", "{{ '-17.42' | abs }}")
end
def test_times
assert_template_result "12", "{{ 3 | times:4 }}"
assert_template_result "0", "{{ 'foo' | times:4 }}"
assert_template_result "6", "{{ '2.1' | times:3 | replace: '.','-' | plus:0}}"
assert_template_result "7.25", "{{ 0.0725 | times:100 }}"
assert_template_result "-7.25", '{{ "-0.0725" | times:100 }}'
assert_template_result "7.25", '{{ "-0.0725" | times: -100 }}'
assert_template_result "4", "{{ price | times:2 }}", 'price' => NumberLikeThing.new(2)
assert_template_result("12", "{{ 3 | times:4 }}")
assert_template_result("0", "{{ 'foo' | times:4 }}")
assert_template_result("6", "{{ '2.1' | times:3 | replace: '.','-' | plus:0}}")
assert_template_result("7.25", "{{ 0.0725 | times:100 }}")
assert_template_result("-7.25", '{{ "-0.0725" | times:100 }}')
assert_template_result("7.25", '{{ "-0.0725" | times: -100 }}')
assert_template_result("4", "{{ price | times:2 }}", 'price' => NumberLikeThing.new(2))
end
def test_divided_by
assert_template_result "4", "{{ 12 | divided_by:3 }}"
assert_template_result "4", "{{ 14 | divided_by:3 }}"
assert_template_result("4", "{{ 12 | divided_by:3 }}")
assert_template_result("4", "{{ 14 | divided_by:3 }}")
assert_template_result "5", "{{ 15 | divided_by:3 }}"
assert_equal "Liquid error: divided by 0", Template.parse("{{ 5 | divided_by:0 }}").render
assert_template_result("5", "{{ 15 | divided_by:3 }}")
assert_equal("Liquid error: divided by 0", Template.parse("{{ 5 | divided_by:0 }}").render)
assert_template_result "0.5", "{{ 2.0 | divided_by:4 }}"
assert_template_result("0.5", "{{ 2.0 | divided_by:4 }}")
assert_raises(Liquid::ZeroDivisionError) do
assert_template_result "4", "{{ 1 | modulo: 0 }}"
end
assert_template_result "5", "{{ price | divided_by:2 }}", 'price' => NumberLikeThing.new(10)
assert_template_result("5", "{{ price | divided_by:2 }}", 'price' => NumberLikeThing.new(10))
end
def test_modulo
assert_template_result "1", "{{ 3 | modulo:2 }}"
assert_template_result("1", "{{ 3 | modulo:2 }}")
assert_raises(Liquid::ZeroDivisionError) do
assert_template_result "4", "{{ 1 | modulo: 0 }}"
end
assert_template_result "1", "{{ price | modulo:2 }}", 'price' => NumberLikeThing.new(3)
assert_template_result("1", "{{ price | modulo:2 }}", 'price' => NumberLikeThing.new(3))
end
def test_round
assert_template_result "5", "{{ input | round }}", 'input' => 4.6
assert_template_result "4", "{{ '4.3' | round }}"
assert_template_result "4.56", "{{ input | round: 2 }}", 'input' => 4.5612
assert_template_result("5", "{{ input | round }}", 'input' => 4.6)
assert_template_result("4", "{{ '4.3' | round }}")
assert_template_result("4.56", "{{ input | round: 2 }}", 'input' => 4.5612)
assert_raises(Liquid::FloatDomainError) do
assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | round }}"
end
assert_template_result "5", "{{ price | round }}", 'price' => NumberLikeThing.new(4.6)
assert_template_result "4", "{{ price | round }}", 'price' => NumberLikeThing.new(4.3)
assert_template_result("5", "{{ price | round }}", 'price' => NumberLikeThing.new(4.6))
assert_template_result("4", "{{ price | round }}", 'price' => NumberLikeThing.new(4.3))
end
def test_ceil
assert_template_result "5", "{{ input | ceil }}", 'input' => 4.6
assert_template_result "5", "{{ '4.3' | ceil }}"
assert_template_result("5", "{{ input | ceil }}", 'input' => 4.6)
assert_template_result("5", "{{ '4.3' | ceil }}")
assert_raises(Liquid::FloatDomainError) do
assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | ceil }}"
end
assert_template_result "5", "{{ price | ceil }}", 'price' => NumberLikeThing.new(4.6)
assert_template_result("5", "{{ price | ceil }}", 'price' => NumberLikeThing.new(4.6))
end
def test_floor
assert_template_result "4", "{{ input | floor }}", 'input' => 4.6
assert_template_result "4", "{{ '4.3' | floor }}"
assert_template_result("4", "{{ input | floor }}", 'input' => 4.6)
assert_template_result("4", "{{ '4.3' | floor }}")
assert_raises(Liquid::FloatDomainError) do
assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | floor }}"
end
assert_template_result "5", "{{ price | floor }}", 'price' => NumberLikeThing.new(5.4)
assert_template_result("5", "{{ price | floor }}", 'price' => NumberLikeThing.new(5.4))
end
def test_at_most
assert_template_result "4", "{{ 5 | at_most:4 }}"
assert_template_result "5", "{{ 5 | at_most:5 }}"
assert_template_result "5", "{{ 5 | at_most:6 }}"
assert_template_result("4", "{{ 5 | at_most:4 }}")
assert_template_result("5", "{{ 5 | at_most:5 }}")
assert_template_result("5", "{{ 5 | at_most:6 }}")
assert_template_result "4.5", "{{ 4.5 | at_most:5 }}"
assert_template_result "5", "{{ width | at_most:5 }}", 'width' => NumberLikeThing.new(6)
assert_template_result "4", "{{ width | at_most:5 }}", 'width' => NumberLikeThing.new(4)
assert_template_result "4", "{{ 5 | at_most: width }}", 'width' => NumberLikeThing.new(4)
assert_template_result("4.5", "{{ 4.5 | at_most:5 }}")
assert_template_result("5", "{{ width | at_most:5 }}", 'width' => NumberLikeThing.new(6))
assert_template_result("4", "{{ width | at_most:5 }}", 'width' => NumberLikeThing.new(4))
assert_template_result("4", "{{ 5 | at_most: width }}", 'width' => NumberLikeThing.new(4))
end
def test_at_least
assert_template_result "5", "{{ 5 | at_least:4 }}"
assert_template_result "5", "{{ 5 | at_least:5 }}"
assert_template_result "6", "{{ 5 | at_least:6 }}"
assert_template_result("5", "{{ 5 | at_least:4 }}")
assert_template_result("5", "{{ 5 | at_least:5 }}")
assert_template_result("6", "{{ 5 | at_least:6 }}")
assert_template_result "5", "{{ 4.5 | at_least:5 }}"
assert_template_result "6", "{{ width | at_least:5 }}", 'width' => NumberLikeThing.new(6)
assert_template_result "5", "{{ width | at_least:5 }}", 'width' => NumberLikeThing.new(4)
assert_template_result "6", "{{ 5 | at_least: width }}", 'width' => NumberLikeThing.new(6)
assert_template_result("5", "{{ 4.5 | at_least:5 }}")
assert_template_result("6", "{{ width | at_least:5 }}", 'width' => NumberLikeThing.new(6))
assert_template_result("5", "{{ width | at_least:5 }}", 'width' => NumberLikeThing.new(4))
assert_template_result("6", "{{ 5 | at_least: width }}", 'width' => NumberLikeThing.new(6))
end
def test_append
@@ -659,9 +663,9 @@ class StandardFiltersTest < Minitest::Test
end
def test_concat
assert_equal [1, 2, 3, 4], @filters.concat([1, 2], [3, 4])
assert_equal [1, 2, 'a'], @filters.concat([1, 2], ['a'])
assert_equal [1, 2, 10], @filters.concat([1, 2], [10])
assert_equal([1, 2, 3, 4], @filters.concat([1, 2], [3, 4]))
assert_equal([1, 2, 'a'], @filters.concat([1, 2], ['a']))
assert_equal([1, 2, 10], @filters.concat([1, 2], [10]))
assert_raises(Liquid::ArgumentError, "concat filter requires an array argument") do
@filters.concat([1, 2], 10)
@@ -675,12 +679,23 @@ class StandardFiltersTest < Minitest::Test
end
def test_default
assert_equal "foo", @filters.default("foo", "bar")
assert_equal "bar", @filters.default(nil, "bar")
assert_equal "bar", @filters.default("", "bar")
assert_equal "bar", @filters.default(false, "bar")
assert_equal "bar", @filters.default([], "bar")
assert_equal "bar", @filters.default({}, "bar")
assert_equal("foo", @filters.default("foo", "bar"))
assert_equal("bar", @filters.default(nil, "bar"))
assert_equal("bar", @filters.default("", "bar"))
assert_equal("bar", @filters.default(false, "bar"))
assert_equal("bar", @filters.default([], "bar"))
assert_equal("bar", @filters.default({}, "bar"))
assert_template_result('bar', "{{ false | default: 'bar' }}")
end
def test_default_handle_false
assert_equal("foo", @filters.default("foo", "bar", "allow_false" => true))
assert_equal("bar", @filters.default(nil, "bar", "allow_false" => true))
assert_equal("bar", @filters.default("", "bar", "allow_false" => true))
assert_equal(false, @filters.default(false, "bar", "allow_false" => true))
assert_equal("bar", @filters.default([], "bar", "allow_false" => true))
assert_equal("bar", @filters.default({}, "bar", "allow_false" => true))
assert_template_result('false', "{{ false | default: 'bar', allow_false: true }}")
end
def test_cannot_access_private_methods
@@ -697,16 +712,16 @@ class StandardFiltersTest < Minitest::Test
{ "handle" => "alpha", "ok" => true },
{ "handle" => "beta", "ok" => false },
{ "handle" => "gamma", "ok" => false },
{ "handle" => "delta", "ok" => true }
{ "handle" => "delta", "ok" => true },
]
expectation = [
{ "handle" => "alpha", "ok" => true },
{ "handle" => "delta", "ok" => true }
{ "handle" => "delta", "ok" => true },
]
assert_equal expectation, @filters.where(input, "ok", true)
assert_equal expectation, @filters.where(input, "ok")
assert_equal(expectation, @filters.where(input, "ok", true))
assert_equal(expectation, @filters.where(input, "ok"))
end
def test_where_no_key_set
@@ -714,21 +729,21 @@ class StandardFiltersTest < Minitest::Test
{ "handle" => "alpha", "ok" => true },
{ "handle" => "beta" },
{ "handle" => "gamma" },
{ "handle" => "delta", "ok" => true }
{ "handle" => "delta", "ok" => true },
]
expectation = [
{ "handle" => "alpha", "ok" => true },
{ "handle" => "delta", "ok" => true }
{ "handle" => "delta", "ok" => true },
]
assert_equal expectation, @filters.where(input, "ok", true)
assert_equal expectation, @filters.where(input, "ok")
assert_equal(expectation, @filters.where(input, "ok", true))
assert_equal(expectation, @filters.where(input, "ok"))
end
def test_where_non_array_map_input
assert_equal [{ "a" => "ok" }], @filters.where({ "a" => "ok" }, "a", "ok")
assert_equal [], @filters.where({ "a" => "not ok" }, "a", "ok")
assert_equal([{ "a" => "ok" }], @filters.where({ "a" => "ok" }, "a", "ok"))
assert_equal([], @filters.where({ "a" => "not ok" }, "a", "ok"))
end
def test_where_indexable_but_non_map_value
@@ -740,17 +755,17 @@ class StandardFiltersTest < Minitest::Test
input = [
{ "message" => "Bonjour!", "language" => "French" },
{ "message" => "Hello!", "language" => "English" },
{ "message" => "Hallo!", "language" => "German" }
{ "message" => "Hallo!", "language" => "German" },
]
assert_equal [{ "message" => "Bonjour!", "language" => "French" }], @filters.where(input, "language", "French")
assert_equal [{ "message" => "Hallo!", "language" => "German" }], @filters.where(input, "language", "German")
assert_equal [{ "message" => "Hello!", "language" => "English" }], @filters.where(input, "language", "English")
assert_equal([{ "message" => "Bonjour!", "language" => "French" }], @filters.where(input, "language", "French"))
assert_equal([{ "message" => "Hallo!", "language" => "German" }], @filters.where(input, "language", "German"))
assert_equal([{ "message" => "Hello!", "language" => "English" }], @filters.where(input, "language", "English"))
end
def test_where_array_of_only_unindexable_values
assert_nil @filters.where([nil], "ok", true)
assert_nil @filters.where([nil], "ok")
assert_nil(@filters.where([nil], "ok", true))
assert_nil(@filters.where([nil], "ok"))
end
def test_where_no_target_value
@@ -758,10 +773,10 @@ class StandardFiltersTest < Minitest::Test
{ "foo" => false },
{ "foo" => true },
{ "foo" => "for sure" },
{ "bar" => true }
{ "bar" => true },
]
assert_equal [{ "foo" => true }, { "foo" => "for sure" }], @filters.where(input, "foo")
assert_equal([{ "foo" => true }, { "foo" => "for sure" }], @filters.where(input, "foo"))
end
private

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper'
class BreakTagTest < Minitest::Test

Some files were not shown because too many files have changed in this diff Show More