Compare commits

..
Author SHA1 Message Date
Umair Choudhary 9b06b71c34 added self time to profiler 2019-09-11 11:56:36 -04:00
Umair Choudhary 78cd06fb92 profiler 2019-09-10 15:38:54 -04:00
Umair Choudhary ca6237cb33 render time attribute 2019-09-10 15:32:31 -04:00
Umair Choudhary b9597b548c Add render time attr to profiler 2019-09-10 15:11:21 -04:00
151 changed files with 5214 additions and 7435 deletions
+2
View File
@@ -0,0 +1,2 @@
enabled:
- cla
-22
View File
@@ -1,22 +0,0 @@
name: Contributor License Agreement (CLA)
on:
pull_request_target:
types: [opened, synchronize]
issue_comment:
types: [created]
jobs:
cla:
runs-on: ubuntu-latest
if: |
(github.event.issue.pull_request
&& !github.event.issue.pull_request.merged_at
&& contains(github.event.comment.body, 'signed')
)
|| (github.event.pull_request && !github.event.pull_request.merged)
steps:
- uses: Shopify/shopify-cla-action@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
cla-token: ${{ secrets.CLA_TOKEN }}
-35
View File
@@ -1,35 +0,0 @@
name: Liquid
on: [push, pull_request]
env:
BUNDLE_JOBS: 4
BUNDLE_RETRY: 3
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
entry:
- { ruby: 2.7, allowed-failure: false } # minimum supported
- { ruby: 3.2, allowed-failure: false } # latest
- { ruby: ruby-head, allowed-failure: true }
name: Test Ruby ${{ matrix.entry.ruby }}
steps:
- uses: actions/checkout@v3
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.entry.ruby }}
bundler-cache: true
- run: bundle exec rake
continue-on-error: ${{ matrix.entry.allowed-failure }}
memory_profile:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ruby/setup-ruby@v1
with:
ruby-version: 2.7
bundler-cache: true
- run: bundle exec rake memory_profile:run
File diff suppressed because it is too large Load Diff
+3 -16
View File
@@ -1,7 +1,5 @@
inherit_gem:
rubocop-shopify: rubocop.yml
inherit_from: inherit_from:
- https://shopify.github.io/ruby-style-guide/rubocop.yml
- .rubocop_todo.yml - .rubocop_todo.yml
require: rubocop-performance require: rubocop-performance
@@ -10,20 +8,9 @@ Performance:
Enabled: true Enabled: true
AllCops: AllCops:
TargetRubyVersion: 2.7
NewCops: disable
SuggestExtensions: false
Exclude: Exclude:
- 'vendor/bundle/**/*' - 'vendor/bundle/**/*'
Naming/MethodName: Naming/MethodName:
Exclude: Exclude:
- 'example/server/liquid_servlet.rb' - 'example/server/liquid_servlet.rb'
Style/ClassMethodsDefinitions:
Enabled: false
# liquid filter calls were being mistaken to be calls on arrays
Style/ConcatArrayLiterals:
Exclude:
- 'test/integration/standard_filter_test.rb'
+335 -104
View File
@@ -1,84 +1,162 @@
# This configuration was generated by # This configuration was generated by
# `rubocop --auto-gen-config` # `rubocop --auto-gen-config`
# on 2022-05-18 19:25:47 UTC using RuboCop version 1.29.1. # on 2019-08-29 12:16:25 +1000 using RuboCop version 0.74.0.
# The point is for the user to remove these configuration records # The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base. # one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new # Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again. # versions of RuboCop, may require this file to be generated again.
# Offense count: 1 # Offense count: 13
# This cop supports safe auto-correction (--auto-correct). # Cop supports --auto-correct.
# Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation, Include. # Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle.
# Include: **/*.gemspec # SupportedHashRocketStyles: key, separator, table
Gemspec/OrderedDependencies: # SupportedColonStyles: key, separator, table
# SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit
Layout/AlignHash:
Exclude: Exclude:
- 'liquid.gemspec' - 'lib/liquid/condition.rb'
- 'lib/liquid/expression.rb'
- 'performance/shopify/comment_form.rb'
- 'performance/shopify/database.rb'
- 'performance/shopify/paginate.rb'
- 'test/unit/context_unit_test.rb'
# Offense count: 6 # Offense count: 3
# This cop supports safe auto-correction (--auto-correct). # Cop supports --auto-correct.
Layout/ClosingHeredocIndentation: # Configuration parameters: AllowForAlignment, AllowBeforeTrailingComments, ForceEqualSignAlignment.
Layout/ExtraSpacing:
Exclude:
- 'performance/shopify/paginate.rb'
# Offense count: 5
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: squiggly, active_support, powerpack, unindent
Layout/IndentHeredoc:
Exclude: Exclude:
- 'test/integration/tags/for_tag_test.rb' - 'test/integration/tags/for_tag_test.rb'
- 'test/integration/trim_mode_test.rb'
# Offense count: 34 # Offense count: 6
# This cop supports safe auto-correction (--auto-correct). # Cop supports --auto-correct.
Layout/EmptyLineAfterGuardClause: # 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: 4
# Cop supports --auto-correct.
# Configuration parameters: AllowForAlignment.
Layout/SpaceAroundOperators:
Exclude:
- 'lib/liquid/condition.rb'
- 'performance/shopify/paginate.rb'
# Offense count: 9
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces.
# SupportedStyles: space, no_space
# SupportedStylesForEmptyBraces: space, no_space
Layout/SpaceBeforeBlockBraces:
Exclude:
- 'example/server/server.rb'
- 'lib/liquid/variable.rb'
- 'test/integration/drop_test.rb'
- 'test/integration/standard_filter_test.rb'
- 'test/integration/tags/if_else_tag_test.rb'
# Offense count: 19
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBrackets.
# SupportedStyles: space, no_space, compact
# SupportedStylesForEmptyBrackets: space, no_space
Layout/SpaceInsideArrayLiteralBrackets:
Exclude:
- 'test/integration/drop_test.rb'
- 'test/integration/standard_filter_test.rb'
- 'test/integration/tags/for_tag_test.rb'
- 'test/integration/tags/include_tag_test.rb'
- 'test/integration/tags/standard_tag_test.rb'
- 'test/unit/context_unit_test.rb'
# Offense count: 2
Lint/AmbiguousOperator:
Exclude:
- 'test/unit/condition_unit_test.rb'
# Offense count: 16
# Configuration parameters: AllowSafeAssignment.
Lint/AssignmentInCondition:
Exclude: Exclude:
- 'lib/liquid/block.rb'
- 'lib/liquid/block_body.rb' - 'lib/liquid/block_body.rb'
- 'lib/liquid/context.rb'
- 'lib/liquid/drop.rb'
- 'lib/liquid/lexer.rb' - 'lib/liquid/lexer.rb'
- 'lib/liquid/parser.rb'
- 'lib/liquid/profiler/hooks.rb'
- 'lib/liquid/standardfilters.rb' - 'lib/liquid/standardfilters.rb'
- 'lib/liquid/tags/for.rb' - 'lib/liquid/tags/for.rb'
- 'lib/liquid/tags/if.rb' - 'lib/liquid/tags/if.rb'
- 'lib/liquid/utils.rb' - 'lib/liquid/tags/include.rb'
- 'lib/liquid/tags/raw.rb'
- 'lib/liquid/variable.rb' - 'lib/liquid/variable.rb'
- 'lib/liquid/variable_lookup.rb' - 'performance/profile.rb'
- 'performance/shopify/money_filter.rb' - 'test/test_helper.rb'
- 'performance/shopify/paginate.rb' - 'test/unit/tokenizer_unit_test.rb'
# Offense count: 8 # Offense count: 2
# This cop supports safe auto-correction (--auto-correct). # Cop supports --auto-correct.
# Configuration parameters: AllowAliasSyntax, AllowedMethods. # Configuration parameters: EnforcedStyle.
# AllowedMethods: alias_method, public, protected, private # SupportedStyles: runtime_error, standard_error
Layout/EmptyLinesAroundAttributeAccessor: Lint/InheritException:
Exclude: Exclude:
- 'lib/liquid/interrupts.rb'
# Offense count: 10
# Cop supports --auto-correct.
# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments.
Lint/UnusedBlockArgument:
Exclude:
- 'lib/liquid/condition.rb'
- 'lib/liquid/context.rb'
- 'lib/liquid/document.rb'
- 'lib/liquid/parse_context.rb'
- 'lib/liquid/template.rb' - 'lib/liquid/template.rb'
- 'performance/shopify/json_filter.rb'
- 'test/integration/filter_test.rb' - 'test/integration/filter_test.rb'
- 'test/integration/tags/include_tag_test.rb' - 'test/integration/render_profiling_test.rb'
- 'test/unit/strainer_template_unit_test.rb' - 'test/integration/variable_test.rb'
- 'test/unit/condition_unit_test.rb'
# Offense count: 17 # Offense count: 12
# This cop supports safe auto-correction (--auto-correct). # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, IndentationWidth. # Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods.
# SupportedStyles: aligned, indented Lint/UnusedMethodArgument:
Layout/LineEndStringConcatenationIndentation:
Exclude: Exclude:
- 'test/integration/tags/for_tag_test.rb' - 'example/server/liquid_servlet.rb'
- 'test/integration/tags/increment_tag_test.rb' - 'test/integration/blank_test.rb'
- 'test/integration/error_handling_test.rb'
- 'test/integration/filter_test.rb'
- 'test/integration/output_test.rb'
- 'test/integration/standard_filter_test.rb'
- 'test/integration/tags/include_tag_test.rb'
- 'test/unit/strainer_unit_test.rb'
# Offense count: 2
Lint/UselessAssignment:
Exclude:
- 'performance/shopify/database.rb'
# Offense count: 1 # Offense count: 1
# This cop supports safe auto-correction (--auto-correct). # Configuration parameters: CheckForMethodsWithNoSideEffects.
# Configuration parameters: EnforcedStyle, IndentationWidth. Lint/Void:
# SupportedStyles: aligned, indented
Layout/MultilineOperationIndentation:
Exclude: Exclude:
- 'lib/liquid/expression.rb' - 'lib/liquid/parse_context.rb'
# Offense count: 9 # Offense count: 95
Lint/MissingSuper: # Cop supports --auto-correct.
Exclude: # Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
- 'lib/liquid/forloop_drop.rb' # URISchemes: http, https
- 'lib/liquid/tablerowloop_drop.rb' Metrics/LineLength:
- 'test/integration/assign_test.rb' Max: 294
- 'test/integration/context_test.rb'
- 'test/integration/filter_test.rb'
- 'test/integration/standard_filter_test.rb'
- 'test/integration/tags/for_tag_test.rb'
- 'test/integration/tags/table_row_test.rb'
# Offense count: 44 # Offense count: 44
Naming/ConstantName: Naming/ConstantName:
@@ -91,6 +169,7 @@ Naming/ConstantName:
- 'lib/liquid/tags/cycle.rb' - 'lib/liquid/tags/cycle.rb'
- 'lib/liquid/tags/for.rb' - 'lib/liquid/tags/for.rb'
- 'lib/liquid/tags/if.rb' - 'lib/liquid/tags/if.rb'
- 'lib/liquid/tags/include.rb'
- 'lib/liquid/tags/raw.rb' - 'lib/liquid/tags/raw.rb'
- 'lib/liquid/tags/table_row.rb' - 'lib/liquid/tags/table_row.rb'
- 'lib/liquid/variable.rb' - 'lib/liquid/variable.rb'
@@ -98,83 +177,235 @@ Naming/ConstantName:
- 'performance/shopify/paginate.rb' - 'performance/shopify/paginate.rb'
- 'test/integration/tags/include_tag_test.rb' - 'test/integration/tags/include_tag_test.rb'
# Offense count: 9
# Configuration parameters: CheckIdentifiers, CheckConstants, CheckVariables, CheckStrings, CheckSymbols, CheckComments, CheckFilepaths, FlaggedTerms.
Naming/InclusiveLanguage:
Exclude:
- 'lib/liquid/drop.rb'
- 'lib/liquid/parse_context.rb'
- 'test/integration/drop_test.rb'
- 'test/integration/tags/if_else_tag_test.rb'
# Offense count: 2 # Offense count: 2
# Configuration parameters: .
# SupportedStyles: snake_case, camelCase
Naming/MethodName:
EnforcedStyle: snake_case
# Offense count: 3
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: always, conditionals
Style/AndOr:
Exclude:
- 'lib/liquid/i18n.rb'
- 'lib/liquid/tags/table_row.rb'
- 'lib/liquid/tokenizer.rb'
# Offense count: 40
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: braces, no_braces, context_dependent
Style/BracesAroundHashParameters:
Exclude:
- 'test/integration/error_handling_test.rb'
- 'test/integration/filter_test.rb'
- 'test/integration/render_profiling_test.rb'
- 'test/integration/standard_filter_test.rb'
- 'test/integration/tags/echo_test.rb'
- 'test/integration/tags/increment_tag_test.rb'
- 'test/integration/tags/standard_tag_test.rb'
- 'test/integration/template_test.rb'
- 'test/unit/condition_unit_test.rb'
- 'test/unit/context_unit_test.rb'
# Offense count: 5
Style/ClassVars: Style/ClassVars:
Exclude: Exclude:
- 'lib/liquid/condition.rb' - 'lib/liquid/condition.rb'
- 'lib/liquid/strainer.rb'
- 'lib/liquid/template.rb'
# Offense count: 3 # Offense count: 2
# This cop supports safe auto-correction (--auto-correct). # Cop supports --auto-correct.
Style/ExplicitBlockArgument: # Configuration parameters: EnforcedStyle, SingleLineConditionsOnly, IncludeTernaryExpressions.
# SupportedStyles: assign_to_condition, assign_inside_condition
Style/ConditionalAssignment:
Exclude: Exclude:
- 'test/integration/context_test.rb' - 'lib/liquid/errors.rb'
- 'test/integration/tag/disableable_test.rb' - 'performance/shopify/shop_filter.rb'
- 'test/integration/tags/for_tag_test.rb'
# Offense count: 2982 # Offense count: 1
# This cop supports safe auto-correction (--auto-correct). # Configuration parameters: AllowCoercion.
# Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. Style/DateTime:
# SupportedStyles: single_quotes, double_quotes Exclude:
Style/StringLiterals: - 'test/unit/context_unit_test.rb'
# Offense count: 2
# Cop supports --auto-correct.
Style/EachWithObject:
Exclude:
- 'performance/shopify/database.rb'
# Offense count: 1
# Cop supports --auto-correct.
Style/EmptyCaseCondition:
Exclude:
- 'lib/liquid/lexer.rb'
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: each, for
Style/For:
Exclude:
- 'performance/shopify/shop_filter.rb'
# Offense count: 9
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: format, sprintf, percent
Style/FormatString:
Exclude:
- 'example/server/example_servlet.rb'
- 'performance/shopify/money_filter.rb'
- 'performance/shopify/weight_filter.rb'
- 'test/integration/filter_test.rb'
- 'test/integration/hash_ordering_test.rb'
# Offense count: 115
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: always, never
Style/FrozenStringLiteralComment:
Enabled: false Enabled: false
# Offense count: 20 # Offense count: 30
# This cop supports safe auto-correction (--auto-correct). # Cop supports --auto-correct.
# Configuration parameters: IgnoreMacros, IgnoredMethods, IncludedMacros, AllowParenthesesInMultilineCall, AllowParenthesesInChaining, AllowParenthesesInCamelCaseMethod, EnforcedStyle.
# SupportedStyles: require_parentheses, omit_parentheses
Style/MethodCallWithArgsParentheses:
Exclude:
- 'Gemfile'
- 'Rakefile'
- 'lib/liquid/block_body.rb'
- 'lib/liquid/parser.rb'
- 'lib/liquid/tags/for.rb'
- 'liquid.gemspec'
- 'performance/shopify/database.rb'
- 'performance/shopify/liquid.rb'
- 'test/test_helper.rb'
- 'test/unit/condition_unit_test.rb'
- 'test/unit/tags/if_tag_unit_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: 52
# Cop supports --auto-correct.
Style/PerlBackrefs:
Enabled: false
# Offense count: 33
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: compact, exploded
Style/RaiseArgs:
Enabled: false
# Offense count: 1
# Cop supports --auto-correct.
Style/RedundantSelf:
Exclude:
- 'lib/liquid/strainer.rb'
# Offense count: 5
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, AllowInnerSlashes.
# SupportedStyles: slashes, percent_r, mixed
Style/RegexpLiteral:
Exclude:
- 'lib/liquid/file_system.rb'
- 'lib/liquid/standardfilters.rb'
- 'performance/shopify/shop_filter.rb'
- 'test/unit/condition_unit_test.rb'
# Offense count: 3
# Cop supports --auto-correct.
# Configuration parameters: ConvertCodeThatCanStartToReturnNil, Whitelist.
# Whitelist: present?, blank?, presence, try, try!
Style/SafeNavigation:
Exclude:
- 'lib/liquid/drop.rb'
- 'lib/liquid/strainer.rb'
- 'lib/liquid/tokenizer.rb'
# Offense count: 10
# Cop supports --auto-correct.
# Configuration parameters: AllowAsExpressionSeparator.
Style/Semicolon:
Exclude:
- 'performance/shopify/database.rb'
- 'test/integration/error_handling_test.rb'
- 'test/integration/template_test.rb'
- 'test/unit/context_unit_test.rb'
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: use_perl_names, use_english_names
Style/SpecialGlobalVars:
Exclude:
- 'performance/shopify/liquid.rb'
# Offense count: 2
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle. # Configuration parameters: EnforcedStyle.
# SupportedStyles: single_quotes, double_quotes # SupportedStyles: single_quotes, double_quotes
Style/StringLiteralsInInterpolation: Style/StringLiteralsInInterpolation:
Exclude: Exclude:
- 'lib/liquid/condition.rb'
- 'lib/liquid/strainer_template.rb'
- 'lib/liquid/tag/disableable.rb'
- 'performance/shopify/shop_filter.rb'
- 'performance/shopify/tag_filter.rb' - 'performance/shopify/tag_filter.rb'
# Offense count: 6 # Offense count: 2
# This cop supports safe auto-correction (--auto-correct). # 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: 21
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyleForMultiline. # Configuration parameters: EnforcedStyleForMultiline.
# SupportedStylesForMultiline: comma, consistent_comma, no_comma # SupportedStylesForMultiline: comma, consistent_comma, no_comma
Style/TrailingCommaInArrayLiteral: Style/TrailingCommaInArrayLiteral:
Exclude: Exclude:
- 'example/server/example_servlet.rb' - 'lib/liquid/parse_tree_visitor.rb'
- 'lib/liquid/condition.rb' - 'lib/liquid/tags/include.rb'
- 'test/integration/context_test.rb' - 'test/integration/parse_tree_visitor_test.rb'
- 'test/integration/standard_filter_test.rb' - 'test/integration/standard_filter_test.rb'
- 'test/unit/parse_tree_visitor_test.rb'
# Offense count: 1 # Offense count: 9
# This cop supports safe auto-correction (--auto-correct). # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyleForMultiline. # Configuration parameters: EnforcedStyleForMultiline.
# SupportedStylesForMultiline: comma, consistent_comma, no_comma # SupportedStylesForMultiline: comma, consistent_comma, no_comma
Style/TrailingCommaInHashLiteral: Style/TrailingCommaInHashLiteral:
Exclude: Exclude:
- 'lib/liquid/expression.rb' - 'lib/liquid/condition.rb'
- 'lib/liquid/lexer.rb'
- 'lib/liquid/standardfilters.rb'
- 'performance/shopify/comment_form.rb'
- 'performance/shopify/database.rb'
- 'performance/shopify/paginate.rb'
- 'performance/theme_runner.rb'
- 'test/integration/output_test.rb'
- 'test/unit/context_unit_test.rb'
# Offense count: 19 # Offense count: 2
# This cop supports safe auto-correction (--auto-correct). # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, MinSize, WordRegex. Style/UnneededPercentQ:
# SupportedStyles: percent, brackets
Style/WordArray:
Exclude: Exclude:
- 'lib/liquid/tags/if.rb' - 'test/integration/error_handling_test.rb'
- 'liquid.gemspec'
- 'test/integration/assign_test.rb'
- 'test/integration/context_test.rb'
- 'test/integration/drop_test.rb'
- 'test/integration/standard_filter_test.rb'
# Offense count: 117 # Offense count: 1
# This cop supports safe auto-correction (--auto-correct). # Cop supports --auto-correct.
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, IgnoredPatterns. Style/WhileUntilModifier:
# URISchemes: http, https Exclude:
Layout/LineLength: - 'lib/liquid/tags/case.rb'
Max: 260
+30
View File
@@ -0,0 +1,30 @@
language: ruby
cache: bundler
rvm:
- 2.4
- 2.5
- &latest_ruby 2.6
- 2.7
- ruby-head
- jruby-head
- truffleruby
matrix:
include:
- rvm: *latest_ruby
script: bundle exec rake memory_profile:run
name: Profiling Memory Usage
allow_failures:
- rvm: ruby-head
- rvm: jruby-head
- rvm: truffleruby
branches:
only:
- master
- gh-pages
- /.*-stable/
notifications:
disable: true
+1 -4
View File
@@ -5,7 +5,6 @@
* Bugfixes * Bugfixes
* Performance improvements * Performance improvements
* Features that are likely to be useful to the majority of Liquid users * Features that are likely to be useful to the majority of Liquid users
* Documentation updates that are concise and likely to be useful to the majority of Liquid users
## Things we won't merge ## Things we won't merge
@@ -15,14 +14,12 @@
* Features that can easily be implemented on top of Liquid (for example as a custom filter or custom filesystem) * Features that can easily be implemented on top of Liquid (for example as a custom filter or custom filesystem)
* Code that does not include tests * Code that does not include tests
* Code that breaks existing tests * Code that breaks existing tests
* Documentation changes that are verbose, incorrect or not important to most people (we want to keep it simple and easy to understand)
## Workflow ## Workflow
* [Sign the CLA](https://cla.shopify.com/) if you haven't already
* Fork the Liquid repository * Fork the Liquid repository
* Create a new branch in your fork * Create a new branch in your fork
* For updating [Liquid documentation](https://shopify.github.io/liquid/), create it from `gh-pages` branch. (You can skip tests.)
* If it makes sense, add tests for your code and/or run a performance benchmark * If it makes sense, add tests for your code and/or run a performance benchmark
* Make sure all tests pass (`bundle exec rake`) * Make sure all tests pass (`bundle exec rake`)
* Create a pull request * Create a pull request
+1 -4
View File
@@ -1,5 +1,3 @@
# frozen_string_literal: true
source 'https://rubygems.org' source 'https://rubygems.org'
git_source(:github) do |repo_name| git_source(:github) do |repo_name|
"https://github.com/#{repo_name}.git" "https://github.com/#{repo_name}.git"
@@ -18,8 +16,7 @@ group :benchmark, :test do
end end
group :test do group :test do
gem 'rubocop', '~> 1.44.0' gem 'rubocop', '~> 0.74.0', require: false
gem 'rubocop-shopify', '~> 2.12.0', require: false
gem 'rubocop-performance', require: false gem 'rubocop-performance', require: false
platform :mri, :truffleruby do platform :mri, :truffleruby do
-89
View File
@@ -1,94 +1,5 @@
# Liquid Change Log # Liquid Change Log
## 5.4.0 2022-07-29
### Breaking Changes
* Drop support for end-of-life Ruby versions (2.5 and 2.6) (#1578) [Andy Waite]
### Features
* Allow `#` to be used as an inline comment tag (#1498) [CP Clermont]
### Fixes
* `PartialCache` now shares snippet cache with subcontexts by default (#1553) [Chris AtLee]
* Hash registers no longer leak into subcontexts as static registers (#1564) [Chris AtLee]
* Fix `ParseTreeVisitor` for `with` variable expressions in `Render` tag (#1596) [CP Clermont]
### Changed
* Liquid::Context#registers now always returns a Liquid::Registers object, though supports the most used Hash functions for compatibility (#1553)
## 5.3.0 2022-03-22
### Fixes
* StandardFilter: Fix missing @context on iterations (#1525) [Thierry Joyal]
* Fix warning about block and default value in `static_registers.rb` (#1531) [Peter Zhu]
### Deprecation
* Condition#evaluate to require mandatory context argument in Liquid 6.0.0 (#1527) [Thierry Joyal]
## 5.2.0 2022-03-01
### Features
* Add `remove_last`, and `replace_last` filters (#1422) [Anders Hagbard]
* Eagerly cache global filters (#1524) [Jean Boussier]
### Fixes
* Fix some internal errors in filters from invalid input (#1476) [Dylan Thacker-Smith]
* Allow dash in filter kwarg name for consistency with Liquid::C (#1518) [CP Clermont]
## 5.1.0 / 2021-09-09
### Features
* Add `base64_encode`, `base64_decode`, `base64_url_safe_encode`, and `base64_url_safe_decode` filters (#1450) [Daniel Insley]
* Introduce `to_liquid_value` in `Liquid::Drop` (#1441) [Michael Go]
### Fixes
* Fix support for using a String subclass for the liquid source (#1421) [Dylan Thacker-Smith]
* Add `ParseTreeVisitor` to `RangeLookup` (#1470) [CP Clermont]
* Translate `RangeError` to `Liquid::Error` for `truncatewords` with large int (#1431) [Dylan Thacker-Smith]
## 5.0.1 / 2021-03-24
### Fixes
* Add ParseTreeVisitor to Echo tag (#1414) [CP Clermont]
* Test with ruby 3.0 as the latest ruby version (#1398) [Dylan Thacker-Smith]
* Handle carriage return in newlines_to_br (#1391) [Unending]
### Performance Improvements
* Use split limit in truncatewords (#1361) [Dylan Thacker-Smith]
## 5.0.0 / 2021-01-06
### Features
* Add new `{% render %}` tag (#1122) [Samuel Doiron]
* Add support for `as` in `{% render %}` and `{% include %}` (#1181) [Mike Angell]
* Add `{% liquid %}` and `{% echo %}` tags (#1086) [Justin Li]
* Add [usage tracking](README.md#usage-tracking) [Mike Angell]
* Add `Tag.disable_tags` for disabling tags that prepend `Tag::Disableable` at render time (#1162, #1274, #1275) [Mike Angell]
* Support using a profiler for multiple renders (#1365, #1366) [Dylan Thacker-Smith]
### Fixes
* Fix catastrophic backtracking in `RANGES_REGEX` regular expression (#1357) [Dylan Thacker-Smith]
* Make sure the for tag's limit and offset are integers (#1094) [David Cornu]
* Invokable methods for enumerable reject include (#1151) [Thierry Joyal]
* Allow `default` filter to handle `false` as value (#1144) [Mike Angell]
* Fix render length resource limit so it doesn't multiply nested output (#1285) [Dylan Thacker-Smith]
* Fix duplication of text in raw tags (#1304) [Peter Zhu]
* Fix strict parsing of find variable with a name expression (#1317) [Dylan Thacker-Smith]
* Use monotonic time to measure durations in Liquid::Profiler (#1362) [Dylan Thacker-Smith]
### Breaking Changes
* Require Ruby >= 2.5 (#1131, #1310) [Mike Angell, Dylan Thacker-Smith]
* Remove support for taint checking (#1268) [Dylan Thacker-Smith]
* Split Strainer class into StrainerFactory and StrainerTemplate (#1208) [Thierry Joyal]
* Remove handling of a nil context in the Strainer class (#1218) [Thierry Joyal]
* Handle `BlockBody#blank?` at parse time (#1287) [Dylan Thacker-Smith]
* Pass the tag markup and tokenizer to `Document#unknown_tag` (#1290) [Dylan Thacker-Smith]
* And several internal changes
### Performance Improvements
* Reduce allocations (#1073, #1091, #1115, #1099, #1117, #1141, #1322, #1341) [Richard Monette, Florian Weingarten, Ashwin Maroli]
* Improve resources limits performance (#1093, #1323) [Florian Weingarten, Dylan Thacker-Smith]
## 4.0.3 / 2019-03-12 ## 4.0.3 / 2019-03-12
### Fixed ### Fixed
+4 -10
View File
@@ -5,7 +5,7 @@
* [Contributing guidelines](CONTRIBUTING.md) * [Contributing guidelines](CONTRIBUTING.md)
* [Version history](History.md) * [Version history](History.md)
* [Liquid documentation from Shopify](https://shopify.dev/docs/api/liquid) * [Liquid documentation from Shopify](http://docs.shopify.com/themes/liquid-basics)
* [Liquid Wiki at GitHub](https://github.com/Shopify/liquid/wiki) * [Liquid Wiki at GitHub](https://github.com/Shopify/liquid/wiki)
* [Website](http://liquidmarkup.org/) * [Website](http://liquidmarkup.org/)
@@ -56,20 +56,20 @@ For standard use you can just pass it the content of a file and call render with
Setting the error mode of Liquid lets you specify how strictly you want your templates to be interpreted. Setting the error mode of Liquid lets you specify how strictly you want your templates to be interpreted.
Normally the parser is very lax and will accept almost anything without error. Unfortunately this can make Normally the parser is very lax and will accept almost anything without error. Unfortunately this can make
it very hard to debug and can lead to unexpected behaviour. it very hard to debug and can lead to unexpected behaviour.
Liquid also comes with a stricter parser that can be used when editing templates to give better error messages Liquid also comes with a stricter parser that can be used when editing templates to give better error messages
when templates are invalid. You can enable this new parser like this: when templates are invalid. You can enable this new parser like this:
```ruby ```ruby
Liquid::Template.error_mode = :strict # Raises a SyntaxError when invalid syntax is used Liquid::Template.error_mode = :strict # Raises a SyntaxError when invalid syntax is used
Liquid::Template.error_mode = :warn # Adds strict errors to template.errors but continues as normal Liquid::Template.error_mode = :warn # Adds errors to template.errors but continues as normal
Liquid::Template.error_mode = :lax # The default mode, accepts almost anything. Liquid::Template.error_mode = :lax # The default mode, accepts almost anything.
``` ```
If you want to set the error mode only on specific templates you can pass `:error_mode` as an option to `parse`: If you want to set the error mode only on specific templates you can pass `:error_mode` as an option to `parse`:
```ruby ```ruby
Liquid::Template.parse(source, error_mode: :strict) Liquid::Template.parse(source, :error_mode => :strict)
``` ```
This is useful for doing things like enabling strict mode only in the theme editor. This is useful for doing things like enabling strict mode only in the theme editor.
@@ -106,9 +106,3 @@ template = Liquid::Template.parse("{{x}} {{y}}")
template.render!({ 'x' => 1}, { strict_variables: true }) template.render!({ 'x' => 1}, { strict_variables: true })
#=> Liquid::UndefinedVariable: Liquid error: undefined variable y #=> 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.
+13 -21
View File
@@ -1,26 +1,18 @@
# frozen_string_literal: true
require 'rake' require 'rake'
require 'rake/testtask' require 'rake/testtask'
$LOAD_PATH.unshift(File.expand_path("../lib", __FILE__)) $LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
require "liquid/version" 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| Rake::TestTask.new(:base_test) do |t|
t.libs << 'lib' << 'test' t.libs << '.' << 'lib' << 'test'
t.test_files = FileList['test/{integration,unit}/**/*_test.rb'] t.test_files = FileList['test/{integration,unit}/**/*_test.rb']
t.verbose = false t.verbose = false
end end
Rake::TestTask.new(:integration_test) do |t| desc 'run test suite with warn error mode'
t.libs << 'lib' << 'test'
t.test_files = FileList['test/integration/**/*_test.rb']
t.verbose = false
end
desc('run test suite with warn error mode')
task :warn_test do task :warn_test do
ENV['LIQUID_PARSER_MODE'] = 'warn' ENV['LIQUID_PARSER_MODE'] = 'warn'
Rake::Task['base_test'].invoke Rake::Task['base_test'].invoke
@@ -33,7 +25,7 @@ task :rubocop do
end end
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 task :test do
ENV['LIQUID_PARSER_MODE'] = 'lax' ENV['LIQUID_PARSER_MODE'] = 'lax'
Rake::Task['base_test'].invoke Rake::Task['base_test'].invoke
@@ -46,16 +38,16 @@ task :test do
ENV['LIQUID_C'] = '1' ENV['LIQUID_C'] = '1'
ENV['LIQUID_PARSER_MODE'] = 'lax' ENV['LIQUID_PARSER_MODE'] = 'lax'
Rake::Task['integration_test'].reenable Rake::Task['base_test'].reenable
Rake::Task['integration_test'].invoke Rake::Task['base_test'].invoke
ENV['LIQUID_PARSER_MODE'] = 'strict' ENV['LIQUID_PARSER_MODE'] = 'strict'
Rake::Task['integration_test'].reenable Rake::Task['base_test'].reenable
Rake::Task['integration_test'].invoke Rake::Task['base_test'].invoke
end end
end end
task(gem: :build) task gem: :build
task :build do task :build do
system "gem build liquid.gemspec" system "gem build liquid.gemspec"
end end
@@ -102,7 +94,7 @@ namespace :memory_profile do
end end
end end
desc("Run example") desc "Run example"
task :example do task :example do
ruby "-w -d -Ilib example/server/server.rb" ruby "-w -d -Ilib example/server/server.rb"
end end
+6 -12
View File
@@ -1,8 +1,6 @@
# frozen_string_literal: true
module ProductsFilter module ProductsFilter
def price(integer) def price(integer)
format("$%.2d USD", integer / 100.0) sprintf("$%.2d USD", integer / 100.0)
end end
def prettyprint(text) def prettyprint(text)
@@ -30,18 +28,14 @@ class Servlet < LiquidServlet
private private
def products_list def products_list
[ [{ 'name' => 'Arbor Draft', 'price' => 39900, 'description' => 'the *arbor draft* is a excellent product' },
{ 'name' => 'Arbor Draft', 'price' => 39900, 'description' => 'the *arbor draft* is a excellent product' }, { 'name' => 'Arbor Element', 'price' => 40000, 'description' => 'the *arbor element* rocks for freestyling' },
{ 'name' => 'Arbor Element', 'price' => 40000, 'description' => 'the *arbor element* rocks for freestyling' }, { 'name' => 'Arbor Diamond', 'price' => 59900, 'description' => 'the *arbor diamond* is a made up product because im obsessed with arbor and have no creativity' }]
{ 'name' => 'Arbor Diamond', 'price' => 59900, 'description' => 'the *arbor diamond* is a made up product because im obsessed with arbor and have no creativity' }
]
end end
def more_products_list def more_products_list
[ [{ 'name' => 'Arbor Catalyst', 'price' => 39900, 'description' => 'the *arbor catalyst* is an advanced drop-through for freestyle and flatground performance and versatility' },
{ 'name' => 'Arbor Catalyst', 'price' => 39900, 'description' => 'the *arbor catalyst* is an advanced drop-through for freestyle and flatground performance and versatility' }, { 'name' => 'Arbor Fish', 'price' => 40000, 'description' => 'the *arbor fish* is a compact pin that features an extended wheelbase and time-honored teardrop shape' }]
{ 'name' => 'Arbor Fish', 'price' => 40000, 'description' => 'the *arbor fish* is a compact pin that features an extended wheelbase and time-honored teardrop shape' }
]
end end
def description def description
+4 -6
View File
@@ -1,5 +1,3 @@
# frozen_string_literal: true
class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet
def do_GET(req, res) def do_GET(req, res)
handle(:get, req, res) handle(:get, req, res)
@@ -11,17 +9,17 @@ class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet
private private
def handle(_type, req, res) def handle(type, req, res)
@request = req @request = req
@response = res @response = res
@request.path_info =~ /(\w+)\z/ @request.path_info =~ /(\w+)\z/
@action = Regexp.last_match(1) || 'index' @action = $1 || 'index'
@assigns = send(@action) if respond_to?(@action) @assigns = send(@action) if respond_to?(@action)
@response['Content-Type'] = "text/html" @response['Content-Type'] = "text/html"
@response.status = 200 @response.status = 200
@response.body = Liquid::Template.parse(read_template).render(@assigns, filters: [ProductsFilter]) @response.body = Liquid::Template.parse(read_template).render(@assigns, filters: [ProductsFilter])
end end
def read_template(filename = @action) def read_template(filename = @action)
+1 -3
View File
@@ -1,5 +1,3 @@
# frozen_string_literal: true
require 'webrick' require 'webrick'
require 'rexml/document' require 'rexml/document'
@@ -10,5 +8,5 @@ require_relative 'example_servlet'
# Setup webrick # Setup webrick
server = WEBrick::HTTPServer.new(Port: ARGV[1] || 3000) server = WEBrick::HTTPServer.new(Port: ARGV[1] || 3000)
server.mount('/', Servlet) server.mount('/', Servlet)
trap("INT") { server.shutdown } trap("INT"){ server.shutdown }
server.start server.start
+7 -18
View File
@@ -1,5 +1,3 @@
# frozen_string_literal: true
# Copyright (c) 2005 Tobias Luetke # Copyright (c) 2005 Tobias Luetke
# #
# Permission is hereby granted, free of charge, to any person obtaining # Permission is hereby granted, free of charge, to any person obtaining
@@ -23,13 +21,12 @@
module Liquid module Liquid
FilterSeparator = /\|/ FilterSeparator = /\|/
ArgumentSeparator = ',' ArgumentSeparator = ','.freeze
FilterArgumentSeparator = ':' FilterArgumentSeparator = ':'.freeze
VariableAttributeSeparator = '.' VariableAttributeSeparator = '.'.freeze
WhitespaceControl = '-' WhitespaceControl = '-'.freeze
TagStart = /\{\%/ TagStart = /\{\%/
TagEnd = /\%\}/ TagEnd = /\%\}/
TagName = /#|\w+/
VariableSignature = /\(?[\w\-\.\[\]]\)?/ VariableSignature = /\(?[\w\-\.\[\]]\)?/
VariableSegment = /[\w\-]/ VariableSegment = /[\w\-]/
VariableStart = /\{\{/ VariableStart = /\{\{/
@@ -37,13 +34,11 @@ module Liquid
VariableIncompleteEnd = /\}\}?/ VariableIncompleteEnd = /\}\}?/
QuotedString = /"[^"]*"|'[^']*'/ QuotedString = /"[^"]*"|'[^']*'/
QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o
TagAttributes = /(\w[\w-]*)\s*\:\s*(#{QuotedFragment})/o TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/o
AnyStartingTag = /#{TagStart}|#{VariableStart}/o AnyStartingTag = /#{TagStart}|#{VariableStart}/o
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om
VariableParser = /\[(?>[^\[\]]+|\g<0>)*\]|#{VariableSegment}+\??/o VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o
RAISE_EXCEPTION_LAMBDA = ->(_e) { raise }
singleton_class.send(:attr_accessor, :cache_classes) singleton_class.send(:attr_accessor, :cache_classes)
self.cache_classes = true self.cache_classes = true
@@ -60,14 +55,11 @@ require 'liquid/forloop_drop'
require 'liquid/extensions' require 'liquid/extensions'
require 'liquid/errors' require 'liquid/errors'
require 'liquid/interrupts' require 'liquid/interrupts'
require 'liquid/strainer_template' require 'liquid/strainer'
require 'liquid/strainer_factory'
require 'liquid/expression' require 'liquid/expression'
require 'liquid/context' require 'liquid/context'
require 'liquid/parser_switching' require 'liquid/parser_switching'
require 'liquid/tag' require 'liquid/tag'
require 'liquid/tag/disabler'
require 'liquid/tag/disableable'
require 'liquid/block' require 'liquid/block'
require 'liquid/block_body' require 'liquid/block_body'
require 'liquid/document' require 'liquid/document'
@@ -83,9 +75,6 @@ require 'liquid/utils'
require 'liquid/tokenizer' require 'liquid/tokenizer'
require 'liquid/parse_context' require 'liquid/parse_context'
require 'liquid/partial_cache' require 'liquid/partial_cache'
require 'liquid/usage'
require 'liquid/registers'
require 'liquid/template_factory'
# Load all the tags of the standard library # Load all the tags of the standard library
# #
+14 -34
View File
@@ -1,5 +1,3 @@
# frozen_string_literal: true
module Liquid module Liquid
class Block < Tag class Block < Tag
MAX_DEPTH = 100 MAX_DEPTH = 100
@@ -10,10 +8,9 @@ module Liquid
end end
def parse(tokens) def parse(tokens)
@body = new_body @body = BlockBody.new
while parse_body(@body, tokens) while parse_body(@body, tokens)
end end
@body.freeze
end end
# For backwards compatibility # For backwards compatibility
@@ -29,33 +26,20 @@ module Liquid
@body.nodelist @body.nodelist
end end
def unknown_tag(tag_name, _markup, _tokenizer) def unknown_tag(tag, _params, _tokens)
Block.raise_unknown_tag(tag_name, block_name, block_delimiter, parse_context) if tag == 'else'.freeze
end raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_else".freeze,
block_name: block_name))
# @api private elsif tag.start_with?('end'.freeze)
def self.raise_unknown_tag(tag, block_name, block_delimiter, parse_context) 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, tag: tag,
block_name: block_name, block_name: block_name,
block_delimiter: block_delimiter, block_delimiter: block_delimiter))
)
else else
raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag) raise SyntaxError.new(parse_context.locale.t("errors.syntax.unknown_tag".freeze, tag: tag))
end end
end end
def raise_tag_never_closed(block_name)
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_never_closed", block_name: block_name)
end
def block_name def block_name
@tag_name @tag_name
end end
@@ -64,17 +48,11 @@ module Liquid
@block_delimiter ||= "end#{block_name}" @block_delimiter ||= "end#{block_name}"
end end
private protected
# @api public
def new_body
parse_context.new_block_body
end
# @api public
def parse_body(body, tokens) def parse_body(body, tokens)
if parse_context.depth >= MAX_DEPTH if parse_context.depth >= MAX_DEPTH
raise StackLevelError, "Nesting too deep" raise StackLevelError, "Nesting too deep".freeze
end end
parse_context.depth += 1 parse_context.depth += 1
begin begin
@@ -82,7 +60,9 @@ module Liquid
@blank &&= body.blank? @blank &&= body.blank?
return false if end_tag_name == block_delimiter return false if end_tag_name == block_delimiter
raise_tag_never_closed(block_name) unless end_tag_name unless end_tag_name
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
end
# this tag is not registered with the system # this tag is not registered with the system
# pass it to the current block for special handling or error reporting # pass it to the current block for special handling or error reporting
+58 -138
View File
@@ -1,26 +1,20 @@
# frozen_string_literal: true
require 'English'
module Liquid module Liquid
class BlockBody class BlockBody
LiquidTagToken = /\A\s*(#{TagName})\s*(.*?)\z/o LiquidTagToken = /\A\s*(\w+)\s*(.*?)\z/o
FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(#{TagName})(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(\w+)(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
WhitespaceOrNothing = /\A\s*\z/ WhitespaceOrNothing = /\A\s*\z/
TAGSTART = "{%" TAGSTART = "{%".freeze
VARSTART = "{{" VARSTART = "{{".freeze
attr_reader :nodelist attr_reader :nodelist
def initialize def initialize
@nodelist = [] @nodelist = []
@blank = true @blank = true
end end
def parse(tokenizer, parse_context, &block) def parse(tokenizer, parse_context, &block)
raise FrozenError, "can't modify frozen Liquid::BlockBody" if frozen?
parse_context.line_number = tokenizer.line_number parse_context.line_number = tokenizer.line_number
if tokenizer.for_liquid_tag if tokenizer.for_liquid_tag
@@ -30,28 +24,17 @@ module Liquid
end end
end end
def freeze
@nodelist.freeze
super
end
private def parse_for_liquid_tag(tokenizer, parse_context) private def parse_for_liquid_tag(tokenizer, parse_context)
while (token = tokenizer.shift) while token = tokenizer.shift
unless token.empty? || token.match?(WhitespaceOrNothing) unless token.empty? || token =~ WhitespaceOrNothing
unless token =~ LiquidTagToken unless token =~ LiquidTagToken
# line isn't empty but didn't match tag syntax, yield and let the # line isn't empty but didn't match tag syntax, yield and let the
# caller raise a syntax error # caller raise a syntax error
return yield token, token return yield token, token
end end
tag_name = Regexp.last_match(1) tag_name = $1
markup = Regexp.last_match(2) markup = $2
unless tag = registered_tags[tag_name]
if tag_name == 'liquid'
parse_context.line_number -= 1
next parse_liquid_tag(markup, parse_context)
end
unless (tag = registered_tags[tag_name])
# end parsing if we reach an unknown tag and let the caller decide # end parsing if we reach an unknown tag and let the caller decide
# determine how to proceed # determine how to proceed
return yield tag_name, markup return yield tag_name, markup
@@ -66,87 +49,30 @@ module Liquid
yield nil, nil yield nil, nil
end end
# @api private
def self.unknown_tag_in_liquid_tag(tag, parse_context)
Block.raise_unknown_tag(tag, 'liquid', '%}', parse_context)
end
# @api private
def self.raise_missing_tag_terminator(token, parse_context)
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_termination", token: token, tag_end: TagEnd.inspect)
end
# @api private
def self.raise_missing_variable_terminator(token, parse_context)
raise SyntaxError, parse_context.locale.t("errors.syntax.variable_termination", token: token, tag_end: VariableEnd.inspect)
end
# @api private
def self.render_node(context, output, node)
node.render_to_output_buffer(context, output)
rescue => exc
blank_tag = !node.instance_of?(Variable) && node.blank?
rescue_render_node(context, output, node.line_number, exc, blank_tag)
end
# @api private
def self.rescue_render_node(context, output, line_number, exc, blank_tag)
case exc
when MemoryError
raise
when UndefinedVariable, UndefinedDropMethod, UndefinedFilter
context.handle_error(exc, line_number)
else
error_message = context.handle_error(exc, line_number)
unless blank_tag # conditional for backwards compatibility
output << error_message
end
end
end
private def parse_liquid_tag(markup, parse_context)
liquid_tag_tokenizer = parse_context.new_tokenizer(
markup, start_line_number: parse_context.line_number, for_liquid_tag: true
)
parse_for_liquid_tag(liquid_tag_tokenizer, parse_context) do |end_tag_name, _end_tag_markup|
if end_tag_name
BlockBody.unknown_tag_in_liquid_tag(end_tag_name, parse_context)
end
end
end
private def handle_invalid_tag_token(token, parse_context)
if token.end_with?('%}')
yield token, token
else
BlockBody.raise_missing_tag_terminator(token, parse_context)
end
end
private def parse_for_document(tokenizer, parse_context, &block) private def parse_for_document(tokenizer, parse_context, &block)
while (token = tokenizer.shift) while token = tokenizer.shift
next if token.empty? next if token.empty?
case case
when token.start_with?(TAGSTART) when token.start_with?(TAGSTART)
whitespace_handler(token, parse_context) whitespace_handler(token, parse_context)
unless token =~ FullToken unless token =~ FullToken
return handle_invalid_tag_token(token, parse_context, &block) raise_missing_tag_terminator(token, parse_context)
end end
tag_name = Regexp.last_match(2) tag_name = $2
markup = Regexp.last_match(4) markup = $4
if parse_context.line_number if parse_context.line_number
# newlines inside the tag should increase the line number, # newlines inside the tag should increase the line number,
# particularly important for multiline {% liquid %} tags # particularly important for multiline {% liquid %} tags
parse_context.line_number += Regexp.last_match(1).count("\n") + Regexp.last_match(3).count("\n") parse_context.line_number += $1.count("\n".freeze) + $3.count("\n".freeze)
end end
if tag_name == 'liquid' if tag_name == 'liquid'.freeze
parse_liquid_tag(markup, parse_context) liquid_tag_tokenizer = Tokenizer.new(markup, line_number: parse_context.line_number, for_liquid_tag: true)
next next parse_for_liquid_tag(liquid_tag_tokenizer, parse_context, &block)
end end
unless (tag = registered_tags[tag_name]) unless tag = registered_tags[tag_name]
# end parsing if we reach an unknown tag and let the caller decide # end parsing if we reach an unknown tag and let the caller decide
# determine how to proceed # determine how to proceed
return yield tag_name, markup return yield tag_name, markup
@@ -164,7 +90,7 @@ module Liquid
end end
parse_context.trim_whitespace = false parse_context.trim_whitespace = false
@nodelist << token @nodelist << token
@blank &&= token.match?(WhitespaceOrNothing) @blank &&= !!(token =~ WhitespaceOrNothing)
end end
parse_context.line_number = tokenizer.line_number parse_context.line_number = tokenizer.line_number
end end
@@ -175,12 +101,8 @@ module Liquid
def whitespace_handler(token, parse_context) def whitespace_handler(token, parse_context)
if token[2] == WhitespaceControl if token[2] == WhitespaceControl
previous_token = @nodelist.last previous_token = @nodelist.last
if previous_token.is_a?(String) if previous_token.is_a? String
first_byte = previous_token.getbyte(0)
previous_token.rstrip! previous_token.rstrip!
if previous_token.empty? && parse_context[:bug_compatible_whitespace_trimming] && first_byte
previous_token << first_byte
end
end end
end end
parse_context.trim_whitespace = (token[-3] == WhitespaceControl) parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
@@ -190,49 +112,38 @@ module Liquid
@blank @blank
end end
# Remove blank strings in the block body for a control flow tag (e.g. `if`, `for`, `case`, `unless`)
# with a blank body.
#
# For example, in a conditional assignment like the following
#
# ```
# {% if size > max_size %}
# {% assign size = max_size %}
# {% endif %}
# ```
#
# we assume the intention wasn't to output the blank spaces in the `if` tag's block body, so this method
# will remove them to reduce the render output size.
#
# Note that it is now preferred to use the `liquid` tag for this use case.
def remove_blank_strings
raise "remove_blank_strings only support being called on a blank block body" unless @blank
@nodelist.reject! { |node| node.instance_of?(String) }
end
def render(context) def render(context)
render_to_output_buffer(context, +'') render_to_output_buffer(context, '')
end end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
freeze unless frozen? context.resource_limits.render_score += @nodelist.length
context.resource_limits.increment_render_score(@nodelist.length)
idx = 0 idx = 0
while (node = @nodelist[idx]) while node = @nodelist[idx]
if node.instance_of?(String) previous_output_size = output.bytesize
case node
when String
output << node output << node
else when Variable
render_node(context, output, node) render_node(context, output, node)
when Block
render_node(context, node.blank? ? '' : output, node)
break if context.interrupt? # might have happened in a for-block
when Continue, Break
# If we get an Interrupt that means the block must stop processing. An # If we get an Interrupt that means the block must stop processing. An
# Interrupt is any command that stops block execution such as {% break %} # Interrupt is any command that stops block execution such as {% break %}
# or {% continue %}. These tags may also occur through Block or Include tags. # or {% continue %}
break if context.interrupt? # might have happened in a for-block context.push_interrupt(node.interrupt)
break
else # Other non-Block tags
render_node(context, output, node)
break if context.interrupt? # might have happened through an include
end end
idx += 1 idx += 1
context.resource_limits.increment_write_score(output) raise_if_resource_limits_reached(context, output.bytesize - previous_output_size)
end end
output output
@@ -241,25 +152,34 @@ module Liquid
private private
def render_node(context, output, node) def render_node(context, output, node)
BlockBody.render_node(context, output, node) node.render_to_output_buffer(context, output)
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
context.handle_error(e, node.line_number)
rescue ::StandardError => e
line_number = node.is_a?(String) ? nil : node.line_number
output << context.handle_error(e, line_number)
end
def raise_if_resource_limits_reached(context, length)
context.resource_limits.render_length += length
return unless context.resource_limits.reached?
raise MemoryError.new("Memory limits exceeded".freeze)
end end
def create_variable(token, parse_context) def create_variable(token, parse_context)
if token =~ ContentOfVariable token.scan(ContentOfVariable) do |content|
markup = Regexp.last_match(1) markup = content.first
return Variable.new(markup, parse_context) return Variable.new(markup, parse_context)
end end
BlockBody.raise_missing_variable_terminator(token, parse_context) raise_missing_variable_terminator(token, parse_context)
end end
# @deprecated Use {.raise_missing_tag_terminator} instead
def raise_missing_tag_terminator(token, parse_context) def raise_missing_tag_terminator(token, parse_context)
BlockBody.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))
end end
# @deprecated Use {.raise_missing_variable_terminator} instead
def raise_missing_variable_terminator(token, parse_context) def raise_missing_variable_terminator(token, parse_context)
BlockBody.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))
end end
def registered_tags def registered_tags
+25 -54
View File
@@ -1,5 +1,3 @@
# frozen_string_literal: true
module Liquid module Liquid
# Container for liquid nodes which conveniently wraps decision making logic # Container for liquid nodes which conveniently wraps decision making logic
# #
@@ -8,60 +6,41 @@ module Liquid
# c = Condition.new(1, '==', 1) # c = Condition.new(1, '==', 1)
# c.evaluate #=> true # c.evaluate #=> true
# #
class Condition # :nodoc: class Condition #:nodoc:
@@operators = { @@operators = {
'==' => ->(cond, left, right) { cond.send(:equal_variables, left, right) }, '=='.freeze => ->(cond, left, right) { cond.send(:equal_variables, left, right) },
'!=' => ->(cond, left, right) { !cond.send(:equal_variables, left, right) }, '!='.freeze => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
'<>' => ->(cond, left, right) { !cond.send(:equal_variables, left, right) }, '<>'.freeze => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
'<' => :<, '<'.freeze => :<,
'>' => :>, '>'.freeze => :>,
'>=' => :>=, '>='.freeze => :>=,
'<=' => :<=, '<='.freeze => :<=,
'contains' => lambda do |_cond, left, right| 'contains'.freeze => lambda do |cond, left, right|
if left && right && left.respond_to?(:include?) if left && right && left.respond_to?(:include?)
right = right.to_s if left.is_a?(String) right = right.to_s if left.is_a?(String)
left.include?(right) left.include?(right)
else else
false false
end end
end,
}
class MethodLiteral
attr_reader :method_name, :to_s
def initialize(method_name, to_s)
@method_name = method_name
@to_s = to_s
end end
end
@@method_literals = {
'blank' => MethodLiteral.new(:blank?, '').freeze,
'empty' => MethodLiteral.new(:empty?, '').freeze,
} }
def self.operators def self.operators
@@operators @@operators
end end
def self.parse_expression(parse_context, markup)
@@method_literals[markup] || parse_context.parse_expression(markup)
end
attr_reader :attachment, :child_condition attr_reader :attachment, :child_condition
attr_accessor :left, :operator, :right attr_accessor :left, :operator, :right
def initialize(left = nil, operator = nil, right = nil) def initialize(left = nil, operator = nil, right = nil)
@left = left @left = left
@operator = operator @operator = operator
@right = right @right = right
@child_relation = nil @child_relation = nil
@child_condition = nil @child_condition = nil
end end
def evaluate(context = deprecated_default_context) def evaluate(context = Context.new)
condition = self condition = self
result = nil result = nil
loop do loop do
@@ -69,9 +48,9 @@ module Liquid
case condition.child_relation case condition.child_relation
when :or when :or
break if Liquid::Utils.to_liquid_value(result) break if result
when :and when :and
break unless Liquid::Utils.to_liquid_value(result) break unless result
else else
break break
end end
@@ -81,12 +60,12 @@ module Liquid
end end
def or(condition) def or(condition)
@child_relation = :or @child_relation = :or
@child_condition = condition @child_condition = condition
end end
def and(condition) def and(condition)
@child_relation = :and @child_relation = :and
@child_condition = condition @child_condition = condition
end end
@@ -99,7 +78,7 @@ module Liquid
end end
def inspect def inspect
"#<Condition #{[@left, @operator, @right].compact.join(' ')}>" "#<Condition #{[@left, @operator, @right].compact.join(' '.freeze)}>"
end end
protected protected
@@ -109,7 +88,7 @@ module Liquid
private private
def equal_variables(left, right) def equal_variables(left, right)
if left.is_a?(MethodLiteral) if left.is_a?(Liquid::Expression::MethodLiteral)
if right.respond_to?(left.method_name) if right.respond_to?(left.method_name)
return right.send(left.method_name) return right.send(left.method_name)
else else
@@ -117,7 +96,7 @@ module Liquid
end end
end end
if right.is_a?(MethodLiteral) if right.is_a?(Liquid::Expression::MethodLiteral)
if left.respond_to?(right.method_name) if left.respond_to?(right.method_name)
return left.send(right.method_name) return left.send(right.method_name)
else else
@@ -134,10 +113,10 @@ module Liquid
# return this as the result. # return this as the result.
return context.evaluate(left) if op.nil? return context.evaluate(left) if op.nil?
left = Liquid::Utils.to_liquid_value(context.evaluate(left)) left = context.evaluate(left)
right = Liquid::Utils.to_liquid_value(context.evaluate(right)) right = context.evaluate(right)
operation = self.class.operators[op] || raise(Liquid::ArgumentError, "Unknown operator #{op}") operation = self.class.operators[op] || raise(Liquid::ArgumentError.new("Unknown operator #{op}"))
if operation.respond_to?(:call) if operation.respond_to?(:call)
operation.call(self, left, right) operation.call(self, left, right)
@@ -145,24 +124,16 @@ module Liquid
begin begin
left.send(operation, right) left.send(operation, right)
rescue ::ArgumentError => e rescue ::ArgumentError => e
raise Liquid::ArgumentError, e.message raise Liquid::ArgumentError.new(e.message)
end end
end end
end end
def deprecated_default_context
warn("DEPRECATION WARNING: Condition#evaluate without a context argument is deprecated" \
" and will be removed from Liquid 6.0.0.")
Context.new
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children def children
[ [
@node.left, @node.left, @node.right,
@node.right, @node.child_condition, @node.attachment
@node.child_condition,
@node.attachment
].compact ].compact
end end
end end
+39 -50
View File
@@ -1,5 +1,3 @@
# frozen_string_literal: true
module Liquid module Liquid
# Context keeps the variable stack and resolves variables, as well as keywords # Context keeps the variable stack and resolves variables, as well as keywords
# #
@@ -18,40 +16,35 @@ module Liquid
attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters
# rubocop:disable Metrics/ParameterLists # rubocop:disable Metrics/ParameterLists
def self.build(environments: {}, outer_scope: {}, registers: {}, rethrow_errors: false, resource_limits: nil, static_environments: {}, &block) def self.build(environments: {}, outer_scope: {}, registers: {}, rethrow_errors: false, resource_limits: nil, static_registers: {}, static_environments: {})
new(environments, outer_scope, registers, rethrow_errors, resource_limits, static_environments, &block) new(environments, outer_scope, registers, rethrow_errors, resource_limits, static_registers, static_environments)
end end
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil, static_environments = {}) def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil, static_registers = {}, static_environments = {})
@environments = [environments] @environments = [environments]
@environments.flatten! @environments.flatten!
@static_environments = [static_environments].flatten(1).freeze @static_environments = [static_environments].flat_map(&:freeze).freeze
@scopes = [(outer_scope || {})] @scopes = [(outer_scope || {})]
@registers = registers.is_a?(Registers) ? registers : Registers.new(registers) @registers = registers
@static_registers = static_registers.freeze
@errors = [] @errors = []
@partial = false @partial = false
@strict_variables = false @strict_variables = false
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits) @resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
@base_scope_depth = 0 @base_scope_depth = 0
@interrupts = [] squash_instance_assigns_with_environments
@filters = []
@global_filter = nil
@disabled_tags = {}
@registers.static[:cached_partials] ||= {} @this_stack_used = false
@registers.static[:file_system] ||= Liquid::Template.file_system
@registers.static[:template_factory] ||= Liquid::TemplateFactory.new
self.exception_renderer = Template.default_exception_renderer self.exception_renderer = Template.default_exception_renderer
if rethrow_errors if rethrow_errors
self.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA self.exception_renderer = ->(e) { raise }
end end
yield self if block_given? @interrupts = []
@filters = []
# Do this last, since it could result in this object being passed to a Proc in the environment @global_filter = nil
squash_instance_assigns_with_environments
end end
# rubocop:enable Metrics/ParameterLists # rubocop:enable Metrics/ParameterLists
@@ -60,7 +53,7 @@ module Liquid
end end
def strainer def strainer
@strainer ||= StrainerFactory.create(self, @filters) @strainer ||= Strainer.create(self, @filters)
end end
# Adds filters to this context. # Adds filters to this context.
@@ -95,7 +88,7 @@ module Liquid
def handle_error(e, line_number = nil) def handle_error(e, line_number = nil)
e = internal_error unless e.is_a?(Liquid::Error) e = internal_error unless e.is_a?(Liquid::Error)
e.template_name ||= template_name e.template_name ||= template_name
e.line_number ||= line_number e.line_number ||= line_number
errors.push(e) errors.push(e)
exception_renderer.call(e).to_s exception_renderer.call(e).to_s
end end
@@ -128,12 +121,20 @@ module Liquid
# context['var'] = 'hi' # context['var'] = 'hi'
# end # end
# #
# context['var'] #=> nil # context['var] #=> nil
def stack(new_scope = {}) def stack(new_scope = nil)
push(new_scope) old_stack_used = @this_stack_used
if new_scope
push(new_scope)
@this_stack_used = true
else
@this_stack_used = false
end
yield yield
ensure ensure
pop pop if @this_stack_used
@this_stack_used = old_stack_used
end end
# Creates a new context inheriting resource limits, filters, environment etc., # Creates a new context inheriting resource limits, filters, environment etc.,
@@ -141,18 +142,17 @@ module Liquid
def new_isolated_subcontext def new_isolated_subcontext
check_overflow check_overflow
self.class.build( Context.build(
resource_limits: resource_limits, resource_limits: resource_limits,
static_environments: static_environments, static_environments: static_environments,
registers: Registers.new(registers), static_registers: static_registers
).tap do |subcontext| ).tap do |subcontext|
subcontext.base_scope_depth = base_scope_depth + 1 subcontext.base_scope_depth = base_scope_depth + 1
subcontext.exception_renderer = exception_renderer subcontext.exception_renderer = exception_renderer
subcontext.filters = @filters subcontext.filters = @filters
subcontext.strainer = nil subcontext.strainer = nil
subcontext.errors = errors subcontext.errors = errors
subcontext.warnings = warnings subcontext.warnings = warnings
subcontext.disabled_tags = @disabled_tags
end end
end end
@@ -162,6 +162,10 @@ module Liquid
# Only allow String, Numeric, Hash, Array, Proc, Boolean or <tt>Liquid::Drop</tt> # Only allow String, Numeric, Hash, Array, Proc, Boolean or <tt>Liquid::Drop</tt>
def []=(key, value) def []=(key, value)
unless @this_stack_used
@this_stack_used = true
push({})
end
@scopes[0][key] = value @scopes[0][key] = value
end end
@@ -197,7 +201,7 @@ module Liquid
try_variable_find_in_environments(key, raise_on_not_found: raise_on_not_found) try_variable_find_in_environments(key, raise_on_not_found: raise_on_not_found)
end end
variable = variable.to_liquid variable = variable.to_liquid
variable.context = self if variable.respond_to?(:context=) variable.context = self if variable.respond_to?(:context=)
variable variable
@@ -211,30 +215,15 @@ module Liquid
value = obj[key] value = obj[key]
if value.is_a?(Proc) && obj.respond_to?(:[]=) 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 else
value value
end end
end end
def with_disabled_tags(tag_names)
tag_names.each do |name|
@disabled_tags[name] = @disabled_tags.fetch(name, 0) + 1
end
yield
ensure
tag_names.each do |name|
@disabled_tags[name] -= 1
end
end
def tag_disabled?(tag_name)
@disabled_tags.fetch(tag_name, 0) > 0
end
protected protected
attr_writer :base_scope_depth, :warnings, :errors, :strainer, :filters, :disabled_tags attr_writer :base_scope_depth, :warnings, :errors, :strainer, :filters
private private
@@ -257,7 +246,7 @@ module Liquid
end end
def check_overflow def check_overflow
raise StackLevelError, "Nesting too deep" if overflow? raise StackLevelError, "Nesting too deep".freeze if overflow?
end end
def overflow? def overflow?
+9 -47
View File
@@ -1,64 +1,26 @@
# frozen_string_literal: true
module Liquid module Liquid
class Document class Document < BlockBody
def self.parse(tokens, parse_context) def self.parse(tokens, parse_context)
doc = new(parse_context) doc = new
doc.parse(tokens, parse_context) doc.parse(tokens, parse_context)
doc doc
end end
attr_reader :parse_context, :body def parse(tokens, parse_context)
super do |end_tag_name, end_tag_params|
def initialize(parse_context) unknown_tag(end_tag_name, parse_context) if end_tag_name
@parse_context = parse_context
@body = new_body
end
def nodelist
@body.nodelist
end
def parse(tokenizer, parse_context)
while parse_body(tokenizer)
end end
@body.freeze
rescue SyntaxError => e rescue SyntaxError => e
e.line_number ||= parse_context.line_number e.line_number ||= parse_context.line_number
raise raise
end end
def unknown_tag(tag, _markup, _tokenizer) def unknown_tag(tag, parse_context)
case tag case tag
when 'else', 'end' when 'else'.freeze, 'end'.freeze
raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_outer_tag", tag: tag) raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_outer_tag".freeze, tag: tag))
else else
raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag) raise SyntaxError.new(parse_context.locale.t("errors.syntax.unknown_tag".freeze, tag: tag))
end
end
def render_to_output_buffer(context, output)
@body.render_to_output_buffer(context, output)
end
def render(context)
render_to_output_buffer(context, +'')
end
private
def new_body
parse_context.new_block_body
end
def parse_body(tokenizer)
@body.parse(tokenizer, parse_context) do |unknown_tag_name, unknown_tag_markup|
if unknown_tag_name
unknown_tag(unknown_tag_name, unknown_tag_markup, tokenizer)
true
else
false
end
end end
end end
end end
+2 -8
View File
@@ -1,5 +1,3 @@
# frozen_string_literal: true
require 'set' require 'set'
module Liquid module Liquid
@@ -25,13 +23,9 @@ module Liquid
class Drop class Drop
attr_writer :context attr_writer :context
def initialize
@context = nil
end
# Catch all for the method # Catch all for the method
def liquid_method_missing(method) def liquid_method_missing(method)
return nil unless @context&.strict_variables return nil unless @context && @context.strict_variables
raise Liquid::UndefinedDropMethod, "undefined method #{method}" raise Liquid::UndefinedDropMethod, "undefined method #{method}"
end end
@@ -73,7 +67,7 @@ module Liquid
if include?(Enumerable) if include?(Enumerable)
blacklist += Enumerable.public_instance_methods blacklist += Enumerable.public_instance_methods
blacklist -= [:sort, :count, :first, :min, :max] blacklist -= [:sort, :count, :first, :min, :max, :include?]
end end
whitelist = [:to_liquid] + (public_instance_methods - blacklist) whitelist = [:to_liquid] + (public_instance_methods - blacklist)
+18 -20
View File
@@ -1,5 +1,3 @@
# frozen_string_literal: true
module Liquid module Liquid
class Error < ::StandardError class Error < ::StandardError
attr_accessor :line_number attr_accessor :line_number
@@ -7,7 +5,7 @@ module Liquid
attr_accessor :markup_context attr_accessor :markup_context
def to_s(with_prefix = true) def to_s(with_prefix = true)
str = +"" str = ""
str << message_prefix if with_prefix str << message_prefix if with_prefix
str << super() str << super()
@@ -22,11 +20,11 @@ module Liquid
private private
def message_prefix def message_prefix
str = +"" str = ""
str << if is_a?(SyntaxError) if is_a?(SyntaxError)
"Liquid syntax error" str << "Liquid syntax error"
else else
"Liquid error" str << "Liquid error"
end end
if line_number if line_number
@@ -40,19 +38,19 @@ module Liquid
end end
end end
ArgumentError = Class.new(Error) ArgumentError = Class.new(Error)
ContextError = Class.new(Error) ContextError = Class.new(Error)
FileSystemError = Class.new(Error) FileSystemError = Class.new(Error)
StandardError = Class.new(Error) StandardError = Class.new(Error)
SyntaxError = Class.new(Error) SyntaxError = Class.new(Error)
StackLevelError = Class.new(Error) StackLevelError = Class.new(Error)
MemoryError = Class.new(Error) TaintedError = Class.new(Error)
ZeroDivisionError = Class.new(Error) MemoryError = Class.new(Error)
FloatDomainError = Class.new(Error) ZeroDivisionError = Class.new(Error)
UndefinedVariable = Class.new(Error) FloatDomainError = Class.new(Error)
UndefinedVariable = Class.new(Error)
UndefinedDropMethod = Class.new(Error) UndefinedDropMethod = Class.new(Error)
UndefinedFilter = Class.new(Error) UndefinedFilter = Class.new(Error)
MethodOverrideError = Class.new(Error) MethodOverrideError = Class.new(Error)
DisabledError = Class.new(Error) InternalError = Class.new(Error)
InternalError = Class.new(Error)
end end
+36 -35
View File
@@ -1,44 +1,45 @@
# frozen_string_literal: true
module Liquid module Liquid
class Expression class Expression
LITERALS = { class MethodLiteral
nil => nil, attr_reader :method_name, :to_s
'nil' => nil,
'null' => nil,
'' => nil,
'true' => true,
'false' => false,
'blank' => '',
'empty' => ''
}.freeze
INTEGERS_REGEX = /\A(-?\d+)\z/ def initialize(method_name, to_s)
FLOATS_REGEX = /\A(-?\d[\d\.]+)\z/ @method_name = method_name
@to_s = to_s
# Use an atomic group (?>...) to avoid pathological backtracing from
# malicious input as described in https://github.com/Shopify/liquid/issues/1357
RANGES_REGEX = /\A\(\s*(?>(\S+)\s*\.\.)\s*(\S+)\s*\)\z/
def self.parse(markup)
return nil unless markup
markup = markup.strip
if (markup.start_with?('"') && markup.end_with?('"')) ||
(markup.start_with?("'") && markup.end_with?("'"))
return markup[1..-2]
end end
case markup def to_liquid
when INTEGERS_REGEX to_s
Regexp.last_match(1).to_i end
when RANGES_REGEX end
RangeLookup.parse(Regexp.last_match(1), Regexp.last_match(2))
when FLOATS_REGEX LITERALS = {
Regexp.last_match(1).to_f 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
}.freeze
SINGLE_QUOTED_STRING = /\A'(.*)'\z/m
DOUBLE_QUOTED_STRING = /\A"(.*)"\z/m
INTEGERS_REGEX = /\A(-?\d+)\z/
FLOATS_REGEX = /\A(-?\d[\d\.]+)\z/
RANGES_REGEX = /\A\((\S+)\.\.(\S+)\)\z/
def self.parse(markup)
if LITERALS.key?(markup)
LITERALS[markup]
else else
if LITERALS.key?(markup) case markup
LITERALS[markup] when SINGLE_QUOTED_STRING, DOUBLE_QUOTED_STRING
$1
when INTEGERS_REGEX
$1.to_i
when RANGES_REGEX
RangeLookup.parse($1, $2)
when FLOATS_REGEX
$1.to_f
else else
VariableLookup.parse(markup) VariableLookup.parse(markup)
end end
-2
View File
@@ -1,5 +1,3 @@
# frozen_string_literal: true
require 'time' require 'time'
require 'date' require 'date'
+4 -6
View File
@@ -1,5 +1,3 @@
# frozen_string_literal: true
module Liquid module Liquid
# A Liquid file system is a way to let your templates retrieve other templates for use with the include tag. # A Liquid file system is a way to let your templates retrieve other templates for use with the include tag.
# #
@@ -46,8 +44,8 @@ module Liquid
class LocalFileSystem class LocalFileSystem
attr_accessor :root attr_accessor :root
def initialize(root, pattern = "_%s.liquid") def initialize(root, pattern = "_%s.liquid".freeze)
@root = root @root = root
@pattern = pattern @pattern = pattern
end end
@@ -59,9 +57,9 @@ module Liquid
end end
def full_path(template_path) def full_path(template_path)
raise FileSystemError, "Illegal template name '#{template_path}'" unless %r{\A[^./][a-zA-Z0-9_/]+\z}.match?(template_path) raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ /\A[^.\/][a-zA-Z0-9_\/]+\z/
full_path = if template_path.include?('/') full_path = if template_path.include?('/'.freeze)
File.join(root, File.dirname(template_path), @pattern % File.basename(template_path)) File.join(root, File.dirname(template_path), @pattern % File.basename(template_path))
else else
File.join(root, @pattern % template_path) File.join(root, @pattern % template_path)
+4 -51
View File
@@ -1,81 +1,34 @@
# frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs
# @liquid_type object
# @liquid_name forloop
# @liquid_summary
# Information about a parent [`for` loop](/docs/api/liquid/tags/for).
class ForloopDrop < Drop class ForloopDrop < Drop
def initialize(name, length, parentloop) def initialize(name, length, parentloop)
@name = name @name = name
@length = length @length = length
@parentloop = parentloop @parentloop = parentloop
@index = 0 @index = 0
end end
# @liquid_public_docs attr_reader :name, :length, :parentloop
# @liquid_name length
# @liquid_summary
# The total number of iterations in the loop.
# @liquid_return [number]
attr_reader :length
# @liquid_public_docs
# @liquid_name parentloop
# @liquid_summary
# The parent `forloop` object.
# @liquid_description
# If the current `for` loop isn't nested inside another `for` loop, then `nil` is returned.
# @liquid_return [forloop]
attr_reader :parentloop
attr_reader :name
# @liquid_public_docs
# @liquid_summary
# The 1-based index of the current iteration.
# @liquid_return [number]
def index def index
@index + 1 @index + 1
end end
# @liquid_public_docs
# @liquid_summary
# The 0-based index of the current iteration.
# @liquid_return [number]
def index0 def index0
@index @index
end end
# @liquid_public_docs
# @liquid_summary
# The 1-based index of the current iteration, in reverse order.
# @liquid_return [number]
def rindex def rindex
@length - @index @length - @index
end end
# @liquid_public_docs
# @liquid_summary
# The 0-based index of the current iteration, in reverse order.
# @liquid_return [number]
def rindex0 def rindex0
@length - @index - 1 @length - @index - 1
end end
# @liquid_public_docs
# @liquid_summary
# Returns `true` if the current iteration is the first. Returns `false` if not.
# @liquid_return [boolean]
def first def first
@index == 0 @index == 0
end end
# @liquid_public_docs
# @liquid_summary
# Returns `true` if the current iteration is the last. Returns `false` if not.
# @liquid_return [boolean]
def last def last
@index == @length - 1 @index == @length - 1
end end
+3 -5
View File
@@ -1,5 +1,3 @@
# frozen_string_literal: true
require 'yaml' require 'yaml'
module Liquid module Liquid
@@ -28,13 +26,13 @@ module Liquid
def interpolate(name, vars) def interpolate(name, vars)
name.gsub(/%\{(\w+)\}/) do name.gsub(/%\{(\w+)\}/) do
# raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym] # raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym]
(vars[Regexp.last_match(1).to_sym]).to_s (vars[$1.to_sym]).to_s
end end
end end
def deep_fetch_translation(name) def deep_fetch_translation(name)
name.split('.').reduce(locale) do |level, cur| name.split('.'.freeze).reduce(locale) do |level, cur|
level[cur] || raise(TranslationError, "Translation for #{name} does not exist in locale #{path}") level[cur] or raise TranslationError, "Translation for #{name} does not exist in locale #{path}"
end end
end end
end end
+1 -3
View File
@@ -1,12 +1,10 @@
# frozen_string_literal: true
module Liquid module Liquid
# An interrupt is any command that breaks processing of a block (ex: a for loop). # An interrupt is any command that breaks processing of a block (ex: a for loop).
class Interrupt class Interrupt
attr_reader :message attr_reader :message
def initialize(message = nil) def initialize(message = nil)
@message = message || "interrupt" @message = message || "interrupt".freeze
end end
end end
+23 -29
View File
@@ -1,27 +1,24 @@
# frozen_string_literal: true
require "strscan" require "strscan"
module Liquid module Liquid
class Lexer class Lexer
SPECIALS = { SPECIALS = {
'|' => :pipe, '|'.freeze => :pipe,
'.' => :dot, '.'.freeze => :dot,
':' => :colon, ':'.freeze => :colon,
',' => :comma, ','.freeze => :comma,
'[' => :open_square, '['.freeze => :open_square,
']' => :close_square, ']'.freeze => :close_square,
'(' => :open_round, '('.freeze => :open_round,
')' => :close_round, ')'.freeze => :close_round,
'?' => :question, '?'.freeze => :question,
'-' => :dash, '-'.freeze => :dash
}.freeze }.freeze
IDENTIFIER = /[a-zA-Z_][\w-]*\??/ IDENTIFIER = /[a-zA-Z_][\w-]*\??/
SINGLE_STRING_LITERAL = /'[^\']*'/ SINGLE_STRING_LITERAL = /'[^\']*'/
DOUBLE_STRING_LITERAL = /"[^\"]*"/ DOUBLE_STRING_LITERAL = /"[^\"]*"/
STRING_LITERAL = Regexp.union(SINGLE_STRING_LITERAL, DOUBLE_STRING_LITERAL) NUMBER_LITERAL = /-?\d+(\.\d+)?/
NUMBER_LITERAL = /-?\d+(\.\d+)?/ DOTDOT = /\.\./
DOTDOT = /\.\./ COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/
WHITESPACE_OR_NOTHING = /\s*/ WHITESPACE_OR_NOTHING = /\s*/
def initialize(input) def initialize(input)
@@ -34,19 +31,16 @@ module Liquid
until @ss.eos? until @ss.eos?
@ss.skip(WHITESPACE_OR_NOTHING) @ss.skip(WHITESPACE_OR_NOTHING)
break if @ss.eos? break if @ss.eos?
tok = if (t = @ss.scan(COMPARISON_OPERATOR)) tok = case
[:comparison, t] when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t]
elsif (t = @ss.scan(STRING_LITERAL)) when t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t]
[:string, t] when t = @ss.scan(DOUBLE_STRING_LITERAL) then [:string, t]
elsif (t = @ss.scan(NUMBER_LITERAL)) when t = @ss.scan(NUMBER_LITERAL) then [:number, t]
[:number, t] when t = @ss.scan(IDENTIFIER) then [:id, t]
elsif (t = @ss.scan(IDENTIFIER)) when t = @ss.scan(DOTDOT) then [:dotdot, t]
[:id, t]
elsif (t = @ss.scan(DOTDOT))
[:dotdot, t]
else else
c = @ss.getch c = @ss.getch
if (s = SPECIALS[c]) if s = SPECIALS[c]
[s, c] [s, c]
else else
raise SyntaxError, "Unexpected character #{c}" raise SyntaxError, "Unexpected character #{c}"
+6 -8
View File
@@ -13,17 +13,15 @@
for_invalid_attribute: "Invalid attribute in for loop. Valid attributes are limit and offset" for_invalid_attribute: "Invalid attribute in for loop. Valid attributes are limit and offset"
if: "Syntax Error in tag 'if' - Valid syntax: if [expression]" if: "Syntax Error in tag 'if' - Valid syntax: if [expression]"
include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]" include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]"
inline_comment_invalid: "Syntax error in tag '#' - Each line of comments must be prefixed by the '#' character" unknown_tag: "Unknown tag '%{tag}'"
invalid_delimiter: "'%{tag}' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}" invalid_delimiter: "'%{tag}' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
render: "Syntax error in tag 'render' - Template name must be a quoted string"
table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
tag_never_closed: "'%{block_name}' tag was never closed"
tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
unexpected_else: "%{block_name} tag does not expect 'else' tag" unexpected_else: "%{block_name} tag does not expect 'else' tag"
unexpected_outer_tag: "Unexpected outer '%{tag}' tag" unexpected_outer_tag: "Unexpected outer '%{tag}' tag"
unknown_tag: "Unknown tag '%{tag}'" tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}" variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}"
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: argument:
include: "Argument error in tag 'include' - Illegal template name" include: "Argument error in tag 'include' - Illegal template name"
disabled:
tag: "usage is not allowed in this context"
+4 -20
View File
@@ -1,5 +1,3 @@
# frozen_string_literal: true
module Liquid module Liquid
class ParseContext class ParseContext
attr_accessor :locale, :line_number, :trim_whitespace, :depth attr_accessor :locale, :line_number, :trim_whitespace, :depth
@@ -7,11 +5,9 @@ module Liquid
def initialize(options = {}) def initialize(options = {})
@template_options = options ? options.dup : {} @template_options = options ? options.dup : {}
@locale = @template_options[:locale] ||= I18n.new
@locale = @template_options[:locale] ||= I18n.new
@warnings = [] @warnings = []
self.depth = 0
self.depth = 0
self.partial = false self.partial = false
end end
@@ -19,23 +15,11 @@ module Liquid
@options[option_key] @options[option_key]
end end
def new_block_body
Liquid::BlockBody.new
end
def new_tokenizer(markup, start_line_number: nil, for_liquid_tag: false)
Tokenizer.new(markup, line_number: start_line_number, for_liquid_tag: for_liquid_tag)
end
def parse_expression(markup)
Expression.parse(markup)
end
def partial=(value) def partial=(value)
@partial = value @partial = value
@options = value ? partial_options : @template_options @options = value ? partial_options : @template_options
@error_mode = @options[:error_mode] || Template.error_mode @error_mode = @options[:error_mode] || Template.error_mode
value
end end
def partial_options def partial_options
@@ -44,7 +28,7 @@ module Liquid
if dont_pass == true if dont_pass == true
{ locale: locale } { locale: locale }
elsif dont_pass.is_a?(Array) 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 else
@template_options @template_options
end end
+2 -2
View File
@@ -11,7 +11,7 @@ module Liquid
end end
def initialize(node, callbacks) def initialize(node, callbacks)
@node = node @node = node
@callbacks = callbacks @callbacks = callbacks
end end
@@ -28,7 +28,7 @@ module Liquid
item, new_context = @callbacks[node.class].call(node, context) item, new_context = @callbacks[node.class].call(node, context)
[ [
item, item,
ParseTreeVisitor.for(node, @callbacks).visit(new_context || context), ParseTreeVisitor.for(node, @callbacks).visit(new_context || context)
] ]
end end
end end
+21 -30
View File
@@ -1,11 +1,9 @@
# frozen_string_literal: true
module Liquid module Liquid
class Parser class Parser
def initialize(input) def initialize(input)
l = Lexer.new(input) l = Lexer.new(input)
@tokens = l.tokenize @tokens = l.tokenize
@p = 0 # pointer to current location @p = 0 # pointer to current location
end end
def jump(point) def jump(point)
@@ -46,20 +44,16 @@ module Liquid
tok[0] == type tok[0] == type
end end
SINGLE_TOKEN_EXPRESSION_TYPES = [:string, :number].freeze
private_constant :SINGLE_TOKEN_EXPRESSION_TYPES
def expression def expression
token = @tokens[@p] token = @tokens[@p]
case token[0] if token[0] == :id
when :id variable_signature
str = consume elsif SINGLE_TOKEN_EXPRESSION_TYPES.include? token[0]
str << variable_lookups
when :open_square
str = consume
str << expression
str << consume(:close_square)
str << variable_lookups
when :string, :number
consume consume
when :open_round elsif token.first == :open_round
consume consume
first = expression first = expression
consume(:dotdot) consume(:dotdot)
@@ -72,29 +66,26 @@ module Liquid
end end
def argument def argument
str = +"" str = ""
# might be a keyword argument (identifier: expression) # might be a keyword argument (identifier: expression)
if look(:id) && look(:colon, 1) if look(:id) && look(:colon, 1)
str << consume << consume << ' ' str << consume << consume << ' '.freeze
end end
str << expression str << expression
str str
end end
def variable_lookups def variable_signature
str = +"" str = consume(:id)
loop do while look(:open_square)
if look(:open_square) str << consume
str << consume str << expression
str << expression str << consume(:close_square)
str << consume(:close_square) end
elsif look(:dot) if look(:dot)
str << consume str << consume
str << consume(:id) str << variable_signature
else
break
end
end end
str str
end end
+3 -17
View File
@@ -1,29 +1,15 @@
# frozen_string_literal: true
module Liquid module Liquid
module ParserSwitching module ParserSwitching
def strict_parse_with_error_mode_fallback(markup)
strict_parse_with_error_context(markup)
rescue SyntaxError => e
case parse_context.error_mode
when :strict
raise
when :warn
parse_context.warnings << e
end
lax_parse(markup)
end
def parse_with_selected_parser(markup) def parse_with_selected_parser(markup)
case parse_context.error_mode case parse_context.error_mode
when :strict then strict_parse_with_error_context(markup) when :strict then strict_parse_with_error_context(markup)
when :lax then lax_parse(markup) when :lax then lax_parse(markup)
when :warn when :warn
begin begin
strict_parse_with_error_context(markup) return strict_parse_with_error_context(markup)
rescue SyntaxError => e rescue SyntaxError => e
parse_context.warnings << e parse_context.warnings << e
lax_parse(markup) return lax_parse(markup)
end end
end end
end end
@@ -33,7 +19,7 @@ module Liquid
def strict_parse_with_error_context(markup) def strict_parse_with_error_context(markup)
strict_parse(markup) strict_parse(markup)
rescue SyntaxError => e rescue SyntaxError => e
e.line_number = line_number e.line_number = line_number
e.markup_context = markup_context(markup) e.markup_context = markup_context(markup)
raise e raise e
end end
+6 -21
View File
@@ -1,31 +1,16 @@
# frozen_string_literal: true
module Liquid module Liquid
class PartialCache class PartialCache
def self.load(template_name, context:, parse_context:) def self.load(template_name, context:, parse_context:)
cached_partials = context.registers[:cached_partials] cached_partials = (context.registers[:cached_partials] ||= {})
cache_key = "#{template_name}:#{parse_context.error_mode}" cached = cached_partials[template_name]
cached = cached_partials[cache_key]
return cached if cached return cached if cached
file_system = context.registers[:file_system] file_system = (context.registers[:file_system] ||= Liquid::Template.file_system)
source = file_system.read_template_file(template_name) source = file_system.read_template_file(template_name)
parse_context.partial = true parse_context.partial = true
template_factory = context.registers[:template_factory] partial = Liquid::Template.parse(source, parse_context)
template = template_factory.for(template_name) cached_partials[template_name] = partial
begin
partial = template.parse(source, parse_context)
rescue Liquid::Error => e
e.template_name = template&.name || template_name
raise e
end
partial.name ||= template_name
cached_partials[cache_key] = partial
ensure ensure
parse_context.partial = false parse_context.partial = false
end end
+92 -61
View File
@@ -1,5 +1,3 @@
# frozen_string_literal: true
require 'liquid/profiler/hooks' require 'liquid/profiler/hooks'
module Liquid module Liquid
@@ -25,7 +23,7 @@ module Liquid
# node.code # node.code
# #
# # Which template and line number of this node. # # Which template and line number of this node.
# # The top-level template name is `nil` by default, but can be set in the Liquid::Context before rendering. # # If top level, this will be "<root>".
# node.partial # node.partial
# node.line_number # node.line_number
# #
@@ -46,94 +44,127 @@ module Liquid
include Enumerable include Enumerable
class Timing class Timing
attr_reader :code, :template_name, :line_number, :children attr_reader :code, :partial, :line_number, :children, :total_time, :self_time
attr_accessor :total_time
alias_method :render_time, :total_time
alias_method :partial, :template_name
def initialize(code: nil, template_name: nil, line_number: nil) def initialize(node, partial)
@code = code @code = node.respond_to?(:raw) ? node.raw : node
@template_name = template_name @partial = partial
@line_number = line_number @line_number = node.respond_to?(:line_number) ? node.line_number : nil
@children = [] @children = []
end end
def self_time def self.start(node, partial)
@self_time ||= begin new(node, partial).tap(&:start)
total_children_time = 0.0 end
def start
@start_time = Time.now
end
def finish
@end_time = Time.now
@total_time = @end_time - @start_time
if @children.size == 0
@self_time = @total_time
else
total_children_time = 0
@children.each do |child| @children.each do |child|
total_children_time += child.total_time total_children_time += child.total_time
end end
@total_time - total_children_time
@self_time = @total_time - total_children_time
end end
end end
def render_time
@end_time - @start_time
end
end end
attr_reader :total_time def self.profile_node_render(node)
alias_method :total_render_time, :total_time if Profiler.current_profile && node.respond_to?(:render)
Profiler.current_profile.start_node(node)
output = yield
Profiler.current_profile.end_node(node)
output
else
yield
end
end
def self.profile_children(template_name)
if Profiler.current_profile
Profiler.current_profile.push_partial(template_name)
output = yield
Profiler.current_profile.pop_partial
output
else
yield
end
end
def self.current_profile
Thread.current[:liquid_profiler]
end
def initialize def initialize
@root_children = [] @partial_stack = ["<root>"]
@current_children = nil
@total_time = 0.0 @root_timing = Timing.new("", current_partial)
@timing_stack = [@root_timing]
@render_start_at = Time.now
@render_end_at = @render_start_at
end end
def profile(template_name, &block) def start
# nested renders are done from a tag that already has a timing node Thread.current[:liquid_profiler] = self
return yield if @current_children @render_start_at = Time.now
root_children = @root_children
render_idx = root_children.length
begin
@current_children = root_children
profile_node(template_name, &block)
ensure
@current_children = nil
if (timing = root_children[render_idx])
@total_time += timing.total_time
end
end
end end
def children def stop
children = @root_children Thread.current[:liquid_profiler] = nil
if children.length == 1 @render_end_at = Time.now
children.first.children end
else
children def total_render_time
end @render_end_at - @render_start_at
end end
def each(&block) def each(&block)
children.each(&block) @root_timing.children.each(&block)
end end
def [](idx) def [](idx)
children[idx] @root_timing.children[idx]
end end
def length def length
children.length @root_timing.children.length
end end
def profile_node(template_name, code: nil, line_number: nil) def start_node(node)
timing = Timing.new(code: code, template_name: template_name, line_number: line_number) @timing_stack.push(Timing.start(node, current_partial))
parent_children = @current_children
start_time = monotonic_time
begin
@current_children = timing.children
yield
ensure
@current_children = parent_children
timing.total_time = monotonic_time - start_time
parent_children << timing
end
end end
private def end_node(_node)
timing = @timing_stack.pop
timing.finish
def monotonic_time @timing_stack.last.children << timing
Process.clock_gettime(Process::CLOCK_MONOTONIC) end
def current_partial
@partial_stack.last
end
def push_partial(partial_name)
@partial_stack.push(partial_name)
end
def pop_partial
@partial_stack.pop
end end
end end
end end
+15 -27
View File
@@ -1,35 +1,23 @@
# frozen_string_literal: true
module Liquid module Liquid
module BlockBodyProfilingHook class BlockBody
def render_node(context, output, node) def render_node_with_profiling(context, output, node)
if (profiler = context.profiler) Profiler.profile_node_render(node) do
profiler.profile_node(context.template_name, code: node.raw, line_number: node.line_number) do render_node_without_profiling(context, output, node)
super
end
else
super
end end
end end
end
BlockBody.prepend(BlockBodyProfilingHook)
module DocumentProfilingHook alias_method :render_node_without_profiling, :render_node
def render_to_output_buffer(context, output) alias_method :render_node, :render_node_with_profiling
return super unless context.profiler end
context.profiler.profile(context.template_name) { super }
class Include < Tag
def render_to_output_buffer_with_profiling(context, output)
Profiler.profile_children(context.evaluate(@template_name_expr).to_s) do
render_to_output_buffer_without_profiling(context, output)
end
end end
end
Document.prepend(DocumentProfilingHook)
module ContextProfilingHook alias_method :render_to_output_buffer_without_profiling, :render_to_output_buffer
attr_accessor :profiler alias_method :render_to_output_buffer, :render_to_output_buffer_with_profiling
def new_isolated_subcontext
new_context = super
new_context.profiler = profiler
new_context
end
end end
Context.prepend(ContextProfilingHook)
end end
+4 -24
View File
@@ -1,37 +1,23 @@
# frozen_string_literal: true
module Liquid module Liquid
class RangeLookup class RangeLookup
def self.parse(start_markup, end_markup) def self.parse(start_markup, end_markup)
start_obj = Expression.parse(start_markup) start_obj = Expression.parse(start_markup)
end_obj = Expression.parse(end_markup) end_obj = Expression.parse(end_markup)
if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate) if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate)
new(start_obj, end_obj) new(start_obj, end_obj)
else else
begin start_obj.to_i..end_obj.to_i
start_obj.to_i..end_obj.to_i
rescue NoMethodError
invalid_expr = start_markup unless start_obj.respond_to?(:to_i)
invalid_expr ||= end_markup unless end_obj.respond_to?(:to_i)
if invalid_expr
raise Liquid::SyntaxError, "Invalid expression type '#{invalid_expr}' in range expression"
end
raise
end
end end
end end
attr_reader :start_obj, :end_obj
def initialize(start_obj, end_obj) def initialize(start_obj, end_obj)
@start_obj = start_obj @start_obj = start_obj
@end_obj = end_obj @end_obj = end_obj
end end
def evaluate(context) def evaluate(context)
start_int = to_integer(context.evaluate(@start_obj)) start_int = to_integer(context.evaluate(@start_obj))
end_int = to_integer(context.evaluate(@end_obj)) end_int = to_integer(context.evaluate(@end_obj))
start_int..end_int start_int..end_int
end end
@@ -47,11 +33,5 @@ module Liquid
Utils.to_integer(input) Utils.to_integer(input)
end end
end end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[@node.start_obj, @node.end_obj]
end
end
end end
end end
-51
View File
@@ -1,51 +0,0 @@
# frozen_string_literal: true
module Liquid
class Registers
attr_reader :static
def initialize(registers = {})
@static = registers.is_a?(Registers) ? registers.static : registers
@changes = {}
end
def []=(key, value)
@changes[key] = value
end
def [](key)
if @changes.key?(key)
@changes[key]
else
@static[key]
end
end
def delete(key)
@changes.delete(key)
end
UNDEFINED = Object.new
def fetch(key, default = UNDEFINED, &block)
if @changes.key?(key)
@changes.fetch(key)
elsif default != UNDEFINED
if block_given?
@static.fetch(key, &block)
else
@static.fetch(key, default)
end
else
@static.fetch(key, &block)
end
end
def key?(key)
@changes.key?(key) || @static.key?(key)
end
end
# Alias for backwards compatibility
StaticRegisters = Registers
end
+8 -47
View File
@@ -1,62 +1,23 @@
# frozen_string_literal: true
module Liquid module Liquid
class ResourceLimits class ResourceLimits
attr_accessor :render_length_limit, :render_score_limit, :assign_score_limit attr_accessor :render_length, :render_score, :assign_score,
attr_reader :render_score, :assign_score :render_length_limit, :render_score_limit, :assign_score_limit
def initialize(limits) def initialize(limits)
@render_length_limit = limits[:render_length_limit] @render_length_limit = limits[:render_length_limit]
@render_score_limit = limits[:render_score_limit] @render_score_limit = limits[:render_score_limit]
@assign_score_limit = limits[:assign_score_limit] @assign_score_limit = limits[:assign_score_limit]
reset reset
end end
def increment_render_score(amount)
@render_score += amount
raise_limits_reached if @render_score_limit && @render_score > @render_score_limit
end
def increment_assign_score(amount)
@assign_score += amount
raise_limits_reached if @assign_score_limit && @assign_score > @assign_score_limit
end
# update either render_length or assign_score based on whether or not the writes are captured
def increment_write_score(output)
if (last_captured = @last_capture_length)
captured = output.bytesize
increment = captured - last_captured
@last_capture_length = captured
increment_assign_score(increment)
elsif @render_length_limit && output.bytesize > @render_length_limit
raise_limits_reached
end
end
def raise_limits_reached
@reached_limit = true
raise MemoryError, "Memory limits exceeded"
end
def reached? def reached?
@reached_limit (@render_length_limit && @render_length > @render_length_limit) ||
(@render_score_limit && @render_score > @render_score_limit) ||
(@assign_score_limit && @assign_score > @assign_score_limit)
end end
def reset def reset
@reached_limit = false @render_length = @render_score = @assign_score = 0
@last_capture_length = nil
@render_score = @assign_score = 0
end
def with_capture
old_capture_length = @last_capture_length
begin
@last_capture_length = 0
yield
ensure
@last_capture_length = old_capture_length
end
end end
end end
end end
+110 -591
View File
@@ -1,127 +1,56 @@
# frozen_string_literal: true
require 'cgi' require 'cgi'
require 'base64'
require 'bigdecimal' require 'bigdecimal'
module Liquid module Liquid
module StandardFilters module StandardFilters
MAX_I32 = (1 << 31) - 1
private_constant :MAX_I32
MIN_I64 = -(1 << 63)
MAX_I64 = (1 << 63) - 1
I64_RANGE = MIN_I64..MAX_I64
private_constant :MIN_I64, :MAX_I64, :I64_RANGE
HTML_ESCAPE = { HTML_ESCAPE = {
'&' => '&amp;', '&'.freeze => '&amp;'.freeze,
'>' => '&gt;', '>'.freeze => '&gt;'.freeze,
'<' => '&lt;', '<'.freeze => '&lt;'.freeze,
'"' => '&quot;', '"'.freeze => '&quot;'.freeze,
"'" => '&#39;', "'".freeze => '&#39;'.freeze
}.freeze }.freeze
HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/ HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/
STRIP_HTML_BLOCKS = Regexp.union( STRIP_HTML_BLOCKS = Regexp.union(
%r{<script.*?</script>}m, /<script.*?<\/script>/m,
/<!--.*?-->/m, /<!--.*?-->/m,
%r{<style.*?</style>}m, /<style.*?<\/style>/m
) )
STRIP_HTML_TAGS = /<.*?>/m STRIP_HTML_TAGS = /<.*?>/m
# @liquid_public_docs # Return the size of an array or of an string
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Returns the size of a string or array.
# @liquid_description
# The size of a string is the number of characters that the string includes. The size of an array is the number of items
# in the array.
# @liquid_syntax variable | size
# @liquid_return [number]
def size(input) def size(input)
input.respond_to?(:size) ? input.size : 0 input.respond_to?(:size) ? input.size : 0
end end
# @liquid_public_docs # convert an input string to DOWNCASE
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Converts a string to all lowercase characters.
# @liquid_syntax string | downcase
# @liquid_return [string]
def downcase(input) def downcase(input)
input.to_s.downcase input.to_s.downcase
end end
# @liquid_public_docs # convert an input string to UPCASE
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Converts a string to all uppercase characters.
# @liquid_syntax string | upcase
# @liquid_return [string]
def upcase(input) def upcase(input)
input.to_s.upcase input.to_s.upcase
end end
# @liquid_public_docs # capitalize words in the input centence
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Capitalizes the first word in a string and downcases the remaining characters.
# @liquid_syntax string | capitalize
# @liquid_return [string]
def capitalize(input) def capitalize(input)
input.to_s.capitalize input.to_s.capitalize
end end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Escapes special characters in HTML, such as `<>`, `'`, and `&`, and converts characters into escape sequences. The filter doesn't effect characters within the string that dont have a corresponding escape sequence.".
# @liquid_syntax string | escape
# @liquid_return [string]
def escape(input) def escape(input)
CGI.escapeHTML(input.to_s) unless input.nil? CGI.escapeHTML(input.to_s).untaint unless input.nil?
end end
alias_method :h, :escape alias_method :h, :escape
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Escapes a string without changing characters that have already been escaped.
# @liquid_syntax string | escape_once
# @liquid_return [string]
def escape_once(input) def escape_once(input)
input.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE) input.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE)
end end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Converts any URL-unsafe characters in a string to the
# [percent-encoded](https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding) equivalent.
# @liquid_description
# > Note:
# > Spaces are converted to a `+` character, instead of a percent-encoded character.
# @liquid_syntax string | url_encode
# @liquid_return [string]
def url_encode(input) def url_encode(input)
CGI.escape(input.to_s) unless input.nil? CGI.escape(input.to_s) unless input.nil?
end end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Decodes any [percent-encoded](https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding) characters
# in a string.
# @liquid_syntax string | url_decode
# @liquid_return [string]
def url_decode(input) def url_decode(input)
return if input.nil? return if input.nil?
@@ -131,229 +60,79 @@ module Liquid
result result
end end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Encodes a string to [Base64 format](https://developer.mozilla.org/en-US/docs/Glossary/Base64).
# @liquid_syntax string | base64_encode
# @liquid_return [string]
def base64_encode(input)
Base64.strict_encode64(input.to_s)
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Decodes a string in [Base64 format](https://developer.mozilla.org/en-US/docs/Glossary/Base64).
# @liquid_syntax string | base64_decode
# @liquid_return [string]
def base64_decode(input)
Base64.strict_decode64(input.to_s)
rescue ::ArgumentError
raise Liquid::ArgumentError, "invalid base64 provided to base64_decode"
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Encodes a string to URL-safe [Base64 format](https://developer.mozilla.org/en-US/docs/Glossary/Base64).
# @liquid_syntax string | base64_url_safe_encode
# @liquid_return [string]
def base64_url_safe_encode(input)
Base64.urlsafe_encode64(input.to_s)
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Decodes a string in URL-safe [Base64 format](https://developer.mozilla.org/en-US/docs/Glossary/Base64).
# @liquid_syntax string | base64_url_safe_decode
# @liquid_return [string]
def base64_url_safe_decode(input)
Base64.urlsafe_decode64(input.to_s)
rescue ::ArgumentError
raise Liquid::ArgumentError, "invalid base64 provided to base64_url_safe_decode"
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Returns a substring or series of array items, starting at a given 0-based index.
# @liquid_description
# By default, the substring has a length of one character, and the array series has one array item. However, you can
# provide a second parameter to specify the number of characters or array items.
# @liquid_syntax string | slice
# @liquid_return [string]
def slice(input, offset, length = nil) def slice(input, offset, length = nil)
offset = Utils.to_integer(offset) offset = Utils.to_integer(offset)
length = length ? Utils.to_integer(length) : 1 length = length ? Utils.to_integer(length) : 1
begin if input.is_a?(Array)
if input.is_a?(Array) input.slice(offset, length) || []
input.slice(offset, length) || [] else
else input.to_s.slice(offset, length) || ''
input.to_s.slice(offset, length) || ''
end
rescue RangeError
if I64_RANGE.cover?(length) && I64_RANGE.cover?(offset)
raise # unexpected error
end
offset = offset.clamp(I64_RANGE)
length = length.clamp(I64_RANGE)
retry
end end
end end
# @liquid_public_docs # Truncate a string down to x characters
# @liquid_type filter def truncate(input, length = 50, truncate_string = "...".freeze)
# @liquid_category string
# @liquid_summary
# Truncates a string down to a given number of characters.
# @liquid_description
# If the specified number of characters is less than the length of the string, then an ellipsis (`...`) is appended to
# the truncated string. The ellipsis is included in the character count of the truncated string.
# @liquid_syntax string | truncate: number
# @liquid_return [string]
def truncate(input, length = 50, truncate_string = "...")
return if input.nil? return if input.nil?
input_str = input.to_s input_str = input.to_s
length = Utils.to_integer(length) length = Utils.to_integer(length)
truncate_string_str = truncate_string.to_s truncate_string_str = truncate_string.to_s
l = length - truncate_string_str.length l = length - truncate_string_str.length
l = 0 if l < 0 l = 0 if l < 0
input_str.length > length ? input_str[0...l].concat(truncate_string_str) : input_str input_str.length > length ? input_str[0...l].concat(truncate_string_str) : input_str
end end
# @liquid_public_docs def truncatewords(input, words = 15, truncate_string = "...".freeze)
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Truncates a string down to a given number of words.
# @liquid_description
# If the specified number of words is less than the number of words in the string, then an ellipsis (`...`) is appended to
# the truncated string.
#
# > Caution:
# > HTML tags are treated as words, so you should strip any HTML from truncated content. If you don't strip HTML, then
# > closing HTML tags can be removed, which can result in unexpected behavior.
# @liquid_syntax string | truncatewords: number
# @liquid_return [string]
def truncatewords(input, words = 15, truncate_string = "...")
return if input.nil? return if input.nil?
input = input.to_s wordlist = input.to_s.split
words = Utils.to_integer(words) words = Utils.to_integer(words)
words = 1 if words <= 0 l = words - 1
l = 0 if l < 0
wordlist = begin wordlist.length > l ? wordlist[0..l].join(" ".freeze).concat(truncate_string.to_s) : input
input.split(" ", words + 1)
rescue RangeError
# integer too big for String#split, but we can semantically assume no truncation is needed
return input if words + 1 > MAX_I32
raise # unexpected error
end
return input if wordlist.length <= words
wordlist.pop
wordlist.join(" ").concat(truncate_string.to_s)
end end
# @liquid_public_docs # Split input string into an array of substrings separated by given pattern.
# @liquid_type filter #
# @liquid_category string # Example:
# @liquid_summary # <div class="summary">{{ post | split '//' | first }}</div>
# Splits a string into an array of substrings based on a given separator. #
# @liquid_syntax string | split: string
# @liquid_return [array[string]]
def split(input, pattern) def split(input, pattern)
input.to_s.split(pattern.to_s) input.to_s.split(pattern.to_s)
end end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Strips all whitespace from the left and right of a string.
# @liquid_syntax string | strip
# @liquid_return [string]
def strip(input) def strip(input)
input.to_s.strip input.to_s.strip
end end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Strips all whitespace from the left of a string.
# @liquid_syntax string | lstrip
# @liquid_return [string]
def lstrip(input) def lstrip(input)
input.to_s.lstrip input.to_s.lstrip
end end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Strips all whitespace from the right of a string.
# @liquid_syntax string | rstrip
# @liquid_return [string]
def rstrip(input) def rstrip(input)
input.to_s.rstrip input.to_s.rstrip
end end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Strips all HTML tags from a string.
# @liquid_syntax string | strip_html
# @liquid_return [string]
def strip_html(input) def strip_html(input)
empty = '' empty = ''.freeze
result = input.to_s.gsub(STRIP_HTML_BLOCKS, empty) result = input.to_s.gsub(STRIP_HTML_BLOCKS, empty)
result.gsub!(STRIP_HTML_TAGS, empty) result.gsub!(STRIP_HTML_TAGS, empty)
result result
end end
# @liquid_public_docs # Remove all newlines from the string
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Strips all newline characters (line breaks) from a string.
# @liquid_syntax string | strip_newlines
# @liquid_return [string]
def strip_newlines(input) def strip_newlines(input)
input.to_s.gsub(/\r?\n/, '') input.to_s.gsub(/\r?\n/, ''.freeze)
end end
# @liquid_public_docs # Join elements of the array with certain character between them
# @liquid_type filter def join(input, glue = ' '.freeze)
# @liquid_category array InputIterator.new(input).join(glue)
# @liquid_summary
# Combines all of the items in an array into a single string, separated by a space.
# @liquid_syntax array | join
# @liquid_return [string]
def join(input, glue = ' ')
InputIterator.new(input, context).join(glue)
end end
# @liquid_public_docs # Sort elements of the array
# @liquid_type filter # provide optional property with which to sort an array of hashes or drops
# @liquid_category array
# @liquid_summary
# Sorts the items in an array in case-sensitive alphabetical, or numerical, order.
# @liquid_syntax array | sort
# @liquid_return [array[untyped]]
def sort(input, property = nil) def sort(input, property = nil)
ary = InputIterator.new(input, context) ary = InputIterator.new(input)
return [] if ary.empty? return [] if ary.empty?
@@ -370,19 +149,10 @@ module Liquid
end end
end end
# @liquid_public_docs # Sort elements of an array ignoring case if strings
# @liquid_type filter # provide optional property with which to sort an array of hashes or drops
# @liquid_category array
# @liquid_summary
# Sorts the items in an array in case-insensitive alphabetical order.
# @liquid_description
# > Caution:
# > You shouldn't use the `sort_natural` filter to sort numerical values. When comparing items an array, each item is converted to a
# > string, so sorting on numerical values can lead to unexpected results.
# @liquid_syntax array | sort_natural
# @liquid_return [array[untyped]]
def sort_natural(input, property = nil) def sort_natural(input, property = nil)
ary = InputIterator.new(input, context) ary = InputIterator.new(input)
return [] if ary.empty? return [] if ary.empty?
@@ -399,91 +169,58 @@ module Liquid
end end
end end
# @liquid_public_docs # Filter the elements of an array to those with a certain property value.
# @liquid_type filter # By default the target is any truthy value.
# @liquid_category array
# @liquid_summary
# Filters an array to include only items with a specific property value.
# @liquid_description
# This requires you to provide both the property name and the associated value.
# @liquid_syntax array | where: string, string
# @liquid_return [array[untyped]]
def where(input, property, target_value = nil) def where(input, property, target_value = nil)
ary = InputIterator.new(input, context) ary = InputIterator.new(input)
if ary.empty? if ary.empty?
[] []
elsif target_value.nil? elsif ary.first.respond_to?(:[]) && target_value.nil?
ary.select do |item| begin
item[property] ary.select { |item| item[property] }
rescue TypeError rescue TypeError
raise_property_error(property) raise_property_error(property)
rescue NoMethodError
return nil unless item.respond_to?(:[])
raise
end end
else elsif ary.first.respond_to?(:[])
ary.select do |item| begin
item[property] == target_value ary.select { |item| item[property] == target_value }
rescue TypeError rescue TypeError
raise_property_error(property) raise_property_error(property)
rescue NoMethodError
return nil unless item.respond_to?(:[])
raise
end end
end end
end end
# @liquid_public_docs # Remove duplicate elements from an array
# @liquid_type filter # provide optional property with which to determine uniqueness
# @liquid_category array
# @liquid_summary
# Removes any duplicate items in an array.
# @liquid_syntax array | uniq
# @liquid_return [array[untyped]]
def uniq(input, property = nil) def uniq(input, property = nil)
ary = InputIterator.new(input, context) ary = InputIterator.new(input)
if property.nil? if property.nil?
ary.uniq ary.uniq
elsif ary.empty? # The next two cases assume a non-empty array. elsif ary.empty? # The next two cases assume a non-empty array.
[] []
else elsif ary.first.respond_to?(:[])
ary.uniq do |item| begin
item[property] ary.uniq { |a| a[property] }
rescue TypeError rescue TypeError
raise_property_error(property) raise_property_error(property)
rescue NoMethodError
return nil unless item.respond_to?(:[])
raise
end end
end end
end end
# @liquid_public_docs # Reverse the elements of an array
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Reverses the order of the items in an array.
# @liquid_syntax array | reverse
# @liquid_return [array[untyped]]
def reverse(input) def reverse(input)
ary = InputIterator.new(input, context) ary = InputIterator.new(input)
ary.reverse ary.reverse
end end
# @liquid_public_docs # map/collect on a given property
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Creates an array of values from a specific property of the items in an array.
# @liquid_syntax array | map: string
# @liquid_return [array[untyped]]
def map(input, property) def map(input, property)
InputIterator.new(input, context).map do |e| InputIterator.new(input).map do |e|
e = e.call if e.is_a?(Proc) e = e.call if e.is_a?(Proc)
if property == "to_liquid" if property == "to_liquid".freeze
e e
elsif e.respond_to?(:[]) elsif e.respond_to?(:[])
r = e[property] r = e[property]
@@ -494,157 +231,64 @@ module Liquid
raise_property_error(property) raise_property_error(property)
end end
# @liquid_public_docs # Remove nils within an array
# @liquid_type filter # provide optional property with which to check for nil
# @liquid_category array
# @liquid_summary
# Removes any `nil` items from an array.
# @liquid_syntax array | compact
# @liquid_return [array[untyped]]
def compact(input, property = nil) def compact(input, property = nil)
ary = InputIterator.new(input, context) ary = InputIterator.new(input)
if property.nil? if property.nil?
ary.compact ary.compact
elsif ary.empty? # The next two cases assume a non-empty array. elsif ary.empty? # The next two cases assume a non-empty array.
[] []
else elsif ary.first.respond_to?(:[])
ary.reject do |item| begin
item[property].nil? ary.reject { |a| a[property].nil? }
rescue TypeError rescue TypeError
raise_property_error(property) raise_property_error(property)
rescue NoMethodError
return nil unless item.respond_to?(:[])
raise
end end
end end
end end
# @liquid_public_docs # Replace occurrences of a string with another
# @liquid_type filter def replace(input, string, replacement = ''.freeze)
# @liquid_category string
# @liquid_summary
# Replaces any instance of a substring inside a string with a given string.
# @liquid_syntax string | replace: string, string
# @liquid_return [string]
def replace(input, string, replacement = '')
input.to_s.gsub(string.to_s, replacement.to_s) input.to_s.gsub(string.to_s, replacement.to_s)
end end
# @liquid_public_docs # Replace the first occurrences of a string with another
# @liquid_type filter def replace_first(input, string, replacement = ''.freeze)
# @liquid_category string
# @liquid_summary
# Replaces the first instance of a substring inside a string with a given string.
# @liquid_syntax string | replace_first: string, string
# @liquid_return [string]
def replace_first(input, string, replacement = '')
input.to_s.sub(string.to_s, replacement.to_s) input.to_s.sub(string.to_s, replacement.to_s)
end end
# @liquid_public_docs # remove a substring
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Replaces the last instance of a substring inside a string with a given string.
# @liquid_syntax string | replace_last: string, string
# @liquid_return [string]
def replace_last(input, string, replacement)
input = input.to_s
string = string.to_s
replacement = replacement.to_s
start_index = input.rindex(string)
return input unless start_index
output = input.dup
output[start_index, string.length] = replacement
output
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Removes any instance of a substring inside a string.
# @liquid_syntax string | remove: string
# @liquid_return [string]
def remove(input, string) def remove(input, string)
replace(input, string, '') input.to_s.gsub(string.to_s, ''.freeze)
end end
# @liquid_public_docs # remove the first occurrences of a substring
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Removes the first instance of a substring inside a string.
# @liquid_syntax string | remove_first: string
# @liquid_return [string]
def remove_first(input, string) def remove_first(input, string)
replace_first(input, string, '') input.to_s.sub(string.to_s, ''.freeze)
end end
# @liquid_public_docs # add one string to another
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Removes the last instance of a substring inside a string.
# @liquid_syntax string | remove_last: string
# @liquid_return [string]
def remove_last(input, string)
replace_last(input, string, '')
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Adds a given string to the end of a string.
# @liquid_syntax string | append: string
# @liquid_return [string]
def append(input, string) def append(input, string)
input.to_s + string.to_s input.to_s + string.to_s
end end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Concatenates (combines) two arrays.
# @liquid_description
# > Note:
# > The `concat` filter won't filter out duplicates. If you want to remove duplicates, then you need to use the
# > [`uniq` filter](/docs/api/liquid/filters/uniq).
# @liquid_syntax array | concat: array
# @liquid_return [array[untyped]]
def concat(input, array) def concat(input, array)
unless array.respond_to?(:to_ary) unless array.respond_to?(:to_ary)
raise ArgumentError, "concat filter requires an array argument" raise ArgumentError.new("concat filter requires an array argument")
end end
InputIterator.new(input, context).concat(array) InputIterator.new(input).concat(array)
end end
# @liquid_public_docs # prepend a string to another
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Adds a given string to the beginning of a string.
# @liquid_syntax string | prepend: string
# @liquid_return [string]
def prepend(input, string) def prepend(input, string)
string.to_s + input.to_s string.to_s + input.to_s
end end
# @liquid_public_docs # Add <br /> tags in front of all newlines in input string
# @liquid_type filter
# @liquid_category string
# @liquid_summary
# Converts newlines (`\n`) in a string to HTML line breaks (`<br>`).
# @liquid_syntax string | newline_to_br
# @liquid_return [string]
def newline_to_br(input) def newline_to_br(input)
input.to_s.gsub(/\r?\n/, "<br />\n") input.to_s.gsub(/\n/, "<br />\n".freeze)
end end
# Reformat a date using Ruby's core Time#strftime( string ) -> string # Reformat a date using Ruby's core Time#strftime( string ) -> string
@@ -681,111 +325,63 @@ module Liquid
def date(input, format) def date(input, format)
return input if format.to_s.empty? 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) date.strftime(format.to_s)
end end
# @liquid_public_docs # Get the first element of the passed in array
# @liquid_type filter #
# @liquid_category array # Example:
# @liquid_summary # {{ product.images | first | to_img }}
# Returns the first item in an array. #
# @liquid_syntax array | first
# @liquid_return [untyped]
def first(array) def first(array)
array.first if array.respond_to?(:first) array.first if array.respond_to?(:first)
end end
# @liquid_public_docs # Get the last element of the passed in array
# @liquid_type filter #
# @liquid_category array # Example:
# @liquid_summary # {{ product.images | last | to_img }}
# Returns the last item in an array. #
# @liquid_syntax array | last
# @liquid_return [untyped]
def last(array) def last(array)
array.last if array.respond_to?(:last) array.last if array.respond_to?(:last)
end end
# @liquid_public_docs # absolute value
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Returns the absolute value of a number.
# @liquid_syntax number | abs
# @liquid_return [number]
def abs(input) def abs(input)
result = Utils.to_number(input).abs result = Utils.to_number(input).abs
result.is_a?(BigDecimal) ? result.to_f : result result.is_a?(BigDecimal) ? result.to_f : result
end end
# @liquid_public_docs # addition
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Adds two numbers.
# @liquid_syntax number | plus: number
# @liquid_return [number]
def plus(input, operand) def plus(input, operand)
apply_operation(input, operand, :+) apply_operation(input, operand, :+)
end end
# @liquid_public_docs # subtraction
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Subtracts a given number from another number.
# @liquid_syntax number | minus: number
# @liquid_return [number]
def minus(input, operand) def minus(input, operand)
apply_operation(input, operand, :-) apply_operation(input, operand, :-)
end end
# @liquid_public_docs # multiplication
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Multiplies a number by a given number.
# @liquid_syntax number | times: number
# @liquid_return [number]
def times(input, operand) def times(input, operand)
apply_operation(input, operand, :*) apply_operation(input, operand, :*)
end end
# @liquid_public_docs # division
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Divides a number by a given number. The `divided_by` filter produces a result of the same type as the divisor. This means if you divide by an integer, the result will be an integer, and if you divide by a float, the result will be a float.
# @liquid_syntax number | divided_by: number
# @liquid_return [number]
def divided_by(input, operand) def divided_by(input, operand)
apply_operation(input, operand, :/) apply_operation(input, operand, :/)
rescue ::ZeroDivisionError => e rescue ::ZeroDivisionError => e
raise Liquid::ZeroDivisionError, e.message raise Liquid::ZeroDivisionError, e.message
end end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Returns the remainder of dividing a number by a given number.
# @liquid_syntax number | modulo: number
# @liquid_return [number]
def modulo(input, operand) def modulo(input, operand)
apply_operation(input, operand, :%) apply_operation(input, operand, :%)
rescue ::ZeroDivisionError => e rescue ::ZeroDivisionError => e
raise Liquid::ZeroDivisionError, e.message raise Liquid::ZeroDivisionError, e.message
end end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Rounds a number to the nearest integer.
# @liquid_syntax number | round
# @liquid_return [number]
def round(input, n = 0) def round(input, n = 0)
result = Utils.to_number(input).round(Utils.to_number(n)) result = Utils.to_number(input).round(Utils.to_number(n))
result = result.to_f if result.is_a?(BigDecimal) result = result.to_f if result.is_a?(BigDecimal)
@@ -795,39 +391,18 @@ module Liquid
raise Liquid::FloatDomainError, e.message raise Liquid::FloatDomainError, e.message
end end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Rounds a number up to the nearest integer.
# @liquid_syntax number | ceil
# @liquid_return [number]
def ceil(input) def ceil(input)
Utils.to_number(input).ceil.to_i Utils.to_number(input).ceil.to_i
rescue ::FloatDomainError => e rescue ::FloatDomainError => e
raise Liquid::FloatDomainError, e.message raise Liquid::FloatDomainError, e.message
end end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Rounds a number down to the nearest integer.
# @liquid_syntax number | floor
# @liquid_return [number]
def floor(input) def floor(input)
Utils.to_number(input).floor.to_i Utils.to_number(input).floor.to_i
rescue ::FloatDomainError => e rescue ::FloatDomainError => e
raise Liquid::FloatDomainError, e.message raise Liquid::FloatDomainError, e.message
end end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Limits a number to a minimum value.
# @liquid_syntax number | at_least
# @liquid_return [number]
def at_least(input, n) def at_least(input, n)
min_value = Utils.to_number(n) min_value = Utils.to_number(n)
@@ -836,13 +411,6 @@ module Liquid
result.is_a?(BigDecimal) ? result.to_f : result result.is_a?(BigDecimal) ? result.to_f : result
end end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Limits a number to a maximum value.
# @liquid_syntax number | at_most
# @liquid_return [number]
def at_most(input, n) def at_most(input, n)
max_value = Utils.to_number(n) max_value = Utils.to_number(n)
@@ -851,58 +419,18 @@ module Liquid
result.is_a?(BigDecimal) ? result.to_f : result result.is_a?(BigDecimal) ? result.to_f : result
end end
# @liquid_public_docs def default(input, default_value = ''.freeze)
# @liquid_type filter if !input || input.respond_to?(:empty?) && input.empty?
# @liquid_category default default_value
# @liquid_summary else
# Sets a default value for any variable whose value is one of the following: input
#
# - [`empty`](/docs/api/liquid/basics#empty)
# - [`false`](/docs/api/liquid/basics#truthy-and-falsy)
# - [`nil`](/docs/api/liquid/basics#nil)
# @liquid_syntax variable | default: variable
# @liquid_return [untyped]
# @liquid_optional_param allow_false [boolean] Whether to use false values instead of the default.
def default(input, default_value = '', options = {})
options = {} unless options.is_a?(Hash)
false_check = options['allow_false'] ? input.nil? : !Liquid::Utils.to_liquid_value(input)
false_check || (input.respond_to?(:empty?) && input.empty?) ? default_value : input
end
# @liquid_public_docs
# @liquid_type filter
# @liquid_category array
# @liquid_summary
# Returns the sum of all elements in an array.
# @liquid_syntax array | sum
# @liquid_return [number]
def sum(input, property = nil)
ary = InputIterator.new(input, context)
return 0 if ary.empty?
values_for_sum = ary.map do |item|
if property.nil?
item
elsif item.respond_to?(:[])
item[property]
else
0
end
rescue TypeError
raise_property_error(property)
end
InputIterator.new(values_for_sum, context).sum do |item|
Utils.to_number(item)
end end
end end
private private
attr_reader :context
def raise_property_error(property) def raise_property_error(property)
raise Liquid::ArgumentError, "cannot select the property '#{property}'" raise Liquid::ArgumentError.new("cannot select the property '#{property}'")
end end
def apply_operation(input, operand, operation) def apply_operation(input, operand, operation)
@@ -911,16 +439,10 @@ module Liquid
end end
def nil_safe_compare(a, b) def nil_safe_compare(a, b)
result = a <=> b if !a.nil? && !b.nil?
a <=> b
if result
result
elsif a.nil?
1
elsif b.nil?
-1
else else
raise Liquid::ArgumentError, "cannot sort values of incompatible types" a.nil? ? 1 : -1
end end
end end
@@ -935,9 +457,8 @@ module Liquid
class InputIterator class InputIterator
include Enumerable include Enumerable
def initialize(input, context) def initialize(input)
@context = context @input = if input.is_a?(Array)
@input = if input.is_a?(Array)
input.flatten input.flatten
elsif input.is_a?(Hash) elsif input.is_a?(Hash)
[input] [input]
@@ -975,9 +496,7 @@ module Liquid
def each def each
@input.each do |e| @input.each do |e|
e = e.respond_to?(:to_liquid) ? e.to_liquid : e yield(e.respond_to?(:to_liquid) ? e.to_liquid : e)
e.context = @context if e.respond_to?(:context=)
yield(e)
end end
end end
end end
+66
View File
@@ -0,0 +1,66 @@
require 'set'
module Liquid
# Strainer is the parent class for the filters system.
# New filters are mixed into the strainer class which is then instantiated for each liquid template render run.
#
# The Strainer only allows method calls defined in filters given to it via Strainer.global_filter,
# Context#add_filters or Template.register_filter
class Strainer #:nodoc:
@@global_strainer = Class.new(Strainer) do
@filter_methods = Set.new
end
@@strainer_class_cache = Hash.new do |hash, filters|
hash[filters] = Class.new(@@global_strainer) do
@filter_methods = @@global_strainer.filter_methods.dup
filters.each { |f| add_filter(f) }
end
end
def initialize(context)
@context = context
end
class << self
attr_reader :filter_methods
end
def self.add_filter(filter)
raise ArgumentError, "Expected module but got: #{filter.class}" unless filter.is_a?(Module)
unless self.include?(filter)
invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }
if invokable_non_public_methods.any?
raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"
else
send(:include, filter)
@filter_methods.merge(filter.public_instance_methods.map(&:to_s))
end
end
end
def self.global_filter(filter)
@@strainer_class_cache.clear
@@global_strainer.add_filter(filter)
end
def self.invokable?(method)
@filter_methods.include?(method.to_s)
end
def self.create(context, filters = [])
@@strainer_class_cache[filters].new(context)
end
def invoke(method, *args)
if self.class.invokable?(method)
send(method, *args)
elsif @context && @context.strict_filters
raise Liquid::UndefinedFilter, "undefined filter #{method}"
else
args.first
end
rescue ::ArgumentError => e
raise Liquid::ArgumentError, e.message, e.backtrace
end
end
end
-41
View File
@@ -1,41 +0,0 @@
# frozen_string_literal: true
module Liquid
# StrainerFactory is the factory for the filters system.
module StrainerFactory
extend self
def add_global_filter(filter)
strainer_class_cache.clear
GlobalCache.add_filter(filter)
end
def create(context, filters = [])
strainer_from_cache(filters).new(context)
end
def global_filter_names
GlobalCache.filter_method_names
end
GlobalCache = Class.new(StrainerTemplate)
private
def strainer_from_cache(filters)
if filters.empty?
GlobalCache
else
strainer_class_cache[filters] ||= begin
klass = Class.new(GlobalCache)
filters.each { |f| klass.add_filter(f) }
klass
end
end
end
def strainer_class_cache
@strainer_class_cache ||= {}
end
end
end
-62
View File
@@ -1,62 +0,0 @@
# frozen_string_literal: true
require 'set'
module Liquid
# StrainerTemplate is the computed class for the filters system.
# New filters are mixed into the strainer class which is then instantiated for each liquid template render run.
#
# The Strainer only allows method calls defined in filters given to it via StrainerFactory.add_global_filter,
# Context#add_filters or Template.register_filter
class StrainerTemplate
def initialize(context)
@context = context
end
class << self
def add_filter(filter)
return if include?(filter)
invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }
if invokable_non_public_methods.any?
raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"
end
include(filter)
filter_methods.merge(filter.public_instance_methods.map(&:to_s))
end
def invokable?(method)
filter_methods.include?(method.to_s)
end
def inherited(subclass)
super
subclass.instance_variable_set(:@filter_methods, @filter_methods.dup)
end
def filter_method_names
filter_methods.map(&:to_s).to_a
end
private
def filter_methods
@filter_methods ||= Set.new
end
end
def invoke(method, *args)
if self.class.invokable?(method)
send(method, *args)
elsif @context.strict_filters
raise Liquid::UndefinedFilter, "undefined filter #{method}"
else
args.first
end
rescue ::ArgumentError => e
raise Liquid::ArgumentError, e.message, e.backtrace
end
end
end
+5 -64
View File
@@ -1,106 +1,47 @@
# frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs
# @liquid_type object
# @liquid_name tablerowloop
# @liquid_summary
# Information about a parent [`tablerow` loop](/docs/api/liquid/tags/tablerow).
class TablerowloopDrop < Drop class TablerowloopDrop < Drop
def initialize(length, cols) def initialize(length, cols)
@length = length @length = length
@row = 1 @row = 1
@col = 1 @col = 1
@cols = cols @cols = cols
@index = 0 @index = 0
end end
# @liquid_public_docs attr_reader :length, :col, :row
# @liquid_summary
# The total number of iterations in the loop.
# @liquid_return [number]
attr_reader :length
# @liquid_public_docs
# @liquid_summary
# The 1-based index of the current column.
# @liquid_return [number]
attr_reader :col
# @liquid_public_docs
# @liquid_summary
# The 1-based index of current row.
# @liquid_return [number]
attr_reader :row
# @liquid_public_docs
# @liquid_summary
# The 1-based index of the current iteration.
# @liquid_return [number]
def index def index
@index + 1 @index + 1
end end
# @liquid_public_docs
# @liquid_summary
# The 0-based index of the current iteration.
# @liquid_return [number]
def index0 def index0
@index @index
end end
# @liquid_public_docs
# @liquid_summary
# The 0-based index of the current column.
# @liquid_return [number]
def col0 def col0
@col - 1 @col - 1
end end
# @liquid_public_docs
# @liquid_summary
# The 1-based index of the current iteration, in reverse order.
# @liquid_return [number]
def rindex def rindex
@length - @index @length - @index
end end
# @liquid_public_docs
# @liquid_summary
# The 0-based index of the current iteration, in reverse order.
# @liquid_return [number]
def rindex0 def rindex0
@length - @index - 1 @length - @index - 1
end end
# @liquid_public_docs
# @liquid_summary
# Returns `true` if the current iteration is the first. Returns `false` if not.
# @liquid_return [boolean]
def first def first
@index == 0 @index == 0
end end
# @liquid_public_docs
# @liquid_summary
# Returns `true` if the current iteration is the last. Returns `false` if not.
# @liquid_return [boolean]
def last def last
@index == @length - 1 @index == @length - 1
end end
# @liquid_public_docs
# @liquid_summary
# Returns `true` if the current column is the first in the row. Returns `false` if not.
# @liquid_return [boolean]
def col_first def col_first
@col == 1 @col == 1
end end
# @liquid_public_docs
# @liquid_summary
# Returns `true` if the current column is the last in the row. Returns `false` if not.
# @liquid_return [boolean]
def col_last def col_last
@col == @cols @col == @cols
end end
+4 -24
View File
@@ -1,5 +1,3 @@
# frozen_string_literal: true
module Liquid module Liquid
class Tag class Tag
attr_reader :nodelist, :tag_name, :line_number, :parse_context attr_reader :nodelist, :tag_name, :line_number, :parse_context
@@ -13,26 +11,14 @@ module Liquid
tag tag
end end
def disable_tags(*tag_names)
tag_names += disabled_tags
define_singleton_method(:disabled_tags) { tag_names }
prepend(Disabler)
end
private :new private :new
protected
def disabled_tags
[]
end
end end
def initialize(tag_name, markup, parse_context) def initialize(tag_name, markup, parse_context)
@tag_name = tag_name @tag_name = tag_name
@markup = markup @markup = markup
@parse_context = parse_context @parse_context = parse_context
@line_number = parse_context.line_number @line_number = parse_context.line_number
end end
def parse(_tokens) def parse(_tokens)
@@ -47,7 +33,7 @@ module Liquid
end end
def render(_context) def render(_context)
'' ''.freeze
end end
# For backwards compatibility with custom tags. In a future release, the semantics # For backwards compatibility with custom tags. In a future release, the semantics
@@ -61,11 +47,5 @@ module Liquid
def blank? def blank?
false false
end end
private
def parse_expression(markup)
parse_context.parse_expression(markup)
end
end end
end end
-22
View File
@@ -1,22 +0,0 @@
# frozen_string_literal: true
module Liquid
class Tag
module Disableable
def render_to_output_buffer(context, output)
if context.tag_disabled?(tag_name)
output << disabled_error(context)
return
end
super
end
def disabled_error(context)
# raise then rescue the exception so that the Context#exception_renderer can re-raise it
raise DisabledError, "#{tag_name} #{parse_context[:locale].t('errors.disabled.tag')}"
rescue DisabledError => exc
context.handle_error(exc, line_number)
end
end
end
end
-13
View File
@@ -1,13 +0,0 @@
# frozen_string_literal: true
module Liquid
class Tag
module Disabler
def render_to_output_buffer(context, output)
context.with_disabled_tags(self.class.disabled_tags) do
super
end
end
end
end
end
+17 -31
View File
@@ -1,42 +1,35 @@
# frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs # Assign sets a variable in your template.
# @liquid_type tag #
# @liquid_category variable # {% assign foo = 'monkey' %}
# @liquid_name assign #
# @liquid_summary # You can then use the variable later in the page.
# Creates a new variable. #
# @liquid_description # {{ foo }}
# You can create variables of any [basic type](/docs/api/liquid/basics#types), [object](/docs/api/liquid/objects), or object property. #
# @liquid_syntax
# {% assign variable_name = value %}
# @liquid_syntax_keyword variable_name The name of the variable being created.
# @liquid_syntax_keyword value The value you want to assign to the variable.
class Assign < Tag class Assign < Tag
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
# @api private def self.syntax_error_translation_key
def self.raise_syntax_error(parse_context) "errors.syntax.assign".freeze
raise Liquid::SyntaxError, parse_context.locale.t('errors.syntax.assign')
end end
attr_reader :to, :from attr_reader :to, :from
def initialize(tag_name, markup, parse_context) def initialize(tag_name, markup, options)
super super
if markup =~ Syntax if markup =~ Syntax
@to = Regexp.last_match(1) @to = $1
@from = Variable.new(Regexp.last_match(2), parse_context) @from = Variable.new($2, options)
else else
self.class.raise_syntax_error(parse_context) raise SyntaxError.new(options[:locale].t(self.class.syntax_error_translation_key))
end end
end end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
val = @from.render(context) val = @from.render(context)
context.scopes.last[@to] = val context.scopes.last[@to] = val
context.resource_limits.increment_assign_score(assign_score_of(val)) context.resource_limits.assign_score += assign_score_of(val)
output output
end end
@@ -49,18 +42,11 @@ module Liquid
def assign_score_of(val) def assign_score_of(val)
if val.instance_of?(String) if val.instance_of?(String)
val.bytesize val.bytesize
elsif val.instance_of?(Array) elsif val.instance_of?(Array) || val.instance_of?(Hash)
sum = 1 sum = 1
# Uses #each to avoid extra allocations. # Uses #each to avoid extra allocations.
val.each { |child| sum += assign_score_of(child) } val.each { |child| sum += assign_score_of(child) }
sum sum
elsif val.instance_of?(Hash)
sum = 1
val.each do |key, entry_value|
sum += assign_score_of(key)
sum += assign_score_of(entry_value)
end
sum
else else
1 1
end end
@@ -73,5 +59,5 @@ module Liquid
end end
end end
Template.register_tag('assign', Assign) Template.register_tag('assign'.freeze, Assign)
end end
+3 -16
View File
@@ -1,5 +1,3 @@
# frozen_string_literal: true
module Liquid module Liquid
# Break tag to be used to break out of a for loop. # Break tag to be used to break out of a for loop.
# #
@@ -10,22 +8,11 @@ module Liquid
# {% endif %} # {% endif %}
# {% endfor %} # {% endfor %}
# #
# @liquid_public_docs
# @liquid_type tag
# @liquid_category iteration
# @liquid_name break
# @liquid_summary
# Stops a [`for` loop](/docs/api/liquid/tags/for) from iterating.
# @liquid_syntax
# {% break %}
class Break < Tag class Break < Tag
INTERRUPT = BreakInterrupt.new.freeze def interrupt
BreakInterrupt.new
def render_to_output_buffer(context, output)
context.push_interrupt(INTERRUPT)
output
end end
end end
Template.register_tag('break', Break) Template.register_tag('break'.freeze, Break)
end end
+17 -22
View File
@@ -1,37 +1,32 @@
# frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs # Capture stores the result of a block into a variable without rendering it inplace.
# @liquid_type tag #
# @liquid_category variable # {% capture heading %}
# @liquid_name capture # Monkeys!
# @liquid_summary
# Creates a new variable with a string value.
# @liquid_description
# You can create complex strings with Liquid logic and variables.
# @liquid_syntax
# {% capture variable %}
# value
# {% endcapture %} # {% endcapture %}
# @liquid_syntax_keyword variable The name of the variable being created. # ...
# @liquid_syntax_keyword value The value you want to assign to the variable. # <h1>{{ heading }}</h1>
#
# Capture is useful for saving content for use later in your template, such as
# in a sidebar or footer.
#
class Capture < Block class Capture < Block
Syntax = /(#{VariableSignature}+)/o Syntax = /(#{VariableSignature}+)/o
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
if markup =~ Syntax if markup =~ Syntax
@to = Regexp.last_match(1) @to = $1
else else
raise SyntaxError, options[:locale].t("errors.syntax.capture") raise SyntaxError.new(options[:locale].t("errors.syntax.capture"))
end end
end end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
context.resource_limits.with_capture do previous_output_size = output.bytesize
capture_output = render(context) super
context.scopes.last[@to] = capture_output context.scopes.last[@to] = output
end context.resource_limits.assign_score += (output.bytesize - previous_output_size)
output output
end end
@@ -40,5 +35,5 @@ module Liquid
end end
end end
Template.register_tag('capture', Capture) Template.register_tag('capture'.freeze, Capture)
end end
+23 -57
View File
@@ -1,27 +1,4 @@
# frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category conditional
# @liquid_name case
# @liquid_summary
# Renders a specific expression depending on the value of a specific variable.
# @liquid_syntax
# {% case variable %}
# {% when first_value %}
# first_expression
# {% when second_value %}
# second_expression
# {% else %}
# third_expression
# {% endcase %}
# @liquid_syntax_keyword variable The name of the variable you want to base your case statement on.
# @liquid_syntax_keyword first_value A specific value to check for.
# @liquid_syntax_keyword second_value A specific value to check for.
# @liquid_syntax_keyword first_expression An expression to be rendered when the variable's value matches `first_value`.
# @liquid_syntax_keyword second_expression An expression to be rendered when the variable's value matches `second_value`.
# @liquid_syntax_keyword third_expression An expression to be rendered when the variable's value has no match.
class Case < Block class Case < Block
Syntax = /(#{QuotedFragment})/o Syntax = /(#{QuotedFragment})/o
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om
@@ -33,23 +10,17 @@ module Liquid
@blocks = [] @blocks = []
if markup =~ Syntax if markup =~ Syntax
@left = parse_expression(Regexp.last_match(1)) @left = Expression.parse($1)
else else
raise SyntaxError, options[:locale].t("errors.syntax.case") raise SyntaxError.new(options[:locale].t("errors.syntax.case".freeze))
end end
end end
def parse(tokens) def parse(tokens)
body = case_body = new_body body = BlockBody.new
body = @blocks.last.attachment while parse_body(body, tokens) while parse_body(body, tokens)
@blocks.reverse_each do |condition| body = @blocks.last.attachment
body = condition.attachment
unless body.frozen?
body.remove_blank_strings if blank?
body.freeze
end
end end
case_body.freeze
end end
def nodelist def nodelist
@@ -58,9 +29,9 @@ module Liquid
def unknown_tag(tag, markup, tokens) def unknown_tag(tag, markup, tokens)
case tag case tag
when 'when' when 'when'.freeze
record_when_condition(markup) record_when_condition(markup)
when 'else' when 'else'.freeze
record_else_condition(markup) record_else_condition(markup)
else else
super super
@@ -68,21 +39,16 @@ module Liquid
end end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
execute_else_block = true context.stack do
execute_else_block = true
@blocks.each do |block| @blocks.each do |block|
if block.else? if block.else?
block.attachment.render_to_output_buffer(context, output) if execute_else_block block.attachment.render_to_output_buffer(context, output) if execute_else_block
next elsif block.evaluate(context)
end execute_else_block = false
block.attachment.render_to_output_buffer(context, output)
result = Liquid::Utils.to_liquid_value( end
block.evaluate(context),
)
if result
execute_else_block = false
block.attachment.render_to_output_buffer(context, output)
end end
end end
@@ -92,16 +58,16 @@ module Liquid
private private
def record_when_condition(markup) def record_when_condition(markup)
body = new_body body = BlockBody.new
while markup while markup
unless markup =~ WhenSyntax unless markup =~ WhenSyntax
raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_when") raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_when".freeze))
end end
markup = Regexp.last_match(2) markup = $2
block = Condition.new(@left, '==', Condition.parse_expression(parse_context, Regexp.last_match(1))) block = Condition.new(@left, '=='.freeze, Expression.parse($1))
block.attach(body) block.attach(body)
@blocks << block @blocks << block
end end
@@ -109,11 +75,11 @@ module Liquid
def record_else_condition(markup) def record_else_condition(markup)
unless markup.strip.empty? unless markup.strip.empty?
raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_else") raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_else".freeze))
end end
block = ElseCondition.new block = ElseCondition.new
block.attach(new_body) block.attach(BlockBody.new)
@blocks << block @blocks << block
end end
@@ -124,5 +90,5 @@ module Liquid
end end
end end
Template.register_tag('case', Case) Template.register_tag('case'.freeze, Case)
end end
+1 -16
View File
@@ -1,19 +1,4 @@
# frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category syntax
# @liquid_name comment
# @liquid_summary
# Prevents an expression from being rendered or output.
# @liquid_description
# Any text inside `comment` tags won't be output, and any Liquid code will be parsed, but not executed.
# @liquid_syntax
# {% comment %}
# content
# {% endcomment %}
# @liquid_syntax_keyword content The content of the comment.
class Comment < Block class Comment < Block
def render_to_output_buffer(_context, output) def render_to_output_buffer(_context, output)
output output
@@ -27,5 +12,5 @@ module Liquid
end end
end end
Template.register_tag('comment', Comment) Template.register_tag('comment'.freeze, Comment)
end end
+12 -16
View File
@@ -1,22 +1,18 @@
# frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs # Continue tag to be used to break out of a for loop.
# @liquid_type tag #
# @liquid_category iteration # == Basic Usage:
# @liquid_name continue # {% for item in collection %}
# @liquid_summary # {% if item.condition %}
# Causes a [`for` loop](/docs/api/liquid/tags/for) to skip to the next iteration. # {% continue %}
# @liquid_syntax # {% endif %}
# {% continue %} # {% endfor %}
#
class Continue < Tag class Continue < Tag
INTERRUPT = ContinueInterrupt.new.freeze def interrupt
ContinueInterrupt.new
def render_to_output_buffer(context, output)
context.push_interrupt(INTERRUPT)
output
end end
end end
Template.register_tag('continue', Continue) Template.register_tag('continue'.freeze, Continue)
end end
+31 -32
View File
@@ -1,19 +1,16 @@
# frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs # Cycle is usually used within a loop to alternate between values, like colors or DOM classes.
# @liquid_type tag #
# @liquid_category iteration # {% for item in items %}
# @liquid_name cycle # <div class="{% cycle 'red', 'green', 'blue' %}"> {{ item }} </div>
# @liquid_summary # {% end %}
# Loops through a group of strings and outputs them one at a time for each iteration of a [`for` loop](/docs/api/liquid/tags/for). #
# @liquid_description # <div class="red"> Item one </div>
# The `cycle` tag must be used inside a `for` loop. # <div class="green"> Item two </div>
# <div class="blue"> Item three </div>
# <div class="red"> Item four </div>
# <div class="green"> Item five</div>
# #
# > Tip:
# > Use the `cycle` tag to output text in a predictable pattern. For example, to apply odd/even classes to rows in a table.
# @liquid_syntax
# {% cycle string, string, ... %}
class Cycle < Tag class Cycle < Tag
SimpleSyntax = /\A#{QuotedFragment}+/o SimpleSyntax = /\A#{QuotedFragment}+/o
NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om
@@ -24,36 +21,38 @@ module Liquid
super super
case markup case markup
when NamedSyntax when NamedSyntax
@variables = variables_from_string(Regexp.last_match(2)) @variables = variables_from_string($2)
@name = parse_expression(Regexp.last_match(1)) @name = Expression.parse($1)
when SimpleSyntax when SimpleSyntax
@variables = variables_from_string(markup) @variables = variables_from_string(markup)
@name = @variables.to_s @name = @variables.to_s
else else
raise SyntaxError, options[:locale].t("errors.syntax.cycle") raise SyntaxError.new(options[:locale].t("errors.syntax.cycle".freeze))
end end
end end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
context.registers[:cycle] ||= {} context.registers[:cycle] ||= {}
key = context.evaluate(@name) context.stack do
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) if val.is_a?(Array)
val = val.join val = val.join
elsif !val.is_a?(String) elsif !val.is_a?(String)
val = val.to_s val = val.to_s
end
output << val
iteration += 1
iteration = 0 if iteration >= @variables.size
context.registers[:cycle][key] = iteration
end end
output << val
iteration += 1
iteration = 0 if iteration >= @variables.size
context.registers[:cycle][key] = iteration
output output
end end
@@ -62,7 +61,7 @@ module Liquid
def variables_from_string(markup) def variables_from_string(markup)
markup.split(',').collect do |var| markup.split(',').collect do |var|
var =~ /\s*(#{QuotedFragment})\s*/o var =~ /\s*(#{QuotedFragment})\s*/o
Regexp.last_match(1) ? parse_expression(Regexp.last_match(1)) : nil $1 ? Expression.parse($1) : nil
end.compact end.compact
end end
+21 -25
View File
@@ -1,40 +1,36 @@
# frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs # decrement is used in a place where one needs to insert a counter
# @liquid_type tag # into a template, and needs the counter to survive across
# @liquid_category variable # multiple instantiations of the template.
# @liquid_name decrement # NOTE: decrement is a pre-decrement, --i,
# @liquid_summary # while increment is post: i++.
# Creates a new variable, with a default value of -1, that's decreased by 1 with each subsequent call.
# @liquid_description
# Variables that are declared with `decrement` are unique to the [layout](/themes/architecture/layouts), [template](/themes/architecture/templates),
# or [section](/themes/architecture/sections) file that they're created in. However, the variable is shared across
# [snippets](/themes/architecture#snippets) included in the file.
# #
# Similarly, variables that are created with `decrement` are independent from those created with [`assign`](/docs/api/liquid/tags/assign) # (To achieve the survival, the application must keep the context)
# and [`capture`](/docs/api/liquid/tags/capture). However, `decrement` and [`increment`](/docs/api/liquid/tags/increment) share #
# variables. # if the variable does not exist, it is created with value 0.
# @liquid_syntax
# {% decrement variable_name %}
# @liquid_syntax_keyword variable_name The name of the variable being decremented.
class Decrement < Tag
attr_reader :variable_name
# Hello: {% decrement variable %}
#
# gives you:
#
# Hello: -1
# Hello: -2
# Hello: -3
#
class Decrement < Tag
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
@variable_name = markup.strip @variable = markup.strip
end end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
counter_environment = context.environments.first value = context.environments.first[@variable] ||= 0
value = counter_environment[@variable_name] || 0
value -= 1 value -= 1
counter_environment[@variable_name] = value context.environments.first[@variable] = value
output << value.to_s output << value.to_s
output output
end end
end end
Template.register_tag('decrement', Decrement) Template.register_tag('decrement'.freeze, Decrement)
end end
+11 -28
View File
@@ -1,41 +1,24 @@
# frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs # Echo outputs an expression
# @liquid_type tag #
# @liquid_category syntax # {% echo monkey %}
# @liquid_name echo # {% echo user.name %}
# @liquid_summary #
# Outputs an expression. # This is identical to variable output syntax, like {{ foo }}, but works
# @liquid_description # inside {% liquid %} tags. The full syntax is supported, including filters:
# Using the `echo` tag is the same as wrapping an expression in curly brackets (`{{` and `}}`). However, unlike the curly #
# bracket method, you can use the `echo` tag inside [`liquid` tags](/docs/api/liquid/tags/liquid). # {% echo user | link %}
# #
# > Tip:
# > You can use [filters](/docs/api/liquid/filters) on expressions inside `echo` tags.
# @liquid_syntax
# {% liquid
# echo expression
# %}
# @liquid_syntax_keyword expression The expression to be output.
class Echo < Tag class Echo < Tag
attr_reader :variable
def initialize(tag_name, markup, parse_context) def initialize(tag_name, markup, parse_context)
super super
@variable = Variable.new(markup, parse_context) @variable = Variable.new(markup, parse_context)
end end
def render(context) def render(context)
@variable.render_to_output_buffer(context, +'') @variable.render_to_output_buffer(context, '')
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[@node.variable]
end
end end
end end
Template.register_tag('echo', Echo) Template.register_tag('echo'.freeze, Echo)
end end
+76 -66
View File
@@ -1,29 +1,48 @@
# frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs # "For" iterates over an array or collection.
# @liquid_type tag # Several useful variables are available to you within the loop.
# @liquid_category iteration #
# @liquid_name for # == Basic usage:
# @liquid_summary # {% for item in collection %}
# Renders an expression for every item in an array. # {{ forloop.index }}: {{ item.name }}
# @liquid_description # {% endfor %}
# You can do a maximum of 50 iterations with a `for` loop. If you need to iterate over more than 50 items, then use the #
# [`paginate` tag](/docs/api/liquid/tags/paginate) to split the items over multiple pages. # == Advanced usage:
# {% for item in collection %}
# <div {% if forloop.first %}class="first"{% endif %}>
# Item {{ forloop.index }}: {{ item.name }}
# </div>
# {% else %}
# There is nothing in the collection.
# {% endfor %}
#
# You can also define a limit and offset much like SQL. Remember
# that offset starts at 0 for the first item.
#
# {% for item in collection limit:5 offset:10 %}
# {{ item.name }}
# {% end %}
#
# To reverse the for loop simply use {% for item in collection reversed %} (note that the flag's spelling is different to the filter `reverse`)
#
# == Available variables:
#
# forloop.name:: 'item-collection'
# forloop.length:: Length of the loop
# forloop.index:: The current item's position in the collection;
# forloop.index starts at 1.
# This is helpful for non-programmers who start believe
# the first item in an array is 1, not 0.
# forloop.index0:: The current item's position in the collection
# where the first item is 0
# forloop.rindex:: Number of items remaining in the loop
# (length - index) where 1 is the last item.
# forloop.rindex0:: Number of items remaining in the loop
# where 0 is the last item.
# forloop.first:: Returns true if the item is the first item.
# forloop.last:: Returns true if the item is the last item.
# forloop.parentloop:: Provides access to the parent loop, if present.
# #
# > Tip:
# > Every `for` loop has an associated [`forloop` object](/docs/api/liquid/objects/forloop) with information about the loop.
# @liquid_syntax
# {% for variable in array %}
# expression
# {% endfor %}
# @liquid_syntax_keyword variable The current item in the array.
# @liquid_syntax_keyword array The array to iterate over.
# @liquid_syntax_keyword expression The expression to render for each iteration.
# @liquid_optional_param limit [number] The number of iterations to perform.
# @liquid_optional_param offset [number] The 1-based index to start iterating at.
# @liquid_optional_param range [untyped] A custom numeric range to iterate over.
# @liquid_optional_param reversed [untyped] Iterate in reverse order.
class For < Block class For < Block
Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
@@ -33,20 +52,13 @@ module Liquid
super super
@from = @limit = nil @from = @limit = nil
parse_with_selected_parser(markup) parse_with_selected_parser(markup)
@for_block = new_body @for_block = BlockBody.new
@else_block = nil @else_block = nil
end end
def parse(tokens) def parse(tokens)
if parse_body(@for_block, tokens) return unless parse_body(@for_block, tokens)
parse_body(@else_block, tokens) parse_body(@else_block, tokens)
end
if blank?
@else_block&.remove_blank_strings
@for_block.remove_blank_strings
end
@else_block&.freeze
@for_block.freeze
end end
def nodelist def nodelist
@@ -54,8 +66,8 @@ module Liquid
end end
def unknown_tag(tag, markup, tokens) def unknown_tag(tag, markup, tokens)
return super unless tag == 'else' return super unless tag == 'else'.freeze
@else_block = new_body @else_block = BlockBody.new
end end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
@@ -74,36 +86,33 @@ module Liquid
def lax_parse(markup) def lax_parse(markup)
if markup =~ Syntax if markup =~ Syntax
@variable_name = Regexp.last_match(1) @variable_name = $1
collection_name = Regexp.last_match(2) collection_name = $2
@reversed = !!Regexp.last_match(3) @reversed = !!$3
@name = "#{@variable_name}-#{collection_name}" @name = "#{@variable_name}-#{collection_name}"
@collection_name = parse_expression(collection_name) @collection_name = Expression.parse(collection_name)
markup.scan(TagAttributes) do |key, value| markup.scan(TagAttributes) do |key, value|
set_attribute(key, value) set_attribute(key, value)
end end
else else
raise SyntaxError, options[:locale].t("errors.syntax.for") raise SyntaxError.new(options[:locale].t("errors.syntax.for".freeze))
end end
end end
def strict_parse(markup) def strict_parse(markup)
p = Parser.new(markup) p = Parser.new(markup)
@variable_name = p.consume(:id) @variable_name = p.consume(:id)
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in') raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_in".freeze)) unless p.id?('in'.freeze)
collection_name = p.expression
@name = "#{@variable_name}-#{collection_name}"
@collection_name = Expression.parse(collection_name)
@reversed = p.id?('reversed'.freeze)
collection_name = p.expression while p.look(:id) && p.look(:colon, 1)
@collection_name = parse_expression(collection_name) unless attribute = p.id?('limit'.freeze) || p.id?('offset'.freeze)
raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_attribute".freeze))
@name = "#{@variable_name}-#{collection_name}"
@reversed = p.id?('reversed')
while p.look(:comma) || p.look(:id)
p.consume?(:comma)
unless (attribute = p.id?('limit') || p.id?('offset'))
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_attribute")
end end
p.consume(:colon) p.consume
set_attribute(attribute, p.expression) set_attribute(attribute, p.expression)
end end
p.consume(:end_of_string) p.consume(:end_of_string)
@@ -145,7 +154,7 @@ module Liquid
def render_segment(context, output, segment) def render_segment(context, output, segment)
for_stack = context.registers[:for_stack] ||= [] for_stack = context.registers[:for_stack] ||= []
length = segment.length length = segment.length
context.stack do context.stack do
loop_vars = Liquid::ForloopDrop.new(@name, length, for_stack[-1]) loop_vars = Liquid::ForloopDrop.new(@name, length, for_stack[-1])
@@ -153,7 +162,7 @@ module Liquid
for_stack.push(loop_vars) for_stack.push(loop_vars)
begin begin
context['forloop'] = loop_vars context['forloop'.freeze] = loop_vars
segment.each do |item| segment.each do |item|
context[@variable_name] = item context[@variable_name] = item
@@ -161,10 +170,11 @@ module Liquid
loop_vars.send(:increment!) loop_vars.send(:increment!)
# Handle any interrupts if they exist. # Handle any interrupts if they exist.
next unless context.interrupt? if context.interrupt?
interrupt = context.pop_interrupt interrupt = context.pop_interrupt
break if interrupt.is_a?(BreakInterrupt) break if interrupt.is_a? BreakInterrupt
next if interrupt.is_a?(ContinueInterrupt) next if interrupt.is_a? ContinueInterrupt
end
end end
ensure ensure
for_stack.pop for_stack.pop
@@ -176,14 +186,14 @@ module Liquid
def set_attribute(key, expr) def set_attribute(key, expr)
case key case key
when 'offset' when 'offset'.freeze
@from = if expr == 'continue' @from = if expr == 'continue'.freeze
:continue :continue
else else
parse_expression(expr) Expression.parse(expr)
end end
when 'limit' when 'limit'.freeze
@limit = parse_expression(expr) @limit = Expression.parse(expr)
end end
end end
@@ -202,5 +212,5 @@ module Liquid
end end
end end
Template.register_tag('for', For) Template.register_tag('for'.freeze, For)
end end
+30 -47
View File
@@ -1,29 +1,25 @@
# frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs # If is the conditional block
# @liquid_type tag #
# @liquid_category conditional # {% if user.admin %}
# @liquid_name if # Admin user!
# @liquid_summary # {% else %}
# Renders an expression if a specific condition is `true`. # Not admin user
# @liquid_syntax
# {% if condition %}
# expression
# {% endif %} # {% endif %}
# @liquid_syntax_keyword condition The condition to evaluate. #
# @liquid_syntax_keyword expression The expression to render if the condition is met. # There are {% if count < 5 %} less {% else %} more {% endif %} items than you need.
#
class If < Block class If < Block
Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o
ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o
BOOLEAN_OPERATORS = %w(and or).freeze BOOLEAN_OPERATORS = %w(and or).freeze
attr_reader :blocks attr_reader :blocks
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
@blocks = [] @blocks = []
push_block('if', markup) push_block('if'.freeze, markup)
end end
def nodelist def nodelist
@@ -33,17 +29,10 @@ module Liquid
def parse(tokens) def parse(tokens)
while parse_body(@blocks.last.attachment, tokens) while parse_body(@blocks.last.attachment, tokens)
end end
@blocks.reverse_each do |block|
block.attachment.remove_blank_strings if blank?
block.attachment.freeze
end
end end
ELSE_TAG_NAMES = ['elsif', 'else'].freeze
private_constant :ELSE_TAG_NAMES
def unknown_tag(tag, markup, tokens) def unknown_tag(tag, markup, tokens)
if ELSE_TAG_NAMES.include?(tag) if ['elsif'.freeze, 'else'.freeze].include?(tag)
push_block(tag, markup) push_block(tag, markup)
else else
super super
@@ -51,13 +40,11 @@ module Liquid
end end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
@blocks.each do |block| context.stack do
result = Liquid::Utils.to_liquid_value( @blocks.each do |block|
block.evaluate(context), if block.evaluate(context)
) return block.attachment.render_to_output_buffer(context, output)
end
if result
return block.attachment.render_to_output_buffer(context, output)
end end
end end
@@ -67,33 +54,29 @@ module Liquid
private private
def push_block(tag, markup) def push_block(tag, markup)
block = if tag == 'else' block = if tag == 'else'.freeze
ElseCondition.new ElseCondition.new
else else
parse_with_selected_parser(markup) parse_with_selected_parser(markup)
end end
@blocks.push(block) @blocks.push(block)
block.attach(new_body) block.attach(BlockBody.new)
end
def parse_expression(markup)
Condition.parse_expression(parse_context, markup)
end end
def lax_parse(markup) def lax_parse(markup)
expressions = markup.scan(ExpressionsAndOperators) expressions = markup.scan(ExpressionsAndOperators)
raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop =~ Syntax raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop =~ Syntax
condition = Condition.new(parse_expression(Regexp.last_match(1)), Regexp.last_match(2), parse_expression(Regexp.last_match(3))) condition = Condition.new(Expression.parse($1), $2, Expression.parse($3))
until expressions.empty? until expressions.empty?
operator = expressions.pop.to_s.strip operator = expressions.pop.to_s.strip
raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop.to_s =~ Syntax raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop.to_s =~ Syntax
new_condition = Condition.new(parse_expression(Regexp.last_match(1)), Regexp.last_match(2), parse_expression(Regexp.last_match(3))) new_condition = Condition.new(Expression.parse($1), $2, Expression.parse($3))
raise SyntaxError, options[:locale].t("errors.syntax.if") unless BOOLEAN_OPERATORS.include?(operator) raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless BOOLEAN_OPERATORS.include?(operator)
new_condition.send(operator, condition) new_condition.send(operator, condition)
condition = new_condition condition = new_condition
end end
@@ -111,7 +94,7 @@ module Liquid
def parse_binary_comparisons(p) def parse_binary_comparisons(p)
condition = parse_comparison(p) condition = parse_comparison(p)
first_condition = condition first_condition = condition
while (op = (p.id?('and') || p.id?('or'))) while op = (p.id?('and'.freeze) || p.id?('or'.freeze))
child_condition = parse_comparison(p) child_condition = parse_comparison(p)
condition.send(op, child_condition) condition.send(op, child_condition)
condition = child_condition condition = child_condition
@@ -120,9 +103,9 @@ module Liquid
end end
def parse_comparison(p) def parse_comparison(p)
a = parse_expression(p.expression) a = Expression.parse(p.expression)
if (op = p.consume?(:comparison)) if op = p.consume?(:comparison)
b = parse_expression(p.expression) b = Expression.parse(p.expression)
Condition.new(a, op, b) Condition.new(a, op, b)
else else
Condition.new(a) Condition.new(a)
@@ -136,5 +119,5 @@ module Liquid
end end
end end
Template.register_tag('if', If) Template.register_tag('if'.freeze, If)
end end
+8 -8
View File
@@ -1,19 +1,19 @@
# frozen_string_literal: true
module Liquid module Liquid
class Ifchanged < Block class Ifchanged < Block
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
block_output = +'' context.stack do
super(context, block_output) block_output = ''
super(context, block_output)
if block_output != context.registers[:ifchanged] if block_output != context.registers[:ifchanged]
context.registers[:ifchanged] = block_output context.registers[:ifchanged] = block_output
output << block_output output << block_output
end
end end
output output
end end
end end
Template.register_tag('ifchanged', Ifchanged) Template.register_tag('ifchanged'.freeze, Ifchanged)
end end
+30 -40
View File
@@ -1,49 +1,41 @@
# frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs # Include allows templates to relate with other templates
# @liquid_type tag #
# @liquid_category theme # Simply include another template:
# @liquid_name include #
# @liquid_summary # {% include 'product' %}
# Renders a [snippet](/themes/architecture#snippets). #
# @liquid_description # Include a template with a local variable:
# Inside the snippet, you can access and alter variables that are [created](/docs/api/liquid/tags/variable-tags) outside of the #
# snippet. # {% include 'product' with products[0] %}
# @liquid_syntax #
# {% include 'filename' %} # Include a template for a collection:
# @liquid_syntax_keyword filename The name of the snippet to render, without the `.liquid` extension. #
# @liquid_deprecated # {% include 'product' for products %}
# Deprecated because the way that variables are handled reduces performance and makes code harder to both read and maintain.
# #
# The `include` tag has been replaced by [`render`](/docs/api/liquid/tags/render).
class Include < Tag class Include < Tag
prepend Tag::Disableable 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 attr_reader :template_name_expr, :variable_name_expr, :attributes
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
if markup =~ SYNTAX if markup =~ Syntax
template_name = Regexp.last_match(1) template_name = $1
variable_name = Regexp.last_match(3) variable_name = $3
@alias_name = Regexp.last_match(5) @variable_name_expr = variable_name ? Expression.parse(variable_name) : nil
@variable_name_expr = variable_name ? parse_expression(variable_name) : nil @template_name_expr = Expression.parse(template_name)
@template_name_expr = parse_expression(template_name) @attributes = {}
@attributes = {}
markup.scan(TagAttributes) do |key, value| markup.scan(TagAttributes) do |key, value|
@attributes[key] = parse_expression(value) @attributes[key] = Expression.parse(value)
end end
else else
raise SyntaxError, options[:locale].t("errors.syntax.include") raise SyntaxError.new(options[:locale].t("errors.syntax.include".freeze))
end end
end end
@@ -52,15 +44,15 @@ module Liquid
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
template_name = context.evaluate(@template_name_expr) template_name = context.evaluate(@template_name_expr)
raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name.is_a?(String) raise ArgumentError.new(options[:locale].t("errors.argument.include")) unless template_name
partial = PartialCache.load( partial = PartialCache.load(
template_name, template_name,
context: context, context: context,
parse_context: parse_context, parse_context: parse_context
) )
context_variable_name = @alias_name || template_name.split('/').last context_variable_name = template_name.split('/'.freeze).last
variable = if @variable_name_expr variable = if @variable_name_expr
context.evaluate(@variable_name_expr) context.evaluate(@variable_name_expr)
@@ -69,12 +61,10 @@ module Liquid
end end
old_template_name = context.template_name old_template_name = context.template_name
old_partial = context.partial old_partial = context.partial
begin begin
context.template_name = partial.name context.template_name = template_name
context.partial = true context.partial = true
context.stack do context.stack do
@attributes.each do |key, value| @attributes.each do |key, value|
context[key] = context.evaluate(value) context[key] = context.evaluate(value)
@@ -92,7 +82,7 @@ module Liquid
end end
ensure ensure
context.template_name = old_template_name context.template_name = old_template_name
context.partial = old_partial context.partial = old_partial
end end
output output
@@ -105,11 +95,11 @@ module Liquid
def children def children
[ [
@node.template_name_expr, @node.template_name_expr,
@node.variable_name_expr, @node.variable_name_expr
] + @node.attributes.values ] + @node.attributes.values
end end
end end
end end
Template.register_tag('include', Include) Template.register_tag('include'.freeze, Include)
end end
+18 -25
View File
@@ -1,40 +1,33 @@
# frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs # increment is used in a place where one needs to insert a counter
# @liquid_type tag # into a template, and needs the counter to survive across
# @liquid_category variable # multiple instantiations of the template.
# @liquid_name increment # (To achieve the survival, the application must keep the context)
# @liquid_summary #
# Creates a new variable, with a default value of 0, that's increased by 1 with each subsequent call. # if the variable does not exist, it is created with value 0.
# @liquid_description #
# Variables that are declared with `increment` are unique to the [layout](/themes/architecture/layouts), [template](/themes/architecture/templates), # Hello: {% increment variable %}
# or [section](/themes/architecture/sections) file that they're created in. However, the variable is shared across #
# [snippets](/themes/architecture#snippets) included in the file. # gives you:
#
# Hello: 0
# Hello: 1
# Hello: 2
# #
# Similarly, variables that are created with `increment` are independent from those created with [`assign`](/docs/api/liquid/tags/assign)
# and [`capture`](/docs/api/liquid/tags/capture). However, `increment` and [`decrement`](/docs/api/liquid/tags/decrement) share
# variables.
# @liquid_syntax
# {% increment variable_name %}
# @liquid_syntax_keyword variable_name The name of the variable being incremented.
class Increment < Tag class Increment < Tag
attr_reader :variable_name
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
@variable_name = markup.strip @variable = markup.strip
end end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
counter_environment = context.environments.first value = context.environments.first[@variable] ||= 0
value = counter_environment[@variable_name] || 0 context.environments.first[@variable] = value + 1
counter_environment[@variable_name] = value + 1
output << value.to_s output << value.to_s
output output
end end
end end
Template.register_tag('increment', Increment) Template.register_tag('increment'.freeze, Increment)
end end
-30
View File
@@ -1,30 +0,0 @@
# frozen_string_literal: true
module Liquid
class InlineComment < Tag
def initialize(tag_name, markup, options)
super
# Semantically, a comment should only ignore everything after it on the line.
# Currently, this implementation doesn't support mixing a comment with another tag
# but we need to reserve future support for this and prevent the introduction
# of inline comments from being backward incompatible change.
#
# As such, we're forcing users to put a # symbol on every line otherwise this
# tag will throw an error.
if markup.match?(/\n\s*[^#\s]/)
raise SyntaxError, options[:locale].t("errors.syntax.inline_comment_invalid")
end
end
def render_to_output_buffer(_context, output)
output
end
def blank?
true
end
end
Template.register_tag('#', InlineComment)
end
+10 -24
View File
@@ -1,20 +1,7 @@
# frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category syntax
# @liquid_name raw
# @liquid_summary
# Outputs any Liquid code as text instead of rendering it.
# @liquid_syntax
# {% raw %}
# expression
# {% endraw %}
# @liquid_syntax_keyword expression The expression to be output without being rendered.
class Raw < Block class Raw < Block
Syntax = /\A\s*\z/ Syntax = /\A\s*\z/
FullTokenPossiblyInvalid = /\A(.*)#{TagStart}#{WhitespaceControl}?\s*(\w+)\s*(.*)?#{WhitespaceControl}?#{TagEnd}\z/om FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
def initialize(tag_name, markup, parse_context) def initialize(tag_name, markup, parse_context)
super super
@@ -23,17 +10,16 @@ module Liquid
end end
def parse(tokens) def parse(tokens)
@body = +'' @body = ''
while (token = tokens.shift) while token = tokens.shift
if token =~ FullTokenPossiblyInvalid && block_delimiter == Regexp.last_match(2) if token =~ FullTokenPossiblyInvalid
parse_context.trim_whitespace = (token[-3] == WhitespaceControl) @body << $1 if $1 != "".freeze
@body << Regexp.last_match(1) if Regexp.last_match(1) != "" return if block_delimiter == $2
return
end end
@body << token unless token.empty? @body << token unless token.empty?
end end
raise_tag_never_closed(block_name) raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
end end
def render_to_output_buffer(_context, output) def render_to_output_buffer(_context, output)
@@ -52,11 +38,11 @@ module Liquid
protected protected
def ensure_valid_markup(tag_name, markup, parse_context) def ensure_valid_markup(tag_name, markup, parse_context)
unless Syntax.match?(markup) unless markup =~ Syntax
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_unexpected_args", tag: tag_name) raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_unexpected_args".freeze, tag: tag_name))
end end
end end
end end
Template.register_tag('raw', Raw) Template.register_tag('raw'.freeze, Raw)
end end
+17 -76
View File
@@ -1,100 +1,42 @@
# frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category theme
# @liquid_name render
# @liquid_summary
# Renders a [snippet](/themes/architecture#snippets) or [app block](/themes/architecture/sections/section-schema#render-app-blocks).
# @liquid_description
# Inside snippets and app blocks, you can't directly access variables that are [created](/docs/api/liquid/tags/variable-tags) outside
# of the snippet or app block. However, you can [specify variables as parameters](/docs/api/liquid/tags/render#render-passing-variables-to-a-snippet)
# to pass outside variables to snippets.
#
# While you can't directly access created variables, you can access global objects, as well as any objects that are
# directly accessible outside the snippet or app block. For example, a snippet or app block inside the [product template](/themes/architecture/templates/product)
# can access the [`product` object](/docs/api/liquid/objects/product), and a snippet or app block inside a [section](/themes/architecture/sections)
# can access the [`section` object](/docs/api/liquid/objects/section).
#
# Outside a snippet or app block, you can't access variables created inside the snippet or app block.
#
# > Note:
# > When you render a snippet using the `render` tag, you can't use the [`include` tag](/docs/api/liquid/tags/include)
# > inside the snippet.
# @liquid_syntax
# {% render 'filename' %}
# @liquid_syntax_keyword filename The name of the snippet to render, without the `.liquid` extension.
class Render < Tag class Render < Tag
FOR = 'for' SYNTAX = /(#{QuotedString})#{QuotedFragment}*/o
SYNTAX = /(#{QuotedString}+)(\s+(with|#{FOR})\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
disable_tags "include" attr_reader :template_name_expr, :attributes
attr_reader :template_name_expr, :variable_name_expr, :attributes, :alias_name
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
raise SyntaxError, options[:locale].t("errors.syntax.render") unless markup =~ SYNTAX raise SyntaxError.new(options[:locale].t("errors.syntax.render".freeze)) unless markup =~ SYNTAX
template_name = Regexp.last_match(1) template_name = $1
with_or_for = Regexp.last_match(3)
variable_name = Regexp.last_match(4)
@alias_name = Regexp.last_match(6) @template_name_expr = Expression.parse(template_name)
@variable_name_expr = variable_name ? parse_expression(variable_name) : nil
@template_name_expr = parse_expression(template_name)
@is_for_loop = (with_or_for == FOR)
@attributes = {} @attributes = {}
markup.scan(TagAttributes) do |key, value| markup.scan(TagAttributes) do |key, value|
@attributes[key] = parse_expression(value) @attributes[key] = Expression.parse(value)
end end
end end
def for_loop?
@is_for_loop
end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
render_tag(context, output) # Though we evaluate this here we will only ever parse it as a string literal.
end template_name = context.evaluate(@template_name_expr)
raise ArgumentError.new(options[:locale].t("errors.argument.include")) unless template_name
def render_tag(context, output)
# The expression should be a String literal, which parses to a String object
template_name = @template_name_expr
raise ::ArgumentError unless template_name.is_a?(String)
partial = PartialCache.load( partial = PartialCache.load(
template_name, template_name,
context: context, context: context,
parse_context: parse_context, parse_context: parse_context
) )
context_variable_name = @alias_name || template_name.split('/').last inner_context = context.new_isolated_subcontext
inner_context.template_name = template_name
render_partial_func = ->(var, forloop) { inner_context.partial = true
inner_context = context.new_isolated_subcontext @attributes.each do |key, value|
inner_context.template_name = partial.name inner_context[key] = context.evaluate(value)
inner_context.partial = true
inner_context['forloop'] = forloop if forloop
@attributes.each do |key, value|
inner_context[key] = context.evaluate(value)
end
inner_context[context_variable_name] = var unless var.nil?
partial.render_to_output_buffer(inner_context, output)
forloop&.send(:increment!)
}
variable = @variable_name_expr ? context.evaluate(@variable_name_expr) : nil
if @is_for_loop && variable.respond_to?(:each) && variable.respond_to?(:count)
forloop = Liquid::ForloopDrop.new(template_name, variable.count, nil)
variable.each { |var| render_partial_func.call(var, forloop) }
else
render_partial_func.call(variable, nil)
end end
partial.render_to_output_buffer(inner_context, output)
output output
end end
@@ -103,11 +45,10 @@ module Liquid
def children def children
[ [
@node.template_name_expr, @node.template_name_expr,
@node.variable_name_expr,
] + @node.attributes.values ] + @node.attributes.values
end end
end end
end end
Template.register_tag('render', Render) Template.register_tag('render'.freeze, Render)
end end
+13 -44
View File
@@ -1,28 +1,4 @@
# frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category iteration
# @liquid_name tablerow
# @liquid_summary
# Generates HTML table rows for every item in an array.
# @liquid_description
# The `tablerow` tag must be wrapped in HTML `<table>` and `</table>` tags.
#
# > Tip:
# > Every `tablerow` loop has an associated [`tablerowloop` object](/docs/api/liquid/objects/tablerowloop) with information about the loop.
# @liquid_syntax
# {% tablerow variable in array %}
# expression
# {% endtablerow %}
# @liquid_syntax_keyword variable The current item in the array.
# @liquid_syntax_keyword array The array to iterate over.
# @liquid_syntax_keyword expression The expression to render.
# @liquid_optional_param cols [number] The number of columns that the table should have.
# @liquid_optional_param limit [number] The number of iterations to perform.
# @liquid_optional_param offset [number] The 1-based index to start iterating at.
# @liquid_optional_param range [untyped] A custom numeric range to iterate over.
class TableRow < Block class TableRow < Block
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
@@ -31,32 +7,33 @@ module Liquid
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
if markup =~ Syntax if markup =~ Syntax
@variable_name = Regexp.last_match(1) @variable_name = $1
@collection_name = parse_expression(Regexp.last_match(2)) @collection_name = Expression.parse($2)
@attributes = {} @attributes = {}
markup.scan(TagAttributes) do |key, value| markup.scan(TagAttributes) do |key, value|
@attributes[key] = parse_expression(value) @attributes[key] = Expression.parse(value)
end end
else else
raise SyntaxError, options[:locale].t("errors.syntax.table_row") raise SyntaxError.new(options[:locale].t("errors.syntax.table_row".freeze))
end end
end end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
(collection = context.evaluate(@collection_name)) || (return '') collection = context.evaluate(@collection_name) or return ''.freeze
from = @attributes.key?('offset') ? to_integer(context.evaluate(@attributes['offset'])) : 0 from = @attributes.key?('offset'.freeze) ? context.evaluate(@attributes['offset'.freeze]).to_i : 0
to = @attributes.key?('limit') ? from + to_integer(context.evaluate(@attributes['limit'])) : nil to = @attributes.key?('limit'.freeze) ? from + context.evaluate(@attributes['limit'.freeze]).to_i : nil
collection = Utils.slice_collection(collection, from, to) collection = Utils.slice_collection(collection, from, to)
length = collection.length
cols = @attributes.key?('cols') ? to_integer(context.evaluate(@attributes['cols'])) : length length = collection.length
cols = context.evaluate(@attributes['cols'.freeze]).to_i
output << "<tr class=\"row1\">\n" output << "<tr class=\"row1\">\n"
context.stack do context.stack do
tablerowloop = Liquid::TablerowloopDrop.new(length, cols) tablerowloop = Liquid::TablerowloopDrop.new(length, cols)
context['tablerowloop'] = tablerowloop context['tablerowloop'.freeze] = tablerowloop
collection.each do |item| collection.each do |item|
context[@variable_name] = item context[@variable_name] = item
@@ -82,15 +59,7 @@ module Liquid
super + @node.attributes.values + [@node.collection_name] super + @node.attributes.values + [@node.collection_name]
end end
end end
private
def to_integer(value)
value.to_i
rescue NoMethodError
raise Liquid::ArgumentError, "invalid integer"
end
end end
Template.register_tag('tablerow', TableRow) Template.register_tag('tablerow'.freeze, TableRow)
end end
+16 -35
View File
@@ -1,43 +1,24 @@
# frozen_string_literal: true
require_relative 'if' require_relative 'if'
module Liquid module Liquid
# @liquid_public_docs # Unless is a conditional just like 'if' but works on the inverse logic.
# @liquid_type tag #
# @liquid_category conditional # {% unless x < 0 %} x is greater than zero {% endunless %}
# @liquid_name unless #
# @liquid_summary
# Renders an expression unless a specific condition is `true`.
# @liquid_description
# > Tip:
# > Similar to the [`if` tag](/docs/api/liquid/tags/if), you can use `elsif` to add more conditions to an `unless` tag.
# @liquid_syntax
# {% unless condition %}
# expression
# {% endunless %}
# @liquid_syntax_keyword condition The condition to evaluate.
# @liquid_syntax_keyword expression The expression to render unless the condition is met.
class Unless < If class Unless < If
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
# First condition is interpreted backwards ( if not ) context.stack do
first_block = @blocks.first # First condition is interpreted backwards ( if not )
result = Liquid::Utils.to_liquid_value( first_block = @blocks.first
first_block.evaluate(context), unless first_block.evaluate(context)
) return first_block.attachment.render_to_output_buffer(context, output)
end
unless result # After the first condition unless works just like if
return first_block.attachment.render_to_output_buffer(context, output) @blocks[1..-1].each do |block|
end if block.evaluate(context)
return block.attachment.render_to_output_buffer(context, output)
# After the first condition unless works just like if end
@blocks[1..-1].each do |block|
result = Liquid::Utils.to_liquid_value(
block.evaluate(context),
)
if result
return block.attachment.render_to_output_buffer(context, output)
end end
end end
@@ -45,5 +26,5 @@ module Liquid
end end
end end
Template.register_tag('unless', Unless) Template.register_tag('unless'.freeze, Unless)
end end
+73 -49
View File
@@ -1,5 +1,3 @@
# frozen_string_literal: true
module Liquid module Liquid
# Templates are central to liquid. # Templates are central to liquid.
# Interpretating templates is a two step process. First you compile the # Interpretating templates is a two step process. First you compile the
@@ -15,14 +13,16 @@ module Liquid
# template.render('user_name' => 'bob') # template.render('user_name' => 'bob')
# #
class Template class Template
attr_accessor :root, :name attr_accessor :root
attr_reader :resource_limits, :warnings attr_reader :resource_limits, :warnings
@@file_system = BlankFileSystem.new
class TagRegistry class TagRegistry
include Enumerable include Enumerable
def initialize def initialize
@tags = {} @tags = {}
@cache = {} @cache = {}
end end
@@ -61,54 +61,76 @@ module Liquid
# :lax acts like liquid 2.5 and silently ignores malformed tags in most cases. # :lax acts like liquid 2.5 and silently ignores malformed tags in most cases.
# :warn is the default and will give deprecation warnings when invalid syntax is used. # :warn is the default and will give deprecation warnings when invalid syntax is used.
# :strict will enforce correct syntax. # :strict will enforce correct syntax.
attr_accessor :error_mode attr_writer :error_mode
Template.error_mode = :lax
# Sets how strict the taint checker should be.
# :lax is the default, and ignores the taint flag completely
# :warn adds a warning, but does not interrupt the rendering
# :error raises an error when tainted output is used
attr_writer :taint_mode
attr_accessor :default_exception_renderer attr_accessor :default_exception_renderer
Template.default_exception_renderer = lambda do |exception| Template.default_exception_renderer = lambda do |exception|
exception exception
end end
attr_accessor :file_system def file_system
Template.file_system = BlankFileSystem.new @@file_system
end
attr_accessor :tags def file_system=(obj)
Template.tags = TagRegistry.new @@file_system = obj
private :tags= end
def register_tag(name, klass) def register_tag(name, klass)
tags[name.to_s] = klass tags[name.to_s] = klass
end end
def tags
@tags ||= TagRegistry.new
end
def error_mode
@error_mode ||= :lax
end
def taint_mode
@taint_mode ||= :lax
end
# Pass a module with filter methods which should be available # Pass a module with filter methods which should be available
# to all liquid views. Good for registering the standard library # to all liquid views. Good for registering the standard library
def register_filter(mod) def register_filter(mod)
StrainerFactory.add_global_filter(mod) Strainer.global_filter(mod)
end end
attr_accessor :default_resource_limits def default_resource_limits
Template.default_resource_limits = {} @default_resource_limits ||= {}
private :default_resource_limits= end
# creates a new <tt>Template</tt> object from liquid source code # creates a new <tt>Template</tt> object from liquid source code
# To enable profiling, pass in <tt>profile: true</tt> as an option. # To enable profiling, pass in <tt>profile: true</tt> as an option.
# See Liquid::Profiler for more information # See Liquid::Profiler for more information
def parse(source, options = {}) def parse(source, options = {})
new.parse(source, options) template = Template.new
template.parse(source, options)
end end
end end
def initialize def initialize
@rethrow_errors = false @rethrow_errors = false
@resource_limits = ResourceLimits.new(Template.default_resource_limits) @resource_limits = ResourceLimits.new(self.class.default_resource_limits)
end end
# Parse source code. # Parse source code.
# Returns self for easy chaining # Returns self for easy chaining
def parse(source, options = {}) def parse(source, options = {})
parse_context = configure_options(options) @options = options
tokenizer = parse_context.new_tokenizer(source, start_line_number: @line_numbers && 1) @profiling = options[:profile]
@root = Document.parse(tokenizer, parse_context) @line_numbers = options[:line_numbers] || @profiling
parse_context = options.is_a?(ParseContext) ? options : ParseContext.new(options)
@root = Document.parse(tokenize(source), parse_context)
@warnings = parse_context.warnings
self self
end end
@@ -143,19 +165,19 @@ module Liquid
# filters and tags and might be useful to integrate liquid more with its host application # filters and tags and might be useful to integrate liquid more with its host application
# #
def render(*args) def render(*args)
return '' if @root.nil? return ''.freeze if @root.nil?
context = case args.first context = case args.first
when Liquid::Context when Liquid::Context
c = args.shift c = args.shift
if @rethrow_errors if @rethrow_errors
c.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA c.exception_renderer = ->(e) { raise }
end end
c c
when Liquid::Drop when Liquid::Drop
drop = args.shift drop = args.shift
drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits) drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
when Hash when Hash
Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits) Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
@@ -170,12 +192,9 @@ module Liquid
case args.last case args.last
when Hash when Hash
options = args.pop options = args.pop
output = options[:output] if options[:output] output = options[:output] if options[:output]
static_registers = context.registers.static
options[:registers]&.each do |key, register| registers.merge!(options[:registers]) if options[:registers].is_a?(Hash)
static_registers[key] = register
end
apply_options_to_context(context, options) apply_options_to_context(context, options)
when Module, Array when Module, Array
@@ -185,15 +204,12 @@ module Liquid
# Retrying a render resets resource usage # Retrying a render resets resource usage
context.resource_limits.reset context.resource_limits.reset
if @profiling && context.profiler.nil?
@profiler = context.profiler = Liquid::Profiler.new
end
context.template_name ||= name
begin begin
# render the nodelist. # render the nodelist.
@root.render_to_output_buffer(context, output || +'') # for performance reasons we get an array back here. join will make a string out of it.
with_profiling(context) do
@root.render_to_output_buffer(context, output || '')
end
rescue Liquid::MemoryError => e rescue Liquid::MemoryError => e
context.handle_error(e) context.handle_error(e)
ensure ensure
@@ -212,25 +228,33 @@ module Liquid
private private
def configure_options(options) def tokenize(source)
if (profiling = options[:profile]) Tokenizer.new(source, @line_numbers)
raise "Profiler not loaded, require 'liquid/profiler' first" unless defined?(Liquid::Profiler) end
end
@options = options def with_profiling(context)
@profiling = profiling if @profiling && !context.partial
@line_numbers = options[:line_numbers] || @profiling raise "Profiler not loaded, require 'liquid/profiler' first" unless defined?(Liquid::Profiler)
parse_context = options.is_a?(ParseContext) ? options : ParseContext.new(options)
@warnings = parse_context.warnings @profiler = Profiler.new
parse_context @profiler.start
begin
yield
ensure
@profiler.stop
end
else
yield
end
end end
def apply_options_to_context(context, options) def apply_options_to_context(context, options)
context.add_filters(options[:filters]) if options[:filters] context.add_filters(options[:filters]) if options[:filters]
context.global_filter = options[:global_filter] if options[:global_filter] context.global_filter = options[:global_filter] if options[:global_filter]
context.exception_renderer = options[:exception_renderer] if options[:exception_renderer] context.exception_renderer = options[:exception_renderer] if options[:exception_renderer]
context.strict_variables = options[:strict_variables] if options[:strict_variables] context.strict_variables = options[:strict_variables] if options[:strict_variables]
context.strict_filters = options[:strict_filters] if options[:strict_filters] context.strict_filters = options[:strict_filters] if options[:strict_filters]
end end
end end
end end
-9
View File
@@ -1,9 +0,0 @@
# frozen_string_literal: true
module Liquid
class TemplateFactory
def for(_template_name)
Liquid::Template.new
end
end
end
+6 -14
View File
@@ -1,22 +1,16 @@
# frozen_string_literal: true
module Liquid module Liquid
class Tokenizer class Tokenizer
attr_reader :line_number, :for_liquid_tag attr_reader :line_number, :for_liquid_tag
def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false) def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false)
@source = source.to_s.to_str @source = source
@line_number = line_number || (line_numbers ? 1 : nil) @line_number = line_number || (line_numbers ? 1 : nil)
@for_liquid_tag = for_liquid_tag @for_liquid_tag = for_liquid_tag
@offset = 0 @tokens = tokenize
@tokens = tokenize
end end
def shift def shift
token = @tokens[@offset] token = @tokens.shift or return
return nil unless token
@offset += 1
if @line_number if @line_number
@line_number += @for_liquid_tag ? 1 : token.count("\n") @line_number += @for_liquid_tag ? 1 : token.count("\n")
@@ -28,16 +22,14 @@ module Liquid
private private
def tokenize def tokenize
return [] if @source.empty? return [] if @source.to_s.empty?
return @source.split("\n") if @for_liquid_tag return @source.split("\n") if @for_liquid_tag
tokens = @source.split(TemplateParser) tokens = @source.split(TemplateParser)
# removes the rogue empty element at the beginning of the array # removes the rogue empty element at the beginning of the array
if tokens[0]&.empty? tokens.shift if tokens[0] && tokens[0].empty?
@offset += 1
end
tokens tokens
end end
-8
View File
@@ -1,8 +0,0 @@
# frozen_string_literal: true
module Liquid
module Usage
def self.increment(name)
end
end
end
+3 -13
View File
@@ -1,5 +1,3 @@
# frozen_string_literal: true
module Liquid module Liquid
module Utils module Utils
def self.slice_collection(collection, from, to) def self.slice_collection(collection, from, to)
@@ -12,7 +10,7 @@ module Liquid
def self.slice_collection_using_each(collection, from, to) def self.slice_collection_using_each(collection, from, to)
segments = [] segments = []
index = 0 index = 0
# Maintains Ruby 1.8.7 String#each behaviour on 1.9 # Maintains Ruby 1.8.7 String#each behaviour on 1.9
if collection.is_a?(String) if collection.is_a?(String)
@@ -52,7 +50,7 @@ module Liquid
when Numeric when Numeric
obj obj
when String when String
/\A-?\d+\.\d+\z/.match?(obj.strip) ? BigDecimal(obj) : obj.to_i (obj.strip =~ /\A-?\d+\.\d+\z/) ? BigDecimal(obj) : obj.to_i
else else
if obj.respond_to?(:to_number) if obj.respond_to?(:to_number)
obj.to_number obj.to_number
@@ -71,7 +69,7 @@ module Liquid
end end
case obj case obj
when 'now', 'today' when 'now'.freeze, 'today'.freeze
Time.now Time.now
when /\A\d+\z/, Integer when /\A\d+\z/, Integer
Time.at(obj.to_i) Time.at(obj.to_i)
@@ -81,13 +79,5 @@ module Liquid
rescue ::ArgumentError rescue ::ArgumentError
nil nil
end end
def self.to_liquid_value(obj)
# Enable "obj" to represent itself as a primitive value like integer, string, or boolean
return obj.to_liquid_value if obj.respond_to?(:to_liquid_value)
# Otherwise return the object itself
obj
end
end end
end end
+43 -36
View File
@@ -1,5 +1,3 @@
# frozen_string_literal: true
module Liquid module Liquid
# Holds variables. Variables are only loaded "just in time" # Holds variables. Variables are only loaded "just in time"
# and are not evaluated as part of the render stage # and are not evaluated as part of the render stage
@@ -12,10 +10,10 @@ module Liquid
# {{ user | link }} # {{ user | link }}
# #
class Variable class Variable
FilterMarkupRegex = /#{FilterSeparator}\s*(.*)/om FilterMarkupRegex = /#{FilterSeparator}\s*(.*)/om
FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
FilterArgsRegex = /(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o FilterArgsRegex = /(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o
JustTagAttributes = /\A#{TagAttributes}\z/o JustTagAttributes = /\A#{TagAttributes}\z/o
MarkupWithQuotedFragment = /(#{QuotedFragment})(.*)/om MarkupWithQuotedFragment = /(#{QuotedFragment})(.*)/om
attr_accessor :filters, :name, :line_number attr_accessor :filters, :name, :line_number
@@ -25,12 +23,12 @@ module Liquid
include ParserSwitching include ParserSwitching
def initialize(markup, parse_context) def initialize(markup, parse_context)
@markup = markup @markup = markup
@name = nil @name = nil
@parse_context = parse_context @parse_context = parse_context
@line_number = parse_context.line_number @line_number = parse_context.line_number
strict_parse_with_error_mode_fallback(markup) parse_with_selected_parser(markup)
end end
def raw def raw
@@ -45,11 +43,11 @@ module Liquid
@filters = [] @filters = []
return unless markup =~ MarkupWithQuotedFragment return unless markup =~ MarkupWithQuotedFragment
name_markup = Regexp.last_match(1) name_markup = $1
filter_markup = Regexp.last_match(2) filter_markup = $2
@name = parse_context.parse_expression(name_markup) @name = Expression.parse(name_markup)
if filter_markup =~ FilterMarkupRegex if filter_markup =~ FilterMarkupRegex
filters = Regexp.last_match(1).scan(FilterParser) filters = $1.scan(FilterParser)
filters.each do |f| filters.each do |f|
next unless f =~ /\w+/ next unless f =~ /\w+/
filtername = Regexp.last_match(0) filtername = Regexp.last_match(0)
@@ -63,9 +61,7 @@ module Liquid
@filters = [] @filters = []
p = Parser.new(markup) p = Parser.new(markup)
return if p.look(:end_of_string) @name = Expression.parse(p.expression)
@name = parse_context.parse_expression(p.expression)
while p.consume?(:pipe) while p.consume?(:pipe)
filtername = p.consume(:id) filtername = p.consume(:id)
filterargs = p.consume?(:colon) ? parse_filterargs(p) : [] filterargs = p.consume?(:colon) ? parse_filterargs(p) : []
@@ -83,14 +79,14 @@ module Liquid
end end
def render(context) def render(context)
obj = context.evaluate(@name) obj = @filters.inject(context.evaluate(@name)) do |output, (filter_name, filter_args, filter_kwargs)|
@filters.each do |filter_name, filter_args, filter_kwargs|
filter_args = evaluate_filter_expressions(context, filter_args, filter_kwargs) filter_args = evaluate_filter_expressions(context, filter_args, filter_kwargs)
obj = context.invoke(filter_name, obj, *filter_args) context.invoke(filter_name, output, *filter_args)
end end
context.apply_global_filter(obj) obj = context.apply_global_filter(obj)
taint_check(context, obj)
obj
end end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
@@ -106,25 +102,17 @@ module Liquid
output output
end end
def disabled?(_context)
false
end
def disabled_tags
[]
end
private private
def parse_filter_expressions(filter_name, unparsed_args) def parse_filter_expressions(filter_name, unparsed_args)
filter_args = [] filter_args = []
keyword_args = nil keyword_args = nil
unparsed_args.each do |a| unparsed_args.each do |a|
if (matches = a.match(JustTagAttributes)) if matches = a.match(JustTagAttributes)
keyword_args ||= {} keyword_args ||= {}
keyword_args[matches[1]] = parse_context.parse_expression(matches[2]) keyword_args[matches[1]] = Expression.parse(matches[2])
else else
filter_args << parse_context.parse_expression(a) filter_args << Expression.parse(a)
end end
end end
result = [filter_name, filter_args] result = [filter_name, filter_args]
@@ -133,7 +121,7 @@ module Liquid
end end
def evaluate_filter_expressions(context, filter_args, filter_kwargs) 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 if filter_kwargs
parsed_kwargs = {} parsed_kwargs = {}
filter_kwargs.each do |key, expr| filter_kwargs.each do |key, expr|
@@ -144,6 +132,25 @@ module Liquid
parsed_args parsed_args
end end
def taint_check(context, obj)
return unless obj.tainted?
return if Template.taint_mode == :lax
@markup =~ QuotedFragment
name = Regexp.last_match(0)
error = TaintedError.new("variable '#{name}' is tainted and was not escaped")
error.line_number = line_number
error.template_name = context.template_name
case Template.taint_mode
when :warn
context.warnings << error
when :error
raise error
end
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children def children
[@node.name] + @node.filters.flatten [@node.name] + @node.filters.flatten
+10 -18
View File
@@ -1,8 +1,7 @@
# frozen_string_literal: true
module Liquid module Liquid
class VariableLookup class VariableLookup
COMMAND_METHODS = ['size', 'first', 'last'].freeze SQUARE_BRACKETED = /\A\[(.*)\]\z/m
COMMAND_METHODS = ['size'.freeze, 'first'.freeze, 'last'.freeze].freeze
attr_reader :name, :lookups attr_reader :name, :lookups
@@ -14,38 +13,31 @@ module Liquid
lookups = markup.scan(VariableParser) lookups = markup.scan(VariableParser)
name = lookups.shift name = lookups.shift
if name&.start_with?('[') && name&.end_with?(']') if name =~ SQUARE_BRACKETED
name = Expression.parse(name[1..-2]) name = Expression.parse($1)
end end
@name = name @name = name
@lookups = lookups @lookups = lookups
@command_flags = 0 @command_flags = 0
@lookups.each_index do |i| @lookups.each_index do |i|
lookup = lookups[i] lookup = lookups[i]
if lookup&.start_with?('[') && lookup&.end_with?(']') if lookup =~ SQUARE_BRACKETED
lookups[i] = Expression.parse(lookup[1..-2]) lookups[i] = Expression.parse($1)
elsif COMMAND_METHODS.include?(lookup) elsif COMMAND_METHODS.include?(lookup)
@command_flags |= 1 << i @command_flags |= 1 << i
end end
end end
end end
def lookup_command?(lookup_index)
@command_flags & (1 << lookup_index) != 0
end
def evaluate(context) def evaluate(context)
name = context.evaluate(@name) name = context.evaluate(@name)
object = context.find_variable(name) object = context.find_variable(name)
@lookups.each_index do |i| @lookups.each_index do |i|
key = context.evaluate(@lookups[i]) key = context.evaluate(@lookups[i])
# Cast "key" to its liquid value to enable it to act as a primitive value
key = Liquid::Utils.to_liquid_value(key)
# If object is a hash- or array-like object we look for the # If object is a hash- or array-like object we look for the
# presence of the key and if its available we return it # presence of the key and if its available we return it
if object.respond_to?(:[]) && if object.respond_to?(:[]) &&
@@ -53,13 +45,13 @@ module Liquid
(object.respond_to?(:fetch) && key.is_a?(Integer))) (object.respond_to?(:fetch) && key.is_a?(Integer)))
# if its a proc we will replace the entry with the proc # if its a proc we will replace the entry with the proc
res = context.lookup_and_evaluate(object, key) res = context.lookup_and_evaluate(object, key)
object = res.to_liquid object = res.to_liquid
# Some special cases. If the part wasn't in square brackets and # Some special cases. If the part wasn't in square brackets and
# no key with the same name was found we interpret following calls # no key with the same name was found we interpret following calls
# as commands and call them on the current object # as commands and call them on the current object
elsif lookup_command?(i) && object.respond_to?(key) elsif @command_flags & (1 << i) != 0 && object.respond_to?(key)
object = object.send(key).to_liquid object = object.send(key).to_liquid
# No key was present with the desired value and it wasn't one of the directly supported # No key was present with the desired value and it wasn't one of the directly supported
+1 -2
View File
@@ -1,6 +1,5 @@
# encoding: utf-8 # encoding: utf-8
# frozen_string_literal: true
module Liquid module Liquid
VERSION = "5.4.0" VERSION = "4.0.3".freeze
end end
+6 -8
View File
@@ -1,8 +1,7 @@
# encoding: utf-8 # encoding: utf-8
# frozen_string_literal: true
lib = File.expand_path('../lib/', __FILE__) 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" require "liquid/version"
@@ -17,17 +16,16 @@ Gem::Specification.new do |s|
s.license = "MIT" s.license = "MIT"
# s.description = "A secure, non-evaling end user template engine with aesthetic markup." # s.description = "A secure, non-evaling end user template engine with aesthetic markup."
s.required_ruby_version = ">= 2.7.0" s.required_ruby_version = ">= 2.4.0"
s.required_rubygems_version = ">= 1.3.7" s.required_rubygems_version = ">= 1.3.7"
s.metadata['allowed_push_host'] = 'https://rubygems.org' s.test_files = Dir.glob("{test}/**/*")
s.files = Dir.glob("{lib}/**/*") + %w(LICENSE README.md)
s.files = Dir.glob("{lib}/**/*") + %w(LICENSE README.md)
s.extra_rdoc_files = ["History.md", "README.md"] s.extra_rdoc_files = ["History.md", "README.md"]
s.require_path = "lib" s.require_path = "lib"
s.add_development_dependency('rake', '~> 13.0') s.add_development_dependency 'rake', '~> 11.3'
s.add_development_dependency('minitest') s.add_development_dependency 'minitest'
end end
+1 -3
View File
@@ -1,5 +1,3 @@
# frozen_string_literal: true
require 'benchmark/ips' require 'benchmark/ips'
require_relative 'theme_runner' require_relative 'theme_runner'
@@ -7,7 +5,7 @@ Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new profiler = ThemeRunner.new
Benchmark.ips do |x| Benchmark.ips do |x|
x.time = 10 x.time = 10
x.warmup = 5 x.warmup = 5
puts puts
+2 -2
View File
@@ -21,8 +21,8 @@ class Profiler
end end
def profile(phase, &block) def profile(phase, &block)
print(LOG_LABEL) print LOG_LABEL
print("#{phase}.. ".ljust(10)) print "#{phase}.. ".ljust(10)
report = MemoryProfiler.report(&block) report = MemoryProfiler.report(&block)
puts 'Done.' puts 'Done.'
@headings << phase.capitalize @headings << phase.capitalize
+1 -3
View File
@@ -1,5 +1,3 @@
# frozen_string_literal: true
require 'stackprof' require 'stackprof'
require_relative 'theme_runner' require_relative 'theme_runner'
@@ -15,7 +13,7 @@ profiler.run
end end
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| File.open(graph_filename, 'w') do |f|
StackProf::Report.new(results).print_graphviz(nil, f) StackProf::Report.new(results).print_graphviz(nil, f)
end end
+5 -7
View File
@@ -1,5 +1,3 @@
# frozen_string_literal: true
class CommentForm < Liquid::Block class CommentForm < Liquid::Block
Syntax = /(#{Liquid::VariableSignature}+)/ Syntax = /(#{Liquid::VariableSignature}+)/
@@ -7,10 +5,10 @@ class CommentForm < Liquid::Block
super super
if markup =~ Syntax if markup =~ Syntax
@variable_name = Regexp.last_match(1) @variable_name = $1
@attributes = {} @attributes = {}
else else
raise SyntaxError, "Syntax Error in 'comment_form' - Valid syntax: comment_form [article]" raise SyntaxError.new("Syntax Error in 'comment_form' - Valid syntax: comment_form [article]")
end end
end end
@@ -22,8 +20,8 @@ class CommentForm < Liquid::Block
'posted_successfully?' => context.registers[:posted_successfully], 'posted_successfully?' => context.registers[:posted_successfully],
'errors' => context['comment.errors'], 'errors' => context['comment.errors'],
'author' => context['comment.author'], 'author' => context['comment.author'],
'email' => context['comment.email'], 'email' => context['comment.email'],
'body' => context['comment.body'], 'body' => context['comment.body']
} }
output << wrap_in_form(article, render_all(@nodelist, context, output)) output << wrap_in_form(article, render_all(@nodelist, context, output))
+12 -18
View File
@@ -1,21 +1,11 @@
# frozen_string_literal: true
require 'yaml' require 'yaml'
module Database module Database
DATABASE_FILE_PATH = "#{__dir__}/vision.database.yml"
# Load the standard vision toolkit database and re-arrage it to be simply exportable # Load the standard vision toolkit database and re-arrage it to be simply exportable
# to liquid as assigns. All this is based on Shopify # to liquid as assigns. All this is based on Shopify
def self.tables def self.tables
@tables ||= begin @tables ||= begin
db = db = YAML.load_file("#{__dir__}/vision.database.yml")
if YAML.respond_to?(:unsafe_load_file) # Only Psych 4+ can use unsafe_load_file
# unsafe_load_file is needed for YAML references
YAML.unsafe_load_file(DATABASE_FILE_PATH)
else
YAML.load_file(DATABASE_FILE_PATH)
end
# From vision source # From vision source
db['products'].each do |product| db['products'].each do |product|
@@ -26,10 +16,9 @@ module Database
end end
# key the tables by handles, as this is how liquid expects it. # key the tables by handles, as this is how liquid expects it.
db = db.each_with_object({}) do |(key, values), assigns| db = db.inject({}) do |assigns, (key, values)|
assigns[key] = values.each_with_object({}) do |v, h| assigns[key] = values.inject({}) { |h, v| h[v['handle']] = v; h; }
h[v['handle']] = v assigns
end
end end
# Some standard direct accessors so that the specialized templates # Some standard direct accessors so that the specialized templates
@@ -40,12 +29,17 @@ module Database
db['article'] = db['blog']['articles'].first db['article'] = db['blog']['articles'].first
db['cart'] = { db['cart'] = {
'total_price' => db['line_items'].values.inject(0) { |sum, item| sum + item['line_price'] * item['quantity'] }, 'total_price' => db['line_items'].values.inject(0) { |sum, item| sum += item['line_price'] * item['quantity'] },
'item_count' => db['line_items'].values.inject(0) { |sum, item| sum + item['quantity'] }, 'item_count' => db['line_items'].values.inject(0) { |sum, item| sum += item['quantity'] },
'items' => db['line_items'].values, 'items' => db['line_items'].values
} }
db db
end end
end end
end end
if __FILE__ == $PROGRAM_NAME
p Database.tables['collections']['frontpage'].keys
# p Database.tables['blog']['articles']
end
+1 -3
View File
@@ -1,9 +1,7 @@
# frozen_string_literal: true
require 'json' require 'json'
module JsonFilter module JsonFilter
def json(object) def json(object)
JSON.dump(object.reject { |k, _v| k == "collections" }) JSON.dump(object.reject { |k, v| k == "collections" })
end end
end end
+8 -10
View File
@@ -1,6 +1,4 @@
# frozen_string_literal: true $:.unshift __dir__ + '/../../lib'
$LOAD_PATH.unshift(__dir__ + '/../../lib')
require_relative '../../lib/liquid' require_relative '../../lib/liquid'
require_relative 'comment_form' require_relative 'comment_form'
@@ -11,11 +9,11 @@ require_relative 'shop_filter'
require_relative 'tag_filter' require_relative 'tag_filter'
require_relative 'weight_filter' require_relative 'weight_filter'
Liquid::Template.register_tag('paginate', Paginate) Liquid::Template.register_tag 'paginate', Paginate
Liquid::Template.register_tag('form', CommentForm) Liquid::Template.register_tag 'form', CommentForm
Liquid::Template.register_filter(JsonFilter) Liquid::Template.register_filter JsonFilter
Liquid::Template.register_filter(MoneyFilter) Liquid::Template.register_filter MoneyFilter
Liquid::Template.register_filter(WeightFilter) Liquid::Template.register_filter WeightFilter
Liquid::Template.register_filter(ShopFilter) Liquid::Template.register_filter ShopFilter
Liquid::Template.register_filter(TagFilter) Liquid::Template.register_filter TagFilter
+2 -4
View File
@@ -1,14 +1,12 @@
# frozen_string_literal: true
module MoneyFilter module MoneyFilter
def money_with_currency(money) def money_with_currency(money)
return '' if money.nil? return '' if money.nil?
format("$ %.2f USD", money / 100.0) sprintf("$ %.2f USD", money / 100.0)
end end
def money(money) def money(money)
return '' if money.nil? return '' if money.nil?
format("$ %.2f", money / 100.0) sprintf("$ %.2f", money / 100.0)
end end
private private
+13 -15
View File
@@ -1,15 +1,13 @@
# frozen_string_literal: true
class Paginate < Liquid::Block class Paginate < Liquid::Block
Syntax = /(#{Liquid::QuotedFragment})\s*(by\s*(\d+))?/ Syntax = /(#{Liquid::QuotedFragment})\s*(by\s*(\d+))?/
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
if markup =~ Syntax if markup =~ Syntax
@collection_name = Regexp.last_match(1) @collection_name = $1
@page_size = if Regexp.last_match(2) @page_size = if $2
Regexp.last_match(3).to_i $3.to_i
else else
20 20
end end
@@ -19,7 +17,7 @@ class Paginate < Liquid::Block
@attributes[key] = value @attributes[key] = value
end end
else else
raise SyntaxError, "Syntax Error in tag 'paginate' - Valid syntax: paginate [collection] by number" raise SyntaxError.new("Syntax Error in tag 'paginate' - Valid syntax: paginate [collection] by number")
end end
end end
@@ -27,26 +25,26 @@ class Paginate < Liquid::Block
@context = context @context = context
context.stack do context.stack do
current_page = context['current_page'].to_i current_page = context['current_page'].to_i
pagination = { pagination = {
'page_size' => @page_size, 'page_size' => @page_size,
'current_page' => 5, 'current_page' => 5,
'current_offset' => @page_size * 5, 'current_offset' => @page_size * 5
} }
context['paginate'] = pagination context['paginate'] = pagination
collection_size = context[@collection_name].size collection_size = context[@collection_name].size
raise ArgumentError, "Cannot paginate array '#{@collection_name}'. Not found." if collection_size.nil? raise ArgumentError.new("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 page_count = (collection_size.to_f / @page_size.to_f).to_f.ceil + 1
pagination['items'] = collection_size pagination['items'] = collection_size
pagination['pages'] = page_count - 1 pagination['pages'] = page_count - 1
pagination['previous'] = link('&laquo; Previous', current_page - 1) if 1 < current_page pagination['previous'] = link('&laquo; Previous', current_page - 1) unless 1 >= current_page
pagination['next'] = link('Next &raquo;', current_page + 1) if page_count > current_page + 1 pagination['next'] = link('Next &raquo;', current_page + 1) unless page_count <= current_page + 1
pagination['parts'] = [] pagination['parts'] = []
hellip_break = false hellip_break = false
+12 -12
View File
@@ -1,5 +1,3 @@
# frozen_string_literal: true
module ShopFilter module ShopFilter
def asset_url(input) def asset_url(input)
"/files/1/[shop_id]/[shop_id]/assets/#{input}" "/files/1/[shop_id]/[shop_id]/assets/#{input}"
@@ -31,7 +29,7 @@ module ShopFilter
def link_to_vendor(vendor) def link_to_vendor(vendor)
if vendor if vendor
link_to(vendor, url_for_vendor(vendor), vendor) link_to vendor, url_for_vendor(vendor), vendor
else else
'Unknown Vendor' 'Unknown Vendor'
end end
@@ -39,7 +37,7 @@ module ShopFilter
def link_to_type(type) def link_to_type(type)
if type if type
link_to(type, url_for_type(type), type) link_to type, url_for_type(type), type
else else
'Unknown Vendor' 'Unknown Vendor'
end end
@@ -54,15 +52,15 @@ module ShopFilter
end end
def product_img_url(url, style = 'small') def product_img_url(url, style = 'small')
unless url =~ %r{\Aproducts/([\w\-\_]+)\.(\w{2,4})} unless url =~ /\Aproducts\/([\w\-\_]+)\.(\w{2,4})/
raise ArgumentError, 'filter "size" can only be called on product images' raise ArgumentError, 'filter "size" can only be called on product images'
end end
case style case style
when 'original' when 'original'
'/files/shops/random_number/' + url return '/files/shops/random_number/' + url
when 'grande', 'large', 'medium', 'compact', 'small', 'thumb', 'icon' when 'grande', 'large', 'medium', 'compact', 'small', 'thumb', 'icon'
"/files/shops/random_number/products/#{Regexp.last_match(1)}_#{style}.#{Regexp.last_match(2)}" "/files/shops/random_number/products/#{$1}_#{style}.#{$2}"
else else
raise ArgumentError, 'valid parameters for filter "size" are: original, grande, large, medium, compact, small, thumb and icon ' raise ArgumentError, 'valid parameters for filter "size" are: original, grande, large, medium, compact, small, thumb and icon '
end end
@@ -72,14 +70,16 @@ module ShopFilter
html = [] html = []
html << %(<span class="prev">#{link_to(paginate['previous']['title'], paginate['previous']['url'])}</span>) if paginate['previous'] html << %(<span class="prev">#{link_to(paginate['previous']['title'], paginate['previous']['url'])}</span>) if paginate['previous']
paginate['parts'].each do |part| for part in paginate['parts']
html << if part['is_link']
%(<span class="page">#{link_to(part['title'], part['url'])}</span>) if part['is_link']
html << %(<span class="page">#{link_to(part['title'], part['url'])}</span>)
elsif part['title'].to_i == paginate['current_page'].to_i elsif part['title'].to_i == paginate['current_page'].to_i
%(<span class="page current">#{part['title']}</span>) html << %(<span class="page current">#{part['title']}</span>)
else else
%(<span class="deco">#{part['title']}</span>) html << %(<span class="deco">#{part['title']}</span>)
end end
end end
html << %(<span class="next">#{link_to(paginate['next']['title'], paginate['next']['url'])}</span>) if paginate['next'] html << %(<span class="next">#{link_to(paginate['next']['title'], paginate['next']['url'])}</span>) if paginate['next']
+2 -4
View File
@@ -1,5 +1,3 @@
# frozen_string_literal: true
module TagFilter module TagFilter
def link_to_tag(label, tag) def link_to_tag(label, tag)
"<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tag}\">#{label}</a>" "<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tag}\">#{label}</a>"
@@ -15,11 +13,11 @@ module TagFilter
def link_to_add_tag(label, tag) def link_to_add_tag(label, tag)
tags = (@context['current_tags'] + [tag]).uniq 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
def link_to_remove_tag(label, tag) def link_to_remove_tag(label, tag)
tags = (@context['current_tags'] - [tag]).uniq 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
end end
+1 -3
View File
@@ -1,8 +1,6 @@
# frozen_string_literal: true
module WeightFilter module WeightFilter
def weight(grams) def weight(grams)
format("%.2f", grams / 1000) sprintf("%.2f", grams / 1000)
end end
def weight_with_unit(grams) def weight_with_unit(grams)
+10 -16
View File
@@ -1,9 +1,7 @@
# frozen_string_literal: true
# This profiler run simulates Shopify. # This profiler run simulates Shopify.
# We are looking in the tests directory for liquid files and render them within the designated layout file. # 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. # We will also export a substantial database to liquid which the templates can render values of.
# All this is to make the benchmark as non synthetic as possible. All templates and tests are lifted from # All this is to make the benchmark as non syntetic as possible. All templates and tests are lifted from
# direct real-world usage and the profiler measures code that looks very similar to the way it looks in # direct real-world usage and the profiler measures code that looks very similar to the way it looks in
# Shopify which is likely the biggest user of liquid in the world which something to the tune of several # Shopify which is likely the biggest user of liquid in the world which something to the tune of several
# million Template#render calls a day. # million Template#render calls a day.
@@ -33,7 +31,7 @@ class ThemeRunner
{ {
liquid: File.read(test), liquid: File.read(test),
layout: (File.file?(theme_path) ? File.read(theme_path) : nil), layout: (File.file?(theme_path) ? File.read(theme_path) : nil),
template_name: test, template_name: test
} }
end.compact end.compact
@@ -58,9 +56,9 @@ class ThemeRunner
# `render` is called to benchmark just the render portion of liquid # `render` is called to benchmark just the render portion of liquid
def render def render
@compiled_tests.each do |test| @compiled_tests.each do |test|
tmpl = test[:tmpl] tmpl = test[:tmpl]
assigns = test[:assigns] assigns = test[:assigns]
layout = test[:layout] layout = test[:layout]
if layout if layout
assigns['content_for_layout'] = tmpl.render!(assigns) assigns['content_for_layout'] = tmpl.render!(assigns)
@@ -73,14 +71,10 @@ class ThemeRunner
private private
def render_layout(template, layout, assigns)
assigns['content_for_layout'] = template.render!(assigns)
layout&.render!(assigns)
end
def compile_and_render(template, layout, assigns, page_template, template_file) def compile_and_render(template, layout, assigns, page_template, template_file)
compiled_test = compile_test(template, layout, assigns, page_template, template_file) compiled_test = compile_test(template, layout, assigns, page_template, template_file)
render_layout(compiled_test[:tmpl], compiled_test[:layout], compiled_test[:assigns]) assigns['content_for_layout'] = compiled_test[:tmpl].render!(assigns)
compiled_test[:layout].render!(assigns) if layout
end end
def compile_all_tests def compile_all_tests
@@ -92,7 +86,7 @@ class ThemeRunner
end end
def compile_test(template, layout, assigns, page_template, template_file) def compile_test(template, layout, assigns, page_template, template_file)
tmpl = init_template(page_template, template_file) tmpl = init_template(page_template, template_file)
parsed_template = tmpl.parse(template).dup parsed_template = tmpl.parse(template).dup
if layout if layout
@@ -117,9 +111,9 @@ class ThemeRunner
# set up a new Liquid::Template object for use in `compile_and_render` and `compile_test` # set up a new Liquid::Template object for use in `compile_and_render` and `compile_test`
def init_template(page_template, template_file) def init_template(page_template, template_file)
tmpl = Liquid::Template.new tmpl = Liquid::Template.new
tmpl.assigns['page_title'] = 'Page title' tmpl.assigns['page_title'] = 'Page title'
tmpl.assigns['template'] = page_template tmpl.assigns['template'] = page_template
tmpl.registers[:file_system] = ThemeRunner::FileSystem.new(File.dirname(template_file)) tmpl.registers[:file_system] = ThemeRunner::FileSystem.new(File.dirname(template_file))
tmpl tmpl
end end
+22 -91
View File
@@ -1,117 +1,48 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class AssignTest < Minitest::Test class AssignTest < Minitest::Test
include Liquid include Liquid
def test_assign_with_hyphen_in_variable_name def test_assign_with_hyphen_in_variable_name
template_source = <<~END_TEMPLATE template_source = <<-END_TEMPLATE
{% assign this-thing = 'Print this-thing' -%} {% assign this-thing = 'Print this-thing' %}
{{ this-thing -}} {{ this-thing }}
END_TEMPLATE END_TEMPLATE
assert_template_result("Print this-thing", template_source) template = Template.parse(template_source)
rendered = template.render!
assert_equal "Print this-thing", rendered.strip
end end
def test_assigned_variable def test_assigned_variable
assert_template_result( assert_template_result('.foo.',
'.foo.',
'{% assign foo = values %}.{{ foo[0] }}.', '{% assign foo = values %}.{{ foo[0] }}.',
{ 'values' => %w(foo bar baz) }, 'values' => %w(foo bar baz))
)
assert_template_result( assert_template_result('.bar.',
'.bar.',
'{% assign foo = values %}.{{ foo[1] }}.', '{% assign foo = values %}.{{ foo[1] }}.',
{ 'values' => %w(foo bar baz) }, 'values' => %w(foo bar baz))
)
end end
def test_assign_with_filter def test_assign_with_filter
assert_template_result( assert_template_result('.bar.',
'.bar.',
'{% assign foo = values | split: "," %}.{{ foo[1] }}.', '{% assign foo = values | split: "," %}.{{ foo[1] }}.',
{ 'values' => "foo,bar,baz" }, 'values' => "foo,bar,baz")
)
end end
def test_assign_syntax_error def test_assign_syntax_error
assert_match_syntax_error(/assign/, '{% assign foo not values %}.') assert_match_syntax_error(/assign/,
'{% assign foo not values %}.',
'values' => "foo,bar,baz")
end end
def test_assign_uses_error_mode def test_assign_uses_error_mode
assert_match_syntax_error( with_error_mode(:strict) do
"Expected dotdot but found pipe in ", assert_raises(SyntaxError) do
"{% assign foo = ('X' | downcase) %}", Template.parse("{% assign foo = ('X' | downcase) %}")
error_mode: :strict, end
)
assert_template_result("", "{% assign foo = ('X' | downcase) %}", error_mode: :lax)
end
def test_expression_with_whitespace_in_square_brackets
source = "{% assign r = a[ 'b' ] %}{{ r }}"
assert_template_result('result', source, { 'a' => { 'b' => 'result' } })
end
def test_assign_score_exceeding_resource_limit
t = Template.parse("{% assign foo = 42 %}{% assign bar = 23 %}")
t.resource_limits.assign_score_limit = 1
assert_equal("Liquid error: Memory limits exceeded", t.render)
assert(t.resource_limits.reached?)
t.resource_limits.assign_score_limit = 2
assert_equal("", t.render!)
refute_nil(t.resource_limits.assign_score)
end
def test_assign_score_exceeding_limit_from_composite_object
t = Template.parse("{% assign foo = 'aaaa' | reverse %}")
t.resource_limits.assign_score_limit = 3
assert_equal("Liquid error: Memory limits exceeded", t.render)
assert(t.resource_limits.reached?)
t.resource_limits.assign_score_limit = 5
assert_equal("", t.render!)
end
def test_assign_score_of_int
assert_equal(1, assign_score_of(123))
end
def test_assign_score_of_string_counts_bytes
assert_equal(3, assign_score_of('123'))
assert_equal(5, assign_score_of('12345'))
assert_equal(9, assign_score_of('すごい'))
end
def test_assign_score_of_array
assert_equal(1, assign_score_of([]))
assert_equal(2, assign_score_of([123]))
assert_equal(6, assign_score_of([123, 'abcd']))
end
def test_assign_score_of_hash
assert_equal(1, assign_score_of({}))
assert_equal(5, assign_score_of('int' => 123))
assert_equal(12, assign_score_of('int' => 123, 'str' => 'abcd'))
end
private
class ObjectWrapperDrop < Liquid::Drop
def initialize(obj)
@obj = obj
end end
with_error_mode(:lax) do
def value assert Template.parse("{% assign foo = ('X' | downcase) %}")
@obj
end end
end end
end # AssignTest
def assign_score_of(obj)
context = Liquid::Context.new('drop' => ObjectWrapperDrop.new(obj))
Liquid::Template.parse('{% assign obj = drop.value %}').render!(context)
context.resource_limits.assign_score
end
end
+14 -23
View File
@@ -1,14 +1,18 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class FoobarTag < Liquid::Tag class FoobarTag < Liquid::Tag
def render_to_output_buffer(_context, output) def render_to_output_buffer(context, output)
output << ' ' output << ' '
output output
end end
end end
class BlankTestFileSystem
def read_template_file(template_path)
template_path
end
end
class BlankTest < Minitest::Test class BlankTest < Minitest::Test
include Liquid include Liquid
N = 10 N = 10
@@ -27,7 +31,7 @@ class BlankTest < Minitest::Test
def test_new_tags_are_not_blank_by_default def test_new_tags_are_not_blank_by_default
with_custom_tag('foobar', FoobarTag) do with_custom_tag('foobar', FoobarTag) do
assert_equal(" " * N, Liquid::Template.parse(wrap_in_for("{% foobar %}")).render!) assert_template_result(" " * N, wrap_in_for("{% foobar %}"))
end end
end end
@@ -57,11 +61,9 @@ class BlankTest < Minitest::Test
def test_nested_blocks_are_blank_but_only_if_all_children_are def test_nested_blocks_are_blank_but_only_if_all_children_are
assert_template_result("", wrap(wrap(" "))) assert_template_result("", wrap(wrap(" ")))
assert_template_result( assert_template_result("\n but this is not " * (N + 1),
"\n but this is not " * (N + 1),
wrap('{% if true %} {% comment %} this is blank {% endcomment %} {% endif %} wrap('{% if true %} {% comment %} this is blank {% endcomment %} {% endif %}
{% if true %} but this is not {% endif %}'), {% if true %} but this is not {% endif %}'))
)
end end
def test_assigns_are_blank def test_assigns_are_blank
@@ -91,21 +93,10 @@ class BlankTest < Minitest::Test
end end
def test_include_is_blank def test_include_is_blank
assert_template_result( Liquid::Template.file_system = BlankTestFileSystem.new
"foobar" * (N + 1), assert_template_result "foobar" * (N + 1), wrap("{% include 'foobar' %}")
wrap("{% include 'foobar' %}"), assert_template_result " foobar " * (N + 1), wrap("{% include ' foobar ' %}")
partials: { 'foobar' => 'foobar' }, assert_template_result " " * (N + 1), wrap(" {% include ' ' %} ")
)
assert_template_result(
" foobar " * (N + 1),
wrap("{% include ' foobar ' %}"),
partials: { ' foobar ' => ' foobar ' },
)
assert_template_result(
" " * (N + 1),
wrap(" {% include ' ' %} "),
partials: { ' ' => ' ' },
)
end end
def test_case_is_blank def test_case_is_blank
+3 -47
View File
@@ -1,56 +1,12 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class BlockTest < Minitest::Test class BlockTest < Minitest::Test
include Liquid include Liquid
def test_unexpected_end_tag def test_unexpected_end_tag
source = '{% if true %}{% endunless %}' exc = assert_raises(SyntaxError) do
assert_match_syntax_error("Liquid syntax error (line 1): 'endunless' is not a valid delimiter for if tags. use endif", source) Template.parse("{% if true %}{% endunless %}")
end
def test_with_custom_tag
with_custom_tag('testtag', Block) do
assert(Liquid::Template.parse("{% testtag %} {% endtesttag %}"))
end
end
def test_custom_block_tags_have_a_default_render_to_output_buffer_method_for_backwards_compatibility
klass1 = Class.new(Block) do
def render(*)
'hello'
end
end
with_custom_tag('blabla', klass1) do
template = Liquid::Template.parse("{% blabla %} bla {% endblabla %}")
assert_equal('hello', template.render)
buf = +''
output = template.render({}, output: buf)
assert_equal('hello', output)
assert_equal('hello', buf)
assert_equal(buf.object_id, output.object_id)
end
klass2 = Class.new(klass1) do
def render(*)
'foo' + super + 'bar'
end
end
with_custom_tag('blabla', klass2) do
template = Liquid::Template.parse("{% blabla %} foo {% endblabla %}")
assert_equal('foohellobar', template.render)
buf = +''
output = template.render({}, output: buf)
assert_equal('foohellobar', output)
assert_equal('foohellobar', buf)
assert_equal(buf.object_id, output.object_id)
end end
assert_equal exc.message, "Liquid syntax error: 'endunless' is not a valid delimiter for if tags. use endif"
end end
end end
+30 -32
View File
@@ -1,5 +1,3 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class CaptureTest < Minitest::Test class CaptureTest < Minitest::Test
@@ -10,43 +8,43 @@ class CaptureTest < Minitest::Test
end end
def test_capture_with_hyphen_in_variable_name def test_capture_with_hyphen_in_variable_name
template_source = <<~END_TEMPLATE template_source = <<-END_TEMPLATE
{% capture this-thing %}Print this-thing{% endcapture -%} {% capture this-thing %}Print this-thing{% endcapture %}
{{ this-thing -}} {{ this-thing }}
END_TEMPLATE END_TEMPLATE
assert_template_result("Print this-thing", template_source) template = Template.parse(template_source)
rendered = template.render!
assert_equal "Print this-thing", rendered.strip
end end
def test_capture_to_variable_from_outer_scope_if_existing def test_capture_to_variable_from_outer_scope_if_existing
template_source = <<~END_TEMPLATE template_source = <<-END_TEMPLATE
{% assign var = '' -%} {% assign var = '' %}
{% if true -%} {% if true %}
{% capture var %}first-block-string{% endcapture -%} {% capture var %}first-block-string{% endcapture %}
{% endif -%} {% endif %}
{% if true -%} {% if true %}
{% capture var %}test-string{% endcapture -%} {% capture var %}test-string{% endcapture %}
{% endif -%} {% endif %}
{{var-}} {{var}}
END_TEMPLATE END_TEMPLATE
assert_template_result("test-string", template_source) template = Template.parse(template_source)
rendered = template.render!
assert_equal "test-string", rendered.gsub(/\s/, '')
end end
def test_assigning_from_capture def test_assigning_from_capture
template_source = <<~END_TEMPLATE template_source = <<-END_TEMPLATE
{% assign first = '' -%} {% assign first = '' %}
{% assign second = '' -%} {% assign second = '' %}
{% for number in (1..3) -%} {% for number in (1..3) %}
{% capture first %}{{number}}{% endcapture -%} {% capture first %}{{number}}{% endcapture %}
{% assign second = first -%} {% assign second = first %}
{% endfor -%} {% endfor %}
{{ first }}-{{ second -}} {{ first }}-{{ second }}
END_TEMPLATE END_TEMPLATE
assert_template_result("3-3", template_source) template = Template.parse(template_source)
rendered = template.render!
assert_equal "3-3", rendered.gsub(/\s/, '')
end end
end # CaptureTest
def test_increment_assign_score_by_bytes_not_characters
t = Template.parse("{% capture foo %}すごい{% endcapture %}")
t.render!
assert_equal(9, t.resource_limits.assign_score)
end
end
+5 -624
View File
@@ -1,599 +1,8 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class HundredCentes
def to_liquid
100
end
end
class CentsDrop < Liquid::Drop
def amount
HundredCentes.new
end
def non_zero?
true
end
end
class ContextSensitiveDrop < Liquid::Drop
def test
@context['test']
end
end
class Category
attr_accessor :name
def initialize(name)
@name = name
end
def to_liquid
CategoryDrop.new(self)
end
end
class CategoryDrop < Liquid::Drop
attr_accessor :category, :context
def initialize(category)
@category = category
end
end
class CounterDrop < Liquid::Drop
def count
@count ||= 0
@count += 1
end
end
class ArrayLike
def fetch(index)
end
def [](index)
@counts ||= []
@counts[index] ||= 0
@counts[index] += 1
end
def to_liquid
self
end
end
class ContextTest < Minitest::Test class ContextTest < Minitest::Test
include Liquid include Liquid
def setup
@context = Liquid::Context.new
end
def test_variables
@context['string'] = 'string'
assert_equal('string', @context['string'])
@context['num'] = 5
assert_equal(5, @context['num'])
@context['time'] = Time.parse('2006-06-06 12:00:00')
assert_equal(Time.parse('2006-06-06 12:00:00'), @context['time'])
@context['date'] = Date.today
assert_equal(Date.today, @context['date'])
now = Time.now
@context['datetime'] = now
assert_equal(now, @context['datetime'])
@context['bool'] = true
assert_equal(true, @context['bool'])
@context['bool'] = false
assert_equal(false, @context['bool'])
@context['nil'] = nil
assert_nil(@context['nil'])
assert_nil(@context['nil'])
end
def test_variables_not_existing
assert_template_result("true", "{% if does_not_exist == nil %}true{% endif %}")
end
def test_scoping
@context.push
@context.pop
assert_raises(Liquid::ContextError) do
@context.pop
end
assert_raises(Liquid::ContextError) do
@context.push
@context.pop
@context.pop
end
end
def test_length_query
assert_template_result(
"true",
"{% if numbers.size == 4 %}true{% endif %}",
{ "numbers" => [1, 2, 3, 4] },
)
assert_template_result(
"true",
"{% if numbers.size == 4 %}true{% endif %}",
{ "numbers" => { 1 => 1, 2 => 2, 3 => 3, 4 => 4 } },
)
assert_template_result(
"true",
"{% if numbers.size == 1000 %}true{% endif %}",
{ "numbers" => { 1 => 1, 2 => 2, 3 => 3, 4 => 4, 'size' => 1000 } },
)
end
def test_hyphenated_variable
assert_template_result("godz", "{{ oh-my }}", { "oh-my" => 'godz' })
end
def test_add_filter
filter = Module.new do
def hi(output)
output + ' hi!'
end
end
context = Context.new
context.add_filters(filter)
assert_equal('hi? hi!', context.invoke(:hi, 'hi?'))
context = Context.new
assert_equal('hi?', context.invoke(:hi, 'hi?'))
context.add_filters(filter)
assert_equal('hi? hi!', context.invoke(:hi, 'hi?'))
end
def test_only_intended_filters_make_it_there
filter = Module.new do
def hi(output)
output + ' hi!'
end
end
context = Context.new
assert_equal("Wookie", context.invoke("hi", "Wookie"))
context.add_filters(filter)
assert_equal("Wookie hi!", context.invoke("hi", "Wookie"))
end
def test_add_item_in_outer_scope
@context['test'] = 'test'
@context.push
assert_equal('test', @context['test'])
@context.pop
assert_equal('test', @context['test'])
end
def test_add_item_in_inner_scope
@context.push
@context['test'] = 'test'
assert_equal('test', @context['test'])
@context.pop
assert_nil(@context['test'])
end
def test_hierachical_data
assigns = { 'hash' => { "name" => 'tobi' } }
assert_template_result("tobi", "{{ hash.name }}", assigns)
assert_template_result("tobi", '{{ hash["name"] }}', assigns)
end
def test_keywords
assert_template_result("pass", "{% if true == expect %}pass{% endif %}", { "expect" => true })
assert_template_result("pass", "{% if false == expect %}pass{% endif %}", { "expect" => false })
end
def test_digits
assert_template_result("pass", "{% if 100 == expect %}pass{% endif %}", { "expect" => 100 })
assert_template_result("pass", "{% if 100.00 == expect %}pass{% endif %}", { "expect" => 100.00 })
end
def test_strings
assert_template_result("hello!", '{{ "hello!" }}')
assert_template_result("hello!", "{{ 'hello!' }}")
end
def test_merge
@context.merge("test" => "test")
assert_equal('test', @context['test'])
@context.merge("test" => "newvalue", "foo" => "bar")
assert_equal('newvalue', @context['test'])
assert_equal('bar', @context['foo'])
end
def test_array_notation
assigns = { "test" => ["a", "b"] }
assert_template_result("a", "{{ test[0] }}", assigns)
assert_template_result("b", "{{ test[1] }}", assigns)
assert_template_result("pass", "{% if test[2] == nil %}pass{% endif %}", assigns)
end
def test_recoursive_array_notation
assigns = { "test" => { 'test' => [1, 2, 3, 4, 5] } }
assert_template_result("1", "{{ test.test[0] }}", assigns)
assigns = { "test" => [{ 'test' => 'worked' }] }
assert_template_result("worked", "{{ test[0].test }}", assigns)
end
def test_hash_to_array_transition
assigns = {
'colors' => {
'Blue' => ['003366', '336699', '6699CC', '99CCFF'],
'Green' => ['003300', '336633', '669966', '99CC99'],
'Yellow' => ['CC9900', 'FFCC00', 'FFFF99', 'FFFFCC'],
'Red' => ['660000', '993333', 'CC6666', 'FF9999'],
},
}
assert_template_result("003366", "{{ colors.Blue[0] }}", assigns)
assert_template_result("FF9999", "{{ colors.Red[3] }}", assigns)
end
def test_try_first
assigns = { 'test' => [1, 2, 3, 4, 5] }
assert_template_result("1", "{{ test.first }}", assigns)
assert_template_result("pass", "{% if test.last == 5 %}pass{% endif %}", assigns)
assigns = { "test" => { "test" => [1, 2, 3, 4, 5] } }
assert_template_result("1", "{{ test.test.first }}", assigns)
assert_template_result("5", "{{ test.test.last }}", assigns)
assigns = { "test" => [1] }
assert_template_result("1", "{{ test.first }}", assigns)
assert_template_result("1", "{{ test.last }}", assigns)
end
def test_access_hashes_with_hash_notation
assigns = { 'products' => { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] } }
assert_template_result("5", '{{ products["count"] }}', assigns)
assert_template_result("deepsnow", '{{ products["tags"][0] }}', assigns)
assert_template_result("deepsnow", '{{ products["tags"].first }}', assigns)
assigns = { 'product' => { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] } }
assert_template_result("draft151cm", '{{ product["variants"][0]["title"] }}', assigns)
assert_template_result("element151cm", '{{ product["variants"][1]["title"] }}', assigns)
assert_template_result("draft151cm", '{{ product["variants"].first["title"] }}', assigns)
assert_template_result("element151cm", '{{ product["variants"].last["title"] }}', assigns)
end
def test_access_variable_with_hash_notation
assert_template_result('baz', '{{ ["foo"] }}', { "foo" => "baz" })
assert_template_result('baz', '{{ [bar] }}', { 'foo' => 'baz', 'bar' => 'foo' })
end
def test_access_hashes_with_hash_access_variables
assigns = {
'var' => 'tags',
'nested' => { 'var' => 'tags' },
'products' => { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] },
}
assert_template_result('deepsnow', '{{ products[var].first }}', assigns)
assert_template_result('freestyle', '{{ products[nested.var].last }}', assigns)
end
def test_hash_notation_only_for_hash_access
assigns = { "array" => [1, 2, 3, 4, 5] }
assert_template_result("1", "{{ array.first }}", assigns)
assert_template_result("pass", '{% if array["first"] == nil %}pass{% endif %}', assigns)
assert_template_result("Hello", '{{ hash["first"] }}', { "hash" => { "first" => "Hello" } })
end
def test_first_can_appear_in_middle_of_callchain
assigns = { "product" => { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] } }
assert_template_result('draft151cm', '{{ product.variants[0].title }}', assigns)
assert_template_result('element151cm', '{{ product.variants[1].title }}', assigns)
assert_template_result('draft151cm', '{{ product.variants.first.title }}', assigns)
assert_template_result('element151cm', '{{ product.variants.last.title }}', assigns)
end
def test_cents
@context.merge("cents" => HundredCentes.new)
assert_equal(100, @context['cents'])
end
def test_nested_cents
@context.merge("cents" => { 'amount' => HundredCentes.new })
assert_equal(100, @context['cents.amount'])
@context.merge("cents" => { 'cents' => { 'amount' => HundredCentes.new } })
assert_equal(100, @context['cents.cents.amount'])
end
def test_cents_through_drop
@context.merge("cents" => CentsDrop.new)
assert_equal(100, @context['cents.amount'])
end
def test_nested_cents_through_drop
@context.merge("vars" => { "cents" => CentsDrop.new })
assert_equal(100, @context['vars.cents.amount'])
end
def test_drop_methods_with_question_marks
@context.merge("cents" => CentsDrop.new)
assert(@context['cents.non_zero?'])
end
def test_context_from_within_drop
@context.merge("test" => '123', "vars" => ContextSensitiveDrop.new)
assert_equal('123', @context['vars.test'])
end
def test_nested_context_from_within_drop
@context.merge("test" => '123', "vars" => { "local" => ContextSensitiveDrop.new })
assert_equal('123', @context['vars.local.test'])
end
def test_ranges
assert_template_result("1..5", '{{ (1..5) }}')
assert_template_result("pass", '{% if (1..5) == expect %}pass{% endif %}', { "expect" => (1..5) })
assigns = { "test" => '5' }
assert_template_result("1..5", "{{ (1..test) }}", assigns)
assert_template_result("5..5", "{{ (test..test) }}", assigns)
end
def test_cents_through_drop_nestedly
@context.merge("cents" => { "cents" => CentsDrop.new })
assert_equal(100, @context['cents.cents.amount'])
@context.merge("cents" => { "cents" => { "cents" => CentsDrop.new } })
assert_equal(100, @context['cents.cents.cents.amount'])
end
def test_drop_with_variable_called_only_once
@context['counter'] = CounterDrop.new
assert_equal(1, @context['counter.count'])
assert_equal(2, @context['counter.count'])
assert_equal(3, @context['counter.count'])
end
def test_drop_with_key_called_only_once
@context['counter'] = CounterDrop.new
assert_equal(1, @context['counter["count"]'])
assert_equal(2, @context['counter["count"]'])
assert_equal(3, @context['counter["count"]'])
end
def test_proc_as_variable
@context['dynamic'] = proc { 'Hello' }
assert_equal('Hello', @context['dynamic'])
end
def test_lambda_as_variable
@context['dynamic'] = proc { 'Hello' }
assert_equal('Hello', @context['dynamic'])
end
def test_nested_lambda_as_variable
@context['dynamic'] = { "lambda" => proc { 'Hello' } }
assert_equal('Hello', @context['dynamic.lambda'])
end
def test_array_containing_lambda_as_variable
@context['dynamic'] = [1, 2, proc { 'Hello' }, 4, 5]
assert_equal('Hello', @context['dynamic[2]'])
end
def test_lambda_is_called_once
@global = 0
@context['callcount'] = proc {
@global += 1
@global.to_s
}
assert_equal('1', @context['callcount'])
assert_equal('1', @context['callcount'])
assert_equal('1', @context['callcount'])
end
def test_nested_lambda_is_called_once
@global = 0
@context['callcount'] = {
"lambda" => proc {
@global += 1
@global.to_s
},
}
assert_equal('1', @context['callcount.lambda'])
assert_equal('1', @context['callcount.lambda'])
assert_equal('1', @context['callcount.lambda'])
end
def test_lambda_in_array_is_called_once
@global = 0
p = proc {
@global += 1
@global.to_s
}
@context['callcount'] = [1, 2, p, 4, 5]
assert_equal('1', @context['callcount[2]'])
assert_equal('1', @context['callcount[2]'])
assert_equal('1', @context['callcount[2]'])
end
def test_access_to_context_from_proc
@context.registers[:magic] = 345392
@context['magic'] = proc { @context.registers[:magic] }
assert_equal(345392, @context['magic'])
end
def test_to_liquid_and_context_at_first_level
@context['category'] = Category.new("foobar")
assert_kind_of(CategoryDrop, @context['category'])
assert_equal(@context, @context['category'].context)
end
def test_interrupt_avoids_object_allocations
@context.interrupt? # ruby 3.0.0 allocates on the first call
assert_no_object_allocations do
@context.interrupt?
end
end
def test_context_initialization_with_a_proc_in_environment
contx = Context.new([test: ->(c) { c['poutine'] }], test: :foo)
assert(contx)
assert_nil(contx['poutine'])
end
def test_apply_global_filter
global_filter_proc = ->(output) { "#{output} filtered" }
context = Context.new
context.global_filter = global_filter_proc
assert_equal('hi filtered', context.apply_global_filter('hi'))
end
def test_static_environments_are_read_with_lower_priority_than_environments
context = Context.build(
static_environments: { 'shadowed' => 'static', 'unshadowed' => 'static' },
environments: { 'shadowed' => 'dynamic' },
)
assert_equal('dynamic', context['shadowed'])
assert_equal('static', context['unshadowed'])
end
def test_apply_global_filter_when_no_global_filter_exist
context = Context.new
assert_equal('hi', context.apply_global_filter('hi'))
end
def test_new_isolated_subcontext_does_not_inherit_variables
super_context = Context.new
super_context['my_variable'] = 'some value'
subcontext = super_context.new_isolated_subcontext
assert_nil(subcontext['my_variable'])
end
def test_new_isolated_subcontext_inherits_static_environment
super_context = Context.build(static_environments: { 'my_environment_value' => 'my value' })
subcontext = super_context.new_isolated_subcontext
assert_equal('my value', subcontext['my_environment_value'])
end
def test_new_isolated_subcontext_inherits_resource_limits
resource_limits = ResourceLimits.new({})
super_context = Context.new({}, {}, {}, false, resource_limits)
subcontext = super_context.new_isolated_subcontext
assert_equal(resource_limits, subcontext.resource_limits)
end
def test_new_isolated_subcontext_inherits_exception_renderer
super_context = Context.new
super_context.exception_renderer = ->(_e) { 'my exception message' }
subcontext = super_context.new_isolated_subcontext
assert_equal('my exception message', subcontext.handle_error(Liquid::Error.new))
end
def test_new_isolated_subcontext_does_not_inherit_non_static_registers
registers = {
my_register: :my_value,
}
super_context = Context.new({}, {}, Registers.new(registers))
super_context.registers[:my_register] = :my_alt_value
subcontext = super_context.new_isolated_subcontext
assert_equal(:my_value, subcontext.registers[:my_register])
end
def test_new_isolated_subcontext_inherits_static_registers
super_context = Context.build(registers: { my_register: :my_value })
subcontext = super_context.new_isolated_subcontext
assert_equal(:my_value, subcontext.registers[:my_register])
end
def test_new_isolated_subcontext_registers_do_not_pollute_context
super_context = Context.build(registers: { my_register: :my_value })
subcontext = super_context.new_isolated_subcontext
subcontext.registers[:my_register] = :my_alt_value
assert_equal(:my_value, super_context.registers[:my_register])
end
def test_new_isolated_subcontext_inherits_filters
my_filter = Module.new do
def my_filter(*)
'my filter result'
end
end
super_context = Context.new
super_context.add_filters([my_filter])
subcontext = super_context.new_isolated_subcontext
template = Template.parse('{{ 123 | my_filter }}')
assert_equal('my filter result', template.render(subcontext))
end
def test_disables_tag_specified
context = Context.new
context.with_disabled_tags(%w(foo bar)) do
assert_equal(true, context.tag_disabled?("foo"))
assert_equal(true, context.tag_disabled?("bar"))
assert_equal(false, context.tag_disabled?("unknown"))
end
end
def test_disables_nested_tags
context = Context.new
context.with_disabled_tags(["foo"]) do
context.with_disabled_tags(["foo"]) do
assert_equal(true, context.tag_disabled?("foo"))
assert_equal(false, context.tag_disabled?("bar"))
end
context.with_disabled_tags(["bar"]) do
assert_equal(true, context.tag_disabled?("foo"))
assert_equal(true, context.tag_disabled?("bar"))
context.with_disabled_tags(["foo"]) do
assert_equal(true, context.tag_disabled?("foo"))
assert_equal(true, context.tag_disabled?("bar"))
end
end
assert_equal(true, context.tag_disabled?("foo"))
assert_equal(false, context.tag_disabled?("bar"))
end
end
def test_override_global_filter def test_override_global_filter
global = Module.new do global = Module.new do
def notice(output) def notice(output)
@@ -608,44 +17,16 @@ class ContextTest < Minitest::Test
end end
with_global_filter(global) do with_global_filter(global) do
assert_equal('Global test', Template.parse("{{'test' | notice }}").render!) assert_equal 'Global test', Template.parse("{{'test' | notice }}").render!
assert_equal('Local test', Template.parse("{{'test' | notice }}").render!({}, filters: [local])) assert_equal 'Local test', Template.parse("{{'test' | notice }}").render!({}, filters: [local])
end end
end end
def test_has_key_will_not_add_an_error_for_missing_keys def test_has_key_will_not_add_an_error_for_missing_keys
with_error_mode(:strict) do with_error_mode :strict do
context = Context.new context = Context.new
context.key?('unknown') context.key?('unknown')
assert_empty(context.errors) assert_empty context.errors
end end
end end
end
def test_context_always_uses_static_registers
registers = {
my_register: :my_value,
}
c = Context.new({}, {}, registers)
assert_instance_of(Registers, c.registers)
assert_equal(:my_value, c.registers[:my_register])
r = Registers.new(registers)
c = Context.new({}, {}, r)
assert_instance_of(Registers, c.registers)
assert_equal(:my_value, c.registers[:my_register])
end
private
def assert_no_object_allocations
unless RUBY_ENGINE == 'ruby'
skip("stackprof needed to count object allocations")
end
require 'stackprof'
profile = StackProf.run(mode: :object) do
yield
end
assert_equal(0, profile[:samples])
end
end # ContextTest
+8 -6
View File
@@ -1,17 +1,19 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class DocumentTest < Minitest::Test class DocumentTest < Minitest::Test
include Liquid include Liquid
def test_unexpected_outer_tag def test_unexpected_outer_tag
source = "{% else %}" exc = assert_raises(SyntaxError) do
assert_match_syntax_error("Liquid syntax error (line 1): Unexpected outer 'else' tag", source) Template.parse("{% else %}")
end
assert_equal exc.message, "Liquid syntax error: Unexpected outer 'else' tag"
end end
def test_unknown_tag def test_unknown_tag
source = "{% foo %}" exc = assert_raises(SyntaxError) do
assert_match_syntax_error("Liquid syntax error (line 1): Unknown tag 'foo'", source) Template.parse("{% foo %}")
end
assert_equal exc.message, "Liquid syntax error: Unknown tag 'foo'"
end end
end end
+83 -67
View File
@@ -1,5 +1,3 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class ContextDrop < Liquid::Drop class ContextDrop < Liquid::Drop
@@ -33,7 +31,7 @@ class ProductDrop < Liquid::Drop
class CatchallDrop < Liquid::Drop class CatchallDrop < Liquid::Drop
def liquid_method_missing(method) def liquid_method_missing(method)
"catchall_method: #{method}" 'catchall_method: ' << method.to_s
end end
end end
@@ -49,6 +47,10 @@ class ProductDrop < Liquid::Drop
ContextDrop.new ContextDrop.new
end end
def user_input
"foo".taint
end
protected protected
def callmenot def callmenot
@@ -107,151 +109,165 @@ class DropsTest < Minitest::Test
def test_product_drop def test_product_drop
tpl = Liquid::Template.parse(' ') 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
with_taint_mode(:error) do
tpl = Liquid::Template.parse('{{ product.user_input }}')
assert_raises TaintedError do
tpl.render!('product' => ProductDrop.new)
end
end
end
def test_rendering_warns_on_tainted_attr
with_taint_mode(:warn) do
tpl = Liquid::Template.parse('{{ product.user_input }}')
context = Context.new('product' => ProductDrop.new)
tpl.render!(context)
assert_equal [Liquid::TaintedError], context.warnings.map(&:class)
assert_equal "variable 'product.user_input' is tainted and was not escaped", context.warnings.first.to_s(false)
end
end
def test_rendering_doesnt_raise_on_escaped_tainted_attr
with_taint_mode(:error) do
tpl = Liquid::Template.parse('{{ product.user_input | escape }}')
tpl.render!('product' => ProductDrop.new)
end
end end
def test_drop_does_only_respond_to_whitelisted_methods def test_drop_does_only_respond_to_whitelisted_methods
assert_equal("", Liquid::Template.parse("{{ product.inspect }}").render!('product' => ProductDrop.new)) assert_equal "", Liquid::Template.parse("{{ product.inspect }}").render!('product' => ProductDrop.new)
assert_equal("", Liquid::Template.parse("{{ product.pretty_inspect }}").render!('product' => ProductDrop.new)) assert_equal "", Liquid::Template.parse("{{ product.pretty_inspect }}").render!('product' => ProductDrop.new)
assert_equal("", Liquid::Template.parse("{{ product.whatever }}").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: "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: "pretty_inspect" }}').render!('product' => ProductDrop.new)
assert_equal("", Liquid::Template.parse('{{ product | map: "whatever" }}').render!('product' => ProductDrop.new)) assert_equal "", Liquid::Template.parse('{{ product | map: "whatever" }}').render!('product' => ProductDrop.new)
end end
def test_drops_respond_to_to_liquid 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.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 | map: "to_liquid" | map: "texts" | map: "text" }}').render!('product' => ProductDrop.new)
end end
def test_text_drop def test_text_drop
output = Liquid::Template.parse(' {{ product.texts.text }} ').render!('product' => ProductDrop.new) output = Liquid::Template.parse(' {{ product.texts.text }} ').render!('product' => ProductDrop.new)
assert_equal(' text1 ', output) assert_equal ' text1 ', output
end end
def test_catchall_unknown_method def test_catchall_unknown_method
output = Liquid::Template.parse(' {{ product.catchall.unknown }} ').render!('product' => ProductDrop.new) output = Liquid::Template.parse(' {{ product.catchall.unknown }} ').render!('product' => ProductDrop.new)
assert_equal(' catchall_method: unknown ', output) assert_equal ' catchall_method: unknown ', output
end end
def test_catchall_integer_argument_drop def test_catchall_integer_argument_drop
output = Liquid::Template.parse(' {{ product.catchall[8] }} ').render!('product' => ProductDrop.new) output = Liquid::Template.parse(' {{ product.catchall[8] }} ').render!('product' => ProductDrop.new)
assert_equal(' catchall_method: 8 ', output) assert_equal ' catchall_method: 8 ', output
end end
def test_text_array_drop def test_text_array_drop
output = Liquid::Template.parse('{% for text in product.texts.array %} {{text}} {% endfor %}').render!('product' => ProductDrop.new) 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 end
def test_context_drop def test_context_drop
output = Liquid::Template.parse(' {{ context.bar }} ').render!('context' => ContextDrop.new, 'bar' => "carrot") 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 end
def test_nested_context_drop def test_nested_context_drop
output = Liquid::Template.parse(' {{ product.context.foo }} ').render!('product' => ProductDrop.new, 'foo' => "monkey") output = Liquid::Template.parse(' {{ product.context.foo }} ').render!('product' => ProductDrop.new, 'foo' => "monkey")
assert_equal(' monkey ', output) assert_equal ' monkey ', output
end end
def test_protected def test_protected
output = Liquid::Template.parse(' {{ product.callmenot }} ').render!('product' => ProductDrop.new) output = Liquid::Template.parse(' {{ product.callmenot }} ').render!('product' => ProductDrop.new)
assert_equal(' ', output) assert_equal ' ', output
end end
def test_object_methods_not_allowed def test_object_methods_not_allowed
[:dup, :clone, :singleton_class, :eval, :class_eval, :inspect].each do |method| [:dup, :clone, :singleton_class, :eval, :class_eval, :inspect].each do |method|
output = Liquid::Template.parse(" {{ product.#{method} }} ").render!('product' => ProductDrop.new) output = Liquid::Template.parse(" {{ product.#{method} }} ").render!('product' => ProductDrop.new)
assert_equal(' ', output) assert_equal ' ', output
end end
end end
def test_scope def test_scope
assert_equal('1', Liquid::Template.parse('{{ context.scopes }}').render!('context' => ContextDrop.new)) 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 '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 '3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ context.scopes }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1])
end end
def test_scope_though_proc def test_scope_though_proc
assert_equal('1', Liquid::Template.parse('{{ s }}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] })) 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 '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 '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 end
def test_scope_with_assigns 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"%}{{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 '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('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 end
def test_scope_from_tags 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 '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 '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 '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 end
def test_access_context_from_drop 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 end
def test_enumerable_drop 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 end
def test_enumerable_drop_size 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 end
def test_enumerable_drop_will_invoke_liquid_method_missing_for_clashing_method_names def test_enumerable_drop_will_invoke_liquid_method_missing_for_clashing_method_names
["select", "each", "map", "cycle"].each do |method| ["select", "each", "map", "cycle"].each do |method|
assert_equal(method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)) assert_equal method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)
assert_equal(method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)) assert_equal method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)
assert_equal(method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new)) assert_equal method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new)
assert_equal(method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new)) assert_equal method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new)
end end
end end
def test_some_enumerable_methods_still_get_invoked 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' => 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)
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 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' => 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)) assert_equal "1", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)
assert_equal("1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)) assert_equal "1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)
end end
end end
def test_empty_string_value_access 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 end
def test_nil_value_access 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 end
def test_default_to_s_on_drops def test_default_to_s_on_drops
assert_equal('ProductDrop', Liquid::Template.parse("{{ product }}").render!('product' => ProductDrop.new)) assert_equal 'ProductDrop', Liquid::Template.parse("{{ product }}").render!('product' => ProductDrop.new)
assert_equal('EnumerableDrop', Liquid::Template.parse('{{ collection }}').render!('collection' => EnumerableDrop.new)) assert_equal 'EnumerableDrop', Liquid::Template.parse('{{ collection }}').render!('collection' => EnumerableDrop.new)
end
def test_invokable_methods
assert_equal(%w(to_liquid catchall context texts).to_set, ProductDrop.invokable_methods)
assert_equal(%w(to_liquid scopes_as_array loop_pos scopes).to_set, ContextDrop.invokable_methods)
assert_equal(%w(to_liquid size max min first count).to_set, EnumerableDrop.invokable_methods)
assert_equal(%w(to_liquid max min sort count first).to_set, RealEnumerableDrop.invokable_methods)
end end
end # DropsTest end # DropsTest
+86 -169
View File
@@ -1,5 +1,3 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class ErrorHandlingTest < Minitest::Test class ErrorHandlingTest < Minitest::Test
@@ -35,35 +33,37 @@ class ErrorHandlingTest < Minitest::Test
TEXT TEXT
output = Liquid::Template.parse(template, line_numbers: true).render('errors' => ErrorDrop.new) output = Liquid::Template.parse(template, line_numbers: true).render('errors' => ErrorDrop.new)
assert_equal(expected, output) assert_equal expected, output
end end
def test_standard_error def test_standard_error
template = Liquid::Template.parse(' {{ errors.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 1, template.errors.size
assert_equal(StandardError, template.errors.first.class) assert_equal StandardError, template.errors.first.class
end end
def test_syntax def test_syntax
template = Liquid::Template.parse(' {{ errors.syntax_error }} ') 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 1, template.errors.size
assert_equal(SyntaxError, template.errors.first.class) assert_equal SyntaxError, template.errors.first.class
end end
def test_argument def test_argument
template = Liquid::Template.parse(' {{ errors.argument_error }} ') 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 1, template.errors.size
assert_equal(ArgumentError, template.errors.first.class) assert_equal ArgumentError, template.errors.first.class
end end
def test_missing_endtag_parse_time_error def test_missing_endtag_parse_time_error
assert_match_syntax_error(/: 'for' tag was never closed\z/, ' {% for a in b %} ... ') assert_raises(Liquid::SyntaxError) do
Liquid::Template.parse(' {% for a in b %} ... ')
end
end end
def test_unrecognized_operator def test_unrecognized_operator
@@ -76,37 +76,45 @@ class ErrorHandlingTest < Minitest::Test
def test_lax_unrecognized_operator def test_lax_unrecognized_operator
template = Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', error_mode: :lax) template = Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', error_mode: :lax)
assert_equal(' Liquid error: Unknown operator =! ', template.render) assert_equal ' Liquid error: Unknown operator =! ', template.render
assert_equal(1, template.errors.size) assert_equal 1, template.errors.size
assert_equal(Liquid::ArgumentError, template.errors.first.class) assert_equal Liquid::ArgumentError, template.errors.first.class
end end
def test_with_line_numbers_adds_numbers_to_parser_errors def test_with_line_numbers_adds_numbers_to_parser_errors
source = <<~LIQUID err = assert_raises(SyntaxError) do
foobar Liquid::Template.parse(%q(
foobar
{% "cat" | foobar %} {% "cat" | foobar %}
bla bla
LIQUID ),
assert_match_syntax_error(/Liquid syntax error \(line 3\)/, source) line_numbers: true
)
end
assert_match(/Liquid syntax error \(line 4\)/, err.message)
end end
def test_with_line_numbers_adds_numbers_to_parser_errors_with_whitespace_trim def test_with_line_numbers_adds_numbers_to_parser_errors_with_whitespace_trim
source = <<~LIQUID err = assert_raises(SyntaxError) do
foobar Liquid::Template.parse(%q(
foobar
{%- "cat" | foobar -%} {%- "cat" | foobar -%}
bla bla
LIQUID ),
line_numbers: true
)
end
assert_match_syntax_error(/Liquid syntax error \(line 3\)/, source) assert_match(/Liquid syntax error \(line 4\)/, err.message)
end end
def test_parsing_warn_with_line_numbers_adds_numbers_to_lexer_errors def test_parsing_warn_with_line_numbers_adds_numbers_to_lexer_errors
template = Liquid::Template.parse( template = Liquid::Template.parse('
'
foobar foobar
{% if 1 =! 2 %}ok{% endif %} {% if 1 =! 2 %}ok{% endif %}
@@ -114,19 +122,16 @@ class ErrorHandlingTest < Minitest::Test
bla bla
', ',
error_mode: :warn, error_mode: :warn,
line_numbers: true, line_numbers: true
) )
assert_equal( assert_equal ['Liquid syntax error (line 4): Unexpected character = in "1 =! 2"'],
['Liquid syntax error (line 4): Unexpected character = in "1 =! 2"'], template.warnings.map(&:message)
template.warnings.map(&:message),
)
end end
def test_parsing_strict_with_line_numbers_adds_numbers_to_lexer_errors def test_parsing_strict_with_line_numbers_adds_numbers_to_lexer_errors
err = assert_raises(SyntaxError) do err = assert_raises(SyntaxError) do
Liquid::Template.parse( Liquid::Template.parse('
'
foobar foobar
{% if 1 =! 2 %}ok{% endif %} {% if 1 =! 2 %}ok{% endif %}
@@ -134,60 +139,64 @@ class ErrorHandlingTest < Minitest::Test
bla bla
', ',
error_mode: :strict, error_mode: :strict,
line_numbers: true, line_numbers: true
) )
end 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 end
def test_syntax_errors_in_nested_blocks_have_correct_line_number def test_syntax_errors_in_nested_blocks_have_correct_line_number
source = <<~LIQUID err = assert_raises(SyntaxError) do
foobar Liquid::Template.parse('
foobar
{% if 1 != 2 %} {% if 1 != 2 %}
{% foo %} {% foo %}
{% endif %} {% endif %}
bla bla
LIQUID ',
line_numbers: true
)
end
assert_match_syntax_error("Liquid syntax error (line 4): Unknown tag 'foo'", source) assert_equal "Liquid syntax error (line 5): Unknown tag 'foo'", err.message
end end
def test_strict_error_messages def test_strict_error_messages
err = assert_raises(SyntaxError) do err = assert_raises(SyntaxError) do
Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', error_mode: :strict) Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', error_mode: :strict)
end 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 err = assert_raises(SyntaxError) do
Liquid::Template.parse('{{%%%}}', error_mode: :strict) Liquid::Template.parse('{{%%%}}', error_mode: :strict)
end end
assert_equal('Liquid syntax error: Unexpected character % in "{{%%%}}"', err.message) assert_equal 'Liquid syntax error: Unexpected character % in "{{%%%}}"', err.message
end end
def test_warnings def test_warnings
template = Liquid::Template.parse('{% if ~~~ %}{{%%%}}{% else %}{{ hello. }}{% endif %}', error_mode: :warn) template = Liquid::Template.parse('{% if ~~~ %}{{%%%}}{% else %}{{ hello. }}{% endif %}', error_mode: :warn)
assert_equal(3, template.warnings.size) assert_equal 3, template.warnings.size
assert_equal('Unexpected character ~ in "~~~"', template.warnings[0].to_s(false)) assert_equal 'Unexpected character ~ in "~~~"', template.warnings[0].to_s(false)
assert_equal('Unexpected character % in "{{%%%}}"', template.warnings[1].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 'Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].to_s(false)
assert_equal('', template.render) assert_equal '', template.render
end end
def test_warning_line_numbers def test_warning_line_numbers
template = Liquid::Template.parse("{% if ~~~ %}\n{{%%%}}{% else %}\n{{ hello. }}{% endif %}", error_mode: :warn, line_numbers: true) 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 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 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 '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 3, template.warnings.size
assert_equal([1, 2, 3], template.warnings.map(&:line_number)) assert_equal [1, 2, 3], template.warnings.map(&:line_number)
end end
# Liquid should not catch Exceptions that are not subclasses of StandardError, like Interrupt and NoMemoryError # Liquid should not catch Exceptions that are not subclasses of StandardError, like Interrupt and NoMemoryError
def test_exceptions_propagate def test_exceptions_propagate
assert_raises(Exception) do assert_raises Exception do
template = Liquid::Template.parse('{{ errors.exception }}') template = Liquid::Template.parse('{{ errors.exception }}')
template.render('errors' => ErrorDrop.new) template.render('errors' => ErrorDrop.new)
end end
@@ -196,47 +205,41 @@ class ErrorHandlingTest < Minitest::Test
def test_default_exception_renderer_with_internal_error def test_default_exception_renderer_with_internal_error
template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true) template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true)
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 'This is a runtime error: Liquid error (line 1): internal', output
assert_equal([Liquid::InternalError], template.errors.map(&:class)) assert_equal [Liquid::InternalError], template.errors.map(&:class)
end end
def test_setting_default_exception_renderer def test_setting_default_exception_renderer
old_exception_renderer = Liquid::Template.default_exception_renderer old_exception_renderer = Liquid::Template.default_exception_renderer
exceptions = [] exceptions = []
Liquid::Template.default_exception_renderer = ->(e) { Liquid::Template.default_exception_renderer = ->(e) { exceptions << e; '' }
exceptions << e
''
}
template = Liquid::Template.parse('This is a runtime error: {{ errors.argument_error }}') 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 'This is a runtime error: ', output
assert_equal([Liquid::ArgumentError], template.errors.map(&:class)) assert_equal [Liquid::ArgumentError], template.errors.map(&:class)
ensure ensure
Liquid::Template.default_exception_renderer = old_exception_renderer if old_exception_renderer Liquid::Template.default_exception_renderer = old_exception_renderer if old_exception_renderer
end end
def test_exception_renderer_exposing_non_liquid_error def test_exception_renderer_exposing_non_liquid_error
template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true) template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true)
exceptions = [] exceptions = []
handler = ->(e) { handler = ->(e) { exceptions << e; e.cause }
exceptions << e
e.cause
}
output = template.render({ 'errors' => ErrorDrop.new }, exception_renderer: handler) output = template.render({ 'errors' => ErrorDrop.new }, exception_renderer: handler)
assert_equal('This is a runtime error: runtime error', output) assert_equal 'This is a runtime error: runtime error', output
assert_equal([Liquid::InternalError], exceptions.map(&:class)) assert_equal [Liquid::InternalError], exceptions.map(&:class)
assert_equal(exceptions, template.errors) assert_equal exceptions, template.errors
assert_equal('#<RuntimeError: runtime error>', exceptions.first.cause.inspect) assert_equal '#<RuntimeError: runtime error>', exceptions.first.cause.inspect
end end
class TestFileSystem class TestFileSystem
def read_template_file(_template_path) def read_template_file(template_path)
"{{ errors.argument_error }}" "{{ errors.argument_error }}"
end end
end end
@@ -246,98 +249,12 @@ class ErrorHandlingTest < Minitest::Test
begin begin
Liquid::Template.file_system = TestFileSystem.new Liquid::Template.file_system = TestFileSystem.new
template = Liquid::Template.parse("Argument error:\n{% include 'product' %}", line_numbers: true) template = Liquid::Template.parse("Argument error:\n{% include 'product' %}", line_numbers: true)
page = template.render('errors' => ErrorDrop.new) page = template.render('errors' => ErrorDrop.new)
ensure ensure
Liquid::Template.file_system = old_file_system Liquid::Template.file_system = old_file_system
end end
assert_equal("Argument error:\nLiquid error (product line 1): argument error", page) assert_equal "Argument error:\nLiquid error (product line 1): argument error", page
assert_equal("product", template.errors.first.template_name) assert_equal "product", template.errors.first.template_name
end
def test_bug_compatible_silencing_of_errors_in_blank_nodes
output = Liquid::Template.parse("{% assign x = 0 %}{% if 1 < '2' %}not blank{% assign x = 3 %}{% endif %}{{ x }}").render
assert_equal("Liquid error: comparison of Integer with String failed0", output)
output = Liquid::Template.parse("{% assign x = 0 %}{% if 1 < '2' %}{% assign x = 3 %}{% endif %}{{ x }}").render
assert_equal("0", output)
end
def test_syntax_error_is_raised_with_template_name
file_system = StubFileSystem.new("snippet" => "1\n2\n{{ 1")
context = Liquid::Context.build(
registers: { file_system: file_system },
)
template = Template.parse(
'{% render "snippet" %}',
line_numbers: true,
)
template.name = "template/index"
assert_equal(
"Liquid syntax error (snippet line 3): Variable '{{' was not properly terminated with regexp: /\\}\\}/",
template.render(context),
)
end
def test_syntax_error_is_raised_with_template_name_from_template_factory
file_system = StubFileSystem.new("snippet" => "1\n2\n{{ 1")
context = Liquid::Context.build(
registers: {
file_system: file_system,
template_factory: StubTemplateFactory.new,
},
)
template = Template.parse(
'{% render "snippet" %}',
line_numbers: true,
)
template.name = "template/index"
assert_equal(
"Liquid syntax error (some/path/snippet line 3): Variable '{{' was not properly terminated with regexp: /\\}\\}/",
template.render(context),
)
end
def test_error_is_raised_during_parse_with_template_name
depth = Liquid::Block::MAX_DEPTH + 1
code = "{% if true %}" * depth + "rendered" + "{% endif %}" * depth
template = Template.parse("{% render 'snippet' %}", line_numbers: true)
context = Liquid::Context.build(
registers: {
file_system: StubFileSystem.new("snippet" => code),
template_factory: StubTemplateFactory.new,
},
)
assert_equal("Liquid error (some/path/snippet line 1): Nesting too deep", template.render(context))
end
def test_internal_error_is_raised_with_template_name
template = Template.new
template.parse(
"{% render 'snippet' %}",
line_numbers: true,
)
template.name = "template/index"
context = Liquid::Context.build(
registers: {
file_system: StubFileSystem.new({}),
},
)
assert_equal(
"Liquid error (template/index line 1): internal",
template.render(context),
)
end end
end end
-49
View File
@@ -1,49 +0,0 @@
# frozen_string_literal: true
require 'test_helper'
class ExpressionTest < Minitest::Test
def test_keyword_literals
assert_template_result("true", "{{ true }}")
assert_expression_result(true, "true")
end
def test_string
assert_template_result("single quoted", "{{'single quoted'}}")
assert_template_result("double quoted", '{{"double quoted"}}')
assert_template_result("spaced", "{{ 'spaced' }}")
assert_template_result("spaced2", "{{ 'spaced2' }}")
end
def test_int
assert_template_result("456", "{{ 456 }}")
assert_expression_result(123, "123")
assert_expression_result(12, "012")
end
def test_float
assert_template_result("2.5", "{{ 2.5 }}")
assert_expression_result(1.5, "1.5")
end
def test_range
assert_template_result("3..4", "{{ ( 3 .. 4 ) }}")
assert_expression_result(1..2, "(1..2)")
assert_match_syntax_error(
"Liquid syntax error (line 1): Invalid expression type 'false' in range expression",
"{{ (false..true) }}",
)
assert_match_syntax_error(
"Liquid syntax error (line 1): Invalid expression type '(1..2)' in range expression",
"{{ ((1..2)..3) }}",
)
end
private
def assert_expression_result(expect, markup, **assigns)
liquid = "{% if expect == #{markup} %}pass{% else %}got {{ #{markup} }}{% endif %}"
assert_template_result("pass", liquid, { "expect" => expect, **assigns })
end
end
-24
View File
@@ -1,24 +0,0 @@
# frozen_string_literal: true
require 'test_helper'
class FilterKwargTest < Minitest::Test
module KwargFilter
def html_tag(_tag, attributes)
attributes
.map { |key, value| "#{key}='#{value}'" }
.join(' ')
end
end
include Liquid
def test_can_parse_data_kwargs
with_global_filter(KwargFilter) do
assert_equal(
"data-src='src' data-widths='100, 200'",
Template.parse("{{ 'img' | html_tag: data-src: 'src', data-widths: '100, 200' }}").render(nil, nil),
)
end
end
end

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