Compare commits

..

595 Commits

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

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

* Revert ForceEqualSignAlignment cop

* Update method alignment

* Undo addition of whitespace to improve readability

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

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

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

* Remove duplicate code

* Default to template name

* Improve variable matching

* Extract render_partial

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

* Change to named parameter

* Remove redundant freeze

* add brackets to make intention clearer

* Use named param format from liquid

* Update syntax

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

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

* Improvements to disable tag

* Resolve disbale tag tests

* Test disable_tags register

* disabled_tags is now always avaiable

* Allow multiple tags to be disabled at once

* Move disabled check to block_body

* Code improvements

* Remove redundant nil check

* Improve disabled tag error output

* Improve disable tag API

* Code improvements

* Switch disabled? to not mutate output

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

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

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

* Update rubocop config

* Prefer string interpolation in simple cases

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

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

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

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

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

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

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

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

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

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

remove performance test

can actually remove one more if branch

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

* Fix Layout/ExtraSpacing offense

* Fix Layout/ClosingParenthesisIndentation offenses

* Fix Style/MutableConstant offense

* Fix Style/UnneededInterpolation offenses

* Fix Style/RedundantParentheses offenses

* Update TODO config for RuboCop

* Add executable bit to test/test_helper.rb

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

https://github.com/rubocop-hq/rubocop/blob/master/CHANGELOG.md#0530-2018-03-05
2018-10-19 14:52:16 -04:00
Justin Li
43f181e211 Merge pull request #1044 from Shopify/enable-cla-bot
Enable CLA bot
2018-10-19 09:34:30 -04:00
Tim Layton
7c613e87cb Enable CLA bot 2018-10-18 23:10:56 -07:00
Stephen Paul Weber (Work)
fe4034ccf9 Merge pull request #1025 from Shopify/traverse-ast
Liquid::ParseTreeVisitor
2018-10-18 09:42:56 -04:00
Stephen Paul Weber
52ee303a36 s/block.call/yield 2018-10-18 09:41:53 -04:00
Stephen Paul Weber
8217a8d86c Add test for the full array structure 2018-10-18 09:39:05 -04:00
Stephen Paul Weber
7d13d88258 s/Traversal/ParseTreeVisitor 2018-10-18 09:38:33 -04:00
Stephen Paul Weber
ff727016ef s/callback_for/add_callback_for 2018-10-18 09:37:48 -04:00
Stephen Paul Weber
c11fc656cf Colocate Traversal classes with classes they traverse
This puts all knowledge of the traversal in the same file, and removes
the need for a CASES registry.
2018-10-18 09:37:48 -04:00
Stephen Paul Weber
d789ec4175 Liquid::Traversal
This enables traversal over whole document tree.
2018-10-15 10:11:58 -04:00
Samuel Doiron
fd09f049b0 Merge pull request #1026 from Shopify/where-filter
Add `where` filter to standard filters
2018-10-11 17:45:30 -04:00
Samuel
842986a972 Add where filter to standard filters
Users of Liquid will often wish to filter an array to only those items that match a certain criteria. For example, showing "pinned" messages at the top of a list.

Example usage:

`{{ comments | where: "pinned" | first }}`

or

`{{ products | where: "category", "kitchen" }}`

* Add where filter to standard filters
* Add tests for new where functionality
2018-10-11 16:52:32 -04:00
Florian Weingarten
4661700a97 bump to v4.0.1 2018-10-09 11:13:19 +02:00
Justin Li
cd5a6dd225 Merge pull request #930 from er1/fix-sort-natural-on-nil
Fix sort and sort_natural on sorting with non-string and nil values
2018-10-04 22:32:37 -04:00
printercu
89c1ba2b0e Fix rubocop warning 2018-09-27 17:24:01 +03:00
printercu
479d8fb4a4 Single regexp for strip_html 2018-09-27 17:13:35 +03:00
Justin Li
53b8babf52 Merge pull request #1027 from Shopify/rubocop-fix
Update deprecated rubocop name
2018-09-13 17:16:36 -04:00
Justin Li
76b4920d3e Update deprecated rubocop name 2018-09-13 17:15:32 -04:00
Justin Li
8dcc319128 Merge pull request #1024 from koic/suppress_warning_bigdecimal_new
Suppress warning: `BigDecimal.new` is deprecated
2018-09-09 08:28:24 -04:00
Koichi ITO
0b36461d80 Suppress warning: BigDecimal.new is deprecated
## Summary

`BigDecimal.new` is deprecated since BigDecimal 1.3.3 for Ruby 2.5.

This PR suppresses the following warnings.

```console
% ruby -v
ruby 2.6.0dev (2018-09-06 trunk 64648) [x86_64-darwin17]
% RUBYOPT=-w bundle exec rake
(snip)
/Users/koic/src/github.com/Shopify/liquid/lib/liquid/utils.rb:49:
warning: BigDecimal.new is deprecated; use Kernel.BigDecimal method
instead.
/Users/koic/src/github.com/Shopify/liquid/lib/liquid/utils.rb:53:
warning: BigDecimal.new is deprecated; use Kernel.BigDecimal method
instead.
```

## Other Information

The following is a change of BigDecimal 1.3.3 for Ruby 2.5 related to this PR.

- 533737338d
- 16738ad0ac
2018-09-09 21:10:20 +09:00
Justin Li
70e75719de Merge pull request #1010 from Shopify/circle-ci-remove-38409b
Goodbye CircleCI 👋
2018-05-15 10:53:51 -04:00
shopify-admins
b037b19688 Removing CircleCI 1.0 [ci skip] 2018-05-15 10:35:38 -04:00
Florian Weingarten
d0f77f6cf4 Merge pull request #1006 from Benhgift/master
add installation instruction
2018-04-26 18:20:02 +01:00
Ben Gift
0be260bc97 add installation instruction 2018-04-26 08:12:47 -07:00
Dylan Thacker-Smith
5f0b64cebc Merge pull request #1005 from christopheraue/render_refactor
Refactored and optimized rendering
2018-04-19 16:44:57 -04:00
Christopher Aue
c086017bc9 refactored and optimized rendering
Measures:
1) A while loop is faster than iterating with #each.
2) Check string, variable and block tokens first. They are far more
   frequent than interrupt tokens. In their case, checking for an
   interrupt can be avoided.
3) String tokens just map to themselves and don't need the special
   treatment of BlockBody#render_node (except the resource limit
   check).

Benchmark
=========

$ bundle exec rake benchmark:run

Before
------

Run 1)
              parse:     41.630  (± 0.0%) i/s -    420.000  in  10.089309s
             render:     75.962  (± 3.9%) i/s -    763.000  in  10.066823s
     parse & render:     25.497  (± 0.0%) i/s -    256.000  in  10.040862s

Run 2)
              parse:     42.130  (± 0.0%) i/s -    424.000  in  10.064738s
             render:     77.003  (± 1.3%) i/s -    777.000  in  10.093524s
     parse & render:     25.739  (± 0.0%) i/s -    258.000  in  10.024581s

Run 3)
              parse:     41.976  (± 2.4%) i/s -    420.000  in  10.021406s
             render:     76.184  (± 1.3%) i/s -    763.000  in  10.018104s
     parse & render:     25.641  (± 0.0%) i/s -    258.000  in  10.062549s

After
-----

Run 1)
              parse:     42.283  (± 0.0%) i/s -    424.000  in  10.028306s
             render:     83.158  (± 2.4%) i/s -    832.000  in  10.009201s
     parse & render:     26.417  (± 0.0%) i/s -    266.000  in  10.069718s

Run 2)
              parse:     41.159  (± 4.9%) i/s -    412.000  in  10.031297s
             render:     81.591  (± 3.7%) i/s -    816.000  in  10.018225s
     parse & render:     25.924  (± 3.9%) i/s -    260.000  in  10.035653s

Run 3)
              parse:     42.418  (± 2.4%) i/s -    424.000  in  10.003100s
             render:     84.183  (± 2.4%) i/s -    847.000  in  10.069781s
     parse & render:     26.726  (± 0.0%) i/s -    268.000  in  10.029857s
2018-04-19 12:10:15 +02:00
Dylan Thacker-Smith
4369fe6c85 Improve the unexpected end delimiter message for block tags. (#1003) 2018-04-05 11:18:13 -04:00
Justin Li
c118e6b435 Merge pull request #992 from ashmaroli/each-without-index
Replace unnecessary Array#each_with_index with Array#each
2018-03-16 14:28:05 -04:00
Ashwin Maroli
0fbaf873d9 replace unnecessary #each_with_index with #each 2018-03-16 14:31:43 +05:30
Justin Li
5980ddbfae Merge pull request #988 from ashmaroli/regex-to-constant
Assign regexps to constants
2018-03-14 16:49:17 -04:00
Ashwin Maroli
193fc0fb7a revert to earlier regex for matching floats 2018-03-14 07:02:04 +05:30
Ashwin Maroli
e4da4d49d2 assign regex to a constant 2018-03-13 23:36:56 +05:30
Justin Li
a0bec1f873 Merge pull request #981 from nicolasleger/patch-1
[CI] Test against Ruby 2.5 version
2018-03-05 11:23:18 -05:00
Nicolas Leger
4aa3261518 [CI] Test against Ruby 2.5 version 2018-02-12 00:23:06 +01:00
Dylan Thacker-Smith
04d552fabb Gemfile: Use https rather than git protocol to fetch liquid-c 2018-02-01 07:08:19 -05:00
Dylan Thacker-Smith
5106466a2d Add a regression test for a liquid-c trim mode bug (#972) 2018-01-25 10:55:01 -05:00
Justin Li
5d6c1ed7c6 Merge pull request #963 from lostapathy/patch-1
have travis test against ruby 2.4
2017-12-15 16:53:26 -05:00
Joe Francis
a594653a0c have travis test against ruby 2.4 2017-12-15 14:27:17 -06:00
Thibaut Courouble
0c802aba17 Merge pull request #958 from Shopify/minmax
Rename min/max filters for clarity
2017-12-06 11:41:12 -05:00
Thibaut Courouble
147d7ae24d Rename min/max filters for clarity 2017-12-06 09:48:30 -05:00
Thibaut Courouble
282d42f98d Fix min/max filters 2017-12-06 08:58:05 -05:00
Justin Li
e6ba6ee87b Revert "Use replacement string for replace filters literally (#924)"
This reverts commit 27c91203ab.
2017-12-04 15:07:59 -05:00
Nithin Bekal
2ad7a37d44 Merge pull request #954 from Shopify/max-min-filters
Add max and min filters
2017-11-30 14:18:43 -05:00
Nithin Bekal
4bdaaf069f Add max/min filters 2017-11-30 13:56:37 -05:00
Justin Li
85b1e91aed Merge pull request #952 from Shopify/bump-rubocop
Bump rubocop
2017-11-22 12:44:36 -05:00
Justin Li
a7c5e247c8 Bump rubocop 2017-11-22 11:59:06 -05:00
Dylan Thacker-Smith
6c117fd7dd refactor: Reduce maximum block nesting in Liquid::BlockBody#parse (#944) 2017-10-19 10:12:40 -04:00
Maxime Bedard
7d2d90d715 Merge pull request #932 from Shopify/avoid-default-values-hash
Avoid hash with default values due to inconsistent marshaling
2017-10-17 16:02:45 -04:00
Maxime Bedard
f761d21215 Use {} notation 2017-09-20 09:48:23 -04:00
Maxime Bedard
a796c17f8b Avoid hash with default values due to inconsistent marshalling 2017-09-19 16:23:14 -04:00
Eric Chan
deb10ebc7a Sorting support for data with undefined values 2017-09-14 02:00:43 -04:00
Eric Chan
cfe1844de9 Added test coverage for sort_natural 2017-09-13 22:17:59 -04:00
Eric Chan
59950bff87 Fix sort_natural on sorting with non-string values 2017-09-13 01:37:40 -04:00
Dylan Thacker-Smith
27c91203ab Use replacement string for replace filters literally (#924) 2017-08-28 11:51:20 -04:00
Justin Li
44eaa4b9d8 Merge pull request #920 from Shopify/symbol_to_liquid
Support rendering symbols as strings
2017-08-18 12:10:53 -04:00
Pascal Betz
a979b3ec95 Do not raise when variable is defined but nil when using strict_variables 2017-08-18 12:09:57 -04:00
Justin Li
bf3e759da3 Support rendering symbols as strings 2017-08-17 23:10:57 -04:00
Rene
59162f7a0e added attr_readers for collection and variable names in for tag (#909) 2017-07-06 09:41:48 -04:00
Thierry Joyal
c582b86f16 Merge pull request #898 from Shopify/cgi-powered-standard-filters-to-handle-non-string-inputs
CGI powered standard filters to handle non string inputs
2017-05-26 18:05:42 +00:00
Thierry Joyal
e340803d12 CGI powered standard filters to handle non string inputs 2017-05-25 15:53:41 +00:00
Dylan Thacker-Smith
48a6d86ac2 Use stackprof to test to lack of object allocations (#896) 2017-05-12 09:20:51 -04:00
Dylan Thacker-Smith
3bb29d5456 Replace assert_equal nil, with a assert_nil (#895) 2017-05-11 14:05:03 -04:00
Dylan Thacker-Smith
9c72ccb82f Limit how much blocks can be nested during parsing (#894) 2017-05-11 09:37:53 -04:00
Dylan Thacker-Smith
62d4625468 Use a loop to strictly parse binary comparisons to avoid recursion (#892)
Using recursion allows a malicious template to cause a SystemStackError
2017-05-10 10:41:52 -04:00
Dylan Thacker-Smith
8928454e29 Use a loop to evaluate binary comparisions to avoid recursion (#891)
Using recursion allows a malicious template to cause a SystemStackError
2017-05-10 10:41:24 -04:00
Florian Weingarten
1370a102c9 Merge pull request #789 from evulse/contains-strict-fix
Allow variables to start with contains in strict parser
2017-03-24 09:50:31 -04:00
Mike Angell
c9bac9befe Merge branch 'master' into contains-strict-fix 2017-03-24 11:09:09 +10:00
Mike
210a0616f3 Update History to include fix 2017-03-24 10:35:56 +10:00
Lasse Skindstad Ebert
5149cde5c3 Fix include tag used with strict_variables (#829)
Fixes https://github.com/Shopify/liquid/issues/828
2017-03-22 16:00:31 -04:00
Florian Weingarten
22f2cec5de Merge pull request #864 from chenxianyu2015/fix-strainer-add_filter-method
fix  #861: duplicate inclusion condition logic error of Liquid::Strainer.add_filter method
2017-02-23 14:06:20 -05:00
chenxianyu
4318240ae0 test: modify Strainer.add_filter duplicate inclusion test case 2017-02-22 10:33:22 +08:00
chenxianyu
aa79c33dda fix: Strainer.add_filter method 2017-02-13 15:50:19 +08:00
Justin Li
b1ef28566e Merge pull request #846 from mrmanc/master
Clarifies spelling of for’s reversed flag to address #843
2017-02-10 19:26:38 -05:00
Justin Li
41bcc48222 Merge pull request #854 from jaredbeck/patch-1
Docs: Help people upgrade to 4, re: liquid_methods
2017-02-10 19:25:04 -05:00
Dylan Thacker-Smith
27d5106dc9 Merge pull request #860 from Shopify/handle-string-node-render-exc
Avoid calling line_number on String node when rescuing a render error.
2017-02-10 14:13:11 -05:00
Dylan Thacker-Smith
7334073be2 Avoid duck typing to detect whether to call render on a node. 2017-02-10 13:49:26 -05:00
Dylan Thacker-Smith
5dcefd7d77 Avoid calling line_number on String node when rescuing a render error. 2017-02-07 15:34:10 -05:00
Richard Monette
25c7b05916 Merge pull request #857 from Shopify/handle-join-on-fixnum
handle join on fixnum
2017-02-01 14:25:40 -05:00
Richard Monette
d17f86ba4d handle join on fixnum 2017-02-01 12:47:35 -05:00
Jerry Liu
384e4313ff Merge pull request #851 from Shopify/benchmark-render
Allow benchmarks to benchmark render by itself
2017-01-31 17:18:56 -05:00
Jerry Liu
23f2af8ff5 fix travis build 2017-01-31 17:04:36 -05:00
Jerry Liu
a93eac0268 Introduce new benchmarking methods to liquid to use on rubybench 2017-01-27 10:56:16 -05:00
Florian Weingarten
2cc7493cb0 Merge pull request #855 from Shopify/bundler-benchmark-group
Create a benchmark group in Gemfile
2017-01-20 16:41:11 -05:00
Jerry Liu
85463e1753 add benchmark-ips to benchmark group in Gemfile 2017-01-20 16:04:42 -05:00
Jared Beck
52ff9b0e84 Docs: Help people upgrade to 4, re: liquid_methods
The discussion in #568 helped me.

[ci skip]
2017-01-19 14:23:39 -05:00
Dylan Thacker-Smith
0c58328a40 test: Equality comparison of two hashes (#850) 2017-01-16 15:56:38 -05:00
Dylan Thacker-Smith
2bb3552033 Fix internal liquid error when comparing hash with incompatible type (#849) 2017-01-16 13:13:17 -05:00
Mark Crossfield
8b751ddf46 Removes a non ascii character from comment to appease Rubocop 2017-01-09 10:16:35 +00:00
Mark Crossfield
e5cbdb2b27 Clarifies spelling of for’s reversed flag to address #843
It should now be harder to read the docs and miss the extra letter required for reversed compared to reverse, which causes a fairly generic syntax warning when trying to reverse sort a collection in a for loop.
2017-01-08 12:44:12 +00:00
Justin Li
ffb0ace303 Update changelog for 4.0.0 2016-12-16 13:11:22 -05:00
Florian Weingarten
ad00998ef8 bump to v4 2016-12-14 11:58:42 -05:00
Dylan Thacker-Smith
869dbc7ebf feature: Allow a default exception renderer to be specified (#837)
This could be used to preserve the old default of rendering
non-Liquid::Error messages or for providing default behaviour like error
reporting which could be missed if the exception renderer needed to be
specified on each render.
2016-12-12 10:29:09 -05:00
Dylan Thacker-Smith
fae3a2de7b Add version constraint to rake to fix CI (#836) 2016-12-09 14:01:15 -05:00
Dylan Thacker-Smith
f27bd619b9 change: Render an opaque internal error by default for non-Liquid::Error (#835)
These errors may contain sensitive information, so is safer to
render a more vague message by default.

This is done by replacing non-Liquid::Error exceptions with a
Liquid::InternalError exception with the non-Liquid::Error accessible on
through the cause method. This also allows the template name and line
number to be attached to the template errors.

The exception_handler render option has been changed to exception_renderer
since now it should raise an exception to re-raise on a liquid rendering
error or return a string to be rendered where the error occurred.
2016-12-07 17:34:29 -05:00
Dylan Thacker-Smith
a9b84b7806 test: Use ruby 2.1 in Circle CI 2016-12-05 15:36:42 -05:00
Dylan Thacker-Smith
6cc2c567c5 Merge pull request #832 from Shopify/drop-ruby-2.0
Drop support for ruby 2.0
2016-12-05 13:56:15 -05:00
Dylan Thacker-Smith
812e3c51b9 test: Add ruby 2.3.3 to CI
Travis doesn't have a ruby 2.3 alias, so the latest 2.3.x version is
specified instead.
2016-12-05 13:53:02 -05:00
Dylan Thacker-Smith
9dd0801f5c Drop support for ruby 2.0
It is no longer maintained upstream
2016-12-05 13:51:49 -05:00
Dylan Thacker-Smith
b146b49f46 fix: Clear the strainer cache when a global filter is added (#826) 2016-11-24 10:32:11 -05:00
Richard Monette
86944fe7b7 Merge pull request #809 from Shopify/introduce-unhandled-liquid-exception
introduce unhandled liquid exception
2016-10-31 10:20:06 -04:00
Richard Monette
a549d289d7 introduce unhandled liquid exception
check arity
2016-10-28 09:40:44 -04:00
Richard Monette
b2feeacbce Merge pull request #812 from Shopify/allow-split-to-accept-numeric
allow split to accept numeric
2016-10-26 10:59:44 -04:00
Richard Monette
143ba39a08 allow split to accept numeric 2016-10-26 10:43:04 -04:00
Richard Monette
43e59796f6 Merge pull request #805 from Shopify/dont-explode-when-sorting-nil-property
dont explode when sorting nil property
2016-10-05 10:18:16 -04:00
Richard Monette
bb3624b799 dont explode when sorting nil property 2016-10-04 13:22:29 -04:00
Konstantin Tennhard
64fca66ef5 Merge pull request #797 from Shopify/truncatewords-resiliency
Standard filter truncate / truncatewords: force truncate_string to string
2016-09-13 10:43:55 -04:00
Florian Weingarten
e9d7486758 4.0.0.rc3 2016-09-13 06:33:20 -04:00
Philibert Dugas
2bb98c1431 Merge pull request #798 from PhilibertDugas/bugfix-#697
Fixing #697 with better exception
2016-09-12 13:53:22 -04:00
Konstantin Tennhard
95d5c24bfc Standard filter truncate: truncate_string string coercion
The argument `truncate_string` is now coerced into a string to avoid
`NoMethodError`s. This is mostly for added resiliency. It is doubtful
that someone would actually intent to use a number as truncate string,
but accidentally supplying one is entirely possible.
2016-09-12 12:13:12 -04:00
Philibert Dugas
b7ee1a2176 Fixing #697 with better exception
When including a template which is not defined, the exception raised is
*undefined method `split` for nil:NilClass*

This occurs for a scenario like the following:
`{% include nil %}`
or
`{% include undefined-var %}`

Making the code raise an argument error to allow better understanding of
the include error
2016-09-12 09:31:59 -04:00
Florian Weingarten
0eca61a977 Merge pull request #799 from kainjow/patch-1
Update liquid-c
2016-09-12 08:12:14 -04:00
Kevin Wojniak
9bfd04da2d Update liquid-c 2016-09-10 09:23:15 -07:00
Konstantin Tennhard
302185a7fc Standard filter truncatewords: force truncate_string to string
Currently, `truncatewords` raises a TypeError when the argument
`truncate_string` is an interger. This PR forces string coercion for any
value provided for this argument. Thus,

```ruby
assert_equal 'one two1', @filters.truncatewords("one two three", 2, 1)
```

holds true. Another option would be to raise a `Liquid::ArgumentError`.

What is preferred?
2016-09-09 16:50:50 -04:00
Michael Angell
6ed6e7e12f Allow :id to start with the word contains 2016-08-20 20:32:46 +10:00
Mike Angell
f41ed78378 Merge pull request #1 from Shopify/master
Pull inline with upstream
2016-08-17 21:30:08 +10:00
Florian Weingarten
50c85afc35 Merge pull request #786 from Shopify/bump-liquid-c
Bump LiquidC for whitespace changes
2016-08-11 13:38:42 -04:00
Florian Weingarten
5876dff326 Bump LiquidC for whitespace changes 2016-08-11 13:21:39 -04:00
Florian Weingarten
f25185631d Merge pull request #773 from evulse/whitespace-trim
Add whitespace control character and associated tests
2016-08-11 13:20:12 -04:00
Michael Angell
283f1bad18 Use .last instead of pop push method for updating last node in nodelist 2016-07-08 20:49:30 +10:00
Michael Angell
e1d40c7d89 Add whitespace control character and associated tests 2016-06-28 09:15:45 +10:00
Justin Li
19c6eb426a Merge pull request #769 from zacstewart/patch-1
Fix doc formatting of code examples in file_system
2016-06-15 17:10:11 -04:00
Zac Stewart
f87b06095d Fix doc formatting of code examples in file_system
These code examples are being rendered as paragraph text in the docs.
2016-06-15 15:34:14 -04:00
Gaurav Chande
b81d54e789 Merge pull request #761 from Shopify/range-to_liquid
Support Range Type
2016-06-02 16:48:30 -04:00
Gaurav Chande
00f53b16e8 Prevent Range usage in templates from blowing up 2016-06-02 16:38:44 -04:00
Gaurav Chande
e4cf55b112 Merge pull request #748 from Shopify/expose-tags
Make Template.tags loop-able
2016-04-25 11:59:37 -04:00
Gaurav Chande
5bb211d933 Ensure no tag leakage since registry is global 2016-04-25 11:50:46 -04:00
Gaurav Chande
6adc431a19 Make tag registry enumerable 2016-04-25 11:38:42 -04:00
Justin Li
23d2beed41 Merge pull request #744 from Shopify/raw-syntax-method
Make markup validation a method on Liquid::Raw
2016-04-13 17:08:02 -04:00
Drew Martin
a80ecb7678 make markup validation a method on Liquid::Raw 2016-04-13 14:52:30 -04:00
Florian Weingarten
361c695264 Merge pull request #736 from Shopify/abs-filter
Abs filter
2016-04-05 09:13:56 -04:00
Florian Weingarten
f93243cc1a abs filter 2016-04-04 09:32:31 -04:00
Florian Weingarten
1e533a52e7 Merge pull request #735 from Shopify/fix-to-number-for-negative-float-strings
Fix to_number filter for negative float strings
2016-03-31 15:52:51 -04:00
Dylan Thacker-Smith
3ea84f095f Merge pull request #734 from Shopify/concat-liquid-error
Raise a Liquid::Error when a non-array is passed into the concat filter.
2016-03-31 15:47:43 -04:00
Dylan Thacker-Smith
4239c899a4 Raise a Liquid::Error when a non-array is passed into the concat filter. 2016-03-31 15:47:06 -04:00
Florian Weingarten
1597f8859f Fix to_number filter for negative float strings 2016-03-31 09:18:55 -04:00
Florian Weingarten
b3dda384c9 Merge pull request #733 from Shopify/fix-some-ruby-warnings
Fix a bunch of Ruby warnings
2016-03-30 17:09:00 -04:00
Florian Weingarten
6828670bfe Merge pull request #732 from Shopify/v400rc2
Release v4.0.0rc2
2016-03-30 17:02:34 -04:00
Florian Weingarten
d2f16d92d6 Fix a bunch of Ruby warnings 2016-03-30 20:42:30 +00:00
Justin Li
d233acb483 Update history to reflect merge of #731 2016-03-30 16:36:57 -04:00
Florian Weingarten
8920e2a2a2 Release v4.0.0rc2 2016-03-30 20:13:21 +00:00
Justin Li
bfee507005 Merge pull request #731 from ismasan/duck_typed_maths_filters
Duck typed maths filters
2016-03-30 16:09:16 -04:00
Ismael Celis
929c89789f Test that all maths filters work with duck-typed #to_number 2016-03-30 13:35:09 -03:00
Ismael Celis
d03c4ae8e8 Allow Utils.to_number to work with anything that responds to #to_number 2016-03-30 01:57:21 -03:00
Justin Li
021bafd260 Merge pull request #725 from jeroenvisser101/performance-start-with-vs-regex
Use start_with? instead of Regex
2016-03-21 10:34:28 -04:00
Jeroen Visser
04c393ab07 Use start_with? instead of Regex
Performance is increased by doing this:

  require 'benchmark'
  require 'tempfile'

  n = 50000
  test = File.expand_path(Tempfile.new('foo'))
  Benchmark.bm(20) do |x|
    x.report("Regex:") do
      n.times { test =~ /\A#{test}/ }
    end
    x.report("String#start_with?:") do
      n.times { test =~ test.start_with?(test) }
    end
  end

Benchmark result:
                             user     system      total        real
  Regex:                 0.440000   0.010000   0.450000 (  0.447357)
  String#start_with?:    0.000000   0.000000   0.000000 (  0.006313)
2016-03-21 14:23:35 +01:00
Gaurav Chande
9a7778e52c Merge pull request #707 from Shopify/drop-without-context
@context not always present on a Drop
2016-03-01 18:07:16 -05:00
Gaurav Chande
dde00253f9 context is not always present on a Drop 2016-03-01 21:22:11 +00:00
Gaurav Chande
18d1644980 Merge pull request #705 from Shopify/register-filter-warn
Strainer#add_filter Raises on Private Override
2016-02-24 15:31:41 -05:00
Gaurav Chande
c424d47274 Add changelog entry for Strainer method override change 2016-02-24 20:23:57 +00:00
Gaurav Chande
8e6b9d503d Make Strainer also raise when registered method is overriden as protected 2016-02-24 20:23:49 +00:00
Gaurav Chande
8be38d1795 Strainer#add_filter should raise when registered method is overriden as private 2016-02-24 20:03:17 +00:00
Justin Li
3146d5c3f2 Grammatic and other fixes to CONTRIBUTING.md 2016-02-02 23:45:37 -05:00
Justin Li
0cc8b68a97 Make logic in Context#lookup_and_evaluate more understandable 2016-02-02 23:22:46 -05:00
Justin Li
5a50c12953 Update history to reflect merge of #691
[ci skip]
2016-02-02 23:14:41 -05:00
Justin Li
a6fa4c5c38 Merge pull request #691 from urbandictionary/missing_variables_and_filters
Merge pull request 691
2016-02-02 23:13:44 -05:00
Ivan Kuznetsov
dadd9b4dd2 Add strict_variables/strict_filters render options to check for undefined variables and filters 2016-02-03 10:49:33 +07:00
Justin Li
6434b8d2bb Merge pull request #696 from Shopify/no-send
Remove possibility for arbitrary sends
2016-02-02 11:01:46 -05:00
Justin Li
2d891ddd8f Merge pull request #695 from Shopify/assign-score
Take nested values into account for assign score
2016-02-01 13:14:40 -05:00
Justin Li
60b508b151 Use #each to avoid extra allocations 2016-02-01 13:01:25 -05:00
Justin Li
3891f14a1a Take nested values into account for assign score 2016-02-01 13:01:25 -05:00
Justin Li
198f0aa366 Add test for nested assign score bookkeeping 2016-02-01 13:01:23 -05:00
Justin Li
f2e6adf566 Remove arbitrary send vector 2016-02-01 11:38:40 -05:00
Justin Li
08de6ed2c5 Merge pull request #687 from pathawks/default
Performance improvement: `default` filter
2016-01-24 11:34:05 -05:00
Pat Hawks
7e322f5cf8 Performance improvement: default filter 2016-01-23 23:18:51 -08:00
Justin Li
bf86a5a069 Merge pull request #688 from Shopify/gmp
Install libgmp3-dev in travis
2016-01-23 21:46:37 -05:00
Justin Li
0141444814 Install libgmp3-dev in travis 2016-01-23 21:41:14 -05:00
Justin Li
6d30226768 Update changelog for 4.0.0rc1 2016-01-08 15:08:06 -05:00
Florian Weingarten
63e8bac1a4 meh 2016-01-08 20:00:45 +00:00
Florian Weingarten
8449849ed5 Merge pull request #682 from Shopify/4-pre-beta1
4.0.0.rc1
2016-01-08 20:59:59 +01:00
Florian Weingarten
4bc198a0db 4.0.0.rc1 2016-01-08 19:59:38 +00:00
Florian Weingarten
3921dbe919 Merge pull request #683 from Shopify/dropify-tablerowloop
Liquid::TablerowloopDrop
2016-01-08 20:41:54 +01:00
Florian Weingarten
79e2d1d8b4 Liquid::TablerowloopDrop 2016-01-08 18:46:23 +00:00
Florian Weingarten
b7c4041db8 Merge pull request #681 from Shopify/save-some-loop-allocations
Reuse 'forloop' hash to save memory allocations
2016-01-06 22:47:39 +01:00
Florian Weingarten
e113c891ec Convert forloop hash to drop 2016-01-06 21:30:32 +00:00
Guillaume Malette
a32ad449c0 Merge pull request #672 from Shopify/fix-proc-mapping
Test mapping over procs
2016-01-06 15:59:53 -05:00
Florian Weingarten
1662ba6679 Reuse 'forloop' hash to save memory allocations 2016-01-06 20:30:25 +00:00
Dylan Thacker-Smith
99b5e86f0a Merge pull request #680 from jcheatham/master
Ensure truncate is operating on a string
2015-12-23 18:41:39 -05:00
Jonathan Cheatham
b892a73463 Ensure truncate is operating on a string 2015-12-22 19:40:35 -08:00
Guillaume Malette
0b55d09cea Fix mapping over proc attributes 2015-11-20 13:04:42 -05:00
Dylan Thacker-Smith
5f8086572b Merge pull request #667 from Shopify/remove-empty-string-check
Remove nil and empty string check in invoke_drop.
2015-11-10 10:43:11 -05:00
Dylan Thacker-Smith
bdb9a4a47f Remove nil and empty string check in invoke_drop. 2015-11-09 15:03:36 -05:00
Dylan Thacker-Smith
c38eec0293 Merge pull request #665 from tanelj/escape_filter_nil_fix
Fixed issue where "nil" value for "escape" filter breaks rendering
2015-11-06 10:54:48 -05:00
Tanel Jakobsoo
8d5a907dc8 Fixed issue where "nil" value for "escape" filter breaks rendering
Closes #664
2015-11-06 16:32:02 +02:00
Florian Weingarten
74cc41ce74 Merge pull request #662 from nickpearson/keep-argument-error-backtrace
Keep original stack trace in Liquid::ArgumentError
2015-10-29 15:24:54 +01:00
Thierry Joyal
a120cc587a Merge pull request #661 from Shopify/rename-before-method-as-dynamic-method
Rename before_method as liquid_method_missing
2015-10-29 09:49:15 -04:00
Nick Pearson
c582023321 Keep original stack trace in Liquid::ArgumentError 2015-10-29 08:15:37 -05:00
Thierry Joyal
ac041c4ad1 Rename before_method as liquid_method_missing 2015-10-28 17:28:19 +00:00
Justin Li
31d7682f4e Update history to reflect merge of #658
[ci skip]
2015-10-21 12:50:12 -04:00
Justin Li
5f1acbc086 Merge pull request #658 from Shopify/url_decode-filter
Merge pull request 658
2015-10-21 12:49:14 -04:00
Justin Li
8612716129 Remove rescue in unescape filter 2015-10-21 02:01:21 -04:00
Larry Archer
e6392d1cc1 Tests for new url_decode filter 2015-10-21 01:58:22 -04:00
Larry Archer
04381418d3 Add url_decode filter to accompany url_encode 2015-10-21 01:58:22 -04:00
Justin Li
89ccdabe9a Merge pull request #655 from dijonkitchen/patch-1
Rename MIT-LICENSE to LICENSE
2015-10-14 12:08:37 -04:00
Jonathan Chen
c0fc6777b0 Rename MIT-LICENSE to LICENSE
Standard name format
2015-10-14 12:06:08 -04:00
Justin Li
cd03346239 Update history to reflect merge of #652
[ci skip]
2015-09-29 21:06:21 -04:00
Justin Li
b4f19da127 Merge pull request #652 from mcary/empty-array-sort
Merge pull request 652
2015-09-29 21:05:10 -04:00
Marcel M. Cary
4100f8d641 Fix "sort" filter on empty array to return empty array
When sorting an empty array with the "sort" filter, it returns nil
instead of [].  This confuses subsequent filters in the chain that
expect an array.  For example, when followed by the "map" filter, it
produces an array containing one nil element: [nil].

I could special-case the nil return value, but that would be more
cumbersome than making sure "sort" always returns an array.

Add a case to the "sort" method to return [] if the array is empty,
before performing any checks on ary.first that assume a non-empty array.

There is still a danger of returning nil if the first item in the array
is nil and it is non-empty, but I'm not sure how better to handle that
case.

Apply a similar fix to sort_natural, uniq, and compact filters.
2015-09-29 10:24:31 -07:00
Dylan Thacker-Smith
d8bda2c892 Merge pull request #653 from Shopify/fix-rubocop-offenses
Fix offenses from the new version of rubocop.
2015-09-25 19:48:09 -04:00
Dylan Thacker-Smith
4f81c0a658 Lock rubocop version to avoid CI failures from new releases. 2015-09-25 19:42:35 -04:00
Dylan Thacker-Smith
704937bc00 Fix offenses from the new version of rubocop. 2015-09-25 19:34:44 -04:00
Justin Li
27c6b8074a Update history to reflect merge of #610
[ci skip]
2015-08-03 20:51:41 -04:00
Justin Li
affae5ebef Merge pull request #610 from boobooninja/gf3
Merge pull request 610
2015-08-03 20:50:14 -04:00
Florian Weingarten
fc1c0d0d83 Merge pull request #632 from knu/fix_date_error
Properly rescue ::ArgumentError in the date filter
2015-07-24 10:50:52 -04:00
Akinori MUSHA
a215b70de9 Properly rescue ::ArgumentError in the date filter 2015-07-24 13:35:06 +09:00
Justin Li
1f70928f8a Update history to reflect merge of #631
[ci skip]
2015-07-23 17:07:40 -04:00
Justin Li
7713f6709d Update history for 3.0.5 2015-07-23 17:06:12 -04:00
Justin Li
239cf0e5f5 Update history for 2.6.3 2015-07-23 17:05:58 -04:00
Dylan Thacker-Smith
fa187665b3 Merge pull request #631 from Shopify/fix-tz-test-failure
Fix a timezone test failure.
2015-07-23 16:34:48 -04:00
Dylan Thacker-Smith
cd0c5e954c Fix a timezone test failure. 2015-07-23 16:19:59 -04:00
Florian Weingarten
490b457738 Merge pull request #626 from Shopify/fix_bracket_thing
Fix bracket thing
2015-07-17 17:19:06 +02:00
Florian Weingarten
4d6dec9b5a Fix chained access to multi-dimensional hash 2015-07-17 10:10:00 -04:00
Loren Hale
0b11b573d9 add global_filter
add a global filter using a proc
only add one proc and not an array
add tests to make sure the global_filter is applied after native filters
2015-07-12 16:46:43 +08:00
Justin Li
b42d35ff36 Merge pull request #620 from Shopify/accept-invalid-range-args
Add param to accept invalid input in to_integer
2015-07-09 13:24:28 -04:00
Justin Li
b4e133e26f Fix regression in range lookup 2015-07-09 13:21:46 -04:00
Justin Li
1f9bd1d809 Add param to accept invalid input in to_integer 2015-07-09 13:18:06 -04:00
Justin Li
e88be60818 Merge pull request #618 from Shopify/move-reraise-for-line-number
Move the syntax error rescue for adding error line numbers.
2015-07-09 11:42:41 -04:00
Dylan Thacker-Smith
14416b3c49 Move the syntax error rescue for adding error line numbers. 2015-07-09 11:25:05 -04:00
Dylan Thacker-Smith
bde14a650d Merge pull request #617 from Shopify/rename-options-iv
Rename options instance variable in Variable and Tag.
2015-07-08 20:50:20 -04:00
Dylan Thacker-Smith
c535af021a Rename options instance variable in Variable and Tag. 2015-07-08 19:59:44 -04:00
Dylan Thacker-Smith
9c9345869b Merge pull request #614 from Shopify/remove-token-class
Implement line numbers without the Liquid::Token class.
2015-07-08 19:48:55 -04:00
Dylan Thacker-Smith
73834a7e52 Use reject rather than dup and delete. 2015-07-08 19:27:24 -04:00
Dylan Thacker-Smith
c45310170b Use parse_context or options instead of @options. 2015-07-08 19:21:59 -04:00
Dylan Thacker-Smith
920e1df643 Rescue and re-raise syntax errors in Template#parse to add line numbers.
This can be done now that the parse context has the line number
information, so it doesn't need to be added on closer to the original
exception.  This has the advantage of not having to rescue and re-raise the
exception multiple times, and simplifies liquid-c which would otherwise
have to rescue the exception in BlockBody#parse.
2015-07-08 19:21:59 -04:00
Dylan Thacker-Smith
cebf75b8d7 Implement line numbers without the Liquid::Token class. 2015-07-08 19:21:59 -04:00
Justin Li
afda01adbb Merge pull request #616 from Shopify/handle-non-int-range-args
Handle non-int range lookup arguments
2015-07-08 17:47:27 -04:00
Justin Li
959cd6d2a2 Temporarily disable rubinius in CI
It takes much longer than the others and is currently broken
2015-07-08 17:47:05 -04:00
Justin Li
4c1b89e20e Add regression test for ranges on non-integer types 2015-07-08 17:41:18 -04:00
Justin Li
83b6dd0268 Use to_integer for range lookup arguments 2015-07-08 17:37:07 -04:00
Justin Li
6fb402e60d Move to_integer, to_date, and to_number to Liquid::Utils 2015-07-08 17:33:05 -04:00
Dylan Thacker-Smith
338287df5e Merge pull request #613 from Shopify/taint-context-warning
Add taint warnings to the context rather than the template.
2015-07-07 16:23:10 -04:00
Dylan Thacker-Smith
c4c398174b Use early returns rather than large if in Variable#taint_check 2015-07-07 15:56:03 -04:00
Dylan Thacker-Smith
80b6ac3bc7 Add taint warnings to the context rather than the template. 2015-07-07 15:53:02 -04:00
Dylan Thacker-Smith
15974d9168 Merge pull request #612 from Shopify/fix-block-body-naming
Use node to refer to objects from the nodelist rather than token.
2015-07-07 15:49:58 -04:00
Dylan Thacker-Smith
f22ab4358b Merge pull request #611 from Shopify/no-escape-rescue
Remove standard exception rescue in escape filter.
2015-07-07 15:49:43 -04:00
Justin Li
9cf0d264e1 Require RuboCop v0.32.0 or later 2015-07-06 15:58:36 -04:00
Justin Li
575e3cae7a Remove class length metric cop 2015-07-06 15:52:11 -04:00
Dylan Thacker-Smith
fad3b8275c Use node to refer to objects from the nodelist rather than token. 2015-07-04 20:57:35 -04:00
Dylan Thacker-Smith
5a071cb7f2 Remove standard exception rescue in escape filter. 2015-07-04 13:48:25 -04:00
Justin Li
8cb2364179 Merge pull request #608 from Shopify/tag-tag_name
Add Liquid::Tag#tag_name
2015-07-02 16:28:37 -04:00
Gaurav Chande
3c23cfc167 Add Liquid::Tag#tag_name 2015-07-02 20:18:09 +00:00
Justin Li
8a8de46c6a Merge pull request #603 from Shopify/format-history
Format changelog attribution to include one name only
2015-06-23 07:40:05 -07:00
Justin Li
58c7f226cc Format changelog attribution to include one name only 2015-06-19 11:45:37 -04:00
Justin Li
adfcd0ab13 Update history to reflect merge of #600
[ci skip]
2015-06-19 11:38:59 -04:00
Justin Li
30ef7d14b0 Merge pull request #600 from carsonreinke/filter-compact
Merge pull request 600
2015-06-19 11:38:14 -04:00
Florian Weingarten
4920ec50e4 update changelog 2015-06-19 07:41:39 -04:00
David Cornu
e395229283 Merge pull request #601 from Shopify/safe-to-integer
Use to_integer instead of to_i on arguments
2015-06-16 11:31:20 -04:00
David Cornu
9470fba0c8 Exclude lib/liquid/standardfilters.rb from ModuleLength 2015-06-16 15:19:06 +00:00
David Cornu
ac180e8402 Use to_integer instead of to_i on arguments 2015-06-16 15:08:29 +00:00
Carson Reinke
7c5d54aced Ignore Rubocop Metrics/ModuleLength for now 2015-06-15 15:07:25 -04:00
Carson Reinke
5fbb312a67 "Trailing whitespace detected." 2015-06-15 14:27:48 -04:00
Carson Reinke
8385099960 Added "compact" filter 2015-06-15 14:14:28 -04:00
Florian Weingarten
504b6fb3c7 Merge pull request #596 from Shopify/liquid_c_tests
Run tests with latest liquid/c gem
2015-06-08 22:52:57 +02:00
Florian Weingarten
01420e8014 fix gem platforms 2015-06-08 18:38:40 +00:00
Florian Weingarten
dde35a2907 shut up rubocop 2015-06-08 18:38:40 +00:00
Florian Weingarten
e2323332cd Run tests with latest liquid/c gem 2015-06-08 18:38:35 +00:00
Florian Weingarten
7b4398d0c4 Merge pull request #595 from Shopify/uniq_on_strings
Fix uniq filter with string input
2015-06-05 16:27:05 +02:00
Florian Weingarten
1e23036b2d Fix uniq filter with string input 2015-06-04 22:55:03 -04:00
Florian Weingarten
13716fa68b Merge pull request #594 from boobooninja/rake_console
add rake console
2015-06-05 04:21:01 +02:00
Loren Hale
232e8bb4cd add rake console
add Rake console task to load irb with liquid
2015-06-05 10:17:55 +08:00
Dylan Thacker-Smith
6968def5dd Merge pull request #574 from Shopify/template-name-in-errors
Include template name with line numbers in render errors.
2015-06-04 15:28:12 -04:00
Dylan Thacker-Smith
ad3748af21 Include template name with line numbers in render errors. 2015-06-04 13:44:01 -04:00
Florian Weingarten
c82e04f4e6 Merge pull request #593 from Shopify/fix_predicate_name
Rename 'has_key?' and 'has_interrupt?'
2015-06-04 19:40:14 +02:00
Florian Weingarten
5919626da4 Rename 'has_key?' and 'has_interrupt?' 2015-06-04 13:14:46 -04:00
Florian Weingarten
82269e2509 fix a few more rubocop offenses 2015-06-04 13:09:58 -04:00
Florian Weingarten
b347fac3c0 Merge pull request #592 from Shopify/method_literal
blank and empty as variable names
2015-06-04 19:09:48 +02:00
Florian Weingarten
e761a6864e clean up some rubocop stuff 2015-06-04 12:56:29 -04:00
Florian Weingarten
4c22cef341 blank and empty as variable names 2015-06-04 12:30:50 -04:00
Florian Weingarten
c319240174 run rubocop on CI 2015-06-04 11:57:25 -04:00
Florian Weingarten
6ace095207 Avoid parallel assignments 2015-06-04 11:56:47 -04:00
Florian Weingarten
e36f366c33 gitignore .bundle 2015-06-04 11:56:00 -04:00
Florian Weingarten
02729e89c0 make rubocop happy 2015-06-04 11:56:00 -04:00
Gaurav Chande
6b0f6401d0 Merge pull request #590 from Shopify/allow-template-tags
Local Tags
2015-06-04 11:19:24 -04:00
Gaurav Chande
fc8e6c8d3a Change Tokenizer test to fetch tokens instead of exposing ivar 2015-06-04 15:10:01 +00:00
Gaurav Chande
79d7dd06df Extract tag fetching into a method (which can be overriden then) 2015-06-04 04:39:54 +00:00
Gaurav Chande
3a907a4db7 Move DEFAULT_OPTIONS related logic to Document 2015-06-04 04:39:54 +00:00
Gaurav Chande
8b98f92c7f Extract tokenize logic from Template to a RubyTokenizer 2015-06-04 04:39:30 +00:00
Dylan Thacker-Smith
b79c0c611c Merge pull request #586 from Shopify/string-contains-non-string
Avoid an exception from checking if a string contains a non-string.
2015-06-03 10:58:38 -04:00
Dylan Thacker-Smith
8a2947865b Avoid an exception from checking if a string contains a non-string. 2015-06-03 02:21:51 -04:00
Dylan Thacker-Smith
ea29f8b4b8 Merge pull request #583 from Shopify/slice-nil-offset
Raise a Liquid::ArgumentError in slice filter for invalid integers.
2015-06-03 01:43:56 -04:00
Dylan Thacker-Smith
c84f4520cc Keep input out of error message and add test for slice Integer parsing. 2015-06-03 01:35:01 -04:00
Dylan Thacker-Smith
3dd6433e2f Merge pull request #584 from Shopify/replace-non-string
Convert arguments to replace filters to strings to avoid exceptions.
2015-06-02 16:41:15 -04:00
Dylan Thacker-Smith
ab7109a335 Raise a Liquid::ArgumentError in slice filter for invalid integers. 2015-06-02 16:05:08 -04:00
Dylan Thacker-Smith
94fe050952 Convert arguments to replace filters to strings to avoid exceptions. 2015-06-02 15:59:29 -04:00
Justin Li
9b98c436c4 Merge pull request #582 from Shopify/require-empty-raw-tag
Ensure raw tag has no arguments
2015-06-02 15:58:15 -04:00
Justin Li
889019f53a Keep old test as well 2015-06-02 15:21:51 -04:00
Justin Li
c290375aec Remove unnecessary regex options 2015-06-02 15:17:36 -04:00
Justin Li
719a98a25e Ensure raw tag has no arguments 2015-06-02 14:32:39 -04:00
Justin Li
86d8b552da Merge pull request #581 from Shopify/require-closed-raw-tag
Raise SyntaxError if raw tag is unclosed
2015-06-02 11:38:45 -04:00
Justin Li
b1ee9129e7 Raise SyntaxError if raw tag is unclosed 2015-06-02 10:56:51 -04:00
Justin Li
be2e41e4d5 Merge pull request #579 from Shopify/ast-match
Ensure For@reversed is a boolean
2015-05-28 16:45:09 -04:00
Justin Li
20ca2b9632 Update history to reflect merge of #570
[ci skip]
2015-05-28 16:43:22 -04:00
Justin Li
6c058823ad Merge pull request #570 from Shopify/fix-strict-conditions
Fix condition parse order in strict mode
2015-05-28 16:33:54 -04:00
Dylan Thacker-Smith
27245c9eab Merge pull request #577 from Shopify/table-row-blank-string-collection
Fix exception from using an empty string for the table row collection.
2015-05-28 16:20:42 -04:00
Justin Li
a639a13380 Use cleaner recursive solution 2015-05-28 16:16:30 -04:00
Justin Li
05a0fe56c8 Ensure For@reversed is a boolean 2015-05-28 16:09:26 -04:00
Dylan Thacker-Smith
c1eb694057 Remove the redundant iterable check in the for tag.
Just do it in slice_collection for consistency with the tablerow tag.
2015-05-28 16:04:50 -04:00
Dylan Thacker-Smith
f53b31c867 Merge pull request #578 from Shopify/filter-error-handling
Handle some more standard filter errors.
2015-05-28 15:00:57 -04:00
Dylan Thacker-Smith
363388e92f Handle some more standard filter errors. 2015-05-28 14:18:53 -04:00
Dylan Thacker-Smith
873eddbb85 Split a line and use String#empty? for readability 2015-05-28 12:55:04 -04:00
Dylan Thacker-Smith
e790b60f60 Fix exception from using an empty string for the table row collection. 2015-05-28 12:11:39 -04:00
Dylan Thacker-Smith
3264d60425 Merge pull request #576 from Shopify/flexible-exception-handler
Allow the exception handler to convert exceptions to hide error messges
2015-05-28 11:38:44 -04:00
Dylan Thacker-Smith
8ff1b8e01f Test set_line_number_from_token after exception is converted. 2015-05-28 09:22:02 -04:00
Dylan Thacker-Smith
8d5e71f856 Allow the exception handler to convert exceptions to hide error messages. 2015-05-27 18:59:51 -04:00
Dylan Thacker-Smith
89c6e605f8 Merge pull request #575 from Shopify/zero-division-error
Raise Liquid::ZeroDivisionError instead of ZeroDivisionError.
2015-05-26 10:43:23 -04:00
Dylan Thacker-Smith
6265c36ec9 Raise Liquid::ZeroDivisionError instead of ZeroDivisionError. 2015-05-25 15:40:17 -04:00
Dylan Thacker-Smith
8af99ff918 Merge pull request #573 from Shopify/optional-error-rendering
Make liquid error rendering optional.
2015-05-25 12:11:10 -04:00
Dylan Thacker-Smith
36200ff704 Make liquid error rendering optional.
Although the author of the liquid template wants to see these errors, they
probably don't want the visitor to see the liquid errors.  Probably the
best fallback when rendering the page for visitors is to render the empty
string for tags with errors.
2015-05-25 11:24:53 -04:00
Justin Li
a9c7df931f Strict parse conditions in reverse order 2015-05-19 11:51:01 -04:00
Justin Li
070639daba Push to for_stack at the beginning of For#render 2015-05-15 23:13:15 -04:00
Justin Li
dad98cfc89 Merge pull request #562 from Shopify/use-find_variable-for-parentloop
Use custom stack for forloop references
2015-05-15 21:48:57 -04:00
Florian Weingarten
1d3c0b3dab Merge pull request #568 from Shopify/remove_liquid_methods
Remove support for `liquid_methods` Module extension
2015-05-14 22:19:02 +02:00
Justin Li
648a4888af Pop the for_stack register in an ensure 2015-05-14 15:02:20 -04:00
Justin Li
b4e5017c79 Add truth table test for multiple if conditions 2015-05-14 14:11:03 -04:00
Justin Li
f1bc9f27df Include message in assert_template_result 2015-05-14 14:10:45 -04:00
Florian Weingarten
f4724f0db3 Remove support for liquid_methods Module extension 2015-05-14 14:44:19 +00:00
Florian Weingarten
df74955ac4 Merge pull request #564 from Shopify/rubocop
Rubocop
2015-05-14 16:41:32 +02:00
Florian Weingarten
3372ca8136 Rubocop 2015-05-14 14:37:18 +00:00
Jean Boussier
8cf524e91c Merge pull request #565 from Shopify/file-dirname
Modernize code base with __dir__ and require_relative
2015-05-13 15:45:22 -04:00
Jean Boussier
5e38626309 Force circle in ruby 2.0 2015-05-13 15:40:34 -04:00
Jean Boussier
b31df0fb3d Mordernize code base with __dir__ and require_relative 2015-05-13 15:33:00 -04:00
Florian Weingarten
9e815ec594 Merge pull request #563 from Shopify/webscale_exceptions
Prefer Class.new() where possible
2015-05-13 06:06:35 +02:00
Florian Weingarten
93b29b67ef Prefer Class.new() where possible 2015-05-13 02:47:43 +00:00
Justin Li
863e8968f0 Use extra stack for forloop references 2015-05-12 17:04:34 -04:00
Justin Li
4c9d2009f9 Add find_own_variable method to look up internal context variables 2015-05-12 16:49:39 -04:00
Justin Li
239cfa5a44 Use find_variable for parentloop 2015-05-12 16:11:32 -04:00
156 changed files with 9499 additions and 4160 deletions

2
.github/probots.yml vendored Normal file
View File

@@ -0,0 +1,2 @@
enabled:
- cla

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

@@ -0,0 +1,40 @@
name: Liquid
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
entry:
- { ruby: 2.5, allowed-failure: false }
- { ruby: 2.6, allowed-failure: false }
- { ruby: 2.7, allowed-failure: false }
- { 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

2
.gitignore vendored
View File

@@ -6,3 +6,5 @@ pkg
.rvmrc
.ruby-version
Gemfile.lock
.bundle
.byebug_history

File diff suppressed because it is too large Load Diff

18
.rubocop.yml Normal file
View File

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

57
.rubocop_todo.yml Normal file
View File

@@ -0,0 +1,57 @@
# This configuration was generated by
# `rubocop --auto-gen-config`
# on 2020-12-11 18:53:41 UTC using RuboCop version 1.6.1.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again.
# Offense count: 2
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: runtime_error, standard_error
Lint/InheritException:
Exclude:
- 'lib/liquid/interrupts.rb'
# Offense count: 113
# Cop supports --auto-correct.
# Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
# URISchemes: http, https
Layout/LineLength:
Max: 260
# Offense count: 8
Lint/MissingSuper:
Exclude:
- 'lib/liquid/forloop_drop.rb'
- 'lib/liquid/tablerowloop_drop.rb'
- 'test/integration/assign_test.rb'
- 'test/integration/context_test.rb'
- 'test/integration/filter_test.rb'
- 'test/integration/standard_filter_test.rb'
- 'test/integration/tags/for_tag_test.rb'
- 'test/integration/tags/table_row_test.rb'
# Offense count: 43
Naming/ConstantName:
Exclude:
- 'lib/liquid.rb'
- 'lib/liquid/block_body.rb'
- 'lib/liquid/tags/assign.rb'
- 'lib/liquid/tags/capture.rb'
- 'lib/liquid/tags/case.rb'
- 'lib/liquid/tags/cycle.rb'
- 'lib/liquid/tags/for.rb'
- 'lib/liquid/tags/if.rb'
- 'lib/liquid/tags/raw.rb'
- 'lib/liquid/tags/table_row.rb'
- 'lib/liquid/variable.rb'
- 'performance/shopify/comment_form.rb'
- 'performance/shopify/paginate.rb'
- 'test/integration/tags/include_tag_test.rb'
# Offense count: 2
Style/ClassVars:
Exclude:
- 'lib/liquid/condition.rb'

View File

@@ -1,20 +0,0 @@
language: ruby
rvm:
- 2.0
- 2.1
- 2.2
- ruby-head
- jruby-head
- rbx-2
sudo: false
matrix:
allow_failures:
- rvm: jruby-head
script: "rake test"
notifications:
disable: true

View File

@@ -4,23 +4,22 @@
* Bugfixes
* Performance improvements
* Features which are likely to be useful to the majority of Liquid users
* Features that are likely to be useful to the majority of Liquid users
## Things we won't merge
* Code which introduces considerable performance degrations
* Code which touches performance critical parts of Liquid and comes without benchmarks
* Features which are not important for most people (we want to keep the core Liquid code small and tidy)
* Features which can easily be implemented on top of Liquid (for example as a custom filter or custom filesystem)
* Code which comes without tests
* Code which breaks existing tests
* Code that introduces considerable performance degrations
* Code that touches performance-critical parts of Liquid and comes without benchmarks
* Features that are not important for most people (we want to keep the core Liquid code small and tidy)
* Features that can easily be implemented on top of Liquid (for example as a custom filter or custom filesystem)
* Code that does not include tests
* Code that breaks existing tests
## Workflow
* Fork the Liquid repository
* Create a new branch in your fork
* If it makes sense, add tests for your code and run a performance benchmark
* Make sure all tests pass
* If it makes sense, add tests for your code and/or run a performance benchmark
* Make sure all tests pass (`bundle exec rake`)
* Create a pull request
* In the description, ping one of [@boourns](https://github.com/boourns), [@fw42](https://github.com/fw42), [@camilo](https://github.com/camilo), [@dylanahsmith](https://github.com/dylanahsmith), or [@arthurnn](https://github.com/arthurnn) and ask for a code review.

24
Gemfile
View File

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

View File

@@ -1,26 +1,149 @@
# Liquid Change Log
## 4.0.0 / not yet released / branch "master"
### Changed
* Add sort_natural filter (#554) [Martin Hanzel, arthanzel]
* Add forloop.parentloop as a reference to the parent loop (#520) [Justin Li, pushrax]
* Block parsing moved to BlockBody class (#458) [Dylan Thacker-Smith, dylanahsmith]
* Add concat filter to concatenate arrays (#429) [Diogo Beato, dvbeato]
* Ruby 1.9 support dropped (#491) [Justin Li, pushrax]
* Liquid::Template.file_system's read_template_file method is no longer passed the context. (#441) [James Reid-Smith, sunblaze]
## 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
### Fixed
* Fix naming of the "context variable" when dynamically including a template (#559) [Justin Li, pushrax]
* Gracefully accept empty strings in the date filter (#555) [Loren Hale, boobooninja]
* Fix capturing into variables with a hyphen in the name (#505) [Florian Weingarten, fw42]
* Fix case sensitivity regression in date standard filter (#499) [Kelley Reynolds, kreynolds]
* Disallow filters with no variable in strict mode (#475) [Justin Li, pushrax]
* Disallow variable names in the strict parser that are not valid in the lax parser (#463) [Justin Li, pushrax]
* Fix BlockBody#warnings taking exponential time to compute (#486) [Justin Li, pushrax]
* Fix break and continue tags inside included templates in loops (#1072) [Justin Li]
## 3.0.2 / 2015-04-24 / branch "3-0-stable"
## 4.0.2 / 2019-03-08
* Expose VariableLookup private members (#551) [Justin Li, pushrax]
### Changed
* Add `where` filter (#1026) [Samuel Doiron]
* Add `ParseTreeVisitor` to iterate the Liquid AST (#1025) [Stephen Paul Weber]
* Improve `strip_html` performance (#1032) [printercu]
### Fixed
* Add error checking for invalid combinations of inputs to sort, sort_natural, where, uniq, map, compact filters (#1059) [Garland Zhang]
* Validate the character encoding in url_decode (#1070) [Clayton Smith]
## 4.0.1 / 2018-10-09
### Changed
* Add benchmark group in Gemfile (#855) [Jerry Liu]
* Allow benchmarks to benchmark render by itself (#851) [Jerry Liu]
* Avoid calling `line_number` on String node when rescuing a render error. (#860) [Dylan Thacker-Smith]
* Avoid duck typing to detect whether to call render on a node. [Dylan Thacker-Smith]
* Clarify spelling of `reversed` on `for` block tag (#843) [Mark Crossfield]
* Replace recursion with loop to avoid potential stack overflow from malicious input (#891, #892) [Dylan Thacker-Smith]
* Limit block tag nesting to 100 (#894) [Dylan Thacker-Smith]
* Replace `assert_equal nil` with `assert_nil` (#895) [Dylan Thacker-Smith]
* Remove Spy Gem (#896) [Dylan Thacker-Smith]
* Add `collection_name` and `variable_name` reader to `For` block (#909)
* Symbols render as strings (#920) [Justin Li]
* Remove default value from Hash objects (#932) [Maxime Bedard]
* Remove one level of nesting (#944) [Dylan Thacker-Smith]
* Update Rubocop version (#952) [Justin Li]
* Add `at_least` and `at_most` filters (#954, #958) [Nithin Bekal]
* Add a regression test for a liquid-c trim mode bug (#972) [Dylan Thacker-Smith]
* Use https rather than git protocol to fetch liquid-c [Dylan Thacker-Smith]
* Add tests against Ruby 2.4 (#963) and 2.5 (#981)
* Replace RegExp literals with constants (#988) [Ashwin Maroli]
* Replace unnecessary `#each_with_index` with `#each` (#992) [Ashwin Maroli]
* Improve the unexpected end delimiter message for block tags. (#1003) [Dylan Thacker-Smith]
* Refactor and optimize rendering (#1005) [Christopher Aue]
* Add installation instruction (#1006) [Ben Gift]
* Remove Circle CI (#1010)
* Rename deprecated `BigDecimal.new` to `BigDecimal` (#1024) [Koichi ITO]
* Rename deprecated Rubocop name (#1027) [Justin Li]
### Fixed
* Handle `join` filter on non String joiners (#857) [Richard Monette]
* Fix duplicate inclusion condition logic error of `Liquid::Strainer.add_filter` method (#861)
* Fix `escape`, `url_encode`, `url_decode` not handling non-string values (#898) [Thierry Joyal]
* Fix raise when variable is defined but nil when using `strict_variables` [Pascal Betz]
* Fix `sort` and `sort_natural` to handle arrays with nils (#930) [Eric Chan]
## 4.0.0 / 2016-12-14 / branch "4-0-stable"
### Changed
* Render an opaque internal error by default for non-Liquid::Error (#835) [Dylan Thacker-Smith]
* Ruby 2.0 support dropped (#832) [Dylan Thacker-Smith]
* Add to_number Drop method to allow custom drops to work with number filters (#731)
* Add strict_variables and strict_filters options to detect undefined references (#691)
* Improve loop performance (#681) [Florian Weingarten]
* Rename Drop method `before_method` to `liquid_method_missing` (#661) [Thierry Joyal]
* Add url_decode filter to invert url_encode (#645) [Larry Archer]
* Add global_filter to apply a filter to all output (#610) [Loren Hale]
* Add compact filter (#600) [Carson Reinke]
* Rename deprecated "has_key?" and "has_interrupt?" methods (#593) [Florian Weingarten]
* Include template name with line numbers in render errors (574) [Dylan Thacker-Smith]
* Add sort_natural filter (#554) [Martin Hanzel]
* Add forloop.parentloop as a reference to the parent loop (#520) [Justin Li]
* Block parsing moved to BlockBody class (#458) [Dylan Thacker-Smith]
* Add concat filter to concatenate arrays (#429) [Diogo Beato]
* Ruby 1.9 support dropped (#491) [Justin Li]
* Liquid::Template.file_system's read_template_file method is no longer passed the context. (#441) [James Reid-Smith]
* Remove `liquid_methods` (See https://github.com/Shopify/liquid/pull/568 for replacement)
* Liquid::Template.register_filter raises when the module overrides registered public methods as private or protected (#705) [Gaurav Chande]
### Fixed
* Fix variable names being detected as an operator when starting with contains (#788) [Michael Angell]
* Fix include tag used with strict_variables (#828) [QuickPay]
* Fix map filter when value is a Proc (#672) [Guillaume Malette]
* Fix truncate filter when value is not a string (#672) [Guillaume Malette]
* Fix behaviour of escape filter when input is nil (#665) [Tanel Jakobsoo]
* Fix sort filter behaviour with empty array input (#652) [Marcel Cary]
* Fix test failure under certain timezones (#631) [Dylan Thacker-Smith]
* Fix bug in uniq filter (#595) [Florian Weingarten]
* Fix bug when "blank" and "empty" are used as variable names (#592) [Florian Weingarten]
* Fix condition parse order in strict mode (#569) [Justin Li]
* Fix naming of the "context variable" when dynamically including a template (#559) [Justin Li]
* Gracefully accept empty strings in the date filter (#555) [Loren Hale]
* Fix capturing into variables with a hyphen in the name (#505) [Florian Weingarten]
* Fix case sensitivity regression in date standard filter (#499) [Kelley Reynolds]
* Disallow filters with no variable in strict mode (#475) [Justin Li]
* Disallow variable names in the strict parser that are not valid in the lax parser (#463) [Justin Li]
* Fix BlockBody#warnings taking exponential time to compute (#486) [Justin Li]
## 3.0.5 / 2015-07-23 / branch "3-0-stable"
* Fix test failure under certain timezones [Dylan Thacker-Smith]
## 3.0.4 / 2015-07-17
* Fix chained access to multi-dimensional hashes [Florian Weingarten]
## 3.0.3 / 2015-05-28
* Fix condition parse order in strict mode (#569) [Justin Li]
## 3.0.2 / 2015-04-24
* Expose VariableLookup private members (#551) [Justin Li]
* Documentation fixes
## 3.0.1 / 2015-01-23
@@ -29,44 +152,52 @@
## 3.0.0 / 2014-11-12
* Removed Block#end_tag. Instead, override parse with `super` followed by your code. See #446 [Dylan Thacker-Smith, dylanahsmith]
* Removed Block#end_tag. Instead, override parse with `super` followed by your code. See #446 [Dylan Thacker-Smith]
* Fixed condition with wrong data types (#423) [Bogdan Gusiev]
* Add url_encode to standard filters (#421) [Derrick Reimer, djreimer]
* Add uniq to standard filters [Florian Weingarten, fw42]
* Add exception_handler feature (#397) and #254 [Bogdan Gusiev, bogdan and Florian Weingarten, fw42]
* Optimize variable parsing to avoid repeated regex evaluation during template rendering #383 [Jason Hiltz-Laforge, jasonhl]
* Optimize checking for block interrupts to reduce object allocation #380 [Jason Hiltz-Laforge, jasonhl]
* Properly set context rethrow_errors on render! #349 [Thierry Joyal, tjoyal]
* Fix broken rendering of variables which are equal to false (#345) [Florian Weingarten, fw42]
* Remove ActionView template handler [Dylan Thacker-Smith, dylanahsmith]
* Freeze lots of string literals for new Ruby 2.1 optimization (#297) [Florian Weingarten, fw42]
* Allow newlines in tags and variables (#324) [Dylan Thacker-Smith, dylanahsmith]
* Tag#parse is called after initialize, which now takes options instead of tokens as the 3rd argument. See #321 [Dylan Thacker-Smith, dylanahsmith]
* Raise `Liquid::ArgumentError` instead of `::ArgumentError` when filter has wrong number of arguments #309 [Bogdan Gusiev, bogdan]
* Add a to_s default for liquid drops (#306) [Adam Doeler, releod]
* Add strip, lstrip, and rstrip to standard filters [Florian Weingarten, fw42]
* Make if, for & case tags return complete and consistent nodelists (#250) [Nick Jones, dntj]
* Prevent arbitrary method invocation on condition objects (#274) [Dylan Thacker-Smith, dylanahsmith]
* Don't call to_sym when creating conditions for security reasons (#273) [Bouke van der Bijl, bouk]
* Fix resource counting bug with respond_to?(:length) (#263) [Florian Weingarten, fw42]
* Allow specifying custom patterns for template filenames (#284) [Andrei Gladkyi, agladkyi]
* Allow drops to optimize loading a slice of elements (#282) [Tom Burns, boourns]
* Support for passing variables to snippets in subdirs (#271) [Joost Hietbrink, joost]
* Add a class cache to avoid runtime extend calls (#249) [James Tucker, raggi]
* Remove some legacy Ruby 1.8 compatibility code (#276) [Florian Weingarten, fw42]
* Add default filter to standard filters (#267) [Derrick Reimer, djreimer]
* Add optional strict parsing and warn parsing (#235) [Tristan Hume, trishume]
* Add url_encode to standard filters (#421) [Derrick Reimer]
* Add uniq to standard filters [Florian Weingarten]
* Add exception_handler feature (#397) and #254 [Bogdan Gusiev, Florian Weingarten]
* Optimize variable parsing to avoid repeated regex evaluation during template rendering #383 [Jason Hiltz-Laforge]
* Optimize checking for block interrupts to reduce object allocation #380 [Jason Hiltz-Laforge]
* Properly set context rethrow_errors on render! #349 [Thierry Joyal]
* Fix broken rendering of variables which are equal to false (#345) [Florian Weingarten]
* Remove ActionView template handler [Dylan Thacker-Smith]
* Freeze lots of string literals for new Ruby 2.1 optimization (#297) [Florian Weingarten]
* Allow newlines in tags and variables (#324) [Dylan Thacker-Smith]
* Tag#parse is called after initialize, which now takes options instead of tokens as the 3rd argument. See #321 [Dylan Thacker-Smith]
* Raise `Liquid::ArgumentError` instead of `::ArgumentError` when filter has wrong number of arguments #309 [Bogdan Gusiev]
* Add a to_s default for liquid drops (#306) [Adam Doeler]
* Add strip, lstrip, and rstrip to standard filters [Florian Weingarten]
* Make if, for & case tags return complete and consistent nodelists (#250) [Nick Jones]
* Prevent arbitrary method invocation on condition objects (#274) [Dylan Thacker-Smith]
* Don't call to_sym when creating conditions for security reasons (#273) [Bouke van der Bijl]
* Fix resource counting bug with respond_to?(:length) (#263) [Florian Weingarten]
* Allow specifying custom patterns for template filenames (#284) [Andrei Gladkyi]
* Allow drops to optimize loading a slice of elements (#282) [Tom Burns]
* Support for passing variables to snippets in subdirs (#271) [Joost Hietbrink]
* Add a class cache to avoid runtime extend calls (#249) [James Tucker]
* Remove some legacy Ruby 1.8 compatibility code (#276) [Florian Weingarten]
* Add default filter to standard filters (#267) [Derrick Reimer]
* Add optional strict parsing and warn parsing (#235) [Tristan Hume]
* Add I18n syntax error translation (#241) [Simon Hørup Eskildsen, Sirupsen]
* Make sort filter work on enumerable drops (#239) [Florian Weingarten, fw42]
* Fix clashing method names in enumerable drops (#238) [Florian Weingarten, fw42]
* Make map filter work on enumerable drops (#233) [Florian Weingarten, fw42]
* Improved whitespace stripping for blank blocks, related to #216 [Florian Weingarten, fw42]
* Make sort filter work on enumerable drops (#239) [Florian Weingarten]
* Fix clashing method names in enumerable drops (#238) [Florian Weingarten]
* Make map filter work on enumerable drops (#233) [Florian Weingarten]
* Improved whitespace stripping for blank blocks, related to #216 [Florian Weingarten]
## 2.6.1 / 2014-01-10 / branch "2-6-stable"
## 2.6.3 / 2015-07-23 / branch "2-6-stable"
* Fix test failure under certain timezones [Dylan Thacker-Smith]
## 2.6.2 / 2015-01-23
* Remove duplicate hash key [Parker Moore]
## 2.6.1 / 2014-01-10
Security fix, cherry-picked from master (4e14a65):
* Don't call to_sym when creating conditions for security reasons (#273) [Bouke van der Bijl, bouk]
* Prevent arbitrary method invocation on condition objects (#274) [Dylan Thacker-Smith, dylanahsmith]
* Don't call to_sym when creating conditions for security reasons (#273) [Bouke van der Bijl]
* Prevent arbitrary method invocation on condition objects (#274) [Dylan Thacker-Smith]
## 2.6.0 / 2013-11-25
@@ -74,37 +205,37 @@ IMPORTANT: Liquid 2.6 is going to be the last version of Liquid which maintains
The following releases will only be tested against Ruby 1.9 and Ruby 2.0 and are likely to break on Ruby 1.8.
* Bugfix for #106: fix example servlet [gnowoel]
* Bugfix for #97: strip_html filter supports multi-line tags [Jo Liss, joliss]
* Bugfix for #114: strip_html filter supports style tags [James Allardice, jamesallardice]
* Bugfix for #117: 'now' support for date filter in Ruby 1.9 [Notre Dame Webgroup, ndwebgroup]
* Bugfix for #166: truncate filter on UTF-8 strings with Ruby 1.8 [Florian Weingarten, fw42]
* Bugfix for #204: 'raw' parsing bug [Florian Weingarten, fw42]
* Bugfix for #150: 'for' parsing bug [Peter Schröder, phoet]
* Bugfix for #126: Strip CRLF in strip_newline [Peter Schröder, phoet]
* Bugfix for #174, "can't convert Fixnum into String" for "replace" [wǒ_is神仙, jsw0528]
* Allow a Liquid::Drop to be passed into Template#render [Daniel Huckstep, darkhelmet]
* Resource limits [Florian Weingarten, fw42]
* Add reverse filter [Jay Strybis, unreal]
* Bugfix for #97: strip_html filter supports multi-line tags [Jo Liss]
* Bugfix for #114: strip_html filter supports style tags [James Allardice]
* Bugfix for #117: 'now' support for date filter in Ruby 1.9 [Notre Dame Webgroup]
* Bugfix for #166: truncate filter on UTF-8 strings with Ruby 1.8 [Florian Weingarten]
* Bugfix for #204: 'raw' parsing bug [Florian Weingarten]
* Bugfix for #150: 'for' parsing bug [Peter Schröder]
* Bugfix for #126: Strip CRLF in strip_newline [Peter Schröder]
* Bugfix for #174, "can't convert Fixnum into String" for "replace" [jsw0528]
* Allow a Liquid::Drop to be passed into Template#render [Daniel Huckstep]
* Resource limits [Florian Weingarten]
* Add reverse filter [Jay Strybis]
* Add utf-8 support
* Use array instead of Hash to keep the registered filters [Tasos Stathopoulos, astathopoulos]
* Cache tokenized partial templates [Tom Burns, boourns]
* Avoid warnings in Ruby 1.9.3 [Marcus Stollsteimer, stomar]
* Better documentation for 'include' tag (closes #163) [Peter Schröder, phoet]
* Use of BigDecimal on filters to have better precision (closes #155) [Arthur Nogueira Neves, arthurnn]
* Use array instead of Hash to keep the registered filters [Tasos Stathopoulos]
* Cache tokenized partial templates [Tom Burns]
* Avoid warnings in Ruby 1.9.3 [Marcus Stollsteimer]
* Better documentation for 'include' tag (closes #163) [Peter Schröder]
* Use of BigDecimal on filters to have better precision (closes #155) [Arthur Nogueira Neves]
## 2.5.5 / 2014-01-10 / branch "2-5-stable"
Security fix, cherry-picked from master (4e14a65):
* Don't call to_sym when creating conditions for security reasons (#273) [Bouke van der Bijl, bouk]
* Prevent arbitrary method invocation on condition objects (#274) [Dylan Thacker-Smith, dylanahsmith]
* Don't call to_sym when creating conditions for security reasons (#273) [Bouke van der Bijl]
* Prevent arbitrary method invocation on condition objects (#274) [Dylan Thacker-Smith]
## 2.5.4 / 2013-11-11
* Fix "can't convert Fixnum into String" for "replace" (#173), [wǒ_is神仙, jsw0528]
* Fix "can't convert Fixnum into String" for "replace" (#173), [jsw0528]
## 2.5.3 / 2013-10-09
* #232, #234, #237: Fix map filter bugs [Florian Weingarten, fw42]
* #232, #234, #237: Fix map filter bugs [Florian Weingarten]
## 2.5.2 / 2013-09-03 / deleted
@@ -112,7 +243,7 @@ Yanked from rubygems, as it contained too many changes that broke compatibility.
## 2.5.1 / 2013-07-24
* #230: Fix security issue with map filter, Use invoke_drop in map filter [Florian Weingarten, fw42]
* #230: Fix security issue with map filter, Use invoke_drop in map filter [Florian Weingarten]
## 2.5.0 / 2013-03-06

View File

@@ -42,6 +42,8 @@ Liquid is a template engine which was written with very specific requirements:
## How to use Liquid
Install Liquid by adding `gem 'liquid'` to your gemfile.
Liquid supports a very simple API based around the Liquid::Template class.
For standard use you can just pass it the content of a file and call render with a parameters hash.
@@ -73,3 +75,40 @@ This is useful for doing things like enabling strict mode only in the theme edit
It is recommended that you enable `:strict` or `:warn` mode on new apps to stop invalid templates from being created.
It is also recommended that you use it in the template editors of existing apps to give editors better error messages.
### Undefined variables and filters
By default, the renderer doesn't raise or in any other way notify you if some variables or filters are missing, i.e. not passed to the `render` method.
You can improve this situation by passing `strict_variables: true` and/or `strict_filters: true` options to the `render` method.
When one of these options is set to true, all errors about undefined variables and undefined filters will be stored in `errors` array of a `Liquid::Template` instance.
Here are some examples:
```ruby
template = Liquid::Template.parse("{{x}} {{y}} {{z.a}} {{z.b}}")
template.render({ 'x' => 1, 'z' => { 'a' => 2 } }, { strict_variables: true })
#=> '1 2 ' # when a variable is undefined, it's rendered as nil
template.errors
#=> [#<Liquid::UndefinedVariable: Liquid error: undefined variable y>, #<Liquid::UndefinedVariable: Liquid error: undefined variable b>]
```
```ruby
template = Liquid::Template.parse("{{x | filter1 | upcase}}")
template.render({ 'x' => 'foo' }, { strict_filters: true })
#=> '' # when at least one filter in the filter chain is undefined, a whole expression is rendered as nil
template.errors
#=> [#<Liquid::UndefinedFilter: Liquid error: undefined filter filter1>]
```
If you want to raise on a first exception instead of pushing all of them in `errors`, you can use `render!` method:
```ruby
template = Liquid::Template.parse("{{x}} {{y}}")
template.render!({ 'x' => 1}, { strict_variables: true })
#=> Liquid::UndefinedVariable: Liquid error: undefined variable y
```
### Usage tracking
To help track usages of a feature or code path in production, we have released opt-in usage tracking. To enable this, we provide an empty `Liquid:: Usage.increment` method which you can customize to your needs. The feature is well suited to https://github.com/Shopify/statsd-instrument. However, the choice of implementation is up to you.
Once you have enabled usage tracking, we recommend reporting any events through Github Issues that your system may be logging. It is highly likely this event has been added to consider deprecating or improving code specific to this event, so please raise any concerns.

View File

@@ -1,42 +1,70 @@
# frozen_string_literal: true
require 'rake'
require 'rake/testtask'
$LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
$LOAD_PATH.unshift(File.expand_path("../lib", __FILE__))
require "liquid/version"
task :default => 'test'
task(default: [:test, :rubocop])
desc 'run test suite with default parser'
desc('run test suite with default parser')
Rake::TestTask.new(:base_test) do |t|
t.libs << '.' << 'lib' << 'test'
t.libs << 'lib' << 'test'
t.test_files = FileList['test/{integration,unit}/**/*_test.rb']
t.verbose = false
t.verbose = false
end
desc 'run test suite with warn error mode'
Rake::TestTask.new(:integration_test) do |t|
t.libs << 'lib' << 'test'
t.test_files = FileList['test/integration/**/*_test.rb']
t.verbose = false
end
desc('run test suite with warn error mode')
task :warn_test do
ENV['LIQUID_PARSER_MODE'] = 'warn'
Rake::Task['base_test'].invoke
end
desc 'runs test suite with both strict and lax parsers'
task :rubocop do
if RUBY_ENGINE == 'ruby'
require 'rubocop/rake_task'
RuboCop::RakeTask.new
end
end
desc('runs test suite with both strict and lax parsers')
task :test do
ENV['LIQUID_PARSER_MODE'] = 'lax'
Rake::Task['base_test'].invoke
ENV['LIQUID_PARSER_MODE'] = 'strict'
Rake::Task['base_test'].reenable
Rake::Task['base_test'].invoke
if RUBY_ENGINE == 'ruby' || RUBY_ENGINE == 'truffleruby'
ENV['LIQUID_C'] = '1'
ENV['LIQUID_PARSER_MODE'] = 'lax'
Rake::Task['integration_test'].reenable
Rake::Task['integration_test'].invoke
ENV['LIQUID_PARSER_MODE'] = 'strict'
Rake::Task['integration_test'].reenable
Rake::Task['integration_test'].invoke
end
end
task :gem => :build
task(gem: :build)
task :build do
system "gem build liquid.gemspec"
end
task :install => :build do
task install: :build do
system "gem install liquid-#{Liquid::VERSION}.gem"
end
task :release => :build do
task release: :build do
system "git tag -a v#{Liquid::VERSION} -m 'Tagging #{Liquid::VERSION}'"
system "git push --tags"
system "gem push liquid-#{Liquid::VERSION}.gem"
@@ -44,7 +72,6 @@ task :release => :build do
end
namespace :benchmark do
desc "Run the liquid benchmark with lax parsing"
task :run do
ruby "./performance/benchmark.rb lax"
@@ -56,9 +83,7 @@ namespace :benchmark do
end
end
namespace :profile do
desc "Run the liquid profile/performance coverage"
task :run do
ruby "./performance/profile.rb"
@@ -68,10 +93,20 @@ namespace :profile do
task :strict do
ruby "./performance/profile.rb strict"
end
end
desc "Run example"
namespace :memory_profile do
desc "Run memory profiler"
task :run do
ruby "./performance/memory_profile.rb"
end
end
desc("Run example")
task :example do
ruby "-w -d -Ilib example/server/server.rb"
end
task :console do
exec 'irb -I lib -r liquid'
end

View File

@@ -1,10 +1,12 @@
# frozen_string_literal: true
module ProductsFilter
def price(integer)
sprintf("$%.2d USD", integer / 100.0)
format("$%.2d USD", integer / 100.0)
end
def prettyprint(text)
text.gsub( /\*(.*)\*/, '<b>\1</b>' )
text.gsub(/\*(.*)\*/, '<b>\1</b>')
end
def count(array)
@@ -17,30 +19,28 @@ module ProductsFilter
end
class Servlet < LiquidServlet
def index
{ 'date' => Time.now }
end
def products
{ 'products' => products_list, 'more_products' => more_products_list, 'description' => description, 'section' => 'Snowboards', 'cool_products' => true}
{ 'products' => products_list, 'more_products' => more_products_list, 'description' => description, 'section' => 'Snowboards', 'cool_products' => true }
end
private
def products_list
[{'name' => 'Arbor Draft', 'price' => 39900, 'description' => 'the *arbor draft* is a excellent product' },
{'name' => 'Arbor Element', 'price' => 40000, 'description' => 'the *arbor element* rocks for freestyling'},
{'name' => 'Arbor Diamond', 'price' => 59900, 'description' => 'the *arbor diamond* is a made up product because im obsessed with arbor and have no creativity'}]
[{ 'name' => 'Arbor Draft', 'price' => 39900, 'description' => 'the *arbor draft* is a excellent product' },
{ 'name' => 'Arbor Element', 'price' => 40000, 'description' => 'the *arbor element* rocks for freestyling' },
{ 'name' => 'Arbor Diamond', 'price' => 59900, 'description' => 'the *arbor diamond* is a made up product because im obsessed with arbor and have no creativity' }]
end
def more_products_list
[{'name' => 'Arbor Catalyst', 'price' => 39900, 'description' => 'the *arbor catalyst* is an advanced drop-through for freestyle and flatground performance and versatility' },
{'name' => 'Arbor Fish', 'price' => 40000, 'description' => 'the *arbor fish* is a compact pin that features an extended wheelbase and time-honored teardrop shape'}]
[{ 'name' => 'Arbor Catalyst', 'price' => 39900, 'description' => 'the *arbor catalyst* is an advanced drop-through for freestyle and flatground performance and versatility' },
{ 'name' => 'Arbor Fish', 'price' => 40000, 'description' => 'the *arbor fish* is a compact pin that features an extended wheelbase and time-honored teardrop shape' }]
end
def description
"List of Products ~ This is a list of products with price and description."
end
end

View File

@@ -1,5 +1,6 @@
class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet
# frozen_string_literal: true
class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet
def do_GET(req, res)
handle(:get, req, res)
end
@@ -10,20 +11,20 @@ class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet
private
def handle(type, req, res)
@request = req
def handle(_type, req, res)
@request = req
@response = res
@request.path_info =~ /(\w+)\z/
@action = $1 || 'index'
@action = Regexp.last_match(1) || 'index'
@assigns = send(@action) if respond_to?(@action)
@response['Content-Type'] = "text/html"
@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
def read_template(filename = @action)
File.read( File.dirname(__FILE__) + "/templates/#{filename}.liquid" )
File.read("#{__dir__}/templates/#{filename}.liquid")
end
end

View File

@@ -1,14 +1,14 @@
# frozen_string_literal: true
require 'webrick'
require 'rexml/document'
DIR = File.expand_path(File.dirname(__FILE__))
require DIR + '/../../lib/liquid'
require DIR + '/liquid_servlet'
require DIR + '/example_servlet'
require_relative '../../lib/liquid'
require_relative 'liquid_servlet'
require_relative 'example_servlet'
# Setup webrick
server = WEBrick::HTTPServer.new( :Port => ARGV[1] || 3000 )
server = WEBrick::HTTPServer.new(Port: ARGV[1] || 3000)
server.mount('/', Servlet)
trap("INT"){ server.shutdown }
trap("INT") { server.shutdown }
server.start

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
# Copyright (c) 2005 Tobias Luetke
#
# Permission is hereby granted, free of charge, to any person obtaining
@@ -21,9 +23,10 @@
module Liquid
FilterSeparator = /\|/
ArgumentSeparator = ','.freeze
FilterArgumentSeparator = ':'.freeze
VariableAttributeSeparator = '.'.freeze
ArgumentSeparator = ','
FilterArgumentSeparator = ':'
VariableAttributeSeparator = '.'
WhitespaceControl = '-'
TagStart = /\{\%/
TagEnd = /\%\}/
VariableSignature = /\(?[\w\-\.\[\]]\)?/
@@ -34,28 +37,36 @@ module Liquid
QuotedString = /"[^"]*"|'[^']*'/
QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o
TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/o
AnyStartingTag = /\{\{|\{\%/
AnyStartingTag = /#{TagStart}|#{VariableStart}/o
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om
VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o
RAISE_EXCEPTION_LAMBDA = ->(_e) { raise }
singleton_class.send(:attr_accessor, :cache_classes)
self.cache_classes = true
end
require "liquid/version"
require 'liquid/parse_tree_visitor'
require 'liquid/lexer'
require 'liquid/parser'
require 'liquid/i18n'
require 'liquid/drop'
require 'liquid/tablerowloop_drop'
require 'liquid/forloop_drop'
require 'liquid/extensions'
require 'liquid/errors'
require 'liquid/interrupts'
require 'liquid/strainer'
require 'liquid/strainer_factory'
require 'liquid/strainer_template'
require 'liquid/expression'
require 'liquid/context'
require 'liquid/parser_switching'
require 'liquid/tag'
require 'liquid/tag/disabler'
require 'liquid/tag/disableable'
require 'liquid/block'
require 'liquid/block_body'
require 'liquid/document'
@@ -67,10 +78,15 @@ require 'liquid/resource_limits'
require 'liquid/template'
require 'liquid/standardfilters'
require 'liquid/condition'
require 'liquid/module_ex'
require 'liquid/utils'
require 'liquid/token'
require 'liquid/tokenizer'
require 'liquid/parse_context'
require 'liquid/partial_cache'
require 'liquid/usage'
require 'liquid/register'
require 'liquid/static_registers'
require 'liquid/template_factory'
# Load all the tags of the standard library
#
Dir[File.dirname(__FILE__) + '/liquid/tags/*.rb'].each { |f| require f }
Dir["#{__dir__}/liquid/tags/*.rb"].each { |f| require f }

View File

@@ -1,16 +1,22 @@
# frozen_string_literal: true
module Liquid
class Block < Tag
MAX_DEPTH = 100
def initialize(tag_name, markup, options)
super
@blank = true
end
def parse(tokens)
@body = BlockBody.new
while more = parse_body(@body, tokens)
@body = new_body
while parse_body(@body, tokens)
end
@body.freeze
end
# For backwards compatibility
def render(context)
@body.render(context)
end
@@ -23,32 +29,29 @@ module Liquid
@body.nodelist
end
# warnings of this block and all sub-tags
def warnings
all_warnings = []
all_warnings.concat(@warnings) if @warnings
(nodelist || []).each do |node|
all_warnings.concat(node.warnings || []) if node.respond_to?(:warnings)
end
all_warnings
def unknown_tag(tag_name, _markup, _tokenizer)
Block.raise_unknown_tag(tag_name, block_name, block_delimiter, parse_context)
end
def unknown_tag(tag, params, tokens)
case tag
when 'else'.freeze
raise SyntaxError.new(options[:locale].t("errors.syntax.unexpected_else".freeze,
:block_name => block_name))
when 'end'.freeze
raise SyntaxError.new(options[:locale].t("errors.syntax.invalid_delimiter".freeze,
:block_name => block_name,
:block_delimiter => block_delimiter))
# @api private
def self.raise_unknown_tag(tag, block_name, block_delimiter, parse_context)
if tag == 'else'
raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_else",
block_name: block_name)
elsif tag.start_with?('end')
raise SyntaxError, parse_context.locale.t("errors.syntax.invalid_delimiter",
tag: tag,
block_name: block_name,
block_delimiter: block_delimiter)
else
raise SyntaxError.new(options[:locale].t("errors.syntax.unknown_tag".freeze, :tag => tag))
raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag)
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
@tag_name
end
@@ -57,24 +60,32 @@ module Liquid
@block_delimiter ||= "end#{block_name}"
end
def format
"{% #{raw} %}#{@body.format}{% #{block_delimiter} %}"
private
# @api public
def new_body
parse_context.new_block_body
end
protected
# @api public
def parse_body(body, tokens)
body.parse(tokens, options) do |end_tag_name, end_tag_params|
@blank &&= body.blank?
if parse_context.depth >= MAX_DEPTH
raise StackLevelError, "Nesting too deep"
end
parse_context.depth += 1
begin
body.parse(tokens, parse_context) do |end_tag_name, end_tag_params|
@blank &&= body.blank?
return false if end_tag_name == block_delimiter
unless end_tag_name
raise SyntaxError.new(@options[:locale].t("errors.syntax.tag_never_closed".freeze, :block_name => block_name))
return false if end_tag_name == block_delimiter
raise_tag_never_closed(block_name) unless end_tag_name
# this tag is not registered with the system
# pass it to the current block for special handling or error reporting
unknown_tag(end_tag_name, end_tag_params, tokens)
end
# this tag is not registered with the system
# pass it to the current block for special handling or error reporting
unknown_tag(end_tag_name, end_tag_params, tokens)
ensure
parse_context.depth -= 1
end
true

View File

@@ -1,143 +1,255 @@
# frozen_string_literal: true
require 'English'
module Liquid
class BlockBody
FullToken = /\A#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
ContentOfVariable = /\A#{VariableStart}(.*)#{VariableEnd}\z/om
TAGSTART = "{%".freeze
VARSTART = "{{".freeze
LiquidTagToken = /\A\s*([\w#]+)\s*(.*?)\z/o
FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(\w+)(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
WhitespaceOrNothing = /\A\s*\z/
TAGSTART = "{%"
VARSTART = "{{"
attr_reader :nodelist
def initialize
@nodelist = []
@blank = true
@blank = true
end
def parse(tokens, options)
while token = tokens.shift
begin
unless token.empty?
case
when token.start_with?(TAGSTART)
if token =~ FullToken
tag_name = $1
markup = $2
# fetch the tag from registered blocks
if tag = Template.tags[tag_name]
markup = token.child(markup) if token.is_a?(Token)
new_tag = tag.parse(tag_name, markup, tokens, options)
new_tag.line_number = token.line_number if token.is_a?(Token)
@blank &&= new_tag.blank?
@nodelist << new_tag
else
# end parsing if we reach an unknown tag and let the caller decide
# determine how to proceed
return yield tag_name, markup
end
else
raise_missing_tag_terminator(token, options)
end
when token.start_with?(VARSTART)
new_var = create_variable(token, options)
new_var.line_number = token.line_number if token.is_a?(Token)
@nodelist << new_var
@blank = false
else
@nodelist << token
@blank &&= !!(token =~ /\A\s*\z/)
end
def parse(tokenizer, parse_context, &block)
raise FrozenError, "can't modify frozen Liquid::BlockBody" if frozen?
parse_context.line_number = tokenizer.line_number
if tokenizer.for_liquid_tag
parse_for_liquid_tag(tokenizer, parse_context, &block)
else
parse_for_document(tokenizer, parse_context, &block)
end
end
def freeze
@nodelist.freeze
super
end
private def parse_for_liquid_tag(tokenizer, parse_context)
while (token = tokenizer.shift)
unless token.empty? || token =~ WhitespaceOrNothing
unless token =~ LiquidTagToken
# line isn't empty but didn't match tag syntax, yield and let the
# caller raise a syntax error
return yield token, token
end
rescue SyntaxError => e
e.set_line_number_from_token(token)
raise
tag_name = Regexp.last_match(1)
markup = Regexp.last_match(2)
unless (tag = registered_tags[tag_name])
# end parsing if we reach an unknown tag and let the caller decide
# determine how to proceed
return yield tag_name, markup
end
new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
@blank &&= new_tag.blank?
@nodelist << new_tag
end
parse_context.line_number = tokenizer.line_number
end
yield nil, nil
end
# @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)
next if token.empty?
case
when token.start_with?(TAGSTART)
whitespace_handler(token, parse_context)
unless token =~ FullToken
BlockBody.raise_missing_tag_terminator(token, parse_context)
end
tag_name = Regexp.last_match(2)
markup = Regexp.last_match(4)
if parse_context.line_number
# newlines inside the tag should increase the line number,
# particularly important for multiline {% liquid %} tags
parse_context.line_number += Regexp.last_match(1).count("\n") + Regexp.last_match(3).count("\n")
end
if tag_name == 'liquid'
parse_liquid_tag(markup, parse_context)
next
end
unless (tag = registered_tags[tag_name])
# end parsing if we reach an unknown tag and let the caller decide
# determine how to proceed
return yield tag_name, markup
end
new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
@blank &&= new_tag.blank?
@nodelist << new_tag
when token.start_with?(VARSTART)
whitespace_handler(token, parse_context)
@nodelist << create_variable(token, parse_context)
@blank = false
else
if parse_context.trim_whitespace
token.lstrip!
end
parse_context.trim_whitespace = false
@nodelist << token
@blank &&= !!(token =~ WhitespaceOrNothing)
end
parse_context.line_number = tokenizer.line_number
end
yield nil, nil
end
def whitespace_handler(token, parse_context)
if token[2] == WhitespaceControl
previous_token = @nodelist.last
if previous_token.is_a?(String)
first_byte = previous_token.getbyte(0)
previous_token.rstrip!
if previous_token.empty? && parse_context[:bug_compatible_whitespace_trimming] && first_byte
previous_token << first_byte
end
end
end
parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
end
def blank?
@blank
end
def warnings
all_warnings = []
nodelist.each do |node|
all_warnings.concat(node.warnings || []) if node.respond_to?(:warnings)
end
all_warnings
# 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)
output = []
context.resource_limits.render_score += @nodelist.length
@nodelist.each do |token|
# Break out if we have any unhanded interrupts.
break if context.has_interrupt?
begin
# If we get an Interrupt that means the block must stop processing. An
# Interrupt is any command that stops block execution such as {% break %}
# or {% continue %}
if token.is_a?(Continue) or token.is_a?(Break)
context.push_interrupt(token.interrupt)
break
end
token_output = render_token(token, context)
unless token.is_a?(Block) && token.blank?
output << token_output
end
rescue MemoryError => e
raise e
rescue ::StandardError => e
output << context.handle_error(e, token)
end
end
output.join
render_to_output_buffer(context, +'')
end
def format
@nodelist.map do |node|
if node.is_a?(Variable)
"{{ #{node.format} }}"
elsif node.is_a?(Tag)
node.format
def render_to_output_buffer(context, output)
freeze unless frozen?
context.resource_limits.increment_render_score(@nodelist.length)
idx = 0
while (node = @nodelist[idx])
if node.instance_of?(String)
output << node
else
node
render_node(context, output, node)
# If we get an Interrupt that means the block must stop processing. An
# Interrupt is any command that stops block execution such as {% break %}
# or {% continue %}. These tags may also occur through Block or Include tags.
break if context.interrupt? # might have happened in a for-block
end
end.join
idx += 1
context.resource_limits.increment_write_score(output)
end
output
end
private
def render_token(token, context)
token_output = (token.respond_to?(:render) ? token.render(context) : token)
token_str = token_output.is_a?(Array) ? token_output.join : token_output.to_s
context.resource_limits.render_length += token_str.length
if context.resource_limits.reached?
raise MemoryError.new("Memory limits exceeded".freeze)
end
token_str
def render_node(context, output, node)
BlockBody.render_node(context, output, node)
end
def create_variable(token, options)
def create_variable(token, parse_context)
token.scan(ContentOfVariable) do |content|
markup = token.is_a?(Token) ? token.child(content.first) : content.first
return Variable.new(markup, options)
markup = content.first
return Variable.new(markup, parse_context)
end
raise_missing_variable_terminator(token, options)
BlockBody.raise_missing_variable_terminator(token, parse_context)
end
def raise_missing_tag_terminator(token, options)
raise SyntaxError.new(options[:locale].t("errors.syntax.tag_termination".freeze, :token => token, :tag_end => TagEnd.inspect))
# @deprecated Use {.raise_missing_tag_terminator} instead
def raise_missing_tag_terminator(token, parse_context)
BlockBody.raise_missing_tag_terminator(token, parse_context)
end
def raise_missing_variable_terminator(token, options)
raise SyntaxError.new(options[:locale].t("errors.syntax.variable_termination".freeze, :token => token, :tag_end => VariableEnd.inspect))
# @deprecated Use {.raise_missing_variable_terminator} instead
def raise_missing_variable_terminator(token, parse_context)
BlockBody.raise_missing_variable_terminator(token, parse_context)
end
def registered_tags
Template.tags
end
end
end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid
# Container for liquid nodes which conveniently wraps decision making logic
#
@@ -8,59 +10,83 @@ module Liquid
#
class Condition #:nodoc:
@@operators = {
'=='.freeze => lambda { |cond, left, right| cond.send(:equal_variables, left, right) },
'!='.freeze => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) },
'<>'.freeze => lambda { |cond, left, right| !cond.send(:equal_variables, left, right) },
'<'.freeze => :<,
'>'.freeze => :>,
'>='.freeze => :>=,
'<='.freeze => :<=,
'contains'.freeze => lambda { |cond, left, right|
left && right && left.respond_to?(:include?) ? left.include?(right) : false
}
'==' => ->(cond, left, right) { cond.send(:equal_variables, left, right) },
'!=' => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
'<>' => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
'<' => :<,
'>' => :>,
'>=' => :>=,
'<=' => :<=,
'contains' => lambda do |_cond, left, right|
if left && right && left.respond_to?(:include?)
right = right.to_s if left.is_a?(String)
left.include?(right)
else
false
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
@@operators
end
attr_reader :attachment
def self.parse_expression(parse_context, markup)
@@method_literals[markup] || parse_context.parse_expression(markup)
end
attr_reader :attachment, :child_condition
attr_accessor :left, :operator, :right
def initialize(left = nil, operator = nil, right = nil)
@left = left
@left = left
@operator = operator
@right = right
@right = right
@child_relation = nil
@child_condition = nil
if right && right.is_a?(VariableLookup) && right.name.nil?
# Strip blank variable lookups resulting from cases like:
# {% if foo && bar %}
@right = nil
end
end
def evaluate(context = Context.new)
result = interpret_condition(left, right, operator, context)
condition = self
result = nil
loop do
result = interpret_condition(condition.left, condition.right, condition.operator, context)
case @child_relation
when :or
result || @child_condition.evaluate(context)
when :and
result && @child_condition.evaluate(context)
else
result
case condition.child_relation
when :or
break if result
when :and
break unless result
else
break
end
condition = condition.child_condition
end
result
end
def or(condition)
@child_relation = :or
@child_relation = :or
@child_condition = condition
end
def and(condition)
@child_relation = :and
@child_relation = :and
@child_condition = condition
end
@@ -73,37 +99,27 @@ module Liquid
end
def inspect
"#<Condition #{[@left, @operator, @right].compact.join(' '.freeze)}>"
"#<Condition #{[@left, @operator, @right].compact.join(' ')}>"
end
def format
out = if operator.nil?
Expression.format(left)
else
"#{Expression.format(left)} #{operator} #{Expression.format(right)}"
end
protected
if @child_relation
"#{out} #{@child_relation.to_s} #{@child_condition.format}"
else
out
end
end
attr_reader :child_relation
private
def equal_variables(left, right)
if left.is_a?(Symbol)
if right.respond_to?(left)
return right.send(left.to_s)
if left.is_a?(MethodLiteral)
if right.respond_to?(left.method_name)
return right.send(left.method_name)
else
return nil
end
end
if right.is_a?(Symbol)
if left.respond_to?(right)
return left.send(right.to_s)
if right.is_a?(MethodLiteral)
if left.respond_to?(right.method_name)
return left.send(right.method_name)
else
return nil
end
@@ -116,36 +132,41 @@ module Liquid
# If the operator is empty this means that the decision statement is just
# a single variable. We can just poll this variable from the context and
# 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)
operation = self.class.operators[op] || raise(Liquid::ArgumentError.new("Unknown operator #{op}"))
operation = self.class.operators[op] || raise(Liquid::ArgumentError, "Unknown operator #{op}")
if operation.respond_to?(:call)
operation.call(self, left, right)
elsif left.respond_to?(operation) and right.respond_to?(operation)
elsif left.respond_to?(operation) && right.respond_to?(operation) && !left.is_a?(Hash) && !right.is_a?(Hash)
begin
left.send(operation, right)
rescue ::ArgumentError => e
raise Liquid::ArgumentError.new(e.message)
raise Liquid::ArgumentError, e.message
end
else
nil
end
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[
@node.left, @node.right,
@node.child_condition, @node.attachment
].compact
end
end
end
class ElseCondition < Condition
def else?
true
end
def evaluate(context)
def evaluate(_context)
true
end
end
end

View File

@@ -1,5 +1,6 @@
module Liquid
# frozen_string_literal: true
module Liquid
# Context keeps the variable stack and resolves variables, as well as keywords
#
# context['variable'] = 'testing'
@@ -13,29 +14,49 @@ module Liquid
#
# context['bob'] #=> nil class Context
class Context
attr_reader :scopes, :errors, :registers, :environments, :resource_limits
attr_accessor :exception_handler
attr_reader :scopes, :errors, :registers, :environments, :resource_limits, :static_registers, :static_environments
attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil)
@environments = [environments].flatten
@scopes = [(outer_scope || {})]
@registers = registers
@errors = []
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
squash_instance_assigns_with_environments
# rubocop:disable Metrics/ParameterLists
def self.build(environments: {}, outer_scope: {}, registers: {}, rethrow_errors: false, resource_limits: nil, static_environments: {}, &block)
new(environments, outer_scope, registers, rethrow_errors, resource_limits, static_environments, &block)
end
@this_stack_used = false
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil, static_environments = {})
@environments = [environments]
@environments.flatten!
@static_environments = [static_environments].flat_map(&:freeze).freeze
@scopes = [(outer_scope || {})]
@registers = registers
@errors = []
@partial = false
@strict_variables = false
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
@base_scope_depth = 0
@interrupts = []
@filters = []
@global_filter = nil
@disabled_tags = {}
self.exception_renderer = Template.default_exception_renderer
if rethrow_errors
self.exception_handler = ->(e) { true }
self.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA
end
@interrupts = []
@filters = []
yield self if block_given?
# Do this last, since it could result in this object being passed to a Proc in the environment
squash_instance_assigns_with_environments
end
# rubocop:enable Metrics/ParameterLists
def warnings
@warnings ||= []
end
def strainer
@strainer ||= Strainer.create(self, @filters)
@strainer ||= StrainerFactory.create(self, @filters)
end
# Adds filters to this context.
@@ -48,8 +69,12 @@ module Liquid
@strainer = nil
end
def apply_global_filter(obj)
global_filter.nil? ? obj : global_filter.call(obj)
end
# are there any not handled interrupts?
def has_interrupt?
def interrupt?
!@interrupts.empty?
end
@@ -63,15 +88,12 @@ module Liquid
@interrupts.pop
end
def handle_error(e, token=nil)
if e.is_a?(Liquid::Error)
e.set_line_number_from_token(token)
end
def handle_error(e, line_number = nil)
e = internal_error unless e.is_a?(Liquid::Error)
e.template_name ||= template_name
e.line_number ||= line_number
errors.push(e)
raise if exception_handler && exception_handler.call(e)
Liquid::Error.render(e)
exception_renderer.call(e).to_s
end
def invoke(method, *args)
@@ -79,9 +101,9 @@ module Liquid
end
# Push new local scope on the stack. use <tt>Context#stack</tt> instead
def push(new_scope={})
def push(new_scope = {})
@scopes.unshift(new_scope)
raise StackLevelError, "Nesting too deep".freeze if @scopes.length > 100
check_overflow
end
# Merge a hash of variables in the current local scope
@@ -103,19 +125,31 @@ module Liquid
# end
#
# context['var] #=> nil
def stack(new_scope=nil)
old_stack_used = @this_stack_used
if new_scope
push(new_scope)
@this_stack_used = true
else
@this_stack_used = false
end
def stack(new_scope = {})
push(new_scope)
yield
ensure
pop if @this_stack_used
@this_stack_used = old_stack_used
pop
end
# Creates a new context inheriting resource limits, filters, environment etc.,
# but with an isolated scope.
def new_isolated_subcontext
check_overflow
self.class.build(
resource_limits: resource_limits,
static_environments: static_environments,
registers: StaticRegisters.new(registers)
).tap do |subcontext|
subcontext.base_scope_depth = base_scope_depth + 1
subcontext.exception_renderer = exception_renderer
subcontext.filters = @filters
subcontext.strainer = nil
subcontext.errors = errors
subcontext.warnings = warnings
subcontext.disabled_tags = @disabled_tags
end
end
def clear_instance_assigns
@@ -124,10 +158,6 @@ module Liquid
# Only allow String, Numeric, Hash, Array, Proc, Boolean or <tt>Liquid::Drop</tt>
def []=(key, value)
unless @this_stack_used
@this_stack_used = true
push({})
end
@scopes[0][key] = value
end
@@ -143,7 +173,7 @@ module Liquid
evaluate(Expression.parse(expression))
end
def has_key?(key)
def key?(key)
self[key] != nil
end
@@ -152,52 +182,100 @@ module Liquid
end
# Fetches an object starting at the local scope and then moving up the hierachy
def find_variable(key)
def find_variable(key, raise_on_not_found: true)
# This was changed from find() to find_index() because this is a very hot
# path and find_index() is optimized in MRI to reduce object allocation
index = @scopes.find_index { |s| s.has_key?(key) }
scope = @scopes[index] if index
index = @scopes.find_index { |s| s.key?(key) }
variable = nil
if scope.nil?
@environments.each do |e|
variable = lookup_and_evaluate(e, key)
unless variable.nil?
scope = e
break
end
end
variable = if index
lookup_and_evaluate(@scopes[index], key, raise_on_not_found: raise_on_not_found)
else
try_variable_find_in_environments(key, raise_on_not_found: raise_on_not_found)
end
scope ||= @environments.last || @scopes.last
variable ||= lookup_and_evaluate(scope, key)
variable = variable.to_liquid
variable = variable.to_liquid
variable.context = self if variable.respond_to?(:context=)
return variable
variable
end
def lookup_and_evaluate(obj, key)
if (value = obj[key]).is_a?(Proc) && obj.respond_to?(:[]=)
obj[key] = (value.arity == 0) ? value.call : value.call(self)
def lookup_and_evaluate(obj, key, raise_on_not_found: true)
if @strict_variables && raise_on_not_found && obj.respond_to?(:key?) && !obj.key?(key)
raise Liquid::UndefinedVariable, "undefined variable #{key}"
end
value = obj[key]
if value.is_a?(Proc) && obj.respond_to?(:[]=)
obj[key] = value.arity == 0 ? value.call : value.call(self)
else
value
end
end
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
attr_writer :base_scope_depth, :warnings, :errors, :strainer, :filters, :disabled_tags
private
def squash_instance_assigns_with_environments
@scopes.last.each_key do |k|
@environments.each do |env|
if env.has_key?(k)
scopes.last[k] = lookup_and_evaluate(env, k)
break
end
attr_reader :base_scope_depth
def try_variable_find_in_environments(key, raise_on_not_found:)
@environments.each do |environment|
found_variable = lookup_and_evaluate(environment, key, raise_on_not_found: raise_on_not_found)
if !found_variable.nil? || @strict_variables && raise_on_not_found
return found_variable
end
end
@static_environments.each do |environment|
found_variable = lookup_and_evaluate(environment, key, raise_on_not_found: raise_on_not_found)
if !found_variable.nil? || @strict_variables && raise_on_not_found
return found_variable
end
end
nil
end
def check_overflow
raise StackLevelError, "Nesting too deep" if overflow?
end
def overflow?
base_scope_depth + @scopes.length > Block::MAX_DEPTH
end
def internal_error
# raise and catch to set backtrace and cause on exception
raise Liquid::InternalError, 'internal'
rescue Liquid::InternalError => exc
exc
end
def squash_instance_assigns_with_environments
@scopes.last.each_key do |k|
@environments.each do |env|
if env.key?(k)
scopes.last[k] = lookup_and_evaluate(env, k)
break
end
end
end # squash_instance_assigns_with_environments
end
end # squash_instance_assigns_with_environments
end # Context
end # Liquid

View File

@@ -1,23 +1,64 @@
# frozen_string_literal: true
module Liquid
class Document < BlockBody
def self.parse(tokens, options)
doc = new
doc.parse(tokens, options)
class Document
def self.parse(tokens, parse_context)
doc = new(parse_context)
doc.parse(tokens, parse_context)
doc
end
def parse(tokens, options)
super do |end_tag_name, end_tag_params|
unknown_tag(end_tag_name, options) if end_tag_name
attr_reader :parse_context, :body
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
@body.freeze
rescue SyntaxError => e
e.line_number ||= parse_context.line_number
raise
end
def unknown_tag(tag, _markup, _tokenizer)
case tag
when 'else', 'end'
raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_outer_tag", tag: tag)
else
raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag)
end
end
def unknown_tag(tag, options)
case tag
when 'else'.freeze, 'end'.freeze
raise SyntaxError.new(options[:locale].t("errors.syntax.unexpected_outer_tag".freeze, :tag => tag))
else
raise SyntaxError.new(options[:locale].t("errors.syntax.unknown_tag".freeze, :tag => tag))
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

View File

@@ -1,7 +1,8 @@
# frozen_string_literal: true
require 'set'
module Liquid
# A drop in liquid is a class which allows you to export DOM like things to liquid.
# Methods of drops are callable.
# The main use for liquid drops is to implement lazy loaded objects.
@@ -19,28 +20,27 @@ module Liquid
# tmpl = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {%endfor%} ' )
# tmpl.render('product' => ProductDrop.new ) # will invoke top_sales query.
#
# Your drop can either implement the methods sans any parameters or implement the before_method(name) method which is a
# catch all.
# Your drop can either implement the methods sans any parameters
# or implement the liquid_method_missing(name) method which is a catch all.
class Drop
attr_writer :context
EMPTY_STRING = ''.freeze
# Catch all for the method
def before_method(method)
nil
def liquid_method_missing(method)
return nil unless @context&.strict_variables
raise Liquid::UndefinedDropMethod, "undefined method #{method}"
end
# called by liquid to invoke a drop
def invoke_drop(method_or_key)
if method_or_key && method_or_key != EMPTY_STRING && self.class.invokable?(method_or_key)
if self.class.invokable?(method_or_key)
send(method_or_key)
else
before_method(method_or_key)
liquid_method_missing(method_or_key)
end
end
def has_key?(name)
def key?(_name)
true
end
@@ -56,26 +56,25 @@ module Liquid
self.class.name
end
alias :[] :invoke_drop
private
alias_method :[], :invoke_drop
# Check for method existence without invoking respond_to?, which creates symbols
def self.invokable?(method_name)
self.invokable_methods.include?(method_name.to_s)
invokable_methods.include?(method_name.to_s)
end
def self.invokable_methods
unless @invokable_methods
@invokable_methods ||= begin
blacklist = Liquid::Drop.public_instance_methods + [:each]
if include?(Enumerable)
blacklist += Enumerable.public_instance_methods
blacklist -= [:sort, :count, :first, :min, :max, :include?]
blacklist -= [:sort, :count, :first, :min, :max]
end
whitelist = [:to_liquid] + (public_instance_methods - blacklist)
@invokable_methods = Set.new(whitelist.map(&:to_s))
Set.new(whitelist.map(&:to_s))
end
@invokable_methods
end
end
end

View File

@@ -1,10 +1,13 @@
# frozen_string_literal: true
module Liquid
class Error < ::StandardError
attr_accessor :line_number
attr_accessor :template_name
attr_accessor :markup_context
def to_s(with_prefix=true)
str = ""
def to_s(with_prefix = true)
str = +""
str << message_prefix if with_prefix
str << super()
@@ -16,32 +19,20 @@ module Liquid
str
end
def set_line_number_from_token(token)
return unless token.respond_to?(:line_number)
return if self.line_number
self.line_number = token.line_number
end
def self.render(e)
if e.is_a?(Liquid::Error)
e.to_s
else
"Liquid error: #{e.to_s}"
end
end
private
def message_prefix
str = ""
if is_a?(SyntaxError)
str << "Liquid syntax error"
str = +""
str << if is_a?(SyntaxError)
"Liquid syntax error"
else
str << "Liquid error"
"Liquid error"
end
if line_number
str << " (line #{line_number})"
str << " ("
str << template_name << " " if template_name
str << "line " << line_number.to_s << ")"
end
str << ": "
@@ -49,12 +40,19 @@ module Liquid
end
end
class ArgumentError < Error; end
class ContextError < Error; end
class FileSystemError < Error; end
class StandardError < Error; end
class SyntaxError < Error; end
class StackLevelError < Error; end
class TaintedError < Error; end
class MemoryError < Error; end
ArgumentError = Class.new(Error)
ContextError = Class.new(Error)
FileSystemError = Class.new(Error)
StandardError = Class.new(Error)
SyntaxError = Class.new(Error)
StackLevelError = Class.new(Error)
MemoryError = Class.new(Error)
ZeroDivisionError = Class.new(Error)
FloatDomainError = Class.new(Error)
UndefinedVariable = Class.new(Error)
UndefinedDropMethod = Class.new(Error)
UndefinedFilter = Class.new(Error)
MethodOverrideError = Class.new(Error)
DisabledError = Class.new(Error)
InternalError = Class.new(Error)
end

View File

@@ -1,48 +1,44 @@
# frozen_string_literal: true
module Liquid
class Expression
LITERALS = {
nil => nil, 'nil'.freeze => nil, 'null'.freeze => nil, ''.freeze => nil,
'true'.freeze => true,
'false'.freeze => false,
'blank'.freeze => :blank?,
'empty'.freeze => :empty?
}
nil => nil, 'nil' => nil, 'null' => nil, '' => nil,
'true' => true,
'false' => false,
'blank' => '',
'empty' => ''
}.freeze
INVERTED_LITERALS = LITERALS.invert
SINGLE_QUOTED_STRING = /\A\s*'(.*)'\s*\z/m
DOUBLE_QUOTED_STRING = /\A\s*"(.*)"\s*\z/m
INTEGERS_REGEX = /\A\s*(-?\d+)\s*\z/
FLOATS_REGEX = /\A\s*(-?\d[\d\.]+)\s*\z/
# 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)
if LITERALS.key?(markup)
LITERALS[markup]
case markup
when nil
nil
when SINGLE_QUOTED_STRING, DOUBLE_QUOTED_STRING
Regexp.last_match(1)
when INTEGERS_REGEX
Regexp.last_match(1).to_i
when RANGES_REGEX
RangeLookup.parse(Regexp.last_match(1), Regexp.last_match(2))
when FLOATS_REGEX
Regexp.last_match(1).to_f
else
case markup
when /\A'(.*)'\z/m # Single quoted strings
$1
when /\A"(.*)"\z/m # Double quoted strings
$1
when /\A(-?\d+)\z/ # Integer and floats
$1.to_i
when /\A\((\S+)\.\.(\S+)\)\z/ # Ranges
RangeLookup.parse($1, $2)
when /\A(-?\d[\d\.]+)\z/ # Floats
$1.to_f
markup = markup.strip
if LITERALS.key?(markup)
LITERALS[markup]
else
VariableLookup.parse(markup)
end
end
end
def self.format(value)
if INVERTED_LITERALS.key?(value)
INVERTED_LITERALS[value].dup
elsif value.is_a?(VariableLookup) || value.is_a?(RangeLookup)
value.format
elsif value.is_a?(String)
"\"#{value}\""
elsif value.is_a?(Range)
"(#{value.to_s})"
else
value.to_s
end
end
end
end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'time'
require 'date'
@@ -7,44 +9,56 @@ class String # :nodoc:
end
end
class Array # :nodoc:
class Symbol # :nodoc:
def to_liquid
to_s
end
end
class Array # :nodoc:
def to_liquid
self
end
end
class Hash # :nodoc:
class Hash # :nodoc:
def to_liquid
self
end
end
class Numeric # :nodoc:
class Numeric # :nodoc:
def to_liquid
self
end
end
class Time # :nodoc:
class Range # :nodoc:
def to_liquid
self
end
end
class DateTime < Date # :nodoc:
class Time # :nodoc:
def to_liquid
self
end
end
class Date # :nodoc:
class DateTime < Date # :nodoc:
def to_liquid
self
end
end
class Date # :nodoc:
def to_liquid
self
end
end
class TrueClass
def to_liquid # :nodoc:
def to_liquid # :nodoc:
self
end
end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid
# A Liquid file system is a way to let your templates retrieve other templates for use with the include tag.
#
@@ -8,13 +10,13 @@ module Liquid
#
# Example:
#
# Liquid::Template.file_system = Liquid::LocalFileSystem.new(template_path)
# liquid = Liquid::Template.parse(template)
# Liquid::Template.file_system = Liquid::LocalFileSystem.new(template_path)
# liquid = Liquid::Template.parse(template)
#
# This will parse the template with a LocalFileSystem implementation rooted at 'template_path'.
class BlankFileSystem
# Called by Liquid to retrieve a template file
def read_template_file(template_path)
def read_template_file(_template_path)
raise FileSystemError, "This liquid context does not allow includes."
end
end
@@ -26,10 +28,10 @@ module Liquid
#
# Example:
#
# file_system = Liquid::LocalFileSystem.new("/some/path")
# file_system = Liquid::LocalFileSystem.new("/some/path")
#
# file_system.full_path("mypartial") # => "/some/path/_mypartial.liquid"
# file_system.full_path("dir/mypartial") # => "/some/path/dir/_mypartial.liquid"
# file_system.full_path("mypartial") # => "/some/path/_mypartial.liquid"
# file_system.full_path("dir/mypartial") # => "/some/path/dir/_mypartial.liquid"
#
# Optionally in the second argument you can specify a custom pattern for template filenames.
# The Kernel::sprintf format specification is used.
@@ -37,35 +39,35 @@ module Liquid
#
# Example:
#
# file_system = Liquid::LocalFileSystem.new("/some/path", "%s.html")
# file_system = Liquid::LocalFileSystem.new("/some/path", "%s.html")
#
# file_system.full_path("index") # => "/some/path/index.html"
# file_system.full_path("index") # => "/some/path/index.html"
#
class LocalFileSystem
attr_accessor :root
def initialize(root, pattern = "_%s.liquid".freeze)
@root = root
def initialize(root, pattern = "_%s.liquid")
@root = root
@pattern = pattern
end
def read_template_file(template_path)
full_path = full_path(template_path)
raise FileSystemError, "No such template '#{template_path}'" unless File.exists?(full_path)
raise FileSystemError, "No such template '#{template_path}'" unless File.exist?(full_path)
File.read(full_path)
end
def full_path(template_path)
raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ /\A[^.\/][a-zA-Z0-9_\/]+\z/
raise FileSystemError, "Illegal template name '#{template_path}'" unless %r{\A[^./][a-zA-Z0-9_/]+\z}.match?(template_path)
full_path = if template_path.include?('/'.freeze)
full_path = if template_path.include?('/')
File.join(root, File.dirname(template_path), @pattern % File.basename(template_path))
else
File.join(root, @pattern % template_path)
end
raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path) =~ /\A#{File.expand_path(root)}/
raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path).start_with?(File.expand_path(root))
full_path
end

View File

@@ -0,0 +1,49 @@
# frozen_string_literal: true
module Liquid
class ForloopDrop < Drop
def initialize(name, length, parentloop)
@name = name
@length = length
@parentloop = parentloop
@index = 0
end
attr_reader :length, :parentloop
def name
Usage.increment('forloop_drop_name')
@name
end
def index
@index + 1
end
def index0
@index
end
def rindex
@length - @index
end
def rindex0
@length - @index - 1
end
def first
@index == 0
end
def last
@index == @length - 1
end
protected
def increment!
@index += 1
end
end
end

View File

@@ -1,11 +1,12 @@
# frozen_string_literal: true
require 'yaml'
module Liquid
class I18n
DEFAULT_LOCALE = File.join(File.expand_path(File.dirname(__FILE__)), "locales", "en.yml")
DEFAULT_LOCALE = File.join(File.expand_path(__dir__), "locales", "en.yml")
class TranslationError < StandardError
end
TranslationError = Class.new(StandardError)
attr_reader :path
@@ -23,16 +24,17 @@ module Liquid
end
private
def interpolate(name, vars)
name.gsub(/%\{(\w+)\}/) {
name.gsub(/%\{(\w+)\}/) do
# raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym]
"#{vars[$1.to_sym]}"
}
(vars[Regexp.last_match(1).to_sym]).to_s
end
end
def deep_fetch_translation(name)
name.split('.'.freeze).reduce(locale) do |level, cur|
level[cur] or raise TranslationError, "Translation for #{name} does not exist in locale #{path}"
name.split('.').reduce(locale) do |level, cur|
level[cur] || raise(TranslationError, "Translation for #{name} does not exist in locale #{path}")
end
end
end

View File

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

View File

@@ -1,45 +1,54 @@
# frozen_string_literal: true
require "strscan"
module Liquid
class Lexer
SPECIALS = {
'|'.freeze => :pipe,
'.'.freeze => :dot,
':'.freeze => :colon,
','.freeze => :comma,
'['.freeze => :open_square,
']'.freeze => :close_square,
'('.freeze => :open_round,
')'.freeze => :close_round,
'?'.freeze => :question,
'-'.freeze => :dash
}
IDENTIFIER = /[a-zA-Z_][\w-]*\??/
'|' => :pipe,
'.' => :dot,
':' => :colon,
',' => :comma,
'[' => :open_square,
']' => :close_square,
'(' => :open_round,
')' => :close_round,
'?' => :question,
'-' => :dash,
}.freeze
IDENTIFIER = /[a-zA-Z_][\w-]*\??/
SINGLE_STRING_LITERAL = /'[^\']*'/
DOUBLE_STRING_LITERAL = /"[^\"]*"/
NUMBER_LITERAL = /-?\d+(\.\d+)?/
DOTDOT = /\.\./
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains/
NUMBER_LITERAL = /-?\d+(\.\d+)?/
DOTDOT = /\.\./
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/
WHITESPACE_OR_NOTHING = /\s*/
def initialize(input)
@ss = StringScanner.new(input.rstrip)
@ss = StringScanner.new(input)
end
def tokenize
@output = []
while !@ss.eos?
@ss.skip(/\s*/)
tok = case
when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t]
when t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t]
when t = @ss.scan(DOUBLE_STRING_LITERAL) then [:string, t]
when t = @ss.scan(NUMBER_LITERAL) then [:number, t]
when t = @ss.scan(IDENTIFIER) then [:id, t]
when t = @ss.scan(DOTDOT) then [:dotdot, t]
until @ss.eos?
@ss.skip(WHITESPACE_OR_NOTHING)
break if @ss.eos?
tok = if (t = @ss.scan(COMPARISON_OPERATOR))
[:comparison, t]
elsif (t = @ss.scan(SINGLE_STRING_LITERAL))
[:string, t]
elsif (t = @ss.scan(DOUBLE_STRING_LITERAL))
[:string, t]
elsif (t = @ss.scan(NUMBER_LITERAL))
[:number, t]
elsif (t = @ss.scan(IDENTIFIER))
[:id, t]
elsif (t = @ss.scan(DOTDOT))
[:dotdot, t]
else
c = @ss.getch
if s = SPECIALS[c]
[s,c]
c = @ss.getch
if (s = SPECIALS[c])
[s, c]
else
raise SyntaxError, "Unexpected character #{c}"
end

View File

@@ -1,6 +1,7 @@
---
errors:
syntax:
tag_unexpected_args: "Syntax Error in '%{tag}' - Valid syntax: %{tag}"
assign: "Syntax Error in 'assign' - Valid syntax: assign [var] = [source]"
capture: "Syntax Error in 'capture' - Valid syntax: capture [var]"
case: "Syntax Error in 'case' - Valid syntax: case [condition]"
@@ -13,11 +14,15 @@
if: "Syntax Error in tag 'if' - Valid syntax: if [expression]"
include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]"
unknown_tag: "Unknown tag '%{tag}'"
invalid_delimiter: "'end' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
invalid_delimiter: "'%{tag}' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
unexpected_else: "%{block_name} tag does not expect 'else' tag"
unexpected_outer_tag: "Unexpected outer '%{tag}' tag"
tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}"
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"
render: "Syntax error in tag 'render' - Template name must be a quoted string"
argument:
include: "Argument error in tag 'include' - Illegal template name"
disabled:
tag: "usage is not allowed in this context"

View File

@@ -1,64 +0,0 @@
# Copyright 2007 by Domizio Demichelis
# This library is free software. It may be used, redistributed and/or modified
# under the same terms as Ruby itself
#
# This extension is used in order to expose the object of the implementing class
# to liquid as it were a Drop. It also limits the liquid-callable methods of the instance
# to the allowed method passed with the liquid_methods call
# Example:
#
# class SomeClass
# liquid_methods :an_allowed_method
#
# def an_allowed_method
# 'this comes from an allowed method'
# end
#
# def unallowed_method
# 'this will never be an output'
# end
# end
#
# if you want to extend the drop to other methods you can defines more methods
# in the class <YourClass>::LiquidDropClass
#
# class SomeClass::LiquidDropClass
# def another_allowed_method
# 'and this from another allowed method'
# end
# end
#
#
# usage:
# @something = SomeClass.new
#
# template:
# {{something.an_allowed_method}}{{something.unallowed_method}} {{something.another_allowed_method}}
#
# output:
# 'this comes from an allowed method and this from another allowed method'
#
# You can also chain associations, by adding the liquid_method call in the
# association models.
#
class Module
def liquid_methods(*allowed_methods)
drop_class = eval "class #{self.to_s}::LiquidDropClass < Liquid::Drop; self; end"
define_method :to_liquid do
drop_class.new(self)
end
drop_class.class_eval do
def initialize(object)
@object = object
end
allowed_methods.each do |sym|
define_method sym do
@object.send sym
end
end
end
end
end

View File

@@ -0,0 +1,54 @@
# frozen_string_literal: true
module Liquid
class ParseContext
attr_accessor :locale, :line_number, :trim_whitespace, :depth
attr_reader :partial, :warnings, :error_mode
def initialize(options = {})
@template_options = options ? options.dup : {}
@locale = @template_options[:locale] ||= I18n.new
@warnings = []
self.depth = 0
self.partial = false
end
def [](option_key)
@options[option_key]
end
def 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)
@partial = value
@options = value ? partial_options : @template_options
@error_mode = @options[:error_mode] || Template.error_mode
end
def partial_options
@partial_options ||= begin
dont_pass = @template_options[:include_options_blacklist]
if dont_pass == true
{ locale: locale }
elsif dont_pass.is_a?(Array)
@template_options.reject { |k, _v| dont_pass.include?(k) }
else
@template_options
end
end
end
end
end

View File

@@ -0,0 +1,42 @@
# frozen_string_literal: true
module Liquid
class ParseTreeVisitor
def self.for(node, callbacks = Hash.new(proc {}))
if defined?(node.class::ParseTreeVisitor)
node.class::ParseTreeVisitor
else
self
end.new(node, callbacks)
end
def initialize(node, callbacks)
@node = node
@callbacks = callbacks
end
def add_callback_for(*classes, &block)
callback = block
callback = ->(node, _) { yield node } if block.arity.abs == 1
callback = ->(_, _) { yield } if block.arity.zero?
classes.each { |klass| @callbacks[klass] = callback }
self
end
def visit(context = nil)
children.map do |node|
item, new_context = @callbacks[node.class].call(node, context)
[
item,
ParseTreeVisitor.for(node, @callbacks).visit(new_context || context),
]
end
end
protected
def children
@node.respond_to?(:nodelist) ? Array(@node.nodelist) : []
end
end
end

View File

@@ -1,9 +1,11 @@
# frozen_string_literal: true
module Liquid
class Parser
def initialize(input)
l = Lexer.new(input)
l = Lexer.new(input)
@tokens = l.tokenize
@p = 0 # pointer to current location
@p = 0 # pointer to current location
end
def jump(point)
@@ -46,11 +48,18 @@ module Liquid
def expression
token = @tokens[@p]
if token[0] == :id
variable_signature
elsif [:string, :number].include? token[0]
case token[0]
when :id
str = consume
str << variable_lookups
when :open_square
str = consume
str << expression
str << consume(:close_square)
str << variable_lookups
when :string, :number
consume
elsif token.first == :open_round
when :open_round
consume
first = expression
consume(:dotdot)
@@ -63,26 +72,29 @@ module Liquid
end
def argument
str = ""
str = +""
# might be a keyword argument (identifier: expression)
if look(:id) && look(:colon, 1)
str << consume << consume << ' '.freeze
str << consume << consume << ' '
end
str << expression
str
end
def variable_signature
str = consume(:id)
if look(:open_square)
str << consume
str << expression
str << consume(:close_square)
end
if look(:dot)
str << consume
str << variable_signature
def variable_lookups
str = +""
loop do
if look(:open_square)
str << consume
str << expression
str << consume(:close_square)
elsif look(:dot)
str << consume
str << consume(:id)
else
break
end
end
str
end

View File

@@ -1,25 +1,39 @@
# frozen_string_literal: true
module Liquid
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)
case @options[:error_mode] || Template.error_mode
case parse_context.error_mode
when :strict then strict_parse_with_error_context(markup)
when :lax then lax_parse(markup)
when :warn
begin
return strict_parse_with_error_context(markup)
strict_parse_with_error_context(markup)
rescue SyntaxError => e
e.set_line_number_from_token(markup)
@warnings ||= []
@warnings << e
return lax_parse(markup)
parse_context.warnings << e
lax_parse(markup)
end
end
end
private
def strict_parse_with_error_context(markup)
strict_parse(markup)
rescue SyntaxError => e
e.line_number = line_number
e.markup_context = markup_context(markup)
raise e
end

View File

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

View File

@@ -1,7 +1,8 @@
# frozen_string_literal: true
require 'liquid/profiler/hooks'
module Liquid
# Profiler enables support for profiling template rendering to help track down performance issues.
#
# To enable profiling, first require 'liquid/profiler'.
@@ -20,11 +21,11 @@ module Liquid
# inside of <tt>{% include %}</tt> tags.
#
# profile.each do |node|
# # Access to the token itself
# # Access to the node itself
# node.code
#
# # Which template and line number of this node.
# # If top level, this will be "<root>".
# # The top-level template name is `nil` by default, but can be set in the Liquid::Context before rendering.
# node.partial
# node.line_number
#
@@ -45,118 +46,94 @@ module Liquid
include Enumerable
class Timing
attr_reader :code, :partial, :line_number, :children
attr_reader :code, :template_name, :line_number, :children
attr_accessor :total_time
alias_method :render_time, :total_time
alias_method :partial, :template_name
def initialize(token, partial)
@code = token.respond_to?(:raw) ? token.raw : token
@partial = partial
@line_number = token.respond_to?(:line_number) ? token.line_number : nil
@children = []
def initialize(code: nil, template_name: nil, line_number: nil)
@code = code
@template_name = template_name
@line_number = line_number
@children = []
end
def self.start(token, partial)
new(token, partial).tap do |t|
t.start
def self_time
@self_time ||= begin
total_children_time = 0.0
@children.each do |child|
total_children_time += child.total_time
end
@total_time - total_children_time
end
end
def start
@start_time = Time.now
end
def finish
@end_time = Time.now
end
def render_time
@end_time - @start_time
end
end
def self.profile_token_render(token)
if Profiler.current_profile && token.respond_to?(:render)
Profiler.current_profile.start_token(token)
output = yield
Profiler.current_profile.end_token(token)
output
else
yield
end
end
def self.profile_children(template_name)
if Profiler.current_profile
Profiler.current_profile.push_partial(template_name)
output = yield
Profiler.current_profile.pop_partial
output
else
yield
end
end
def self.current_profile
Thread.current[:liquid_profiler]
end
attr_reader :total_time
alias_method :total_render_time, :total_time
def initialize
@partial_stack = ["<root>"]
@root_timing = Timing.new("", current_partial)
@timing_stack = [@root_timing]
@render_start_at = Time.now
@render_end_at = @render_start_at
@root_children = []
@current_children = nil
@total_time = 0.0
end
def start
Thread.current[:liquid_profiler] = self
@render_start_at = Time.now
def profile(template_name, &block)
# nested renders are done from a tag that already has a timing node
return yield if @current_children
root_children = @root_children
render_idx = root_children.length
begin
@current_children = root_children
profile_node(template_name, &block)
ensure
@current_children = nil
if (timing = root_children[render_idx])
@total_time += timing.total_time
end
end
end
def stop
Thread.current[:liquid_profiler] = nil
@render_end_at = Time.now
end
def total_render_time
@render_end_at - @render_start_at
def children
children = @root_children
if children.length == 1
children.first.children
else
children
end
end
def each(&block)
@root_timing.children.each(&block)
children.each(&block)
end
def [](idx)
@root_timing.children[idx]
children[idx]
end
def length
@root_timing.children.length
children.length
end
def start_token(token)
@timing_stack.push(Timing.start(token, current_partial))
def profile_node(template_name, code: nil, line_number: nil)
timing = Timing.new(code: code, template_name: template_name, line_number: line_number)
parent_children = @current_children
start_time = monotonic_time
begin
@current_children = timing.children
yield
ensure
@current_children = parent_children
timing.total_time = monotonic_time - start_time
parent_children << timing
end
end
def end_token(token)
timing = @timing_stack.pop
timing.finish
private
@timing_stack.last.children << timing
def monotonic_time
Process.clock_gettime(Process::CLOCK_MONOTONIC)
end
def current_partial
@partial_stack.last
end
def push_partial(partial_name)
@partial_stack.push(partial_name)
end
def pop_partial
@partial_stack.pop
end
end
end

View File

@@ -1,23 +1,35 @@
# frozen_string_literal: true
module Liquid
class BlockBody
def render_token_with_profiling(token, context)
Profiler.profile_token_render(token) do
render_token_without_profiling(token, context)
module BlockBodyProfilingHook
def render_node(context, output, node)
if (profiler = context.profiler)
profiler.profile_node(context.template_name, code: node.raw, line_number: node.line_number) do
super
end
else
super
end
end
alias_method :render_token_without_profiling, :render_token
alias_method :render_token, :render_token_with_profiling
end
BlockBody.prepend(BlockBodyProfilingHook)
class Include < Tag
def render_with_profiling(context)
Profiler.profile_children(context.evaluate(@template_name_expr).to_s) do
render_without_profiling(context)
end
module DocumentProfilingHook
def render_to_output_buffer(context, output)
return super unless context.profiler
context.profiler.profile(context.template_name) { super }
end
alias_method :render_without_profiling, :render
alias_method :render, :render_with_profiling
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

View File

@@ -1,8 +1,10 @@
# frozen_string_literal: true
module Liquid
class RangeLookup
def self.parse(start_markup, end_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)
new(start_obj, end_obj)
else
@@ -12,15 +14,26 @@ module Liquid
def initialize(start_obj, end_obj)
@start_obj = start_obj
@end_obj = end_obj
@end_obj = end_obj
end
def evaluate(context)
context.evaluate(@start_obj).to_i..context.evaluate(@end_obj).to_i
start_int = to_integer(context.evaluate(@start_obj))
end_int = to_integer(context.evaluate(@end_obj))
start_int..end_int
end
def format
"(#{Expression.format(@start_obj)}..#{Expression.format(@end_obj)})"
private
def to_integer(input)
case input
when Integer
input
when NilClass, String
input.to_i
else
Utils.to_integer(input)
end
end
end
end

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

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

View File

@@ -1,23 +1,62 @@
# frozen_string_literal: true
module Liquid
class ResourceLimits
attr_accessor :render_length, :render_score, :assign_score,
:render_length_limit, :render_score_limit, :assign_score_limit
attr_accessor :render_length_limit, :render_score_limit, :assign_score_limit
attr_reader :render_score, :assign_score
def initialize(limits)
@render_length_limit = limits[:render_length_limit]
@render_score_limit = limits[:render_score_limit]
@assign_score_limit = limits[:assign_score_limit]
@render_score_limit = limits[:render_score_limit]
@assign_score_limit = limits[:assign_score_limit]
reset
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?
(@render_length_limit && @render_length > @render_length_limit) ||
(@render_score_limit && @render_score > @render_score_limit ) ||
(@assign_score_limit && @assign_score > @assign_score_limit )
@reached_limit
end
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

View File

@@ -1,17 +1,24 @@
# frozen_string_literal: true
require 'cgi'
require 'bigdecimal'
module Liquid
module StandardFilters
HTML_ESCAPE = {
'&'.freeze => '&amp;'.freeze,
'>'.freeze => '&gt;'.freeze,
'<'.freeze => '&lt;'.freeze,
'"'.freeze => '&quot;'.freeze,
"'".freeze => '&#39;'.freeze
}
'&' => '&amp;',
'>' => '&gt;',
'<' => '&lt;',
'"' => '&quot;',
"'" => '&#39;',
}.freeze
HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/
STRIP_HTML_BLOCKS = Regexp.union(
%r{<script.*?</script>}m,
/<!--.*?-->/m,
%r{<style.*?</style>}m
)
STRIP_HTML_TAGS = /<.*?>/m
# Return the size of an array or of an string
def size(input)
@@ -34,7 +41,7 @@ module Liquid
end
def escape(input)
CGI.escapeHTML(input).untaint rescue input
CGI.escapeHTML(input.to_s) unless input.nil?
end
alias_method :h, :escape
@@ -43,12 +50,21 @@ module Liquid
end
def url_encode(input)
CGI.escape(input) rescue input
CGI.escape(input.to_s) unless input.nil?
end
def slice(input, offset, length=nil)
offset = Integer(offset)
length = length ? Integer(length) : 1
def url_decode(input)
return if input.nil?
result = CGI.unescape(input.to_s)
raise Liquid::ArgumentError, "invalid byte sequence in #{result.encoding}" unless result.valid_encoding?
result
end
def slice(input, offset, length = nil)
offset = Utils.to_integer(offset)
length = length ? Utils.to_integer(length) : 1
if input.is_a?(Array)
input.slice(offset, length) || []
@@ -58,19 +74,28 @@ module Liquid
end
# Truncate a string down to x characters
def truncate(input, length = 50, truncate_string = "...".freeze)
if input.nil? then return end
l = length.to_i - truncate_string.length
def truncate(input, length = 50, truncate_string = "...")
return if input.nil?
input_str = input.to_s
length = Utils.to_integer(length)
truncate_string_str = truncate_string.to_s
l = length - truncate_string_str.length
l = 0 if l < 0
input.length > length.to_i ? input[0...l] + truncate_string : input
input_str.length > length ? input_str[0...l].concat(truncate_string_str) : input_str
end
def truncatewords(input, words = 15, truncate_string = "...".freeze)
if input.nil? then return end
def truncatewords(input, words = 15, truncate_string = "...")
return if input.nil?
wordlist = input.to_s.split
l = words.to_i - 1
words = Utils.to_integer(words)
l = words - 1
l = 0 if l < 0
wordlist.length > l ? wordlist[0..l].join(" ".freeze) + truncate_string : input
wordlist.length > l ? wordlist[0..l].join(" ").concat(truncate_string.to_s) : input
end
# Split input string into an array of substrings separated by given pattern.
@@ -79,7 +104,7 @@ module Liquid
# <div class="summary">{{ post | split '//' | first }}</div>
#
def split(input, pattern)
input.to_s.split(pattern)
input.to_s.split(pattern.to_s)
end
def strip(input)
@@ -95,95 +120,160 @@ module Liquid
end
def strip_html(input)
empty = ''.freeze
input.to_s.gsub(/<script.*?<\/script>/m, empty).gsub(/<!--.*?-->/m, empty).gsub(/<style.*?<\/style>/m, empty).gsub(/<.*?>/m, empty)
empty = ''
result = input.to_s.gsub(STRIP_HTML_BLOCKS, empty)
result.gsub!(STRIP_HTML_TAGS, empty)
result
end
# Remove all newlines from the string
def strip_newlines(input)
input.to_s.gsub(/\r?\n/, ''.freeze)
input.to_s.gsub(/\r?\n/, '')
end
# Join elements of the array with certain character between them
def join(input, glue = ' '.freeze)
InputIterator.new(input).join(glue)
def join(input, glue = ' ')
InputIterator.new(input, context).join(glue)
end
# Sort elements of the array
# provide optional property with which to sort an array of hashes or drops
def sort(input, property = nil)
ary = InputIterator.new(input)
ary = InputIterator.new(input, context)
return [] if ary.empty?
if property.nil?
ary.sort
elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
ary.sort {|a,b| a[property] <=> b[property] }
elsif ary.first.respond_to?(property)
ary.sort {|a,b| a.send(property) <=> b.send(property) }
ary.sort do |a, b|
nil_safe_compare(a, b)
end
elsif ary.all? { |el| el.respond_to?(:[]) }
begin
ary.sort { |a, b| nil_safe_compare(a[property], b[property]) }
rescue TypeError
raise_property_error(property)
end
end
end
# Sort elements of an array ignoring case if strings
# provide optional property with which to sort an array of hashes or drops
def sort_natural(input, property = nil)
ary = InputIterator.new(input)
ary = InputIterator.new(input, context)
return [] if ary.empty?
if property.nil?
ary.sort {|a,b| a.casecmp(b) }
elsif ary.first.respond_to?(:[]) && !ary.first[property].nil?
ary.sort {|a,b| a[property].casecmp(b[property]) }
elsif ary.first.respond_to?(property)
ary.sort {|a,b| a.send(property).casecmp(b.send(property)) }
ary.sort do |a, b|
nil_safe_casecmp(a, b)
end
elsif ary.all? { |el| el.respond_to?(:[]) }
begin
ary.sort { |a, b| nil_safe_casecmp(a[property], b[property]) }
rescue TypeError
raise_property_error(property)
end
end
end
# Filter the elements of an array to those with a certain property value.
# By default the target is any truthy value.
def where(input, property, target_value = nil)
ary = InputIterator.new(input, context)
if ary.empty?
[]
elsif ary.first.respond_to?(:[]) && target_value.nil?
begin
ary.select { |item| item[property] }
rescue TypeError
raise_property_error(property)
end
elsif ary.first.respond_to?(:[])
begin
ary.select { |item| item[property] == target_value }
rescue TypeError
raise_property_error(property)
end
end
end
# Remove duplicate elements from an array
# provide optional property with which to determine uniqueness
def uniq(input, property = nil)
ary = InputIterator.new(input)
ary = InputIterator.new(input, context)
if property.nil?
input.uniq
elsif input.first.respond_to?(:[])
input.uniq{ |a| a[property] }
ary.uniq
elsif ary.empty? # The next two cases assume a non-empty array.
[]
elsif ary.first.respond_to?(:[])
begin
ary.uniq { |a| a[property] }
rescue TypeError
raise_property_error(property)
end
end
end
# Reverse the elements of an array
def reverse(input)
ary = InputIterator.new(input)
ary = InputIterator.new(input, context)
ary.reverse
end
# map/collect on a given property
def map(input, property)
InputIterator.new(input).map do |e|
InputIterator.new(input, context).map do |e|
e = e.call if e.is_a?(Proc)
if property == "to_liquid".freeze
if property == "to_liquid"
e
elsif e.respond_to?(:[])
e[property]
r = e[property]
r.is_a?(Proc) ? r.call : r
end
end
rescue TypeError
raise_property_error(property)
end
# Remove nils within an array
# provide optional property with which to check for nil
def compact(input, property = nil)
ary = InputIterator.new(input, context)
if property.nil?
ary.compact
elsif ary.empty? # The next two cases assume a non-empty array.
[]
elsif ary.first.respond_to?(:[])
begin
ary.reject { |a| a[property].nil? }
rescue TypeError
raise_property_error(property)
end
end
end
# Replace occurrences of a string with another
def replace(input, string, replacement = ''.freeze)
input.to_s.gsub(string, replacement.to_s)
def replace(input, string, replacement = '')
input.to_s.gsub(string.to_s, replacement.to_s)
end
# Replace the first occurrences of a string with another
def replace_first(input, string, replacement = ''.freeze)
input.to_s.sub(string, replacement.to_s)
def replace_first(input, string, replacement = '')
input.to_s.sub(string.to_s, replacement.to_s)
end
# remove a substring
def remove(input, string)
input.to_s.gsub(string, ''.freeze)
input.to_s.gsub(string.to_s, '')
end
# remove the first occurrences of a substring
def remove_first(input, string)
input.to_s.sub(string, ''.freeze)
input.to_s.sub(string.to_s, '')
end
# add one string to another
@@ -192,7 +282,10 @@ module Liquid
end
def concat(input, array)
InputIterator.new(input).concat(array)
unless array.respond_to?(:to_ary)
raise ArgumentError, "concat filter requires an array argument"
end
InputIterator.new(input, context).concat(array)
end
# prepend a string to another
@@ -202,7 +295,7 @@ module Liquid
# Add <br /> tags in front of all newlines in input string
def newline_to_br(input)
input.to_s.gsub(/\n/, "<br />\n".freeze)
input.to_s.gsub(/\n/, "<br />\n")
end
# Reformat a date using Ruby's core Time#strftime( string ) -> string
@@ -239,7 +332,7 @@ module Liquid
def date(input, format)
return input if format.to_s.empty?
return input unless date = to_date(input)
return input unless (date = Utils.to_date(input))
date.strftime(format.to_s)
end
@@ -262,6 +355,12 @@ module Liquid
array.last if array.respond_to?(:last)
end
# absolute value
def abs(input)
result = Utils.to_number(input).abs
result.is_a?(BigDecimal) ? result.to_f : result
end
# addition
def plus(input, operand)
apply_operation(input, operand, :+)
@@ -280,79 +379,104 @@ module Liquid
# division
def divided_by(input, operand)
apply_operation(input, operand, :/)
rescue ::ZeroDivisionError => e
raise Liquid::ZeroDivisionError, e.message
end
def modulo(input, operand)
apply_operation(input, operand, :%)
rescue ::ZeroDivisionError => e
raise Liquid::ZeroDivisionError, e.message
end
def round(input, n = 0)
result = to_number(input).round(to_number(n))
result = Utils.to_number(input).round(Utils.to_number(n))
result = result.to_f if result.is_a?(BigDecimal)
result = result.to_i if n == 0
result
rescue ::FloatDomainError => e
raise Liquid::FloatDomainError, e.message
end
def ceil(input)
to_number(input).ceil.to_i
Utils.to_number(input).ceil.to_i
rescue ::FloatDomainError => e
raise Liquid::FloatDomainError, e.message
end
def floor(input)
to_number(input).floor.to_i
Utils.to_number(input).floor.to_i
rescue ::FloatDomainError => e
raise Liquid::FloatDomainError, e.message
end
def default(input, default_value = "".freeze)
is_blank = input.respond_to?(:empty?) ? input.empty? : !input
is_blank ? default_value : input
def at_least(input, n)
min_value = Utils.to_number(n)
result = Utils.to_number(input)
result = min_value if min_value > result
result.is_a?(BigDecimal) ? result.to_f : result
end
def at_most(input, n)
max_value = Utils.to_number(n)
result = Utils.to_number(input)
result = max_value if max_value < result
result.is_a?(BigDecimal) ? result.to_f : result
end
# Set a default value when the input is nil, false or empty
#
# Example:
# {{ product.title | default: "No Title" }}
#
# Use `allow_false` when an input should only be tested against nil or empty and not false.
#
# Example:
# {{ product.title | default: "No Title", allow_false: true }}
#
def default(input, default_value = '', options = {})
options = {} unless options.is_a?(Hash)
false_check = options['allow_false'] ? input.nil? : !input
false_check || (input.respond_to?(:empty?) && input.empty?) ? default_value : input
end
private
def to_number(obj)
case obj
when Float
BigDecimal.new(obj.to_s)
when Numeric
obj
when String
(obj.strip =~ /\A\d+\.\d+\z/) ? BigDecimal.new(obj) : obj.to_i
else
0
end
end
attr_reader :context
def to_date(obj)
return obj if obj.respond_to?(:strftime)
if obj.is_a?(String)
return nil if obj.empty?
obj = obj.downcase
end
case obj
when 'now'.freeze, 'today'.freeze
Time.now
when /\A\d+\z/, Integer
Time.at(obj.to_i)
when String
Time.parse(obj)
else
nil
end
rescue ArgumentError
nil
def raise_property_error(property)
raise Liquid::ArgumentError, "cannot select the property '#{property}'"
end
def apply_operation(input, operand, operation)
result = to_number(input).send(operation, to_number(operand))
result = Utils.to_number(input).send(operation, Utils.to_number(operand))
result.is_a?(BigDecimal) ? result.to_f : result
end
def nil_safe_compare(a, b)
if !a.nil? && !b.nil?
a <=> b
else
a.nil? ? 1 : -1
end
end
def nil_safe_casecmp(a, b)
if !a.nil? && !b.nil?
a.to_s.casecmp(b.to_s)
else
a.nil? ? 1 : -1
end
end
class InputIterator
include Enumerable
def initialize(input)
@input = if input.is_a?(Array)
def initialize(input, context)
@context = context
@input = if input.is_a?(Array)
input.flatten
elsif input.is_a?(Hash)
[input]
@@ -364,19 +488,33 @@ module Liquid
end
def join(glue)
to_a.join(glue)
to_a.join(glue.to_s)
end
def concat(args)
to_a.concat args
to_a.concat(args)
end
def reverse
reverse_each.to_a
end
def uniq(&block)
to_a.uniq(&block)
end
def compact
to_a.compact
end
def empty?
@input.each { return false }
true
end
def each
@input.each do |e|
e.context = @context if e.respond_to?(:context=)
yield(e.respond_to?(:to_liquid) ? e.to_liquid : e)
end
end

View File

@@ -0,0 +1,44 @@
# frozen_string_literal: true
module Liquid
class StaticRegisters
attr_reader :static
def initialize(registers = {})
@static = registers.is_a?(StaticRegisters) ? registers.static : registers
@registers = {}
end
def []=(key, value)
@registers[key] = value
end
def [](key)
if @registers.key?(key)
@registers[key]
else
@static[key]
end
end
def delete(key)
@registers.delete(key)
end
UNDEFINED = Object.new
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
def key?(key)
@registers.key?(key) || @static.key?(key)
end
end
end

View File

@@ -1,59 +0,0 @@
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
def self.filter_methods
@filter_methods
end
def self.add_filter(filter)
raise ArgumentError, "Expected module but got: #{f.class}" unless filter.is_a?(Module)
unless self.class.include?(filter)
self.send(:include, filter)
@filter_methods.merge(filter.public_instance_methods.map(&:to_s))
end
end
def self.global_filter(filter)
@@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)
else
args.first
end
rescue ::ArgumentError => e
raise Liquid::ArgumentError.new(e.message)
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

@@ -0,0 +1,64 @@
# frozen_string_literal: true
module Liquid
class TablerowloopDrop < Drop
def initialize(length, cols)
@length = length
@row = 1
@col = 1
@cols = cols
@index = 0
end
attr_reader :length, :col, :row
def index
@index + 1
end
def index0
@index
end
def col0
@col - 1
end
def rindex
@length - @index
end
def rindex0
@length - @index - 1
end
def first
@index == 0
end
def last
@index == @length - 1
end
def col_first
@col == 1
end
def col_last
@col == @cols
end
protected
def increment!
@index += 1
if @col == @cols
@col = 1
@row += 1
else
@col += 1
end
end
end
end

View File

@@ -1,49 +1,65 @@
# frozen_string_literal: true
module Liquid
class Tag
attr_accessor :options, :line_number
attr_reader :nodelist, :warnings
attr_reader :nodelist, :tag_name, :line_number, :parse_context
alias_method :options, :parse_context
include ParserSwitching
class << self
def parse(tag_name, markup, tokens, options)
tag = new(tag_name, markup, options)
tag.parse(tokens)
def parse(tag_name, markup, tokenizer, parse_context)
tag = new(tag_name, markup, parse_context)
tag.parse(tokenizer)
tag
end
def disable_tags(*tag_names)
@disabled_tags ||= []
@disabled_tags.concat(tag_names)
prepend(Disabler)
end
private :new
end
def initialize(tag_name, markup, options)
@tag_name = tag_name
@markup = markup
@options = options
def initialize(tag_name, markup, parse_context)
@tag_name = tag_name
@markup = markup
@parse_context = parse_context
@line_number = parse_context.line_number
end
def parse(tokens)
def parse(_tokens)
end
def raw
tag = @tag_name.strip
markup = @markup.strip
tag << " #{markup}" unless markup.empty?
tag
"#{@tag_name} #{@markup}"
end
def name
self.class.name.downcase
end
def render(context)
''.freeze
def render(_context)
''
end
# For backwards compatibility with custom tags. In a future release, the semantics
# of the `render_to_output_buffer` method will become the default and the `render`
# method will be removed.
def render_to_output_buffer(context, output)
output << render(context)
output
end
def blank?
false
end
def format
"{% #{raw} %}"
private
def parse_expression(markup)
parse_context.parse_expression(markup)
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

@@ -1,5 +1,6 @@
module Liquid
# frozen_string_literal: true
module Liquid
# Assign sets a variable in your template.
#
# {% assign foo = 'monkey' %}
@@ -11,35 +12,62 @@ module Liquid
class Assign < Tag
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
def initialize(tag_name, markup, options)
# @api private
def self.raise_syntax_error(parse_context)
raise Liquid::SyntaxError, parse_context.locale.t('errors.syntax.assign')
end
attr_reader :to, :from
def initialize(tag_name, markup, parse_context)
super
if markup =~ Syntax
@to = $1
@from = Variable.new($2,options)
@from.line_number = line_number
@to = Regexp.last_match(1)
@from = Variable.new(Regexp.last_match(2), parse_context)
else
raise SyntaxError.new options[:locale].t("errors.syntax.assign".freeze)
self.class.raise_syntax_error(parse_context)
end
end
def render(context)
def render_to_output_buffer(context, output)
val = @from.render(context)
context.scopes.last[@to] = val
inc = val.instance_of?(String) || val.instance_of?(Array) || val.instance_of?(Hash) ? val.length : 1
context.resource_limits.assign_score += inc
''.freeze
context.resource_limits.increment_assign_score(assign_score_of(val))
output
end
def blank?
true
end
def format
"{% assign #{@to} = #{@from.format} %}"
private
def assign_score_of(val)
if val.instance_of?(String)
val.bytesize
elsif val.instance_of?(Array)
sum = 1
# Uses #each to avoid extra allocations.
val.each { |child| sum += assign_score_of(child) }
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
1
end
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[@node.from]
end
end
end
Template.register_tag('assign'.freeze, Assign)
Template.register_tag('assign', Assign)
end

View File

@@ -1,5 +1,6 @@
module Liquid
# frozen_string_literal: true
module Liquid
# Break tag to be used to break out of a for loop.
#
# == Basic Usage:
@@ -10,12 +11,13 @@ module Liquid
# {% endfor %}
#
class Break < Tag
INTERRUPT = BreakInterrupt.new.freeze
def interrupt
BreakInterrupt.new
def render_to_output_buffer(context, output)
context.push_interrupt(INTERRUPT)
output
end
end
Template.register_tag('break'.freeze, Break)
Template.register_tag('break', Break)
end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid
# Capture stores the result of a block into a variable without rendering it inplace.
#
@@ -16,27 +18,24 @@ module Liquid
def initialize(tag_name, markup, options)
super
if markup =~ Syntax
@to = $1
@to = Regexp.last_match(1)
else
raise SyntaxError.new(options[:locale].t("errors.syntax.capture"))
raise SyntaxError, options[:locale].t("errors.syntax.capture")
end
end
def render(context)
output = super
context.scopes.last[@to] = output
context.resource_limits.assign_score += output.length
''.freeze
def render_to_output_buffer(context, output)
context.resource_limits.with_capture do
capture_output = render(context)
context.scopes.last[@to] = capture_output
end
output
end
def blank?
true
end
def format
"{% #{block_name} #{@to} %}#{@body.format}{% #{block_delimiter} %}"
end
end
Template.register_tag('capture'.freeze, Capture)
Template.register_tag('capture', Capture)
end

View File

@@ -1,24 +1,34 @@
# frozen_string_literal: true
module Liquid
class Case < Block
Syntax = /(#{QuotedFragment})/o
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om
attr_reader :blocks, :left
def initialize(tag_name, markup, options)
super
@blocks = []
if markup =~ Syntax
@left = Expression.parse($1)
@left = parse_expression(Regexp.last_match(1))
else
raise SyntaxError.new(options[:locale].t("errors.syntax.case".freeze))
raise SyntaxError, options[:locale].t("errors.syntax.case")
end
end
def parse(tokens)
@initial_body = body = BlockBody.new
while more = parse_body(body, tokens)
body = @blocks.last.attachment
body = case_body = new_body
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
def nodelist
@@ -27,75 +37,64 @@ module Liquid
def unknown_tag(tag, markup, tokens)
case tag
when 'when'.freeze
when 'when'
record_when_condition(markup)
when 'else'.freeze
when 'else'
record_else_condition(markup)
else
super
end
end
def render(context)
context.stack do
execute_else_block = true
output = ''
@blocks.each do |block|
if block.else?
return block.attachment.render(context) if execute_else_block
elsif block.evaluate(context)
execute_else_block = false
output << block.attachment.render(context)
end
end
output
end
end
def format
out = "{% case #{Expression.format(@left)} %}#{@initial_body.format}"
def render_to_output_buffer(context, output)
execute_else_block = true
@blocks.each do |block|
if block.else?
out << "{% else %}"
else
out << "{% when #{Expression.format(block.right)} %}"
block.attachment.render_to_output_buffer(context, output) if execute_else_block
elsif block.evaluate(context)
execute_else_block = false
block.attachment.render_to_output_buffer(context, output)
end
out << block.attachment.format
end
out + "{% endcase %}"
output
end
private
def record_when_condition(markup)
body = BlockBody.new
body = new_body
while markup
if not markup =~ WhenSyntax
raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_when".freeze))
unless markup =~ WhenSyntax
raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_when")
end
markup = $2
markup = Regexp.last_match(2)
block = Condition.new(@left, '=='.freeze, Expression.parse($1))
block = Condition.new(@left, '==', Condition.parse_expression(parse_context, Regexp.last_match(1)))
block.attach(body)
@blocks << block
end
end
def record_else_condition(markup)
if not markup.strip.empty?
raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_else".freeze))
unless markup.strip.empty?
raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_else")
end
block = ElseCondition.new
block.attach(BlockBody.new)
block.attach(new_body)
@blocks << block
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[@node.left] + @node.blocks
end
end
end
Template.register_tag('case'.freeze, Case)
Template.register_tag('case', Case)
end

View File

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

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid
# Continue tag to be used to break out of a for loop.
#
@@ -9,10 +11,13 @@ module Liquid
# {% endfor %}
#
class Continue < Tag
def interrupt
ContinueInterrupt.new
INTERRUPT = ContinueInterrupt.new.freeze
def render_to_output_buffer(context, output)
context.push_interrupt(INTERRUPT)
output
end
end
Template.register_tag('continue'.freeze, Continue)
Template.register_tag('continue', Continue)
end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid
# Cycle is usually used within a loop to alternate between values, like colors or DOM classes.
#
@@ -15,42 +17,43 @@ module Liquid
SimpleSyntax = /\A#{QuotedFragment}+/o
NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om
attr_reader :variables
def initialize(tag_name, markup, options)
super
case markup
when NamedSyntax
@variables = variables_from_string($2)
@name = Expression.parse($1)
@named = true
@variables = variables_from_string(Regexp.last_match(2))
@name = parse_expression(Regexp.last_match(1))
when SimpleSyntax
@variables = variables_from_string(markup)
@name = @variables.to_s
@name = @variables.to_s
else
raise SyntaxError.new(options[:locale].t("errors.syntax.cycle".freeze))
raise SyntaxError, options[:locale].t("errors.syntax.cycle")
end
end
def render(context)
context.registers[:cycle] ||= Hash.new(0)
def render_to_output_buffer(context, output)
context.registers[:cycle] ||= {}
context.stack do
key = context.evaluate(@name)
iteration = context.registers[:cycle][key]
result = context.evaluate(@variables[iteration])
iteration += 1
iteration = 0 if iteration >= @variables.size
context.registers[:cycle][key] = iteration
result
end
end
key = context.evaluate(@name)
iteration = context.registers[:cycle][key].to_i
def format
formatted_vars = @variables.map { |v| Expression.format(v) }.join(', ')
if @named
"{% cycle #{Expression.format(@name)}: #{formatted_vars} %}"
else
"{% cycle #{formatted_vars} %}"
val = context.evaluate(@variables[iteration])
if val.is_a?(Array)
val = val.join
elsif !val.is_a?(String)
val = val.to_s
end
output << val
iteration += 1
iteration = 0 if iteration >= @variables.size
context.registers[:cycle][key] = iteration
output
end
private
@@ -58,9 +61,15 @@ module Liquid
def variables_from_string(markup)
markup.split(',').collect do |var|
var =~ /\s*(#{QuotedFragment})\s*/o
$1 ? Expression.parse($1) : nil
Regexp.last_match(1) ? parse_expression(Regexp.last_match(1)) : nil
end.compact
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
Array(@node.variables)
end
end
end
Template.register_tag('cycle', Cycle)

View File

@@ -1,5 +1,6 @@
module Liquid
# frozen_string_literal: true
module Liquid
# decrement is used in a place where one needs to insert a counter
# into a template, and needs the counter to survive across
# multiple instantiations of the template.
@@ -24,15 +25,14 @@ module Liquid
@variable = markup.strip
end
def render(context)
def render_to_output_buffer(context, output)
value = context.environments.first[@variable] ||= 0
value = value - 1
value -= 1
context.environments.first[@variable] = value
value.to_s
output << value.to_s
output
end
private
end
Template.register_tag('decrement'.freeze, Decrement)
Template.register_tag('decrement', Decrement)
end

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

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

View File

@@ -1,5 +1,6 @@
module Liquid
# frozen_string_literal: true
module Liquid
# "For" iterates over an array or collection.
# Several useful variables are available to you within the loop.
#
@@ -24,7 +25,7 @@ module Liquid
# {{ item.name }}
# {% end %}
#
# To reverse the for loop simply use {% for item in collection reversed %}
# To reverse the for loop simply use {% for item in collection reversed %} (note that the flag's spelling is different to the filter `reverse`)
#
# == Available variables:
#
@@ -47,16 +48,26 @@ module Liquid
class For < Block
Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
attr_reader :collection_name, :variable_name, :limit, :from
def initialize(tag_name, markup, options)
super
@from = @limit = nil
parse_with_selected_parser(markup)
@for_block = BlockBody.new
@for_block = new_body
@else_block = nil
end
def parse(tokens)
if more = parse_body(@for_block, tokens)
if parse_body(@for_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
def nodelist
@@ -64,115 +75,53 @@ module Liquid
end
def unknown_tag(tag, markup, tokens)
return super unless tag == 'else'.freeze
@else_block = BlockBody.new
return super unless tag == 'else'
@else_block = new_body
end
def render(context)
context.registers[:for] ||= Hash.new(0)
def render_to_output_buffer(context, output)
segment = collection_segment(context)
collection = context.evaluate(@collection_name)
collection = collection.to_a if collection.is_a?(Range)
# Maintains Ruby 1.8.7 String#each behaviour on 1.9
return render_else(context) unless iterable?(collection)
from = if @from == :continue
context.registers[:for][@name].to_i
if segment.empty?
render_else(context, output)
else
context.evaluate(@from).to_i
render_segment(context, output, segment)
end
limit = context.evaluate(@limit)
to = limit ? limit.to_i + from : nil
segment = Utils.slice_collection(collection, from, to)
return render_else(context) if segment.empty?
segment.reverse! if @reversed
result = ''
length = segment.length
# Store our progress through the collection for the continue flag
context.registers[:for][@name] = from + segment.length
parent_loop = context['forloop'.freeze]
context.stack do
segment.each_with_index do |item, index|
context[@variable_name] = item
context['forloop'.freeze] = {
'name'.freeze => @name,
'length'.freeze => length,
'index'.freeze => index + 1,
'index0'.freeze => index,
'rindex'.freeze => length - index,
'rindex0'.freeze => length - index - 1,
'first'.freeze => (index == 0),
'last'.freeze => (index == length - 1),
'parentloop'.freeze => parent_loop
}
result << @for_block.render(context)
# Handle any interrupts if they exist.
if context.has_interrupt?
interrupt = context.pop_interrupt
break if interrupt.is_a? BreakInterrupt
next if interrupt.is_a? ContinueInterrupt
end
end
end
result
end
def format
args = [Expression.format(@collection_name)]
args << "reversed" if @reversed
args << "limit: #{Expression.format(@limit)}" if @limit
args << "offset: " << (@from == :continue ? 'continue' : Expression.format(@from)) if @from
out = "{% for #{@variable_name} in #{args.join(' ')} %}"
out << @for_block.format
if @else_block
out << "{% else %}"
out << @else_block.format
end
out + "{% endfor %}"
output
end
protected
def lax_parse(markup)
if markup =~ Syntax
@variable_name = $1
collection_name = $2
@reversed = $3
@name = "#{@variable_name}-#{collection_name}"
@collection_name = Expression.parse(collection_name)
@variable_name = Regexp.last_match(1)
collection_name = Regexp.last_match(2)
@reversed = !!Regexp.last_match(3)
@name = "#{@variable_name}-#{collection_name}"
@collection_name = parse_expression(collection_name)
markup.scan(TagAttributes) do |key, value|
set_attribute(key, value)
end
else
raise SyntaxError.new(options[:locale].t("errors.syntax.for".freeze))
raise SyntaxError, options[:locale].t("errors.syntax.for")
end
end
def strict_parse(markup)
p = Parser.new(markup)
@variable_name = p.consume(:id)
raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_in".freeze)) unless p.id?('in'.freeze)
collection_name = p.expression
@name = "#{@variable_name}-#{collection_name}"
@collection_name = Expression.parse(collection_name)
@reversed = p.id?('reversed'.freeze)
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in')
collection_name = p.expression
@collection_name = parse_expression(collection_name)
@name = "#{@variable_name}-#{collection_name}"
@reversed = p.id?('reversed')
while p.look(:id) && p.look(:colon, 1)
unless attribute = p.id?('limit'.freeze) || p.id?('offset'.freeze)
raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_attribute".freeze))
unless (attribute = p.id?('limit') || p.id?('offset'))
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_attribute")
end
p.consume
set_attribute(attribute, p.expression)
@@ -182,27 +131,97 @@ module Liquid
private
def collection_segment(context)
offsets = context.registers[:for] ||= {}
from = if @from == :continue
offsets[@name].to_i
else
from_value = context.evaluate(@from)
if from_value.nil?
0
else
Utils.to_integer(from_value)
end
end
collection = context.evaluate(@collection_name)
collection = collection.to_a if collection.is_a?(Range)
limit_value = context.evaluate(@limit)
to = if limit_value.nil?
nil
else
Utils.to_integer(limit_value) + from
end
segment = Utils.slice_collection(collection, from, to)
segment.reverse! if @reversed
offsets[@name] = from + segment.length
segment
end
def render_segment(context, output, segment)
for_stack = context.registers[:for_stack] ||= []
length = segment.length
context.stack do
loop_vars = Liquid::ForloopDrop.new(@name, length, for_stack[-1])
for_stack.push(loop_vars)
begin
context['forloop'] = loop_vars
segment.each do |item|
context[@variable_name] = item
@for_block.render_to_output_buffer(context, output)
loop_vars.send(:increment!)
# Handle any interrupts if they exist.
next unless context.interrupt?
interrupt = context.pop_interrupt
break if interrupt.is_a?(BreakInterrupt)
next if interrupt.is_a?(ContinueInterrupt)
end
ensure
for_stack.pop
end
end
output
end
def set_attribute(key, expr)
case key
when 'offset'.freeze
@from = if expr == 'continue'.freeze
when 'offset'
@from = if expr == 'continue'
Usage.increment('for_offset_continue')
:continue
else
Expression.parse(expr)
parse_expression(expr)
end
when 'limit'.freeze
@limit = Expression.parse(expr)
when 'limit'
@limit = parse_expression(expr)
end
end
def render_else(context)
@else_block ? @else_block.render(context) : ''.freeze
def render_else(context, output)
if @else_block
@else_block.render_to_output_buffer(context, output)
else
output
end
end
def iterable?(collection)
collection.respond_to?(:each) || Utils.non_blank_string?(collection)
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
(super + [@node.limit, @node.from, @node.collection_name]).compact
end
end
end
Template.register_tag('for'.freeze, For)
Template.register_tag('for', For)
end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
module Liquid
# If is the conditional block
#
@@ -10,121 +12,123 @@ module Liquid
# There are {% if count < 5 %} less {% else %} more {% endif %} items than you need.
#
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
BOOLEAN_OPERATORS = %w(and or)
BOOLEAN_OPERATORS = %w(and or).freeze
attr_reader :blocks
def initialize(tag_name, markup, options)
super
@blocks = []
push_block('if'.freeze, markup)
end
def parse(tokens)
while more = parse_body(@blocks.last.attachment, tokens)
end
push_block('if', markup)
end
def nodelist
@blocks.map(&:attachment)
end
def parse(tokens)
while parse_body(@blocks.last.attachment, tokens)
end
@blocks.reverse_each do |block|
block.attachment.remove_blank_strings if blank?
block.attachment.freeze
end
end
ELSE_TAG_NAMES = ['elsif', 'else'].freeze
private_constant :ELSE_TAG_NAMES
def unknown_tag(tag, markup, tokens)
if ['elsif'.freeze, 'else'.freeze].include?(tag)
if ELSE_TAG_NAMES.include?(tag)
push_block(tag, markup)
else
super
end
end
def render(context)
context.stack do
@blocks.each do |block|
if block.evaluate(context)
return block.attachment.render(context)
end
end
''.freeze
end
end
def format
out = ""
first_condition = true
def render_to_output_buffer(context, output)
@blocks.each do |block|
if block.else?
out << "{% else %}"
elsif first_condition
out << "{% #{block_name} #{block.format} %}"
first_condition = false
else
out << "{% elsif #{block.format} %}"
if block.evaluate(context)
return block.attachment.render_to_output_buffer(context, output)
end
out << block.attachment.format
end
out + "{% #{block_delimiter} %}"
output
end
private
def push_block(tag, markup)
block = if tag == 'else'.freeze
ElseCondition.new
else
parse_with_selected_parser(markup)
end
@blocks.push(block)
block.attach(BlockBody.new)
def push_block(tag, markup)
block = if tag == 'else'
ElseCondition.new
else
parse_with_selected_parser(markup)
end
def lax_parse(markup)
expressions = markup.scan(ExpressionsAndOperators)
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop =~ Syntax
@blocks.push(block)
block.attach(new_body)
end
condition = Condition.new(Expression.parse($1), $2, Expression.parse($3))
def parse_expression(markup)
Condition.parse_expression(parse_context, markup)
end
while not expressions.empty?
operator = expressions.pop.to_s.strip
def lax_parse(markup)
expressions = markup.scan(ExpressionsAndOperators)
raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop =~ Syntax
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop.to_s =~ Syntax
condition = Condition.new(parse_expression(Regexp.last_match(1)), Regexp.last_match(2), parse_expression(Regexp.last_match(3)))
new_condition = Condition.new(Expression.parse($1), $2, Expression.parse($3))
raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless BOOLEAN_OPERATORS.include?(operator)
new_condition.send(operator, condition)
condition = new_condition
end
until expressions.empty?
operator = expressions.pop.to_s.strip
condition
raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop.to_s =~ Syntax
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)
new_condition.send(operator, condition)
condition = new_condition
end
def strict_parse(markup)
p = Parser.new(markup)
condition
end
condition = parse_comparison(p)
def strict_parse(markup)
p = Parser.new(markup)
condition = parse_binary_comparisons(p)
p.consume(:end_of_string)
condition
end
while op = (p.id?('and'.freeze) || p.id?('or'.freeze))
new_cond = parse_comparison(p)
new_cond.send(op, condition)
condition = new_cond
end
p.consume(:end_of_string)
condition
def parse_binary_comparisons(p)
condition = parse_comparison(p)
first_condition = condition
while (op = (p.id?('and') || p.id?('or')))
child_condition = parse_comparison(p)
condition.send(op, child_condition)
condition = child_condition
end
first_condition
end
def parse_comparison(p)
a = Expression.parse(p.expression)
if op = p.consume?(:comparison)
b = Expression.parse(p.expression)
Condition.new(a, op, b)
else
Condition.new(a)
end
def parse_comparison(p)
a = parse_expression(p.expression)
if (op = p.consume?(:comparison))
b = parse_expression(p.expression)
Condition.new(a, op, b)
else
Condition.new(a)
end
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
@node.blocks
end
end
end
Template.register_tag('if'.freeze, If)
Template.register_tag('if', If)
end

View File

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

View File

@@ -1,5 +1,6 @@
module Liquid
# frozen_string_literal: true
module Liquid
# Include allows templates to relate with other templates
#
# Simply include another template:
@@ -15,107 +16,96 @@ module Liquid
# {% include 'product' for products %}
#
class Include < Tag
Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/o
prepend Tag::Disableable
SYNTAX = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
Syntax = SYNTAX
attr_reader :template_name_expr, :variable_name_expr, :attributes
def initialize(tag_name, markup, options)
super
if markup =~ Syntax
if markup =~ SYNTAX
template_name = $1
variable_name = $3
template_name = Regexp.last_match(1)
variable_name = Regexp.last_match(3)
@variable_name_expr = variable_name ? Expression.parse(variable_name) : nil
@template_name_expr = Expression.parse(template_name)
@attributes = {}
@alias_name = Regexp.last_match(5)
@variable_name_expr = variable_name ? parse_expression(variable_name) : nil
@template_name_expr = parse_expression(template_name)
@attributes = {}
markup.scan(TagAttributes) do |key, value|
@attributes[key] = Expression.parse(value)
@attributes[key] = parse_expression(value)
end
else
raise SyntaxError.new(options[:locale].t("errors.syntax.include".freeze))
raise SyntaxError, options[:locale].t("errors.syntax.include")
end
end
def parse(tokens)
def parse(_tokens)
end
def render(context)
partial = load_cached_partial(context)
def render_to_output_buffer(context, output)
template_name = context.evaluate(@template_name_expr)
context_variable_name = template_name.split('/'.freeze).last
raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name
partial = PartialCache.load(
template_name,
context: context,
parse_context: parse_context
)
context_variable_name = @alias_name || template_name.split('/').last
variable = if @variable_name_expr
context.evaluate(@variable_name_expr)
else
context.find_variable(template_name)
context.find_variable(template_name, raise_on_not_found: false)
end
context.stack do
@attributes.each do |key, value|
context[key] = context.evaluate(value)
end
if variable.is_a?(Array)
variable.collect do |var|
context[context_variable_name] = var
partial.render(context)
old_template_name = context.template_name
old_partial = context.partial
begin
context.template_name = template_name
context.partial = true
context.stack do
@attributes.each do |key, value|
context[key] = context.evaluate(value)
end
if variable.is_a?(Array)
variable.each do |var|
context[context_variable_name] = var
partial.render_to_output_buffer(context, output)
end
else
context[context_variable_name] = variable
partial.render_to_output_buffer(context, output)
end
else
context[context_variable_name] = variable
partial.render(context)
end
ensure
context.template_name = old_template_name
context.partial = old_partial
end
output
end
def format
segments = [@tag_name, Expression.format(@template_name_expr)]
alias_method :parse_context, :options
private :parse_context
if @variable_name_expr
segments << "with"
segments << Expression.format(@variable_name_expr)
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[
@node.template_name_expr,
@node.variable_name_expr,
] + @node.attributes.values
end
unless @attributes.empty?
segments << @attributes.map { |k, v| "#{k}: #{Expression.format(v)}" }.join(", ")
end
"{% #{segments.join(' ')} %}"
end
private
def load_cached_partial(context)
cached_partials = context.registers[:cached_partials] || {}
template_name = context.evaluate(@template_name_expr)
if cached = cached_partials[template_name]
return cached
end
source = read_template_from_file_system(context)
partial = Liquid::Template.parse(source, pass_options)
cached_partials[template_name] = partial
context.registers[:cached_partials] = cached_partials
partial
end
def read_template_from_file_system(context)
file_system = context.registers[:file_system] || Liquid::Template.file_system
file_system.read_template_file(context.evaluate(@template_name_expr))
end
def pass_options
dont_pass = @options[:include_options_blacklist]
return {locale: @options[:locale]} if dont_pass == true
opts = @options.merge(included: true, include_options_blacklist: false)
if dont_pass.is_a?(Array)
dont_pass.each {|o| opts.delete(o)}
end
opts
end
end
Template.register_tag('include'.freeze, Include)
Template.register_tag('include', Include)
end

View File

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

View File

@@ -0,0 +1,15 @@
# frozen_string_literal: true
module Liquid
class InlineComment < Tag
def blank?
true
end
def render_to_output_buffer(_context, _output)
end
end
Template.register_tag('--', InlineComment)
end

View File

@@ -1,20 +1,32 @@
# frozen_string_literal: true
module Liquid
class Raw < Block
Syntax = /\A\s*\z/
FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
def parse(tokens)
@body = ''
while token = tokens.shift
if token =~ FullTokenPossiblyInvalid
@body << $1 if $1 != "".freeze
return if block_delimiter == $2
end
@body << token if not token.empty?
end
def initialize(tag_name, markup, parse_context)
super
ensure_valid_markup(tag_name, markup, parse_context)
end
def render(context)
@body
def parse(tokens)
@body = +''
while (token = tokens.shift)
if token =~ FullTokenPossiblyInvalid && block_delimiter == Regexp.last_match(2)
@body << Regexp.last_match(1) if Regexp.last_match(1) != ""
return
end
@body << token unless token.empty?
end
raise_tag_never_closed(block_name)
end
def render_to_output_buffer(_context, output)
output << @body
output
end
def nodelist
@@ -25,10 +37,14 @@ module Liquid
@body.empty?
end
def format
"{% #{block_name} %}#{@body}{% #{block_delimiter} %}"
protected
def ensure_valid_markup(tag_name, markup, parse_context)
unless Syntax.match?(markup)
raise SyntaxError, parse_context.locale.t("errors.syntax.tag_unexpected_args", tag: tag_name)
end
end
end
Template.register_tag('raw'.freeze, Raw)
Template.register_tag('raw', Raw)
end

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

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

View File

@@ -1,78 +1,66 @@
# frozen_string_literal: true
module Liquid
class TableRow < Block
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
attr_reader :variable_name, :collection_name, :attributes
def initialize(tag_name, markup, options)
super
if markup =~ Syntax
@variable_name = $1
@collection_name = Expression.parse($2)
@attributes = {}
@variable_name = Regexp.last_match(1)
@collection_name = parse_expression(Regexp.last_match(2))
@attributes = {}
markup.scan(TagAttributes) do |key, value|
@attributes[key] = Expression.parse(value)
@attributes[key] = parse_expression(value)
end
else
raise SyntaxError.new(options[:locale].t("errors.syntax.table_row".freeze))
raise SyntaxError, options[:locale].t("errors.syntax.table_row")
end
end
def render(context)
collection = context.evaluate(@collection_name) or return ''.freeze
def render_to_output_buffer(context, output)
(collection = context.evaluate(@collection_name)) || (return '')
from = @attributes.key?('offset'.freeze) ? context.evaluate(@attributes['offset'.freeze]).to_i : 0
to = @attributes.key?('limit'.freeze) ? from + context.evaluate(@attributes['limit'.freeze]).to_i : nil
from = @attributes.key?('offset') ? context.evaluate(@attributes['offset']).to_i : 0
to = @attributes.key?('limit') ? from + context.evaluate(@attributes['limit']).to_i : nil
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'.freeze]).to_i
row = 1
col = 0
result = "<tr class=\"row1\">\n"
output << "<tr class=\"row1\">\n"
context.stack do
tablerowloop = Liquid::TablerowloopDrop.new(length, cols)
context['tablerowloop'] = tablerowloop
collection.each_with_index do |item, index|
collection.each do |item|
context[@variable_name] = item
context['tablerowloop'.freeze] = {
'length'.freeze => length,
'index'.freeze => index + 1,
'index0'.freeze => index,
'col'.freeze => col + 1,
'col0'.freeze => col,
'rindex'.freeze => length - index,
'rindex0'.freeze => length - index - 1,
'first'.freeze => (index == 0),
'last'.freeze => (index == length - 1),
'col_first'.freeze => (col == 0),
'col_last'.freeze => (col == cols - 1)
}
output << "<td class=\"col#{tablerowloop.col}\">"
super
output << '</td>'
col += 1
result << "<td class=\"col#{col}\">" << super << '</td>'
if col == cols and (index != length - 1)
col = 0
row += 1
result << "</tr>\n<tr class=\"row#{row}\">"
if tablerowloop.col_last && !tablerowloop.last
output << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">"
end
tablerowloop.send(:increment!)
end
end
result << "</tr>\n"
result
output << "</tr>\n"
output
end
def format
tag_main = "#{block_name} #{@variable_name} in #{Expression.format(@collection_name)}"
args = @attributes.map { |k, v| "#{k}: #{Expression.format(v)}" }.join(", ")
"{% #{tag_main} #{args} %}#{@body.format}{% #{block_delimiter} %}"
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
super + @node.attributes.values + [@node.collection_name]
end
end
end
Template.register_tag('tablerow'.freeze, TableRow)
Template.register_tag('tablerow', TableRow)
end

View File

@@ -1,4 +1,6 @@
require File.dirname(__FILE__) + '/if'
# frozen_string_literal: true
require_relative 'if'
module Liquid
# Unless is a conditional just like 'if' but works on the inverse logic.
@@ -6,26 +8,23 @@ module Liquid
# {% unless x < 0 %} x is greater than zero {% endunless %}
#
class Unless < If
def render(context)
context.stack do
# First condition is interpreted backwards ( if not )
first_block = @blocks.first
unless first_block.evaluate(context)
return first_block.attachment.render(context)
end
# After the first condition unless works just like if
@blocks[1..-1].each do |block|
if block.evaluate(context)
return block.attachment.render(context)
end
end
''.freeze
def render_to_output_buffer(context, output)
# First condition is interpreted backwards ( if not )
first_block = @blocks.first
unless first_block.evaluate(context)
return first_block.attachment.render_to_output_buffer(context, output)
end
# After the first condition unless works just like if
@blocks[1..-1].each do |block|
if block.evaluate(context)
return block.attachment.render_to_output_buffer(context, output)
end
end
output
end
end
Template.register_tag('unless'.freeze, Unless)
Template.register_tag('unless', Unless)
end

View File

@@ -1,5 +1,6 @@
module Liquid
# frozen_string_literal: true
module Liquid
# Templates are central to liquid.
# Interpretating templates is a two step process. First you compile the
# source code you got. During compile time some extensive error checking is performed.
@@ -14,23 +15,19 @@ module Liquid
# template.render('user_name' => 'bob')
#
class Template
DEFAULT_OPTIONS = {
:locale => I18n.new
}
attr_accessor :root
attr_reader :resource_limits
@@file_system = BlankFileSystem.new
attr_reader :resource_limits, :warnings
class TagRegistry
include Enumerable
def initialize
@tags = {}
@cache = {}
end
def [](tag_name)
return nil unless @tags.has_key?(tag_name)
return nil unless @tags.key?(tag_name)
return @cache[tag_name] if Liquid.cache_classes
lookup_class(@tags[tag_name]).tap { |o| @cache[tag_name] = o }
@@ -46,10 +43,14 @@ module Liquid
@cache.delete(tag_name)
end
def each(&block)
@tags.each(&block)
end
private
def lookup_class(name)
name.split("::").reject(&:empty?).reduce(Object) { |scope, const| scope.const_get(const) }
Object.const_get(name)
end
end
@@ -60,77 +61,57 @@ module Liquid
# :lax acts like liquid 2.5 and silently ignores malformed tags in most cases.
# :warn is the default and will give deprecation warnings when invalid syntax is used.
# :strict will enforce correct syntax.
attr_writer :error_mode
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
def file_system
@@file_system
attr_accessor :default_exception_renderer
Template.default_exception_renderer = lambda do |exception|
exception
end
def file_system=(obj)
@@file_system = obj
end
attr_accessor :file_system
Template.file_system = BlankFileSystem.new
attr_accessor :tags
Template.tags = TagRegistry.new
private :tags=
def register_tag(name, klass)
tags[name.to_s] = klass
end
def tags
@tags ||= TagRegistry.new
end
def error_mode
@error_mode || :lax
end
def taint_mode
@taint_mode || :lax
end
# Pass a module with filter methods which should be available
# to all liquid views. Good for registering the standard library
def register_filter(mod)
Strainer.global_filter(mod)
StrainerFactory.add_global_filter(mod)
end
def default_resource_limits
@default_resource_limits ||= {}
end
attr_accessor :default_resource_limits
Template.default_resource_limits = {}
private :default_resource_limits=
# creates a new <tt>Template</tt> object from liquid source code
# To enable profiling, pass in <tt>profile: true</tt> as an option.
# See Liquid::Profiler for more information
def parse(source, options = {})
template = Template.new
template.parse(source, options)
new.parse(source, options)
end
end
def initialize
@resource_limits = ResourceLimits.new(self.class.default_resource_limits)
@rethrow_errors = false
@resource_limits = ResourceLimits.new(Template.default_resource_limits)
end
# Parse source code.
# Returns self for easy chaining
def parse(source, options = {})
@options = options
@profiling = options[:profile]
@line_numbers = options[:line_numbers] || @profiling
@root = Document.parse(tokenize(source), DEFAULT_OPTIONS.merge(options))
@warnings = nil
parse_context = configure_options(options)
tokenizer = parse_context.new_tokenizer(source, start_line_number: @line_numbers && 1)
@root = Document.parse(tokenizer, parse_context)
self
end
def warnings
return [] unless @root
@warnings ||= @root.warnings
end
def registers
@registers ||= {}
end
@@ -162,19 +143,19 @@ module Liquid
# filters and tags and might be useful to integrate liquid more with its host application
#
def render(*args)
return ''.freeze if @root.nil?
return '' if @root.nil?
context = case args.first
when Liquid::Context
c = args.shift
if @rethrow_errors
c.exception_handler = ->(e) { true }
c.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA
end
c
when Liquid::Drop
drop = args.shift
drop = args.shift
drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
when Hash
Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
@@ -184,37 +165,34 @@ module Liquid
raise ArgumentError, "Expected Hash or Liquid::Context as parameter"
end
output = nil
context_register = context.registers.is_a?(StaticRegisters) ? context.registers.static : context.registers
case args.last
when Hash
options = args.pop
output = options[:output] if options[:output]
if options[:registers].is_a?(Hash)
self.registers.merge!(options[:registers])
options[:registers]&.each do |key, register|
context_register[key] = register
end
if options[:filters]
context.add_filters(options[:filters])
end
if options[:exception_handler]
context.exception_handler = options[:exception_handler]
end
when Module
context.add_filters(args.pop)
when Array
apply_options_to_context(context, options)
when Module, Array
context.add_filters(args.pop)
end
# Retrying a render resets resource usage
context.resource_limits.reset
if @profiling && context.profiler.nil?
@profiler = context.profiler = Liquid::Profiler.new
end
begin
# render the nodelist.
# for performance reasons we get an array back here. join will make a string out of it.
result = with_profiling do
@root.render(context)
end
result.respond_to?(:join) ? result.join : result
@root.render_to_output_buffer(context, output || +'')
rescue Liquid::MemoryError => e
context.handle_error(e)
ensure
@@ -227,47 +205,31 @@ module Liquid
render(*args)
end
def render_to_output_buffer(context, output)
render(context, output: output)
end
private
# Uses the <tt>Liquid::TemplateParser</tt> regexp to tokenize the passed source
def tokenize(source)
source = source.source if source.respond_to?(:source)
return [] if source.to_s.empty?
tokens = calculate_line_numbers(source.split(TemplateParser))
# removes the rogue empty element at the beginning of the array
tokens.shift if tokens[0] and tokens[0].empty?
tokens
end
def calculate_line_numbers(raw_tokens)
return raw_tokens unless @line_numbers
current_line = 1
raw_tokens.map do |token|
Token.new(token, current_line).tap do
current_line += token.count("\n")
end
end
end
def with_profiling
if @profiling && !@options[:included]
def configure_options(options)
if (profiling = options[:profile])
raise "Profiler not loaded, require 'liquid/profiler' first" unless defined?(Liquid::Profiler)
@profiler = Profiler.new
@profiler.start
begin
yield
ensure
@profiler.stop
end
else
yield
end
@options = options
@profiling = profiling
@line_numbers = options[:line_numbers] || @profiling
parse_context = options.is_a?(ParseContext) ? options : ParseContext.new(options)
@warnings = parse_context.warnings
parse_context
end
def apply_options_to_context(context, options)
context.add_filters(options[:filters]) if options[:filters]
context.global_filter = options[:global_filter] if options[:global_filter]
context.exception_renderer = options[:exception_renderer] if options[:exception_renderer]
context.strict_variables = options[:strict_variables] if options[:strict_variables]
context.strict_filters = options[:strict_filters] if options[:strict_filters]
end
end
end

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

@@ -1,18 +0,0 @@
module Liquid
class Token < String
attr_reader :line_number
def initialize(content, line_number)
super(content)
@line_number = line_number
end
def raw
"<raw>"
end
def child(string)
Token.new(string, @line_number)
end
end
end

39
lib/liquid/tokenizer.rb Normal file
View File

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

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

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

View File

@@ -1,27 +1,26 @@
# frozen_string_literal: true
module Liquid
module Utils
def self.slice_collection(collection, from, to)
if (from != 0 || to != nil) && collection.respond_to?(:load_slice)
if (from != 0 || !to.nil?) && collection.respond_to?(:load_slice)
collection.load_slice(from, to)
else
slice_collection_using_each(collection, from, to)
end
end
def self.non_blank_string?(collection)
collection.is_a?(String) && collection != ''.freeze
end
def self.slice_collection_using_each(collection, from, to)
segments = []
index = 0
index = 0
# Maintains Ruby 1.8.7 String#each behaviour on 1.9
return [collection] if non_blank_string?(collection)
if collection.is_a?(String)
return collection.empty? ? [] : [collection]
end
return [] unless collection.respond_to?(:each)
collection.each do |item|
if to && to <= index
break
end
@@ -35,5 +34,52 @@ module Liquid
segments
end
def self.to_integer(num)
return num if num.is_a?(Integer)
num = num.to_s
begin
Integer(num)
rescue ::ArgumentError
raise Liquid::ArgumentError, "invalid integer"
end
end
def self.to_number(obj)
case obj
when Float
BigDecimal(obj.to_s)
when Numeric
obj
when String
/\A-?\d+\.\d+\z/.match?(obj.strip) ? BigDecimal(obj) : obj.to_i
else
if obj.respond_to?(:to_number)
obj.to_number
else
0
end
end
end
def self.to_date(obj)
return obj if obj.respond_to?(:strftime)
if obj.is_a?(String)
return nil if obj.empty?
obj = obj.downcase
end
case obj
when 'now', 'today'
Time.now
when /\A\d+\z/, Integer
Time.at(obj.to_i)
when String
Time.parse(obj)
end
rescue ::ArgumentError
nil
end
end
end

View File

@@ -1,5 +1,6 @@
module Liquid
# frozen_string_literal: true
module Liquid
# Holds variables. Variables are only loaded "just in time"
# and are not evaluated as part of the render stage
#
@@ -11,64 +12,49 @@ module Liquid
# {{ user | link }}
#
class Variable
FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
attr_accessor :filters, :name, :warnings
attr_accessor :line_number
FilterMarkupRegex = /#{FilterSeparator}\s*(.*)/om
FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
FilterArgsRegex = /(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o
JustTagAttributes = /\A#{TagAttributes}\z/o
MarkupWithQuotedFragment = /(#{QuotedFragment})(.*)/om
attr_accessor :filters, :name, :line_number
attr_reader :parse_context
alias_method :options, :parse_context
include ParserSwitching
def initialize(markup, options = {})
@markup = markup
@name = nil
@options = options || {}
def initialize(markup, parse_context)
@markup = markup
@name = nil
@parse_context = parse_context
@line_number = parse_context.line_number
parse_with_selected_parser(markup)
strict_parse_with_error_mode_fallback(markup)
end
def raw
@markup
end
def format
out = Expression.format(@name)
@filters.each do |filter|
out << " | "
out << filter[0]
args = filter[1].map { |arg| Expression.format(arg) }
if filter.size > 2
args += filter[2].map { |key, arg| "#{key}: #{Expression.format(arg)}" }
end
unless args.empty?
out << ": "
out << args.join(', ')
end
end
out
end
def markup_context(markup)
"in \"{{#{markup}}}\""
end
def lax_parse(markup)
@filters = []
if markup =~ /(#{QuotedFragment})(.*)/om
name_markup = $1
filter_markup = $2
@name = Expression.parse(name_markup)
if filter_markup =~ /#{FilterSeparator}\s*(.*)/om
filters = $1.scan(FilterParser)
filters.each do |f|
if f =~ /\w+/
filtername = Regexp.last_match(0)
filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o).flatten
@filters << parse_filter_expressions(filtername, filterargs)
end
end
return unless markup =~ MarkupWithQuotedFragment
name_markup = Regexp.last_match(1)
filter_markup = Regexp.last_match(2)
@name = Expression.parse(name_markup)
if filter_markup =~ FilterMarkupRegex
filters = Regexp.last_match(1).scan(FilterParser)
filters.each do |f|
next unless f =~ /\w+/
filtername = Regexp.last_match(0)
filterargs = f.scan(FilterArgsRegex).flatten
@filters << parse_filter_expressions(filtername, filterargs)
end
end
end
@@ -77,6 +63,8 @@ module Liquid
@filters = []
p = Parser.new(markup)
return if p.look(:end_of_string)
@name = Expression.parse(p.expression)
while p.consume?(:pipe)
filtername = p.consume(:id)
@@ -90,38 +78,62 @@ module Liquid
# first argument
filterargs = [p.argument]
# followed by comma separated others
while p.consume?(:comma)
filterargs << p.argument
end
filterargs << p.argument while p.consume?(:comma)
filterargs
end
def render(context)
@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)
output = context.invoke(filter_name, output, *filter_args)
end.tap{ |obj| taint_check(obj) }
obj = context.invoke(filter_name, obj, *filter_args)
end
context.apply_global_filter(obj)
end
def render_to_output_buffer(context, output)
obj = render(context)
if obj.is_a?(Array)
output << obj.join
elsif obj.nil?
else
output << obj.to_s
end
output
end
def disabled?(_context)
false
end
def disabled_tags
[]
end
private
def parse_filter_expressions(filter_name, unparsed_args)
filter_args = []
keyword_args = {}
filter_args = []
keyword_args = nil
unparsed_args.each do |a|
if matches = a.match(/\A#{TagAttributes}\z/o)
if (matches = a.match(JustTagAttributes))
keyword_args ||= {}
keyword_args[matches[1]] = Expression.parse(matches[2])
else
filter_args << Expression.parse(a)
end
end
result = [filter_name, filter_args]
result << keyword_args unless keyword_args.empty?
result << keyword_args if keyword_args
result
end
def evaluate_filter_expressions(context, filter_args, filter_kwargs)
parsed_args = filter_args.map{ |expr| context.evaluate(expr) }
parsed_args = filter_args.map { |expr| context.evaluate(expr) }
if filter_kwargs
parsed_kwargs = {}
filter_kwargs.each do |key, expr|
@@ -132,17 +144,9 @@ module Liquid
parsed_args
end
def taint_check(obj)
if obj.tainted?
@markup =~ QuotedFragment
name = Regexp.last_match(0)
case Template.taint_mode
when :warn
@warnings ||= []
@warnings << "variable '#{name}' is tainted and was not escaped"
when :error
raise TaintedError, "Error - variable '#{name}' is tainted and was not escaped"
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[@node.name] + @node.filters.flatten
end
end
end

View File

@@ -1,7 +1,9 @@
# frozen_string_literal: true
module Liquid
class VariableLookup
SQUARE_BRACKETED = /\A\[(.*)\]\z/m
COMMAND_METHODS = ['size'.freeze, 'first'.freeze, 'last'.freeze]
COMMAND_METHODS = ['size', 'first', 'last'].freeze
attr_reader :name, :lookups
@@ -14,18 +16,17 @@ module Liquid
name = lookups.shift
if name =~ SQUARE_BRACKETED
@name_expression = true
name = Expression.parse($1)
name = Expression.parse(Regexp.last_match(1))
end
@name = name
@lookups = lookups
@lookups = lookups
@command_flags = 0
@lookups.each_index do |i|
lookup = lookups[i]
if lookup =~ SQUARE_BRACKETED
lookups[i] = Expression.parse($1)
lookups[i] = Expression.parse(Regexp.last_match(1))
elsif COMMAND_METHODS.include?(lookup)
@command_flags |= 1 << i
end
@@ -33,7 +34,7 @@ module Liquid
end
def evaluate(context)
name = context.evaluate(@name)
name = context.evaluate(@name)
object = context.find_variable(name)
@lookups.each_index do |i|
@@ -42,11 +43,11 @@ module Liquid
# If object is a hash- or array-like object we look for the
# presence of the key and if its available we return it
if object.respond_to?(:[]) &&
((object.respond_to?(:has_key?) && object.has_key?(key)) ||
(object.respond_to?(:fetch) && key.is_a?(Integer)))
((object.respond_to?(:key?) && object.key?(key)) ||
(object.respond_to?(:fetch) && key.is_a?(Integer)))
# if its a proc we will replace the entry with the proc
res = context.lookup_and_evaluate(object, key)
res = context.lookup_and_evaluate(object, key)
object = res.to_liquid
# Some special cases. If the part wasn't in square brackets and
@@ -56,9 +57,11 @@ module Liquid
object = object.send(key).to_liquid
# No key was present with the desired value and it wasn't one of the directly supported
# keywords either. The only thing we got left is to return nil
# keywords either. The only thing we got left is to return nil or
# raise an exception if `strict_variables` option is set to true
else
return nil
return nil unless context.strict_variables
raise Liquid::UndefinedVariable, "undefined variable #{key}"
end
# If we are dealing with a drop here we have to
@@ -69,21 +72,7 @@ module Liquid
end
def ==(other)
self.class == other.class && self.state == other.state
end
def format
out = @name_expression ? Expression.format(@name) : @name.dup
@lookups.each do |lookup|
if lookup.is_a?(String) && lookup =~ /^#{VariableSegment}+$/
out << ".#{lookup}"
else
out << "[#{Expression.format(lookup)}]"
end
end
out
self.class == other.class && state == other.state
end
protected
@@ -91,5 +80,11 @@ module Liquid
def state
[@name, @lookups, @command_flags]
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
@node.lookups
end
end
end
end

View File

@@ -1,4 +1,6 @@
# encoding: utf-8
# frozen_string_literal: true
module Liquid
VERSION = "4.0.0.alpha"
VERSION = "5.0.0"
end

View File

@@ -1,6 +1,8 @@
# encoding: utf-8
# frozen_string_literal: true
lib = File.expand_path('../lib/', __FILE__)
$:.unshift lib unless $:.include?(lib)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require "liquid/version"
@@ -13,17 +15,20 @@ Gem::Specification.new do |s|
s.email = ["tobi@leetsoft.com"]
s.homepage = "http://www.liquidmarkup.org"
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.5.0"
s.required_rubygems_version = ">= 1.3.7"
s.test_files = Dir.glob("{test}/**/*")
s.files = Dir.glob("{lib}/**/*") + %w(MIT-LICENSE README.md)
s.metadata['allowed_push_host'] = 'https://rubygems.org'
s.extra_rdoc_files = ["History.md", "README.md"]
s.test_files = Dir.glob("{test}/**/*")
s.files = Dir.glob("{lib}/**/*") + %w(LICENSE README.md)
s.extra_rdoc_files = ["History.md", "README.md"]
s.require_path = "lib"
s.add_development_dependency 'rake'
s.add_development_dependency 'minitest'
s.add_development_dependency('rake', '~> 13.0')
s.add_development_dependency('minitest')
end

View File

@@ -1,11 +1,13 @@
# frozen_string_literal: true
require 'benchmark/ips'
require File.dirname(__FILE__) + '/theme_runner'
require_relative 'theme_runner'
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new
Benchmark.ips do |x|
x.time = 60
x.time = 10
x.warmup = 5
puts
@@ -13,5 +15,6 @@ Benchmark.ips do |x|
puts
x.report("parse:") { profiler.compile }
x.report("parse & run:") { profiler.run }
x.report("render:") { profiler.render }
x.report("parse & render:") { profiler.run }
end

View File

@@ -0,0 +1,63 @@
# frozen_string_literal: true
require 'benchmark/ips'
require 'memory_profiler'
require 'terminal-table'
require_relative 'theme_runner'
class Profiler
LOG_LABEL = "Profiling: ".rjust(14).freeze
REPORTS_DIR = File.expand_path('.memprof', __dir__).freeze
def self.run
puts
yield new
end
def initialize
@allocated = []
@retained = []
@headings = []
end
def profile(phase, &block)
print(LOG_LABEL)
print("#{phase}.. ".ljust(10))
report = MemoryProfiler.report(&block)
puts 'Done.'
@headings << phase.capitalize
@allocated << "#{report.scale_bytes(report.total_allocated_memsize)} (#{report.total_allocated} objects)"
@retained << "#{report.scale_bytes(report.total_retained_memsize)} (#{report.total_retained} objects)"
return if ENV['CI']
require 'fileutils'
report_file = File.join(REPORTS_DIR, "#{sanitize(phase)}.txt")
FileUtils.mkdir_p(REPORTS_DIR)
report.pretty_print(to_file: report_file, scale_bytes: true)
end
def tabulate
table = Terminal::Table.new(headings: @headings.unshift('Phase')) do |t|
t << @allocated.unshift('Total allocated')
t << @retained.unshift('Total retained')
end
puts
puts table
puts "\nDetailed report(s) saved to #{REPORTS_DIR}/" unless ENV['CI']
end
def sanitize(string)
string.downcase.gsub(/[\W]/, '-').squeeze('-')
end
end
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
runner = ThemeRunner.new
Profiler.run do |x|
x.profile('parse') { runner.compile }
x.profile('render') { runner.render }
x.tabulate
end

View File

@@ -1,19 +1,21 @@
require 'stackprof' rescue fail("install stackprof extension/gem")
require File.dirname(__FILE__) + '/theme_runner'
# frozen_string_literal: true
require 'stackprof'
require_relative 'theme_runner'
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new
profiler.run
[:cpu, :object].each do |profile_type|
puts "Profiling in #{profile_type.to_s} mode..."
puts "Profiling in #{profile_type} mode..."
results = StackProf.run(mode: profile_type) do
200.times do
profiler.run
end
end
if profile_type == :cpu && graph_filename = ENV['GRAPH_FILENAME']
if profile_type == :cpu && (graph_filename = ENV['GRAPH_FILENAME'])
File.open(graph_filename, 'w') do |f|
StackProf::Report.new(results).print_graphviz(nil, f)
end

View File

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

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'yaml'
module Database
@@ -5,7 +7,7 @@ module Database
# to liquid as assigns. All this is based on Shopify
def self.tables
@tables ||= begin
db = YAML.load_file(File.dirname(__FILE__) + '/vision.database.yml')
db = YAML.load_file("#{__dir__}/vision.database.yml")
# From vision source
db['products'].each do |product|
@@ -16,9 +18,10 @@ module Database
end
# key the tables by handles, as this is how liquid expects it.
db = db.inject({}) do |assigns, (key, values)|
assigns[key] = values.inject({}) { |h, v| h[v['handle']] = v; h; }
assigns
db = db.each_with_object({}) do |(key, values), assigns|
assigns[key] = values.each_with_object({}) do |v, h|
h[v['handle']] = v
end
end
# Some standard direct accessors so that the specialized templates
@@ -29,9 +32,9 @@ module Database
db['article'] = db['blog']['articles'].first
db['cart'] = {
'total_price' => db['line_items'].values.inject(0) { |sum, item| sum += item['line_price'] * item['quantity'] },
'item_count' => db['line_items'].values.inject(0) { |sum, item| sum += item['quantity'] },
'items' => db['line_items'].values
'total_price' => db['line_items'].values.inject(0) { |sum, item| sum + item['line_price'] * item['quantity'] },
'item_count' => db['line_items'].values.inject(0) { |sum, item| sum + item['quantity'] },
'items' => db['line_items'].values,
}
db
@@ -39,7 +42,7 @@ module Database
end
end
if __FILE__ == $0
p Database.tables['collections']['frontpage'].keys
#p Database.tables['blog']['articles']
if __FILE__ == $PROGRAM_NAME
p(Database.tables['collections']['frontpage'].keys)
# p Database.tables['blog']['articles']
end

View File

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

View File

@@ -1,19 +1,21 @@
$:.unshift File.dirname(__FILE__) + '/../../lib'
require File.dirname(__FILE__) + '/../../lib/liquid'
# frozen_string_literal: true
require File.dirname(__FILE__) + '/comment_form'
require File.dirname(__FILE__) + '/paginate'
require File.dirname(__FILE__) + '/json_filter'
require File.dirname(__FILE__) + '/money_filter'
require File.dirname(__FILE__) + '/shop_filter'
require File.dirname(__FILE__) + '/tag_filter'
require File.dirname(__FILE__) + '/weight_filter'
$LOAD_PATH.unshift(__dir__ + '/../../lib')
require_relative '../../lib/liquid'
Liquid::Template.register_tag 'paginate', Paginate
Liquid::Template.register_tag 'form', CommentForm
require_relative 'comment_form'
require_relative 'paginate'
require_relative 'json_filter'
require_relative 'money_filter'
require_relative 'shop_filter'
require_relative 'tag_filter'
require_relative 'weight_filter'
Liquid::Template.register_filter JsonFilter
Liquid::Template.register_filter MoneyFilter
Liquid::Template.register_filter WeightFilter
Liquid::Template.register_filter ShopFilter
Liquid::Template.register_filter TagFilter
Liquid::Template.register_tag('paginate', Paginate)
Liquid::Template.register_tag('form', CommentForm)
Liquid::Template.register_filter(JsonFilter)
Liquid::Template.register_filter(MoneyFilter)
Liquid::Template.register_filter(WeightFilter)
Liquid::Template.register_filter(ShopFilter)
Liquid::Template.register_filter(TagFilter)

View File

@@ -1,13 +1,14 @@
module MoneyFilter
# frozen_string_literal: true
module MoneyFilter
def money_with_currency(money)
return '' if money.nil?
sprintf("$ %.2f USD", money/100.0)
format("$ %.2f USD", money / 100.0)
end
def money(money)
return '' if money.nil?
sprintf("$ %.2f", money/100.0)
format("$ %.2f", money / 100.0)
end
private

View File

@@ -1,13 +1,15 @@
# frozen_string_literal: true
class Paginate < Liquid::Block
Syntax = /(#{Liquid::QuotedFragment})\s*(by\s*(\d+))?/
Syntax = /(#{Liquid::QuotedFragment})\s*(by\s*(\d+))?/
def initialize(tag_name, markup, options)
super
if markup =~ Syntax
@collection_name = $1
@page_size = if $2
$3.to_i
@collection_name = Regexp.last_match(1)
@page_size = if Regexp.last_match(2)
Regexp.last_match(3).to_i
else
20
end
@@ -17,48 +19,47 @@ class Paginate < Liquid::Block
@attributes[key] = value
end
else
raise SyntaxError.new("Syntax Error in tag 'paginate' - Valid syntax: paginate [collection] by number")
raise SyntaxError, "Syntax Error in tag 'paginate' - Valid syntax: paginate [collection] by number"
end
end
def render(context)
def render_to_output_buffer(context, output)
@context = context
context.stack do
current_page = context['current_page'].to_i
current_page = context['current_page'].to_i
pagination = {
'page_size' => @page_size,
'current_page' => 5,
'current_offset' => @page_size * 5
'page_size' => @page_size,
'current_page' => 5,
'current_offset' => @page_size * 5,
}
context['paginate'] = pagination
collection_size = context[@collection_name].size
collection_size = context[@collection_name].size
raise ArgumentError.new("Cannot paginate array '#{@collection_name}'. Not found.") if collection_size.nil?
raise ArgumentError, "Cannot paginate array '#{@collection_name}'. Not found." if collection_size.nil?
page_count = (collection_size.to_f / @page_size.to_f).to_f.ceil + 1
pagination['items'] = collection_size
pagination['pages'] = page_count -1
pagination['previous'] = link('&laquo; Previous', current_page-1 ) unless 1 >= current_page
pagination['next'] = link('Next &raquo;', current_page+1 ) unless page_count <= current_page+1
pagination['pages'] = page_count - 1
pagination['previous'] = link('&laquo; Previous', current_page - 1) unless 1 >= current_page
pagination['next'] = link('Next &raquo;', current_page + 1) unless page_count <= current_page + 1
pagination['parts'] = []
hellip_break = false
if page_count > 2
1.upto(page_count-1) do |page|
1.upto(page_count - 1) do |page|
if current_page == page
pagination['parts'] << no_link(page)
elsif page == 1
pagination['parts'] << link(page, page)
elsif page == page_count -1
elsif page == page_count - 1
pagination['parts'] << link(page, page)
elsif page <= current_page - @attributes['window_size'] or page >= current_page + @attributes['window_size']
elsif page <= current_page - @attributes['window_size'] || page >= current_page + @attributes['window_size']
next if hellip_break
pagination['parts'] << no_link('&hellip;')
hellip_break = true
@@ -78,11 +79,11 @@ class Paginate < Liquid::Block
private
def no_link(title)
{ 'title' => title, 'is_link' => false}
{ 'title' => title, 'is_link' => false }
end
def link(title, page)
{ 'title' => title, 'url' => current_url + "?page=#{page}", 'is_link' => true}
{ 'title' => title, 'url' => current_url + "?page=#{page}", 'is_link' => true }
end
def current_url

View File

@@ -1,5 +1,6 @@
module ShopFilter
# frozen_string_literal: true
module ShopFilter
def asset_url(input)
"/files/1/[shop_id]/[shop_id]/assets/#{input}"
end
@@ -16,21 +17,21 @@ module ShopFilter
%(<script src="#{url}" type="text/javascript"></script>)
end
def stylesheet_tag(url, media="all")
def stylesheet_tag(url, media = "all")
%(<link href="#{url}" rel="stylesheet" type="text/css" media="#{media}" />)
end
def link_to(link, url, title="")
%|<a href="#{url}" title="#{title}">#{link}</a>|
def link_to(link, url, title = "")
%(<a href="#{url}" title="#{title}">#{link}</a>)
end
def img_tag(url, alt="")
%|<img src="#{url}" alt="#{alt}" />|
def img_tag(url, alt = "")
%(<img src="#{url}" alt="#{alt}" />)
end
def link_to_vendor(vendor)
if vendor
link_to vendor, url_for_vendor(vendor), vendor
link_to(vendor, url_for_vendor(vendor), vendor)
else
'Unknown Vendor'
end
@@ -38,7 +39,7 @@ module ShopFilter
def link_to_type(type)
if type
link_to type, url_for_type(type), type
link_to(type, url_for_type(type), type)
else
'Unknown Vendor'
end
@@ -53,36 +54,32 @@ module ShopFilter
end
def product_img_url(url, style = 'small')
unless url =~ /\Aproducts\/([\w\-\_]+)\.(\w{2,4})/
unless url =~ %r{\Aproducts/([\w\-\_]+)\.(\w{2,4})}
raise ArgumentError, 'filter "size" can only be called on product images'
end
case style
when 'original'
return '/files/shops/random_number/' + url
'/files/shops/random_number/' + url
when 'grande', 'large', 'medium', 'compact', 'small', 'thumb', 'icon'
"/files/shops/random_number/products/#{$1}_#{style}.#{$2}"
"/files/shops/random_number/products/#{Regexp.last_match(1)}_#{style}.#{Regexp.last_match(2)}"
else
raise ArgumentError, 'valid parameters for filter "size" are: original, grande, large, medium, compact, small, thumb and icon '
end
end
def default_pagination(paginate)
html = []
html << %(<span class="prev">#{link_to(paginate['previous']['title'], paginate['previous']['url'])}</span>) if paginate['previous']
for part in paginate['parts']
if part['is_link']
html << %(<span class="page">#{link_to(part['title'], part['url'])}</span>)
paginate['parts'].each do |part|
html << if part['is_link']
%(<span class="page">#{link_to(part['title'], part['url'])}</span>)
elsif part['title'].to_i == paginate['current_page'].to_i
html << %(<span class="page current">#{part['title']}</span>)
%(<span class="page current">#{part['title']}</span>)
else
html << %(<span class="deco">#{part['title']}</span>)
%(<span class="deco">#{part['title']}</span>)
end
end
html << %(<span class="next">#{link_to(paginate['next']['title'], paginate['next']['url'])}</span>) if paginate['next']
@@ -106,5 +103,4 @@ module ShopFilter
result.gsub!(/\A-+/, '') if result[0] == '-'
result
end
end

View File

@@ -1,10 +1,11 @@
module TagFilter
# frozen_string_literal: true
module TagFilter
def link_to_tag(label, tag)
"<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tag}\">#{label}</a>"
end
def highlight_active_tag(tag, css_class='active')
def highlight_active_tag(tag, css_class = 'active')
if @context['current_tags'].include?(tag)
"<span class=\"#{css_class}\">#{tag}</span>"
else
@@ -14,12 +15,11 @@ module TagFilter
def link_to_add_tag(label, tag)
tags = (@context['current_tags'] + [tag]).uniq
"<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tags.join("+")}\">#{label}</a>"
"<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tags.join('+')}\">#{label}</a>"
end
def link_to_remove_tag(label, tag)
tags = (@context['current_tags'] - [tag]).uniq
"<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tags.join("+")}\">#{label}</a>"
"<a title=\"Show tag #{tag}\" href=\"/collections/#{@context['handle']}/#{tags.join('+')}\">#{label}</a>"
end
end

View File

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

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
# This profiler run simulates Shopify.
# We are looking in the tests directory for liquid files and render them within the designated layout file.
# We will also export a substantial database to liquid which the templates can render values of.
@@ -6,12 +8,11 @@
# Shopify which is likely the biggest user of liquid in the world which something to the tune of several
# million Template#render calls a day.
require File.dirname(__FILE__) + '/shopify/liquid'
require File.dirname(__FILE__) + '/shopify/database.rb'
require_relative 'shopify/liquid'
require_relative 'shopify/database'
class ThemeRunner
class FileSystem
def initialize(path)
@path = path
end
@@ -22,57 +23,104 @@ class ThemeRunner
end
end
# Load all templates into memory, do this now so that
# we don't profile IO.
# Initialize a new liquid ThemeRunner instance
# Will load all templates into memory, do this now so that we don't profile IO.
def initialize
@tests = Dir[File.dirname(__FILE__) + '/tests/**/*.liquid'].collect do |test|
@tests = Dir[__dir__ + '/tests/**/*.liquid'].collect do |test|
next if File.basename(test) == 'theme.liquid'
theme_path = File.dirname(test) + '/theme.liquid'
[File.read(test), (File.file?(theme_path) ? File.read(theme_path) : nil), test]
{
liquid: File.read(test),
layout: (File.file?(theme_path) ? File.read(theme_path) : nil),
template_name: test,
}
end.compact
compile_all_tests
end
# `compile` will test just the compilation portion of liquid without any templates
def compile
# Dup assigns because will make some changes to them
@tests.each do |liquid, layout, template_name|
tmpl = Liquid::Template.new
tmpl.parse(liquid)
tmpl = Liquid::Template.new
tmpl.parse(layout)
@tests.each do |test_hash|
Liquid::Template.new.parse(test_hash[:liquid])
Liquid::Template.new.parse(test_hash[:layout])
end
end
def run
# `run` is called to benchmark rendering and compiling at the same time
def run
each_test do |liquid, layout, assigns, page_template, template_name|
compile_and_render(liquid, layout, assigns, page_template, template_name)
end
end
# `render` is called to benchmark just the render portion of liquid
def render
@compiled_tests.each do |test|
tmpl = test[:tmpl]
assigns = test[:assigns]
layout = test[:layout]
if layout
assigns['content_for_layout'] = tmpl.render!(assigns)
layout.render!(assigns)
else
tmpl.render!(assigns)
end
end
end
private
def render_layout(template, layout, assigns)
assigns['content_for_layout'] = template.render!(assigns)
layout&.render!(assigns)
end
def compile_and_render(template, layout, assigns, page_template, template_file)
compiled_test = compile_test(template, layout, assigns, page_template, template_file)
render_layout(compiled_test[:tmpl], compiled_test[:layout], compiled_test[:assigns])
end
def compile_all_tests
@compiled_tests = []
each_test do |liquid, layout, assigns, page_template, template_name|
@compiled_tests << compile_test(liquid, layout, assigns, page_template, template_name)
end
@compiled_tests
end
def compile_test(template, layout, assigns, page_template, template_file)
tmpl = init_template(page_template, template_file)
parsed_template = tmpl.parse(template).dup
if layout
parsed_layout = tmpl.parse(layout)
{ tmpl: parsed_template, assigns: assigns, layout: parsed_layout }
else
{ tmpl: parsed_template, assigns: assigns }
end
end
# utility method with similar functionality needed in `compile_all_tests` and `run`
def each_test
# Dup assigns because will make some changes to them
assigns = Database.tables.dup
@tests.each do |liquid, layout, template_name|
# Compute page_tempalte outside of profiler run, uninteresting to profiler
page_template = File.basename(template_name, File.extname(template_name))
compile_and_render(liquid, layout, assigns, page_template, template_name)
@tests.each do |test_hash|
# Compute page_template outside of profiler run, uninteresting to profiler
page_template = File.basename(test_hash[:template_name], File.extname(test_hash[:template_name]))
yield(test_hash[:liquid], test_hash[:layout], assigns, page_template, test_hash[:template_name])
end
end
def compile_and_render(template, layout, assigns, page_template, template_file)
tmpl = Liquid::Template.new
tmpl.assigns['page_title'] = 'Page title'
tmpl.assigns['template'] = page_template
# set up a new Liquid::Template object for use in `compile_and_render` and `compile_test`
def init_template(page_template, template_file)
tmpl = Liquid::Template.new
tmpl.assigns['page_title'] = 'Page title'
tmpl.assigns['template'] = page_template
tmpl.registers[:file_system] = ThemeRunner::FileSystem.new(File.dirname(template_file))
content_for_layout = tmpl.parse(template).render!(assigns)
if layout
assigns['content_for_layout'] = content_for_layout
tmpl.parse(layout).render!(assigns)
else
content_for_layout
end
tmpl
end
end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper'
class AssignTest < Minitest::Test
@@ -8,31 +10,31 @@ class AssignTest < Minitest::Test
{% assign this-thing = 'Print this-thing' %}
{{ this-thing }}
END_TEMPLATE
template = Template.parse(template_source)
rendered = template.render!
assert_equal "Print this-thing", rendered.strip
template = Template.parse(template_source)
rendered = template.render!
assert_equal("Print this-thing", rendered.strip)
end
def test_assigned_variable
assert_template_result('.foo.',
'{% assign foo = values %}.{{ foo[0] }}.',
'values' => %w{foo bar baz})
'{% assign foo = values %}.{{ foo[0] }}.',
'values' => %w(foo bar baz))
assert_template_result('.bar.',
'{% assign foo = values %}.{{ foo[1] }}.',
'values' => %w{foo bar baz})
'{% assign foo = values %}.{{ foo[1] }}.',
'values' => %w(foo bar baz))
end
def test_assign_with_filter
assert_template_result('.bar.',
'{% assign foo = values | split: "," %}.{{ foo[1] }}.',
'values' => "foo,bar,baz")
'{% assign foo = values | split: "," %}.{{ foo[1] }}.',
'values' => "foo,bar,baz")
end
def test_assign_syntax_error
assert_match_syntax_error(/assign/,
'{% assign foo not values %}.',
'values' => "foo,bar,baz")
'{% assign foo not values %}.',
'values' => "foo,bar,baz")
end
def test_assign_uses_error_mode
@@ -42,7 +44,74 @@ class AssignTest < Minitest::Test
end
end
with_error_mode(:lax) do
assert Template.parse("{% assign foo = ('X' | downcase) %}")
assert(Template.parse("{% assign foo = ('X' | downcase) %}"))
end
end
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

@@ -1,11 +1,12 @@
# frozen_string_literal: true
require 'test_helper'
class FoobarTag < Liquid::Tag
def render(*args)
" "
def render_to_output_buffer(_context, output)
output << ' '
output
end
Liquid::Template.register_tag('foobar', FoobarTag)
end
class BlankTestFileSystem
@@ -31,7 +32,9 @@ class BlankTest < Minitest::Test
end
def test_new_tags_are_not_blank_by_default
assert_template_result(" "*N, wrap_in_for("{% foobar %}"))
with_custom_tag('foobar', FoobarTag) do
assert_template_result(" " * N, wrap_in_for("{% foobar %}"))
end
end
def test_loops_are_blank
@@ -47,7 +50,7 @@ class BlankTest < Minitest::Test
end
def test_mark_as_blank_only_during_parsing
assert_template_result(" "*(N+1), wrap(" {% if false %} this never happens, but still, this block is not blank {% endif %}"))
assert_template_result(" " * (N + 1), wrap(" {% if false %} this never happens, but still, this block is not blank {% endif %}"))
end
def test_comments_are_blank
@@ -60,9 +63,9 @@ class BlankTest < Minitest::Test
def test_nested_blocks_are_blank_but_only_if_all_children_are
assert_template_result("", wrap(wrap(" ")))
assert_template_result("\n but this is not "*(N+1),
wrap(%q{{% if true %} {% comment %} this is blank {% endcomment %} {% endif %}
{% if true %} but this is not {% endif %}}))
assert_template_result("\n but this is not " * (N + 1),
wrap('{% if true %} {% comment %} this is blank {% endcomment %} {% endif %}
{% if true %} but this is not {% endif %}'))
end
def test_assigns_are_blank
@@ -76,31 +79,31 @@ class BlankTest < Minitest::Test
def test_whitespace_is_not_blank_if_other_stuff_is_present
body = " x "
assert_template_result(body*(N+1), wrap(body))
assert_template_result(body * (N + 1), wrap(body))
end
def test_increment_is_not_blank
assert_template_result(" 0"*2*(N+1), wrap("{% assign foo = 0 %} {% increment foo %} {% decrement foo %}"))
assert_template_result(" 0" * 2 * (N + 1), wrap("{% assign foo = 0 %} {% increment foo %} {% decrement foo %}"))
end
def test_cycle_is_not_blank
assert_template_result(" "*((N+1)/2)+" ", wrap("{% cycle ' ', ' ' %}"))
assert_template_result(" " * ((N + 1) / 2) + " ", wrap("{% cycle ' ', ' ' %}"))
end
def test_raw_is_not_blank
assert_template_result(" "*(N+1), wrap(" {% raw %} {% endraw %}"))
assert_template_result(" " * (N + 1), wrap(" {% raw %} {% endraw %}"))
end
def test_include_is_blank
Liquid::Template.file_system = BlankTestFileSystem.new
assert_template_result "foobar"*(N+1), wrap("{% include 'foobar' %}")
assert_template_result " foobar "*(N+1), wrap("{% include ' foobar ' %}")
assert_template_result " "*(N+1), wrap(" {% include ' ' %} ")
assert_template_result("foobar" * (N + 1), wrap("{% include 'foobar' %}"))
assert_template_result(" foobar " * (N + 1), wrap("{% include ' foobar ' %}"))
assert_template_result(" " * (N + 1), wrap(" {% include ' ' %} "))
end
def test_case_is_blank
assert_template_result("", wrap(" {% assign foo = 'bar' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} {% endcase %} "))
assert_template_result("", wrap(" {% assign foo = 'else' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} {% endcase %} "))
assert_template_result(" x "*(N+1), wrap(" {% assign foo = 'else' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} x {% endcase %} "))
assert_template_result(" x " * (N + 1), wrap(" {% assign foo = 'else' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} x {% endcase %} "))
end
end

View File

@@ -0,0 +1,58 @@
# frozen_string_literal: true
require 'test_helper'
class BlockTest < Minitest::Test
include Liquid
def test_unexpected_end_tag
exc = assert_raises(SyntaxError) do
Template.parse("{% if true %}{% endunless %}")
end
assert_equal(exc.message, "Liquid syntax error: 'endunless' is not a valid delimiter for if tags. use endif")
end
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

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'test_helper'
class CaptureTest < Minitest::Test
@@ -12,9 +14,9 @@ class CaptureTest < Minitest::Test
{% capture this-thing %}Print this-thing{% endcapture %}
{{ this-thing }}
END_TEMPLATE
template = Template.parse(template_source)
rendered = template.render!
assert_equal "Print this-thing", rendered.strip
template = Template.parse(template_source)
rendered = template.render!
assert_equal("Print this-thing", rendered.strip)
end
def test_capture_to_variable_from_outer_scope_if_existing
@@ -28,9 +30,9 @@ class CaptureTest < Minitest::Test
{% endif %}
{{var}}
END_TEMPLATE
template = Template.parse(template_source)
rendered = template.render!
assert_equal "test-string", rendered.gsub(/\s/, '')
template = Template.parse(template_source)
rendered = template.render!
assert_equal("test-string", rendered.gsub(/\s/, ''))
end
def test_assigning_from_capture
@@ -43,8 +45,14 @@ class CaptureTest < Minitest::Test
{% endfor %}
{{ first }}-{{ second }}
END_TEMPLATE
template = Template.parse(template_source)
rendered = template.render!
assert_equal "3-3", rendered.gsub(/\s/, '')
template = Template.parse(template_source)
rendered = template.render!
assert_equal("3-3", rendered.gsub(/\s/, ''))
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

@@ -1,8 +1,597 @@
# frozen_string_literal: true
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
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
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
global = Module.new do
def notice(output)
@@ -17,16 +606,30 @@ class ContextTest < Minitest::Test
end
with_global_filter(global) do
assert_equal 'Global test', Template.parse("{{'test' | notice }}").render!
assert_equal 'Local test', Template.parse("{{'test' | notice }}").render!({}, :filters => [local])
assert_equal('Global test', Template.parse("{{'test' | notice }}").render!)
assert_equal('Local test', Template.parse("{{'test' | notice }}").render!({}, filters: [local]))
end
end
def test_has_key_will_not_add_an_error_for_missing_keys
with_error_mode :strict do
with_error_mode(:strict) do
context = Context.new
context.has_key?('unknown')
assert_empty context.errors
context.key?('unknown')
assert_empty(context.errors)
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

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

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