mirror of
https://github.com/kemko/liquid.git
synced 2026-01-02 08:15:41 +03:00
Compare commits
20 Commits
context_si
...
string-sli
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb954bce1e | ||
|
|
cc0276bb97 | ||
|
|
03d586aafe | ||
|
|
dc8a34a52f | ||
|
|
99cebf74bc | ||
|
|
7eb64886dc | ||
|
|
f89046e81f | ||
|
|
9ee4573ef4 | ||
|
|
a48b4f47f6 | ||
|
|
72d402837e | ||
|
|
06bef40527 | ||
|
|
a48b245e6e | ||
|
|
d4aabda625 | ||
|
|
dab6bdfdee | ||
|
|
8c075fca1f | ||
|
|
ea8406e36e | ||
|
|
8bb3bca64a | ||
|
|
5de1082201 | ||
|
|
7ba02d2811 | ||
|
|
2066676bf4 |
2
.github/probots.yml
vendored
2
.github/probots.yml
vendored
@@ -1,2 +0,0 @@
|
|||||||
enabled:
|
|
||||||
- cla
|
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -5,6 +5,6 @@ pkg
|
|||||||
*.rbc
|
*.rbc
|
||||||
.rvmrc
|
.rvmrc
|
||||||
.ruby-version
|
.ruby-version
|
||||||
|
*.bundle
|
||||||
|
/tmp
|
||||||
Gemfile.lock
|
Gemfile.lock
|
||||||
.bundle
|
|
||||||
.byebug_history
|
|
||||||
|
|||||||
132
.rubocop.yml
132
.rubocop.yml
@@ -1,132 +0,0 @@
|
|||||||
inherit_from:
|
|
||||||
- .rubocop_todo.yml
|
|
||||||
- ./.rubocop_todo.yml
|
|
||||||
|
|
||||||
AllCops:
|
|
||||||
Exclude:
|
|
||||||
- 'performance/shopify/*'
|
|
||||||
- 'pkg/**'
|
|
||||||
|
|
||||||
Metrics/BlockNesting:
|
|
||||||
Max: 3
|
|
||||||
|
|
||||||
Metrics/ModuleLength:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
Metrics/ClassLength:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
Lint/AssignmentInCondition:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
Lint/AmbiguousOperator:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
Lint/AmbiguousRegexpLiteral:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
Lint/ParenthesesAsGroupedExpression:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
Lint/UnusedBlockArgument:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
Layout/EndAlignment:
|
|
||||||
EnforcedStyleAlignWith: variable
|
|
||||||
|
|
||||||
Lint/UnusedMethodArgument:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
Style/SingleLineBlockParams:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
Style/DoubleNegation:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
Style/StringLiteralsInInterpolation:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
Style/AndOr:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
Style/SignalException:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
Style/StringLiterals:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
Style/BracesAroundHashParameters:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
Style/NumericLiterals:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
Layout/SpaceInsideArrayLiteralBrackets:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
Layout/SpaceBeforeBlockBraces:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
Style/Documentation:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
Style/ClassAndModuleChildren:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
Style/TrailingCommaInArrayLiteral:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
Style/TrailingCommaInHashLiteral:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
Layout/IndentHash:
|
|
||||||
EnforcedStyle: consistent
|
|
||||||
|
|
||||||
Style/FormatString:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
Layout/AlignParameters:
|
|
||||||
EnforcedStyle: with_fixed_indentation
|
|
||||||
|
|
||||||
Layout/MultilineOperationIndentation:
|
|
||||||
EnforcedStyle: indented
|
|
||||||
|
|
||||||
Style/IfUnlessModifier:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
Style/RaiseArgs:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
Style/PreferredHashMethods:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
Style/RegexpLiteral:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
Style/SymbolLiteral:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
Performance/Count:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
Naming/ConstantName:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
Layout/CaseIndentation:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
Style/ClassVars:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
Style/PerlBackrefs:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
Style/TrivialAccessors:
|
|
||||||
AllowPredicates: true
|
|
||||||
|
|
||||||
Style/WordArray:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
Naming/MethodName:
|
|
||||||
Exclude:
|
|
||||||
- 'example/server/liquid_servlet.rb'
|
|
||||||
@@ -1,260 +0,0 @@
|
|||||||
# This configuration was generated by
|
|
||||||
# `rubocop --auto-gen-config`
|
|
||||||
# on 2019-04-22 19:11:24 -0400 using RuboCop version 0.53.0.
|
|
||||||
# The point is for the user to remove these configuration records
|
|
||||||
# one by one as the offenses are removed from the code base.
|
|
||||||
# Note that changes in the inspected code, or installation of new
|
|
||||||
# versions of RuboCop, may require this file to be generated again.
|
|
||||||
|
|
||||||
# Offense count: 1
|
|
||||||
# Cop supports --auto-correct.
|
|
||||||
# Configuration parameters: Include, TreatCommentsAsGroupSeparators.
|
|
||||||
# Include: **/*.gemspec
|
|
||||||
Gemspec/OrderedDependencies:
|
|
||||||
Exclude:
|
|
||||||
- 'liquid.gemspec'
|
|
||||||
|
|
||||||
# Offense count: 5
|
|
||||||
# Cop supports --auto-correct.
|
|
||||||
# Configuration parameters: EnforcedStyle.
|
|
||||||
# SupportedStyles: auto_detection, squiggly, active_support, powerpack, unindent
|
|
||||||
Layout/IndentHeredoc:
|
|
||||||
Exclude:
|
|
||||||
- 'test/integration/tags/for_tag_test.rb'
|
|
||||||
- 'test/integration/trim_mode_test.rb'
|
|
||||||
|
|
||||||
# Offense count: 6
|
|
||||||
# Cop supports --auto-correct.
|
|
||||||
# Configuration parameters: EnforcedStyle.
|
|
||||||
# SupportedStyles: symmetrical, new_line, same_line
|
|
||||||
Layout/MultilineMethodCallBraceLayout:
|
|
||||||
Exclude:
|
|
||||||
- 'test/integration/error_handling_test.rb'
|
|
||||||
- 'test/unit/strainer_unit_test.rb'
|
|
||||||
|
|
||||||
# Offense count: 2
|
|
||||||
# Cop supports --auto-correct.
|
|
||||||
# Configuration parameters: EnforcedStyle.
|
|
||||||
# SupportedStyles: runtime_error, standard_error
|
|
||||||
Lint/InheritException:
|
|
||||||
Exclude:
|
|
||||||
- 'lib/liquid/interrupts.rb'
|
|
||||||
|
|
||||||
# Offense count: 1
|
|
||||||
# Configuration parameters: CheckForMethodsWithNoSideEffects.
|
|
||||||
Lint/Void:
|
|
||||||
Exclude:
|
|
||||||
- 'lib/liquid/parse_context.rb'
|
|
||||||
|
|
||||||
# Offense count: 53
|
|
||||||
Metrics/AbcSize:
|
|
||||||
Max: 56
|
|
||||||
|
|
||||||
# Offense count: 12
|
|
||||||
Metrics/CyclomaticComplexity:
|
|
||||||
Max: 13
|
|
||||||
|
|
||||||
# Offense count: 112
|
|
||||||
# Configuration parameters: CountComments.
|
|
||||||
Metrics/MethodLength:
|
|
||||||
Max: 38
|
|
||||||
|
|
||||||
# Offense count: 8
|
|
||||||
Metrics/PerceivedComplexity:
|
|
||||||
Max: 11
|
|
||||||
|
|
||||||
# Offense count: 52
|
|
||||||
# Configuration parameters: Blacklist.
|
|
||||||
# Blacklist: END, (?-mix:EO[A-Z]{1})
|
|
||||||
Naming/HeredocDelimiterNaming:
|
|
||||||
Exclude:
|
|
||||||
- 'test/integration/assign_test.rb'
|
|
||||||
- 'test/integration/capture_test.rb'
|
|
||||||
- 'test/integration/trim_mode_test.rb'
|
|
||||||
|
|
||||||
# Offense count: 23
|
|
||||||
# Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames.
|
|
||||||
# AllowedNames: io, id
|
|
||||||
Naming/UncommunicativeMethodParamName:
|
|
||||||
Exclude:
|
|
||||||
- 'example/server/example_servlet.rb'
|
|
||||||
- 'lib/liquid/condition.rb'
|
|
||||||
- 'lib/liquid/context.rb'
|
|
||||||
- 'lib/liquid/standardfilters.rb'
|
|
||||||
- 'lib/liquid/tags/if.rb'
|
|
||||||
- 'lib/liquid/utils.rb'
|
|
||||||
- 'lib/liquid/variable.rb'
|
|
||||||
- 'test/integration/filter_test.rb'
|
|
||||||
- 'test/integration/standard_filter_test.rb'
|
|
||||||
- 'test/integration/tags/for_tag_test.rb'
|
|
||||||
- 'test/integration/template_test.rb'
|
|
||||||
- 'test/unit/condition_unit_test.rb'
|
|
||||||
|
|
||||||
# Offense count: 12
|
|
||||||
# Cop supports --auto-correct.
|
|
||||||
# Configuration parameters: EnforcedStyle.
|
|
||||||
# SupportedStyles: prefer_alias, prefer_alias_method
|
|
||||||
Style/Alias:
|
|
||||||
Exclude:
|
|
||||||
- 'lib/liquid/drop.rb'
|
|
||||||
- 'lib/liquid/i18n.rb'
|
|
||||||
- 'lib/liquid/profiler/hooks.rb'
|
|
||||||
- 'lib/liquid/standardfilters.rb'
|
|
||||||
- 'lib/liquid/tag.rb'
|
|
||||||
- 'lib/liquid/tags/include.rb'
|
|
||||||
- 'lib/liquid/variable.rb'
|
|
||||||
|
|
||||||
# Offense count: 22
|
|
||||||
Style/CommentedKeyword:
|
|
||||||
Enabled: false
|
|
||||||
|
|
||||||
# Offense count: 1
|
|
||||||
# Cop supports --auto-correct.
|
|
||||||
# Configuration parameters: EnforcedStyle, SingleLineConditionsOnly, IncludeTernaryExpressions.
|
|
||||||
# SupportedStyles: assign_to_condition, assign_inside_condition
|
|
||||||
Style/ConditionalAssignment:
|
|
||||||
Exclude:
|
|
||||||
- 'lib/liquid/errors.rb'
|
|
||||||
|
|
||||||
# Offense count: 1
|
|
||||||
Style/DateTime:
|
|
||||||
Exclude:
|
|
||||||
- 'test/unit/context_unit_test.rb'
|
|
||||||
|
|
||||||
# Offense count: 2
|
|
||||||
# Cop supports --auto-correct.
|
|
||||||
Style/EmptyCaseCondition:
|
|
||||||
Exclude:
|
|
||||||
- 'lib/liquid/block_body.rb'
|
|
||||||
- 'lib/liquid/lexer.rb'
|
|
||||||
|
|
||||||
# Offense count: 5
|
|
||||||
# Cop supports --auto-correct.
|
|
||||||
# Configuration parameters: EnforcedStyle.
|
|
||||||
# SupportedStyles: compact, expanded
|
|
||||||
Style/EmptyMethod:
|
|
||||||
Exclude:
|
|
||||||
- 'lib/liquid/tag.rb'
|
|
||||||
- 'lib/liquid/tags/comment.rb'
|
|
||||||
- 'lib/liquid/tags/include.rb'
|
|
||||||
- 'test/integration/tags/include_tag_test.rb'
|
|
||||||
- 'test/unit/context_unit_test.rb'
|
|
||||||
|
|
||||||
# Offense count: 3
|
|
||||||
# Cop supports --auto-correct.
|
|
||||||
Style/Encoding:
|
|
||||||
Exclude:
|
|
||||||
- 'lib/liquid/version.rb'
|
|
||||||
- 'liquid.gemspec'
|
|
||||||
- 'test/integration/standard_filter_test.rb'
|
|
||||||
|
|
||||||
# Offense count: 2
|
|
||||||
# Cop supports --auto-correct.
|
|
||||||
Style/ExpandPathArguments:
|
|
||||||
Exclude:
|
|
||||||
- 'Rakefile'
|
|
||||||
- 'liquid.gemspec'
|
|
||||||
|
|
||||||
# Offense count: 7
|
|
||||||
# Configuration parameters: EnforcedStyle.
|
|
||||||
# SupportedStyles: annotated, template, unannotated
|
|
||||||
Style/FormatStringToken:
|
|
||||||
Exclude:
|
|
||||||
- 'test/integration/filter_test.rb'
|
|
||||||
- 'test/integration/hash_ordering_test.rb'
|
|
||||||
|
|
||||||
# Offense count: 14
|
|
||||||
# Configuration parameters: MinBodyLength.
|
|
||||||
Style/GuardClause:
|
|
||||||
Exclude:
|
|
||||||
- 'lib/liquid/condition.rb'
|
|
||||||
- 'lib/liquid/lexer.rb'
|
|
||||||
- 'lib/liquid/strainer.rb'
|
|
||||||
- 'lib/liquid/tags/assign.rb'
|
|
||||||
- 'lib/liquid/tags/capture.rb'
|
|
||||||
- 'lib/liquid/tags/case.rb'
|
|
||||||
- 'lib/liquid/tags/for.rb'
|
|
||||||
- 'lib/liquid/tags/include.rb'
|
|
||||||
- 'lib/liquid/tags/raw.rb'
|
|
||||||
- 'lib/liquid/tags/table_row.rb'
|
|
||||||
- 'lib/liquid/variable.rb'
|
|
||||||
- 'test/unit/tokenizer_unit_test.rb'
|
|
||||||
|
|
||||||
# Offense count: 1
|
|
||||||
# Cop supports --auto-correct.
|
|
||||||
# Configuration parameters: EnforcedStyle, MinBodyLength.
|
|
||||||
# SupportedStyles: skip_modifier_ifs, always
|
|
||||||
Style/Next:
|
|
||||||
Exclude:
|
|
||||||
- 'lib/liquid/tags/for.rb'
|
|
||||||
|
|
||||||
# Offense count: 4
|
|
||||||
# Cop supports --auto-correct.
|
|
||||||
# Configuration parameters: AutoCorrect, EnforcedStyle.
|
|
||||||
# SupportedStyles: predicate, comparison
|
|
||||||
Style/NumericPredicate:
|
|
||||||
Exclude:
|
|
||||||
- 'spec/**/*'
|
|
||||||
- 'lib/liquid/context.rb'
|
|
||||||
- 'lib/liquid/forloop_drop.rb'
|
|
||||||
- 'lib/liquid/standardfilters.rb'
|
|
||||||
- 'lib/liquid/tablerowloop_drop.rb'
|
|
||||||
|
|
||||||
# Offense count: 14
|
|
||||||
# Cop supports --auto-correct.
|
|
||||||
# Configuration parameters: PreferredDelimiters.
|
|
||||||
Style/PercentLiteralDelimiters:
|
|
||||||
Exclude:
|
|
||||||
- 'lib/liquid/tags/if.rb'
|
|
||||||
- 'liquid.gemspec'
|
|
||||||
- 'test/integration/assign_test.rb'
|
|
||||||
- 'test/integration/standard_filter_test.rb'
|
|
||||||
|
|
||||||
# Offense count: 1
|
|
||||||
# Cop supports --auto-correct.
|
|
||||||
Style/RedundantSelf:
|
|
||||||
Exclude:
|
|
||||||
- 'lib/liquid/strainer.rb'
|
|
||||||
|
|
||||||
# Offense count: 9
|
|
||||||
# Cop supports --auto-correct.
|
|
||||||
# Configuration parameters: AllowAsExpressionSeparator.
|
|
||||||
Style/Semicolon:
|
|
||||||
Exclude:
|
|
||||||
- 'test/integration/error_handling_test.rb'
|
|
||||||
- 'test/integration/template_test.rb'
|
|
||||||
- 'test/unit/context_unit_test.rb'
|
|
||||||
|
|
||||||
# Offense count: 7
|
|
||||||
# Cop supports --auto-correct.
|
|
||||||
# Configuration parameters: MinSize.
|
|
||||||
# SupportedStyles: percent, brackets
|
|
||||||
Style/SymbolArray:
|
|
||||||
EnforcedStyle: brackets
|
|
||||||
|
|
||||||
# Offense count: 2
|
|
||||||
# Cop supports --auto-correct.
|
|
||||||
# Configuration parameters: EnforcedStyle, AllowSafeAssignment.
|
|
||||||
# SupportedStyles: require_parentheses, require_no_parentheses, require_parentheses_when_complex
|
|
||||||
Style/TernaryParentheses:
|
|
||||||
Exclude:
|
|
||||||
- 'lib/liquid/context.rb'
|
|
||||||
- 'lib/liquid/utils.rb'
|
|
||||||
|
|
||||||
# Offense count: 2
|
|
||||||
# Cop supports --auto-correct.
|
|
||||||
Style/UnneededPercentQ:
|
|
||||||
Exclude:
|
|
||||||
- 'test/integration/error_handling_test.rb'
|
|
||||||
|
|
||||||
# Offense count: 1
|
|
||||||
# Cop supports --auto-correct.
|
|
||||||
Style/WhileUntilModifier:
|
|
||||||
Exclude:
|
|
||||||
- 'lib/liquid/tags/case.rb'
|
|
||||||
|
|
||||||
# Offense count: 648
|
|
||||||
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
|
|
||||||
# URISchemes: http, https
|
|
||||||
Metrics/LineLength:
|
|
||||||
Max: 294
|
|
||||||
32
.travis.yml
32
.travis.yml
@@ -1,34 +1,16 @@
|
|||||||
language: ruby
|
|
||||||
|
|
||||||
rvm:
|
rvm:
|
||||||
- 2.1
|
- 1.9.3
|
||||||
- 2.2
|
- 2.0.0
|
||||||
- 2.3
|
- 2.1.0
|
||||||
- 2.4
|
- jruby-19mode
|
||||||
- 2.5
|
|
||||||
- &latest_ruby 2.6
|
|
||||||
- ruby-head
|
|
||||||
- jruby-head
|
- jruby-head
|
||||||
# - rbx-2
|
- rbx-19mode
|
||||||
|
|
||||||
addons:
|
|
||||||
apt:
|
|
||||||
packages:
|
|
||||||
- libgmp3-dev
|
|
||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
|
||||||
- rvm: *latest_ruby
|
|
||||||
script: bundle exec rake memory_profile:run
|
|
||||||
name: Profiling Memory Usage
|
|
||||||
allow_failures:
|
allow_failures:
|
||||||
- rvm: ruby-head
|
- rvm: rbx-19mode
|
||||||
- rvm: jruby-head
|
- rvm: jruby-head
|
||||||
|
|
||||||
install:
|
script: "rake test"
|
||||||
- bundle install
|
|
||||||
|
|
||||||
script: bundle exec rake
|
|
||||||
|
|
||||||
notifications:
|
notifications:
|
||||||
disable: true
|
disable: true
|
||||||
|
|||||||
@@ -4,22 +4,23 @@
|
|||||||
|
|
||||||
* Bugfixes
|
* Bugfixes
|
||||||
* Performance improvements
|
* Performance improvements
|
||||||
* Features that are likely to be useful to the majority of Liquid users
|
* Features which are likely to be useful to the majority of Liquid users
|
||||||
|
|
||||||
## Things we won't merge
|
## Things we won't merge
|
||||||
|
|
||||||
* Code that introduces considerable performance degrations
|
* Code which introduces considerable performance degrations
|
||||||
* Code that touches performance-critical parts of Liquid and comes without benchmarks
|
* Code which touches performance critical parts of Liquid and comes without benchmarks
|
||||||
* Features that are not important for most people (we want to keep the core Liquid code small and tidy)
|
* Features which are not important for most people (we want to keep the core Liquid code small and tidy)
|
||||||
* Features that can easily be implemented on top of Liquid (for example as a custom filter or custom filesystem)
|
* Features which can easily be implemented on top of Liquid (for example as a custom filter or custom filesystem)
|
||||||
* Code that does not include tests
|
* Code which comes without tests
|
||||||
* Code that breaks existing tests
|
* Code which breaks existing tests
|
||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
* Fork the Liquid repository
|
* Fork the Liquid repository
|
||||||
* Create a new branch in your fork
|
* Create a new branch in your fork
|
||||||
* 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 run a performance benchmark
|
||||||
* Make sure all tests pass (`bundle exec rake`)
|
* Make sure all tests pass
|
||||||
* Create a pull request
|
* Create a pull request
|
||||||
|
* In the description, ping one of [@boourns](https://github.com/boourns), [@fw42](https://github.com/fw42), [@camilo](https://github.com/camilo), [@dylanahsmith](https://github.com/dylanahsmith), or [@arthurnn](https://github.com/arthurnn) and ask for a code review.
|
||||||
|
|
||||||
|
|||||||
21
Gemfile
21
Gemfile
@@ -1,24 +1,3 @@
|
|||||||
source 'https://rubygems.org'
|
source 'https://rubygems.org'
|
||||||
git_source(:github) do |repo_name|
|
|
||||||
"https://github.com/#{repo_name}.git"
|
|
||||||
end
|
|
||||||
|
|
||||||
gemspec
|
gemspec
|
||||||
|
|
||||||
group :benchmark, :test do
|
|
||||||
gem 'benchmark-ips'
|
|
||||||
gem 'memory_profiler'
|
|
||||||
gem 'terminal-table'
|
|
||||||
|
|
||||||
install_if -> { RUBY_PLATFORM !~ /mingw|mswin|java/ } do
|
|
||||||
gem 'stackprof'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
group :test do
|
|
||||||
gem 'rubocop', '~> 0.53.0'
|
|
||||||
|
|
||||||
platform :mri do
|
|
||||||
gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'liquid-tag'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|||||||
235
History.md
235
History.md
@@ -1,208 +1,59 @@
|
|||||||
# Liquid Change Log
|
# Liquid Version History
|
||||||
|
|
||||||
## 4.0.3 / 2019-03-12
|
## 3.0.0 / not yet released / branch "master"
|
||||||
|
|
||||||
### Fixed
|
* ...
|
||||||
* Fix break and continue tags inside included templates in loops (#1072) [Justin Li]
|
* Add a to_s default for liquid drops, see #306 [Adam Doeler, releod]
|
||||||
|
* Add strip, lstrip, and rstrip to standard filters [Florian Weingarten, fw42]
|
||||||
|
* Make if, for & case tags return complete and consistent nodelists, see #250 [Nick Jones, dntj]
|
||||||
|
* Prevent arbitrary method invocation on condition objects, see #274 [Dylan Thacker-Smith, dylanahsmith]
|
||||||
|
* Don't call to_sym when creating conditions for security reasons, see #273 [Bouke van der Bijl, bouk]
|
||||||
|
* Fix resource counting bug with respond_to?(:length), see #263 [Florian Weingarten, fw42]
|
||||||
|
* Allow specifying custom patterns for template filenames, see #284 [Andrei Gladkyi, agladkyi]
|
||||||
|
* Allow drops to optimize loading a slice of elements, see #282 [Tom Burns, boourns]
|
||||||
|
* Support for passing variables to snippets in subdirs, see #271 [Joost Hietbrink, joost]
|
||||||
|
* Add a class cache to avoid runtime extend calls, see #249 [James Tucker, raggi]
|
||||||
|
* Remove some legacy Ruby 1.8 compatibility code, see #276 [Florian Weingarten, fw42]
|
||||||
|
* Add default filter to standard filters, see #267 [Derrick Reimer, djreimer]
|
||||||
|
* Add optional strict parsing and warn parsing, see #235 [Tristan Hume, trishume]
|
||||||
|
* Add I18n syntax error translation, see #241 [Simon Hørup Eskildsen, Sirupsen]
|
||||||
|
* Make sort filter work on enumerable drops, see #239 [Florian Weingarten, fw42]
|
||||||
|
* Fix clashing method names in enumerable drops, see #238 [Florian Weingarten, fw42]
|
||||||
|
* Make map filter work on enumerable drops, see #233 [Florian Weingarten, fw42]
|
||||||
|
* Improved whitespace stripping for blank blocks, related to #216 [Florian Weingarten, fw42]
|
||||||
|
* Raise `Liquid::ArgumentError` instead of `::ArgumentError` when filter has wrong number of arguments #309 [Bogdan Gusiev, bogdan]
|
||||||
|
|
||||||
## 4.0.2 / 2019-03-08
|
## 2.6.0 / 2013-11-25 / branch "2.6-stable"
|
||||||
|
|
||||||
### Changed
|
|
||||||
* Add `where` filter (#1026) [Samuel Doiron]
|
|
||||||
* Add `ParseTreeVisitor` to iterate the Liquid AST (#1025) [Stephen Paul Weber]
|
|
||||||
* Improve `strip_html` performance (#1032) [printercu]
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
* Add error checking for invalid combinations of inputs to sort, sort_natural, where, uniq, map, compact filters (#1059) [Garland Zhang]
|
|
||||||
* Validate the character encoding in url_decode (#1070) [Clayton Smith]
|
|
||||||
|
|
||||||
## 4.0.1 / 2018-10-09
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
* Add benchmark group in Gemfile (#855) [Jerry Liu]
|
|
||||||
* Allow benchmarks to benchmark render by itself (#851) [Jerry Liu]
|
|
||||||
* Avoid calling `line_number` on String node when rescuing a render error. (#860) [Dylan Thacker-Smith]
|
|
||||||
* Avoid duck typing to detect whether to call render on a node. [Dylan Thacker-Smith]
|
|
||||||
* Clarify spelling of `reversed` on `for` block tag (#843) [Mark Crossfield]
|
|
||||||
* Replace recursion with loop to avoid potential stack overflow from malicious input (#891, #892) [Dylan Thacker-Smith]
|
|
||||||
* Limit block tag nesting to 100 (#894) [Dylan Thacker-Smith]
|
|
||||||
* Replace `assert_equal nil` with `assert_nil` (#895) [Dylan Thacker-Smith]
|
|
||||||
* Remove Spy Gem (#896) [Dylan Thacker-Smith]
|
|
||||||
* Add `collection_name` and `variable_name` reader to `For` block (#909)
|
|
||||||
* Symbols render as strings (#920) [Justin Li]
|
|
||||||
* Remove default value from Hash objects (#932) [Maxime Bedard]
|
|
||||||
* Remove one level of nesting (#944) [Dylan Thacker-Smith]
|
|
||||||
* Update Rubocop version (#952) [Justin Li]
|
|
||||||
* Add `at_least` and `at_most` filters (#954, #958) [Nithin Bekal]
|
|
||||||
* Add a regression test for a liquid-c trim mode bug (#972) [Dylan Thacker-Smith]
|
|
||||||
* Use https rather than git protocol to fetch liquid-c [Dylan Thacker-Smith]
|
|
||||||
* Add tests against Ruby 2.4 (#963) and 2.5 (#981)
|
|
||||||
* Replace RegExp literals with constants (#988) [Ashwin Maroli]
|
|
||||||
* Replace unnecessary `#each_with_index` with `#each` (#992) [Ashwin Maroli]
|
|
||||||
* Improve the unexpected end delimiter message for block tags. (#1003) [Dylan Thacker-Smith]
|
|
||||||
* Refactor and optimize rendering (#1005) [Christopher Aue]
|
|
||||||
* Add installation instruction (#1006) [Ben Gift]
|
|
||||||
* Remove Circle CI (#1010)
|
|
||||||
* Rename deprecated `BigDecimal.new` to `BigDecimal` (#1024) [Koichi ITO]
|
|
||||||
* Rename deprecated Rubocop name (#1027) [Justin Li]
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
* Handle `join` filter on non String joiners (#857) [Richard Monette]
|
|
||||||
* Fix duplicate inclusion condition logic error of `Liquid::Strainer.add_filter` method (#861)
|
|
||||||
* Fix `escape`, `url_encode`, `url_decode` not handling non-string values (#898) [Thierry Joyal]
|
|
||||||
* Fix raise when variable is defined but nil when using `strict_variables` [Pascal Betz]
|
|
||||||
* Fix `sort` and `sort_natural` to handle arrays with nils (#930) [Eric Chan]
|
|
||||||
|
|
||||||
## 4.0.0 / 2016-12-14 / branch "4-0-stable"
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
* Render an opaque internal error by default for non-Liquid::Error (#835) [Dylan Thacker-Smith]
|
|
||||||
* Ruby 2.0 support dropped (#832) [Dylan Thacker-Smith]
|
|
||||||
* Add to_number Drop method to allow custom drops to work with number filters (#731)
|
|
||||||
* Add strict_variables and strict_filters options to detect undefined references (#691)
|
|
||||||
* Improve loop performance (#681) [Florian Weingarten]
|
|
||||||
* Rename Drop method `before_method` to `liquid_method_missing` (#661) [Thierry Joyal]
|
|
||||||
* Add url_decode filter to invert url_encode (#645) [Larry Archer]
|
|
||||||
* Add global_filter to apply a filter to all output (#610) [Loren Hale]
|
|
||||||
* Add compact filter (#600) [Carson Reinke]
|
|
||||||
* Rename deprecated "has_key?" and "has_interrupt?" methods (#593) [Florian Weingarten]
|
|
||||||
* Include template name with line numbers in render errors (574) [Dylan Thacker-Smith]
|
|
||||||
* Add sort_natural filter (#554) [Martin Hanzel]
|
|
||||||
* Add forloop.parentloop as a reference to the parent loop (#520) [Justin Li]
|
|
||||||
* Block parsing moved to BlockBody class (#458) [Dylan Thacker-Smith]
|
|
||||||
* Add concat filter to concatenate arrays (#429) [Diogo Beato]
|
|
||||||
* Ruby 1.9 support dropped (#491) [Justin Li]
|
|
||||||
* Liquid::Template.file_system's read_template_file method is no longer passed the context. (#441) [James Reid-Smith]
|
|
||||||
* Remove `liquid_methods` (See https://github.com/Shopify/liquid/pull/568 for replacement)
|
|
||||||
* Liquid::Template.register_filter raises when the module overrides registered public methods as private or protected (#705) [Gaurav Chande]
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
* Fix variable names being detected as an operator when starting with contains (#788) [Michael Angell]
|
|
||||||
* Fix include tag used with strict_variables (#828) [QuickPay]
|
|
||||||
* Fix map filter when value is a Proc (#672) [Guillaume Malette]
|
|
||||||
* Fix truncate filter when value is not a string (#672) [Guillaume Malette]
|
|
||||||
* Fix behaviour of escape filter when input is nil (#665) [Tanel Jakobsoo]
|
|
||||||
* Fix sort filter behaviour with empty array input (#652) [Marcel Cary]
|
|
||||||
* Fix test failure under certain timezones (#631) [Dylan Thacker-Smith]
|
|
||||||
* Fix bug in uniq filter (#595) [Florian Weingarten]
|
|
||||||
* Fix bug when "blank" and "empty" are used as variable names (#592) [Florian Weingarten]
|
|
||||||
* Fix condition parse order in strict mode (#569) [Justin Li]
|
|
||||||
* Fix naming of the "context variable" when dynamically including a template (#559) [Justin Li]
|
|
||||||
* Gracefully accept empty strings in the date filter (#555) [Loren Hale]
|
|
||||||
* Fix capturing into variables with a hyphen in the name (#505) [Florian Weingarten]
|
|
||||||
* Fix case sensitivity regression in date standard filter (#499) [Kelley Reynolds]
|
|
||||||
* Disallow filters with no variable in strict mode (#475) [Justin Li]
|
|
||||||
* Disallow variable names in the strict parser that are not valid in the lax parser (#463) [Justin Li]
|
|
||||||
* Fix BlockBody#warnings taking exponential time to compute (#486) [Justin Li]
|
|
||||||
|
|
||||||
## 3.0.5 / 2015-07-23 / branch "3-0-stable"
|
|
||||||
|
|
||||||
* Fix test failure under certain timezones [Dylan Thacker-Smith]
|
|
||||||
|
|
||||||
## 3.0.4 / 2015-07-17
|
|
||||||
|
|
||||||
* Fix chained access to multi-dimensional hashes [Florian Weingarten]
|
|
||||||
|
|
||||||
## 3.0.3 / 2015-05-28
|
|
||||||
|
|
||||||
* Fix condition parse order in strict mode (#569) [Justin Li]
|
|
||||||
|
|
||||||
## 3.0.2 / 2015-04-24
|
|
||||||
|
|
||||||
* Expose VariableLookup private members (#551) [Justin Li]
|
|
||||||
* Documentation fixes
|
|
||||||
|
|
||||||
## 3.0.1 / 2015-01-23
|
|
||||||
|
|
||||||
* Remove duplicate `index0` key in TableRow tag (#502) [Alfred Xing]
|
|
||||||
|
|
||||||
## 3.0.0 / 2014-11-12
|
|
||||||
|
|
||||||
* Removed Block#end_tag. Instead, override parse with `super` followed by your code. See #446 [Dylan Thacker-Smith]
|
|
||||||
* Fixed condition with wrong data types (#423) [Bogdan Gusiev]
|
|
||||||
* Add url_encode to standard filters (#421) [Derrick Reimer]
|
|
||||||
* Add uniq to standard filters [Florian Weingarten]
|
|
||||||
* Add exception_handler feature (#397) and #254 [Bogdan Gusiev, Florian Weingarten]
|
|
||||||
* Optimize variable parsing to avoid repeated regex evaluation during template rendering #383 [Jason Hiltz-Laforge]
|
|
||||||
* Optimize checking for block interrupts to reduce object allocation #380 [Jason Hiltz-Laforge]
|
|
||||||
* Properly set context rethrow_errors on render! #349 [Thierry Joyal]
|
|
||||||
* Fix broken rendering of variables which are equal to false (#345) [Florian Weingarten]
|
|
||||||
* Remove ActionView template handler [Dylan Thacker-Smith]
|
|
||||||
* Freeze lots of string literals for new Ruby 2.1 optimization (#297) [Florian Weingarten]
|
|
||||||
* Allow newlines in tags and variables (#324) [Dylan Thacker-Smith]
|
|
||||||
* Tag#parse is called after initialize, which now takes options instead of tokens as the 3rd argument. See #321 [Dylan Thacker-Smith]
|
|
||||||
* Raise `Liquid::ArgumentError` instead of `::ArgumentError` when filter has wrong number of arguments #309 [Bogdan Gusiev]
|
|
||||||
* Add a to_s default for liquid drops (#306) [Adam Doeler]
|
|
||||||
* Add strip, lstrip, and rstrip to standard filters [Florian Weingarten]
|
|
||||||
* Make if, for & case tags return complete and consistent nodelists (#250) [Nick Jones]
|
|
||||||
* Prevent arbitrary method invocation on condition objects (#274) [Dylan Thacker-Smith]
|
|
||||||
* Don't call to_sym when creating conditions for security reasons (#273) [Bouke van der Bijl]
|
|
||||||
* Fix resource counting bug with respond_to?(:length) (#263) [Florian Weingarten]
|
|
||||||
* Allow specifying custom patterns for template filenames (#284) [Andrei Gladkyi]
|
|
||||||
* Allow drops to optimize loading a slice of elements (#282) [Tom Burns]
|
|
||||||
* Support for passing variables to snippets in subdirs (#271) [Joost Hietbrink]
|
|
||||||
* Add a class cache to avoid runtime extend calls (#249) [James Tucker]
|
|
||||||
* Remove some legacy Ruby 1.8 compatibility code (#276) [Florian Weingarten]
|
|
||||||
* Add default filter to standard filters (#267) [Derrick Reimer]
|
|
||||||
* Add optional strict parsing and warn parsing (#235) [Tristan Hume]
|
|
||||||
* Add I18n syntax error translation (#241) [Simon Hørup Eskildsen, Sirupsen]
|
|
||||||
* Make sort filter work on enumerable drops (#239) [Florian Weingarten]
|
|
||||||
* Fix clashing method names in enumerable drops (#238) [Florian Weingarten]
|
|
||||||
* Make map filter work on enumerable drops (#233) [Florian Weingarten]
|
|
||||||
* Improved whitespace stripping for blank blocks, related to #216 [Florian Weingarten]
|
|
||||||
|
|
||||||
## 2.6.3 / 2015-07-23 / branch "2-6-stable"
|
|
||||||
|
|
||||||
* Fix test failure under certain timezones [Dylan Thacker-Smith]
|
|
||||||
|
|
||||||
## 2.6.2 / 2015-01-23
|
|
||||||
|
|
||||||
* Remove duplicate hash key [Parker Moore]
|
|
||||||
|
|
||||||
## 2.6.1 / 2014-01-10
|
|
||||||
|
|
||||||
Security fix, cherry-picked from master (4e14a65):
|
|
||||||
* Don't call to_sym when creating conditions for security reasons (#273) [Bouke van der Bijl]
|
|
||||||
* Prevent arbitrary method invocation on condition objects (#274) [Dylan Thacker-Smith]
|
|
||||||
|
|
||||||
## 2.6.0 / 2013-11-25
|
|
||||||
|
|
||||||
IMPORTANT: Liquid 2.6 is going to be the last version of Liquid which maintains explicit Ruby 1.8 compatability.
|
IMPORTANT: Liquid 2.6 is going to be the last version of Liquid which maintains explicit Ruby 1.8 compatability.
|
||||||
The following releases will only be tested against Ruby 1.9 and Ruby 2.0 and are likely to break on Ruby 1.8.
|
The following releases will only be tested against Ruby 1.9 and Ruby 2.0 and are likely to break on Ruby 1.8.
|
||||||
|
|
||||||
* Bugfix for #106: fix example servlet [gnowoel]
|
* Bugfix for #106: fix example servlet [gnowoel]
|
||||||
* Bugfix for #97: strip_html filter supports multi-line tags [Jo Liss]
|
* Bugfix for #97: strip_html filter supports multi-line tags [Jo Liss, joliss]
|
||||||
* Bugfix for #114: strip_html filter supports style tags [James Allardice]
|
* Bugfix for #114: strip_html filter supports style tags [James Allardice, jamesallardice]
|
||||||
* Bugfix for #117: 'now' support for date filter in Ruby 1.9 [Notre Dame Webgroup]
|
* Bugfix for #117: 'now' support for date filter in Ruby 1.9 [Notre Dame Webgroup, ndwebgroup]
|
||||||
* Bugfix for #166: truncate filter on UTF-8 strings with Ruby 1.8 [Florian Weingarten]
|
* Bugfix for #166: truncate filter on UTF-8 strings with Ruby 1.8 [Florian Weingarten, fw42]
|
||||||
* Bugfix for #204: 'raw' parsing bug [Florian Weingarten]
|
* Bugfix for #204: 'raw' parsing bug [Florian Weingarten, fw42]
|
||||||
* Bugfix for #150: 'for' parsing bug [Peter Schröder]
|
* Bugfix for #150: 'for' parsing bug [Peter Schröder, phoet]
|
||||||
* Bugfix for #126: Strip CRLF in strip_newline [Peter Schröder]
|
* Bugfix for #126: Strip CRLF in strip_newline [Peter Schröder, phoet]
|
||||||
* Bugfix for #174, "can't convert Fixnum into String" for "replace" [jsw0528]
|
* Bugfix for #174, "can't convert Fixnum into String" for "replace" [wǒ_is神仙, jsw0528]
|
||||||
* Allow a Liquid::Drop to be passed into Template#render [Daniel Huckstep]
|
* Allow a Liquid::Drop to be passed into Template#render [Daniel Huckstep, darkhelmet]
|
||||||
* Resource limits [Florian Weingarten]
|
* Resource limits [Florian Weingarten, fw42]
|
||||||
* Add reverse filter [Jay Strybis]
|
* Add reverse filter [Jay Strybis, unreal]
|
||||||
* Add utf-8 support
|
* Add utf-8 support
|
||||||
* Use array instead of Hash to keep the registered filters [Tasos Stathopoulos]
|
* Use array instead of Hash to keep the registered filters [Tasos Stathopoulos, astathopoulos]
|
||||||
* Cache tokenized partial templates [Tom Burns]
|
* Cache tokenized partial templates [Tom Burns, boourns]
|
||||||
* Avoid warnings in Ruby 1.9.3 [Marcus Stollsteimer]
|
* Avoid warnings in Ruby 1.9.3 [Marcus Stollsteimer, stomar]
|
||||||
* Better documentation for 'include' tag (closes #163) [Peter Schröder]
|
* Better documentation for 'include' tag (closes #163) [Peter Schröder, phoet]
|
||||||
* Use of BigDecimal on filters to have better precision (closes #155) [Arthur Nogueira Neves]
|
* Use of BigDecimal on filters to have better precision (closes #155) [Arthur Nogueira Neves, arthurnn]
|
||||||
|
|
||||||
## 2.5.5 / 2014-01-10 / branch "2-5-stable"
|
## 2.5.4 / 2013-11-11 / branch "2.5-stable"
|
||||||
|
|
||||||
Security fix, cherry-picked from master (4e14a65):
|
* Fix "can't convert Fixnum into String" for "replace", see #173, [wǒ_is神仙, jsw0528]
|
||||||
* Don't call to_sym when creating conditions for security reasons (#273) [Bouke van der Bijl]
|
|
||||||
* Prevent arbitrary method invocation on condition objects (#274) [Dylan Thacker-Smith]
|
|
||||||
|
|
||||||
## 2.5.4 / 2013-11-11
|
|
||||||
|
|
||||||
* Fix "can't convert Fixnum into String" for "replace" (#173), [jsw0528]
|
|
||||||
|
|
||||||
## 2.5.3 / 2013-10-09
|
## 2.5.3 / 2013-10-09
|
||||||
|
|
||||||
* #232, #234, #237: Fix map filter bugs [Florian Weingarten]
|
* #232, #234, #237: Fix map filter bugs [Florian Weingarten, fw42]
|
||||||
|
|
||||||
## 2.5.2 / 2013-09-03 / deleted
|
## 2.5.2 / 2013-09-03 / deleted
|
||||||
|
|
||||||
@@ -210,7 +61,7 @@ Yanked from rubygems, as it contained too many changes that broke compatibility.
|
|||||||
|
|
||||||
## 2.5.1 / 2013-07-24
|
## 2.5.1 / 2013-07-24
|
||||||
|
|
||||||
* #230: Fix security issue with map filter, Use invoke_drop in map filter [Florian Weingarten]
|
* #230: Fix security issue with map filter, Use invoke_drop in map filter [Florian Weingarten, fw42]
|
||||||
|
|
||||||
## 2.5.0 / 2013-03-06
|
## 2.5.0 / 2013-03-06
|
||||||
|
|
||||||
|
|||||||
37
README.md
37
README.md
@@ -1,6 +1,4 @@
|
|||||||
[](http://travis-ci.org/Shopify/liquid)
|
[](http://travis-ci.org/Shopify/liquid)
|
||||||
[](http://inch-ci.org/github/Shopify/liquid)
|
|
||||||
|
|
||||||
# Liquid template engine
|
# Liquid template engine
|
||||||
|
|
||||||
* [Contributing guidelines](CONTRIBUTING.md)
|
* [Contributing guidelines](CONTRIBUTING.md)
|
||||||
@@ -42,8 +40,6 @@ Liquid is a template engine which was written with very specific requirements:
|
|||||||
|
|
||||||
## How to use Liquid
|
## How to use Liquid
|
||||||
|
|
||||||
Install Liquid by adding `gem 'liquid'` to your gemfile.
|
|
||||||
|
|
||||||
Liquid supports a very simple API based around the Liquid::Template class.
|
Liquid supports a very simple API based around the Liquid::Template class.
|
||||||
For standard use you can just pass it the content of a file and call render with a parameters hash.
|
For standard use you can just pass it the content of a file and call render with a parameters hash.
|
||||||
|
|
||||||
@@ -75,34 +71,3 @@ This is useful for doing things like enabling strict mode only in the theme edit
|
|||||||
|
|
||||||
It is recommended that you enable `:strict` or `:warn` mode on new apps to stop invalid templates from being created.
|
It is recommended that you enable `:strict` or `:warn` mode on new apps to stop invalid templates from being created.
|
||||||
It is also recommended that you use it in the template editors of existing apps to give editors better error messages.
|
It is also recommended that you use it in the template editors of existing apps to give editors better error messages.
|
||||||
|
|
||||||
### Undefined variables and filters
|
|
||||||
|
|
||||||
By default, the renderer doesn't raise or in any other way notify you if some variables or filters are missing, i.e. not passed to the `render` method.
|
|
||||||
You can improve this situation by passing `strict_variables: true` and/or `strict_filters: true` options to the `render` method.
|
|
||||||
When one of these options is set to true, all errors about undefined variables and undefined filters will be stored in `errors` array of a `Liquid::Template` instance.
|
|
||||||
Here are some examples:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
template = Liquid::Template.parse("{{x}} {{y}} {{z.a}} {{z.b}}")
|
|
||||||
template.render({ 'x' => 1, 'z' => { 'a' => 2 } }, { strict_variables: true })
|
|
||||||
#=> '1 2 ' # when a variable is undefined, it's rendered as nil
|
|
||||||
template.errors
|
|
||||||
#=> [#<Liquid::UndefinedVariable: Liquid error: undefined variable y>, #<Liquid::UndefinedVariable: Liquid error: undefined variable b>]
|
|
||||||
```
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
template = Liquid::Template.parse("{{x | filter1 | upcase}}")
|
|
||||||
template.render({ 'x' => 'foo' }, { strict_filters: true })
|
|
||||||
#=> '' # when at least one filter in the filter chain is undefined, a whole expression is rendered as nil
|
|
||||||
template.errors
|
|
||||||
#=> [#<Liquid::UndefinedFilter: Liquid error: undefined filter filter1>]
|
|
||||||
```
|
|
||||||
|
|
||||||
If you want to raise on a first exception instead of pushing all of them in `errors`, you can use `render!` method:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
template = Liquid::Template.parse("{{x}} {{y}}")
|
|
||||||
template.render!({ 'x' => 1}, { strict_variables: true })
|
|
||||||
#=> Liquid::UndefinedVariable: Liquid error: undefined variable y
|
|
||||||
```
|
|
||||||
|
|||||||
51
Rakefile
51
Rakefile
@@ -1,14 +1,15 @@
|
|||||||
require 'rake'
|
require 'rake'
|
||||||
require 'rake/testtask'
|
require 'rake/testtask'
|
||||||
|
require 'rake/extensiontask'
|
||||||
$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'
|
||||||
|
|
||||||
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/liquid/**/*_test.rb']
|
||||||
t.verbose = false
|
t.verbose = false
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -18,43 +19,25 @@ task :warn_test do
|
|||||||
Rake::Task['base_test'].invoke
|
Rake::Task['base_test'].invoke
|
||||||
end
|
end
|
||||||
|
|
||||||
task :rubocop do
|
|
||||||
require 'rubocop/rake_task'
|
|
||||||
RuboCop::RakeTask.new
|
|
||||||
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
|
||||||
|
|
||||||
ENV['LIQUID_PARSER_MODE'] = 'strict'
|
ENV['LIQUID_PARSER_MODE'] = 'strict'
|
||||||
Rake::Task['base_test'].reenable
|
Rake::Task['base_test'].reenable
|
||||||
Rake::Task['base_test'].invoke
|
Rake::Task['base_test'].invoke
|
||||||
|
|
||||||
if RUBY_ENGINE == 'ruby'
|
|
||||||
ENV['LIQUID-C'] = '1'
|
|
||||||
|
|
||||||
ENV['LIQUID_PARSER_MODE'] = 'lax'
|
|
||||||
Rake::Task['base_test'].reenable
|
|
||||||
Rake::Task['base_test'].invoke
|
|
||||||
|
|
||||||
ENV['LIQUID_PARSER_MODE'] = 'strict'
|
|
||||||
Rake::Task['base_test'].reenable
|
|
||||||
Rake::Task['base_test'].invoke
|
|
||||||
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
|
||||||
|
|
||||||
task install: :build do
|
task :install => :build do
|
||||||
system "gem install liquid-#{Liquid::VERSION}.gem"
|
system "gem install liquid-#{Liquid::VERSION}.gem"
|
||||||
end
|
end
|
||||||
|
|
||||||
task release: :build do
|
task :release => :build do
|
||||||
system "git tag -a v#{Liquid::VERSION} -m 'Tagging #{Liquid::VERSION}'"
|
system "git tag -a v#{Liquid::VERSION} -m 'Tagging #{Liquid::VERSION}'"
|
||||||
system "git push --tags"
|
system "git push --tags"
|
||||||
system "gem push liquid-#{Liquid::VERSION}.gem"
|
system "gem push liquid-#{Liquid::VERSION}.gem"
|
||||||
@@ -62,6 +45,7 @@ task release: :build do
|
|||||||
end
|
end
|
||||||
|
|
||||||
namespace :benchmark do
|
namespace :benchmark do
|
||||||
|
|
||||||
desc "Run the liquid benchmark with lax parsing"
|
desc "Run the liquid benchmark with lax parsing"
|
||||||
task :run do
|
task :run do
|
||||||
ruby "./performance/benchmark.rb lax"
|
ruby "./performance/benchmark.rb lax"
|
||||||
@@ -73,23 +57,23 @@ namespace :benchmark do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
namespace :profile do
|
namespace :profile do
|
||||||
|
|
||||||
desc "Run the liquid profile/performance coverage"
|
desc "Run the liquid profile/performance coverage"
|
||||||
task :run do
|
task :run do
|
||||||
ruby "./performance/profile.rb"
|
ruby "./performance/profile.rb"
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "Run the liquid profile/performance coverage with strict parsing"
|
task :stackprof do
|
||||||
task :strict do
|
ruby "./performance/stackprof.rb"
|
||||||
ruby "./performance/profile.rb strict"
|
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
namespace :memory_profile do
|
desc "Run KCacheGrind"
|
||||||
desc "Run memory profiler"
|
task :grind => :run do
|
||||||
task :run do
|
system "qcachegrind /tmp/liquid.rubyprof_calltreeprinter.txt"
|
||||||
ruby "./performance/memory_profile.rb"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "Run example"
|
desc "Run example"
|
||||||
@@ -97,6 +81,7 @@ task :example do
|
|||||||
ruby "-w -d -Ilib example/server/server.rb"
|
ruby "-w -d -Ilib example/server/server.rb"
|
||||||
end
|
end
|
||||||
|
|
||||||
task :console do
|
Rake::ExtensionTask.new "liquid" do |ext|
|
||||||
exec 'irb -I lib -r liquid'
|
ext.lib_dir = "lib/liquid"
|
||||||
end
|
end
|
||||||
|
Rake::Task[:test].prerequisites << :compile
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ module ProductsFilter
|
|||||||
end
|
end
|
||||||
|
|
||||||
def prettyprint(text)
|
def prettyprint(text)
|
||||||
text.gsub(/\*(.*)\*/, '<b>\1</b>')
|
text.gsub( /\*(.*)\*/, '<b>\1</b>' )
|
||||||
end
|
end
|
||||||
|
|
||||||
def count(array)
|
def count(array)
|
||||||
@@ -17,28 +17,25 @@ module ProductsFilter
|
|||||||
end
|
end
|
||||||
|
|
||||||
class Servlet < LiquidServlet
|
class Servlet < LiquidServlet
|
||||||
|
|
||||||
def index
|
def index
|
||||||
{ 'date' => Time.now }
|
{ 'date' => Time.now }
|
||||||
end
|
end
|
||||||
|
|
||||||
def products
|
def products
|
||||||
{ 'products' => products_list, 'more_products' => more_products_list, 'description' => description, 'section' => 'Snowboards', 'cool_products' => true }
|
{ 'products' => products_list, 'description' => description, 'section' => 'Snowboards', 'cool_products' => true}
|
||||||
end
|
end
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
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 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
|
||||||
"List of Products ~ This is a list of products with price and description."
|
"List of Products ~ This is a list of products with price and description."
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
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)
|
||||||
end
|
end
|
||||||
@@ -10,8 +11,7 @@ class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet
|
|||||||
private
|
private
|
||||||
|
|
||||||
def handle(type, req, res)
|
def handle(type, req, res)
|
||||||
@request = req
|
@request, @response = req, res
|
||||||
@response = res
|
|
||||||
|
|
||||||
@request.path_info =~ /(\w+)\z/
|
@request.path_info =~ /(\w+)\z/
|
||||||
@action = $1 || 'index'
|
@action = $1 || 'index'
|
||||||
@@ -19,10 +19,10 @@ class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet
|
|||||||
|
|
||||||
@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)
|
||||||
File.read("#{__dir__}/templates/#{filename}.liquid")
|
File.read( File.dirname(__FILE__) + "/templates/#{filename}.liquid" )
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
require 'webrick'
|
require 'webrick'
|
||||||
require 'rexml/document'
|
require 'rexml/document'
|
||||||
|
|
||||||
require_relative '../../lib/liquid'
|
DIR = File.expand_path(File.dirname(__FILE__))
|
||||||
require_relative 'liquid_servlet'
|
|
||||||
require_relative 'example_servlet'
|
require DIR + '/../../lib/liquid'
|
||||||
|
require DIR + '/liquid_servlet'
|
||||||
|
require DIR + '/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
|
||||||
|
|||||||
@@ -3,4 +3,4 @@
|
|||||||
<p>It is {{date}}</p>
|
<p>It is {{date}}</p>
|
||||||
|
|
||||||
|
|
||||||
<p>Check out the <a href="/products">Products</a> screen </p>
|
<p>Check out the <a href="http://localhost:3000/products">Products</a> screen </p>
|
||||||
|
|||||||
@@ -16,12 +16,12 @@
|
|||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
{% assign all_products = products | concat: more_products %}
|
|
||||||
<h1>{{ description | split: '~' | first }}</h1>
|
<h1>{{ description | split: '~' | first }}</h1>
|
||||||
|
|
||||||
<h2>{{ description | split: '~' | last }}</h2>
|
<h2>{{ description | split: '~' | last }}</h2>
|
||||||
|
|
||||||
<h2>There are currently {{all_products | count}} products in the {{section}} catalog</h2>
|
<h2>There are currently {{products | count}} products in the {{section}} catalog</h2>
|
||||||
|
|
||||||
{% if cool_products %}
|
{% if cool_products %}
|
||||||
Cool products :)
|
Cool products :)
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
<ul id="products">
|
<ul id="products">
|
||||||
|
|
||||||
{% for product in all_products %}
|
{% for product in products %}
|
||||||
<li>
|
<li>
|
||||||
<h2>{{product.name}}</h2>
|
<h2>{{product.name}}</h2>
|
||||||
Only {{product.price | price }}
|
Only {{product.price | price }}
|
||||||
|
|||||||
167
ext/liquid/block.c
Normal file
167
ext/liquid/block.c
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
#include "liquid_ext.h"
|
||||||
|
|
||||||
|
VALUE cLiquidBlock;
|
||||||
|
ID intern_assert_missing_delimitation, intern_block_delimiter, intern_is_blank,
|
||||||
|
intern_new_with_options, intern_tags, intern_unknown_tag, intern_unterminated_tag,
|
||||||
|
intern_unterminated_variable;
|
||||||
|
|
||||||
|
struct liquid_tag
|
||||||
|
{
|
||||||
|
char *name, *markup;
|
||||||
|
long name_length, markup_length;
|
||||||
|
};
|
||||||
|
|
||||||
|
static bool parse_tag(struct liquid_tag *tag, char *token, long token_length)
|
||||||
|
{
|
||||||
|
// Strip {{ and }} braces
|
||||||
|
token += 2;
|
||||||
|
token_length -= 4;
|
||||||
|
|
||||||
|
char *end = token + token_length;
|
||||||
|
while (token < end && isspace(*token))
|
||||||
|
token++;
|
||||||
|
tag->name = token;
|
||||||
|
|
||||||
|
char c = *token;
|
||||||
|
while (token < end && (isalnum(c) || c == '_'))
|
||||||
|
c = *(++token);
|
||||||
|
tag->name_length = token - tag->name;
|
||||||
|
if (!tag->name_length) {
|
||||||
|
memset(tag, 0, sizeof(*tag));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (token < end && isspace(*token))
|
||||||
|
token++;
|
||||||
|
tag->markup = token;
|
||||||
|
|
||||||
|
char *last = end - 1;
|
||||||
|
while (token < last && isspace(*last))
|
||||||
|
last--;
|
||||||
|
end = last + 1;
|
||||||
|
tag->markup_length = end - token;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static VALUE rb_parse_body(VALUE self, VALUE tokenizerObj)
|
||||||
|
{
|
||||||
|
struct liquid_tokenizer *tokenizer = LIQUID_TOKENIZER_GET_STRUCT(tokenizerObj);
|
||||||
|
|
||||||
|
bool blank = true;
|
||||||
|
VALUE nodelist = rb_iv_get(self, "@nodelist");
|
||||||
|
if (nodelist == Qnil) {
|
||||||
|
nodelist = rb_ary_new();
|
||||||
|
rb_iv_set(self, "@nodelist", nodelist);
|
||||||
|
} else {
|
||||||
|
rb_ary_clear(nodelist);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct token token;
|
||||||
|
while (true) {
|
||||||
|
liquid_tokenizer_next(tokenizer, &token);
|
||||||
|
switch (token.type) {
|
||||||
|
case TOKEN_NONE:
|
||||||
|
/*
|
||||||
|
* Make sure that it's ok to end parsing in the current block.
|
||||||
|
* Effectively this method will throw an exception unless the current block is
|
||||||
|
* of type Document
|
||||||
|
*/
|
||||||
|
rb_funcall(self, intern_assert_missing_delimitation, 0);
|
||||||
|
goto done;
|
||||||
|
case TOKEN_INVALID:
|
||||||
|
{
|
||||||
|
VALUE token_obj = rb_str_new(token.str, token.length);
|
||||||
|
if (token.str[1] == '%')
|
||||||
|
rb_funcall(self, intern_unterminated_tag, 1, token_obj);
|
||||||
|
else
|
||||||
|
rb_funcall(self, intern_unterminated_variable, 1, token_obj);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TOKEN_TAG:
|
||||||
|
{
|
||||||
|
struct liquid_tag tag;
|
||||||
|
if (!parse_tag(&tag, token.str, token.length)) {
|
||||||
|
// FIXME: provide more appropriate error message
|
||||||
|
rb_funcall(self, intern_unterminated_tag, 1, rb_str_new(token.str, token.length));
|
||||||
|
} else {
|
||||||
|
if (tag.name_length >= 3 && !memcmp(tag.name, "end", 3)) {
|
||||||
|
VALUE block_delimiter = rb_funcall(self, intern_block_delimiter, 0);
|
||||||
|
if (TYPE(block_delimiter) == T_STRING &&
|
||||||
|
tag.name_length == RSTRING_LEN(block_delimiter) &&
|
||||||
|
!memcmp(tag.name, RSTRING_PTR(block_delimiter), tag.name_length))
|
||||||
|
{
|
||||||
|
goto done;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VALUE tags = rb_funcall(cLiquidTemplate, intern_tags, 0);
|
||||||
|
Check_Type(tags, T_HASH);
|
||||||
|
VALUE tag_name = rb_str_new(tag.name, tag.name_length);
|
||||||
|
VALUE tag_class = rb_hash_lookup(tags, tag_name);
|
||||||
|
VALUE markup = rb_str_new(tag.markup, tag.markup_length);
|
||||||
|
if (tag_class != Qnil) {
|
||||||
|
VALUE options = rb_iv_get(self, "@options");
|
||||||
|
if (options == Qnil)
|
||||||
|
options = rb_hash_new();
|
||||||
|
VALUE new_tag = rb_funcall(tag_class, intern_new_with_options, 4,
|
||||||
|
tag_name, markup, tokenizerObj, options);
|
||||||
|
if (blank) {
|
||||||
|
VALUE blank_block = rb_funcall(new_tag, intern_is_blank, 0);
|
||||||
|
if (blank_block == Qnil || blank_block == Qfalse)
|
||||||
|
blank = false;
|
||||||
|
}
|
||||||
|
rb_ary_push(nodelist, new_tag);
|
||||||
|
} else {
|
||||||
|
rb_funcall(self, intern_unknown_tag, 3, tag_name, markup, tokenizerObj);
|
||||||
|
/*
|
||||||
|
* multi-block tags may store the nodelist in a block array on unknown_tag
|
||||||
|
* then replace @nodelist with a new array. We need to use the new array
|
||||||
|
* for the block following the tag token.
|
||||||
|
*/
|
||||||
|
nodelist = rb_iv_get(self, "@nodelist");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TOKEN_VARIABLE:
|
||||||
|
{
|
||||||
|
VALUE markup = rb_str_new(token.str + 2, token.length - 4);
|
||||||
|
VALUE options = rb_iv_get(self, "@options");
|
||||||
|
VALUE new_var = rb_funcall(cLiquidVariable, intern_new, 2, markup, options);
|
||||||
|
rb_ary_push(nodelist, new_var);
|
||||||
|
blank = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case TOKEN_STRING:
|
||||||
|
rb_ary_push(nodelist, liquid_string_slice_new(token.str, token.length));
|
||||||
|
if (blank) {
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < token.length; i++) {
|
||||||
|
if (!isspace(token.str[i])) {
|
||||||
|
blank = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
done:
|
||||||
|
rb_iv_set(self, "@blank", blank ? Qtrue : Qfalse);
|
||||||
|
return Qnil;
|
||||||
|
}
|
||||||
|
|
||||||
|
void init_liquid_block()
|
||||||
|
{
|
||||||
|
intern_assert_missing_delimitation = rb_intern("assert_missing_delimitation!");
|
||||||
|
intern_block_delimiter = rb_intern("block_delimiter");
|
||||||
|
intern_is_blank = rb_intern("blank?");
|
||||||
|
intern_new_with_options = rb_intern("new_with_options");
|
||||||
|
intern_tags = rb_intern("tags");
|
||||||
|
intern_unknown_tag = rb_intern("unknown_tag");
|
||||||
|
intern_unterminated_tag = rb_intern("unterminated_tag");
|
||||||
|
intern_unterminated_variable = rb_intern("unterminated_variable");
|
||||||
|
|
||||||
|
cLiquidBlock = rb_define_class_under(mLiquid, "Block", cLiquidTag);
|
||||||
|
rb_define_method(cLiquidBlock, "parse_body", rb_parse_body, 1);
|
||||||
|
}
|
||||||
8
ext/liquid/block.h
Normal file
8
ext/liquid/block.h
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#ifndef LIQUID_BLOCK_H
|
||||||
|
#define LIQUID_BLOCK_H
|
||||||
|
|
||||||
|
void init_liquid_block();
|
||||||
|
|
||||||
|
extern VALUE cLiquidBlock;
|
||||||
|
|
||||||
|
#endif
|
||||||
3
ext/liquid/extconf.rb
Normal file
3
ext/liquid/extconf.rb
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
require 'mkmf'
|
||||||
|
$CFLAGS << ' -Wall'
|
||||||
|
create_makefile("liquid/liquid")
|
||||||
18
ext/liquid/liquid_ext.c
Normal file
18
ext/liquid/liquid_ext.c
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
#include "liquid_ext.h"
|
||||||
|
|
||||||
|
VALUE mLiquid;
|
||||||
|
VALUE cLiquidTemplate, cLiquidTag, cLiquidVariable;
|
||||||
|
ID intern_new;
|
||||||
|
|
||||||
|
void Init_liquid(void)
|
||||||
|
{
|
||||||
|
intern_new = rb_intern("new");
|
||||||
|
mLiquid = rb_define_module("Liquid");
|
||||||
|
cLiquidTemplate = rb_define_class_under(mLiquid, "Template", rb_cObject);
|
||||||
|
cLiquidTag = rb_define_class_under(mLiquid, "Tag", rb_cObject);
|
||||||
|
cLiquidVariable = rb_define_class_under(mLiquid, "Variable", rb_cObject);
|
||||||
|
|
||||||
|
init_liquid_tokenizer();
|
||||||
|
init_liquid_block();
|
||||||
|
init_liquid_string_slice();
|
||||||
|
}
|
||||||
17
ext/liquid/liquid_ext.h
Normal file
17
ext/liquid/liquid_ext.h
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
#ifndef LIQUID_EXT_H
|
||||||
|
#define LIQUID_EXT_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <ruby.h>
|
||||||
|
|
||||||
|
#include "tokenizer.h"
|
||||||
|
#include "block.h"
|
||||||
|
#include "slice.h"
|
||||||
|
#include "utils.h"
|
||||||
|
|
||||||
|
extern ID intern_new;
|
||||||
|
extern VALUE mLiquid;
|
||||||
|
extern VALUE cLiquidTemplate, cLiquidTag, cLiquidVariable;
|
||||||
|
|
||||||
|
#endif
|
||||||
167
ext/liquid/slice.c
Normal file
167
ext/liquid/slice.c
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
#include "liquid_ext.h"
|
||||||
|
|
||||||
|
VALUE cLiquidStringSlice;
|
||||||
|
|
||||||
|
static void mark_slice(void *ptr)
|
||||||
|
{
|
||||||
|
if (!ptr)
|
||||||
|
return;
|
||||||
|
struct string_slice *slice = ptr;
|
||||||
|
rb_gc_mark(slice->source);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void free_slice(void *ptr)
|
||||||
|
{
|
||||||
|
struct string_slice *slice = ptr;
|
||||||
|
xfree(slice);
|
||||||
|
}
|
||||||
|
|
||||||
|
VALUE liquid_string_slice_new(const char *str, long length)
|
||||||
|
{
|
||||||
|
return rb_funcall(cLiquidStringSlice, intern_new, 3, rb_str_new(str, length), INT2FIX(0), INT2FIX(length));
|
||||||
|
}
|
||||||
|
|
||||||
|
static VALUE rb_allocate(VALUE klass)
|
||||||
|
{
|
||||||
|
struct string_slice *slice;
|
||||||
|
VALUE obj = Data_Make_Struct(klass, struct string_slice, mark_slice, free_slice, slice);
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
static VALUE rb_initialize(VALUE self, VALUE source, VALUE offset_value, VALUE length_value)
|
||||||
|
{
|
||||||
|
long offset = rb_fix2int(offset_value);
|
||||||
|
long length = rb_fix2int(length_value);
|
||||||
|
if (length < 0)
|
||||||
|
rb_raise(rb_eArgError, "negative string length");
|
||||||
|
if (offset < 0)
|
||||||
|
rb_raise(rb_eArgError, "negative string offset");
|
||||||
|
|
||||||
|
if (TYPE(source) == T_DATA && RBASIC_CLASS(source) == cLiquidStringSlice) {
|
||||||
|
struct string_slice *source_slice = DATA_PTR(source);
|
||||||
|
source = source_slice->source;
|
||||||
|
offset += source_slice->str - RSTRING_PTR(source);
|
||||||
|
} else {
|
||||||
|
source = rb_string_value(&source);
|
||||||
|
source = rb_str_dup_frozen(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct string_slice *slice;
|
||||||
|
Data_Get_Struct(self, struct string_slice, slice);
|
||||||
|
slice->source = source;
|
||||||
|
slice->str = RSTRING_PTR(source) + offset;
|
||||||
|
slice->length = length;
|
||||||
|
if (length > RSTRING_LEN(source) - offset)
|
||||||
|
rb_raise(rb_eArgError, "slice bounds outside source string bounds");
|
||||||
|
|
||||||
|
return Qnil;
|
||||||
|
}
|
||||||
|
|
||||||
|
static VALUE rb_slice_to_str(VALUE self)
|
||||||
|
{
|
||||||
|
struct string_slice *slice;
|
||||||
|
Data_Get_Struct(self, struct string_slice, slice);
|
||||||
|
|
||||||
|
VALUE source = slice->source;
|
||||||
|
if (slice->str == RSTRING_PTR(source) && slice->length == RSTRING_LEN(source))
|
||||||
|
return source;
|
||||||
|
|
||||||
|
source = rb_str_new(slice->str, slice->length);
|
||||||
|
slice->source = source;
|
||||||
|
slice->str = RSTRING_PTR(source);
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
|
||||||
|
static VALUE rb_slice_slice(VALUE self, VALUE offset, VALUE length)
|
||||||
|
{
|
||||||
|
return rb_funcall(cLiquidStringSlice, intern_new, 3, self, offset, length);
|
||||||
|
}
|
||||||
|
|
||||||
|
static VALUE rb_slice_length(VALUE self)
|
||||||
|
{
|
||||||
|
struct string_slice *slice;
|
||||||
|
Data_Get_Struct(self, struct string_slice, slice);
|
||||||
|
return INT2FIX(slice->length);
|
||||||
|
}
|
||||||
|
|
||||||
|
static VALUE rb_slice_equal(VALUE self, VALUE other)
|
||||||
|
{
|
||||||
|
struct string_slice *this_slice;
|
||||||
|
Data_Get_Struct(self, struct string_slice, this_slice);
|
||||||
|
|
||||||
|
const char *other_str;
|
||||||
|
long other_length;
|
||||||
|
if (TYPE(other) == T_DATA && RBASIC_CLASS(other) == cLiquidStringSlice) {
|
||||||
|
struct string_slice *other_slice = DATA_PTR(other);
|
||||||
|
other_str = other_slice->str;
|
||||||
|
other_length = other_slice->length;
|
||||||
|
} else {
|
||||||
|
other = rb_string_value(&other);
|
||||||
|
other_length = RSTRING_LEN(other);
|
||||||
|
other_str = RSTRING_PTR(other);
|
||||||
|
}
|
||||||
|
bool equal = this_slice->length == other_length && !memcmp(this_slice->str, other_str, other_length);
|
||||||
|
return equal ? Qtrue : Qfalse;
|
||||||
|
}
|
||||||
|
|
||||||
|
static VALUE rb_slice_inspect(VALUE self)
|
||||||
|
{
|
||||||
|
VALUE quoted = rb_str_inspect(rb_slice_to_str(self));
|
||||||
|
return rb_sprintf("#<Liquid::StringSlice: %.*s>", (int)RSTRING_LEN(quoted), RSTRING_PTR(quoted));
|
||||||
|
}
|
||||||
|
|
||||||
|
static VALUE rb_slice_join(VALUE klass, VALUE ary)
|
||||||
|
{
|
||||||
|
ary = rb_ary_to_ary(ary);
|
||||||
|
|
||||||
|
long i;
|
||||||
|
long result_length = 0;
|
||||||
|
for (i = 0; i < RARRAY_LEN(ary); i++) {
|
||||||
|
VALUE element = RARRAY_AREF(ary, i);
|
||||||
|
|
||||||
|
if (TYPE(element) == T_DATA && RBASIC_CLASS(element) == cLiquidStringSlice) {
|
||||||
|
struct string_slice *slice = DATA_PTR(element);
|
||||||
|
result_length += slice->length;
|
||||||
|
} else if (TYPE(element) == T_STRING) {
|
||||||
|
result_length += RSTRING_LEN(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VALUE result = rb_str_buf_new(result_length);
|
||||||
|
for (i = 0; i < RARRAY_LEN(ary); i++) {
|
||||||
|
VALUE element = RARRAY_AREF(ary, i);
|
||||||
|
|
||||||
|
const char *element_string;
|
||||||
|
long element_length;
|
||||||
|
if (TYPE(element) == T_DATA && RBASIC_CLASS(element) == cLiquidStringSlice) {
|
||||||
|
struct string_slice *slice = DATA_PTR(element);
|
||||||
|
element_string = slice->str;
|
||||||
|
element_length = slice->length;
|
||||||
|
} else if (NIL_P(element)) {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
element = rb_check_string_type(element);
|
||||||
|
if (NIL_P(element))
|
||||||
|
continue;
|
||||||
|
element_string = RSTRING_PTR(element);
|
||||||
|
element_length = RSTRING_LEN(element);
|
||||||
|
}
|
||||||
|
rb_str_buf_cat(result, element_string, element_length);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void init_liquid_string_slice()
|
||||||
|
{
|
||||||
|
cLiquidStringSlice = rb_define_class_under(mLiquid, "StringSlice", rb_cObject);
|
||||||
|
rb_define_singleton_method(cLiquidStringSlice, "join", rb_slice_join, 1);
|
||||||
|
rb_define_alloc_func(cLiquidStringSlice, rb_allocate);
|
||||||
|
rb_define_method(cLiquidStringSlice, "initialize", rb_initialize, 3);
|
||||||
|
rb_define_method(cLiquidStringSlice, "==", rb_slice_equal, 1);
|
||||||
|
rb_define_method(cLiquidStringSlice, "length", rb_slice_length, 0);
|
||||||
|
rb_define_alias(cLiquidStringSlice, "size", "length");
|
||||||
|
rb_define_method(cLiquidStringSlice, "slice", rb_slice_slice, 2);
|
||||||
|
rb_define_method(cLiquidStringSlice, "to_str", rb_slice_to_str, 0);
|
||||||
|
rb_define_alias(cLiquidStringSlice, "to_s", "to_str");
|
||||||
|
rb_define_method(cLiquidStringSlice, "inspect", rb_slice_inspect, 0);
|
||||||
|
}
|
||||||
18
ext/liquid/slice.h
Normal file
18
ext/liquid/slice.h
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
#ifndef LIQUID_SLICE_H
|
||||||
|
#define LIQUID_SLICE_H
|
||||||
|
|
||||||
|
extern VALUE cLiquidStringSlice;
|
||||||
|
|
||||||
|
struct string_slice {
|
||||||
|
VALUE source;
|
||||||
|
const char *str;
|
||||||
|
long length;
|
||||||
|
};
|
||||||
|
|
||||||
|
VALUE liquid_string_slice_new(const char *str, long length);
|
||||||
|
|
||||||
|
void init_liquid_string_slice();
|
||||||
|
|
||||||
|
#define STRING_SLICE_GET_STRUCT(obj) ((struct string_slice *)obj_get_data_ptr(obj, cLiquidStringSlice))
|
||||||
|
|
||||||
|
#endif
|
||||||
113
ext/liquid/tokenizer.c
Normal file
113
ext/liquid/tokenizer.c
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
#include "liquid_ext.h"
|
||||||
|
|
||||||
|
VALUE cLiquidTokenizer;
|
||||||
|
|
||||||
|
static void free_tokenizer(void *ptr)
|
||||||
|
{
|
||||||
|
struct liquid_tokenizer *tokenizer = ptr;
|
||||||
|
xfree(tokenizer);
|
||||||
|
}
|
||||||
|
|
||||||
|
static VALUE rb_allocate(VALUE klass)
|
||||||
|
{
|
||||||
|
VALUE obj;
|
||||||
|
struct liquid_tokenizer *tokenizer;
|
||||||
|
|
||||||
|
obj = Data_Make_Struct(klass, struct liquid_tokenizer, NULL, free_tokenizer, tokenizer);
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
static VALUE rb_initialize(VALUE self, VALUE source)
|
||||||
|
{
|
||||||
|
struct liquid_tokenizer *tokenizer;
|
||||||
|
|
||||||
|
source = rb_string_value(&source);
|
||||||
|
Data_Get_Struct(self, struct liquid_tokenizer, tokenizer);
|
||||||
|
tokenizer->cursor = RSTRING_PTR(source);
|
||||||
|
tokenizer->length = RSTRING_LEN(source);
|
||||||
|
return Qnil;
|
||||||
|
}
|
||||||
|
|
||||||
|
void liquid_tokenizer_next(struct liquid_tokenizer *tokenizer, struct token *token)
|
||||||
|
{
|
||||||
|
if (tokenizer->length <= 0) {
|
||||||
|
memset(token, 0, sizeof(*token));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
token->type = TOKEN_STRING;
|
||||||
|
|
||||||
|
char *cursor = tokenizer->cursor;
|
||||||
|
char *last = tokenizer->cursor + tokenizer->length - 1;
|
||||||
|
|
||||||
|
while (cursor < last) {
|
||||||
|
if (*cursor++ != '{')
|
||||||
|
continue;
|
||||||
|
|
||||||
|
char c = *cursor++;
|
||||||
|
if (c != '%' && c != '{')
|
||||||
|
continue;
|
||||||
|
if (cursor - tokenizer->cursor > 2) {
|
||||||
|
token->type = TOKEN_STRING;
|
||||||
|
cursor -= 2;
|
||||||
|
goto found;
|
||||||
|
}
|
||||||
|
char *incomplete_end = cursor;
|
||||||
|
token->type = TOKEN_INVALID;
|
||||||
|
if (c == '%') {
|
||||||
|
while (cursor < last) {
|
||||||
|
if (*cursor++ != '%')
|
||||||
|
continue;
|
||||||
|
c = *cursor++;
|
||||||
|
while (c == '%' && cursor <= last)
|
||||||
|
c = *cursor++;
|
||||||
|
if (c != '}')
|
||||||
|
continue;
|
||||||
|
token->type = TOKEN_TAG;
|
||||||
|
goto found;
|
||||||
|
}
|
||||||
|
cursor = incomplete_end;
|
||||||
|
goto found;
|
||||||
|
} else {
|
||||||
|
while (cursor < last) {
|
||||||
|
if (*cursor++ != '}')
|
||||||
|
continue;
|
||||||
|
if (*cursor++ != '}') {
|
||||||
|
incomplete_end = cursor - 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
token->type = TOKEN_VARIABLE;
|
||||||
|
goto found;
|
||||||
|
}
|
||||||
|
cursor = incomplete_end;
|
||||||
|
goto found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cursor = last + 1;
|
||||||
|
found:
|
||||||
|
token->str = tokenizer->cursor;
|
||||||
|
token->length = cursor - tokenizer->cursor;
|
||||||
|
tokenizer->cursor += token->length;
|
||||||
|
tokenizer->length -= token->length;
|
||||||
|
}
|
||||||
|
|
||||||
|
static VALUE rb_next(VALUE self)
|
||||||
|
{
|
||||||
|
struct liquid_tokenizer *tokenizer;
|
||||||
|
Data_Get_Struct(self, struct liquid_tokenizer, tokenizer);
|
||||||
|
|
||||||
|
struct token token;
|
||||||
|
liquid_tokenizer_next(tokenizer, &token);
|
||||||
|
if (token.type == TOKEN_NONE)
|
||||||
|
return Qnil;
|
||||||
|
|
||||||
|
return rb_str_new(token.str, token.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
void init_liquid_tokenizer()
|
||||||
|
{
|
||||||
|
cLiquidTokenizer = rb_define_class_under(mLiquid, "Tokenizer", rb_cObject);
|
||||||
|
rb_define_alloc_func(cLiquidTokenizer, rb_allocate);
|
||||||
|
rb_define_method(cLiquidTokenizer, "initialize", rb_initialize, 1);
|
||||||
|
rb_define_method(cLiquidTokenizer, "next", rb_next, 0);
|
||||||
|
rb_define_alias(cLiquidTokenizer, "shift", "next");
|
||||||
|
}
|
||||||
30
ext/liquid/tokenizer.h
Normal file
30
ext/liquid/tokenizer.h
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#ifndef LIQUID_TOKENIZER_H
|
||||||
|
#define LIQUID_TOKENIZER_H
|
||||||
|
|
||||||
|
extern VALUE cLiquidTokenizer;
|
||||||
|
|
||||||
|
enum token_type {
|
||||||
|
TOKEN_NONE,
|
||||||
|
TOKEN_INVALID,
|
||||||
|
TOKEN_STRING,
|
||||||
|
TOKEN_TAG,
|
||||||
|
TOKEN_VARIABLE
|
||||||
|
};
|
||||||
|
|
||||||
|
struct token {
|
||||||
|
enum token_type type;
|
||||||
|
char *str;
|
||||||
|
int length;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct liquid_tokenizer {
|
||||||
|
char *cursor;
|
||||||
|
int length;
|
||||||
|
};
|
||||||
|
|
||||||
|
void init_liquid_tokenizer();
|
||||||
|
void liquid_tokenizer_next(struct liquid_tokenizer *tokenizer, struct token *token);
|
||||||
|
|
||||||
|
#define LIQUID_TOKENIZER_GET_STRUCT(obj) ((struct liquid_tokenizer *)obj_get_data_ptr(obj, cLiquidTokenizer))
|
||||||
|
|
||||||
|
#endif
|
||||||
21
ext/liquid/utils.c
Normal file
21
ext/liquid/utils.c
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
#include <ruby.h>
|
||||||
|
|
||||||
|
void raise_type_error(VALUE expected, VALUE got)
|
||||||
|
{
|
||||||
|
rb_raise(rb_eTypeError, "wrong argument type %s (expected %s)",
|
||||||
|
rb_class2name(got), rb_class2name(expected));
|
||||||
|
}
|
||||||
|
|
||||||
|
void check_class(VALUE obj, int type, VALUE klass)
|
||||||
|
{
|
||||||
|
Check_Type(obj, type);
|
||||||
|
VALUE obj_klass = RBASIC_CLASS(obj);
|
||||||
|
if (obj_klass != klass)
|
||||||
|
raise_type_error(klass, obj_klass);
|
||||||
|
}
|
||||||
|
|
||||||
|
void *obj_get_data_ptr(VALUE obj, VALUE klass)
|
||||||
|
{
|
||||||
|
check_class(obj, T_DATA, klass);
|
||||||
|
return DATA_PTR(obj);
|
||||||
|
}
|
||||||
8
ext/liquid/utils.h
Normal file
8
ext/liquid/utils.h
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
#ifndef LIQUID_UTILS_H
|
||||||
|
#define LIQUID_UTILS_H
|
||||||
|
|
||||||
|
void raise_type_error(VALUE expected, VALUE got);
|
||||||
|
void check_class(VALUE klass);
|
||||||
|
void *obj_get_data_ptr(VALUE obj, VALUE klass);
|
||||||
|
|
||||||
|
#endif
|
||||||
8
init.rb
Normal file
8
init.rb
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
require 'liquid'
|
||||||
|
require 'extras/liquid_view'
|
||||||
|
|
||||||
|
if defined? ActionView::Template and ActionView::Template.respond_to? :register_template_handler
|
||||||
|
ActionView::Template
|
||||||
|
else
|
||||||
|
ActionView::Base
|
||||||
|
end.register_template_handler(:liquid, LiquidView)
|
||||||
51
lib/extras/liquid_view.rb
Normal file
51
lib/extras/liquid_view.rb
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# LiquidView is a action view extension class. You can register it with rails
|
||||||
|
# and use liquid as an template system for .liquid files
|
||||||
|
#
|
||||||
|
# Example
|
||||||
|
#
|
||||||
|
# ActionView::Base::register_template_handler :liquid, LiquidView
|
||||||
|
class LiquidView
|
||||||
|
PROTECTED_ASSIGNS = %w( template_root response _session template_class action_name request_origin session template
|
||||||
|
_response url _request _cookies variables_added _flash params _headers request cookies
|
||||||
|
ignore_missing_templates flash _params logger before_filter_chain_aborted headers )
|
||||||
|
PROTECTED_INSTANCE_VARIABLES = %w( @_request @controller @_first_render @_memoized__pick_template @view_paths
|
||||||
|
@helpers @assigns_added @template @_render_stack @template_format @assigns )
|
||||||
|
|
||||||
|
def self.call(template)
|
||||||
|
"LiquidView.new(self).render(template, local_assigns)"
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(view)
|
||||||
|
@view = view
|
||||||
|
end
|
||||||
|
|
||||||
|
def render(template, local_assigns = nil)
|
||||||
|
@view.controller.headers["Content-Type"] ||= 'text/html; charset=utf-8'
|
||||||
|
|
||||||
|
# Rails 2.2 Template has source, but not locals
|
||||||
|
if template.respond_to?(:source) && !template.respond_to?(:locals)
|
||||||
|
assigns = (@view.instance_variables - PROTECTED_INSTANCE_VARIABLES).inject({}) do |hash, ivar|
|
||||||
|
hash[ivar[1..-1]] = @view.instance_variable_get(ivar)
|
||||||
|
hash
|
||||||
|
end
|
||||||
|
else
|
||||||
|
assigns = @view.assigns.reject{ |k,v| PROTECTED_ASSIGNS.include?(k) }
|
||||||
|
end
|
||||||
|
|
||||||
|
source = template.respond_to?(:source) ? template.source : template
|
||||||
|
local_assigns = (template.respond_to?(:locals) ? template.locals : local_assigns) || {}
|
||||||
|
|
||||||
|
if content_for_layout = @view.instance_variable_get("@content_for_layout")
|
||||||
|
assigns['content_for_layout'] = content_for_layout
|
||||||
|
end
|
||||||
|
assigns.merge!(local_assigns.stringify_keys)
|
||||||
|
|
||||||
|
liquid = Liquid::Template.parse(source)
|
||||||
|
liquid.render(assigns, :filters => [@view.controller.master_helper_module], :registers => {:action_view => @view, :controller => @view.controller})
|
||||||
|
end
|
||||||
|
|
||||||
|
def compilable?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
@@ -21,60 +21,49 @@
|
|||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
FilterSeparator = /\|/
|
FilterSeparator = /\|/
|
||||||
ArgumentSeparator = ','.freeze
|
ArgumentSeparator = ','
|
||||||
FilterArgumentSeparator = ':'.freeze
|
FilterArgumentSeparator = ':'
|
||||||
VariableAttributeSeparator = '.'.freeze
|
VariableAttributeSeparator = '.'
|
||||||
WhitespaceControl = '-'.freeze
|
|
||||||
TagStart = /\{\%/
|
TagStart = /\{\%/
|
||||||
TagEnd = /\%\}/
|
TagEnd = /\%\}/
|
||||||
VariableSignature = /\(?[\w\-\.\[\]]\)?/
|
VariableSignature = /\(?[\w\-\.\[\]]\)?/
|
||||||
VariableSegment = /[\w\-]/
|
VariableSegment = /[\w\-]/
|
||||||
VariableStart = /\{\{/
|
VariableStart = /\{\{/
|
||||||
VariableEnd = /\}\}/
|
VariableEnd = /\}\}/
|
||||||
VariableIncompleteEnd = /\}\}?/
|
|
||||||
QuotedString = /"[^"]*"|'[^']*'/
|
QuotedString = /"[^"]*"|'[^']*'/
|
||||||
QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o
|
QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o
|
||||||
|
StrictQuotedFragment = /"[^"]+"|'[^']+'|[^\s|:,]+/
|
||||||
|
FirstFilterArgument = /#{FilterArgumentSeparator}(?:#{StrictQuotedFragment})/o
|
||||||
|
OtherFilterArgument = /#{ArgumentSeparator}(?:#{StrictQuotedFragment})/o
|
||||||
|
SpacelessFilter = /\A(?:'[^']+'|"[^"]+"|[^'"])*#{FilterSeparator}(?:#{StrictQuotedFragment})(?:#{FirstFilterArgument}(?:#{OtherFilterArgument})*)?/o
|
||||||
|
Expression = /(?:#{QuotedFragment}(?:#{SpacelessFilter})*)/o
|
||||||
TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/o
|
TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/o
|
||||||
AnyStartingTag = /#{TagStart}|#{VariableStart}/o
|
|
||||||
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om
|
|
||||||
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om
|
|
||||||
VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o
|
VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o
|
||||||
|
|
||||||
singleton_class.send(:attr_accessor, :cache_classes)
|
|
||||||
self.cache_classes = true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
require 'liquid/liquid'
|
||||||
require "liquid/version"
|
require "liquid/version"
|
||||||
require 'liquid/parse_tree_visitor'
|
|
||||||
require 'liquid/lexer'
|
require 'liquid/lexer'
|
||||||
require 'liquid/parser'
|
require 'liquid/parser'
|
||||||
require 'liquid/i18n'
|
require 'liquid/i18n'
|
||||||
require 'liquid/drop'
|
require 'liquid/drop'
|
||||||
require 'liquid/tablerowloop_drop'
|
|
||||||
require 'liquid/forloop_drop'
|
|
||||||
require 'liquid/extensions'
|
require 'liquid/extensions'
|
||||||
require 'liquid/errors'
|
require 'liquid/errors'
|
||||||
require 'liquid/interrupts'
|
require 'liquid/interrupts'
|
||||||
require 'liquid/strainer'
|
require 'liquid/strainer'
|
||||||
require 'liquid/expression'
|
|
||||||
require 'liquid/context'
|
require 'liquid/context'
|
||||||
require 'liquid/parser_switching'
|
|
||||||
require 'liquid/tag'
|
require 'liquid/tag'
|
||||||
require 'liquid/block'
|
require 'liquid/block'
|
||||||
require 'liquid/block_body'
|
|
||||||
require 'liquid/document'
|
require 'liquid/document'
|
||||||
require 'liquid/variable'
|
require 'liquid/variable'
|
||||||
require 'liquid/variable_lookup'
|
|
||||||
require 'liquid/range_lookup'
|
|
||||||
require 'liquid/file_system'
|
require 'liquid/file_system'
|
||||||
require 'liquid/resource_limits'
|
|
||||||
require 'liquid/template'
|
require 'liquid/template'
|
||||||
|
require 'liquid/htmltags'
|
||||||
require 'liquid/standardfilters'
|
require 'liquid/standardfilters'
|
||||||
require 'liquid/condition'
|
require 'liquid/condition'
|
||||||
|
require 'liquid/module_ex'
|
||||||
require 'liquid/utils'
|
require 'liquid/utils'
|
||||||
require 'liquid/tokenizer'
|
|
||||||
require 'liquid/parse_context'
|
|
||||||
|
|
||||||
# Load all the tags of the standard library
|
# Load all the tags of the standard library
|
||||||
#
|
#
|
||||||
Dir["#{__dir__}/liquid/tags/*.rb"].each { |f| require f }
|
Dir[File.dirname(__FILE__) + '/liquid/tags/*.rb'].each { |f| require f }
|
||||||
|
|||||||
@@ -1,78 +1,101 @@
|
|||||||
module Liquid
|
module Liquid
|
||||||
class Block < Tag
|
class Block < Tag
|
||||||
MAX_DEPTH = 100
|
def initialize(tag_name, markup, tokens)
|
||||||
|
|
||||||
def initialize(tag_name, markup, options)
|
|
||||||
super
|
super
|
||||||
@blank = true
|
parse_body(tokens)
|
||||||
end
|
|
||||||
|
|
||||||
def parse(tokens)
|
|
||||||
@body = BlockBody.new
|
|
||||||
while parse_body(@body, tokens)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# For backwards compatibility
|
|
||||||
def render(context)
|
|
||||||
@body.render(context)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def blank?
|
def blank?
|
||||||
@blank
|
@blank || false
|
||||||
end
|
end
|
||||||
|
|
||||||
def nodelist
|
# warnings of this block and all sub-tags
|
||||||
@body.nodelist
|
def warnings
|
||||||
end
|
all_warnings = []
|
||||||
|
all_warnings.concat(@warnings) if @warnings
|
||||||
|
|
||||||
def unknown_tag(tag, _params, _tokens)
|
(nodelist || []).each do |node|
|
||||||
if tag == 'else'.freeze
|
all_warnings.concat(node.warnings || []) if node.respond_to?(:warnings)
|
||||||
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_else".freeze,
|
|
||||||
block_name: block_name))
|
|
||||||
elsif tag.start_with?('end'.freeze)
|
|
||||||
raise SyntaxError.new(parse_context.locale.t("errors.syntax.invalid_delimiter".freeze,
|
|
||||||
tag: tag,
|
|
||||||
block_name: block_name,
|
|
||||||
block_delimiter: block_delimiter))
|
|
||||||
else
|
|
||||||
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unknown_tag".freeze, tag: tag))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
all_warnings
|
||||||
|
end
|
||||||
|
|
||||||
|
def unknown_tag(tag, params, tokens)
|
||||||
|
case tag
|
||||||
|
when 'else'
|
||||||
|
raise SyntaxError.new(options[:locale].t("errors.syntax.unexpected_else",
|
||||||
|
:block_name => block_name))
|
||||||
|
when 'end'
|
||||||
|
raise SyntaxError.new(options[:locale].t("errors.syntax.invalid_delimiter",
|
||||||
|
:block_name => block_name,
|
||||||
|
:block_delimiter => block_delimiter))
|
||||||
|
else
|
||||||
|
raise SyntaxError.new(options[:locale].t("errors.syntax.unknown_tag", :tag => tag))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def block_delimiter
|
||||||
|
"end#{block_name}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def block_name
|
def block_name
|
||||||
@tag_name
|
@tag_name
|
||||||
end
|
end
|
||||||
|
|
||||||
def block_delimiter
|
def render(context)
|
||||||
@block_delimiter ||= "end#{block_name}"
|
render_all(@nodelist, context)
|
||||||
end
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def parse_body(body, tokens)
|
def unterminated_variable(token)
|
||||||
if parse_context.depth >= MAX_DEPTH
|
raise SyntaxError.new(options[:locale].t("errors.syntax.variable_termination", :token => token, :tag_end => VariableEnd.inspect))
|
||||||
raise StackLevelError, "Nesting too deep".freeze
|
end
|
||||||
end
|
|
||||||
parse_context.depth += 1
|
|
||||||
begin
|
|
||||||
body.parse(tokens, parse_context) do |end_tag_name, end_tag_params|
|
|
||||||
@blank &&= body.blank?
|
|
||||||
|
|
||||||
return false if end_tag_name == block_delimiter
|
def unterminated_tag(token)
|
||||||
unless end_tag_name
|
raise SyntaxError.new(options[:locale].t("errors.syntax.tag_termination", :token => token, :tag_end => TagEnd.inspect))
|
||||||
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
|
end
|
||||||
|
|
||||||
|
def assert_missing_delimitation!
|
||||||
|
raise SyntaxError.new(options[:locale].t("errors.syntax.tag_never_closed", :block_name => block_name))
|
||||||
|
end
|
||||||
|
|
||||||
|
def render_all(list, context)
|
||||||
|
output = []
|
||||||
|
context.resource_limits[:render_length_current] = 0
|
||||||
|
context.resource_limits[:render_score_current] += list.length
|
||||||
|
|
||||||
|
list.each do |token|
|
||||||
|
# Break out if we have any unhanded interrupts.
|
||||||
|
break if context.has_interrupt?
|
||||||
|
|
||||||
|
begin
|
||||||
|
# If we get an Interrupt that means the block must stop processing. An
|
||||||
|
# Interrupt is any command that stops block execution such as {% break %}
|
||||||
|
# or {% continue %}
|
||||||
|
if token.is_a? Continue or token.is_a? Break
|
||||||
|
context.push_interrupt(token.interrupt)
|
||||||
|
break
|
||||||
end
|
end
|
||||||
|
|
||||||
# this tag is not registered with the system
|
token_output = (token.respond_to?(:render) ? token.render(context) : token)
|
||||||
# pass it to the current block for special handling or error reporting
|
context.increment_used_resources(:render_length_current, token_output)
|
||||||
unknown_tag(end_tag_name, end_tag_params, tokens)
|
if context.resource_limits_reached?
|
||||||
|
context.resource_limits[:reached] = true
|
||||||
|
raise MemoryError.new("Memory limits exceeded")
|
||||||
|
end
|
||||||
|
unless token.is_a?(Block) && token.blank?
|
||||||
|
output << token_output
|
||||||
|
end
|
||||||
|
rescue MemoryError => e
|
||||||
|
raise e
|
||||||
|
rescue ::StandardError => e
|
||||||
|
output << (context.handle_error(e))
|
||||||
end
|
end
|
||||||
ensure
|
|
||||||
parse_context.depth -= 1
|
|
||||||
end
|
end
|
||||||
|
|
||||||
true
|
StringSlice.join(output)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,189 +0,0 @@
|
|||||||
module Liquid
|
|
||||||
class BlockBody
|
|
||||||
LiquidTagToken = /\A\s*(\w+)\s*(.*?)\z/o
|
|
||||||
FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(\w+)(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
|
|
||||||
ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
|
|
||||||
WhitespaceOrNothing = /\A\s*\z/
|
|
||||||
TAGSTART = "{%".freeze
|
|
||||||
VARSTART = "{{".freeze
|
|
||||||
|
|
||||||
attr_reader :nodelist
|
|
||||||
|
|
||||||
def initialize
|
|
||||||
@nodelist = []
|
|
||||||
@blank = true
|
|
||||||
end
|
|
||||||
|
|
||||||
def parse(tokenizer, parse_context, &block)
|
|
||||||
parse_context.line_number = tokenizer.line_number
|
|
||||||
|
|
||||||
if tokenizer.for_liquid_tag
|
|
||||||
parse_for_liquid_tag(tokenizer, parse_context, &block)
|
|
||||||
else
|
|
||||||
parse_for_document(tokenizer, parse_context, &block)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private def parse_for_liquid_tag(tokenizer, parse_context)
|
|
||||||
while token = tokenizer.shift
|
|
||||||
unless token.empty? || token =~ WhitespaceOrNothing
|
|
||||||
unless token =~ LiquidTagToken
|
|
||||||
# line isn't empty but didn't match tag syntax, yield and let the
|
|
||||||
# caller raise a syntax error
|
|
||||||
return yield token, token
|
|
||||||
end
|
|
||||||
tag_name = $1
|
|
||||||
markup = $2
|
|
||||||
unless tag = registered_tags[tag_name]
|
|
||||||
# end parsing if we reach an unknown tag and let the caller decide
|
|
||||||
# determine how to proceed
|
|
||||||
return yield tag_name, markup
|
|
||||||
end
|
|
||||||
new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
|
|
||||||
@blank &&= new_tag.blank?
|
|
||||||
@nodelist << new_tag
|
|
||||||
end
|
|
||||||
parse_context.line_number = tokenizer.line_number
|
|
||||||
end
|
|
||||||
|
|
||||||
yield nil, nil
|
|
||||||
end
|
|
||||||
|
|
||||||
private def parse_for_document(tokenizer, parse_context, &block)
|
|
||||||
while token = tokenizer.shift
|
|
||||||
next if token.empty?
|
|
||||||
case
|
|
||||||
when token.start_with?(TAGSTART)
|
|
||||||
whitespace_handler(token, parse_context)
|
|
||||||
unless token =~ FullToken
|
|
||||||
raise_missing_tag_terminator(token, parse_context)
|
|
||||||
end
|
|
||||||
tag_name = $2
|
|
||||||
markup = $4
|
|
||||||
|
|
||||||
if parse_context.line_number
|
|
||||||
# newlines inside the tag should increase the line number,
|
|
||||||
# particularly important for multiline {% liquid %} tags
|
|
||||||
parse_context.line_number += $1.count("\n".freeze) + $3.count("\n".freeze)
|
|
||||||
end
|
|
||||||
|
|
||||||
if tag_name == 'liquid'.freeze
|
|
||||||
liquid_tag_tokenizer = Tokenizer.new(markup, line_number: parse_context.line_number, for_liquid_tag: true)
|
|
||||||
next parse_for_liquid_tag(liquid_tag_tokenizer, parse_context, &block)
|
|
||||||
end
|
|
||||||
|
|
||||||
unless tag = registered_tags[tag_name]
|
|
||||||
# end parsing if we reach an unknown tag and let the caller decide
|
|
||||||
# determine how to proceed
|
|
||||||
return yield tag_name, markup
|
|
||||||
end
|
|
||||||
new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
|
|
||||||
@blank &&= new_tag.blank?
|
|
||||||
@nodelist << new_tag
|
|
||||||
when token.start_with?(VARSTART)
|
|
||||||
whitespace_handler(token, parse_context)
|
|
||||||
@nodelist << create_variable(token, parse_context)
|
|
||||||
@blank = false
|
|
||||||
else
|
|
||||||
if parse_context.trim_whitespace
|
|
||||||
token.lstrip!
|
|
||||||
end
|
|
||||||
parse_context.trim_whitespace = false
|
|
||||||
@nodelist << token
|
|
||||||
@blank &&= !!(token =~ WhitespaceOrNothing)
|
|
||||||
end
|
|
||||||
parse_context.line_number = tokenizer.line_number
|
|
||||||
end
|
|
||||||
|
|
||||||
yield nil, nil
|
|
||||||
end
|
|
||||||
|
|
||||||
def whitespace_handler(token, parse_context)
|
|
||||||
if token[2] == WhitespaceControl
|
|
||||||
previous_token = @nodelist.last
|
|
||||||
if previous_token.is_a? String
|
|
||||||
previous_token.rstrip!
|
|
||||||
end
|
|
||||||
end
|
|
||||||
parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
|
|
||||||
end
|
|
||||||
|
|
||||||
def blank?
|
|
||||||
@blank
|
|
||||||
end
|
|
||||||
|
|
||||||
def render(context)
|
|
||||||
render_to_output_buffer(context, '')
|
|
||||||
end
|
|
||||||
|
|
||||||
def render_to_output_buffer(context, output)
|
|
||||||
context.resource_limits.render_score += @nodelist.length
|
|
||||||
|
|
||||||
idx = 0
|
|
||||||
while node = @nodelist[idx]
|
|
||||||
previous_output_size = output.bytesize
|
|
||||||
|
|
||||||
case node
|
|
||||||
when String
|
|
||||||
output << node
|
|
||||||
when Variable
|
|
||||||
render_node(context, output, node)
|
|
||||||
when Block
|
|
||||||
render_node(context, node.blank? ? '' : output, node)
|
|
||||||
break if context.interrupt? # might have happened in a for-block
|
|
||||||
when Continue, Break
|
|
||||||
# If we get an Interrupt that means the block must stop processing. An
|
|
||||||
# Interrupt is any command that stops block execution such as {% break %}
|
|
||||||
# or {% continue %}
|
|
||||||
context.push_interrupt(node.interrupt)
|
|
||||||
break
|
|
||||||
else # Other non-Block tags
|
|
||||||
render_node(context, output, node)
|
|
||||||
break if context.interrupt? # might have happened through an include
|
|
||||||
end
|
|
||||||
idx += 1
|
|
||||||
|
|
||||||
raise_if_resource_limits_reached(context, output.bytesize - previous_output_size)
|
|
||||||
end
|
|
||||||
|
|
||||||
output
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def render_node(context, output, node)
|
|
||||||
node.render_to_output_buffer(context, output)
|
|
||||||
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
|
|
||||||
context.handle_error(e, node.line_number)
|
|
||||||
rescue ::StandardError => e
|
|
||||||
line_number = node.is_a?(String) ? nil : node.line_number
|
|
||||||
output << context.handle_error(e, line_number)
|
|
||||||
end
|
|
||||||
|
|
||||||
def raise_if_resource_limits_reached(context, length)
|
|
||||||
context.resource_limits.render_length += length
|
|
||||||
return unless context.resource_limits.reached?
|
|
||||||
raise MemoryError.new("Memory limits exceeded".freeze)
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_variable(token, parse_context)
|
|
||||||
token.scan(ContentOfVariable) do |content|
|
|
||||||
markup = content.first
|
|
||||||
return Variable.new(markup, parse_context)
|
|
||||||
end
|
|
||||||
raise_missing_variable_terminator(token, parse_context)
|
|
||||||
end
|
|
||||||
|
|
||||||
def raise_missing_tag_terminator(token, parse_context)
|
|
||||||
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_termination".freeze, token: token, tag_end: TagEnd.inspect))
|
|
||||||
end
|
|
||||||
|
|
||||||
def raise_missing_variable_terminator(token, parse_context)
|
|
||||||
raise SyntaxError.new(parse_context.locale.t("errors.syntax.variable_termination".freeze, token: token, tag_end: VariableEnd.inspect))
|
|
||||||
end
|
|
||||||
|
|
||||||
def registered_tags
|
|
||||||
Template.tags
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -3,70 +3,53 @@ module Liquid
|
|||||||
#
|
#
|
||||||
# Example:
|
# Example:
|
||||||
#
|
#
|
||||||
# c = Condition.new(1, '==', 1)
|
# c = Condition.new('1', '==', '1')
|
||||||
# c.evaluate #=> true
|
# c.evaluate #=> true
|
||||||
#
|
#
|
||||||
class Condition #:nodoc:
|
class Condition #:nodoc:
|
||||||
@@operators = {
|
@@operators = {
|
||||||
'=='.freeze => ->(cond, left, right) { cond.send(:equal_variables, left, right) },
|
'==' => lambda { |cond, left, right| cond.send(:equal_variables, left, right) },
|
||||||
'!='.freeze => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
|
'!=' => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) },
|
||||||
'<>'.freeze => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
|
'<>' => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) },
|
||||||
'<'.freeze => :<,
|
'<' => :<,
|
||||||
'>'.freeze => :>,
|
'>' => :>,
|
||||||
'>='.freeze => :>=,
|
'>=' => :>=,
|
||||||
'<='.freeze => :<=,
|
'<=' => :<=,
|
||||||
'contains'.freeze => lambda do |cond, left, right|
|
'contains' => lambda { |cond, left, right| left && right ? left.include?(right) : false }
|
||||||
if left && right && left.respond_to?(:include?)
|
|
||||||
right = right.to_s if left.is_a?(String)
|
|
||||||
left.include?(right)
|
|
||||||
else
|
|
||||||
false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def self.operators
|
def self.operators
|
||||||
@@operators
|
@@operators
|
||||||
end
|
end
|
||||||
|
|
||||||
attr_reader :attachment, :child_condition
|
attr_reader :attachment
|
||||||
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, @operator, @right = left, operator, right
|
||||||
@operator = operator
|
|
||||||
@right = right
|
|
||||||
@child_relation = nil
|
@child_relation = nil
|
||||||
@child_condition = nil
|
@child_condition = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
def evaluate(context = Context.new)
|
def evaluate(context = Context.new)
|
||||||
condition = self
|
result = interpret_condition(left, right, operator, context)
|
||||||
result = nil
|
|
||||||
loop do
|
|
||||||
result = interpret_condition(condition.left, condition.right, condition.operator, context)
|
|
||||||
|
|
||||||
case condition.child_relation
|
case @child_relation
|
||||||
when :or
|
when :or
|
||||||
break if result
|
result || @child_condition.evaluate(context)
|
||||||
when :and
|
when :and
|
||||||
break unless result
|
result && @child_condition.evaluate(context)
|
||||||
else
|
else
|
||||||
break
|
result
|
||||||
end
|
|
||||||
condition = condition.child_condition
|
|
||||||
end
|
end
|
||||||
result
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def or(condition)
|
def or(condition)
|
||||||
@child_relation = :or
|
@child_relation, @child_condition = :or, condition
|
||||||
@child_condition = condition
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def and(condition)
|
def and(condition)
|
||||||
@child_relation = :and
|
@child_relation, @child_condition = :and, condition
|
||||||
@child_condition = condition
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def attach(attachment)
|
def attach(attachment)
|
||||||
@@ -78,27 +61,23 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
def inspect
|
def inspect
|
||||||
"#<Condition #{[@left, @operator, @right].compact.join(' '.freeze)}>"
|
"#<Condition #{[@left, @operator, @right].compact.join(' ')}>"
|
||||||
end
|
end
|
||||||
|
|
||||||
protected
|
|
||||||
|
|
||||||
attr_reader :child_relation
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def equal_variables(left, right)
|
def equal_variables(left, right)
|
||||||
if left.is_a?(Liquid::Expression::MethodLiteral)
|
if left.is_a?(Symbol)
|
||||||
if right.respond_to?(left.method_name)
|
if right.respond_to?(left)
|
||||||
return right.send(left.method_name)
|
return right.send(left.to_s)
|
||||||
else
|
else
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if right.is_a?(Liquid::Expression::MethodLiteral)
|
if right.is_a?(Symbol)
|
||||||
if left.respond_to?(right.method_name)
|
if left.respond_to?(right)
|
||||||
return left.send(right.method_name)
|
return left.send(right.to_s)
|
||||||
else
|
else
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
@@ -111,41 +90,31 @@ module Liquid
|
|||||||
# If the operator is empty this means that the decision statement is just
|
# If the operator is empty this means that the decision statement is just
|
||||||
# a single variable. We can just poll this variable from the context and
|
# a single variable. We can just poll this variable from the context and
|
||||||
# return this as the result.
|
# return this as the result.
|
||||||
return context.evaluate(left) if op.nil?
|
return context[left] if op == nil
|
||||||
|
|
||||||
left = context.evaluate(left)
|
left, right = context[left], context[right]
|
||||||
right = context.evaluate(right)
|
|
||||||
|
|
||||||
operation = self.class.operators[op] || raise(Liquid::ArgumentError.new("Unknown operator #{op}"))
|
operation = self.class.operators[op] || raise(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)
|
||||||
elsif left.respond_to?(operation) && right.respond_to?(operation) && !left.is_a?(Hash) && !right.is_a?(Hash)
|
elsif left.respond_to?(operation) and right.respond_to?(operation)
|
||||||
begin
|
left.send(operation, right)
|
||||||
left.send(operation, right)
|
else
|
||||||
rescue ::ArgumentError => e
|
nil
|
||||||
raise Liquid::ArgumentError.new(e.message)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
|
||||||
def children
|
|
||||||
[
|
|
||||||
@node.left, @node.right,
|
|
||||||
@node.child_condition, @node.attachment
|
|
||||||
].compact
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
class ElseCondition < Condition
|
class ElseCondition < Condition
|
||||||
def else?
|
def else?
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
def evaluate(_context)
|
def evaluate(context)
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
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
|
||||||
#
|
#
|
||||||
# context['variable'] = 'testing'
|
# context['variable'] = 'testing'
|
||||||
@@ -13,28 +14,32 @@ module Liquid
|
|||||||
# context['bob'] #=> nil class Context
|
# context['bob'] #=> nil class Context
|
||||||
class Context
|
class Context
|
||||||
attr_reader :scopes, :errors, :registers, :environments, :resource_limits
|
attr_reader :scopes, :errors, :registers, :environments, :resource_limits
|
||||||
attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters
|
|
||||||
|
|
||||||
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil)
|
|
||||||
@environments = [environments].flatten
|
|
||||||
@scopes = [(outer_scope || {})]
|
|
||||||
@registers = registers
|
|
||||||
@errors = []
|
|
||||||
@interrupts = []
|
|
||||||
@filters = []
|
|
||||||
@global_filter = nil
|
|
||||||
@partial = false
|
|
||||||
@strict_variables = false
|
|
||||||
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
|
|
||||||
|
|
||||||
self.exception_renderer = Template.default_exception_renderer
|
|
||||||
self.exception_renderer = ->(e) { raise } if rethrow_errors
|
|
||||||
|
|
||||||
|
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = {})
|
||||||
|
@environments = [environments].flatten
|
||||||
|
@scopes = [(outer_scope || {})]
|
||||||
|
@registers = registers
|
||||||
|
@errors = []
|
||||||
|
@rethrow_errors = rethrow_errors
|
||||||
|
@resource_limits = (resource_limits || {}).merge!({ :render_score_current => 0, :assign_score_current => 0 })
|
||||||
squash_instance_assigns_with_environments
|
squash_instance_assigns_with_environments
|
||||||
|
|
||||||
|
@interrupts = []
|
||||||
|
@filters = []
|
||||||
end
|
end
|
||||||
|
|
||||||
def warnings
|
def increment_used_resources(key, obj)
|
||||||
@warnings ||= []
|
@resource_limits[key] += if obj.kind_of?(StringSlice) || obj.kind_of?(String) || obj.kind_of?(Array) || obj.kind_of?(Hash)
|
||||||
|
obj.length
|
||||||
|
else
|
||||||
|
1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def resource_limits_reached?
|
||||||
|
(@resource_limits[:render_length_limit] && @resource_limits[:render_length_current] > @resource_limits[:render_length_limit]) ||
|
||||||
|
(@resource_limits[:render_score_limit] && @resource_limits[:render_score_current] > @resource_limits[:render_score_limit] ) ||
|
||||||
|
(@resource_limits[:assign_score_limit] && @resource_limits[:assign_score_current] > @resource_limits[:assign_score_limit] )
|
||||||
end
|
end
|
||||||
|
|
||||||
def strainer
|
def strainer
|
||||||
@@ -47,17 +52,26 @@ module Liquid
|
|||||||
# for that
|
# for that
|
||||||
def add_filters(filters)
|
def add_filters(filters)
|
||||||
filters = [filters].flatten.compact
|
filters = [filters].flatten.compact
|
||||||
@filters += filters
|
filters.each do |f|
|
||||||
@strainer = nil
|
raise ArgumentError, "Expected module but got: #{f.class}" unless f.is_a?(Module)
|
||||||
end
|
Strainer.add_known_filter(f)
|
||||||
|
end
|
||||||
|
|
||||||
def apply_global_filter(obj)
|
# If strainer is already setup then there's no choice but to use a runtime
|
||||||
global_filter.nil? ? obj : global_filter.call(obj)
|
# extend call. If strainer is not yet created, we can utilize strainers
|
||||||
|
# cached class based API, which avoids busting the method cache.
|
||||||
|
if @strainer
|
||||||
|
filters.each do |f|
|
||||||
|
strainer.extend(f)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
@filters.concat filters
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# are there any not handled interrupts?
|
# are there any not handled interrupts?
|
||||||
def interrupt?
|
def has_interrupt?
|
||||||
!@interrupts.empty?
|
@interrupts.any?
|
||||||
end
|
end
|
||||||
|
|
||||||
# push an interrupt to the stack. this interrupt is considered not handled.
|
# push an interrupt to the stack. this interrupt is considered not handled.
|
||||||
@@ -70,22 +84,26 @@ module Liquid
|
|||||||
@interrupts.pop
|
@interrupts.pop
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_error(e, line_number = nil)
|
def handle_error(e)
|
||||||
e = internal_error unless e.is_a?(Liquid::Error)
|
|
||||||
e.template_name ||= template_name
|
|
||||||
e.line_number ||= line_number
|
|
||||||
errors.push(e)
|
errors.push(e)
|
||||||
exception_renderer.call(e).to_s
|
raise if @rethrow_errors
|
||||||
|
|
||||||
|
case e
|
||||||
|
when SyntaxError
|
||||||
|
"Liquid syntax error: #{e.message}"
|
||||||
|
else
|
||||||
|
"Liquid error: #{e.message}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def invoke(method, *args)
|
def invoke(method, *args)
|
||||||
strainer.invoke(method, *args).to_liquid
|
strainer.invoke(method, *args)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Push new local scope on the stack. use <tt>Context#stack</tt> instead
|
# Push new local scope on the stack. use <tt>Context#stack</tt> instead
|
||||||
def push
|
def push(new_scope={})
|
||||||
@scopes.unshift({})
|
@scopes.unshift(new_scope)
|
||||||
raise StackLevelError, "Nesting too deep".freeze if @scopes.length > (Block::MAX_DEPTH + 1)
|
raise StackLevelError, "Nesting too deep" if @scopes.length > 100
|
||||||
end
|
end
|
||||||
|
|
||||||
# Merge a hash of variables in the current local scope
|
# Merge a hash of variables in the current local scope
|
||||||
@@ -107,85 +125,161 @@ module Liquid
|
|||||||
# end
|
# end
|
||||||
#
|
#
|
||||||
# context['var] #=> nil
|
# context['var] #=> nil
|
||||||
def stack
|
def stack(new_scope={})
|
||||||
push
|
push(new_scope)
|
||||||
yield
|
yield
|
||||||
ensure
|
ensure
|
||||||
pop
|
pop
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def clear_instance_assigns
|
||||||
|
@scopes[0] = {}
|
||||||
|
end
|
||||||
|
|
||||||
# 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)
|
||||||
@scopes[0][key] = value
|
@scopes[0][key] = value
|
||||||
end
|
end
|
||||||
|
|
||||||
# Look up variable, either resolve directly after considering the name. We can directly handle
|
def [](key)
|
||||||
# Strings, digits, floats and booleans (true,false).
|
resolve(key)
|
||||||
# If no match is made we lookup the variable in the current scope and
|
|
||||||
# later move up to the parent blocks to see if we can resolve the variable somewhere up the tree.
|
|
||||||
# Some special keywords return symbols. Those symbols are to be called on the rhs object in expressions
|
|
||||||
#
|
|
||||||
# Example:
|
|
||||||
# products == empty #=> products.empty?
|
|
||||||
def [](expression)
|
|
||||||
evaluate(Expression.parse(expression))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def key?(key)
|
def has_key?(key)
|
||||||
self[key] != nil
|
resolve(key) != nil
|
||||||
end
|
|
||||||
|
|
||||||
def evaluate(object)
|
|
||||||
object.respond_to?(:evaluate) ? object.evaluate(self) : object
|
|
||||||
end
|
|
||||||
|
|
||||||
# Fetches an object starting at the local scope and then moving up the hierachy
|
|
||||||
def find_variable(key, raise_on_not_found: true)
|
|
||||||
# This was changed from find() to find_index() because this is a very hot
|
|
||||||
# path and find_index() is optimized in MRI to reduce object allocation
|
|
||||||
|
|
||||||
scope = (index = @scopes.find_index { |s| s.key?(key) }) && @scopes[index]
|
|
||||||
scope ||= (index = @environments.find_index { |s| s.key?(key) }) && @environments[index]
|
|
||||||
scope ||= {}
|
|
||||||
|
|
||||||
variable = lookup_and_evaluate(scope, key, raise_on_not_found: raise_on_not_found).to_liquid
|
|
||||||
variable.context = self if variable.respond_to?(:context=)
|
|
||||||
|
|
||||||
variable
|
|
||||||
end
|
|
||||||
|
|
||||||
def lookup_and_evaluate(obj, key, raise_on_not_found: true)
|
|
||||||
if @strict_variables && raise_on_not_found && obj.respond_to?(:key?) && !obj.key?(key)
|
|
||||||
raise Liquid::UndefinedVariable, "undefined variable #{key}"
|
|
||||||
end
|
|
||||||
|
|
||||||
value = obj[key]
|
|
||||||
|
|
||||||
if value.is_a?(Proc) && obj.respond_to?(:[]=)
|
|
||||||
obj[key] = (value.arity == 0) ? value.call : value.call(self)
|
|
||||||
else
|
|
||||||
value
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
LITERALS = {
|
||||||
|
nil => nil, 'nil' => nil, 'null' => nil, '' => nil,
|
||||||
|
'true' => true,
|
||||||
|
'false' => false,
|
||||||
|
'blank' => :blank?,
|
||||||
|
'empty' => :empty?
|
||||||
|
}
|
||||||
|
|
||||||
def internal_error
|
# Look up variable, either resolve directly after considering the name. We can directly handle
|
||||||
# raise and catch to set backtrace and cause on exception
|
# Strings, digits, floats and booleans (true,false).
|
||||||
raise Liquid::InternalError, 'internal'
|
# If no match is made we lookup the variable in the current scope and
|
||||||
rescue Liquid::InternalError => exc
|
# later move up to the parent blocks to see if we can resolve the variable somewhere up the tree.
|
||||||
exc
|
# Some special keywords return symbols. Those symbols are to be called on the rhs object in expressions
|
||||||
end
|
#
|
||||||
|
# Example:
|
||||||
def squash_instance_assigns_with_environments
|
# products == empty #=> products.empty?
|
||||||
@scopes.last.each_key do |k|
|
def resolve(key)
|
||||||
@environments.each do |env|
|
if LITERALS.key?(key)
|
||||||
if env.key?(k)
|
LITERALS[key]
|
||||||
scopes.last[k] = lookup_and_evaluate(env, k)
|
else
|
||||||
break
|
case key
|
||||||
|
when /\A'(.*)'\z/ # Single quoted strings
|
||||||
|
$1
|
||||||
|
when /\A"(.*)"\z/ # Double quoted strings
|
||||||
|
$1
|
||||||
|
when /\A(-?\d+)\z/ # Integer and floats
|
||||||
|
$1.to_i
|
||||||
|
when /\A\((\S+)\.\.(\S+)\)\z/ # Ranges
|
||||||
|
(resolve($1).to_i..resolve($2).to_i)
|
||||||
|
when /\A(-?\d[\d\.]+)\z/ # Floats
|
||||||
|
$1.to_f
|
||||||
|
else
|
||||||
|
variable(key)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end # squash_instance_assigns_with_environments
|
|
||||||
|
# Fetches an object starting at the local scope and then moving up the hierachy
|
||||||
|
def find_variable(key)
|
||||||
|
scope = @scopes.find { |s| s.has_key?(key) }
|
||||||
|
variable = nil
|
||||||
|
|
||||||
|
if scope.nil?
|
||||||
|
@environments.each do |e|
|
||||||
|
if variable = lookup_and_evaluate(e, key)
|
||||||
|
scope = e
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
scope ||= @environments.last || @scopes.last
|
||||||
|
variable ||= lookup_and_evaluate(scope, key)
|
||||||
|
|
||||||
|
variable = variable.to_liquid
|
||||||
|
variable.context = self if variable.respond_to?(:context=)
|
||||||
|
|
||||||
|
return variable
|
||||||
|
end
|
||||||
|
|
||||||
|
# Resolves namespaced queries gracefully.
|
||||||
|
#
|
||||||
|
# Example
|
||||||
|
# @context['hash'] = {"name" => 'tobi'}
|
||||||
|
# assert_equal 'tobi', @context['hash.name']
|
||||||
|
# assert_equal 'tobi', @context['hash["name"]']
|
||||||
|
def variable(markup)
|
||||||
|
parts = markup.scan(VariableParser)
|
||||||
|
square_bracketed = /\A\[(.*)\]\z/
|
||||||
|
|
||||||
|
first_part = parts.shift
|
||||||
|
|
||||||
|
if first_part =~ square_bracketed
|
||||||
|
first_part = resolve($1)
|
||||||
|
end
|
||||||
|
|
||||||
|
if object = find_variable(first_part)
|
||||||
|
|
||||||
|
parts.each do |part|
|
||||||
|
part = resolve($1) if part_resolved = (part =~ square_bracketed)
|
||||||
|
|
||||||
|
# If object is a hash- or array-like object we look for the
|
||||||
|
# presence of the key and if its available we return it
|
||||||
|
if object.respond_to?(:[]) and
|
||||||
|
((object.respond_to?(:has_key?) and object.has_key?(part)) or
|
||||||
|
(object.respond_to?(:fetch) and part.is_a?(Integer)))
|
||||||
|
|
||||||
|
# if its a proc we will replace the entry with the proc
|
||||||
|
res = lookup_and_evaluate(object, part)
|
||||||
|
object = res.to_liquid
|
||||||
|
|
||||||
|
# Some special cases. If the part wasn't in square brackets and
|
||||||
|
# no key with the same name was found we interpret following calls
|
||||||
|
# as commands and call them on the current object
|
||||||
|
elsif !part_resolved and object.respond_to?(part) and ['size', 'first', 'last'].include?(part)
|
||||||
|
|
||||||
|
object = object.send(part.intern).to_liquid
|
||||||
|
|
||||||
|
# No key was present with the desired value and it wasn't one of the directly supported
|
||||||
|
# keywords either. The only thing we got left is to return nil
|
||||||
|
else
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
# If we are dealing with a drop here we have to
|
||||||
|
object.context = self if object.respond_to?(:context=)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
object
|
||||||
|
end # variable
|
||||||
|
|
||||||
|
def lookup_and_evaluate(obj, key)
|
||||||
|
if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=)
|
||||||
|
obj[key] = (value.arity == 0) ? value.call : value.call(self)
|
||||||
|
else
|
||||||
|
value
|
||||||
|
end
|
||||||
|
end # lookup_and_evaluate
|
||||||
|
|
||||||
|
def squash_instance_assigns_with_environments
|
||||||
|
@scopes.last.each_key do |k|
|
||||||
|
@environments.each do |env|
|
||||||
|
if env.has_key?(k)
|
||||||
|
scopes.last[k] = lookup_and_evaluate(env, k)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end # squash_instance_assigns_with_environments
|
||||||
end # Context
|
end # Context
|
||||||
|
|
||||||
end # Liquid
|
end # Liquid
|
||||||
|
|||||||
@@ -1,27 +1,18 @@
|
|||||||
module Liquid
|
module Liquid
|
||||||
class Document < BlockBody
|
class Document < Block
|
||||||
def self.parse(tokens, parse_context)
|
# we don't need markup to open this block
|
||||||
doc = new
|
def initialize(tokens, options = {})
|
||||||
doc.parse(tokens, parse_context)
|
@options = options
|
||||||
doc
|
parse_body(tokens)
|
||||||
end
|
end
|
||||||
|
|
||||||
def parse(tokens, parse_context)
|
# There isn't a real delimiter
|
||||||
super do |end_tag_name, end_tag_params|
|
def block_delimiter
|
||||||
unknown_tag(end_tag_name, parse_context) if end_tag_name
|
nil
|
||||||
end
|
|
||||||
rescue SyntaxError => e
|
|
||||||
e.line_number ||= parse_context.line_number
|
|
||||||
raise
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def unknown_tag(tag, parse_context)
|
# Document blocks don't need to be terminated since they are not actually opened
|
||||||
case tag
|
def assert_missing_delimitation!
|
||||||
when 'else'.freeze, 'end'.freeze
|
|
||||||
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_outer_tag".freeze, tag: tag))
|
|
||||||
else
|
|
||||||
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unknown_tag".freeze, tag: tag))
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
require 'set'
|
require 'set'
|
||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
|
|
||||||
# A drop in liquid is a class which allows you to export DOM like things to liquid.
|
# A drop in liquid is a class which allows you to export DOM like things to liquid.
|
||||||
# Methods of drops are callable.
|
# Methods of drops are callable.
|
||||||
# The main use for liquid drops is to implement lazy loaded objects.
|
# The main use for liquid drops is to implement lazy loaded objects.
|
||||||
@@ -18,27 +19,28 @@ module Liquid
|
|||||||
# tmpl = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {%endfor%} ' )
|
# tmpl = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {%endfor%} ' )
|
||||||
# tmpl.render('product' => ProductDrop.new ) # will invoke top_sales query.
|
# tmpl.render('product' => ProductDrop.new ) # will invoke top_sales query.
|
||||||
#
|
#
|
||||||
# Your drop can either implement the methods sans any parameters
|
# Your drop can either implement the methods sans any parameters or implement the before_method(name) method which is a
|
||||||
# or implement the liquid_method_missing(name) method which is a catch all.
|
# catch all.
|
||||||
class Drop
|
class Drop
|
||||||
attr_writer :context
|
attr_writer :context
|
||||||
|
|
||||||
|
EMPTY_STRING = ''.freeze
|
||||||
|
|
||||||
# Catch all for the method
|
# Catch all for the method
|
||||||
def liquid_method_missing(method)
|
def before_method(method)
|
||||||
return nil unless @context && @context.strict_variables
|
nil
|
||||||
raise Liquid::UndefinedDropMethod, "undefined method #{method}"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# called by liquid to invoke a drop
|
# called by liquid to invoke a drop
|
||||||
def invoke_drop(method_or_key)
|
def invoke_drop(method_or_key)
|
||||||
if self.class.invokable?(method_or_key)
|
if method_or_key && method_or_key != EMPTY_STRING && self.class.invokable?(method_or_key)
|
||||||
send(method_or_key)
|
send(method_or_key)
|
||||||
else
|
else
|
||||||
liquid_method_missing(method_or_key)
|
before_method(method_or_key)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def key?(_name)
|
def has_key?(name)
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -54,25 +56,22 @@ module Liquid
|
|||||||
self.class.name
|
self.class.name
|
||||||
end
|
end
|
||||||
|
|
||||||
alias_method :[], :invoke_drop
|
alias :[] :invoke_drop
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
# Check for method existence without invoking respond_to?, which creates symbols
|
# Check for method existence without invoking respond_to?, which creates symbols
|
||||||
def self.invokable?(method_name)
|
def self.invokable?(method_name)
|
||||||
invokable_methods.include?(method_name.to_s)
|
unless @invokable_methods
|
||||||
end
|
|
||||||
|
|
||||||
def self.invokable_methods
|
|
||||||
@invokable_methods ||= begin
|
|
||||||
blacklist = Liquid::Drop.public_instance_methods + [:each]
|
blacklist = Liquid::Drop.public_instance_methods + [:each]
|
||||||
|
|
||||||
if include?(Enumerable)
|
if include?(Enumerable)
|
||||||
blacklist += Enumerable.public_instance_methods
|
blacklist += Enumerable.public_instance_methods
|
||||||
blacklist -= [:sort, :count, :first, :min, :max, :include?]
|
blacklist -= [:sort, :count, :first, :min, :max, :include?]
|
||||||
end
|
end
|
||||||
|
|
||||||
whitelist = [:to_liquid] + (public_instance_methods - blacklist)
|
whitelist = [:to_liquid] + (public_instance_methods - blacklist)
|
||||||
Set.new(whitelist.map(&:to_s))
|
@invokable_methods = Set.new(whitelist.map(&:to_s))
|
||||||
end
|
end
|
||||||
|
@invokable_methods.include?(method_name.to_s)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,56 +1,12 @@
|
|||||||
module Liquid
|
module Liquid
|
||||||
class Error < ::StandardError
|
class Error < ::StandardError; end
|
||||||
attr_accessor :line_number
|
|
||||||
attr_accessor :template_name
|
|
||||||
attr_accessor :markup_context
|
|
||||||
|
|
||||||
def to_s(with_prefix = true)
|
class ArgumentError < Error; end
|
||||||
str = ""
|
class ContextError < Error; end
|
||||||
str << message_prefix if with_prefix
|
class FilterNotFound < Error; end
|
||||||
str << super()
|
class FileSystemError < Error; end
|
||||||
|
class StandardError < Error; end
|
||||||
if markup_context
|
class SyntaxError < Error; end
|
||||||
str << " "
|
class StackLevelError < Error; end
|
||||||
str << markup_context
|
class MemoryError < Error; end
|
||||||
end
|
|
||||||
|
|
||||||
str
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def message_prefix
|
|
||||||
str = ""
|
|
||||||
if is_a?(SyntaxError)
|
|
||||||
str << "Liquid syntax error"
|
|
||||||
else
|
|
||||||
str << "Liquid error"
|
|
||||||
end
|
|
||||||
|
|
||||||
if line_number
|
|
||||||
str << " ("
|
|
||||||
str << template_name << " " if template_name
|
|
||||||
str << "line " << line_number.to_s << ")"
|
|
||||||
end
|
|
||||||
|
|
||||||
str << ": "
|
|
||||||
str
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
ArgumentError = Class.new(Error)
|
|
||||||
ContextError = Class.new(Error)
|
|
||||||
FileSystemError = Class.new(Error)
|
|
||||||
StandardError = Class.new(Error)
|
|
||||||
SyntaxError = Class.new(Error)
|
|
||||||
StackLevelError = Class.new(Error)
|
|
||||||
TaintedError = Class.new(Error)
|
|
||||||
MemoryError = Class.new(Error)
|
|
||||||
ZeroDivisionError = Class.new(Error)
|
|
||||||
FloatDomainError = Class.new(Error)
|
|
||||||
UndefinedVariable = Class.new(Error)
|
|
||||||
UndefinedDropMethod = Class.new(Error)
|
|
||||||
UndefinedFilter = Class.new(Error)
|
|
||||||
MethodOverrideError = Class.new(Error)
|
|
||||||
InternalError = Class.new(Error)
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
module Liquid
|
|
||||||
class Expression
|
|
||||||
class MethodLiteral
|
|
||||||
attr_reader :method_name, :to_s
|
|
||||||
|
|
||||||
def initialize(method_name, to_s)
|
|
||||||
@method_name = method_name
|
|
||||||
@to_s = to_s
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_liquid
|
|
||||||
to_s
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
LITERALS = {
|
|
||||||
nil => nil, 'nil'.freeze => nil, 'null'.freeze => nil, ''.freeze => nil,
|
|
||||||
'true'.freeze => true,
|
|
||||||
'false'.freeze => false,
|
|
||||||
'blank'.freeze => MethodLiteral.new(:blank?, '').freeze,
|
|
||||||
'empty'.freeze => MethodLiteral.new(:empty?, '').freeze
|
|
||||||
}.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
|
|
||||||
case 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
|
|
||||||
VariableLookup.parse(markup)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -7,56 +7,44 @@ class String # :nodoc:
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class Symbol # :nodoc:
|
class Array # :nodoc:
|
||||||
def to_liquid
|
|
||||||
to_s
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class Array # :nodoc:
|
|
||||||
def to_liquid
|
def to_liquid
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class Hash # :nodoc:
|
class Hash # :nodoc:
|
||||||
def to_liquid
|
def to_liquid
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class Numeric # :nodoc:
|
class Numeric # :nodoc:
|
||||||
def to_liquid
|
def to_liquid
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class Range # :nodoc:
|
class Time # :nodoc:
|
||||||
def to_liquid
|
def to_liquid
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class Time # :nodoc:
|
class DateTime < Date # :nodoc:
|
||||||
def to_liquid
|
def to_liquid
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class DateTime < Date # :nodoc:
|
class Date # :nodoc:
|
||||||
def to_liquid
|
|
||||||
self
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class Date # :nodoc:
|
|
||||||
def to_liquid
|
def to_liquid
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class TrueClass
|
class TrueClass
|
||||||
def to_liquid # :nodoc:
|
def to_liquid # :nodoc:
|
||||||
self
|
self
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,13 +8,13 @@ module Liquid
|
|||||||
#
|
#
|
||||||
# Example:
|
# Example:
|
||||||
#
|
#
|
||||||
# Liquid::Template.file_system = Liquid::LocalFileSystem.new(template_path)
|
# Liquid::Template.file_system = Liquid::LocalFileSystem.new(template_path)
|
||||||
# liquid = Liquid::Template.parse(template)
|
# liquid = Liquid::Template.parse(template)
|
||||||
#
|
#
|
||||||
# This will parse the template with a LocalFileSystem implementation rooted at 'template_path'.
|
# This will parse the template with a LocalFileSystem implementation rooted at 'template_path'.
|
||||||
class BlankFileSystem
|
class BlankFileSystem
|
||||||
# Called by Liquid to retrieve a template file
|
# Called by Liquid to retrieve a template file
|
||||||
def read_template_file(_template_path)
|
def read_template_file(template_path, context)
|
||||||
raise FileSystemError, "This liquid context does not allow includes."
|
raise FileSystemError, "This liquid context does not allow includes."
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -26,10 +26,10 @@ module Liquid
|
|||||||
#
|
#
|
||||||
# Example:
|
# Example:
|
||||||
#
|
#
|
||||||
# file_system = Liquid::LocalFileSystem.new("/some/path")
|
# file_system = Liquid::LocalFileSystem.new("/some/path")
|
||||||
#
|
#
|
||||||
# file_system.full_path("mypartial") # => "/some/path/_mypartial.liquid"
|
# file_system.full_path("mypartial") # => "/some/path/_mypartial.liquid"
|
||||||
# file_system.full_path("dir/mypartial") # => "/some/path/dir/_mypartial.liquid"
|
# file_system.full_path("dir/mypartial") # => "/some/path/dir/_mypartial.liquid"
|
||||||
#
|
#
|
||||||
# Optionally in the second argument you can specify a custom pattern for template filenames.
|
# Optionally in the second argument you can specify a custom pattern for template filenames.
|
||||||
# The Kernel::sprintf format specification is used.
|
# The Kernel::sprintf format specification is used.
|
||||||
@@ -37,21 +37,21 @@ module Liquid
|
|||||||
#
|
#
|
||||||
# Example:
|
# Example:
|
||||||
#
|
#
|
||||||
# file_system = Liquid::LocalFileSystem.new("/some/path", "%s.html")
|
# file_system = Liquid::LocalFileSystem.new("/some/path", "%s.html")
|
||||||
#
|
#
|
||||||
# file_system.full_path("index") # => "/some/path/index.html"
|
# file_system.full_path("index") # => "/some/path/index.html"
|
||||||
#
|
#
|
||||||
class LocalFileSystem
|
class LocalFileSystem
|
||||||
attr_accessor :root
|
attr_accessor :root
|
||||||
|
|
||||||
def initialize(root, pattern = "_%s.liquid".freeze)
|
def initialize(root, pattern = "_%s.liquid")
|
||||||
@root = root
|
@root = root
|
||||||
@pattern = pattern
|
@pattern = pattern
|
||||||
end
|
end
|
||||||
|
|
||||||
def read_template_file(template_path)
|
def read_template_file(template_path, context)
|
||||||
full_path = full_path(template_path)
|
full_path = full_path(template_path)
|
||||||
raise FileSystemError, "No such template '#{template_path}'" unless File.exist?(full_path)
|
raise FileSystemError, "No such template '#{template_path}'" unless File.exists?(full_path)
|
||||||
|
|
||||||
File.read(full_path)
|
File.read(full_path)
|
||||||
end
|
end
|
||||||
@@ -59,13 +59,13 @@ module Liquid
|
|||||||
def full_path(template_path)
|
def full_path(template_path)
|
||||||
raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ /\A[^.\/][a-zA-Z0-9_\/]+\z/
|
raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ /\A[^.\/][a-zA-Z0-9_\/]+\z/
|
||||||
|
|
||||||
full_path = if template_path.include?('/'.freeze)
|
full_path = if template_path.include?('/')
|
||||||
File.join(root, File.dirname(template_path), @pattern % File.basename(template_path))
|
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)
|
||||||
end
|
end
|
||||||
|
|
||||||
raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path).start_with?(File.expand_path(root))
|
raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path) =~ /\A#{File.expand_path(root)}/
|
||||||
|
|
||||||
full_path
|
full_path
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
module Liquid
|
|
||||||
class ForloopDrop < Drop
|
|
||||||
def initialize(name, length, parentloop)
|
|
||||||
@name = name
|
|
||||||
@length = length
|
|
||||||
@parentloop = parentloop
|
|
||||||
@index = 0
|
|
||||||
end
|
|
||||||
|
|
||||||
attr_reader :name, :length, :parentloop
|
|
||||||
|
|
||||||
def index
|
|
||||||
@index + 1
|
|
||||||
end
|
|
||||||
|
|
||||||
def index0
|
|
||||||
@index
|
|
||||||
end
|
|
||||||
|
|
||||||
def rindex
|
|
||||||
@length - @index
|
|
||||||
end
|
|
||||||
|
|
||||||
def rindex0
|
|
||||||
@length - @index - 1
|
|
||||||
end
|
|
||||||
|
|
||||||
def first
|
|
||||||
@index == 0
|
|
||||||
end
|
|
||||||
|
|
||||||
def last
|
|
||||||
@index == @length - 1
|
|
||||||
end
|
|
||||||
|
|
||||||
protected
|
|
||||||
|
|
||||||
def increment!
|
|
||||||
@index += 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
74
lib/liquid/htmltags.rb
Normal file
74
lib/liquid/htmltags.rb
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
module Liquid
|
||||||
|
class TableRow < Block
|
||||||
|
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
|
||||||
|
|
||||||
|
def initialize(tag_name, markup, tokens)
|
||||||
|
if markup =~ Syntax
|
||||||
|
@variable_name = $1
|
||||||
|
@collection_name = $2
|
||||||
|
@attributes = {}
|
||||||
|
markup.scan(TagAttributes) do |key, value|
|
||||||
|
@attributes[key] = value
|
||||||
|
end
|
||||||
|
else
|
||||||
|
raise SyntaxError.new(options[:locale].t("errors.syntax.table_row"))
|
||||||
|
end
|
||||||
|
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
|
def render(context)
|
||||||
|
collection = context[@collection_name] or return ''
|
||||||
|
|
||||||
|
from = @attributes['offset'] ? context[@attributes['offset']].to_i : 0
|
||||||
|
to = @attributes['limit'] ? from + context[@attributes['limit']].to_i : nil
|
||||||
|
|
||||||
|
collection = Utils.slice_collection(collection, from, to)
|
||||||
|
|
||||||
|
length = collection.length
|
||||||
|
|
||||||
|
cols = context[@attributes['cols']].to_i
|
||||||
|
|
||||||
|
row = 1
|
||||||
|
col = 0
|
||||||
|
|
||||||
|
result = "<tr class=\"row1\">\n"
|
||||||
|
context.stack do
|
||||||
|
|
||||||
|
collection.each_with_index do |item, index|
|
||||||
|
context[@variable_name] = item
|
||||||
|
context['tablerowloop'] = {
|
||||||
|
'length' => length,
|
||||||
|
'index' => index + 1,
|
||||||
|
'index0' => index,
|
||||||
|
'col' => col + 1,
|
||||||
|
'col0' => col,
|
||||||
|
'index0' => index,
|
||||||
|
'rindex' => length - index,
|
||||||
|
'rindex0' => length - index - 1,
|
||||||
|
'first' => (index == 0),
|
||||||
|
'last' => (index == length - 1),
|
||||||
|
'col_first' => (col == 0),
|
||||||
|
'col_last' => (col == cols - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
col += 1
|
||||||
|
|
||||||
|
result << "<td class=\"col#{col}\">" << super << '</td>'
|
||||||
|
|
||||||
|
if col == cols and (index != length - 1)
|
||||||
|
col = 0
|
||||||
|
row += 1
|
||||||
|
result << "</tr>\n<tr class=\"row#{row}\">"
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
end
|
||||||
|
result << "</tr>\n"
|
||||||
|
result
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Template.register_tag('tablerow', TableRow)
|
||||||
|
end
|
||||||
@@ -2,9 +2,10 @@ require 'yaml'
|
|||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
class I18n
|
class I18n
|
||||||
DEFAULT_LOCALE = File.join(File.expand_path(__dir__), "locales", "en.yml")
|
DEFAULT_LOCALE = File.join(File.expand_path(File.dirname(__FILE__)), "locales", "en.yml")
|
||||||
|
|
||||||
TranslationError = Class.new(StandardError)
|
class TranslationError < StandardError
|
||||||
|
end
|
||||||
|
|
||||||
attr_reader :path
|
attr_reader :path
|
||||||
|
|
||||||
@@ -22,16 +23,15 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def interpolate(name, vars)
|
def interpolate(name, vars)
|
||||||
name.gsub(/%\{(\w+)\}/) do
|
name.gsub(/%\{(\w+)\}/) {
|
||||||
# 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[$1.to_sym]).to_s
|
"#{vars[$1.to_sym]}"
|
||||||
end
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
def deep_fetch_translation(name)
|
def deep_fetch_translation(name)
|
||||||
name.split('.'.freeze).reduce(locale) do |level, cur|
|
name.split('.').reduce(locale) do |level, cur|
|
||||||
level[cur] or 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
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
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".freeze
|
@message = message || "interrupt"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -2,35 +2,31 @@ require "strscan"
|
|||||||
module Liquid
|
module Liquid
|
||||||
class Lexer
|
class Lexer
|
||||||
SPECIALS = {
|
SPECIALS = {
|
||||||
'|'.freeze => :pipe,
|
'|' => :pipe,
|
||||||
'.'.freeze => :dot,
|
'.' => :dot,
|
||||||
':'.freeze => :colon,
|
':' => :colon,
|
||||||
','.freeze => :comma,
|
',' => :comma,
|
||||||
'['.freeze => :open_square,
|
'[' => :open_square,
|
||||||
']'.freeze => :close_square,
|
']' => :close_square,
|
||||||
'('.freeze => :open_round,
|
'(' => :open_round,
|
||||||
')'.freeze => :close_round,
|
')' => :close_round
|
||||||
'?'.freeze => :question,
|
}
|
||||||
'-'.freeze => :dash
|
IDENTIFIER = /[\w\-?!]+/
|
||||||
}.freeze
|
|
||||||
IDENTIFIER = /[a-zA-Z_][\w-]*\??/
|
|
||||||
SINGLE_STRING_LITERAL = /'[^\']*'/
|
SINGLE_STRING_LITERAL = /'[^\']*'/
|
||||||
DOUBLE_STRING_LITERAL = /"[^\"]*"/
|
DOUBLE_STRING_LITERAL = /"[^\"]*"/
|
||||||
NUMBER_LITERAL = /-?\d+(\.\d+)?/
|
NUMBER_LITERAL = /-?\d+(\.\d+)?/
|
||||||
DOTDOT = /\.\./
|
DOTDOT = /\.\./
|
||||||
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/
|
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains/
|
||||||
WHITESPACE_OR_NOTHING = /\s*/
|
|
||||||
|
|
||||||
def initialize(input)
|
def initialize(input)
|
||||||
@ss = StringScanner.new(input)
|
@ss = StringScanner.new(input.rstrip)
|
||||||
end
|
end
|
||||||
|
|
||||||
def tokenize
|
def tokenize
|
||||||
@output = []
|
@output = []
|
||||||
|
|
||||||
until @ss.eos?
|
while !@ss.eos?
|
||||||
@ss.skip(WHITESPACE_OR_NOTHING)
|
@ss.skip(/\s*/)
|
||||||
break if @ss.eos?
|
|
||||||
tok = case
|
tok = case
|
||||||
when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t]
|
when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t]
|
||||||
when t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t]
|
when t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t]
|
||||||
@@ -41,7 +37,7 @@ module Liquid
|
|||||||
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}"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
errors:
|
errors:
|
||||||
syntax:
|
syntax:
|
||||||
tag_unexpected_args: "Syntax Error in '%{tag}' - Valid syntax: %{tag}"
|
|
||||||
assign: "Syntax Error in 'assign' - Valid syntax: assign [var] = [source]"
|
assign: "Syntax Error in 'assign' - Valid syntax: assign [var] = [source]"
|
||||||
capture: "Syntax Error in 'capture' - Valid syntax: capture [var]"
|
capture: "Syntax Error in 'capture' - Valid syntax: capture [var]"
|
||||||
case: "Syntax Error in 'case' - Valid syntax: case [condition]"
|
case: "Syntax Error in 'case' - Valid syntax: case [condition]"
|
||||||
@@ -14,13 +13,10 @@
|
|||||||
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]"
|
||||||
unknown_tag: "Unknown tag '%{tag}'"
|
unknown_tag: "Unknown tag '%{tag}'"
|
||||||
invalid_delimiter: "'%{tag}' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
|
invalid_delimiter: "'end' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
|
||||||
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"
|
|
||||||
tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
|
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"
|
tag_never_closed: "'%{block_name}' tag was never closed"
|
||||||
meta_syntax_error: "Liquid syntax error: #{e.message}"
|
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"
|
table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
|
||||||
argument:
|
|
||||||
include: "Argument error in tag 'include' - Illegal template name"
|
|
||||||
|
|||||||
62
lib/liquid/module_ex.rb
Normal file
62
lib/liquid/module_ex.rb
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# Copyright 2007 by Domizio Demichelis
|
||||||
|
# This library is free software. It may be used, redistributed and/or modified
|
||||||
|
# under the same terms as Ruby itself
|
||||||
|
#
|
||||||
|
# This extension is used in order to expose the object of the implementing class
|
||||||
|
# to liquid as it were a Drop. It also limits the liquid-callable methods of the instance
|
||||||
|
# to the allowed method passed with the liquid_methods call
|
||||||
|
# Example:
|
||||||
|
#
|
||||||
|
# class SomeClass
|
||||||
|
# liquid_methods :an_allowed_method
|
||||||
|
#
|
||||||
|
# def an_allowed_method
|
||||||
|
# 'this comes from an allowed method'
|
||||||
|
# end
|
||||||
|
# def unallowed_method
|
||||||
|
# 'this will never be an output'
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# if you want to extend the drop to other methods you can defines more methods
|
||||||
|
# in the class <YourClass>::LiquidDropClass
|
||||||
|
#
|
||||||
|
# class SomeClass::LiquidDropClass
|
||||||
|
# def another_allowed_method
|
||||||
|
# 'and this from another allowed method'
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# usage:
|
||||||
|
# @something = SomeClass.new
|
||||||
|
#
|
||||||
|
# template:
|
||||||
|
# {{something.an_allowed_method}}{{something.unallowed_method}} {{something.another_allowed_method}}
|
||||||
|
#
|
||||||
|
# output:
|
||||||
|
# 'this comes from an allowed method and this from another allowed method'
|
||||||
|
#
|
||||||
|
# You can also chain associations, by adding the liquid_method call in the
|
||||||
|
# association models.
|
||||||
|
#
|
||||||
|
class Module
|
||||||
|
|
||||||
|
def liquid_methods(*allowed_methods)
|
||||||
|
drop_class = eval "class #{self.to_s}::LiquidDropClass < Liquid::Drop; self; end"
|
||||||
|
define_method :to_liquid do
|
||||||
|
drop_class.new(self)
|
||||||
|
end
|
||||||
|
drop_class.class_eval do
|
||||||
|
def initialize(object)
|
||||||
|
@object = object
|
||||||
|
end
|
||||||
|
allowed_methods.each do |sym|
|
||||||
|
define_method sym do
|
||||||
|
@object.send sym
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
module Liquid
|
|
||||||
class ParseContext
|
|
||||||
attr_accessor :locale, :line_number, :trim_whitespace, :depth
|
|
||||||
attr_reader :partial, :warnings, :error_mode
|
|
||||||
|
|
||||||
def initialize(options = {})
|
|
||||||
@template_options = options ? options.dup : {}
|
|
||||||
@locale = @template_options[:locale] ||= I18n.new
|
|
||||||
@warnings = []
|
|
||||||
self.depth = 0
|
|
||||||
self.partial = false
|
|
||||||
end
|
|
||||||
|
|
||||||
def [](option_key)
|
|
||||||
@options[option_key]
|
|
||||||
end
|
|
||||||
|
|
||||||
def partial=(value)
|
|
||||||
@partial = value
|
|
||||||
@options = value ? partial_options : @template_options
|
|
||||||
@error_mode = @options[:error_mode] || Template.error_mode
|
|
||||||
value
|
|
||||||
end
|
|
||||||
|
|
||||||
def partial_options
|
|
||||||
@partial_options ||= begin
|
|
||||||
dont_pass = @template_options[:include_options_blacklist]
|
|
||||||
if dont_pass == true
|
|
||||||
{ locale: locale }
|
|
||||||
elsif dont_pass.is_a?(Array)
|
|
||||||
@template_options.reject { |k, v| dont_pass.include?(k) }
|
|
||||||
else
|
|
||||||
@template_options
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Liquid
|
|
||||||
class ParseTreeVisitor
|
|
||||||
def self.for(node, callbacks = Hash.new(proc {}))
|
|
||||||
if defined?(node.class::ParseTreeVisitor)
|
|
||||||
node.class::ParseTreeVisitor
|
|
||||||
else
|
|
||||||
self
|
|
||||||
end.new(node, callbacks)
|
|
||||||
end
|
|
||||||
|
|
||||||
def initialize(node, callbacks)
|
|
||||||
@node = node
|
|
||||||
@callbacks = callbacks
|
|
||||||
end
|
|
||||||
|
|
||||||
def add_callback_for(*classes, &block)
|
|
||||||
callback = block
|
|
||||||
callback = ->(node, _) { yield node } if block.arity.abs == 1
|
|
||||||
callback = ->(_, _) { yield } if block.arity.zero?
|
|
||||||
classes.each { |klass| @callbacks[klass] = callback }
|
|
||||||
self
|
|
||||||
end
|
|
||||||
|
|
||||||
def visit(context = nil)
|
|
||||||
children.map do |node|
|
|
||||||
item, new_context = @callbacks[node.class].call(node, context)
|
|
||||||
[
|
|
||||||
item,
|
|
||||||
ParseTreeVisitor.for(node, @callbacks).visit(new_context || context)
|
|
||||||
]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
protected
|
|
||||||
|
|
||||||
def children
|
|
||||||
@node.respond_to?(:nodelist) ? Array(@node.nodelist) : []
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -44,14 +44,11 @@ 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]
|
||||||
if token[0] == :id
|
if token[0] == :id
|
||||||
variable_signature
|
variable_signature
|
||||||
elsif SINGLE_TOKEN_EXPRESSION_TYPES.include? token[0]
|
elsif [:string, :number].include? token[0]
|
||||||
consume
|
consume
|
||||||
elsif token.first == :open_round
|
elsif token.first == :open_round
|
||||||
consume
|
consume
|
||||||
@@ -69,16 +66,15 @@ module Liquid
|
|||||||
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 << ' '.freeze
|
str << consume << consume << ' '
|
||||||
end
|
end
|
||||||
|
|
||||||
str << expression
|
str << expression
|
||||||
str
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def variable_signature
|
def variable_signature
|
||||||
str = consume(:id)
|
str = consume(:id)
|
||||||
while look(:open_square)
|
if look(:open_square)
|
||||||
str << consume
|
str << consume
|
||||||
str << expression
|
str << expression
|
||||||
str << consume(:close_square)
|
str << consume(:close_square)
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
module Liquid
|
|
||||||
module ParserSwitching
|
|
||||||
def parse_with_selected_parser(markup)
|
|
||||||
case parse_context.error_mode
|
|
||||||
when :strict then strict_parse_with_error_context(markup)
|
|
||||||
when :lax then lax_parse(markup)
|
|
||||||
when :warn
|
|
||||||
begin
|
|
||||||
return strict_parse_with_error_context(markup)
|
|
||||||
rescue SyntaxError => e
|
|
||||||
parse_context.warnings << e
|
|
||||||
return lax_parse(markup)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def strict_parse_with_error_context(markup)
|
|
||||||
strict_parse(markup)
|
|
||||||
rescue SyntaxError => e
|
|
||||||
e.line_number = line_number
|
|
||||||
e.markup_context = markup_context(markup)
|
|
||||||
raise e
|
|
||||||
end
|
|
||||||
|
|
||||||
def markup_context(markup)
|
|
||||||
"in \"#{markup.strip}\""
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
require 'liquid/profiler/hooks'
|
|
||||||
|
|
||||||
module Liquid
|
|
||||||
# Profiler enables support for profiling template rendering to help track down performance issues.
|
|
||||||
#
|
|
||||||
# To enable profiling, first require 'liquid/profiler'.
|
|
||||||
# Then, to profile a parse/render cycle, pass the <tt>profile: true</tt> option to <tt>Liquid::Template.parse</tt>.
|
|
||||||
# After <tt>Liquid::Template#render</tt> is called, the template object makes available an instance of this
|
|
||||||
# class via the <tt>Liquid::Template#profiler</tt> method.
|
|
||||||
#
|
|
||||||
# template = Liquid::Template.parse(template_content, profile: true)
|
|
||||||
# output = template.render
|
|
||||||
# profile = template.profiler
|
|
||||||
#
|
|
||||||
# This object contains all profiling information, containing information on what tags were rendered,
|
|
||||||
# where in the templates these tags live, and how long each tag took to render.
|
|
||||||
#
|
|
||||||
# This is a tree structure that is Enumerable all the way down, and keeps track of tags and rendering times
|
|
||||||
# inside of <tt>{% include %}</tt> tags.
|
|
||||||
#
|
|
||||||
# profile.each do |node|
|
|
||||||
# # Access to the node itself
|
|
||||||
# node.code
|
|
||||||
#
|
|
||||||
# # Which template and line number of this node.
|
|
||||||
# # If top level, this will be "<root>".
|
|
||||||
# node.partial
|
|
||||||
# node.line_number
|
|
||||||
#
|
|
||||||
# # Render time in seconds of this node
|
|
||||||
# node.render_time
|
|
||||||
#
|
|
||||||
# # If the template used {% include %}, this node will also have children.
|
|
||||||
# node.children.each do |child2|
|
|
||||||
# # ...
|
|
||||||
# end
|
|
||||||
# end
|
|
||||||
#
|
|
||||||
# Profiler also exposes the total time of the template's render in <tt>Liquid::Profiler#total_render_time</tt>.
|
|
||||||
#
|
|
||||||
# All render times are in seconds. There is a small performance hit when profiling is enabled.
|
|
||||||
#
|
|
||||||
class Profiler
|
|
||||||
include Enumerable
|
|
||||||
|
|
||||||
class Timing
|
|
||||||
attr_reader :code, :partial, :line_number, :children
|
|
||||||
|
|
||||||
def initialize(node, partial)
|
|
||||||
@code = node.respond_to?(:raw) ? node.raw : node
|
|
||||||
@partial = partial
|
|
||||||
@line_number = node.respond_to?(:line_number) ? node.line_number : nil
|
|
||||||
@children = []
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.start(node, partial)
|
|
||||||
new(node, partial).tap(&:start)
|
|
||||||
end
|
|
||||||
|
|
||||||
def start
|
|
||||||
@start_time = Time.now
|
|
||||||
end
|
|
||||||
|
|
||||||
def finish
|
|
||||||
@end_time = Time.now
|
|
||||||
end
|
|
||||||
|
|
||||||
def render_time
|
|
||||||
@end_time - @start_time
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.profile_node_render(node)
|
|
||||||
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
|
|
||||||
@partial_stack = ["<root>"]
|
|
||||||
|
|
||||||
@root_timing = Timing.new("", current_partial)
|
|
||||||
@timing_stack = [@root_timing]
|
|
||||||
|
|
||||||
@render_start_at = Time.now
|
|
||||||
@render_end_at = @render_start_at
|
|
||||||
end
|
|
||||||
|
|
||||||
def start
|
|
||||||
Thread.current[:liquid_profiler] = self
|
|
||||||
@render_start_at = Time.now
|
|
||||||
end
|
|
||||||
|
|
||||||
def stop
|
|
||||||
Thread.current[:liquid_profiler] = nil
|
|
||||||
@render_end_at = Time.now
|
|
||||||
end
|
|
||||||
|
|
||||||
def total_render_time
|
|
||||||
@render_end_at - @render_start_at
|
|
||||||
end
|
|
||||||
|
|
||||||
def each(&block)
|
|
||||||
@root_timing.children.each(&block)
|
|
||||||
end
|
|
||||||
|
|
||||||
def [](idx)
|
|
||||||
@root_timing.children[idx]
|
|
||||||
end
|
|
||||||
|
|
||||||
def length
|
|
||||||
@root_timing.children.length
|
|
||||||
end
|
|
||||||
|
|
||||||
def start_node(node)
|
|
||||||
@timing_stack.push(Timing.start(node, current_partial))
|
|
||||||
end
|
|
||||||
|
|
||||||
def end_node(_node)
|
|
||||||
timing = @timing_stack.pop
|
|
||||||
timing.finish
|
|
||||||
|
|
||||||
@timing_stack.last.children << timing
|
|
||||||
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
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
module Liquid
|
|
||||||
class BlockBody
|
|
||||||
def render_node_with_profiling(context, output, node)
|
|
||||||
Profiler.profile_node_render(node) do
|
|
||||||
render_node_without_profiling(context, output, node)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
alias_method :render_node_without_profiling, :render_node
|
|
||||||
alias_method :render_node, :render_node_with_profiling
|
|
||||||
end
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
alias_method :render_to_output_buffer_without_profiling, :render_to_output_buffer
|
|
||||||
alias_method :render_to_output_buffer, :render_to_output_buffer_with_profiling
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
module Liquid
|
|
||||||
class RangeLookup
|
|
||||||
def self.parse(start_markup, end_markup)
|
|
||||||
start_obj = Expression.parse(start_markup)
|
|
||||||
end_obj = Expression.parse(end_markup)
|
|
||||||
if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate)
|
|
||||||
new(start_obj, end_obj)
|
|
||||||
else
|
|
||||||
start_obj.to_i..end_obj.to_i
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def initialize(start_obj, end_obj)
|
|
||||||
@start_obj = start_obj
|
|
||||||
@end_obj = end_obj
|
|
||||||
end
|
|
||||||
|
|
||||||
def evaluate(context)
|
|
||||||
start_int = to_integer(context.evaluate(@start_obj))
|
|
||||||
end_int = to_integer(context.evaluate(@end_obj))
|
|
||||||
start_int..end_int
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def to_integer(input)
|
|
||||||
case input
|
|
||||||
when Integer
|
|
||||||
input
|
|
||||||
when NilClass, String
|
|
||||||
input.to_i
|
|
||||||
else
|
|
||||||
Utils.to_integer(input)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
module Liquid
|
|
||||||
class ResourceLimits
|
|
||||||
attr_accessor :render_length, :render_score, :assign_score,
|
|
||||||
:render_length_limit, :render_score_limit, :assign_score_limit
|
|
||||||
|
|
||||||
def initialize(limits)
|
|
||||||
@render_length_limit = limits[:render_length_limit]
|
|
||||||
@render_score_limit = limits[:render_score_limit]
|
|
||||||
@assign_score_limit = limits[:assign_score_limit]
|
|
||||||
reset
|
|
||||||
end
|
|
||||||
|
|
||||||
def reached?
|
|
||||||
(@render_length_limit && @render_length > @render_length_limit) ||
|
|
||||||
(@render_score_limit && @render_score > @render_score_limit) ||
|
|
||||||
(@assign_score_limit && @assign_score > @assign_score_limit)
|
|
||||||
end
|
|
||||||
|
|
||||||
def reset
|
|
||||||
@render_length = @render_score = @assign_score = 0
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -2,24 +2,14 @@ require 'cgi'
|
|||||||
require 'bigdecimal'
|
require 'bigdecimal'
|
||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
|
|
||||||
module StandardFilters
|
module StandardFilters
|
||||||
HTML_ESCAPE = {
|
HTML_ESCAPE = { '&' => '&', '>' => '>', '<' => '<', '"' => '"', "'" => ''' }
|
||||||
'&'.freeze => '&'.freeze,
|
|
||||||
'>'.freeze => '>'.freeze,
|
|
||||||
'<'.freeze => '<'.freeze,
|
|
||||||
'"'.freeze => '"'.freeze,
|
|
||||||
"'".freeze => '''.freeze
|
|
||||||
}.freeze
|
|
||||||
HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/
|
HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/
|
||||||
STRIP_HTML_BLOCKS = Regexp.union(
|
|
||||||
/<script.*?<\/script>/m,
|
|
||||||
/<!--.*?-->/m,
|
|
||||||
/<style.*?<\/style>/m
|
|
||||||
)
|
|
||||||
STRIP_HTML_TAGS = /<.*?>/m
|
|
||||||
|
|
||||||
# Return the size of an array or of an string
|
# Return the size of an array or of an string
|
||||||
def size(input)
|
def size(input)
|
||||||
|
|
||||||
input.respond_to?(:size) ? input.size : 0
|
input.respond_to?(:size) ? input.size : 0
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -39,56 +29,29 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
def escape(input)
|
def escape(input)
|
||||||
CGI.escapeHTML(input.to_s).untaint unless input.nil?
|
CGI.escapeHTML(input) rescue input
|
||||||
end
|
end
|
||||||
alias_method :h, :escape
|
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
def url_encode(input)
|
alias_method :h, :escape
|
||||||
CGI.escape(input.to_s) unless input.nil?
|
|
||||||
end
|
|
||||||
|
|
||||||
def url_decode(input)
|
|
||||||
return if input.nil?
|
|
||||||
|
|
||||||
result = CGI.unescape(input.to_s)
|
|
||||||
raise Liquid::ArgumentError, "invalid byte sequence in #{result.encoding}" unless result.valid_encoding?
|
|
||||||
|
|
||||||
result
|
|
||||||
end
|
|
||||||
|
|
||||||
def slice(input, offset, length = nil)
|
|
||||||
offset = Utils.to_integer(offset)
|
|
||||||
length = length ? Utils.to_integer(length) : 1
|
|
||||||
|
|
||||||
if input.is_a?(Array)
|
|
||||||
input.slice(offset, length) || []
|
|
||||||
else
|
|
||||||
input.to_s.slice(offset, length) || ''
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Truncate a string down to x characters
|
# Truncate a string down to x characters
|
||||||
def truncate(input, length = 50, truncate_string = "...".freeze)
|
def truncate(input, length = 50, truncate_string = "...")
|
||||||
return if input.nil?
|
if input.nil? then return end
|
||||||
input_str = input.to_s
|
l = length.to_i - truncate_string.length
|
||||||
length = Utils.to_integer(length)
|
|
||||||
truncate_string_str = truncate_string.to_s
|
|
||||||
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.length > length.to_i ? input[0...l] + truncate_string : input
|
||||||
end
|
end
|
||||||
|
|
||||||
def truncatewords(input, words = 15, truncate_string = "...".freeze)
|
def truncatewords(input, words = 15, truncate_string = "...")
|
||||||
return if input.nil?
|
if input.nil? then return end
|
||||||
wordlist = input.to_s.split
|
wordlist = input.to_s.split
|
||||||
words = Utils.to_integer(words)
|
l = words.to_i - 1
|
||||||
l = words - 1
|
|
||||||
l = 0 if l < 0
|
l = 0 if l < 0
|
||||||
wordlist.length > l ? wordlist[0..l].join(" ".freeze).concat(truncate_string.to_s) : input
|
wordlist.length > l ? wordlist[0..l].join(" ") + truncate_string : input
|
||||||
end
|
end
|
||||||
|
|
||||||
# Split input string into an array of substrings separated by given pattern.
|
# Split input string into an array of substrings separated by given pattern.
|
||||||
@@ -97,7 +60,7 @@ module Liquid
|
|||||||
# <div class="summary">{{ post | split '//' | first }}</div>
|
# <div class="summary">{{ post | split '//' | first }}</div>
|
||||||
#
|
#
|
||||||
def split(input, pattern)
|
def split(input, pattern)
|
||||||
input.to_s.split(pattern.to_s)
|
input.split(pattern)
|
||||||
end
|
end
|
||||||
|
|
||||||
def strip(input)
|
def strip(input)
|
||||||
@@ -113,160 +76,69 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
def strip_html(input)
|
def strip_html(input)
|
||||||
empty = ''.freeze
|
input.to_s.gsub(/<script.*?<\/script>/m, '').gsub(/<!--.*?-->/m, '').gsub(/<style.*?<\/style>/m, '').gsub(/<.*?>/m, '')
|
||||||
result = input.to_s.gsub(STRIP_HTML_BLOCKS, empty)
|
|
||||||
result.gsub!(STRIP_HTML_TAGS, empty)
|
|
||||||
result
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Remove all newlines from the string
|
# Remove all newlines from the string
|
||||||
def strip_newlines(input)
|
def strip_newlines(input)
|
||||||
input.to_s.gsub(/\r?\n/, ''.freeze)
|
input.to_s.gsub(/\r?\n/, '')
|
||||||
end
|
end
|
||||||
|
|
||||||
# Join elements of the array with certain character between them
|
# Join elements of the array with certain character between them
|
||||||
def join(input, glue = ' '.freeze)
|
def join(input, glue = ' ')
|
||||||
InputIterator.new(input).join(glue)
|
[input].flatten.join(glue)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Sort elements of the array
|
# Sort elements of the array
|
||||||
# provide optional property with which to sort an array of hashes or drops
|
# provide optional property with which to sort an array of hashes or drops
|
||||||
def sort(input, property = nil)
|
def sort(input, property = nil)
|
||||||
ary = InputIterator.new(input)
|
ary = flatten_if_necessary(input)
|
||||||
|
|
||||||
return [] if ary.empty?
|
|
||||||
|
|
||||||
if property.nil?
|
if property.nil?
|
||||||
ary.sort do |a, b|
|
ary.sort
|
||||||
nil_safe_compare(a, b)
|
elsif ary.first.respond_to?('[]') and !ary.first[property].nil?
|
||||||
end
|
ary.sort {|a,b| a[property] <=> b[property] }
|
||||||
elsif ary.all? { |el| el.respond_to?(:[]) }
|
elsif ary.first.respond_to?(property)
|
||||||
begin
|
ary.sort {|a,b| a.send(property) <=> b.send(property) }
|
||||||
ary.sort { |a, b| nil_safe_compare(a[property], b[property]) }
|
|
||||||
rescue TypeError
|
|
||||||
raise_property_error(property)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Sort elements of an array ignoring case if strings
|
|
||||||
# provide optional property with which to sort an array of hashes or drops
|
|
||||||
def sort_natural(input, property = nil)
|
|
||||||
ary = InputIterator.new(input)
|
|
||||||
|
|
||||||
return [] if ary.empty?
|
|
||||||
|
|
||||||
if property.nil?
|
|
||||||
ary.sort do |a, b|
|
|
||||||
nil_safe_casecmp(a, b)
|
|
||||||
end
|
|
||||||
elsif ary.all? { |el| el.respond_to?(:[]) }
|
|
||||||
begin
|
|
||||||
ary.sort { |a, b| nil_safe_casecmp(a[property], b[property]) }
|
|
||||||
rescue TypeError
|
|
||||||
raise_property_error(property)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Filter the elements of an array to those with a certain property value.
|
|
||||||
# By default the target is any truthy value.
|
|
||||||
def where(input, property, target_value = nil)
|
|
||||||
ary = InputIterator.new(input)
|
|
||||||
|
|
||||||
if ary.empty?
|
|
||||||
[]
|
|
||||||
elsif ary.first.respond_to?(:[]) && target_value.nil?
|
|
||||||
begin
|
|
||||||
ary.select { |item| item[property] }
|
|
||||||
rescue TypeError
|
|
||||||
raise_property_error(property)
|
|
||||||
end
|
|
||||||
elsif ary.first.respond_to?(:[])
|
|
||||||
begin
|
|
||||||
ary.select { |item| item[property] == target_value }
|
|
||||||
rescue TypeError
|
|
||||||
raise_property_error(property)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Remove duplicate elements from an array
|
|
||||||
# provide optional property with which to determine uniqueness
|
|
||||||
def uniq(input, property = nil)
|
|
||||||
ary = InputIterator.new(input)
|
|
||||||
|
|
||||||
if property.nil?
|
|
||||||
ary.uniq
|
|
||||||
elsif ary.empty? # The next two cases assume a non-empty array.
|
|
||||||
[]
|
|
||||||
elsif ary.first.respond_to?(:[])
|
|
||||||
begin
|
|
||||||
ary.uniq { |a| a[property] }
|
|
||||||
rescue TypeError
|
|
||||||
raise_property_error(property)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Reverse the elements of an array
|
# Reverse the elements of an array
|
||||||
def reverse(input)
|
def reverse(input)
|
||||||
ary = InputIterator.new(input)
|
ary = [input].flatten
|
||||||
ary.reverse
|
ary.reverse
|
||||||
end
|
end
|
||||||
|
|
||||||
# map/collect on a given property
|
# map/collect on a given property
|
||||||
def map(input, property)
|
def map(input, property)
|
||||||
InputIterator.new(input).map do |e|
|
flatten_if_necessary(input).map do |e|
|
||||||
e = e.call if e.is_a?(Proc)
|
e = e.call if e.is_a?(Proc)
|
||||||
|
|
||||||
if property == "to_liquid".freeze
|
if property == "to_liquid"
|
||||||
e
|
e
|
||||||
elsif e.respond_to?(:[])
|
elsif e.respond_to?(:[])
|
||||||
r = e[property]
|
e[property]
|
||||||
r.is_a?(Proc) ? r.call : r
|
|
||||||
end
|
|
||||||
end
|
|
||||||
rescue TypeError
|
|
||||||
raise_property_error(property)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Remove nils within an array
|
|
||||||
# provide optional property with which to check for nil
|
|
||||||
def compact(input, property = nil)
|
|
||||||
ary = InputIterator.new(input)
|
|
||||||
|
|
||||||
if property.nil?
|
|
||||||
ary.compact
|
|
||||||
elsif ary.empty? # The next two cases assume a non-empty array.
|
|
||||||
[]
|
|
||||||
elsif ary.first.respond_to?(:[])
|
|
||||||
begin
|
|
||||||
ary.reject { |a| a[property].nil? }
|
|
||||||
rescue TypeError
|
|
||||||
raise_property_error(property)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Replace occurrences of a string with another
|
# Replace occurrences of a string with another
|
||||||
def replace(input, string, replacement = ''.freeze)
|
def replace(input, string, replacement = '')
|
||||||
input.to_s.gsub(string.to_s, replacement.to_s)
|
input.to_s.gsub(string, replacement.to_s)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Replace the first occurrences of a string with another
|
# Replace the first occurrences of a string with another
|
||||||
def replace_first(input, string, replacement = ''.freeze)
|
def replace_first(input, string, replacement = '')
|
||||||
input.to_s.sub(string.to_s, replacement.to_s)
|
input.to_s.sub(string, replacement.to_s)
|
||||||
end
|
end
|
||||||
|
|
||||||
# remove a substring
|
# remove a substring
|
||||||
def remove(input, string)
|
def remove(input, string)
|
||||||
input.to_s.gsub(string.to_s, ''.freeze)
|
input.to_s.gsub(string, '')
|
||||||
end
|
end
|
||||||
|
|
||||||
# remove the first occurrences of a substring
|
# remove the first occurrences of a substring
|
||||||
def remove_first(input, string)
|
def remove_first(input, string)
|
||||||
input.to_s.sub(string.to_s, ''.freeze)
|
input.to_s.sub(string, '')
|
||||||
end
|
end
|
||||||
|
|
||||||
# add one string to another
|
# add one string to another
|
||||||
@@ -274,13 +146,6 @@ module Liquid
|
|||||||
input.to_s + string.to_s
|
input.to_s + string.to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
def concat(input, array)
|
|
||||||
unless array.respond_to?(:to_ary)
|
|
||||||
raise ArgumentError.new("concat filter requires an array argument")
|
|
||||||
end
|
|
||||||
InputIterator.new(input).concat(array)
|
|
||||||
end
|
|
||||||
|
|
||||||
# prepend a string to another
|
# prepend a string to another
|
||||||
def prepend(input, string)
|
def prepend(input, string)
|
||||||
string.to_s + input.to_s
|
string.to_s + input.to_s
|
||||||
@@ -288,10 +153,10 @@ module Liquid
|
|||||||
|
|
||||||
# Add <br /> tags in front of all newlines in input string
|
# Add <br /> tags in front of all newlines in input string
|
||||||
def newline_to_br(input)
|
def newline_to_br(input)
|
||||||
input.to_s.gsub(/\n/, "<br />\n".freeze)
|
input.to_s.gsub(/\n/, "<br />\n")
|
||||||
end
|
end
|
||||||
|
|
||||||
# Reformat a date using Ruby's core Time#strftime( string ) -> string
|
# Reformat a date
|
||||||
#
|
#
|
||||||
# %a - The abbreviated weekday name (``Sun'')
|
# %a - The abbreviated weekday name (``Sun'')
|
||||||
# %A - The full weekday name (``Sunday'')
|
# %A - The full weekday name (``Sunday'')
|
||||||
@@ -305,7 +170,6 @@ module Liquid
|
|||||||
# %m - Month of the year (01..12)
|
# %m - Month of the year (01..12)
|
||||||
# %M - Minute of the hour (00..59)
|
# %M - Minute of the hour (00..59)
|
||||||
# %p - Meridian indicator (``AM'' or ``PM'')
|
# %p - Meridian indicator (``AM'' or ``PM'')
|
||||||
# %s - Number of seconds since 1970-01-01 00:00:00 UTC.
|
|
||||||
# %S - Second of the minute (00..60)
|
# %S - Second of the minute (00..60)
|
||||||
# %U - Week number of the current year,
|
# %U - Week number of the current year,
|
||||||
# starting with the first Sunday as the first
|
# starting with the first Sunday as the first
|
||||||
@@ -320,14 +184,34 @@ module Liquid
|
|||||||
# %Y - Year with century
|
# %Y - Year with century
|
||||||
# %Z - Time zone name
|
# %Z - Time zone name
|
||||||
# %% - Literal ``%'' character
|
# %% - Literal ``%'' character
|
||||||
#
|
|
||||||
# See also: http://www.ruby-doc.org/core/Time.html#method-i-strftime
|
|
||||||
def date(input, format)
|
def date(input, format)
|
||||||
return input if format.to_s.empty?
|
|
||||||
|
|
||||||
return input unless date = Utils.to_date(input)
|
if format.to_s.empty?
|
||||||
|
return input.to_s
|
||||||
|
end
|
||||||
|
|
||||||
date.strftime(format.to_s)
|
if ((input.is_a?(String) && !/\A\d+\z/.match(input.to_s).nil?) || input.is_a?(Integer)) && input.to_i > 0
|
||||||
|
input = Time.at(input.to_i)
|
||||||
|
end
|
||||||
|
|
||||||
|
date = if input.is_a?(String)
|
||||||
|
case input.downcase
|
||||||
|
when 'now', 'today'
|
||||||
|
Time.now
|
||||||
|
else
|
||||||
|
Time.parse(input)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
input
|
||||||
|
end
|
||||||
|
|
||||||
|
if date.respond_to?(:strftime)
|
||||||
|
date.strftime(format.to_s)
|
||||||
|
else
|
||||||
|
input
|
||||||
|
end
|
||||||
|
rescue
|
||||||
|
input
|
||||||
end
|
end
|
||||||
|
|
||||||
# Get the first element of the passed in array
|
# Get the first element of the passed in array
|
||||||
@@ -348,12 +232,6 @@ module Liquid
|
|||||||
array.last if array.respond_to?(:last)
|
array.last if array.respond_to?(:last)
|
||||||
end
|
end
|
||||||
|
|
||||||
# absolute value
|
|
||||||
def abs(input)
|
|
||||||
result = Utils.to_number(input).abs
|
|
||||||
result.is_a?(BigDecimal) ? result.to_f : result
|
|
||||||
end
|
|
||||||
|
|
||||||
# addition
|
# addition
|
||||||
def plus(input, operand)
|
def plus(input, operand)
|
||||||
apply_operation(input, operand, :+)
|
apply_operation(input, operand, :+)
|
||||||
@@ -372,134 +250,47 @@ module Liquid
|
|||||||
# division
|
# division
|
||||||
def divided_by(input, operand)
|
def divided_by(input, operand)
|
||||||
apply_operation(input, operand, :/)
|
apply_operation(input, operand, :/)
|
||||||
rescue ::ZeroDivisionError => e
|
|
||||||
raise Liquid::ZeroDivisionError, e.message
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def modulo(input, operand)
|
def modulo(input, operand)
|
||||||
apply_operation(input, operand, :%)
|
apply_operation(input, operand, :%)
|
||||||
rescue ::ZeroDivisionError => e
|
|
||||||
raise Liquid::ZeroDivisionError, e.message
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def round(input, n = 0)
|
def default(input, default_value = "")
|
||||||
result = Utils.to_number(input).round(Utils.to_number(n))
|
is_blank = input.respond_to?(:empty?) ? input.empty? : !input
|
||||||
result = result.to_f if result.is_a?(BigDecimal)
|
is_blank ? default_value : input
|
||||||
result = result.to_i if n == 0
|
|
||||||
result
|
|
||||||
rescue ::FloatDomainError => e
|
|
||||||
raise Liquid::FloatDomainError, e.message
|
|
||||||
end
|
|
||||||
|
|
||||||
def ceil(input)
|
|
||||||
Utils.to_number(input).ceil.to_i
|
|
||||||
rescue ::FloatDomainError => e
|
|
||||||
raise Liquid::FloatDomainError, e.message
|
|
||||||
end
|
|
||||||
|
|
||||||
def floor(input)
|
|
||||||
Utils.to_number(input).floor.to_i
|
|
||||||
rescue ::FloatDomainError => e
|
|
||||||
raise Liquid::FloatDomainError, e.message
|
|
||||||
end
|
|
||||||
|
|
||||||
def at_least(input, n)
|
|
||||||
min_value = Utils.to_number(n)
|
|
||||||
|
|
||||||
result = Utils.to_number(input)
|
|
||||||
result = min_value if min_value > result
|
|
||||||
result.is_a?(BigDecimal) ? result.to_f : result
|
|
||||||
end
|
|
||||||
|
|
||||||
def at_most(input, n)
|
|
||||||
max_value = Utils.to_number(n)
|
|
||||||
|
|
||||||
result = Utils.to_number(input)
|
|
||||||
result = max_value if max_value < result
|
|
||||||
result.is_a?(BigDecimal) ? result.to_f : result
|
|
||||||
end
|
|
||||||
|
|
||||||
def default(input, default_value = ''.freeze)
|
|
||||||
if !input || input.respond_to?(:empty?) && input.empty?
|
|
||||||
default_value
|
|
||||||
else
|
|
||||||
input
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def raise_property_error(property)
|
def flatten_if_necessary(input)
|
||||||
raise Liquid::ArgumentError.new("cannot select the property '#{property}'")
|
ary = if input.is_a?(Array)
|
||||||
|
input.flatten
|
||||||
|
elsif input.is_a?(Enumerable) && !input.is_a?(Hash)
|
||||||
|
input
|
||||||
|
else
|
||||||
|
[input].flatten
|
||||||
|
end
|
||||||
|
ary.map{ |e| e.respond_to?(:to_liquid) ? e.to_liquid : e }
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_number(obj)
|
||||||
|
case obj
|
||||||
|
when Float
|
||||||
|
BigDecimal.new(obj.to_s)
|
||||||
|
when Numeric
|
||||||
|
obj
|
||||||
|
when String
|
||||||
|
(obj.strip =~ /\A\d+\.\d+\z/) ? BigDecimal.new(obj) : obj.to_i
|
||||||
|
else
|
||||||
|
0
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def apply_operation(input, operand, operation)
|
def apply_operation(input, operand, operation)
|
||||||
result = Utils.to_number(input).send(operation, Utils.to_number(operand))
|
result = to_number(input).send(operation, to_number(operand))
|
||||||
result.is_a?(BigDecimal) ? result.to_f : result
|
result.is_a?(BigDecimal) ? result.to_f : result
|
||||||
end
|
end
|
||||||
|
|
||||||
def nil_safe_compare(a, b)
|
|
||||||
if !a.nil? && !b.nil?
|
|
||||||
a <=> b
|
|
||||||
else
|
|
||||||
a.nil? ? 1 : -1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def nil_safe_casecmp(a, b)
|
|
||||||
if !a.nil? && !b.nil?
|
|
||||||
a.to_s.casecmp(b.to_s)
|
|
||||||
else
|
|
||||||
a.nil? ? 1 : -1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class InputIterator
|
|
||||||
include Enumerable
|
|
||||||
|
|
||||||
def initialize(input)
|
|
||||||
@input = if input.is_a?(Array)
|
|
||||||
input.flatten
|
|
||||||
elsif input.is_a?(Hash)
|
|
||||||
[input]
|
|
||||||
elsif input.is_a?(Enumerable)
|
|
||||||
input
|
|
||||||
else
|
|
||||||
Array(input)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def join(glue)
|
|
||||||
to_a.join(glue.to_s)
|
|
||||||
end
|
|
||||||
|
|
||||||
def concat(args)
|
|
||||||
to_a.concat(args)
|
|
||||||
end
|
|
||||||
|
|
||||||
def reverse
|
|
||||||
reverse_each.to_a
|
|
||||||
end
|
|
||||||
|
|
||||||
def uniq(&block)
|
|
||||||
to_a.uniq(&block)
|
|
||||||
end
|
|
||||||
|
|
||||||
def compact
|
|
||||||
to_a.compact
|
|
||||||
end
|
|
||||||
|
|
||||||
def empty?
|
|
||||||
@input.each { return false }
|
|
||||||
true
|
|
||||||
end
|
|
||||||
|
|
||||||
def each
|
|
||||||
@input.each do |e|
|
|
||||||
yield(e.respond_to?(:to_liquid) ? e.to_liquid : e)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
Template.register_filter(StandardFilters)
|
Template.register_filter(StandardFilters)
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
require 'set'
|
require 'set'
|
||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
|
|
||||||
# Strainer is the parent class for the filters system.
|
# 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.
|
# 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,
|
# The Strainer only allows method calls defined in filters given to it via Strainer.global_filter,
|
||||||
# Context#add_filters or Template.register_filter
|
# Context#add_filters or Template.register_filter
|
||||||
class Strainer #:nodoc:
|
class Strainer #:nodoc:
|
||||||
@@global_strainer = Class.new(Strainer) do
|
@@filters = []
|
||||||
@filter_methods = Set.new
|
@@known_filters = Set.new
|
||||||
end
|
@@known_methods = Set.new
|
||||||
@@strainer_class_cache = Hash.new do |hash, filters|
|
@@strainer_class_cache = Hash.new do |hash, filters|
|
||||||
hash[filters] = Class.new(@@global_strainer) do
|
hash[filters] = Class.new(Strainer) do
|
||||||
@filter_methods = @@global_strainer.filter_methods.dup
|
filters.each { |f| include f }
|
||||||
filters.each { |f| add_filter(f) }
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -21,46 +21,43 @@ module Liquid
|
|||||||
@context = context
|
@context = context
|
||||||
end
|
end
|
||||||
|
|
||||||
class << self
|
def self.global_filter(filter)
|
||||||
attr_reader :filter_methods
|
raise ArgumentError, "Passed filter is not a module" unless filter.is_a?(Module)
|
||||||
|
add_known_filter(filter)
|
||||||
|
@@filters << filter unless @@filters.include?(filter)
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.add_filter(filter)
|
def self.add_known_filter(filter)
|
||||||
raise ArgumentError, "Expected module but got: #{filter.class}" unless filter.is_a?(Module)
|
unless @@known_filters.include?(filter)
|
||||||
unless self.include?(filter)
|
@@method_blacklist ||= Set.new(Strainer.instance_methods.map(&:to_s))
|
||||||
invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }
|
new_methods = filter.instance_methods.map(&:to_s)
|
||||||
if invokable_non_public_methods.any?
|
new_methods.reject!{ |m| @@method_blacklist.include?(m) }
|
||||||
raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"
|
@@known_methods.merge(new_methods)
|
||||||
else
|
@@known_filters.add(filter)
|
||||||
send(:include, filter)
|
|
||||||
@filter_methods.merge(filter.public_instance_methods.map(&:to_s))
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.global_filter(filter)
|
def self.strainer_class_cache
|
||||||
@@strainer_class_cache.clear
|
@@strainer_class_cache
|
||||||
@@global_strainer.add_filter(filter)
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.invokable?(method)
|
|
||||||
@filter_methods.include?(method.to_s)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.create(context, filters = [])
|
def self.create(context, filters = [])
|
||||||
@@strainer_class_cache[filters].new(context)
|
filters = @@filters + filters
|
||||||
|
strainer_class_cache[filters].new(context)
|
||||||
end
|
end
|
||||||
|
|
||||||
def invoke(method, *args)
|
def invoke(method, *args)
|
||||||
if self.class.invokable?(method)
|
if invokable?(method)
|
||||||
send(method, *args)
|
send(method, *args)
|
||||||
elsif @context && @context.strict_filters
|
|
||||||
raise Liquid::UndefinedFilter, "undefined filter #{method}"
|
|
||||||
else
|
else
|
||||||
args.first
|
args.first
|
||||||
end
|
end
|
||||||
rescue ::ArgumentError => e
|
rescue ::ArgumentError => e
|
||||||
raise Liquid::ArgumentError, e.message, e.backtrace
|
raise Liquid::ArgumentError.new(e.message)
|
||||||
|
end
|
||||||
|
|
||||||
|
def invokable?(method)
|
||||||
|
@@known_methods.include?(method.to_s) && respond_to?(method)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
module Liquid
|
|
||||||
class TablerowloopDrop < Drop
|
|
||||||
def initialize(length, cols)
|
|
||||||
@length = length
|
|
||||||
@row = 1
|
|
||||||
@col = 1
|
|
||||||
@cols = cols
|
|
||||||
@index = 0
|
|
||||||
end
|
|
||||||
|
|
||||||
attr_reader :length, :col, :row
|
|
||||||
|
|
||||||
def index
|
|
||||||
@index + 1
|
|
||||||
end
|
|
||||||
|
|
||||||
def index0
|
|
||||||
@index
|
|
||||||
end
|
|
||||||
|
|
||||||
def col0
|
|
||||||
@col - 1
|
|
||||||
end
|
|
||||||
|
|
||||||
def rindex
|
|
||||||
@length - @index
|
|
||||||
end
|
|
||||||
|
|
||||||
def rindex0
|
|
||||||
@length - @index - 1
|
|
||||||
end
|
|
||||||
|
|
||||||
def first
|
|
||||||
@index == 0
|
|
||||||
end
|
|
||||||
|
|
||||||
def last
|
|
||||||
@index == @length - 1
|
|
||||||
end
|
|
||||||
|
|
||||||
def col_first
|
|
||||||
@col == 1
|
|
||||||
end
|
|
||||||
|
|
||||||
def col_last
|
|
||||||
@col == @cols
|
|
||||||
end
|
|
||||||
|
|
||||||
protected
|
|
||||||
|
|
||||||
def increment!
|
|
||||||
@index += 1
|
|
||||||
|
|
||||||
if @col == @cols
|
|
||||||
@col = 1
|
|
||||||
@row += 1
|
|
||||||
else
|
|
||||||
@col += 1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,51 +1,56 @@
|
|||||||
module Liquid
|
module Liquid
|
||||||
class Tag
|
class Tag
|
||||||
attr_reader :nodelist, :tag_name, :line_number, :parse_context
|
attr_accessor :options
|
||||||
alias_method :options, :parse_context
|
attr_reader :nodelist, :warnings
|
||||||
include ParserSwitching
|
|
||||||
|
|
||||||
class << self
|
def self.new_with_options(tag_name, markup, tokens, options)
|
||||||
def parse(tag_name, markup, tokenizer, parse_context)
|
# Forgive me Matz for I have sinned. I know this code is weird
|
||||||
tag = new(tag_name, markup, parse_context)
|
# but it was necessary to maintain API compatibility.
|
||||||
tag.parse(tokenizer)
|
new_tag = self.allocate
|
||||||
tag
|
new_tag.options = options
|
||||||
end
|
new_tag.send(:initialize, tag_name, markup, tokens)
|
||||||
|
new_tag
|
||||||
private :new
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def initialize(tag_name, markup, parse_context)
|
def initialize(tag_name, markup, tokens)
|
||||||
@tag_name = tag_name
|
@tag_name = tag_name
|
||||||
@markup = markup
|
@markup = markup
|
||||||
@parse_context = parse_context
|
@options ||= {} # needs || because might be set before initialize
|
||||||
@line_number = parse_context.line_number
|
|
||||||
end
|
|
||||||
|
|
||||||
def parse(_tokens)
|
|
||||||
end
|
|
||||||
|
|
||||||
def raw
|
|
||||||
"#{@tag_name} #{@markup}"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def name
|
def name
|
||||||
self.class.name.downcase
|
self.class.name.downcase
|
||||||
end
|
end
|
||||||
|
|
||||||
def render(_context)
|
def render(context)
|
||||||
''.freeze
|
''
|
||||||
end
|
|
||||||
|
|
||||||
# For backwards compatibility with custom tags. In a future release, the semantics
|
|
||||||
# of the `render_to_output_buffer` method will become the default and the `render`
|
|
||||||
# method will be removed.
|
|
||||||
def render_to_output_buffer(context, output)
|
|
||||||
output << render(context)
|
|
||||||
output
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def blank?
|
def blank?
|
||||||
false
|
@blank || false
|
||||||
end
|
end
|
||||||
end
|
|
||||||
end
|
def parse_with_selected_parser(markup)
|
||||||
|
case @options[:error_mode] || Template.error_mode
|
||||||
|
when :strict then strict_parse_with_error_context(markup)
|
||||||
|
when :lax then lax_parse(markup)
|
||||||
|
when :warn
|
||||||
|
begin
|
||||||
|
return strict_parse_with_error_context(markup)
|
||||||
|
rescue SyntaxError => e
|
||||||
|
@warnings ||= []
|
||||||
|
@warnings << e
|
||||||
|
return lax_parse(markup)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def strict_parse_with_error_context(markup)
|
||||||
|
strict_parse(markup)
|
||||||
|
rescue SyntaxError => e
|
||||||
|
e.message << " in \"#{markup.strip}\""
|
||||||
|
raise e
|
||||||
|
end
|
||||||
|
end # Tag
|
||||||
|
end # Liquid
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
module Liquid
|
module Liquid
|
||||||
|
|
||||||
# Assign sets a variable in your template.
|
# Assign sets a variable in your template.
|
||||||
#
|
#
|
||||||
# {% assign foo = 'monkey' %}
|
# {% assign foo = 'monkey' %}
|
||||||
@@ -8,56 +9,30 @@ module Liquid
|
|||||||
# {{ foo }}
|
# {{ foo }}
|
||||||
#
|
#
|
||||||
class Assign < Tag
|
class Assign < Tag
|
||||||
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
|
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/o
|
||||||
|
|
||||||
def self.syntax_error_translation_key
|
def initialize(tag_name, markup, tokens)
|
||||||
"errors.syntax.assign".freeze
|
|
||||||
end
|
|
||||||
|
|
||||||
attr_reader :to, :from
|
|
||||||
|
|
||||||
def initialize(tag_name, markup, options)
|
|
||||||
super
|
|
||||||
if markup =~ Syntax
|
if markup =~ Syntax
|
||||||
@to = $1
|
@to = $1
|
||||||
@from = Variable.new($2, options)
|
@from = Variable.new($2)
|
||||||
else
|
else
|
||||||
raise SyntaxError.new(options[:locale].t(self.class.syntax_error_translation_key))
|
raise SyntaxError.new options[:locale].t("errors.syntax.assign")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_to_output_buffer(context, output)
|
def render(context)
|
||||||
val = @from.render(context)
|
val = @from.render(context)
|
||||||
context.scopes.last[@to] = val
|
context.scopes.last[@to] = val
|
||||||
context.resource_limits.assign_score += assign_score_of(val)
|
context.increment_used_resources(:assign_score_current, val)
|
||||||
output
|
''
|
||||||
end
|
end
|
||||||
|
|
||||||
def blank?
|
def blank?
|
||||||
true
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def assign_score_of(val)
|
|
||||||
if val.instance_of?(String)
|
|
||||||
val.bytesize
|
|
||||||
elsif val.instance_of?(Array) || val.instance_of?(Hash)
|
|
||||||
sum = 1
|
|
||||||
# Uses #each to avoid extra allocations.
|
|
||||||
val.each { |child| sum += assign_score_of(child) }
|
|
||||||
sum
|
|
||||||
else
|
|
||||||
1
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
|
||||||
def children
|
|
||||||
[@node.from]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
Template.register_tag('assign'.freeze, Assign)
|
Template.register_tag('assign', Assign)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
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.
|
||||||
#
|
#
|
||||||
# == Basic Usage:
|
# == Basic Usage:
|
||||||
@@ -9,10 +10,12 @@ module Liquid
|
|||||||
# {% endfor %}
|
# {% endfor %}
|
||||||
#
|
#
|
||||||
class Break < Tag
|
class Break < Tag
|
||||||
|
|
||||||
def interrupt
|
def interrupt
|
||||||
BreakInterrupt.new
|
BreakInterrupt.new
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
Template.register_tag('break'.freeze, Break)
|
Template.register_tag('break', Break)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
module Liquid
|
module Liquid
|
||||||
|
|
||||||
# Capture stores the result of a block into a variable without rendering it inplace.
|
# Capture stores the result of a block into a variable without rendering it inplace.
|
||||||
#
|
#
|
||||||
# {% capture heading %}
|
# {% capture heading %}
|
||||||
@@ -11,23 +12,23 @@ module Liquid
|
|||||||
# in a sidebar or footer.
|
# in a sidebar or footer.
|
||||||
#
|
#
|
||||||
class Capture < Block
|
class Capture < Block
|
||||||
Syntax = /(#{VariableSignature}+)/o
|
Syntax = /(\w+)/
|
||||||
|
|
||||||
def initialize(tag_name, markup, options)
|
def initialize(tag_name, markup, tokens)
|
||||||
super
|
|
||||||
if markup =~ Syntax
|
if markup =~ Syntax
|
||||||
@to = $1
|
@to = $1
|
||||||
else
|
else
|
||||||
raise SyntaxError.new(options[:locale].t("errors.syntax.capture"))
|
raise SyntaxError.new(options[:locale].t("errors.syntax.capture"))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_to_output_buffer(context, output)
|
def render(context)
|
||||||
previous_output_size = output.bytesize
|
output = super
|
||||||
super
|
|
||||||
context.scopes.last[@to] = output
|
context.scopes.last[@to] = output
|
||||||
context.resource_limits.assign_score += (output.bytesize - previous_output_size)
|
context.increment_used_resources(:assign_score_current, output)
|
||||||
output
|
''
|
||||||
end
|
end
|
||||||
|
|
||||||
def blank?
|
def blank?
|
||||||
@@ -35,5 +36,5 @@ module Liquid
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
Template.register_tag('capture'.freeze, Capture)
|
Template.register_tag('capture', Capture)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,94 +1,80 @@
|
|||||||
module Liquid
|
module Liquid
|
||||||
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}.*))?/o
|
||||||
|
|
||||||
attr_reader :blocks, :left
|
def initialize(tag_name, markup, tokens)
|
||||||
|
|
||||||
def initialize(tag_name, markup, options)
|
|
||||||
super
|
|
||||||
@blocks = []
|
@blocks = []
|
||||||
|
|
||||||
if markup =~ Syntax
|
if markup =~ Syntax
|
||||||
@left = Expression.parse($1)
|
@left = $1
|
||||||
else
|
else
|
||||||
raise SyntaxError.new(options[:locale].t("errors.syntax.case".freeze))
|
raise SyntaxError.new(options[:locale].t("errors.syntax.case"))
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def parse(tokens)
|
super
|
||||||
body = BlockBody.new
|
|
||||||
while parse_body(body, tokens)
|
|
||||||
body = @blocks.last.attachment
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def nodelist
|
def nodelist
|
||||||
@blocks.map(&:attachment)
|
@blocks.map(&:attachment).flatten
|
||||||
end
|
end
|
||||||
|
|
||||||
def unknown_tag(tag, markup, tokens)
|
def unknown_tag(tag, markup, tokens)
|
||||||
|
@nodelist = []
|
||||||
case tag
|
case tag
|
||||||
when 'when'.freeze
|
when 'when'
|
||||||
record_when_condition(markup)
|
record_when_condition(markup)
|
||||||
when 'else'.freeze
|
when 'else'
|
||||||
record_else_condition(markup)
|
record_else_condition(markup)
|
||||||
else
|
else
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_to_output_buffer(context, output)
|
def render(context)
|
||||||
context.stack do
|
context.stack do
|
||||||
execute_else_block = true
|
execute_else_block = true
|
||||||
|
|
||||||
|
output = ''
|
||||||
@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
|
return render_all(block.attachment, context) if execute_else_block
|
||||||
elsif block.evaluate(context)
|
elsif block.evaluate(context)
|
||||||
execute_else_block = false
|
execute_else_block = false
|
||||||
block.attachment.render_to_output_buffer(context, output)
|
output << render_all(block.attachment, context)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
output
|
||||||
end
|
end
|
||||||
|
|
||||||
output
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def record_when_condition(markup)
|
def record_when_condition(markup)
|
||||||
body = BlockBody.new
|
|
||||||
|
|
||||||
while markup
|
while markup
|
||||||
unless markup =~ WhenSyntax
|
# Create a new nodelist and assign it to the new block
|
||||||
raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_when".freeze))
|
if not markup =~ WhenSyntax
|
||||||
|
raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_when"))
|
||||||
end
|
end
|
||||||
|
|
||||||
markup = $2
|
markup = $2
|
||||||
|
|
||||||
block = Condition.new(@left, '=='.freeze, Expression.parse($1))
|
block = Condition.new(@left, '==', $1)
|
||||||
block.attach(body)
|
block.attach(@nodelist)
|
||||||
@blocks << block
|
@blocks.push(block)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def record_else_condition(markup)
|
def record_else_condition(markup)
|
||||||
unless markup.strip.empty?
|
if not markup.strip.empty?
|
||||||
raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_else".freeze))
|
raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_else"))
|
||||||
end
|
end
|
||||||
|
|
||||||
block = ElseCondition.new
|
block = ElseCondition.new
|
||||||
block.attach(BlockBody.new)
|
block.attach(@nodelist)
|
||||||
@blocks << block
|
@blocks << block
|
||||||
end
|
end
|
||||||
|
|
||||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
|
||||||
def children
|
|
||||||
[@node.left] + @node.blocks
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
Template.register_tag('case'.freeze, Case)
|
Template.register_tag('case', Case)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
module Liquid
|
module Liquid
|
||||||
class Comment < Block
|
class Comment < Block
|
||||||
def render_to_output_buffer(_context, output)
|
def render(context)
|
||||||
output
|
''
|
||||||
end
|
end
|
||||||
|
|
||||||
def unknown_tag(_tag, _markup, _tokens)
|
def unknown_tag(tag, markup, tokens)
|
||||||
end
|
end
|
||||||
|
|
||||||
def blank?
|
def blank?
|
||||||
@@ -12,5 +12,5 @@ module Liquid
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
Template.register_tag('comment'.freeze, Comment)
|
Template.register_tag('comment', Comment)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
module Liquid
|
module Liquid
|
||||||
|
|
||||||
# Continue tag to be used to break out of a for loop.
|
# Continue tag to be used to break out of a for loop.
|
||||||
#
|
#
|
||||||
# == Basic Usage:
|
# == Basic Usage:
|
||||||
@@ -9,10 +10,12 @@ module Liquid
|
|||||||
# {% endfor %}
|
# {% endfor %}
|
||||||
#
|
#
|
||||||
class Continue < Tag
|
class Continue < Tag
|
||||||
|
|
||||||
def interrupt
|
def interrupt
|
||||||
ContinueInterrupt.new
|
ContinueInterrupt.new
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
Template.register_tag('continue'.freeze, Continue)
|
Template.register_tag('continue', Continue)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -13,63 +13,47 @@ module Liquid
|
|||||||
#
|
#
|
||||||
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*(.*)/o
|
||||||
|
|
||||||
attr_reader :variables
|
def initialize(tag_name, markup, tokens)
|
||||||
|
|
||||||
def initialize(tag_name, markup, options)
|
|
||||||
super
|
|
||||||
case markup
|
case markup
|
||||||
when NamedSyntax
|
when NamedSyntax
|
||||||
@variables = variables_from_string($2)
|
@variables = variables_from_string($2)
|
||||||
@name = Expression.parse($1)
|
@name = $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.new(options[:locale].t("errors.syntax.cycle".freeze))
|
raise SyntaxError.new(options[:locale].t("errors.syntax.cycle"))
|
||||||
|
end
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
|
def render(context)
|
||||||
|
context.registers[:cycle] ||= Hash.new(0)
|
||||||
|
|
||||||
|
context.stack do
|
||||||
|
key = context[@name]
|
||||||
|
iteration = context.registers[:cycle][key]
|
||||||
|
result = context[@variables[iteration]]
|
||||||
|
iteration += 1
|
||||||
|
iteration = 0 if iteration >= @variables.size
|
||||||
|
context.registers[:cycle][key] = iteration
|
||||||
|
result
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_to_output_buffer(context, output)
|
def blank?
|
||||||
context.registers[:cycle] ||= {}
|
false
|
||||||
|
|
||||||
context.stack do
|
|
||||||
key = context.evaluate(@name)
|
|
||||||
iteration = context.registers[:cycle][key].to_i
|
|
||||||
|
|
||||||
val = context.evaluate(@variables[iteration])
|
|
||||||
|
|
||||||
if val.is_a?(Array)
|
|
||||||
val = val.join
|
|
||||||
elsif !val.is_a?(String)
|
|
||||||
val = val.to_s
|
|
||||||
end
|
|
||||||
|
|
||||||
output << val
|
|
||||||
|
|
||||||
iteration += 1
|
|
||||||
iteration = 0 if iteration >= @variables.size
|
|
||||||
context.registers[:cycle][key] = iteration
|
|
||||||
end
|
|
||||||
|
|
||||||
output
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
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
|
||||||
$1 ? Expression.parse($1) : nil
|
$1 ? $1 : nil
|
||||||
end.compact
|
end.compact
|
||||||
end
|
end
|
||||||
|
|
||||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
|
||||||
def children
|
|
||||||
Array(@node.variables)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
Template.register_tag('cycle', Cycle)
|
Template.register_tag('cycle', Cycle)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
module Liquid
|
module Liquid
|
||||||
|
|
||||||
# decrement is used in a place where one needs to insert a counter
|
# decrement is used in a place where one needs to insert a counter
|
||||||
# into a template, and needs the counter to survive across
|
# into a template, and needs the counter to survive across
|
||||||
# multiple instantiations of the template.
|
# multiple instantiations of the template.
|
||||||
@@ -18,19 +19,21 @@ module Liquid
|
|||||||
# Hello: -3
|
# Hello: -3
|
||||||
#
|
#
|
||||||
class Decrement < Tag
|
class Decrement < Tag
|
||||||
def initialize(tag_name, markup, options)
|
def initialize(tag_name, markup, tokens)
|
||||||
super
|
|
||||||
@variable = markup.strip
|
@variable = markup.strip
|
||||||
|
|
||||||
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_to_output_buffer(context, output)
|
def render(context)
|
||||||
value = context.environments.first[@variable] ||= 0
|
value = context.environments.first[@variable] ||= 0
|
||||||
value -= 1
|
value = value - 1
|
||||||
context.environments.first[@variable] = value
|
context.environments.first[@variable] = value
|
||||||
output << value.to_s
|
value.to_s
|
||||||
output
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
end
|
end
|
||||||
|
|
||||||
Template.register_tag('decrement'.freeze, Decrement)
|
Template.register_tag('decrement', Decrement)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
module Liquid
|
|
||||||
# Echo outputs an expression
|
|
||||||
#
|
|
||||||
# {% echo monkey %}
|
|
||||||
# {% echo user.name %}
|
|
||||||
#
|
|
||||||
# This is identical to variable output syntax, like {{ foo }}, but works
|
|
||||||
# inside {% liquid %} tags. The full syntax is supported, including filters:
|
|
||||||
#
|
|
||||||
# {% echo user | link %}
|
|
||||||
#
|
|
||||||
class Echo < Tag
|
|
||||||
def initialize(tag_name, markup, parse_context)
|
|
||||||
super
|
|
||||||
@variable = Variable.new(markup, parse_context)
|
|
||||||
end
|
|
||||||
|
|
||||||
def render(context)
|
|
||||||
@variable.render(context)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
Template.register_tag('echo'.freeze, Echo)
|
|
||||||
end
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
module Liquid
|
module Liquid
|
||||||
|
|
||||||
# "For" iterates over an array or collection.
|
# "For" iterates over an array or collection.
|
||||||
# Several useful variables are available to you within the loop.
|
# Several useful variables are available to you within the loop.
|
||||||
#
|
#
|
||||||
@@ -23,7 +24,7 @@ module Liquid
|
|||||||
# {{ item.name }}
|
# {{ item.name }}
|
||||||
# {% end %}
|
# {% 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`)
|
# To reverse the for loop simply use {% for item in collection reversed %}
|
||||||
#
|
#
|
||||||
# == Available variables:
|
# == Available variables:
|
||||||
#
|
#
|
||||||
@@ -41,45 +42,84 @@ module Liquid
|
|||||||
# where 0 is the last item.
|
# where 0 is the last item.
|
||||||
# forloop.first:: Returns true if the item is the first item.
|
# forloop.first:: Returns true if the item is the first item.
|
||||||
# forloop.last:: Returns true if the item is the last item.
|
# forloop.last:: Returns true if the item is the last item.
|
||||||
# forloop.parentloop:: Provides access to the parent loop, if present.
|
|
||||||
#
|
#
|
||||||
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
|
||||||
|
|
||||||
attr_reader :collection_name, :variable_name, :limit, :from
|
def initialize(tag_name, markup, tokens)
|
||||||
|
|
||||||
def initialize(tag_name, markup, options)
|
|
||||||
super
|
|
||||||
@from = @limit = nil
|
|
||||||
parse_with_selected_parser(markup)
|
parse_with_selected_parser(markup)
|
||||||
@for_block = BlockBody.new
|
@nodelist = @for_block = []
|
||||||
@else_block = nil
|
super
|
||||||
end
|
|
||||||
|
|
||||||
def parse(tokens)
|
|
||||||
return unless parse_body(@for_block, tokens)
|
|
||||||
parse_body(@else_block, tokens)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def nodelist
|
def nodelist
|
||||||
@else_block ? [@for_block, @else_block] : [@for_block]
|
if @else_block
|
||||||
|
@for_block + @else_block
|
||||||
|
else
|
||||||
|
@for_block
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def unknown_tag(tag, markup, tokens)
|
def unknown_tag(tag, markup, tokens)
|
||||||
return super unless tag == 'else'.freeze
|
return super unless tag == 'else'
|
||||||
@else_block = BlockBody.new
|
@nodelist = @else_block = []
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_to_output_buffer(context, output)
|
def render(context)
|
||||||
segment = collection_segment(context)
|
context.registers[:for] ||= Hash.new(0)
|
||||||
|
|
||||||
if segment.empty?
|
collection = context[@collection_name]
|
||||||
render_else(context, output)
|
collection = collection.to_a if collection.is_a?(Range)
|
||||||
|
|
||||||
|
# Maintains Ruby 1.8.7 String#each behaviour on 1.9
|
||||||
|
return render_else(context) unless iterable?(collection)
|
||||||
|
|
||||||
|
from = if @attributes['offset'] == 'continue'
|
||||||
|
context.registers[:for][@name].to_i
|
||||||
else
|
else
|
||||||
render_segment(context, output, segment)
|
context[@attributes['offset']].to_i
|
||||||
end
|
end
|
||||||
|
|
||||||
output
|
limit = context[@attributes['limit']]
|
||||||
|
to = limit ? limit.to_i + from : nil
|
||||||
|
|
||||||
|
segment = Utils.slice_collection(collection, from, to)
|
||||||
|
|
||||||
|
return render_else(context) if segment.empty?
|
||||||
|
|
||||||
|
segment.reverse! if @reversed
|
||||||
|
|
||||||
|
result = ''
|
||||||
|
|
||||||
|
length = segment.length
|
||||||
|
|
||||||
|
# Store our progress through the collection for the continue flag
|
||||||
|
context.registers[:for][@name] = from + segment.length
|
||||||
|
|
||||||
|
context.stack do
|
||||||
|
segment.each_with_index do |item, index|
|
||||||
|
context[@variable_name] = item
|
||||||
|
context['forloop'] = {
|
||||||
|
'name' => @name,
|
||||||
|
'length' => length,
|
||||||
|
'index' => index + 1,
|
||||||
|
'index0' => index,
|
||||||
|
'rindex' => length - index,
|
||||||
|
'rindex0' => length - index - 1,
|
||||||
|
'first' => (index == 0),
|
||||||
|
'last' => (index == length - 1) }
|
||||||
|
|
||||||
|
result << render_all(@for_block, context)
|
||||||
|
|
||||||
|
# Handle any interrupts if they exist.
|
||||||
|
if context.has_interrupt?
|
||||||
|
interrupt = context.pop_interrupt
|
||||||
|
break if interrupt.is_a? BreakInterrupt
|
||||||
|
next if interrupt.is_a? ContinueInterrupt
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
result
|
||||||
end
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
@@ -87,130 +127,48 @@ module Liquid
|
|||||||
def lax_parse(markup)
|
def lax_parse(markup)
|
||||||
if markup =~ Syntax
|
if markup =~ Syntax
|
||||||
@variable_name = $1
|
@variable_name = $1
|
||||||
collection_name = $2
|
@collection_name = $2
|
||||||
@reversed = !!$3
|
@name = "#{$1}-#{$2}"
|
||||||
@name = "#{@variable_name}-#{collection_name}"
|
@reversed = $3
|
||||||
@collection_name = Expression.parse(collection_name)
|
@attributes = {}
|
||||||
markup.scan(TagAttributes) do |key, value|
|
markup.scan(TagAttributes) do |key, value|
|
||||||
set_attribute(key, value)
|
@attributes[key] = value
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
raise SyntaxError.new(options[:locale].t("errors.syntax.for".freeze))
|
raise SyntaxError.new(options[:locale].t("errors.syntax.for"))
|
||||||
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.new(options[:locale].t("errors.syntax.for_invalid_in".freeze)) unless p.id?('in'.freeze)
|
raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_in")) unless p.id?('in')
|
||||||
collection_name = p.expression
|
@collection_name = p.expression
|
||||||
@name = "#{@variable_name}-#{collection_name}"
|
@name = "#{@variable_name}-#{@collection_name}"
|
||||||
@collection_name = Expression.parse(collection_name)
|
@reversed = p.id?('reversed')
|
||||||
@reversed = p.id?('reversed'.freeze)
|
|
||||||
|
|
||||||
|
@attributes = {}
|
||||||
while p.look(:id) && p.look(:colon, 1)
|
while p.look(:id) && p.look(:colon, 1)
|
||||||
unless attribute = p.id?('limit'.freeze) || p.id?('offset'.freeze)
|
unless attribute = p.id?('limit') || p.id?('offset')
|
||||||
raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_attribute".freeze))
|
raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_attribute"))
|
||||||
end
|
end
|
||||||
p.consume
|
p.consume
|
||||||
set_attribute(attribute, p.expression)
|
val = p.expression
|
||||||
|
@attributes[attribute] = val
|
||||||
end
|
end
|
||||||
p.consume(:end_of_string)
|
p.consume(:end_of_string)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def collection_segment(context)
|
def render_else(context)
|
||||||
offsets = context.registers[:for] ||= {}
|
return @else_block ? [render_all(@else_block, context)] : ''
|
||||||
|
|
||||||
from = if @from == :continue
|
|
||||||
offsets[@name].to_i
|
|
||||||
else
|
|
||||||
from_value = context.evaluate(@from)
|
|
||||||
if from_value.nil?
|
|
||||||
0
|
|
||||||
else
|
|
||||||
Utils.to_integer(from_value)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
collection = context.evaluate(@collection_name)
|
def iterable?(collection)
|
||||||
collection = collection.to_a if collection.is_a?(Range)
|
collection.respond_to?(:each) || Utils.non_blank_string?(collection)
|
||||||
|
|
||||||
limit_value = context.evaluate(@limit)
|
|
||||||
to = if limit_value.nil?
|
|
||||||
nil
|
|
||||||
else
|
|
||||||
Utils.to_integer(limit_value) + from
|
|
||||||
end
|
end
|
||||||
|
|
||||||
segment = Utils.slice_collection(collection, from, to)
|
|
||||||
segment.reverse! if @reversed
|
|
||||||
|
|
||||||
offsets[@name] = from + segment.length
|
|
||||||
|
|
||||||
segment
|
|
||||||
end
|
|
||||||
|
|
||||||
def render_segment(context, output, segment)
|
|
||||||
for_stack = context.registers[:for_stack] ||= []
|
|
||||||
length = segment.length
|
|
||||||
|
|
||||||
context.stack do
|
|
||||||
loop_vars = Liquid::ForloopDrop.new(@name, length, for_stack[-1])
|
|
||||||
|
|
||||||
for_stack.push(loop_vars)
|
|
||||||
|
|
||||||
begin
|
|
||||||
context['forloop'.freeze] = loop_vars
|
|
||||||
|
|
||||||
segment.each do |item|
|
|
||||||
context[@variable_name] = item
|
|
||||||
@for_block.render_to_output_buffer(context, output)
|
|
||||||
loop_vars.send(:increment!)
|
|
||||||
|
|
||||||
# Handle any interrupts if they exist.
|
|
||||||
if context.interrupt?
|
|
||||||
interrupt = context.pop_interrupt
|
|
||||||
break if interrupt.is_a? BreakInterrupt
|
|
||||||
next if interrupt.is_a? ContinueInterrupt
|
|
||||||
end
|
|
||||||
end
|
|
||||||
ensure
|
|
||||||
for_stack.pop
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
output
|
|
||||||
end
|
|
||||||
|
|
||||||
def set_attribute(key, expr)
|
|
||||||
case key
|
|
||||||
when 'offset'.freeze
|
|
||||||
@from = if expr == 'continue'.freeze
|
|
||||||
:continue
|
|
||||||
else
|
|
||||||
Expression.parse(expr)
|
|
||||||
end
|
|
||||||
when 'limit'.freeze
|
|
||||||
@limit = Expression.parse(expr)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def render_else(context, output)
|
|
||||||
if @else_block
|
|
||||||
@else_block.render_to_output_buffer(context, output)
|
|
||||||
else
|
|
||||||
output
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
|
||||||
def children
|
|
||||||
(super + [@node.limit, @node.from, @node.collection_name]).compact
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
Template.register_tag('for'.freeze, For)
|
Template.register_tag('for', For)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -12,112 +12,95 @@ module Liquid
|
|||||||
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)
|
||||||
|
|
||||||
attr_reader :blocks
|
def initialize(tag_name, markup, tokens)
|
||||||
|
|
||||||
def initialize(tag_name, markup, options)
|
|
||||||
super
|
|
||||||
@blocks = []
|
@blocks = []
|
||||||
push_block('if'.freeze, markup)
|
push_block('if', markup)
|
||||||
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
def nodelist
|
def nodelist
|
||||||
@blocks.map(&:attachment)
|
@blocks.map(&:attachment).flatten
|
||||||
end
|
|
||||||
|
|
||||||
def parse(tokens)
|
|
||||||
while parse_body(@blocks.last.attachment, tokens)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def unknown_tag(tag, markup, tokens)
|
def unknown_tag(tag, markup, tokens)
|
||||||
if ['elsif'.freeze, 'else'.freeze].include?(tag)
|
if ['elsif', 'else'].include?(tag)
|
||||||
push_block(tag, markup)
|
push_block(tag, markup)
|
||||||
else
|
else
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_to_output_buffer(context, output)
|
def render(context)
|
||||||
context.stack do
|
context.stack do
|
||||||
@blocks.each do |block|
|
@blocks.each do |block|
|
||||||
if block.evaluate(context)
|
if block.evaluate(context)
|
||||||
return block.attachment.render_to_output_buffer(context, output)
|
return render_all(block.attachment, context)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
''
|
||||||
end
|
end
|
||||||
|
|
||||||
output
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def push_block(tag, markup)
|
def push_block(tag, markup)
|
||||||
block = if tag == 'else'.freeze
|
block = if tag == 'else'
|
||||||
ElseCondition.new
|
ElseCondition.new
|
||||||
else
|
else
|
||||||
parse_with_selected_parser(markup)
|
parse_with_selected_parser(markup)
|
||||||
|
end
|
||||||
|
|
||||||
|
@blocks.push(block)
|
||||||
|
@nodelist = block.attach(Array.new)
|
||||||
end
|
end
|
||||||
|
|
||||||
@blocks.push(block)
|
def lax_parse(markup)
|
||||||
block.attach(BlockBody.new)
|
expressions = markup.scan(ExpressionsAndOperators).reverse
|
||||||
end
|
raise(SyntaxError.new(options[:locale].t("errors.syntax.if"))) unless expressions.shift =~ Syntax
|
||||||
|
|
||||||
def lax_parse(markup)
|
condition = Condition.new($1, $2, $3)
|
||||||
expressions = markup.scan(ExpressionsAndOperators)
|
|
||||||
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop =~ Syntax
|
|
||||||
|
|
||||||
condition = Condition.new(Expression.parse($1), $2, Expression.parse($3))
|
while not expressions.empty?
|
||||||
|
operator = (expressions.shift).to_s.strip
|
||||||
|
|
||||||
until expressions.empty?
|
raise(SyntaxError.new(options[:locale].t("errors.syntax.if"))) unless expressions.shift.to_s =~ Syntax
|
||||||
operator = expressions.pop.to_s.strip
|
|
||||||
|
|
||||||
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop.to_s =~ Syntax
|
new_condition = Condition.new($1, $2, $3)
|
||||||
|
raise(SyntaxError.new(options[:locale].t("errors.syntax.if"))) unless BOOLEAN_OPERATORS.include?(operator)
|
||||||
|
new_condition.send(operator, condition)
|
||||||
|
condition = new_condition
|
||||||
|
end
|
||||||
|
|
||||||
new_condition = Condition.new(Expression.parse($1), $2, Expression.parse($3))
|
condition
|
||||||
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless BOOLEAN_OPERATORS.include?(operator)
|
|
||||||
new_condition.send(operator, condition)
|
|
||||||
condition = new_condition
|
|
||||||
end
|
end
|
||||||
|
|
||||||
condition
|
def strict_parse(markup)
|
||||||
end
|
p = Parser.new(markup)
|
||||||
|
|
||||||
def strict_parse(markup)
|
condition = parse_comparison(p)
|
||||||
p = Parser.new(markup)
|
|
||||||
condition = parse_binary_comparisons(p)
|
|
||||||
p.consume(:end_of_string)
|
|
||||||
condition
|
|
||||||
end
|
|
||||||
|
|
||||||
def parse_binary_comparisons(p)
|
while op = (p.id?('and') || p.id?('or'))
|
||||||
condition = parse_comparison(p)
|
new_cond = parse_comparison(p)
|
||||||
first_condition = condition
|
new_cond.send(op, condition)
|
||||||
while op = (p.id?('and'.freeze) || p.id?('or'.freeze))
|
condition = new_cond
|
||||||
child_condition = parse_comparison(p)
|
end
|
||||||
condition.send(op, child_condition)
|
p.consume(:end_of_string)
|
||||||
condition = child_condition
|
|
||||||
|
condition
|
||||||
end
|
end
|
||||||
first_condition
|
|
||||||
end
|
|
||||||
|
|
||||||
def parse_comparison(p)
|
def parse_comparison(p)
|
||||||
a = Expression.parse(p.expression)
|
a = p.expression
|
||||||
if op = p.consume?(:comparison)
|
if op = p.consume?(:comparison)
|
||||||
b = Expression.parse(p.expression)
|
b = p.expression
|
||||||
Condition.new(a, op, b)
|
Condition.new(a, op, b)
|
||||||
else
|
else
|
||||||
Condition.new(a)
|
Condition.new(a)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
|
||||||
def children
|
|
||||||
@node.blocks
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
Template.register_tag('if'.freeze, If)
|
Template.register_tag('if', If)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
module Liquid
|
module Liquid
|
||||||
class Ifchanged < Block
|
class Ifchanged < Block
|
||||||
def render_to_output_buffer(context, output)
|
|
||||||
context.stack do
|
|
||||||
block_output = ''
|
|
||||||
super(context, block_output)
|
|
||||||
|
|
||||||
if block_output != context.registers[:ifchanged]
|
def render(context)
|
||||||
context.registers[:ifchanged] = block_output
|
context.stack do
|
||||||
output << block_output
|
|
||||||
|
output = super
|
||||||
|
|
||||||
|
if output != context.registers[:ifchanged]
|
||||||
|
context.registers[:ifchanged] = output
|
||||||
|
output
|
||||||
|
else
|
||||||
|
''
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
output
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
Template.register_tag('ifchanged'.freeze, Ifchanged)
|
Template.register_tag('ifchanged', Ifchanged)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
module Liquid
|
module Liquid
|
||||||
|
|
||||||
# Include allows templates to relate with other templates
|
# Include allows templates to relate with other templates
|
||||||
#
|
#
|
||||||
# Simply include another template:
|
# Simply include another template:
|
||||||
@@ -16,111 +17,79 @@ module Liquid
|
|||||||
class Include < Tag
|
class Include < Tag
|
||||||
Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/o
|
Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/o
|
||||||
|
|
||||||
attr_reader :template_name_expr, :variable_name_expr, :attributes
|
def initialize(tag_name, markup, tokens)
|
||||||
|
|
||||||
def initialize(tag_name, markup, options)
|
|
||||||
super
|
|
||||||
|
|
||||||
if markup =~ Syntax
|
if markup =~ Syntax
|
||||||
|
|
||||||
template_name = $1
|
@template_name = $1
|
||||||
variable_name = $3
|
@variable_name = $3
|
||||||
|
@attributes = {}
|
||||||
@variable_name_expr = variable_name ? Expression.parse(variable_name) : nil
|
|
||||||
@template_name_expr = Expression.parse(template_name)
|
|
||||||
@attributes = {}
|
|
||||||
|
|
||||||
markup.scan(TagAttributes) do |key, value|
|
markup.scan(TagAttributes) do |key, value|
|
||||||
@attributes[key] = Expression.parse(value)
|
@attributes[key] = value
|
||||||
end
|
end
|
||||||
|
|
||||||
else
|
else
|
||||||
raise SyntaxError.new(options[:locale].t("errors.syntax.include".freeze))
|
raise SyntaxError.new(options[:locale].t("errors.syntax.include"))
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def parse(_tokens)
|
|
||||||
end
|
|
||||||
|
|
||||||
def render_to_output_buffer(context, output)
|
|
||||||
template_name = context.evaluate(@template_name_expr)
|
|
||||||
raise ArgumentError.new(options[:locale].t("errors.argument.include")) unless template_name
|
|
||||||
|
|
||||||
partial = load_cached_partial(template_name, context)
|
|
||||||
context_variable_name = template_name.split('/'.freeze).last
|
|
||||||
|
|
||||||
variable = if @variable_name_expr
|
|
||||||
context.evaluate(@variable_name_expr)
|
|
||||||
else
|
|
||||||
context.find_variable(template_name, raise_on_not_found: false)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
old_template_name = context.template_name
|
super
|
||||||
old_partial = context.partial
|
end
|
||||||
begin
|
|
||||||
context.template_name = template_name
|
|
||||||
context.partial = true
|
|
||||||
context.stack do
|
|
||||||
@attributes.each do |key, value|
|
|
||||||
context[key] = context.evaluate(value)
|
|
||||||
end
|
|
||||||
|
|
||||||
if variable.is_a?(Array)
|
def blank?
|
||||||
variable.each do |var|
|
false
|
||||||
context[context_variable_name] = var
|
end
|
||||||
partial.render_to_output_buffer(context, output)
|
|
||||||
end
|
def render(context)
|
||||||
else
|
partial = load_cached_partial(context)
|
||||||
context[context_variable_name] = variable
|
variable = context[@variable_name || @template_name[1..-2]]
|
||||||
partial.render_to_output_buffer(context, output)
|
|
||||||
end
|
context.stack do
|
||||||
|
@attributes.each do |key, value|
|
||||||
|
context[key] = context[value]
|
||||||
end
|
end
|
||||||
ensure
|
|
||||||
context.template_name = old_template_name
|
|
||||||
context.partial = old_partial
|
|
||||||
end
|
|
||||||
|
|
||||||
output
|
context_variable_name = @template_name[1..-2].split('/').last
|
||||||
|
if variable.is_a?(Array)
|
||||||
|
variable.collect do |var|
|
||||||
|
context[context_variable_name] = var
|
||||||
|
partial.render(context)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
context[context_variable_name] = variable
|
||||||
|
partial.render(context)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
def load_cached_partial(context)
|
||||||
|
cached_partials = context.registers[:cached_partials] || {}
|
||||||
|
template_name = context[@template_name]
|
||||||
|
|
||||||
alias_method :parse_context, :options
|
if cached = cached_partials[template_name]
|
||||||
private :parse_context
|
return cached
|
||||||
|
end
|
||||||
def load_cached_partial(template_name, context)
|
source = read_template_from_file_system(context)
|
||||||
cached_partials = context.registers[:cached_partials] || {}
|
partial = Liquid::Template.parse(source)
|
||||||
|
cached_partials[template_name] = partial
|
||||||
if cached = cached_partials[template_name]
|
context.registers[:cached_partials] = cached_partials
|
||||||
return cached
|
partial
|
||||||
end
|
end
|
||||||
source = read_template_from_file_system(context)
|
|
||||||
begin
|
def read_template_from_file_system(context)
|
||||||
parse_context.partial = true
|
file_system = context.registers[:file_system] || Liquid::Template.file_system
|
||||||
partial = Liquid::Template.parse(source, parse_context)
|
|
||||||
ensure
|
# make read_template_file call backwards-compatible.
|
||||||
parse_context.partial = false
|
case file_system.method(:read_template_file).arity
|
||||||
|
when 1
|
||||||
|
file_system.read_template_file(context[@template_name])
|
||||||
|
when 2
|
||||||
|
file_system.read_template_file(context[@template_name], context)
|
||||||
|
else
|
||||||
|
raise ArgumentError, "file_system.read_template_file expects two parameters: (template_name, context)"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
cached_partials[template_name] = partial
|
|
||||||
context.registers[:cached_partials] = cached_partials
|
|
||||||
partial
|
|
||||||
end
|
|
||||||
|
|
||||||
def read_template_from_file_system(context)
|
|
||||||
file_system = context.registers[:file_system] || Liquid::Template.file_system
|
|
||||||
|
|
||||||
file_system.read_template_file(context.evaluate(@template_name_expr))
|
|
||||||
end
|
|
||||||
|
|
||||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
|
||||||
def children
|
|
||||||
[
|
|
||||||
@node.template_name_expr,
|
|
||||||
@node.variable_name_expr
|
|
||||||
] + @node.attributes.values
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
Template.register_tag('include'.freeze, Include)
|
Template.register_tag('include', Include)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -15,18 +15,21 @@ module Liquid
|
|||||||
# Hello: 2
|
# Hello: 2
|
||||||
#
|
#
|
||||||
class Increment < Tag
|
class Increment < Tag
|
||||||
def initialize(tag_name, markup, options)
|
def initialize(tag_name, markup, tokens)
|
||||||
super
|
|
||||||
@variable = markup.strip
|
@variable = markup.strip
|
||||||
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_to_output_buffer(context, output)
|
def render(context)
|
||||||
value = context.environments.first[@variable] ||= 0
|
value = context.environments.first[@variable] ||= 0
|
||||||
context.environments.first[@variable] = value + 1
|
context.environments.first[@variable] = value + 1
|
||||||
output << value.to_s
|
value.to_s
|
||||||
output
|
end
|
||||||
|
|
||||||
|
def blank?
|
||||||
|
false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
Template.register_tag('increment'.freeze, Increment)
|
Template.register_tag('increment', Increment)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,48 +1,19 @@
|
|||||||
module Liquid
|
module Liquid
|
||||||
class Raw < Block
|
class Raw < Block
|
||||||
Syntax = /\A\s*\z/
|
FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/o
|
||||||
FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
|
|
||||||
|
|
||||||
def initialize(tag_name, markup, parse_context)
|
def parse_body(tokens)
|
||||||
super
|
@nodelist ||= []
|
||||||
|
@nodelist.clear
|
||||||
ensure_valid_markup(tag_name, markup, parse_context)
|
|
||||||
end
|
|
||||||
|
|
||||||
def parse(tokens)
|
|
||||||
@body = ''
|
|
||||||
while token = tokens.shift
|
while token = tokens.shift
|
||||||
if token =~ FullTokenPossiblyInvalid
|
if token =~ FullTokenPossiblyInvalid
|
||||||
@body << $1 if $1 != "".freeze
|
@nodelist << $1 if $1 != ""
|
||||||
return if block_delimiter == $2
|
return if block_delimiter == $2
|
||||||
end
|
end
|
||||||
@body << token unless token.empty?
|
@nodelist << token if not token.empty?
|
||||||
end
|
|
||||||
|
|
||||||
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
|
|
||||||
end
|
|
||||||
|
|
||||||
def render_to_output_buffer(_context, output)
|
|
||||||
output << @body
|
|
||||||
output
|
|
||||||
end
|
|
||||||
|
|
||||||
def nodelist
|
|
||||||
[@body]
|
|
||||||
end
|
|
||||||
|
|
||||||
def blank?
|
|
||||||
@body.empty?
|
|
||||||
end
|
|
||||||
|
|
||||||
protected
|
|
||||||
|
|
||||||
def ensure_valid_markup(tag_name, markup, parse_context)
|
|
||||||
unless markup =~ Syntax
|
|
||||||
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_unexpected_args".freeze, tag: tag_name))
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
Template.register_tag('raw'.freeze, Raw)
|
Template.register_tag('raw', Raw)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
module Liquid
|
|
||||||
class TableRow < Block
|
|
||||||
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
|
|
||||||
|
|
||||||
attr_reader :variable_name, :collection_name, :attributes
|
|
||||||
|
|
||||||
def initialize(tag_name, markup, options)
|
|
||||||
super
|
|
||||||
if markup =~ Syntax
|
|
||||||
@variable_name = $1
|
|
||||||
@collection_name = Expression.parse($2)
|
|
||||||
@attributes = {}
|
|
||||||
markup.scan(TagAttributes) do |key, value|
|
|
||||||
@attributes[key] = Expression.parse(value)
|
|
||||||
end
|
|
||||||
else
|
|
||||||
raise SyntaxError.new(options[:locale].t("errors.syntax.table_row".freeze))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def render_to_output_buffer(context, output)
|
|
||||||
collection = context.evaluate(@collection_name) or return ''.freeze
|
|
||||||
|
|
||||||
from = @attributes.key?('offset'.freeze) ? context.evaluate(@attributes['offset'.freeze]).to_i : 0
|
|
||||||
to = @attributes.key?('limit'.freeze) ? from + context.evaluate(@attributes['limit'.freeze]).to_i : nil
|
|
||||||
|
|
||||||
collection = Utils.slice_collection(collection, from, to)
|
|
||||||
|
|
||||||
length = collection.length
|
|
||||||
|
|
||||||
cols = context.evaluate(@attributes['cols'.freeze]).to_i
|
|
||||||
|
|
||||||
output << "<tr class=\"row1\">\n"
|
|
||||||
context.stack do
|
|
||||||
tablerowloop = Liquid::TablerowloopDrop.new(length, cols)
|
|
||||||
context['tablerowloop'.freeze] = tablerowloop
|
|
||||||
|
|
||||||
collection.each do |item|
|
|
||||||
context[@variable_name] = item
|
|
||||||
|
|
||||||
output << "<td class=\"col#{tablerowloop.col}\">"
|
|
||||||
super
|
|
||||||
output << '</td>'
|
|
||||||
|
|
||||||
if tablerowloop.col_last && !tablerowloop.last
|
|
||||||
output << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">"
|
|
||||||
end
|
|
||||||
|
|
||||||
tablerowloop.send(:increment!)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
output << "</tr>\n"
|
|
||||||
output
|
|
||||||
end
|
|
||||||
|
|
||||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
|
||||||
def children
|
|
||||||
super + @node.attributes.values + [@node.collection_name]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
Template.register_tag('tablerow'.freeze, TableRow)
|
|
||||||
end
|
|
||||||
@@ -1,30 +1,33 @@
|
|||||||
require_relative 'if'
|
require File.dirname(__FILE__) + '/if'
|
||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
|
|
||||||
# Unless is a conditional just like 'if' but works on the inverse logic.
|
# Unless is a conditional just like 'if' but works on the inverse logic.
|
||||||
#
|
#
|
||||||
# {% unless x < 0 %} x is greater than zero {% endunless %}
|
# {% unless x < 0 %} x is greater than zero {% end %}
|
||||||
#
|
#
|
||||||
class Unless < If
|
class Unless < If
|
||||||
def render_to_output_buffer(context, output)
|
def render(context)
|
||||||
context.stack do
|
context.stack do
|
||||||
|
|
||||||
# First condition is interpreted backwards ( if not )
|
# First condition is interpreted backwards ( if not )
|
||||||
first_block = @blocks.first
|
first_block = @blocks.first
|
||||||
unless first_block.evaluate(context)
|
unless first_block.evaluate(context)
|
||||||
return first_block.attachment.render_to_output_buffer(context, output)
|
return render_all(first_block.attachment, context)
|
||||||
end
|
end
|
||||||
|
|
||||||
# After the first condition unless works just like if
|
# After the first condition unless works just like if
|
||||||
@blocks[1..-1].each do |block|
|
@blocks[1..-1].each do |block|
|
||||||
if block.evaluate(context)
|
if block.evaluate(context)
|
||||||
return block.attachment.render_to_output_buffer(context, output)
|
return render_all(block.attachment, context)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
output
|
''
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
Template.register_tag('unless'.freeze, Unless)
|
|
||||||
|
Template.register_tag('unless', Unless)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
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
|
||||||
# source code you got. During compile time some extensive error checking is performed.
|
# source code you got. During compile time some extensive error checking is performed.
|
||||||
@@ -13,67 +14,14 @@ module Liquid
|
|||||||
# template.render('user_name' => 'bob')
|
# template.render('user_name' => 'bob')
|
||||||
#
|
#
|
||||||
class Template
|
class Template
|
||||||
attr_accessor :root
|
DEFAULT_OPTIONS = {
|
||||||
attr_reader :resource_limits, :warnings
|
:locale => I18n.new
|
||||||
|
}
|
||||||
|
|
||||||
|
attr_accessor :root, :resource_limits
|
||||||
@@file_system = BlankFileSystem.new
|
@@file_system = BlankFileSystem.new
|
||||||
|
|
||||||
class TagRegistry
|
|
||||||
include Enumerable
|
|
||||||
|
|
||||||
def initialize
|
|
||||||
@tags = {}
|
|
||||||
@cache = {}
|
|
||||||
end
|
|
||||||
|
|
||||||
def [](tag_name)
|
|
||||||
return nil unless @tags.key?(tag_name)
|
|
||||||
return @cache[tag_name] if Liquid.cache_classes
|
|
||||||
|
|
||||||
lookup_class(@tags[tag_name]).tap { |o| @cache[tag_name] = o }
|
|
||||||
end
|
|
||||||
|
|
||||||
def []=(tag_name, klass)
|
|
||||||
@tags[tag_name] = klass.name
|
|
||||||
@cache[tag_name] = klass
|
|
||||||
end
|
|
||||||
|
|
||||||
def delete(tag_name)
|
|
||||||
@tags.delete(tag_name)
|
|
||||||
@cache.delete(tag_name)
|
|
||||||
end
|
|
||||||
|
|
||||||
def each(&block)
|
|
||||||
@tags.each(&block)
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def lookup_class(name)
|
|
||||||
Object.const_get(name)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
attr_reader :profiler
|
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
# Sets how strict the parser should be.
|
|
||||||
# :lax acts like liquid 2.5 and silently ignores malformed tags in most cases.
|
|
||||||
# :warn is the default and will give deprecation warnings when invalid syntax is used.
|
|
||||||
# :strict will enforce correct syntax.
|
|
||||||
attr_writer :error_mode
|
|
||||||
|
|
||||||
# Sets how strict the taint checker should be.
|
|
||||||
# :lax is the default, and ignores the taint flag completely
|
|
||||||
# :warn adds a warning, but does not interrupt the rendering
|
|
||||||
# :error raises an error when tainted output is used
|
|
||||||
attr_writer :taint_mode
|
|
||||||
|
|
||||||
attr_accessor :default_exception_renderer
|
|
||||||
Template.default_exception_renderer = lambda do |exception|
|
|
||||||
exception
|
|
||||||
end
|
|
||||||
|
|
||||||
def file_system
|
def file_system
|
||||||
@@file_system
|
@@file_system
|
||||||
end
|
end
|
||||||
@@ -87,15 +35,19 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
def tags
|
def tags
|
||||||
@tags ||= TagRegistry.new
|
@tags ||= {}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Sets how strict the parser should be.
|
||||||
|
# :lax acts like liquid 2.5 and silently ignores malformed tags in most cases.
|
||||||
|
# :warn is the default and will give deprecation warnings when invalid syntax is used.
|
||||||
|
# :strict will enforce correct syntax.
|
||||||
|
def error_mode=(mode)
|
||||||
|
@error_mode = mode
|
||||||
end
|
end
|
||||||
|
|
||||||
def error_mode
|
def error_mode
|
||||||
@error_mode ||= :lax
|
@error_mode || :lax
|
||||||
end
|
|
||||||
|
|
||||||
def taint_mode
|
|
||||||
@taint_mode ||= :lax
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Pass a module with filter methods which should be available
|
# Pass a module with filter methods which should be available
|
||||||
@@ -104,36 +56,32 @@ module Liquid
|
|||||||
Strainer.global_filter(mod)
|
Strainer.global_filter(mod)
|
||||||
end
|
end
|
||||||
|
|
||||||
def default_resource_limits
|
|
||||||
@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.
|
|
||||||
# See Liquid::Profiler for more information
|
|
||||||
def parse(source, options = {})
|
def parse(source, options = {})
|
||||||
template = Template.new
|
template = Template.new
|
||||||
template.parse(source, options)
|
template.parse(source, options)
|
||||||
|
template
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# creates a new <tt>Template</tt> from an array of tokens. Use <tt>Template.parse</tt> instead
|
||||||
def initialize
|
def initialize
|
||||||
@rethrow_errors = false
|
@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 = {})
|
||||||
@options = options
|
@root = Document.new(tokenize(source), DEFAULT_OPTIONS.merge(options))
|
||||||
@profiling = options[:profile]
|
@warnings = nil
|
||||||
@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
|
||||||
|
|
||||||
|
def warnings
|
||||||
|
return [] unless @root
|
||||||
|
@warnings ||= @root.warnings
|
||||||
|
end
|
||||||
|
|
||||||
def registers
|
def registers
|
||||||
@registers ||= {}
|
@registers ||= {}
|
||||||
end
|
end
|
||||||
@@ -155,9 +103,6 @@ module Liquid
|
|||||||
# if you use the same filters over and over again consider registering them globally
|
# if you use the same filters over and over again consider registering them globally
|
||||||
# with <tt>Template.register_filter</tt>
|
# with <tt>Template.register_filter</tt>
|
||||||
#
|
#
|
||||||
# if profiling was enabled in <tt>Template#parse</tt> then the resulting profiling information
|
|
||||||
# will be available via <tt>Template#profiler</tt>
|
|
||||||
#
|
|
||||||
# Following options can be passed:
|
# Following options can be passed:
|
||||||
#
|
#
|
||||||
# * <tt>filters</tt> : array with local filters
|
# * <tt>filters</tt> : array with local filters
|
||||||
@@ -165,17 +110,11 @@ 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 ''.freeze if @root.nil?
|
return '' if @root.nil?
|
||||||
|
|
||||||
context = case args.first
|
context = case args.first
|
||||||
when Liquid::Context
|
when Liquid::Context
|
||||||
c = args.shift
|
args.shift
|
||||||
|
|
||||||
if @rethrow_errors
|
|
||||||
c.exception_renderer = ->(e) { raise }
|
|
||||||
end
|
|
||||||
|
|
||||||
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)
|
||||||
@@ -187,29 +126,29 @@ module Liquid
|
|||||||
raise ArgumentError, "Expected Hash or Liquid::Context as parameter"
|
raise ArgumentError, "Expected Hash or Liquid::Context as parameter"
|
||||||
end
|
end
|
||||||
|
|
||||||
output = nil
|
|
||||||
|
|
||||||
case args.last
|
case args.last
|
||||||
when Hash
|
when Hash
|
||||||
options = args.pop
|
options = args.pop
|
||||||
output = options[:output] if options[:output]
|
|
||||||
|
|
||||||
registers.merge!(options[:registers]) if options[:registers].is_a?(Hash)
|
if options[:registers].is_a?(Hash)
|
||||||
|
self.registers.merge!(options[:registers])
|
||||||
|
end
|
||||||
|
|
||||||
apply_options_to_context(context, options)
|
if options[:filters]
|
||||||
when Module, Array
|
context.add_filters(options[:filters])
|
||||||
|
end
|
||||||
|
|
||||||
|
when Module
|
||||||
|
context.add_filters(args.pop)
|
||||||
|
when Array
|
||||||
context.add_filters(args.pop)
|
context.add_filters(args.pop)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Retrying a render resets resource usage
|
|
||||||
context.resource_limits.reset
|
|
||||||
|
|
||||||
begin
|
begin
|
||||||
# render the nodelist.
|
# render the nodelist.
|
||||||
# for performance reasons we get an array back here. join will make a string out of it.
|
# for performance reasons we get an array back here. join will make a string out of it.
|
||||||
with_profiling(context) do
|
result = @root.render(context)
|
||||||
@root.render_to_output_buffer(context, output || '')
|
result.respond_to?(:join) ? result.join : result
|
||||||
end
|
|
||||||
rescue Liquid::MemoryError => e
|
rescue Liquid::MemoryError => e
|
||||||
context.handle_error(e)
|
context.handle_error(e)
|
||||||
ensure
|
ensure
|
||||||
@@ -218,43 +157,15 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
def render!(*args)
|
def render!(*args)
|
||||||
@rethrow_errors = true
|
@rethrow_errors = true; render(*args)
|
||||||
render(*args)
|
|
||||||
end
|
|
||||||
|
|
||||||
def render_to_output_buffer(context, output)
|
|
||||||
render(context, output: output)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def tokenize(source)
|
def tokenize(source)
|
||||||
Tokenizer.new(source, @line_numbers)
|
source = source.source if source.respond_to?(:source)
|
||||||
|
Tokenizer.new(source.to_s)
|
||||||
end
|
end
|
||||||
|
|
||||||
def with_profiling(context)
|
|
||||||
if @profiling && !context.partial
|
|
||||||
raise "Profiler not loaded, require 'liquid/profiler' first" unless defined?(Liquid::Profiler)
|
|
||||||
|
|
||||||
@profiler = Profiler.new
|
|
||||||
@profiler.start
|
|
||||||
|
|
||||||
begin
|
|
||||||
yield
|
|
||||||
ensure
|
|
||||||
@profiler.stop
|
|
||||||
end
|
|
||||||
else
|
|
||||||
yield
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def apply_options_to_context(context, options)
|
|
||||||
context.add_filters(options[:filters]) if options[:filters]
|
|
||||||
context.global_filter = options[:global_filter] if options[:global_filter]
|
|
||||||
context.exception_renderer = options[:exception_renderer] if options[:exception_renderer]
|
|
||||||
context.strict_variables = options[:strict_variables] if options[:strict_variables]
|
|
||||||
context.strict_filters = options[:strict_filters] if options[:strict_filters]
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
module Liquid
|
|
||||||
class Tokenizer
|
|
||||||
attr_reader :line_number, :for_liquid_tag
|
|
||||||
|
|
||||||
def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false)
|
|
||||||
@source = source
|
|
||||||
@line_number = line_number || (line_numbers ? 1 : nil)
|
|
||||||
@for_liquid_tag = for_liquid_tag
|
|
||||||
@tokens = tokenize
|
|
||||||
end
|
|
||||||
|
|
||||||
def shift
|
|
||||||
token = @tokens.shift or return
|
|
||||||
|
|
||||||
if @line_number
|
|
||||||
@line_number += @for_liquid_tag ? 1 : token.count("\n")
|
|
||||||
end
|
|
||||||
|
|
||||||
token
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def tokenize
|
|
||||||
return [] if @source.to_s.empty?
|
|
||||||
|
|
||||||
return @source.split("\n") if @for_liquid_tag
|
|
||||||
|
|
||||||
tokens = @source.split(TemplateParser)
|
|
||||||
|
|
||||||
# removes the rogue empty element at the beginning of the array
|
|
||||||
tokens.shift if tokens[0] && tokens[0].empty?
|
|
||||||
|
|
||||||
tokens
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,24 +1,27 @@
|
|||||||
module Liquid
|
module Liquid
|
||||||
module Utils
|
module Utils
|
||||||
|
|
||||||
def self.slice_collection(collection, from, to)
|
def self.slice_collection(collection, from, to)
|
||||||
if (from != 0 || !to.nil?) && collection.respond_to?(:load_slice)
|
if (from != 0 || to != nil) && collection.respond_to?(:load_slice)
|
||||||
collection.load_slice(from, to)
|
collection.load_slice(from, to)
|
||||||
else
|
else
|
||||||
slice_collection_using_each(collection, from, to)
|
slice_collection_using_each(collection, from, to)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.non_blank_string?(collection)
|
||||||
|
collection.is_a?(String) && collection != ''
|
||||||
|
end
|
||||||
|
|
||||||
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)
|
return [collection] if non_blank_string?(collection)
|
||||||
return collection.empty? ? [] : [collection]
|
|
||||||
end
|
|
||||||
return [] unless collection.respond_to?(:each)
|
|
||||||
|
|
||||||
collection.each do |item|
|
collection.each do |item|
|
||||||
|
|
||||||
if to && to <= index
|
if to && to <= index
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
@@ -32,52 +35,5 @@ module Liquid
|
|||||||
|
|
||||||
segments
|
segments
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.to_integer(num)
|
|
||||||
return num if num.is_a?(Integer)
|
|
||||||
num = num.to_s
|
|
||||||
begin
|
|
||||||
Integer(num)
|
|
||||||
rescue ::ArgumentError
|
|
||||||
raise Liquid::ArgumentError, "invalid integer"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.to_number(obj)
|
|
||||||
case obj
|
|
||||||
when Float
|
|
||||||
BigDecimal(obj.to_s)
|
|
||||||
when Numeric
|
|
||||||
obj
|
|
||||||
when String
|
|
||||||
(obj.strip =~ /\A-?\d+\.\d+\z/) ? BigDecimal(obj) : obj.to_i
|
|
||||||
else
|
|
||||||
if obj.respond_to?(:to_number)
|
|
||||||
obj.to_number
|
|
||||||
else
|
|
||||||
0
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.to_date(obj)
|
|
||||||
return obj if obj.respond_to?(:strftime)
|
|
||||||
|
|
||||||
if obj.is_a?(String)
|
|
||||||
return nil if obj.empty?
|
|
||||||
obj = obj.downcase
|
|
||||||
end
|
|
||||||
|
|
||||||
case obj
|
|
||||||
when 'now'.freeze, 'today'.freeze
|
|
||||||
Time.now
|
|
||||||
when /\A\d+\z/, Integer
|
|
||||||
Time.at(obj.to_i)
|
|
||||||
when String
|
|
||||||
Time.parse(obj)
|
|
||||||
end
|
|
||||||
rescue ::ArgumentError
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
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
|
||||||
#
|
#
|
||||||
@@ -10,150 +11,98 @@ module Liquid
|
|||||||
# {{ user | link }}
|
# {{ user | link }}
|
||||||
#
|
#
|
||||||
class Variable
|
class Variable
|
||||||
FilterMarkupRegex = /#{FilterSeparator}\s*(.*)/om
|
FilterParser = /(?:#{FilterSeparator}|(?:\s*(?:#{QuotedFragment}|#{ArgumentSeparator})\s*)+)/o
|
||||||
FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
|
EasyParse = /\A *(\w+(?:\.\w+)*) *\z/
|
||||||
FilterArgsRegex = /(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o
|
attr_accessor :filters, :name, :warnings
|
||||||
JustTagAttributes = /\A#{TagAttributes}\z/o
|
|
||||||
MarkupWithQuotedFragment = /(#{QuotedFragment})(.*)/om
|
|
||||||
|
|
||||||
attr_accessor :filters, :name, :line_number
|
def initialize(markup, options = {})
|
||||||
attr_reader :parse_context
|
|
||||||
alias_method :options, :parse_context
|
|
||||||
|
|
||||||
include ParserSwitching
|
|
||||||
|
|
||||||
def initialize(markup, parse_context)
|
|
||||||
@markup = markup
|
@markup = markup
|
||||||
@name = nil
|
@name = nil
|
||||||
@parse_context = parse_context
|
@options = options || {}
|
||||||
@line_number = parse_context.line_number
|
|
||||||
|
|
||||||
parse_with_selected_parser(markup)
|
|
||||||
end
|
|
||||||
|
|
||||||
def raw
|
case @options[:error_mode] || Template.error_mode
|
||||||
@markup
|
when :strict then strict_parse(markup)
|
||||||
end
|
when :lax then lax_parse(markup)
|
||||||
|
when :warn
|
||||||
def markup_context(markup)
|
begin
|
||||||
"in \"{{#{markup}}}\""
|
strict_parse(markup)
|
||||||
|
rescue SyntaxError => e
|
||||||
|
@warnings ||= []
|
||||||
|
@warnings << e
|
||||||
|
lax_parse(markup)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def lax_parse(markup)
|
def lax_parse(markup)
|
||||||
@filters = []
|
@filters = []
|
||||||
return unless markup =~ MarkupWithQuotedFragment
|
if match = markup.match(/\s*(#{QuotedFragment})(.*)/o)
|
||||||
|
@name = match[1]
|
||||||
name_markup = $1
|
if match[2].match(/#{FilterSeparator}\s*(.*)/o)
|
||||||
filter_markup = $2
|
filters = Regexp.last_match(1).scan(FilterParser)
|
||||||
@name = Expression.parse(name_markup)
|
filters.each do |f|
|
||||||
if filter_markup =~ FilterMarkupRegex
|
if matches = f.match(/\s*(\w+)/)
|
||||||
filters = $1.scan(FilterParser)
|
filtername = matches[1]
|
||||||
filters.each do |f|
|
filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o).flatten
|
||||||
next unless f =~ /\w+/
|
@filters << [filtername, filterargs]
|
||||||
filtername = Regexp.last_match(0)
|
end
|
||||||
filterargs = f.scan(FilterArgsRegex).flatten
|
end
|
||||||
@filters << parse_filter_expressions(filtername, filterargs)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def strict_parse(markup)
|
def strict_parse(markup)
|
||||||
|
# Very simple valid cases
|
||||||
|
if markup =~ EasyParse
|
||||||
|
@name = $1
|
||||||
|
@filters = []
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
@filters = []
|
@filters = []
|
||||||
p = Parser.new(markup)
|
p = Parser.new(markup)
|
||||||
|
# Could be just filters with no input
|
||||||
@name = Expression.parse(p.expression)
|
@name = p.look(:pipe) ? '' : 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) : []
|
||||||
@filters << parse_filter_expressions(filtername, filterargs)
|
@filters << [filtername, filterargs]
|
||||||
end
|
end
|
||||||
p.consume(:end_of_string)
|
p.consume(:end_of_string)
|
||||||
|
rescue SyntaxError => e
|
||||||
|
e.message << " in \"{{#{markup}}}\""
|
||||||
|
raise e
|
||||||
end
|
end
|
||||||
|
|
||||||
def parse_filterargs(p)
|
def parse_filterargs(p)
|
||||||
# first argument
|
# first argument
|
||||||
filterargs = [p.argument]
|
filterargs = [p.argument]
|
||||||
# followed by comma separated others
|
# followed by comma separated others
|
||||||
filterargs << p.argument while p.consume?(:comma)
|
while p.consume?(:comma)
|
||||||
|
filterargs << p.argument
|
||||||
|
end
|
||||||
filterargs
|
filterargs
|
||||||
end
|
end
|
||||||
|
|
||||||
def render(context)
|
def render(context)
|
||||||
obj = @filters.inject(context.evaluate(@name)) do |output, (filter_name, filter_args, filter_kwargs)|
|
return '' if @name.nil?
|
||||||
filter_args = evaluate_filter_expressions(context, filter_args, filter_kwargs)
|
@filters.inject(context[@name]) do |output, filter|
|
||||||
context.invoke(filter_name, output, *filter_args)
|
filterargs = []
|
||||||
end
|
keyword_args = {}
|
||||||
|
filter[1].to_a.each do |a|
|
||||||
obj = context.apply_global_filter(obj)
|
if matches = a.match(/\A#{TagAttributes}\z/o)
|
||||||
taint_check(context, obj)
|
keyword_args[matches[1]] = context[matches[2]]
|
||||||
obj
|
else
|
||||||
end
|
filterargs << context[a]
|
||||||
|
end
|
||||||
def render_to_output_buffer(context, output)
|
|
||||||
obj = render(context)
|
|
||||||
|
|
||||||
if obj.is_a?(Array)
|
|
||||||
output << obj.join
|
|
||||||
elsif obj.nil?
|
|
||||||
else
|
|
||||||
output << obj.to_s
|
|
||||||
end
|
|
||||||
|
|
||||||
output
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def parse_filter_expressions(filter_name, unparsed_args)
|
|
||||||
filter_args = []
|
|
||||||
keyword_args = nil
|
|
||||||
unparsed_args.each do |a|
|
|
||||||
if matches = a.match(JustTagAttributes)
|
|
||||||
keyword_args ||= {}
|
|
||||||
keyword_args[matches[1]] = Expression.parse(matches[2])
|
|
||||||
else
|
|
||||||
filter_args << Expression.parse(a)
|
|
||||||
end
|
end
|
||||||
end
|
filterargs << keyword_args unless keyword_args.empty?
|
||||||
result = [filter_name, filter_args]
|
begin
|
||||||
result << keyword_args if keyword_args
|
output = context.invoke(filter[0], output, *filterargs)
|
||||||
result
|
rescue FilterNotFound
|
||||||
end
|
raise FilterNotFound, "Error - filter '#{filter[0]}' in '#{@markup.strip}' could not be found."
|
||||||
|
|
||||||
def evaluate_filter_expressions(context, filter_args, filter_kwargs)
|
|
||||||
parsed_args = filter_args.map{ |expr| context.evaluate(expr) }
|
|
||||||
if filter_kwargs
|
|
||||||
parsed_kwargs = {}
|
|
||||||
filter_kwargs.each do |key, expr|
|
|
||||||
parsed_kwargs[key] = context.evaluate(expr)
|
|
||||||
end
|
end
|
||||||
parsed_args << parsed_kwargs
|
|
||||||
end
|
|
||||||
parsed_args
|
|
||||||
end
|
|
||||||
|
|
||||||
def taint_check(context, obj)
|
|
||||||
return unless obj.tainted?
|
|
||||||
return if Template.taint_mode == :lax
|
|
||||||
|
|
||||||
@markup =~ QuotedFragment
|
|
||||||
name = Regexp.last_match(0)
|
|
||||||
|
|
||||||
error = TaintedError.new("variable '#{name}' is tainted and was not escaped")
|
|
||||||
error.line_number = line_number
|
|
||||||
error.template_name = context.template_name
|
|
||||||
|
|
||||||
case Template.taint_mode
|
|
||||||
when :warn
|
|
||||||
context.warnings << error
|
|
||||||
when :error
|
|
||||||
raise error
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
|
||||||
def children
|
|
||||||
[@node.name] + @node.filters.flatten
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,88 +0,0 @@
|
|||||||
module Liquid
|
|
||||||
class VariableLookup
|
|
||||||
SQUARE_BRACKETED = /\A\[(.*)\]\z/m
|
|
||||||
COMMAND_METHODS = ['size'.freeze, 'first'.freeze, 'last'.freeze].freeze
|
|
||||||
|
|
||||||
attr_reader :name, :lookups
|
|
||||||
|
|
||||||
def self.parse(markup)
|
|
||||||
new(markup)
|
|
||||||
end
|
|
||||||
|
|
||||||
def initialize(markup)
|
|
||||||
lookups = markup.scan(VariableParser)
|
|
||||||
|
|
||||||
name = lookups.shift
|
|
||||||
if name =~ SQUARE_BRACKETED
|
|
||||||
name = Expression.parse($1)
|
|
||||||
end
|
|
||||||
@name = name
|
|
||||||
|
|
||||||
@lookups = lookups
|
|
||||||
@command_flags = 0
|
|
||||||
|
|
||||||
@lookups.each_index do |i|
|
|
||||||
lookup = lookups[i]
|
|
||||||
if lookup =~ SQUARE_BRACKETED
|
|
||||||
lookups[i] = Expression.parse($1)
|
|
||||||
elsif COMMAND_METHODS.include?(lookup)
|
|
||||||
@command_flags |= 1 << i
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def evaluate(context)
|
|
||||||
name = context.evaluate(@name)
|
|
||||||
object = context.find_variable(name)
|
|
||||||
|
|
||||||
@lookups.each_index do |i|
|
|
||||||
key = context.evaluate(@lookups[i])
|
|
||||||
|
|
||||||
# If object is a hash- or array-like object we look for the
|
|
||||||
# presence of the key and if its available we return it
|
|
||||||
if object.respond_to?(:[]) &&
|
|
||||||
((object.respond_to?(:key?) && object.key?(key)) ||
|
|
||||||
(object.respond_to?(:fetch) && key.is_a?(Integer)))
|
|
||||||
|
|
||||||
# if its a proc we will replace the entry with the proc
|
|
||||||
res = context.lookup_and_evaluate(object, key)
|
|
||||||
object = res.to_liquid
|
|
||||||
|
|
||||||
# Some special cases. If the part wasn't in square brackets and
|
|
||||||
# no key with the same name was found we interpret following calls
|
|
||||||
# as commands and call them on the current object
|
|
||||||
elsif @command_flags & (1 << i) != 0 && object.respond_to?(key)
|
|
||||||
object = object.send(key).to_liquid
|
|
||||||
|
|
||||||
# No key was present with the desired value and it wasn't one of the directly supported
|
|
||||||
# keywords either. The only thing we got left is to return nil or
|
|
||||||
# raise an exception if `strict_variables` option is set to true
|
|
||||||
else
|
|
||||||
return nil unless context.strict_variables
|
|
||||||
raise Liquid::UndefinedVariable, "undefined variable #{key}"
|
|
||||||
end
|
|
||||||
|
|
||||||
# If we are dealing with a drop here we have to
|
|
||||||
object.context = context if object.respond_to?(:context=)
|
|
||||||
end
|
|
||||||
|
|
||||||
object
|
|
||||||
end
|
|
||||||
|
|
||||||
def ==(other)
|
|
||||||
self.class == other.class && state == other.state
|
|
||||||
end
|
|
||||||
|
|
||||||
protected
|
|
||||||
|
|
||||||
def state
|
|
||||||
[@name, @lookups, @command_flags]
|
|
||||||
end
|
|
||||||
|
|
||||||
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
|
||||||
def children
|
|
||||||
@node.lookups
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
# encoding: utf-8
|
# encoding: utf-8
|
||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
VERSION = "4.0.3".freeze
|
VERSION = "3.0.0"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
# encoding: utf-8
|
# encoding: utf-8
|
||||||
|
|
||||||
lib = File.expand_path('../lib/', __FILE__)
|
lib = File.expand_path('../lib/', __FILE__)
|
||||||
$LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib)
|
$:.unshift lib unless $:.include?(lib)
|
||||||
|
|
||||||
require "liquid/version"
|
require "liquid/version"
|
||||||
|
|
||||||
@@ -10,22 +9,24 @@ Gem::Specification.new do |s|
|
|||||||
s.version = Liquid::VERSION
|
s.version = Liquid::VERSION
|
||||||
s.platform = Gem::Platform::RUBY
|
s.platform = Gem::Platform::RUBY
|
||||||
s.summary = "A secure, non-evaling end user template engine with aesthetic markup."
|
s.summary = "A secure, non-evaling end user template engine with aesthetic markup."
|
||||||
s.authors = ["Tobias Lütke"]
|
s.authors = ["Tobias Luetke"]
|
||||||
s.email = ["tobi@leetsoft.com"]
|
s.email = ["tobi@leetsoft.com"]
|
||||||
s.homepage = "http://www.liquidmarkup.org"
|
s.homepage = "http://www.liquidmarkup.org"
|
||||||
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.1.0"
|
|
||||||
s.required_rubygems_version = ">= 1.3.7"
|
s.required_rubygems_version = ">= 1.3.7"
|
||||||
|
|
||||||
s.test_files = Dir.glob("{test}/**/*")
|
s.test_files = Dir.glob("{test}/**/*")
|
||||||
s.files = Dir.glob("{lib}/**/*") + %w(LICENSE README.md)
|
s.files = Dir.glob("{lib,ext}/**/*") + %w(MIT-LICENSE README.md)
|
||||||
|
s.extensions = ['ext/liquid/extconf.rb']
|
||||||
|
|
||||||
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', '~> 11.3'
|
s.add_development_dependency 'rake-compiler'
|
||||||
s.add_development_dependency 'minitest'
|
s.add_development_dependency 'stackprof'
|
||||||
|
s.add_development_dependency 'rake'
|
||||||
|
s.add_development_dependency 'activesupport'
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,18 +1,12 @@
|
|||||||
require 'benchmark/ips'
|
require 'rubygems'
|
||||||
require_relative 'theme_runner'
|
require 'benchmark'
|
||||||
|
require File.dirname(__FILE__) + '/theme_runner'
|
||||||
|
|
||||||
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
|
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
|
||||||
profiler = ThemeRunner.new
|
profiler = ThemeRunner.new
|
||||||
|
|
||||||
Benchmark.ips do |x|
|
Benchmark.bmbm do |x|
|
||||||
x.time = 10
|
x.report("parse:") { 100.times { profiler.compile } }
|
||||||
x.warmup = 5
|
x.report("parse & run:") { 100.times { profiler.run } }
|
||||||
|
|
||||||
puts
|
|
||||||
puts "Running benchmark for #{x.time} seconds (with #{x.warmup} seconds warmup)."
|
|
||||||
puts
|
|
||||||
|
|
||||||
x.report("parse:") { profiler.compile }
|
|
||||||
x.report("render:") { profiler.render }
|
|
||||||
x.report("parse & render:") { profiler.run }
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'benchmark/ips'
|
|
||||||
require 'memory_profiler'
|
|
||||||
require 'terminal-table'
|
|
||||||
require_relative 'theme_runner'
|
|
||||||
|
|
||||||
class Profiler
|
|
||||||
LOG_LABEL = "Profiling: ".rjust(14).freeze
|
|
||||||
REPORTS_DIR = File.expand_path('.memprof', __dir__).freeze
|
|
||||||
|
|
||||||
def self.run
|
|
||||||
puts
|
|
||||||
yield new
|
|
||||||
end
|
|
||||||
|
|
||||||
def initialize
|
|
||||||
@allocated = []
|
|
||||||
@retained = []
|
|
||||||
@headings = []
|
|
||||||
end
|
|
||||||
|
|
||||||
def profile(phase, &block)
|
|
||||||
print LOG_LABEL
|
|
||||||
print "#{phase}.. ".ljust(10)
|
|
||||||
report = MemoryProfiler.report(&block)
|
|
||||||
puts 'Done.'
|
|
||||||
@headings << phase.capitalize
|
|
||||||
@allocated << "#{report.scale_bytes(report.total_allocated_memsize)} (#{report.total_allocated} objects)"
|
|
||||||
@retained << "#{report.scale_bytes(report.total_retained_memsize)} (#{report.total_retained} objects)"
|
|
||||||
|
|
||||||
return if ENV['CI']
|
|
||||||
require 'fileutils'
|
|
||||||
report_file = File.join(REPORTS_DIR, "#{sanitize(phase)}.txt")
|
|
||||||
FileUtils.mkdir_p(REPORTS_DIR)
|
|
||||||
report.pretty_print(to_file: report_file, scale_bytes: true)
|
|
||||||
end
|
|
||||||
|
|
||||||
def tabulate
|
|
||||||
table = Terminal::Table.new(headings: @headings.unshift('Phase')) do |t|
|
|
||||||
t << @allocated.unshift('Total allocated')
|
|
||||||
t << @retained.unshift('Total retained')
|
|
||||||
end
|
|
||||||
|
|
||||||
puts
|
|
||||||
puts table
|
|
||||||
puts "\nDetailed report(s) saved to #{REPORTS_DIR}/" unless ENV['CI']
|
|
||||||
end
|
|
||||||
|
|
||||||
def sanitize(string)
|
|
||||||
string.downcase.gsub(/[\W]/, '-').squeeze('-')
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
|
|
||||||
|
|
||||||
runner = ThemeRunner.new
|
|
||||||
Profiler.run do |x|
|
|
||||||
x.profile('parse') { runner.compile }
|
|
||||||
x.profile('render') { runner.render }
|
|
||||||
x.tabulate
|
|
||||||
end
|
|
||||||
@@ -1,24 +1,19 @@
|
|||||||
require 'stackprof'
|
require 'rubygems'
|
||||||
require_relative 'theme_runner'
|
require 'ruby-prof' rescue fail("install ruby-prof extension/gem")
|
||||||
|
require File.dirname(__FILE__) + '/theme_runner'
|
||||||
|
|
||||||
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
|
|
||||||
profiler = ThemeRunner.new
|
profiler = ThemeRunner.new
|
||||||
profiler.run
|
|
||||||
|
|
||||||
[:cpu, :object].each do |profile_type|
|
puts 'Running profiler...'
|
||||||
puts "Profiling in #{profile_type} mode..."
|
|
||||||
results = StackProf.run(mode: profile_type) do
|
|
||||||
200.times do
|
|
||||||
profiler.run
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if profile_type == :cpu && graph_filename = ENV['GRAPH_FILENAME']
|
results = profiler.run_profile
|
||||||
File.open(graph_filename, 'w') do |f|
|
|
||||||
StackProf::Report.new(results).print_graphviz(nil, f)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
StackProf::Report.new(results).print_text(false, 20)
|
puts 'Success'
|
||||||
File.write(ENV['FILENAME'] + "." + profile_type.to_s, Marshal.dump(results)) if ENV['FILENAME']
|
puts
|
||||||
|
|
||||||
|
[RubyProf::FlatPrinter, RubyProf::GraphHtmlPrinter, RubyProf::CallTreePrinter, RubyProf::DotPrinter].each do |klass|
|
||||||
|
filename = (ENV['TMP'] || '/tmp') + (klass.name.include?('Html') ? "/liquid.#{klass.name.downcase}.html" : "/callgrind.liquid.#{klass.name.downcase}.txt")
|
||||||
|
filename.gsub!(/:+/, '_')
|
||||||
|
File.open(filename, "w+") { |fp| klass.new(results).print(fp, :print_file => true, :min_percent => 3) }
|
||||||
|
$stderr.puts "wrote #{klass.name} output to #{filename}"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
class CommentForm < Liquid::Block
|
class CommentForm < Liquid::Block
|
||||||
Syntax = /(#{Liquid::VariableSignature}+)/
|
Syntax = /(#{Liquid::VariableSignature}+)/
|
||||||
|
|
||||||
def initialize(tag_name, markup, options)
|
def initialize(tag_name, markup, tokens)
|
||||||
super
|
|
||||||
|
|
||||||
if markup =~ Syntax
|
if markup =~ Syntax
|
||||||
@variable_name = $1
|
@variable_name = $1
|
||||||
@attributes = {}
|
@attributes = {}
|
||||||
else
|
else
|
||||||
raise SyntaxError.new("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
|
||||||
|
|
||||||
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_to_output_buffer(context, output)
|
def render(context)
|
||||||
article = context[@variable_name]
|
article = context[@variable_name]
|
||||||
|
|
||||||
context.stack do
|
context.stack do
|
||||||
@@ -23,13 +23,11 @@ class CommentForm < Liquid::Block
|
|||||||
'email' => context['comment.email'],
|
'email' => context['comment.email'],
|
||||||
'body' => context['comment.body']
|
'body' => context['comment.body']
|
||||||
}
|
}
|
||||||
|
wrap_in_form(article, render_all(@nodelist, context))
|
||||||
output << wrap_in_form(article, render_all(@nodelist, context, output))
|
|
||||||
output
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def wrap_in_form(article, input)
|
def wrap_in_form(article, input)
|
||||||
%(<form id="article-#{article.id}-comment-form" class="comment-form" method="post" action="">\n#{input}\n</form>)
|
%Q{<form id="article-#{article.id}-comment-form" class="comment-form" method="post" action="">\n#{input}\n</form>}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
require 'yaml'
|
require 'yaml'
|
||||||
|
|
||||||
module Database
|
module Database
|
||||||
|
|
||||||
# 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 = YAML.load_file("#{__dir__}/vision.database.yml")
|
db = YAML.load_file(File.dirname(__FILE__) + '/vision.database.yml')
|
||||||
|
|
||||||
# From vision source
|
# From vision source
|
||||||
db['products'].each do |product|
|
db['products'].each do |product|
|
||||||
@@ -39,7 +39,7 @@ module Database
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if __FILE__ == $PROGRAM_NAME
|
if __FILE__ == $0
|
||||||
p Database.tables['collections']['frontpage'].keys
|
p Database.tables['collections']['frontpage'].keys
|
||||||
# p Database.tables['blog']['articles']
|
#p Database.tables['blog']['articles']
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
require 'json'
|
|
||||||
|
|
||||||
module JsonFilter
|
module JsonFilter
|
||||||
|
|
||||||
def json(object)
|
def json(object)
|
||||||
JSON.dump(object.reject { |k, v| k == "collections" })
|
object.reject {|k,v| k == "collections" }.to_json
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
$:.unshift __dir__ + '/../../lib'
|
$:.unshift File.dirname(__FILE__) + '/../../lib'
|
||||||
require_relative '../../lib/liquid'
|
require File.dirname(__FILE__) + '/../../lib/liquid'
|
||||||
|
|
||||||
require_relative 'comment_form'
|
require File.dirname(__FILE__) + '/comment_form'
|
||||||
require_relative 'paginate'
|
require File.dirname(__FILE__) + '/paginate'
|
||||||
require_relative 'json_filter'
|
require File.dirname(__FILE__) + '/json_filter'
|
||||||
require_relative 'money_filter'
|
require File.dirname(__FILE__) + '/money_filter'
|
||||||
require_relative 'shop_filter'
|
require File.dirname(__FILE__) + '/shop_filter'
|
||||||
require_relative 'tag_filter'
|
require File.dirname(__FILE__) + '/tag_filter'
|
||||||
require_relative 'weight_filter'
|
require File.dirname(__FILE__) + '/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
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
module MoneyFilter
|
module MoneyFilter
|
||||||
|
|
||||||
def money_with_currency(money)
|
def money_with_currency(money)
|
||||||
return '' if money.nil?
|
return '' if money.nil?
|
||||||
sprintf("$ %.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?
|
||||||
sprintf("$ %.2f", money / 100.0)
|
sprintf("$ %.2f", money/100.0)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
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, tokens)
|
||||||
super
|
@nodelist = []
|
||||||
|
|
||||||
if markup =~ Syntax
|
if markup =~ Syntax
|
||||||
@collection_name = $1
|
@collection_name = $1
|
||||||
@@ -19,9 +19,11 @@ class Paginate < Liquid::Block
|
|||||||
else
|
else
|
||||||
raise SyntaxError.new("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
|
||||||
|
|
||||||
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_to_output_buffer(context, output)
|
def render(context)
|
||||||
@context = context
|
@context = context
|
||||||
|
|
||||||
context.stack do
|
context.stack do
|
||||||
@@ -42,22 +44,23 @@ class Paginate < Liquid::Block
|
|||||||
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('« Previous', current_page - 1) unless 1 >= current_page
|
pagination['previous'] = link('« Previous', current_page-1 ) unless 1 >= current_page
|
||||||
pagination['next'] = link('Next »', current_page + 1) unless page_count <= current_page + 1
|
pagination['next'] = link('Next »', current_page+1 ) unless page_count <= current_page+1
|
||||||
pagination['parts'] = []
|
pagination['parts'] = []
|
||||||
|
|
||||||
hellip_break = false
|
hellip_break = false
|
||||||
|
|
||||||
if page_count > 2
|
if page_count > 2
|
||||||
1.upto(page_count - 1) do |page|
|
1.upto(page_count-1) do |page|
|
||||||
|
|
||||||
if current_page == page
|
if current_page == page
|
||||||
pagination['parts'] << no_link(page)
|
pagination['parts'] << no_link(page)
|
||||||
elsif page == 1
|
elsif page == 1
|
||||||
pagination['parts'] << link(page, page)
|
pagination['parts'] << link(page, page)
|
||||||
elsif page == page_count - 1
|
elsif page == page_count -1
|
||||||
pagination['parts'] << link(page, page)
|
pagination['parts'] << link(page, page)
|
||||||
elsif page <= current_page - @attributes['window_size'] || page >= current_page + @attributes['window_size']
|
elsif page <= current_page - @attributes['window_size'] or page >= current_page + @attributes['window_size']
|
||||||
next if hellip_break
|
next if hellip_break
|
||||||
pagination['parts'] << no_link('…')
|
pagination['parts'] << no_link('…')
|
||||||
hellip_break = true
|
hellip_break = true
|
||||||
@@ -70,18 +73,18 @@ class Paginate < Liquid::Block
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
super
|
render_all(@nodelist, context)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def no_link(title)
|
def no_link(title)
|
||||||
{ 'title' => title, 'is_link' => false }
|
{ 'title' => title, 'is_link' => false}
|
||||||
end
|
end
|
||||||
|
|
||||||
def link(title, page)
|
def link(title, page)
|
||||||
{ 'title' => title, 'url' => current_url + "?page=#{page}", 'is_link' => true }
|
{ 'title' => title, 'url' => current_url + "?page=#{page}", 'is_link' => true}
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_url
|
def current_url
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
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}"
|
||||||
end
|
end
|
||||||
@@ -15,16 +16,16 @@ module ShopFilter
|
|||||||
%(<script src="#{url}" type="text/javascript"></script>)
|
%(<script src="#{url}" type="text/javascript"></script>)
|
||||||
end
|
end
|
||||||
|
|
||||||
def stylesheet_tag(url, media = "all")
|
def stylesheet_tag(url, media="all")
|
||||||
%(<link href="#{url}" rel="stylesheet" type="text/css" media="#{media}" />)
|
%(<link href="#{url}" rel="stylesheet" type="text/css" media="#{media}" />)
|
||||||
end
|
end
|
||||||
|
|
||||||
def link_to(link, url, title = "")
|
def link_to(link, url, title="")
|
||||||
%(<a href="#{url}" title="#{title}">#{link}</a>)
|
%|<a href="#{url}" title="#{title}">#{link}</a>|
|
||||||
end
|
end
|
||||||
|
|
||||||
def img_tag(url, alt = "")
|
def img_tag(url, alt="")
|
||||||
%(<img src="#{url}" alt="#{alt}" />)
|
%|<img src="#{url}" alt="#{alt}" />|
|
||||||
end
|
end
|
||||||
|
|
||||||
def link_to_vendor(vendor)
|
def link_to_vendor(vendor)
|
||||||
@@ -44,14 +45,15 @@ module ShopFilter
|
|||||||
end
|
end
|
||||||
|
|
||||||
def url_for_vendor(vendor_title)
|
def url_for_vendor(vendor_title)
|
||||||
"/collections/#{to_handle(vendor_title)}"
|
"/collections/#{vendor_title.to_handle}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def url_for_type(type_title)
|
def url_for_type(type_title)
|
||||||
"/collections/#{to_handle(type_title)}"
|
"/collections/#{type_title.to_handle}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def product_img_url(url, style = 'small')
|
def product_img_url(url, style = 'small')
|
||||||
|
|
||||||
unless url =~ /\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
|
||||||
@@ -67,6 +69,7 @@ module ShopFilter
|
|||||||
end
|
end
|
||||||
|
|
||||||
def default_pagination(paginate)
|
def default_pagination(paginate)
|
||||||
|
|
||||||
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']
|
||||||
|
|
||||||
@@ -92,15 +95,4 @@ module ShopFilter
|
|||||||
input == 1 ? singular : plural
|
input == 1 ? singular : plural
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def to_handle(str)
|
|
||||||
result = str.dup
|
|
||||||
result.downcase!
|
|
||||||
result.delete!("'\"()[]")
|
|
||||||
result.gsub!(/\W+/, '-')
|
|
||||||
result.gsub!(/-+\z/, '') if result[-1] == '-'
|
|
||||||
result.gsub!(/\A-+/, '') if result[0] == '-'
|
|
||||||
result
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
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>"
|
||||||
end
|
end
|
||||||
|
|
||||||
def highlight_active_tag(tag, css_class = 'active')
|
def highlight_active_tag(tag, css_class='active')
|
||||||
if @context['current_tags'].include?(tag)
|
if @context['current_tags'].include?(tag)
|
||||||
"<span class=\"#{css_class}\">#{tag}</span>"
|
"<span class=\"#{css_class}\">#{tag}</span>"
|
||||||
else
|
else
|
||||||
@@ -20,4 +21,5 @@ module TagFilter
|
|||||||
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,4 +1,5 @@
|
|||||||
module WeightFilter
|
module WeightFilter
|
||||||
|
|
||||||
def weight(grams)
|
def weight(grams)
|
||||||
sprintf("%.2f", grams / 1000)
|
sprintf("%.2f", grams / 1000)
|
||||||
end
|
end
|
||||||
@@ -6,4 +7,5 @@ module WeightFilter
|
|||||||
def weight_with_unit(grams)
|
def weight_with_unit(grams)
|
||||||
"#{weight(grams)} kg"
|
"#{weight(grams)} kg"
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|||||||
15
performance/stackprof.rb
Normal file
15
performance/stackprof.rb
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
require 'stackprof' rescue fail("install stackprof extension/gem")
|
||||||
|
require File.dirname(__FILE__) + '/theme_runner'
|
||||||
|
|
||||||
|
profiler = ThemeRunner.new
|
||||||
|
profiler.run
|
||||||
|
results = StackProf.run(mode: :cpu, out: ENV['FILENAME']) do
|
||||||
|
100.times do
|
||||||
|
profiler.run
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if results.kind_of?(File)
|
||||||
|
puts "wrote stackprof dump to #{results.path}"
|
||||||
|
else
|
||||||
|
StackProf::Report.new(results).print_text(false, 20)
|
||||||
|
end
|
||||||
@@ -6,115 +6,115 @@
|
|||||||
# 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.
|
||||||
|
|
||||||
require_relative 'shopify/liquid'
|
require 'rubygems'
|
||||||
require_relative 'shopify/database'
|
require 'active_support'
|
||||||
|
require 'yaml'
|
||||||
|
require 'digest/md5'
|
||||||
|
require File.dirname(__FILE__) + '/shopify/liquid'
|
||||||
|
require File.dirname(__FILE__) + '/shopify/database.rb'
|
||||||
|
|
||||||
class ThemeRunner
|
class ThemeRunner
|
||||||
class FileSystem
|
class FileSystem
|
||||||
|
|
||||||
def initialize(path)
|
def initialize(path)
|
||||||
@path = path
|
@path = path
|
||||||
end
|
end
|
||||||
|
|
||||||
# Called by Liquid to retrieve a template file
|
# Called by Liquid to retrieve a template file
|
||||||
def read_template_file(template_path)
|
def read_template_file(template_path, context)
|
||||||
File.read(@path + '/' + template_path + '.liquid')
|
File.read(@path + '/' + template_path + '.liquid')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# Initialize a new liquid ThemeRunner instance
|
# Load all templates into memory, do this now so that
|
||||||
# Will load all templates into memory, do this now so that we don't profile IO.
|
# we don't profile IO.
|
||||||
def initialize
|
def initialize
|
||||||
@tests = Dir[__dir__ + '/tests/**/*.liquid'].collect do |test|
|
@tests = Dir[File.dirname(__FILE__) + '/tests/**/*.liquid'].collect do |test|
|
||||||
next if File.basename(test) == 'theme.liquid'
|
next if File.basename(test) == 'theme.liquid'
|
||||||
|
|
||||||
theme_path = File.dirname(test) + '/theme.liquid'
|
theme_path = File.dirname(test) + '/theme.liquid'
|
||||||
{
|
|
||||||
liquid: File.read(test),
|
[File.read(test), (File.file?(theme_path) ? File.read(theme_path) : nil), test]
|
||||||
layout: (File.file?(theme_path) ? File.read(theme_path) : nil),
|
|
||||||
template_name: test
|
|
||||||
}
|
|
||||||
end.compact
|
end.compact
|
||||||
|
|
||||||
compile_all_tests
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# `compile` will test just the compilation portion of liquid without any templates
|
|
||||||
def compile
|
def compile
|
||||||
@tests.each do |test_hash|
|
# Dup assigns because will make some changes to them
|
||||||
Liquid::Template.new.parse(test_hash[:liquid])
|
|
||||||
Liquid::Template.new.parse(test_hash[:layout])
|
@tests.each do |liquid, layout, template_name|
|
||||||
|
|
||||||
|
tmpl = Liquid::Template.new
|
||||||
|
tmpl.parse(liquid)
|
||||||
|
tmpl = Liquid::Template.new
|
||||||
|
tmpl.parse(layout)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# `run` is called to benchmark rendering and compiling at the same time
|
def run
|
||||||
def run
|
|
||||||
each_test do |liquid, layout, assigns, page_template, template_name|
|
|
||||||
compile_and_render(liquid, layout, assigns, page_template, template_name)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# `render` is called to benchmark just the render portion of liquid
|
|
||||||
def render
|
|
||||||
@compiled_tests.each do |test|
|
|
||||||
tmpl = test[:tmpl]
|
|
||||||
assigns = test[:assigns]
|
|
||||||
layout = test[:layout]
|
|
||||||
|
|
||||||
if layout
|
|
||||||
assigns['content_for_layout'] = tmpl.render!(assigns)
|
|
||||||
layout.render!(assigns)
|
|
||||||
else
|
|
||||||
tmpl.render!(assigns)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def compile_and_render(template, layout, assigns, page_template, template_file)
|
|
||||||
compiled_test = compile_test(template, layout, assigns, page_template, template_file)
|
|
||||||
assigns['content_for_layout'] = compiled_test[:tmpl].render!(assigns)
|
|
||||||
compiled_test[:layout].render!(assigns) if layout
|
|
||||||
end
|
|
||||||
|
|
||||||
def compile_all_tests
|
|
||||||
@compiled_tests = []
|
|
||||||
each_test do |liquid, layout, assigns, page_template, template_name|
|
|
||||||
@compiled_tests << compile_test(liquid, layout, assigns, page_template, template_name)
|
|
||||||
end
|
|
||||||
@compiled_tests
|
|
||||||
end
|
|
||||||
|
|
||||||
def compile_test(template, layout, assigns, page_template, template_file)
|
|
||||||
tmpl = init_template(page_template, template_file)
|
|
||||||
parsed_template = tmpl.parse(template).dup
|
|
||||||
|
|
||||||
if layout
|
|
||||||
parsed_layout = tmpl.parse(layout)
|
|
||||||
{ tmpl: parsed_template, assigns: assigns, layout: parsed_layout }
|
|
||||||
else
|
|
||||||
{ tmpl: parsed_template, assigns: assigns }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# utility method with similar functionality needed in `compile_all_tests` and `run`
|
|
||||||
def each_test
|
|
||||||
# Dup assigns because will make some changes to them
|
# Dup assigns because will make some changes to them
|
||||||
assigns = Database.tables.dup
|
assigns = Database.tables.dup
|
||||||
|
|
||||||
@tests.each do |test_hash|
|
@tests.each do |liquid, layout, template_name|
|
||||||
# Compute page_template outside of profiler run, uninteresting to profiler
|
|
||||||
page_template = File.basename(test_hash[:template_name], File.extname(test_hash[:template_name]))
|
# Compute page_tempalte outside of profiler run, uninteresting to profiler
|
||||||
yield(test_hash[:liquid], test_hash[:layout], assigns, page_template, test_hash[:template_name])
|
page_template = File.basename(template_name, File.extname(template_name))
|
||||||
|
compile_and_render(liquid, layout, assigns, page_template, template_name)
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# set up a new Liquid::Template object for use in `compile_and_render` and `compile_test`
|
|
||||||
def init_template(page_template, template_file)
|
def run_profile
|
||||||
|
RubyProf.measure_mode = RubyProf::WALL_TIME
|
||||||
|
|
||||||
|
# Dup assigns because will make some changes to them
|
||||||
|
assigns = Database.tables.dup
|
||||||
|
|
||||||
|
@tests.each do |liquid, layout, template_name|
|
||||||
|
|
||||||
|
# Compute page_tempalte outside of profiler run, uninteresting to profiler
|
||||||
|
html = nil
|
||||||
|
page_template = File.basename(template_name, File.extname(template_name))
|
||||||
|
|
||||||
|
unless @started
|
||||||
|
RubyProf.start
|
||||||
|
RubyProf.pause
|
||||||
|
@started = true
|
||||||
|
end
|
||||||
|
|
||||||
|
html = nil
|
||||||
|
|
||||||
|
RubyProf.resume
|
||||||
|
html = compile_and_render(liquid, layout, assigns, page_template, template_name)
|
||||||
|
RubyProf.pause
|
||||||
|
|
||||||
|
|
||||||
|
# return the result and the MD5 of the content, this can be used to detect regressions between liquid version
|
||||||
|
$stdout.puts "* rendered template %s, content: %s" % [template_name, Digest::MD5.hexdigest(html)]
|
||||||
|
|
||||||
|
# Uncomment to dump html files to /tmp so that you can inspect for errors
|
||||||
|
# File.open("/tmp/#{File.basename(template_name)}.html", "w+") { |fp| fp <<html}
|
||||||
|
end
|
||||||
|
|
||||||
|
RubyProf.stop
|
||||||
|
end
|
||||||
|
|
||||||
|
def compile_and_render(template, layout, assigns, 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
|
|
||||||
|
content_for_layout = tmpl.parse(template).render(assigns)
|
||||||
|
|
||||||
|
if layout
|
||||||
|
assigns['content_for_layout'] = content_for_layout
|
||||||
|
tmpl.parse(layout).render(assigns)
|
||||||
|
else
|
||||||
|
content_for_layout
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
require 'test_helper'
|
|
||||||
|
|
||||||
class AssignTest < Minitest::Test
|
|
||||||
include Liquid
|
|
||||||
|
|
||||||
def test_assign_with_hyphen_in_variable_name
|
|
||||||
template_source = <<-END_TEMPLATE
|
|
||||||
{% assign this-thing = 'Print this-thing' %}
|
|
||||||
{{ this-thing }}
|
|
||||||
END_TEMPLATE
|
|
||||||
template = Template.parse(template_source)
|
|
||||||
rendered = template.render!
|
|
||||||
assert_equal "Print this-thing", rendered.strip
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_assigned_variable
|
|
||||||
assert_template_result('.foo.',
|
|
||||||
'{% assign foo = values %}.{{ foo[0] }}.',
|
|
||||||
'values' => %w(foo bar baz))
|
|
||||||
|
|
||||||
assert_template_result('.bar.',
|
|
||||||
'{% assign foo = values %}.{{ foo[1] }}.',
|
|
||||||
'values' => %w(foo bar baz))
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_assign_with_filter
|
|
||||||
assert_template_result('.bar.',
|
|
||||||
'{% assign foo = values | split: "," %}.{{ foo[1] }}.',
|
|
||||||
'values' => "foo,bar,baz")
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_assign_syntax_error
|
|
||||||
assert_match_syntax_error(/assign/,
|
|
||||||
'{% assign foo not values %}.',
|
|
||||||
'values' => "foo,bar,baz")
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_assign_uses_error_mode
|
|
||||||
with_error_mode(:strict) do
|
|
||||||
assert_raises(SyntaxError) do
|
|
||||||
Template.parse("{% assign foo = ('X' | downcase) %}")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
with_error_mode(:lax) do
|
|
||||||
assert Template.parse("{% assign foo = ('X' | downcase) %}")
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end # AssignTest
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
require 'test_helper'
|
|
||||||
|
|
||||||
class BlockTest < Minitest::Test
|
|
||||||
include Liquid
|
|
||||||
|
|
||||||
def test_unexpected_end_tag
|
|
||||||
exc = assert_raises(SyntaxError) do
|
|
||||||
Template.parse("{% if true %}{% endunless %}")
|
|
||||||
end
|
|
||||||
assert_equal exc.message, "Liquid syntax error: 'endunless' is not a valid delimiter for if tags. use endif"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
require 'test_helper'
|
|
||||||
|
|
||||||
class ContextTest < Minitest::Test
|
|
||||||
include Liquid
|
|
||||||
|
|
||||||
def test_override_global_filter
|
|
||||||
global = Module.new do
|
|
||||||
def notice(output)
|
|
||||||
"Global #{output}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local = Module.new do
|
|
||||||
def notice(output)
|
|
||||||
"Local #{output}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
with_global_filter(global) do
|
|
||||||
assert_equal 'Global test', Template.parse("{{'test' | notice }}").render!
|
|
||||||
assert_equal 'Local test', Template.parse("{{'test' | notice }}").render!({}, filters: [local])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_has_key_will_not_add_an_error_for_missing_keys
|
|
||||||
with_error_mode :strict do
|
|
||||||
context = Context.new
|
|
||||||
context.key?('unknown')
|
|
||||||
assert_empty context.errors
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user