Compare commits

..

99 Commits

Author SHA1 Message Date
Mike Angell
9bb533985e styling fixes 2019-10-04 15:25:06 +10:00
Mike Angell
1c6a5d91fe Merge branch 'master' into sort-numeric 2019-10-04 15:20:00 +10:00
Mike Angell
1aa7d3d2ba Change registers to by symbols (#1178) 2019-09-27 04:32:24 +10:00
Mike Angell
0db9c56f34 Disable rendering of tag based on register (#1162)
* Disable rendering of tag based on register

* Improvements to disable tag

* Resolve disbale tag tests

* Test disable_tags register

* disabled_tags is now always avaiable

* Allow multiple tags to be disabled at once

* Move disabled check to block_body

* Code improvements

* Remove redundant nil check

* Improve disabled tag error output

* Improve disable tag API

* Code improvements

* Switch disabled? to not mutate output

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

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

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

* Update rubocop config

* Prefer string interpolation in simple cases

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

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

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

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

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

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

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

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

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

This will pave the way for adding the `render` tag, which renders
templates in such a subcontext.
2019-08-29 10:27:15 -04:00
Mike Angell
d67de1c9b2 Follow Shopify ruby style
This is the first step in bringing Liquid style inline with Shopify ruby style
2019-08-29 13:39:57 +10:00
Mike Angell
2324564743 Remove lazy load stacks
Remove lazy load stacks and instead only create a new scope when a tag is known to need one
2019-08-29 09:09:32 +10:00
Ashwin Maroli
b3097f143c Build only pushes to certain branches on Travis CI 2019-08-28 21:28:49 +05:30
Mike Angell
7b309dc75d Merge pull request #1135 from Shopify/fix-failing-rubocop
Resolve failing rubocop issues
2019-08-29 01:11:25 +10:00
Mike Angell
8f68cffdf1 Resolve failing rubocop issues 2019-08-29 00:45:38 +10:00
Mike Angell
dd27d0fd1d Merge pull request #1133 from Shopify/liquid-tag-fixes
Bugfix for new Liquid tag
2019-08-29 00:36:13 +10:00
Mike Angell
7a26e6b3d8 Merge pull request #1131 from Shopify/bump-ruby-2-4
Rubocop upgrade, Ruby 2.4 minimum and TruffleRuby
2019-08-29 00:33:42 +10:00
Mike Angell
cf4e77ab0c Merge branch 'master' into bump-ruby-2-4 2019-08-29 00:24:45 +10:00
Mike Angell
7bae55dd39 Bugfix for new Liquid tag 2019-08-28 23:39:19 +10:00
Tobias Lütke
0ce8aef229 Merge pull request #1103 from ashmaroli/ci-profile-memory
Add a CI job to profile memory usage of commit
2019-08-27 15:11:55 -04:00
Tobias Lütke
6eab595fae Merge pull request #1086 from Shopify/liquid-tag
Add {% liquid %} and {% echo %} tags
2019-08-27 15:10:20 -04:00
Mike Angell
b16b109a80 Bump Minimum version to 2.4 and bump Rubocop 2019-08-28 00:31:44 +10:00
Justin Li
831355dfbd Merge pull request #1117 from ashmaroli/reduce-allocations-template-lookup-class
Reduce allocations while registering Liquid tags
2019-08-07 16:37:39 -04:00
Ashwin Maroli
00702d8e63 Use Object.const_get directly 2019-08-07 11:44:53 +05:30
Justin Li
197c058208 Merge pull request #1099 from ashmaroli/stash-types-private-constant
Use a private constant to stash token-types
2019-08-06 17:56:56 -04:00
Justin Li
98dfe198e1 Merge pull request #1115 from ashmaroli/reduce-allocations-from-truncate-filters
Reduce string allocations from truncate filters
2019-08-06 17:48:43 -04:00
Ashwin Maroli
c2c1497ca8 Reduce allocations while registering Liquid tags 2019-07-22 20:42:37 +05:30
Ashwin Maroli
d19967a79d Reduce string allocations from truncate filters 2019-07-22 17:35:45 +05:30
Florian Weingarten
248c54a386 Merge pull request #1091 from Shopify/rendering-with-less-garbage
Rendering with less garbage
2019-07-19 15:53:22 +01:00
Ashwin Maroli
2c42447659 Rename constant to SINGLE_TOKEN_EXPRESSION_TYPES 2019-05-17 23:30:24 +05:30
Ashwin Maroli
ab698191b9 Add a CI job to profile memory usage of commit 2019-05-17 22:47:05 +05:30
Ashwin Maroli
9ef6f9b642 Freeze mutable object assigned to constant 2019-04-29 23:50:49 +05:30
Ashwin Maroli
4684478e94 Use a private constant to stash token-types 2019-04-29 23:45:45 +05:30
Florian Weingarten
b3b63a683f Merge pull request #1097 from ashmaroli/stackprof-no-jruby
Don't attempt to install stackprof gem on JRuby
2019-04-24 09:11:44 -04:00
Ashwin Maroli
1c577c5b62 Don't attempt to install stackprof gem on JRuby 2019-04-24 11:31:20 +05:30
David Cornu
755d2821f3 Merge pull request #1094 from Shopify/for-tag/invalid-limit-offset
Make sure the for tag's limit and offset are integers
2019-04-23 17:20:54 -04:00
David Cornu
495b3d312f Merge pull request #1095 from Shopify/travis/remove-rainbow-gem
Stop installing the rainbow gem on Travis
2019-04-23 17:20:38 -04:00
Florian Weingarten
9640e77805 render_to_output_buffer 2019-04-23 17:06:29 -04:00
David Cornu
453f6348c2 Stop installing the rainbow gem on Travis 2019-04-23 16:55:37 -04:00
David Cornu
70ed1fc86d Make sure the limit and offset values are integers 2019-04-23 16:44:37 -04:00
Florian Weingarten
2a1ca3152d liquid without the garbage 2019-04-22 16:34:31 -04:00
Florian Weingarten
c2ef247be5 Merge pull request #1092 from Shopify/rake-memory-profiler-task
rake memory_profile:run
2019-04-22 16:33:32 -04:00
Florian Weingarten
1518d3f6f9 Merge pull request #1093 from Shopify/bytesize-not-length
use bytesize, not length
2019-04-18 18:39:21 +01:00
Florian Weingarten
c67b77709d rake memory_profile:run 2019-04-17 19:09:26 +01:00
Florian Weingarten
c89ce9c2ed use bytesize, not length 2019-04-17 18:55:13 +01:00
Justin Li
7dc488a73b Simplifications from review 2019-04-09 15:19:47 -04:00
Justin Li
e6ed804ca5 Fix line number tracking after a non-empty blank token 2019-04-08 18:43:09 -04:00
Justin Li
951abb67ee Remove {% local %} tag 2019-04-08 18:34:39 -04:00
Justin Li
8d1cd41453 Add {% liquid %}, {% echo %}, and {% local %} tags 2019-04-01 20:08:38 -04:00
Richard Monette
b0629f17f7 Merge pull request #1073 from Shopify/defer-alloc-hash
defer hash allocation until needed for unparsed_args
2019-03-20 13:34:48 -04:00
Richard Monette
274f078806 defer hash allocation in parse_filter_expressions
add exploration of GC object allocation

remove performance test

can actually remove one more if branch

use named locals to improve readability
2019-03-20 13:20:31 -04:00
Richard Monette
d7171aa084 Merge pull request #1077 from Shopify/update-cops-for-trailing-comma
update Rubocop for trailing comma styles
2019-03-19 16:02:26 -04:00
Richard Monette
06c4789dc5 update Rubocop for trailing comma styles 2019-03-19 11:05:05 -04:00
Justin Li
f2f467bdbc v4.0.3 2019-03-12 12:43:48 -04:00
Justin Li
ff99d92c18 Merge pull request #1072 from Shopify/fix-interrupts
Fix interrupts through includes
2019-03-12 12:26:12 -04:00
Justin Li
39fecd06db Fix interrupts through includes 2019-03-12 12:18:22 -04:00
Justin Li
8013df8ca2 v4.0.2 2019-03-08 15:43:46 -05:00
Clayton Smith
14cd011cb5 Merge pull request #1070 from Shopify/url-decode-validation
Validate the character encoding in url_decode.
2019-03-08 11:09:40 -05:00
Clayton Smith
e2d9907df2 Validate the character encoding in url_decode. 2019-03-07 14:01:10 -05:00
Justin Li
23d669f5e6 Merge pull request #1032 from printercu/patch-1
Single regexp for strip_html
2019-02-22 13:04:04 -05:00
Justin Li
ed73794f82 Preserve existing strip_html behaviour for weird inputs 2019-02-22 13:00:36 -05:00
Ashwin Maroli
f59f6dea83 Fix simple RuboCop offenses and update TODO file (#1062)
* Fix Layout/EmptyLineAfterMagicComment offense

* Fix Layout/ExtraSpacing offense

* Fix Layout/ClosingParenthesisIndentation offenses

* Fix Style/MutableConstant offense

* Fix Style/UnneededInterpolation offenses

* Fix Style/RedundantParentheses offenses

* Update TODO config for RuboCop

* Add executable bit to test/test_helper.rb

ref: https://travis-ci.org/Shopify/liquid/jobs/488169512#L578
2019-02-22 12:32:56 -05:00
Garland Zhang
7a81fb821a Merge pull request #1059 from Shopify/map_error_checking
Apply error-checking to sort, sort_natural, where, uniq, map, compact filter(s)
2019-02-22 10:42:16 -05:00
Garland Zhang
cec27ea326 Extract raise error line and some filters with begin/rescue blocks 2019-02-21 17:00:20 -05:00
Justin Li
14999e8f7c Merge pull request #1053 from er1/update-changelog-v4.0.1
Updated changelog for v4.0.1 for (#1038)
2019-02-10 10:37:31 -05:00
Eric Chan
b41fc10d8e Updated changelog for v4.0.1 2018-12-03 23:54:00 -05:00
David Cornu
2b3c81cfd0 Merge pull request #1046 from Shopify/make-builds-green
Make builds green
2018-10-24 10:46:01 -04:00
David Cornu
2a2376bfd9 Run :test before :rubocop in the default Rake task 2018-10-19 15:06:36 -04:00
David Cornu
ca9e75db53 Reduce perceived complexity for #sort and #sort_natural 2018-10-19 14:57:33 -04:00
David Cornu
407c8abf30 Use TrailingCommaInLiteral
TrailingCommaInArrayLiteral and TrailingCommaInHashLiteral were introduced in v0.53.0 and we're running v0.49.0.

https://github.com/rubocop-hq/rubocop/blob/master/CHANGELOG.md#0530-2018-03-05
2018-10-19 14:52:16 -04:00
Justin Li
43f181e211 Merge pull request #1044 from Shopify/enable-cla-bot
Enable CLA bot
2018-10-19 09:34:30 -04:00
printercu
89c1ba2b0e Fix rubocop warning 2018-09-27 17:24:01 +03:00
printercu
479d8fb4a4 Single regexp for strip_html 2018-09-27 17:13:35 +03:00
Justin Li
2f0a2c66f3 Add sort_numeric filter 2018-09-13 17:16:50 -04:00
137 changed files with 3615 additions and 1093 deletions

1
.gitignore vendored
View File

@@ -7,3 +7,4 @@ pkg
.ruby-version .ruby-version
Gemfile.lock Gemfile.lock
.bundle .bundle
.byebug_history

File diff suppressed because it is too large Load Diff

View File

@@ -1,129 +1,16 @@
inherit_from: ./.rubocop_todo.yml inherit_from:
- 'https://shopify.github.io/ruby-style-guide/rubocop.yml'
- .rubocop_todo.yml
require: rubocop-performance
Performance:
Enabled: true
AllCops: AllCops:
TargetRubyVersion: 2.4
Exclude: Exclude:
- 'performance/shopify/*' - 'vendor/bundle/**/*'
- '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
Lint/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/SpaceInsideBrackets:
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
Style/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: Naming/MethodName:
Exclude: Exclude:

View File

@@ -1,248 +1,48 @@
# This configuration was generated by # This configuration was generated by
# `rubocop --auto-gen-config` # `rubocop --auto-gen-config`
# on 2017-11-22 11:35:55 -0500 using RuboCop version 0.49.1. # on 2019-09-11 06:34:25 +1000 using RuboCop version 0.74.0.
# The point is for the user to remove these configuration records # The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base. # one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new # Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again. # versions of RuboCop, may require this file to be generated again.
# Offense count: 3
# Cop supports --auto-correct.
Layout/ClosingParenthesisIndentation:
Exclude:
- 'test/integration/error_handling_test.rb'
# Offense count: 1
# Cop supports --auto-correct.
Layout/EmptyLineAfterMagicComment:
Exclude:
- 'lib/liquid/version.rb'
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: AllowForAlignment, ForceEqualSignAlignment.
Layout/ExtraSpacing:
Exclude:
- 'test/integration/parsing_quirks_test.rb'
# Offense count: 5
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# 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.
# SupportedStyles: symmetrical, new_line, same_line
Layout/MultilineMethodCallBraceLayout:
Exclude:
- 'test/integration/error_handling_test.rb'
- 'test/unit/strainer_unit_test.rb'
# Offense count: 2 # Offense count: 2
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles. # Configuration parameters: EnforcedStyle.
# SupportedStyles: runtime_error, standard_error # SupportedStyles: runtime_error, standard_error
Lint/InheritException: Lint/InheritException:
Exclude: Exclude:
- 'lib/liquid/interrupts.rb' - 'lib/liquid/interrupts.rb'
# Offense count: 1 # Offense count: 98
Lint/ScriptPermission: # Cop supports --auto-correct.
Exclude: # Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
- 'test/test_helper.rb'
# Offense count: 52
Metrics/AbcSize:
Max: 56
# Offense count: 13
Metrics/CyclomaticComplexity:
Max: 12
# Offense count: 620
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
# URISchemes: http, https # URISchemes: http, https
Metrics/LineLength: Metrics/LineLength:
Max: 294 Max: 294
# Offense count: 102 # Offense count: 44
# Configuration parameters: CountComments. Naming/ConstantName:
Metrics/MethodLength:
Max: 37
# Offense count: 9
Metrics/PerceivedComplexity:
Max: 11
# Offense count: 10
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# 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: 1
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, SingleLineConditionsOnly, IncludeTernaryExpressions.
# SupportedStyles: assign_to_condition, assign_inside_condition
Style/ConditionalAssignment:
Exclude:
- 'lib/liquid/errors.rb'
# Offense count: 2
# Cop supports --auto-correct.
Style/EmptyCaseCondition:
Exclude: Exclude:
- 'lib/liquid.rb'
- 'lib/liquid/block_body.rb' - 'lib/liquid/block_body.rb'
- 'lib/liquid/lexer.rb'
# Offense count: 5
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# 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: 2
# Configuration parameters: SupportedStyles.
# SupportedStyles: annotated, template
Style/FormatStringToken:
EnforcedStyle: template
# 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/assign.rb'
- 'lib/liquid/tags/capture.rb' - 'lib/liquid/tags/capture.rb'
- 'lib/liquid/tags/case.rb' - 'lib/liquid/tags/case.rb'
- 'lib/liquid/tags/cycle.rb'
- 'lib/liquid/tags/for.rb' - 'lib/liquid/tags/for.rb'
- 'lib/liquid/tags/if.rb'
- 'lib/liquid/tags/include.rb' - 'lib/liquid/tags/include.rb'
- 'lib/liquid/tags/raw.rb' - 'lib/liquid/tags/raw.rb'
- 'lib/liquid/tags/table_row.rb' - 'lib/liquid/tags/table_row.rb'
- 'lib/liquid/variable.rb' - 'lib/liquid/variable.rb'
- 'test/unit/tokenizer_unit_test.rb' - 'performance/shopify/comment_form.rb'
- 'performance/shopify/paginate.rb'
- 'test/integration/tags/include_tag_test.rb'
# Offense count: 4 # Offense count: 5
# Configuration parameters: SupportedStyles. Style/ClassVars:
# SupportedStyles: snake_case, camelCase
Style/MethodName:
EnforcedStyle: snake_case
# Offense count: 6
# Cop supports --auto-correct.
Style/MutableConstant:
Exclude:
- 'lib/liquid/expression.rb'
- 'lib/liquid/lexer.rb'
- 'lib/liquid/standardfilters.rb'
- 'lib/liquid/tags/if.rb'
- 'lib/liquid/variable_lookup.rb'
- 'lib/liquid/version.rb'
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, MinBodyLength, SupportedStyles.
# 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.
# 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: 2
# Cop supports --auto-correct.
Style/RedundantParentheses:
Exclude:
- 'test/unit/condition_unit_test.rb'
# Offense count: 1
# Cop supports --auto-correct.
Style/RedundantSelf:
Exclude: Exclude:
- 'lib/liquid/condition.rb'
- 'lib/liquid/strainer.rb' - 'lib/liquid/strainer.rb'
- 'lib/liquid/template.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.
# SupportedStyles: percent, brackets
Style/SymbolArray:
EnforcedStyle: brackets
# Offense count: 2
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, AllowSafeAssignment.
# SupportedStyles: require_parentheses, require_no_parentheses, require_parentheses_when_complex
Style/TernaryParentheses:
Exclude:
- 'lib/liquid/context.rb'
- 'lib/liquid/utils.rb'
# Offense count: 4
# Cop supports --auto-correct.
Style/UnneededInterpolation:
Exclude:
- 'lib/liquid/i18n.rb'
- 'test/integration/standard_filter_test.rb'
# Offense count: 2
# Cop supports --auto-correct.
Style/UnneededPercentQ:
Exclude:
- 'test/integration/error_handling_test.rb'
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: MaxLineLength.
Style/WhileUntilModifier:
Exclude:
- 'lib/liquid/tags/case.rb'

View File

@@ -1,32 +1,26 @@
language: ruby language: ruby
cache: bundler
rvm: rvm:
- 2.1
- 2.2
- 2.3
- 2.4 - 2.4
- 2.5 - 2.5
- &latest_ruby 2.6
- 2.7
- ruby-head - ruby-head
- jruby-head
# - rbx-2
sudo: false
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: ruby-head
- rvm: jruby-head
install: branches:
- gem install rainbow -v 2.2.1 only:
- bundle install - master
- gh-pages
script: bundle exec rake - /.*-stable/
notifications: notifications:
disable: true disable: true

17
Gemfile
View File

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

View File

@@ -1,5 +1,58 @@
# Liquid Change Log # Liquid Change Log
## 4.0.3 / 2019-03-12
### Fixed
* Fix break and continue tags inside included templates in loops (#1072) [Justin Li]
## 4.0.2 / 2019-03-08
### 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" ## 4.0.0 / 2016-12-14 / branch "4-0-stable"
### Changed ### Changed

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (c) 2005 Tobias Luetke # Copyright (c) 2005 Tobias Luetke
# #
# Permission is hereby granted, free of charge, to any person obtaining # Permission is hereby granted, free of charge, to any person obtaining
@@ -21,10 +23,10 @@
module Liquid module Liquid
FilterSeparator = /\|/ FilterSeparator = /\|/
ArgumentSeparator = ','.freeze ArgumentSeparator = ','
FilterArgumentSeparator = ':'.freeze FilterArgumentSeparator = ':'
VariableAttributeSeparator = '.'.freeze VariableAttributeSeparator = '.'
WhitespaceControl = '-'.freeze WhitespaceControl = '-'
TagStart = /\{\%/ TagStart = /\{\%/
TagEnd = /\%\}/ TagEnd = /\%\}/
VariableSignature = /\(?[\w\-\.\[\]]\)?/ VariableSignature = /\(?[\w\-\.\[\]]\)?/
@@ -74,7 +76,12 @@ require 'liquid/condition'
require 'liquid/utils' require 'liquid/utils'
require 'liquid/tokenizer' require 'liquid/tokenizer'
require 'liquid/parse_context' require 'liquid/parse_context'
require 'liquid/partial_cache'
require 'liquid/usage'
require 'liquid/register'
require 'liquid/static_registers'
# Load all the tags of the standard library # Load all the tags of the standard library
# #
Dir["#{__dir__}/liquid/tags/*.rb"].each { |f| require f } Dir["#{__dir__}/liquid/tags/*.rb"].each { |f| require f }
Dir["#{__dir__}/liquid/registers/*.rb"].each { |f| require f }

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
class Block < Tag class Block < Tag
MAX_DEPTH = 100 MAX_DEPTH = 100
@@ -13,6 +15,7 @@ module Liquid
end end
end end
# For backwards compatibility
def render(context) def render(context)
@body.render(context) @body.render(context)
end end
@@ -26,16 +29,16 @@ module Liquid
end end
def unknown_tag(tag, _params, _tokens) def unknown_tag(tag, _params, _tokens)
if tag == 'else'.freeze if tag == 'else'
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_else".freeze, raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_else",
block_name: block_name)) block_name: block_name)
elsif tag.start_with?('end'.freeze) elsif tag.start_with?('end')
raise SyntaxError.new(parse_context.locale.t("errors.syntax.invalid_delimiter".freeze, raise SyntaxError, parse_context.locale.t("errors.syntax.invalid_delimiter",
tag: tag, tag: tag,
block_name: block_name, block_name: block_name,
block_delimiter: block_delimiter)) block_delimiter: block_delimiter)
else else
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unknown_tag".freeze, tag: tag)) raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag)
end end
end end
@@ -51,7 +54,7 @@ module Liquid
def parse_body(body, tokens) def parse_body(body, tokens)
if parse_context.depth >= MAX_DEPTH if parse_context.depth >= MAX_DEPTH
raise StackLevelError, "Nesting too deep".freeze raise StackLevelError, "Nesting too deep"
end end
parse_context.depth += 1 parse_context.depth += 1
begin begin
@@ -60,7 +63,7 @@ module Liquid
return false if end_tag_name == block_delimiter return false if end_tag_name == block_delimiter
unless end_tag_name unless end_tag_name
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name)) raise SyntaxError, parse_context.locale.t("errors.syntax.tag_never_closed", block_name: block_name)
end end
# this tag is not registered with the system # this tag is not registered with the system

View File

@@ -1,10 +1,13 @@
# frozen_string_literal: true
module Liquid module Liquid
class BlockBody class BlockBody
FullToken = /\A#{TagStart}#{WhitespaceControl}?\s*(\w+)\s*(.*?)#{WhitespaceControl}?#{TagEnd}\z/om LiquidTagToken = /\A\s*(\w+)\s*(.*?)\z/o
FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(\w+)(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
WhitespaceOrNothing = /\A\s*\z/ WhitespaceOrNothing = /\A\s*\z/
TAGSTART = "{%".freeze TAGSTART = "{%"
VARSTART = "{{".freeze VARSTART = "{{"
attr_reader :nodelist attr_reader :nodelist
@@ -13,9 +16,43 @@ module Liquid
@blank = true @blank = true
end end
def parse(tokenizer, parse_context) def parse(tokenizer, parse_context, &block)
parse_context.line_number = tokenizer.line_number parse_context.line_number = tokenizer.line_number
while token = tokenizer.shift
if tokenizer.for_liquid_tag
parse_for_liquid_tag(tokenizer, parse_context, &block)
else
parse_for_document(tokenizer, parse_context, &block)
end
end
private def parse_for_liquid_tag(tokenizer, parse_context)
while (token = tokenizer.shift)
unless token.empty? || token =~ WhitespaceOrNothing
unless token =~ LiquidTagToken
# line isn't empty but didn't match tag syntax, yield and let the
# caller raise a syntax error
return yield token, token
end
tag_name = Regexp.last_match(1)
markup = Regexp.last_match(2)
unless (tag = registered_tags[tag_name])
# end parsing if we reach an unknown tag and let the caller decide
# determine how to proceed
return yield tag_name, markup
end
new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
@blank &&= new_tag.blank?
@nodelist << new_tag
end
parse_context.line_number = tokenizer.line_number
end
yield nil, nil
end
private def parse_for_document(tokenizer, parse_context, &block)
while (token = tokenizer.shift)
next if token.empty? next if token.empty?
case case
when token.start_with?(TAGSTART) when token.start_with?(TAGSTART)
@@ -23,10 +60,21 @@ module Liquid
unless token =~ FullToken unless token =~ FullToken
raise_missing_tag_terminator(token, parse_context) raise_missing_tag_terminator(token, parse_context)
end end
tag_name = $1 tag_name = Regexp.last_match(2)
markup = $2 markup = Regexp.last_match(4)
# fetch the tag from registered blocks
unless tag = registered_tags[tag_name] if parse_context.line_number
# newlines inside the tag should increase the line number,
# particularly important for multiline {% liquid %} tags
parse_context.line_number += Regexp.last_match(1).count("\n") + Regexp.last_match(3).count("\n")
end
if tag_name == 'liquid'
liquid_tag_tokenizer = Tokenizer.new(markup, line_number: parse_context.line_number, for_liquid_tag: true)
next parse_for_liquid_tag(liquid_tag_tokenizer, parse_context, &block)
end
unless (tag = registered_tags[tag_name])
# end parsing if we reach an unknown tag and let the caller decide # end parsing if we reach an unknown tag and let the caller decide
# determine how to proceed # determine how to proceed
return yield tag_name, markup return yield tag_name, markup
@@ -55,7 +103,7 @@ module Liquid
def whitespace_handler(token, parse_context) def whitespace_handler(token, parse_context)
if token[2] == WhitespaceControl if token[2] == WhitespaceControl
previous_token = @nodelist.last previous_token = @nodelist.last
if previous_token.is_a? String if previous_token.is_a?(String)
previous_token.rstrip! previous_token.rstrip!
end end
end end
@@ -67,19 +115,23 @@ module Liquid
end end
def render(context) def render(context)
output = [] render_to_output_buffer(context, +'')
end
def render_to_output_buffer(context, output)
context.resource_limits.render_score += @nodelist.length context.resource_limits.render_score += @nodelist.length
idx = 0 idx = 0
while node = @nodelist[idx] while (node = @nodelist[idx])
previous_output_size = output.bytesize
case node case node
when String when String
check_resources(context, node)
output << node output << node
when Variable when Variable
render_node_to_output(node, output, context) render_node(context, output, node)
when Block when Block
render_node_to_output(node, output, context, node.blank?) render_node(context, node.blank? ? +'' : output, node)
break if context.interrupt? # might have happened in a for-block break if context.interrupt? # might have happened in a for-block
when Continue, Break when Continue, Break
# If we get an Interrupt that means the block must stop processing. An # If we get an Interrupt that means the block must stop processing. An
@@ -88,35 +140,43 @@ module Liquid
context.push_interrupt(node.interrupt) context.push_interrupt(node.interrupt)
break break
else # Other non-Block tags else # Other non-Block tags
render_node_to_output(node, output, context) render_node(context, output, node)
break if context.interrupt? # might have happened through an include
end end
idx += 1 idx += 1
raise_if_resource_limits_reached(context, output.bytesize - previous_output_size)
end end
output.join output
end end
private private
def render_node_to_output(node, output, context, skip_output = false) def render_node(context, output, node)
node_output = node.render(context) if node.disabled?(context)
node_output = node_output.is_a?(Array) ? node_output.join : node_output.to_s output << node.disabled_error_message
check_resources(context, node_output) return
output << node_output unless skip_output end
rescue MemoryError => e disable_tags(context, node.disabled_tags) do
raise e node.render_to_output_buffer(context, output)
end
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
context.handle_error(e, node.line_number) context.handle_error(e, node.line_number)
output << nil
rescue ::StandardError => e rescue ::StandardError => e
line_number = node.is_a?(String) ? nil : node.line_number line_number = node.is_a?(String) ? nil : node.line_number
output << context.handle_error(e, line_number) output << context.handle_error(e, line_number)
end end
def check_resources(context, node_output) def disable_tags(context, tags, &block)
context.resource_limits.render_length += node_output.length return yield if tags.empty?
context.registers[:disabled_tags].disable(tags, &block)
end
def raise_if_resource_limits_reached(context, length)
context.resource_limits.render_length += length
return unless context.resource_limits.reached? return unless context.resource_limits.reached?
raise MemoryError.new("Memory limits exceeded".freeze) raise MemoryError, "Memory limits exceeded"
end end
def create_variable(token, parse_context) def create_variable(token, parse_context)
@@ -128,11 +188,11 @@ module Liquid
end end
def raise_missing_tag_terminator(token, parse_context) def raise_missing_tag_terminator(token, parse_context)
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_termination".freeze, token: token, tag_end: TagEnd.inspect)) raise SyntaxError, parse_context.locale.t("errors.syntax.tag_termination", token: token, tag_end: TagEnd.inspect)
end end
def raise_missing_variable_terminator(token, parse_context) def raise_missing_variable_terminator(token, parse_context)
raise SyntaxError.new(parse_context.locale.t("errors.syntax.variable_termination".freeze, token: token, tag_end: VariableEnd.inspect)) raise SyntaxError, parse_context.locale.t("errors.syntax.variable_termination", token: token, tag_end: VariableEnd.inspect)
end end
def registered_tags def registered_tags

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
# Container for liquid nodes which conveniently wraps decision making logic # Container for liquid nodes which conveniently wraps decision making logic
# #
@@ -8,21 +10,21 @@ module Liquid
# #
class Condition #:nodoc: class Condition #:nodoc:
@@operators = { @@operators = {
'=='.freeze => ->(cond, left, right) { cond.send(:equal_variables, left, right) }, '==' => ->(cond, left, right) { cond.send(:equal_variables, left, right) },
'!='.freeze => ->(cond, left, right) { !cond.send(:equal_variables, left, right) }, '!=' => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
'<>'.freeze => ->(cond, left, right) { !cond.send(:equal_variables, left, right) }, '<>' => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
'<'.freeze => :<, '<' => :<,
'>'.freeze => :>, '>' => :>,
'>='.freeze => :>=, '>=' => :>=,
'<='.freeze => :<=, '<=' => :<=,
'contains'.freeze => lambda do |cond, left, right| 'contains' => lambda do |_cond, left, right|
if left && right && left.respond_to?(:include?) if left && right && left.respond_to?(:include?)
right = right.to_s if left.is_a?(String) right = right.to_s if left.is_a?(String)
left.include?(right) left.include?(right)
else else
false false
end end
end end,
} }
def self.operators def self.operators
@@ -36,7 +38,7 @@ module Liquid
@left = left @left = left
@operator = operator @operator = operator
@right = right @right = right
@child_relation = nil @child_relation = nil
@child_condition = nil @child_condition = nil
end end
@@ -78,7 +80,7 @@ module Liquid
end end
def inspect def inspect
"#<Condition #{[@left, @operator, @right].compact.join(' '.freeze)}>" "#<Condition #{[@left, @operator, @right].compact.join(' ')}>"
end end
protected protected
@@ -116,7 +118,7 @@ module Liquid
left = context.evaluate(left) left = context.evaluate(left)
right = context.evaluate(right) right = context.evaluate(right)
operation = self.class.operators[op] || raise(Liquid::ArgumentError.new("Unknown operator #{op}")) operation = self.class.operators[op] || raise(Liquid::ArgumentError, "Unknown operator #{op}")
if operation.respond_to?(:call) if operation.respond_to?(:call)
operation.call(self, left, right) operation.call(self, left, right)
@@ -124,7 +126,7 @@ module Liquid
begin begin
left.send(operation, right) left.send(operation, right)
rescue ::ArgumentError => e rescue ::ArgumentError => e
raise Liquid::ArgumentError.new(e.message) raise Liquid::ArgumentError, e.message
end end
end end
end end

View File

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

View File

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

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'set' require 'set'
module Liquid module Liquid
@@ -25,7 +27,7 @@ module Liquid
# Catch all for the method # Catch all for the method
def liquid_method_missing(method) def liquid_method_missing(method)
return nil unless @context && @context.strict_variables return nil unless @context&.strict_variables
raise Liquid::UndefinedDropMethod, "undefined method #{method}" raise Liquid::UndefinedDropMethod, "undefined method #{method}"
end end
@@ -67,7 +69,7 @@ module Liquid
if include?(Enumerable) if include?(Enumerable)
blacklist += Enumerable.public_instance_methods blacklist += Enumerable.public_instance_methods
blacklist -= [:sort, :count, :first, :min, :max, :include?] blacklist -= [:sort, :count, :first, :min, :max]
end end
whitelist = [:to_liquid] + (public_instance_methods - blacklist) whitelist = [:to_liquid] + (public_instance_methods - blacklist)

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
class Error < ::StandardError class Error < ::StandardError
attr_accessor :line_number attr_accessor :line_number
@@ -5,7 +7,7 @@ module Liquid
attr_accessor :markup_context attr_accessor :markup_context
def to_s(with_prefix = true) def to_s(with_prefix = true)
str = "" str = +""
str << message_prefix if with_prefix str << message_prefix if with_prefix
str << super() str << super()
@@ -20,11 +22,11 @@ module Liquid
private private
def message_prefix def message_prefix
str = "" str = +""
if is_a?(SyntaxError) str << if is_a?(SyntaxError)
str << "Liquid syntax error" "Liquid syntax error"
else else
str << "Liquid error" "Liquid error"
end end
if line_number if line_number

View File

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

View File

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

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
# A Liquid file system is a way to let your templates retrieve other templates for use with the include tag. # A Liquid file system is a way to let your templates retrieve other templates for use with the include tag.
# #
@@ -44,7 +46,7 @@ module Liquid
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
@@ -57,9 +59,9 @@ module Liquid
end end
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 %r{\A[^./][a-zA-Z0-9_/]+\z}.match?(template_path)
full_path = if template_path.include?('/'.freeze) full_path = if template_path.include?('/')
File.join(root, File.dirname(template_path), @pattern % File.basename(template_path)) 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)

View File

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

View File

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

View File

@@ -1,10 +1,12 @@
# frozen_string_literal: true
module Liquid module Liquid
# An interrupt is any command that breaks processing of a block (ex: a for loop). # An interrupt is any command that breaks processing of a block (ex: a for loop).
class Interrupt class Interrupt
attr_reader :message attr_reader :message
def initialize(message = nil) def initialize(message = nil)
@message = message || "interrupt".freeze @message = message || "interrupt"
end end
end end

View File

@@ -1,18 +1,20 @@
# frozen_string_literal: true
require "strscan" 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, '?' => :question,
'-'.freeze => :dash '-' => :dash,
} }.freeze
IDENTIFIER = /[a-zA-Z_][\w-]*\??/ IDENTIFIER = /[a-zA-Z_][\w-]*\??/
SINGLE_STRING_LITERAL = /'[^\']*'/ SINGLE_STRING_LITERAL = /'[^\']*'/
DOUBLE_STRING_LITERAL = /"[^\"]*"/ DOUBLE_STRING_LITERAL = /"[^\"]*"/
@@ -31,16 +33,21 @@ module Liquid
until @ss.eos? until @ss.eos?
@ss.skip(WHITESPACE_OR_NOTHING) @ss.skip(WHITESPACE_OR_NOTHING)
break if @ss.eos? break if @ss.eos?
tok = case tok = if (t = @ss.scan(COMPARISON_OPERATOR))
when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t] [:comparison, t]
when t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t] elsif (t = @ss.scan(SINGLE_STRING_LITERAL))
when t = @ss.scan(DOUBLE_STRING_LITERAL) then [:string, t] [:string, t]
when t = @ss.scan(NUMBER_LITERAL) then [:number, t] elsif (t = @ss.scan(DOUBLE_STRING_LITERAL))
when t = @ss.scan(IDENTIFIER) then [:id, t] [:string, t]
when t = @ss.scan(DOTDOT) then [:dotdot, t] elsif (t = @ss.scan(NUMBER_LITERAL))
[:number, t]
elsif (t = @ss.scan(IDENTIFIER))
[:id, t]
elsif (t = @ss.scan(DOTDOT))
[:dotdot, t]
else else
c = @ss.getch c = @ss.getch
if s = SPECIALS[c] if (s = SPECIALS[c])
[s, c] [s, c]
else else
raise SyntaxError, "Unexpected character #{c}" raise SyntaxError, "Unexpected character #{c}"

View File

@@ -22,5 +22,8 @@
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"
render: "Syntax error in tag 'render' - Template name must be a quoted string"
argument: argument:
include: "Argument error in tag 'include' - Illegal template name" include: "Argument error in tag 'include' - Illegal template name"
disabled:
tag: "usage is not allowed in this context"

View File

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

View File

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

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
class Parser class Parser
def initialize(input) def initialize(input)
@@ -44,11 +46,14 @@ 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 [:string, :number].include? token[0] elsif SINGLE_TOKEN_EXPRESSION_TYPES.include?(token[0])
consume consume
elsif token.first == :open_round elsif token.first == :open_round
consume consume
@@ -63,10 +68,10 @@ module Liquid
end end
def argument def argument
str = "" str = +""
# might be a keyword argument (identifier: expression) # might be a keyword argument (identifier: expression)
if look(:id) && look(:colon, 1) if look(:id) && look(:colon, 1)
str << consume << consume << ' '.freeze str << consume << consume << ' '
end end
str << expression str << expression

View File

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

View File

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

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'liquid/profiler/hooks' require 'liquid/profiler/hooks'
module Liquid module Liquid

View File

@@ -1,23 +1,25 @@
# frozen_string_literal: true
module Liquid module Liquid
class BlockBody class BlockBody
def render_node_with_profiling(node, output, context, skip_output = false) def render_node_with_profiling(context, output, node)
Profiler.profile_node_render(node) do Profiler.profile_node_render(node) do
render_node_without_profiling(node, output, context, skip_output) render_node_without_profiling(context, output, node)
end end
end end
alias_method :render_node_without_profiling, :render_node_to_output alias_method :render_node_without_profiling, :render_node
alias_method :render_node_to_output, :render_node_with_profiling alias_method :render_node, :render_node_with_profiling
end end
class Include < Tag class Include < Tag
def render_with_profiling(context) def render_to_output_buffer_with_profiling(context, output)
Profiler.profile_children(context.evaluate(@template_name_expr).to_s) do Profiler.profile_children(context.evaluate(@template_name_expr).to_s) do
render_without_profiling(context) render_to_output_buffer_without_profiling(context, output)
end end
end end
alias_method :render_without_profiling, :render alias_method :render_to_output_buffer_without_profiling, :render_to_output_buffer
alias_method :render, :render_with_profiling alias_method :render_to_output_buffer, :render_to_output_buffer_with_profiling
end end
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
class RangeLookup class RangeLookup
def self.parse(start_markup, end_markup) def self.parse(start_markup, end_markup)

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

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

View File

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

View File

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

View File

@@ -1,16 +1,24 @@
# frozen_string_literal: true
require 'cgi' require 'cgi'
require 'bigdecimal' require 'bigdecimal'
module Liquid module Liquid
module StandardFilters module StandardFilters
HTML_ESCAPE = { HTML_ESCAPE = {
'&'.freeze => '&amp;'.freeze, '&' => '&amp;',
'>'.freeze => '&gt;'.freeze, '>' => '&gt;',
'<'.freeze => '&lt;'.freeze, '<' => '&lt;',
'"'.freeze => '&quot;'.freeze, '"' => '&quot;',
"'".freeze => '&#39;'.freeze "'" => '&#39;',
} }.freeze
HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/ HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/
STRIP_HTML_BLOCKS = Regexp.union(
%r{<script.*?</script>}m,
/<!--.*?-->/m,
%r{<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)
@@ -46,7 +54,12 @@ module Liquid
end end
def url_decode(input) def url_decode(input)
CGI.unescape(input.to_s) unless input.nil? 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 end
def slice(input, offset, length = nil) def slice(input, offset, length = nil)
@@ -61,23 +74,23 @@ module Liquid
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? return if input.nil?
input_str = input.to_s input_str = input.to_s
length = Utils.to_integer(length) length = Utils.to_integer(length)
truncate_string_str = truncate_string.to_s truncate_string_str = truncate_string.to_s
l = length - truncate_string_str.length l = length - truncate_string_str.length
l = 0 if l < 0 l = 0 if l < 0
input_str.length > length ? input_str[0...l] + truncate_string_str : input_str input_str.length > length ? input_str[0...l].concat(truncate_string_str) : input_str
end end
def truncatewords(input, words = 15, truncate_string = "...".freeze) def truncatewords(input, words = 15, truncate_string = "...")
return if input.nil? return if input.nil?
wordlist = input.to_s.split wordlist = input.to_s.split
words = Utils.to_integer(words) words = Utils.to_integer(words)
l = words - 1 l = words - 1
l = 0 if l < 0 l = 0 if l < 0
wordlist.length > l ? wordlist[0..l].join(" ".freeze) + truncate_string.to_s : input wordlist.length > l ? wordlist[0..l].join(" ").concat(truncate_string.to_s) : 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.
@@ -102,17 +115,19 @@ module Liquid
end end
def strip_html(input) def strip_html(input)
empty = ''.freeze empty = ''
input.to_s.gsub(/<script.*?<\/script>/m, empty).gsub(/<!--.*?-->/m, empty).gsub(/<style.*?<\/style>/m, empty).gsub(/<.*?>/m, empty) 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) InputIterator.new(input).join(glue)
end end
@@ -120,25 +135,18 @@ module Liquid
# provide optional property with which to sort an array of hashes or drops # provide optional property with which to sort an array of hashes or drops
def sort(input, property = nil) def sort(input, property = nil)
ary = InputIterator.new(input) ary = InputIterator.new(input)
return [] if ary.empty?
if property.nil? if property.nil?
ary.sort do |a, b| ary.sort do |a, b|
if !a.nil? && !b.nil? nil_safe_compare(a, b)
a <=> b
else
a.nil? ? 1 : -1
end
end end
elsif ary.empty? # The next two cases assume a non-empty array.
[]
elsif ary.all? { |el| el.respond_to?(:[]) } elsif ary.all? { |el| el.respond_to?(:[]) }
ary.sort do |a, b| begin
a = a[property] ary.sort { |a, b| nil_safe_compare(a[property], b[property]) }
b = b[property] rescue TypeError
if !a.nil? && !b.nil? raise_property_error(property)
a <=> b
else
a.nil? ? 1 : -1
end
end end
end end
end end
@@ -148,25 +156,17 @@ module Liquid
def sort_natural(input, property = nil) def sort_natural(input, property = nil)
ary = InputIterator.new(input) ary = InputIterator.new(input)
return [] if ary.empty?
if property.nil? if property.nil?
ary.sort do |a, b| ary.sort do |a, b|
if !a.nil? && !b.nil? nil_safe_casecmp(a, b)
a.to_s.casecmp(b.to_s)
else
a.nil? ? 1 : -1
end
end end
elsif ary.empty? # The next two cases assume a non-empty array.
[]
elsif ary.all? { |el| el.respond_to?(:[]) } elsif ary.all? { |el| el.respond_to?(:[]) }
ary.sort do |a, b| begin
a = a[property] ary.sort { |a, b| nil_safe_casecmp(a[property], b[property]) }
b = b[property] rescue TypeError
if !a.nil? && !b.nil? raise_property_error(property)
a.to_s.casecmp(b.to_s)
else
a.nil? ? 1 : -1
end
end end
end end
end end
@@ -179,9 +179,34 @@ module Liquid
if ary.empty? if ary.empty?
[] []
elsif ary.first.respond_to?(:[]) && target_value.nil? elsif ary.first.respond_to?(:[]) && target_value.nil?
ary.where_present(property) begin
ary.select { |item| item[property] }
rescue TypeError
raise_property_error(property)
end
elsif ary.first.respond_to?(:[]) elsif ary.first.respond_to?(:[])
ary.where(property, target_value) begin
ary.select { |item| item[property] == target_value }
rescue TypeError
raise_property_error(property)
end
end
end
# Sort elements of an array in numeric order
# provide optional property with which to sort an array of hashes or drops
def sort_numeric(input, property = nil)
ary = InputIterator.new(input)
if property.nil?
ary.sort do |a, b|
Utils.to_number(a) <=> Utils.to_number(b)
end
elsif ary.empty? # The next two cases assume a non-empty array.
[]
elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
ary.sort do |a, b|
Utils.to_number(a[property]) <=> Utils.to_number(b[property])
end
end end
end end
@@ -195,7 +220,11 @@ module Liquid
elsif ary.empty? # The next two cases assume a non-empty array. elsif ary.empty? # The next two cases assume a non-empty array.
[] []
elsif ary.first.respond_to?(:[]) elsif ary.first.respond_to?(:[])
ary.uniq{ |a| a[property] } begin
ary.uniq { |a| a[property] }
rescue TypeError
raise_property_error(property)
end
end end
end end
@@ -210,13 +239,15 @@ module Liquid
InputIterator.new(input).map do |e| InputIterator.new(input).map do |e|
e = e.call if e.is_a?(Proc) e = e.call if e.is_a?(Proc)
if property == "to_liquid".freeze if property == "to_liquid"
e e
elsif e.respond_to?(:[]) elsif e.respond_to?(:[])
r = e[property] r = e[property]
r.is_a?(Proc) ? r.call : r r.is_a?(Proc) ? r.call : r
end end
end end
rescue TypeError
raise_property_error(property)
end end
# Remove nils within an array # Remove nils within an array
@@ -229,28 +260,32 @@ module Liquid
elsif ary.empty? # The next two cases assume a non-empty array. elsif ary.empty? # The next two cases assume a non-empty array.
[] []
elsif ary.first.respond_to?(:[]) elsif ary.first.respond_to?(:[])
ary.reject{ |a| a[property].nil? } begin
ary.reject { |a| a[property].nil? }
rescue TypeError
raise_property_error(property)
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.to_s, 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.to_s, 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.to_s, '')
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.to_s, '')
end end
# add one string to another # add one string to another
@@ -260,7 +295,7 @@ module Liquid
def concat(input, array) def concat(input, array)
unless array.respond_to?(:to_ary) unless array.respond_to?(:to_ary)
raise ArgumentError.new("concat filter requires an array argument") raise ArgumentError, "concat filter requires an array argument"
end end
InputIterator.new(input).concat(array) InputIterator.new(input).concat(array)
end end
@@ -272,7 +307,7 @@ 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 using Ruby's core Time#strftime( string ) -> string
@@ -309,7 +344,7 @@ module Liquid
def date(input, format) def date(input, format)
return input if format.to_s.empty? return input if format.to_s.empty?
return input unless date = Utils.to_date(input) return input unless (date = Utils.to_date(input))
date.strftime(format.to_s) date.strftime(format.to_s)
end end
@@ -403,8 +438,9 @@ module Liquid
result.is_a?(BigDecimal) ? result.to_f : result result.is_a?(BigDecimal) ? result.to_f : result
end end
def default(input, default_value = ''.freeze) def default(input, default_value = '')
if !input || input.respond_to?(:empty?) && input.empty? if !input || input.respond_to?(:empty?) && input.empty?
Usage.increment("default_filter_received_false_value") if input == false # See https://github.com/Shopify/liquid/issues/1127
default_value default_value
else else
input input
@@ -413,11 +449,31 @@ module Liquid
private private
def raise_property_error(property)
raise Liquid::ArgumentError, "cannot select the property '#{property}'"
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 = Utils.to_number(input).send(operation, Utils.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 class InputIterator
include Enumerable include Enumerable
@@ -463,24 +519,6 @@ module Liquid
yield(e.respond_to?(:to_liquid) ? e.to_liquid : e) yield(e.respond_to?(:to_liquid) ? e.to_liquid : e)
end end
end end
def where(property, target_value)
select do |item|
item[property] == target_value
end
rescue TypeError
# Cannot index with the given property type (eg. indexing integers with strings
# which are only allowed to be indexed by other integers).
raise ArgumentError.new("cannot select the property `#{property}`")
end
def where_present(property)
select { |item| item[property] }
rescue TypeError
# Cannot index with the given property type (eg. indexing integers with strings
# which are only allowed to be indexed by other integers).
raise ArgumentError.new("cannot select the property `#{property}`")
end
end end
end end

View File

@@ -0,0 +1,36 @@
# frozen_string_literal: true
module Liquid
class StaticRegisters
attr_reader :static, :registers
def initialize(registers = {})
@static = registers.is_a?(StaticRegisters) ? registers.static : registers
@registers = {}
end
def []=(key, value)
@registers[key] = value
end
def [](key)
if @registers.key?(key)
@registers[key]
else
@static[key]
end
end
def delete(key)
@registers.delete(key)
end
def fetch(key, default = nil)
key?(key) ? self[key] : default
end
def key?(key)
@registers.key?(key) || @static.key?(key)
end
end
end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'set' require 'set'
module Liquid module Liquid
@@ -27,7 +29,7 @@ module Liquid
def self.add_filter(filter) def self.add_filter(filter)
raise ArgumentError, "Expected module but got: #{filter.class}" unless filter.is_a?(Module) raise ArgumentError, "Expected module but got: #{filter.class}" unless filter.is_a?(Module)
unless self.include?(filter) unless include?(filter)
invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) } invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }
if invokable_non_public_methods.any? if invokable_non_public_methods.any?
raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}" raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"
@@ -54,7 +56,7 @@ module Liquid
def invoke(method, *args) def invoke(method, *args)
if self.class.invokable?(method) if self.class.invokable?(method)
send(method, *args) send(method, *args)
elsif @context && @context.strict_filters elsif @context&.strict_filters
raise Liquid::UndefinedFilter, "undefined filter #{method}" raise Liquid::UndefinedFilter, "undefined filter #{method}"
else else
args.first args.first

View File

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

View File

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

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
# Assign sets a variable in your template. # Assign sets a variable in your template.
# #
@@ -10,23 +12,27 @@ module Liquid
class Assign < Tag class Assign < Tag
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
def self.syntax_error_translation_key
"errors.syntax.assign"
end
attr_reader :to, :from attr_reader :to, :from
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
if markup =~ Syntax if markup =~ Syntax
@to = $1 @to = Regexp.last_match(1)
@from = Variable.new($2, options) @from = Variable.new(Regexp.last_match(2), options)
else else
raise SyntaxError.new options[:locale].t("errors.syntax.assign".freeze) raise SyntaxError, options[:locale].t(self.class.syntax_error_translation_key)
end end
end end
def render(context) def render_to_output_buffer(context, output)
val = @from.render(context) val = @from.render(context)
context.scopes.last[@to] = val context.scopes.last[@to] = val
context.resource_limits.assign_score += assign_score_of(val) context.resource_limits.assign_score += assign_score_of(val)
''.freeze output
end end
def blank? def blank?
@@ -37,7 +43,7 @@ module Liquid
def assign_score_of(val) def assign_score_of(val)
if val.instance_of?(String) if val.instance_of?(String)
val.length val.bytesize
elsif val.instance_of?(Array) || val.instance_of?(Hash) elsif val.instance_of?(Array) || val.instance_of?(Hash)
sum = 1 sum = 1
# Uses #each to avoid extra allocations. # Uses #each to avoid extra allocations.
@@ -55,5 +61,5 @@ module Liquid
end end
end end
Template.register_tag('assign'.freeze, Assign) Template.register_tag('assign', Assign)
end end

View File

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

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
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.
# #
@@ -16,17 +18,18 @@ module Liquid
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
if markup =~ Syntax if markup =~ Syntax
@to = $1 @to = Regexp.last_match(1)
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.capture")) raise SyntaxError, options[:locale].t("errors.syntax.capture")
end end
end end
def render(context) def render_to_output_buffer(context, output)
output = super previous_output_size = output.bytesize
super
context.scopes.last[@to] = output context.scopes.last[@to] = output
context.resource_limits.assign_score += output.length context.resource_limits.assign_score += (output.bytesize - previous_output_size)
''.freeze output
end end
def blank? def blank?
@@ -34,5 +37,5 @@ module Liquid
end end
end end
Template.register_tag('capture'.freeze, Capture) Template.register_tag('capture', Capture)
end end

View File

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

View File

@@ -1,7 +1,9 @@
# frozen_string_literal: true
module Liquid module Liquid
class Comment < Block class Comment < Block
def render(_context) def render_to_output_buffer(_context, output)
''.freeze output
end end
def unknown_tag(_tag, _markup, _tokens) def unknown_tag(_tag, _markup, _tokens)
@@ -12,5 +14,5 @@ module Liquid
end end
end end
Template.register_tag('comment'.freeze, Comment) Template.register_tag('comment', Comment)
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
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.
# #
@@ -14,5 +16,5 @@ module Liquid
end end
end end
Template.register_tag('continue'.freeze, Continue) Template.register_tag('continue', Continue)
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
# Cycle is usually used within a loop to alternate between values, like colors or DOM classes. # Cycle is usually used within a loop to alternate between values, like colors or DOM classes.
# #
@@ -21,28 +23,37 @@ module Liquid
super super
case markup case markup
when NamedSyntax when NamedSyntax
@variables = variables_from_string($2) @variables = variables_from_string(Regexp.last_match(2))
@name = Expression.parse($1) @name = Expression.parse(Regexp.last_match(1))
when SimpleSyntax when SimpleSyntax
@variables = variables_from_string(markup) @variables = variables_from_string(markup)
@name = @variables.to_s @name = @variables.to_s
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.cycle".freeze)) raise SyntaxError, options[:locale].t("errors.syntax.cycle")
end end
end end
def render(context) def render_to_output_buffer(context, output)
context.registers[:cycle] ||= {} context.registers[:cycle] ||= {}
context.stack do key = context.evaluate(@name)
key = context.evaluate(@name) iteration = context.registers[:cycle][key].to_i
iteration = context.registers[:cycle][key].to_i
result = context.evaluate(@variables[iteration]) val = context.evaluate(@variables[iteration])
iteration += 1
iteration = 0 if iteration >= @variables.size if val.is_a?(Array)
context.registers[:cycle][key] = iteration val = val.join
result elsif !val.is_a?(String)
val = val.to_s
end end
output << val
iteration += 1
iteration = 0 if iteration >= @variables.size
context.registers[:cycle][key] = iteration
output
end end
private private
@@ -50,7 +61,7 @@ module Liquid
def variables_from_string(markup) def variables_from_string(markup)
markup.split(',').collect do |var| markup.split(',').collect do |var|
var =~ /\s*(#{QuotedFragment})\s*/o var =~ /\s*(#{QuotedFragment})\s*/o
$1 ? Expression.parse($1) : nil Regexp.last_match(1) ? Expression.parse(Regexp.last_match(1)) : nil
end.compact end.compact
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
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
@@ -23,13 +25,14 @@ module Liquid
@variable = markup.strip @variable = markup.strip
end end
def render(context) def render_to_output_buffer(context, output)
value = context.environments.first[@variable] ||= 0 value = context.environments.first[@variable] ||= 0
value -= 1 value -= 1
context.environments.first[@variable] = value context.environments.first[@variable] = value
value.to_s output << value.to_s
output
end end
end end
Template.register_tag('decrement'.freeze, Decrement) Template.register_tag('decrement', Decrement)
end end

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

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

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid 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.
@@ -66,49 +68,51 @@ module Liquid
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 @else_block = BlockBody.new
end end
def render(context) def render_to_output_buffer(context, output)
segment = collection_segment(context) segment = collection_segment(context)
if segment.empty? if segment.empty?
render_else(context) render_else(context, output)
else else
render_segment(context, segment) render_segment(context, output, segment)
end end
output
end end
protected protected
def lax_parse(markup) def lax_parse(markup)
if markup =~ Syntax if markup =~ Syntax
@variable_name = $1 @variable_name = Regexp.last_match(1)
collection_name = $2 collection_name = Regexp.last_match(2)
@reversed = !!$3 @reversed = !!Regexp.last_match(3)
@name = "#{@variable_name}-#{collection_name}" @name = "#{@variable_name}-#{collection_name}"
@collection_name = Expression.parse(collection_name) @collection_name = Expression.parse(collection_name)
markup.scan(TagAttributes) do |key, value| markup.scan(TagAttributes) do |key, value|
set_attribute(key, value) set_attribute(key, value)
end end
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.for".freeze)) raise SyntaxError, 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, 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) @collection_name = Expression.parse(collection_name)
@reversed = p.id?('reversed'.freeze) @reversed = p.id?('reversed')
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, options[:locale].t("errors.syntax.for_invalid_attribute")
end end
p.consume p.consume
set_attribute(attribute, p.expression) set_attribute(attribute, p.expression)
@@ -124,14 +128,23 @@ module Liquid
from = if @from == :continue from = if @from == :continue
offsets[@name].to_i offsets[@name].to_i
else else
context.evaluate(@from).to_i from_value = context.evaluate(@from)
if from_value.nil?
0
else
Utils.to_integer(from_value)
end
end end
collection = context.evaluate(@collection_name) collection = context.evaluate(@collection_name)
collection = collection.to_a if collection.is_a?(Range) collection = collection.to_a if collection.is_a?(Range)
limit = context.evaluate(@limit) limit_value = context.evaluate(@limit)
to = limit ? limit.to_i + from : nil to = if limit_value.nil?
nil
else
Utils.to_integer(limit_value) + from
end
segment = Utils.slice_collection(collection, from, to) segment = Utils.slice_collection(collection, from, to)
segment.reverse! if @reversed segment.reverse! if @reversed
@@ -141,55 +154,56 @@ module Liquid
segment segment
end end
def render_segment(context, segment) def render_segment(context, output, segment)
for_stack = context.registers[:for_stack] ||= [] for_stack = context.registers[:for_stack] ||= []
length = segment.length length = segment.length
result = ''
context.stack do context.stack do
loop_vars = Liquid::ForloopDrop.new(@name, length, for_stack[-1]) loop_vars = Liquid::ForloopDrop.new(@name, length, for_stack[-1])
for_stack.push(loop_vars) for_stack.push(loop_vars)
begin begin
context['forloop'.freeze] = loop_vars context['forloop'] = loop_vars
segment.each do |item| segment.each do |item|
context[@variable_name] = item context[@variable_name] = item
result << @for_block.render(context) @for_block.render_to_output_buffer(context, output)
loop_vars.send(:increment!) loop_vars.send(:increment!)
# Handle any interrupts if they exist. # Handle any interrupts if they exist.
if context.interrupt? next unless context.interrupt?
interrupt = context.pop_interrupt interrupt = context.pop_interrupt
break if interrupt.is_a? BreakInterrupt break if interrupt.is_a?(BreakInterrupt)
next if interrupt.is_a? ContinueInterrupt next if interrupt.is_a?(ContinueInterrupt)
end
end end
ensure ensure
for_stack.pop for_stack.pop
end end
end end
result output
end end
def set_attribute(key, expr) def set_attribute(key, expr)
case key case key
when 'offset'.freeze when 'offset'
@from = if expr == 'continue'.freeze @from = if expr == 'continue'
:continue :continue
else else
Expression.parse(expr) Expression.parse(expr)
end end
when 'limit'.freeze when 'limit'
@limit = Expression.parse(expr) @limit = Expression.parse(expr)
end end
end end
def render_else(context) def render_else(context, output)
@else_block ? @else_block.render(context) : ''.freeze if @else_block
@else_block.render_to_output_buffer(context, output)
else
output
end
end end
class ParseTreeVisitor < Liquid::ParseTreeVisitor class ParseTreeVisitor < Liquid::ParseTreeVisitor
@@ -199,5 +213,5 @@ module Liquid
end end
end end
Template.register_tag('for'.freeze, For) Template.register_tag('for', For)
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
# If is the conditional block # If is the conditional block
# #
@@ -12,14 +14,14 @@ 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) BOOLEAN_OPERATORS = %w(and or).freeze
attr_reader :blocks attr_reader :blocks
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
@blocks = [] @blocks = []
push_block('if'.freeze, markup) push_block('if', markup)
end end
def nodelist def nodelist
@@ -32,28 +34,27 @@ module Liquid
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(context) def render_to_output_buffer(context, output)
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 block.attachment.render(context)
end
end end
''.freeze
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)
@@ -65,17 +66,17 @@ module Liquid
def lax_parse(markup) def lax_parse(markup)
expressions = markup.scan(ExpressionsAndOperators) expressions = markup.scan(ExpressionsAndOperators)
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop =~ Syntax raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop =~ Syntax
condition = Condition.new(Expression.parse($1), $2, Expression.parse($3)) condition = Condition.new(Expression.parse(Regexp.last_match(1)), Regexp.last_match(2), Expression.parse(Regexp.last_match(3)))
until expressions.empty? until expressions.empty?
operator = expressions.pop.to_s.strip operator = expressions.pop.to_s.strip
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop.to_s =~ Syntax raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop.to_s =~ Syntax
new_condition = Condition.new(Expression.parse($1), $2, Expression.parse($3)) new_condition = Condition.new(Expression.parse(Regexp.last_match(1)), Regexp.last_match(2), Expression.parse(Regexp.last_match(3)))
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless BOOLEAN_OPERATORS.include?(operator) raise SyntaxError, options[:locale].t("errors.syntax.if") unless BOOLEAN_OPERATORS.include?(operator)
new_condition.send(operator, condition) new_condition.send(operator, condition)
condition = new_condition condition = new_condition
end end
@@ -93,7 +94,7 @@ module Liquid
def parse_binary_comparisons(p) def parse_binary_comparisons(p)
condition = parse_comparison(p) condition = parse_comparison(p)
first_condition = condition first_condition = condition
while op = (p.id?('and'.freeze) || p.id?('or'.freeze)) while (op = (p.id?('and') || p.id?('or')))
child_condition = parse_comparison(p) child_condition = parse_comparison(p)
condition.send(op, child_condition) condition.send(op, child_condition)
condition = child_condition condition = child_condition
@@ -103,7 +104,7 @@ module Liquid
def parse_comparison(p) def parse_comparison(p)
a = Expression.parse(p.expression) a = Expression.parse(p.expression)
if op = p.consume?(:comparison) if (op = p.consume?(:comparison))
b = Expression.parse(p.expression) b = Expression.parse(p.expression)
Condition.new(a, op, b) Condition.new(a, op, b)
else else
@@ -118,5 +119,5 @@ module Liquid
end end
end end
Template.register_tag('if'.freeze, If) Template.register_tag('if', If)
end end

View File

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

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
# Include allows templates to relate with other templates # Include allows templates to relate with other templates
# #
@@ -23,8 +25,8 @@ module Liquid
if markup =~ Syntax if markup =~ Syntax
template_name = $1 template_name = Regexp.last_match(1)
variable_name = $3 variable_name = Regexp.last_match(3)
@variable_name_expr = variable_name ? Expression.parse(variable_name) : nil @variable_name_expr = variable_name ? Expression.parse(variable_name) : nil
@template_name_expr = Expression.parse(template_name) @template_name_expr = Expression.parse(template_name)
@@ -35,19 +37,24 @@ module Liquid
end end
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.include".freeze)) raise SyntaxError, options[:locale].t("errors.syntax.include")
end end
end end
def parse(_tokens) def parse(_tokens)
end end
def render(context) def render_to_output_buffer(context, output)
template_name = context.evaluate(@template_name_expr) template_name = context.evaluate(@template_name_expr)
raise ArgumentError.new(options[:locale].t("errors.argument.include")) unless template_name raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name
partial = load_cached_partial(template_name, context) partial = PartialCache.load(
context_variable_name = template_name.split('/'.freeze).last template_name,
context: context,
parse_context: parse_context
)
context_variable_name = template_name.split('/').last
variable = if @variable_name_expr variable = if @variable_name_expr
context.evaluate(@variable_name_expr) context.evaluate(@variable_name_expr)
@@ -66,59 +73,35 @@ module Liquid
end end
if variable.is_a?(Array) if variable.is_a?(Array)
variable.collect do |var| variable.each do |var|
context[context_variable_name] = var context[context_variable_name] = var
partial.render(context) partial.render_to_output_buffer(context, output)
end end
else else
context[context_variable_name] = variable context[context_variable_name] = variable
partial.render(context) partial.render_to_output_buffer(context, output)
end end
end end
ensure ensure
context.template_name = old_template_name context.template_name = old_template_name
context.partial = old_partial context.partial = old_partial
end end
end
private output
end
alias_method :parse_context, :options alias_method :parse_context, :options
private :parse_context private :parse_context
def load_cached_partial(template_name, context)
cached_partials = context.registers[:cached_partials] || {}
if cached = cached_partials[template_name]
return cached
end
source = read_template_from_file_system(context)
begin
parse_context.partial = true
partial = Liquid::Template.parse(source, parse_context)
ensure
parse_context.partial = false
end
cached_partials[template_name] = partial
context.registers[:cached_partials] = cached_partials
partial
end
def read_template_from_file_system(context)
file_system = context.registers[:file_system] || Liquid::Template.file_system
file_system.read_template_file(context.evaluate(@template_name_expr))
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children def children
[ [
@node.template_name_expr, @node.template_name_expr,
@node.variable_name_expr @node.variable_name_expr,
] + @node.attributes.values ] + @node.attributes.values
end end
end end
end end
Template.register_tag('include'.freeze, Include) Template.register_tag('include', Include)
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
# increment is used in a place where one needs to insert a counter # increment 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
@@ -20,12 +22,14 @@ module Liquid
@variable = markup.strip @variable = markup.strip
end end
def render(context) def render_to_output_buffer(context, output)
value = context.environments.first[@variable] ||= 0 value = context.environments.first[@variable] ||= 0
context.environments.first[@variable] = value + 1 context.environments.first[@variable] = value + 1
value.to_s
output << value.to_s
output
end end
end end
Template.register_tag('increment'.freeze, Increment) Template.register_tag('increment', Increment)
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
class Raw < Block class Raw < Block
Syntax = /\A\s*\z/ Syntax = /\A\s*\z/
@@ -10,20 +12,21 @@ module Liquid
end end
def parse(tokens) def parse(tokens)
@body = '' @body = +''
while token = tokens.shift while (token = tokens.shift)
if token =~ FullTokenPossiblyInvalid if token =~ FullTokenPossiblyInvalid
@body << $1 if $1 != "".freeze @body << Regexp.last_match(1) if Regexp.last_match(1) != ""
return if block_delimiter == $2 return if block_delimiter == Regexp.last_match(2)
end end
@body << token unless token.empty? @body << token unless token.empty?
end end
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name)) raise SyntaxError, parse_context.locale.t("errors.syntax.tag_never_closed", block_name: block_name)
end end
def render(_context) def render_to_output_buffer(_context, output)
@body output << @body
output
end end
def nodelist def nodelist
@@ -37,11 +40,11 @@ module Liquid
protected protected
def ensure_valid_markup(tag_name, markup, parse_context) def ensure_valid_markup(tag_name, markup, parse_context)
unless markup =~ Syntax unless Syntax.match?(markup)
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_unexpected_args".freeze, tag: tag_name)) raise SyntaxError, parse_context.locale.t("errors.syntax.tag_unexpected_args", tag: tag_name)
end end
end end
end end
Template.register_tag('raw'.freeze, Raw) Template.register_tag('raw', Raw)
end end

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

@@ -0,0 +1,62 @@
# frozen_string_literal: true
module Liquid
class Render < Tag
SYNTAX = /(#{QuotedString})#{QuotedFragment}*/o
disable_tags "include"
attr_reader :template_name_expr, :attributes
def initialize(tag_name, markup, options)
super
raise SyntaxError, options[:locale].t("errors.syntax.render") unless markup =~ SYNTAX
template_name = Regexp.last_match(1)
@template_name_expr = Expression.parse(template_name)
@attributes = {}
markup.scan(TagAttributes) do |key, value|
@attributes[key] = Expression.parse(value)
end
end
def render_to_output_buffer(context, output)
render_tag(context, output)
end
def render_tag(context, output)
# Though we evaluate this here we will only ever parse it as a string literal.
template_name = context.evaluate(@template_name_expr)
raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name
partial = PartialCache.load(
template_name,
context: context,
parse_context: parse_context
)
inner_context = context.new_isolated_subcontext
inner_context.template_name = template_name
inner_context.partial = true
@attributes.each do |key, value|
inner_context[key] = context.evaluate(value)
end
partial.render_to_output_buffer(inner_context, output)
output
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[
@node.template_name_expr,
] + @node.attributes.values
end
end
end
Template.register_tag('render', Render)
end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
class TableRow < Block class TableRow < Block
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
@@ -7,48 +9,51 @@ module Liquid
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
if markup =~ Syntax if markup =~ Syntax
@variable_name = $1 @variable_name = Regexp.last_match(1)
@collection_name = Expression.parse($2) @collection_name = Expression.parse(Regexp.last_match(2))
@attributes = {} @attributes = {}
markup.scan(TagAttributes) do |key, value| markup.scan(TagAttributes) do |key, value|
@attributes[key] = Expression.parse(value) @attributes[key] = Expression.parse(value)
end end
else else
raise SyntaxError.new(options[:locale].t("errors.syntax.table_row".freeze)) raise SyntaxError, options[:locale].t("errors.syntax.table_row")
end end
end end
def render(context) def render_to_output_buffer(context, output)
collection = context.evaluate(@collection_name) or return ''.freeze (collection = context.evaluate(@collection_name)) || (return '')
from = @attributes.key?('offset'.freeze) ? context.evaluate(@attributes['offset'.freeze]).to_i : 0 from = @attributes.key?('offset') ? context.evaluate(@attributes['offset']).to_i : 0
to = @attributes.key?('limit'.freeze) ? from + context.evaluate(@attributes['limit'.freeze]).to_i : nil to = @attributes.key?('limit') ? from + context.evaluate(@attributes['limit']).to_i : nil
collection = Utils.slice_collection(collection, from, to) collection = Utils.slice_collection(collection, from, to)
length = collection.length length = collection.length
cols = context.evaluate(@attributes['cols'.freeze]).to_i cols = context.evaluate(@attributes['cols']).to_i
result = "<tr class=\"row1\">\n" output << "<tr class=\"row1\">\n"
context.stack do context.stack do
tablerowloop = Liquid::TablerowloopDrop.new(length, cols) tablerowloop = Liquid::TablerowloopDrop.new(length, cols)
context['tablerowloop'.freeze] = tablerowloop context['tablerowloop'] = tablerowloop
collection.each do |item| collection.each do |item|
context[@variable_name] = item context[@variable_name] = item
result << "<td class=\"col#{tablerowloop.col}\">" << super << '</td>' output << "<td class=\"col#{tablerowloop.col}\">"
super
output << '</td>'
if tablerowloop.col_last && !tablerowloop.last if tablerowloop.col_last && !tablerowloop.last
result << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">" output << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">"
end end
tablerowloop.send(:increment!) tablerowloop.send(:increment!)
end end
end end
result << "</tr>\n"
result output << "</tr>\n"
output
end end
class ParseTreeVisitor < Liquid::ParseTreeVisitor class ParseTreeVisitor < Liquid::ParseTreeVisitor
@@ -58,5 +63,5 @@ module Liquid
end end
end end
Template.register_tag('tablerow'.freeze, TableRow) Template.register_tag('tablerow', TableRow)
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require_relative 'if' require_relative 'if'
module Liquid module Liquid
@@ -6,25 +8,23 @@ module Liquid
# {% unless x < 0 %} x is greater than zero {% endunless %} # {% unless x < 0 %} x is greater than zero {% endunless %}
# #
class Unless < If class Unless < If
def render(context) def render_to_output_buffer(context, output)
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 first_block.attachment.render(context)
end
# After the first condition unless works just like if
@blocks[1..-1].each do |block|
if block.evaluate(context)
return block.attachment.render(context)
end
end
''.freeze
end end
# After the first condition unless works just like if
@blocks[1..-1].each do |block|
if block.evaluate(context)
return block.attachment.render_to_output_buffer(context, output)
end
end
output
end end
end end
Template.register_tag('unless'.freeze, Unless) Template.register_tag('unless', Unless)
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
# Templates are central to liquid. # Templates are central to liquid.
# Interpretating templates is a two step process. First you compile the # Interpretating templates is a two step process. First you compile the
@@ -50,7 +52,7 @@ module Liquid
private private
def lookup_class(name) def lookup_class(name)
name.split("::").reject(&:empty?).reduce(Object) { |scope, const| scope.const_get(const) } Object.const_get(name)
end end
end end
@@ -90,6 +92,14 @@ module Liquid
@tags ||= TagRegistry.new @tags ||= TagRegistry.new
end end
def add_register(name, klass)
registers[name.to_sym] = klass
end
def registers
@registers ||= {}
end
def error_mode def error_mode
@error_mode ||= :lax @error_mode ||= :lax
end end
@@ -165,14 +175,14 @@ 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 c = args.shift
if @rethrow_errors if @rethrow_errors
c.exception_renderer = ->(e) { raise } c.exception_renderer = ->(_e) { raise }
end end
c c
@@ -187,27 +197,37 @@ module Liquid
raise ArgumentError, "Expected Hash or Liquid::Context as parameter" raise ArgumentError, "Expected Hash or Liquid::Context as parameter"
end end
output = nil
context_register = context.registers.is_a?(StaticRegisters) ? context.registers.static : context.registers
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) options[:registers]&.each do |key, register|
context_register[key] = register
end
apply_options_to_context(context, options) apply_options_to_context(context, options)
when Module, Array when Module, Array
context.add_filters(args.pop) context.add_filters(args.pop)
end end
Template.registers.each do |key, register|
context_register[key] = register
end
# Retrying a render resets resource usage # Retrying a render resets resource usage
context.resource_limits.reset context.resource_limits.reset
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.
result = with_profiling(context) do with_profiling(context) do
@root.render(context) @root.render_to_output_buffer(context, output || +'')
end end
result.respond_to?(:join) ? result.join : result
rescue Liquid::MemoryError => e rescue Liquid::MemoryError => e
context.handle_error(e) context.handle_error(e)
ensure ensure
@@ -220,6 +240,10 @@ module Liquid
render(*args) render(*args)
end end
def render_to_output_buffer(context, output)
render(context, output: output)
end
private private
def tokenize(source) def tokenize(source)

View File

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

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

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

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
module Utils module Utils
def self.slice_collection(collection, from, to) def self.slice_collection(collection, from, to)
@@ -50,7 +52,7 @@ module Liquid
when Numeric when Numeric
obj obj
when String when String
(obj.strip =~ /\A-?\d+\.\d+\z/) ? BigDecimal(obj) : obj.to_i /\A-?\d+\.\d+\z/.match?(obj.strip) ? BigDecimal(obj) : obj.to_i
else else
if obj.respond_to?(:to_number) if obj.respond_to?(:to_number)
obj.to_number obj.to_number
@@ -69,7 +71,7 @@ module Liquid
end end
case obj case obj
when 'now'.freeze, 'today'.freeze when 'now', 'today'
Time.now Time.now
when /\A\d+\z/, Integer when /\A\d+\z/, Integer
Time.at(obj.to_i) Time.at(obj.to_i)

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid module Liquid
# Holds variables. Variables are only loaded "just in time" # Holds variables. Variables are only loaded "just in time"
# and are not evaluated as part of the render stage # and are not evaluated as part of the render stage
@@ -43,11 +45,11 @@ module Liquid
@filters = [] @filters = []
return unless markup =~ MarkupWithQuotedFragment return unless markup =~ MarkupWithQuotedFragment
name_markup = $1 name_markup = Regexp.last_match(1)
filter_markup = $2 filter_markup = Regexp.last_match(2)
@name = Expression.parse(name_markup) @name = Expression.parse(name_markup)
if filter_markup =~ FilterMarkupRegex if filter_markup =~ FilterMarkupRegex
filters = $1.scan(FilterParser) filters = Regexp.last_match(1).scan(FilterParser)
filters.each do |f| filters.each do |f|
next unless f =~ /\w+/ next unless f =~ /\w+/
filtername = Regexp.last_match(0) filtername = Regexp.last_match(0)
@@ -85,31 +87,51 @@ module Liquid
end end
obj = context.apply_global_filter(obj) obj = context.apply_global_filter(obj)
taint_check(context, obj) taint_check(context, obj)
obj obj
end 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
def disabled?(_context)
false
end
def disabled_tags
[]
end
private private
def parse_filter_expressions(filter_name, unparsed_args) def parse_filter_expressions(filter_name, unparsed_args)
filter_args = [] filter_args = []
keyword_args = {} keyword_args = nil
unparsed_args.each do |a| unparsed_args.each do |a|
if matches = a.match(JustTagAttributes) if (matches = a.match(JustTagAttributes))
keyword_args ||= {}
keyword_args[matches[1]] = Expression.parse(matches[2]) keyword_args[matches[1]] = Expression.parse(matches[2])
else else
filter_args << Expression.parse(a) filter_args << Expression.parse(a)
end end
end end
result = [filter_name, filter_args] result = [filter_name, filter_args]
result << keyword_args unless keyword_args.empty? result << keyword_args if keyword_args
result result
end end
def evaluate_filter_expressions(context, filter_args, filter_kwargs) def evaluate_filter_expressions(context, filter_args, filter_kwargs)
parsed_args = filter_args.map{ |expr| context.evaluate(expr) } parsed_args = filter_args.map { |expr| context.evaluate(expr) }
if filter_kwargs if filter_kwargs
parsed_kwargs = {} parsed_kwargs = {}
filter_kwargs.each do |key, expr| filter_kwargs.each do |key, expr|

View File

@@ -1,7 +1,9 @@
# frozen_string_literal: true
module Liquid module Liquid
class VariableLookup class VariableLookup
SQUARE_BRACKETED = /\A\[(.*)\]\z/m SQUARE_BRACKETED = /\A\[(.*)\]\z/m
COMMAND_METHODS = ['size'.freeze, 'first'.freeze, 'last'.freeze] COMMAND_METHODS = ['size', 'first', 'last'].freeze
attr_reader :name, :lookups attr_reader :name, :lookups
@@ -14,7 +16,7 @@ module Liquid
name = lookups.shift name = lookups.shift
if name =~ SQUARE_BRACKETED if name =~ SQUARE_BRACKETED
name = Expression.parse($1) name = Expression.parse(Regexp.last_match(1))
end end
@name = name @name = name
@@ -24,7 +26,7 @@ module Liquid
@lookups.each_index do |i| @lookups.each_index do |i|
lookup = lookups[i] lookup = lookups[i]
if lookup =~ SQUARE_BRACKETED if lookup =~ SQUARE_BRACKETED
lookups[i] = Expression.parse($1) lookups[i] = Expression.parse(Regexp.last_match(1))
elsif COMMAND_METHODS.include?(lookup) elsif COMMAND_METHODS.include?(lookup)
@command_flags |= 1 << i @command_flags |= 1 << i
end end

View File

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

View File

@@ -1,7 +1,8 @@
# encoding: utf-8 # encoding: utf-8
# frozen_string_literal: true
lib = File.expand_path('../lib/', __FILE__) lib = File.expand_path('../lib/', __FILE__)
$LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require "liquid/version" require "liquid/version"
@@ -16,7 +17,7 @@ Gem::Specification.new do |s|
s.license = "MIT" s.license = "MIT"
# s.description = "A secure, non-evaling end user template engine with aesthetic markup." # s.description = "A secure, non-evaling end user template engine with aesthetic markup."
s.required_ruby_version = ">= 2.1.0" s.required_ruby_version = ">= 2.4.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}/**/*")
@@ -26,6 +27,6 @@ Gem::Specification.new do |s|
s.require_path = "lib" s.require_path = "lib"
s.add_development_dependency 'rake', '~> 11.3' s.add_development_dependency('rake', '~> 11.3')
s.add_development_dependency 'minitest' s.add_development_dependency('minitest')
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'benchmark/ips' require 'benchmark/ips'
require_relative 'theme_runner' require_relative 'theme_runner'

View File

@@ -0,0 +1,63 @@
# 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

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'stackprof' require 'stackprof'
require_relative 'theme_runner' require_relative 'theme_runner'
@@ -13,7 +15,7 @@ profiler.run
end end
end end
if profile_type == :cpu && graph_filename = ENV['GRAPH_FILENAME'] if profile_type == :cpu && (graph_filename = ENV['GRAPH_FILENAME'])
File.open(graph_filename, 'w') do |f| File.open(graph_filename, 'w') do |f|
StackProf::Report.new(results).print_graphviz(nil, f) StackProf::Report.new(results).print_graphviz(nil, f)
end end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module TagFilter module TagFilter
def link_to_tag(label, tag) def link_to_tag(label, tag)
"<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tag}\">#{label}</a>" "<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tag}\">#{label}</a>"
@@ -13,11 +15,11 @@ module TagFilter
def link_to_add_tag(label, tag) def link_to_add_tag(label, tag)
tags = (@context['current_tags'] + [tag]).uniq tags = (@context['current_tags'] + [tag]).uniq
"<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tags.join("+")}\">#{label}</a>" "<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tags.join('+')}\">#{label}</a>"
end end
def link_to_remove_tag(label, tag) def link_to_remove_tag(label, tag)
tags = (@context['current_tags'] - [tag]).uniq tags = (@context['current_tags'] - [tag]).uniq
"<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tags.join("+")}\">#{label}</a>" "<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tags.join('+')}\">#{label}</a>"
end end
end end

View File

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

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
# This profiler run simulates Shopify. # This profiler run simulates Shopify.
# We are looking in the tests directory for liquid files and render them within the designated layout file. # We are looking in the tests directory for liquid files and render them within the designated layout file.
# We will also export a substantial database to liquid which the templates can render values of. # We will also export a substantial database to liquid which the templates can render values of.
@@ -31,7 +33,7 @@ class ThemeRunner
{ {
liquid: File.read(test), liquid: File.read(test),
layout: (File.file?(theme_path) ? File.read(theme_path) : nil), layout: (File.file?(theme_path) ? File.read(theme_path) : nil),
template_name: test template_name: test,
} }
end.compact end.compact

View File

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

View File

@@ -1,11 +1,12 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class FoobarTag < Liquid::Tag class FoobarTag < Liquid::Tag
def render(*args) def render_to_output_buffer(_context, output)
" " output << ' '
output
end end
Liquid::Template.register_tag('foobar', FoobarTag)
end end
class BlankTestFileSystem class BlankTestFileSystem
@@ -31,7 +32,9 @@ class BlankTest < Minitest::Test
end end
def test_new_tags_are_not_blank_by_default def test_new_tags_are_not_blank_by_default
assert_template_result(" " * N, wrap_in_for("{% foobar %}")) with_custom_tag('foobar', FoobarTag) do
assert_template_result(" " * N, wrap_in_for("{% foobar %}"))
end
end end
def test_loops_are_blank def test_loops_are_blank

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class ContextDrop < Liquid::Drop class ContextDrop < Liquid::Drop
@@ -31,7 +33,7 @@ class ProductDrop < Liquid::Drop
class CatchallDrop < Liquid::Drop class CatchallDrop < Liquid::Drop
def liquid_method_missing(method) def liquid_method_missing(method)
'catchall_method: ' << method.to_s "catchall_method: #{method}"
end end
end end
@@ -48,7 +50,7 @@ class ProductDrop < Liquid::Drop
end end
def user_input def user_input
"foo".taint (+"foo").taint
end end
protected protected
@@ -201,9 +203,9 @@ class DropsTest < Minitest::Test
end end
def test_scope_though_proc def test_scope_though_proc
assert_equal '1', Liquid::Template.parse('{{ s }}').render!('context' => ContextDrop.new, 's' => proc{ |c| c['context.scopes'] }) assert_equal '1', Liquid::Template.parse('{{ s }}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] })
assert_equal '2', Liquid::Template.parse('{%for i in dummy%}{{ s }}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc{ |c| c['context.scopes'] }, 'dummy' => [1]) assert_equal '2', Liquid::Template.parse('{%for i in dummy%}{{ s }}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }, 'dummy' => [1])
assert_equal '3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ s }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc{ |c| c['context.scopes'] }, 'dummy' => [1]) assert_equal '3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ s }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }, 'dummy' => [1])
end end
def test_scope_with_assigns def test_scope_with_assigns
@@ -241,7 +243,7 @@ class DropsTest < Minitest::Test
end end
def test_some_enumerable_methods_still_get_invoked def test_some_enumerable_methods_still_get_invoked
[ :count, :max ].each do |method| [:count, :max].each do |method|
assert_equal "3", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new) assert_equal "3", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new)
assert_equal "3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new) assert_equal "3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new)
assert_equal "3", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new) assert_equal "3", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)
@@ -250,7 +252,7 @@ class DropsTest < Minitest::Test
assert_equal "yes", Liquid::Template.parse("{% if collection contains 3 %}yes{% endif %}").render!('collection' => RealEnumerableDrop.new) assert_equal "yes", Liquid::Template.parse("{% if collection contains 3 %}yes{% endif %}").render!('collection' => RealEnumerableDrop.new)
[ :min, :first ].each do |method| [:min, :first].each do |method|
assert_equal "1", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new) assert_equal "1", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new)
assert_equal "1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new) assert_equal "1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new)
assert_equal "1", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new) assert_equal "1", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)
@@ -270,4 +272,11 @@ class DropsTest < Minitest::Test
assert_equal 'ProductDrop', Liquid::Template.parse("{{ product }}").render!('product' => ProductDrop.new) assert_equal 'ProductDrop', Liquid::Template.parse("{{ product }}").render!('product' => ProductDrop.new)
assert_equal 'EnumerableDrop', Liquid::Template.parse('{{ collection }}').render!('collection' => EnumerableDrop.new) assert_equal 'EnumerableDrop', Liquid::Template.parse('{{ collection }}').render!('collection' => EnumerableDrop.new)
end end
def test_invokable_methods
assert_equal %w(to_liquid catchall user_input context texts).to_set, ProductDrop.invokable_methods
assert_equal %w(to_liquid scopes_as_array loop_pos scopes).to_set, ContextDrop.invokable_methods
assert_equal %w(to_liquid size max min first count).to_set, EnumerableDrop.invokable_methods
assert_equal %w(to_liquid max min sort count first).to_set, RealEnumerableDrop.invokable_methods
end
end # DropsTest end # DropsTest

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class ErrorHandlingTest < Minitest::Test class ErrorHandlingTest < Minitest::Test
@@ -83,15 +85,14 @@ class ErrorHandlingTest < Minitest::Test
def test_with_line_numbers_adds_numbers_to_parser_errors def test_with_line_numbers_adds_numbers_to_parser_errors
err = assert_raises(SyntaxError) do err = assert_raises(SyntaxError) do
Liquid::Template.parse(%q( Liquid::Template.parse('
foobar foobar
{% "cat" | foobar %} {% "cat" | foobar %}
bla bla
), ',
line_numbers: true line_numbers: true)
)
end end
assert_match(/Liquid syntax error \(line 4\)/, err.message) assert_match(/Liquid syntax error \(line 4\)/, err.message)
@@ -99,15 +100,14 @@ class ErrorHandlingTest < Minitest::Test
def test_with_line_numbers_adds_numbers_to_parser_errors_with_whitespace_trim def test_with_line_numbers_adds_numbers_to_parser_errors_with_whitespace_trim
err = assert_raises(SyntaxError) do err = assert_raises(SyntaxError) do
Liquid::Template.parse(%q( Liquid::Template.parse('
foobar foobar
{%- "cat" | foobar -%} {%- "cat" | foobar -%}
bla bla
), ',
line_numbers: true line_numbers: true)
)
end end
assert_match(/Liquid syntax error \(line 4\)/, err.message) assert_match(/Liquid syntax error \(line 4\)/, err.message)
@@ -122,8 +122,7 @@ class ErrorHandlingTest < Minitest::Test
bla bla
', ',
error_mode: :warn, error_mode: :warn,
line_numbers: true line_numbers: true)
)
assert_equal ['Liquid syntax error (line 4): Unexpected character = in "1 =! 2"'], assert_equal ['Liquid syntax error (line 4): Unexpected character = in "1 =! 2"'],
template.warnings.map(&:message) template.warnings.map(&:message)
@@ -139,8 +138,7 @@ class ErrorHandlingTest < Minitest::Test
bla bla
', ',
error_mode: :strict, error_mode: :strict,
line_numbers: true line_numbers: true)
)
end end
assert_equal 'Liquid syntax error (line 4): Unexpected character = in "1 =! 2"', err.message assert_equal 'Liquid syntax error (line 4): Unexpected character = in "1 =! 2"', err.message
@@ -157,8 +155,7 @@ class ErrorHandlingTest < Minitest::Test
bla bla
', ',
line_numbers: true line_numbers: true)
)
end end
assert_equal "Liquid syntax error (line 5): Unknown tag 'foo'", err.message assert_equal "Liquid syntax error (line 5): Unknown tag 'foo'", err.message
@@ -205,7 +202,7 @@ class ErrorHandlingTest < Minitest::Test
def test_default_exception_renderer_with_internal_error def test_default_exception_renderer_with_internal_error
template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true) template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true)
output = template.render({ 'errors' => ErrorDrop.new }) output = template.render('errors' => ErrorDrop.new)
assert_equal 'This is a runtime error: Liquid error (line 1): internal', output assert_equal 'This is a runtime error: Liquid error (line 1): internal', output
assert_equal [Liquid::InternalError], template.errors.map(&:class) assert_equal [Liquid::InternalError], template.errors.map(&:class)
@@ -214,10 +211,13 @@ class ErrorHandlingTest < Minitest::Test
def test_setting_default_exception_renderer def test_setting_default_exception_renderer
old_exception_renderer = Liquid::Template.default_exception_renderer old_exception_renderer = Liquid::Template.default_exception_renderer
exceptions = [] exceptions = []
Liquid::Template.default_exception_renderer = ->(e) { exceptions << e; '' } Liquid::Template.default_exception_renderer = ->(e) {
exceptions << e
''
}
template = Liquid::Template.parse('This is a runtime error: {{ errors.argument_error }}') template = Liquid::Template.parse('This is a runtime error: {{ errors.argument_error }}')
output = template.render({ 'errors' => ErrorDrop.new }) output = template.render('errors' => ErrorDrop.new)
assert_equal 'This is a runtime error: ', output assert_equal 'This is a runtime error: ', output
assert_equal [Liquid::ArgumentError], template.errors.map(&:class) assert_equal [Liquid::ArgumentError], template.errors.map(&:class)
@@ -228,7 +228,10 @@ class ErrorHandlingTest < Minitest::Test
def test_exception_renderer_exposing_non_liquid_error def test_exception_renderer_exposing_non_liquid_error
template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true) template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true)
exceptions = [] exceptions = []
handler = ->(e) { exceptions << e; e.cause } handler = ->(e) {
exceptions << e
e.cause
}
output = template.render({ 'errors' => ErrorDrop.new }, exception_renderer: handler) output = template.render({ 'errors' => ErrorDrop.new }, exception_renderer: handler)
@@ -239,7 +242,7 @@ class ErrorHandlingTest < Minitest::Test
end end
class TestFileSystem class TestFileSystem
def read_template_file(template_path) def read_template_file(_template_path)
"{{ errors.argument_error }}" "{{ errors.argument_error }}"
end end
end end

View File

@@ -1,24 +1,26 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
module MoneyFilter module MoneyFilter
def money(input) def money(input)
sprintf(' %d$ ', input) format(' %d$ ', input)
end end
def money_with_underscore(input) def money_with_underscore(input)
sprintf(' %d$ ', input) format(' %d$ ', input)
end end
end end
module CanadianMoneyFilter module CanadianMoneyFilter
def money(input) def money(input)
sprintf(' %d$ CAD ', input) format(' %d$ CAD ', input)
end end
end end
module SubstituteFilter module SubstituteFilter
def substitute(input, params = {}) def substitute(input, params = {})
input.gsub(/%\{(\w+)\}/) { |match| params[$1] } input.gsub(/%\{(\w+)\}/) { |_match| params[Regexp.last_match(1)] }
end end
end end
@@ -26,7 +28,7 @@ class FiltersTest < Minitest::Test
include Liquid include Liquid
module OverrideObjectMethodFilter module OverrideObjectMethodFilter
def tap(input) def tap(_input)
"tap overridden" "tap overridden"
end end
end end
@@ -149,7 +151,7 @@ class FiltersTest < Minitest::Test
assert_equal "tap overridden", Template.parse("{{var | tap}}").render!({ 'var' => 1000 }, filters: [OverrideObjectMethodFilter]) assert_equal "tap overridden", Template.parse("{{var | tap}}").render!({ 'var' => 1000 }, filters: [OverrideObjectMethodFilter])
# tap still treated as a non-existent filter # tap still treated as a non-existent filter
assert_equal "1000", Template.parse("{{var | tap}}").render!({ 'var' => 1000 }) assert_equal "1000", Template.parse("{{var | tap}}").render!('var' => 1000)
end end
end end

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class ParsingQuirksTest < Minitest::Test class ParsingQuirksTest < Minitest::Test
@@ -99,7 +101,7 @@ class ParsingQuirksTest < Minitest::Test
# After the messed up quotes a filter without parameters (reverse) should work # After the messed up quotes a filter without parameters (reverse) should work
# but one with parameters (remove) shouldn't be detected. # but one with parameters (remove) shouldn't be detected.
assert_template_result('here', "{{ 'hi there' | split:\"t\"\" | reverse | first}}") assert_template_result('here', "{{ 'hi there' | split:\"t\"\" | reverse | first}}")
assert_template_result('hi ', "{{ 'hi there' | split:\"t\"\" | remove:\"i\" | first}}") assert_template_result('hi ', "{{ 'hi there' | split:\"t\"\" | remove:\"i\" | first}}")
end end
end end

View File

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

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
class RenderProfilingTest < Minitest::Test class RenderProfilingTest < Minitest::Test
@@ -128,7 +130,7 @@ class RenderProfilingTest < Minitest::Test
t.render! t.render!
timing_count = 0 timing_count = 0
t.profiler.each do |timing| t.profiler.each do |_timing|
timing_count += 1 timing_count += 1
end end
@@ -145,7 +147,7 @@ class RenderProfilingTest < Minitest::Test
def test_profiling_marks_children_of_for_blocks def test_profiling_marks_children_of_for_blocks
t = Template.parse("{% for item in collection %} {{ item }} {% endfor %}", profile: true) t = Template.parse("{% for item in collection %} {{ item }} {% endfor %}", profile: true)
t.render!({ "collection" => ["one", "two"] }) t.render!("collection" => ["one", "two"])
assert_equal 1, t.profiler.length assert_equal 1, t.profiler.length
# Will profile each invocation of the for block # Will profile each invocation of the for block

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
module SecurityFilter module SecurityFilter

View File

@@ -1,4 +1,5 @@
# encoding: utf-8 # encoding: utf-8
# frozen_string_literal: true
require 'test_helper' require 'test_helper'
@@ -17,7 +18,7 @@ class TestThing
"woot: #{@foo}" "woot: #{@foo}"
end end
def [](whatever) def [](_whatever)
to_s to_s
end end
@@ -37,7 +38,7 @@ class TestEnumerable < Liquid::Drop
include Enumerable include Enumerable
def each(&block) def each(&block)
[ { "foo" => 1, "bar" => 2 }, { "foo" => 2, "bar" => 1 }, { "foo" => 3, "bar" => 3 } ].each(&block) [{ "foo" => 1, "bar" => 2 }, { "foo" => 2, "bar" => 1 }, { "foo" => 3, "bar" => 3 }].each(&block)
end end
end end
@@ -158,6 +159,10 @@ class StandardFiltersTest < Minitest::Test
assert_equal '1', @filters.url_decode(1) assert_equal '1', @filters.url_decode(1)
assert_equal '2001-02-03', @filters.url_decode(Date.new(2001, 2, 3)) assert_equal '2001-02-03', @filters.url_decode(Date.new(2001, 2, 3))
assert_nil @filters.url_decode(nil) assert_nil @filters.url_decode(nil)
exception = assert_raises Liquid::ArgumentError do
@filters.url_decode('%ff')
end
assert_equal 'Liquid error: invalid byte sequence in UTF-8', exception.message
end end
def test_truncatewords def test_truncatewords
@@ -177,6 +182,9 @@ class StandardFiltersTest < Minitest::Test
assert_equal 'test', @filters.strip_html("<div\nclass='multiline'>test</div>") assert_equal 'test', @filters.strip_html("<div\nclass='multiline'>test</div>")
assert_equal 'test', @filters.strip_html("<!-- foo bar \n test -->test") assert_equal 'test', @filters.strip_html("<!-- foo bar \n test -->test")
assert_equal '', @filters.strip_html(nil) assert_equal '', @filters.strip_html(nil)
# Quirk of the existing implementation
assert_equal 'foo;', @filters.strip_html("<<<script </script>script>foo;</script>")
end end
def test_join def test_join
@@ -190,6 +198,12 @@ class StandardFiltersTest < Minitest::Test
assert_equal [{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }], @filters.sort([{ "a" => 4 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a") assert_equal [{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }], @filters.sort([{ "a" => 4 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a")
end end
def test_sort_numeric
assert_equal ['1', '2', '3', '10'], @filters.sort_numeric(['10', '3', '2', '1'])
assert_equal [{ "a" => '1' }, { "a" => '2' }, { "a" => '3' }, { "a" => '10' }],
@filters.sort_numeric([{ "a" => '10' }, { "a" => '3' }, { "a" => '1' }, { "a" => '2' }], "a")
end
def test_sort_with_nils def test_sort_with_nils
assert_equal [1, 2, 3, 4, nil], @filters.sort([nil, 4, 3, 2, 1]) assert_equal [1, 2, 3, 4, nil], @filters.sort([nil, 4, 3, 2, 1])
assert_equal [{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }, {}], @filters.sort([{ "a" => 4 }, { "a" => 3 }, {}, { "a" => 1 }, { "a" => 2 }], "a") assert_equal [{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }, {}], @filters.sort([{ "a" => 4 }, { "a" => 3 }, {}, { "a" => 1 }, { "a" => 2 }], "a")
@@ -201,14 +215,14 @@ class StandardFiltersTest < Minitest::Test
{ "handle" => "beta" }, { "handle" => "beta" },
{ "price" => 1, "handle" => "gamma" }, { "price" => 1, "handle" => "gamma" },
{ "handle" => "delta" }, { "handle" => "delta" },
{ "price" => 2, "handle" => "epsilon" } { "price" => 2, "handle" => "epsilon" },
] ]
expectation = [ expectation = [
{ "price" => 1, "handle" => "gamma" }, { "price" => 1, "handle" => "gamma" },
{ "price" => 2, "handle" => "epsilon" }, { "price" => 2, "handle" => "epsilon" },
{ "price" => 4, "handle" => "alpha" }, { "price" => 4, "handle" => "alpha" },
{ "handle" => "delta" }, { "handle" => "delta" },
{ "handle" => "beta" } { "handle" => "beta" },
] ]
assert_equal expectation, @filters.sort(input, "price") assert_equal expectation, @filters.sort(input, "price")
end end
@@ -229,14 +243,14 @@ class StandardFiltersTest < Minitest::Test
{ "handle" => "beta" }, { "handle" => "beta" },
{ "price" => "1", "handle" => "gamma" }, { "price" => "1", "handle" => "gamma" },
{ "handle" => "delta" }, { "handle" => "delta" },
{ "price" => 2, "handle" => "epsilon" } { "price" => 2, "handle" => "epsilon" },
] ]
expectation = [ expectation = [
{ "price" => "1", "handle" => "gamma" }, { "price" => "1", "handle" => "gamma" },
{ "price" => 2, "handle" => "epsilon" }, { "price" => 2, "handle" => "epsilon" },
{ "price" => "4", "handle" => "alpha" }, { "price" => "4", "handle" => "alpha" },
{ "handle" => "delta" }, { "handle" => "delta" },
{ "handle" => "beta" } { "handle" => "beta" },
] ]
assert_equal expectation, @filters.sort_natural(input, "price") assert_equal expectation, @filters.sort_natural(input, "price")
end end
@@ -249,7 +263,7 @@ class StandardFiltersTest < Minitest::Test
{ "fake" => "t" }, { "fake" => "t" },
{ "key" => "a" }, { "key" => "a" },
{ "key" => "b" }, { "key" => "b" },
{ "key" => "c" } { "key" => "c" },
] ]
expectation = [ expectation = [
{ "key" => "a" }, { "key" => "a" },
@@ -258,7 +272,7 @@ class StandardFiltersTest < Minitest::Test
{ "key" => "X" }, { "key" => "X" },
{ "key" => "Y" }, { "key" => "Y" },
{ "key" => "Z" }, { "key" => "Z" },
{ "fake" => "t" } { "fake" => "t" },
] ]
assert_equal expectation, @filters.sort_natural(input, "key") assert_equal expectation, @filters.sort_natural(input, "key")
assert_equal ["a", "b", "c", "X", "Y", "Z"], @filters.sort_natural(["X", "Y", "Z", "a", "b", "c"]) assert_equal ["a", "b", "c", "X", "Y", "Z"], @filters.sort_natural(["X", "Y", "Z", "a", "b", "c"])
@@ -268,12 +282,40 @@ class StandardFiltersTest < Minitest::Test
assert_equal [], @filters.sort([], "a") assert_equal [], @filters.sort([], "a")
end end
def test_sort_invalid_property
foo = [
[1],
[2],
[3],
]
assert_raises Liquid::ArgumentError do
@filters.sort(foo, "bar")
end
end
def test_sort_natural_empty_array def test_sort_natural_empty_array
assert_equal [], @filters.sort_natural([], "a") assert_equal [], @filters.sort_natural([], "a")
end end
def test_sort_numeric_empty_array
assert_equal [], @filters.sort_numeric([], "a")
end
def test_sort_natural_invalid_property
foo = [
[1],
[2],
[3],
]
assert_raises Liquid::ArgumentError do
@filters.sort_natural(foo, "bar")
end
end
def test_legacy_sort_hash def test_legacy_sort_hash
assert_equal [{ a: 1, b: 2 }], @filters.sort({ a: 1, b: 2 }) assert_equal [{ a: 1, b: 2 }], @filters.sort(a: 1, b: 2)
end end
def test_numerical_vs_lexicographical_sort def test_numerical_vs_lexicographical_sort
@@ -295,10 +337,34 @@ class StandardFiltersTest < Minitest::Test
assert_equal [], @filters.uniq([], "a") assert_equal [], @filters.uniq([], "a")
end end
def test_uniq_invalid_property
foo = [
[1],
[2],
[3],
]
assert_raises Liquid::ArgumentError do
@filters.uniq(foo, "bar")
end
end
def test_compact_empty_array def test_compact_empty_array
assert_equal [], @filters.compact([], "a") assert_equal [], @filters.compact([], "a")
end end
def test_compact_invalid_property
foo = [
[1],
[2],
[3],
]
assert_raises Liquid::ArgumentError do
@filters.compact(foo, "bar")
end
end
def test_reverse def test_reverse
assert_equal [4, 3, 2, 1], @filters.reverse([1, 2, 3, 4]) assert_equal [4, 3, 2, 1], @filters.reverse([1, 2, 3, 4])
end end
@@ -325,7 +391,7 @@ class StandardFiltersTest < Minitest::Test
def test_map_on_hashes def test_map_on_hashes
assert_template_result "4217", '{{ thing | map: "foo" | map: "bar" }}', assert_template_result "4217", '{{ thing | map: "foo" | map: "bar" }}',
"thing" => { "foo" => [ { "bar" => 42 }, { "bar" => 17 } ] } "thing" => { "foo" => [{ "bar" => 42 }, { "bar" => 17 }] }
end end
def test_legacy_map_on_hashes_with_dynamic_key def test_legacy_map_on_hashes_with_dynamic_key
@@ -342,7 +408,7 @@ class StandardFiltersTest < Minitest::Test
def test_map_over_proc def test_map_over_proc
drop = TestDrop.new drop = TestDrop.new
p = proc{ drop } p = proc { drop }
templ = '{{ procs | map: "test" }}' templ = '{{ procs | map: "test" }}'
assert_template_result "testfoo", templ, "procs" => [p] assert_template_result "testfoo", templ, "procs" => [p]
end end
@@ -350,10 +416,10 @@ class StandardFiltersTest < Minitest::Test
def test_map_over_drops_returning_procs def test_map_over_drops_returning_procs
drops = [ drops = [
{ {
"proc" => ->{ "foo" }, "proc" => -> { "foo" },
}, },
{ {
"proc" => ->{ "bar" }, "proc" => -> { "bar" },
}, },
] ]
templ = '{{ drops | map: "proc" }}' templ = '{{ drops | map: "proc" }}'
@@ -364,6 +430,29 @@ class StandardFiltersTest < Minitest::Test
assert_template_result "123", '{{ foo | map: "foo" }}', "foo" => TestEnumerable.new assert_template_result "123", '{{ foo | map: "foo" }}', "foo" => TestEnumerable.new
end end
def test_map_returns_empty_on_2d_input_array
foo = [
[1],
[2],
[3],
]
assert_raises Liquid::ArgumentError do
@filters.map(foo, "bar")
end
end
def test_map_returns_empty_with_no_property
foo = [
[1],
[2],
[3],
]
assert_raises Liquid::ArgumentError do
@filters.map(foo, nil)
end
end
def test_sort_works_on_enumerables def test_sort_works_on_enumerables
assert_template_result "213", '{{ foo | sort: "bar" | map: "foo" }}', "foo" => TestEnumerable.new assert_template_result "213", '{{ foo | sort: "bar" | map: "foo" }}', "foo" => TestEnumerable.new
end end
@@ -394,9 +483,9 @@ class StandardFiltersTest < Minitest::Test
assert_equal '07/05/2006', @filters.date("2006-07-05 10:00:00", "%m/%d/%Y") assert_equal '07/05/2006', @filters.date("2006-07-05 10:00:00", "%m/%d/%Y")
assert_equal "07/16/2004", @filters.date("Fri Jul 16 01:00:00 2004", "%m/%d/%Y") assert_equal "07/16/2004", @filters.date("Fri Jul 16 01:00:00 2004", "%m/%d/%Y")
assert_equal "#{Date.today.year}", @filters.date('now', '%Y') assert_equal Date.today.year.to_s, @filters.date('now', '%Y')
assert_equal "#{Date.today.year}", @filters.date('today', '%Y') assert_equal Date.today.year.to_s, @filters.date('today', '%Y')
assert_equal "#{Date.today.year}", @filters.date('Today', '%Y') assert_equal Date.today.year.to_s, @filters.date('Today', '%Y')
assert_nil @filters.date(nil, "%B") assert_nil @filters.date(nil, "%B")
@@ -619,12 +708,12 @@ class StandardFiltersTest < Minitest::Test
{ "handle" => "alpha", "ok" => true }, { "handle" => "alpha", "ok" => true },
{ "handle" => "beta", "ok" => false }, { "handle" => "beta", "ok" => false },
{ "handle" => "gamma", "ok" => false }, { "handle" => "gamma", "ok" => false },
{ "handle" => "delta", "ok" => true } { "handle" => "delta", "ok" => true },
] ]
expectation = [ expectation = [
{ "handle" => "alpha", "ok" => true }, { "handle" => "alpha", "ok" => true },
{ "handle" => "delta", "ok" => true } { "handle" => "delta", "ok" => true },
] ]
assert_equal expectation, @filters.where(input, "ok", true) assert_equal expectation, @filters.where(input, "ok", true)
@@ -636,12 +725,12 @@ class StandardFiltersTest < Minitest::Test
{ "handle" => "alpha", "ok" => true }, { "handle" => "alpha", "ok" => true },
{ "handle" => "beta" }, { "handle" => "beta" },
{ "handle" => "gamma" }, { "handle" => "gamma" },
{ "handle" => "delta", "ok" => true } { "handle" => "delta", "ok" => true },
] ]
expectation = [ expectation = [
{ "handle" => "alpha", "ok" => true }, { "handle" => "alpha", "ok" => true },
{ "handle" => "delta", "ok" => true } { "handle" => "delta", "ok" => true },
] ]
assert_equal expectation, @filters.where(input, "ok", true) assert_equal expectation, @filters.where(input, "ok", true)
@@ -662,7 +751,7 @@ class StandardFiltersTest < Minitest::Test
input = [ input = [
{ "message" => "Bonjour!", "language" => "French" }, { "message" => "Bonjour!", "language" => "French" },
{ "message" => "Hello!", "language" => "English" }, { "message" => "Hello!", "language" => "English" },
{ "message" => "Hallo!", "language" => "German" } { "message" => "Hallo!", "language" => "German" },
] ]
assert_equal [{ "message" => "Bonjour!", "language" => "French" }], @filters.where(input, "language", "French") assert_equal [{ "message" => "Bonjour!", "language" => "French" }], @filters.where(input, "language", "French")
@@ -680,7 +769,7 @@ class StandardFiltersTest < Minitest::Test
{ "foo" => false }, { "foo" => false },
{ "foo" => true }, { "foo" => true },
{ "foo" => "for sure" }, { "foo" => "for sure" },
{ "bar" => true } { "bar" => true },
] ]
assert_equal [{ "foo" => true }, { "foo" => "for sure" }], @filters.where(input, "foo") assert_equal [{ "foo" => true }, { "foo" => "for sure" }], @filters.where(input, "foo")

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