Compare commits

..

196 Commits

Author SHA1 Message Date
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-Smith
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-Smith
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
ADTC
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 <dylan.smith@shopify.com>
2021-02-26 16:31:16 +08:00
ADTC
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
ADTC
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-Smith
dcb5a67089 performance: Use split limit in truncatewords (#1361) 2021-02-19 13:11:35 -05:00
Justin Li
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-Smith
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 Zhu
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-Smith
260c863e23 Build the tokenizer through the parse context for liquid-c (#1386) 2021-01-07 14:51:41 -05:00
Marc-André Cournoyer
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é Cournoyer
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 Zhu
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-Smith
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é Cournoyer
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é Cournoyer
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-Smith
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-Smith
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-Smith
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-Smith
60214b957c Store the profiler in the context instead of a thread-local variable (#1364) 2020-12-09 10:04:20 -05:00
Dylan Thacker-Smith
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-Smith
f18084203d Use monotonic time to measure durations in Liquid::Profiler (#1362) 2020-12-09 10:00:44 -05:00
Peter Zhu
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 Zhu
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-Smith
1850511334 Use an atomic subgroup in range regex to avoid pathological backtracking (#1360) 2020-11-16 10:29:36 -05:00
Peter Zhu
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
Peter Zhu
ed0aebcbc9 Merge pull request #1355 from Shopify/pz-instrument-forloop-name
Instrument forloop.name
2020-11-12 14:24:08 -05:00
Peter Zhu
ea4f1885f8 Instrument forloop.name 2020-11-12 14:19:12 -05:00
Peter Zhu
2f75db604f Merge pull request #1354 from Shopify/pz-instrument-for-offset-continue
Instrument usage of offset:continue in for loops
2020-11-12 14:14:09 -05:00
Peter Zhu
d844a3dd8b Instrument usage of offset:continue in for loops 2020-11-12 13:39:02 -05:00
Max Melentiev
9fcba1a26c Remove unused translation (#1033) 2020-11-11 10:21:04 -05:00
Peter Zhu
0659891e68 Merge pull request #1352 from Shopify/pz-test-trim-blank
Test trim without any content
2020-11-10 10:36:21 -05:00
Peter Zhu
e7fb3b18f3 Test trim without any content 2020-11-10 10:31:07 -05:00
Peter Zhu
e6eef4b2c4 Merge pull request #1350 from Shopify/pz-instrument-invalid-end-tag
Instrument invalid end tags
2020-11-10 10:06:25 -05:00
Peter Zhu
2ce577e36b Instrument for bug #1346 2020-11-09 14:27:17 -05:00
Peter Zhu
c7c21e88f0 Merge pull request #1344 from Shopify/pz-test-space-in-dot
Test space between dot for attributes
2020-11-06 10:10:08 -05:00
Peter Zhu
a89371b0b9 Test space between dot 2020-11-05 15:39:44 -05:00
Dylan Thacker-Smith
8f7f8761d1 Use Array#each instead of Array#inject to avoid an object allocation (#1341) 2020-10-29 11:24:19 -04:00
Justin Li
a3ff300419 Merge pull request #1330 from ashmaroli/exception-renderer-lambda
Stash exception_renderer lambda in a constant
2020-10-28 13:38:20 -04:00
Dylan Thacker-Smith
ea6e326b9c Fix FrozenError for blank case tag with multiple expression when tag (#1340) 2020-10-28 13:37:17 -04:00
Ashwin Maroli
740f8759cc Rename constant to RAISE_EXCEPTION_LAMBDA 2020-10-28 23:06:13 +05:30
Ashwin Maroli
bb9cd4eb6a Merge upstream branch 'master' into this branch 2020-10-28 22:14:09 +05:30
Peter Zhu
3a591fbf26 Merge pull request #1336 from ashmaroli/trigger-github-actions-on-pull-requests
Run workflows for pull requests from repo forks
2020-10-28 11:35:52 -04:00
Dylan Thacker-Smith
7754d5aef5 Attempt to strict parse variables before lax parsing in lax error mode (#1338) 2020-10-28 10:37:00 -04:00
Dylan Thacker-Smith
1d63d5db5f Fix a leaky test that set Tempate.error_mode without resetting it (#1339) 2020-10-28 10:36:33 -04:00
Ashwin Maroli
26640368e5 Run workflows for pull requests from repo forks 2020-10-28 12:45:10 +05:30
Dylan Thacker-Smith
f23c2a83f2 Fix lax parsing expressions surrounded by spaces (#1335)
to make it compatible with strict parsing and liquid-c
2020-10-27 14:53:57 -04:00
Peter Zhu
61d54d1b19 Merge pull request #1331 from Shopify/pz-freeze-block
Freeze block body after parsing completes
2020-10-27 13:17:54 -04:00
Dylan Thacker-Smith
10ea6144e0 Add Liquid::ParseContext#parse_expression for liquid-c node disabling (#1333)
We would like to be able to disable liquid-c VM rendering at runtime,
but right now expression parsing is done using Expression.parse, which
isn't aware of the parse context.  That prevents us from conditionally
compiling to VM code based on a parse option.
2020-10-27 11:00:04 -04:00
Peter Zhu
292d971937 Merge loops 2020-10-27 10:42:30 -04:00
Peter Zhu
5c082472a1 Address comments 2020-10-26 16:16:30 -04:00
Peter Zhu
0bedc71854 Address comments 2020-10-26 15:11:00 -04:00
Peter Zhu
fe66edb825 Freeze block body after parsing completes 2020-10-26 11:06:55 -04:00
Ashwin Maroli
bfa2df7036 Stash exception_renderer lambda in a constant 2020-10-26 19:44:00 +05:30
Ashwin Maroli
0e52706a5b Remove redundant comment in Liquid::Template (#1328) 2020-10-22 12:49:02 -04:00
Dylan Thacker-Smith
4c6166f989 Add parsing quirk test for lookup on variable with literal name (#1325) 2020-10-21 16:30:17 -04:00
Justin Li
8e99b3bd7f Merge pull request #1322 from ashmaroli/else-tag-names
Stash array of tag names in a constant
2020-10-21 12:09:14 -04:00
Dylan Thacker-Smith
f6532de1fd Merge pull request #1323 from Shopify/assign-score-hash
Avoid allocating arrays of key value pairs in assign_score_of
2020-10-21 11:18:35 -04:00
Dylan Thacker-Smith
001fde7694 Avoid allocating arrays of key value pairs for hashes in assign_score_of 2020-10-21 10:36:00 -04:00
Dylan Thacker-Smith
b872eac2b9 More comprehensively test assign_score_of 2020-10-21 10:35:56 -04:00
Dylan Thacker-Smith
038d0585cf Move some assign score increment tests to the tag that increments 2020-10-21 10:21:00 -04:00
Ashwin Maroli
b15428ea83 Stash array of tag names in a constant 2020-10-21 18:50:56 +05:30
Dylan Thacker-Smith
c9ad9d338c Extract method for raising a syntax error in the assign tag for liquid-c (#1321) 2020-10-20 16:59:52 -04:00
Dylan Thacker-Smith
ae6bd9f6b0 Allow an empty variable tag during strict parsing for liquid-c compat (#1320) 2020-10-20 14:11:48 -04:00
Dylan Thacker-Smith
866e437c05 Test tag disabling using custom tags (#1318)
Since I don't think we have any use case to disable the `raw` or
`echo` tags, so I would like liquid-c to not have to support that
2020-10-19 16:32:02 -04:00
Dylan Thacker-Smith
784db053f2 Merge pull request #1317 from Shopify/strict-parse-dynamic-find-var
Fix strict parsing of find variable with a name expression
2020-10-19 13:43:26 -04:00
Dylan Thacker-Smith
ff1c6bd26e Actually remove test file with no extension moved into another test file (#1316) 2020-10-19 12:40:02 -04:00
Dylan Thacker-Smith
46fd63da5f Fix strict parsing of find variable with a name expression 2020-10-19 12:17:25 -04:00
Dylan Thacker-Smith
420a1c79e1 Refactor variable lookup strict parsing to reduce coupling on dot lookup 2020-10-19 12:10:32 -04:00
Dylan Thacker-Smith
6d39050e1e Use a case statement in Liquid::Parser#expression 2020-10-19 12:10:11 -04:00
Dylan Thacker-Smith
077bf2a409 Test reporting of liquid error for filter call with wrong number of arguments (#1311) 2020-10-08 11:55:40 -04:00
Dylan Thacker-Smith
1a3e38c018 Merge pull request #1310 from Shopify/only-integration-test-liquid-c
Fix liquid-c integration testing
2020-10-08 11:52:50 -04:00
Dylan Thacker-Smith
e495f75cc2 Remove support for ruby 2.4, which is no longer supported upstream 2020-10-08 09:48:16 -04:00
Dylan Thacker-Smith
e781449c36 Remove root directory from library search path for tests
It isn't in the gemspec's require_path, so we shouldn't add any dependence
on it.
2020-10-08 01:53:11 -04:00
Dylan Thacker-Smith
7eb03ea198 Only test liquid-c integration using the integration tests 2020-10-08 01:52:40 -04:00
Peter Zhu
bd34cd5613 Merge pull request #1308 from Shopify/pz-gh-actions
Use GitHub Actions for CI
2020-10-07 14:38:15 -04:00
Peter Zhu
c28d455f7b Use GitHub Actions for CI 2020-10-07 13:29:39 -04:00
Dylan Thacker-Smith
d250a7f502 Set Context#initialize instance variables before squashing assigns (#1307) 2020-10-06 21:00:08 -04:00
Peter Zhu
b0f46326ca Merge pull request #1306 from Shopify/pz-raise-tag-never-closed
Refactor raising tag never closed error to method
2020-10-06 17:13:35 -04:00
Peter Zhu
7aed2f122c Refactor raising tag never closed to method 2020-10-06 15:55:55 -04:00
Peter Zhu
5199a34d9b Merge pull request #1304 from Shopify/pz-raw-bug
Fix duplication of text in raw tags
2020-10-05 10:59:15 -04:00
Peter Zhu
4c2ab6f878 Fix bug in raw tags 2020-10-05 10:47:28 -04:00
Dylan Thacker-Smith
a818dd9d19 Fix test with missing extension (#1302) 2020-09-30 13:44:28 -04:00
Dylan Thacker-Smith
efef03d944 Merge pull request #1294 from Shopify/changes-for-liquid-c-vm-variable
Refactor to support liquid-c VM compilation of variables
2020-09-29 21:02:26 -04:00
Dylan Thacker-Smith
33760f083a Extract rescue code from BlockBody#render_node for re-use in liquid-c 2020-09-25 11:24:39 -04:00
Dylan Thacker-Smith
013802c877 Move some unit tests without internal coupling to integration tests
since I would like to continue supporting these tests in liquid-c
in the foreseeable future.
2020-09-25 11:24:39 -04:00
Dylan Thacker-Smith
3dcad3b3cd Move test/integration/parse_tree_visitor_test.rb to test/unit
The ParseTreeVisitor exposes the liquid internals that won't be
kept compatible with liquid-c, so move it out of the integration
tests directory so that we can easily ignore it when testing liquid-c
2020-09-25 11:24:39 -04:00
Dylan Thacker-Smith
db065315ba Allow creating symbols that are garbage collected in a test 2020-09-25 11:24:39 -04:00
Dylan Thacker-Smith
a03f02789b Only use MethodLiteral in condition expressions (#1300) 2020-09-25 11:10:33 -04:00
Dylan Thacker-Smith
ca4b9b43af Port liquid-c bug compatible whitespace trimming (#1291) 2020-09-16 16:07:36 -04:00
Dylan Thacker-Smith
77084930e9 Bring back silencing of errors in blank nodes for backwards compatibility (#1292) 2020-09-15 10:35:18 -04:00
Dylan Thacker-Smith
fb77921b15 Merge pull request #1290 from Shopify/document-unknown-tag-refactor
Pass the tag markup and tokenizer to Document#unknown_tag
2020-09-11 09:34:16 -04:00
Dylan Thacker-Smith
0d02dea20b Rename Liquid::Block#unknown_tag parameters for clarity 2020-09-11 09:33:12 -04:00
Dylan Thacker-Smith
86b47ba28b Pass the tag markup and tokenizer to Document#unknown_tag
The parse_context no longer needs to be passed in because it is available
through through an attr_reader on the instance. However, the markup and
tokenizer weren't made available.  This refactor also makes the parameters
given to Document#unknown_tag consistent with Block#unknown_tag.
2020-09-11 09:33:12 -04:00
Dylan Thacker-Smith
95ff0595c6 Merge pull request #1289 from Shopify/refactor-for-c-block-body
Avoid direct coupling to BlockBody instances for liquid-c replacement
2020-09-11 09:15:58 -04:00
Dylan Thacker-Smith
bbc56f35ec Add ParseContext#new_block_body to centralize the liquid-c override point 2020-09-09 12:25:35 -04:00
Dylan Thacker-Smith
dfbbf87ba9 Use BlockBody from Document using composition rather than inheritence
This way liquid-c can more cleanly use a Liquid::C::BlockBody object
for the block body by overriding Liquid::Document#new_body.
2020-09-08 14:00:52 -04:00
Dylan Thacker-Smith
037b603603 Turn some Liquid::BlockBody methods into class methods for liquid-c
So they can be used from a Liquid::C::BlockBody
2020-09-08 14:00:48 -04:00
Dylan Thacker-Smith
bd33df09de Provide Block#new_body so that liquid-c can override it
This way liquid-c can return a body of a different class that wraps
a C implementation.
2020-09-08 13:59:48 -04:00
Dylan Thacker-Smith
6ca5b62112 Merge pull request #1285 from Shopify/fix-render-length-resource-limit
Fix render length resource limit so it doesn't multiply nested output
2020-09-08 13:57:30 -04:00
Dylan Thacker-Smith
e1a2057a1b Update assign_score during capturing
To stop long captures before they grow the heap more then they should.
2020-09-03 11:13:08 -04:00
Dylan Thacker-Smith
ae9dbe0ca7 Fix render length resource limit so it doesn't multiply nested output 2020-09-03 11:13:04 -04:00
Dylan Thacker-Smith
3b486425b0 Handle BlockBody#blank? at parse time (#1287) 2020-09-03 11:07:13 -04:00
Dylan Thacker-Smith
b08bcf00ac Push interrupts from Continue and Break tags rather than from BlockBody (#1286) 2020-09-03 06:55:24 -04:00
Dylan Thacker-Smith
0740e8b431 Remove unused quirk allowing liquid tags to close a block it is nested in (#1284) 2020-09-03 06:51:56 -04:00
Dylan Thacker-Smith
5532df880f Handle disabled tags errors like other liquid errors (#1275) 2020-08-18 11:39:54 -04:00
Dylan Thacker-Smith
2b11efc3ae Fix performance regression from introduction of Template#disable_tags (#1274) 2020-08-18 11:25:51 -04:00
Thierry Joyal
a1d982ca76 Merge pull request #1272 from Shopify/StaticRegisters/add-test-coverage
[StaticRegisters] Add test coverage
2020-08-04 08:31:45 -04:00
Thierry Joyal
03be7f1ee3 [StaticRegisters] Add test coverage 2020-07-28 10:23:51 -04:00
Dylan Thacker-Smith
1ced4eaf10 Merge pull request #1268 from Shopify/remove-taint-checking
Remove support for taint checking
2020-07-25 21:27:46 -04:00
Dylan Thacker-Smith
4970167726 Bump rake development dependency
Gets rid of a deprecation warning when running the tests.
2020-07-23 16:23:18 -04:00
Dylan Thacker-Smith
065ccbc4aa Remove support for taint checking 2020-07-23 16:22:46 -04:00
Feken Baboyan
1feaa63813 Merge pull request #1258 from Shopify/fix-context-overriding-in-templates
Fix how Template overrides static registers when #render is invoked
2020-05-28 09:32:31 -04:00
Feken Baboyan
8541c6be35 make Template override static registers only when the register key is not defined 2020-05-28 09:08:03 -04:00
Thierry Joyal
18654526c8 Merge pull request #1257 from Shopify/StaticRegisters/remove-registers-attr-reader
[StaticRegisters] Remove registers attr_reader
2020-05-22 14:01:37 -04:00
Thierry Joyal
bd1f7f9492 [StaticRegisters] Remove assertion for delete to not remove static content 2020-05-22 13:42:44 -04:00
Thierry Joyal
40d75dd283 Update test/unit/static_registers_unit_test.rb
Co-authored-by: Dylan Thacker-Smith <dylan.smith@shopify.com>
2020-05-22 12:15:52 -04:00
Thierry Joyal
f5011365f1 [StaticRegisters] Remove registers attr_reader 2020-05-22 10:51:07 -04:00
Thierry Joyal
ebbd046c92 Merge pull request #1250 from Shopify/static-registers/fetch-raise-on-missing
[StaticRegisters] Fetch raise on missing
2020-05-22 09:56:05 -04:00
Thierry Joyal
b9979088ec [StaticRegisters] Fetch raise on missing
Co-authored-by: Dylan Thacker-Smith <dylan.smith@shopify.com>
2020-05-22 09:35:47 -04:00
Dylan Thacker-Smith
bd0e53bd2e Merge pull request #1239 from Shopify/remove-bad-arity-assumption
Fix ParseTreeVisitorTest for ruby-head
2020-05-21 14:02:04 -04:00
Thierry Joyal
4b586f4105 Merge pull request #1251 from Shopify/travis/optional-head
[Travis] Optional head
2020-05-21 13:51:11 -04:00
Thierry Joyal
0410119d5f [Travis] Optional head 2020-05-21 12:45:14 -04:00
Dylan Thacker-Smith
c2f67398d0 Allow ruby-head failures
Ignore an object allocation test failure on ruby-head for now.
2020-03-31 10:53:49 -04:00
Dylan Thacker-Smith
81149344a5 Fix ParseTreeVisitorTest for ruby-head 2020-03-31 10:53:46 -04:00
Dylan Thacker-Smith
e9b649b345 Fix Liquid::Template inheritance (#1227)
self.class.default_resource_limits would return `nil` in a subclass, since
the attribute isn't set on subclasses.
2020-01-21 15:09:22 -05:00
Celso Dantas
9c538f4237 Merge pull request #1207 from Shopify/moving-const-to-const
Use String literal instead
2020-01-20 12:37:57 -05:00
Celso Dantas
c08a358a2b Use String literal instead of using a class method
The class method string definition is not needed here, so it can be removed.
2020-01-16 09:42:32 -05:00
Justin Li
dbaef5e79b Merge pull request #1180 from Shopify/test-all-filters
Test all filters against random data to detect exceptions
2020-01-13 15:52:44 -05:00
Dylan Thacker-Smith
48a155a213 Initialize Liquid::Template class attributes eagerly instead of lazily (#1223) 2020-01-10 17:42:01 -05:00
Dylan Thacker-Smith
c69a9a77c6 Merge pull request #1215 from Shopify/nested-liquid-tag
Fix liquid tag nested in outer block
2020-01-09 19:24:52 -05:00
Dylan Thacker-Smith
ef79fa3898 style: Avoid deep nesting for parsing the liquid tag in the block body 2020-01-09 19:13:13 -05:00
Dylan Thacker-Smith
f7ad602bfc Fix liquid tag nested in outer block 2020-01-09 19:13:13 -05:00
Dylan Thacker-Smith
ffd6049ba2 Merge pull request #1222 from Shopify/bump-ci-ruby
Test against the latest ruby in CI
2020-01-09 18:12:36 -05:00
Dylan Thacker-Smith
b3ad54c0c2 Test against the latest ruby in CI 2020-01-09 13:15:34 -05:00
Dylan Thacker-Smith
67eca3f58d Upgrade rubocop and style guide for ruby 2.7 compatibility 2020-01-09 13:15:34 -05:00
Thierry Joyal
0847bf560f Merge pull request #1218 from Shopify/strainer/remove-safe-navigation
Remove handling of a nil context in the Strainer class
2020-01-07 11:54:49 -05:00
Dylan Thacker-Smith
8074565c3e Merge pull request #1216 from Shopify/unsupported-taint-mode
Remove support for taint_mode on ruby versions that don't support it
2020-01-07 10:12:36 -05:00
Thierry Joyal
24e81267b9 Merge pull request #1208 from Shopify/strainer/revisit
[Strainer] Separate factory from template
2020-01-06 13:56:22 -05:00
Thierry Joyal
c0ffee3ff9 [Strainer] Remove safe navigation 2020-01-06 18:34:13 +00:00
Thierry Joyal
c0ffeeef26 [Strainer] Separate factory from template 2020-01-06 17:45:25 +00:00
Dylan Thacker-Smith
22dbf90b7d Try to stay compatible with ruby-head 2019-12-19 11:12:55 -05:00
Dylan Thacker-Smith
40c68c9c83 Remove support for taint_mode on ruby versions that don't support it 2019-12-19 11:12:51 -05:00
Martin Morissette
b7f0f158ab Merge pull request #1212 from Shopify/template-factory
Introduce template factory
2019-12-19 08:52:22 -05:00
Martin Morissette
d8f31046a9 Introduce template factory 2019-12-17 21:45:08 -05:00
Martin Morissette
6c6382ed69 Merge pull request #1213 from Shopify/allow-failures-ruby-27
Make ruby 2.7 optional in CI
2019-12-16 09:46:12 -05:00
Martin Morissette
53ba1372f9 Make ruby 2.7 optional in CI 2019-12-16 08:59:26 -05:00
Mike Angell
57c9cf64eb Allow render to handle with and for correctly (#1193)
* Allow render to handle with and for correctly

* code improvements
2019-10-23 04:12:46 +10:00
Alessandro Diogo Brückheimer
e83b1e4159 Add ForceEqualSignAlignment to .rubocop.yml (#1190)
* Add ForceEqualSignAlignment to .rubocop.yml

* Revert ForceEqualSignAlignment cop

* Update method alignment

* Undo addition of whitespace to improve readability

* Fix missing alignment
2019-10-21 21:18:48 +10:00
Mike Angell
3784020a8d [New Feature] Add forloop inside render tag when using for syntax (#1191)
* Add forloop to render for syntax

* Remove forloop guard
2019-10-17 23:06:13 +10:00
uchoudh
1223444738 Fix flaky tests (#1186) 2019-10-12 02:52:07 +11:00
Mike Angell
2bfeed2b00 Resolve InputIterator dropping context (#1184)
* Resolve InputIterator dropping context

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

* Remove duplicate code

* Default to template name

* Improve variable matching

* Extract render_partial

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

* Change to named parameter

* Remove redundant freeze

* add brackets to make intention clearer

* Use named param format from liquid

* Update syntax

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

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

* Improvements to disable tag

* Resolve disbale tag tests

* Test disable_tags register

* disabled_tags is now always avaiable

* Allow multiple tags to be disabled at once

* Move disabled check to block_body

* Code improvements

* Remove redundant nil check

* Improve disabled tag error output

* Improve disable tag API

* Code improvements

* Switch disabled? to not mutate output

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

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

* Add frozen test to static registers
2019-09-20 00:24:48 +10:00
Mike Angell
adb40c41b7 Enable frozen_string_literal 2019-09-18 13:40:07 +10:00
124 changed files with 3823 additions and 3891 deletions

39
.github/workflows/liquid.yml vendored Normal file
View File

@@ -0,0 +1,39 @@
name: Liquid
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
entry:
- { ruby: 2.5, allowed-failure: false } # minimum supported
- { ruby: 3.0, allowed-failure: false } # latest
- { ruby: ruby-head, allowed-failure: true }
name: test (${{ matrix.entry.ruby }})
steps:
- uses: actions/checkout@v2
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.entry.ruby }}
- uses: actions/cache@v1
with:
path: vendor/bundle
key: ${{ runner.os }}-gems-${{ hashFiles('Gemfile') }}
restore-keys: ${{ runner.os }}-gems-
- run: bundle install --jobs=3 --retry=3 --path=vendor/bundle
- run: bundle exec rake
continue-on-error: ${{ matrix.entry.allowed-failure }}
memory_profile:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: ruby/setup-ruby@v1
with:
ruby-version: 2.7
- uses: actions/cache@v1
with:
path: vendor/bundle
key: ${{ runner.os }}-gems-${{ hashFiles('Gemfile') }}
restore-keys: ${{ runner.os }}-gems-
- run: bundle install --jobs=3 --retry=3 --path=vendor/bundle
- run: bundle exec rake memory_profile:run

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,7 @@
inherit_gem:
rubocop-shopify: rubocop.yml
inherit_from: inherit_from:
- https://shopify.github.io/ruby-style-guide/rubocop.yml
- .rubocop_todo.yml - .rubocop_todo.yml
require: rubocop-performance require: rubocop-performance
@@ -8,9 +10,11 @@ Performance:
Enabled: true Enabled: true
AllCops: AllCops:
TargetRubyVersion: 2.4
NewCops: disable
Exclude: Exclude:
- 'vendor/bundle/**/*' - 'vendor/bundle/**/*'
Naming/MethodName: Naming/MethodName:
Exclude: Exclude:
- 'example/server/liquid_servlet.rb' - 'example/server/liquid_servlet.rb'

View File

@@ -1,31 +1,11 @@
# This configuration was generated by # This configuration was generated by
# `rubocop --auto-gen-config` # `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 # The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base. # one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new # Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again. # versions of RuboCop, may require this file to be generated again.
# Offense count: 2
Lint/AmbiguousOperator:
Exclude:
- 'test/unit/condition_unit_test.rb'
# Offense count: 21
# Configuration parameters: AllowSafeAssignment.
Lint/AssignmentInCondition:
Exclude:
- 'lib/liquid/block_body.rb'
- 'lib/liquid/lexer.rb'
- 'lib/liquid/standardfilters.rb'
- 'lib/liquid/tags/for.rb'
- 'lib/liquid/tags/if.rb'
- 'lib/liquid/tags/raw.rb'
- 'lib/liquid/variable.rb'
- 'performance/profile.rb'
- 'test/test_helper.rb'
- 'test/unit/tokenizer_unit_test.rb'
# Offense count: 2 # Offense count: 2
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle. # Configuration parameters: EnforcedStyle.
@@ -34,25 +14,26 @@ Lint/InheritException:
Exclude: Exclude:
- 'lib/liquid/interrupts.rb' - 'lib/liquid/interrupts.rb'
# Offense count: 2 # Offense count: 113
Lint/UselessAssignment:
Exclude:
- 'performance/shopify/database.rb'
# Offense count: 1
# Configuration parameters: CheckForMethodsWithNoSideEffects.
Lint/Void:
Exclude:
- 'lib/liquid/parse_context.rb'
# Offense count: 98
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. # Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
# URISchemes: http, https # URISchemes: http, https
Metrics/LineLength: 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: Naming/ConstantName:
Exclude: Exclude:
- 'lib/liquid.rb' - 'lib/liquid.rb'
@@ -63,7 +44,6 @@ Naming/ConstantName:
- 'lib/liquid/tags/cycle.rb' - 'lib/liquid/tags/cycle.rb'
- 'lib/liquid/tags/for.rb' - 'lib/liquid/tags/for.rb'
- 'lib/liquid/tags/if.rb' - 'lib/liquid/tags/if.rb'
- 'lib/liquid/tags/include.rb'
- 'lib/liquid/tags/raw.rb' - 'lib/liquid/tags/raw.rb'
- 'lib/liquid/tags/table_row.rb' - 'lib/liquid/tags/table_row.rb'
- 'lib/liquid/variable.rb' - 'lib/liquid/variable.rb'
@@ -71,24 +51,7 @@ Naming/ConstantName:
- 'performance/shopify/paginate.rb' - 'performance/shopify/paginate.rb'
- 'test/integration/tags/include_tag_test.rb' - 'test/integration/tags/include_tag_test.rb'
# Offense count: 5 # Offense count: 2
Style/ClassVars: Style/ClassVars:
Exclude: Exclude:
- 'lib/liquid/condition.rb' - 'lib/liquid/condition.rb'
- 'lib/liquid/strainer.rb'
- 'lib/liquid/template.rb'
# Offense count: 1
# Configuration parameters: AllowCoercion.
Style/DateTime:
Exclude:
- 'test/unit/context_unit_test.rb'
# Offense count: 9
# Cop supports --auto-correct.
# Configuration parameters: AllowAsExpressionSeparator.
Style/Semicolon:
Exclude:
- 'test/integration/error_handling_test.rb'
- 'test/integration/template_test.rb'
- 'test/unit/context_unit_test.rb'

View File

@@ -1,30 +0,0 @@
language: ruby
cache: bundler
rvm:
- 2.4
- 2.5
- &latest_ruby 2.6
- 2.7
- ruby-head
- jruby-head
- truffleruby
matrix:
include:
- rvm: *latest_ruby
script: bundle exec rake memory_profile:run
name: Profiling Memory Usage
allow_failures:
- rvm: ruby-head
- rvm: jruby-head
- rvm: truffleruby
branches:
only:
- master
- gh-pages
- /.*-stable/
notifications:
disable: true

View File

@@ -5,6 +5,7 @@
* Bugfixes * Bugfixes
* Performance improvements * Performance improvements
* Features that are likely to be useful to the majority of Liquid users * Features that are likely to be useful to the majority of Liquid users
* Documentation updates that are concise and likely to be useful to the majority of Liquid users
## Things we won't merge ## Things we won't merge
@@ -14,12 +15,14 @@
* Features that can easily be implemented on top of Liquid (for example as a custom filter or custom filesystem) * Features that can easily be implemented on top of Liquid (for example as a custom filter or custom filesystem)
* Code that does not include tests * Code that does not include tests
* Code that breaks existing tests * Code that breaks existing tests
* Documentation changes that are verbose, incorrect or not important to most people (we want to keep it simple and easy to understand)
## Workflow ## Workflow
* [Sign the CLA](https://cla.shopify.com/) if you haven't already
* Fork the Liquid repository * Fork the Liquid repository
* Create a new branch in your fork * Create a new branch in your fork
* For updating [Liquid documentation](https://shopify.github.io/liquid/), create it from `gh-pages` branch. (You can skip tests.)
* If it makes sense, add tests for your code and/or run a performance benchmark * If it makes sense, add tests for your code and/or run a performance benchmark
* Make sure all tests pass (`bundle exec rake`) * Make sure all tests pass (`bundle exec rake`)
* Create a pull request * Create a pull request

View File

@@ -18,7 +18,8 @@ group :benchmark, :test do
end end
group :test do group :test do
gem 'rubocop', '~> 0.74.0', require: false gem 'rubocop', '~> 1.4', require: false
gem 'rubocop-shopify', '~> 1.0.7', require: false
gem 'rubocop-performance', require: false gem 'rubocop-performance', require: false
platform :mri, :truffleruby do platform :mri, :truffleruby do

View File

@@ -1,5 +1,38 @@
# Liquid Change Log # Liquid Change Log
## 5.0.0 / 2021-01-06
### Features
* Add new `{% render %}` tag (#1122) [Samuel Doiron]
* Add support for `as` in `{% render %}` and `{% include %}` (#1181) [Mike Angell]
* Add `{% liquid %}` and `{% echo %}` tags (#1086) [Justin Li]
* Add [usage tracking](README.md#usage-tracking) [Mike Angell]
* Add `Tag.disable_tags` for disabling tags that prepend `Tag::Disableable` at render time (#1162, #1274, #1275) [Mike Angell]
* Support using a profiler for multiple renders (#1365, #1366) [Dylan Thacker-Smith]
### Fixes
* Fix catastrophic backtracking in `RANGES_REGEX` regular expression (#1357) [Dylan Thacker-Smith]
* Make sure the for tag's limit and offset are integers (#1094) [David Cornu]
* Invokable methods for enumerable reject include (#1151) [Thierry Joyal]
* Allow `default` filter to handle `false` as value (#1144) [Mike Angell]
* Fix render length resource limit so it doesn't multiply nested output (#1285) [Dylan Thacker-Smith]
* Fix duplication of text in raw tags (#1304) [Peter Zhu]
* Fix strict parsing of find variable with a name expression (#1317) [Dylan Thacker-Smith]
* Use monotonic time to measure durations in Liquid::Profiler (#1362) [Dylan Thacker-Smith]
### Breaking Changes
* Require Ruby >= 2.5 (#1131, #1310) [Mike Angell, Dylan Thacker-Smith]
* Remove support for taint checking (#1268) [Dylan Thacker-Smith]
* Split Strainer class into StrainerFactory and StrainerTemplate (#1208) [Thierry Joyal]
* Remove handling of a nil context in the Strainer class (#1218) [Thierry Joyal]
* Handle `BlockBody#blank?` at parse time (#1287) [Dylan Thacker-Smith]
* Pass the tag markup and tokenizer to `Document#unknown_tag` (#1290) [Dylan Thacker-Smith]
* And several internal changes
### Performance Improvements
* Reduce allocations (#1073, #1091, #1115, #1099, #1117, #1141, #1322, #1341) [Richard Monette, Florian Weingarten, Ashwin Maroli]
* Improve resources limits performance (#1093, #1323) [Florian Weingarten, Dylan Thacker-Smith]
## 4.0.3 / 2019-03-12 ## 4.0.3 / 2019-03-12
### Fixed ### Fixed

View File

@@ -9,9 +9,15 @@ task(default: [:test, :rubocop])
desc('run test suite with default parser') desc('run test suite with default parser')
Rake::TestTask.new(:base_test) do |t| Rake::TestTask.new(:base_test) do |t|
t.libs << '.' << 'lib' << 'test' t.libs << 'lib' << 'test'
t.test_files = FileList['test/{integration,unit}/**/*_test.rb'] t.test_files = FileList['test/{integration,unit}/**/*_test.rb']
t.verbose = false t.verbose = false
end
Rake::TestTask.new(:integration_test) do |t|
t.libs << 'lib' << 'test'
t.test_files = FileList['test/integration/**/*_test.rb']
t.verbose = false
end end
desc('run test suite with warn error mode') desc('run test suite with warn error mode')
@@ -40,12 +46,12 @@ task :test do
ENV['LIQUID_C'] = '1' ENV['LIQUID_C'] = '1'
ENV['LIQUID_PARSER_MODE'] = 'lax' ENV['LIQUID_PARSER_MODE'] = 'lax'
Rake::Task['base_test'].reenable Rake::Task['integration_test'].reenable
Rake::Task['base_test'].invoke Rake::Task['integration_test'].invoke
ENV['LIQUID_PARSER_MODE'] = 'strict' ENV['LIQUID_PARSER_MODE'] = 'strict'
Rake::Task['base_test'].reenable Rake::Task['integration_test'].reenable
Rake::Task['base_test'].invoke Rake::Task['integration_test'].invoke
end end
end end

View File

@@ -12,16 +12,16 @@ class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet
private private
def handle(_type, req, res) def handle(_type, req, res)
@request = req @request = req
@response = res @response = res
@request.path_info =~ /(\w+)\z/ @request.path_info =~ /(\w+)\z/
@action = Regexp.last_match(1) || 'index' @action = Regexp.last_match(1) || 'index'
@assigns = send(@action) if respond_to?(@action) @assigns = send(@action) if respond_to?(@action)
@response['Content-Type'] = "text/html" @response['Content-Type'] = "text/html"
@response.status = 200 @response.status = 200
@response.body = Liquid::Template.parse(read_template).render(@assigns, filters: [ProductsFilter]) @response.body = Liquid::Template.parse(read_template).render(@assigns, filters: [ProductsFilter])
end end
def read_template(filename = @action) def read_template(filename = @action)

View File

@@ -42,6 +42,8 @@ module Liquid
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om
VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o
RAISE_EXCEPTION_LAMBDA = ->(_e) { raise }
singleton_class.send(:attr_accessor, :cache_classes) singleton_class.send(:attr_accessor, :cache_classes)
self.cache_classes = true self.cache_classes = true
end end
@@ -57,11 +59,14 @@ require 'liquid/forloop_drop'
require 'liquid/extensions' require 'liquid/extensions'
require 'liquid/errors' require 'liquid/errors'
require 'liquid/interrupts' require 'liquid/interrupts'
require 'liquid/strainer' require 'liquid/strainer_factory'
require 'liquid/strainer_template'
require 'liquid/expression' require 'liquid/expression'
require 'liquid/context' require 'liquid/context'
require 'liquid/parser_switching' require 'liquid/parser_switching'
require 'liquid/tag' require 'liquid/tag'
require 'liquid/tag/disabler'
require 'liquid/tag/disableable'
require 'liquid/block' require 'liquid/block'
require 'liquid/block_body' require 'liquid/block_body'
require 'liquid/document' require 'liquid/document'
@@ -78,7 +83,9 @@ require 'liquid/tokenizer'
require 'liquid/parse_context' require 'liquid/parse_context'
require 'liquid/partial_cache' require 'liquid/partial_cache'
require 'liquid/usage' require 'liquid/usage'
require 'liquid/register'
require 'liquid/static_registers' require 'liquid/static_registers'
require 'liquid/template_factory'
# Load all the tags of the standard library # Load all the tags of the standard library
# #

View File

@@ -10,9 +10,10 @@ module Liquid
end end
def parse(tokens) def parse(tokens)
@body = BlockBody.new @body = new_body
while parse_body(@body, tokens) while parse_body(@body, tokens)
end end
@body.freeze
end end
# For backwards compatibility # For backwards compatibility
@@ -28,7 +29,12 @@ module Liquid
@body.nodelist @body.nodelist
end end
def unknown_tag(tag, _params, _tokens) def unknown_tag(tag_name, _markup, _tokenizer)
Block.raise_unknown_tag(tag_name, block_name, block_delimiter, parse_context)
end
# @api private
def self.raise_unknown_tag(tag, block_name, block_delimiter, parse_context)
if tag == 'else' if tag == 'else'
raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_else", raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_else",
block_name: block_name) block_name: block_name)
@@ -42,6 +48,10 @@ module Liquid
end end
end end
def raise_tag_never_closed(block_name)
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_never_closed", block_name: block_name)
end
def block_name def block_name
@tag_name @tag_name
end end
@@ -50,8 +60,14 @@ module Liquid
@block_delimiter ||= "end#{block_name}" @block_delimiter ||= "end#{block_name}"
end end
protected private
# @api public
def new_body
parse_context.new_block_body
end
# @api public
def parse_body(body, tokens) def parse_body(body, tokens)
if parse_context.depth >= MAX_DEPTH if parse_context.depth >= MAX_DEPTH
raise StackLevelError, "Nesting too deep" raise StackLevelError, "Nesting too deep"
@@ -62,9 +78,7 @@ module Liquid
@blank &&= body.blank? @blank &&= body.blank?
return false if end_tag_name == block_delimiter return false if end_tag_name == block_delimiter
unless end_tag_name raise_tag_never_closed(block_name) unless end_tag_name
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_never_closed", block_name: block_name)
end
# this tag is not registered with the system # this tag is not registered with the system
# pass it to the current block for special handling or error reporting # pass it to the current block for special handling or error reporting

View File

@@ -1,22 +1,26 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'English'
module Liquid module Liquid
class BlockBody class BlockBody
LiquidTagToken = /\A\s*(\w+)\s*(.*?)\z/o LiquidTagToken = /\A\s*(\w+)\s*(.*?)\z/o
FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(\w+)(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(\w+)(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
WhitespaceOrNothing = /\A\s*\z/ WhitespaceOrNothing = /\A\s*\z/
TAGSTART = "{%" TAGSTART = "{%"
VARSTART = "{{" VARSTART = "{{"
attr_reader :nodelist attr_reader :nodelist
def initialize def initialize
@nodelist = [] @nodelist = []
@blank = true @blank = true
end end
def parse(tokenizer, parse_context, &block) def parse(tokenizer, parse_context, &block)
raise FrozenError, "can't modify frozen Liquid::BlockBody" if frozen?
parse_context.line_number = tokenizer.line_number parse_context.line_number = tokenizer.line_number
if tokenizer.for_liquid_tag if tokenizer.for_liquid_tag
@@ -26,8 +30,13 @@ module Liquid
end end
end end
def freeze
@nodelist.freeze
super
end
private def parse_for_liquid_tag(tokenizer, parse_context) private def parse_for_liquid_tag(tokenizer, parse_context)
while token = tokenizer.shift while (token = tokenizer.shift)
unless token.empty? || token =~ WhitespaceOrNothing unless token.empty? || token =~ WhitespaceOrNothing
unless token =~ LiquidTagToken unless token =~ LiquidTagToken
# line isn't empty but didn't match tag syntax, yield and let the # line isn't empty but didn't match tag syntax, yield and let the
@@ -35,8 +44,8 @@ module Liquid
return yield token, token return yield token, token
end end
tag_name = Regexp.last_match(1) tag_name = Regexp.last_match(1)
markup = Regexp.last_match(2) markup = Regexp.last_match(2)
unless tag = registered_tags[tag_name] unless (tag = registered_tags[tag_name])
# end parsing if we reach an unknown tag and let the caller decide # end parsing if we reach an unknown tag and let the caller decide
# determine how to proceed # determine how to proceed
return yield tag_name, markup return yield tag_name, markup
@@ -51,17 +60,66 @@ module Liquid
yield nil, nil yield nil, nil
end end
private def parse_for_document(tokenizer, parse_context, &block) # @api private
while token = tokenizer.shift def self.unknown_tag_in_liquid_tag(tag, parse_context)
Block.raise_unknown_tag(tag, 'liquid', '%}', parse_context)
end
# @api private
def self.raise_missing_tag_terminator(token, parse_context)
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_termination", token: token, tag_end: TagEnd.inspect)
end
# @api private
def self.raise_missing_variable_terminator(token, parse_context)
raise SyntaxError, parse_context.locale.t("errors.syntax.variable_termination", token: token, tag_end: VariableEnd.inspect)
end
# @api private
def self.render_node(context, output, node)
node.render_to_output_buffer(context, output)
rescue => exc
blank_tag = !node.instance_of?(Variable) && node.blank?
rescue_render_node(context, output, node.line_number, exc, blank_tag)
end
# @api private
def self.rescue_render_node(context, output, line_number, exc, blank_tag)
case exc
when MemoryError
raise
when UndefinedVariable, UndefinedDropMethod, UndefinedFilter
context.handle_error(exc, line_number)
else
error_message = context.handle_error(exc, line_number)
unless blank_tag # conditional for backwards compatibility
output << error_message
end
end
end
private def parse_liquid_tag(markup, parse_context)
liquid_tag_tokenizer = parse_context.new_tokenizer(
markup, start_line_number: parse_context.line_number, for_liquid_tag: true
)
parse_for_liquid_tag(liquid_tag_tokenizer, parse_context) do |end_tag_name, _end_tag_markup|
if end_tag_name
BlockBody.unknown_tag_in_liquid_tag(end_tag_name, parse_context)
end
end
end
private def parse_for_document(tokenizer, parse_context)
while (token = tokenizer.shift)
next if token.empty? next if token.empty?
case case
when token.start_with?(TAGSTART) when token.start_with?(TAGSTART)
whitespace_handler(token, parse_context) whitespace_handler(token, parse_context)
unless token =~ FullToken unless token =~ FullToken
raise_missing_tag_terminator(token, parse_context) BlockBody.raise_missing_tag_terminator(token, parse_context)
end end
tag_name = Regexp.last_match(2) tag_name = Regexp.last_match(2)
markup = Regexp.last_match(4) markup = Regexp.last_match(4)
if parse_context.line_number if parse_context.line_number
# newlines inside the tag should increase the line number, # newlines inside the tag should increase the line number,
@@ -70,11 +128,11 @@ module Liquid
end end
if tag_name == 'liquid' if tag_name == 'liquid'
liquid_tag_tokenizer = Tokenizer.new(markup, line_number: parse_context.line_number, for_liquid_tag: true) parse_liquid_tag(markup, parse_context)
next parse_for_liquid_tag(liquid_tag_tokenizer, parse_context, &block) next
end end
unless tag = registered_tags[tag_name] unless (tag = registered_tags[tag_name])
# end parsing if we reach an unknown tag and let the caller decide # end parsing if we reach an unknown tag and let the caller decide
# determine how to proceed # determine how to proceed
return yield tag_name, markup return yield tag_name, markup
@@ -104,7 +162,11 @@ module Liquid
if token[2] == WhitespaceControl if token[2] == WhitespaceControl
previous_token = @nodelist.last previous_token = @nodelist.last
if previous_token.is_a?(String) if previous_token.is_a?(String)
first_byte = previous_token.getbyte(0)
previous_token.rstrip! previous_token.rstrip!
if previous_token.empty? && parse_context[:bug_compatible_whitespace_trimming] && first_byte
previous_token << first_byte
end
end end
end end
parse_context.trim_whitespace = (token[-3] == WhitespaceControl) parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
@@ -114,38 +176,49 @@ module Liquid
@blank @blank
end end
# Remove blank strings in the block body for a control flow tag (e.g. `if`, `for`, `case`, `unless`)
# with a blank body.
#
# For example, in a conditional assignment like the following
#
# ```
# {% if size > max_size %}
# {% assign size = max_size %}
# {% endif %}
# ```
#
# we assume the intention wasn't to output the blank spaces in the `if` tag's block body, so this method
# will remove them to reduce the render output size.
#
# Note that it is now preferred to use the `liquid` tag for this use case.
def remove_blank_strings
raise "remove_blank_strings only support being called on a blank block body" unless @blank
@nodelist.reject! { |node| node.instance_of?(String) }
end
def render(context) def render(context)
render_to_output_buffer(context, +'') render_to_output_buffer(context, +'')
end end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
context.resource_limits.render_score += @nodelist.length freeze unless frozen?
context.resource_limits.increment_render_score(@nodelist.length)
idx = 0 idx = 0
while node = @nodelist[idx] while (node = @nodelist[idx])
previous_output_size = output.bytesize if node.instance_of?(String)
case node
when String
output << node output << node
when Variable else
render_node(context, output, node) render_node(context, output, node)
when Block
render_node(context, node.blank? ? +'' : output, node)
break if context.interrupt? # might have happened in a for-block
when Continue, Break
# If we get an Interrupt that means the block must stop processing. An # If we get an Interrupt that means the block must stop processing. An
# Interrupt is any command that stops block execution such as {% break %} # Interrupt is any command that stops block execution such as {% break %}
# or {% continue %} # or {% continue %}. These tags may also occur through Block or Include tags.
context.push_interrupt(node.interrupt) break if context.interrupt? # might have happened in a for-block
break
else # Other non-Block tags
render_node(context, output, node)
break if context.interrupt? # might have happened through an include
end end
idx += 1 idx += 1
raise_if_resource_limits_reached(context, output.bytesize - previous_output_size) context.resource_limits.increment_write_score(output)
end end
output output
@@ -154,18 +227,7 @@ module Liquid
private private
def render_node(context, output, node) def render_node(context, output, node)
node.render_to_output_buffer(context, output) BlockBody.render_node(context, output, node)
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
context.handle_error(e, node.line_number)
rescue ::StandardError => e
line_number = node.is_a?(String) ? nil : node.line_number
output << context.handle_error(e, line_number)
end
def raise_if_resource_limits_reached(context, length)
context.resource_limits.render_length += length
return unless context.resource_limits.reached?
raise MemoryError, "Memory limits exceeded"
end end
def create_variable(token, parse_context) def create_variable(token, parse_context)
@@ -173,15 +235,17 @@ module Liquid
markup = content.first markup = content.first
return Variable.new(markup, parse_context) return Variable.new(markup, parse_context)
end end
raise_missing_variable_terminator(token, parse_context) BlockBody.raise_missing_variable_terminator(token, parse_context)
end end
# @deprecated Use {.raise_missing_tag_terminator} instead
def raise_missing_tag_terminator(token, parse_context) def raise_missing_tag_terminator(token, parse_context)
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_termination", token: token, tag_end: TagEnd.inspect) BlockBody.raise_missing_tag_terminator(token, parse_context)
end end
# @deprecated Use {.raise_missing_variable_terminator} instead
def raise_missing_variable_terminator(token, parse_context) def raise_missing_variable_terminator(token, parse_context)
raise SyntaxError, parse_context.locale.t("errors.syntax.variable_termination", token: token, tag_end: VariableEnd.inspect) BlockBody.raise_missing_variable_terminator(token, parse_context)
end end
def registered_tags def registered_tags

View File

@@ -27,18 +27,37 @@ module Liquid
end, end,
} }
class MethodLiteral
attr_reader :method_name, :to_s
def initialize(method_name, to_s)
@method_name = method_name
@to_s = to_s
end
end
@@method_literals = {
'blank' => MethodLiteral.new(:blank?, '').freeze,
'empty' => MethodLiteral.new(:empty?, '').freeze,
}
def self.operators def self.operators
@@operators @@operators
end end
def self.parse_expression(parse_context, markup)
@@method_literals[markup] || parse_context.parse_expression(markup)
end
attr_reader :attachment, :child_condition attr_reader :attachment, :child_condition
attr_accessor :left, :operator, :right attr_accessor :left, :operator, :right
def initialize(left = nil, operator = nil, right = nil) def initialize(left = nil, operator = nil, right = nil)
@left = left @left = left
@operator = operator @operator = operator
@right = right @right = right
@child_relation = nil
@child_relation = nil
@child_condition = nil @child_condition = nil
end end
@@ -62,12 +81,12 @@ module Liquid
end end
def or(condition) def or(condition)
@child_relation = :or @child_relation = :or
@child_condition = condition @child_condition = condition
end end
def and(condition) def and(condition)
@child_relation = :and @child_relation = :and
@child_condition = condition @child_condition = condition
end end
@@ -90,7 +109,7 @@ module Liquid
private private
def equal_variables(left, right) def equal_variables(left, right)
if left.is_a?(Liquid::Expression::MethodLiteral) if left.is_a?(MethodLiteral)
if right.respond_to?(left.method_name) if right.respond_to?(left.method_name)
return right.send(left.method_name) return right.send(left.method_name)
else else
@@ -98,7 +117,7 @@ module Liquid
end end
end end
if right.is_a?(Liquid::Expression::MethodLiteral) if right.is_a?(MethodLiteral)
if left.respond_to?(right.method_name) if left.respond_to?(right.method_name)
return left.send(right.method_name) return left.send(right.method_name)
else else
@@ -115,7 +134,7 @@ module Liquid
# return this as the result. # return this as the result.
return context.evaluate(left) if op.nil? return context.evaluate(left) if op.nil?
left = context.evaluate(left) left = context.evaluate(left)
right = context.evaluate(right) right = context.evaluate(right)
operation = self.class.operators[op] || raise(Liquid::ArgumentError, "Unknown operator #{op}") operation = self.class.operators[op] || raise(Liquid::ArgumentError, "Unknown operator #{op}")

View File

@@ -18,8 +18,8 @@ module Liquid
attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters
# rubocop:disable Metrics/ParameterLists # rubocop:disable Metrics/ParameterLists
def self.build(environments: {}, outer_scope: {}, registers: {}, rethrow_errors: false, resource_limits: nil, static_environments: {}) 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) new(environments, outer_scope, registers, rethrow_errors, resource_limits, static_environments, &block)
end end
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil, static_environments = {}) def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil, static_environments = {})
@@ -34,16 +34,20 @@ module Liquid
@strict_variables = false @strict_variables = false
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits) @resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
@base_scope_depth = 0 @base_scope_depth = 0
squash_instance_assigns_with_environments @interrupts = []
@filters = []
@global_filter = nil
@disabled_tags = {}
self.exception_renderer = Template.default_exception_renderer self.exception_renderer = Template.default_exception_renderer
if rethrow_errors if rethrow_errors
self.exception_renderer = ->(_e) { raise } self.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA
end end
@interrupts = [] yield self if block_given?
@filters = []
@global_filter = nil # Do this last, since it could result in this object being passed to a Proc in the environment
squash_instance_assigns_with_environments
end end
# rubocop:enable Metrics/ParameterLists # rubocop:enable Metrics/ParameterLists
@@ -52,7 +56,7 @@ module Liquid
end end
def strainer def strainer
@strainer ||= Strainer.create(self, @filters) @strainer ||= StrainerFactory.create(self, @filters)
end end
# Adds filters to this context. # Adds filters to this context.
@@ -87,7 +91,7 @@ module Liquid
def handle_error(e, line_number = nil) def handle_error(e, line_number = nil)
e = internal_error unless e.is_a?(Liquid::Error) e = internal_error unless e.is_a?(Liquid::Error)
e.template_name ||= template_name e.template_name ||= template_name
e.line_number ||= line_number e.line_number ||= line_number
errors.push(e) errors.push(e)
exception_renderer.call(e).to_s exception_renderer.call(e).to_s
end end
@@ -133,17 +137,18 @@ module Liquid
def new_isolated_subcontext def new_isolated_subcontext
check_overflow check_overflow
Context.build( self.class.build(
resource_limits: resource_limits, resource_limits: resource_limits,
static_environments: static_environments, static_environments: static_environments,
registers: StaticRegisters.new(registers) registers: StaticRegisters.new(registers)
).tap do |subcontext| ).tap do |subcontext|
subcontext.base_scope_depth = base_scope_depth + 1 subcontext.base_scope_depth = base_scope_depth + 1
subcontext.exception_renderer = exception_renderer subcontext.exception_renderer = exception_renderer
subcontext.filters = @filters subcontext.filters = @filters
subcontext.strainer = nil subcontext.strainer = nil
subcontext.errors = errors subcontext.errors = errors
subcontext.warnings = warnings subcontext.warnings = warnings
subcontext.disabled_tags = @disabled_tags
end end
end end
@@ -188,7 +193,7 @@ module Liquid
try_variable_find_in_environments(key, raise_on_not_found: raise_on_not_found) try_variable_find_in_environments(key, raise_on_not_found: raise_on_not_found)
end end
variable = variable.to_liquid variable = variable.to_liquid
variable.context = self if variable.respond_to?(:context=) variable.context = self if variable.respond_to?(:context=)
variable variable
@@ -208,9 +213,24 @@ module Liquid
end end
end end
def with_disabled_tags(tag_names)
tag_names.each do |name|
@disabled_tags[name] = @disabled_tags.fetch(name, 0) + 1
end
yield
ensure
tag_names.each do |name|
@disabled_tags[name] -= 1
end
end
def tag_disabled?(tag_name)
@disabled_tags.fetch(tag_name, 0) > 0
end
protected protected
attr_writer :base_scope_depth, :warnings, :errors, :strainer, :filters attr_writer :base_scope_depth, :warnings, :errors, :strainer, :filters, :disabled_tags
private private

View File

@@ -1,23 +1,34 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
class Document < BlockBody class Document
def self.parse(tokens, parse_context) def self.parse(tokens, parse_context)
doc = new doc = new(parse_context)
doc.parse(tokens, parse_context) doc.parse(tokens, parse_context)
doc doc
end end
def parse(tokens, parse_context) attr_reader :parse_context, :body
super do |end_tag_name, _end_tag_params|
unknown_tag(end_tag_name, parse_context) if end_tag_name def initialize(parse_context)
@parse_context = parse_context
@body = new_body
end
def nodelist
@body.nodelist
end
def parse(tokenizer, parse_context)
while parse_body(tokenizer)
end end
@body.freeze
rescue SyntaxError => e rescue SyntaxError => e
e.line_number ||= parse_context.line_number e.line_number ||= parse_context.line_number
raise raise
end end
def unknown_tag(tag, parse_context) def unknown_tag(tag, _markup, _tokenizer)
case tag case tag
when 'else', 'end' when 'else', 'end'
raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_outer_tag", tag: tag) raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_outer_tag", tag: tag)
@@ -25,5 +36,30 @@ module Liquid
raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag) raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag)
end end
end end
def render_to_output_buffer(context, output)
@body.render_to_output_buffer(context, output)
end
def render(context)
render_to_output_buffer(context, +'')
end
private
def new_body
parse_context.new_block_body
end
def parse_body(tokenizer)
@body.parse(tokenizer, parse_context) do |unknown_tag_name, unknown_tag_markup|
if unknown_tag_name
unknown_tag(unknown_tag_name, unknown_tag_markup, tokenizer)
true
else
false
end
end
end
end end
end end

View File

@@ -40,19 +40,19 @@ module Liquid
end end
end end
ArgumentError = Class.new(Error) ArgumentError = Class.new(Error)
ContextError = Class.new(Error) ContextError = Class.new(Error)
FileSystemError = Class.new(Error) FileSystemError = Class.new(Error)
StandardError = Class.new(Error) StandardError = Class.new(Error)
SyntaxError = Class.new(Error) SyntaxError = Class.new(Error)
StackLevelError = Class.new(Error) StackLevelError = Class.new(Error)
TaintedError = Class.new(Error) MemoryError = Class.new(Error)
MemoryError = Class.new(Error) ZeroDivisionError = Class.new(Error)
ZeroDivisionError = Class.new(Error) FloatDomainError = Class.new(Error)
FloatDomainError = Class.new(Error) UndefinedVariable = Class.new(Error)
UndefinedVariable = Class.new(Error)
UndefinedDropMethod = Class.new(Error) UndefinedDropMethod = Class.new(Error)
UndefinedFilter = Class.new(Error) UndefinedFilter = Class.new(Error)
MethodOverrideError = Class.new(Error) MethodOverrideError = Class.new(Error)
InternalError = Class.new(Error) DisabledError = Class.new(Error)
InternalError = Class.new(Error)
end end

View File

@@ -2,46 +2,39 @@
module Liquid module Liquid
class Expression class Expression
class MethodLiteral
attr_reader :method_name, :to_s
def initialize(method_name, to_s)
@method_name = method_name
@to_s = to_s
end
def to_liquid
to_s
end
end
LITERALS = { LITERALS = {
nil => nil, 'nil' => nil, 'null' => nil, '' => nil, nil => nil, 'nil' => nil, 'null' => nil, '' => nil,
'true' => true, 'true' => true,
'false' => false, 'false' => false,
'blank' => MethodLiteral.new(:blank?, '').freeze, 'blank' => '',
'empty' => MethodLiteral.new(:empty?, '').freeze 'empty' => ''
}.freeze }.freeze
SINGLE_QUOTED_STRING = /\A'(.*)'\z/m SINGLE_QUOTED_STRING = /\A\s*'(.*)'\s*\z/m
DOUBLE_QUOTED_STRING = /\A"(.*)"\z/m DOUBLE_QUOTED_STRING = /\A\s*"(.*)"\s*\z/m
INTEGERS_REGEX = /\A(-?\d+)\z/ INTEGERS_REGEX = /\A\s*(-?\d+)\s*\z/
FLOATS_REGEX = /\A(-?\d[\d\.]+)\z/ FLOATS_REGEX = /\A\s*(-?\d[\d\.]+)\s*\z/
RANGES_REGEX = /\A\((\S+)\.\.(\S+)\)\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+)\s*\)\s*\z/
def self.parse(markup) def self.parse(markup)
if LITERALS.key?(markup) case markup
LITERALS[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
RangeLookup.parse(Regexp.last_match(1), Regexp.last_match(2))
when FLOATS_REGEX
Regexp.last_match(1).to_f
else else
case markup markup = markup.strip
when SINGLE_QUOTED_STRING, DOUBLE_QUOTED_STRING if LITERALS.key?(markup)
Regexp.last_match(1) LITERALS[markup]
when INTEGERS_REGEX
Regexp.last_match(1).to_i
when RANGES_REGEX
RangeLookup.parse(Regexp.last_match(1), Regexp.last_match(2))
when FLOATS_REGEX
Regexp.last_match(1).to_f
else else
VariableLookup.parse(markup) VariableLookup.parse(markup)
end end

View File

@@ -47,7 +47,7 @@ module Liquid
attr_accessor :root attr_accessor :root
def initialize(root, pattern = "_%s.liquid") def initialize(root, pattern = "_%s.liquid")
@root = root @root = root
@pattern = pattern @pattern = pattern
end end
@@ -59,7 +59,7 @@ module Liquid
end end
def full_path(template_path) def full_path(template_path)
raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ %r{\A[^./][a-zA-Z0-9_/]+\z} raise FileSystemError, "Illegal template name '#{template_path}'" unless %r{\A[^./][a-zA-Z0-9_/]+\z}.match?(template_path)
full_path = if template_path.include?('/') full_path = if template_path.include?('/')
File.join(root, File.dirname(template_path), @pattern % File.basename(template_path)) File.join(root, File.dirname(template_path), @pattern % File.basename(template_path))

View File

@@ -3,13 +3,18 @@
module Liquid module Liquid
class ForloopDrop < Drop class ForloopDrop < Drop
def initialize(name, length, parentloop) def initialize(name, length, parentloop)
@name = name @name = name
@length = length @length = length
@parentloop = parentloop @parentloop = parentloop
@index = 0 @index = 0
end end
attr_reader :name, :length, :parentloop attr_reader :length, :parentloop
def name
Usage.increment('forloop_drop_name')
@name
end
def index def index
@index + 1 @index + 1

View File

@@ -15,12 +15,12 @@ module Liquid
'?' => :question, '?' => :question,
'-' => :dash, '-' => :dash,
}.freeze }.freeze
IDENTIFIER = /[a-zA-Z_][\w-]*\??/ IDENTIFIER = /[a-zA-Z_][\w-]*\??/
SINGLE_STRING_LITERAL = /'[^\']*'/ SINGLE_STRING_LITERAL = /'[^\']*'/
DOUBLE_STRING_LITERAL = /"[^\"]*"/ DOUBLE_STRING_LITERAL = /"[^\"]*"/
NUMBER_LITERAL = /-?\d+(\.\d+)?/ NUMBER_LITERAL = /-?\d+(\.\d+)?/
DOTDOT = /\.\./ DOTDOT = /\.\./
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/ COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/
WHITESPACE_OR_NOTHING = /\s*/ WHITESPACE_OR_NOTHING = /\s*/
def initialize(input) def initialize(input)
@@ -33,15 +33,21 @@ module Liquid
until @ss.eos? until @ss.eos?
@ss.skip(WHITESPACE_OR_NOTHING) @ss.skip(WHITESPACE_OR_NOTHING)
break if @ss.eos? break if @ss.eos?
tok = if t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t] tok = if (t = @ss.scan(COMPARISON_OPERATOR))
elsif t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t] [:comparison, t]
elsif t = @ss.scan(DOUBLE_STRING_LITERAL) then [:string, t] elsif (t = @ss.scan(SINGLE_STRING_LITERAL))
elsif t = @ss.scan(NUMBER_LITERAL) then [:number, t] [:string, t]
elsif t = @ss.scan(IDENTIFIER) then [:id, t] elsif (t = @ss.scan(DOUBLE_STRING_LITERAL))
elsif t = @ss.scan(DOTDOT) then [:dotdot, t] [:string, t]
elsif (t = @ss.scan(NUMBER_LITERAL))
[:number, t]
elsif (t = @ss.scan(IDENTIFIER))
[:id, t]
elsif (t = @ss.scan(DOTDOT))
[:dotdot, t]
else else
c = @ss.getch c = @ss.getch
if s = SPECIALS[c] if (s = SPECIALS[c])
[s, c] [s, c]
else else
raise SyntaxError, "Unexpected character #{c}" raise SyntaxError, "Unexpected character #{c}"

View File

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

View File

@@ -7,9 +7,11 @@ module Liquid
def initialize(options = {}) def initialize(options = {})
@template_options = options ? options.dup : {} @template_options = options ? options.dup : {}
@locale = @template_options[:locale] ||= I18n.new
@locale = @template_options[:locale] ||= I18n.new
@warnings = [] @warnings = []
self.depth = 0
self.depth = 0
self.partial = false self.partial = false
end end
@@ -17,11 +19,23 @@ module Liquid
@options[option_key] @options[option_key]
end end
def new_block_body
Liquid::BlockBody.new
end
def new_tokenizer(markup, start_line_number: nil, for_liquid_tag: false)
Tokenizer.new(markup, line_number: start_line_number, for_liquid_tag: for_liquid_tag)
end
def parse_expression(markup)
Expression.parse(markup)
end
def partial=(value) def partial=(value)
@partial = value @partial = value
@options = value ? partial_options : @template_options @options = value ? partial_options : @template_options
@error_mode = @options[:error_mode] || Template.error_mode @error_mode = @options[:error_mode] || Template.error_mode
value
end end
def partial_options def partial_options

View File

@@ -11,7 +11,7 @@ module Liquid
end end
def initialize(node, callbacks) def initialize(node, callbacks)
@node = node @node = node
@callbacks = callbacks @callbacks = callbacks
end end

View File

@@ -3,9 +3,9 @@
module Liquid module Liquid
class Parser class Parser
def initialize(input) def initialize(input)
l = Lexer.new(input) l = Lexer.new(input)
@tokens = l.tokenize @tokens = l.tokenize
@p = 0 # pointer to current location @p = 0 # pointer to current location
end end
def jump(point) def jump(point)
@@ -46,16 +46,20 @@ module Liquid
tok[0] == type tok[0] == type
end end
SINGLE_TOKEN_EXPRESSION_TYPES = [:string, :number].freeze
private_constant :SINGLE_TOKEN_EXPRESSION_TYPES
def expression def expression
token = @tokens[@p] token = @tokens[@p]
if token[0] == :id case token[0]
variable_signature when :id
elsif SINGLE_TOKEN_EXPRESSION_TYPES.include?(token[0]) str = consume
str << variable_lookups
when :open_square
str = consume
str << expression
str << consume(:close_square)
str << variable_lookups
when :string, :number
consume consume
elsif token.first == :open_round when :open_round
consume consume
first = expression first = expression
consume(:dotdot) consume(:dotdot)
@@ -78,16 +82,19 @@ module Liquid
str str
end end
def variable_signature def variable_lookups
str = consume(:id) str = +""
while look(:open_square) loop do
str << consume if look(:open_square)
str << expression str << consume
str << consume(:close_square) str << expression
end str << consume(:close_square)
if look(:dot) elsif look(:dot)
str << consume str << consume
str << variable_signature str << consume(:id)
else
break
end
end end
str str
end end

View File

@@ -2,16 +2,28 @@
module Liquid module Liquid
module ParserSwitching module ParserSwitching
def strict_parse_with_error_mode_fallback(markup)
strict_parse_with_error_context(markup)
rescue SyntaxError => e
case parse_context.error_mode
when :strict
raise
when :warn
parse_context.warnings << e
end
lax_parse(markup)
end
def parse_with_selected_parser(markup) def parse_with_selected_parser(markup)
case parse_context.error_mode case parse_context.error_mode
when :strict then strict_parse_with_error_context(markup) when :strict then strict_parse_with_error_context(markup)
when :lax then lax_parse(markup) when :lax then lax_parse(markup)
when :warn when :warn
begin begin
return strict_parse_with_error_context(markup) strict_parse_with_error_context(markup)
rescue SyntaxError => e rescue SyntaxError => e
parse_context.warnings << e parse_context.warnings << e
return lax_parse(markup) lax_parse(markup)
end end
end end
end end
@@ -21,7 +33,7 @@ module Liquid
def strict_parse_with_error_context(markup) def strict_parse_with_error_context(markup)
strict_parse(markup) strict_parse(markup)
rescue SyntaxError => e rescue SyntaxError => e
e.line_number = line_number e.line_number = line_number
e.markup_context = markup_context(markup) e.markup_context = markup_context(markup)
raise e raise e
end end

View File

@@ -8,10 +8,14 @@ module Liquid
return cached if cached return cached if cached
file_system = (context.registers[:file_system] ||= Liquid::Template.file_system) file_system = (context.registers[:file_system] ||= Liquid::Template.file_system)
source = file_system.read_template_file(template_name) source = file_system.read_template_file(template_name)
parse_context.partial = true parse_context.partial = true
partial = Liquid::Template.parse(source, parse_context) template_factory = (context.registers[:template_factory] ||= Liquid::TemplateFactory.new)
template = template_factory.for(template_name)
partial = template.parse(source, parse_context)
cached_partials[template_name] = partial cached_partials[template_name] = partial
ensure ensure
parse_context.partial = false parse_context.partial = false

View File

@@ -25,7 +25,7 @@ module Liquid
# node.code # node.code
# #
# # Which template and line number of this node. # # 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.partial
# node.line_number # node.line_number
# #
@@ -46,115 +46,94 @@ module Liquid
include Enumerable include Enumerable
class Timing class Timing
attr_reader :code, :partial, :line_number, :children 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) def initialize(code: nil, template_name: nil, line_number: nil)
@code = node.respond_to?(:raw) ? node.raw : node @code = code
@partial = partial @template_name = template_name
@line_number = node.respond_to?(:line_number) ? node.line_number : nil @line_number = line_number
@children = [] @children = []
end end
def self.start(node, partial) def self_time
new(node, partial).tap(&:start) @self_time ||= begin
end total_children_time = 0.0
@children.each do |child|
def start total_children_time += child.total_time
@start_time = Time.now end
end @total_time - total_children_time
end
def finish
@end_time = Time.now
end
def render_time
@end_time - @start_time
end end
end end
def self.profile_node_render(node) attr_reader :total_time
if Profiler.current_profile && node.respond_to?(:render) alias_method :total_render_time, :total_time
Profiler.current_profile.start_node(node)
output = yield
Profiler.current_profile.end_node(node)
output
else
yield
end
end
def self.profile_children(template_name)
if Profiler.current_profile
Profiler.current_profile.push_partial(template_name)
output = yield
Profiler.current_profile.pop_partial
output
else
yield
end
end
def self.current_profile
Thread.current[:liquid_profiler]
end
def initialize def initialize
@partial_stack = ["<root>"] @root_children = []
@current_children = nil
@root_timing = Timing.new("", current_partial) @total_time = 0.0
@timing_stack = [@root_timing]
@render_start_at = Time.now
@render_end_at = @render_start_at
end end
def start def profile(template_name, &block)
Thread.current[:liquid_profiler] = self # nested renders are done from a tag that already has a timing node
@render_start_at = Time.now 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 end
def stop def children
Thread.current[:liquid_profiler] = nil children = @root_children
@render_end_at = Time.now if children.length == 1
end children.first.children
else
def total_render_time children
@render_end_at - @render_start_at end
end end
def each(&block) def each(&block)
@root_timing.children.each(&block) children.each(&block)
end end
def [](idx) def [](idx)
@root_timing.children[idx] children[idx]
end end
def length def length
@root_timing.children.length children.length
end end
def start_node(node) def profile_node(template_name, code: nil, line_number: nil)
@timing_stack.push(Timing.start(node, current_partial)) 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 end
def end_node(_node) private
timing = @timing_stack.pop
timing.finish
@timing_stack.last.children << timing def monotonic_time
end Process.clock_gettime(Process::CLOCK_MONOTONIC)
def current_partial
@partial_stack.last
end
def push_partial(partial_name)
@partial_stack.push(partial_name)
end
def pop_partial
@partial_stack.pop
end end
end end
end end

View File

@@ -1,25 +1,35 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
class BlockBody module BlockBodyProfilingHook
def render_node_with_profiling(context, output, node) def render_node(context, output, node)
Profiler.profile_node_render(node) do if (profiler = context.profiler)
render_node_without_profiling(context, output, node) profiler.profile_node(context.template_name, code: node.raw, line_number: node.line_number) do
super
end
else
super
end end
end end
alias_method :render_node_without_profiling, :render_node
alias_method :render_node, :render_node_with_profiling
end end
BlockBody.prepend(BlockBodyProfilingHook)
class Include < Tag module DocumentProfilingHook
def render_to_output_buffer_with_profiling(context, output) def render_to_output_buffer(context, output)
Profiler.profile_children(context.evaluate(@template_name_expr).to_s) do return super unless context.profiler
render_to_output_buffer_without_profiling(context, output) context.profiler.profile(context.template_name) { super }
end
end end
alias_method :render_to_output_buffer_without_profiling, :render_to_output_buffer
alias_method :render_to_output_buffer, :render_to_output_buffer_with_profiling
end end
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 end

View File

@@ -4,7 +4,7 @@ module Liquid
class RangeLookup class RangeLookup
def self.parse(start_markup, end_markup) def self.parse(start_markup, end_markup)
start_obj = Expression.parse(start_markup) start_obj = Expression.parse(start_markup)
end_obj = Expression.parse(end_markup) end_obj = Expression.parse(end_markup)
if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate) if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate)
new(start_obj, end_obj) new(start_obj, end_obj)
else else
@@ -14,12 +14,12 @@ module Liquid
def initialize(start_obj, end_obj) def initialize(start_obj, end_obj)
@start_obj = start_obj @start_obj = start_obj
@end_obj = end_obj @end_obj = end_obj
end end
def evaluate(context) def evaluate(context)
start_int = to_integer(context.evaluate(@start_obj)) start_int = to_integer(context.evaluate(@start_obj))
end_int = to_integer(context.evaluate(@end_obj)) end_int = to_integer(context.evaluate(@end_obj))
start_int..end_int start_int..end_int
end end

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

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

View File

@@ -2,24 +2,61 @@
module Liquid module Liquid
class ResourceLimits class ResourceLimits
attr_accessor :render_length, :render_score, :assign_score, attr_accessor :render_length_limit, :render_score_limit, :assign_score_limit
:render_length_limit, :render_score_limit, :assign_score_limit attr_reader :render_score, :assign_score
def initialize(limits) def initialize(limits)
@render_length_limit = limits[:render_length_limit] @render_length_limit = limits[:render_length_limit]
@render_score_limit = limits[:render_score_limit] @render_score_limit = limits[:render_score_limit]
@assign_score_limit = limits[:assign_score_limit] @assign_score_limit = limits[:assign_score_limit]
reset reset
end end
def increment_render_score(amount)
@render_score += amount
raise_limits_reached if @render_score_limit && @render_score > @render_score_limit
end
def increment_assign_score(amount)
@assign_score += amount
raise_limits_reached if @assign_score_limit && @assign_score > @assign_score_limit
end
# update either render_length or assign_score based on whether or not the writes are captured
def increment_write_score(output)
if (last_captured = @last_capture_length)
captured = output.bytesize
increment = captured - last_captured
@last_capture_length = captured
increment_assign_score(increment)
elsif @render_length_limit && output.bytesize > @render_length_limit
raise_limits_reached
end
end
def raise_limits_reached
@reached_limit = true
raise MemoryError, "Memory limits exceeded"
end
def reached? def reached?
(@render_length_limit && @render_length > @render_length_limit) || @reached_limit
(@render_score_limit && @render_score > @render_score_limit) ||
(@assign_score_limit && @assign_score > @assign_score_limit)
end end
def reset def reset
@render_length = @render_score = @assign_score = 0 @reached_limit = false
@last_capture_length = nil
@render_score = @assign_score = 0
end
def with_capture
old_capture_length = @last_capture_length
begin
@last_capture_length = 0
yield
ensure
@last_capture_length = old_capture_length
end
end end
end end
end end

View File

@@ -13,7 +13,7 @@ module Liquid
"'" => '&#39;', "'" => '&#39;',
}.freeze }.freeze
HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/ HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/
STRIP_HTML_BLOCKS = Regexp.union( STRIP_HTML_BLOCKS = Regexp.union(
%r{<script.*?</script>}m, %r{<script.*?</script>}m,
/<!--.*?-->/m, /<!--.*?-->/m,
%r{<style.*?</style>}m %r{<style.*?</style>}m
@@ -41,7 +41,7 @@ module Liquid
end end
def escape(input) def escape(input)
CGI.escapeHTML(input.to_s).untaint unless input.nil? CGI.escapeHTML(input.to_s) unless input.nil?
end end
alias_method :h, :escape alias_method :h, :escape
@@ -77,20 +77,27 @@ module Liquid
def truncate(input, length = 50, truncate_string = "...") def truncate(input, length = 50, truncate_string = "...")
return if input.nil? return if input.nil?
input_str = input.to_s input_str = input.to_s
length = Utils.to_integer(length) length = Utils.to_integer(length)
truncate_string_str = truncate_string.to_s truncate_string_str = truncate_string.to_s
l = length - truncate_string_str.length l = length - truncate_string_str.length
l = 0 if l < 0 l = 0 if l < 0
input_str.length > length ? input_str[0...l].concat(truncate_string_str) : input_str input_str.length > length ? input_str[0...l].concat(truncate_string_str) : input_str
end end
def truncatewords(input, words = 15, truncate_string = "...") def truncatewords(input, words = 15, truncate_string = "...")
return if input.nil? return if input.nil?
wordlist = input.to_s.split input = input.to_s
words = Utils.to_integer(words) words = Utils.to_integer(words)
l = words - 1 words = 1 if words <= 0
l = 0 if l < 0
wordlist.length > l ? wordlist[0..l].join(" ").concat(truncate_string.to_s) : input wordlist = input.split(" ", words + 1)
return input if wordlist.length <= words
wordlist.pop
wordlist.join(" ").concat(truncate_string.to_s)
end end
# Split input string into an array of substrings separated by given pattern. # Split input string into an array of substrings separated by given pattern.
@@ -115,7 +122,7 @@ module Liquid
end end
def strip_html(input) def strip_html(input)
empty = '' empty = ''
result = input.to_s.gsub(STRIP_HTML_BLOCKS, empty) result = input.to_s.gsub(STRIP_HTML_BLOCKS, empty)
result.gsub!(STRIP_HTML_TAGS, empty) result.gsub!(STRIP_HTML_TAGS, empty)
result result
@@ -128,13 +135,13 @@ module Liquid
# Join elements of the array with certain character between them # Join elements of the array with certain character between them
def join(input, glue = ' ') def join(input, glue = ' ')
InputIterator.new(input).join(glue) InputIterator.new(input, context).join(glue)
end end
# Sort elements of the array # Sort elements of the array
# provide optional property with which to sort an array of hashes or drops # provide optional property with which to sort an array of hashes or drops
def sort(input, property = nil) def sort(input, property = nil)
ary = InputIterator.new(input) ary = InputIterator.new(input, context)
return [] if ary.empty? return [] if ary.empty?
@@ -154,7 +161,7 @@ module Liquid
# Sort elements of an array ignoring case if strings # Sort elements of an array ignoring case if strings
# provide optional property with which to sort an array of hashes or drops # provide optional property with which to sort an array of hashes or drops
def sort_natural(input, property = nil) def sort_natural(input, property = nil)
ary = InputIterator.new(input) ary = InputIterator.new(input, context)
return [] if ary.empty? return [] if ary.empty?
@@ -174,7 +181,7 @@ module Liquid
# Filter the elements of an array to those with a certain property value. # Filter the elements of an array to those with a certain property value.
# By default the target is any truthy value. # By default the target is any truthy value.
def where(input, property, target_value = nil) def where(input, property, target_value = nil)
ary = InputIterator.new(input) ary = InputIterator.new(input, context)
if ary.empty? if ary.empty?
[] []
@@ -196,7 +203,7 @@ module Liquid
# Remove duplicate elements from an array # Remove duplicate elements from an array
# provide optional property with which to determine uniqueness # provide optional property with which to determine uniqueness
def uniq(input, property = nil) def uniq(input, property = nil)
ary = InputIterator.new(input) ary = InputIterator.new(input, context)
if property.nil? if property.nil?
ary.uniq ary.uniq
@@ -213,13 +220,13 @@ module Liquid
# Reverse the elements of an array # Reverse the elements of an array
def reverse(input) def reverse(input)
ary = InputIterator.new(input) ary = InputIterator.new(input, context)
ary.reverse ary.reverse
end end
# map/collect on a given property # map/collect on a given property
def map(input, property) def map(input, property)
InputIterator.new(input).map do |e| InputIterator.new(input, context).map do |e|
e = e.call if e.is_a?(Proc) e = e.call if e.is_a?(Proc)
if property == "to_liquid" if property == "to_liquid"
@@ -236,7 +243,7 @@ module Liquid
# Remove nils within an array # Remove nils within an array
# provide optional property with which to check for nil # provide optional property with which to check for nil
def compact(input, property = nil) def compact(input, property = nil)
ary = InputIterator.new(input) ary = InputIterator.new(input, context)
if property.nil? if property.nil?
ary.compact ary.compact
@@ -280,7 +287,7 @@ module Liquid
unless array.respond_to?(:to_ary) unless array.respond_to?(:to_ary)
raise ArgumentError, "concat filter requires an array argument" raise ArgumentError, "concat filter requires an array argument"
end end
InputIterator.new(input).concat(array) InputIterator.new(input, context).concat(array)
end end
# prepend a string to another # prepend a string to another
@@ -290,7 +297,7 @@ module Liquid
# Add <br /> tags in front of all newlines in input string # Add <br /> tags in front of all newlines in input string
def newline_to_br(input) def newline_to_br(input)
input.to_s.gsub(/\n/, "<br />\n") input.to_s.gsub(/\r?\n/, "<br />\n")
end end
# Reformat a date using Ruby's core Time#strftime( string ) -> string # Reformat a date using Ruby's core Time#strftime( string ) -> string
@@ -327,7 +334,7 @@ module Liquid
def date(input, format) def date(input, format)
return input if format.to_s.empty? return input if format.to_s.empty?
return input unless date = Utils.to_date(input) return input unless (date = Utils.to_date(input))
date.strftime(format.to_s) date.strftime(format.to_s)
end end
@@ -421,17 +428,26 @@ module Liquid
result.is_a?(BigDecimal) ? result.to_f : result result.is_a?(BigDecimal) ? result.to_f : result
end end
def default(input, default_value = '') # Set a default value when the input is nil, false or empty
if !input || input.respond_to?(:empty?) && input.empty? #
Usage.increment("default_filter_received_false_value") if input == false # See https://github.com/Shopify/liquid/issues/1127 # Example:
default_value # {{ product.title | default: "No Title" }}
else #
input # Use `allow_false` when an input should only be tested against nil or empty and not false.
end #
# Example:
# {{ product.title | default: "No Title", allow_false: true }}
#
def default(input, default_value = '', options = {})
options = {} unless options.is_a?(Hash)
false_check = options['allow_false'] ? input.nil? : !input
false_check || (input.respond_to?(:empty?) && input.empty?) ? default_value : input
end end
private private
attr_reader :context
def raise_property_error(property) def raise_property_error(property)
raise Liquid::ArgumentError, "cannot select the property '#{property}'" raise Liquid::ArgumentError, "cannot select the property '#{property}'"
end end
@@ -460,8 +476,9 @@ module Liquid
class InputIterator class InputIterator
include Enumerable include Enumerable
def initialize(input) def initialize(input, context)
@input = if input.is_a?(Array) @context = context
@input = if input.is_a?(Array)
input.flatten input.flatten
elsif input.is_a?(Hash) elsif input.is_a?(Hash)
[input] [input]
@@ -499,6 +516,7 @@ module Liquid
def each def each
@input.each do |e| @input.each do |e|
e.context = @context if e.respond_to?(:context=)
yield(e.respond_to?(:to_liquid) ? e.to_liquid : e) yield(e.respond_to?(:to_liquid) ? e.to_liquid : e)
end end
end end

View File

@@ -2,10 +2,10 @@
module Liquid module Liquid
class StaticRegisters class StaticRegisters
attr_reader :static_registers, :registers attr_reader :static
def initialize(registers = {}) def initialize(registers = {})
@static_registers = registers.is_a?(StaticRegisters) ? registers.static_registers : registers.freeze @static = registers.is_a?(StaticRegisters) ? registers.static : registers
@registers = {} @registers = {}
end end
@@ -17,7 +17,7 @@ module Liquid
if @registers.key?(key) if @registers.key?(key)
@registers[key] @registers[key]
else else
@static_registers[key] @static[key]
end end
end end
@@ -25,12 +25,20 @@ module Liquid
@registers.delete(key) @registers.delete(key)
end end
def fetch(key, default = nil) UNDEFINED = Object.new
key?(key) ? self[key] : default
def fetch(key, default = UNDEFINED, &block)
if @registers.key?(key)
@registers.fetch(key)
elsif default != UNDEFINED
@static.fetch(key, default, &block)
else
@static.fetch(key, &block)
end
end end
def key?(key) def key?(key)
@registers.key?(key) || @static_registers.key?(key) @registers.key?(key) || @static.key?(key)
end end
end end
end end

View File

@@ -1,68 +0,0 @@
# frozen_string_literal: true
require 'set'
module Liquid
# Strainer is the parent class for the filters system.
# New filters are mixed into the strainer class which is then instantiated for each liquid template render run.
#
# The Strainer only allows method calls defined in filters given to it via Strainer.global_filter,
# Context#add_filters or Template.register_filter
class Strainer #:nodoc:
@@global_strainer = Class.new(Strainer) do
@filter_methods = Set.new
end
@@strainer_class_cache = Hash.new do |hash, filters|
hash[filters] = Class.new(@@global_strainer) do
@filter_methods = @@global_strainer.filter_methods.dup
filters.each { |f| add_filter(f) }
end
end
def initialize(context)
@context = context
end
class << self
attr_reader :filter_methods
end
def self.add_filter(filter)
raise ArgumentError, "Expected module but got: #{filter.class}" unless filter.is_a?(Module)
unless include?(filter)
invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }
if invokable_non_public_methods.any?
raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"
else
send(:include, filter)
@filter_methods.merge(filter.public_instance_methods.map(&:to_s))
end
end
end
def self.global_filter(filter)
@@strainer_class_cache.clear
@@global_strainer.add_filter(filter)
end
def self.invokable?(method)
@filter_methods.include?(method.to_s)
end
def self.create(context, filters = [])
@@strainer_class_cache[filters].new(context)
end
def invoke(method, *args)
if self.class.invokable?(method)
send(method, *args)
elsif @context&.strict_filters
raise Liquid::UndefinedFilter, "undefined filter #{method}"
else
args.first
end
rescue ::ArgumentError => e
raise Liquid::ArgumentError, e.message, e.backtrace
end
end
end

View File

@@ -0,0 +1,36 @@
# frozen_string_literal: true
module Liquid
# StrainerFactory is the factory for the filters system.
module StrainerFactory
extend self
def add_global_filter(filter)
strainer_class_cache.clear
global_filters << filter
end
def create(context, filters = [])
strainer_from_cache(filters).new(context)
end
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
end
end
def strainer_class_cache
@strainer_class_cache ||= {}
end
end
end

View File

@@ -0,0 +1,53 @@
# frozen_string_literal: true
require 'set'
module Liquid
# StrainerTemplate is the computed class for the filters system.
# New filters are mixed into the strainer class which is then instantiated for each liquid template render run.
#
# The Strainer only allows method calls defined in filters given to it via StrainerFactory.add_global_filter,
# Context#add_filters or Template.register_filter
class StrainerTemplate
def initialize(context)
@context = context
end
class << self
def add_filter(filter)
return if include?(filter)
invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }
if invokable_non_public_methods.any?
raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"
end
include(filter)
filter_methods.merge(filter.public_instance_methods.map(&:to_s))
end
def invokable?(method)
filter_methods.include?(method.to_s)
end
private
def filter_methods
@filter_methods ||= Set.new
end
end
def invoke(method, *args)
if self.class.invokable?(method)
send(method, *args)
elsif @context.strict_filters
raise Liquid::UndefinedFilter, "undefined filter #{method}"
else
args.first
end
rescue ::ArgumentError => e
raise Liquid::ArgumentError, e.message, e.backtrace
end
end
end

View File

@@ -4,10 +4,10 @@ module Liquid
class TablerowloopDrop < Drop class TablerowloopDrop < Drop
def initialize(length, cols) def initialize(length, cols)
@length = length @length = length
@row = 1 @row = 1
@col = 1 @col = 1
@cols = cols @cols = cols
@index = 0 @index = 0
end end
attr_reader :length, :col, :row attr_reader :length, :col, :row

View File

@@ -13,14 +13,20 @@ module Liquid
tag tag
end end
def disable_tags(*tag_names)
@disabled_tags ||= []
@disabled_tags.concat(tag_names)
prepend(Disabler)
end
private :new private :new
end end
def initialize(tag_name, markup, parse_context) def initialize(tag_name, markup, parse_context)
@tag_name = tag_name @tag_name = tag_name
@markup = markup @markup = markup
@parse_context = parse_context @parse_context = parse_context
@line_number = parse_context.line_number @line_number = parse_context.line_number
end end
def parse(_tokens) def parse(_tokens)
@@ -49,5 +55,11 @@ module Liquid
def blank? def blank?
false false
end end
private
def parse_expression(markup)
parse_context.parse_expression(markup)
end
end end
end end

View File

@@ -0,0 +1,22 @@
# frozen_string_literal: true
module Liquid
class Tag
module Disableable
def render_to_output_buffer(context, output)
if context.tag_disabled?(tag_name)
output << disabled_error(context)
return
end
super
end
def disabled_error(context)
# raise then rescue the exception so that the Context#exception_renderer can re-raise it
raise DisabledError, "#{tag_name} #{parse_context[:locale].t('errors.disabled.tag')}"
rescue DisabledError => exc
context.handle_error(exc, line_number)
end
end
end
end

View File

@@ -0,0 +1,21 @@
# frozen_string_literal: true
module Liquid
class Tag
module Disabler
module ClassMethods
attr_reader :disabled_tags
end
def self.prepended(base)
base.extend(ClassMethods)
end
def render_to_output_buffer(context, output)
context.with_disabled_tags(self.class.disabled_tags) do
super
end
end
end
end
end

View File

@@ -12,26 +12,27 @@ module Liquid
class Assign < Tag class Assign < Tag
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
def self.syntax_error_translation_key # @api private
"errors.syntax.assign" def self.raise_syntax_error(parse_context)
raise Liquid::SyntaxError, parse_context.locale.t('errors.syntax.assign')
end end
attr_reader :to, :from attr_reader :to, :from
def initialize(tag_name, markup, options) def initialize(tag_name, markup, parse_context)
super super
if markup =~ Syntax if markup =~ Syntax
@to = Regexp.last_match(1) @to = Regexp.last_match(1)
@from = Variable.new(Regexp.last_match(2), options) @from = Variable.new(Regexp.last_match(2), parse_context)
else else
raise SyntaxError, options[:locale].t(self.class.syntax_error_translation_key) self.class.raise_syntax_error(parse_context)
end end
end end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
val = @from.render(context) val = @from.render(context)
context.scopes.last[@to] = val context.scopes.last[@to] = val
context.resource_limits.assign_score += assign_score_of(val) context.resource_limits.increment_assign_score(assign_score_of(val))
output output
end end
@@ -44,11 +45,18 @@ module Liquid
def assign_score_of(val) def assign_score_of(val)
if val.instance_of?(String) if val.instance_of?(String)
val.bytesize val.bytesize
elsif val.instance_of?(Array) || val.instance_of?(Hash) elsif val.instance_of?(Array)
sum = 1 sum = 1
# Uses #each to avoid extra allocations. # Uses #each to avoid extra allocations.
val.each { |child| sum += assign_score_of(child) } val.each { |child| sum += assign_score_of(child) }
sum sum
elsif val.instance_of?(Hash)
sum = 1
val.each do |key, entry_value|
sum += assign_score_of(key)
sum += assign_score_of(entry_value)
end
sum
else else
1 1
end end

View File

@@ -11,8 +11,11 @@ module Liquid
# {% endfor %} # {% endfor %}
# #
class Break < Tag class Break < Tag
def interrupt INTERRUPT = BreakInterrupt.new.freeze
BreakInterrupt.new
def render_to_output_buffer(context, output)
context.push_interrupt(INTERRUPT)
output
end end
end end

View File

@@ -25,10 +25,10 @@ module Liquid
end end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
previous_output_size = output.bytesize context.resource_limits.with_capture do
super capture_output = render(context)
context.scopes.last[@to] = output context.scopes.last[@to] = capture_output
context.resource_limits.assign_score += (output.bytesize - previous_output_size) end
output output
end end

View File

@@ -12,15 +12,23 @@ module Liquid
@blocks = [] @blocks = []
if markup =~ Syntax if markup =~ Syntax
@left = Expression.parse(Regexp.last_match(1)) @left = parse_expression(Regexp.last_match(1))
else else
raise SyntaxError, options[:locale].t("errors.syntax.case") raise SyntaxError, options[:locale].t("errors.syntax.case")
end end
end end
def parse(tokens) def parse(tokens)
body = BlockBody.new body = case_body = new_body
body = @blocks.last.attachment while parse_body(body, tokens) body = @blocks.last.attachment while parse_body(body, tokens)
@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 end
def nodelist def nodelist
@@ -56,7 +64,7 @@ module Liquid
private private
def record_when_condition(markup) def record_when_condition(markup)
body = BlockBody.new body = new_body
while markup while markup
unless markup =~ WhenSyntax unless markup =~ WhenSyntax
@@ -65,7 +73,7 @@ module Liquid
markup = Regexp.last_match(2) markup = Regexp.last_match(2)
block = Condition.new(@left, '==', Expression.parse(Regexp.last_match(1))) block = Condition.new(@left, '==', Condition.parse_expression(parse_context, Regexp.last_match(1)))
block.attach(body) block.attach(body)
@blocks << block @blocks << block
end end
@@ -77,7 +85,7 @@ module Liquid
end end
block = ElseCondition.new block = ElseCondition.new
block.attach(BlockBody.new) block.attach(new_body)
@blocks << block @blocks << block
end end

View File

@@ -11,8 +11,11 @@ module Liquid
# {% endfor %} # {% endfor %}
# #
class Continue < Tag class Continue < Tag
def interrupt INTERRUPT = ContinueInterrupt.new.freeze
ContinueInterrupt.new
def render_to_output_buffer(context, output)
context.push_interrupt(INTERRUPT)
output
end end
end end

View File

@@ -24,10 +24,10 @@ module Liquid
case markup case markup
when NamedSyntax when NamedSyntax
@variables = variables_from_string(Regexp.last_match(2)) @variables = variables_from_string(Regexp.last_match(2))
@name = Expression.parse(Regexp.last_match(1)) @name = parse_expression(Regexp.last_match(1))
when SimpleSyntax when SimpleSyntax
@variables = variables_from_string(markup) @variables = variables_from_string(markup)
@name = @variables.to_s @name = @variables.to_s
else else
raise SyntaxError, options[:locale].t("errors.syntax.cycle") raise SyntaxError, options[:locale].t("errors.syntax.cycle")
end end
@@ -36,7 +36,7 @@ module Liquid
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
context.registers[:cycle] ||= {} context.registers[:cycle] ||= {}
key = context.evaluate(@name) key = context.evaluate(@name)
iteration = context.registers[:cycle][key].to_i iteration = context.registers[:cycle][key].to_i
val = context.evaluate(@variables[iteration]) val = context.evaluate(@variables[iteration])
@@ -50,9 +50,9 @@ module Liquid
output << val output << val
iteration += 1 iteration += 1
iteration = 0 if iteration >= @variables.size iteration = 0 if iteration >= @variables.size
context.registers[:cycle][key] = iteration
context.registers[:cycle][key] = iteration
output output
end end
@@ -61,7 +61,7 @@ module Liquid
def variables_from_string(markup) def variables_from_string(markup)
markup.split(',').collect do |var| markup.split(',').collect do |var|
var =~ /\s*(#{QuotedFragment})\s*/o var =~ /\s*(#{QuotedFragment})\s*/o
Regexp.last_match(1) ? Expression.parse(Regexp.last_match(1)) : nil Regexp.last_match(1) ? parse_expression(Regexp.last_match(1)) : nil
end.compact end.compact
end end

View File

@@ -12,6 +12,8 @@ module Liquid
# {% echo user | link %} # {% echo user | link %}
# #
class Echo < Tag class Echo < Tag
attr_reader :variable
def initialize(tag_name, markup, parse_context) def initialize(tag_name, markup, parse_context)
super super
@variable = Variable.new(markup, parse_context) @variable = Variable.new(markup, parse_context)
@@ -20,6 +22,12 @@ module Liquid
def render(context) def render(context)
@variable.render_to_output_buffer(context, +'') @variable.render_to_output_buffer(context, +'')
end end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[@node.variable]
end
end
end end
Template.register_tag('echo', Echo) Template.register_tag('echo', Echo)

View File

@@ -54,13 +54,20 @@ module Liquid
super super
@from = @limit = nil @from = @limit = nil
parse_with_selected_parser(markup) parse_with_selected_parser(markup)
@for_block = BlockBody.new @for_block = new_body
@else_block = nil @else_block = nil
end end
def parse(tokens) def parse(tokens)
return unless parse_body(@for_block, tokens) if parse_body(@for_block, tokens)
parse_body(@else_block, tokens) parse_body(@else_block, tokens)
end
if blank?
@else_block&.remove_blank_strings
@for_block.remove_blank_strings
end
@else_block&.freeze
@for_block.freeze
end end
def nodelist def nodelist
@@ -69,7 +76,7 @@ module Liquid
def unknown_tag(tag, markup, tokens) def unknown_tag(tag, markup, tokens)
return super unless tag == 'else' return super unless tag == 'else'
@else_block = BlockBody.new @else_block = new_body
end end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
@@ -88,11 +95,11 @@ module Liquid
def lax_parse(markup) def lax_parse(markup)
if markup =~ Syntax if markup =~ Syntax
@variable_name = Regexp.last_match(1) @variable_name = Regexp.last_match(1)
collection_name = Regexp.last_match(2) collection_name = Regexp.last_match(2)
@reversed = !!Regexp.last_match(3) @reversed = !!Regexp.last_match(3)
@name = "#{@variable_name}-#{collection_name}" @name = "#{@variable_name}-#{collection_name}"
@collection_name = Expression.parse(collection_name) @collection_name = parse_expression(collection_name)
markup.scan(TagAttributes) do |key, value| markup.scan(TagAttributes) do |key, value|
set_attribute(key, value) set_attribute(key, value)
end end
@@ -105,13 +112,15 @@ module Liquid
p = Parser.new(markup) p = Parser.new(markup)
@variable_name = p.consume(:id) @variable_name = p.consume(:id)
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in') raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in')
collection_name = p.expression
@name = "#{@variable_name}-#{collection_name}" collection_name = p.expression
@collection_name = Expression.parse(collection_name) @collection_name = parse_expression(collection_name)
@name = "#{@variable_name}-#{collection_name}"
@reversed = p.id?('reversed') @reversed = p.id?('reversed')
while p.look(:id) && p.look(:colon, 1) while p.look(:id) && p.look(:colon, 1)
unless attribute = p.id?('limit') || p.id?('offset') unless (attribute = p.id?('limit') || p.id?('offset'))
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_attribute") raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_attribute")
end end
p.consume p.consume
@@ -156,7 +165,7 @@ module Liquid
def render_segment(context, output, segment) def render_segment(context, output, segment)
for_stack = context.registers[:for_stack] ||= [] for_stack = context.registers[:for_stack] ||= []
length = segment.length length = segment.length
context.stack do context.stack do
loop_vars = Liquid::ForloopDrop.new(@name, length, for_stack[-1]) loop_vars = Liquid::ForloopDrop.new(@name, length, for_stack[-1])
@@ -189,12 +198,13 @@ module Liquid
case key case key
when 'offset' when 'offset'
@from = if expr == 'continue' @from = if expr == 'continue'
Usage.increment('for_offset_continue')
:continue :continue
else else
Expression.parse(expr) parse_expression(expr)
end end
when 'limit' when 'limit'
@limit = Expression.parse(expr) @limit = parse_expression(expr)
end end
end end

View File

@@ -12,9 +12,9 @@ module Liquid
# There are {% if count < 5 %} less {% else %} more {% endif %} items than you need. # There are {% if count < 5 %} less {% else %} more {% endif %} items than you need.
# #
class If < Block class If < Block
Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o
ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o
BOOLEAN_OPERATORS = %w(and or).freeze BOOLEAN_OPERATORS = %w(and or).freeze
attr_reader :blocks attr_reader :blocks
@@ -31,10 +31,17 @@ module Liquid
def parse(tokens) def parse(tokens)
while parse_body(@blocks.last.attachment, tokens) while parse_body(@blocks.last.attachment, tokens)
end end
@blocks.reverse_each do |block|
block.attachment.remove_blank_strings if blank?
block.attachment.freeze
end
end end
ELSE_TAG_NAMES = ['elsif', 'else'].freeze
private_constant :ELSE_TAG_NAMES
def unknown_tag(tag, markup, tokens) def unknown_tag(tag, markup, tokens)
if ['elsif', 'else'].include?(tag) if ELSE_TAG_NAMES.include?(tag)
push_block(tag, markup) push_block(tag, markup)
else else
super super
@@ -61,21 +68,25 @@ module Liquid
end end
@blocks.push(block) @blocks.push(block)
block.attach(BlockBody.new) block.attach(new_body)
end
def parse_expression(markup)
Condition.parse_expression(parse_context, markup)
end end
def lax_parse(markup) def lax_parse(markup)
expressions = markup.scan(ExpressionsAndOperators) expressions = markup.scan(ExpressionsAndOperators)
raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop =~ Syntax raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop =~ Syntax
condition = Condition.new(Expression.parse(Regexp.last_match(1)), Regexp.last_match(2), Expression.parse(Regexp.last_match(3))) condition = Condition.new(parse_expression(Regexp.last_match(1)), Regexp.last_match(2), parse_expression(Regexp.last_match(3)))
until expressions.empty? until expressions.empty?
operator = expressions.pop.to_s.strip operator = expressions.pop.to_s.strip
raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop.to_s =~ Syntax raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop.to_s =~ Syntax
new_condition = Condition.new(Expression.parse(Regexp.last_match(1)), Regexp.last_match(2), Expression.parse(Regexp.last_match(3))) new_condition = Condition.new(parse_expression(Regexp.last_match(1)), Regexp.last_match(2), parse_expression(Regexp.last_match(3)))
raise SyntaxError, options[:locale].t("errors.syntax.if") unless BOOLEAN_OPERATORS.include?(operator) raise SyntaxError, options[:locale].t("errors.syntax.if") unless BOOLEAN_OPERATORS.include?(operator)
new_condition.send(operator, condition) new_condition.send(operator, condition)
condition = new_condition condition = new_condition
@@ -94,7 +105,7 @@ module Liquid
def parse_binary_comparisons(p) def parse_binary_comparisons(p)
condition = parse_comparison(p) condition = parse_comparison(p)
first_condition = condition first_condition = condition
while op = (p.id?('and') || p.id?('or')) while (op = (p.id?('and') || p.id?('or')))
child_condition = parse_comparison(p) child_condition = parse_comparison(p)
condition.send(op, child_condition) condition.send(op, child_condition)
condition = child_condition condition = child_condition
@@ -103,9 +114,9 @@ module Liquid
end end
def parse_comparison(p) def parse_comparison(p)
a = Expression.parse(p.expression) a = parse_expression(p.expression)
if op = p.consume?(:comparison) if (op = p.consume?(:comparison))
b = Expression.parse(p.expression) b = parse_expression(p.expression)
Condition.new(a, op, b) Condition.new(a, op, b)
else else
Condition.new(a) Condition.new(a)

View File

@@ -16,24 +16,28 @@ module Liquid
# {% include 'product' for products %} # {% include 'product' for products %}
# #
class Include < Tag class Include < Tag
Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/o prepend Tag::Disableable
SYNTAX = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
Syntax = SYNTAX
attr_reader :template_name_expr, :variable_name_expr, :attributes attr_reader :template_name_expr, :variable_name_expr, :attributes
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
if markup =~ Syntax if markup =~ SYNTAX
template_name = Regexp.last_match(1) template_name = Regexp.last_match(1)
variable_name = Regexp.last_match(3) variable_name = Regexp.last_match(3)
@variable_name_expr = variable_name ? Expression.parse(variable_name) : nil @alias_name = Regexp.last_match(5)
@template_name_expr = Expression.parse(template_name) @variable_name_expr = variable_name ? parse_expression(variable_name) : nil
@attributes = {} @template_name_expr = parse_expression(template_name)
@attributes = {}
markup.scan(TagAttributes) do |key, value| markup.scan(TagAttributes) do |key, value|
@attributes[key] = Expression.parse(value) @attributes[key] = parse_expression(value)
end end
else else
@@ -54,7 +58,7 @@ module Liquid
parse_context: parse_context parse_context: parse_context
) )
context_variable_name = template_name.split('/').last context_variable_name = @alias_name || template_name.split('/').last
variable = if @variable_name_expr variable = if @variable_name_expr
context.evaluate(@variable_name_expr) context.evaluate(@variable_name_expr)
@@ -63,10 +67,10 @@ module Liquid
end end
old_template_name = context.template_name old_template_name = context.template_name
old_partial = context.partial old_partial = context.partial
begin begin
context.template_name = template_name context.template_name = template_name
context.partial = true context.partial = true
context.stack do context.stack do
@attributes.each do |key, value| @attributes.each do |key, value|
context[key] = context.evaluate(value) context[key] = context.evaluate(value)
@@ -84,7 +88,7 @@ module Liquid
end end
ensure ensure
context.template_name = old_template_name context.template_name = old_template_name
context.partial = old_partial context.partial = old_partial
end end
output output

View File

@@ -13,15 +13,15 @@ module Liquid
def parse(tokens) def parse(tokens)
@body = +'' @body = +''
while token = tokens.shift while (token = tokens.shift)
if token =~ FullTokenPossiblyInvalid if token =~ FullTokenPossiblyInvalid && block_delimiter == Regexp.last_match(2)
@body << Regexp.last_match(1) if Regexp.last_match(1) != "" @body << Regexp.last_match(1) if Regexp.last_match(1) != ""
return if block_delimiter == Regexp.last_match(2) return
end end
@body << token unless token.empty? @body << token unless token.empty?
end end
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_never_closed", block_name: block_name) raise_tag_never_closed(block_name)
end end
def render_to_output_buffer(_context, output) def render_to_output_buffer(_context, output)
@@ -40,7 +40,7 @@ module Liquid
protected protected
def ensure_valid_markup(tag_name, markup, parse_context) def ensure_valid_markup(tag_name, markup, parse_context)
unless markup =~ Syntax unless Syntax.match?(markup)
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_unexpected_args", tag: tag_name) raise SyntaxError, parse_context.locale.t("errors.syntax.tag_unexpected_args", tag: tag_name)
end end
end end

View File

@@ -2,7 +2,10 @@
module Liquid module Liquid
class Render < Tag class Render < Tag
SYNTAX = /(#{QuotedString})#{QuotedFragment}*/o FOR = 'for'
SYNTAX = /(#{QuotedString}+)(\s+(with|#{FOR})\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
disable_tags "include"
attr_reader :template_name_expr, :attributes attr_reader :template_name_expr, :attributes
@@ -12,16 +15,25 @@ module Liquid
raise SyntaxError, options[:locale].t("errors.syntax.render") unless markup =~ SYNTAX raise SyntaxError, options[:locale].t("errors.syntax.render") unless markup =~ SYNTAX
template_name = Regexp.last_match(1) template_name = Regexp.last_match(1)
with_or_for = Regexp.last_match(3)
variable_name = Regexp.last_match(4)
@template_name_expr = Expression.parse(template_name) @alias_name = Regexp.last_match(6)
@variable_name_expr = variable_name ? parse_expression(variable_name) : nil
@template_name_expr = parse_expression(template_name)
@for = (with_or_for == FOR)
@attributes = {} @attributes = {}
markup.scan(TagAttributes) do |key, value| markup.scan(TagAttributes) do |key, value|
@attributes[key] = Expression.parse(value) @attributes[key] = parse_expression(value)
end end
end end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
render_tag(context, output)
end
def render_tag(context, output)
# Though we evaluate this here we will only ever parse it as a string literal. # Though we evaluate this here we will only ever parse it as a string literal.
template_name = context.evaluate(@template_name_expr) template_name = context.evaluate(@template_name_expr)
raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name
@@ -32,13 +44,29 @@ module Liquid
parse_context: parse_context parse_context: parse_context
) )
inner_context = context.new_isolated_subcontext context_variable_name = @alias_name || template_name.split('/').last
inner_context.template_name = template_name
inner_context.partial = true render_partial_func = ->(var, forloop) {
@attributes.each do |key, value| inner_context = context.new_isolated_subcontext
inner_context[key] = context.evaluate(value) inner_context.template_name = template_name
inner_context.partial = true
inner_context['forloop'] = forloop if forloop
@attributes.each do |key, value|
inner_context[key] = context.evaluate(value)
end
inner_context[context_variable_name] = var unless var.nil?
partial.render_to_output_buffer(inner_context, output)
forloop&.send(:increment!)
}
variable = @variable_name_expr ? context.evaluate(@variable_name_expr) : nil
if @for && variable.respond_to?(:each) && variable.respond_to?(:count)
forloop = Liquid::ForloopDrop.new(template_name, variable.count, nil)
variable.each { |var| render_partial_func.call(var, forloop) }
else
render_partial_func.call(variable, nil)
end end
partial.render_to_output_buffer(inner_context, output)
output output
end end

View File

@@ -9,11 +9,11 @@ module Liquid
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
if markup =~ Syntax if markup =~ Syntax
@variable_name = Regexp.last_match(1) @variable_name = Regexp.last_match(1)
@collection_name = Expression.parse(Regexp.last_match(2)) @collection_name = parse_expression(Regexp.last_match(2))
@attributes = {} @attributes = {}
markup.scan(TagAttributes) do |key, value| markup.scan(TagAttributes) do |key, value|
@attributes[key] = Expression.parse(value) @attributes[key] = parse_expression(value)
end end
else else
raise SyntaxError, options[:locale].t("errors.syntax.table_row") raise SyntaxError, options[:locale].t("errors.syntax.table_row")
@@ -24,11 +24,10 @@ module Liquid
(collection = context.evaluate(@collection_name)) || (return '') (collection = context.evaluate(@collection_name)) || (return '')
from = @attributes.key?('offset') ? context.evaluate(@attributes['offset']).to_i : 0 from = @attributes.key?('offset') ? context.evaluate(@attributes['offset']).to_i : 0
to = @attributes.key?('limit') ? from + context.evaluate(@attributes['limit']).to_i : nil to = @attributes.key?('limit') ? from + context.evaluate(@attributes['limit']).to_i : nil
collection = Utils.slice_collection(collection, from, to) collection = Utils.slice_collection(collection, from, to)
length = collection.length
length = collection.length
cols = context.evaluate(@attributes['cols']).to_i cols = context.evaluate(@attributes['cols']).to_i

View File

@@ -18,13 +18,11 @@ module Liquid
attr_accessor :root attr_accessor :root
attr_reader :resource_limits, :warnings attr_reader :resource_limits, :warnings
@@file_system = BlankFileSystem.new
class TagRegistry class TagRegistry
include Enumerable include Enumerable
def initialize def initialize
@tags = {} @tags = {}
@cache = {} @cache = {}
end end
@@ -63,76 +61,54 @@ module Liquid
# :lax acts like liquid 2.5 and silently ignores malformed tags in most cases. # :lax acts like liquid 2.5 and silently ignores malformed tags in most cases.
# :warn is the default and will give deprecation warnings when invalid syntax is used. # :warn is the default and will give deprecation warnings when invalid syntax is used.
# :strict will enforce correct syntax. # :strict will enforce correct syntax.
attr_writer :error_mode attr_accessor :error_mode
Template.error_mode = :lax
# Sets how strict the taint checker should be.
# :lax is the default, and ignores the taint flag completely
# :warn adds a warning, but does not interrupt the rendering
# :error raises an error when tainted output is used
attr_writer :taint_mode
attr_accessor :default_exception_renderer attr_accessor :default_exception_renderer
Template.default_exception_renderer = lambda do |exception| Template.default_exception_renderer = lambda do |exception|
exception exception
end end
def file_system attr_accessor :file_system
@@file_system Template.file_system = BlankFileSystem.new
end
def file_system=(obj) attr_accessor :tags
@@file_system = obj Template.tags = TagRegistry.new
end private :tags=
def register_tag(name, klass) def register_tag(name, klass)
tags[name.to_s] = klass tags[name.to_s] = klass
end end
def tags
@tags ||= TagRegistry.new
end
def error_mode
@error_mode ||= :lax
end
def taint_mode
@taint_mode ||= :lax
end
# Pass a module with filter methods which should be available # Pass a module with filter methods which should be available
# to all liquid views. Good for registering the standard library # to all liquid views. Good for registering the standard library
def register_filter(mod) def register_filter(mod)
Strainer.global_filter(mod) StrainerFactory.add_global_filter(mod)
end end
def default_resource_limits attr_accessor :default_resource_limits
@default_resource_limits ||= {} Template.default_resource_limits = {}
end private :default_resource_limits=
# creates a new <tt>Template</tt> object from liquid source code # creates a new <tt>Template</tt> object from liquid source code
# To enable profiling, pass in <tt>profile: true</tt> as an option. # To enable profiling, pass in <tt>profile: true</tt> as an option.
# See Liquid::Profiler for more information # See Liquid::Profiler for more information
def parse(source, options = {}) def parse(source, options = {})
template = Template.new new.parse(source, options)
template.parse(source, options)
end end
end end
def initialize def initialize
@rethrow_errors = false @rethrow_errors = false
@resource_limits = ResourceLimits.new(self.class.default_resource_limits) @resource_limits = ResourceLimits.new(Template.default_resource_limits)
end end
# Parse source code. # Parse source code.
# Returns self for easy chaining # Returns self for easy chaining
def parse(source, options = {}) def parse(source, options = {})
@options = options parse_context = configure_options(options)
@profiling = options[:profile] tokenizer = parse_context.new_tokenizer(source, start_line_number: @line_numbers && 1)
@line_numbers = options[:line_numbers] || @profiling @root = Document.parse(tokenizer, parse_context)
parse_context = options.is_a?(ParseContext) ? options : ParseContext.new(options)
@root = Document.parse(tokenize(source), parse_context)
@warnings = parse_context.warnings
self self
end end
@@ -174,12 +150,12 @@ module Liquid
c = args.shift c = args.shift
if @rethrow_errors if @rethrow_errors
c.exception_renderer = ->(_e) { raise } c.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA
end end
c c
when Liquid::Drop when Liquid::Drop
drop = args.shift drop = args.shift
drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits) drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
when Hash when Hash
Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits) Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
@@ -191,12 +167,16 @@ module Liquid
output = nil output = nil
context_register = context.registers.is_a?(StaticRegisters) ? context.registers.static : context.registers
case args.last case args.last
when Hash when Hash
options = args.pop options = args.pop
output = options[:output] if options[:output] output = options[:output] if options[:output]
registers.merge!(options[:registers]) if options[:registers].is_a?(Hash) options[:registers]&.each do |key, register|
context_register[key] = register
end
apply_options_to_context(context, options) apply_options_to_context(context, options)
when Module, Array when Module, Array
@@ -206,12 +186,13 @@ module Liquid
# Retrying a render resets resource usage # Retrying a render resets resource usage
context.resource_limits.reset context.resource_limits.reset
if @profiling && context.profiler.nil?
@profiler = context.profiler = Liquid::Profiler.new
end
begin begin
# render the nodelist. # render the nodelist.
# for performance reasons we get an array back here. join will make a string out of it. @root.render_to_output_buffer(context, output || +'')
with_profiling(context) do
@root.render_to_output_buffer(context, output || +'')
end
rescue Liquid::MemoryError => e rescue Liquid::MemoryError => e
context.handle_error(e) context.handle_error(e)
ensure ensure
@@ -230,33 +211,25 @@ module Liquid
private private
def tokenize(source) def configure_options(options)
Tokenizer.new(source, @line_numbers) if (profiling = options[:profile])
end
def with_profiling(context)
if @profiling && !context.partial
raise "Profiler not loaded, require 'liquid/profiler' first" unless defined?(Liquid::Profiler) raise "Profiler not loaded, require 'liquid/profiler' first" unless defined?(Liquid::Profiler)
@profiler = Profiler.new
@profiler.start
begin
yield
ensure
@profiler.stop
end
else
yield
end end
@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 end
def apply_options_to_context(context, options) def apply_options_to_context(context, options)
context.add_filters(options[:filters]) if options[:filters] context.add_filters(options[:filters]) if options[:filters]
context.global_filter = options[:global_filter] if options[:global_filter] context.global_filter = options[:global_filter] if options[:global_filter]
context.exception_renderer = options[:exception_renderer] if options[:exception_renderer] context.exception_renderer = options[:exception_renderer] if options[:exception_renderer]
context.strict_variables = options[:strict_variables] if options[:strict_variables] context.strict_variables = options[:strict_variables] if options[:strict_variables]
context.strict_filters = options[:strict_filters] if options[:strict_filters] context.strict_filters = options[:strict_filters] if options[:strict_filters]
end end
end end
end end

View File

@@ -0,0 +1,9 @@
# frozen_string_literal: true
module Liquid
class TemplateFactory
def for(_template_name)
Liquid::Template.new
end
end
end

View File

@@ -5,10 +5,10 @@ module Liquid
attr_reader :line_number, :for_liquid_tag attr_reader :line_number, :for_liquid_tag
def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false) def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false)
@source = source @source = source
@line_number = line_number || (line_numbers ? 1 : nil) @line_number = line_number || (line_numbers ? 1 : nil)
@for_liquid_tag = for_liquid_tag @for_liquid_tag = for_liquid_tag
@tokens = tokenize @tokens = tokenize
end end
def shift def shift

View File

@@ -12,7 +12,7 @@ module Liquid
def self.slice_collection_using_each(collection, from, to) def self.slice_collection_using_each(collection, from, to)
segments = [] segments = []
index = 0 index = 0
# Maintains Ruby 1.8.7 String#each behaviour on 1.9 # Maintains Ruby 1.8.7 String#each behaviour on 1.9
if collection.is_a?(String) if collection.is_a?(String)
@@ -52,7 +52,7 @@ module Liquid
when Numeric when Numeric
obj obj
when String when String
obj.strip =~ /\A-?\d+\.\d+\z/ ? BigDecimal(obj) : obj.to_i /\A-?\d+\.\d+\z/.match?(obj.strip) ? BigDecimal(obj) : obj.to_i
else else
if obj.respond_to?(:to_number) if obj.respond_to?(:to_number)
obj.to_number obj.to_number

View File

@@ -12,10 +12,10 @@ module Liquid
# {{ user | link }} # {{ user | link }}
# #
class Variable class Variable
FilterMarkupRegex = /#{FilterSeparator}\s*(.*)/om FilterMarkupRegex = /#{FilterSeparator}\s*(.*)/om
FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
FilterArgsRegex = /(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o FilterArgsRegex = /(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o
JustTagAttributes = /\A#{TagAttributes}\z/o JustTagAttributes = /\A#{TagAttributes}\z/o
MarkupWithQuotedFragment = /(#{QuotedFragment})(.*)/om MarkupWithQuotedFragment = /(#{QuotedFragment})(.*)/om
attr_accessor :filters, :name, :line_number attr_accessor :filters, :name, :line_number
@@ -25,12 +25,12 @@ module Liquid
include ParserSwitching include ParserSwitching
def initialize(markup, parse_context) def initialize(markup, parse_context)
@markup = markup @markup = markup
@name = nil @name = nil
@parse_context = parse_context @parse_context = parse_context
@line_number = parse_context.line_number @line_number = parse_context.line_number
parse_with_selected_parser(markup) strict_parse_with_error_mode_fallback(markup)
end end
def raw def raw
@@ -45,9 +45,9 @@ module Liquid
@filters = [] @filters = []
return unless markup =~ MarkupWithQuotedFragment return unless markup =~ MarkupWithQuotedFragment
name_markup = Regexp.last_match(1) name_markup = Regexp.last_match(1)
filter_markup = Regexp.last_match(2) filter_markup = Regexp.last_match(2)
@name = Expression.parse(name_markup) @name = Expression.parse(name_markup)
if filter_markup =~ FilterMarkupRegex if filter_markup =~ FilterMarkupRegex
filters = Regexp.last_match(1).scan(FilterParser) filters = Regexp.last_match(1).scan(FilterParser)
filters.each do |f| filters.each do |f|
@@ -63,6 +63,8 @@ module Liquid
@filters = [] @filters = []
p = Parser.new(markup) p = Parser.new(markup)
return if p.look(:end_of_string)
@name = Expression.parse(p.expression) @name = Expression.parse(p.expression)
while p.consume?(:pipe) while p.consume?(:pipe)
filtername = p.consume(:id) filtername = p.consume(:id)
@@ -81,14 +83,14 @@ module Liquid
end end
def render(context) def render(context)
obj = @filters.inject(context.evaluate(@name)) do |output, (filter_name, filter_args, filter_kwargs)| obj = context.evaluate(@name)
@filters.each do |filter_name, filter_args, filter_kwargs|
filter_args = evaluate_filter_expressions(context, filter_args, filter_kwargs) filter_args = evaluate_filter_expressions(context, filter_args, filter_kwargs)
context.invoke(filter_name, output, *filter_args) obj = context.invoke(filter_name, obj, *filter_args)
end end
obj = context.apply_global_filter(obj) context.apply_global_filter(obj)
taint_check(context, obj)
obj
end end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
@@ -104,14 +106,22 @@ module Liquid
output output
end end
def disabled?(_context)
false
end
def disabled_tags
[]
end
private private
def parse_filter_expressions(filter_name, unparsed_args) def parse_filter_expressions(filter_name, unparsed_args)
filter_args = [] filter_args = []
keyword_args = nil keyword_args = nil
unparsed_args.each do |a| unparsed_args.each do |a|
if matches = a.match(JustTagAttributes) if (matches = a.match(JustTagAttributes))
keyword_args ||= {} keyword_args ||= {}
keyword_args[matches[1]] = Expression.parse(matches[2]) keyword_args[matches[1]] = Expression.parse(matches[2])
else else
filter_args << Expression.parse(a) filter_args << Expression.parse(a)
@@ -134,25 +144,6 @@ module Liquid
parsed_args parsed_args
end end
def taint_check(context, obj)
return unless obj.tainted?
return if Template.taint_mode == :lax
@markup =~ QuotedFragment
name = Regexp.last_match(0)
error = TaintedError.new("variable '#{name}' is tainted and was not escaped")
error.line_number = line_number
error.template_name = context.template_name
case Template.taint_mode
when :warn
context.warnings << error
when :error
raise error
end
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children def children
[@node.name] + @node.filters.flatten [@node.name] + @node.filters.flatten

View File

@@ -3,7 +3,7 @@
module Liquid module Liquid
class VariableLookup class VariableLookup
SQUARE_BRACKETED = /\A\[(.*)\]\z/m SQUARE_BRACKETED = /\A\[(.*)\]\z/m
COMMAND_METHODS = ['size', 'first', 'last'].freeze COMMAND_METHODS = ['size', 'first', 'last'].freeze
attr_reader :name, :lookups attr_reader :name, :lookups
@@ -20,7 +20,7 @@ module Liquid
end end
@name = name @name = name
@lookups = lookups @lookups = lookups
@command_flags = 0 @command_flags = 0
@lookups.each_index do |i| @lookups.each_index do |i|
@@ -34,7 +34,7 @@ module Liquid
end end
def evaluate(context) def evaluate(context)
name = context.evaluate(@name) name = context.evaluate(@name)
object = context.find_variable(name) object = context.find_variable(name)
@lookups.each_index do |i| @lookups.each_index do |i|
@@ -47,7 +47,7 @@ module Liquid
(object.respond_to?(:fetch) && key.is_a?(Integer))) (object.respond_to?(:fetch) && key.is_a?(Integer)))
# if its a proc we will replace the entry with the proc # if its a proc we will replace the entry with the proc
res = context.lookup_and_evaluate(object, key) res = context.lookup_and_evaluate(object, key)
object = res.to_liquid object = res.to_liquid
# Some special cases. If the part wasn't in square brackets and # Some special cases. If the part wasn't in square brackets and

View File

@@ -2,5 +2,5 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
VERSION = "4.0.3" VERSION = "5.0.0"
end end

View File

@@ -17,9 +17,11 @@ Gem::Specification.new do |s|
s.license = "MIT" s.license = "MIT"
# s.description = "A secure, non-evaling end user template engine with aesthetic markup." # s.description = "A secure, non-evaling end user template engine with aesthetic markup."
s.required_ruby_version = ">= 2.4.0" s.required_ruby_version = ">= 2.5.0"
s.required_rubygems_version = ">= 1.3.7" s.required_rubygems_version = ">= 1.3.7"
s.metadata['allowed_push_host'] = 'https://rubygems.org'
s.test_files = Dir.glob("{test}/**/*") s.test_files = Dir.glob("{test}/**/*")
s.files = Dir.glob("{lib}/**/*") + %w(LICENSE README.md) s.files = Dir.glob("{lib}/**/*") + %w(LICENSE README.md)
@@ -27,6 +29,6 @@ Gem::Specification.new do |s|
s.require_path = "lib" s.require_path = "lib"
s.add_development_dependency('rake', '~> 11.3') s.add_development_dependency('rake', '~> 13.0')
s.add_development_dependency('minitest') s.add_development_dependency('minitest')
end end

View File

@@ -7,7 +7,7 @@ Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new profiler = ThemeRunner.new
Benchmark.ips do |x| Benchmark.ips do |x|
x.time = 10 x.time = 10
x.warmup = 5 x.warmup = 5
puts puts

View File

@@ -21,8 +21,8 @@ class Profiler
end end
def profile(phase, &block) def profile(phase, &block)
print LOG_LABEL print(LOG_LABEL)
print "#{phase}.. ".ljust(10) print("#{phase}.. ".ljust(10))
report = MemoryProfiler.report(&block) report = MemoryProfiler.report(&block)
puts 'Done.' puts 'Done.'
@headings << phase.capitalize @headings << phase.capitalize

View File

@@ -15,7 +15,7 @@ profiler.run
end end
end end
if profile_type == :cpu && graph_filename = ENV['GRAPH_FILENAME'] if profile_type == :cpu && (graph_filename = ENV['GRAPH_FILENAME'])
File.open(graph_filename, 'w') do |f| File.open(graph_filename, 'w') do |f|
StackProf::Report.new(results).print_graphviz(nil, f) StackProf::Report.new(results).print_graphviz(nil, f)
end end

View File

@@ -8,7 +8,7 @@ class CommentForm < Liquid::Block
if markup =~ Syntax if markup =~ Syntax
@variable_name = Regexp.last_match(1) @variable_name = Regexp.last_match(1)
@attributes = {} @attributes = {}
else else
raise SyntaxError, "Syntax Error in 'comment_form' - Valid syntax: comment_form [article]" raise SyntaxError, "Syntax Error in 'comment_form' - Valid syntax: comment_form [article]"
end end

View File

@@ -32,8 +32,8 @@ module Database
db['article'] = db['blog']['articles'].first db['article'] = db['blog']['articles'].first
db['cart'] = { db['cart'] = {
'total_price' => db['line_items'].values.inject(0) { |sum, item| sum += item['line_price'] * item['quantity'] }, 'total_price' => db['line_items'].values.inject(0) { |sum, item| sum + item['line_price'] * item['quantity'] },
'item_count' => db['line_items'].values.inject(0) { |sum, item| sum += item['quantity'] }, 'item_count' => db['line_items'].values.inject(0) { |sum, item| sum + item['quantity'] },
'items' => db['line_items'].values, 'items' => db['line_items'].values,
} }

View File

@@ -8,7 +8,7 @@ class Paginate < Liquid::Block
if markup =~ Syntax if markup =~ Syntax
@collection_name = Regexp.last_match(1) @collection_name = Regexp.last_match(1)
@page_size = if Regexp.last_match(2) @page_size = if Regexp.last_match(2)
Regexp.last_match(3).to_i Regexp.last_match(3).to_i
else else
20 20

View File

@@ -31,7 +31,7 @@ module ShopFilter
def link_to_vendor(vendor) def link_to_vendor(vendor)
if vendor if vendor
link_to vendor, url_for_vendor(vendor), vendor link_to(vendor, url_for_vendor(vendor), vendor)
else else
'Unknown Vendor' 'Unknown Vendor'
end end
@@ -39,7 +39,7 @@ module ShopFilter
def link_to_type(type) def link_to_type(type)
if type if type
link_to type, url_for_type(type), type link_to(type, url_for_type(type), type)
else else
'Unknown Vendor' 'Unknown Vendor'
end end
@@ -60,7 +60,7 @@ module ShopFilter
case style case style
when 'original' when 'original'
return '/files/shops/random_number/' + url '/files/shops/random_number/' + url
when 'grande', 'large', 'medium', 'compact', 'small', 'thumb', 'icon' when 'grande', 'large', 'medium', 'compact', 'small', 'thumb', 'icon'
"/files/shops/random_number/products/#{Regexp.last_match(1)}_#{style}.#{Regexp.last_match(2)}" "/files/shops/random_number/products/#{Regexp.last_match(1)}_#{style}.#{Regexp.last_match(2)}"
else else

View File

@@ -58,9 +58,9 @@ class ThemeRunner
# `render` is called to benchmark just the render portion of liquid # `render` is called to benchmark just the render portion of liquid
def render def render
@compiled_tests.each do |test| @compiled_tests.each do |test|
tmpl = test[:tmpl] tmpl = test[:tmpl]
assigns = test[:assigns] assigns = test[:assigns]
layout = test[:layout] layout = test[:layout]
if layout if layout
assigns['content_for_layout'] = tmpl.render!(assigns) assigns['content_for_layout'] = tmpl.render!(assigns)
@@ -73,10 +73,14 @@ class ThemeRunner
private private
def render_layout(template, layout, assigns)
assigns['content_for_layout'] = template.render!(assigns)
layout&.render!(assigns)
end
def compile_and_render(template, layout, assigns, page_template, template_file) def compile_and_render(template, layout, assigns, page_template, template_file)
compiled_test = compile_test(template, layout, assigns, page_template, template_file) compiled_test = compile_test(template, layout, assigns, page_template, template_file)
assigns['content_for_layout'] = compiled_test[:tmpl].render!(assigns) render_layout(compiled_test[:tmpl], compiled_test[:layout], compiled_test[:assigns])
compiled_test[:layout].render!(assigns) if layout
end end
def compile_all_tests def compile_all_tests
@@ -88,7 +92,7 @@ class ThemeRunner
end end
def compile_test(template, layout, assigns, page_template, template_file) def compile_test(template, layout, assigns, page_template, template_file)
tmpl = init_template(page_template, template_file) tmpl = init_template(page_template, template_file)
parsed_template = tmpl.parse(template).dup parsed_template = tmpl.parse(template).dup
if layout if layout
@@ -113,9 +117,9 @@ class ThemeRunner
# set up a new Liquid::Template object for use in `compile_and_render` and `compile_test` # set up a new Liquid::Template object for use in `compile_and_render` and `compile_test`
def init_template(page_template, template_file) def init_template(page_template, template_file)
tmpl = Liquid::Template.new tmpl = Liquid::Template.new
tmpl.assigns['page_title'] = 'Page title' tmpl.assigns['page_title'] = 'Page title'
tmpl.assigns['template'] = page_template tmpl.assigns['template'] = page_template
tmpl.registers[:file_system] = ThemeRunner::FileSystem.new(File.dirname(template_file)) tmpl.registers[:file_system] = ThemeRunner::FileSystem.new(File.dirname(template_file))
tmpl tmpl
end end

View File

@@ -10,9 +10,9 @@ class AssignTest < Minitest::Test
{% assign this-thing = 'Print this-thing' %} {% assign this-thing = 'Print this-thing' %}
{{ this-thing }} {{ this-thing }}
END_TEMPLATE END_TEMPLATE
template = Template.parse(template_source) template = Template.parse(template_source)
rendered = template.render! rendered = template.render!
assert_equal "Print this-thing", rendered.strip assert_equal("Print this-thing", rendered.strip)
end end
def test_assigned_variable def test_assigned_variable
@@ -44,7 +44,74 @@ class AssignTest < Minitest::Test
end end
end end
with_error_mode(:lax) do with_error_mode(:lax) do
assert Template.parse("{% assign foo = ('X' | downcase) %}") assert(Template.parse("{% assign foo = ('X' | downcase) %}"))
end end
end end
end # AssignTest
def test_expression_with_whitespace_in_square_brackets
source = "{% assign r = a[ 'b' ] %}{{ r }}"
assert_template_result('result', source, 'a' => { 'b' => 'result' })
end
def test_assign_score_exceeding_resource_limit
t = Template.parse("{% assign foo = 42 %}{% assign bar = 23 %}")
t.resource_limits.assign_score_limit = 1
assert_equal("Liquid error: Memory limits exceeded", t.render)
assert(t.resource_limits.reached?)
t.resource_limits.assign_score_limit = 2
assert_equal("", t.render!)
refute_nil(t.resource_limits.assign_score)
end
def test_assign_score_exceeding_limit_from_composite_object
t = Template.parse("{% assign foo = 'aaaa' | reverse %}")
t.resource_limits.assign_score_limit = 3
assert_equal("Liquid error: Memory limits exceeded", t.render)
assert(t.resource_limits.reached?)
t.resource_limits.assign_score_limit = 5
assert_equal("", t.render!)
end
def test_assign_score_of_int
assert_equal(1, assign_score_of(123))
end
def test_assign_score_of_string_counts_bytes
assert_equal(3, assign_score_of('123'))
assert_equal(5, assign_score_of('12345'))
assert_equal(9, assign_score_of('すごい'))
end
def test_assign_score_of_array
assert_equal(1, assign_score_of([]))
assert_equal(2, assign_score_of([123]))
assert_equal(6, assign_score_of([123, 'abcd']))
end
def test_assign_score_of_hash
assert_equal(1, assign_score_of({}))
assert_equal(5, assign_score_of('int' => 123))
assert_equal(12, assign_score_of('int' => 123, 'str' => 'abcd'))
end
private
class ObjectWrapperDrop < Liquid::Drop
def initialize(obj)
@obj = obj
end
def value
@obj
end
end
def assign_score_of(obj)
context = Liquid::Context.new('drop' => ObjectWrapperDrop.new(obj))
Liquid::Template.parse('{% assign obj = drop.value %}').render!(context)
context.resource_limits.assign_score
end
end

View File

@@ -96,9 +96,9 @@ class BlankTest < Minitest::Test
def test_include_is_blank def test_include_is_blank
Liquid::Template.file_system = BlankTestFileSystem.new Liquid::Template.file_system = BlankTestFileSystem.new
assert_template_result "foobar" * (N + 1), wrap("{% include 'foobar' %}") assert_template_result("foobar" * (N + 1), wrap("{% include 'foobar' %}"))
assert_template_result " foobar " * (N + 1), wrap("{% include ' foobar ' %}") assert_template_result(" foobar " * (N + 1), wrap("{% include ' foobar ' %}"))
assert_template_result " " * (N + 1), wrap(" {% include ' ' %} ") assert_template_result(" " * (N + 1), wrap(" {% include ' ' %} "))
end end
def test_case_is_blank def test_case_is_blank

View File

@@ -9,6 +9,50 @@ class BlockTest < Minitest::Test
exc = assert_raises(SyntaxError) do exc = assert_raises(SyntaxError) do
Template.parse("{% if true %}{% endunless %}") Template.parse("{% if true %}{% endunless %}")
end end
assert_equal exc.message, "Liquid syntax error: 'endunless' is not a valid delimiter for if tags. use endif" assert_equal(exc.message, "Liquid syntax error: 'endunless' is not a valid delimiter for if tags. use endif")
end
def test_with_custom_tag
with_custom_tag('testtag', Block) do
assert(Liquid::Template.parse("{% testtag %} {% endtesttag %}"))
end
end
def test_custom_block_tags_have_a_default_render_to_output_buffer_method_for_backwards_compatibility
klass1 = Class.new(Block) do
def render(*)
'hello'
end
end
with_custom_tag('blabla', klass1) do
template = Liquid::Template.parse("{% blabla %} bla {% endblabla %}")
assert_equal('hello', template.render)
buf = +''
output = template.render({}, output: buf)
assert_equal('hello', output)
assert_equal('hello', buf)
assert_equal(buf.object_id, output.object_id)
end
klass2 = Class.new(klass1) do
def render(*)
'foo' + super + 'bar'
end
end
with_custom_tag('blabla', klass2) do
template = Liquid::Template.parse("{% blabla %} foo {% endblabla %}")
assert_equal('foohellobar', template.render)
buf = +''
output = template.render({}, output: buf)
assert_equal('foohellobar', output)
assert_equal('foohellobar', buf)
assert_equal(buf.object_id, output.object_id)
end
end end
end end

View File

@@ -14,9 +14,9 @@ class CaptureTest < Minitest::Test
{% capture this-thing %}Print this-thing{% endcapture %} {% capture this-thing %}Print this-thing{% endcapture %}
{{ this-thing }} {{ this-thing }}
END_TEMPLATE END_TEMPLATE
template = Template.parse(template_source) template = Template.parse(template_source)
rendered = template.render! rendered = template.render!
assert_equal "Print this-thing", rendered.strip assert_equal("Print this-thing", rendered.strip)
end end
def test_capture_to_variable_from_outer_scope_if_existing def test_capture_to_variable_from_outer_scope_if_existing
@@ -30,9 +30,9 @@ class CaptureTest < Minitest::Test
{% endif %} {% endif %}
{{var}} {{var}}
END_TEMPLATE END_TEMPLATE
template = Template.parse(template_source) template = Template.parse(template_source)
rendered = template.render! rendered = template.render!
assert_equal "test-string", rendered.gsub(/\s/, '') assert_equal("test-string", rendered.gsub(/\s/, ''))
end end
def test_assigning_from_capture def test_assigning_from_capture
@@ -45,8 +45,14 @@ class CaptureTest < Minitest::Test
{% endfor %} {% endfor %}
{{ first }}-{{ second }} {{ first }}-{{ second }}
END_TEMPLATE END_TEMPLATE
template = Template.parse(template_source) template = Template.parse(template_source)
rendered = template.render! rendered = template.render!
assert_equal "3-3", rendered.gsub(/\s/, '') assert_equal("3-3", rendered.gsub(/\s/, ''))
end end
end # CaptureTest
def test_increment_assign_score_by_bytes_not_characters
t = Template.parse("{% capture foo %}すごい{% endcapture %}")
t.render!
assert_equal(9, t.resource_limits.assign_score)
end
end

View File

@@ -2,9 +2,597 @@
require 'test_helper' require 'test_helper'
class HundredCentes
def to_liquid
100
end
end
class CentsDrop < Liquid::Drop
def amount
HundredCentes.new
end
def non_zero?
true
end
end
class ContextSensitiveDrop < Liquid::Drop
def test
@context['test']
end
end
class Category < Liquid::Drop
attr_accessor :name
def initialize(name)
@name = name
end
def to_liquid
CategoryDrop.new(self)
end
end
class CategoryDrop
attr_accessor :category, :context
def initialize(category)
@category = category
end
end
class CounterDrop < Liquid::Drop
def count
@count ||= 0
@count += 1
end
end
class ArrayLike
def fetch(index)
end
def [](index)
@counts ||= []
@counts[index] ||= 0
@counts[index] += 1
end
def to_liquid
self
end
end
class ContextTest < Minitest::Test class ContextTest < Minitest::Test
include Liquid include Liquid
def setup
@context = Liquid::Context.new
end
def test_variables
@context['string'] = 'string'
assert_equal('string', @context['string'])
@context['num'] = 5
assert_equal(5, @context['num'])
@context['time'] = Time.parse('2006-06-06 12:00:00')
assert_equal(Time.parse('2006-06-06 12:00:00'), @context['time'])
@context['date'] = Date.today
assert_equal(Date.today, @context['date'])
now = Time.now
@context['datetime'] = now
assert_equal(now, @context['datetime'])
@context['bool'] = true
assert_equal(true, @context['bool'])
@context['bool'] = false
assert_equal(false, @context['bool'])
@context['nil'] = nil
assert_nil(@context['nil'])
assert_nil(@context['nil'])
end
def test_variables_not_existing
assert_nil(@context['does_not_exist'])
end
def test_scoping
@context.push
@context.pop
assert_raises(Liquid::ContextError) do
@context.pop
end
assert_raises(Liquid::ContextError) do
@context.push
@context.pop
@context.pop
end
end
def test_length_query
@context['numbers'] = [1, 2, 3, 4]
assert_equal(4, @context['numbers.size'])
@context['numbers'] = { 1 => 1, 2 => 2, 3 => 3, 4 => 4 }
assert_equal(4, @context['numbers.size'])
@context['numbers'] = { 1 => 1, 2 => 2, 3 => 3, 4 => 4, 'size' => 1000 }
assert_equal(1000, @context['numbers.size'])
end
def test_hyphenated_variable
@context['oh-my'] = 'godz'
assert_equal('godz', @context['oh-my'])
end
def test_add_filter
filter = Module.new do
def hi(output)
output + ' hi!'
end
end
context = Context.new
context.add_filters(filter)
assert_equal('hi? hi!', context.invoke(:hi, 'hi?'))
context = Context.new
assert_equal('hi?', context.invoke(:hi, 'hi?'))
context.add_filters(filter)
assert_equal('hi? hi!', context.invoke(:hi, 'hi?'))
end
def test_only_intended_filters_make_it_there
filter = Module.new do
def hi(output)
output + ' hi!'
end
end
context = Context.new
assert_equal("Wookie", context.invoke("hi", "Wookie"))
context.add_filters(filter)
assert_equal("Wookie hi!", context.invoke("hi", "Wookie"))
end
def test_add_item_in_outer_scope
@context['test'] = 'test'
@context.push
assert_equal('test', @context['test'])
@context.pop
assert_equal('test', @context['test'])
end
def test_add_item_in_inner_scope
@context.push
@context['test'] = 'test'
assert_equal('test', @context['test'])
@context.pop
assert_nil(@context['test'])
end
def test_hierachical_data
@context['hash'] = { "name" => 'tobi' }
assert_equal('tobi', @context['hash.name'])
assert_equal('tobi', @context['hash["name"]'])
end
def test_keywords
assert_equal(true, @context['true'])
assert_equal(false, @context['false'])
end
def test_digits
assert_equal(100, @context['100'])
assert_equal(100.00, @context['100.00'])
end
def test_strings
assert_equal("hello!", @context['"hello!"'])
assert_equal("hello!", @context["'hello!'"])
end
def test_merge
@context.merge("test" => "test")
assert_equal('test', @context['test'])
@context.merge("test" => "newvalue", "foo" => "bar")
assert_equal('newvalue', @context['test'])
assert_equal('bar', @context['foo'])
end
def test_array_notation
@context['test'] = [1, 2, 3, 4, 5]
assert_equal(1, @context['test[0]'])
assert_equal(2, @context['test[1]'])
assert_equal(3, @context['test[2]'])
assert_equal(4, @context['test[3]'])
assert_equal(5, @context['test[4]'])
end
def test_recoursive_array_notation
@context['test'] = { 'test' => [1, 2, 3, 4, 5] }
assert_equal(1, @context['test.test[0]'])
@context['test'] = [{ 'test' => 'worked' }]
assert_equal('worked', @context['test[0].test'])
end
def test_hash_to_array_transition
@context['colors'] = {
'Blue' => ['003366', '336699', '6699CC', '99CCFF'],
'Green' => ['003300', '336633', '669966', '99CC99'],
'Yellow' => ['CC9900', 'FFCC00', 'FFFF99', 'FFFFCC'],
'Red' => ['660000', '993333', 'CC6666', 'FF9999'],
}
assert_equal('003366', @context['colors.Blue[0]'])
assert_equal('FF9999', @context['colors.Red[3]'])
end
def test_try_first
@context['test'] = [1, 2, 3, 4, 5]
assert_equal(1, @context['test.first'])
assert_equal(5, @context['test.last'])
@context['test'] = { 'test' => [1, 2, 3, 4, 5] }
assert_equal(1, @context['test.test.first'])
assert_equal(5, @context['test.test.last'])
@context['test'] = [1]
assert_equal(1, @context['test.first'])
assert_equal(1, @context['test.last'])
end
def test_access_hashes_with_hash_notation
@context['products'] = { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] }
@context['product'] = { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] }
assert_equal(5, @context['products["count"]'])
assert_equal('deepsnow', @context['products["tags"][0]'])
assert_equal('deepsnow', @context['products["tags"].first'])
assert_equal('draft151cm', @context['product["variants"][0]["title"]'])
assert_equal('element151cm', @context['product["variants"][1]["title"]'])
assert_equal('draft151cm', @context['product["variants"][0]["title"]'])
assert_equal('element151cm', @context['product["variants"].last["title"]'])
end
def test_access_variable_with_hash_notation
@context['foo'] = 'baz'
@context['bar'] = 'foo'
assert_equal('baz', @context['["foo"]'])
assert_equal('baz', @context['[bar]'])
end
def test_access_hashes_with_hash_access_variables
@context['var'] = 'tags'
@context['nested'] = { 'var' => 'tags' }
@context['products'] = { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] }
assert_equal('deepsnow', @context['products[var].first'])
assert_equal('freestyle', @context['products[nested.var].last'])
end
def test_hash_notation_only_for_hash_access
@context['array'] = [1, 2, 3, 4, 5]
@context['hash'] = { 'first' => 'Hello' }
assert_equal(1, @context['array.first'])
assert_nil(@context['array["first"]'])
assert_equal('Hello', @context['hash["first"]'])
end
def test_first_can_appear_in_middle_of_callchain
@context['product'] = { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] }
assert_equal('draft151cm', @context['product.variants[0].title'])
assert_equal('element151cm', @context['product.variants[1].title'])
assert_equal('draft151cm', @context['product.variants.first.title'])
assert_equal('element151cm', @context['product.variants.last.title'])
end
def test_cents
@context.merge("cents" => HundredCentes.new)
assert_equal(100, @context['cents'])
end
def test_nested_cents
@context.merge("cents" => { 'amount' => HundredCentes.new })
assert_equal(100, @context['cents.amount'])
@context.merge("cents" => { 'cents' => { 'amount' => HundredCentes.new } })
assert_equal(100, @context['cents.cents.amount'])
end
def test_cents_through_drop
@context.merge("cents" => CentsDrop.new)
assert_equal(100, @context['cents.amount'])
end
def test_nested_cents_through_drop
@context.merge("vars" => { "cents" => CentsDrop.new })
assert_equal(100, @context['vars.cents.amount'])
end
def test_drop_methods_with_question_marks
@context.merge("cents" => CentsDrop.new)
assert(@context['cents.non_zero?'])
end
def test_context_from_within_drop
@context.merge("test" => '123', "vars" => ContextSensitiveDrop.new)
assert_equal('123', @context['vars.test'])
end
def test_nested_context_from_within_drop
@context.merge("test" => '123', "vars" => { "local" => ContextSensitiveDrop.new })
assert_equal('123', @context['vars.local.test'])
end
def test_ranges
@context.merge("test" => '5')
assert_equal((1..5), @context['(1..5)'])
assert_equal((1..5), @context['(1..test)'])
assert_equal((5..5), @context['(test..test)'])
end
def test_cents_through_drop_nestedly
@context.merge("cents" => { "cents" => CentsDrop.new })
assert_equal(100, @context['cents.cents.amount'])
@context.merge("cents" => { "cents" => { "cents" => CentsDrop.new } })
assert_equal(100, @context['cents.cents.cents.amount'])
end
def test_drop_with_variable_called_only_once
@context['counter'] = CounterDrop.new
assert_equal(1, @context['counter.count'])
assert_equal(2, @context['counter.count'])
assert_equal(3, @context['counter.count'])
end
def test_drop_with_key_called_only_once
@context['counter'] = CounterDrop.new
assert_equal(1, @context['counter["count"]'])
assert_equal(2, @context['counter["count"]'])
assert_equal(3, @context['counter["count"]'])
end
def test_proc_as_variable
@context['dynamic'] = proc { 'Hello' }
assert_equal('Hello', @context['dynamic'])
end
def test_lambda_as_variable
@context['dynamic'] = proc { 'Hello' }
assert_equal('Hello', @context['dynamic'])
end
def test_nested_lambda_as_variable
@context['dynamic'] = { "lambda" => proc { 'Hello' } }
assert_equal('Hello', @context['dynamic.lambda'])
end
def test_array_containing_lambda_as_variable
@context['dynamic'] = [1, 2, proc { 'Hello' }, 4, 5]
assert_equal('Hello', @context['dynamic[2]'])
end
def test_lambda_is_called_once
@context['callcount'] = proc {
@global ||= 0
@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
@context['callcount'] = { "lambda" => proc {
@global ||= 0
@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
@context['callcount'] = [1, 2, proc {
@global ||= 0
@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
@context.registers[:magic] = 345392
@context['magic'] = proc { @context.registers[:magic] }
assert_equal(345392, @context['magic'])
end
def test_to_liquid_and_context_at_first_level
@context['category'] = Category.new("foobar")
assert_kind_of(CategoryDrop, @context['category'])
assert_equal(@context, @context['category'].context)
end
def test_interrupt_avoids_object_allocations
@context.interrupt? # ruby 3.0.0 allocates on the first call
assert_no_object_allocations do
@context.interrupt?
end
end
def test_context_initialization_with_a_proc_in_environment
contx = Context.new([test: ->(c) { c['poutine'] }], test: :foo)
assert(contx)
assert_nil(contx['poutine'])
end
def test_apply_global_filter
global_filter_proc = ->(output) { "#{output} filtered" }
context = Context.new
context.global_filter = global_filter_proc
assert_equal('hi filtered', context.apply_global_filter('hi'))
end
def test_static_environments_are_read_with_lower_priority_than_environments
context = Context.build(
static_environments: { 'shadowed' => 'static', 'unshadowed' => 'static' },
environments: { 'shadowed' => 'dynamic' }
)
assert_equal('dynamic', context['shadowed'])
assert_equal('static', context['unshadowed'])
end
def test_apply_global_filter_when_no_global_filter_exist
context = Context.new
assert_equal('hi', context.apply_global_filter('hi'))
end
def test_new_isolated_subcontext_does_not_inherit_variables
super_context = Context.new
super_context['my_variable'] = 'some value'
subcontext = super_context.new_isolated_subcontext
assert_nil(subcontext['my_variable'])
end
def test_new_isolated_subcontext_inherits_static_environment
super_context = Context.build(static_environments: { 'my_environment_value' => 'my value' })
subcontext = super_context.new_isolated_subcontext
assert_equal('my value', subcontext['my_environment_value'])
end
def test_new_isolated_subcontext_inherits_resource_limits
resource_limits = ResourceLimits.new({})
super_context = Context.new({}, {}, {}, false, resource_limits)
subcontext = super_context.new_isolated_subcontext
assert_equal(resource_limits, subcontext.resource_limits)
end
def test_new_isolated_subcontext_inherits_exception_renderer
super_context = Context.new
super_context.exception_renderer = ->(_e) { 'my exception message' }
subcontext = super_context.new_isolated_subcontext
assert_equal('my exception message', subcontext.handle_error(Liquid::Error.new))
end
def test_new_isolated_subcontext_does_not_inherit_non_static_registers
registers = {
my_register: :my_value,
}
super_context = Context.new({}, {}, StaticRegisters.new(registers))
super_context.registers[:my_register] = :my_alt_value
subcontext = super_context.new_isolated_subcontext
assert_equal(:my_value, subcontext.registers[:my_register])
end
def test_new_isolated_subcontext_inherits_static_registers
super_context = Context.build(registers: { my_register: :my_value })
subcontext = super_context.new_isolated_subcontext
assert_equal(:my_value, subcontext.registers[:my_register])
end
def test_new_isolated_subcontext_registers_do_not_pollute_context
super_context = Context.build(registers: { my_register: :my_value })
subcontext = super_context.new_isolated_subcontext
subcontext.registers[:my_register] = :my_alt_value
assert_equal(:my_value, super_context.registers[:my_register])
end
def test_new_isolated_subcontext_inherits_filters
my_filter = Module.new do
def my_filter(*)
'my filter result'
end
end
super_context = Context.new
super_context.add_filters([my_filter])
subcontext = super_context.new_isolated_subcontext
template = Template.parse('{{ 123 | my_filter }}')
assert_equal('my filter result', template.render(subcontext))
end
def test_disables_tag_specified
context = Context.new
context.with_disabled_tags(%w(foo bar)) do
assert_equal(true, context.tag_disabled?("foo"))
assert_equal(true, context.tag_disabled?("bar"))
assert_equal(false, context.tag_disabled?("unknown"))
end
end
def test_disables_nested_tags
context = Context.new
context.with_disabled_tags(["foo"]) do
context.with_disabled_tags(["foo"]) do
assert_equal(true, context.tag_disabled?("foo"))
assert_equal(false, context.tag_disabled?("bar"))
end
context.with_disabled_tags(["bar"]) do
assert_equal(true, context.tag_disabled?("foo"))
assert_equal(true, context.tag_disabled?("bar"))
context.with_disabled_tags(["foo"]) do
assert_equal(true, context.tag_disabled?("foo"))
assert_equal(true, context.tag_disabled?("bar"))
end
end
assert_equal(true, context.tag_disabled?("foo"))
assert_equal(false, context.tag_disabled?("bar"))
end
end
def test_override_global_filter def test_override_global_filter
global = Module.new do global = Module.new do
def notice(output) def notice(output)
@@ -19,16 +607,30 @@ class ContextTest < Minitest::Test
end end
with_global_filter(global) do with_global_filter(global) do
assert_equal 'Global test', Template.parse("{{'test' | notice }}").render! assert_equal('Global test', Template.parse("{{'test' | notice }}").render!)
assert_equal 'Local test', Template.parse("{{'test' | notice }}").render!({}, filters: [local]) assert_equal('Local test', Template.parse("{{'test' | notice }}").render!({}, filters: [local]))
end end
end end
def test_has_key_will_not_add_an_error_for_missing_keys def test_has_key_will_not_add_an_error_for_missing_keys
with_error_mode :strict do with_error_mode(:strict) do
context = Context.new context = Context.new
context.key?('unknown') context.key?('unknown')
assert_empty context.errors assert_empty(context.errors)
end end
end end
end
private
def assert_no_object_allocations
unless RUBY_ENGINE == 'ruby'
skip("stackprof needed to count object allocations")
end
require 'stackprof'
profile = StackProf.run(mode: :object) do
yield
end
assert_equal(0, profile[:samples])
end
end # ContextTest

View File

@@ -9,13 +9,13 @@ class DocumentTest < Minitest::Test
exc = assert_raises(SyntaxError) do exc = assert_raises(SyntaxError) do
Template.parse("{% else %}") Template.parse("{% else %}")
end end
assert_equal exc.message, "Liquid syntax error: Unexpected outer 'else' tag" assert_equal(exc.message, "Liquid syntax error: Unexpected outer 'else' tag")
end end
def test_unknown_tag def test_unknown_tag
exc = assert_raises(SyntaxError) do exc = assert_raises(SyntaxError) do
Template.parse("{% foo %}") Template.parse("{% foo %}")
end end
assert_equal exc.message, "Liquid syntax error: Unknown tag 'foo'" assert_equal(exc.message, "Liquid syntax error: Unknown tag 'foo'")
end end
end end

View File

@@ -49,10 +49,6 @@ class ProductDrop < Liquid::Drop
ContextDrop.new ContextDrop.new
end end
def user_input
(+"foo").taint
end
protected protected
def callmenot def callmenot
@@ -111,172 +107,151 @@ class DropsTest < Minitest::Test
def test_product_drop def test_product_drop
tpl = Liquid::Template.parse(' ') tpl = Liquid::Template.parse(' ')
assert_equal ' ', tpl.render!('product' => ProductDrop.new) assert_equal(' ', tpl.render!('product' => ProductDrop.new))
end
def test_rendering_raises_on_tainted_attr
with_taint_mode(:error) do
tpl = Liquid::Template.parse('{{ product.user_input }}')
assert_raises TaintedError do
tpl.render!('product' => ProductDrop.new)
end
end
end
def test_rendering_warns_on_tainted_attr
with_taint_mode(:warn) do
tpl = Liquid::Template.parse('{{ product.user_input }}')
context = Context.new('product' => ProductDrop.new)
tpl.render!(context)
assert_equal [Liquid::TaintedError], context.warnings.map(&:class)
assert_equal "variable 'product.user_input' is tainted and was not escaped", context.warnings.first.to_s(false)
end
end
def test_rendering_doesnt_raise_on_escaped_tainted_attr
with_taint_mode(:error) do
tpl = Liquid::Template.parse('{{ product.user_input | escape }}')
tpl.render!('product' => ProductDrop.new)
end
end end
def test_drop_does_only_respond_to_whitelisted_methods def test_drop_does_only_respond_to_whitelisted_methods
assert_equal "", Liquid::Template.parse("{{ product.inspect }}").render!('product' => ProductDrop.new) assert_equal("", Liquid::Template.parse("{{ product.inspect }}").render!('product' => ProductDrop.new))
assert_equal "", Liquid::Template.parse("{{ product.pretty_inspect }}").render!('product' => ProductDrop.new) assert_equal("", Liquid::Template.parse("{{ product.pretty_inspect }}").render!('product' => ProductDrop.new))
assert_equal "", Liquid::Template.parse("{{ product.whatever }}").render!('product' => ProductDrop.new) assert_equal("", Liquid::Template.parse("{{ product.whatever }}").render!('product' => ProductDrop.new))
assert_equal "", Liquid::Template.parse('{{ product | map: "inspect" }}').render!('product' => ProductDrop.new) assert_equal("", Liquid::Template.parse('{{ product | map: "inspect" }}').render!('product' => ProductDrop.new))
assert_equal "", Liquid::Template.parse('{{ product | map: "pretty_inspect" }}').render!('product' => ProductDrop.new) assert_equal("", Liquid::Template.parse('{{ product | map: "pretty_inspect" }}').render!('product' => ProductDrop.new))
assert_equal "", Liquid::Template.parse('{{ product | map: "whatever" }}').render!('product' => ProductDrop.new) assert_equal("", Liquid::Template.parse('{{ product | map: "whatever" }}').render!('product' => ProductDrop.new))
end end
def test_drops_respond_to_to_liquid def test_drops_respond_to_to_liquid
assert_equal "text1", Liquid::Template.parse("{{ product.to_liquid.texts.text }}").render!('product' => ProductDrop.new) assert_equal("text1", Liquid::Template.parse("{{ product.to_liquid.texts.text }}").render!('product' => ProductDrop.new))
assert_equal "text1", Liquid::Template.parse('{{ product | map: "to_liquid" | map: "texts" | map: "text" }}').render!('product' => ProductDrop.new) assert_equal("text1", Liquid::Template.parse('{{ product | map: "to_liquid" | map: "texts" | map: "text" }}').render!('product' => ProductDrop.new))
end end
def test_text_drop def test_text_drop
output = Liquid::Template.parse(' {{ product.texts.text }} ').render!('product' => ProductDrop.new) output = Liquid::Template.parse(' {{ product.texts.text }} ').render!('product' => ProductDrop.new)
assert_equal ' text1 ', output assert_equal(' text1 ', output)
end end
def test_catchall_unknown_method def test_catchall_unknown_method
output = Liquid::Template.parse(' {{ product.catchall.unknown }} ').render!('product' => ProductDrop.new) output = Liquid::Template.parse(' {{ product.catchall.unknown }} ').render!('product' => ProductDrop.new)
assert_equal ' catchall_method: unknown ', output assert_equal(' catchall_method: unknown ', output)
end end
def test_catchall_integer_argument_drop def test_catchall_integer_argument_drop
output = Liquid::Template.parse(' {{ product.catchall[8] }} ').render!('product' => ProductDrop.new) output = Liquid::Template.parse(' {{ product.catchall[8] }} ').render!('product' => ProductDrop.new)
assert_equal ' catchall_method: 8 ', output assert_equal(' catchall_method: 8 ', output)
end end
def test_text_array_drop def test_text_array_drop
output = Liquid::Template.parse('{% for text in product.texts.array %} {{text}} {% endfor %}').render!('product' => ProductDrop.new) output = Liquid::Template.parse('{% for text in product.texts.array %} {{text}} {% endfor %}').render!('product' => ProductDrop.new)
assert_equal ' text1 text2 ', output assert_equal(' text1 text2 ', output)
end end
def test_context_drop def test_context_drop
output = Liquid::Template.parse(' {{ context.bar }} ').render!('context' => ContextDrop.new, 'bar' => "carrot") output = Liquid::Template.parse(' {{ context.bar }} ').render!('context' => ContextDrop.new, 'bar' => "carrot")
assert_equal ' carrot ', output assert_equal(' carrot ', output)
end
def test_context_drop_array_with_map
output = Liquid::Template.parse(' {{ contexts | map: "bar" }} ').render!('contexts' => [ContextDrop.new, ContextDrop.new], 'bar' => "carrot")
assert_equal(' carrotcarrot ', output)
end end
def test_nested_context_drop def test_nested_context_drop
output = Liquid::Template.parse(' {{ product.context.foo }} ').render!('product' => ProductDrop.new, 'foo' => "monkey") output = Liquid::Template.parse(' {{ product.context.foo }} ').render!('product' => ProductDrop.new, 'foo' => "monkey")
assert_equal ' monkey ', output assert_equal(' monkey ', output)
end end
def test_protected def test_protected
output = Liquid::Template.parse(' {{ product.callmenot }} ').render!('product' => ProductDrop.new) output = Liquid::Template.parse(' {{ product.callmenot }} ').render!('product' => ProductDrop.new)
assert_equal ' ', output assert_equal(' ', output)
end end
def test_object_methods_not_allowed def test_object_methods_not_allowed
[:dup, :clone, :singleton_class, :eval, :class_eval, :inspect].each do |method| [:dup, :clone, :singleton_class, :eval, :class_eval, :inspect].each do |method|
output = Liquid::Template.parse(" {{ product.#{method} }} ").render!('product' => ProductDrop.new) output = Liquid::Template.parse(" {{ product.#{method} }} ").render!('product' => ProductDrop.new)
assert_equal ' ', output assert_equal(' ', output)
end end
end end
def test_scope def test_scope
assert_equal '1', Liquid::Template.parse('{{ context.scopes }}').render!('context' => ContextDrop.new) assert_equal('1', Liquid::Template.parse('{{ context.scopes }}').render!('context' => ContextDrop.new))
assert_equal '2', Liquid::Template.parse('{%for i in dummy%}{{ context.scopes }}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1]) assert_equal('2', Liquid::Template.parse('{%for i in dummy%}{{ context.scopes }}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1]))
assert_equal '3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ context.scopes }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1]) assert_equal('3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ context.scopes }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1]))
end end
def test_scope_though_proc def test_scope_though_proc
assert_equal '1', Liquid::Template.parse('{{ s }}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }) assert_equal('1', Liquid::Template.parse('{{ s }}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }))
assert_equal '2', Liquid::Template.parse('{%for i in dummy%}{{ s }}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }, 'dummy' => [1]) assert_equal('2', Liquid::Template.parse('{%for i in dummy%}{{ s }}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }, 'dummy' => [1]))
assert_equal '3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ s }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }, 'dummy' => [1]) assert_equal('3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ s }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }, 'dummy' => [1]))
end end
def test_scope_with_assigns def test_scope_with_assigns
assert_equal 'variable', Liquid::Template.parse('{% assign a = "variable"%}{{a}}').render!('context' => ContextDrop.new) assert_equal('variable', Liquid::Template.parse('{% assign a = "variable"%}{{a}}').render!('context' => ContextDrop.new))
assert_equal 'variable', Liquid::Template.parse('{% assign a = "variable"%}{%for i in dummy%}{{a}}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1]) assert_equal('variable', Liquid::Template.parse('{% assign a = "variable"%}{%for i in dummy%}{{a}}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1]))
assert_equal 'test', Liquid::Template.parse('{% assign header_gif = "test"%}{{header_gif}}').render!('context' => ContextDrop.new) assert_equal('test', Liquid::Template.parse('{% assign header_gif = "test"%}{{header_gif}}').render!('context' => ContextDrop.new))
assert_equal 'test', Liquid::Template.parse("{% assign header_gif = 'test'%}{{header_gif}}").render!('context' => ContextDrop.new) assert_equal('test', Liquid::Template.parse("{% assign header_gif = 'test'%}{{header_gif}}").render!('context' => ContextDrop.new))
end end
def test_scope_from_tags def test_scope_from_tags
assert_equal '1', Liquid::Template.parse('{% for i in context.scopes_as_array %}{{i}}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1]) assert_equal('1', Liquid::Template.parse('{% for i in context.scopes_as_array %}{{i}}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1]))
assert_equal '12', Liquid::Template.parse('{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1]) assert_equal('12', Liquid::Template.parse('{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1]))
assert_equal '123', Liquid::Template.parse('{%for a in dummy%}{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1]) assert_equal('123', Liquid::Template.parse('{%for a in dummy%}{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1]))
end end
def test_access_context_from_drop def test_access_context_from_drop
assert_equal '123', Liquid::Template.parse('{%for a in dummy%}{{ context.loop_pos }}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1, 2, 3]) assert_equal('123', Liquid::Template.parse('{%for a in dummy%}{{ context.loop_pos }}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1, 2, 3]))
end end
def test_enumerable_drop def test_enumerable_drop
assert_equal '123', Liquid::Template.parse('{% for c in collection %}{{c}}{% endfor %}').render!('collection' => EnumerableDrop.new) assert_equal('123', Liquid::Template.parse('{% for c in collection %}{{c}}{% endfor %}').render!('collection' => EnumerableDrop.new))
end end
def test_enumerable_drop_size def test_enumerable_drop_size
assert_equal '3', Liquid::Template.parse('{{collection.size}}').render!('collection' => EnumerableDrop.new) assert_equal('3', Liquid::Template.parse('{{collection.size}}').render!('collection' => EnumerableDrop.new))
end end
def test_enumerable_drop_will_invoke_liquid_method_missing_for_clashing_method_names def test_enumerable_drop_will_invoke_liquid_method_missing_for_clashing_method_names
["select", "each", "map", "cycle"].each do |method| ["select", "each", "map", "cycle"].each do |method|
assert_equal method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new) assert_equal(method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new))
assert_equal method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new) assert_equal(method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new))
assert_equal method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new) assert_equal(method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new))
assert_equal method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new) assert_equal(method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new))
end end
end end
def test_some_enumerable_methods_still_get_invoked def test_some_enumerable_methods_still_get_invoked
[:count, :max].each do |method| [:count, :max].each do |method|
assert_equal "3", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new) assert_equal("3", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new))
assert_equal "3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new) assert_equal("3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new))
assert_equal "3", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new) assert_equal("3", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new))
assert_equal "3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new) assert_equal("3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new))
end end
assert_equal "yes", Liquid::Template.parse("{% if collection contains 3 %}yes{% endif %}").render!('collection' => RealEnumerableDrop.new) assert_equal("yes", Liquid::Template.parse("{% if collection contains 3 %}yes{% endif %}").render!('collection' => RealEnumerableDrop.new))
[:min, :first].each do |method| [:min, :first].each do |method|
assert_equal "1", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new) assert_equal("1", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new))
assert_equal "1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new) assert_equal("1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new))
assert_equal "1", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new) assert_equal("1", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new))
assert_equal "1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new) assert_equal("1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new))
end end
end end
def test_empty_string_value_access def test_empty_string_value_access
assert_equal '', Liquid::Template.parse('{{ product[value] }}').render!('product' => ProductDrop.new, 'value' => '') assert_equal('', Liquid::Template.parse('{{ product[value] }}').render!('product' => ProductDrop.new, 'value' => ''))
end end
def test_nil_value_access def test_nil_value_access
assert_equal '', Liquid::Template.parse('{{ product[value] }}').render!('product' => ProductDrop.new, 'value' => nil) assert_equal('', Liquid::Template.parse('{{ product[value] }}').render!('product' => ProductDrop.new, 'value' => nil))
end end
def test_default_to_s_on_drops def test_default_to_s_on_drops
assert_equal 'ProductDrop', Liquid::Template.parse("{{ product }}").render!('product' => ProductDrop.new) assert_equal('ProductDrop', Liquid::Template.parse("{{ product }}").render!('product' => ProductDrop.new))
assert_equal 'EnumerableDrop', Liquid::Template.parse('{{ collection }}').render!('collection' => EnumerableDrop.new) assert_equal('EnumerableDrop', Liquid::Template.parse('{{ collection }}').render!('collection' => EnumerableDrop.new))
end end
def test_invokable_methods def test_invokable_methods
assert_equal %w(to_liquid catchall user_input context texts).to_set, ProductDrop.invokable_methods assert_equal(%w(to_liquid catchall context texts).to_set, ProductDrop.invokable_methods)
assert_equal %w(to_liquid scopes_as_array loop_pos scopes).to_set, ContextDrop.invokable_methods assert_equal(%w(to_liquid scopes_as_array loop_pos scopes).to_set, ContextDrop.invokable_methods)
assert_equal %w(to_liquid size max min first count).to_set, EnumerableDrop.invokable_methods assert_equal(%w(to_liquid size max min first count).to_set, EnumerableDrop.invokable_methods)
assert_equal %w(to_liquid max min sort count first).to_set, RealEnumerableDrop.invokable_methods assert_equal(%w(to_liquid max min sort count first).to_set, RealEnumerableDrop.invokable_methods)
end end
end # DropsTest end # DropsTest

View File

@@ -35,31 +35,31 @@ class ErrorHandlingTest < Minitest::Test
TEXT TEXT
output = Liquid::Template.parse(template, line_numbers: true).render('errors' => ErrorDrop.new) output = Liquid::Template.parse(template, line_numbers: true).render('errors' => ErrorDrop.new)
assert_equal expected, output assert_equal(expected, output)
end end
def test_standard_error def test_standard_error
template = Liquid::Template.parse(' {{ errors.standard_error }} ') template = Liquid::Template.parse(' {{ errors.standard_error }} ')
assert_equal ' Liquid error: standard error ', template.render('errors' => ErrorDrop.new) assert_equal(' Liquid error: standard error ', template.render('errors' => ErrorDrop.new))
assert_equal 1, template.errors.size assert_equal(1, template.errors.size)
assert_equal StandardError, template.errors.first.class assert_equal(StandardError, template.errors.first.class)
end end
def test_syntax def test_syntax
template = Liquid::Template.parse(' {{ errors.syntax_error }} ') template = Liquid::Template.parse(' {{ errors.syntax_error }} ')
assert_equal ' Liquid syntax error: syntax error ', template.render('errors' => ErrorDrop.new) assert_equal(' Liquid syntax error: syntax error ', template.render('errors' => ErrorDrop.new))
assert_equal 1, template.errors.size assert_equal(1, template.errors.size)
assert_equal SyntaxError, template.errors.first.class assert_equal(SyntaxError, template.errors.first.class)
end end
def test_argument def test_argument
template = Liquid::Template.parse(' {{ errors.argument_error }} ') template = Liquid::Template.parse(' {{ errors.argument_error }} ')
assert_equal ' Liquid error: argument error ', template.render('errors' => ErrorDrop.new) assert_equal(' Liquid error: argument error ', template.render('errors' => ErrorDrop.new))
assert_equal 1, template.errors.size assert_equal(1, template.errors.size)
assert_equal ArgumentError, template.errors.first.class assert_equal(ArgumentError, template.errors.first.class)
end end
def test_missing_endtag_parse_time_error def test_missing_endtag_parse_time_error
@@ -78,9 +78,9 @@ class ErrorHandlingTest < Minitest::Test
def test_lax_unrecognized_operator def test_lax_unrecognized_operator
template = Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', error_mode: :lax) template = Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', error_mode: :lax)
assert_equal ' Liquid error: Unknown operator =! ', template.render assert_equal(' Liquid error: Unknown operator =! ', template.render)
assert_equal 1, template.errors.size assert_equal(1, template.errors.size)
assert_equal Liquid::ArgumentError, template.errors.first.class assert_equal(Liquid::ArgumentError, template.errors.first.class)
end end
def test_with_line_numbers_adds_numbers_to_parser_errors def test_with_line_numbers_adds_numbers_to_parser_errors
@@ -124,8 +124,8 @@ class ErrorHandlingTest < Minitest::Test
error_mode: :warn, error_mode: :warn,
line_numbers: true) line_numbers: true)
assert_equal ['Liquid syntax error (line 4): Unexpected character = in "1 =! 2"'], assert_equal(['Liquid syntax error (line 4): Unexpected character = in "1 =! 2"'],
template.warnings.map(&:message) template.warnings.map(&:message))
end end
def test_parsing_strict_with_line_numbers_adds_numbers_to_lexer_errors def test_parsing_strict_with_line_numbers_adds_numbers_to_lexer_errors
@@ -141,7 +141,7 @@ class ErrorHandlingTest < Minitest::Test
line_numbers: true) line_numbers: true)
end end
assert_equal 'Liquid syntax error (line 4): Unexpected character = in "1 =! 2"', err.message assert_equal('Liquid syntax error (line 4): Unexpected character = in "1 =! 2"', err.message)
end end
def test_syntax_errors_in_nested_blocks_have_correct_line_number def test_syntax_errors_in_nested_blocks_have_correct_line_number
@@ -158,42 +158,42 @@ class ErrorHandlingTest < Minitest::Test
line_numbers: true) line_numbers: true)
end end
assert_equal "Liquid syntax error (line 5): Unknown tag 'foo'", err.message assert_equal("Liquid syntax error (line 5): Unknown tag 'foo'", err.message)
end end
def test_strict_error_messages def test_strict_error_messages
err = assert_raises(SyntaxError) do err = assert_raises(SyntaxError) do
Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', error_mode: :strict) Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', error_mode: :strict)
end end
assert_equal 'Liquid syntax error: Unexpected character = in "1 =! 2"', err.message assert_equal('Liquid syntax error: Unexpected character = in "1 =! 2"', err.message)
err = assert_raises(SyntaxError) do err = assert_raises(SyntaxError) do
Liquid::Template.parse('{{%%%}}', error_mode: :strict) Liquid::Template.parse('{{%%%}}', error_mode: :strict)
end end
assert_equal 'Liquid syntax error: Unexpected character % in "{{%%%}}"', err.message assert_equal('Liquid syntax error: Unexpected character % in "{{%%%}}"', err.message)
end end
def test_warnings def test_warnings
template = Liquid::Template.parse('{% if ~~~ %}{{%%%}}{% else %}{{ hello. }}{% endif %}', error_mode: :warn) template = Liquid::Template.parse('{% if ~~~ %}{{%%%}}{% else %}{{ hello. }}{% endif %}', error_mode: :warn)
assert_equal 3, template.warnings.size assert_equal(3, template.warnings.size)
assert_equal 'Unexpected character ~ in "~~~"', template.warnings[0].to_s(false) assert_equal('Unexpected character ~ in "~~~"', template.warnings[0].to_s(false))
assert_equal 'Unexpected character % in "{{%%%}}"', template.warnings[1].to_s(false) assert_equal('Unexpected character % in "{{%%%}}"', template.warnings[1].to_s(false))
assert_equal 'Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].to_s(false) assert_equal('Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].to_s(false))
assert_equal '', template.render assert_equal('', template.render)
end end
def test_warning_line_numbers def test_warning_line_numbers
template = Liquid::Template.parse("{% if ~~~ %}\n{{%%%}}{% else %}\n{{ hello. }}{% endif %}", error_mode: :warn, line_numbers: true) template = Liquid::Template.parse("{% if ~~~ %}\n{{%%%}}{% else %}\n{{ hello. }}{% endif %}", error_mode: :warn, line_numbers: true)
assert_equal 'Liquid syntax error (line 1): Unexpected character ~ in "~~~"', template.warnings[0].message assert_equal('Liquid syntax error (line 1): Unexpected character ~ in "~~~"', template.warnings[0].message)
assert_equal 'Liquid syntax error (line 2): Unexpected character % in "{{%%%}}"', template.warnings[1].message assert_equal('Liquid syntax error (line 2): Unexpected character % in "{{%%%}}"', template.warnings[1].message)
assert_equal 'Liquid syntax error (line 3): Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].message assert_equal('Liquid syntax error (line 3): Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].message)
assert_equal 3, template.warnings.size assert_equal(3, template.warnings.size)
assert_equal [1, 2, 3], template.warnings.map(&:line_number) assert_equal([1, 2, 3], template.warnings.map(&:line_number))
end end
# Liquid should not catch Exceptions that are not subclasses of StandardError, like Interrupt and NoMemoryError # Liquid should not catch Exceptions that are not subclasses of StandardError, like Interrupt and NoMemoryError
def test_exceptions_propagate def test_exceptions_propagate
assert_raises Exception do assert_raises(Exception) do
template = Liquid::Template.parse('{{ errors.exception }}') template = Liquid::Template.parse('{{ errors.exception }}')
template.render('errors' => ErrorDrop.new) template.render('errors' => ErrorDrop.new)
end end
@@ -204,35 +204,41 @@ class ErrorHandlingTest < Minitest::Test
output = template.render('errors' => ErrorDrop.new) output = template.render('errors' => ErrorDrop.new)
assert_equal 'This is a runtime error: Liquid error (line 1): internal', output assert_equal('This is a runtime error: Liquid error (line 1): internal', output)
assert_equal [Liquid::InternalError], template.errors.map(&:class) assert_equal([Liquid::InternalError], template.errors.map(&:class))
end end
def test_setting_default_exception_renderer def test_setting_default_exception_renderer
old_exception_renderer = Liquid::Template.default_exception_renderer old_exception_renderer = Liquid::Template.default_exception_renderer
exceptions = [] exceptions = []
Liquid::Template.default_exception_renderer = ->(e) { exceptions << e; '' } Liquid::Template.default_exception_renderer = ->(e) {
exceptions << e
''
}
template = Liquid::Template.parse('This is a runtime error: {{ errors.argument_error }}') template = Liquid::Template.parse('This is a runtime error: {{ errors.argument_error }}')
output = template.render('errors' => ErrorDrop.new) output = template.render('errors' => ErrorDrop.new)
assert_equal 'This is a runtime error: ', output assert_equal('This is a runtime error: ', output)
assert_equal [Liquid::ArgumentError], template.errors.map(&:class) assert_equal([Liquid::ArgumentError], template.errors.map(&:class))
ensure ensure
Liquid::Template.default_exception_renderer = old_exception_renderer if old_exception_renderer Liquid::Template.default_exception_renderer = old_exception_renderer if old_exception_renderer
end end
def test_exception_renderer_exposing_non_liquid_error def test_exception_renderer_exposing_non_liquid_error
template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true) template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true)
exceptions = [] exceptions = []
handler = ->(e) { exceptions << e; e.cause } handler = ->(e) {
exceptions << e
e.cause
}
output = template.render({ 'errors' => ErrorDrop.new }, exception_renderer: handler) output = template.render({ 'errors' => ErrorDrop.new }, exception_renderer: handler)
assert_equal 'This is a runtime error: runtime error', output assert_equal('This is a runtime error: runtime error', output)
assert_equal [Liquid::InternalError], exceptions.map(&:class) assert_equal([Liquid::InternalError], exceptions.map(&:class))
assert_equal exceptions, template.errors assert_equal(exceptions, template.errors)
assert_equal '#<RuntimeError: runtime error>', exceptions.first.cause.inspect assert_equal('#<RuntimeError: runtime error>', exceptions.first.cause.inspect)
end end
class TestFileSystem class TestFileSystem
@@ -246,12 +252,21 @@ class ErrorHandlingTest < Minitest::Test
begin begin
Liquid::Template.file_system = TestFileSystem.new Liquid::Template.file_system = TestFileSystem.new
template = Liquid::Template.parse("Argument error:\n{% include 'product' %}", line_numbers: true) template = Liquid::Template.parse("Argument error:\n{% include 'product' %}", line_numbers: true)
page = template.render('errors' => ErrorDrop.new) page = template.render('errors' => ErrorDrop.new)
ensure ensure
Liquid::Template.file_system = old_file_system Liquid::Template.file_system = old_file_system
end end
assert_equal "Argument error:\nLiquid error (product line 1): argument error", page assert_equal("Argument error:\nLiquid error (product line 1): argument error", page)
assert_equal "product", template.errors.first.template_name assert_equal("product", template.errors.first.template_name)
end
def test_bug_compatible_silencing_of_errors_in_blank_nodes
output = Liquid::Template.parse("{% assign x = 0 %}{% if 1 < '2' %}not blank{% assign x = 3 %}{% endif %}{{ x }}").render
assert_equal("Liquid error: comparison of Integer with String failed0", output)
output = Liquid::Template.parse("{% assign x = 0 %}{% if 1 < '2' %}{% assign x = 3 %}{% endif %}{{ x }}").render
assert_equal("0", output)
end end
end end

View File

@@ -0,0 +1,46 @@
# frozen_string_literal: true
require 'test_helper'
class ExpressionTest < Minitest::Test
def test_keyword_literals
assert_equal(true, parse_and_eval("true"))
assert_equal(true, parse_and_eval(" true "))
end
def test_string
assert_equal("single quoted", parse_and_eval("'single quoted'"))
assert_equal("double quoted", parse_and_eval('"double quoted"'))
assert_equal("spaced", parse_and_eval(" 'spaced' "))
assert_equal("spaced2", parse_and_eval(' "spaced2" '))
end
def test_int
assert_equal(123, parse_and_eval("123"))
assert_equal(456, parse_and_eval(" 456 "))
assert_equal(12, parse_and_eval("012"))
end
def test_float
assert_equal(1.5, parse_and_eval("1.5"))
assert_equal(2.5, parse_and_eval(" 2.5 "))
end
def test_range
assert_equal(1..2, parse_and_eval("(1..2)"))
assert_equal(3..4, parse_and_eval(" ( 3 .. 4 ) "))
end
private
def parse_and_eval(markup, **assigns)
if Liquid::Template.error_mode == :strict
p = Liquid::Parser.new(markup)
markup = p.expression
p.consume(:end_of_string)
end
expression = Liquid::Expression.parse(markup)
context = Liquid::Context.new(assigns)
context.evaluate(expression)
end
end

View File

@@ -41,13 +41,13 @@ class FiltersTest < Minitest::Test
@context['var'] = 1000 @context['var'] = 1000
@context.add_filters(MoneyFilter) @context.add_filters(MoneyFilter)
assert_equal ' 1000$ ', Template.parse("{{var | money}}").render(@context) assert_equal(' 1000$ ', Template.parse("{{var | money}}").render(@context))
end end
def test_underscore_in_filter_name def test_underscore_in_filter_name
@context['var'] = 1000 @context['var'] = 1000
@context.add_filters(MoneyFilter) @context.add_filters(MoneyFilter)
assert_equal ' 1000$ ', Template.parse("{{var | money_with_underscore}}").render(@context) assert_equal(' 1000$ ', Template.parse("{{var | money_with_underscore}}").render(@context))
end end
def test_second_filter_overwrites_first def test_second_filter_overwrites_first
@@ -55,103 +55,112 @@ class FiltersTest < Minitest::Test
@context.add_filters(MoneyFilter) @context.add_filters(MoneyFilter)
@context.add_filters(CanadianMoneyFilter) @context.add_filters(CanadianMoneyFilter)
assert_equal ' 1000$ CAD ', Template.parse("{{var | money}}").render(@context) assert_equal(' 1000$ CAD ', Template.parse("{{var | money}}").render(@context))
end end
def test_size def test_size
@context['var'] = 'abcd' @context['var'] = 'abcd'
@context.add_filters(MoneyFilter) @context.add_filters(MoneyFilter)
assert_equal '4', Template.parse("{{var | size}}").render(@context) assert_equal('4', Template.parse("{{var | size}}").render(@context))
end end
def test_join def test_join
@context['var'] = [1, 2, 3, 4] @context['var'] = [1, 2, 3, 4]
assert_equal "1 2 3 4", Template.parse("{{var | join}}").render(@context) assert_equal("1 2 3 4", Template.parse("{{var | join}}").render(@context))
end end
def test_sort def test_sort
@context['value'] = 3 @context['value'] = 3
@context['numbers'] = [2, 1, 4, 3] @context['numbers'] = [2, 1, 4, 3]
@context['words'] = ['expected', 'as', 'alphabetic'] @context['words'] = ['expected', 'as', 'alphabetic']
@context['arrays'] = ['flower', 'are'] @context['arrays'] = ['flower', 'are']
@context['case_sensitive'] = ['sensitive', 'Expected', 'case'] @context['case_sensitive'] = ['sensitive', 'Expected', 'case']
assert_equal '1 2 3 4', Template.parse("{{numbers | sort | join}}").render(@context) assert_equal('1 2 3 4', Template.parse("{{numbers | sort | join}}").render(@context))
assert_equal 'alphabetic as expected', Template.parse("{{words | sort | join}}").render(@context) assert_equal('alphabetic as expected', Template.parse("{{words | sort | join}}").render(@context))
assert_equal '3', Template.parse("{{value | sort}}").render(@context) assert_equal('3', Template.parse("{{value | sort}}").render(@context))
assert_equal 'are flower', Template.parse("{{arrays | sort | join}}").render(@context) assert_equal('are flower', Template.parse("{{arrays | sort | join}}").render(@context))
assert_equal 'Expected case sensitive', Template.parse("{{case_sensitive | sort | join}}").render(@context) assert_equal('Expected case sensitive', Template.parse("{{case_sensitive | sort | join}}").render(@context))
end end
def test_sort_natural def test_sort_natural
@context['words'] = ['case', 'Assert', 'Insensitive'] @context['words'] = ['case', 'Assert', 'Insensitive']
@context['hashes'] = [{ 'a' => 'A' }, { 'a' => 'b' }, { 'a' => 'C' }] @context['hashes'] = [{ 'a' => 'A' }, { 'a' => 'b' }, { 'a' => 'C' }]
@context['objects'] = [TestObject.new('A'), TestObject.new('b'), TestObject.new('C')] @context['objects'] = [TestObject.new('A'), TestObject.new('b'), TestObject.new('C')]
# Test strings # Test strings
assert_equal 'Assert case Insensitive', Template.parse("{{words | sort_natural | join}}").render(@context) assert_equal('Assert case Insensitive', Template.parse("{{words | sort_natural | join}}").render(@context))
# Test hashes # Test hashes
assert_equal 'A b C', Template.parse("{{hashes | sort_natural: 'a' | map: 'a' | join}}").render(@context) assert_equal('A b C', Template.parse("{{hashes | sort_natural: 'a' | map: 'a' | join}}").render(@context))
# Test objects # Test objects
assert_equal 'A b C', Template.parse("{{objects | sort_natural: 'a' | map: 'a' | join}}").render(@context) assert_equal('A b C', Template.parse("{{objects | sort_natural: 'a' | map: 'a' | join}}").render(@context))
end end
def test_compact def test_compact
@context['words'] = ['a', nil, 'b', nil, 'c'] @context['words'] = ['a', nil, 'b', nil, 'c']
@context['hashes'] = [{ 'a' => 'A' }, { 'a' => nil }, { 'a' => 'C' }] @context['hashes'] = [{ 'a' => 'A' }, { 'a' => nil }, { 'a' => 'C' }]
@context['objects'] = [TestObject.new('A'), TestObject.new(nil), TestObject.new('C')] @context['objects'] = [TestObject.new('A'), TestObject.new(nil), TestObject.new('C')]
# Test strings # Test strings
assert_equal 'a b c', Template.parse("{{words | compact | join}}").render(@context) assert_equal('a b c', Template.parse("{{words | compact | join}}").render(@context))
# Test hashes # Test hashes
assert_equal 'A C', Template.parse("{{hashes | compact: 'a' | map: 'a' | join}}").render(@context) assert_equal('A C', Template.parse("{{hashes | compact: 'a' | map: 'a' | join}}").render(@context))
# Test objects # Test objects
assert_equal 'A C', Template.parse("{{objects | compact: 'a' | map: 'a' | join}}").render(@context) assert_equal('A C', Template.parse("{{objects | compact: 'a' | map: 'a' | join}}").render(@context))
end end
def test_strip_html def test_strip_html
@context['var'] = "<b>bla blub</a>" @context['var'] = "<b>bla blub</a>"
assert_equal "bla blub", Template.parse("{{ var | strip_html }}").render(@context) assert_equal("bla blub", Template.parse("{{ var | strip_html }}").render(@context))
end end
def test_strip_html_ignore_comments_with_html def test_strip_html_ignore_comments_with_html
@context['var'] = "<!-- split and some <ul> tag --><b>bla blub</a>" @context['var'] = "<!-- split and some <ul> tag --><b>bla blub</a>"
assert_equal "bla blub", Template.parse("{{ var | strip_html }}").render(@context) assert_equal("bla blub", Template.parse("{{ var | strip_html }}").render(@context))
end end
def test_capitalize def test_capitalize
@context['var'] = "blub" @context['var'] = "blub"
assert_equal "Blub", Template.parse("{{ var | capitalize }}").render(@context) assert_equal("Blub", Template.parse("{{ var | capitalize }}").render(@context))
end end
def test_nonexistent_filter_is_ignored def test_nonexistent_filter_is_ignored
@context['var'] = 1000 @context['var'] = 1000
assert_equal '1000', Template.parse("{{ var | xyzzy }}").render(@context) assert_equal('1000', Template.parse("{{ var | xyzzy }}").render(@context))
end end
def test_filter_with_keyword_arguments def test_filter_with_keyword_arguments
@context['surname'] = 'john' @context['surname'] = 'john'
@context['input'] = 'hello %{first_name}, %{last_name}' @context['input'] = 'hello %{first_name}, %{last_name}'
@context.add_filters(SubstituteFilter) @context.add_filters(SubstituteFilter)
output = Template.parse(%({{ input | substitute: first_name: surname, last_name: 'doe' }})).render(@context) output = Template.parse(%({{ input | substitute: first_name: surname, last_name: 'doe' }})).render(@context)
assert_equal 'hello john, doe', output assert_equal('hello john, doe', output)
end end
def test_override_object_method_in_filter def test_override_object_method_in_filter
assert_equal "tap overridden", Template.parse("{{var | tap}}").render!({ 'var' => 1000 }, filters: [OverrideObjectMethodFilter]) assert_equal("tap overridden", Template.parse("{{var | tap}}").render!({ 'var' => 1000 }, filters: [OverrideObjectMethodFilter]))
# tap still treated as a non-existent filter # tap still treated as a non-existent filter
assert_equal "1000", Template.parse("{{var | tap}}").render!('var' => 1000) assert_equal("1000", Template.parse("{{var | tap}}").render!('var' => 1000))
end
def test_liquid_argument_error
source = "{{ '' | size: 'too many args' }}"
exc = assert_raises(Liquid::ArgumentError) do
Template.parse(source).render!
end
assert_match(/\ALiquid error: wrong number of arguments /, exc.message)
assert_equal(exc.message, Template.parse(source).render)
end end
end end
@@ -160,15 +169,15 @@ class FiltersInTemplate < Minitest::Test
def test_local_global def test_local_global
with_global_filter(MoneyFilter) do with_global_filter(MoneyFilter) do
assert_equal " 1000$ ", Template.parse("{{1000 | money}}").render!(nil, nil) 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$ CAD ", Template.parse("{{1000 | money}}").render!(nil, filters: [CanadianMoneyFilter]) assert_equal(" 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, filters: [CanadianMoneyFilter]))
end end
end end
def test_local_filter_with_deprecated_syntax def test_local_filter_with_deprecated_syntax
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, CanadianMoneyFilter) assert_equal(" 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, CanadianMoneyFilter))
assert_equal " 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, [CanadianMoneyFilter]) assert_equal(" 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, [CanadianMoneyFilter]))
end end
end # FiltersTest end # FiltersTest

View File

@@ -19,7 +19,7 @@ class HashOrderingTest < Minitest::Test
def test_global_register_order def test_global_register_order
with_global_filter(MoneyFilter, CanadianMoneyFilter) do 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 end
end end

View File

@@ -42,84 +42,84 @@ class OutputTest < Minitest::Test
text = %( {{best_cars}} ) text = %( {{best_cars}} )
expected = %( bmw ) expected = %( bmw )
assert_equal expected, Template.parse(text).render!(@assigns) assert_equal(expected, Template.parse(text).render!(@assigns))
end end
def test_variable_traversing_with_two_brackets def test_variable_traversing_with_two_brackets
text = %({{ site.data.menu[include.menu][include.locale] }}) text = %({{ site.data.menu[include.menu][include.locale] }})
assert_equal "it works!", Template.parse(text).render!( assert_equal("it works!", Template.parse(text).render!(
"site" => { "data" => { "menu" => { "foo" => { "bar" => "it works!" } } } }, "site" => { "data" => { "menu" => { "foo" => { "bar" => "it works!" } } } },
"include" => { "menu" => "foo", "locale" => "bar" } "include" => { "menu" => "foo", "locale" => "bar" }
) ))
end end
def test_variable_traversing def test_variable_traversing
text = %( {{car.bmw}} {{car.gm}} {{car.bmw}} ) text = %( {{car.bmw}} {{car.gm}} {{car.bmw}} )
expected = %( good bad good ) expected = %( good bad good )
assert_equal expected, Template.parse(text).render!(@assigns) assert_equal(expected, Template.parse(text).render!(@assigns))
end end
def test_variable_piping def test_variable_piping
text = %( {{ car.gm | make_funny }} ) text = %( {{ car.gm | make_funny }} )
expected = %( LOL ) expected = %( LOL )
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]) assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
end end
def test_variable_piping_with_input def test_variable_piping_with_input
text = %( {{ car.gm | cite_funny }} ) text = %( {{ car.gm | cite_funny }} )
expected = %( LOL: bad ) expected = %( LOL: bad )
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]) assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
end end
def test_variable_piping_with_args def test_variable_piping_with_args
text = %! {{ car.gm | add_smiley : ':-(' }} ! text = %! {{ car.gm | add_smiley : ':-(' }} !
expected = %| bad :-( | expected = %| bad :-( |
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]) assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
end end
def test_variable_piping_with_no_args def test_variable_piping_with_no_args
text = %( {{ car.gm | add_smiley }} ) text = %( {{ car.gm | add_smiley }} )
expected = %| bad :-) | expected = %| bad :-) |
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]) assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
end end
def test_multiple_variable_piping_with_args def test_multiple_variable_piping_with_args
text = %! {{ car.gm | add_smiley : ':-(' | add_smiley : ':-('}} ! text = %! {{ car.gm | add_smiley : ':-(' | add_smiley : ':-('}} !
expected = %| bad :-( :-( | expected = %| bad :-( :-( |
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]) assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
end end
def test_variable_piping_with_multiple_args def test_variable_piping_with_multiple_args
text = %( {{ car.gm | add_tag : 'span', 'bar'}} ) text = %( {{ car.gm | add_tag : 'span', 'bar'}} )
expected = %( <span id="bar">bad</span> ) expected = %( <span id="bar">bad</span> )
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]) assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
end end
def test_variable_piping_with_variable_args def test_variable_piping_with_variable_args
text = %( {{ car.gm | add_tag : 'span', car.bmw}} ) text = %( {{ car.gm | add_tag : 'span', car.bmw}} )
expected = %( <span id="good">bad</span> ) expected = %( <span id="good">bad</span> )
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]) assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
end end
def test_multiple_pipings def test_multiple_pipings
text = %( {{ best_cars | cite_funny | paragraph }} ) text = %( {{ best_cars | cite_funny | paragraph }} )
expected = %( <p>LOL: bmw</p> ) expected = %( <p>LOL: bmw</p> )
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]) assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
end end
def test_link_to def test_link_to
text = %( {{ 'Typo' | link_to: 'http://typo.leetsoft.com' }} ) text = %( {{ 'Typo' | link_to: 'http://typo.leetsoft.com' }} )
expected = %( <a href="http://typo.leetsoft.com">Typo</a> ) expected = %( <a href="http://typo.leetsoft.com">Typo</a> )
assert_equal expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]) assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter]))
end end
end # OutputTest end # OutputTest

View File

@@ -7,7 +7,7 @@ class ParsingQuirksTest < Minitest::Test
def test_parsing_css def test_parsing_css
text = " div { font-weight: bold; } " text = " div { font-weight: bold; } "
assert_equal text, Template.parse(text).render! assert_equal(text, Template.parse(text).render!)
end end
def test_raise_on_single_close_bracet def test_raise_on_single_close_bracet
@@ -29,10 +29,10 @@ class ParsingQuirksTest < Minitest::Test
end end
def test_error_on_empty_filter def test_error_on_empty_filter
assert Template.parse("{{test}}") assert(Template.parse("{{test}}"))
with_error_mode(:lax) do with_error_mode(:lax) do
assert Template.parse("{{|test}}") assert(Template.parse("{{|test}}"))
end end
with_error_mode(:strict) do with_error_mode(:strict) do
@@ -64,15 +64,15 @@ class ParsingQuirksTest < Minitest::Test
end end
def test_no_error_on_lax_empty_filter def test_no_error_on_lax_empty_filter
assert Template.parse("{{test |a|b|}}", error_mode: :lax) assert(Template.parse("{{test |a|b|}}", error_mode: :lax))
assert Template.parse("{{test}}", error_mode: :lax) assert(Template.parse("{{test}}", error_mode: :lax))
assert Template.parse("{{|test|}}", error_mode: :lax) assert(Template.parse("{{|test|}}", error_mode: :lax))
end end
def test_meaningless_parens_lax def test_meaningless_parens_lax
with_error_mode(:lax) do with_error_mode(:lax) do
assigns = { 'b' => 'bar', 'c' => 'baz' } assigns = { 'b' => 'bar', 'c' => 'baz' }
markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false" markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false"
assert_template_result(' YES ', "{% if #{markup} %} YES {% endif %}", assigns) assert_template_result(' YES ', "{% if #{markup} %} YES {% endif %}", assigns)
end end
end end
@@ -118,6 +118,16 @@ class ParsingQuirksTest < Minitest::Test
end end
end end
def test_blank_variable_markup
assert_template_result('', "{{}}")
end
def test_lookup_on_var_with_literal_name
assigns = { "blank" => { "x" => "result" } }
assert_template_result('result', "{{ blank.x }}", assigns)
assert_template_result('result', "{{ blank['x'] }}", assigns)
end
def test_contains_in_id def test_contains_in_id
assert_template_result(' YES ', '{% if containsallshipments == true %} YES {% endif %}', 'containsallshipments' => true) assert_template_result(' YES ', '{% if containsallshipments == true %} YES {% endif %}', 'containsallshipments' => true)
end end

View File

@@ -2,7 +2,7 @@
require 'test_helper' require 'test_helper'
class RenderProfilingTest < Minitest::Test class ProfilerTest < Minitest::Test
include Liquid include Liquid
class ProfilingFileSystem class ProfilingFileSystem
@@ -19,35 +19,35 @@ class RenderProfilingTest < Minitest::Test
t = Template.parse("{{ 'a string' | upcase }}") t = Template.parse("{{ 'a string' | upcase }}")
t.render! t.render!
assert_nil t.profiler assert_nil(t.profiler)
end end
def test_parse_makes_available_simple_profiling def test_parse_makes_available_simple_profiling
t = Template.parse("{{ 'a string' | upcase }}", profile: true) t = Template.parse("{{ 'a string' | upcase }}", profile: true)
t.render! t.render!
assert_equal 1, t.profiler.length assert_equal(1, t.profiler.length)
node = t.profiler[0] node = t.profiler[0]
assert_equal " 'a string' | upcase ", node.code assert_equal(" 'a string' | upcase ", node.code)
end end
def test_render_ignores_raw_strings_when_profiling def test_render_ignores_raw_strings_when_profiling
t = Template.parse("This is raw string\nstuff\nNewline", profile: true) t = Template.parse("This is raw string\nstuff\nNewline", profile: true)
t.render! t.render!
assert_equal 0, t.profiler.length assert_equal(0, t.profiler.length)
end end
def test_profiling_includes_line_numbers_of_liquid_nodes def test_profiling_includes_line_numbers_of_liquid_nodes
t = Template.parse("{{ 'a string' | upcase }}\n{% increment test %}", profile: true) t = Template.parse("{{ 'a string' | upcase }}\n{% increment test %}", profile: true)
t.render! t.render!
assert_equal 2, t.profiler.length assert_equal(2, t.profiler.length)
# {{ 'a string' | upcase }} # {{ 'a string' | upcase }}
assert_equal 1, t.profiler[0].line_number assert_equal(1, t.profiler[0].line_number)
# {{ increment test }} # {{ increment test }}
assert_equal 2, t.profiler[1].line_number assert_equal(2, t.profiler[1].line_number)
end end
def test_profiling_includes_line_numbers_of_included_partials def test_profiling_includes_line_numbers_of_included_partials
@@ -57,9 +57,20 @@ class RenderProfilingTest < Minitest::Test
included_children = t.profiler[0].children included_children = t.profiler[0].children
# {% assign template_name = 'a_template' %} # {% assign template_name = 'a_template' %}
assert_equal 1, included_children[0].line_number assert_equal(1, included_children[0].line_number)
# {{ template_name }} # {{ template_name }}
assert_equal 2, included_children[1].line_number 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 end
def test_profiling_times_the_rendering_of_tokens def test_profiling_times_the_rendering_of_tokens
@@ -67,14 +78,45 @@ class RenderProfilingTest < Minitest::Test
t.render! t.render!
node = t.profiler[0] node = t.profiler[0]
refute_nil node.render_time refute_nil(node.render_time)
end end
def test_profiling_times_the_entire_render def test_profiling_times_the_entire_render
t = Template.parse("{% include 'a_template' %}", profile: true) t = Template.parse("{% include 'a_template' %}", profile: true)
t.render! t.render!
assert t.profiler.total_render_time >= 0, "Total render time was not calculated" assert(t.profiler.total_render_time >= 0, "Total render time was not calculated")
end
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 end
def test_profiling_uses_include_to_mark_children def test_profiling_uses_include_to_mark_children
@@ -82,7 +124,7 @@ class RenderProfilingTest < Minitest::Test
t.render! t.render!
include_node = t.profiler[1] include_node = t.profiler[1]
assert_equal 2, include_node.children.length assert_equal(2, include_node.children.length)
end end
def test_profiling_marks_children_with_the_name_of_included_partial def test_profiling_marks_children_with_the_name_of_included_partial
@@ -91,7 +133,7 @@ class RenderProfilingTest < Minitest::Test
include_node = t.profiler[1] include_node = t.profiler[1]
include_node.children.each do |child| include_node.children.each do |child|
assert_equal "a_template", child.partial assert_equal("a_template", child.partial)
end end
end end
@@ -101,12 +143,12 @@ class RenderProfilingTest < Minitest::Test
a_template = t.profiler[1] a_template = t.profiler[1]
a_template.children.each do |child| a_template.children.each do |child|
assert_equal "a_template", child.partial assert_equal("a_template", child.partial)
end end
b_template = t.profiler[2] b_template = t.profiler[2]
b_template.children.each do |child| b_template.children.each do |child|
assert_equal "b_template", child.partial assert_equal("b_template", child.partial)
end end
end end
@@ -116,12 +158,12 @@ class RenderProfilingTest < Minitest::Test
a_template1 = t.profiler[1] a_template1 = t.profiler[1]
a_template1.children.each do |child| a_template1.children.each do |child|
assert_equal "a_template", child.partial assert_equal("a_template", child.partial)
end end
a_template2 = t.profiler[2] a_template2 = t.profiler[2]
a_template2.children.each do |child| a_template2.children.each do |child|
assert_equal "a_template", child.partial assert_equal("a_template", child.partial)
end end
end end
@@ -134,23 +176,38 @@ class RenderProfilingTest < Minitest::Test
timing_count += 1 timing_count += 1
end end
assert_equal 2, timing_count assert_equal(2, timing_count)
end end
def test_profiling_marks_children_of_if_blocks def test_profiling_marks_children_of_if_blocks
t = Template.parse("{% if true %} {% increment test %} {{ test }} {% endif %}", profile: true) t = Template.parse("{% if true %} {% increment test %} {{ test }} {% endif %}", profile: true)
t.render! t.render!
assert_equal 1, t.profiler.length assert_equal(1, t.profiler.length)
assert_equal 2, t.profiler[0].children.length assert_equal(2, t.profiler[0].children.length)
end end
def test_profiling_marks_children_of_for_blocks def test_profiling_marks_children_of_for_blocks
t = Template.parse("{% for item in collection %} {{ item }} {% endfor %}", profile: true) t = Template.parse("{% for item in collection %} {{ item }} {% endfor %}", profile: true)
t.render!("collection" => ["one", "two"]) t.render!("collection" => ["one", "two"])
assert_equal 1, t.profiler.length assert_equal(1, t.profiler.length)
# Will profile each invocation of the for block # Will profile each invocation of the for block
assert_equal 2, t.profiler[0].children.length assert_equal(2, t.profiler[0].children.length)
end
def test_profiling_supports_self_time
t = Template.parse("{% for item in collection %} {{ item }} {% endfor %}", profile: true)
t.render!("collection" => ["one", "two"])
leaf = t.profiler[0].children[0]
assert_operator(leaf.self_time, :>, 0)
end
def test_profiling_supports_total_time
t = Template.parse("{% if true %} {% increment test %} {{ test }} {% endif %}", profile: true)
t.render!
assert_operator(t.profiler[0].total_time, :>, 0)
end end
end end

View File

@@ -16,65 +16,72 @@ class SecurityTest < Minitest::Test
end end
def test_no_instance_eval def test_no_instance_eval
text = %( {{ '1+1' | instance_eval }} ) text = %( {{ '1+1' | instance_eval }} )
expected = %( 1+1 ) expected = %( 1+1 )
assert_equal expected, Template.parse(text).render!(@assigns) assert_equal(expected, Template.parse(text).render!(@assigns))
end end
def test_no_existing_instance_eval def test_no_existing_instance_eval
text = %( {{ '1+1' | __instance_eval__ }} ) text = %( {{ '1+1' | __instance_eval__ }} )
expected = %( 1+1 ) expected = %( 1+1 )
assert_equal expected, Template.parse(text).render!(@assigns) assert_equal(expected, Template.parse(text).render!(@assigns))
end end
def test_no_instance_eval_after_mixing_in_new_filter def test_no_instance_eval_after_mixing_in_new_filter
text = %( {{ '1+1' | instance_eval }} ) text = %( {{ '1+1' | instance_eval }} )
expected = %( 1+1 ) expected = %( 1+1 )
assert_equal expected, Template.parse(text).render!(@assigns) assert_equal(expected, Template.parse(text).render!(@assigns))
end end
def test_no_instance_eval_later_in_chain def test_no_instance_eval_later_in_chain
text = %( {{ '1+1' | add_one | instance_eval }} ) text = %( {{ '1+1' | add_one | instance_eval }} )
expected = %( 1+1 + 1 ) expected = %( 1+1 + 1 )
assert_equal expected, Template.parse(text).render!(@assigns, filters: SecurityFilter) assert_equal(expected, Template.parse(text).render!(@assigns, filters: SecurityFilter))
end end
def test_does_not_add_filters_to_symbol_table def test_does_not_permanently_add_filters_to_symbol_table
current_symbols = Symbol.all_symbols current_symbols = Symbol.all_symbols
test = %( {{ "some_string" | a_bad_filter }} ) # MRI imprecisely marks objects found on the C stack, which can result
# in uninitialized memory being marked. This can even result in the test failing
# deterministically for a given compilation of ruby. Using a separate thread will
# keep these writes of the symbol pointer on a separate stack that will be garbage
# collected after Thread#join.
Thread.new do
test = %( {{ "some_string" | a_bad_filter }} )
Template.parse(test).render!
nil
end.join
template = Template.parse(test) GC.start
assert_equal [], (Symbol.all_symbols - current_symbols)
template.render! assert_equal([], (Symbol.all_symbols - current_symbols))
assert_equal [], (Symbol.all_symbols - current_symbols)
end end
def test_does_not_add_drop_methods_to_symbol_table def test_does_not_add_drop_methods_to_symbol_table
current_symbols = Symbol.all_symbols current_symbols = Symbol.all_symbols
assigns = { 'drop' => Drop.new } assigns = { 'drop' => Drop.new }
assert_equal "", Template.parse("{{ drop.custom_method_1 }}", assigns).render! assert_equal("", Template.parse("{{ drop.custom_method_1 }}", assigns).render!)
assert_equal "", Template.parse("{{ drop.custom_method_2 }}", assigns).render! assert_equal("", Template.parse("{{ drop.custom_method_2 }}", assigns).render!)
assert_equal "", Template.parse("{{ drop.custom_method_3 }}", assigns).render! assert_equal("", Template.parse("{{ drop.custom_method_3 }}", assigns).render!)
assert_equal [], (Symbol.all_symbols - current_symbols) assert_equal([], (Symbol.all_symbols - current_symbols))
end end
def test_max_depth_nested_blocks_does_not_raise_exception def test_max_depth_nested_blocks_does_not_raise_exception
depth = Liquid::Block::MAX_DEPTH depth = Liquid::Block::MAX_DEPTH
code = "{% if true %}" * depth + "rendered" + "{% endif %}" * depth code = "{% if true %}" * depth + "rendered" + "{% endif %}" * depth
assert_equal "rendered", Template.parse(code).render! assert_equal("rendered", Template.parse(code).render!)
end end
def test_more_than_max_depth_nested_blocks_raises_exception def test_more_than_max_depth_nested_blocks_raises_exception
depth = Liquid::Block::MAX_DEPTH + 1 depth = Liquid::Block::MAX_DEPTH + 1
code = "{% if true %}" * depth + "rendered" + "{% endif %}" * depth code = "{% if true %}" * depth + "rendered" + "{% endif %}" * depth
assert_raises(Liquid::StackLevelError) do assert_raises(Liquid::StackLevelError) do
Template.parse(code).render! Template.parse(code).render!
end end

View File

@@ -60,34 +60,34 @@ class StandardFiltersTest < Minitest::Test
end end
def test_size def test_size
assert_equal 3, @filters.size([1, 2, 3]) assert_equal(3, @filters.size([1, 2, 3]))
assert_equal 0, @filters.size([]) assert_equal(0, @filters.size([]))
assert_equal 0, @filters.size(nil) assert_equal(0, @filters.size(nil))
end end
def test_downcase def test_downcase
assert_equal 'testing', @filters.downcase("Testing") assert_equal('testing', @filters.downcase("Testing"))
assert_equal '', @filters.downcase(nil) assert_equal('', @filters.downcase(nil))
end end
def test_upcase def test_upcase
assert_equal 'TESTING', @filters.upcase("Testing") assert_equal('TESTING', @filters.upcase("Testing"))
assert_equal '', @filters.upcase(nil) assert_equal('', @filters.upcase(nil))
end end
def test_slice def test_slice
assert_equal 'oob', @filters.slice('foobar', 1, 3) assert_equal('oob', @filters.slice('foobar', 1, 3))
assert_equal 'oobar', @filters.slice('foobar', 1, 1000) assert_equal('oobar', @filters.slice('foobar', 1, 1000))
assert_equal '', @filters.slice('foobar', 1, 0) assert_equal('', @filters.slice('foobar', 1, 0))
assert_equal 'o', @filters.slice('foobar', 1, 1) assert_equal('o', @filters.slice('foobar', 1, 1))
assert_equal 'bar', @filters.slice('foobar', 3, 3) assert_equal('bar', @filters.slice('foobar', 3, 3))
assert_equal 'ar', @filters.slice('foobar', -2, 2) assert_equal('ar', @filters.slice('foobar', -2, 2))
assert_equal 'ar', @filters.slice('foobar', -2, 1000) assert_equal('ar', @filters.slice('foobar', -2, 1000))
assert_equal 'r', @filters.slice('foobar', -1) assert_equal('r', @filters.slice('foobar', -1))
assert_equal '', @filters.slice(nil, 0) assert_equal('', @filters.slice(nil, 0))
assert_equal '', @filters.slice('foobar', 100, 10) assert_equal('', @filters.slice('foobar', 100, 10))
assert_equal '', @filters.slice('foobar', -100, 10) assert_equal('', @filters.slice('foobar', -100, 10))
assert_equal 'oob', @filters.slice('foobar', '1', '3') assert_equal('oob', @filters.slice('foobar', '1', '3'))
assert_raises(Liquid::ArgumentError) do assert_raises(Liquid::ArgumentError) do
@filters.slice('foobar', nil) @filters.slice('foobar', nil)
end end
@@ -98,113 +98,119 @@ class StandardFiltersTest < Minitest::Test
def test_slice_on_arrays def test_slice_on_arrays
input = 'foobar'.split(//) input = 'foobar'.split(//)
assert_equal %w(o o b), @filters.slice(input, 1, 3) assert_equal(%w(o o b), @filters.slice(input, 1, 3))
assert_equal %w(o o b a r), @filters.slice(input, 1, 1000) assert_equal(%w(o o b a r), @filters.slice(input, 1, 1000))
assert_equal %w(), @filters.slice(input, 1, 0) assert_equal(%w(), @filters.slice(input, 1, 0))
assert_equal %w(o), @filters.slice(input, 1, 1) assert_equal(%w(o), @filters.slice(input, 1, 1))
assert_equal %w(b a r), @filters.slice(input, 3, 3) assert_equal(%w(b a r), @filters.slice(input, 3, 3))
assert_equal %w(a r), @filters.slice(input, -2, 2) assert_equal(%w(a r), @filters.slice(input, -2, 2))
assert_equal %w(a r), @filters.slice(input, -2, 1000) assert_equal(%w(a r), @filters.slice(input, -2, 1000))
assert_equal %w(r), @filters.slice(input, -1) assert_equal(%w(r), @filters.slice(input, -1))
assert_equal %w(), @filters.slice(input, 100, 10) assert_equal(%w(), @filters.slice(input, 100, 10))
assert_equal %w(), @filters.slice(input, -100, 10) assert_equal(%w(), @filters.slice(input, -100, 10))
end end
def test_truncate def test_truncate
assert_equal '1234...', @filters.truncate('1234567890', 7) assert_equal('1234...', @filters.truncate('1234567890', 7))
assert_equal '1234567890', @filters.truncate('1234567890', 20) assert_equal('1234567890', @filters.truncate('1234567890', 20))
assert_equal '...', @filters.truncate('1234567890', 0) assert_equal('...', @filters.truncate('1234567890', 0))
assert_equal '1234567890', @filters.truncate('1234567890') assert_equal('1234567890', @filters.truncate('1234567890'))
assert_equal "测试...", @filters.truncate("测试测试测试测试", 5) assert_equal("测试...", @filters.truncate("测试测试测试测试", 5))
assert_equal '12341', @filters.truncate("1234567890", 5, 1) assert_equal('12341', @filters.truncate("1234567890", 5, 1))
end end
def test_split def test_split
assert_equal ['12', '34'], @filters.split('12~34', '~') assert_equal(['12', '34'], @filters.split('12~34', '~'))
assert_equal ['A? ', ' ,Z'], @filters.split('A? ~ ~ ~ ,Z', '~ ~ ~') assert_equal(['A? ', ' ,Z'], @filters.split('A? ~ ~ ~ ,Z', '~ ~ ~'))
assert_equal ['A?Z'], @filters.split('A?Z', '~') assert_equal(['A?Z'], @filters.split('A?Z', '~'))
assert_equal [], @filters.split(nil, ' ') assert_equal([], @filters.split(nil, ' '))
assert_equal ['A', 'Z'], @filters.split('A1Z', 1) assert_equal(['A', 'Z'], @filters.split('A1Z', 1))
end end
def test_escape def test_escape
assert_equal '&lt;strong&gt;', @filters.escape('<strong>') assert_equal('&lt;strong&gt;', @filters.escape('<strong>'))
assert_equal '1', @filters.escape(1) assert_equal('1', @filters.escape(1))
assert_equal '2001-02-03', @filters.escape(Date.new(2001, 2, 3)) assert_equal('2001-02-03', @filters.escape(Date.new(2001, 2, 3)))
assert_nil @filters.escape(nil) assert_nil(@filters.escape(nil))
end end
def test_h def test_h
assert_equal '&lt;strong&gt;', @filters.h('<strong>') assert_equal('&lt;strong&gt;', @filters.h('<strong>'))
assert_equal '1', @filters.h(1) assert_equal('1', @filters.h(1))
assert_equal '2001-02-03', @filters.h(Date.new(2001, 2, 3)) assert_equal('2001-02-03', @filters.h(Date.new(2001, 2, 3)))
assert_nil @filters.h(nil) assert_nil(@filters.h(nil))
end end
def test_escape_once def test_escape_once
assert_equal '&lt;strong&gt;Hulk&lt;/strong&gt;', @filters.escape_once('&lt;strong&gt;Hulk</strong>') assert_equal('&lt;strong&gt;Hulk&lt;/strong&gt;', @filters.escape_once('&lt;strong&gt;Hulk</strong>'))
end end
def test_url_encode def test_url_encode
assert_equal 'foo%2B1%40example.com', @filters.url_encode('foo+1@example.com') assert_equal('foo%2B1%40example.com', @filters.url_encode('foo+1@example.com'))
assert_equal '1', @filters.url_encode(1) assert_equal('1', @filters.url_encode(1))
assert_equal '2001-02-03', @filters.url_encode(Date.new(2001, 2, 3)) assert_equal('2001-02-03', @filters.url_encode(Date.new(2001, 2, 3)))
assert_nil @filters.url_encode(nil) assert_nil(@filters.url_encode(nil))
end end
def test_url_decode def test_url_decode
assert_equal 'foo bar', @filters.url_decode('foo+bar') assert_equal('foo bar', @filters.url_decode('foo+bar'))
assert_equal 'foo bar', @filters.url_decode('foo%20bar') assert_equal('foo bar', @filters.url_decode('foo%20bar'))
assert_equal 'foo+1@example.com', @filters.url_decode('foo%2B1%40example.com') assert_equal('foo+1@example.com', @filters.url_decode('foo%2B1%40example.com'))
assert_equal '1', @filters.url_decode(1) assert_equal('1', @filters.url_decode(1))
assert_equal '2001-02-03', @filters.url_decode(Date.new(2001, 2, 3)) assert_equal('2001-02-03', @filters.url_decode(Date.new(2001, 2, 3)))
assert_nil @filters.url_decode(nil) assert_nil(@filters.url_decode(nil))
exception = assert_raises Liquid::ArgumentError do exception = assert_raises(Liquid::ArgumentError) do
@filters.url_decode('%ff') @filters.url_decode('%ff')
end end
assert_equal 'Liquid error: invalid byte sequence in UTF-8', exception.message assert_equal('Liquid error: invalid byte sequence in UTF-8', exception.message)
end end
def test_truncatewords def test_truncatewords
assert_equal 'one two three', @filters.truncatewords('one two three', 4) assert_equal('one two three', @filters.truncatewords('one two three', 4))
assert_equal 'one two...', @filters.truncatewords('one two three', 2) assert_equal('one two...', @filters.truncatewords('one two three', 2))
assert_equal 'one two three', @filters.truncatewords('one two three') assert_equal('one two three', @filters.truncatewords('one two three'))
assert_equal 'Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221;...', @filters.truncatewords('Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221; x 16&#8221; x 10.5&#8221; high) with cover.', 15) assert_equal(
assert_equal "测试测试测试测试", @filters.truncatewords('测试测试测试测试', 5) 'Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221;...',
assert_equal 'one two1', @filters.truncatewords("one two three", 2, 1) @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))
end end
def test_strip_html def test_strip_html
assert_equal 'test', @filters.strip_html("<div>test</div>") assert_equal('test', @filters.strip_html("<div>test</div>"))
assert_equal 'test', @filters.strip_html("<div id='test'>test</div>") assert_equal('test', @filters.strip_html("<div id='test'>test</div>"))
assert_equal '', @filters.strip_html("<script type='text/javascript'>document.write('some stuff');</script>") assert_equal('', @filters.strip_html("<script type='text/javascript'>document.write('some stuff');</script>"))
assert_equal '', @filters.strip_html("<style type='text/css'>foo bar</style>") assert_equal('', @filters.strip_html("<style type='text/css'>foo bar</style>"))
assert_equal 'test', @filters.strip_html("<div\nclass='multiline'>test</div>") assert_equal('test', @filters.strip_html("<div\nclass='multiline'>test</div>"))
assert_equal 'test', @filters.strip_html("<!-- foo bar \n test -->test") assert_equal('test', @filters.strip_html("<!-- foo bar \n test -->test"))
assert_equal '', @filters.strip_html(nil) assert_equal('', @filters.strip_html(nil))
# Quirk of the existing implementation # Quirk of the existing implementation
assert_equal 'foo;', @filters.strip_html("<<<script </script>script>foo;</script>") assert_equal('foo;', @filters.strip_html("<<<script </script>script>foo;</script>"))
end end
def test_join def test_join
assert_equal '1 2 3 4', @filters.join([1, 2, 3, 4]) assert_equal('1 2 3 4', @filters.join([1, 2, 3, 4]))
assert_equal '1 - 2 - 3 - 4', @filters.join([1, 2, 3, 4], ' - ') assert_equal('1 - 2 - 3 - 4', @filters.join([1, 2, 3, 4], ' - '))
assert_equal '1121314', @filters.join([1, 2, 3, 4], 1) assert_equal('1121314', @filters.join([1, 2, 3, 4], 1))
end end
def test_sort def test_sort
assert_equal [1, 2, 3, 4], @filters.sort([4, 3, 2, 1]) assert_equal([1, 2, 3, 4], @filters.sort([4, 3, 2, 1]))
assert_equal [{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }], @filters.sort([{ "a" => 4 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a") assert_equal([{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }], @filters.sort([{ "a" => 4 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a"))
end end
def test_sort_with_nils def test_sort_with_nils
assert_equal [1, 2, 3, 4, nil], @filters.sort([nil, 4, 3, 2, 1]) assert_equal([1, 2, 3, 4, nil], @filters.sort([nil, 4, 3, 2, 1]))
assert_equal [{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }, {}], @filters.sort([{ "a" => 4 }, { "a" => 3 }, {}, { "a" => 1 }, { "a" => 2 }], "a") assert_equal([{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }, {}], @filters.sort([{ "a" => 4 }, { "a" => 3 }, {}, { "a" => 1 }, { "a" => 2 }], "a"))
end end
def test_sort_when_property_is_sometimes_missing_puts_nils_last def test_sort_when_property_is_sometimes_missing_puts_nils_last
input = [ input = [
{ "price" => 4, "handle" => "alpha" }, { "price" => 4, "handle" => "alpha" },
{ "handle" => "beta" }, { "handle" => "beta" },
{ "price" => 1, "handle" => "gamma" }, { "price" => 1, "handle" => "gamma" },
@@ -218,21 +224,21 @@ class StandardFiltersTest < Minitest::Test
{ "handle" => "delta" }, { "handle" => "delta" },
{ "handle" => "beta" }, { "handle" => "beta" },
] ]
assert_equal expectation, @filters.sort(input, "price") assert_equal(expectation, @filters.sort(input, "price"))
end end
def test_sort_natural def test_sort_natural
assert_equal ["a", "B", "c", "D"], @filters.sort_natural(["c", "D", "a", "B"]) assert_equal(["a", "B", "c", "D"], @filters.sort_natural(["c", "D", "a", "B"]))
assert_equal [{ "a" => "a" }, { "a" => "B" }, { "a" => "c" }, { "a" => "D" }], @filters.sort_natural([{ "a" => "D" }, { "a" => "c" }, { "a" => "a" }, { "a" => "B" }], "a") assert_equal([{ "a" => "a" }, { "a" => "B" }, { "a" => "c" }, { "a" => "D" }], @filters.sort_natural([{ "a" => "D" }, { "a" => "c" }, { "a" => "a" }, { "a" => "B" }], "a"))
end end
def test_sort_natural_with_nils def test_sort_natural_with_nils
assert_equal ["a", "B", "c", "D", nil], @filters.sort_natural([nil, "c", "D", "a", "B"]) assert_equal(["a", "B", "c", "D", nil], @filters.sort_natural([nil, "c", "D", "a", "B"]))
assert_equal [{ "a" => "a" }, { "a" => "B" }, { "a" => "c" }, { "a" => "D" }, {}], @filters.sort_natural([{ "a" => "D" }, { "a" => "c" }, {}, { "a" => "a" }, { "a" => "B" }], "a") assert_equal([{ "a" => "a" }, { "a" => "B" }, { "a" => "c" }, { "a" => "D" }, {}], @filters.sort_natural([{ "a" => "D" }, { "a" => "c" }, {}, { "a" => "a" }, { "a" => "B" }], "a"))
end end
def test_sort_natural_when_property_is_sometimes_missing_puts_nils_last def test_sort_natural_when_property_is_sometimes_missing_puts_nils_last
input = [ input = [
{ "price" => "4", "handle" => "alpha" }, { "price" => "4", "handle" => "alpha" },
{ "handle" => "beta" }, { "handle" => "beta" },
{ "price" => "1", "handle" => "gamma" }, { "price" => "1", "handle" => "gamma" },
@@ -246,7 +252,7 @@ class StandardFiltersTest < Minitest::Test
{ "handle" => "delta" }, { "handle" => "delta" },
{ "handle" => "beta" }, { "handle" => "beta" },
] ]
assert_equal expectation, @filters.sort_natural(input, "price") assert_equal(expectation, @filters.sort_natural(input, "price"))
end end
def test_sort_natural_case_check def test_sort_natural_case_check
@@ -268,12 +274,12 @@ class StandardFiltersTest < Minitest::Test
{ "key" => "Z" }, { "key" => "Z" },
{ "fake" => "t" }, { "fake" => "t" },
] ]
assert_equal expectation, @filters.sort_natural(input, "key") assert_equal(expectation, @filters.sort_natural(input, "key"))
assert_equal ["a", "b", "c", "X", "Y", "Z"], @filters.sort_natural(["X", "Y", "Z", "a", "b", "c"]) assert_equal(["a", "b", "c", "X", "Y", "Z"], @filters.sort_natural(["X", "Y", "Z", "a", "b", "c"]))
end end
def test_sort_empty_array def test_sort_empty_array
assert_equal [], @filters.sort([], "a") assert_equal([], @filters.sort([], "a"))
end end
def test_sort_invalid_property def test_sort_invalid_property
@@ -283,13 +289,13 @@ class StandardFiltersTest < Minitest::Test
[3], [3],
] ]
assert_raises Liquid::ArgumentError do assert_raises(Liquid::ArgumentError) do
@filters.sort(foo, "bar") @filters.sort(foo, "bar")
end end
end end
def test_sort_natural_empty_array def test_sort_natural_empty_array
assert_equal [], @filters.sort_natural([], "a") assert_equal([], @filters.sort_natural([], "a"))
end end
def test_sort_natural_invalid_property def test_sort_natural_invalid_property
@@ -299,32 +305,32 @@ class StandardFiltersTest < Minitest::Test
[3], [3],
] ]
assert_raises Liquid::ArgumentError do assert_raises(Liquid::ArgumentError) do
@filters.sort_natural(foo, "bar") @filters.sort_natural(foo, "bar")
end end
end end
def test_legacy_sort_hash def test_legacy_sort_hash
assert_equal [{ a: 1, b: 2 }], @filters.sort(a: 1, b: 2) assert_equal([{ a: 1, b: 2 }], @filters.sort(a: 1, b: 2))
end end
def test_numerical_vs_lexicographical_sort def test_numerical_vs_lexicographical_sort
assert_equal [2, 10], @filters.sort([10, 2]) assert_equal([2, 10], @filters.sort([10, 2]))
assert_equal [{ "a" => 2 }, { "a" => 10 }], @filters.sort([{ "a" => 10 }, { "a" => 2 }], "a") assert_equal([{ "a" => 2 }, { "a" => 10 }], @filters.sort([{ "a" => 10 }, { "a" => 2 }], "a"))
assert_equal ["10", "2"], @filters.sort(["10", "2"]) assert_equal(["10", "2"], @filters.sort(["10", "2"]))
assert_equal [{ "a" => "10" }, { "a" => "2" }], @filters.sort([{ "a" => "10" }, { "a" => "2" }], "a") assert_equal([{ "a" => "10" }, { "a" => "2" }], @filters.sort([{ "a" => "10" }, { "a" => "2" }], "a"))
end end
def test_uniq def test_uniq
assert_equal ["foo"], @filters.uniq("foo") 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([1, 3, 2, 4], @filters.uniq([1, 1, 3, 2, 3, 1, 4, 3, 2, 1]))
assert_equal [{ "a" => 1 }, { "a" => 3 }, { "a" => 2 }], @filters.uniq([{ "a" => 1 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a") assert_equal([{ "a" => 1 }, { "a" => 3 }, { "a" => 2 }], @filters.uniq([{ "a" => 1 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a"))
testdrop = TestDrop.new testdrop = TestDrop.new
assert_equal [testdrop], @filters.uniq([testdrop, TestDrop.new], 'test') assert_equal([testdrop], @filters.uniq([testdrop, TestDrop.new], 'test'))
end end
def test_uniq_empty_array def test_uniq_empty_array
assert_equal [], @filters.uniq([], "a") assert_equal([], @filters.uniq([], "a"))
end end
def test_uniq_invalid_property def test_uniq_invalid_property
@@ -334,13 +340,13 @@ class StandardFiltersTest < Minitest::Test
[3], [3],
] ]
assert_raises Liquid::ArgumentError do assert_raises(Liquid::ArgumentError) do
@filters.uniq(foo, "bar") @filters.uniq(foo, "bar")
end end
end end
def test_compact_empty_array def test_compact_empty_array
assert_equal [], @filters.compact([], "a") assert_equal([], @filters.compact([], "a"))
end end
def test_compact_invalid_property def test_compact_invalid_property
@@ -350,57 +356,57 @@ class StandardFiltersTest < Minitest::Test
[3], [3],
] ]
assert_raises Liquid::ArgumentError do assert_raises(Liquid::ArgumentError) do
@filters.compact(foo, "bar") @filters.compact(foo, "bar")
end end
end end
def test_reverse def test_reverse
assert_equal [4, 3, 2, 1], @filters.reverse([1, 2, 3, 4]) assert_equal([4, 3, 2, 1], @filters.reverse([1, 2, 3, 4]))
end end
def test_legacy_reverse_hash def test_legacy_reverse_hash
assert_equal [{ a: 1, b: 2 }], @filters.reverse(a: 1, b: 2) assert_equal([{ a: 1, b: 2 }], @filters.reverse(a: 1, b: 2))
end end
def test_map def test_map
assert_equal [1, 2, 3, 4], @filters.map([{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }], 'a') assert_equal([1, 2, 3, 4], @filters.map([{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }], 'a'))
assert_template_result 'abc', "{{ ary | map:'foo' | map:'bar' }}", assert_template_result('abc', "{{ ary | map:'foo' | map:'bar' }}",
'ary' => [{ 'foo' => { 'bar' => 'a' } }, { 'foo' => { 'bar' => 'b' } }, { 'foo' => { 'bar' => 'c' } }] 'ary' => [{ 'foo' => { 'bar' => 'a' } }, { 'foo' => { 'bar' => 'b' } }, { 'foo' => { 'bar' => 'c' } }])
end end
def test_map_doesnt_call_arbitrary_stuff def test_map_doesnt_call_arbitrary_stuff
assert_template_result "", '{{ "foo" | map: "__id__" }}' assert_template_result("", '{{ "foo" | map: "__id__" }}')
assert_template_result "", '{{ "foo" | map: "inspect" }}' assert_template_result("", '{{ "foo" | map: "inspect" }}')
end end
def test_map_calls_to_liquid def test_map_calls_to_liquid
t = TestThing.new t = TestThing.new
assert_template_result "woot: 1", '{{ foo | map: "whatever" }}', "foo" => [t] assert_template_result("woot: 1", '{{ foo | map: "whatever" }}', "foo" => [t])
end end
def test_map_on_hashes def test_map_on_hashes
assert_template_result "4217", '{{ thing | map: "foo" | map: "bar" }}', assert_template_result("4217", '{{ thing | map: "foo" | map: "bar" }}',
"thing" => { "foo" => [{ "bar" => 42 }, { "bar" => 17 }] } "thing" => { "foo" => [{ "bar" => 42 }, { "bar" => 17 }] })
end end
def test_legacy_map_on_hashes_with_dynamic_key def test_legacy_map_on_hashes_with_dynamic_key
template = "{% assign key = 'foo' %}{{ thing | map: key | map: 'bar' }}" template = "{% assign key = 'foo' %}{{ thing | map: key | map: 'bar' }}"
hash = { "foo" => { "bar" => 42 } } hash = { "foo" => { "bar" => 42 } }
assert_template_result "42", template, "thing" => hash assert_template_result("42", template, "thing" => hash)
end end
def test_sort_calls_to_liquid def test_sort_calls_to_liquid
t = TestThing.new t = TestThing.new
Liquid::Template.parse('{{ foo | sort: "whatever" }}').render("foo" => [t]) Liquid::Template.parse('{{ foo | sort: "whatever" }}').render("foo" => [t])
assert t.foo > 0 assert(t.foo > 0)
end end
def test_map_over_proc def test_map_over_proc
drop = TestDrop.new drop = TestDrop.new
p = proc { drop } p = proc { drop }
templ = '{{ procs | map: "test" }}' templ = '{{ procs | map: "test" }}'
assert_template_result "testfoo", templ, "procs" => [p] assert_template_result("testfoo", templ, "procs" => [p])
end end
def test_map_over_drops_returning_procs def test_map_over_drops_returning_procs
@@ -413,11 +419,11 @@ class StandardFiltersTest < Minitest::Test
}, },
] ]
templ = '{{ drops | map: "proc" }}' templ = '{{ drops | map: "proc" }}'
assert_template_result "foobar", templ, "drops" => drops assert_template_result("foobar", templ, "drops" => drops)
end end
def test_map_works_on_enumerables def test_map_works_on_enumerables
assert_template_result "123", '{{ foo | map: "foo" }}', "foo" => TestEnumerable.new assert_template_result("123", '{{ foo | map: "foo" }}', "foo" => TestEnumerable.new)
end end
def test_map_returns_empty_on_2d_input_array def test_map_returns_empty_on_2d_input_array
@@ -427,7 +433,7 @@ class StandardFiltersTest < Minitest::Test
[3], [3],
] ]
assert_raises Liquid::ArgumentError do assert_raises(Liquid::ArgumentError) do
@filters.map(foo, "bar") @filters.map(foo, "bar")
end end
end end
@@ -438,219 +444,220 @@ class StandardFiltersTest < Minitest::Test
[2], [2],
[3], [3],
] ]
assert_raises Liquid::ArgumentError do assert_raises(Liquid::ArgumentError) do
@filters.map(foo, nil) @filters.map(foo, nil)
end end
end end
def test_sort_works_on_enumerables def test_sort_works_on_enumerables
assert_template_result "213", '{{ foo | sort: "bar" | map: "foo" }}', "foo" => TestEnumerable.new assert_template_result("213", '{{ foo | sort: "bar" | map: "foo" }}', "foo" => TestEnumerable.new)
end end
def test_first_and_last_call_to_liquid def test_first_and_last_call_to_liquid
assert_template_result 'foobar', '{{ foo | first }}', 'foo' => [ThingWithToLiquid.new] assert_template_result('foobar', '{{ foo | first }}', 'foo' => [ThingWithToLiquid.new])
assert_template_result 'foobar', '{{ foo | last }}', 'foo' => [ThingWithToLiquid.new] assert_template_result('foobar', '{{ foo | last }}', 'foo' => [ThingWithToLiquid.new])
end end
def test_truncate_calls_to_liquid def test_truncate_calls_to_liquid
assert_template_result "wo...", '{{ foo | truncate: 5 }}', "foo" => TestThing.new assert_template_result("wo...", '{{ foo | truncate: 5 }}', "foo" => TestThing.new)
end end
def test_date def test_date
assert_equal 'May', @filters.date(Time.parse("2006-05-05 10:00:00"), "%B") assert_equal('May', @filters.date(Time.parse("2006-05-05 10:00:00"), "%B"))
assert_equal 'June', @filters.date(Time.parse("2006-06-05 10:00:00"), "%B") assert_equal('June', @filters.date(Time.parse("2006-06-05 10:00:00"), "%B"))
assert_equal 'July', @filters.date(Time.parse("2006-07-05 10:00:00"), "%B") assert_equal('July', @filters.date(Time.parse("2006-07-05 10:00:00"), "%B"))
assert_equal 'May', @filters.date("2006-05-05 10:00:00", "%B") assert_equal('May', @filters.date("2006-05-05 10:00:00", "%B"))
assert_equal 'June', @filters.date("2006-06-05 10:00:00", "%B") assert_equal('June', @filters.date("2006-06-05 10:00:00", "%B"))
assert_equal 'July', @filters.date("2006-07-05 10:00:00", "%B") assert_equal('July', @filters.date("2006-07-05 10:00:00", "%B"))
assert_equal '2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", "") assert_equal('2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", ""))
assert_equal '2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", "") assert_equal('2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", ""))
assert_equal '2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", "") assert_equal('2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", ""))
assert_equal '2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", nil) assert_equal('2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", nil))
assert_equal '07/05/2006', @filters.date("2006-07-05 10:00:00", "%m/%d/%Y") assert_equal('07/05/2006', @filters.date("2006-07-05 10:00:00", "%m/%d/%Y"))
assert_equal "07/16/2004", @filters.date("Fri Jul 16 01:00:00 2004", "%m/%d/%Y") assert_equal("07/16/2004", @filters.date("Fri Jul 16 01:00:00 2004", "%m/%d/%Y"))
assert_equal Date.today.year.to_s, @filters.date('now', '%Y') assert_equal(Date.today.year.to_s, @filters.date('now', '%Y'))
assert_equal Date.today.year.to_s, @filters.date('today', '%Y') assert_equal(Date.today.year.to_s, @filters.date('today', '%Y'))
assert_equal Date.today.year.to_s, @filters.date('Today', '%Y') assert_equal(Date.today.year.to_s, @filters.date('Today', '%Y'))
assert_nil @filters.date(nil, "%B") assert_nil(@filters.date(nil, "%B"))
assert_equal '', @filters.date('', "%B") assert_equal('', @filters.date('', "%B"))
with_timezone("UTC") do 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
end end
def test_first_last def test_first_last
assert_equal 1, @filters.first([1, 2, 3]) assert_equal(1, @filters.first([1, 2, 3]))
assert_equal 3, @filters.last([1, 2, 3]) assert_equal(3, @filters.last([1, 2, 3]))
assert_nil @filters.first([]) assert_nil(@filters.first([]))
assert_nil @filters.last([]) assert_nil(@filters.last([]))
end end
def test_replace def test_replace
assert_equal '2 2 2 2', @filters.replace('1 1 1 1', '1', 2) assert_equal('2 2 2 2', @filters.replace('1 1 1 1', '1', 2))
assert_equal '2 2 2 2', @filters.replace('1 1 1 1', 1, 2) assert_equal('2 2 2 2', @filters.replace('1 1 1 1', 1, 2))
assert_equal '2 1 1 1', @filters.replace_first('1 1 1 1', '1', 2) assert_equal('2 1 1 1', @filters.replace_first('1 1 1 1', '1', 2))
assert_equal '2 1 1 1', @filters.replace_first('1 1 1 1', 1, 2) assert_equal('2 1 1 1', @filters.replace_first('1 1 1 1', 1, 2))
assert_template_result '2 1 1 1', "{{ '1 1 1 1' | replace_first: '1', 2 }}" assert_template_result('2 1 1 1', "{{ '1 1 1 1' | replace_first: '1', 2 }}")
end end
def test_remove def test_remove
assert_equal ' ', @filters.remove("a a a a", 'a') assert_equal(' ', @filters.remove("a a a a", 'a'))
assert_equal ' ', @filters.remove("1 1 1 1", 1) assert_equal(' ', @filters.remove("1 1 1 1", 1))
assert_equal 'a a a', @filters.remove_first("a a a a", 'a ') 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_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('a a a', "{{ 'a a a a' | remove_first: 'a ' }}")
end end
def test_pipes_in_string_arguments def test_pipes_in_string_arguments
assert_template_result 'foobar', "{{ 'foo|bar' | remove: '|' }}" assert_template_result('foobar', "{{ 'foo|bar' | remove: '|' }}")
end end
def test_strip def test_strip
assert_template_result 'ab c', "{{ source | strip }}", 'source' => " ab c " assert_template_result('ab c', "{{ source | strip }}", 'source' => " ab c ")
assert_template_result 'ab c', "{{ source | strip }}", 'source' => " \tab c \n \t" assert_template_result('ab c', "{{ source | strip }}", 'source' => " \tab c \n \t")
end end
def test_lstrip def test_lstrip
assert_template_result 'ab c ', "{{ source | lstrip }}", 'source' => " ab c " assert_template_result('ab c ', "{{ source | lstrip }}", 'source' => " ab c ")
assert_template_result "ab c \n \t", "{{ source | lstrip }}", 'source' => " \tab c \n \t" assert_template_result("ab c \n \t", "{{ source | lstrip }}", 'source' => " \tab c \n \t")
end end
def test_rstrip def test_rstrip
assert_template_result " ab c", "{{ source | rstrip }}", 'source' => " ab c " assert_template_result(" ab c", "{{ source | rstrip }}", 'source' => " ab c ")
assert_template_result " \tab c", "{{ source | rstrip }}", 'source' => " \tab c \n \t" assert_template_result(" \tab c", "{{ source | rstrip }}", 'source' => " \tab c \n \t")
end end
def test_strip_newlines def test_strip_newlines
assert_template_result 'abc', "{{ source | strip_newlines }}", 'source' => "a\nb\nc" assert_template_result('abc', "{{ source | strip_newlines }}", 'source' => "a\nb\nc")
assert_template_result 'abc', "{{ source | strip_newlines }}", 'source' => "a\r\nb\nc" assert_template_result('abc', "{{ source | strip_newlines }}", 'source' => "a\r\nb\nc")
end end
def test_newlines_to_br def test_newlines_to_br
assert_template_result "a<br />\nb<br />\nc", "{{ source | newline_to_br }}", 'source' => "a\nb\nc" assert_template_result("a<br />\nb<br />\nc", "{{ source | newline_to_br }}", 'source' => "a\nb\nc")
assert_template_result("a<br />\nb<br />\nc", "{{ source | newline_to_br }}", 'source' => "a\r\nb\nc")
end end
def test_plus def test_plus
assert_template_result "2", "{{ 1 | plus:1 }}" assert_template_result("2", "{{ 1 | plus:1 }}")
assert_template_result "2.0", "{{ '1' | plus:'1.0' }}" assert_template_result("2.0", "{{ '1' | plus:'1.0' }}")
assert_template_result "5", "{{ price | plus:'2' }}", 'price' => NumberLikeThing.new(3) assert_template_result("5", "{{ price | plus:'2' }}", 'price' => NumberLikeThing.new(3))
end end
def test_minus def test_minus
assert_template_result "4", "{{ input | minus:operand }}", 'input' => 5, 'operand' => 1 assert_template_result("4", "{{ input | minus:operand }}", 'input' => 5, 'operand' => 1)
assert_template_result "2.3", "{{ '4.3' | minus:'2' }}" assert_template_result("2.3", "{{ '4.3' | minus:'2' }}")
assert_template_result "5", "{{ price | minus:'2' }}", 'price' => NumberLikeThing.new(7) assert_template_result("5", "{{ price | minus:'2' }}", 'price' => NumberLikeThing.new(7))
end end
def test_abs def test_abs
assert_template_result "17", "{{ 17 | abs }}" assert_template_result("17", "{{ 17 | abs }}")
assert_template_result "17", "{{ -17 | abs }}" assert_template_result("17", "{{ -17 | abs }}")
assert_template_result "17", "{{ '17' | abs }}" assert_template_result("17", "{{ '17' | abs }}")
assert_template_result "17", "{{ '-17' | abs }}" assert_template_result("17", "{{ '-17' | abs }}")
assert_template_result "0", "{{ 0 | abs }}" assert_template_result("0", "{{ 0 | abs }}")
assert_template_result "0", "{{ '0' | abs }}" assert_template_result("0", "{{ '0' | abs }}")
assert_template_result "17.42", "{{ 17.42 | abs }}" assert_template_result("17.42", "{{ 17.42 | abs }}")
assert_template_result "17.42", "{{ -17.42 | abs }}" assert_template_result("17.42", "{{ -17.42 | abs }}")
assert_template_result "17.42", "{{ '17.42' | abs }}" assert_template_result("17.42", "{{ '17.42' | abs }}")
assert_template_result "17.42", "{{ '-17.42' | abs }}" assert_template_result("17.42", "{{ '-17.42' | abs }}")
end end
def test_times def test_times
assert_template_result "12", "{{ 3 | times:4 }}" assert_template_result("12", "{{ 3 | times:4 }}")
assert_template_result "0", "{{ 'foo' | times:4 }}" assert_template_result("0", "{{ 'foo' | times:4 }}")
assert_template_result "6", "{{ '2.1' | times:3 | replace: '.','-' | plus:0}}" assert_template_result("6", "{{ '2.1' | times:3 | replace: '.','-' | plus:0}}")
assert_template_result "7.25", "{{ 0.0725 | times:100 }}" assert_template_result("7.25", "{{ 0.0725 | times:100 }}")
assert_template_result "-7.25", '{{ "-0.0725" | times:100 }}' assert_template_result("-7.25", '{{ "-0.0725" | times:100 }}')
assert_template_result "7.25", '{{ "-0.0725" | times: -100 }}' assert_template_result("7.25", '{{ "-0.0725" | times: -100 }}')
assert_template_result "4", "{{ price | times:2 }}", 'price' => NumberLikeThing.new(2) assert_template_result("4", "{{ price | times:2 }}", 'price' => NumberLikeThing.new(2))
end end
def test_divided_by def test_divided_by
assert_template_result "4", "{{ 12 | divided_by:3 }}" assert_template_result("4", "{{ 12 | divided_by:3 }}")
assert_template_result "4", "{{ 14 | divided_by:3 }}" assert_template_result("4", "{{ 14 | divided_by:3 }}")
assert_template_result "5", "{{ 15 | divided_by:3 }}" assert_template_result("5", "{{ 15 | divided_by:3 }}")
assert_equal "Liquid error: divided by 0", Template.parse("{{ 5 | divided_by:0 }}").render assert_equal("Liquid error: divided by 0", Template.parse("{{ 5 | divided_by:0 }}").render)
assert_template_result "0.5", "{{ 2.0 | divided_by:4 }}" assert_template_result("0.5", "{{ 2.0 | divided_by:4 }}")
assert_raises(Liquid::ZeroDivisionError) do assert_raises(Liquid::ZeroDivisionError) do
assert_template_result "4", "{{ 1 | modulo: 0 }}" assert_template_result("4", "{{ 1 | modulo: 0 }}")
end end
assert_template_result "5", "{{ price | divided_by:2 }}", 'price' => NumberLikeThing.new(10) assert_template_result("5", "{{ price | divided_by:2 }}", 'price' => NumberLikeThing.new(10))
end end
def test_modulo def test_modulo
assert_template_result "1", "{{ 3 | modulo:2 }}" assert_template_result("1", "{{ 3 | modulo:2 }}")
assert_raises(Liquid::ZeroDivisionError) do assert_raises(Liquid::ZeroDivisionError) do
assert_template_result "4", "{{ 1 | modulo: 0 }}" assert_template_result("4", "{{ 1 | modulo: 0 }}")
end end
assert_template_result "1", "{{ price | modulo:2 }}", 'price' => NumberLikeThing.new(3) assert_template_result("1", "{{ price | modulo:2 }}", 'price' => NumberLikeThing.new(3))
end end
def test_round def test_round
assert_template_result "5", "{{ input | round }}", 'input' => 4.6 assert_template_result("5", "{{ input | round }}", 'input' => 4.6)
assert_template_result "4", "{{ '4.3' | round }}" assert_template_result("4", "{{ '4.3' | round }}")
assert_template_result "4.56", "{{ input | round: 2 }}", 'input' => 4.5612 assert_template_result("4.56", "{{ input | round: 2 }}", 'input' => 4.5612)
assert_raises(Liquid::FloatDomainError) do 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 end
assert_template_result "5", "{{ price | round }}", 'price' => NumberLikeThing.new(4.6) assert_template_result("5", "{{ price | round }}", 'price' => NumberLikeThing.new(4.6))
assert_template_result "4", "{{ price | round }}", 'price' => NumberLikeThing.new(4.3) assert_template_result("4", "{{ price | round }}", 'price' => NumberLikeThing.new(4.3))
end end
def test_ceil def test_ceil
assert_template_result "5", "{{ input | ceil }}", 'input' => 4.6 assert_template_result("5", "{{ input | ceil }}", 'input' => 4.6)
assert_template_result "5", "{{ '4.3' | ceil }}" assert_template_result("5", "{{ '4.3' | ceil }}")
assert_raises(Liquid::FloatDomainError) do 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 end
assert_template_result "5", "{{ price | ceil }}", 'price' => NumberLikeThing.new(4.6) assert_template_result("5", "{{ price | ceil }}", 'price' => NumberLikeThing.new(4.6))
end end
def test_floor def test_floor
assert_template_result "4", "{{ input | floor }}", 'input' => 4.6 assert_template_result("4", "{{ input | floor }}", 'input' => 4.6)
assert_template_result "4", "{{ '4.3' | floor }}" assert_template_result("4", "{{ '4.3' | floor }}")
assert_raises(Liquid::FloatDomainError) do 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 end
assert_template_result "5", "{{ price | floor }}", 'price' => NumberLikeThing.new(5.4) assert_template_result("5", "{{ price | floor }}", 'price' => NumberLikeThing.new(5.4))
end end
def test_at_most def test_at_most
assert_template_result "4", "{{ 5 | at_most:4 }}" assert_template_result("4", "{{ 5 | at_most:4 }}")
assert_template_result "5", "{{ 5 | at_most:5 }}" assert_template_result("5", "{{ 5 | at_most:5 }}")
assert_template_result "5", "{{ 5 | at_most:6 }}" assert_template_result("5", "{{ 5 | at_most:6 }}")
assert_template_result "4.5", "{{ 4.5 | at_most:5 }}" assert_template_result("4.5", "{{ 4.5 | at_most:5 }}")
assert_template_result "5", "{{ width | at_most:5 }}", 'width' => NumberLikeThing.new(6) assert_template_result("5", "{{ width | at_most:5 }}", 'width' => NumberLikeThing.new(6))
assert_template_result "4", "{{ width | at_most:5 }}", 'width' => NumberLikeThing.new(4) assert_template_result("4", "{{ width | at_most:5 }}", 'width' => NumberLikeThing.new(4))
assert_template_result "4", "{{ 5 | at_most: width }}", 'width' => NumberLikeThing.new(4) assert_template_result("4", "{{ 5 | at_most: width }}", 'width' => NumberLikeThing.new(4))
end end
def test_at_least def test_at_least
assert_template_result "5", "{{ 5 | at_least:4 }}" assert_template_result("5", "{{ 5 | at_least:4 }}")
assert_template_result "5", "{{ 5 | at_least:5 }}" assert_template_result("5", "{{ 5 | at_least:5 }}")
assert_template_result "6", "{{ 5 | at_least:6 }}" assert_template_result("6", "{{ 5 | at_least:6 }}")
assert_template_result "5", "{{ 4.5 | at_least:5 }}" assert_template_result("5", "{{ 4.5 | at_least:5 }}")
assert_template_result "6", "{{ width | at_least:5 }}", 'width' => NumberLikeThing.new(6) assert_template_result("6", "{{ width | at_least:5 }}", 'width' => NumberLikeThing.new(6))
assert_template_result "5", "{{ width | at_least:5 }}", 'width' => NumberLikeThing.new(4) assert_template_result("5", "{{ width | at_least:5 }}", 'width' => NumberLikeThing.new(4))
assert_template_result "6", "{{ 5 | at_least: width }}", 'width' => NumberLikeThing.new(6) assert_template_result("6", "{{ 5 | at_least: width }}", 'width' => NumberLikeThing.new(6))
end end
def test_append def test_append
@@ -660,9 +667,9 @@ class StandardFiltersTest < Minitest::Test
end end
def test_concat def test_concat
assert_equal [1, 2, 3, 4], @filters.concat([1, 2], [3, 4]) assert_equal([1, 2, 3, 4], @filters.concat([1, 2], [3, 4]))
assert_equal [1, 2, 'a'], @filters.concat([1, 2], ['a']) assert_equal([1, 2, 'a'], @filters.concat([1, 2], ['a']))
assert_equal [1, 2, 10], @filters.concat([1, 2], [10]) assert_equal([1, 2, 10], @filters.concat([1, 2], [10]))
assert_raises(Liquid::ArgumentError, "concat filter requires an array argument") do assert_raises(Liquid::ArgumentError, "concat filter requires an array argument") do
@filters.concat([1, 2], 10) @filters.concat([1, 2], 10)
@@ -676,12 +683,23 @@ class StandardFiltersTest < Minitest::Test
end end
def test_default def test_default
assert_equal "foo", @filters.default("foo", "bar") assert_equal("foo", @filters.default("foo", "bar"))
assert_equal "bar", @filters.default(nil, "bar") assert_equal("bar", @filters.default(nil, "bar"))
assert_equal "bar", @filters.default("", "bar") assert_equal("bar", @filters.default("", "bar"))
assert_equal "bar", @filters.default(false, "bar") assert_equal("bar", @filters.default(false, "bar"))
assert_equal "bar", @filters.default([], "bar") assert_equal("bar", @filters.default([], "bar"))
assert_equal "bar", @filters.default({}, "bar") assert_equal("bar", @filters.default({}, "bar"))
assert_template_result('bar', "{{ false | default: 'bar' }}")
end
def test_default_handle_false
assert_equal("foo", @filters.default("foo", "bar", "allow_false" => true))
assert_equal("bar", @filters.default(nil, "bar", "allow_false" => true))
assert_equal("bar", @filters.default("", "bar", "allow_false" => true))
assert_equal(false, @filters.default(false, "bar", "allow_false" => true))
assert_equal("bar", @filters.default([], "bar", "allow_false" => true))
assert_equal("bar", @filters.default({}, "bar", "allow_false" => true))
assert_template_result('false', "{{ false | default: 'bar', allow_false: true }}")
end end
def test_cannot_access_private_methods def test_cannot_access_private_methods
@@ -706,8 +724,8 @@ class StandardFiltersTest < Minitest::Test
{ "handle" => "delta", "ok" => true }, { "handle" => "delta", "ok" => true },
] ]
assert_equal expectation, @filters.where(input, "ok", true) assert_equal(expectation, @filters.where(input, "ok", true))
assert_equal expectation, @filters.where(input, "ok") assert_equal(expectation, @filters.where(input, "ok"))
end end
def test_where_no_key_set def test_where_no_key_set
@@ -723,13 +741,13 @@ class StandardFiltersTest < Minitest::Test
{ "handle" => "delta", "ok" => true }, { "handle" => "delta", "ok" => true },
] ]
assert_equal expectation, @filters.where(input, "ok", true) assert_equal(expectation, @filters.where(input, "ok", true))
assert_equal expectation, @filters.where(input, "ok") assert_equal(expectation, @filters.where(input, "ok"))
end end
def test_where_non_array_map_input def test_where_non_array_map_input
assert_equal [{ "a" => "ok" }], @filters.where({ "a" => "ok" }, "a", "ok") assert_equal([{ "a" => "ok" }], @filters.where({ "a" => "ok" }, "a", "ok"))
assert_equal [], @filters.where({ "a" => "not ok" }, "a", "ok") assert_equal([], @filters.where({ "a" => "not ok" }, "a", "ok"))
end end
def test_where_indexable_but_non_map_value def test_where_indexable_but_non_map_value
@@ -744,14 +762,57 @@ class StandardFiltersTest < Minitest::Test
{ "message" => "Hallo!", "language" => "German" }, { "message" => "Hallo!", "language" => "German" },
] ]
assert_equal [{ "message" => "Bonjour!", "language" => "French" }], @filters.where(input, "language", "French") assert_equal([{ "message" => "Bonjour!", "language" => "French" }], @filters.where(input, "language", "French"))
assert_equal [{ "message" => "Hallo!", "language" => "German" }], @filters.where(input, "language", "German") assert_equal([{ "message" => "Hallo!", "language" => "German" }], @filters.where(input, "language", "German"))
assert_equal [{ "message" => "Hello!", "language" => "English" }], @filters.where(input, "language", "English") assert_equal([{ "message" => "Hello!", "language" => "English" }], @filters.where(input, "language", "English"))
end end
def test_where_array_of_only_unindexable_values def test_where_array_of_only_unindexable_values
assert_nil @filters.where([nil], "ok", true) assert_nil(@filters.where([nil], "ok", true))
assert_nil @filters.where([nil], "ok") assert_nil(@filters.where([nil], "ok"))
end
def test_all_filters_never_raise_non_liquid_exception
test_drop = TestDrop.new
test_drop.context = Context.new
test_enum = TestEnumerable.new
test_enum.context = Context.new
test_types = [
"foo",
123,
0,
0.0,
-1234.003030303,
-99999999,
1234.38383000383830003838300,
nil,
true,
false,
TestThing.new,
test_drop,
test_enum,
["foo", "bar"],
{ "foo" => "bar" },
{ foo: "bar" },
[{ "foo" => "bar" }, { "foo" => 123 }, { "foo" => nil }, { "foo" => true }, { "foo" => ["foo", "bar"] }],
{ 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
end
end
end end
def test_where_no_target_value def test_where_no_target_value
@@ -762,13 +823,13 @@ class StandardFiltersTest < Minitest::Test
{ "bar" => true }, { "bar" => true },
] ]
assert_equal [{ "foo" => true }, { "foo" => "for sure" }], @filters.where(input, "foo") assert_equal([{ "foo" => true }, { "foo" => "for sure" }], @filters.where(input, "foo"))
end end
private private
def with_timezone(tz) def with_timezone(tz)
old_tz = ENV['TZ'] old_tz = ENV['TZ']
ENV['TZ'] = tz ENV['TZ'] = tz
yield yield
ensure ensure

View File

@@ -0,0 +1,59 @@
# frozen_string_literal: true
require 'test_helper'
class TagDisableableTest < Minitest::Test
include Liquid
module RenderTagName
def render(_context)
tag_name
end
end
class Custom < Tag
prepend Liquid::Tag::Disableable
include RenderTagName
end
class Custom2 < Tag
prepend Liquid::Tag::Disableable
include RenderTagName
end
class DisableCustom < Block
disable_tags "custom"
end
class DisableBoth < Block
disable_tags "custom", "custom2"
end
def test_block_tag_disabling_nested_tag
with_disableable_tags do
with_custom_tag('disable', DisableCustom) do
output = Template.parse('{% disable %}{% custom %};{% custom2 %}{% enddisable %}').render
assert_equal('Liquid error: custom usage is not allowed in this context;custom2', output)
end
end
end
def test_block_tag_disabling_multiple_nested_tags
with_disableable_tags do
with_custom_tag('disable', DisableBoth) do
output = Template.parse('{% disable %}{% custom %};{% custom2 %}{% enddisable %}').render
assert_equal('Liquid error: custom usage is not allowed in this context;Liquid error: custom2 usage is not allowed in this context', output)
end
end
end
private
def with_disableable_tags
with_custom_tag('custom', Custom) do
with_custom_tag('custom2', Custom2) do
yield
end
end
end
end

View File

@@ -0,0 +1,45 @@
# frozen_string_literal: true
require 'test_helper'
class TagTest < Minitest::Test
include Liquid
def test_custom_tags_have_a_default_render_to_output_buffer_method_for_backwards_compatibility
klass1 = Class.new(Tag) do
def render(*)
'hello'
end
end
with_custom_tag('blabla', klass1) do
template = Liquid::Template.parse("{% blabla %}")
assert_equal('hello', template.render)
buf = +''
output = template.render({}, output: buf)
assert_equal('hello', output)
assert_equal('hello', buf)
assert_equal(buf.object_id, output.object_id)
end
klass2 = Class.new(klass1) do
def render(*)
'foo' + super + 'bar'
end
end
with_custom_tag('blabla', klass2) do
template = Liquid::Template.parse("{% blabla %}")
assert_equal('foohellobar', template.render)
buf = +''
output = template.render({}, output: buf)
assert_equal('foohellobar', output)
assert_equal('foohellobar', buf)
assert_equal(buf.object_id, output.object_id)
end
end
end

View File

@@ -8,8 +8,8 @@ class BreakTagTest < Minitest::Test
# tests that no weird errors are raised if break is called outside of a # tests that no weird errors are raised if break is called outside of a
# block # block
def test_break_with_no_block def test_break_with_no_block
assigns = { 'i' => 1 } assigns = { 'i' => 1 }
markup = '{% break %}' markup = '{% break %}'
expected = '' expected = ''
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)

View File

@@ -8,8 +8,8 @@ class ContinueTagTest < Minitest::Test
# tests that no weird errors are raised if continue is called outside of a # tests that no weird errors are raised if continue is called outside of a
# block # block
def test_continue_with_no_block def test_continue_with_no_block
assigns = {} assigns = {}
markup = '{% continue %}' markup = '{% continue %}'
expected = '' expected = ''
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)

View File

@@ -106,7 +106,7 @@ HERE
end end
def test_limiting_with_invalid_limit def test_limiting_with_invalid_limit
assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] }
template = <<-MKUP template = <<-MKUP
{% for i in array limit: true offset: 1 %} {% for i in array limit: true offset: 1 %}
{{ i }} {{ i }}
@@ -120,7 +120,7 @@ HERE
end end
def test_limiting_with_invalid_offset def test_limiting_with_invalid_offset
assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] }
template = <<-MKUP template = <<-MKUP
{% for i in array limit: 1 offset: true %} {% for i in array limit: 1 offset: true %}
{{ i }} {{ i }}
@@ -134,8 +134,8 @@ HERE
end end
def test_dynamic_variable_limiting def test_dynamic_variable_limiting
assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] }
assigns['limit'] = 2 assigns['limit'] = 2
assigns['offset'] = 2 assigns['offset'] = 2
assert_template_result('34', '{%for i in array limit: limit offset: offset %}{{ i }}{%endfor%}', assigns) assert_template_result('34', '{%for i in array limit: limit offset: offset %}{{ i }}{%endfor%}', assigns)
@@ -152,8 +152,8 @@ HERE
end end
def test_pause_resume def test_pause_resume
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } } assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } }
markup = <<-MKUP markup = <<-MKUP
{%for i in array.items limit: 3 %}{{i}}{%endfor%} {%for i in array.items limit: 3 %}{{i}}{%endfor%}
next next
{%for i in array.items offset:continue limit: 3 %}{{i}}{%endfor%} {%for i in array.items offset:continue limit: 3 %}{{i}}{%endfor%}
@@ -171,8 +171,8 @@ HERE
end end
def test_pause_resume_limit def test_pause_resume_limit
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } } assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } }
markup = <<-MKUP markup = <<-MKUP
{%for i in array.items limit:3 %}{{i}}{%endfor%} {%for i in array.items limit:3 %}{{i}}{%endfor%}
next next
{%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%} {%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%}
@@ -190,8 +190,8 @@ HERE
end end
def test_pause_resume_big_limit def test_pause_resume_big_limit
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } } assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } }
markup = <<-MKUP markup = <<-MKUP
{%for i in array.items limit:3 %}{{i}}{%endfor%} {%for i in array.items limit:3 %}{{i}}{%endfor%}
next next
{%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%} {%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%}
@@ -209,8 +209,8 @@ HERE
end end
def test_pause_resume_big_offset def test_pause_resume_big_offset
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } } assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } }
markup = '{%for i in array.items limit:3 %}{{i}}{%endfor%} markup = '{%for i in array.items limit:3 %}{{i}}{%endfor%}
next next
{%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%} {%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%}
next next
@@ -226,26 +226,26 @@ HERE
def test_for_with_break def test_for_with_break
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] } } assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] } }
markup = '{% for i in array.items %}{% break %}{% endfor %}' markup = '{% for i in array.items %}{% break %}{% endfor %}'
expected = "" expected = ""
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
markup = '{% for i in array.items %}{{ i }}{% break %}{% endfor %}' markup = '{% for i in array.items %}{{ i }}{% break %}{% endfor %}'
expected = "1" expected = "1"
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
markup = '{% for i in array.items %}{% break %}{{ i }}{% endfor %}' markup = '{% for i in array.items %}{% break %}{{ i }}{% endfor %}'
expected = "" expected = ""
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
markup = '{% for i in array.items %}{{ i }}{% if i > 3 %}{% break %}{% endif %}{% endfor %}' markup = '{% for i in array.items %}{{ i }}{% if i > 3 %}{% break %}{% endif %}{% endfor %}'
expected = "1234" expected = "1234"
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
# tests to ensure it only breaks out of the local for loop # tests to ensure it only breaks out of the local for loop
# and not all of them. # and not all of them.
assigns = { 'array' => [[1, 2], [3, 4], [5, 6]] } assigns = { 'array' => [[1, 2], [3, 4], [5, 6]] }
markup = '{% for item in array %}' \ markup = '{% for item in array %}' \
'{% for i in item %}' \ '{% for i in item %}' \
'{% if i == 1 %}' \ '{% if i == 1 %}' \
'{% break %}' \ '{% break %}' \
@@ -257,8 +257,8 @@ HERE
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
# test break does nothing when unreached # test break does nothing when unreached
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5] } } assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5] } }
markup = '{% for i in array.items %}{% if i == 9999 %}{% break %}{% endif %}{{ i }}{% endfor %}' markup = '{% for i in array.items %}{% if i == 9999 %}{% break %}{% endif %}{{ i }}{% endfor %}'
expected = '12345' expected = '12345'
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
end end
@@ -266,29 +266,29 @@ HERE
def test_for_with_continue def test_for_with_continue
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5] } } assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5] } }
markup = '{% for i in array.items %}{% continue %}{% endfor %}' markup = '{% for i in array.items %}{% continue %}{% endfor %}'
expected = "" expected = ""
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
markup = '{% for i in array.items %}{{ i }}{% continue %}{% endfor %}' markup = '{% for i in array.items %}{{ i }}{% continue %}{% endfor %}'
expected = "12345" expected = "12345"
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
markup = '{% for i in array.items %}{% continue %}{{ i }}{% endfor %}' markup = '{% for i in array.items %}{% continue %}{{ i }}{% endfor %}'
expected = "" expected = ""
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
markup = '{% for i in array.items %}{% if i > 3 %}{% continue %}{% endif %}{{ i }}{% endfor %}' markup = '{% for i in array.items %}{% if i > 3 %}{% continue %}{% endif %}{{ i }}{% endfor %}'
expected = "123" expected = "123"
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
markup = '{% for i in array.items %}{% if i == 3 %}{% continue %}{% else %}{{ i }}{% endif %}{% endfor %}' markup = '{% for i in array.items %}{% if i == 3 %}{% continue %}{% else %}{{ i }}{% endif %}{% endfor %}'
expected = "1245" expected = "1245"
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
# tests to ensure it only continues the local for loop and not all of them. # tests to ensure it only continues the local for loop and not all of them.
assigns = { 'array' => [[1, 2], [3, 4], [5, 6]] } assigns = { 'array' => [[1, 2], [3, 4], [5, 6]] }
markup = '{% for item in array %}' \ markup = '{% for item in array %}' \
'{% for i in item %}' \ '{% for i in item %}' \
'{% if i == 1 %}' \ '{% if i == 1 %}' \
'{% continue %}' \ '{% continue %}' \
@@ -300,8 +300,8 @@ HERE
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
# test continue does nothing when unreached # test continue does nothing when unreached
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5] } } assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5] } }
markup = '{% for i in array.items %}{% if i == 9999 %}{% continue %}{% endif %}{{ i }}{% endfor %}' markup = '{% for i in array.items %}{% if i == 9999 %}{% continue %}{% endif %}{{ i }}{% endfor %}'
expected = '12345' expected = '12345'
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
end end
@@ -350,7 +350,7 @@ HERE
end end
def test_inner_for_over_empty_input def test_inner_for_over_empty_input
assert_template_result 'oo', '{% for a in (1..2) %}o{% for b in empty %}{% endfor %}{% endfor %}' assert_template_result('oo', '{% for a in (1..2) %}o{% for b in empty %}{% endfor %}{% endfor %}')
end end
def test_blank_string_not_iterable def test_blank_string_not_iterable
@@ -389,41 +389,41 @@ HERE
end end
def test_iterate_with_each_when_no_limit_applied def test_iterate_with_each_when_no_limit_applied
loader = LoaderDrop.new([1, 2, 3, 4, 5]) loader = LoaderDrop.new([1, 2, 3, 4, 5])
assigns = { 'items' => loader } assigns = { 'items' => loader }
expected = '12345' expected = '12345'
template = '{% for item in items %}{{item}}{% endfor %}' template = '{% for item in items %}{{item}}{% endfor %}'
assert_template_result(expected, template, assigns) assert_template_result(expected, template, assigns)
assert loader.each_called assert(loader.each_called)
assert !loader.load_slice_called assert(!loader.load_slice_called)
end end
def test_iterate_with_load_slice_when_limit_applied def test_iterate_with_load_slice_when_limit_applied
loader = LoaderDrop.new([1, 2, 3, 4, 5]) loader = LoaderDrop.new([1, 2, 3, 4, 5])
assigns = { 'items' => loader } assigns = { 'items' => loader }
expected = '1' expected = '1'
template = '{% for item in items limit:1 %}{{item}}{% endfor %}' template = '{% for item in items limit:1 %}{{item}}{% endfor %}'
assert_template_result(expected, template, assigns) assert_template_result(expected, template, assigns)
assert !loader.each_called assert(!loader.each_called)
assert loader.load_slice_called assert(loader.load_slice_called)
end end
def test_iterate_with_load_slice_when_limit_and_offset_applied def test_iterate_with_load_slice_when_limit_and_offset_applied
loader = LoaderDrop.new([1, 2, 3, 4, 5]) loader = LoaderDrop.new([1, 2, 3, 4, 5])
assigns = { 'items' => loader } assigns = { 'items' => loader }
expected = '34' expected = '34'
template = '{% for item in items offset:2 limit:2 %}{{item}}{% endfor %}' template = '{% for item in items offset:2 limit:2 %}{{item}}{% endfor %}'
assert_template_result(expected, template, assigns) assert_template_result(expected, template, assigns)
assert !loader.each_called assert(!loader.each_called)
assert loader.load_slice_called assert(loader.load_slice_called)
end end
def test_iterate_with_load_slice_returns_same_results_as_without def test_iterate_with_load_slice_returns_same_results_as_without
loader = LoaderDrop.new([1, 2, 3, 4, 5]) loader = LoaderDrop.new([1, 2, 3, 4, 5])
loader_assigns = { 'items' => loader } loader_assigns = { 'items' => loader }
array_assigns = { 'items' => [1, 2, 3, 4, 5] } array_assigns = { 'items' => [1, 2, 3, 4, 5] }
expected = '34' expected = '34'
template = '{% for item in items offset:2 limit:2 %}{{item}}{% endfor %}' template = '{% for item in items offset:2 limit:2 %}{{item}}{% endfor %}'
assert_template_result(expected, template, loader_assigns) assert_template_result(expected, template, loader_assigns)
assert_template_result(expected, template, array_assigns) assert_template_result(expected, template, array_assigns)
end end
@@ -435,6 +435,32 @@ HERE
Liquid::Template.parse('{% for i in (1..2) %}{{ standard_error }}{% endfor %}').render!(context) Liquid::Template.parse('{% for i in (1..2) %}{{ standard_error }}{% endfor %}').render!(context)
end end
assert context.registers[:for_stack].empty? assert(context.registers[:for_stack].empty?)
end
def test_instrument_for_offset_continue
assert_usage_increment('for_offset_continue') do
Template.parse('{% for item in items offset:continue %}{{item}}{% endfor %}')
end
assert_usage_increment('for_offset_continue', times: 0) do
Template.parse('{% for item in items offset:2 %}{{item}}{% endfor %}')
end
end
def test_instrument_forloop_drop_name
assigns = { 'items' => [1, 2, 3, 4, 5] }
assert_usage_increment('forloop_drop_name', times: 5) do
Template.parse('{% for item in items %}{{forloop.name}}{% endfor %}').render!(assigns)
end
assert_usage_increment('forloop_drop_name', times: 0) do
Template.parse('{% for item in items %}{{forloop.index}}{% endfor %}').render!(assigns)
end
assert_usage_increment('forloop_drop_name', times: 0) do
Template.parse('{% for item in items %}{{item}}{% endfor %}').render!(assigns)
end
end end
end end

View File

@@ -45,7 +45,7 @@ class IfElseTagTest < Minitest::Test
def test_comparison_of_strings_containing_and_or_or def test_comparison_of_strings_containing_and_or_or
awful_markup = "a == 'and' and b == 'or' and c == 'foo and bar' and d == 'bar or baz' and e == 'foo' and foo and bar" awful_markup = "a == 'and' and b == 'or' and c == 'foo and bar' and d == 'bar or baz' and e == 'foo' and foo and bar"
assigns = { 'a' => 'and', 'b' => 'or', 'c' => 'foo and bar', 'd' => 'bar or baz', 'e' => 'foo', 'foo' => true, 'bar' => true } assigns = { 'a' => 'and', 'b' => 'or', 'c' => 'foo and bar', 'd' => 'bar or baz', 'e' => 'foo', 'foo' => true, 'bar' => true }
assert_template_result(' YES ', "{% if #{awful_markup} %} YES {% endif %}", assigns) assert_template_result(' YES ', "{% if #{awful_markup} %} YES {% endif %}", assigns)
end end
@@ -184,7 +184,7 @@ class IfElseTagTest < Minitest::Test
tests.each do |vals, expected| tests.each do |vals, expected|
a, b, c = vals a, b, c = vals
assigns = { 'a' => a, 'b' => b, 'c' => c } 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 end
end end

View File

@@ -8,6 +8,9 @@ class TestFileSystem
when "product" when "product"
"Product: {{ product.title }} " "Product: {{ product.title }} "
when "product_alias"
"Product: {{ product.title }} "
when "locale_variables" when "locale_variables"
"Locale: {{echo1}} {{echo2}}" "Locale: {{echo1}} {{echo2}}"
@@ -51,7 +54,7 @@ class CountingFileSystem
attr_reader :count attr_reader :count
def read_template_file(_template_path) def read_template_file(_template_path)
@count ||= 0 @count ||= 0
@count += 1 @count += 1
'from CountingFileSystem' 'from CountingFileSystem'
end end
end end
@@ -82,56 +85,66 @@ class IncludeTagTest < Minitest::Test
end end
def test_include_tag_looks_for_file_system_in_registers_first def test_include_tag_looks_for_file_system_in_registers_first
assert_equal 'from OtherFileSystem', assert_equal('from OtherFileSystem',
Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: OtherFileSystem.new }) Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: OtherFileSystem.new }))
end end
def test_include_tag_with def test_include_tag_with
assert_template_result "Product: Draft 151cm ", assert_template_result("Product: Draft 151cm ",
"{% include 'product' with products[0] %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] "{% include 'product' with products[0] %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
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' }])
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' }])
end end
def test_include_tag_with_default_name def test_include_tag_with_default_name
assert_template_result "Product: Draft 151cm ", assert_template_result("Product: Draft 151cm ",
"{% include 'product' %}", "product" => { 'title' => 'Draft 151cm' } "{% include 'product' %}", "product" => { 'title' => 'Draft 151cm' })
end end
def test_include_tag_for def test_include_tag_for
assert_template_result "Product: Draft 151cm Product: Element 155cm ", assert_template_result("Product: Draft 151cm Product: Element 155cm ",
"{% include 'product' for products %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] "{% include 'product' for products %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end end
def test_include_tag_with_local_variables def test_include_tag_with_local_variables
assert_template_result "Locale: test123 ", "{% include 'locale_variables' echo1: 'test123' %}" assert_template_result("Locale: test123 ", "{% include 'locale_variables' echo1: 'test123' %}")
end end
def test_include_tag_with_multiple_local_variables def test_include_tag_with_multiple_local_variables
assert_template_result "Locale: test123 test321", assert_template_result("Locale: test123 test321",
"{% include 'locale_variables' echo1: 'test123', echo2: 'test321' %}" "{% include 'locale_variables' echo1: 'test123', echo2: 'test321' %}")
end end
def test_include_tag_with_multiple_local_variables_from_context def test_include_tag_with_multiple_local_variables_from_context
assert_template_result "Locale: test123 test321", assert_template_result("Locale: test123 test321",
"{% include 'locale_variables' echo1: echo1, echo2: more_echos.echo2 %}", "{% include 'locale_variables' echo1: echo1, echo2: more_echos.echo2 %}",
'echo1' => 'test123', 'more_echos' => { "echo2" => 'test321' } 'echo1' => 'test123', 'more_echos' => { "echo2" => 'test321' })
end end
def test_included_templates_assigns_variables def test_included_templates_assigns_variables
assert_template_result "bar", "{% include 'assignments' %}{{ foo }}" assert_template_result("bar", "{% include 'assignments' %}{{ foo }}")
end end
def test_nested_include_tag def test_nested_include_tag
assert_template_result "body body_detail", "{% include 'body' %}" assert_template_result("body body_detail", "{% include 'body' %}")
assert_template_result "header body body_detail footer", "{% include 'nested_template' %}" assert_template_result("header body body_detail footer", "{% include 'nested_template' %}")
end end
def test_nested_include_with_variable def test_nested_include_with_variable
assert_template_result "Product: Draft 151cm details ", assert_template_result("Product: Draft 151cm details ",
"{% include 'nested_product_template' with product %}", "product" => { "title" => 'Draft 151cm' } "{% include 'nested_product_template' with product %}", "product" => { "title" => 'Draft 151cm' })
assert_template_result "Product: Draft 151cm details Product: Element 155cm details ", assert_template_result("Product: Draft 151cm details Product: Element 155cm details ",
"{% include 'nested_product_template' for products %}", "products" => [{ "title" => 'Draft 151cm' }, { "title" => 'Element 155cm' }] "{% include 'nested_product_template' for products %}", "products" => [{ "title" => 'Draft 151cm' }, { "title" => 'Element 155cm' }])
end end
def test_recursively_included_template_does_not_produce_endless_loop def test_recursively_included_template_does_not_produce_endless_loop
@@ -149,41 +162,41 @@ class IncludeTagTest < Minitest::Test
end end
def test_dynamically_choosen_template def test_dynamically_choosen_template
assert_template_result "Test123", "{% include template %}", "template" => 'Test123' assert_template_result("Test123", "{% include template %}", "template" => 'Test123')
assert_template_result "Test321", "{% include template %}", "template" => 'Test321' assert_template_result("Test321", "{% include template %}", "template" => 'Test321')
assert_template_result "Product: Draft 151cm ", "{% include template for product %}", assert_template_result("Product: Draft 151cm ", "{% include template for product %}",
"template" => 'product', 'product' => { 'title' => 'Draft 151cm' } "template" => 'product', 'product' => { 'title' => 'Draft 151cm' })
end end
def test_include_tag_caches_second_read_of_same_partial def test_include_tag_caches_second_read_of_same_partial
file_system = CountingFileSystem.new file_system = CountingFileSystem.new
assert_equal 'from CountingFileSystemfrom CountingFileSystem', assert_equal('from CountingFileSystemfrom CountingFileSystem',
Template.parse("{% include 'pick_a_source' %}{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system }) Template.parse("{% include 'pick_a_source' %}{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system }))
assert_equal 1, file_system.count assert_equal(1, file_system.count)
end end
def test_include_tag_doesnt_cache_partials_across_renders def test_include_tag_doesnt_cache_partials_across_renders
file_system = CountingFileSystem.new file_system = CountingFileSystem.new
assert_equal 'from CountingFileSystem', assert_equal('from CountingFileSystem',
Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system }) Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system }))
assert_equal 1, file_system.count assert_equal(1, file_system.count)
assert_equal 'from CountingFileSystem', assert_equal('from CountingFileSystem',
Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system }) Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system }))
assert_equal 2, file_system.count assert_equal(2, file_system.count)
end end
def test_include_tag_within_if_statement def test_include_tag_within_if_statement
assert_template_result "foo_if_true", "{% if true %}{% include 'foo_if_true' %}{% endif %}" assert_template_result("foo_if_true", "{% if true %}{% include 'foo_if_true' %}{% endif %}")
end end
def test_custom_include_tag def test_custom_include_tag
original_tag = Liquid::Template.tags['include'] original_tag = Liquid::Template.tags['include']
Liquid::Template.tags['include'] = CustomInclude Liquid::Template.tags['include'] = CustomInclude
begin begin
assert_equal "custom_foo", assert_equal("custom_foo",
Template.parse("{% include 'custom_foo' %}").render! Template.parse("{% include 'custom_foo' %}").render!)
ensure ensure
Liquid::Template.tags['include'] = original_tag Liquid::Template.tags['include'] = original_tag
end end
@@ -193,8 +206,8 @@ class IncludeTagTest < Minitest::Test
original_tag = Liquid::Template.tags['include'] original_tag = Liquid::Template.tags['include']
Liquid::Template.tags['include'] = CustomInclude Liquid::Template.tags['include'] = CustomInclude
begin begin
assert_equal "custom_foo_if_true", assert_equal("custom_foo_if_true",
Template.parse("{% if true %}{% include 'custom_foo_if_true' %}{% endif %}").render! Template.parse("{% if true %}{% include 'custom_foo_if_true' %}{% endif %}").render!)
ensure ensure
Liquid::Template.tags['include'] = original_tag Liquid::Template.tags['include'] = original_tag
end end
@@ -205,7 +218,7 @@ class IncludeTagTest < Minitest::Test
a = Liquid::Template.parse(' {% include "nested_template" %}') a = Liquid::Template.parse(' {% include "nested_template" %}')
a.render! a.render!
assert_empty a.errors assert_empty(a.errors)
end end
def test_passing_options_to_included_templates def test_passing_options_to_included_templates
@@ -213,13 +226,13 @@ class IncludeTagTest < Minitest::Test
Template.parse("{% include template %}", error_mode: :strict).render!("template" => '{{ "X" || downcase }}') Template.parse("{% include template %}", error_mode: :strict).render!("template" => '{{ "X" || downcase }}')
end end
with_error_mode(:lax) do 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 end
assert_raises(Liquid::SyntaxError) do assert_raises(Liquid::SyntaxError) do
Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:locale]).render!("template" => '{{ "X" || downcase }}') Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:locale]).render!("template" => '{{ "X" || downcase }}')
end end
with_error_mode(:lax) do 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
end end
@@ -235,22 +248,22 @@ class IncludeTagTest < Minitest::Test
end end
def test_including_via_variable_value def test_including_via_variable_value
assert_template_result "from TestFileSystem", "{% assign page = 'pick_a_source' %}{% include page %}" assert_template_result("from TestFileSystem", "{% assign page = 'pick_a_source' %}{% include page %}")
assert_template_result "Product: Draft 151cm ", "{% assign page = 'product' %}{% include page %}", "product" => { 'title' => 'Draft 151cm' } assert_template_result("Product: Draft 151cm ", "{% assign page = 'product' %}{% include page %}", "product" => { 'title' => 'Draft 151cm' })
assert_template_result "Product: Draft 151cm ", "{% assign page = 'product' %}{% include page for foo %}", "foo" => { 'title' => 'Draft 151cm' } assert_template_result("Product: Draft 151cm ", "{% assign page = 'product' %}{% include page for foo %}", "foo" => { 'title' => 'Draft 151cm' })
end end
def test_including_with_strict_variables def test_including_with_strict_variables
template = Liquid::Template.parse("{% include 'simple' %}", error_mode: :warn) template = Liquid::Template.parse("{% include 'simple' %}", error_mode: :warn)
template.render(nil, strict_variables: true) template.render(nil, strict_variables: true)
assert_equal [], template.errors assert_equal([], template.errors)
end end
def test_break_through_include def test_break_through_include
assert_template_result "1", "{% for i in (1..3) %}{{ i }}{% break %}{{ i }}{% endfor %}" assert_template_result("1", "{% for i in (1..3) %}{{ i }}{% break %}{{ i }}{% endfor %}")
assert_template_result "1", "{% for i in (1..3) %}{{ i }}{% include 'break' %}{{ i }}{% endfor %}" assert_template_result("1", "{% for i in (1..3) %}{{ i }}{% include 'break' %}{{ i }}{% endfor %}")
end end
end # IncludeTagTest end # IncludeTagTest

View File

@@ -81,6 +81,16 @@ class LiquidTagTest < Minitest::Test
assert_match_syntax_error("syntax error (line 3): Unknown tag 'error'", "{% liquid echo ''\n \n error %}") assert_match_syntax_error("syntax error (line 3): Unknown tag 'error'", "{% liquid echo ''\n \n error %}")
end end
def test_nested_liquid_tag
assert_template_result('good', <<~LIQUID)
{%- if true %}
{%- liquid
echo "good"
%}
{%- endif -%}
LIQUID
end
def test_cannot_open_blocks_living_past_a_liquid_tag def test_cannot_open_blocks_living_past_a_liquid_tag
assert_match_syntax_error("syntax error (line 3): 'if' tag was never closed", <<~LIQUID) assert_match_syntax_error("syntax error (line 3): 'if' tag was never closed", <<~LIQUID)
{%- liquid {%- liquid
@@ -90,8 +100,8 @@ class LiquidTagTest < Minitest::Test
LIQUID LIQUID
end end
def test_quirk_can_close_blocks_created_before_a_liquid_tag def test_cannot_close_blocks_created_before_a_liquid_tag
assert_template_result("42", <<~LIQUID) assert_match_syntax_error("syntax error (line 3): 'endif' is not a valid delimiter for liquid tags. use %}", <<~LIQUID)
{%- if true -%} {%- if true -%}
42 42
{%- liquid endif -%} {%- liquid endif -%}

View File

@@ -6,23 +6,24 @@ class RawTagTest < Minitest::Test
include Liquid include Liquid
def test_tag_in_raw def test_tag_in_raw
assert_template_result '{% comment %} test {% endcomment %}', assert_template_result('{% comment %} test {% endcomment %}',
'{% raw %}{% comment %} test {% endcomment %}{% endraw %}' '{% raw %}{% comment %} test {% endcomment %}{% endraw %}')
end end
def test_output_in_raw def test_output_in_raw
assert_template_result '{{ test }}', '{% raw %}{{ test }}{% endraw %}' assert_template_result('{{ test }}', '{% raw %}{{ test }}{% endraw %}')
end end
def test_open_tag_in_raw def test_open_tag_in_raw
assert_template_result ' Foobar {% invalid ', '{% raw %} Foobar {% invalid {% endraw %}' assert_template_result(' Foobar {% invalid ', '{% raw %} Foobar {% invalid {% endraw %}')
assert_template_result ' Foobar invalid %} ', '{% raw %} Foobar invalid %} {% endraw %}' assert_template_result(' Foobar invalid %} ', '{% raw %} Foobar invalid %} {% endraw %}')
assert_template_result ' Foobar {{ invalid ', '{% raw %} Foobar {{ invalid {% endraw %}' assert_template_result(' Foobar {{ invalid ', '{% raw %} Foobar {{ invalid {% endraw %}')
assert_template_result ' Foobar invalid }} ', '{% raw %} Foobar invalid }} {% endraw %}' assert_template_result(' Foobar invalid }} ', '{% raw %} Foobar invalid }} {% endraw %}')
assert_template_result ' Foobar {% invalid {% {% endraw ', '{% raw %} Foobar {% invalid {% {% endraw {% endraw %}' assert_template_result(' Foobar {% invalid {% {% endraw ', '{% raw %} Foobar {% invalid {% {% endraw {% endraw %}')
assert_template_result ' Foobar {% {% {% ', '{% raw %} Foobar {% {% {% {% endraw %}' assert_template_result(' Foobar {% {% {% ', '{% raw %} Foobar {% {% {% {% endraw %}')
assert_template_result ' test {% raw %} {% endraw %}', '{% raw %} test {% raw %} {% {% endraw %}endraw %}' assert_template_result(' test {% raw %} {% endraw %}', '{% raw %} test {% raw %} {% {% endraw %}endraw %}')
assert_template_result ' Foobar {{ invalid 1', '{% raw %} Foobar {{ invalid {% endraw %}{{ 1 }}' assert_template_result(' Foobar {{ invalid 1', '{% raw %} Foobar {{ invalid {% endraw %}{{ 1 }}')
assert_template_result(' Foobar {% foo {% bar %}', '{% raw %} Foobar {% foo {% bar %}{% endraw %}')
end end
def test_invalid_raw def test_invalid_raw

View File

@@ -7,70 +7,44 @@ class RenderTagTest < Minitest::Test
def test_render_with_no_arguments def test_render_with_no_arguments
Liquid::Template.file_system = StubFileSystem.new('source' => 'rendered content') Liquid::Template.file_system = StubFileSystem.new('source' => 'rendered content')
assert_template_result 'rendered content', '{% render "source" %}' assert_template_result('rendered content', '{% render "source" %}')
end end
def test_render_tag_looks_for_file_system_in_registers_first def test_render_tag_looks_for_file_system_in_registers_first
file_system = StubFileSystem.new('pick_a_source' => 'from register file system') file_system = StubFileSystem.new('pick_a_source' => 'from register file system')
assert_equal 'from register file system', assert_equal('from register file system',
Template.parse('{% render "pick_a_source" %}').render!({}, registers: { file_system: file_system }) Template.parse('{% render "pick_a_source" %}').render!({}, registers: { file_system: file_system }))
end end
def test_render_passes_named_arguments_into_inner_scope def test_render_passes_named_arguments_into_inner_scope
Liquid::Template.file_system = StubFileSystem.new('product' => '{{ inner_product.title }}') Liquid::Template.file_system = StubFileSystem.new('product' => '{{ inner_product.title }}')
assert_template_result 'My Product', '{% render "product", inner_product: outer_product %}', assert_template_result('My Product', '{% render "product", inner_product: outer_product %}',
'outer_product' => { 'title' => 'My Product' } 'outer_product' => { 'title' => 'My Product' })
end end
def test_render_accepts_literals_as_arguments def test_render_accepts_literals_as_arguments
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ price }}') Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ price }}')
assert_template_result '123', '{% render "snippet", price: 123 %}' assert_template_result('123', '{% render "snippet", price: 123 %}')
end end
def test_render_accepts_multiple_named_arguments def test_render_accepts_multiple_named_arguments
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ one }} {{ two }}') Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ one }} {{ two }}')
assert_template_result '1 2', '{% render "snippet", one: 1, two: 2 %}' assert_template_result('1 2', '{% render "snippet", one: 1, two: 2 %}')
end end
def test_render_does_not_inherit_parent_scope_variables def test_render_does_not_inherit_parent_scope_variables
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ outer_variable }}') Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ outer_variable }}')
assert_template_result '', '{% assign outer_variable = "should not be visible" %}{% render "snippet" %}' assert_template_result('', '{% assign outer_variable = "should not be visible" %}{% render "snippet" %}')
end end
def test_render_does_not_inherit_variable_with_same_name_as_snippet def test_render_does_not_inherit_variable_with_same_name_as_snippet
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ snippet }}') Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ snippet }}')
assert_template_result '', "{% assign snippet = 'should not be visible' %}{% render 'snippet' %}" assert_template_result('', "{% assign snippet = 'should not be visible' %}{% render 'snippet' %}")
end
def test_render_sets_the_correct_template_name_for_errors
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ unsafe }}')
with_taint_mode :error do
template = Liquid::Template.parse('{% render "snippet", unsafe: unsafe %}')
context = Context.new('unsafe' => (+'unsafe').tap(&:taint))
template.render(context)
assert_equal [Liquid::TaintedError], template.errors.map(&:class)
assert_equal 'snippet', template.errors.first.template_name
end
end
def test_render_sets_the_correct_template_name_for_warnings
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ unsafe }}')
with_taint_mode :warn do
template = Liquid::Template.parse('{% render "snippet", unsafe: unsafe %}')
context = Context.new('unsafe' => (+'unsafe').tap(&:taint))
template.render(context)
assert_equal [Liquid::TaintedError], context.warnings.map(&:class)
assert_equal 'snippet', context.warnings.first.template_name
end
end end
def test_render_does_not_mutate_parent_scope def test_render_does_not_mutate_parent_scope
Liquid::Template.file_system = StubFileSystem.new('snippet' => '{% assign inner = 1 %}') Liquid::Template.file_system = StubFileSystem.new('snippet' => '{% assign inner = 1 %}')
assert_template_result '', "{% render 'snippet' %}{{ inner }}" assert_template_result('', "{% render 'snippet' %}{{ inner }}")
end end
def test_nested_render_tag def test_nested_render_tag
@@ -78,74 +52,162 @@ class RenderTagTest < Minitest::Test
'one' => "one {% render 'two' %}", 'one' => "one {% render 'two' %}",
'two' => 'two' 'two' => 'two'
) )
assert_template_result 'one two', "{% render 'one' %}" assert_template_result('one two', "{% render 'one' %}")
end end
def test_recursively_rendered_template_does_not_produce_endless_loop def test_recursively_rendered_template_does_not_produce_endless_loop
Liquid::Template.file_system = StubFileSystem.new('loop' => '{% render "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! Template.parse('{% render "loop" %}').render!
end end
end end
def test_includes_and_renders_count_towards_the_same_recursion_limit def test_sub_contexts_count_towards_the_same_recursion_limit
Liquid::Template.file_system = StubFileSystem.new( Liquid::Template.file_system = StubFileSystem.new(
'loop_render' => '{% render "loop_include" %}', 'loop_render' => '{% render "loop_render" %}',
'loop_include' => '{% include "loop_render" %}'
) )
assert_raises(Liquid::StackLevelError) do
assert_raises Liquid::StackLevelError do Template.parse('{% render "loop_render" %}').render!
Template.parse('{% render "loop_include" %}').render!
end end
end end
def test_dynamically_choosen_templates_are_not_allowed def test_dynamically_choosen_templates_are_not_allowed
Liquid::Template.file_system = StubFileSystem.new('snippet' => 'should not be rendered') 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 %}") Liquid::Template.parse("{% assign name = 'snippet' %}{% render name %}")
end end
end end
def test_include_tag_caches_second_read_of_same_partial def test_include_tag_caches_second_read_of_same_partial
file_system = StubFileSystem.new('snippet' => 'echo') file_system = StubFileSystem.new('snippet' => 'echo')
assert_equal 'echoecho', assert_equal('echoecho',
Template.parse('{% render "snippet" %}{% render "snippet" %}') Template.parse('{% render "snippet" %}{% render "snippet" %}')
.render!({}, registers: { file_system: file_system }) .render!({}, registers: { file_system: file_system }))
assert_equal 1, file_system.file_read_count assert_equal(1, file_system.file_read_count)
end end
def test_render_tag_doesnt_cache_partials_across_renders def test_render_tag_doesnt_cache_partials_across_renders
file_system = StubFileSystem.new('snippet' => 'my message') file_system = StubFileSystem.new('snippet' => 'my message')
assert_equal 'my message', assert_equal('my message',
Template.parse('{% include "snippet" %}').render!({}, registers: { file_system: file_system }) Template.parse('{% include "snippet" %}').render!({}, registers: { file_system: file_system }))
assert_equal 1, file_system.file_read_count assert_equal(1, file_system.file_read_count)
assert_equal 'my message', assert_equal('my message',
Template.parse('{% include "snippet" %}').render!({}, registers: { file_system: file_system }) Template.parse('{% include "snippet" %}').render!({}, registers: { file_system: file_system }))
assert_equal 2, file_system.file_read_count assert_equal(2, file_system.file_read_count)
end end
def test_render_tag_within_if_statement def test_render_tag_within_if_statement
Liquid::Template.file_system = StubFileSystem.new('snippet' => 'my message') Liquid::Template.file_system = StubFileSystem.new('snippet' => 'my message')
assert_template_result 'my message', '{% if true %}{% render "snippet" %}{% endif %}' assert_template_result('my message', '{% if true %}{% render "snippet" %}{% endif %}')
end end
def test_break_through_render def test_break_through_render
Liquid::Template.file_system = StubFileSystem.new('break' => '{% break %}') Liquid::Template.file_system = StubFileSystem.new('break' => '{% break %}')
assert_template_result '1', '{% for i in (1..3) %}{{ i }}{% break %}{{ i }}{% endfor %}' assert_template_result('1', '{% for i in (1..3) %}{{ i }}{% break %}{{ i }}{% endfor %}')
assert_template_result '112233', '{% for i in (1..3) %}{{ i }}{% render "break" %}{{ i }}{% endfor %}' assert_template_result('112233', '{% for i in (1..3) %}{{ i }}{% render "break" %}{{ i }}{% endfor %}')
end end
def test_increment_is_isolated_between_renders def test_increment_is_isolated_between_renders
Liquid::Template.file_system = StubFileSystem.new('incr' => '{% increment %}') Liquid::Template.file_system = StubFileSystem.new('incr' => '{% increment %}')
assert_template_result '010', '{% increment %}{% increment %}{% render "incr" %}' assert_template_result('010', '{% increment %}{% increment %}{% render "incr" %}')
end end
def test_decrement_is_isolated_between_renders def test_decrement_is_isolated_between_renders
Liquid::Template.file_system = StubFileSystem.new('decr' => '{% decrement %}') Liquid::Template.file_system = StubFileSystem.new('decr' => '{% decrement %}')
assert_template_result '-1-2-1', '{% decrement %}{% decrement %}{% render "decr" %}' assert_template_result('-1-2-1', '{% decrement %}{% decrement %}{% render "decr" %}')
end
def test_includes_will_not_render_inside_render_tag
Liquid::Template.file_system = StubFileSystem.new(
'foo' => 'bar',
'test_include' => '{% include "foo" %}'
)
exc = assert_raises(Liquid::DisabledError) do
Liquid::Template.parse('{% render "test_include" %}').render!
end
assert_equal('Liquid error: include usage is not allowed in this context', exc.message)
end
def test_includes_will_not_render_inside_nested_sibling_tags
Liquid::Template.file_system = StubFileSystem.new(
'foo' => 'bar',
'nested_render_with_sibling_include' => '{% render "test_include" %}{% include "foo" %}',
'test_include' => '{% include "foo" %}'
)
output = Liquid::Template.parse('{% render "nested_render_with_sibling_include" %}').render
assert_equal('Liquid error: include usage is not allowed in this contextLiquid error: include usage is not allowed in this context', output)
end
def test_render_tag_with
Liquid::Template.file_system = StubFileSystem.new(
'product' => "Product: {{ product.title }} ",
'product_alias' => "Product: {{ product.title }} ",
)
assert_template_result("Product: Draft 151cm ",
"{% render 'product' with products[0] %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
def test_render_tag_with_alias
Liquid::Template.file_system = StubFileSystem.new(
'product' => "Product: {{ product.title }} ",
'product_alias' => "Product: {{ product.title }} ",
)
assert_template_result("Product: Draft 151cm ",
"{% render 'product_alias' with products[0] as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
def test_render_tag_for_alias
Liquid::Template.file_system = StubFileSystem.new(
'product' => "Product: {{ product.title }} ",
'product_alias' => "Product: {{ product.title }} ",
)
assert_template_result("Product: Draft 151cm Product: Element 155cm ",
"{% render 'product_alias' for products as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
def test_render_tag_for
Liquid::Template.file_system = StubFileSystem.new(
'product' => "Product: {{ product.title }} ",
'product_alias' => "Product: {{ product.title }} ",
)
assert_template_result("Product: Draft 151cm Product: Element 155cm ",
"{% render 'product' for products %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
def test_render_tag_forloop
Liquid::Template.file_system = StubFileSystem.new(
'product' => "Product: {{ product.title }} {% if forloop.first %}first{% endif %} {% if forloop.last %}last{% endif %} index:{{ forloop.index }} ",
)
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' }])
end
def test_render_tag_for_drop
Liquid::Template.file_system = StubFileSystem.new(
'loop' => "{{ value.foo }}",
)
assert_template_result("123",
"{% render 'loop' for loop as value %}", "loop" => TestEnumerable.new)
end
def test_render_tag_with_drop
Liquid::Template.file_system = StubFileSystem.new(
'loop' => "{{ value }}",
)
assert_template_result("TestEnumerable",
"{% render 'loop' with loop as value %}", "loop" => TestEnumerable.new)
end end
end end

View File

@@ -174,13 +174,13 @@ class StandardTagTest < Minitest::Test
def test_assign_from_case def test_assign_from_case
# Example from the shopify forums # Example from the shopify forums
code = "{% case collection.handle %}{% when 'menswear-jackets' %}{% assign ptitle = 'menswear' %}{% when 'menswear-t-shirts' %}{% assign ptitle = 'menswear' %}{% else %}{% assign ptitle = 'womenswear' %}{% endcase %}{{ ptitle }}" code = "{% case collection.handle %}{% when 'menswear-jackets' %}{% assign ptitle = 'menswear' %}{% when 'menswear-t-shirts' %}{% assign ptitle = 'menswear' %}{% else %}{% assign ptitle = 'womenswear' %}{% endcase %}{{ ptitle }}"
template = Liquid::Template.parse(code) template = Liquid::Template.parse(code)
assert_equal "menswear", template.render!("collection" => { 'handle' => 'menswear-jackets' }) assert_equal("menswear", template.render!("collection" => { 'handle' => 'menswear-jackets' }))
assert_equal "menswear", template.render!("collection" => { 'handle' => 'menswear-t-shirts' }) assert_equal("menswear", template.render!("collection" => { 'handle' => 'menswear-t-shirts' }))
assert_equal "womenswear", template.render!("collection" => { 'handle' => 'x' }) assert_equal("womenswear", template.render!("collection" => { 'handle' => 'x' }))
assert_equal "womenswear", template.render!("collection" => { 'handle' => 'y' }) assert_equal("womenswear", template.render!("collection" => { 'handle' => 'y' }))
assert_equal "womenswear", template.render!("collection" => { 'handle' => 'z' }) assert_equal("womenswear", template.render!("collection" => { 'handle' => 'z' }))
end end
def test_case_when_or def test_case_when_or
@@ -213,8 +213,13 @@ class StandardTagTest < Minitest::Test
assert_template_result('', code, 'condition' => 'something else') assert_template_result('', code, 'condition' => 'something else')
end end
def test_case_when_comma_and_blank_body
code = '{% case condition %}{% when 1, 2 %} {% assign r = "result" %} {% endcase %}{{ r }}'
assert_template_result('result', code, 'condition' => 2)
end
def test_assign def test_assign
assert_template_result 'variable', '{% assign a = "variable"%}{{a}}' assert_template_result('variable', '{% assign a = "variable"%}{{a}}')
end end
def test_assign_unassigned def test_assign_unassigned
@@ -223,11 +228,11 @@ class StandardTagTest < Minitest::Test
end end
def test_assign_an_empty_string def test_assign_an_empty_string
assert_template_result '', '{% assign a = ""%}{{a}}' assert_template_result('', '{% assign a = ""%}{{a}}')
end end
def test_assign_is_global def test_assign_is_global
assert_template_result 'variable', '{%for i in (1..2) %}{% assign a = "variable"%}{% endfor %}{{a}}' assert_template_result('variable', '{%for i in (1..2) %}{% assign a = "variable"%}{% endfor %}{{a}}')
end end
def test_case_detects_bad_syntax def test_case_detects_bad_syntax
@@ -293,6 +298,6 @@ class StandardTagTest < Minitest::Test
end end
def test_multiline_tag def test_multiline_tag
assert_template_result '0 1 2 3', "0{%\nfor i in (1..3)\n%} {{\ni\n}}{%\nendfor\n%}" assert_template_result('0 1 2 3', "0{%\nfor i in (1..3)\n%} {{\ni\n}}{%\nendfor\n%}")
end end
end # StandardTagTest end # StandardTagTest

View File

@@ -7,75 +7,75 @@ class StatementsTest < Minitest::Test
def test_true_eql_true def test_true_eql_true
text = ' {% if true == true %} true {% else %} false {% endif %} ' text = ' {% if true == true %} true {% else %} false {% endif %} '
assert_template_result ' true ', text assert_template_result(' true ', text)
end end
def test_true_not_eql_true def test_true_not_eql_true
text = ' {% if true != true %} true {% else %} false {% endif %} ' text = ' {% if true != true %} true {% else %} false {% endif %} '
assert_template_result ' false ', text assert_template_result(' false ', text)
end end
def test_true_lq_true def test_true_lq_true
text = ' {% if 0 > 0 %} true {% else %} false {% endif %} ' text = ' {% if 0 > 0 %} true {% else %} false {% endif %} '
assert_template_result ' false ', text assert_template_result(' false ', text)
end end
def test_one_lq_zero def test_one_lq_zero
text = ' {% if 1 > 0 %} true {% else %} false {% endif %} ' text = ' {% if 1 > 0 %} true {% else %} false {% endif %} '
assert_template_result ' true ', text assert_template_result(' true ', text)
end end
def test_zero_lq_one def test_zero_lq_one
text = ' {% if 0 < 1 %} true {% else %} false {% endif %} ' text = ' {% if 0 < 1 %} true {% else %} false {% endif %} '
assert_template_result ' true ', text assert_template_result(' true ', text)
end end
def test_zero_lq_or_equal_one def test_zero_lq_or_equal_one
text = ' {% if 0 <= 0 %} true {% else %} false {% endif %} ' text = ' {% if 0 <= 0 %} true {% else %} false {% endif %} '
assert_template_result ' true ', text assert_template_result(' true ', text)
end end
def test_zero_lq_or_equal_one_involving_nil def test_zero_lq_or_equal_one_involving_nil
text = ' {% if null <= 0 %} true {% else %} false {% endif %} ' text = ' {% if null <= 0 %} true {% else %} false {% endif %} '
assert_template_result ' false ', text assert_template_result(' false ', text)
text = ' {% if 0 <= null %} true {% else %} false {% endif %} ' text = ' {% if 0 <= null %} true {% else %} false {% endif %} '
assert_template_result ' false ', text assert_template_result(' false ', text)
end end
def test_zero_lqq_or_equal_one def test_zero_lqq_or_equal_one
text = ' {% if 0 >= 0 %} true {% else %} false {% endif %} ' text = ' {% if 0 >= 0 %} true {% else %} false {% endif %} '
assert_template_result ' true ', text assert_template_result(' true ', text)
end end
def test_strings def test_strings
text = " {% if 'test' == 'test' %} true {% else %} false {% endif %} " text = " {% if 'test' == 'test' %} true {% else %} false {% endif %} "
assert_template_result ' true ', text assert_template_result(' true ', text)
end end
def test_strings_not_equal def test_strings_not_equal
text = " {% if 'test' != 'test' %} true {% else %} false {% endif %} " text = " {% if 'test' != 'test' %} true {% else %} false {% endif %} "
assert_template_result ' false ', text assert_template_result(' false ', text)
end end
def test_var_strings_equal def test_var_strings_equal
text = ' {% if var == "hello there!" %} true {% else %} false {% endif %} ' text = ' {% if var == "hello there!" %} true {% else %} false {% endif %} '
assert_template_result ' true ', text, 'var' => 'hello there!' assert_template_result(' true ', text, 'var' => 'hello there!')
end end
def test_var_strings_are_not_equal def test_var_strings_are_not_equal
text = ' {% if "hello there!" == var %} true {% else %} false {% endif %} ' text = ' {% if "hello there!" == var %} true {% else %} false {% endif %} '
assert_template_result ' true ', text, 'var' => 'hello there!' assert_template_result(' true ', text, 'var' => 'hello there!')
end end
def test_var_and_long_string_are_equal def test_var_and_long_string_are_equal
text = " {% if var == 'hello there!' %} true {% else %} false {% endif %} " text = " {% if var == 'hello there!' %} true {% else %} false {% endif %} "
assert_template_result ' true ', text, 'var' => 'hello there!' assert_template_result(' true ', text, 'var' => 'hello there!')
end end
def test_var_and_long_string_are_equal_backwards def test_var_and_long_string_are_equal_backwards
text = " {% if 'hello there!' == var %} true {% else %} false {% endif %} " text = " {% if 'hello there!' == var %} true {% else %} false {% endif %} "
assert_template_result ' true ', text, 'var' => 'hello there!' assert_template_result(' true ', text, 'var' => 'hello there!')
end end
# def test_is_nil # def test_is_nil
@@ -87,27 +87,27 @@ class StatementsTest < Minitest::Test
def test_is_collection_empty def test_is_collection_empty
text = ' {% if array == empty %} true {% else %} false {% endif %} ' text = ' {% if array == empty %} true {% else %} false {% endif %} '
assert_template_result ' true ', text, 'array' => [] assert_template_result(' true ', text, 'array' => [])
end end
def test_is_not_collection_empty def test_is_not_collection_empty
text = ' {% if array == empty %} true {% else %} false {% endif %} ' text = ' {% if array == empty %} true {% else %} false {% endif %} '
assert_template_result ' false ', text, 'array' => [1, 2, 3] assert_template_result(' false ', text, 'array' => [1, 2, 3])
end end
def test_nil def test_nil
text = ' {% if var == nil %} true {% else %} false {% endif %} ' text = ' {% if var == nil %} true {% else %} false {% endif %} '
assert_template_result ' true ', text, 'var' => nil assert_template_result(' true ', text, 'var' => nil)
text = ' {% if var == null %} true {% else %} false {% endif %} ' text = ' {% if var == null %} true {% else %} false {% endif %} '
assert_template_result ' true ', text, 'var' => nil assert_template_result(' true ', text, 'var' => nil)
end end
def test_not_nil def test_not_nil
text = ' {% if var != nil %} true {% else %} false {% endif %} ' text = ' {% if var != nil %} true {% else %} false {% endif %} '
assert_template_result ' true ', text, 'var' => 1 assert_template_result(' true ', text, 'var' => 1)
text = ' {% if var != null %} true {% else %} false {% endif %} ' text = ' {% if var != null %} true {% else %} false {% endif %} '
assert_template_result ' true ', text, 'var' => 1 assert_template_result(' true ', text, 'var' => 1)
end end
end # StatementsTest end # StatementsTest

View File

@@ -19,10 +19,10 @@ class UnlessElseTagTest < Minitest::Test
end end
def test_unless_in_loop def test_unless_in_loop
assert_template_result '23', '{% for i in choices %}{% unless i %}{{ forloop.index }}{% endunless %}{% endfor %}', 'choices' => [1, nil, false] assert_template_result('23', '{% for i in choices %}{% unless i %}{{ forloop.index }}{% endunless %}{% endfor %}', 'choices' => [1, nil, false])
end end
def test_unless_else_in_loop def test_unless_else_in_loop
assert_template_result ' TRUE 2 3 ', '{% for i in choices %}{% unless i %} {{ forloop.index }} {% else %} TRUE {% endunless %}{% endfor %}', 'choices' => [1, nil, false] assert_template_result(' TRUE 2 3 ', '{% for i in choices %}{% unless i %} {{ forloop.index }} {% else %} TRUE {% endunless %}{% endfor %}', 'choices' => [1, nil, false])
end end
end # UnlessElseTest end # UnlessElseTest

View File

@@ -40,8 +40,8 @@ class TemplateTest < Minitest::Test
def test_instance_assigns_persist_on_same_template_object_between_parses def test_instance_assigns_persist_on_same_template_object_between_parses
t = Template.new t = Template.new
assert_equal 'from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render! assert_equal('from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render!)
assert_equal 'from instance assigns', t.parse("{{ foo }}").render! assert_equal('from instance assigns', t.parse("{{ foo }}").render!)
end end
def test_warnings_is_not_exponential_time def test_warnings_is_not_exponential_time
@@ -51,249 +51,223 @@ class TemplateTest < Minitest::Test
end end
t = Template.parse(str) t = Template.parse(str)
assert_equal [], Timeout.timeout(1) { t.warnings } assert_equal([], Timeout.timeout(1) { t.warnings })
end end
def test_instance_assigns_persist_on_same_template_parsing_between_renders def test_instance_assigns_persist_on_same_template_parsing_between_renders
t = Template.new.parse("{{ foo }}{% assign foo = 'foo' %}{{ foo }}") t = Template.new.parse("{{ foo }}{% assign foo = 'foo' %}{{ foo }}")
assert_equal 'foo', t.render! assert_equal('foo', t.render!)
assert_equal 'foofoo', t.render! assert_equal('foofoo', t.render!)
end end
def test_custom_assigns_do_not_persist_on_same_template def test_custom_assigns_do_not_persist_on_same_template
t = Template.new t = Template.new
assert_equal 'from custom assigns', t.parse("{{ foo }}").render!('foo' => 'from custom assigns') assert_equal('from custom assigns', t.parse("{{ foo }}").render!('foo' => 'from custom assigns'))
assert_equal '', t.parse("{{ foo }}").render! assert_equal('', t.parse("{{ foo }}").render!)
end end
def test_custom_assigns_squash_instance_assigns def test_custom_assigns_squash_instance_assigns
t = Template.new t = Template.new
assert_equal 'from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render! assert_equal('from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render!)
assert_equal 'from custom assigns', t.parse("{{ foo }}").render!('foo' => 'from custom assigns') assert_equal('from custom assigns', t.parse("{{ foo }}").render!('foo' => 'from custom assigns'))
end end
def test_persistent_assigns_squash_instance_assigns def test_persistent_assigns_squash_instance_assigns
t = Template.new t = Template.new
assert_equal 'from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render! assert_equal('from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render!)
t.assigns['foo'] = 'from persistent assigns' t.assigns['foo'] = 'from persistent assigns'
assert_equal 'from persistent assigns', t.parse("{{ foo }}").render! assert_equal('from persistent assigns', t.parse("{{ foo }}").render!)
end end
def test_lambda_is_called_once_from_persistent_assigns_over_multiple_parses_and_renders def test_lambda_is_called_once_from_persistent_assigns_over_multiple_parses_and_renders
t = Template.new t = Template.new
t.assigns['number'] = -> { @global ||= 0; @global += 1 } t.assigns['number'] = -> {
assert_equal '1', t.parse("{{number}}").render! @global ||= 0
assert_equal '1', t.parse("{{number}}").render! @global += 1
assert_equal '1', t.render! }
assert_equal('1', t.parse("{{number}}").render!)
assert_equal('1', t.parse("{{number}}").render!)
assert_equal('1', t.render!)
@global = nil @global = nil
end end
def test_lambda_is_called_once_from_custom_assigns_over_multiple_parses_and_renders def test_lambda_is_called_once_from_custom_assigns_over_multiple_parses_and_renders
t = Template.new t = Template.new
assigns = { 'number' => -> { @global ||= 0; @global += 1 } } assigns = { 'number' => -> {
assert_equal '1', t.parse("{{number}}").render!(assigns) @global ||= 0
assert_equal '1', t.parse("{{number}}").render!(assigns) @global += 1
assert_equal '1', t.render!(assigns) } }
assert_equal('1', t.parse("{{number}}").render!(assigns))
assert_equal('1', t.parse("{{number}}").render!(assigns))
assert_equal('1', t.render!(assigns))
@global = nil @global = nil
end end
def test_resource_limits_works_with_custom_length_method def test_resource_limits_works_with_custom_length_method
t = Template.parse("{% assign foo = bar %}") t = Template.parse("{% assign foo = bar %}")
t.resource_limits.render_length_limit = 42 t.resource_limits.render_length_limit = 42
assert_equal "", t.render!("bar" => SomethingWithLength.new) assert_equal("", t.render!("bar" => SomethingWithLength.new))
end end
def test_resource_limits_render_length def test_resource_limits_render_length
t = Template.parse("0123456789") t = Template.parse("0123456789")
t.resource_limits.render_length_limit = 5 t.resource_limits.render_length_limit = 9
assert_equal "Liquid error: Memory limits exceeded", t.render assert_equal("Liquid error: Memory limits exceeded", t.render)
assert t.resource_limits.reached? assert(t.resource_limits.reached?)
t.resource_limits.render_length_limit = 10 t.resource_limits.render_length_limit = 10
assert_equal "0123456789", t.render! assert_equal("0123456789", t.render!)
refute_nil t.resource_limits.render_length
end end
def test_resource_limits_render_score def test_resource_limits_render_score
t = Template.parse("{% for a in (1..10) %} {% for a in (1..10) %} foo {% endfor %} {% endfor %}") t = Template.parse("{% for a in (1..10) %} {% for a in (1..10) %} foo {% endfor %} {% endfor %}")
t.resource_limits.render_score_limit = 50 t.resource_limits.render_score_limit = 50
assert_equal "Liquid error: Memory limits exceeded", t.render assert_equal("Liquid error: Memory limits exceeded", t.render)
assert t.resource_limits.reached? assert(t.resource_limits.reached?)
t = Template.parse("{% for a in (1..100) %} foo {% endfor %}") t = Template.parse("{% for a in (1..100) %} foo {% endfor %}")
t.resource_limits.render_score_limit = 50 t.resource_limits.render_score_limit = 50
assert_equal "Liquid error: Memory limits exceeded", t.render assert_equal("Liquid error: Memory limits exceeded", t.render)
assert t.resource_limits.reached? assert(t.resource_limits.reached?)
t.resource_limits.render_score_limit = 200 t.resource_limits.render_score_limit = 200
assert_equal (" foo " * 100), t.render! assert_equal((" foo " * 100), t.render!)
refute_nil t.resource_limits.render_score refute_nil(t.resource_limits.render_score)
end
def test_resource_limits_assign_score
t = Template.parse("{% assign foo = 42 %}{% assign bar = 23 %}")
t.resource_limits.assign_score_limit = 1
assert_equal "Liquid error: Memory limits exceeded", t.render
assert t.resource_limits.reached?
t.resource_limits.assign_score_limit = 2
assert_equal "", t.render!
refute_nil t.resource_limits.assign_score
end
def test_resource_limits_assign_score_counts_bytes_not_characters
t = Template.parse("{% assign foo = 'すごい' %}")
t.render
assert_equal 9, t.resource_limits.assign_score
t = Template.parse("{% capture foo %}すごい{% endcapture %}")
t.render
assert_equal 9, t.resource_limits.assign_score
end
def test_resource_limits_assign_score_nested
t = Template.parse("{% assign foo = 'aaaa' | reverse %}")
t.resource_limits.assign_score_limit = 3
assert_equal "Liquid error: Memory limits exceeded", t.render
assert t.resource_limits.reached?
t.resource_limits.assign_score_limit = 5
assert_equal "", t.render!
end end
def test_resource_limits_aborts_rendering_after_first_error def test_resource_limits_aborts_rendering_after_first_error
t = Template.parse("{% for a in (1..100) %} foo1 {% endfor %} bar {% for a in (1..100) %} foo2 {% endfor %}") t = Template.parse("{% for a in (1..100) %} foo1 {% endfor %} bar {% for a in (1..100) %} foo2 {% endfor %}")
t.resource_limits.render_score_limit = 50 t.resource_limits.render_score_limit = 50
assert_equal "Liquid error: Memory limits exceeded", t.render assert_equal("Liquid error: Memory limits exceeded", t.render)
assert t.resource_limits.reached? assert(t.resource_limits.reached?)
end end
def test_resource_limits_hash_in_template_gets_updated_even_if_no_limits_are_set def test_resource_limits_hash_in_template_gets_updated_even_if_no_limits_are_set
t = Template.parse("{% for a in (1..100) %} {% assign foo = 1 %} {% endfor %}") t = Template.parse("{% for a in (1..100) %}x{% assign foo = 1 %} {% endfor %}")
t.render! t.render!
assert t.resource_limits.assign_score > 0 assert(t.resource_limits.assign_score > 0)
assert t.resource_limits.render_score > 0 assert(t.resource_limits.render_score > 0)
assert t.resource_limits.render_length > 0
end end
def test_render_length_persists_between_blocks def test_render_length_persists_between_blocks
t = Template.parse("{% if true %}aaaa{% endif %}") t = Template.parse("{% if true %}aaaa{% endif %}")
t.resource_limits.render_length_limit = 7 t.resource_limits.render_length_limit = 3
assert_equal "Liquid error: Memory limits exceeded", t.render assert_equal("Liquid error: Memory limits exceeded", t.render)
t.resource_limits.render_length_limit = 8 t.resource_limits.render_length_limit = 4
assert_equal "aaaa", t.render assert_equal("aaaa", t.render)
t = Template.parse("{% if true %}aaaa{% endif %}{% if true %}bbb{% endif %}") t = Template.parse("{% if true %}aaaa{% endif %}{% if true %}bbb{% endif %}")
t.resource_limits.render_length_limit = 13 t.resource_limits.render_length_limit = 6
assert_equal "Liquid error: Memory limits exceeded", t.render assert_equal("Liquid error: Memory limits exceeded", t.render)
t.resource_limits.render_length_limit = 14 t.resource_limits.render_length_limit = 7
assert_equal "aaaabbb", t.render assert_equal("aaaabbb", t.render)
t = Template.parse("{% if true %}a{% endif %}{% if true %}b{% endif %}{% if true %}a{% endif %}{% if true %}b{% endif %}{% if true %}a{% endif %}{% if true %}b{% endif %}") t = Template.parse("{% if true %}a{% endif %}{% if true %}b{% endif %}{% if true %}a{% endif %}{% if true %}b{% endif %}{% if true %}a{% endif %}{% if true %}b{% endif %}")
t.resource_limits.render_length_limit = 5 t.resource_limits.render_length_limit = 5
assert_equal "Liquid error: Memory limits exceeded", t.render assert_equal("Liquid error: Memory limits exceeded", t.render)
t.resource_limits.render_length_limit = 11 t.resource_limits.render_length_limit = 6
assert_equal "Liquid error: Memory limits exceeded", t.render assert_equal("ababab", t.render)
t.resource_limits.render_length_limit = 12
assert_equal "ababab", t.render
end end
def test_render_length_uses_number_of_bytes_not_characters def test_render_length_uses_number_of_bytes_not_characters
t = Template.parse("{% if true %}すごい{% endif %}") t = Template.parse("{% if true %}すごい{% endif %}")
t.resource_limits.render_length_limit = 10 t.resource_limits.render_length_limit = 8
assert_equal "Liquid error: Memory limits exceeded", t.render assert_equal("Liquid error: Memory limits exceeded", t.render)
t.resource_limits.render_length_limit = 18 t.resource_limits.render_length_limit = 9
assert_equal "すごい", t.render assert_equal("すごい", t.render)
end end
def test_default_resource_limits_unaffected_by_render_with_context def test_default_resource_limits_unaffected_by_render_with_context
context = Context.new context = Context.new
t = Template.parse("{% for a in (1..100) %} {% assign foo = 1 %} {% endfor %}") t = Template.parse("{% for a in (1..100) %}x{% assign foo = 1 %} {% endfor %}")
t.render!(context) t.render!(context)
assert context.resource_limits.assign_score > 0 assert(context.resource_limits.assign_score > 0)
assert context.resource_limits.render_score > 0 assert(context.resource_limits.render_score > 0)
assert context.resource_limits.render_length > 0
end end
def test_can_use_drop_as_context def test_can_use_drop_as_context
t = Template.new t = Template.new
t.registers['lulz'] = 'haha' t.registers['lulz'] = 'haha'
drop = TemplateContextDrop.new drop = TemplateContextDrop.new
assert_equal 'fizzbuzz', t.parse('{{foo}}').render!(drop) assert_equal('fizzbuzz', t.parse('{{foo}}').render!(drop))
assert_equal 'bar', t.parse('{{bar}}').render!(drop) assert_equal('bar', t.parse('{{bar}}').render!(drop))
assert_equal 'haha', t.parse("{{baz}}").render!(drop) assert_equal('haha', t.parse("{{baz}}").render!(drop))
end end
def test_render_bang_force_rethrow_errors_on_passed_context def test_render_bang_force_rethrow_errors_on_passed_context
context = Context.new('drop' => ErroneousDrop.new) context = Context.new('drop' => ErroneousDrop.new)
t = Template.new.parse('{{ drop.bad_method }}') t = Template.new.parse('{{ drop.bad_method }}')
e = assert_raises RuntimeError do e = assert_raises(RuntimeError) do
t.render!(context) t.render!(context)
end end
assert_equal 'ruby error in drop', e.message assert_equal('ruby error in drop', e.message)
end end
def test_exception_renderer_that_returns_string def test_exception_renderer_that_returns_string
exception = nil exception = nil
handler = ->(e) { exception = e; '<!-- error -->' } handler = ->(e) {
exception = e
'<!-- error -->'
}
output = Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_renderer: handler) output = Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_renderer: handler)
assert exception.is_a?(Liquid::ZeroDivisionError) assert(exception.is_a?(Liquid::ZeroDivisionError))
assert_equal '<!-- error -->', output assert_equal('<!-- error -->', output)
end end
def test_exception_renderer_that_raises def test_exception_renderer_that_raises
exception = nil exception = nil
assert_raises(Liquid::ZeroDivisionError) do assert_raises(Liquid::ZeroDivisionError) do
Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_renderer: ->(e) { exception = e; raise }) Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_renderer: ->(e) {
exception = e
raise
})
end end
assert exception.is_a?(Liquid::ZeroDivisionError) assert(exception.is_a?(Liquid::ZeroDivisionError))
end end
def test_global_filter_option_on_render def test_global_filter_option_on_render
global_filter_proc = ->(output) { "#{output} filtered" } global_filter_proc = ->(output) { "#{output} filtered" }
rendered_template = Template.parse("{{name}}").render({ "name" => "bob" }, global_filter: global_filter_proc) rendered_template = Template.parse("{{name}}").render({ "name" => "bob" }, global_filter: global_filter_proc)
assert_equal 'bob filtered', rendered_template assert_equal('bob filtered', rendered_template)
end end
def test_global_filter_option_when_native_filters_exist def test_global_filter_option_when_native_filters_exist
global_filter_proc = ->(output) { "#{output} filtered" } global_filter_proc = ->(output) { "#{output} filtered" }
rendered_template = Template.parse("{{name | upcase}}").render({ "name" => "bob" }, global_filter: global_filter_proc) rendered_template = Template.parse("{{name | upcase}}").render({ "name" => "bob" }, global_filter: global_filter_proc)
assert_equal 'BOB filtered', rendered_template assert_equal('BOB filtered', rendered_template)
end end
def test_undefined_variables def test_undefined_variables
t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}") t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}")
result = t.render({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, strict_variables: true) result = t.render({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, strict_variables: true)
assert_equal '33 32 ', result assert_equal('33 32 ', result)
assert_equal 3, t.errors.count assert_equal(3, t.errors.count)
assert_instance_of Liquid::UndefinedVariable, t.errors[0] assert_instance_of(Liquid::UndefinedVariable, t.errors[0])
assert_equal 'Liquid error: undefined variable y', t.errors[0].message assert_equal('Liquid error: undefined variable y', t.errors[0].message)
assert_instance_of Liquid::UndefinedVariable, t.errors[1] assert_instance_of(Liquid::UndefinedVariable, t.errors[1])
assert_equal 'Liquid error: undefined variable b', t.errors[1].message assert_equal('Liquid error: undefined variable b', t.errors[1].message)
assert_instance_of Liquid::UndefinedVariable, t.errors[2] assert_instance_of(Liquid::UndefinedVariable, t.errors[2])
assert_equal 'Liquid error: undefined variable d', t.errors[2].message assert_equal('Liquid error: undefined variable d', t.errors[2].message)
end end
def test_nil_value_does_not_raise def test_nil_value_does_not_raise
Liquid::Template.error_mode = :strict t = Template.parse("some{{x}}thing", error_mode: :strict)
t = Template.parse("some{{x}}thing")
result = t.render!({ 'x' => nil }, strict_variables: true) result = t.render!({ 'x' => nil }, strict_variables: true)
assert_equal 0, t.errors.count assert_equal(0, t.errors.count)
assert_equal 'something', result assert_equal('something', result)
end end
def test_undefined_variables_raise def test_undefined_variables_raise
t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}") 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) t.render!({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, strict_variables: true)
end end
end end
@@ -303,16 +277,16 @@ class TemplateTest < Minitest::Test
t = Template.new.parse('{{ foo }} {{ woot }}') t = Template.new.parse('{{ foo }} {{ woot }}')
result = t.render(d, strict_variables: true) result = t.render(d, strict_variables: true)
assert_equal 'foo ', result assert_equal('foo ', result)
assert_equal 1, t.errors.count assert_equal(1, t.errors.count)
assert_instance_of Liquid::UndefinedDropMethod, t.errors[0] assert_instance_of(Liquid::UndefinedDropMethod, t.errors[0])
end end
def test_undefined_drop_methods_raise def test_undefined_drop_methods_raise
d = DropWithUndefinedMethod.new d = DropWithUndefinedMethod.new
t = Template.new.parse('{{ foo }} {{ woot }}') t = Template.new.parse('{{ foo }} {{ woot }}')
assert_raises UndefinedDropMethod do assert_raises(UndefinedDropMethod) do
t.render!(d, strict_variables: true) t.render!(d, strict_variables: true)
end end
end end
@@ -326,16 +300,16 @@ class TemplateTest < Minitest::Test
end end
result = t.render({ 'a' => 123, 'x' => 'foo' }, filters: [filters], strict_filters: true) result = t.render({ 'a' => 123, 'x' => 'foo' }, filters: [filters], strict_filters: true)
assert_equal '123 ', result assert_equal('123 ', result)
assert_equal 1, t.errors.count assert_equal(1, t.errors.count)
assert_instance_of Liquid::UndefinedFilter, t.errors[0] assert_instance_of(Liquid::UndefinedFilter, t.errors[0])
assert_equal 'Liquid error: undefined filter somefilter1', t.errors[0].message assert_equal('Liquid error: undefined filter somefilter1', t.errors[0].message)
end end
def test_undefined_filters_raise def test_undefined_filters_raise
t = Template.parse("{{x | somefilter1 | upcase | somefilter2}}") t = Template.parse("{{x | somefilter1 | upcase | somefilter2}}")
assert_raises UndefinedFilter do assert_raises(UndefinedFilter) do
t.render!({ 'x' => 'foo' }, strict_filters: true) t.render!({ 'x' => 'foo' }, strict_filters: true)
end end
end end
@@ -343,10 +317,10 @@ class TemplateTest < Minitest::Test
def test_using_range_literal_works_as_expected def test_using_range_literal_works_as_expected
t = Template.parse("{% assign foo = (x..y) %}{{ foo }}") t = Template.parse("{% assign foo = (x..y) %}{{ foo }}")
result = t.render('x' => 1, 'y' => 5) result = t.render('x' => 1, 'y' => 5)
assert_equal '1..5', result assert_equal('1..5', result)
t = Template.parse("{% assign nums = (x..y) %}{% for num in nums %}{{ num }}{% endfor %}") t = Template.parse("{% assign nums = (x..y) %}{% for num in nums %}{{ num }}{% endfor %}")
result = t.render('x' => 1, 'y' => 5) result = t.render('x' => 1, 'y' => 5)
assert_equal '12345', result assert_equal('12345', result)
end end
end end

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