Compare commits

...
Author SHA1 Message Date
Shaina Raskas 9eeaf89665 remove unnecessary tags 2022-03-16 16:13:02 -04:00
Pierre-Olivier BédardandShaina Raskas a77c37db0a Docs: YARD tags for some resources 2022-03-16 10:57:56 -04:00
Thierry JoyalandGitHub 7357dcf185 Merge pull request #1536 from Shopify/flaky-profiler-test-v2
Add artificial execution time in profiler tests
2022-03-07 10:06:13 -05:00
Thierry JoyalandGitHub 68c3827ef2 Merge pull request #1525 from Shopify/standardfilter/fix-missing-context-on-iterations
[StandardFilter] Fix missing @context on iterations
2022-03-07 09:19:18 -05:00
Thierry Joyal 4af38bc549 [StandardFilter] Fix missing @context on iterations 2022-03-07 09:17:07 -05:00
Thierry Joyal df241abf70 Add artificial execution time in profiler tests 2022-03-07 08:50:03 -05:00
10f8337209 Test Ruby 3.1 in CI (#1533)
Co-authored-by: Dylan Thacker-Smith <[email protected]>
2022-03-04 13:23:04 -05:00
Thierry JoyalandGitHub 5ed0410a8b Merge pull request #1534 from Shopify/context-test-cleanup
Context test cleanup
2022-03-04 11:31:00 -05:00
Thierry Joyal 0f5220c391 ContextTest: Classes to use appropriate ancestor 2022-03-04 09:14:57 -05:00
Thierry Joyal 7a23f46fab ContextTest: Cleanup global variable assignments 2022-03-04 09:10:38 -05:00
Peter ZhuandGitHub 3f7edf00b9 Merge pull request #1531 from Shopify/pz-array-fetch-warning
Fix warning about block and default value
2022-03-02 16:25:23 -05:00
Thierry JoyalandGitHub b4a2a79e26 Merge pull request #1527 from Shopify/condition/receive-mandatory-context-argument
Condition#evaluate to receive mandatory context argument
2022-03-02 15:04:24 -05:00
Thierry Joyal 1d2bee1f60 Condition#evaluate to receive mandatory context argument 2022-03-02 14:35:31 -05:00
Peter Zhu 01e6eec97a Fix warning about block and default value
Ruby's Array#fetch accepts either a default value or a block, but not
both. If both are passed in, then it uses the block and outputs this
warning:

```
lib/liquid/static_registers.rb:34: warning: block supersedes default value argument
```
2022-03-02 14:33:12 -05:00
Jean Boussier fbdab19358 We're in 2022... 2022-03-02 18:27:25 +01:00
Thierry JoyalandGitHub ce85ac5d3d Merge pull request #1529 from Shopify/tests/standard-filters-with-context
StandardFiltersTest: Initialize following production code paths with context
2022-03-02 08:44:16 -05:00
Thierry Joyal c0ffee16a3 StandardFiltersTest: Initialize following production code paths with context 2022-03-01 16:01:00 +00:00
Jean Boussier a7eb33fa39 Release 5.2.0 2022-03-01 16:18:49 +01:00
Jean byroot BoussierandGitHub 1a85e98793 Merge pull request #1524 from Shopify/global-constant-cache
Eagerly cache global filters
2022-03-01 16:14:31 +01:00
Jean Boussier c588337aac Eagerly cache global filters
Including a module can cause Ruby's global constant cache to be busted
if the included module contain constants. So that's something you don't
want to happen at "runtime", otherwise it will severely degrade performance
and if you are using YJIT or MJIT most of the compiled code will be invalidated.

To limit the impact of this, we can pre-include the global filters,
as they're generally registered during boot, that limits the problem
to non-global filters.
2022-03-01 13:40:40 +01:00
Dylan Thacker-SmithandGitHub 97f7922457 Add missing changelog entry for PR #1518 (#1521) 2022-02-24 14:46:26 -05:00
0d83e64cfe Add replace_last and remove_last filters (#1422)
Co-authored-by: ADTC <[email protected]>
Co-authored-by: Dylan Thacker-Smith <[email protected]>
2022-02-24 14:02:15 -05:00
Dylan Thacker-SmithandGitHub 0d5e01ae98 Fix some internal errors in filters from invalid input. (#1476)
These fixes came from improving the corresponding test, so these might not
actually be causing problems in practice.
2022-02-24 09:17:37 -05:00
Charles-Philippe ClermontandGitHub 15eaa49e48 Merge pull request #1518 from Shopify/fix/kwarg-key-name-liquid-c-inconsistency
Fix kwarg parsing inconsistency with Liquid::C
2022-02-14 13:22:12 -05:00
Tobias LütkeandGitHub 91c54c579d Merge pull request #1477 from Watson1978/performance
Increase parsing performance
2022-02-14 12:25:19 -05:00
Charles-P. Clermont 1310c4978d Fix kwarg parsing inconsistency with Liquid::C
Liquid::C parses liquid filter arguments with dashes in them, Liquid does not.

For tags that accept kwargs and dumps them on the HTML tag, this is an important feature.

e.g. {{ ... | image_tag: loading: 'lazy', data-something: 'value!' }}

Without this change, Liquid would incorrectly parse the
`data-something` kwarg as a single argument and would skip over the
invalid characters.

See https://github.com/Shopify/theme-check/issues/539 for more context
2022-02-11 15:10:07 -05:00
shainaraskasandGitHub 3de1db3c3a Merge pull request #1509 from Shopify/1508-shopify-docs-link
Fix Shopify documentation link
2022-01-20 12:06:16 -05:00
Shaina Raskas 03522caaf8 fix Shopify documentation link 2022-01-20 09:40:48 -05:00
Shaina Raskas 7acea2a9c9 Revert "fix Shopify documentation link"
This reverts commit d8ef698539.
2022-01-20 09:37:26 -05:00
Shaina Raskas d8ef698539 fix Shopify documentation link 2022-01-20 09:27:00 -05:00
Watson ebdfdb80e5 Detect quoted string using String#{start_with?, end_with?} to reduce Regexp#=== calling 2021-09-26 04:30:49 +09:00
Watson 95e9fa5010 Use String#=~ and Regexp.last_match instead to retrieve the markup content
If the first value is only used obtained with String#scan,
it will increase the performance if replace with `String#=~` and `Regexp.last_match`.

### Environment
- MacBook Air (M1, 2020)
- macOS 12.0 beta 7
- Apple M1
- Ruby 3.0.2

### Test code
```ruby
require 'benchmark/ips'

WhitespaceControl           = '-'
VariableStart               = /\{\{/
VariableEnd                 = /\}\}/

ContentOfVariable   = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
token = "{{item.product.featured_image | product_img_url: 'thumb' }}"

Benchmark.ips do |x|
  x.report("String#scan")  { token.scan(ContentOfVariable) {|content| break }  }
  x.report("String#match") { m = token.match(ContentOfVariable); m[1] }
  x.report("String#=~")    { token =~ ContentOfVariable; Regexp.last_match(1) }

  x.compare!
end
```

### Result
```
Warming up --------------------------------------
         String#scan   135.724k i/100ms
        String#match   117.397k i/100ms
           String#=~   151.637k i/100ms
Calculating -------------------------------------
         String#scan      1.351M (± 0.8%) i/s -      6.786M in   5.021955s
        String#match      1.169M (± 1.3%) i/s -      5.870M in   5.020429s
           String#=~      1.520M (± 0.9%) i/s -      7.733M in   5.087427s

Comparison:
           String#=~:  1520250.9 i/s
         String#scan:  1351399.0 i/s - 1.12x  (± 0.00) slower
        String#match:  1169384.1 i/s - 1.30x  (± 0.00) slower
```
2021-09-26 04:12:53 +09:00
Anders SøgaardandGitHub db3999a008 Improve where filter tests (#1472) 2021-09-16 10:02:39 -04:00
Marc-André CournoyerandGitHub 10e2aa8d5b Merge pull request #1471 from Shopify/bump-5.1.0
Prep for 5.1.0 release
2021-09-15 15:15:13 -04:00
7c4114671b Remove to_liquid_value fixes in changelog
Co-authored-by: Dylan Thacker-Smith <[email protected]>
2021-09-13 10:53:46 -04:00
Marc-André Cournoyer b01de9d325 Prep for 5.1.0 release 2021-09-09 14:28:45 -04:00
Zac ClayandGitHub a5369c26a8 Add missing quote in comment (#1468) 2021-09-09 14:23:11 -04:00
Charles-Philippe ClermontandGitHub a03de8f9ea Merge pull request #1470 from Shopify/fix/range-parse-tree-visitor
Add ParseTreeVisitor to RangeLookup
2021-09-09 11:39:14 -04:00
Charles-P. Clermont e86fe27259 Fix lint 2021-09-09 11:22:39 -04:00
Charles-P. Clermont c8906d05b9 Add ParseTreeVisitor to RangeLookup 2021-09-09 11:22:39 -04:00
Michael GoandGitHub 50d1a2ffc9 Merge pull request #1458 from Shopify/use-to-liquid-value-with-conditions
use Utils.to_liquid_value on conditionals
2021-06-15 12:18:14 -03:00
Michael Go f686c5dec7 use Utils.to_liquid_value on conditionals 2021-06-14 18:19:37 -03:00
Michael GoandGitHub aa8ce87b96 Merge pull request #1454 from Shopify/default-filter-with-to-liquid-value
utilize input's to_liquid_value on default filter
2021-06-14 15:43:56 -03:00
Peter ZhuandGitHub 698f5e0d96 Merge pull request #1456 from Shopify/pz-psych-4-unsafe-load
Fix benchmark for breaking change in Psych 4
2021-06-10 13:14:13 -04:00
Peter Zhu 996bfe0c82 Fix benchmark for breaking change in Psych 4
Psych 4 introduces a breaking change (ruby/psych#487) where
`Psych#load`/`Psych#load_file` now default to safe loading,
meaning that YAML references are not allowed anymore. This
commit changes the benchmark to use `Psych#unsafe_load_file`
when it's available.
2021-06-10 13:01:16 -04:00
Michael GoandGitHub be81c9ae5a Merge pull request #1457 from Shopify/fix-unless-to-use-to-liquid-value-helper
fix unless to use to_liquid_value helper with multiple conditions
2021-06-10 14:00:19 -03:00
Michael Go edd4d70aee fix unless to use to_liquid_value helper with multiple conditions 2021-06-10 13:28:17 -03:00
Michael Go ac66dbbafe utilize input's to_liquid_value on default filter 2021-06-08 15:20:53 -03:00
Daniel InsleyandGitHub 017c1b5e83 Base64 Decode & Encode Filters (#1450) 2021-06-03 13:23:11 -04:00
Michael GoandGitHub 250555c9a8 Merge pull request #1441 from Shopify/to-raw-value
Implement to_liquid_value to Liquid::Drop
2021-05-27 10:21:05 -03:00
Michael Go e361a4d53c introduce to_liquid_value on variable look and conditional statements 2021-05-26 17:27:18 -03:00
Dylan Thacker-Smith b9e0d28729 rubocop: Clarify that a config is a rubocop-shopify backport 2021-04-20 09:01:06 -07:00
Dylan Thacker-Smith 020f6b93c5 rubocop: Fix unsupported ruby version in TargetRubyVersion error 2021-04-20 09:00:20 -07:00
Dylan Thacker-SmithandGitHub cfe1637bdd Translate RangeError to Liquid::Error for truncatewords with large int (#1431) 2021-04-20 11:48:22 -04:00
Dylan Thacker-Smith eab13a07d9 Add changelog entry for a recent fix 2021-03-29 13:43:01 -07:00
Dylan Thacker-SmithandGitHub ca96ca0fef Fix support for using a String subclass for the liquid source (#1421) 2021-03-29 16:22:05 -04:00
Marc-André CournoyerandGitHub 4e7a953e73 Merge pull request #1417 from Shopify/prep-release
Bump to 5.0.1 and add changelog for release
2021-03-24 16:55:10 -04:00
6ac2499f7f Remove internal tokenizer fix from History.md
Co-authored-by: Dylan Thacker-Smith <[email protected]>
2021-03-24 16:53:45 -04:00
ff70161512 Remove internal fixes from History.md
Co-authored-by: Dylan Thacker-Smith <[email protected]>
2021-03-24 16:52:57 -04:00
Marc-André Cournoyer 026157e128 Bump to 5.0.1 and add changelog for release 2021-03-24 16:19:19 -04:00
Charles-Philippe ClermontandGitHub bf64239ea6 Merge pull request #1414 from Shopify/fix/echo-parse-tree-visitor
Add ParseTreeVisitor to Echo tag
2021-03-24 09:38:48 -04:00
Charles-P. Clermont c270a6f378 Add ParseTreeVisitor to Echo tag
This fixes theme-check#218, wherein variables used in echo tags are not
considered used by the linter. It is because our visitor doesn't see the
:variable_lookup's in the echo tag since the children array is empty.
But this array is empty because it is swallowed by the @variable.
2021-03-24 09:35:43 -04:00
Dylan Thacker-SmithandGitHub 4fba61a802 Merge pull request #1402 from Shopify/rubocop-shopify
Use the rubocop-shopify gem and autocorrect
2021-03-16 17:39:09 -04:00
Dylan Thacker-SmithandGitHub 6b6baece25 Merge pull request #1406 from ADTC/patch-2
Add a line to Sign the CLA as the first step in Workflow
2021-02-26 09:03:40 -05:00
15b2d193ec Add a hint to sign CLA only if it's the first time
The CLA isn't signed per repository. They might have done so for another repository. In which case, they won't need to do that again.

Change attributed to @dylanahsmith

Co-authored-by: Dylan Thacker-Smith <[email protected]>
2021-02-26 16:31:16 +08:00
ADTCandGitHub fd712d134a Give info in the Contributing page about doc updates (#1405)
Please see #1399 for more info and discussion.
2021-02-25 09:31:18 -05:00
ADTCandGitHub 0c2db998cf Add a line to Sign the CLA as the first step in Workflow
If a pull request is created by someone who didn't sign the CLA yet, the tests will fail. It requires the contributor to submit a new PR or add an insignificant commit (like an empty commit) to force the tests to rerun on the same PR. If we gently nudge a new contributor to sign the CLA in advance, we can help them avoid the hassle.
2021-02-25 09:59:51 +08:00
Dylan Thacker-Smith 9dac68cce1 Use the rubocop-shopify gem 2021-02-21 12:22:03 -05:00
Dylan Thacker-Smith c50509b741 Commit rubocop remote configuration update 2021-02-21 12:02:55 -05:00
Dylan Thacker-Smith cd66572514 Rubocop autocorrections to prepare for rubocop config updates 2021-02-21 12:01:59 -05:00
Dylan Thacker-SmithandGitHub dcb5a67089 performance: Use split limit in truncatewords (#1361) 2021-02-19 13:11:35 -05:00
Justin LiandGitHub efe44a7e6a Merge pull request #1391 from Unending/replace_carriage_return
handle carriage return in newlines_to_br
2021-02-19 12:49:13 -05:00
Dylan Thacker-SmithandGitHub 8625e66453 CI: Test with ruby 3.0 as the latest ruby version (#1398) 2021-02-10 10:10:27 -05:00
Unending 3cae09b968 handle carriage return in newlines_to_br 2021-01-16 18:49:01 +01:00
Peter ZhuandGitHub 3c499d0241 Merge pull request #1387 from Shopify/pz-serialize-benchmark-refactor
Refactor render_layout method for serialization
2021-01-11 15:51:00 -05:00
Peter Zhu e71e53ffb5 Refactor render_layout method for serialization 2021-01-11 14:00:39 -05:00
Dylan Thacker-SmithandGitHub 260c863e23 Build the tokenizer through the parse context for liquid-c (#1386) 2021-01-07 14:51:41 -05:00
Marc-André CournoyerandGitHub 42b6c07cd0 Merge pull request #1384 from Shopify/allowed_push_host
Add allowed_push_host to gemspec
2021-01-06 10:18:16 -05:00
Marc-André Cournoyer c91a6827f2 Add allowed_push_host to gemspec
Required to push gem to rubygems.
2021-01-06 10:03:13 -05:00
Marc-André CournoyerandGitHub 5dbc3d5701 Merge pull request #1383 from Shopify/bump-version
Bump version to 5.0.0
2021-01-06 09:38:04 -05:00
Marc-André Cournoyer 22683cbd2a Bump version to 5.0.0
Bump major because of the numerous breaking changes.
2021-01-05 15:02:14 -05:00
Peter ZhuandGitHub abfab3bef2 Merge pull request #1380 from Shopify/pz-serialize-compat
Fixes for serialization
2021-01-05 14:10:54 -05:00
Peter Zhu 51e8d6234a Freeze blocks in reverse order 2020-12-16 15:18:34 -05:00
Peter Zhu 7ca2846d9c Move configure options to a method 2020-12-16 15:08:55 -05:00
Dylan Thacker-SmithandGitHub 7ba0fc7952 Merge pull request #1376 from Shopify/support-app-liquid-context
Use the same context class for isolated subcontexts for the require tag
2020-12-14 12:12:46 -05:00
Marc-André CournoyerandGitHub e2c86d137f Merge pull request #1377 from Shopify/update-history
Fix latest changelog
2020-12-13 12:27:15 -05:00
Marc-André Cournoyer 776a63b61d Update latest changelog to mention Ruby 2.5 req 2020-12-13 12:13:49 -05:00
Marc-André CournoyerandGitHub 84f9d6957c Merge pull request #1374 from Shopify/update-history
Update History.md with changes since last release
2020-12-11 17:05:33 -05:00
Marc-André Cournoyer 7d32728e16 Split latest changelog in Features, Fixes, Changes & Perf
Also remove duplicated entries
2020-12-11 16:35:19 -05:00
Dylan Thacker-Smith 40a9b72b3c Allow a block to finish context init before squashing instance assigns
Liquid::Context#squash_instance_assigns_with_environments can result in
Proc objects in the environment to be eagerly evaluated. So it should be
possible to finish initializing the context object before this is done.
Allowing a block to be used for this purpose avoids the need to add
additional parameters for this purpose.
2020-12-11 14:52:09 -05:00
Dylan Thacker-Smith 4ff26cd707 Use the same context class for isolated subcontexts for the require tag 2020-12-11 14:52:09 -05:00
Dylan Thacker-SmithandGitHub 462919a28f Merge pull request #1375 from Shopify/update-rubocop
Update rubocop and related configuration
2020-12-11 14:47:56 -05:00
Dylan Thacker-Smith f3e2be9f85 Update rubocop 2020-12-11 14:14:15 -05:00
Dylan Thacker-Smith 4d40f83457 Update .rubocop_todo.yml to ignore Lint/MissingSuper offenses in drops
I don't think we can rely on the application to call `super` in their
drop's initializers at the moment, so doing that consistently in liquid
would prevent this from being properly tested.
2020-12-11 14:14:15 -05:00
Dylan Thacker-Smith 00be1e4dd4 Update inherited rubocop shopify style guide configuration 2020-12-11 14:14:15 -05:00
Dylan Thacker-Smith f7d67b946e rubocop autocorrect Style/MethodCallWithArgsParentheses 2020-12-11 13:51:00 -05:00
Marc-André Cournoyer ae9aee896b Update History.md with changes since last release 2020-12-10 08:03:17 -05:00
Dylan Thacker-SmithandGitHub 6dec172743 Merge pull request #1366 from Shopify/profile-render-node
Create top-level profile timing nodes for multiple template renders
2020-12-09 10:07:31 -05:00
Dylan Thacker-Smith da581d988a Create top-level profile timing nodes for multiple template renders 2020-12-09 10:06:02 -05:00
Dylan Thacker-Smith 7960826552 Rename render_profiling_test.rb to profiler_test.rb
so it corresponds to the class name being tested
2020-12-09 10:06:02 -05:00
Dylan Thacker-SmithandGitHub 84059691b8 Merge pull request #1365 from Shopify/profiling-multiple-renders
Support using a profiler for multiple renders
2020-12-09 10:05:48 -05:00
Dylan Thacker-Smith 896288eff1 Move start of profiling to a Document#render_to_output_buffer patch 2020-12-09 10:04:34 -05:00
Dylan Thacker-Smith b3f132efd1 Fix total_render_time if a Profiler gets used for multiple renders 2020-12-09 10:04:34 -05:00
Dylan Thacker-SmithandGitHub 60214b957c Store the profiler in the context instead of a thread-local variable (#1364) 2020-12-09 10:04:20 -05:00
Dylan Thacker-SmithandGitHub 7361220af6 Merge pull request #1363 from Shopify/profiler-context-template-name
Fix template name in profile result for render tag timing objects
2020-12-09 10:03:42 -05:00
Dylan Thacker-Smith cb2ad71a31 Remove the Profiler#initialize argument which is effectively now unused
The @root_timing Timing object that it was used with never exposed that
name.
2020-12-09 10:01:16 -05:00
Dylan Thacker-Smith 900e3a6491 Fix template name in profile result for render tag timing objects 2020-12-09 10:01:16 -05:00
Dylan Thacker-SmithandGitHub f18084203d Use monotonic time to measure durations in Liquid::Profiler (#1362) 2020-12-09 10:00:44 -05:00
Peter ZhuandGitHub 3358a892f2 Merge pull request #1371 from Shopify/pz-revert-instrument
Revert instrumentation of end_tag_params and range_float
2020-12-01 15:24:52 -05:00
Peter Zhu bbfcaa2cc0 Revert "Merge pull request #1350 from Shopify/pz-instrument-invalid-end-tag"
This reverts commit e6eef4b2c4, reversing
changes made to c7c21e88f0.
2020-12-01 14:38:04 -05:00
Peter Zhu ba657871bc Revert "Merge pull request #1359 from Shopify/pz-instrument-range-floats"
This reverts commit 300adfd7ae, reversing
changes made to ed0aebcbc9.
2020-12-01 14:37:23 -05:00
Peter ZhuandGitHub 29d5d9674a Merge pull request #1370 from Shopify/pz-freeze-case-body
Freeze the body for case tags
2020-11-27 12:03:03 -05:00
Peter Zhu 0a645e72c1 Freeze the body for case 2020-11-27 11:29:17 -05:00
Dylan Thacker-SmithandGitHub 1850511334 Use an atomic subgroup in range regex to avoid pathological backtracking (#1360) 2020-11-16 10:29:36 -05:00
Peter ZhuandGitHub 300adfd7ae Merge pull request #1359 from Shopify/pz-instrument-range-floats
Instrument usage of floats in ranges
2020-11-13 16:08:47 -05:00
Peter Zhu f357662f37 Instrument floats in ranges 2020-11-13 16:05:29 -05:00
66 changed files with 1359 additions and 1499 deletions
+2 -3
View File
@@ -6,9 +6,8 @@ jobs:
strategy:
matrix:
entry:
- { ruby: 2.5, allowed-failure: false }
- { ruby: 2.6, allowed-failure: false }
- { ruby: 2.7, allowed-failure: false }
- { ruby: 2.5, allowed-failure: false } # minimum supported
- { ruby: 3.1, allowed-failure: false } # latest
- { ruby: ruby-head, allowed-failure: true }
name: test (${{ matrix.entry.ruby }})
steps:
File diff suppressed because it is too large Load Diff
+9 -2
View File
@@ -1,5 +1,7 @@
inherit_gem:
rubocop-shopify: rubocop.yml
inherit_from:
- 'https://shopify.github.io/ruby-style-guide/rubocop.yml'
- .rubocop_todo.yml
require: rubocop-performance
@@ -8,10 +10,15 @@ Performance:
Enabled: true
AllCops:
TargetRubyVersion: 2.4
TargetRubyVersion: 2.5
NewCops: disable
Exclude:
- 'vendor/bundle/**/*'
Naming/MethodName:
Exclude:
- 'example/server/liquid_servlet.rb'
# Backport https://github.com/Shopify/ruby-style-guide/pull/258
Layout/BeginEndAlignment:
Enabled: true
+17 -8
View File
@@ -1,6 +1,6 @@
# This configuration was generated by
# `rubocop --auto-gen-config`
# on 2019-09-11 06:34:25 +1000 using RuboCop version 0.74.0.
# on 2020-12-11 18:53:41 UTC using RuboCop version 1.6.1.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
@@ -14,14 +14,26 @@ Lint/InheritException:
Exclude:
- 'lib/liquid/interrupts.rb'
# Offense count: 98
# Offense count: 113
# Cop supports --auto-correct.
# Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
# URISchemes: http, https
Layout/LineLength:
Max: 294
Max: 260
# Offense count: 44
# Offense count: 8
Lint/MissingSuper:
Exclude:
- 'lib/liquid/forloop_drop.rb'
- 'lib/liquid/tablerowloop_drop.rb'
- 'test/integration/assign_test.rb'
- 'test/integration/context_test.rb'
- 'test/integration/filter_test.rb'
- 'test/integration/standard_filter_test.rb'
- 'test/integration/tags/for_tag_test.rb'
- 'test/integration/tags/table_row_test.rb'
# Offense count: 43
Naming/ConstantName:
Exclude:
- 'lib/liquid.rb'
@@ -32,7 +44,6 @@ Naming/ConstantName:
- 'lib/liquid/tags/cycle.rb'
- 'lib/liquid/tags/for.rb'
- 'lib/liquid/tags/if.rb'
- 'lib/liquid/tags/include.rb'
- 'lib/liquid/tags/raw.rb'
- 'lib/liquid/tags/table_row.rb'
- 'lib/liquid/variable.rb'
@@ -40,9 +51,7 @@ Naming/ConstantName:
- 'performance/shopify/paginate.rb'
- 'test/integration/tags/include_tag_test.rb'
# Offense count: 5
# Offense count: 2
Style/ClassVars:
Exclude:
- 'lib/liquid/condition.rb'
- 'lib/liquid/strainer.rb'
- 'lib/liquid/template.rb'
+4 -1
View File
@@ -5,6 +5,7 @@
* Bugfixes
* Performance improvements
* Features that are likely to be useful to the majority of Liquid users
* Documentation updates that are concise and likely to be useful to the majority of Liquid users
## Things we won't merge
@@ -14,12 +15,14 @@
* Features that can easily be implemented on top of Liquid (for example as a custom filter or custom filesystem)
* Code that does not include tests
* Code that breaks existing tests
* Documentation changes that are verbose, incorrect or not important to most people (we want to keep it simple and easy to understand)
## Workflow
* [Sign the CLA](https://cla.shopify.com/) if you haven't already
* Fork the Liquid repository
* Create a new branch in your fork
* For updating [Liquid documentation](https://shopify.github.io/liquid/), create it from `gh-pages` branch. (You can skip tests.)
* If it makes sense, add tests for your code and/or run a performance benchmark
* Make sure all tests pass (`bundle exec rake`)
* Create a pull request
+2 -1
View File
@@ -18,7 +18,8 @@ group :benchmark, :test do
end
group :test do
gem 'rubocop', '~> 0.78.0', require: false
gem 'rubocop', '~> 1.4', require: false
gem 'rubocop-shopify', '~> 1.0.7', require: false
gem 'rubocop-performance', require: false
platform :mri, :truffleruby do
+69 -3
View File
@@ -1,10 +1,76 @@
# Liquid Change Log
### Unreleased
## 5.3.0 (unreleased)
* Split Strainer class as a factory and a template (#1208) [Thierry Joyal]
### Fixes
* StandardFilter: Fix missing @context on iterations (#1525) [Thierry Joyal]
### Deprecation
* Condition#evaluate to require mandatory context argument in Liquid 6.0.0 (#1527) [Thierry Joyal]
## 5.2.0 2022-03-01
### Features
* Add `remove_last`, and `replace_last` filters (#1422) [Anders Hagbard]
* Eagerly cache global filters (#1524) [Jean Boussier]
### Fixes
* Fix some internal errors in filters from invalid input (#1476) [Dylan Thacker-Smith]
* Allow dash in filter kwarg name for consistency with Liquid::C (#1518) [CP Clermont]
## 5.1.0 / 2021-09-09
### Features
* Add `base64_encode`, `base64_decode`, `base64_url_safe_encode`, and `base64_url_safe_decode` filters (#1450) [Daniel Insley]
* Introduce `to_liquid_value` in `Liquid::Drop` (#1441) [Michael Go]
### Fixes
* Fix support for using a String subclass for the liquid source (#1421) [Dylan Thacker-Smith]
* Add `ParseTreeVisitor` to `RangeLookup` (#1470) [CP Clermont]
* Translate `RangeError` to `Liquid::Error` for `truncatewords` with large int (#1431) [Dylan Thacker-Smith]
## 5.0.1 / 2021-03-24
### Fixes
* Add ParseTreeVisitor to Echo tag (#1414) [CP Clermont]
* Test with ruby 3.0 as the latest ruby version (#1398) [Dylan Thacker-Smith]
* Handle carriage return in newlines_to_br (#1391) [Unending]
### Performance Improvements
* Use split limit in truncatewords (#1361) [Dylan Thacker-Smith]
## 5.0.0 / 2021-01-06
### Features
* Add new `{% render %}` tag (#1122) [Samuel Doiron]
* Add support for `as` in `{% render %}` and `{% include %}` (#1181) [Mike Angell]
* Add `{% liquid %}` and `{% echo %}` tags (#1086) [Justin Li]
* Add [usage tracking](README.md#usage-tracking) [Mike Angell]
* Add `Tag.disable_tags` for disabling tags that prepend `Tag::Disableable` at render time (#1162, #1274, #1275) [Mike Angell]
* Support using a profiler for multiple renders (#1365, #1366) [Dylan Thacker-Smith]
### Fixes
* Fix catastrophic backtracking in `RANGES_REGEX` regular expression (#1357) [Dylan Thacker-Smith]
* Make sure the for tag's limit and offset are integers (#1094) [David Cornu]
* Invokable methods for enumerable reject include (#1151) [Thierry Joyal]
* Allow `default` filter to handle `false` as value (#1144) [Mike Angell]
* Fix render length resource limit so it doesn't multiply nested output (#1285) [Dylan Thacker-Smith]
* Fix duplication of text in raw tags (#1304) [Peter Zhu]
* Fix strict parsing of find variable with a name expression (#1317) [Dylan Thacker-Smith]
* Use monotonic time to measure durations in Liquid::Profiler (#1362) [Dylan Thacker-Smith]
### Breaking Changes
* Require Ruby >= 2.5 (#1131, #1310) [Mike Angell, Dylan Thacker-Smith]
* Remove support for taint checking (#1268) [Dylan Thacker-Smith]
* Split Strainer class into StrainerFactory and StrainerTemplate (#1208) [Thierry Joyal]
* Remove handling of a nil context in the Strainer class (#1218) [Thierry Joyal]
* StaticRegisters#fetch to raise on missing key (#1250) [Thierry Joyal]
* Handle `BlockBody#blank?` at parse time (#1287) [Dylan Thacker-Smith]
* Pass the tag markup and tokenizer to `Document#unknown_tag` (#1290) [Dylan Thacker-Smith]
* And several internal changes
### Performance Improvements
* Reduce allocations (#1073, #1091, #1115, #1099, #1117, #1141, #1322, #1341) [Richard Monette, Florian Weingarten, Ashwin Maroli]
* Improve resources limits performance (#1093, #1323) [Florian Weingarten, Dylan Thacker-Smith]
## 4.0.3 / 2019-03-12
+2 -2
View File
@@ -5,7 +5,7 @@
* [Contributing guidelines](CONTRIBUTING.md)
* [Version history](History.md)
* [Liquid documentation from Shopify](http://docs.shopify.com/themes/liquid-basics)
* [Liquid documentation from Shopify](https://shopify.dev/api/liquid)
* [Liquid Wiki at GitHub](https://github.com/Shopify/liquid/wiki)
* [Website](http://liquidmarkup.org/)
@@ -56,7 +56,7 @@ For standard use you can just pass it the content of a file and call render with
Setting the error mode of Liquid lets you specify how strictly you want your templates to be interpreted.
Normally the parser is very lax and will accept almost anything without error. Unfortunately this can make
it very hard to debug and can lead to unexpected behaviour.
it very hard to debug and can lead to unexpected behaviour.
Liquid also comes with a stricter parser that can be used when editing templates to give better error messages
when templates are invalid. You can enable this new parser like this:
+2 -2
View File
@@ -36,7 +36,7 @@ module Liquid
VariableIncompleteEnd = /\}\}?/
QuotedString = /"[^"]*"|'[^']*'/
QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o
TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/o
TagAttributes = /(\w[\w-]*)\s*\:\s*(#{QuotedFragment})/o
AnyStartingTag = /#{TagStart}|#{VariableStart}/o
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om
@@ -59,8 +59,8 @@ require 'liquid/forloop_drop'
require 'liquid/extensions'
require 'liquid/errors'
require 'liquid/interrupts'
require 'liquid/strainer_factory'
require 'liquid/strainer_template'
require 'liquid/strainer_factory'
require 'liquid/expression'
require 'liquid/context'
require 'liquid/parser_switching'
-3
View File
@@ -77,9 +77,6 @@ module Liquid
body.parse(tokens, parse_context) do |end_tag_name, end_tag_params|
@blank &&= body.blank?
# Instrument for bug 1346
Usage.increment("end_tag_params") if end_tag_params && !end_tag_params.empty?
return false if end_tag_name == block_delimiter
raise_tag_never_closed(block_name) unless end_tag_name
+17 -3
View File
@@ -35,6 +35,18 @@ module Liquid
super
end
# @public_docs
# @type tag
# @category theme
# @title liquid
# @summary
# Allows you to write multiple tags within one set of delimiters.
# @description
# Use the [`echo`](/api/liquid/tags/theme-tags#echo) tag to output an expression within a `liquid` tag.
# @syntax
# {% liquid
# statement
# %}
private def parse_for_liquid_tag(tokenizer, parse_context)
while (token = tokenizer.shift)
unless token.empty? || token =~ WhitespaceOrNothing
@@ -99,7 +111,9 @@ module Liquid
end
private def parse_liquid_tag(markup, parse_context)
liquid_tag_tokenizer = Tokenizer.new(markup, line_number: parse_context.line_number, for_liquid_tag: true)
liquid_tag_tokenizer = parse_context.new_tokenizer(
markup, start_line_number: parse_context.line_number, for_liquid_tag: true
)
parse_for_liquid_tag(liquid_tag_tokenizer, parse_context) do |end_tag_name, _end_tag_markup|
if end_tag_name
BlockBody.unknown_tag_in_liquid_tag(end_tag_name, parse_context)
@@ -229,8 +243,8 @@ module Liquid
end
def create_variable(token, parse_context)
token.scan(ContentOfVariable) do |content|
markup = content.first
if token =~ ContentOfVariable
markup = Regexp.last_match(1)
return Variable.new(markup, parse_context)
end
BlockBody.raise_missing_variable_terminator(token, parse_context)
+10 -4
View File
@@ -8,7 +8,7 @@ module Liquid
# c = Condition.new(1, '==', 1)
# c.evaluate #=> true
#
class Condition #:nodoc:
class Condition # :nodoc:
@@operators = {
'==' => ->(cond, left, right) { cond.send(:equal_variables, left, right) },
'!=' => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
@@ -61,7 +61,7 @@ module Liquid
@child_condition = nil
end
def evaluate(context = Context.new)
def evaluate(context = deprecated_default_context)
condition = self
result = nil
loop do
@@ -134,8 +134,8 @@ module Liquid
# return this as the result.
return context.evaluate(left) if op.nil?
left = context.evaluate(left)
right = context.evaluate(right)
left = Liquid::Utils.to_liquid_value(context.evaluate(left))
right = Liquid::Utils.to_liquid_value(context.evaluate(right))
operation = self.class.operators[op] || raise(Liquid::ArgumentError, "Unknown operator #{op}")
@@ -150,6 +150,12 @@ module Liquid
end
end
def deprecated_default_context
warn("DEPRECATION WARNING: Condition#evaluate without a context argument is deprecated" \
" and will be removed from Liquid 6.0.0.")
Context.new
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[
+6 -4
View File
@@ -18,8 +18,8 @@ module Liquid
attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters
# rubocop:disable Metrics/ParameterLists
def self.build(environments: {}, outer_scope: {}, registers: {}, rethrow_errors: false, resource_limits: nil, static_environments: {})
new(environments, outer_scope, registers, rethrow_errors, resource_limits, static_environments)
def self.build(environments: {}, outer_scope: {}, registers: {}, rethrow_errors: false, resource_limits: nil, static_environments: {}, &block)
new(environments, outer_scope, registers, rethrow_errors, resource_limits, static_environments, &block)
end
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil, static_environments = {})
@@ -44,6 +44,8 @@ module Liquid
self.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA
end
yield self if block_given?
# Do this last, since it could result in this object being passed to a Proc in the environment
squash_instance_assigns_with_environments
end
@@ -122,7 +124,7 @@ module Liquid
# context['var'] = 'hi'
# end
#
# context['var] #=> nil
# context['var'] #=> nil
def stack(new_scope = {})
push(new_scope)
yield
@@ -135,7 +137,7 @@ module Liquid
def new_isolated_subcontext
check_overflow
Context.build(
self.class.build(
resource_limits: resource_limits,
static_environments: static_environments,
registers: StaticRegisters.new(registers)
+1 -1
View File
@@ -42,7 +42,7 @@ module Liquid
end
def render(context)
@body.render(context)
render_to_output_buffer(context, +'')
end
private
+14 -10
View File
@@ -10,18 +10,23 @@ module Liquid
'empty' => ''
}.freeze
SINGLE_QUOTED_STRING = /\A\s*'(.*)'\s*\z/m
DOUBLE_QUOTED_STRING = /\A\s*"(.*)"\s*\z/m
INTEGERS_REGEX = /\A\s*(-?\d+)\s*\z/
FLOATS_REGEX = /\A\s*(-?\d[\d\.]+)\s*\z/
RANGES_REGEX = /\A\s*\(\s*(\S+)\s*\.\.\s*(\S+)\s*\)\s*\z/
INTEGERS_REGEX = /\A(-?\d+)\z/
FLOATS_REGEX = /\A(-?\d[\d\.]+)\z/
# Use an atomic group (?>...) to avoid pathological backtracing from
# malicious input as described in https://github.com/Shopify/liquid/issues/1357
RANGES_REGEX = /\A\(\s*(?>(\S+)\s*\.\.)\s*(\S+)\s*\)\z/
def self.parse(markup)
return nil unless markup
markup = markup.strip
if (markup.start_with?('"') && markup.end_with?('"')) ||
(markup.start_with?("'") && markup.end_with?("'"))
return markup[1..-2]
end
case markup
when nil
nil
when SINGLE_QUOTED_STRING, DOUBLE_QUOTED_STRING
Regexp.last_match(1)
when INTEGERS_REGEX
Regexp.last_match(1).to_i
when RANGES_REGEX
@@ -29,7 +34,6 @@ module Liquid
when FLOATS_REGEX
Regexp.last_match(1).to_f
else
markup = markup.strip
if LITERALS.key?(markup)
LITERALS[markup]
else
+4
View File
@@ -23,6 +23,10 @@ module Liquid
Liquid::BlockBody.new
end
def new_tokenizer(markup, start_line_number: nil, for_liquid_tag: false)
Tokenizer.new(markup, line_number: start_line_number, for_liquid_tag: for_liquid_tag)
end
def parse_expression(markup)
Expression.parse(markup)
end
+61 -93
View File
@@ -25,7 +25,7 @@ module Liquid
# node.code
#
# # Which template and line number of this node.
# # If top level, this will be "<root>".
# # The top-level template name is `nil` by default, but can be set in the Liquid::Context before rendering.
# node.partial
# node.line_number
#
@@ -46,126 +46,94 @@ module Liquid
include Enumerable
class Timing
attr_reader :code, :partial, :line_number, :children, :total_time, :self_time
attr_reader :code, :template_name, :line_number, :children
attr_accessor :total_time
alias_method :render_time, :total_time
alias_method :partial, :template_name
def initialize(node, partial)
@code = node.respond_to?(:raw) ? node.raw : node
@partial = partial
@line_number = node.respond_to?(:line_number) ? node.line_number : nil
@children = []
def initialize(code: nil, template_name: nil, line_number: nil)
@code = code
@template_name = template_name
@line_number = line_number
@children = []
end
def self.start(node, partial)
new(node, partial).tap(&:start)
end
def start
@start_time = Time.now
end
def finish
@end_time = Time.now
@total_time = @end_time - @start_time
if @children.empty?
@self_time = @total_time
else
total_children_time = 0
def self_time
@self_time ||= begin
total_children_time = 0.0
@children.each do |child|
total_children_time += child.total_time
end
@self_time = @total_time - total_children_time
@total_time - total_children_time
end
end
end
def render_time
@end_time - @start_time
attr_reader :total_time
alias_method :total_render_time, :total_time
def initialize
@root_children = []
@current_children = nil
@total_time = 0.0
end
def profile(template_name, &block)
# nested renders are done from a tag that already has a timing node
return yield if @current_children
root_children = @root_children
render_idx = root_children.length
begin
@current_children = root_children
profile_node(template_name, &block)
ensure
@current_children = nil
if (timing = root_children[render_idx])
@total_time += timing.total_time
end
end
end
def self.profile_node_render(node)
if Profiler.current_profile && node.respond_to?(:render)
Profiler.current_profile.start_node(node)
output = yield
Profiler.current_profile.end_node(node)
output
def children
children = @root_children
if children.length == 1
children.first.children
else
yield
children
end
end
def self.profile_children(template_name)
if Profiler.current_profile
Profiler.current_profile.push_partial(template_name)
output = yield
Profiler.current_profile.pop_partial
output
else
yield
end
end
def self.current_profile
Thread.current[:liquid_profiler]
end
def initialize(partial_name = "<root>")
@partial_stack = [partial_name]
@root_timing = Timing.new("", current_partial)
@timing_stack = [@root_timing]
@render_start_at = Time.now
@render_end_at = @render_start_at
end
def start
Thread.current[:liquid_profiler] = self
@render_start_at = Time.now
end
def stop
Thread.current[:liquid_profiler] = nil
@render_end_at = Time.now
end
def total_render_time
@render_end_at - @render_start_at
end
def each(&block)
@root_timing.children.each(&block)
children.each(&block)
end
def [](idx)
@root_timing.children[idx]
children[idx]
end
def length
@root_timing.children.length
children.length
end
def start_node(node)
@timing_stack.push(Timing.start(node, current_partial))
def profile_node(template_name, code: nil, line_number: nil)
timing = Timing.new(code: code, template_name: template_name, line_number: line_number)
parent_children = @current_children
start_time = monotonic_time
begin
@current_children = timing.children
yield
ensure
@current_children = parent_children
timing.total_time = monotonic_time - start_time
parent_children << timing
end
end
def end_node(_node)
timing = @timing_stack.pop
timing.finish
private
@timing_stack.last.children << timing
end
def current_partial
@partial_stack.last
end
def push_partial(partial_name)
@partial_stack.push(partial_name)
end
def pop_partial
@partial_stack.pop
def monotonic_time
Process.clock_gettime(Process::CLOCK_MONOTONIC)
end
end
end
+20 -6
View File
@@ -3,19 +3,33 @@
module Liquid
module BlockBodyProfilingHook
def render_node(context, output, node)
Profiler.profile_node_render(node) do
if (profiler = context.profiler)
profiler.profile_node(context.template_name, code: node.raw, line_number: node.line_number) do
super
end
else
super
end
end
end
BlockBody.prepend(BlockBodyProfilingHook)
module IncludeProfilingHook
module DocumentProfilingHook
def render_to_output_buffer(context, output)
Profiler.profile_children(context.evaluate(@template_name_expr).to_s) do
super
end
return super unless context.profiler
context.profiler.profile(context.template_name) { super }
end
end
Include.prepend(IncludeProfilingHook)
Document.prepend(DocumentProfilingHook)
module ContextProfilingHook
attr_accessor :profiler
def new_isolated_subcontext
new_context = super
new_context.profiler = profiler
new_context
end
end
Context.prepend(ContextProfilingHook)
end
+8
View File
@@ -12,6 +12,8 @@ module Liquid
end
end
attr_reader :start_obj, :end_obj
def initialize(start_obj, end_obj)
@start_obj = start_obj
@end_obj = end_obj
@@ -35,5 +37,11 @@ module Liquid
Utils.to_integer(input)
end
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[@node.start_obj, @node.end_obj]
end
end
end
end
+393 -53
View File
@@ -1,10 +1,12 @@
# frozen_string_literal: true
require 'cgi'
require 'base64'
require 'bigdecimal'
module Liquid
module StandardFilters
MAX_INT = (1 << 31) - 1
HTML_ESCAPE = {
'&' => '&amp;',
'>' => '&gt;',
@@ -26,20 +28,47 @@ module Liquid
end
# convert an input string to DOWNCASE
#
# @public_docs
# @type filter
# @summary Converts a string into lowercase.
# @category string
# @syntax {{ string | downcase }}
# @return string
def downcase(input)
input.to_s.downcase
end
# convert an input string to UPCASE
#
# @public_docs
# @type filter
# @summary Converts a string into uppercase.
# @category string
# @syntax {{ string | upcase }}
# @return string
def upcase(input)
input.to_s.upcase
end
# capitalize words in the input centence
# capitalize words in the input sentence
#
# @public_docs
# @type filter
# @summary Capitalizes the first word in a string.
# @category string
# @syntax {{ string | capitalize }}
# @return string
def capitalize(input)
input.to_s.capitalize
end
# @public_docs
# @type filter
# @summary Escapes a string.
# @category string
# @syntax {{ string | escape }}
# @return string
def escape(input)
CGI.escapeHTML(input.to_s) unless input.nil?
end
@@ -62,6 +91,53 @@ module Liquid
result
end
# @public_docs
# @type filter
# @title base64_encode
# @summary Encodes a string into Base64.
# @category string
# @syntax {{ string | base64_encode }}
# @return string
def base64_encode(input)
Base64.strict_encode64(input.to_s)
end
# @public_docs
# @type filter
# @summary Decodes a string from Base64.
# @category string
# @syntax {{ string | base64_decode }}
# @return string
def base64_decode(input)
Base64.strict_decode64(input.to_s)
rescue ::ArgumentError
raise Liquid::ArgumentError, "invalid base64 provided to base64_decode"
end
# @public_docs
# @type filter
# @summary Encodes a string into URL-safe Base64
# @category string
# @syntax {{ string | base64_url_safe_encode }}
# @return string
# @description
# To produce URL-safe Base64, this filter uses `-`` and `_`` in place of `+`` and `/``.
def base64_url_safe_encode(input)
Base64.urlsafe_encode64(input.to_s)
end
# @public_docs
# @type filter
# @summary Decodes a string from URL-safe Base64.
# @category string
# @syntax {{ string | base64_url_safe_decode }}
# @return string
def base64_url_safe_decode(input)
Base64.urlsafe_decode64(input.to_s)
rescue ::ArgumentError
raise Liquid::ArgumentError, "invalid base64 provided to base64_url_safe_decode"
end
def slice(input, offset, length = nil)
offset = Utils.to_integer(offset)
length = length ? Utils.to_integer(length) : 1
@@ -73,7 +149,34 @@ module Liquid
end
end
# Truncate a string down to x characters
# @public_docs
# @type filter
# @category string
# @summary
# Truncates a string down to a specified number of characters.
# @description
# By default, an ellipsis (`...`)
# is appended to the truncated string.
#
# > Tip:
# > The number of characters in both default and custom ellipses is included in the
# > character count for the truncated string. For example, if you want to truncate a string
# > down to ten characters and use the default ellipsis (`...`), then you should set the
# > character count parameter to `13`.
#
# ### Custom ellipsis
#
# The `truncate` filter accepts an optional parameter to specify a custom ellipsis to be
# appended to the truncated string.
#
# ### No ellipsis
#
# If you don't want an ellipsis appended to your truncated string, then you can set the
# ellipsis parameter to a blank string (`''`).
# @syntax {{ string | truncate: character_count, ellipsis }}
# @required_param character_count [number] The number of characters to include in the truncated string. Includes the characters in the ellipsis.
# @optional_param ellipsis [string] The format of the ellipsis appended to the truncated string. Default is `...`. Pass a blank string (`''`) to remove the ellipsis completely.
# @return string
def truncate(input, length = 50, truncate_string = "...")
return if input.nil?
input_str = input.to_s
@@ -87,15 +190,44 @@ module Liquid
input_str.length > length ? input_str[0...l].concat(truncate_string_str) : input_str
end
# @public_docs
# @type filter
# @category string
# @summary
# Truncates a string down to a specified number of words.
# @description
# By default, an ellipsis (`...`) is appended to the truncated string.
#
# ### Custom ellipsis
#
# The `truncate` filter accepts an optional parameter to specify a custom ellipsis to be
# appended to the truncated string.
#
# ### No ellipsis
#
# If you don't want an ellipsis appended to your truncated string, then you can set the
# ellipsis parameter to a blank string (`''`).
# @syntax {{ string | truncatewords: word_count, ellipsis }}
# @required_param word_count [number] The number of words to include in the truncated string. Includes the characters in the ellipsis.
# @optional_param ellipsis [string] The format of the ellipsis appended to the truncated string. Default is `...`. Pass a blank string (`''`) to remove the ellipsis completely.
# @return string
def truncatewords(input, words = 15, truncate_string = "...")
return if input.nil?
wordlist = input.to_s.split
words = Utils.to_integer(words)
input = input.to_s
words = Utils.to_integer(words)
words = 1 if words <= 0
l = words - 1
l = 0 if l < 0
wordlist = begin
input.split(" ", words + 1)
rescue RangeError
raise if words + 1 < MAX_INT
# e.g. integer #{words} too big to convert to `int'
raise Liquid::ArgumentError, "integer #{words} too big for truncatewords"
end
return input if wordlist.length <= words
wordlist.length > l ? wordlist[0..l].join(" ").concat(truncate_string.to_s) : input
wordlist.pop
wordlist.join(" ").concat(truncate_string.to_s)
end
# Split input string into an array of substrings separated by given pattern.
@@ -107,18 +239,46 @@ module Liquid
input.to_s.split(pattern.to_s)
end
# @public_docs
# @type filter
# @category string
# @summary
# Strips all whitespace, such as tabs, spaces, and newlines, from the left and right sides of a string.
# @syntax {{ string | strip }}
# @return string
def strip(input)
input.to_s.strip
end
# @public_docs
# @type filter
# @category string
# @summary
# Strips all whitespace, such as tabs, spaces, and newlines, from the left side of a string.
# @syntax {{ string | lstrip }}
# @return string
def lstrip(input)
input.to_s.lstrip
end
# @public_docs
# @type filter
# @category string
# @summary
# Strips all whitespace, such as tabs, spaces, and newlines, from the right side of a string.
# @syntax {{ string | rstrip }}
# @return string
def rstrip(input)
input.to_s.rstrip
end
# @public_docs
# @type filter
# @category string
# @summary
# Strips all HTML tags from a string.
# @syntax {{ string | strip_html }}
# @return string
def strip_html(input)
empty = ''
result = input.to_s.gsub(STRIP_HTML_BLOCKS, empty)
@@ -126,18 +286,36 @@ module Liquid
result
end
# Remove all newlines from the string
# @public_docs
# @type filter
# @category string
# @summary
# Strips all line breaks and newlines from a string.
# @syntax {{ string | strip_newlines }}
# @return string
def strip_newlines(input)
input.to_s.gsub(/\r?\n/, '')
end
# Join elements of the array with certain character between them
# @public_docs
# @type filter
# @category array
# @summary Joins the elements in an array. The result is a single string.
# @syntax {{ array | join: ', ' }}
# @required_param delimiter [string] The separator to join the array elements with.
# @return string
def join(input, glue = ' ')
InputIterator.new(input, context).join(glue)
end
# Sort elements of the array
# provide optional property with which to sort an array of hashes or drops
# @public_docs
# @type filter
# @category array
# @summary Sorts the elements of an array.
# @description The order of the sorted array is case-sensitive.
# @syntax {{ array | sort: property }}
# @optional_param property [string] The property of the element to sort by.
# @return array
def sort(input, property = nil)
ary = InputIterator.new(input, context)
@@ -183,17 +361,23 @@ module Liquid
if ary.empty?
[]
elsif ary.first.respond_to?(:[]) && target_value.nil?
begin
ary.select { |item| item[property] }
elsif target_value.nil?
ary.select do |item|
item[property]
rescue TypeError
raise_property_error(property)
rescue NoMethodError
return nil unless item.respond_to?(:[])
raise
end
elsif ary.first.respond_to?(:[])
begin
ary.select { |item| item[property] == target_value }
else
ary.select do |item|
item[property] == target_value
rescue TypeError
raise_property_error(property)
rescue NoMethodError
return nil unless item.respond_to?(:[])
raise
end
end
end
@@ -207,22 +391,36 @@ module Liquid
ary.uniq
elsif ary.empty? # The next two cases assume a non-empty array.
[]
elsif ary.first.respond_to?(:[])
begin
ary.uniq { |a| a[property] }
else
ary.uniq do |item|
item[property]
rescue TypeError
raise_property_error(property)
rescue NoMethodError
return nil unless item.respond_to?(:[])
raise
end
end
end
# Reverse the elements of an array
# @public_docs
# @type tag
# @category array
# @summary Reverses the order of the items in an array.
# @syntax {{ array | reverse }}
# @return array
def reverse(input)
ary = InputIterator.new(input, context)
ary.reverse
end
# map/collect on a given property
# @public_docs
# @type tag
# @category array
# @summary Accepts an array element's property as a parameter and creates an array out of the property value for each array element.
# @required_param property [string] The property to extract from the element.
# @syntax {{ array | map: 'property' }}
# @return array
def map(input, property)
InputIterator.new(input, context).map do |e|
e = e.call if e.is_a?(Proc)
@@ -247,16 +445,26 @@ module Liquid
ary.compact
elsif ary.empty? # The next two cases assume a non-empty array.
[]
elsif ary.first.respond_to?(:[])
begin
ary.reject { |a| a[property].nil? }
else
ary.reject do |item|
item[property].nil?
rescue TypeError
raise_property_error(property)
rescue NoMethodError
return nil unless item.respond_to?(:[])
raise
end
end
end
# Replace occurrences of a string with another
# @public_docs
# @summary Replaces all occurrences of a string with a substring.
# @type filter
# @category string
# @syntax {{ string | replace: original_string, replacement_string }}
# @required_param original_string [string] The string to replace.
# @required_param replacement_string [string] The replacement string.
# @return string
def replace(input, string, replacement = '')
input.to_s.gsub(string.to_s, replacement.to_s)
end
@@ -266,21 +474,71 @@ module Liquid
input.to_s.sub(string.to_s, replacement.to_s)
end
# remove a substring
def remove(input, string)
input.to_s.gsub(string.to_s, '')
# Replace the last occurrences of a string with another
def replace_last(input, string, replacement)
input = input.to_s
string = string.to_s
replacement = replacement.to_s
start_index = input.rindex(string)
return input unless start_index
output = input.dup
output[start_index, string.length] = replacement
output
end
# remove the first occurrences of a substring
# @public_docs
# @summary Removes a substring from a string.
# @type filter
# @category string
# @syntax {{ string | remove: substring }}
# @required_param substring [string] The substring to remove from the string.
# @return string
def remove(input, string)
replace(input, string, '')
end
# @public_docs
# @summary Removes the first occurrences of a substring.
# @type filter
# @category string
# @syntax {{ string | remove_first: substring }}
# @required_param substring [string] The substring to remove from the string.
# @return string
def remove_first(input, string)
input.to_s.sub(string.to_s, '')
replace_first(input, string, '')
end
# remove the last occurences of a substring
def remove_last(input, string)
replace_last(input, string, '')
end
# add one string to another
#
# @public_docs
# @type filter
# @summary Appends characters to a string.
# @category string
# @syntax {{ string | append: to_append }}
# @required_param to_append [string] characters to append to the original string
# @return string
def append(input, string)
input.to_s + string.to_s
end
# @public_docs
# @type filter
# @category array
# @summary Concatenates (combines) an array with another array.
# @description
# The resulting array contains all the elements of the original arrays.
# `concat` won't remove duplicate entries from the concatenated array unless you also use the [`uniq`](/api/liquid/filters/array-filters#uniq) filter.
# @syntax {{ first_array | concat: second_array }}
# @required_param second_array [array] The array to concatenate with the primary array.
# @return array
def concat(input, array)
unless array.respond_to?(:to_ary)
raise ArgumentError, "concat filter requires an array argument"
@@ -288,14 +546,20 @@ module Liquid
InputIterator.new(input, context).concat(array)
end
# prepend a string to another
# @public_docs
# @summary Prepends a string to another string.
# @type filter
# @category string
# @syntax {{ string | prepend: additional_string }}
# @required_param additional_string [string] The string to prepend to the original string.
# @return string
def prepend(input, string)
string.to_s + input.to_s
end
# Add <br /> tags in front of all newlines in input string
def newline_to_br(input)
input.to_s.gsub(/\n/, "<br />\n")
input.to_s.gsub(/\r?\n/, "<br />\n")
end
# Reformat a date using Ruby's core Time#strftime( string ) -> string
@@ -337,58 +601,101 @@ module Liquid
date.strftime(format.to_s)
end
# Get the first element of the passed in array
#
# Example:
# {{ product.images | first | to_img }}
#
# @public_docs
# @type filter
# @category array
# @summary Returns the first element of an array.
# @syntax {{ array | first }}
def first(array)
array.first if array.respond_to?(:first)
end
# Get the last element of the passed in array
#
# Example:
# {{ product.images | last | to_img }}
#
# @public_docs
# @type filter
# @category array
# @summary Returns the last element of an array, or the last character inside of a string.
# @syntax {{ array | last }}
def last(array)
array.last if array.respond_to?(:last)
end
# absolute value
# @public_docs
# @syntax {{ number | abs }}
# @summary Returns the absolute value of a number.
# @type filter
# @category math
# @return number
# @description
# `abs` will also work on a string if the string only contains a number.
def abs(input)
result = Utils.to_number(input).abs
result.is_a?(BigDecimal) ? result.to_f : result
end
# addition
# @public_docs
# @summary Adds a number to an output.
# @type filter
# @category math
# @syntax {{ number | plus: number }}
# @required_param number [number] The number to add to the original number.
# @return number
def plus(input, operand)
apply_operation(input, operand, :+)
end
# subtraction
# @public_docs
# @summary Subtracts a number from an output.
# @type filter
# @category math
# @syntax {{ number | minus: number }}
# @return number
def minus(input, operand)
apply_operation(input, operand, :-)
end
# multiplication
# @public_docs
# @summary Multiplies an output by a number.
# @type filter
# @category math
# @syntax {{ number | times: number }}
# @required_param number [number] The number to multiply the original number by.
# @return number
def times(input, operand)
apply_operation(input, operand, :*)
end
# division
# @public_docs
# @summary Divides an output by a number. The output is rounded down to the nearest integer.
# @type filter
# @category math
# @syntax {{ number | divided_by: number }}
# @return number
def divided_by(input, operand)
apply_operation(input, operand, :/)
rescue ::ZeroDivisionError => e
raise Liquid::ZeroDivisionError, e.message
end
# @public_docs
# @summary Divides an output by a number and returns the remainder.
# @type filter
# @category math
# @syntax {{ number | modulo: number }}
# @required_param number [number] The number to divide the original number by.
# @return number
def modulo(input, operand)
apply_operation(input, operand, :%)
rescue ::ZeroDivisionError => e
raise Liquid::ZeroDivisionError, e.message
end
# @public_docs
# @summary Rounds the output to the nearest integer or specified number of decimals.
# @type filter
# @category math
# @syntax {{ number | round }}
# @optional_param decimals [number] The number of decimal places to round to.
# @return number
def round(input, n = 0)
result = Utils.to_number(input).round(Utils.to_number(n))
result = result.to_f if result.is_a?(BigDecimal)
@@ -398,18 +705,37 @@ module Liquid
raise Liquid::FloatDomainError, e.message
end
# @public_docs
# @summary Rounds an output up to the nearest integer.
# @type filter
# @category math
# @syntax {{ number | ceil }}
# @return number
def ceil(input)
Utils.to_number(input).ceil.to_i
rescue ::FloatDomainError => e
raise Liquid::FloatDomainError, e.message
end
# @public_docs
# @summary Rounds an output down to the nearest integer.
# @type filter
# @category math
# @syntax {{ number | floor }}
# @return number
def floor(input)
Utils.to_number(input).floor.to_i
rescue ::FloatDomainError => e
raise Liquid::FloatDomainError, e.message
end
# @public_docs
# @summary Limits a number to a minimum value.
# @type filter
# @category math
# @syntax {{ number | at_least: number }}
# @required_param number [number] The minimum valid value.
# @return number
def at_least(input, n)
min_value = Utils.to_number(n)
@@ -418,6 +744,13 @@ module Liquid
result.is_a?(BigDecimal) ? result.to_f : result
end
# @public_docs
# @summary Limits a number to a maximum value.
# @type filter
# @category math
# @syntax {{ number | at_most: number }}
# @required_param number [number] The maximum valid value.
# @return number
def at_most(input, n)
max_value = Utils.to_number(n)
@@ -438,7 +771,7 @@ module Liquid
#
def default(input, default_value = '', options = {})
options = {} unless options.is_a?(Hash)
false_check = options['allow_false'] ? input.nil? : !input
false_check = options['allow_false'] ? input.nil? : !Liquid::Utils.to_liquid_value(input)
false_check || (input.respond_to?(:empty?) && input.empty?) ? default_value : input
end
@@ -456,10 +789,16 @@ module Liquid
end
def nil_safe_compare(a, b)
if !a.nil? && !b.nil?
a <=> b
result = a <=> b
if result
result
elsif a.nil?
1
elsif b.nil?
-1
else
a.nil? ? 1 : -1
raise Liquid::ArgumentError, "cannot sort values of incompatible types"
end
end
@@ -514,8 +853,9 @@ module Liquid
def each
@input.each do |e|
e = e.respond_to?(:to_liquid) ? e.to_liquid : e
e.context = @context if e.respond_to?(:context=)
yield(e.respond_to?(:to_liquid) ? e.to_liquid : e)
yield(e)
end
end
end
+5 -1
View File
@@ -31,7 +31,11 @@ module Liquid
if @registers.key?(key)
@registers.fetch(key)
elsif default != UNDEFINED
@static.fetch(key, default, &block)
if block_given?
@static.fetch(key, &block)
else
@static.fetch(key, default)
end
else
@static.fetch(key, &block)
end
+11 -10
View File
@@ -7,25 +7,26 @@ module Liquid
def add_global_filter(filter)
strainer_class_cache.clear
global_filters << filter
GlobalCache.add_filter(filter)
end
def create(context, filters = [])
strainer_from_cache(filters).new(context)
end
GlobalCache = Class.new(StrainerTemplate)
private
def global_filters
@global_filters ||= []
end
def strainer_from_cache(filters)
strainer_class_cache[filters] ||= begin
klass = Class.new(StrainerTemplate)
global_filters.each { |f| klass.add_filter(f) }
filters.each { |f| klass.add_filter(f) }
klass
if filters.empty?
GlobalCache
else
strainer_class_cache[filters] ||= begin
klass = Class.new(GlobalCache)
filters.each { |f| klass.add_filter(f) }
klass
end
end
end
+5
View File
@@ -31,6 +31,11 @@ module Liquid
filter_methods.include?(method.to_s)
end
def inherited(subclass)
super
subclass.instance_variable_set(:@filter_methods, @filter_methods.dup)
end
private
def filter_methods
+32 -3
View File
@@ -1,6 +1,27 @@
# frozen_string_literal: true
module Liquid
# @public_docs
# @title case
# @type tag
# @category controlflow
# @summary Creates a switch statement to execute a particular block of code when a variable has a specified value.
# @description
# `case` initializes the switch statement, and `when` statements define the various conditions.
#
# An optional `else` statement at the end of the case provides code to execute if none of the conditions are met.
# @syntax
# {% case variable %}
# {% when value1 %}
# statement1
# {% when value2 %}
# statement2
# {% when value3 %}
# statement3
# ...
# {% else %}
# else_statement
# {% endcase %}
class Case < Block
Syntax = /(#{QuotedFragment})/o
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om
@@ -19,15 +40,16 @@ module Liquid
end
def parse(tokens)
body = new_body
body = case_body = new_body
body = @blocks.last.attachment while parse_body(body, tokens)
@blocks.each do |condition|
@blocks.reverse_each do |condition|
body = condition.attachment
unless body.frozen?
body.remove_blank_strings if blank?
body.freeze
end
end
case_body.freeze
end
def nodelist
@@ -51,7 +73,14 @@ module Liquid
@blocks.each do |block|
if block.else?
block.attachment.render_to_output_buffer(context, output) if execute_else_block
elsif block.evaluate(context)
next
end
result = Liquid::Utils.to_liquid_value(
block.evaluate(context)
)
if result
execute_else_block = false
block.attachment.render_to_output_buffer(context, output)
end
+12 -1
View File
@@ -1,6 +1,17 @@
# frozen_string_literal: true
module Liquid
# @public_docs
# @type tag
# @category theme
# @title comment
# @summary
# Allows you to comment out parts of a Liquid file.
# Any text within the opening and closing `comment` blocks won't be output,
# and any Liquid code won't be executed.
# @syntax
# {% comment %}
# statement
# {% endcomment %}
class Comment < Block
def render_to_output_buffer(_context, output)
output
+13
View File
@@ -13,6 +13,19 @@ module Liquid
# <div class="red"> Item four </div>
# <div class="green"> Item five</div>
#
# @public_docs
# @title cycle
# @type tag
# @category iteration
# @summary Loops through a group of strings and prints them in the order that they were passed as parameters.
# @description
# Each time `cycle` is called, the next string that was passed as a parameter is printed.
#
# `cycle` must be used within a `for` loop block.
# @syntax
# {% for condition %}
# {% cycle value1, value2 ... %}
# {% endfor %}
class Cycle < Tag
SimpleSyntax = /\A#{QuotedFragment}+/o
NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om
+18 -10
View File
@@ -1,17 +1,19 @@
# 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 %}
#
# @public_docs
# @type tag
# @category theme
# @title echo
# @summary
# Outputs an expression, or Liquid object, in the rendered HTML.
# Works the same as wrapping an expression in double curly brace delimiters `{{ }}`.
# Works inside the [`liquid`](/api/liquid/tags/theme-tags#liquid) tag and supports [filters](/api/liquid/filters).
# @syntax
# {% echo 'string' %}
class Echo < Tag
attr_reader :variable
def initialize(tag_name, markup, parse_context)
super
@variable = Variable.new(markup, parse_context)
@@ -20,6 +22,12 @@ module Liquid
def render(context)
@variable.render_to_output_buffer(context, +'')
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[@node.variable]
end
end
end
Template.register_tag('echo', Echo)
+16 -2
View File
@@ -45,6 +45,20 @@ module Liquid
# forloop.last:: Returns true if the item is the last item.
# forloop.parentloop:: Provides access to the parent loop, if present.
#
# @public_docs
# @title for
# @type tag
# @category iteration
# @summary Repeatedly executes a block of code.
# @description
# You can output a maximum of 50 results per page with `for` loops. In cases where there are more than 50 results,
# use the [`paginate`](/api/liquid/tags/theme-tags#paginate) tag to split them across multiple pages.
#
# For a full list of attributes available within a `for` loop, refer to the [`forloop`](/api/liquid/objects/for-loops) object.
# @syntax
# {% for variable in iterable parameters %}
# @optional_param limit [number] Exit the `for` loop at the specified index.
# @optional_param offset [number] Start the `for` loop at the specified index.
class For < Block
Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
@@ -63,11 +77,11 @@ module Liquid
parse_body(@else_block, tokens)
end
if blank?
@for_block.remove_blank_strings
@else_block&.remove_blank_strings
@for_block.remove_blank_strings
end
@for_block.freeze
@else_block&.freeze
@for_block.freeze
end
def nodelist
+14 -9
View File
@@ -3,14 +3,15 @@
module Liquid
# If is the conditional block
#
# {% if user.admin %}
# Admin user!
# {% else %}
# Not admin user
# @public_docs
# @title if
# @type tag
# @category controlflow
# @summary Executes a block of code only if a certain condition is met (if the result is `truthy`).
# @syntax
# {% if variable operator value %}
# statement
# {% endif %}
#
# There are {% if count < 5 %} less {% else %} more {% endif %} items than you need.
#
class If < Block
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
@@ -31,7 +32,7 @@ module Liquid
def parse(tokens)
while parse_body(@blocks.last.attachment, tokens)
end
@blocks.each do |block|
@blocks.reverse_each do |block|
block.attachment.remove_blank_strings if blank?
block.attachment.freeze
end
@@ -50,7 +51,11 @@ module Liquid
def render_to_output_buffer(context, output)
@blocks.each do |block|
if block.evaluate(context)
result = Liquid::Utils.to_liquid_value(
block.evaluate(context)
)
if result
return block.attachment.render_to_output_buffer(context, output)
end
end
+10
View File
@@ -1,6 +1,16 @@
# frozen_string_literal: true
module Liquid
# @public_docs
# @type tag
# @category theme
# @title raw
# @summary
# Allows you to output Liquid code on a page without it being parsed.
# @syntax
# {% raw %}
# liquid
# {% endraw %}
class Raw < Block
Syntax = /\A\s*\z/
FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
+22
View File
@@ -1,6 +1,28 @@
# frozen_string_literal: true
module Liquid
# @public_docs
# @type tag
# @category theme
# @title render
# @summary
# Renders a snippet from the **snippets** folder of a theme, or [code for an app block](/themes/architecture/sections/section-schema#render-app-blocks).
# You don't need to write the file's `.liquid` extension.
# @description
# When a snippet is rendered, the code inside it doesn't automatically have access to the variables assigned
# using [variable tags](/api/liquid/tags/variable-tags) within the snippet's parent template.
# Similarly, variables assigned within the snippet can't be accessed by the code outside of the snippet.
# This encapsulation increases performance and helps make theme code easier to understand and maintain.
#
# You can't use an [`include`](/api/liquid/tags/theme-tags#include) tag inside of a snippet rendered using the `render` tag.
#
# > Tip:
# > This tag replaces the deprecated [`include`](/api/liquid/tags/theme-tags#include) tag.
# @syntax
# {% render reference %}
# @optional_param with [string] Pass an object to use in the snippet. Use with the `as` parameter.
# @optional_param for [string] Render the snippet once for each value of an enumerable object. Use with the `as` parameter. When using the `for` parameter, the [`forloop`](/api/liquid/objects/for-loops) object is accessible within the snippet.
# @optional_param as [string] The variable that the object referenced by a `with` or `for` parameter represents within the snippet.
class Render < Tag
FOR = 'for'
SYNTAX = /(#{QuotedString}+)(\s+(with|#{FOR})\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
+17
View File
@@ -1,6 +1,23 @@
# frozen_string_literal: true
module Liquid
# @public_docs
# @title tablerow
# @type tag
# @category iteration
# @summary Generates rows for an HTML table.
# @description
# Must be wrapped in opening `<table>` and closing `</table>` HTML tags.
# For a full list of attributes available within a `tablerow` loop, refer to the [`tablerow`](/api/liquid/objects/tablerow) object.
# @syntax
# <table>
# {% tablerow variable in interable parameters %}
# content
# {% endtablerow %}
# </table>
# @optional_param cols [number] The number of columns the table should have.
# @optional_param limit [number] Exit the `tablerow` loop at the specified index.
# @optional_param offset [number] Start the `tablerow` loop at the specified index.
class TableRow < Block
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
+19 -6
View File
@@ -3,21 +3,34 @@
require_relative 'if'
module Liquid
# Unless is a conditional just like 'if' but works on the inverse logic.
#
# {% unless x < 0 %} x is greater than zero {% endunless %}
#
# @public_docs
# @title unless
# @type tag
# @category controlflow
# @summary Executes a block of code only if a certain condition is not met (if the result is `falsy`).
# @syntax
# {% unless variable operator value %}
# statement
# {% endunless %}
class Unless < If
def render_to_output_buffer(context, output)
# First condition is interpreted backwards ( if not )
first_block = @blocks.first
unless first_block.evaluate(context)
result = Liquid::Utils.to_liquid_value(
first_block.evaluate(context)
)
unless result
return first_block.attachment.render_to_output_buffer(context, output)
end
# After the first condition unless works just like if
@blocks[1..-1].each do |block|
if block.evaluate(context)
result = Liquid::Utils.to_liquid_value(
block.evaluate(context)
)
if result
return block.attachment.render_to_output_buffer(context, output)
end
end
+17 -26
View File
@@ -106,12 +106,9 @@ module Liquid
# Parse source code.
# Returns self for easy chaining
def parse(source, options = {})
@options = options
@profiling = options[:profile]
@line_numbers = options[:line_numbers] || @profiling
parse_context = options.is_a?(ParseContext) ? options : ParseContext.new(options)
@root = Document.parse(tokenize(source), parse_context)
@warnings = parse_context.warnings
parse_context = configure_options(options)
tokenizer = parse_context.new_tokenizer(source, start_line_number: @line_numbers && 1)
@root = Document.parse(tokenizer, parse_context)
self
end
@@ -189,11 +186,13 @@ module Liquid
# Retrying a render resets resource usage
context.resource_limits.reset
if @profiling && context.profiler.nil?
@profiler = context.profiler = Liquid::Profiler.new
end
begin
# render the nodelist.
with_profiling(context) do
@root.render_to_output_buffer(context, output || +'')
end
@root.render_to_output_buffer(context, output || +'')
rescue Liquid::MemoryError => e
context.handle_error(e)
ensure
@@ -212,25 +211,17 @@ module Liquid
private
def tokenize(source)
Tokenizer.new(source, @line_numbers)
end
def with_profiling(context)
if @profiling && !context.partial
def configure_options(options)
if (profiling = options[:profile])
raise "Profiler not loaded, require 'liquid/profiler' first" unless defined?(Liquid::Profiler)
@profiler = Profiler.new(context.template_name)
@profiler.start
begin
yield
ensure
@profiler.stop
end
else
yield
end
@options = options
@profiling = profiling
@line_numbers = options[:line_numbers] || @profiling
parse_context = options.is_a?(ParseContext) ? options : ParseContext.new(options)
@warnings = parse_context.warnings
parse_context
end
def apply_options_to_context(context, options)
+2 -2
View File
@@ -5,7 +5,7 @@ module Liquid
attr_reader :line_number, :for_liquid_tag
def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false)
@source = source
@source = source.to_s.to_str
@line_number = line_number || (line_numbers ? 1 : nil)
@for_liquid_tag = for_liquid_tag
@tokens = tokenize
@@ -24,7 +24,7 @@ module Liquid
private
def tokenize
return [] if @source.to_s.empty?
return [] if @source.empty?
return @source.split("\n") if @for_liquid_tag
+8
View File
@@ -81,5 +81,13 @@ module Liquid
rescue ::ArgumentError
nil
end
def self.to_liquid_value(obj)
# Enable "obj" to represent itself as a primitive value like integer, string, or boolean
return obj.to_liquid_value if obj.respond_to?(:to_liquid_value)
# Otherwise return the object itself
obj
end
end
end
+3
View File
@@ -40,6 +40,9 @@ module Liquid
@lookups.each_index do |i|
key = context.evaluate(@lookups[i])
# Cast "key" to its liquid value to enable it to act as a primitive value
key = Liquid::Utils.to_liquid_value(key)
# If object is a hash- or array-like object we look for the
# presence of the key and if its available we return it
if object.respond_to?(:[]) &&
+1 -1
View File
@@ -2,5 +2,5 @@
# frozen_string_literal: true
module Liquid
VERSION = "4.0.3"
VERSION = "5.3.0.alpha"
end
+2
View File
@@ -20,6 +20,8 @@ Gem::Specification.new do |s|
s.required_ruby_version = ">= 2.5.0"
s.required_rubygems_version = ">= 1.3.7"
s.metadata['allowed_push_host'] = 'https://rubygems.org'
s.test_files = Dir.glob("{test}/**/*")
s.files = Dir.glob("{lib}/**/*") + %w(LICENSE README.md)
+9 -1
View File
@@ -3,11 +3,19 @@
require 'yaml'
module Database
DATABASE_FILE_PATH = "#{__dir__}/vision.database.yml"
# Load the standard vision toolkit database and re-arrage it to be simply exportable
# to liquid as assigns. All this is based on Shopify
def self.tables
@tables ||= begin
db = YAML.load_file("#{__dir__}/vision.database.yml")
db =
if YAML.respond_to?(:unsafe_load_file) # Only Psych 4+ can use unsafe_load_file
# unsafe_load_file is needed for YAML references
YAML.unsafe_load_file(DATABASE_FILE_PATH)
else
YAML.load_file(DATABASE_FILE_PATH)
end
# From vision source
db['products'].each do |product|
+7 -3
View File
@@ -73,10 +73,14 @@ class ThemeRunner
private
def render_layout(template, layout, assigns)
assigns['content_for_layout'] = template.render!(assigns)
layout&.render!(assigns)
end
def compile_and_render(template, layout, assigns, page_template, template_file)
compiled_test = compile_test(template, layout, assigns, page_template, template_file)
assigns['content_for_layout'] = compiled_test[:tmpl].render!(assigns)
compiled_test[:layout].render!(assigns) if layout
compiled_test = compile_test(template, layout, assigns, page_template, template_file)
render_layout(compiled_test[:tmpl], compiled_test[:layout], compiled_test[:assigns])
end
def compile_all_tests
+1 -1
View File
@@ -44,7 +44,7 @@ class AssignTest < Minitest::Test
end
end
with_error_mode(:lax) do
assert Template.parse("{% assign foo = ('X' | downcase) %}")
assert(Template.parse("{% assign foo = ('X' | downcase) %}"))
end
end
+9 -29
View File
@@ -14,7 +14,7 @@ class BlockTest < Minitest::Test
def test_with_custom_tag
with_custom_tag('testtag', Block) do
assert Liquid::Template.parse("{% testtag %} {% endtesttag %}")
assert(Liquid::Template.parse("{% testtag %} {% endtesttag %}"))
end
end
@@ -28,13 +28,13 @@ class BlockTest < Minitest::Test
with_custom_tag('blabla', klass1) do
template = Liquid::Template.parse("{% blabla %} bla {% endblabla %}")
assert_equal 'hello', template.render
assert_equal('hello', template.render)
buf = +''
output = template.render({}, output: buf)
assert_equal 'hello', output
assert_equal 'hello', buf
assert_equal buf.object_id, output.object_id
assert_equal('hello', output)
assert_equal('hello', buf)
assert_equal(buf.object_id, output.object_id)
end
klass2 = Class.new(klass1) do
@@ -46,33 +46,13 @@ class BlockTest < Minitest::Test
with_custom_tag('blabla', klass2) do
template = Liquid::Template.parse("{% blabla %} foo {% endblabla %}")
assert_equal 'foohellobar', template.render
assert_equal('foohellobar', template.render)
buf = +''
output = template.render({}, output: buf)
assert_equal 'foohellobar', output
assert_equal 'foohellobar', buf
assert_equal buf.object_id, output.object_id
assert_equal('foohellobar', output)
assert_equal('foohellobar', buf)
assert_equal(buf.object_id, output.object_id)
end
end
def test_instrument_for_bug_1346
calls = []
Liquid::Usage.stub(:increment, ->(name) { calls << name }) do
Liquid::Template.parse("{% for i in (1..2) %}{{ i }}{% endfor {% foo %}")
end
assert_equal(["end_tag_params"], calls)
calls = []
Liquid::Usage.stub(:increment, ->(name) { calls << name }) do
Liquid::Template.parse("{% for i in (1..2) %}{{ i }}{% endfor test %}")
end
assert_equal(["end_tag_params"], calls)
calls = []
Liquid::Usage.stub(:increment, ->(name) { calls << name }) do
Liquid::Template.parse("{% for i in (1..2) %}{{ i }}{% endfor %}")
end
assert_equal([], calls)
end
end
+28 -29
View File
@@ -24,7 +24,7 @@ class ContextSensitiveDrop < Liquid::Drop
end
end
class Category < Liquid::Drop
class Category
attr_accessor :name
def initialize(name)
@@ -36,8 +36,9 @@ class Category < Liquid::Drop
end
end
class CategoryDrop
class CategoryDrop < Liquid::Drop
attr_accessor :category, :context
def initialize(category)
@category = category
end
@@ -405,45 +406,42 @@ class ContextTest < Minitest::Test
end
def test_lambda_is_called_once
@global = 0
@context['callcount'] = proc {
@global ||= 0
@global += 1
@global += 1
@global.to_s
}
assert_equal('1', @context['callcount'])
assert_equal('1', @context['callcount'])
assert_equal('1', @context['callcount'])
@global = nil
end
def test_nested_lambda_is_called_once
@global = 0
@context['callcount'] = { "lambda" => proc {
@global ||= 0
@global += 1
@global += 1
@global.to_s
} }
assert_equal('1', @context['callcount.lambda'])
assert_equal('1', @context['callcount.lambda'])
assert_equal('1', @context['callcount.lambda'])
@global = nil
end
def test_lambda_in_array_is_called_once
@global = 0
@context['callcount'] = [1, 2, proc {
@global ||= 0
@global += 1
@global += 1
@global.to_s
}, 4, 5]
assert_equal('1', @context['callcount[2]'])
assert_equal('1', @context['callcount[2]'])
assert_equal('1', @context['callcount[2]'])
@global = nil
end
def test_access_to_context_from_proc
@@ -461,6 +459,7 @@ class ContextTest < Minitest::Test
end
def test_interrupt_avoids_object_allocations
@context.interrupt? # ruby 3.0.0 allocates on the first call
assert_no_object_allocations do
@context.interrupt?
end
@@ -566,9 +565,9 @@ class ContextTest < Minitest::Test
def test_disables_tag_specified
context = Context.new
context.with_disabled_tags(%w(foo bar)) do
assert_equal true, context.tag_disabled?("foo")
assert_equal true, context.tag_disabled?("bar")
assert_equal false, context.tag_disabled?("unknown")
assert_equal(true, context.tag_disabled?("foo"))
assert_equal(true, context.tag_disabled?("bar"))
assert_equal(false, context.tag_disabled?("unknown"))
end
end
@@ -576,19 +575,19 @@ class ContextTest < Minitest::Test
context = Context.new
context.with_disabled_tags(["foo"]) do
context.with_disabled_tags(["foo"]) do
assert_equal true, context.tag_disabled?("foo")
assert_equal false, context.tag_disabled?("bar")
assert_equal(true, context.tag_disabled?("foo"))
assert_equal(false, context.tag_disabled?("bar"))
end
context.with_disabled_tags(["bar"]) do
assert_equal true, context.tag_disabled?("foo")
assert_equal true, context.tag_disabled?("bar")
assert_equal(true, context.tag_disabled?("foo"))
assert_equal(true, context.tag_disabled?("bar"))
context.with_disabled_tags(["foo"]) do
assert_equal true, context.tag_disabled?("foo")
assert_equal true, context.tag_disabled?("bar")
assert_equal(true, context.tag_disabled?("foo"))
assert_equal(true, context.tag_disabled?("bar"))
end
end
assert_equal true, context.tag_disabled?("foo")
assert_equal false, context.tag_disabled?("bar")
assert_equal(true, context.tag_disabled?("foo"))
assert_equal(false, context.tag_disabled?("bar"))
end
end
@@ -606,16 +605,16 @@ class ContextTest < Minitest::Test
end
with_global_filter(global) do
assert_equal 'Global test', Template.parse("{{'test' | notice }}").render!
assert_equal 'Local test', Template.parse("{{'test' | notice }}").render!({}, filters: [local])
assert_equal('Global test', Template.parse("{{'test' | notice }}").render!)
assert_equal('Local test', Template.parse("{{'test' | notice }}").render!({}, filters: [local]))
end
end
def test_has_key_will_not_add_an_error_for_missing_keys
with_error_mode :strict do
with_error_mode(:strict) do
context = Context.new
context.key?('unknown')
assert_empty context.errors
assert_empty(context.errors)
end
end
+13 -13
View File
@@ -167,7 +167,7 @@ class DropsTest < Minitest::Test
def test_object_methods_not_allowed
[:dup, :clone, :singleton_class, :eval, :class_eval, :inspect].each do |method|
output = Liquid::Template.parse(" {{ product.#{method} }} ").render!('product' => ProductDrop.new)
assert_equal ' ', output
assert_equal(' ', output)
end
end
@@ -210,28 +210,28 @@ class DropsTest < Minitest::Test
def test_enumerable_drop_will_invoke_liquid_method_missing_for_clashing_method_names
["select", "each", "map", "cycle"].each do |method|
assert_equal method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)
assert_equal method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)
assert_equal method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new)
assert_equal method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new)
assert_equal(method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new))
assert_equal(method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new))
assert_equal(method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new))
assert_equal(method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new))
end
end
def test_some_enumerable_methods_still_get_invoked
[:count, :max].each do |method|
assert_equal "3", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new)
assert_equal "3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new)
assert_equal "3", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)
assert_equal "3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)
assert_equal("3", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new))
assert_equal("3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new))
assert_equal("3", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new))
assert_equal("3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new))
end
assert_equal("yes", Liquid::Template.parse("{% if collection contains 3 %}yes{% endif %}").render!('collection' => RealEnumerableDrop.new))
[:min, :first].each do |method|
assert_equal "1", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new)
assert_equal "1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new)
assert_equal "1", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)
assert_equal "1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.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))
end
end
+1 -1
View File
@@ -193,7 +193,7 @@ class ErrorHandlingTest < Minitest::Test
# Liquid should not catch Exceptions that are not subclasses of StandardError, like Interrupt and NoMemoryError
def test_exceptions_propagate
assert_raises Exception do
assert_raises(Exception) do
template = Liquid::Template.parse('{{ errors.exception }}')
template.render('errors' => ErrorDrop.new)
end
+24
View File
@@ -0,0 +1,24 @@
# frozen_string_literal: true
require 'test_helper'
class FilterKwargTest < Minitest::Test
module KwargFilter
def html_tag(_tag, attributes)
attributes
.map { |key, value| "#{key}='#{value}'" }
.join(' ')
end
end
include Liquid
def test_can_parse_data_kwargs
with_global_filter(KwargFilter) do
assert_equal(
"data-src='src' data-widths='100, 200'",
Template.parse("{{ 'img' | html_tag: data-src: 'src', data-widths: '100, 200' }}").render(nil, nil)
)
end
end
end
+3 -3
View File
@@ -169,9 +169,9 @@ class FiltersInTemplate < Minitest::Test
def test_local_global
with_global_filter(MoneyFilter) do
assert_equal " 1000$ ", Template.parse("{{1000 | money}}").render!(nil, nil)
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, filters: CanadianMoneyFilter)
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, filters: [CanadianMoneyFilter])
assert_equal(" 1000$ ", Template.parse("{{1000 | money}}").render!(nil, nil))
assert_equal(" 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, filters: CanadianMoneyFilter))
assert_equal(" 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, filters: [CanadianMoneyFilter]))
end
end
+1 -1
View File
@@ -19,7 +19,7 @@ class HashOrderingTest < Minitest::Test
def test_global_register_order
with_global_filter(MoneyFilter, CanadianMoneyFilter) do
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render(nil, nil)
assert_equal(" 1000$ CAD ", Template.parse("{{1000 | money}}").render(nil, nil))
end
end
end
+1 -1
View File
@@ -32,7 +32,7 @@ class ParsingQuirksTest < Minitest::Test
assert(Template.parse("{{test}}"))
with_error_mode(:lax) do
assert Template.parse("{{|test}}")
assert(Template.parse("{{|test}}"))
end
with_error_mode(:strict) do
@@ -2,7 +2,28 @@
require 'test_helper'
class RenderProfilingTest < Minitest::Test
class ProfilerTest < Minitest::Test
class TestDrop < Liquid::Drop
def initialize(value)
super()
@value = value
end
def to_s
artificial_execution_time
@value
end
private
# Monotonic clock precision fluctuate based on the operating system
# By introducing a small sleep we ensure ourselves to register a non zero unit of time
def artificial_execution_time
sleep(Process.clock_getres(Process::CLOCK_MONOTONIC))
end
end
include Liquid
class ProfilingFileSystem
@@ -62,6 +83,17 @@ class RenderProfilingTest < Minitest::Test
assert_equal(2, included_children[1].line_number)
end
def test_profiling_render_tag
t = Template.parse("{% render 'a_template' %}", profile: true)
t.render!
render_children = t.profiler[0].children
render_children.each do |timing|
assert_equal('a_template', timing.partial)
end
assert_equal([1, 2], render_children.map(&:line_number))
end
def test_profiling_times_the_rendering_of_tokens
t = Template.parse("{% include 'a_template' %}", profile: true)
t.render!
@@ -77,6 +109,37 @@ class RenderProfilingTest < Minitest::Test
assert(t.profiler.total_render_time >= 0, "Total render time was not calculated")
end
class SleepTag < Liquid::Tag
def initialize(tag_name, markup, parse_context)
super
@duration = Float(markup)
end
def render_to_output_buffer(_context, _output)
sleep(@duration)
end
end
def test_profiling_multiple_renders
with_custom_tag('sleep', SleepTag) do
context = Liquid::Context.new
t = Liquid::Template.parse("{% sleep 0.001 %}", profile: true)
context.template_name = 'index'
t.render!(context)
context.template_name = 'layout'
first_render_time = context.profiler.total_time
t.render!(context)
profiler = context.profiler
children = profiler.children
assert_operator(first_render_time, :>=, 0.001)
assert_operator(profiler.total_time, :>=, 0.001 + first_render_time)
assert_equal(["index", "layout"], children.map(&:template_name))
assert_equal([nil, nil], children.map(&:code))
assert_equal(profiler.total_time, children.map(&:total_time).reduce(&:+))
end
end
def test_profiling_uses_include_to_mark_children
t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}", profile: true)
t.render!
@@ -91,7 +154,7 @@ class RenderProfilingTest < Minitest::Test
include_node = t.profiler[1]
include_node.children.each do |child|
assert_equal "a_template", child.partial
assert_equal("a_template", child.partial)
end
end
@@ -101,12 +164,12 @@ class RenderProfilingTest < Minitest::Test
a_template = t.profiler[1]
a_template.children.each do |child|
assert_equal "a_template", child.partial
assert_equal("a_template", child.partial)
end
b_template = t.profiler[2]
b_template.children.each do |child|
assert_equal "b_template", child.partial
assert_equal("b_template", child.partial)
end
end
@@ -116,12 +179,12 @@ class RenderProfilingTest < Minitest::Test
a_template1 = t.profiler[1]
a_template1.children.each do |child|
assert_equal "a_template", child.partial
assert_equal("a_template", child.partial)
end
a_template2 = t.profiler[2]
a_template2.children.each do |child|
assert_equal "a_template", child.partial
assert_equal("a_template", child.partial)
end
end
@@ -156,16 +219,22 @@ class RenderProfilingTest < Minitest::Test
def test_profiling_supports_self_time
t = Template.parse("{% for item in collection %} {{ item }} {% endfor %}", profile: true)
t.render!("collection" => ["one", "two"])
leaf = t.profiler[0].children[0]
collection = [
TestDrop.new("one"),
TestDrop.new("two"),
]
output = t.render!("collection" => collection)
assert_equal(" one two ", output)
assert_operator(leaf.self_time, :>, 0)
leaf = t.profiler[0].children[0]
assert_operator(leaf.self_time, :>, 0.0)
end
def test_profiling_supports_total_time
t = Template.parse("{% if true %} {% increment test %} {{ test }} {% endif %}", profile: true)
t.render!
t = Template.parse("{% if true %} {{ test }} {% endif %}", profile: true)
output = t.render!("test" => TestDrop.new("one"))
assert_equal(" one ", output)
assert_operator(t.profiler[0].total_time, :>, 0)
assert_operator(t.profiler[0].total_time, :>, 0.0)
end
end
+138 -47
View File
@@ -3,10 +3,6 @@
require 'test_helper'
class Filters
include Liquid::StandardFilters
end
class TestThing
attr_reader :foo
@@ -29,8 +25,24 @@ class TestThing
end
class TestDrop < Liquid::Drop
def test
"testfoo"
def initialize(value:)
@value = value
end
attr_reader :value
def registers
@context.registers
end
end
class TestModel
def initialize(value:)
@value = value
end
def to_liquid
TestDrop.new(value: @value)
end
end
@@ -53,10 +65,13 @@ class NumberLikeThing < Liquid::Drop
end
class StandardFiltersTest < Minitest::Test
Filters = Class.new(Liquid::StrainerTemplate)
Filters.add_filter(Liquid::StandardFilters)
include Liquid
def setup
@filters = Filters.new
@filters = Filters.new(Context.new)
end
def test_size
@@ -145,6 +160,40 @@ class StandardFiltersTest < Minitest::Test
assert_equal('&lt;strong&gt;Hulk&lt;/strong&gt;', @filters.escape_once('&lt;strong&gt;Hulk</strong>'))
end
def test_base64_encode
assert_equal('b25lIHR3byB0aHJlZQ==', @filters.base64_encode('one two three'))
assert_equal('', @filters.base64_encode(nil))
end
def test_base64_decode
assert_equal('one two three', @filters.base64_decode('b25lIHR3byB0aHJlZQ=='))
exception = assert_raises(Liquid::ArgumentError) do
@filters.base64_decode("invalidbase64")
end
assert_equal('Liquid error: invalid base64 provided to base64_decode', exception.message)
end
def test_base64_url_safe_encode
assert_equal(
'YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXogQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVogMTIzNDU2Nzg5MCAhQCMkJV4mKigpLT1fKy8_Ljo7W117fVx8',
@filters.base64_url_safe_encode('abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890 !@#$%^&*()-=_+/?.:;[]{}\|')
)
assert_equal('', @filters.base64_url_safe_encode(nil))
end
def test_base64_url_safe_decode
assert_equal(
'abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890 !@#$%^&*()-=_+/?.:;[]{}\|',
@filters.base64_url_safe_decode('YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXogQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVogMTIzNDU2Nzg5MCAhQCMkJV4mKigpLT1fKy8_Ljo7W117fVx8')
)
exception = assert_raises(Liquid::ArgumentError) do
@filters.base64_url_safe_decode("invalidbase64")
end
assert_equal('Liquid error: invalid base64 provided to base64_url_safe_decode', exception.message)
end
def test_url_encode
assert_equal('foo%2B1%40example.com', @filters.url_encode('[email protected]'))
assert_equal('1', @filters.url_encode(1))
@@ -159,7 +208,7 @@ class StandardFiltersTest < Minitest::Test
assert_equal('1', @filters.url_decode(1))
assert_equal('2001-02-03', @filters.url_decode(Date.new(2001, 2, 3)))
assert_nil(@filters.url_decode(nil))
exception = assert_raises Liquid::ArgumentError do
exception = assert_raises(Liquid::ArgumentError) do
@filters.url_decode('%ff')
end
assert_equal('Liquid error: invalid byte sequence in UTF-8', exception.message)
@@ -171,10 +220,17 @@ class StandardFiltersTest < Minitest::Test
assert_equal('one two three', @filters.truncatewords('one two three'))
assert_equal(
'Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221;...',
@filters.truncatewords('Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221; x 16&#8221; x 10.5&#8221; high) with cover.', 15)
@filters.truncatewords('Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221; x 16&#8221; x 10.5&#8221; high) with cover.', 15)
)
assert_equal("测试测试测试测试", @filters.truncatewords('测试测试测试测试', 5))
assert_equal('one two1', @filters.truncatewords("one two three", 2, 1))
assert_equal('one two three...', @filters.truncatewords("one two\tthree\nfour", 3))
assert_equal('one two...', @filters.truncatewords("one two three four", 2))
assert_equal('one...', @filters.truncatewords("one two three four", 0))
exception = assert_raises(Liquid::ArgumentError) do
@filters.truncatewords("one two three four", 1 << 31)
end
assert_equal("Liquid error: integer #{1 << 31} too big for truncatewords", exception.message)
end
def test_strip_html
@@ -218,8 +274,8 @@ class StandardFiltersTest < Minitest::Test
{ "price" => 1, "handle" => "gamma" },
{ "price" => 2, "handle" => "epsilon" },
{ "price" => 4, "handle" => "alpha" },
{ "handle" => "delta" },
{ "handle" => "beta" },
{ "handle" => "delta" },
]
assert_equal(expectation, @filters.sort(input, "price"))
end
@@ -286,7 +342,7 @@ class StandardFiltersTest < Minitest::Test
[3],
]
assert_raises Liquid::ArgumentError do
assert_raises(Liquid::ArgumentError) do
@filters.sort(foo, "bar")
end
end
@@ -302,7 +358,7 @@ class StandardFiltersTest < Minitest::Test
[3],
]
assert_raises Liquid::ArgumentError do
assert_raises(Liquid::ArgumentError) do
@filters.sort_natural(foo, "bar")
end
end
@@ -322,8 +378,9 @@ class StandardFiltersTest < Minitest::Test
assert_equal(["foo"], @filters.uniq("foo"))
assert_equal([1, 3, 2, 4], @filters.uniq([1, 1, 3, 2, 3, 1, 4, 3, 2, 1]))
assert_equal([{ "a" => 1 }, { "a" => 3 }, { "a" => 2 }], @filters.uniq([{ "a" => 1 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a"))
testdrop = TestDrop.new
assert_equal([testdrop], @filters.uniq([testdrop, TestDrop.new], 'test'))
test_drop = TestDrop.new(value: "test")
test_drop_alternate = TestDrop.new(value: "test")
assert_equal([test_drop], @filters.uniq([test_drop, test_drop_alternate], 'value'))
end
def test_uniq_empty_array
@@ -337,7 +394,7 @@ class StandardFiltersTest < Minitest::Test
[3],
]
assert_raises Liquid::ArgumentError do
assert_raises(Liquid::ArgumentError) do
@filters.uniq(foo, "bar")
end
end
@@ -353,7 +410,7 @@ class StandardFiltersTest < Minitest::Test
[3],
]
assert_raises Liquid::ArgumentError do
assert_raises(Liquid::ArgumentError) do
@filters.compact(foo, "bar")
end
end
@@ -382,6 +439,16 @@ class StandardFiltersTest < Minitest::Test
assert_template_result("woot: 1", '{{ foo | map: "whatever" }}', "foo" => [t])
end
def test_map_calls_context=
model = TestModel.new(value: "test")
template = Template.parse('{{ foo | map: "registers" }}')
template.registers[:test] = 1234
template.assigns['foo'] = [model]
assert_template_result("{:test=>1234}", template.render!)
end
def test_map_on_hashes
assert_template_result("4217", '{{ thing | map: "foo" | map: "bar" }}',
"thing" => { "foo" => [{ "bar" => 42 }, { "bar" => 17 }] })
@@ -400,9 +467,9 @@ class StandardFiltersTest < Minitest::Test
end
def test_map_over_proc
drop = TestDrop.new
drop = TestDrop.new(value: "testfoo")
p = proc { drop }
templ = '{{ procs | map: "test" }}'
templ = '{{ procs | map: "value" }}'
assert_template_result("testfoo", templ, "procs" => [p])
end
@@ -430,7 +497,7 @@ class StandardFiltersTest < Minitest::Test
[3],
]
assert_raises Liquid::ArgumentError do
assert_raises(Liquid::ArgumentError) do
@filters.map(foo, "bar")
end
end
@@ -441,7 +508,7 @@ class StandardFiltersTest < Minitest::Test
[2],
[3],
]
assert_raises Liquid::ArgumentError do
assert_raises(Liquid::ArgumentError) do
@filters.map(foo, nil)
end
end
@@ -485,8 +552,8 @@ class StandardFiltersTest < Minitest::Test
assert_equal('', @filters.date('', "%B"))
with_timezone("UTC") do
assert_equal "07/05/2006", @filters.date(1152098955, "%m/%d/%Y")
assert_equal "07/05/2006", @filters.date("1152098955", "%m/%d/%Y")
assert_equal("07/05/2006", @filters.date(1152098955, "%m/%d/%Y"))
assert_equal("07/05/2006", @filters.date("1152098955", "%m/%d/%Y"))
end
end
@@ -498,19 +565,31 @@ class StandardFiltersTest < Minitest::Test
end
def test_replace
assert_equal('2 2 2 2', @filters.replace('1 1 1 1', '1', 2))
assert_equal('b b b b', @filters.replace('a a a a', 'a', 'b'))
assert_equal('2 2 2 2', @filters.replace('1 1 1 1', 1, 2))
assert_equal('2 1 1 1', @filters.replace_first('1 1 1 1', '1', 2))
assert_equal('1 1 1 1', @filters.replace('1 1 1 1', 2, 3))
assert_template_result('2 2 2 2', "{{ '1 1 1 1' | replace: '1', 2 }}")
assert_equal('b a a a', @filters.replace_first('a a a a', 'a', 'b'))
assert_equal('2 1 1 1', @filters.replace_first('1 1 1 1', 1, 2))
assert_equal('1 1 1 1', @filters.replace_first('1 1 1 1', 2, 3))
assert_template_result('2 1 1 1', "{{ '1 1 1 1' | replace_first: '1', 2 }}")
assert_equal('a a a b', @filters.replace_last('a a a a', 'a', 'b'))
assert_equal('1 1 1 2', @filters.replace_last('1 1 1 1', 1, 2))
assert_equal('1 1 1 1', @filters.replace_last('1 1 1 1', 2, 3))
assert_template_result('1 1 1 2', "{{ '1 1 1 1' | replace_last: '1', 2 }}")
end
def test_remove
assert_equal(' ', @filters.remove("a a a a", 'a'))
assert_equal(' ', @filters.remove("1 1 1 1", 1))
assert_equal('a a a', @filters.remove_first("a a a a", 'a '))
assert_equal(' 1 1 1', @filters.remove_first("1 1 1 1", 1))
assert_template_result('a a a', "{{ 'a a a a' | remove_first: 'a ' }}")
assert_template_result(' ', "{{ '1 1 1 1' | remove: 1 }}")
assert_equal('b a a', @filters.remove_first("a b a a", 'a '))
assert_template_result(' 1 1 1', "{{ '1 1 1 1' | remove_first: 1 }}")
assert_equal('a a b', @filters.remove_last("a a b a", ' a'))
assert_template_result('1 1 1 ', "{{ '1 1 1 1' | remove_last: 1 }}")
end
def test_pipes_in_string_arguments
@@ -539,6 +618,7 @@ class StandardFiltersTest < Minitest::Test
def test_newlines_to_br
assert_template_result("a<br />\nb<br />\nc", "{{ source | newline_to_br }}", 'source' => "a\nb\nc")
assert_template_result("a<br />\nb<br />\nc", "{{ source | newline_to_br }}", 'source' => "a\r\nb\nc")
end
def test_plus
@@ -587,7 +667,7 @@ class StandardFiltersTest < Minitest::Test
assert_template_result("0.5", "{{ 2.0 | divided_by:4 }}")
assert_raises(Liquid::ZeroDivisionError) do
assert_template_result "4", "{{ 1 | modulo: 0 }}"
assert_template_result("4", "{{ 1 | modulo: 0 }}")
end
assert_template_result("5", "{{ price | divided_by:2 }}", 'price' => NumberLikeThing.new(10))
@@ -596,7 +676,7 @@ class StandardFiltersTest < Minitest::Test
def test_modulo
assert_template_result("1", "{{ 3 | modulo:2 }}")
assert_raises(Liquid::ZeroDivisionError) do
assert_template_result "4", "{{ 1 | modulo: 0 }}"
assert_template_result("4", "{{ 1 | modulo: 0 }}")
end
assert_template_result("1", "{{ price | modulo:2 }}", 'price' => NumberLikeThing.new(3))
@@ -607,7 +687,7 @@ class StandardFiltersTest < Minitest::Test
assert_template_result("4", "{{ '4.3' | round }}")
assert_template_result("4.56", "{{ input | round: 2 }}", 'input' => 4.5612)
assert_raises(Liquid::FloatDomainError) do
assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | round }}"
assert_template_result("4", "{{ 1.0 | divided_by: 0.0 | round }}")
end
assert_template_result("5", "{{ price | round }}", 'price' => NumberLikeThing.new(4.6))
@@ -618,7 +698,7 @@ class StandardFiltersTest < Minitest::Test
assert_template_result("5", "{{ input | ceil }}", 'input' => 4.6)
assert_template_result("5", "{{ '4.3' | ceil }}")
assert_raises(Liquid::FloatDomainError) do
assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | ceil }}"
assert_template_result("4", "{{ 1.0 | divided_by: 0.0 | ceil }}")
end
assert_template_result("5", "{{ price | ceil }}", 'price' => NumberLikeThing.new(4.6))
@@ -628,7 +708,7 @@ class StandardFiltersTest < Minitest::Test
assert_template_result("4", "{{ input | floor }}", 'input' => 4.6)
assert_template_result("4", "{{ '4.3' | floor }}")
assert_raises(Liquid::FloatDomainError) do
assert_template_result "4", "{{ 1.0 | divided_by: 0.0 | floor }}"
assert_template_result("4", "{{ 1.0 | divided_by: 0.0 | floor }}")
end
assert_template_result("5", "{{ price | floor }}", 'price' => NumberLikeThing.new(5.4))
@@ -686,6 +766,8 @@ class StandardFiltersTest < Minitest::Test
assert_equal("bar", @filters.default([], "bar"))
assert_equal("bar", @filters.default({}, "bar"))
assert_template_result('bar', "{{ false | default: 'bar' }}")
assert_template_result('bar', "{{ drop | default: 'bar' }}", 'drop' => BooleanDrop.new(false))
assert_template_result('Yay', "{{ drop | default: 'bar' }}", 'drop' => BooleanDrop.new(true))
end
def test_default_handle_false
@@ -696,6 +778,8 @@ class StandardFiltersTest < Minitest::Test
assert_equal("bar", @filters.default([], "bar", "allow_false" => true))
assert_equal("bar", @filters.default({}, "bar", "allow_false" => true))
assert_template_result('false', "{{ false | default: 'bar', allow_false: true }}")
assert_template_result('Nay', "{{ drop | default: 'bar', allow_false: true }}", 'drop' => BooleanDrop.new(false))
assert_template_result('Yay', "{{ drop | default: 'bar', allow_false: true }}", 'drop' => BooleanDrop.new(true))
end
def test_cannot_access_private_methods
@@ -724,6 +808,18 @@ class StandardFiltersTest < Minitest::Test
assert_equal(expectation, @filters.where(input, "ok"))
end
def test_where_string_keys
input = [
"alpha", "beta", "gamma", "delta"
]
expectation = [
"beta",
]
assert_equal(expectation, @filters.where(input, "be"))
end
def test_where_no_key_set
input = [
{ "handle" => "alpha", "ok" => true },
@@ -769,7 +865,7 @@ class StandardFiltersTest < Minitest::Test
end
def test_all_filters_never_raise_non_liquid_exception
test_drop = TestDrop.new
test_drop = TestDrop.new(value: "test")
test_drop.context = Context.new
test_enum = TestEnumerable.new
test_enum.context = Context.new
@@ -794,19 +890,14 @@ class StandardFiltersTest < Minitest::Test
{ 1 => "bar" },
["foo", 123, nil, true, false, Drop, ["foo"], { foo: "bar" }],
]
test_types.each do |first|
test_types.each do |other|
(@filters.methods - Object.methods).each do |method|
arg_count = @filters.method(method).arity
arg_count *= -1 if arg_count < 0
inputs = [first]
inputs << ([other] * (arg_count - 1)) if arg_count > 1
begin
@filters.send(method, *inputs)
rescue Liquid::ArgumentError, Liquid::ZeroDivisionError
nil
end
end
StandardFilters.public_instance_methods(false).each do |method|
arg_count = @filters.method(method).arity
arg_count *= -1 if arg_count < 0
test_types.repeated_permutation(arg_count) do |args|
@filters.send(method, *args)
rescue Liquid::Error
nil
end
end
end
+8 -8
View File
@@ -15,13 +15,13 @@ class TagTest < Minitest::Test
with_custom_tag('blabla', klass1) do
template = Liquid::Template.parse("{% blabla %}")
assert_equal 'hello', template.render
assert_equal('hello', template.render)
buf = +''
output = template.render({}, output: buf)
assert_equal 'hello', output
assert_equal 'hello', buf
assert_equal buf.object_id, output.object_id
assert_equal('hello', output)
assert_equal('hello', buf)
assert_equal(buf.object_id, output.object_id)
end
klass2 = Class.new(klass1) do
@@ -33,13 +33,13 @@ class TagTest < Minitest::Test
with_custom_tag('blabla', klass2) do
template = Liquid::Template.parse("{% blabla %}")
assert_equal 'foohellobar', template.render
assert_equal('foohellobar', template.render)
buf = +''
output = template.render({}, output: buf)
assert_equal 'foohellobar', output
assert_equal 'foohellobar', buf
assert_equal buf.object_id, output.object_id
assert_equal('foohellobar', output)
assert_equal('foohellobar', buf)
assert_equal(buf.object_id, output.object_id)
end
end
end
+1 -1
View File
@@ -184,7 +184,7 @@ class IfElseTagTest < Minitest::Test
tests.each do |vals, expected|
a, b, c = vals
assigns = { 'a' => a, 'b' => b, 'c' => c }
assert_template_result expected.to_s, tpl, assigns, assigns.to_s
assert_template_result(expected.to_s, tpl, assigns, assigns.to_s)
end
end
end
+4 -4
View File
@@ -96,12 +96,12 @@ class IncludeTagTest < Minitest::Test
def test_include_tag_with_alias
assert_template_result("Product: Draft 151cm ",
"{% include 'product_alias' with products[0] as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
"{% include 'product_alias' with products[0] as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
def test_include_tag_for_alias
assert_template_result("Product: Draft 151cm Product: Element 155cm ",
"{% include 'product_alias' for products as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
"{% include 'product_alias' for products as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
def test_include_tag_with_default_name
@@ -226,13 +226,13 @@ class IncludeTagTest < Minitest::Test
Template.parse("{% include template %}", error_mode: :strict).render!("template" => '{{ "X" || downcase }}')
end
with_error_mode(:lax) do
assert_equal 'x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: true).render!("template" => '{{ "X" || downcase }}')
assert_equal('x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: true).render!("template" => '{{ "X" || downcase }}'))
end
assert_raises(Liquid::SyntaxError) do
Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:locale]).render!("template" => '{{ "X" || downcase }}')
end
with_error_mode(:lax) do
assert_equal 'x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:error_mode]).render!("template" => '{{ "X" || downcase }}')
assert_equal('x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:error_mode]).render!("template" => '{{ "X" || downcase }}'))
end
end
+10 -10
View File
@@ -58,7 +58,7 @@ class RenderTagTest < Minitest::Test
def test_recursively_rendered_template_does_not_produce_endless_loop
Liquid::Template.file_system = StubFileSystem.new('loop' => '{% render "loop" %}')
assert_raises Liquid::StackLevelError do
assert_raises(Liquid::StackLevelError) do
Template.parse('{% render "loop" %}').render!
end
end
@@ -67,7 +67,7 @@ class RenderTagTest < Minitest::Test
Liquid::Template.file_system = StubFileSystem.new(
'loop_render' => '{% render "loop_render" %}',
)
assert_raises Liquid::StackLevelError do
assert_raises(Liquid::StackLevelError) do
Template.parse('{% render "loop_render" %}').render!
end
end
@@ -75,7 +75,7 @@ class RenderTagTest < Minitest::Test
def test_dynamically_choosen_templates_are_not_allowed
Liquid::Template.file_system = StubFileSystem.new('snippet' => 'should not be rendered')
assert_raises Liquid::SyntaxError do
assert_raises(Liquid::SyntaxError) do
Liquid::Template.parse("{% assign name = 'snippet' %}{% render name %}")
end
end
@@ -151,7 +151,7 @@ class RenderTagTest < Minitest::Test
)
assert_template_result("Product: Draft 151cm ",
"{% render 'product' with products[0] %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
"{% render 'product' with products[0] %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
def test_render_tag_with_alias
@@ -161,7 +161,7 @@ class RenderTagTest < Minitest::Test
)
assert_template_result("Product: Draft 151cm ",
"{% render 'product_alias' with products[0] as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
"{% render 'product_alias' with products[0] as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
def test_render_tag_for_alias
@@ -171,7 +171,7 @@ class RenderTagTest < Minitest::Test
)
assert_template_result("Product: Draft 151cm Product: Element 155cm ",
"{% render 'product_alias' for products as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
"{% render 'product_alias' for products as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
def test_render_tag_for
@@ -181,7 +181,7 @@ class RenderTagTest < Minitest::Test
)
assert_template_result("Product: Draft 151cm Product: Element 155cm ",
"{% render 'product' for products %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
"{% render 'product' for products %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
def test_render_tag_forloop
@@ -190,7 +190,7 @@ class RenderTagTest < Minitest::Test
)
assert_template_result("Product: Draft 151cm first index:1 Product: Element 155cm last index:2 ",
"{% render 'product' for products %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
"{% render 'product' for products %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
def test_render_tag_for_drop
@@ -199,7 +199,7 @@ class RenderTagTest < Minitest::Test
)
assert_template_result("123",
"{% render 'loop' for loop as value %}", "loop" => TestEnumerable.new)
"{% render 'loop' for loop as value %}", "loop" => TestEnumerable.new)
end
def test_render_tag_with_drop
@@ -208,6 +208,6 @@ class RenderTagTest < Minitest::Test
)
assert_template_result("TestEnumerable",
"{% render 'loop' with loop as value %}", "loop" => TestEnumerable.new)
"{% render 'loop' with loop as value %}", "loop" => TestEnumerable.new)
end
end
+18 -4
View File
@@ -198,7 +198,7 @@ class TemplateTest < Minitest::Test
context = Context.new('drop' => ErroneousDrop.new)
t = Template.new.parse('{{ drop.bad_method }}')
e = assert_raises RuntimeError do
e = assert_raises(RuntimeError) do
t.render!(context)
end
assert_equal('ruby error in drop', e.message)
@@ -267,7 +267,7 @@ class TemplateTest < Minitest::Test
def test_undefined_variables_raise
t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}")
assert_raises UndefinedVariable do
assert_raises(UndefinedVariable) do
t.render!({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, strict_variables: true)
end
end
@@ -286,7 +286,7 @@ class TemplateTest < Minitest::Test
d = DropWithUndefinedMethod.new
t = Template.new.parse('{{ foo }} {{ woot }}')
assert_raises UndefinedDropMethod do
assert_raises(UndefinedDropMethod) do
t.render!(d, strict_variables: true)
end
end
@@ -309,7 +309,7 @@ class TemplateTest < Minitest::Test
def test_undefined_filters_raise
t = Template.parse("{{x | somefilter1 | upcase | somefilter2}}")
assert_raises UndefinedFilter do
assert_raises(UndefinedFilter) do
t.render!({ 'x' => 'foo' }, strict_filters: true)
end
end
@@ -323,4 +323,18 @@ class TemplateTest < Minitest::Test
result = t.render('x' => 1, 'y' => 5)
assert_equal('12345', result)
end
def test_source_string_subclass
string_subclass = Class.new(String) do
# E.g. ActiveSupport::SafeBuffer does this, so don't just rely on to_s to return a String
def to_s
self
end
end
source = string_subclass.new("{% assign x = 2 -%} x= {{- x }}")
assert_instance_of(string_subclass, source)
output = Template.parse(source).render!
assert_equal("x=2", output)
assert_instance_of(String, output)
end
end
+31
View File
@@ -15,6 +15,33 @@ class VariableTest < Minitest::Test
assert_template_result('foobar', '{{ foo }}', 'foo' => ThingWithToLiquid.new)
end
def test_variable_lookup_calls_to_liquid_value
assert_template_result('1', '{{ foo }}', 'foo' => IntegerDrop.new('1'))
assert_template_result('2', '{{ list[foo] }}', 'foo' => IntegerDrop.new('1'), 'list' => [1, 2, 3])
assert_template_result('one', '{{ list[foo] }}', 'foo' => IntegerDrop.new('1'), 'list' => { 1 => 'one' })
assert_template_result('Yay', '{{ foo }}', 'foo' => BooleanDrop.new(true))
assert_template_result('YAY', '{{ foo | upcase }}', 'foo' => BooleanDrop.new(true))
end
def test_if_tag_calls_to_liquid_value
assert_template_result('one', '{% if foo == 1 %}one{% endif %}', 'foo' => IntegerDrop.new('1'))
assert_template_result('one', '{% if 0 < foo %}one{% endif %}', 'foo' => IntegerDrop.new('1'))
assert_template_result('one', '{% if foo > 0 %}one{% endif %}', 'foo' => IntegerDrop.new('1'))
assert_template_result('true', '{% if foo == true %}true{% endif %}', 'foo' => BooleanDrop.new(true))
assert_template_result('true', '{% if foo %}true{% endif %}', 'foo' => BooleanDrop.new(true))
assert_template_result('', '{% if foo %}true{% endif %}', 'foo' => BooleanDrop.new(false))
assert_template_result('', '{% if foo == true %}True{% endif %}', 'foo' => BooleanDrop.new(false))
end
def test_unless_tag_calls_to_liquid_value
assert_template_result('', '{% unless foo %}true{% endunless %}', 'foo' => BooleanDrop.new(true))
end
def test_case_tag_calls_to_liquid_value
assert_template_result('One', '{% case foo %}{% when 1 %}One{% endcase %}', 'foo' => IntegerDrop.new('1'))
end
def test_simple_with_whitespaces
template = Template.parse(%( {{ test }} ))
assert_equal(' worked ', template.render!('test' => 'worked'))
@@ -104,4 +131,8 @@ class VariableTest < Minitest::Test
def test_dynamic_find_var
assert_template_result('bar', '{{ [key] }}', 'key' => 'foo', 'foo' => 'bar')
end
def test_raw_value_variable
assert_template_result('bar', '{{ [key] }}', 'key' => 'foo', 'foo' => 'bar')
end
end
+48 -10
View File
@@ -72,21 +72,21 @@ module Minitest
end
def with_global_filter(*globals)
original_global_filters = Liquid::StrainerFactory.instance_variable_get(:@global_filters)
Liquid::StrainerFactory.instance_variable_set(:@global_filters, [])
globals.each do |global|
Liquid::StrainerFactory.add_global_filter(global)
end
Liquid::StrainerFactory.send(:strainer_class_cache).clear
original_global_cache = Liquid::StrainerFactory::GlobalCache
Liquid::StrainerFactory.send(:remove_const, :GlobalCache)
Liquid::StrainerFactory.const_set(:GlobalCache, Class.new(Liquid::StrainerTemplate))
globals.each do |global|
Liquid::Template.register_filter(global)
end
yield
ensure
Liquid::StrainerFactory.send(:strainer_class_cache).clear
Liquid::StrainerFactory.instance_variable_set(:@global_filters, original_global_filters)
begin
yield
ensure
Liquid::StrainerFactory.send(:remove_const, :GlobalCache)
Liquid::StrainerFactory.const_set(:GlobalCache, original_global_cache)
Liquid::StrainerFactory.send(:strainer_class_cache).clear
end
end
def with_error_mode(mode)
@@ -119,6 +119,44 @@ class ThingWithToLiquid
end
end
class IntegerDrop < Liquid::Drop
def initialize(value)
super()
@value = value.to_i
end
def ==(other)
@value == other
end
def to_s
@value.to_s
end
def to_liquid_value
@value
end
end
class BooleanDrop < Liquid::Drop
def initialize(value)
super()
@value = value
end
def ==(other)
@value == other
end
def to_liquid_value
@value
end
def to_s
@value ? "Yay" : "Nay"
end
end
class ErrorDrop < Liquid::Drop
def standard_error
raise Liquid::StandardError, 'standard error'
+27 -14
View File
@@ -10,8 +10,8 @@ class ConditionUnitTest < Minitest::Test
end
def test_basic_condition
assert_equal(false, Condition.new(1, '==', 2).evaluate)
assert_equal(true, Condition.new(1, '==', 1).evaluate)
assert_equal(false, Condition.new(1, '==', 2).evaluate(Context.new))
assert_equal(true, Condition.new(1, '==', 1).evaluate(Context.new))
end
def test_default_operators_evalute_true
@@ -67,11 +67,11 @@ class ConditionUnitTest < Minitest::Test
end
def test_hash_compare_backwards_compatibility
assert_nil(Condition.new({}, '>', 2).evaluate)
assert_nil(Condition.new(2, '>', {}).evaluate)
assert_equal(false, Condition.new({}, '==', 2).evaluate)
assert_equal(true, Condition.new({ 'a' => 1 }, '==', 'a' => 1).evaluate)
assert_equal(true, Condition.new({ 'a' => 2 }, 'contains', 'a').evaluate)
assert_nil(Condition.new({}, '>', 2).evaluate(Context.new))
assert_nil(Condition.new(2, '>', {}).evaluate(Context.new))
assert_equal(false, Condition.new({}, '==', 2).evaluate(Context.new))
assert_equal(true, Condition.new({ 'a' => 1 }, '==', 'a' => 1).evaluate(Context.new))
assert_equal(true, Condition.new({ 'a' => 2 }, 'contains', 'a').evaluate(Context.new))
end
def test_contains_works_on_arrays
@@ -106,30 +106,29 @@ class ConditionUnitTest < Minitest::Test
def test_or_condition
condition = Condition.new(1, '==', 2)
assert_equal(false, condition.evaluate)
assert_equal(false, condition.evaluate(Context.new))
condition.or(Condition.new(2, '==', 1))
assert_equal(false, condition.evaluate)
assert_equal(false, condition.evaluate(Context.new))
condition.or(Condition.new(1, '==', 1))
assert_equal(true, condition.evaluate)
assert_equal(true, condition.evaluate(Context.new))
end
def test_and_condition
condition = Condition.new(1, '==', 1)
assert_equal(true, condition.evaluate)
assert_equal(true, condition.evaluate(Context.new))
condition.and(Condition.new(2, '==', 2))
assert_equal(true, condition.evaluate)
assert_equal(true, condition.evaluate(Context.new))
condition.and(Condition.new(2, '==', 1))
assert_equal(false, condition.evaluate)
assert_equal(false, condition.evaluate(Context.new))
end
def test_should_allow_custom_proc_operator
@@ -148,6 +147,20 @@ class ConditionUnitTest < Minitest::Test
assert_evaluates_true(VariableLookup.new("one"), '==', VariableLookup.new("another"))
end
def test_default_context_is_deprecated
if Gem::Version.new(Liquid::VERSION) >= Gem::Version.new('6.0.0')
flunk("Condition#evaluate without a context argument is to be removed")
end
_out, err = capture_io do
assert_equal(true, Condition.new(1, '==', 1).evaluate)
end
expected = "DEPRECATION WARNING: Condition#evaluate without a context argument is deprecated" \
" and will be removed from Liquid 6.0.0."
assert_includes(err.lines.map(&:strip), expected)
end
private
def assert_evaluates_true(left, op, right)
+1 -1
View File
@@ -28,7 +28,7 @@ class I18nUnitTest < Minitest::Test
# end
def test_raises_unknown_translation
assert_raises I18n::TranslationError do
assert_raises(I18n::TranslationError) do
@i18n.translate("doesnt_exist")
end
end
+14
View File
@@ -26,6 +26,13 @@ class ParseTreeVisitorTest < Minitest::Test
)
end
def test_echo
assert_equal(
["test"],
visit(%({% echo test %}))
)
end
def test_if_condition
assert_equal(
["test"],
@@ -152,6 +159,13 @@ class ParseTreeVisitorTest < Minitest::Test
)
end
def test_for_range
assert_equal(
["test"],
visit(%({% for x in (1..test) %}{% endfor %}))
)
end
def test_tablerow_in
assert_equal(
["test"],
+2 -1
View File
@@ -52,7 +52,8 @@ class StrainerFactoryUnitTest < Minitest::Test
/\ALiquid error: wrong number of arguments \((1 for 0|given 1, expected 0)\)\z/,
exception.message
)
assert_equal(exception.backtrace[0].split(':')[0], __FILE__)
source = AccessScopeFilters.instance_method(:public_filter).source_location
assert_equal(source.map(&:to_s), exception.backtrace[0].split(':')[0..1])
end
def test_strainer_only_invokes_public_filter_methods
+1 -1
View File
@@ -57,8 +57,8 @@ class StrainerTemplateUnitTest < Minitest::Test
end
def test_add_filter_does_not_raise_when_module_overrides_previously_registered_method
strainer = Context.new.strainer
with_global_filter do
strainer = Context.new.strainer
strainer.class.add_filter(PublicMethodOverrideFilter)
assert(strainer.class.send(:filter_methods).include?('public_filter'))
end
+9 -4
View File
@@ -32,21 +32,26 @@ class TokenizerTest < Minitest::Test
private
def new_tokenizer(source, parse_context: Liquid::ParseContext.new, start_line_number: nil)
parse_context.new_tokenizer(source, start_line_number: start_line_number)
end
def tokenize(source)
tokenizer = Liquid::Tokenizer.new(source)
tokenizer = new_tokenizer(source)
tokens = []
while (t = tokenizer.shift)
# shift is private in Liquid::C::Tokenizer, since it is only for unit testing
while (t = tokenizer.send(:shift))
tokens << t
end
tokens
end
def tokenize_line_numbers(source)
tokenizer = Liquid::Tokenizer.new(source, true)
tokenizer = new_tokenizer(source, start_line_number: 1)
line_numbers = []
loop do
line_number = tokenizer.line_number
if tokenizer.shift
if tokenizer.send(:shift)
line_numbers << line_number
else
break
+1 -1
View File
@@ -108,7 +108,7 @@ class VariableUnitTest < Minitest::Test
assert_equal(VariableLookup.new('foo-bar'), create_variable('foo-bar').name)
assert_equal(VariableLookup.new('foo-bar-2'), create_variable('foo-bar-2').name)
with_error_mode :strict do
with_error_mode(:strict) do
assert_raises(Liquid::SyntaxError) { create_variable('foo - bar') }
assert_raises(Liquid::SyntaxError) { create_variable('-foo') }
assert_raises(Liquid::SyntaxError) { create_variable('2foo') }