Compare commits

..

192 Commits

Author SHA1 Message Date
Michael Go
e361a4d53c introduce to_liquid_value on variable look and conditional statements 2021-05-26 17:27:18 -03:00
Dylan Thacker-Smith
b9e0d28729 rubocop: Clarify that a config is a rubocop-shopify backport 2021-04-20 09:01:06 -07:00
Dylan Thacker-Smith
020f6b93c5 rubocop: Fix unsupported ruby version in TargetRubyVersion error 2021-04-20 09:00:20 -07:00
Dylan Thacker-Smith
cfe1637bdd Translate RangeError to Liquid::Error for truncatewords with large int (#1431) 2021-04-20 11:48:22 -04:00
Dylan Thacker-Smith
eab13a07d9 Add changelog entry for a recent fix 2021-03-29 13:43:01 -07:00
Dylan Thacker-Smith
ca96ca0fef Fix support for using a String subclass for the liquid source (#1421) 2021-03-29 16:22:05 -04:00
Marc-André Cournoyer
4e7a953e73 Merge pull request #1417 from Shopify/prep-release
Bump to 5.0.1 and add changelog for release
2021-03-24 16:55:10 -04:00
Marc-André Cournoyer
6ac2499f7f Remove internal tokenizer fix from History.md
Co-authored-by: Dylan Thacker-Smith <dylan.smith@shopify.com>
2021-03-24 16:53:45 -04:00
Marc-André Cournoyer
ff70161512 Remove internal fixes from History.md
Co-authored-by: Dylan Thacker-Smith <dylan.smith@shopify.com>
2021-03-24 16:52:57 -04:00
Marc-André Cournoyer
026157e128 Bump to 5.0.1 and add changelog for release 2021-03-24 16:19:19 -04:00
Charles-Philippe Clermont
bf64239ea6 Merge pull request #1414 from Shopify/fix/echo-parse-tree-visitor
Add ParseTreeVisitor to Echo tag
2021-03-24 09:38:48 -04:00
Charles-P. Clermont
c270a6f378 Add ParseTreeVisitor to Echo tag
This fixes theme-check#218, wherein variables used in echo tags are not
considered used by the linter. It is because our visitor doesn't see the
:variable_lookup's in the echo tag since the children array is empty.
But this array is empty because it is swallowed by the @variable.
2021-03-24 09:35:43 -04:00
Dylan Thacker-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
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
114 changed files with 2869 additions and 3141 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,10 +10,15 @@ Performance:
Enabled: true Enabled: true
AllCops: AllCops:
TargetRubyVersion: 2.4 TargetRubyVersion: 2.5
NewCops: disable
Exclude: Exclude:
- 'vendor/bundle/**/*' - 'vendor/bundle/**/*'
Naming/MethodName: Naming/MethodName:
Exclude: Exclude:
- 'example/server/liquid_servlet.rb' - 'example/server/liquid_servlet.rb'
# Backport https://github.com/Shopify/ruby-style-guide/pull/258
Layout/BeginEndAlignment:
Enabled: true

View File

@@ -1,6 +1,6 @@
# 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
@@ -14,14 +14,26 @@ Lint/InheritException:
Exclude: Exclude:
- 'lib/liquid/interrupts.rb' - 'lib/liquid/interrupts.rb'
# Offense count: 98 # Offense count: 113
# 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'
@@ -32,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'
@@ -40,9 +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'

View File

@@ -1,26 +0,0 @@
language: ruby
cache: bundler
rvm:
- 2.4
- 2.5
- &latest_ruby 2.6
- 2.7
- ruby-head
matrix:
include:
- rvm: *latest_ruby
script: bundle exec rake memory_profile:run
name: Profiling Memory Usage
allow_failures:
- rvm: ruby-head
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,53 @@
# Liquid Change Log # Liquid Change Log
## 5.0.2 (unreleased)
### Fixes
* Fix support for using a String subclass for the liquid source (#1421) [Dylan Thacker-Smith]
## 5.0.1 / 2021-03-24
### Fixes
* Add ParseTreeVisitor to Echo tag (#1414) [CP Clermont]
* Test with ruby 3.0 as the latest ruby version (#1398) [Dylan Thacker-Smith]
* Handle carriage return in newlines_to_br (#1391) [Unending]
### Performance Improvements
* Use split limit in truncatewords (#1361) [Dylan Thacker-Smith]
## 5.0.0 / 2021-01-06
### Features
* Add new `{% render %}` tag (#1122) [Samuel Doiron]
* Add support for `as` in `{% render %}` and `{% include %}` (#1181) [Mike Angell]
* Add `{% liquid %}` and `{% echo %}` tags (#1086) [Justin Li]
* Add [usage tracking](README.md#usage-tracking) [Mike Angell]
* Add `Tag.disable_tags` for disabling tags that prepend `Tag::Disableable` at render time (#1162, #1274, #1275) [Mike Angell]
* Support using a profiler for multiple renders (#1365, #1366) [Dylan Thacker-Smith]
### Fixes
* Fix catastrophic backtracking in `RANGES_REGEX` regular expression (#1357) [Dylan Thacker-Smith]
* Make sure the for tag's limit and offset are integers (#1094) [David Cornu]
* Invokable methods for enumerable reject include (#1151) [Thierry Joyal]
* Allow `default` filter to handle `false` as value (#1144) [Mike Angell]
* Fix render length resource limit so it doesn't multiply nested output (#1285) [Dylan Thacker-Smith]
* Fix duplication of text in raw tags (#1304) [Peter Zhu]
* Fix strict parsing of find variable with a name expression (#1317) [Dylan Thacker-Smith]
* Use monotonic time to measure durations in Liquid::Profiler (#1362) [Dylan Thacker-Smith]
### Breaking Changes
* Require Ruby >= 2.5 (#1131, #1310) [Mike Angell, Dylan Thacker-Smith]
* Remove support for taint checking (#1268) [Dylan Thacker-Smith]
* Split Strainer class into StrainerFactory and StrainerTemplate (#1208) [Thierry Joyal]
* Remove handling of a nil context in the Strainer class (#1218) [Thierry Joyal]
* Handle `BlockBody#blank?` at parse time (#1287) [Dylan Thacker-Smith]
* Pass the tag markup and tokenizer to `Document#unknown_tag` (#1290) [Dylan Thacker-Smith]
* And several internal changes
### Performance Improvements
* Reduce allocations (#1073, #1091, #1115, #1099, #1117, #1141, #1322, #1341) [Richard Monette, Florian Weingarten, Ashwin Maroli]
* Improve resources limits performance (#1093, #1323) [Florian Weingarten, Dylan Thacker-Smith]
## 4.0.3 / 2019-03-12 ## 4.0.3 / 2019-03-12
### Fixed ### Fixed

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'
@@ -80,8 +85,8 @@ require 'liquid/partial_cache'
require 'liquid/usage' require 'liquid/usage'
require 'liquid/register' 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
# #
Dir["#{__dir__}/liquid/tags/*.rb"].each { |f| require f } Dir["#{__dir__}/liquid/tags/*.rb"].each { |f| require f }
Dir["#{__dir__}/liquid/registers/*.rb"].each { |f| require f }

View File

@@ -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,6 +30,11 @@ 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
@@ -35,7 +44,7 @@ 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
@@ -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
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) 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,8 +128,8 @@ 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])
@@ -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,29 +227,7 @@ module Liquid
private private
def render_node(context, output, node) def render_node(context, output, node)
if node.disabled?(context) BlockBody.render_node(context, output, node)
output << node.disabled_error_message
return
end
disable_tags(context, node.disabled_tags) do
node.render_to_output_buffer(context, output)
end
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 disable_tags(context, tags, &block)
return yield if tags.empty?
context.registers[:disabled_tags].disable(tags, &block)
end
def raise_if_resource_limits_reached(context, length)
context.resource_limits.render_length += length
return unless context.resource_limits.reached?
raise MemoryError, "Memory limits exceeded"
end end
def create_variable(token, parse_context) def create_variable(token, parse_context)
@@ -184,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

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,7 +33,7 @@ module Liquid
until @ss.eos? until @ss.eos?
@ss.skip(WHITESPACE_OR_NOTHING) @ss.skip(WHITESPACE_OR_NOTHING)
break if @ss.eos? break if @ss.eos?
tok = if (t = @ss.scan(COMPARISON_OPERATOR)) tok = if (t = @ss.scan(COMPARISON_OPERATOR))
[:comparison, t] [:comparison, t]
elsif (t = @ss.scan(SINGLE_STRING_LITERAL)) elsif (t = @ss.scan(SINGLE_STRING_LITERAL))
[:string, t] [:string, t]
@@ -46,7 +46,7 @@ module Liquid
elsif (t = @ss.scan(DOTDOT)) elsif (t = @ss.scan(DOTDOT))
[:dotdot, t] [:dotdot, t]
else else
c = @ss.getch c = @ss.getch
if (s = SPECIALS[c]) if (s = SPECIALS[c])
[s, c] [s, c]
else else

View File

@@ -20,7 +20,6 @@
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:

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,9 +19,22 @@ 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
end end

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,126 +46,94 @@ module Liquid
include Enumerable include Enumerable
class Timing class Timing
attr_reader :code, :partial, :line_number, :children, :total_time, :self_time attr_reader :code, :template_name, :line_number, :children
attr_accessor :total_time
alias_method :render_time, :total_time
alias_method :partial, :template_name
def initialize(node, partial) 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
def start
@start_time = Time.now
end
def finish
@end_time = Time.now
@total_time = @end_time - @start_time
if @children.empty?
@self_time = @total_time
else
total_children_time = 0
@children.each do |child| @children.each do |child|
total_children_time += child.total_time total_children_time += child.total_time
end end
@self_time = @total_time - total_children_time @total_time - total_children_time
end end
end end
end
def render_time attr_reader :total_time
@end_time - @start_time alias_method :total_render_time, :total_time
def initialize
@root_children = []
@current_children = nil
@total_time = 0.0
end
def profile(template_name, &block)
# nested renders are done from a tag that already has a timing node
return yield if @current_children
root_children = @root_children
render_idx = root_children.length
begin
@current_children = root_children
profile_node(template_name, &block)
ensure
@current_children = nil
if (timing = root_children[render_idx])
@total_time += timing.total_time
end
end end
end end
def self.profile_node_render(node) def children
if Profiler.current_profile && node.respond_to?(:render) children = @root_children
Profiler.current_profile.start_node(node) if children.length == 1
output = yield children.first.children
Profiler.current_profile.end_node(node)
output
else else
yield children
end end
end end
def self.profile_children(template_name)
if Profiler.current_profile
Profiler.current_profile.push_partial(template_name)
output = yield
Profiler.current_profile.pop_partial
output
else
yield
end
end
def self.current_profile
Thread.current[:liquid_profiler]
end
def initialize(partial_name = "<root>")
@partial_stack = [partial_name]
@root_timing = Timing.new("", current_partial)
@timing_stack = [@root_timing]
@render_start_at = Time.now
@render_end_at = @render_start_at
end
def start
Thread.current[:liquid_profiler] = self
@render_start_at = Time.now
end
def stop
Thread.current[:liquid_profiler] = nil
@render_end_at = Time.now
end
def total_render_time
@render_end_at - @render_start_at
end
def each(&block) 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

View File

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

View File

@@ -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

@@ -5,6 +5,7 @@ require 'bigdecimal'
module Liquid module Liquid
module StandardFilters module StandardFilters
MAX_INT = (1 << 31) - 1
HTML_ESCAPE = { HTML_ESCAPE = {
'&' => '&amp;', '&' => '&amp;',
'>' => '&gt;', '>' => '&gt;',
@@ -13,7 +14,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 +42,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 +78,33 @@ 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 = begin
input.split(" ", words + 1)
rescue RangeError
raise if words + 1 < MAX_INT
# e.g. integer #{words} too big to convert to `int'
raise Liquid::ArgumentError, "integer #{words} too big for truncatewords"
end
return input if wordlist.length <= words
wordlist.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 +129,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
@@ -290,7 +304,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
@@ -471,7 +485,7 @@ module Liquid
def initialize(input, context) def initialize(input, context)
@context = context @context = context
@input = if input.is_a?(Array) @input = if input.is_a?(Array)
input.flatten input.flatten
elsif input.is_a?(Hash) elsif input.is_a?(Hash)
[input] [input]

View File

@@ -2,10 +2,10 @@
module Liquid module Liquid
class StaticRegisters class StaticRegisters
attr_reader :static, :registers attr_reader :static
def initialize(registers = {}) def initialize(registers = {})
@static = registers.is_a?(StaticRegisters) ? registers.static : registers @static = registers.is_a?(StaticRegisters) ? registers.static : registers
@registers = {} @registers = {}
end end
@@ -25,8 +25,16 @@ 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)

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,22 +13,20 @@ module Liquid
tag tag
end end
def disable_tags(*tags) def disable_tags(*tag_names)
disabled_tags.push(*tags) @disabled_tags ||= []
@disabled_tags.concat(tag_names)
prepend(Disabler)
end end
private :new private :new
def disabled_tags
@disabled_tags ||= []
end
end end
def initialize(tag_name, markup, parse_context) def initialize(tag_name, markup, parse_context)
@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)
@@ -46,14 +44,6 @@ module Liquid
'' ''
end end
def disabled?(context)
context.registers[:disabled_tags].disabled?(tag_name)
end
def disabled_error_message
"#{tag_name} #{options[:locale].t('errors.disabled.tag')}"
end
# For backwards compatibility with custom tags. In a future release, the semantics # For backwards compatibility with custom tags. In a future release, the semantics
# of the `render_to_output_buffer` method will become the default and the `render` # of the `render_to_output_buffer` method will become the default and the `render`
# method will be removed. # method will be removed.
@@ -66,8 +56,10 @@ module Liquid
false false
end end
def disabled_tags private
self.class.disabled_tags
def parse_expression(markup)
parse_context.parse_expression(markup)
end 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
@@ -44,7 +52,14 @@ module Liquid
@blocks.each do |block| @blocks.each do |block|
if block.else? if block.else?
block.attachment.render_to_output_buffer(context, output) if execute_else_block block.attachment.render_to_output_buffer(context, output) if execute_else_block
elsif block.evaluate(context) next
end
result = Liquid::Utils.to_liquid_value(
block.evaluate(context)
)
if result
execute_else_block = false execute_else_block = false
block.attachment.render_to_output_buffer(context, output) block.attachment.render_to_output_buffer(context, output)
end end
@@ -56,7 +71,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 +80,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 +92,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,9 +112,11 @@ 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)
@@ -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
@@ -43,7 +50,11 @@ module Liquid
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
@blocks.each do |block| @blocks.each do |block|
if block.evaluate(context) result = Liquid::Utils.to_liquid_value(
block.evaluate(context)
)
if result
return block.attachment.render_to_output_buffer(context, output) return block.attachment.render_to_output_buffer(context, output)
end end
end end
@@ -61,21 +72,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
@@ -103,9 +118,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,6 +16,8 @@ module Liquid
# {% include 'product' for products %} # {% include 'product' for products %}
# #
class Include < Tag class Include < Tag
prepend Tag::Disableable
SYNTAX = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o SYNTAX = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
Syntax = SYNTAX Syntax = SYNTAX
@@ -29,13 +31,13 @@ module Liquid
template_name = Regexp.last_match(1) template_name = Regexp.last_match(1)
variable_name = Regexp.last_match(3) variable_name = Regexp.last_match(3)
@alias_name = Regexp.last_match(5) @alias_name = Regexp.last_match(5)
@variable_name_expr = variable_name ? Expression.parse(variable_name) : nil @variable_name_expr = variable_name ? parse_expression(variable_name) : nil
@template_name_expr = Expression.parse(template_name) @template_name_expr = parse_expression(template_name)
@attributes = {} @attributes = {}
markup.scan(TagAttributes) do |key, value| markup.scan(TagAttributes) do |key, value|
@attributes[key] = Expression.parse(value) @attributes[key] = parse_expression(value)
end end
else else
@@ -65,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)
@@ -86,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

@@ -14,14 +14,14 @@ 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)

View File

@@ -2,7 +2,8 @@
module Liquid module Liquid
class Render < Tag class Render < Tag
SYNTAX = /(#{QuotedString}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o FOR = 'for'
SYNTAX = /(#{QuotedString}+)(\s+(with|#{FOR})\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
disable_tags "include" disable_tags "include"
@@ -14,15 +15,17 @@ 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)
variable_name = Regexp.last_match(3) with_or_for = Regexp.last_match(3)
variable_name = Regexp.last_match(4)
@alias_name = Regexp.last_match(5) @alias_name = Regexp.last_match(6)
@variable_name_expr = variable_name ? Expression.parse(variable_name) : nil @variable_name_expr = variable_name ? parse_expression(variable_name) : nil
@template_name_expr = Expression.parse(template_name) @template_name_expr = parse_expression(template_name)
@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
@@ -44,10 +47,11 @@ module Liquid
context_variable_name = @alias_name || template_name.split('/').last context_variable_name = @alias_name || template_name.split('/').last
render_partial_func = ->(var, forloop) { render_partial_func = ->(var, forloop) {
inner_context = context.new_isolated_subcontext inner_context = context.new_isolated_subcontext
inner_context.template_name = template_name inner_context.template_name = template_name
inner_context.partial = true inner_context.partial = true
inner_context['forloop'] = forloop if forloop inner_context['forloop'] = forloop if forloop
@attributes.each do |key, value| @attributes.each do |key, value|
inner_context[key] = context.evaluate(value) inner_context[key] = context.evaluate(value)
end end
@@ -57,7 +61,7 @@ module Liquid
} }
variable = @variable_name_expr ? context.evaluate(@variable_name_expr) : nil variable = @variable_name_expr ? context.evaluate(@variable_name_expr) : nil
if variable.is_a?(Array) if @for && variable.respond_to?(:each) && variable.respond_to?(:count)
forloop = Liquid::ForloopDrop.new(template_name, variable.count, nil) forloop = Liquid::ForloopDrop.new(template_name, variable.count, nil)
variable.each { |var| render_partial_func.call(var, forloop) } variable.each { |var| render_partial_func.call(var, forloop) }
else else

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

@@ -11,13 +11,20 @@ module Liquid
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
# First condition is interpreted backwards ( if not ) # First condition is interpreted backwards ( if not )
first_block = @blocks.first first_block = @blocks.first
unless first_block.evaluate(context) result = Liquid::Utils.to_liquid_value(
first_block.evaluate(context)
)
unless result
return first_block.attachment.render_to_output_buffer(context, output) return first_block.attachment.render_to_output_buffer(context, output)
end end
# After the first condition unless works just like if # After the first condition unless works just like if
@blocks[1..-1].each do |block| @blocks[1..-1].each do |block|
if block.evaluate(context) result = block.evaluate(context)
result = result.to_liquid_value if result.is_a?(Liquid::Drop)
if result
return block.attachment.render_to_output_buffer(context, output) return block.attachment.render_to_output_buffer(context, output)
end end
end end

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,84 +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 add_register(name, klass)
registers[name.to_sym] = klass
end
def registers
@registers ||= {}
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
@@ -182,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)
@@ -204,7 +172,7 @@ module Liquid
case args.last case args.last
when Hash when Hash
options = args.pop options = args.pop
output = options[:output] if options[:output] output = options[:output] if options[:output]
options[:registers]&.each do |key, register| options[:registers]&.each do |key, register|
context_register[key] = register context_register[key] = register
@@ -215,19 +183,16 @@ module Liquid
context.add_filters(args.pop) context.add_filters(args.pop)
end end
Template.registers.each do |key, register|
context_register[key] = register
end
# Retrying a render resets resource usage # Retrying a render resets resource usage
context.resource_limits.reset context.resource_limits.reset
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
@@ -246,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(context.template_name)
@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.to_s.to_str
@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
@@ -24,7 +24,7 @@ module Liquid
private private
def tokenize def tokenize
return [] if @source.to_s.empty? return [] if @source.empty?
return @source.split("\n") if @for_liquid_tag return @source.split("\n") if @for_liquid_tag

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)
@@ -81,5 +81,13 @@ module Liquid
rescue ::ArgumentError rescue ::ArgumentError
nil nil
end end
def self.to_liquid_value(obj)
# Enable "obj" to represent itself as a primitive value like integer, string, or boolean
return obj.to_liquid_value if obj.respond_to?(:to_liquid_value)
# Otherwise return the object itself
obj
end
end end
end end

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)
@@ -115,11 +117,11 @@ module Liquid
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)
@@ -142,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,12 +34,15 @@ 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|
key = context.evaluate(@lookups[i]) key = context.evaluate(@lookups[i])
# Cast "key" to its liquid value to enable it to act as a primitive value
key = Liquid::Utils.to_liquid_value(key)
# If object is a hash- or array-like object we look for the # If object is a hash- or array-like object we look for the
# presence of the key and if its available we return it # presence of the key and if its available we return it
if object.respond_to?(:[]) && if object.respond_to?(:[]) &&
@@ -47,7 +50,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.2.alpha"
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

@@ -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

@@ -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

@@ -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,8 +10,8 @@ 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
@@ -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

@@ -11,4 +11,48 @@ class BlockTest < Minitest::Test
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 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

View File

@@ -14,8 +14,8 @@ 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
@@ -30,8 +30,8 @@ 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
@@ -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

@@ -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
@@ -114,32 +110,6 @@ class DropsTest < Minitest::Test
assert_equal(' ', tpl.render!('product' => ProductDrop.new)) assert_equal(' ', tpl.render!('product' => ProductDrop.new))
end 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
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))
@@ -197,7 +167,7 @@ class DropsTest < Minitest::Test
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
@@ -240,28 +210,28 @@ class DropsTest < Minitest::Test
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
@@ -279,7 +249,7 @@ class DropsTest < Minitest::Test
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)

View File

@@ -193,7 +193,7 @@ class ErrorHandlingTest < Minitest::Test
# Liquid should not catch Exceptions that are not subclasses of StandardError, like Interrupt and NoMemoryError # 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
@@ -226,9 +226,9 @@ class ErrorHandlingTest < Minitest::Test
end end
def test_exception_renderer_exposing_non_liquid_error def test_exception_renderer_exposing_non_liquid_error
template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true) template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true)
exceptions = [] exceptions = []
handler = ->(e) { handler = ->(e) {
exceptions << e exceptions << e
e.cause e.cause
} }
@@ -252,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 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

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

@@ -72,10 +72,10 @@ class FiltersTest < Minitest::Test
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))
@@ -86,8 +86,8 @@ class FiltersTest < Minitest::Test
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
@@ -101,8 +101,8 @@ class FiltersTest < Minitest::Test
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
@@ -141,9 +141,9 @@ class FiltersTest < Minitest::Test
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
@@ -153,6 +153,15 @@ class FiltersTest < Minitest::Test
# tap still treated as a non-existent filter # tap still treated as a non-existent filter
assert_equal("1000", Template.parse("{{var | tap}}").render!('var' => 1000)) assert_equal("1000", Template.parse("{{var | tap}}").render!('var' => 1000))
end end
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
class FiltersInTemplate < Minitest::Test class FiltersInTemplate < Minitest::Test
@@ -160,9 +169,9 @@ 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

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

@@ -61,63 +61,63 @@ class OutputTest < Minitest::Test
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]))

View File

@@ -32,7 +32,7 @@ class ParsingQuirksTest < Minitest::Test
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
@@ -72,7 +72,7 @@ class ParsingQuirksTest < Minitest::Test
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
@@ -62,6 +62,17 @@ class RenderProfilingTest < Minitest::Test
assert_equal(2, included_children[1].line_number) assert_equal(2, included_children[1].line_number)
end end
def test_profiling_render_tag
t = Template.parse("{% render 'a_template' %}", profile: true)
t.render!
render_children = t.profiler[0].children
render_children.each do |timing|
assert_equal('a_template', timing.partial)
end
assert_equal([1, 2], render_children.map(&:line_number))
end
def test_profiling_times_the_rendering_of_tokens def test_profiling_times_the_rendering_of_tokens
t = Template.parse("{% include 'a_template' %}", profile: true) t = Template.parse("{% include 'a_template' %}", profile: true)
t.render! t.render!
@@ -77,6 +88,37 @@ class RenderProfilingTest < Minitest::Test
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 end
class SleepTag < Liquid::Tag
def initialize(tag_name, markup, parse_context)
super
@duration = Float(markup)
end
def render_to_output_buffer(_context, _output)
sleep(@duration)
end
end
def test_profiling_multiple_renders
with_custom_tag('sleep', SleepTag) do
context = Liquid::Context.new
t = Liquid::Template.parse("{% sleep 0.001 %}", profile: true)
context.template_name = 'index'
t.render!(context)
context.template_name = 'layout'
first_render_time = context.profiler.total_time
t.render!(context)
profiler = context.profiler
children = profiler.children
assert_operator(first_render_time, :>=, 0.001)
assert_operator(profiler.total_time, :>=, 0.001 + first_render_time)
assert_equal(["index", "layout"], children.map(&:template_name))
assert_equal([nil, nil], children.map(&:code))
assert_equal(profiler.total_time, children.map(&:total_time).reduce(&:+))
end
end
def test_profiling_uses_include_to_mark_children def test_profiling_uses_include_to_mark_children
t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}", profile: true) t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}", profile: true)
t.render! t.render!
@@ -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
@@ -159,13 +201,13 @@ class RenderProfilingTest < Minitest::Test
t.render!("collection" => ["one", "two"]) t.render!("collection" => ["one", "two"])
leaf = t.profiler[0].children[0] leaf = t.profiler[0].children[0]
assert_operator leaf.self_time, :>, 0 assert_operator(leaf.self_time, :>, 0)
end end
def test_profiling_supports_total_time def test_profiling_supports_total_time
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_operator t.profiler[0].total_time, :>, 0 assert_operator(t.profiler[0].total_time, :>, 0)
end end
end end

View File

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

View File

@@ -16,42 +16,49 @@ 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
@@ -68,13 +75,13 @@ class SecurityTest < Minitest::Test
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

@@ -159,7 +159,7 @@ class StandardFiltersTest < Minitest::Test
assert_equal('1', @filters.url_decode(1)) assert_equal('1', @filters.url_decode(1))
assert_equal('2001-02-03', @filters.url_decode(Date.new(2001, 2, 3))) assert_equal('2001-02-03', @filters.url_decode(Date.new(2001, 2, 3)))
assert_nil(@filters.url_decode(nil)) assert_nil(@filters.url_decode(nil))
exception = assert_raises Liquid::ArgumentError do 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)
@@ -171,10 +171,17 @@ class StandardFiltersTest < Minitest::Test
assert_equal('one two three', @filters.truncatewords('one two three')) assert_equal('one two three', @filters.truncatewords('one two three'))
assert_equal( assert_equal(
'Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221;...', 'Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221;...',
@filters.truncatewords('Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221; x 16&#8221; x 10.5&#8221; high) with cover.', 15) @filters.truncatewords('Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221; x 16&#8221; x 10.5&#8221; high) with cover.', 15)
) )
assert_equal("测试测试测试测试", @filters.truncatewords('测试测试测试测试', 5)) assert_equal("测试测试测试测试", @filters.truncatewords('测试测试测试测试', 5))
assert_equal('one two1', @filters.truncatewords("one two three", 2, 1)) assert_equal('one two1', @filters.truncatewords("one two three", 2, 1))
assert_equal('one two three...', @filters.truncatewords("one two\tthree\nfour", 3))
assert_equal('one two...', @filters.truncatewords("one two three four", 2))
assert_equal('one...', @filters.truncatewords("one two three four", 0))
exception = assert_raises(Liquid::ArgumentError) do
@filters.truncatewords("one two three four", 1 << 31)
end
assert_equal("Liquid error: integer #{1 << 31} too big for truncatewords", exception.message)
end end
def test_strip_html def test_strip_html
@@ -207,7 +214,7 @@ class StandardFiltersTest < Minitest::Test
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" },
@@ -235,7 +242,7 @@ class StandardFiltersTest < Minitest::Test
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" },
@@ -286,7 +293,7 @@ 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
@@ -302,7 +309,7 @@ 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
@@ -337,7 +344,7 @@ 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
@@ -353,7 +360,7 @@ 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
@@ -389,7 +396,7 @@ class StandardFiltersTest < Minitest::Test
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
@@ -400,8 +407,8 @@ class StandardFiltersTest < Minitest::Test
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
@@ -430,7 +437,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
@@ -441,7 +448,7 @@ 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
@@ -485,8 +492,8 @@ class StandardFiltersTest < Minitest::Test
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
@@ -539,6 +546,7 @@ class StandardFiltersTest < Minitest::Test
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
@@ -587,7 +595,7 @@ class StandardFiltersTest < Minitest::Test
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))
@@ -596,7 +604,7 @@ class StandardFiltersTest < Minitest::Test
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))
@@ -607,7 +615,7 @@ class StandardFiltersTest < Minitest::Test
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))
@@ -618,7 +626,7 @@ class StandardFiltersTest < Minitest::Test
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))
@@ -628,7 +636,7 @@ class StandardFiltersTest < Minitest::Test
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))
@@ -768,6 +776,49 @@ class StandardFiltersTest < Minitest::Test
assert_nil(@filters.where([nil], "ok")) assert_nil(@filters.where([nil], "ok"))
end 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
def test_where_no_target_value def test_where_no_target_value
input = [ input = [
{ "foo" => false }, { "foo" => false },
@@ -782,7 +833,7 @@ class StandardFiltersTest < Minitest::Test
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
@@ -389,8 +389,8 @@ 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)
@@ -399,8 +399,8 @@ HERE
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)
@@ -409,8 +409,8 @@ HERE
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)
@@ -419,11 +419,11 @@ HERE
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
@@ -437,4 +437,30 @@ HERE
assert(context.registers[:for_stack].empty?) assert(context.registers[:for_stack].empty?)
end 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

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

@@ -54,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
@@ -96,12 +96,12 @@ class IncludeTagTest < Minitest::Test
def test_include_tag_with_alias def test_include_tag_with_alias
assert_template_result("Product: Draft 151cm ", assert_template_result("Product: Draft 151cm ",
"{% include 'product_alias' with products[0] as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }]) "{% include 'product_alias' with products[0] as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end end
def test_include_tag_for_alias def test_include_tag_for_alias
assert_template_result("Product: Draft 151cm Product: Element 155cm ", assert_template_result("Product: Draft 151cm Product: Element 155cm ",
"{% include 'product_alias' for products as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }]) "{% include 'product_alias' for products as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end end
def test_include_tag_with_default_name def test_include_tag_with_default_name
@@ -226,13 +226,13 @@ class IncludeTagTest < Minitest::Test
Template.parse("{% include template %}", error_mode: :strict).render!("template" => '{{ "X" || downcase }}') 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

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

@@ -23,6 +23,7 @@ class RawTagTest < Minitest::Test
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

@@ -42,32 +42,6 @@ class RenderTagTest < Minitest::Test
assert_template_result('', "{% assign snippet = 'should not be visible' %}{% render 'snippet' %}") assert_template_result('', "{% assign snippet = 'should not be visible' %}{% render 'snippet' %}")
end 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
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 }}")
@@ -84,7 +58,7 @@ class RenderTagTest < Minitest::Test
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
@@ -93,7 +67,7 @@ class RenderTagTest < Minitest::Test
Liquid::Template.file_system = StubFileSystem.new( Liquid::Template.file_system = StubFileSystem.new(
'loop_render' => '{% render "loop_render" %}', 'loop_render' => '{% render "loop_render" %}',
) )
assert_raises Liquid::StackLevelError do assert_raises(Liquid::StackLevelError) do
Template.parse('{% render "loop_render" %}').render! Template.parse('{% render "loop_render" %}').render!
end end
end end
@@ -101,7 +75,7 @@ class RenderTagTest < Minitest::Test
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
@@ -153,7 +127,10 @@ class RenderTagTest < Minitest::Test
'test_include' => '{% include "foo" %}' 'test_include' => '{% include "foo" %}'
) )
assert_template_result('include usage is not allowed in this context', '{% render "test_include" %}') 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 end
def test_includes_will_not_render_inside_nested_sibling_tags def test_includes_will_not_render_inside_nested_sibling_tags
@@ -163,7 +140,8 @@ class RenderTagTest < Minitest::Test
'test_include' => '{% include "foo" %}' 'test_include' => '{% include "foo" %}'
) )
assert_template_result('include usage is not allowed in this contextinclude usage is not allowed in this context', '{% render "nested_render_with_sibling_include" %}') 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 end
def test_render_tag_with def test_render_tag_with
@@ -173,7 +151,7 @@ class RenderTagTest < Minitest::Test
) )
assert_template_result("Product: Draft 151cm ", assert_template_result("Product: Draft 151cm ",
"{% render 'product' with products[0] %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }]) "{% render 'product' with products[0] %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end end
def test_render_tag_with_alias def test_render_tag_with_alias
@@ -183,7 +161,7 @@ class RenderTagTest < Minitest::Test
) )
assert_template_result("Product: Draft 151cm ", assert_template_result("Product: Draft 151cm ",
"{% render 'product_alias' with products[0] as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }]) "{% render 'product_alias' with products[0] as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end end
def test_render_tag_for_alias def test_render_tag_for_alias
@@ -193,7 +171,7 @@ class RenderTagTest < Minitest::Test
) )
assert_template_result("Product: Draft 151cm Product: Element 155cm ", assert_template_result("Product: Draft 151cm Product: Element 155cm ",
"{% render 'product_alias' for products as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }]) "{% render 'product_alias' for products as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end end
def test_render_tag_for def test_render_tag_for
@@ -203,7 +181,7 @@ class RenderTagTest < Minitest::Test
) )
assert_template_result("Product: Draft 151cm Product: Element 155cm ", assert_template_result("Product: Draft 151cm Product: Element 155cm ",
"{% render 'product' for products %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }]) "{% render 'product' for products %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end end
def test_render_tag_forloop def test_render_tag_forloop
@@ -212,6 +190,24 @@ class RenderTagTest < Minitest::Test
) )
assert_template_result("Product: Draft 151cm first index:1 Product: Element 155cm last index:2 ", assert_template_result("Product: Draft 151cm first index:1 Product: Element 155cm last index:2 ",
"{% render 'product' for products %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }]) "{% render 'product' for products %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }])
end
def test_render_tag_for_drop
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,7 +174,7 @@ 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' }))
@@ -213,6 +213,11 @@ 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

View File

@@ -83,7 +83,7 @@ class TemplateTest < Minitest::Test
t = Template.new t = Template.new
t.assigns['number'] = -> { t.assigns['number'] = -> {
@global ||= 0 @global ||= 0
@global += 1 @global += 1
} }
assert_equal('1', t.parse("{{number}}").render!) assert_equal('1', t.parse("{{number}}").render!)
assert_equal('1', t.parse("{{number}}").render!) assert_equal('1', t.parse("{{number}}").render!)
@@ -95,7 +95,7 @@ class TemplateTest < Minitest::Test
t = Template.new t = Template.new
assigns = { 'number' => -> { assigns = { 'number' => -> {
@global ||= 0 @global ||= 0
@global += 1 @global += 1
} } } }
assert_equal('1', t.parse("{{number}}").render!(assigns)) assert_equal('1', t.parse("{{number}}").render!(assigns))
assert_equal('1', t.parse("{{number}}").render!(assigns)) assert_equal('1', t.parse("{{number}}").render!(assigns))
@@ -111,13 +111,12 @@ class TemplateTest < Minitest::Test
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
@@ -136,38 +135,6 @@ class TemplateTest < Minitest::Test
refute_nil(t.resource_limits.render_score) refute_nil(t.resource_limits.render_score)
end 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
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
@@ -176,50 +143,46 @@ class TemplateTest < Minitest::Test
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)
t.resource_limits.render_length_limit = 12
assert_equal("ababab", t.render) 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
@@ -235,7 +198,7 @@ class TemplateTest < Minitest::Test
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)
@@ -243,7 +206,7 @@ class TemplateTest < Minitest::Test
def test_exception_renderer_that_returns_string def test_exception_renderer_that_returns_string
exception = nil exception = nil
handler = ->(e) { handler = ->(e) {
exception = e exception = e
'<!-- error -->' '<!-- error -->'
} }
@@ -267,20 +230,20 @@ class TemplateTest < Minitest::Test
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)
@@ -294,8 +257,7 @@ class TemplateTest < Minitest::Test
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)
@@ -305,7 +267,7 @@ class TemplateTest < Minitest::Test
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
@@ -324,7 +286,7 @@ class TemplateTest < Minitest::Test
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
@@ -347,7 +309,7 @@ class TemplateTest < Minitest::Test
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
@@ -361,4 +323,18 @@ class TemplateTest < Minitest::Test
result = t.render('x' => 1, 'y' => 5) result = t.render('x' => 1, 'y' => 5)
assert_equal('12345', result) assert_equal('12345', result)
end end
def test_source_string_subclass
string_subclass = Class.new(String) do
# E.g. ActiveSupport::SafeBuffer does this, so don't just rely on to_s to return a String
def to_s
self
end
end
source = string_subclass.new("{% assign x = 2 -%} x= {{- x }}")
assert_instance_of(string_subclass, source)
output = Template.parse(source).render!
assert_equal("x=2", output)
assert_instance_of(String, output)
end
end end

View File

@@ -69,7 +69,7 @@ class TrimModeTest < Minitest::Test
# Make sure the trim isn't applied to standard tags # Make sure the trim isn't applied to standard tags
def test_standard_tags def test_standard_tags
whitespace = ' ' whitespace = ' '
text = <<-END_TEMPLATE text = <<-END_TEMPLATE
<div> <div>
<p> <p>
{% if true %} {% if true %}
@@ -110,58 +110,58 @@ class TrimModeTest < Minitest::Test
# Make sure the trim isn't too agressive # Make sure the trim isn't too agressive
def test_no_trim_output def test_no_trim_output
text = '<p>{{- \'John\' -}}</p>' text = '<p>{{- \'John\' -}}</p>'
expected = '<p>John</p>' expected = '<p>John</p>'
assert_template_result(expected, text) assert_template_result(expected, text)
end end
# Make sure the trim isn't too agressive # Make sure the trim isn't too agressive
def test_no_trim_tags def test_no_trim_tags
text = '<p>{%- if true -%}yes{%- endif -%}</p>' text = '<p>{%- if true -%}yes{%- endif -%}</p>'
expected = '<p>yes</p>' expected = '<p>yes</p>'
assert_template_result(expected, text) assert_template_result(expected, text)
text = '<p>{%- if false -%}no{%- endif -%}</p>' text = '<p>{%- if false -%}no{%- endif -%}</p>'
expected = '<p></p>' expected = '<p></p>'
assert_template_result(expected, text) assert_template_result(expected, text)
end end
def test_single_line_outer_tag def test_single_line_outer_tag
text = '<p> {%- if true %} yes {% endif -%} </p>' text = '<p> {%- if true %} yes {% endif -%} </p>'
expected = '<p> yes </p>' expected = '<p> yes </p>'
assert_template_result(expected, text) assert_template_result(expected, text)
text = '<p> {%- if false %} no {% endif -%} </p>' text = '<p> {%- if false %} no {% endif -%} </p>'
expected = '<p></p>' expected = '<p></p>'
assert_template_result(expected, text) assert_template_result(expected, text)
end end
def test_single_line_inner_tag def test_single_line_inner_tag
text = '<p> {% if true -%} yes {%- endif %} </p>' text = '<p> {% if true -%} yes {%- endif %} </p>'
expected = '<p> yes </p>' expected = '<p> yes </p>'
assert_template_result(expected, text) assert_template_result(expected, text)
text = '<p> {% if false -%} no {%- endif %} </p>' text = '<p> {% if false -%} no {%- endif %} </p>'
expected = '<p> </p>' expected = '<p> </p>'
assert_template_result(expected, text) assert_template_result(expected, text)
end end
def test_single_line_post_tag def test_single_line_post_tag
text = '<p> {% if true -%} yes {% endif -%} </p>' text = '<p> {% if true -%} yes {% endif -%} </p>'
expected = '<p> yes </p>' expected = '<p> yes </p>'
assert_template_result(expected, text) assert_template_result(expected, text)
text = '<p> {% if false -%} no {% endif -%} </p>' text = '<p> {% if false -%} no {% endif -%} </p>'
expected = '<p> </p>' expected = '<p> </p>'
assert_template_result(expected, text) assert_template_result(expected, text)
end end
def test_single_line_pre_tag def test_single_line_pre_tag
text = '<p> {%- if true %} yes {%- endif %} </p>' text = '<p> {%- if true %} yes {%- endif %} </p>'
expected = '<p> yes </p>' expected = '<p> yes </p>'
assert_template_result(expected, text) assert_template_result(expected, text)
text = '<p> {%- if false %} no {%- endif %} </p>' text = '<p> {%- if false %} no {%- endif %} </p>'
expected = '<p> </p>' expected = '<p> </p>'
assert_template_result(expected, text) assert_template_result(expected, text)
end end
@@ -330,7 +330,7 @@ class TrimModeTest < Minitest::Test
assert_template_result(expected, text) assert_template_result(expected, text)
whitespace = ' ' whitespace = ' '
text = <<-END_TEMPLATE text = <<-END_TEMPLATE
<div> <div>
<p> <p>
{% if false -%} {% if false -%}
@@ -504,7 +504,7 @@ class TrimModeTest < Minitest::Test
def test_raw_output def test_raw_output
whitespace = ' ' whitespace = ' '
text = <<-END_TEMPLATE text = <<-END_TEMPLATE
<div> <div>
{% raw %} {% raw %}
{%- if true -%} {%- if true -%}
@@ -528,4 +528,36 @@ class TrimModeTest < Minitest::Test
END_EXPECTED END_EXPECTED
assert_template_result(expected, text) assert_template_result(expected, text)
end end
def test_pre_trim_blank_preceding_text
template = Liquid::Template.parse("\n{%- raw %}{% endraw %}")
assert_equal("", template.render)
template = Liquid::Template.parse("\n{%- if true %}{% endif %}")
assert_equal("", template.render)
template = Liquid::Template.parse("{{ 'B' }} \n{%- if true %}C{% endif %}")
assert_equal("BC", template.render)
end
def test_bug_compatible_pre_trim
template = Liquid::Template.parse("\n {%- raw %}{% endraw %}", bug_compatible_whitespace_trimming: true)
assert_equal("\n", template.render)
template = Liquid::Template.parse("\n {%- if true %}{% endif %}", bug_compatible_whitespace_trimming: true)
assert_equal("\n", template.render)
template = Liquid::Template.parse("{{ 'B' }} \n{%- if true %}C{% endif %}", bug_compatible_whitespace_trimming: true)
assert_equal("B C", template.render)
template = Liquid::Template.parse("B\n {%- raw %}{% endraw %}", bug_compatible_whitespace_trimming: true)
assert_equal("B", template.render)
template = Liquid::Template.parse("B\n {%- if true %}{% endif %}", bug_compatible_whitespace_trimming: true)
assert_equal("B", template.render)
end
def test_trim_blank
assert_template_result('foobar', 'foo {{-}} bar')
end
end # TrimModeTest end # TrimModeTest

View File

@@ -15,12 +15,42 @@ class VariableTest < Minitest::Test
assert_template_result('foobar', '{{ foo }}', 'foo' => ThingWithToLiquid.new) assert_template_result('foobar', '{{ foo }}', 'foo' => ThingWithToLiquid.new)
end end
def test_variable_lookup_calls_to_liquid_value
assert_template_result('1', '{{ foo }}', 'foo' => IntegerDrop.new('1'))
assert_template_result('2', '{{ list[foo] }}', 'foo' => IntegerDrop.new('1'), 'list' => [1, 2, 3])
assert_template_result('one', '{{ list[foo] }}', 'foo' => IntegerDrop.new('1'), 'list' => { 1 => 'one' })
assert_template_result('Yay', '{{ foo }}', 'foo' => BooleanDrop.new(true))
assert_template_result('YAY', '{{ foo | upcase }}', 'foo' => BooleanDrop.new(true))
end
def test_if_tag_calls_to_liquid_value
assert_template_result('one', '{% if foo == 1 %}one{% endif %}', 'foo' => IntegerDrop.new('1'))
assert_template_result('true', '{% if foo == true %}true{% endif %}', 'foo' => BooleanDrop.new(true))
assert_template_result('true', '{% if foo %}true{% endif %}', 'foo' => BooleanDrop.new(true))
assert_template_result('', '{% if foo %}true{% endif %}', 'foo' => BooleanDrop.new(false))
assert_template_result('', '{% if foo == true %}True{% endif %}', 'foo' => BooleanDrop.new(false))
end
def test_unless_tag_calls_to_liquid_value
assert_template_result('', '{% unless foo %}true{% endunless %}', 'foo' => BooleanDrop.new(true))
end
def test_case_tag_calls_to_liquid_value
assert_template_result('One', '{% case foo %}{% when 1 %}One{% endcase %}', 'foo' => IntegerDrop.new('1'))
end
def test_simple_with_whitespaces def test_simple_with_whitespaces
template = Template.parse(%( {{ test }} )) template = Template.parse(%( {{ test }} ))
assert_equal(' worked ', template.render!('test' => 'worked')) assert_equal(' worked ', template.render!('test' => 'worked'))
assert_equal(' worked wonderfully ', template.render!('test' => 'worked wonderfully')) assert_equal(' worked wonderfully ', template.render!('test' => 'worked wonderfully'))
end end
def test_expression_with_whitespace_in_square_brackets
assert_template_result('result', "{{ a[ 'b' ] }}", 'a' => { 'b' => 'result' })
assert_template_result('result', "{{ a[ [ 'b' ] ] }}", 'b' => 'c', 'a' => { 'c' => 'result' })
end
def test_ignore_unknown def test_ignore_unknown
template = Template.parse(%({{ test }})) template = Template.parse(%({{ test }}))
assert_equal('', template.render!) assert_equal('', template.render!)
@@ -37,8 +67,8 @@ class VariableTest < Minitest::Test
end end
def test_hash_scoping def test_hash_scoping
template = Template.parse(%({{ test.test }})) assert_template_result('worked', "{{ test.test }}", 'test' => { 'test' => 'worked' })
assert_equal('worked', template.render!('test' => { 'test' => 'worked' })) assert_template_result('worked', "{{ test . test }}", 'test' => { 'test' => 'worked' })
end end
def test_false_renders_as_false def test_false_renders_as_false
@@ -52,13 +82,13 @@ class VariableTest < Minitest::Test
end end
def test_preset_assigns def test_preset_assigns
template = Template.parse(%({{ test }})) template = Template.parse(%({{ test }}))
template.assigns['test'] = 'worked' template.assigns['test'] = 'worked'
assert_equal('worked', template.render!) assert_equal('worked', template.render!)
end end
def test_reuse_parsed_template def test_reuse_parsed_template
template = Template.parse(%({{ greeting }} {{ name }})) template = Template.parse(%({{ greeting }} {{ name }}))
template.assigns['greeting'] = 'Goodbye' template.assigns['greeting'] = 'Goodbye'
assert_equal('Hello Tobi', template.render!('greeting' => 'Hello', 'name' => 'Tobi')) assert_equal('Hello Tobi', template.render!('greeting' => 'Hello', 'name' => 'Tobi'))
assert_equal('Hello ', template.render!('greeting' => 'Hello', 'unknown' => 'Tobi')) assert_equal('Hello ', template.render!('greeting' => 'Hello', 'unknown' => 'Tobi'))
@@ -68,7 +98,7 @@ class VariableTest < Minitest::Test
end end
def test_assigns_not_polluted_from_template def test_assigns_not_polluted_from_template
template = Template.parse(%({{ test }}{% assign test = 'bar' %}{{ test }})) template = Template.parse(%({{ test }}{% assign test = 'bar' %}{{ test }}))
template.assigns['test'] = 'baz' template.assigns['test'] = 'baz'
assert_equal('bazbar', template.render!) assert_equal('bazbar', template.render!)
assert_equal('bazbar', template.render!) assert_equal('bazbar', template.render!)
@@ -77,8 +107,8 @@ class VariableTest < Minitest::Test
end end
def test_hash_with_default_proc def test_hash_with_default_proc
template = Template.parse(%(Hello {{ test }})) template = Template.parse(%(Hello {{ test }}))
assigns = Hash.new { |_h, k| raise "Unknown variable '#{k}'" } assigns = Hash.new { |_h, k| raise "Unknown variable '#{k}'" }
assigns['test'] = 'Tobi' assigns['test'] = 'Tobi'
assert_equal('Hello Tobi', template.render!(assigns)) assert_equal('Hello Tobi', template.render!(assigns))
assigns.delete('test') assigns.delete('test')
@@ -95,4 +125,12 @@ class VariableTest < Minitest::Test
def test_render_symbol def test_render_symbol
assert_template_result('bar', '{{ foo }}', 'foo' => :bar) assert_template_result('bar', '{{ foo }}', 'foo' => :bar)
end end
def test_dynamic_find_var
assert_template_result('bar', '{{ [key] }}', 'key' => 'foo', 'foo' => 'bar')
end
def test_raw_value_variable
assert_template_result('bar', '{{ [key] }}', 'key' => 'foo', 'foo' => 'bar')
end
end end

View File

@@ -54,28 +54,39 @@ module Minitest
assert_match(match, exception.message) assert_match(match, exception.message)
end end
def assert_usage_increment(name, times: 1)
old_method = Liquid::Usage.method(:increment)
calls = 0
begin
Liquid::Usage.singleton_class.send(:remove_method, :increment)
Liquid::Usage.define_singleton_method(:increment) do |got_name|
calls += 1 if got_name == name
old_method.call(got_name)
end
yield
ensure
Liquid::Usage.singleton_class.send(:remove_method, :increment)
Liquid::Usage.define_singleton_method(:increment, old_method)
end
assert_equal(times, calls, "Number of calls to Usage.increment with #{name.inspect}")
end
def with_global_filter(*globals) def with_global_filter(*globals)
original_global_strainer = Liquid::Strainer.class_variable_get(:@@global_strainer) original_global_filters = Liquid::StrainerFactory.instance_variable_get(:@global_filters)
Liquid::Strainer.class_variable_set(:@@global_strainer, Class.new(Liquid::Strainer) do Liquid::StrainerFactory.instance_variable_set(:@global_filters, [])
@filter_methods = Set.new globals.each do |global|
end) Liquid::StrainerFactory.add_global_filter(global)
Liquid::Strainer.class_variable_get(:@@strainer_class_cache).clear end
Liquid::StrainerFactory.send(:strainer_class_cache).clear
globals.each do |global| globals.each do |global|
Liquid::Template.register_filter(global) Liquid::Template.register_filter(global)
end end
yield yield
ensure ensure
Liquid::Strainer.class_variable_get(:@@strainer_class_cache).clear Liquid::StrainerFactory.send(:strainer_class_cache).clear
Liquid::Strainer.class_variable_set(:@@global_strainer, original_global_strainer) Liquid::StrainerFactory.instance_variable_set(:@global_filters, original_global_filters)
end
def with_taint_mode(mode)
old_mode = Liquid::Template.taint_mode
Liquid::Template.taint_mode = mode
yield
ensure
Liquid::Template.taint_mode = old_mode
end end
def with_error_mode(mode) def with_error_mode(mode)
@@ -87,10 +98,17 @@ module Minitest
end end
def with_custom_tag(tag_name, tag_class) def with_custom_tag(tag_name, tag_class)
Liquid::Template.register_tag(tag_name, tag_class) old_tag = Liquid::Template.tags[tag_name]
yield begin
ensure Liquid::Template.register_tag(tag_name, tag_class)
Liquid::Template.tags.delete(tag_name) yield
ensure
if old_tag
Liquid::Template.tags[tag_name] = old_tag
else
Liquid::Template.tags.delete(tag_name)
end
end
end end
end end
end end
@@ -101,6 +119,44 @@ class ThingWithToLiquid
end end
end end
class IntegerDrop < Liquid::Drop
def initialize(value)
super()
@value = value.to_i
end
def ==(other)
@value == other
end
def to_s
@value.to_s
end
def to_liquid_value
@value
end
end
class BooleanDrop < Liquid::Drop
def initialize(value)
super()
@value = value
end
def ==(other)
@value == other
end
def to_liquid_value
@value
end
def to_s
@value ? "Yay" : "Nay"
end
end
class ErrorDrop < Liquid::Drop class ErrorDrop < Liquid::Drop
def standard_error def standard_error
raise Liquid::StandardError, 'standard error' raise Liquid::StandardError, 'standard error'
@@ -128,7 +184,7 @@ class StubFileSystem
def initialize(values) def initialize(values)
@file_read_count = 0 @file_read_count = 0
@values = values @values = values
end end
def read_template_file(template_path) def read_template_file(template_path)
@@ -136,3 +192,16 @@ class StubFileSystem
@values.fetch(template_path) @values.fetch(template_path)
end end
end end
class StubTemplateFactory
attr_reader :count
def initialize
@count = 0
end
def for(_template_name)
@count += 1
Liquid::Template.new
end
end

View File

@@ -45,53 +45,9 @@ class BlockUnitTest < Minitest::Test
assert_equal(3, template.root.nodelist.size) assert_equal(3, template.root.nodelist.size)
end 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
private private
def block_types(nodelist) def block_types(nodelist)
nodelist.collect(&:class) nodelist.collect(&:class)
end end
end # VariableTest end

View File

@@ -75,9 +75,9 @@ class ConditionUnitTest < Minitest::Test
end end
def test_contains_works_on_arrays def test_contains_works_on_arrays
@context = Liquid::Context.new @context = Liquid::Context.new
@context['array'] = [1, 2, 3, 4, 5] @context['array'] = [1, 2, 3, 4, 5]
array_expr = VariableLookup.new("array") array_expr = VariableLookup.new("array")
assert_evaluates_false(array_expr, 'contains', 0) assert_evaluates_false(array_expr, 'contains', 0)
assert_evaluates_true(array_expr, 'contains', 1) assert_evaluates_true(array_expr, 'contains', 1)
@@ -142,7 +142,7 @@ class ConditionUnitTest < Minitest::Test
end end
def test_left_or_right_may_contain_operators def test_left_or_right_may_contain_operators
@context = Liquid::Context.new @context = Liquid::Context.new
@context['one'] = @context['another'] = "gnomeslab-and-or-liquid" @context['one'] = @context['another'] = "gnomeslab-and-or-liquid"
assert_evaluates_true(VariableLookup.new("one"), '==', VariableLookup.new("another")) assert_evaluates_true(VariableLookup.new("one"), '==', VariableLookup.new("another"))

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