Compare commits

..
Author SHA1 Message Date
Dominic Petrick d6e07f9e5f pretend we're on 3.4 2025-01-15 11:17:05 +01:00
Ian Ker-Seymer 8c89e6a0dd Implement logic for stringify Hashes to keep compat with 3.4 2025-01-14 18:27:54 -05:00
Dominic Petrick c2530b233b Exploring 2025-01-14 19:37:19 +01:00
ecf25ea83d Add the "Releasing" section to CONTRIBUTING.md (#1891)
* Add 'Releasing' workflow to 'CONTRIBUTING.md'

* Update CONTRIBUTING.md

Co-authored-by: Michael Go <[email protected]>

---------

Co-authored-by: Michael Go <[email protected]>
2025-01-14 19:07:11 +01:00
6909570f8e Add find, find_index, has, and reject filters to arrays (#1869)
* Add reject filter #1573

* Add deep search for filter taking in properties #1749

* Update branch with main

* Add `find`, `find_index`, `has`, and `reject` filters to arrays

* Refactor: avoid usage of public_send

---------

Co-authored-by: Anders Søgaard <[email protected]>
Co-authored-by: Anders Søgaard <[email protected]>
2025-01-14 10:33:31 +01:00
Michael GoandGitHub cd9971579f Merge pull request #1889 from Shopify/default-variable-lookup-string-scanner
Add default string_scanner to `Liquid::VariableLookup.parse`
2025-01-13 23:28:11 -04:00
Ian Ker-Seymer 8e37c5e18b Bump to v5.6.4 2025-01-13 21:57:52 -05:00
Ian Ker-Seymer b3f9639e7d Add default string_scanner to Liquid::VariableLookup.parse 2025-01-13 21:56:39 -05:00
Michael GoandGitHub fe3da0e17d Merge pull request #1887 from Shopify/remove-lru-redux
remove lru-redux
2025-01-13 18:37:17 -04:00
Michael Go e200c4544b remove expression caching while rendering 2025-01-13 18:36:30 -04:00
Michael Go 7124540563 remove lru-redux 2025-01-13 18:27:56 -04:00
Michael GoandGitHub 0558bd12c4 Merge pull request #1886 from Shopify/fix-parsing-float-with-leading-point
float has to start with a digit
2025-01-13 17:17:15 -04:00
Michael Go 0639a094a8 bump version to 5.6.2 2025-01-13 17:16:33 -04:00
Michael Go 58777bcd93 float has to start with a digit 2025-01-13 17:13:27 -04:00
Michael GoandGitHub 323951b36f Merge pull request #1844 from Shopify/fast-expression-parse
Faster Expression parser / Tokenizer with StringScanner
2025-01-10 15:34:06 -04:00
Benjamin SteinandGitHub a5b91e83ae Update README.md with default environment (#1879)
Smaller projects, especially when upgrading from earlier versions, may not need different environment scopes, so just adding to the docs.
2025-01-10 14:28:46 -05:00
Michael Go 10114b333e minor version bump 2025-01-10 15:28:02 -04:00
Michael Go a07ae90523 use Ruby Hash as default expression cache 2025-01-10 15:07:57 -04:00
Michael Go 2e236b0a0e fix expression cache to be compatible with Ruby Hash and LruCache 2025-01-10 14:58:23 -04:00
Michael Go bd05dfbd1c use lru_redux getset for simplicity 2025-01-10 14:41:35 -04:00
Michael Go 2a5ecf068f remove lru_redux from Gemfile 2025-01-10 14:38:50 -04:00
Michael Go 0b8e30b819 update unit test to be compatible with new source_location format 2025-01-10 14:37:54 -04:00
Michael Go 97cada61f9 remove Expression2 strscan workaround 2025-01-10 14:23:57 -04:00
Michael Go 7c9b77e8cf remove StringScanner version check in Lexer and Tokenizer 2025-01-07 14:51:29 -04:00
Michael Go d48708ae46 skip expression caching with Expression1 2025-01-07 14:32:46 -04:00
Michael Go 42d822bda9 users can provide optional expression cache 2025-01-07 14:32:46 -04:00
Michael Go 2c7d686690 adding caching to context variable lookup 2025-01-07 14:32:46 -04:00
Michael Go 313d01706a code styling 2025-01-07 14:32:46 -04:00
Michael Go e9f86724f6 code styling 2025-01-07 14:32:46 -04:00
Michael Go 0a17c15289 freeze '-' VariableLookup in Expression 2025-01-07 14:32:46 -04:00
Michael Go 04bd9dbe90 refactor Lexer to only take StringScanner 2025-01-07 14:32:46 -04:00
Michael Go 253ec81b56 don't add a breaking change to Parser constructor 2025-01-07 14:32:46 -04:00
Michael Go e05719fb82 use Kernel Integer parsing for simple Integer expression 2025-01-07 14:32:46 -04:00
Michael Go ac374a208f freeze Expression2 parse results 2025-01-07 14:32:46 -04:00
Michael Go 91be3dd75e keep the expression parse caching per document parsing 2025-01-07 14:32:46 -04:00
Michael Go 40191022e8 initialize StringScanner with input for default 2025-01-07 14:32:46 -04:00
Michael Go 71506ad54e avoid setting up StringScanner for parsing simple numbers 2025-01-07 14:32:46 -04:00
Michael Go cef64e277e early match number expressions with Regex 2025-01-07 14:32:46 -04:00
Michael Go 7c592c1c00 store StringScanner in ParseContext and reuse it through parsing 2025-01-07 14:32:45 -04:00
Michael Go 002e4caea7 refactor Lexer to be static class function 2025-01-07 14:32:07 -04:00
Michael Go 3c16c27ee1 add StringScannerPool for thread safety 2025-01-07 14:32:07 -04:00
Michael Go eff5c5de8e code clean up 2025-01-07 14:32:07 -04:00
Michael Go 19adfbd863 don't force strscan 3.1 2025-01-07 14:32:07 -04:00
Michael Go a672f3836c don't cache literals for Expression parser 2025-01-07 14:32:07 -04:00
Michael Go 92fa334192 don't cache string expressions to be more perfomant 2025-01-07 14:32:07 -04:00
Michael Go 7c8a269b4d fix cycle tag not resetting 2025-01-07 14:32:07 -04:00
Michael Go 0b1dc295ff fix quirky negative sign expression markup parsing 2025-01-07 14:32:07 -04:00
Michael Go 1de6025362 decrease the size of Expression2 LRU cache to store 5 themes 2025-01-07 14:32:07 -04:00
Michael Go 8ecb703d4d use StringScanner to improve Expression Parsing and Tokenizer 2025-01-07 14:32:06 -04:00
Ian Ker-SeymerandGitHub b4667adadf Bump to v5.6.0 (#1876) 2024-12-19 15:01:16 -05:00
Bahar PourazarandGitHub 94e02d765f Merge pull request #1874 from Shopify/bp/bump-version
Bump version patch
2024-12-17 17:39:35 -05:00
Bahar Pourazar 60701f865d Bump version patch 2024-12-17 17:37:00 +00:00
Bahar Pourazar 0c49dd592f bring back to_s.to_str 2024-12-17 17:37:00 +00:00
Bahar PourazarandGitHub c3ac0e0127 Merge pull request #1873 from Shopify/bp/tokenizer-fix
Fix bug in tokenizer with nil source value
2024-12-17 11:43:16 -05:00
Bahar Pourazar a0a4307e7d Fix bug in tokenizer with nil value 2024-12-17 15:34:13 +00:00
Ian Ker-SeymerandGitHub fdd8c714b2 Stop testing against liquid-c (#1868)
* Stop testing against `liquid-c`

* Bump to `v5.6.0.rc2`
2024-12-11 12:23:50 -05:00
Ian Ker-SeymerandGitHub 63583ffe5b Write one value at a time for array variables (#1863)
* Write one value at a time for array variables

* Handle recursive array
2024-12-11 10:16:58 -05:00
Benjamin SehlandGitHub 9a06cedbba Merge pull request #1634 from tjoyal/patch-1
Update homepage url
2024-12-11 09:20:15 -05:00
Ian Ker-Seymer 42b6763546 Bump to v5.6.0.rc1 2024-11-04 15:26:56 -05:00
Michael GoandGitHub e5d18c83bb Merge pull request #1848 from Shopify/env-warn-cleanup
clean up all warnings by using new Environment
2024-11-04 16:17:17 -04:00
Michael Go c77ff68573 clean up all warnings by using new Environment 2024-11-04 16:15:05 -04:00
Ian Ker-SeymerandGitHub b0cba0bfd2 Remove Liquid.cache_classes option (#1847) 2024-11-04 14:41:56 -05:00
Michael GoandGitHub 8d8661349a Merge pull request #1843 from Shopify/empty-array
avoid allocating new empty array
2024-11-04 15:36:46 -04:00
Michael Go 1f3ea7322b avoid allocating new empty array 2024-11-04 15:35:45 -04:00
Michael GoandGitHub 06f44226c0 Merge pull request #1846 from Shopify/env-propgating
propagate Environment on new Context creation
2024-11-04 15:33:50 -04:00
Michael GoandGitHub 4bd22a26dc Merge pull request #1845 from Shopify/remove-tag-registry
remove TagRegistry
2024-11-04 15:33:30 -04:00
Michael Go 3ed54bfdf9 propagate Environment on new Context creation 2024-11-04 15:32:07 -04:00
Michael Go 29986d3704 remove TagRegistry 2024-11-04 15:22:56 -04:00
Michael GoandGitHub 8e40f8050a Merge pull request #1838 from Shopify/quirky-lexer-parsing
fix parsing quirky incomplete expressions
2024-10-30 13:52:47 -03:00
Michael Go ffce6de8bb avoid using StringScanner eos 2024-10-30 13:45:10 -03:00
Michael Go f00670cb01 refactor lexer unit test 2024-10-30 13:44:36 -03:00
Michael Go f6a3e25e2e fix parsing quirky incomplete expressions 2024-10-30 13:44:35 -03:00
Michael GoandGitHub f6ffc37cf2 Merge pull request #1840 from Shopify/fix-lexer-contains-as-id
fix lexer parsing ID 'contains' as comparison
2024-10-30 13:43:48 -03:00
Michael Go 1375a9e4dc fix lexer parsing ID 'contains' as comparison 2024-10-30 13:39:55 -03:00
Michael GoandGitHub c626dfa1a1 Merge pull request #1839 from Shopify/lexer-parse-error-with-utf8
raise syntax error from lexer parser with UTF-8 character
2024-10-30 13:39:05 -03:00
Michael Go 8a9f33a060 raise syntax error from lexer parser with utf8 character 2024-10-29 22:04:37 -03:00
Michael GoandGitHub 1943441361 Merge pull request #1835 from Shopify/fix-multibyte-variable-parsing
fix parsing Variable blockbody with multibyte character
2024-10-28 19:31:21 -03:00
Michael GoandGitHub 36251e640c Merge pull request #1837 from Shopify/lexer-comparison-fix
fix lexer parsing comparison without whitespaces
2024-10-28 19:31:11 -03:00
Michael Go d94293a464 fix lexer parsing comparison without whitespaces 2024-10-28 19:30:12 -03:00
Michael Go 6c13805a60 fix parsing Variable blockbody with multibyte character 2024-10-28 17:33:55 -03:00
Michael GoandGitHub b4196489c2 Merge pull request #1833 from Shopify/fast-variable-parse
Faster Variable BlockBody Matching
2024-10-28 15:28:04 -03:00
Gray GilmoreandGitHub 6d58c41440 Merge pull request #1831 from Shopify/gg-add-named-params-docs
Update liquid docs for named parameters
2024-10-28 09:20:55 -07:00
Michael Go fb6ac72520 use byteslice to create Variable BlockBody 2024-10-25 15:41:35 -03:00
Michael Go cb16219552 faster BlockBody variable matching 2024-10-25 15:22:56 -03:00
Gray Gilmore 8d7ed706f4 Update liquid docs for named parameters
The YARD liquid gem now supports specifying named parameters. For the
core liquid tags and filters this is the only object I could find that
needed to be updated.
2024-10-24 09:56:50 -07:00
Ian Ker-SeymerandGitHub b3553787c8 Speed up the lexer for Ruby 3.4+ (#1832)
* Speed up lexing

* Bump msrv to 3.0 (from 2.7)

* Normalize test for ruby-head compat

* Fix bug when parsing negative numbers
2024-10-23 14:15:33 -04:00
Ian Ker-SeymerandGitHub b233b3d081 Bump to v5.6.0.alpha (#1819) 2024-08-15 16:12:16 -04:00
Alex CocoandGitHub ac91d31268 Merge pull request #1818 from Shopify/tablerow-interrupt
Handle interrupts in table row
2024-08-15 14:04:35 -04:00
Alex Coco 9067e5167a Handle interrupts in table row 2024-08-15 11:56:29 -04:00
Ian Ker-SeymerandGitHub fb6634f454 Add concept of Liquid::Environment (#1815)
* Add concept of `Liquid::World`

* Rename `World` to `Environment`
2024-08-07 15:00:44 -04:00
Ian Ker-SeymerandIan Ker-Seymer a0411e0927 Bump to v5.5.1 2024-07-22 23:25:56 -04:00
Ian Ker-SeymerandGitHub ed421202e2 Merge pull request #1811 from Shopify/marco-cycle-update
Add named? method to Cycle class for checking if the cycle is named
2024-07-16 15:58:21 -04:00
Ian Ker-Seymer d6ca569e8a Require base64 2024-07-16 15:56:47 -04:00
Marco Concetto RudilossoandIan Ker-Seymer d36937d17f Add named? method to Cycle class for checking if the cycle is named 2024-07-16 15:52:25 -04:00
Peter ZhuandGitHub 77bc56a1c2 Merge pull request #1792 from Shopify/centralize_ruby_version
Centralize Ruby Version to `.ruby-version`
2024-04-11 11:12:08 -04:00
Jenny Shen 88d013c8da Use latest compatible Bundler of each Ruby version run 2024-04-05 10:33:27 -04:00
Jenny Shen 36c7fc8e07 Remove ruby version definition in CI/CD
it will be read from .ruby-version
2024-04-05 10:33:06 -04:00
Jenny Shen 2b4810006b Remove TargetRubyVersion in RuboCop config
reads from required_ruby_version in RuboCop 1.61+
2024-04-05 10:23:39 -04:00
Jenny Shen fc4f19471e Update rubocop to 1.61.0 2024-04-05 10:23:37 -04:00
Jenny Shen 8596bb2e38 Commit a .ruby-version 2024-04-05 10:23:05 -04:00
Ian Ker-SeymerandGitHub 6bf18775e7 Merge pull request #1791 from Shopify/v5.5.0
Bump to v5.5.0
2024-03-21 15:50:59 -04:00
Ian Ker-Seymer 56a0b7c42b Bump to v5.5.0 2024-03-21 15:48:54 -04:00
Ian Ker-SeymerandGitHub dba733084e Merge pull request #1760 from mtasaka/nil-nil-comparison-fix
change: make nil_safe_casecmp judge compatible for nil-nil comparison
2024-03-21 15:38:13 -04:00
Michael GoandGitHub 4a4fe3c72a Merge pull request #1781 from Shopify/contextualize-before-to-liquid
update variable's context before invoking its to_liquid
2024-02-12 16:58:54 -04:00
Michael Go 4f35b0bc66 update liquid-c 2024-02-12 16:49:52 -04:00
Michael Go a5e5fab82a update variable's context before invoking its to_liquid 2024-02-12 16:48:20 -04:00
Ian Ker-SeymerandGitHub 02ecaab9d1 Merge pull request #1783 from Shopify/contains-encoding
Fallback to binary comparison when `contains` RHS is  UTF8 encoded
2024-01-31 11:14:01 -05:00
Ian Ker-Seymer 1b2b62964e Allow for binary comparison of incompatible strings 2024-01-30 19:03:05 -05:00
Michael GoandGitHub 9b38a15282 Merge pull request #1776 from Shopify/refactor/invalid-encoding-error
add new TemplateEncodingError
2024-01-12 12:38:34 -04:00
Michael Go 7b25b770af add new TemplateEncodingError 2024-01-12 12:17:20 -04:00
Michael GoandGitHub cf76c0bbec Merge pull request #1775 from Shopify/allow-nil-template-source
allow non-string template source
2024-01-12 10:48:30 -04:00
Michael Go 6a0fe3f7e3 convert template source to string to ensure encoding validity 2024-01-10 16:50:31 -04:00
Michael Go 730ad3684a allow nil template source 2024-01-10 16:32:08 -04:00
Michael GoandGitHub 3ac7e470e6 Merge pull request #1774 from Shopify/check-utf8-validity
check template UTF8 validity before parsing
2024-01-10 15:27:49 -04:00
Michael Go 369a6c55e3 check template UTF8 validity before parsing 2024-01-10 18:58:15 +00:00
Michael GoandGitHub f5ed5404b5 Merge pull request #1773 from Shopify/liquid-tag-whitespace-control-with-comment
don't reset Liquid tag's whitespace control from comment tag
2024-01-05 10:54:32 -04:00
Michael Go a3c837687e don't reset Liquid tag's whitespace control from comment tag 2024-01-05 10:21:33 -04:00
Michael GoandGitHub 96a036372d Merge pull request #1770 from Shopify/revert-invalid-comment-body
don't allow invalid syntax inside comment tag
2024-01-02 11:07:05 -04:00
Mamoru TASAKA 4924822c88 change: make nil_safe_casecmp judge compatible for nil-nil comparison
Ruby returns 0 (not nil) for nil <=> nil, i.e. nil and nil are judged
as equal for comparison, and so returns nil_safe_compare .
ref: https://github.com/Shopify/liquid/pull/1476

To make the behavior of nil_safe_casecmp consistent with
nil_safe_compare , change nil_safe_casecmp so that comparison between
nil <=> nil return 0 (equal).

Also change testsuite to reflect this change.

Fixes #1759 .
2023-12-15 15:09:57 +09:00
Michael Go fc9c338682 refactor: rename comment tag unit test 2023-12-14 17:08:26 -04:00
Michael Go 3c5ad7db61 don't allow invalid syntax inside comment tag 2023-12-14 17:01:03 -04:00
Michael GoandGitHub c658bf970a Merge pull request #1769 from Shopify/comment-tag-with-extra-string
fix parsing comment tag with extra string
2023-12-13 14:16:44 -04:00
Michael Go c618ac1c9f fix parsing comment tag with extra string 2023-12-12 16:23:37 -04:00
Ian Ker-SeymerandGitHub 0f0d5d889f Merge pull request #1750 from IevaGraz/fix/custom_tag_rendering
Fix for custom tag rendering
2023-12-10 11:28:50 -05:00
Michael GoandGitHub 11a1f8e673 Merge pull request #1764 from Shopify/fix-comment-tag-whitespace-control
implement whitespace control to comment tag
2023-12-06 14:30:16 -05:00
Michael Go 24d461a9e3 implement whitespace control to comment tag 2023-12-01 16:04:03 -04:00
Michael GoandGitHub cbb422e5d3 Merge pull request #1763 from Shopify/fix-comment-tag-delim-parsing
fix parsing comment tag delimiter
2023-12-01 15:35:32 -04:00
Michael Go bf0f79f36c fix parsing comment tag delimiter 2023-11-30 13:24:54 -04:00
Michael GoandGitHub cf2787791e Merge pull request #1755 from Shopify/non-parsing-comment
Don't parse nodes inside a comment tag
2023-11-29 14:10:20 -04:00
Michael Go 6a5ebb0e85 fix parsing nested comment tag with extra strings 2023-11-11 12:19:44 -04:00
Michael Go 6dafc19b6d fix parsing comment tag delimiter with extra strings 2023-11-10 15:42:54 -04:00
Michael Go 41f65173b0 fix parsing comment block body inside a liquid tag 2023-11-09 16:27:23 -04:00
Michael Go e180535784 allow incomplete tags inside a comment tag 2023-11-08 16:50:41 -04:00
Michael Go a681e73aec refactor comment tag unit test to use test helper 2023-11-08 16:45:11 -04:00
Michael Go 2abf52d546 add a quirky comment tag unit test 2023-11-08 11:23:00 -04:00
eada2b65a2 clean up comment tag body parsing
Co-authored-by: Peter Zhu <[email protected]>
2023-11-08 11:13:13 -04:00
Michael GoandAlex Coco dbf0aa8cd5 don't parse nodes inside a comment tag
Co-authored-by: Alex Coco <[email protected]>
2023-11-07 18:38:13 -04:00
Ieva Grazuleviciute 407a8e5b0f Fix for custom tag rendering 2023-10-20 11:42:54 +03:00
Ateş GöralandGitHub e3dcc75ab5 Merge pull request #1748 from Shopify/atesgoral/update-ci-status-badge
Update CI status badge
2023-10-16 11:23:04 -04:00
Ates Goral ceb7a4237f Update CI status badge 2023-10-16 11:17:24 -04:00
Guillaume MaletteandGitHub 7b60b7fef5 Merge pull request #1746 from Shopify/gm/base64-encoding
[base64] Respect string encoding of input in base64_decode filters
2023-09-29 11:13:18 -04:00
Guillaume Malette 33e1a8ffbc fix style 2023-09-29 09:50:42 -04:00
Guillaume Malette cc47fa8f03 [base64] Respect string encoding of input in base64_decode filters 2023-09-29 09:39:59 -04:00
Adam KlingbaumandGitHub b1b9b9f691 Merge pull request #1739 from Shopify/klingbaum/fix-sum-filter-float-output
Fix `BigDecimal` output in `sum` filter
2023-09-27 13:34:27 -04:00
Adam Klingbaum 75e7725f57 Streamline new tests for sum filter 2023-08-08 16:15:48 +00:00
James PriorandAdam Klingbaum de6d15a73e Test sum floats from properties. 2023-08-08 16:10:04 +00:00
James PriorandAdam Klingbaum 2c5d2be193 Don't render sum filter results in scientific notation. 2023-08-08 16:10:03 +00:00
Samuel DoironandGitHub 0b9318222b Merge pull request #1735 from Shopify/marco-condition-to-liquid-value
Call to_liquid_value when short circuiting conditions
2023-07-19 15:05:00 -03:00
Marco Rudilosso 21d6197533 Call to_liquid_value when short circuiting conditions 2023-07-19 15:22:12 +00:00
Marco Concetto RudilossoandGitHub 5e92b3a89a Merge pull request #1731 from Shopify/allow-recursive-liquid-tags
Allow liquid tag inside liquid tag
2023-07-10 13:25:13 +01:00
Marco Concetto Rudilosso 7f2cf1fe67 Recursively parse for liquid tag 2023-07-07 11:20:34 +01:00
Marco Concetto Rudilosso 10e0fb795e Allow liquid tag inside liquid tag 2023-07-06 10:45:12 +01:00
Michelle VinciandGitHub 546dd9bc06 Merge pull request #1730 from Shopify/capitalize-desc-fix
[Liquid reference]: Update description of `capitalize`
2023-07-05 08:52:26 -07:00
Michelle Vinci 9a77e3e923 update desc 2023-07-05 08:44:06 -07:00
Michael GoandGitHub c44d1d9193 Merge pull request #1727 from Shopify/partial-cache-with-error-mode
include parse mode to partial cache key
2023-06-27 15:06:58 -03:00
Michael Go dd7bbf26bc include error mode to partial cache key 2023-06-23 18:48:07 -03:00
Adam KlingbaumandGitHub cca24a2226 Merge pull request #1726 from Shopify/klingbaum/ensure_sum_filter_calls_#to_liquid
Ensure `sum` filter calls `to_liquid` on evaluated property value
2023-06-22 15:47:36 -04:00
Adam Klingbaum 98ce25cb40 Ensure sum filter calls to_liquid 2023-06-22 16:25:11 +00:00
Adam KlingbaumandGitHub 77293d4524 Merge pull request #1722 from Shopify/klingbaum/sum-filter
Add `sum` filter to standard filters
2023-06-19 15:15:49 -04:00
Adam Klingbaum af66bc8a5f Add sum filter to standard filters 2023-06-19 19:11:05 +00:00
Guillaume MaletteandGitHub 42e5c52336 Merge pull request #1717 from Shopify/gm/fix-raw-trimmode-at-end-of-block
[trim] Fix trimming at end of raw block when specified in the opening tag
2023-05-19 14:08:02 -04:00
Guillaume Malette 649cca1349 [trim] Fix trimming at end of raw block when specified in the opening tag 2023-05-19 11:41:45 -04:00
Ian Ker-SeymerandGitHub 81ed65f2a1 Merge pull request #1688 from Shopify/fix-readme-docs-link
Fix broken documentantion link in README
2023-05-11 18:48:09 -04:00
Jeffrey LoppertandGitHub 6ca06c22b8 Merge pull request #1712 from Shopify/jloppert/update-readme-docs-link
Update Liquid Documentation from Shopify URL
2023-05-10 16:20:40 -04:00
Jeffrey LoppertandGitHub 80bc7ffdf2 Update Liquid Documentation from Shopify URL
Current url https://shopify.dev/docs/docs/api/liquid 404s. Updated to https://shopify.dev/docs/api/liquid
2023-05-10 14:47:16 -04:00
Michael GoandGitHub 48cb643c02 Merge pull request #1695 from Shopify/syntax-error-with-filename
render template name with Liquid Syntax errors
2023-03-02 17:08:27 -04:00
Michael Go 1d97389fb0 render template name with Liquid Syntax errors 2023-03-02 16:50:55 -04:00
Michael GoandGitHub 3ff4170cb0 Merge pull request #1692 from Shopify/error-message-with-filepath
set context's template_name with template.name in render and include tag
2023-02-28 10:57:24 -04:00
Michael Go 24dceef552 set context's template_name with template's name 2023-02-28 10:55:38 -04:00
Michael Go 428c66ffac add name attribute to Template for more flexibility to set context's template name 2023-02-27 17:11:52 -04:00
Michael Go 0fe4a5d144 render error message with actual template path 2023-02-27 11:01:21 -04:00
Dylan Thacker-SmithandGitHub e650dc4195 Revert "Instrument usage of bug with iteration of String with offset or 0 limit (#1667)" (#1677)
This reverts commit c743936a78.
2023-02-22 12:48:18 -05:00
Robertas Godelis a75517e2c7 Fix broken documentantion link in README 2023-02-15 18:27:36 +02:00
Guilherme CarreiroandGitHub 9ab688eada Update rubocop-shopify (2.7.0 -> 2.12.0) (#1687) 2023-02-15 08:40:59 +01:00
Erin MorrisseyandGitHub abef59d129 Merge pull request #1685 from Shopify/update-liquid-paths
Update link paths in docs markup
2023-02-14 10:37:15 -08:00
Erin Morrissey 940c3a4207 update link paths in docs markup 2023-02-14 10:34:15 -08:00
Guilherme CarreiroandGitHub 84a0289ebc Fix CI (as it installs RuboCop 1.45.1 and fails) (#1686) 2023-02-14 12:24:38 +01:00
Michael GoandGitHub 4599e5459f Merge pull request #1684 from Shopify/fix-variable-lookup-parse-timeout
fix variable lookup parse timing out with missing closing bracket
2023-02-02 10:39:46 -04:00
Dylan Thacker-SmithandGitHub 59c445f0e1 Avoid a couple of duplicate assertions (#1675) 2023-02-02 09:31:01 -05:00
Michael Go bd9c3802c8 add variable parser timeout unit tests 2023-02-01 21:15:35 -04:00
Michael Go 2b40850e4a fix variable lookup parse timing out with missing closing bracket 2023-02-01 19:52:30 -04:00
Marco Concetto RudilossoandGitHub 22ded5f304 Merge pull request #1683 from Shopify/marco/raw-tag-whitespace-control
Allow raw tag to be used with whitespace control
2023-02-01 16:51:46 +00:00
Marco Concetto Rudilosso ddc32b7bd8 actually test whitespace stripping 2023-02-01 15:52:34 +00:00
Marco Concetto Rudilosso 74e505f6fa Allow raw tag to be used with whitespace control 2023-02-01 13:49:51 +00:00
Michael GoandGitHub 6a888d4564 Merge pull request #1680 from Shopify/recursively-parse-brackets
recursively parse brackets on variable lookup
2023-01-31 16:29:54 -04:00
Michael Go dd257b3d66 add an unit test for double nest variable lookup 2023-01-30 20:36:14 -04:00
Michael Go 1aaf6ed019 recursively parse brackets on variable lookup 2023-01-30 20:19:35 -04:00
Michael GoandGitHub daf93a83c2 Merge pull request #1676 from Shopify/tablerow-nil-params
raise invalid integer argument error from tablerow
2023-01-18 11:08:04 -04:00
Michael Go e889a9da0b use to_i to parse parameters of tablerow tag 2023-01-17 17:48:16 -04:00
Michael Go 0f11c97623 raise invalid integer argument error from tablerow 2023-01-16 18:53:53 -04:00
Dylan Thacker-SmithandGitHub e804f36681 Merge pull request #1674 from Shopify/better-test-to-liquid-value
Improve test coverage of the to_liquid_value feature.
2023-01-16 10:44:41 -05:00
Dylan Thacker-Smith 619ed3fcd7 Add some additional to_liquid_value assertions 2023-01-13 15:46:12 -05:00
Dylan Thacker-Smith cdb5cb06b2 Remove == method from drops from testing to_liquid_value
Since they could cause tests to pass without to_liquid_value being called
on the left side of the equality comparison.
2023-01-13 15:37:27 -05:00
Jean byroot BoussierandGitHub bc153159e6 Merge pull request #1673 from Shopify/initialize-context
Initialize context to nil on the Drop class
2023-01-12 22:19:28 +01:00
Jemma Issroff 128b4e35be Initialize context to nil on the Drop class
We believe this will reduce megamorphic exits in YJIT. There are
currently 241 separate shapes generated with edge_name "@context" in
SFR. Initializing it to nil will significantly reduce this number.
2023-01-12 15:35:22 -05:00
Dylan Thacker-SmithandGitHub c743936a78 Instrument usage of bug with iteration of String with offset or 0 limit (#1667) 2023-01-11 14:05:50 -05:00
Dylan Thacker-SmithandGitHub bf711a0521 Provide another assertion for to_liquid_value unless tag test (#1672)
Since the original one would pass even if to_liquid_value isn't called on
the BooleanDrop object.
2023-01-11 14:05:27 -05:00
Dylan Thacker-SmithandGitHub e8731f27d9 Merge pull request #1671 from ashmaroli/use-builtin-bundle-cache
Use cache built into `ruby/setup-ruby` action
2023-01-11 11:40:11 -05:00
Ashwin Maroli 6a44c1ec77 Do not set ENV["BUNDLE_PATH"] 2023-01-11 22:06:47 +05:30
Ashwin Maroli 1beb87b446 Bump latest Ruby version in CI matrix 2023-01-11 20:58:17 +05:30
Ashwin Maroli b839deb3a8 Use cache built into ruby/setup-ruby action 2023-01-11 20:50:44 +05:30
liamgriffinandGitHub 0b826120c0 Merge pull request #1665 from Shopify/update-description-divided_by-filter
[Liquid reference docs] Adding more content to divided_by filter
2023-01-09 14:12:23 +00:00
liamgriffin 936f803a4e Removed trailing space 2023-01-06 11:36:20 +00:00
liamgriffin 5cd8a83fa6 Adding more content to divided_by filter
Adding more content to divided_by filter, by detailing the behaviour of how different results are generated by different types of divisors.
2023-01-06 11:19:13 +00:00
Dylan Thacker-SmithandGitHub c2c6cb2b15 Allow commas to separate for tag attributes (#1658)
For consistency with tags like the `render` tag, where we actually
prefer to use commas to separate attributes
2022-11-29 09:00:55 -05:00
shainaraskasandGitHub 9ccbf64571 Merge pull request #1656 from Shopify/sr-inline-comment
clarify inline_comment
2022-11-14 10:38:55 -05:00
Shaina Raskas 8b1b9f649a remove separate inline comment tag for IA reasons 2022-11-11 16:21:18 -05:00
Shaina Raskas 474315c6bb edits 2022-11-11 16:03:52 -05:00
Shaina Raskas 667664bb22 clarify inline_comment 2022-11-11 15:34:35 -05:00
Dylan Thacker-SmithandGitHub 1cdd1f0834 refactor: Advance tokenizer array offset instead of using Array#shift (#1653)
Array#shift would move all the remaining elements of the array, which
is slower for larger arrays.
2022-11-09 13:03:33 -05:00
Dylan Thacker-SmithandGitHub 1cd5ec54f0 Merge pull request #1641 from Shopify/assert-template-result-static-env
Use static environment assert_template_result input values
2022-11-02 10:05:52 -04:00
Peter ZhuandGitHub 29732f4305 Merge pull request #1345 from Shopify/pz-fix-tags-in-comment
Fix tags in comment
2022-11-01 13:07:03 -04:00
Peter Zhu abed47547c Fix tags in comment 2022-11-01 13:04:41 -04:00
Dylan Thacker-SmithandGitHub ace3fe15ac Merge pull request #1648 from Shopify/fix-include-internal-error
Prevent an internal error in include tag from non-string template_name
2022-10-28 14:29:03 -04:00
Dylan Thacker-Smith db8e85ab31 Prevent an internal error in include tag from non-string template_name
which would otherwise happen on `template_name.split('/')`
2022-10-28 13:59:00 -04:00
Dylan Thacker-Smith f484b868d0 assert_template_result: Avoid using the BlankFileSystem
Since it doesn't reflect the real liquid usage that we are trying
to test against.
2022-10-28 13:55:33 -04:00
Dylan Thacker-SmithandGitHub 3e8994c258 Have subclasses of a tag inherit superclass's disabled tags (#1646) 2022-10-27 11:13:50 -04:00
Erin MorrisseyandGitHub 7ccac29688 Merge pull request #1635 from Shopify/update-doc-links
Update link paths in docs markup
2022-10-26 11:07:46 -07:00
Dylan Thacker-SmithandGitHub 96b5325f87 Remove Usage.increment calls that have been shown to in fact be used (#1645)
I think these were added for undocumented features to see if they were
actually used and they were being used.
2022-10-26 09:21:43 -04:00
Erin Morrissey b8f05267a9 update link paths in docs markup 2022-10-25 11:53:27 -07:00
Dylan Thacker-SmithandGitHub 6ce4ec1011 tablerow: Avoid accidental special case for constant nil cols (#1644)
It should behave the same as an expression that evaluates to nil
2022-10-25 10:21:03 -04:00
Dylan Thacker-Smith a39422feac Use static environment assert_template_result input values
To match how we use liquid in practice
2022-10-21 11:45:29 -04:00
Dylan Thacker-Smith 2c2f5826d5 Change increment/decrement tests to avoid relying on input
So these can be tested using language tests, without needing support
for counter value input, which isn't needed if input is provided
through the static environment.
2022-10-21 11:43:58 -04:00
Dylan Thacker-SmithandGitHub c99c93255d Merge pull request #1617 from Shopify/better-filter-overflow-handling
Support big integers in truncatewords and slice filters
2022-10-11 12:55:33 -04:00
Dylan Thacker-Smith c0c191cabd Add assertions for truncate filter with large integers 2022-10-11 12:44:33 -04:00
Dylan Thacker-Smith 6765d93938 Avoid internal errors for large arguments to slice filter
Use saturating conversion, which has expected semantics for large integers.
2022-10-11 12:44:32 -04:00
Dylan Thacker-Smith 4f17abfb4a Handle truncatewords word length out of range as if no truncation is needed 2022-10-11 12:44:32 -04:00
Michael GoandGitHub eff2a63204 Merge pull request #1633 from Shopify/fix-tablerow-drop-cols-last-attr
fix tablerow drop's last attribute with missing cols param
2022-10-05 14:15:03 -03:00
Michael Go fbab19ac8c fix tablerow drop's last attribute with missing cols param 2022-10-05 11:15:23 -03:00
Jan Gregor TriebelandGitHub b14bf94c98 Merge pull request #1636 from Shopify/documentation-clarification-for-comment
Clarifying description for {% comment %}
2022-10-05 14:54:29 +02:00
Thierry JoyalandGitHub 347a2418c4 Update homepage url
`http://www.liquidmarkup.org` is `http` 
`http://www.liquidmarkup.org` redirects to `https://shopify.github.io/liquid/`
`https://www.liquidmarkup.org` can’t provide a secure connection (ERR_SSL_PROTOCOL_ERROR)
2022-10-03 16:52:22 -04:00
Jan Emge-Triebel 951be6c1f5 Clarified comment's liquid description 2022-09-30 09:07:06 +00:00
liamgriffinandGitHub c8f3cfa8fc Merge pull request #1628 from Shopify/updating-escape-filter-reference
Update escape filter reference summary
2022-09-29 14:32:36 +01:00
liamgriffin f21d1c5d9d Update escape filter reference summary
Added more context/ explanation for escape filter

Fixes issue in dev docs https://github.com/Shopify/shopify-dev/pull/26494
2022-09-28 12:24:46 +01:00
Dylan Thacker-SmithandGitHub 456be2f75e Add a test and improve one for testing break (#1616) 2022-09-15 10:08:15 -04:00
Dylan Thacker-SmithandGitHub ff1c35b986 Stop using assert_template_result in some tests depending on language extension (#1622) 2022-09-14 17:03:48 -04:00
Dylan Thacker-SmithandGitHub cab08cfe57 Stop freezing the static environment hashes to allow Proc memoization (#1623) 2022-09-14 15:00:57 -04:00
Dylan Thacker-SmithandGitHub bb7027138e Merge pull request #1620 from Shopify/use-partials-test-option
Use assert_template_result partials option to specify file system state
2022-09-13 15:36:50 -04:00
Dylan Thacker-Smith 34512df8e9 Use assert_template_result partials option to specify file system state 2022-09-13 10:11:01 -04:00
Dylan Thacker-Smith bca01e8944 Add assert_syntax_error convenience method 2022-09-13 10:11:01 -04:00
Dylan Thacker-SmithandGitHub d3647e280d Merge pull request #1615 from Shopify/render-expose-for-alias
Expose alias_name and for_loop? in render tag
2022-09-08 10:40:27 -04:00
Marco Concetto RudilossoandDylan Thacker-Smith c0f565ce7b Update lib/liquid/tags/render.rb
Co-authored-by: Dylan Thacker-Smith <[email protected]>
2022-09-08 10:37:23 -04:00
Marco Concetto RudilossoandGitHub 753015d8fc remove question mark 2022-09-08 15:59:46 +02:00
Marco Concetto RudilossoandGitHub f4e32d2214 Expose alias_name and for_loop? in render tag 2022-09-08 15:38:18 +02:00
Dylan Thacker-SmithandGitHub 8adbbfeaa6 Merge pull request #1614 from Shopify/extend-assert-tempate-result
Add partials, error_mode and render_errors options to assert_template_result
2022-09-07 13:17:08 -04:00
Dylan Thacker-Smith 4648f0fa64 Add sample usage of render_errors: true assert_template_result option 2022-09-06 16:54:38 -04:00
Dylan Thacker-Smith 3a64b3741f Add sample usage of error_mode test helper option 2022-09-06 16:44:40 -04:00
Dylan Thacker-Smith 2c51a1922a Add error_mode option to assert_match_syntax_error 2022-09-06 16:44:40 -04:00
Dylan Thacker-Smith 57fdc1b5fb Add sample use of partials assert_template_result options 2022-09-06 16:44:39 -04:00
Dylan Thacker-Smith 308dfc3cb6 Add partials, error_mode and render_errors options to assert_template_result 2022-09-06 16:44:39 -04:00
Dylan Thacker-SmithandGitHub 2515f3be09 Use assert_template_result & assert_match_syntax_error in more places (#1611) 2022-09-01 17:40:39 -04:00
Dylan Thacker-SmithandGitHub 433ed0fff2 Merge pull request #1612 from Shopify/assert-template-result-reserve-kwargs
Reserve keyword arguments for new options in assert_template_result
2022-09-01 17:39:28 -04:00
Dylan Thacker-Smith 0787660603 Reserve keyword arguments for new options in assert_template_result 2022-09-01 17:38:09 -04:00
Dylan Thacker-Smith 93c252fe5a Stop passing unnecessary assigns to assert_match_syntax_error 2022-09-01 17:38:09 -04:00
Dylan Thacker-Smith df13389940 Stop passing assigns as keyword arguments to assert_template_result 2022-09-01 17:38:08 -04:00
Dylan Thacker-SmithandGitHub ca2d850eea Stop using Liquid::Expression.parse for integration testing (#1610)
Since liquid-c no longer monkey patches it, so we need a higher-level
tests to ensure they are shared across liquid implementations.
2022-09-01 17:37:47 -04:00
Dylan Thacker-SmithandGitHub 3a736da222 Add a variable_name method Increment and Decrement tags objects (#1609)
In order to expose this state when using the parse tree.
2022-08-31 09:18:33 -04:00
Dylan Thacker-SmithandGitHub eb89f22d93 Raise Liquid::SyntaxError instead of NoMethodError for invalid range (#1607) 2022-08-25 12:03:56 -04:00
Dylan Thacker-SmithandGitHub 98e146ebf7 Merge pull request #1606 from Watson1978/performance-lexer-tokenize
Increase performance in Liquid::Lexer#tokenize
2022-08-24 15:03:41 -04:00
Watson 7611463f02 Increase performance in Liquid::Lexer#tokenize
To obtain String Literal, the regular expression might be executed at two times.
It would be slightly faster to run them all at once using `Regexp.union`.

−               | before   | after   | result
--               | --       | --      | --
parse            | 63.418   | 65.183  | 1.028x
render           | 195.389  | 195.648 | -
parse & render   | 46.091   | 46.917  | 1.018x

### Environment
- MacBook Pro (14 inch, 2021)
- macOS 13.0 Beta
- Apple M1 Max
- Ruby 3.1.2

### Before
```
Running benchmark for 10 seconds (with 5 seconds warmup).

Warming up --------------------------------------
              parse:     6.000  i/100ms
             render:    19.000  i/100ms
     parse & render:     4.000  i/100ms
Calculating -------------------------------------
              parse:     63.418  (± 0.0%) i/s -    636.000  in  10.028939s
             render:    195.389  (± 0.5%) i/s -      1.957k in  10.016466s
     parse & render:     46.091  (± 0.0%) i/s -    464.000  in  10.067445s
```

### After
```
Running benchmark for 10 seconds (with 5 seconds warmup).

Warming up --------------------------------------
              parse:     6.000  i/100ms
             render:    19.000  i/100ms
     parse & render:     4.000  i/100ms
Calculating -------------------------------------
              parse:     65.183  (± 0.0%) i/s -    654.000  in  10.033549s
             render:    195.648  (± 1.0%) i/s -      1.957k in  10.003511s
     parse & render:     46.917  (± 0.0%) i/s -    472.000  in  10.060782s
```
2022-08-24 04:23:05 +09:00
Dylan Thacker-SmithandGitHub f1846d63a3 Merge pull request #1597 from Shopify/replace-cla-probot-with-action
Migrate off probot-CLA to new GitHub Action
2022-07-29 13:01:24 -04:00
CP ClermontandGitHub af3f8612bf Merge pull request #1601 from Shopify/bump/5.4.0
Bump version to 5.4.0 for release
2022-07-29 10:29:21 -04:00
Charles-P. Clermont 6f8722a6d3 Bump version to 5.4.0 for release 2022-07-28 13:47:53 -04:00
Zoey LanandGitHub 81f44e36be Merge pull request #1600 from Shopify/zoey/expose-filter-names
Expose global filter names
2022-07-21 12:55:02 -06:00
Zoey Lan c9ec8f4635 Expose global filter names 2022-07-21 12:34:41 -06:00
CP ClermontandGitHub 3fb467f069 Merge pull request #1596 from Shopify/fix/theme-check-582-render-with-parse-tree-visitor
Add variable_name_expr to render's ParseTreeVisitor
2022-07-18 08:49:47 -04:00
Charles-P. Clermont 992e15a173 Add variable_name_expr to render's ParseTreeVisitor
Solves Shopify/theme-check#582

`icon` should be visited in the render tag for the following snippet:

```liquid
{% assign icon = 'warning' }
{% render 'icon' with icon %}
```
2022-07-18 08:48:18 -04:00
Yevhenii Huselietov 0bb6539dce Remove CLA from probot and use new GitHub action 2022-07-16 11:59:44 -04:00
NadaMarawanandGitHub 1fdc577246 Merge pull request #1595 from Shopify/jake-clarify-sort-natural
Clarify sort_natural and fix unless syntax
2022-07-13 15:17:15 -04:00
Jake Olney 74245cd396 clarify sort_natural and fix unless syntax 2022-07-12 18:33:15 -07:00
Dylan Thacker-SmithandGitHub 86605016e1 Add Liquid::VariableLookup#lookup_command? to expose this parse node state (#1583) 2022-07-04 14:42:05 -04:00
NadaMarawanandGitHub 6981305736 Merge pull request #1588 from Shopify/jake-add-forloop-parentloop
document forloop.parentloop
2022-06-30 10:16:25 -04:00
Jake Olney 6d3c5ef3d3 document forloop.parentloop 2022-06-28 17:10:25 -07:00
Melanie WangandGitHub 150ddf4c3b Merge pull request #1576 from Shopify/jake-liquid-schema-update
Add yard tags
2022-06-27 15:26:07 -04:00
Jake Olney a3e9088a0e fix concat category 2022-06-27 12:22:18 -07:00
Jake OlneyandMelanie Wang b05393884d update empty 2022-06-27 15:10:24 -04:00
Jake OlneyandMelanie Wang ef9db08642 fix typo 2022-06-27 15:10:24 -04:00
Jake OlneyandMelanie Wang 5dd8c84b47 remove liquid tag 2022-06-27 15:10:23 -04:00
Jake OlneyandMelanie Wang 1af12c74cc fix default category 2022-06-27 15:10:23 -04:00
Jake OlneyandMelanie Wang 72bbbda022 updates from audit 2022-06-27 15:10:23 -04:00
Jake OlneyandMelanie Wang 489e3ca7bf update tablerow to tablerowloop 2022-06-27 15:10:23 -04:00
Jake OlneyandMelanie Wang 65542f9e4f small change for for syntax keyword 2022-06-27 15:10:23 -04:00
Jake OlneyandMelanie Wang d497bfffe9 update for and tablerow 2022-06-27 15:10:22 -04:00
Jake OlneyandMelanie Wang ebafb0a3fe changes from example feedback 2022-06-27 15:10:22 -04:00
Jake OlneyandMelanie Wang 4ec9db3f99 iain feedback 2022-06-27 15:10:22 -04:00
Jake OlneyandMelanie Wang 95eb5d6036 add inline_comment 2022-06-27 15:10:22 -04:00
Jake OlneyandMelanie Wang 7ba40d48c0 fix lint error 2022-06-27 15:10:22 -04:00
Jake OlneyandMelanie Wang 9020dbcd41 shaina feedback 2022-06-27 15:10:22 -04:00
Jake OlneyandMelanie Wang 73f7467258 fix lint errors 2022-06-27 15:10:21 -04:00
Jake OlneyandMelanie Wang f3aa5fbd7c add yard tags 2022-06-27 15:10:21 -04:00
Dylan Thacker-SmithandGitHub eb70bb9b87 Refactor Liquid::Variable to respect disabling liquid-c nodes (#1584)
parse_context.parse_expression is overriden in liquid-c and will avoid
parsing to a Liquid::C::Expression when parsing with
`disable_liquid_c_nodes: true`
2022-06-21 15:19:56 -04:00
Dylan Thacker-SmithandGitHub 4d8e55dbc4 Merge pull request #1580 from Shopify/rubocop-autocorrect
Rubocop autocorrect Gemspec/DeprecatedAttributeAssignment offense
2022-06-06 11:53:06 -04:00
Dylan Thacker-Smith f697093e94 Use stricter rubocop-shopify version constraint
to reduce CI failures being introduced without a code change.
2022-06-06 11:20:05 -04:00
Dylan Thacker-Smith 7594bed88a Rubocop autocorrect Gemspec/DeprecatedAttributeAssignment offense 2022-06-06 11:09:47 -04:00
Dylan Thacker-SmithandGitHub f64471eb4e Merge pull request #1569 from Shopify/registers-refactor
Rename and alias Liquid::StaticRegisters to Liquid::Registers
2022-06-06 10:26:11 -04:00
Andy WaiteandGitHub d4c24f3ce2 Merge pull request #1578 from Shopify/andyw8/drop-support-for-ruby2.5-and-2.6
Drop support for Ruby 2.5 and 2.6
2022-05-26 12:51:44 -04:00
Andy Waite 3bbb7aa7ba Reword Ruby breaking change notice 2022-05-19 10:10:44 -04:00
Andy Waite 102bac2e33 Regenerate .rubocop_todo.yml 2022-05-18 15:25:59 -04:00
Andy Waite e69f729f76 Use latest rubocop-shopify 2022-05-18 15:25:34 -04:00
Andy Waite 4ec0b85d80 Drop support for Ruby 2.5 and 2.6 2022-05-18 15:23:32 -04:00
Dylan Thacker-Smith f77c766262 Rename Liquid::Registers @registers to @changes
since it doesn't represent all the registers
2022-05-04 17:25:53 -04:00
Dylan Thacker-Smith 7bc14ae2be Remove unused class Liquid::Register to avoid confusion 2022-05-04 17:25:52 -04:00
Dylan Thacker-Smith 41c19929ad Rename and alias Liquid::StaticRegisters to Liquid::Registers 2022-05-04 17:25:52 -04:00
Dylan Thacker-SmithandGitHub 05d768c6ab Merge pull request #1498 from Shopify/feature/new-comment-syntax
Add `#` inline comment tag.
2022-04-28 09:46:59 -04:00
Dylan Thacker-Smith 54414dfd83 Add changelog entry, making the next release a feature release 2022-04-28 09:43:08 -04:00
Dylan Thacker-Smith 23a8438fa6 Use liquid-c master branch again, it now has inline comment support 2022-04-28 09:39:16 -04:00
Dylan Thacker-Smith 21f3337dec Test a blank line in a comment tag 2022-04-28 09:38:44 -04:00
Charles-P. ClermontandDylan Thacker-Smith 1f0a0ad55c Add # inline comment tag.
This commit adds a new tag named `#` that behaves like a comment.

Therefore it behaves as you'd expect any tag would work. The difference
with the comment tag is that the comment is in the tag markup and that
there is no block delimiter.

What it looks like in practice:

```liquid
{%- # this is an inline comment -%}
{% # this too is an inline comment %}

{% liquid
  # required args:
  assign product = product

  # optional args:
  assign should_show_border = should_show_border | default: true
  assign should_show_cursor = should_show_cursor | default: true
%}

{% liquid
  # This is a very long comment that spans multiple lines.
  # It looks very similar to what it would look like if you wrote
  # ruby code instead of liquid. But it doesn't have all the clunk
  # of having an open tag and a close tag with so many characters.
%}
```

Co-authored-by: Dylan Thacker-Smith <[email protected]>
2022-04-28 09:38:44 -04:00
Dylan Thacker-SmithandGitHub 36dce29776 Avoid evaluating the template name in the render tag (#1568) 2022-04-21 16:10:42 -04:00
Dylan Thacker-SmithandGitHub 8b68630a11 Merge pull request #1474 from Shopify/clarify-warn-error-mode-doc
Clarify that the error_mode: :warn parse option is only for strict errors
2022-04-21 12:25:36 -04:00
Chris AtLeeandGitHub 8882338aa1 Merge pull request #1553 from Shopify/catlee/shared_partial_cache
Ensure that partial caches are shared with subcontexts
2022-04-08 10:09:49 -04:00
Chris AtLeeandDylan Thacker-Smith 6c2c621712 Ensure that partial caches are shared with subcontexts
Make Context use StaticRegisters by default. This makes it easier to
ensure that all subcontexts share the same static registers.

Co-authored-by: Dylan Thacker-Smith <[email protected]>
2022-04-08 10:05:58 -04:00
Dylan Thacker-SmithandGitHub 1cae1e497f Merge pull request #1560 from ghousemohamed/fix-typo
Fix typo: syntetic -> synthetic
2022-04-05 20:15:09 -04:00
Dylan Thacker-SmithandGitHub ed7dae50aa Merge pull request #1562 from ghousemohamed/bump-actions-checkout-to-v3
Bump actions/checkout to v3
2022-04-05 19:46:41 -04:00
Ghouse Mohamed 7e99432bd1 Bumped actions/checkout to v3 2022-04-04 03:57:24 +05:30
Ghouse Mohamed 6c187b8470 Fix typo: syntetic -> synthetic 2022-03-27 23:18:01 +05:30
Dylan Thacker-SmithandGitHub 6e07f73f68 History.md: Remove non-fix from fixes section of recent release. (#1556) 2022-03-23 11:59:50 -04:00
Marc-André CournoyerandGitHub f64af57b7b Merge pull request #1540 from Watson1978/remove-redundant-regexp
Remove redundant regexp
2022-03-22 14:52:49 -04:00
Marc-André CournoyerandGitHub c60c3c7802 Merge pull request #1554 from Shopify/bump-5.3.0
Update changelog & bump version for 5.3.0 release
2022-03-22 13:30:28 -04:00
Marc-André Cournoyer 11625b1bc9 Update release date 2022-03-22 13:24:02 -04:00
Marc-André Cournoyer ec6fb4d5fa Update changelog & bump version for 5.3.0 release 2022-03-17 15:39:04 -04:00
Watson fad58ef436 Use String#match? instead of String#=~ to reduce allocation for backreferecne
## Test code
```ruby
require 'benchmark/ips'

WhitespaceOrNothing = /\A\s*\z/
token = " " * 20
token =~ WhitespaceOrNothing

Benchmark.ips do |x|
  x.report("=~") {
    token =~ WhitespaceOrNothing
  }
  x.report("match?") {
    token.match?(WhitespaceOrNothing)
  }

  x.compare!
end
```

## Result
```
Warming up --------------------------------------
                  =~   271.356k i/100ms
              match?   579.655k i/100ms
Calculating -------------------------------------
                  =~      2.717M (± 0.4%) i/s -     13.839M in   5.092947s
              match?      5.695M (± 1.6%) i/s -     28.983M in   5.090640s

Comparison:
              match?:  5694747.3 i/s
                  =~:  2717370.9 i/s - 2.10x  (± 0.00) slower
```
2022-03-16 12:44:27 +09:00
Watson 22568080b1 Revert "Use strip & empty? to detect Whitespaces"
This reverts commit dd7ed00ec4.
2022-03-16 12:33:45 +09:00
Watson dd7ed00ec4 Use strip & empty? to detect Whitespaces
## Test code
```ruby
require 'benchmark/ips'

WhitespaceOrNothing = /\A\s*\z/
token = " " * 20

Benchmark.ips do |x|
  x.report("WhitespaceOrNothing") {
    token =~ WhitespaceOrNothing
  }
  x.report("strip & empty?") {
    token.strip.empty?
  }

  x.compare!
end
```

## Result
```
Warming up --------------------------------------
 WhitespaceOrNothing   266.391k i/100ms
      strip & empty?     1.044M i/100ms
Calculating -------------------------------------
 WhitespaceOrNothing      2.705M (± 0.4%) i/s -     13.586M in   5.023453s
      strip & empty?     10.400M (± 1.1%) i/s -     52.182M in   5.017990s

Comparison:
      strip & empty?: 10400286.2 i/s
 WhitespaceOrNothing:  2704552.3 i/s - 3.85x  (± 0.00) slower
```
2022-03-12 19:24:56 +09:00
Watson 1667c1180e Use start_with? and end_with? to detect SQUARE_BRAKET
## Test code
```ruby
require 'benchmark/ips'

SQUARE_BRACKETED = /\A\[(.*)\]\z/m
markup = "[product.catchall]"

Benchmark.ips do |x|
  x.report("SQUARE_BRACKETED") {
    if markup =~ SQUARE_BRACKETED
      Regexp.last_match(1)
    end
  }
  x.report("start/end_with?") {
    if markup&.start_with?('[') && markup&.end_with?(']')
      markup[1..-2]
    end
  }

  x.compare!
end
```

## Result
```
Warming up --------------------------------------
    SQUARE_BRACKETED   261.300k i/100ms
     start/end_with?   548.813k i/100ms
Calculating -------------------------------------
    SQUARE_BRACKETED      2.632M (± 0.6%) i/s -     13.326M in   5.064085s
     start/end_with?      5.471M (± 0.5%) i/s -     27.441M in   5.015770s

Comparison:
     start/end_with?:  5470994.1 i/s
    SQUARE_BRACKETED:  2631642.3 i/s - 2.08x  (± 0.00) slower
```
2022-03-12 18:14:16 +09:00
Thierry JoyalandGitHub 7357dcf185 Merge pull request #1536 from Shopify/flaky-profiler-test-v2
Add artificial execution time in profiler tests
2022-03-07 10:06:13 -05:00
Thierry JoyalandGitHub 68c3827ef2 Merge pull request #1525 from Shopify/standardfilter/fix-missing-context-on-iterations
[StandardFilter] Fix missing @context on iterations
2022-03-07 09:19:18 -05:00
Thierry Joyal 4af38bc549 [StandardFilter] Fix missing @context on iterations 2022-03-07 09:17:07 -05:00
Thierry Joyal df241abf70 Add artificial execution time in profiler tests 2022-03-07 08:50:03 -05:00
10f8337209 Test Ruby 3.1 in CI (#1533)
Co-authored-by: Dylan Thacker-Smith <[email protected]>
2022-03-04 13:23:04 -05:00
Thierry JoyalandGitHub 5ed0410a8b Merge pull request #1534 from Shopify/context-test-cleanup
Context test cleanup
2022-03-04 11:31:00 -05:00
Thierry Joyal 0f5220c391 ContextTest: Classes to use appropriate ancestor 2022-03-04 09:14:57 -05:00
Thierry Joyal 7a23f46fab ContextTest: Cleanup global variable assignments 2022-03-04 09:10:38 -05:00
Peter ZhuandGitHub 3f7edf00b9 Merge pull request #1531 from Shopify/pz-array-fetch-warning
Fix warning about block and default value
2022-03-02 16:25:23 -05:00
Thierry JoyalandGitHub b4a2a79e26 Merge pull request #1527 from Shopify/condition/receive-mandatory-context-argument
Condition#evaluate to receive mandatory context argument
2022-03-02 15:04:24 -05:00
Thierry Joyal 1d2bee1f60 Condition#evaluate to receive mandatory context argument 2022-03-02 14:35:31 -05:00
Peter Zhu 01e6eec97a Fix warning about block and default value
Ruby's Array#fetch accepts either a default value or a block, but not
both. If both are passed in, then it uses the block and outputs this
warning:

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

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

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

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

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

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

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

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

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

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

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

  x.compare!
end
```

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

Comparison:
           String#=~:  1520250.9 i/s
         String#scan:  1351399.0 i/s - 1.12x  (± 0.00) slower
        String#match:  1169384.1 i/s - 1.30x  (± 0.00) slower
```
2021-09-26 04:12:53 +09:00
Dylan Thacker-Smith 0e14d539a3 README: Use newer hash syntax for the error_mode parse option 2021-09-16 10:16:36 -04:00
Dylan Thacker-Smith db106ae058 Clarify that the error_mode: :warn parse option is only for strict errors 2021-09-16 10:13:57 -04:00
Anders SøgaardandGitHub db3999a008 Improve where filter tests (#1472) 2021-09-16 10:02:39 -04:00
Marc-André CournoyerandGitHub 10e2aa8d5b Merge pull request #1471 from Shopify/bump-5.1.0
Prep for 5.1.0 release
2021-09-15 15:15:13 -04:00
7c4114671b Remove to_liquid_value fixes in changelog
Co-authored-by: Dylan Thacker-Smith <[email protected]>
2021-09-13 10:53:46 -04:00
Marc-André Cournoyer b01de9d325 Prep for 5.1.0 release 2021-09-09 14:28:45 -04:00
Zac ClayandGitHub a5369c26a8 Add missing quote in comment (#1468) 2021-09-09 14:23:11 -04:00
Charles-Philippe ClermontandGitHub a03de8f9ea Merge pull request #1470 from Shopify/fix/range-parse-tree-visitor
Add ParseTreeVisitor to RangeLookup
2021-09-09 11:39:14 -04:00
Charles-P. Clermont e86fe27259 Fix lint 2021-09-09 11:22:39 -04:00
Charles-P. Clermont c8906d05b9 Add ParseTreeVisitor to RangeLookup 2021-09-09 11:22:39 -04:00
Michael GoandGitHub 50d1a2ffc9 Merge pull request #1458 from Shopify/use-to-liquid-value-with-conditions
use Utils.to_liquid_value on conditionals
2021-06-15 12:18:14 -03:00
Michael Go f686c5dec7 use Utils.to_liquid_value on conditionals 2021-06-14 18:19:37 -03:00
Michael GoandGitHub aa8ce87b96 Merge pull request #1454 from Shopify/default-filter-with-to-liquid-value
utilize input's to_liquid_value on default filter
2021-06-14 15:43:56 -03:00
Peter ZhuandGitHub 698f5e0d96 Merge pull request #1456 from Shopify/pz-psych-4-unsafe-load
Fix benchmark for breaking change in Psych 4
2021-06-10 13:14:13 -04:00
Peter Zhu 996bfe0c82 Fix benchmark for breaking change in Psych 4
Psych 4 introduces a breaking change (ruby/psych#487) where
`Psych#load`/`Psych#load_file` now default to safe loading,
meaning that YAML references are not allowed anymore. This
commit changes the benchmark to use `Psych#unsafe_load_file`
when it's available.
2021-06-10 13:01:16 -04:00
Michael GoandGitHub be81c9ae5a Merge pull request #1457 from Shopify/fix-unless-to-use-to-liquid-value-helper
fix unless to use to_liquid_value helper with multiple conditions
2021-06-10 14:00:19 -03:00
Michael Go edd4d70aee fix unless to use to_liquid_value helper with multiple conditions 2021-06-10 13:28:17 -03:00
Michael Go ac66dbbafe utilize input's to_liquid_value on default filter 2021-06-08 15:20:53 -03:00
Daniel InsleyandGitHub 017c1b5e83 Base64 Decode & Encode Filters (#1450) 2021-06-03 13:23:11 -04:00
Michael GoandGitHub 250555c9a8 Merge pull request #1441 from Shopify/to-raw-value
Implement to_liquid_value to Liquid::Drop
2021-05-27 10:21:05 -03:00
Michael Go e361a4d53c introduce to_liquid_value on variable look and conditional statements 2021-05-26 17:27:18 -03:00
Dylan Thacker-Smith b9e0d28729 rubocop: Clarify that a config is a rubocop-shopify backport 2021-04-20 09:01:06 -07:00
Dylan Thacker-Smith 020f6b93c5 rubocop: Fix unsupported ruby version in TargetRubyVersion error 2021-04-20 09:00:20 -07:00
Dylan Thacker-SmithandGitHub cfe1637bdd Translate RangeError to Liquid::Error for truncatewords with large int (#1431) 2021-04-20 11:48:22 -04:00
Dylan Thacker-Smith eab13a07d9 Add changelog entry for a recent fix 2021-03-29 13:43:01 -07:00
Dylan Thacker-SmithandGitHub ca96ca0fef Fix support for using a String subclass for the liquid source (#1421) 2021-03-29 16:22:05 -04:00
123 changed files with 6088 additions and 1923 deletions
-2
View File
@@ -1,2 +0,0 @@
enabled:
- cla
+22
View File
@@ -0,0 +1,22 @@
name: Contributor License Agreement (CLA)
on:
pull_request_target:
types: [opened, synchronize]
issue_comment:
types: [created]
jobs:
cla:
runs-on: ubuntu-latest
if: |
(github.event.issue.pull_request
&& !github.event.issue.pull_request.merged_at
&& contains(github.event.comment.body, 'signed')
)
|| (github.event.pull_request && !github.event.pull_request.merged)
steps:
- uses: Shopify/shopify-cla-action@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
cla-token: ${{ secrets.CLA_TOKEN }}
+17 -19
View File
@@ -1,39 +1,37 @@
name: Liquid name: Liquid
on: [push, pull_request] on: [push, pull_request]
env:
BUNDLE_JOBS: 4
BUNDLE_RETRY: 3
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
entry: entry:
- { ruby: 2.5, allowed-failure: false } # minimum supported - { ruby: 3.0, allowed-failure: false } # minimum supported
- { ruby: 3.0, allowed-failure: false } # latest - { ruby: 3.2, allowed-failure: false }
- { ruby: ruby-head, allowed-failure: true } - { ruby: 3.3, allowed-failure: false }
name: test (${{ matrix.entry.ruby }}) - { ruby: 3.4, allowed-failure: false } # latest
- { ruby: ruby-head, allowed-failure: false }
name: Test Ruby ${{ matrix.entry.ruby }}
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: ruby/setup-ruby@v1 - uses: ruby/setup-ruby@v1
with: with:
ruby-version: ${{ matrix.entry.ruby }} ruby-version: ${{ matrix.entry.ruby }}
- uses: actions/cache@v1 bundler-cache: true
with: bundler: latest
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 - run: bundle exec rake
continue-on-error: ${{ matrix.entry.allowed-failure }} continue-on-error: ${{ matrix.entry.allowed-failure }}
memory_profile: memory_profile:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
- uses: ruby/setup-ruby@v1 - uses: ruby/setup-ruby@v1
with: with:
ruby-version: 2.7 bundler-cache: true
- 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 - run: bundle exec rake memory_profile:run
+1 -2
View File
@@ -4,7 +4,6 @@
pkg pkg
*.rbc *.rbc
.rvmrc .rvmrc
.ruby-version
Gemfile.lock
.bundle .bundle
.byebug_history .byebug_history
Gemfile.lock
+9 -1
View File
@@ -10,11 +10,19 @@ Performance:
Enabled: true Enabled: true
AllCops: AllCops:
TargetRubyVersion: 2.4
NewCops: disable NewCops: disable
SuggestExtensions: false
Exclude: Exclude:
- 'vendor/bundle/**/*' - 'vendor/bundle/**/*'
Naming/MethodName: Naming/MethodName:
Exclude: Exclude:
- 'example/server/liquid_servlet.rb' - 'example/server/liquid_servlet.rb'
Style/ClassMethodsDefinitions:
Enabled: false
# liquid filter calls were being mistaken to be calls on arrays
Style/ConcatArrayLiterals:
Exclude:
- 'test/integration/standard_filter_test.rb'
+137 -14
View File
@@ -1,27 +1,74 @@
# This configuration was generated by # This configuration was generated by
# `rubocop --auto-gen-config` # `rubocop --auto-gen-config`
# on 2020-12-11 18:53:41 UTC using RuboCop version 1.6.1. # on 2022-05-18 19:25:47 UTC using RuboCop version 1.29.1.
# The point is for the user to remove these configuration records # The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base. # one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new # Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again. # versions of RuboCop, may require this file to be generated again.
# Offense count: 2 # Offense count: 1
# Cop supports --auto-correct. # This cop supports safe auto-correction (--auto-correct).
# Configuration parameters: EnforcedStyle. # Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation, Include.
# SupportedStyles: runtime_error, standard_error # Include: **/*.gemspec
Lint/InheritException: Gemspec/OrderedDependencies:
Exclude: Exclude:
- 'lib/liquid/interrupts.rb' - 'liquid.gemspec'
# Offense count: 113 # Offense count: 6
# Cop supports --auto-correct. # This cop supports safe auto-correction (--auto-correct).
# Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. Layout/ClosingHeredocIndentation:
# URISchemes: http, https Exclude:
Layout/LineLength: - 'test/integration/tags/for_tag_test.rb'
Max: 260
# Offense count: 34
# This cop supports safe auto-correction (--auto-correct).
Layout/EmptyLineAfterGuardClause:
Exclude:
- 'lib/liquid/block.rb'
- 'lib/liquid/block_body.rb'
- 'lib/liquid/context.rb'
- 'lib/liquid/drop.rb'
- 'lib/liquid/lexer.rb'
- 'lib/liquid/parser.rb'
- 'lib/liquid/profiler/hooks.rb'
- 'lib/liquid/standardfilters.rb'
- 'lib/liquid/tags/for.rb'
- 'lib/liquid/tags/if.rb'
- 'lib/liquid/utils.rb'
- 'lib/liquid/variable.rb'
- 'lib/liquid/variable_lookup.rb'
- 'performance/shopify/money_filter.rb'
- 'performance/shopify/paginate.rb'
# Offense count: 8 # Offense count: 8
# This cop supports safe auto-correction (--auto-correct).
# Configuration parameters: AllowAliasSyntax, AllowedMethods.
# AllowedMethods: alias_method, public, protected, private
Layout/EmptyLinesAroundAttributeAccessor:
Exclude:
- 'lib/liquid/template.rb'
- 'test/integration/filter_test.rb'
- 'test/integration/tags/include_tag_test.rb'
- 'test/unit/strainer_template_unit_test.rb'
# Offense count: 17
# This cop supports safe auto-correction (--auto-correct).
# Configuration parameters: EnforcedStyle, IndentationWidth.
# SupportedStyles: aligned, indented
Layout/LineEndStringConcatenationIndentation:
Exclude:
- 'test/integration/tags/for_tag_test.rb'
- 'test/integration/tags/increment_tag_test.rb'
# Offense count: 1
# This cop supports safe auto-correction (--auto-correct).
# Configuration parameters: EnforcedStyle, IndentationWidth.
# SupportedStyles: aligned, indented
Layout/MultilineOperationIndentation:
Exclude:
- 'lib/liquid/expression.rb'
# Offense count: 9
Lint/MissingSuper: Lint/MissingSuper:
Exclude: Exclude:
- 'lib/liquid/forloop_drop.rb' - 'lib/liquid/forloop_drop.rb'
@@ -33,7 +80,7 @@ Lint/MissingSuper:
- 'test/integration/tags/for_tag_test.rb' - 'test/integration/tags/for_tag_test.rb'
- 'test/integration/tags/table_row_test.rb' - 'test/integration/tags/table_row_test.rb'
# Offense count: 43 # Offense count: 44
Naming/ConstantName: Naming/ConstantName:
Exclude: Exclude:
- 'lib/liquid.rb' - 'lib/liquid.rb'
@@ -51,7 +98,83 @@ Naming/ConstantName:
- 'performance/shopify/paginate.rb' - 'performance/shopify/paginate.rb'
- 'test/integration/tags/include_tag_test.rb' - 'test/integration/tags/include_tag_test.rb'
# Offense count: 9
# Configuration parameters: CheckIdentifiers, CheckConstants, CheckVariables, CheckStrings, CheckSymbols, CheckComments, CheckFilepaths, FlaggedTerms.
Naming/InclusiveLanguage:
Exclude:
- 'lib/liquid/drop.rb'
- 'lib/liquid/parse_context.rb'
- 'test/integration/drop_test.rb'
- 'test/integration/tags/if_else_tag_test.rb'
# Offense count: 2 # Offense count: 2
Style/ClassVars: Style/ClassVars:
Exclude: Exclude:
- 'lib/liquid/condition.rb' - 'lib/liquid/condition.rb'
# Offense count: 3
# This cop supports safe auto-correction (--auto-correct).
Style/ExplicitBlockArgument:
Exclude:
- 'test/integration/context_test.rb'
- 'test/integration/tag/disableable_test.rb'
- 'test/integration/tags/for_tag_test.rb'
# Offense count: 2982
# This cop supports safe auto-correction (--auto-correct).
# Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline.
# SupportedStyles: single_quotes, double_quotes
Style/StringLiterals:
Enabled: false
# Offense count: 20
# This cop supports safe auto-correction (--auto-correct).
# Configuration parameters: EnforcedStyle.
# SupportedStyles: single_quotes, double_quotes
Style/StringLiteralsInInterpolation:
Exclude:
- 'lib/liquid/condition.rb'
- 'lib/liquid/strainer_template.rb'
- 'lib/liquid/tag/disableable.rb'
- 'performance/shopify/shop_filter.rb'
- 'performance/shopify/tag_filter.rb'
# Offense count: 6
# This cop supports safe auto-correction (--auto-correct).
# Configuration parameters: EnforcedStyleForMultiline.
# SupportedStylesForMultiline: comma, consistent_comma, no_comma
Style/TrailingCommaInArrayLiteral:
Exclude:
- 'example/server/example_servlet.rb'
- 'lib/liquid/condition.rb'
- 'test/integration/context_test.rb'
- 'test/integration/standard_filter_test.rb'
- 'test/unit/parse_tree_visitor_test.rb'
# Offense count: 1
# This cop supports safe auto-correction (--auto-correct).
# Configuration parameters: EnforcedStyleForMultiline.
# SupportedStylesForMultiline: comma, consistent_comma, no_comma
Style/TrailingCommaInHashLiteral:
Exclude:
- 'lib/liquid/expression.rb'
# Offense count: 19
# This cop supports safe auto-correction (--auto-correct).
# Configuration parameters: EnforcedStyle, MinSize, WordRegex.
# SupportedStyles: percent, brackets
Style/WordArray:
Exclude:
- 'lib/liquid/tags/if.rb'
- 'liquid.gemspec'
- 'test/integration/assign_test.rb'
- 'test/integration/context_test.rb'
- 'test/integration/drop_test.rb'
- 'test/integration/standard_filter_test.rb'
# Offense count: 117
# This cop supports safe auto-correction (--auto-correct).
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns, IgnoredPatterns.
# URISchemes: http, https
Layout/LineLength:
Max: 260
+1
View File
@@ -0,0 +1 @@
3.3.6
+6
View File
@@ -26,3 +26,9 @@
* If it makes sense, add tests for your code and/or run a performance benchmark * If it makes sense, add tests for your code and/or run a performance benchmark
* Make sure all tests pass (`bundle exec rake`) * Make sure all tests pass (`bundle exec rake`)
* Create a pull request * Create a pull request
## Releasing
* Bump the version in `lib/liquid/version.rb` and merge it on `main`
* Create a new release using the [GitHub UI](https://github.com/Shopify/liquid/releases/new)
+5 -6
View File
@@ -7,10 +7,13 @@ end
gemspec gemspec
gem "base64"
group :benchmark, :test do group :benchmark, :test do
gem 'benchmark-ips' gem 'benchmark-ips'
gem 'memory_profiler' gem 'memory_profiler'
gem 'terminal-table' gem 'terminal-table'
gem "lru_redux"
install_if -> { RUBY_PLATFORM !~ /mingw|mswin|java/ && RUBY_ENGINE != 'truffleruby' } do install_if -> { RUBY_PLATFORM !~ /mingw|mswin|java/ && RUBY_ENGINE != 'truffleruby' } do
gem 'stackprof' gem 'stackprof'
@@ -18,11 +21,7 @@ group :benchmark, :test do
end end
group :test do group :test do
gem 'rubocop', '~> 1.4', require: false gem 'rubocop', '~> 1.61.0'
gem 'rubocop-shopify', '~> 1.0.7', require: false gem 'rubocop-shopify', '~> 2.12.0', require: false
gem 'rubocop-performance', require: false gem 'rubocop-performance', require: false
platform :mri, :truffleruby do
gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'master'
end
end end
+57
View File
@@ -1,5 +1,62 @@
# Liquid Change Log # Liquid Change Log
## 5.6.0 (unreleased)
### Fixes
* Fix Tokenizer to handle null source value (#1873) [Bahar Pourazar]
## 5.5.0 2024-03-21
Please reference the GitHub release for more information.
## 5.4.0 2022-07-29
### Breaking Changes
* Drop support for end-of-life Ruby versions (2.5 and 2.6) (#1578) [Andy Waite]
### Features
* Allow `#` to be used as an inline comment tag (#1498) [CP Clermont]
### Fixes
* `PartialCache` now shares snippet cache with subcontexts by default (#1553) [Chris AtLee]
* Hash registers no longer leak into subcontexts as static registers (#1564) [Chris AtLee]
* Fix `ParseTreeVisitor` for `with` variable expressions in `Render` tag (#1596) [CP Clermont]
### Changed
* Liquid::Context#registers now always returns a Liquid::Registers object, though supports the most used Hash functions for compatibility (#1553)
## 5.3.0 2022-03-22
### Fixes
* StandardFilter: Fix missing @context on iterations (#1525) [Thierry Joyal]
* Fix warning about block and default value in `static_registers.rb` (#1531) [Peter Zhu]
### Deprecation
* Condition#evaluate to require mandatory context argument in Liquid 6.0.0 (#1527) [Thierry Joyal]
## 5.2.0 2022-03-01
### Features
* Add `remove_last`, and `replace_last` filters (#1422) [Anders Hagbard]
* Eagerly cache global filters (#1524) [Jean Boussier]
### Fixes
* Fix some internal errors in filters from invalid input (#1476) [Dylan Thacker-Smith]
* Allow dash in filter kwarg name for consistency with Liquid::C (#1518) [CP Clermont]
## 5.1.0 / 2021-09-09
### Features
* Add `base64_encode`, `base64_decode`, `base64_url_safe_encode`, and `base64_url_safe_decode` filters (#1450) [Daniel Insley]
* Introduce `to_liquid_value` in `Liquid::Drop` (#1441) [Michael Go]
### Fixes
* Fix support for using a String subclass for the liquid source (#1421) [Dylan Thacker-Smith]
* Add `ParseTreeVisitor` to `RangeLookup` (#1470) [CP Clermont]
* Translate `RangeError` to `Liquid::Error` for `truncatewords` with large int (#1431) [Dylan Thacker-Smith]
## 5.0.1 / 2021-03-24 ## 5.0.1 / 2021-03-24
### Fixes ### Fixes
+48 -6
View File
@@ -1,11 +1,11 @@
[![Build Status](https://api.travis-ci.org/Shopify/liquid.svg?branch=master)](http://travis-ci.org/Shopify/liquid) [![Build status](https://github.com/Shopify/liquid/actions/workflows/liquid.yml/badge.svg)](https://github.com/Shopify/liquid/actions/workflows/liquid.yml)
[![Inline docs](http://inch-ci.org/github/Shopify/liquid.svg?branch=master)](http://inch-ci.org/github/Shopify/liquid) [![Inline docs](http://inch-ci.org/github/Shopify/liquid.svg?branch=master)](http://inch-ci.org/github/Shopify/liquid)
# Liquid template engine # Liquid template engine
* [Contributing guidelines](CONTRIBUTING.md) * [Contributing guidelines](CONTRIBUTING.md)
* [Version history](History.md) * [Version history](History.md)
* [Liquid documentation from Shopify](http://docs.shopify.com/themes/liquid-basics) * [Liquid documentation from Shopify](https://shopify.dev/docs/api/liquid)
* [Liquid Wiki at GitHub](https://github.com/Shopify/liquid/wiki) * [Liquid Wiki at GitHub](https://github.com/Shopify/liquid/wiki)
* [Website](http://liquidmarkup.org/) * [Website](http://liquidmarkup.org/)
@@ -52,6 +52,47 @@ For standard use you can just pass it the content of a file and call render with
@template.render('name' => 'tobi') # => "hi tobi" @template.render('name' => 'tobi') # => "hi tobi"
``` ```
### Concept of Environments
In Liquid, a "Environment" is a scoped environment that encapsulates custom tags, filters, and other configurations. This allows you to define and isolate different sets of functionality for different contexts, avoiding global overrides that can lead to conflicts and unexpected behavior.
By using environments, you can:
1. **Encapsulate Logic**: Keep the logic for different parts of your application separate.
2. **Avoid Conflicts**: Prevent custom tags and filters from clashing with each other.
3. **Improve Maintainability**: Make it easier to manage and understand the scope of customizations.
4. **Enhance Security**: Limit the availability of certain tags and filters to specific contexts.
We encourage the use of Environments over globally overriding things because it promotes better software design principles such as modularity, encapsulation, and separation of concerns.
Here's an example of how you can define and use Environments in Liquid:
```ruby
user_environment = Liquid::Environment.build do |environment|
environment.register_tag("renderobj", RenderObjTag)
end
Liquid::Template.parse(<<~LIQUID, environment: user_environment)
{% renderobj src: "path/to/model.obj" %}
LIQUID
```
In this example, `RenderObjTag` is a custom tag that is only available within the `user_environment`.
Similarly, you can define another environment for a different context, such as email templates:
```ruby
email_environment = Liquid::Environment.build do |environment|
environment.register_tag("unsubscribe_footer", UnsubscribeFooter)
end
Liquid::Template.parse(<<~LIQUID, environment: email_environment)
{% unsubscribe_footer %}
LIQUID
```
By using Environments, you ensure that custom tags and filters are only available in the contexts where they are needed, making your Liquid templates more robust and easier to manage. For smaller projects, a global environment is available via `Liquid::Environment.default`.
### Error Modes ### Error Modes
Setting the error mode of Liquid lets you specify how strictly you want your templates to be interpreted. Setting the error mode of Liquid lets you specify how strictly you want your templates to be interpreted.
@@ -62,14 +103,15 @@ Liquid also comes with a stricter parser that can be used when editing templates
when templates are invalid. You can enable this new parser like this: when templates are invalid. You can enable this new parser like this:
```ruby ```ruby
Liquid::Template.error_mode = :strict # Raises a SyntaxError when invalid syntax is used Liquid::Environment.default.error_mode = :strict
Liquid::Template.error_mode = :warn # Adds errors to template.errors but continues as normal Liquid::Environment.default.error_mode = :strict # Raises a SyntaxError when invalid syntax is used
Liquid::Template.error_mode = :lax # The default mode, accepts almost anything. Liquid::Environment.default.error_mode = :warn # Adds strict errors to template.errors but continues as normal
Liquid::Environment.default.error_mode = :lax # The default mode, accepts almost anything.
``` ```
If you want to set the error mode only on specific templates you can pass `:error_mode` as an option to `parse`: If you want to set the error mode only on specific templates you can pass `:error_mode` as an option to `parse`:
```ruby ```ruby
Liquid::Template.parse(source, :error_mode => :strict) Liquid::Template.parse(source, error_mode: :strict)
``` ```
This is useful for doing things like enabling strict mode only in the theme editor. This is useful for doing things like enabling strict mode only in the theme editor.
+28 -3
View File
@@ -43,8 +43,6 @@ task :test do
Rake::Task['base_test'].invoke Rake::Task['base_test'].invoke
if RUBY_ENGINE == 'ruby' || RUBY_ENGINE == 'truffleruby' if RUBY_ENGINE == 'ruby' || RUBY_ENGINE == 'truffleruby'
ENV['LIQUID_C'] = '1'
ENV['LIQUID_PARSER_MODE'] = 'lax' ENV['LIQUID_PARSER_MODE'] = 'lax'
Rake::Task['integration_test'].reenable Rake::Task['integration_test'].reenable
Rake::Task['integration_test'].invoke Rake::Task['integration_test'].invoke
@@ -73,7 +71,7 @@ end
namespace :benchmark do namespace :benchmark do
desc "Run the liquid benchmark with lax parsing" desc "Run the liquid benchmark with lax parsing"
task :run do task :lax do
ruby "./performance/benchmark.rb lax" ruby "./performance/benchmark.rb lax"
end end
@@ -81,6 +79,33 @@ namespace :benchmark do
task :strict do task :strict do
ruby "./performance/benchmark.rb strict" ruby "./performance/benchmark.rb strict"
end end
desc "Run the liquid benchmark with both lax and strict parsing"
task run: [:lax, :strict]
desc "Run unit benchmarks"
namespace :unit do
task :all do
Dir["./performance/unit/*_benchmark.rb"].each do |file|
puts "🧪 Running #{file}"
ruby file
end
end
task :lexer do
Dir["./performance/unit/lexer_benchmark.rb"].each do |file|
puts "🧪 Running #{file}"
ruby file
end
end
task :expression do
Dir["./performance/unit/expression_benchmark.rb"].each do |file|
puts "🧪 Running #{file}"
ruby file
end
end
end
end end
namespace :profile do namespace :profile do
+8 -4
View File
@@ -30,14 +30,18 @@ class Servlet < LiquidServlet
private private
def products_list def products_list
[{ 'name' => 'Arbor Draft', 'price' => 39900, 'description' => 'the *arbor draft* is a excellent product' }, [
{ '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 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 Diamond', 'price' => 59900, 'description' => 'the *arbor diamond* is a made up product because im obsessed with arbor and have no creativity' }
]
end end
def more_products_list 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 end
def description def description
+18 -19
View File
@@ -21,6 +21,8 @@
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
require "strscan"
module Liquid module Liquid
FilterSeparator = /\|/ FilterSeparator = /\|/
ArgumentSeparator = ',' ArgumentSeparator = ','
@@ -29,6 +31,7 @@ module Liquid
WhitespaceControl = '-' WhitespaceControl = '-'
TagStart = /\{\%/ TagStart = /\{\%/
TagEnd = /\%\}/ TagEnd = /\%\}/
TagName = /#|\w+/
VariableSignature = /\(?[\w\-\.\[\]]\)?/ VariableSignature = /\(?[\w\-\.\[\]]\)?/
VariableSegment = /[\w\-]/ VariableSegment = /[\w\-]/
VariableStart = /\{\{/ VariableStart = /\{\{/
@@ -36,20 +39,28 @@ module Liquid
VariableIncompleteEnd = /\}\}?/ VariableIncompleteEnd = /\}\}?/
QuotedString = /"[^"]*"|'[^']*'/ QuotedString = /"[^"]*"|'[^']*'/
QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o
TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/o TagAttributes = /(\w[\w-]*)\s*\:\s*(#{QuotedFragment})/o
AnyStartingTag = /#{TagStart}|#{VariableStart}/o AnyStartingTag = /#{TagStart}|#{VariableStart}/o
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om
VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o VariableParser = /\[(?>[^\[\]]+|\g<0>)*\]|#{VariableSegment}+\??/o
RAISE_EXCEPTION_LAMBDA = ->(_e) { raise } RAISE_EXCEPTION_LAMBDA = ->(_e) { raise }
HAS_STRING_SCANNER_SCAN_BYTE = StringScanner.instance_methods.include?(:scan_byte)
singleton_class.send(:attr_accessor, :cache_classes)
self.cache_classes = true
end end
require "liquid/version" require "liquid/version"
require "liquid/deprecations"
require "liquid/const"
require 'liquid/standardfilters'
require 'liquid/file_system'
require 'liquid/parser_switching'
require 'liquid/tag'
require 'liquid/block'
require 'liquid/parse_tree_visitor' require 'liquid/parse_tree_visitor'
require 'liquid/interrupts'
require 'liquid/tags'
require "liquid/environment"
require 'liquid/lexer' require 'liquid/lexer'
require 'liquid/parser' require 'liquid/parser'
require 'liquid/i18n' require 'liquid/i18n'
@@ -59,34 +70,22 @@ require 'liquid/forloop_drop'
require 'liquid/extensions' require 'liquid/extensions'
require 'liquid/errors' require 'liquid/errors'
require 'liquid/interrupts' require 'liquid/interrupts'
require 'liquid/strainer_factory'
require 'liquid/strainer_template' require 'liquid/strainer_template'
require 'liquid/expression'
require 'liquid/context' require 'liquid/context'
require 'liquid/parser_switching'
require 'liquid/tag' require 'liquid/tag'
require 'liquid/tag/disabler'
require 'liquid/tag/disableable'
require 'liquid/block'
require 'liquid/block_body' require 'liquid/block_body'
require 'liquid/document' require 'liquid/document'
require 'liquid/variable' require 'liquid/variable'
require 'liquid/variable_lookup' require 'liquid/variable_lookup'
require 'liquid/range_lookup' require 'liquid/range_lookup'
require 'liquid/file_system'
require 'liquid/resource_limits' require 'liquid/resource_limits'
require 'liquid/expression'
require 'liquid/template' require 'liquid/template'
require 'liquid/standardfilters'
require 'liquid/condition' require 'liquid/condition'
require 'liquid/utils' require 'liquid/utils'
require 'liquid/tokenizer' require 'liquid/tokenizer'
require 'liquid/parse_context' require 'liquid/parse_context'
require 'liquid/partial_cache' require 'liquid/partial_cache'
require 'liquid/usage' require 'liquid/usage'
require 'liquid/register' require 'liquid/registers'
require 'liquid/static_registers'
require 'liquid/template_factory' require 'liquid/template_factory'
# Load all the tags of the standard library
#
Dir["#{__dir__}/liquid/tags/*.rb"].each { |f| require f }
+8 -4
View File
@@ -36,13 +36,17 @@ module Liquid
# @api private # @api private
def self.raise_unknown_tag(tag, block_name, block_delimiter, parse_context) def self.raise_unknown_tag(tag, block_name, block_delimiter, parse_context)
if tag == 'else' if tag == 'else'
raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_else", raise SyntaxError, parse_context.locale.t(
block_name: block_name) "errors.syntax.unexpected_else",
block_name: block_name,
)
elsif tag.start_with?('end') elsif tag.start_with?('end')
raise SyntaxError, parse_context.locale.t("errors.syntax.invalid_delimiter", raise SyntaxError, parse_context.locale.t(
"errors.syntax.invalid_delimiter",
tag: tag, tag: tag,
block_name: block_name, block_name: block_name,
block_delimiter: block_delimiter) block_delimiter: block_delimiter,
)
else else
raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag) raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag)
end end
+32 -14
View File
@@ -4,8 +4,9 @@ require 'English'
module Liquid module Liquid
class BlockBody class BlockBody
LiquidTagToken = /\A\s*(\w+)\s*(.*?)\z/o LiquidTagToken = /\A\s*(#{TagName})\s*(.*?)\z/o
FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(\w+)(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(#{TagName})(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
FullTokenPossiblyInvalid = /\A(.*)#{TagStart}#{WhitespaceControl}?\s*(\w+)\s*(.*)?#{WhitespaceControl}?#{TagEnd}\z/om
ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
WhitespaceOrNothing = /\A\s*\z/ WhitespaceOrNothing = /\A\s*\z/
TAGSTART = "{%" TAGSTART = "{%"
@@ -37,7 +38,7 @@ module Liquid
private def parse_for_liquid_tag(tokenizer, parse_context) private def parse_for_liquid_tag(tokenizer, parse_context)
while (token = tokenizer.shift) while (token = tokenizer.shift)
unless token.empty? || token =~ WhitespaceOrNothing unless token.empty? || token.match?(WhitespaceOrNothing)
unless token =~ LiquidTagToken unless token =~ LiquidTagToken
# line isn't empty but didn't match tag syntax, yield and let the # line isn't empty but didn't match tag syntax, yield and let the
# caller raise a syntax error # caller raise a syntax error
@@ -45,7 +46,13 @@ module Liquid
end end
tag_name = Regexp.last_match(1) tag_name = Regexp.last_match(1)
markup = Regexp.last_match(2) markup = Regexp.last_match(2)
unless (tag = registered_tags[tag_name])
if tag_name == 'liquid'
parse_context.line_number -= 1
next parse_liquid_tag(markup, parse_context)
end
unless (tag = parse_context.environment.tag_for_name(tag_name))
# end parsing if we reach an unknown tag and let the caller decide # end parsing if we reach an unknown tag and let the caller decide
# determine how to proceed # determine how to proceed
return yield tag_name, markup return yield tag_name, markup
@@ -109,14 +116,22 @@ module Liquid
end end
end end
private def parse_for_document(tokenizer, parse_context) private def handle_invalid_tag_token(token, parse_context)
if token.end_with?('%}')
yield token, token
else
BlockBody.raise_missing_tag_terminator(token, parse_context)
end
end
private def parse_for_document(tokenizer, parse_context, &block)
while (token = tokenizer.shift) while (token = tokenizer.shift)
next if token.empty? next if token.empty?
case case
when token.start_with?(TAGSTART) when token.start_with?(TAGSTART)
whitespace_handler(token, parse_context) whitespace_handler(token, parse_context)
unless token =~ FullToken unless token =~ FullToken
BlockBody.raise_missing_tag_terminator(token, parse_context) return handle_invalid_tag_token(token, parse_context, &block)
end end
tag_name = Regexp.last_match(2) tag_name = Regexp.last_match(2)
markup = Regexp.last_match(4) markup = Regexp.last_match(4)
@@ -132,7 +147,7 @@ module Liquid
next next
end end
unless (tag = registered_tags[tag_name]) unless (tag = parse_context.environment.tag_for_name(tag_name))
# end parsing if we reach an unknown tag and let the caller decide # end parsing if we reach an unknown tag and let the caller decide
# determine how to proceed # determine how to proceed
return yield tag_name, markup return yield tag_name, markup
@@ -150,7 +165,7 @@ module Liquid
end end
parse_context.trim_whitespace = false parse_context.trim_whitespace = false
@nodelist << token @nodelist << token
@blank &&= !!(token =~ WhitespaceOrNothing) @blank &&= token.match?(WhitespaceOrNothing)
end end
parse_context.line_number = tokenizer.line_number parse_context.line_number = tokenizer.line_number
end end
@@ -231,10 +246,17 @@ module Liquid
end end
def create_variable(token, parse_context) def create_variable(token, parse_context)
token.scan(ContentOfVariable) do |content| if token.end_with?("}}")
markup = content.first i = 2
i = 3 if token[i] == "-"
parse_end = token.length - 3
parse_end -= 1 if token[parse_end] == "-"
markup_end = parse_end - i + 1
markup = markup_end <= 0 ? "" : token.slice(i, markup_end)
return Variable.new(markup, parse_context) return Variable.new(markup, parse_context)
end end
BlockBody.raise_missing_variable_terminator(token, parse_context) BlockBody.raise_missing_variable_terminator(token, parse_context)
end end
@@ -247,9 +269,5 @@ module Liquid
def raise_missing_variable_terminator(token, parse_context) def raise_missing_variable_terminator(token, parse_context)
BlockBody.raise_missing_variable_terminator(token, parse_context) BlockBody.raise_missing_variable_terminator(token, parse_context)
end end
def registered_tags
Template.tags
end
end end
end end
+19 -8
View File
@@ -8,7 +8,7 @@ module Liquid
# c = Condition.new(1, '==', 1) # c = Condition.new(1, '==', 1)
# c.evaluate #=> true # c.evaluate #=> true
# #
class Condition #:nodoc: class Condition # :nodoc:
@@operators = { @@operators = {
'==' => ->(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) }, '!=' => ->(cond, left, right) { !cond.send(:equal_variables, left, right) },
@@ -24,6 +24,9 @@ module Liquid
else else
false false
end end
rescue Encoding::CompatibilityError
# "✅".b.include?("✅") raises Encoding::CompatibilityError despite being materially equal
left.b.include?(right.b)
end, end,
} }
@@ -61,7 +64,7 @@ module Liquid
@child_condition = nil @child_condition = nil
end end
def evaluate(context = Context.new) def evaluate(context = deprecated_default_context)
condition = self condition = self
result = nil result = nil
loop do loop do
@@ -69,9 +72,9 @@ module Liquid
case condition.child_relation case condition.child_relation
when :or when :or
break if result break if Liquid::Utils.to_liquid_value(result)
when :and when :and
break unless result break unless Liquid::Utils.to_liquid_value(result)
else else
break break
end end
@@ -134,8 +137,8 @@ module Liquid
# return this as the result. # return this as the result.
return context.evaluate(left) if op.nil? return context.evaluate(left) if op.nil?
left = context.evaluate(left) left = Liquid::Utils.to_liquid_value(context.evaluate(left))
right = context.evaluate(right) right = Liquid::Utils.to_liquid_value(context.evaluate(right))
operation = self.class.operators[op] || raise(Liquid::ArgumentError, "Unknown operator #{op}") operation = self.class.operators[op] || raise(Liquid::ArgumentError, "Unknown operator #{op}")
@@ -150,11 +153,19 @@ module Liquid
end end
end end
def deprecated_default_context
warn("DEPRECATION WARNING: Condition#evaluate without a context argument is deprecated" \
" and will be removed from Liquid 6.0.0.")
Context.new
end
class ParseTreeVisitor < Liquid::ParseTreeVisitor class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children def children
[ [
@node.left, @node.right, @node.left,
@node.child_condition, @node.attachment @node.right,
@node.child_condition,
@node.attachment
].compact ].compact
end end
end end
+8
View File
@@ -0,0 +1,8 @@
# frozen_string_literal: true
module Liquid
module Const
EMPTY_HASH = {}.freeze
EMPTY_ARRAY = [].freeze
end
end
+29 -15
View File
@@ -15,31 +15,40 @@ module Liquid
# context['bob'] #=> nil class Context # context['bob'] #=> nil class Context
class Context class Context
attr_reader :scopes, :errors, :registers, :environments, :resource_limits, :static_registers, :static_environments attr_reader :scopes, :errors, :registers, :environments, :resource_limits, :static_registers, :static_environments
attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters, :environment
# rubocop:disable Metrics/ParameterLists # rubocop:disable Metrics/ParameterLists
def self.build(environments: {}, outer_scope: {}, registers: {}, rethrow_errors: false, resource_limits: nil, static_environments: {}, &block) def self.build(environment: Environment.default, 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) new(environments, outer_scope, registers, rethrow_errors, resource_limits, static_environments, environment, &block)
end end
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil, static_environments = {}) def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil, static_environments = {}, environment = Environment.default)
@environment = environment
@environments = [environments] @environments = [environments]
@environments.flatten! @environments.flatten!
@static_environments = [static_environments].flat_map(&:freeze).freeze @static_environments = [static_environments].flatten(1).freeze
@scopes = [(outer_scope || {})] @scopes = [outer_scope || {}]
@registers = registers @registers = registers.is_a?(Registers) ? registers : Registers.new(registers)
@errors = [] @errors = []
@partial = false @partial = false
@strict_variables = false @strict_variables = false
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits) @resource_limits = resource_limits || ResourceLimits.new(environment.default_resource_limits)
@base_scope_depth = 0 @base_scope_depth = 0
@interrupts = [] @interrupts = []
@filters = [] @filters = []
@global_filter = nil @global_filter = nil
@disabled_tags = {} @disabled_tags = {}
self.exception_renderer = Template.default_exception_renderer # Instead of constructing new StringScanner objects for each Expression parse,
# we recycle the same one.
@string_scanner = StringScanner.new("")
@registers.static[:cached_partials] ||= {}
@registers.static[:file_system] ||= environment.file_system
@registers.static[:template_factory] ||= Liquid::TemplateFactory.new
self.exception_renderer = environment.exception_renderer
if rethrow_errors if rethrow_errors
self.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA self.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA
end end
@@ -56,7 +65,7 @@ module Liquid
end end
def strainer def strainer
@strainer ||= StrainerFactory.create(self, @filters) @strainer ||= @environment.create_strainer(self, @filters)
end end
# Adds filters to this context. # Adds filters to this context.
@@ -124,7 +133,7 @@ module Liquid
# context['var'] = 'hi' # context['var'] = 'hi'
# end # end
# #
# context['var] #=> nil # context['var'] #=> nil
def stack(new_scope = {}) def stack(new_scope = {})
push(new_scope) push(new_scope)
yield yield
@@ -138,9 +147,10 @@ module Liquid
check_overflow check_overflow
self.class.build( self.class.build(
environment: @environment,
resource_limits: resource_limits, resource_limits: resource_limits,
static_environments: static_environments, static_environments: static_environments,
registers: StaticRegisters.new(registers) registers: Registers.new(registers),
).tap do |subcontext| ).tap do |subcontext|
subcontext.base_scope_depth = base_scope_depth + 1 subcontext.base_scope_depth = base_scope_depth + 1
subcontext.exception_renderer = exception_renderer subcontext.exception_renderer = exception_renderer
@@ -170,7 +180,7 @@ module Liquid
# Example: # Example:
# products == empty #=> products.empty? # products == empty #=> products.empty?
def [](expression) def [](expression)
evaluate(Expression.parse(expression)) evaluate(Expression.parse(expression, @string_scanner))
end end
def key?(key) def key?(key)
@@ -193,10 +203,14 @@ module Liquid
try_variable_find_in_environments(key, raise_on_not_found: raise_on_not_found) try_variable_find_in_environments(key, raise_on_not_found: raise_on_not_found)
end end
variable = variable.to_liquid # update variable's context before invoking #to_liquid
variable.context = self if variable.respond_to?(:context=) variable.context = self if variable.respond_to?(:context=)
variable liquid_variable = variable.to_liquid
liquid_variable.context = self if variable != liquid_variable && liquid_variable.respond_to?(:context=)
liquid_variable
end end
def lookup_and_evaluate(obj, key, raise_on_not_found: true) def lookup_and_evaluate(obj, key, raise_on_not_found: true)
+22
View File
@@ -0,0 +1,22 @@
# frozen_string_literal: true
require "set"
module Liquid
class Deprecations
class << self
attr_accessor :warned
Deprecations.warned = Set.new
def warn(name, alternative)
return if warned.include?(name)
warned << name
caller_location = caller_locations(2, 1).first
Warning.warn("[DEPRECATION] #{name} is deprecated. Use #{alternative} instead. Called from #{caller_location}\n")
end
end
end
end
+4
View File
@@ -25,6 +25,10 @@ module Liquid
class Drop class Drop
attr_writer :context attr_writer :context
def initialize
@context = nil
end
# Catch all for the method # Catch all for the method
def liquid_method_missing(method) def liquid_method_missing(method)
return nil unless @context&.strict_variables return nil unless @context&.strict_variables
+159
View File
@@ -0,0 +1,159 @@
# frozen_string_literal: true
module Liquid
# The Environment is the container for all configuration options of Liquid, such as
# the registered tags, filters, and the default error mode.
class Environment
# The default error mode for all templates. This can be overridden on a
# per-template basis.
attr_accessor :error_mode
# The tags that are available to use in the template.
attr_accessor :tags
# The strainer template which is used to store filters that are available to
# use in templates.
attr_accessor :strainer_template
# The exception renderer that is used to render exceptions that are raised
# when rendering a template
attr_accessor :exception_renderer
# The default file system that is used to load templates from.
attr_accessor :file_system
# The default resource limits that are used to limit the resources that a
# template can consume.
attr_accessor :default_resource_limits
class << self
# Creates a new environment instance.
#
# @param tags [Hash] The tags that are available to use in
# the template.
# @param file_system The default file system that is used
# to load templates from.
# @param error_mode [Symbol] The default error mode for all templates
# (either :strict, :warn, or :lax).
# @param exception_renderer [Proc] The exception renderer that is used to
# render exceptions.
# @yieldparam environment [Environment] The environment instance that is being built.
# @return [Environment] The new environment instance.
def build(tags: nil, file_system: nil, error_mode: nil, exception_renderer: nil)
ret = new
ret.tags = tags if tags
ret.file_system = file_system if file_system
ret.error_mode = error_mode if error_mode
ret.exception_renderer = exception_renderer if exception_renderer
yield ret if block_given?
ret.freeze
end
# Returns the default environment instance.
#
# @return [Environment] The default environment instance.
def default
@default ||= new
end
# Sets the default environment instance for the duration of the block
#
# @param environment [Environment] The environment instance to use as the default for the
# duration of the block.
# @yield
# @return [Object] The return value of the block.
def dangerously_override(environment)
original_default = @default
@default = environment
yield
ensure
@default = original_default
end
end
# Initializes a new environment instance.
# @api private
def initialize
@tags = Tags::STANDARD_TAGS.dup
@error_mode = :lax
@strainer_template = Class.new(StrainerTemplate).tap do |klass|
klass.add_filter(StandardFilters)
end
@exception_renderer = ->(exception) { exception }
@file_system = BlankFileSystem.new
@default_resource_limits = Const::EMPTY_HASH
@strainer_template_class_cache = {}
end
# Registers a new tag with the environment.
#
# @param name [String] The name of the tag.
# @param klass [Liquid::Tag] The class that implements the tag.
# @return [void]
def register_tag(name, klass)
@tags[name] = klass
end
# Registers a new filter with the environment.
#
# @param filter [Module] The module that contains the filter methods.
# @return [void]
def register_filter(filter)
@strainer_template_class_cache.clear
@strainer_template.add_filter(filter)
end
# Registers multiple filters with this environment.
#
# @param filters [Array<Module>] The modules that contain the filter methods.
# @return [self]
def register_filters(filters)
@strainer_template_class_cache.clear
filters.each { |f| @strainer_template.add_filter(f) }
self
end
# Creates a new strainer instance with the given filters, caching the result
# for faster lookup.
#
# @param context [Liquid::Context] The context that the strainer will be
# used in.
# @param filters [Array<Module>] The filters that the strainer will have
# access to.
# @return [Liquid::Strainer] The new strainer instance.
def create_strainer(context, filters = Const::EMPTY_ARRAY)
return @strainer_template.new(context) if filters.empty?
strainer_template = @strainer_template_class_cache[filters] ||= begin
klass = Class.new(@strainer_template)
filters.each { |f| klass.add_filter(f) }
klass
end
strainer_template.new(context)
end
# Returns the names of all the filter methods that are available to use in
# the strainer template.
#
# @return [Array<String>] The names of all the filter methods.
def filter_method_names
@strainer_template.filter_method_names
end
# Returns the tag class for the given tag name.
#
# @param name [String] The name of the tag.
# @return [Liquid::Tag] The tag class.
def tag_for_name(name)
@tags[name]
end
def freeze
@tags.freeze
# TODO: freeze the tags, currently this is not possible because of liquid-c
# @strainer_template.freeze
super
end
end
end
+1
View File
@@ -55,4 +55,5 @@ module Liquid
MethodOverrideError = Class.new(Error) MethodOverrideError = Class.new(Error)
DisabledError = Class.new(Error) DisabledError = Class.new(Error)
InternalError = Class.new(Error) InternalError = Class.new(Error)
TemplateEncodingError = Class.new(Error)
end end
+102 -22
View File
@@ -3,40 +3,120 @@
module Liquid module Liquid
class Expression class Expression
LITERALS = { LITERALS = {
nil => nil, 'nil' => nil, 'null' => nil, '' => nil, nil => nil,
'nil' => nil,
'null' => nil,
'' => nil,
'true' => true, 'true' => true,
'false' => false, 'false' => false,
'blank' => '', 'blank' => '',
'empty' => '' 'empty' => '',
# in lax mode, minus sign can be a VariableLookup
# For simplicity and performace, we treat it like a literal
'-' => VariableLookup.parse("-", nil).freeze,
}.freeze }.freeze
SINGLE_QUOTED_STRING = /\A\s*'(.*)'\s*\z/m DOT = ".".ord
DOUBLE_QUOTED_STRING = /\A\s*"(.*)"\s*\z/m ZERO = "0".ord
INTEGERS_REGEX = /\A\s*(-?\d+)\s*\z/ NINE = "9".ord
FLOATS_REGEX = /\A\s*(-?\d[\d\.]+)\s*\z/ DASH = "-".ord
# Use an atomic group (?>...) to avoid pathological backtracing from # Use an atomic group (?>...) to avoid pathological backtracing from
# malicious input as described in https://github.com/Shopify/liquid/issues/1357 # malicious input as described in https://github.com/Shopify/liquid/issues/1357
RANGES_REGEX = /\A\s*\(\s*(?>(\S+)\s*\.\.)\s*(\S+)\s*\)\s*\z/ RANGES_REGEX = /\A\(\s*(?>(\S+)\s*\.\.)\s*(\S+)\s*\)\z/
INTEGER_REGEX = /\A(-?\d+)\z/
FLOAT_REGEX = /\A(-?\d+)\.\d+\z/
def self.parse(markup) class << self
def parse(markup, ss = StringScanner.new(""), cache = nil)
return unless markup
markup = markup.strip # markup can be a frozen string
if (markup.start_with?('"') && markup.end_with?('"')) ||
(markup.start_with?("'") && markup.end_with?("'"))
return markup[1..-2]
elsif LITERALS.key?(markup)
return LITERALS[markup]
end
# Cache only exists during parsing
if cache
return cache[markup] if cache.key?(markup)
cache[markup] = inner_parse(markup, ss, cache).freeze
else
inner_parse(markup, ss, nil).freeze
end
end
def inner_parse(markup, ss, cache)
if (markup.start_with?("(") && markup.end_with?(")")) && markup =~ RANGES_REGEX
return RangeLookup.parse(
Regexp.last_match(1),
Regexp.last_match(2),
ss,
cache,
)
end
if (num = parse_number(markup, ss))
num
else
VariableLookup.parse(markup, ss, cache)
end
end
def parse_number(markup, ss)
# check if the markup is simple integer or float
case markup case markup
when nil when INTEGER_REGEX
nil return Integer(markup, 10)
when SINGLE_QUOTED_STRING, DOUBLE_QUOTED_STRING when FLOAT_REGEX
Regexp.last_match(1) return markup.to_f
when INTEGERS_REGEX end
Regexp.last_match(1).to_i
when RANGES_REGEX ss.string = markup
RangeLookup.parse(Regexp.last_match(1), Regexp.last_match(2)) # the first byte must be a digit or a dash
when FLOATS_REGEX byte = ss.scan_byte
Regexp.last_match(1).to_f
return false if byte != DASH && (byte < ZERO || byte > NINE)
if byte == DASH
peek_byte = ss.peek_byte
# if it starts with a dash, the next byte must be a digit
return false if peek_byte.nil? || !(peek_byte >= ZERO && peek_byte <= NINE)
end
# The markup could be a float with multiple dots
first_dot_pos = nil
num_end_pos = nil
while (byte = ss.scan_byte)
return false if byte != DOT && (byte < ZERO || byte > NINE)
# we found our number and now we are just scanning the rest of the string
next if num_end_pos
if byte == DOT
if first_dot_pos.nil?
first_dot_pos = ss.pos
else else
markup = markup.strip # we found another dot, so we know that the number ends here
if LITERALS.key?(markup) num_end_pos = ss.pos - 1
LITERALS[markup] end
end
end
num_end_pos = markup.length if ss.eos?
if num_end_pos
# number ends with a number "123.123"
markup.byteslice(0, num_end_pos).to_f
else else
VariableLookup.parse(markup) # number ends with a dot "123."
markup.byteslice(0, first_dot_pos).to_f
end end
end end
end end
+45 -5
View File
@@ -1,6 +1,11 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs
# @liquid_type object
# @liquid_name forloop
# @liquid_summary
# Information about a parent [`for` loop](/docs/api/liquid/tags/for).
class ForloopDrop < Drop class ForloopDrop < Drop
def initialize(name, length, parentloop) def initialize(name, length, parentloop)
@name = name @name = name
@@ -9,33 +14,68 @@ module Liquid
@index = 0 @index = 0
end end
attr_reader :length, :parentloop # @liquid_public_docs
# @liquid_name length
# @liquid_summary
# The total number of iterations in the loop.
# @liquid_return [number]
attr_reader :length
def name # @liquid_public_docs
Usage.increment('forloop_drop_name') # @liquid_name parentloop
@name # @liquid_summary
end # The parent `forloop` object.
# @liquid_description
# If the current `for` loop isn't nested inside another `for` loop, then `nil` is returned.
# @liquid_return [forloop]
attr_reader :parentloop
attr_reader :name
# @liquid_public_docs
# @liquid_summary
# The 1-based index of the current iteration.
# @liquid_return [number]
def index def index
@index + 1 @index + 1
end end
# @liquid_public_docs
# @liquid_summary
# The 0-based index of the current iteration.
# @liquid_return [number]
def index0 def index0
@index @index
end end
# @liquid_public_docs
# @liquid_summary
# The 1-based index of the current iteration, in reverse order.
# @liquid_return [number]
def rindex def rindex
@length - @index @length - @index
end end
# @liquid_public_docs
# @liquid_summary
# The 0-based index of the current iteration, in reverse order.
# @liquid_return [number]
def rindex0 def rindex0
@length - @index - 1 @length - @index - 1
end end
# @liquid_public_docs
# @liquid_summary
# Returns `true` if the current iteration is the first. Returns `false` if not.
# @liquid_return [boolean]
def first def first
@index == 0 @index == 0
end end
# @liquid_public_docs
# @liquid_summary
# Returns `true` if the current iteration is the last. Returns `false` if not.
# @liquid_return [boolean]
def last def last
@index == @length - 1 @index == @length - 1
end end
+153 -42
View File
@@ -1,62 +1,173 @@
# frozen_string_literal: true # frozen_string_literal: true
require "strscan"
module Liquid module Liquid
class Lexer class Lexer
SPECIALS = { CLOSE_ROUND = [:close_round, ")"].freeze
'|' => :pipe, CLOSE_SQUARE = [:close_square, "]"].freeze
'.' => :dot, COLON = [:colon, ":"].freeze
':' => :colon, COMMA = [:comma, ","].freeze
',' => :comma, COMPARISION_NOT_EQUAL = [:comparison, "!="].freeze
'[' => :open_square, COMPARISON_CONTAINS = [:comparison, "contains"].freeze
']' => :close_square, COMPARISON_EQUAL = [:comparison, "=="].freeze
'(' => :open_round, COMPARISON_GREATER_THAN = [:comparison, ">"].freeze
')' => :close_round, COMPARISON_GREATER_THAN_OR_EQUAL = [:comparison, ">="].freeze
'?' => :question, COMPARISON_LESS_THAN = [:comparison, "<"].freeze
'-' => :dash, COMPARISON_LESS_THAN_OR_EQUAL = [:comparison, "<="].freeze
}.freeze COMPARISON_NOT_EQUAL_ALT = [:comparison, "<>"].freeze
IDENTIFIER = /[a-zA-Z_][\w-]*\??/ DASH = [:dash, "-"].freeze
SINGLE_STRING_LITERAL = /'[^\']*'/ DOT = [:dot, "."].freeze
DOTDOT = [:dotdot, ".."].freeze
DOT_ORD = ".".ord
DOUBLE_STRING_LITERAL = /"[^\"]*"/ DOUBLE_STRING_LITERAL = /"[^\"]*"/
EOS = [:end_of_string].freeze
IDENTIFIER = /[a-zA-Z_][\w-]*\??/
NUMBER_LITERAL = /-?\d+(\.\d+)?/ NUMBER_LITERAL = /-?\d+(\.\d+)?/
DOTDOT = /\.\./ OPEN_ROUND = [:open_round, "("].freeze
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/ OPEN_SQUARE = [:open_square, "["].freeze
PIPE = [:pipe, "|"].freeze
QUESTION = [:question, "?"].freeze
RUBY_WHITESPACE = [" ", "\t", "\r", "\n", "\f"].freeze
SINGLE_STRING_LITERAL = /'[^\']*'/
WHITESPACE_OR_NOTHING = /\s*/ WHITESPACE_OR_NOTHING = /\s*/
def initialize(input) SINGLE_COMPARISON_TOKENS = [].tap do |table|
@ss = StringScanner.new(input) table["<".ord] = COMPARISON_LESS_THAN
table[">".ord] = COMPARISON_GREATER_THAN
table.freeze
end end
def tokenize TWO_CHARS_COMPARISON_JUMP_TABLE = [].tap do |table|
@output = [] table["=".ord] = [].tap do |sub_table|
sub_table["=".ord] = COMPARISON_EQUAL
sub_table.freeze
end
table["!".ord] = [].tap do |sub_table|
sub_table["=".ord] = COMPARISION_NOT_EQUAL
sub_table.freeze
end
table.freeze
end
until @ss.eos? COMPARISON_JUMP_TABLE = [].tap do |table|
@ss.skip(WHITESPACE_OR_NOTHING) table["<".ord] = [].tap do |sub_table|
break if @ss.eos? sub_table["=".ord] = COMPARISON_LESS_THAN_OR_EQUAL
tok = if (t = @ss.scan(COMPARISON_OPERATOR)) sub_table[">".ord] = COMPARISON_NOT_EQUAL_ALT
[:comparison, t] sub_table.freeze
elsif (t = @ss.scan(SINGLE_STRING_LITERAL)) end
[:string, t] table[">".ord] = [].tap do |sub_table|
elsif (t = @ss.scan(DOUBLE_STRING_LITERAL)) sub_table["=".ord] = COMPARISON_GREATER_THAN_OR_EQUAL
[:string, t] sub_table.freeze
elsif (t = @ss.scan(NUMBER_LITERAL)) end
[:number, t] table.freeze
elsif (t = @ss.scan(IDENTIFIER)) end
[:id, t]
elsif (t = @ss.scan(DOTDOT)) NEXT_MATCHER_JUMP_TABLE = [].tap do |table|
[:dotdot, t] "a".upto("z") do |c|
table[c.ord] = [:id, IDENTIFIER].freeze
table[c.upcase.ord] = [:id, IDENTIFIER].freeze
end
table["_".ord] = [:id, IDENTIFIER].freeze
"0".upto("9") do |c|
table[c.ord] = [:number, NUMBER_LITERAL].freeze
end
table["-".ord] = [:number, NUMBER_LITERAL].freeze
table["'".ord] = [:string, SINGLE_STRING_LITERAL].freeze
table["\"".ord] = [:string, DOUBLE_STRING_LITERAL].freeze
table.freeze
end
SPECIAL_TABLE = [].tap do |table|
table["|".ord] = PIPE
table[".".ord] = DOT
table[":".ord] = COLON
table[",".ord] = COMMA
table["[".ord] = OPEN_SQUARE
table["]".ord] = CLOSE_SQUARE
table["(".ord] = OPEN_ROUND
table[")".ord] = CLOSE_ROUND
table["?".ord] = QUESTION
table["-".ord] = DASH
end
NUMBER_TABLE = [].tap do |table|
"0".upto("9") do |c|
table[c.ord] = true
end
table.freeze
end
# rubocop:disable Metrics/BlockNesting
class << self
def tokenize(ss)
output = []
until ss.eos?
ss.skip(WHITESPACE_OR_NOTHING)
break if ss.eos?
start_pos = ss.pos
peeked = ss.peek_byte
if (special = SPECIAL_TABLE[peeked])
ss.scan_byte
# Special case for ".."
if special == DOT && ss.peek_byte == DOT_ORD
ss.scan_byte
output << DOTDOT
elsif special == DASH
# Special case for negative numbers
if (peeked_byte = ss.peek_byte) && NUMBER_TABLE[peeked_byte]
ss.pos -= 1
output << [:number, ss.scan(NUMBER_LITERAL)]
else else
c = @ss.getch output << special
if (s = SPECIALS[c]) end
[s, c]
else else
raise SyntaxError, "Unexpected character #{c}" output << special
end
elsif (sub_table = TWO_CHARS_COMPARISON_JUMP_TABLE[peeked])
ss.scan_byte
if (peeked_byte = ss.peek_byte) && (found = sub_table[peeked_byte])
output << found
ss.scan_byte
else
raise_syntax_error(start_pos, ss)
end
elsif (sub_table = COMPARISON_JUMP_TABLE[peeked])
ss.scan_byte
if (peeked_byte = ss.peek_byte) && (found = sub_table[peeked_byte])
output << found
ss.scan_byte
else
output << SINGLE_COMPARISON_TOKENS[peeked]
end
else
type, pattern = NEXT_MATCHER_JUMP_TABLE[peeked]
if type && (t = ss.scan(pattern))
# Special case for "contains"
output << if type == :id && t == "contains" && output.last&.first != :dot
COMPARISON_CONTAINS
else
[type, t]
end
else
raise_syntax_error(start_pos, ss)
end end
end end
@output << tok end
# rubocop:enable Metrics/BlockNesting
output << EOS
end end
@output << [:end_of_string] def raise_syntax_error(start_pos, ss)
ss.pos = start_pos
# the character could be a UTF-8 character, use getch to get all the bytes
raise SyntaxError, "Unexpected character #{ss.getch}"
end
end end
end end
end end
+7 -5
View File
@@ -13,15 +13,17 @@
for_invalid_attribute: "Invalid attribute in for loop. Valid attributes are limit and offset" for_invalid_attribute: "Invalid attribute in for loop. Valid attributes are limit and offset"
if: "Syntax Error in tag 'if' - Valid syntax: if [expression]" if: "Syntax Error in tag 'if' - Valid syntax: if [expression]"
include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]" include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]"
unknown_tag: "Unknown tag '%{tag}'" inline_comment_invalid: "Syntax error in tag '#' - Each line of comments must be prefixed by the '#' character"
invalid_delimiter: "'%{tag}' 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}"
invalid_template_encoding: "Invalid template encoding"
render: "Syntax error in tag 'render' - Template name must be a quoted string"
table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
tag_never_closed: "'%{block_name}' tag was never closed"
tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
unexpected_else: "%{block_name} tag does not expect 'else' tag" unexpected_else: "%{block_name} tag does not expect 'else' tag"
unexpected_outer_tag: "Unexpected outer '%{tag}' tag" unexpected_outer_tag: "Unexpected outer '%{tag}' tag"
tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}" unknown_tag: "Unknown tag '%{tag}'"
variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}" variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}"
tag_never_closed: "'%{block_name}' tag was never closed"
table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
render: "Syntax error in tag 'render' - Template name must be a quoted string"
argument: argument:
include: "Argument error in tag 'include' - Illegal template name" include: "Argument error in tag 'include' - Illegal template name"
disabled: disabled:
+29 -6
View File
@@ -3,14 +3,27 @@
module Liquid module Liquid
class ParseContext class ParseContext
attr_accessor :locale, :line_number, :trim_whitespace, :depth attr_accessor :locale, :line_number, :trim_whitespace, :depth
attr_reader :partial, :warnings, :error_mode attr_reader :partial, :warnings, :error_mode, :environment
def initialize(options = {}) def initialize(options = Const::EMPTY_HASH)
@environment = options.fetch(:environment, Environment.default)
@template_options = options ? options.dup : {} @template_options = options ? options.dup : {}
@locale = @template_options[:locale] ||= I18n.new @locale = @template_options[:locale] ||= I18n.new
@warnings = [] @warnings = []
# constructing new StringScanner in Lexer, Tokenizer, etc is expensive
# This StringScanner will be shared by all of them
@string_scanner = StringScanner.new("")
@expression_cache = if options[:expression_cache].nil?
{}
elsif options[:expression_cache].respond_to?(:[]) && options[:expression_cache].respond_to?(:[]=)
options[:expression_cache]
elsif options[:expression_cache]
{}
end
self.depth = 0 self.depth = 0
self.partial = false self.partial = false
end end
@@ -23,19 +36,29 @@ module Liquid
Liquid::BlockBody.new Liquid::BlockBody.new
end end
def new_tokenizer(markup, start_line_number: nil, for_liquid_tag: false) def new_parser(input)
Tokenizer.new(markup, line_number: start_line_number, for_liquid_tag: for_liquid_tag) @string_scanner.string = input
Parser.new(@string_scanner)
end
def new_tokenizer(source, start_line_number: nil, for_liquid_tag: false)
Tokenizer.new(
source: source,
string_scanner: @string_scanner,
line_number: start_line_number,
for_liquid_tag: for_liquid_tag,
)
end end
def parse_expression(markup) def parse_expression(markup)
Expression.parse(markup) Expression.parse(markup, @string_scanner, @expression_cache)
end end
def partial=(value) def partial=(value)
@partial = value @partial = value
@options = value ? partial_options : @template_options @options = value ? partial_options : @template_options
@error_mode = @options[:error_mode] || Template.error_mode @error_mode = @options[:error_mode] || @environment.error_mode
end end
def partial_options def partial_options
+1 -1
View File
@@ -36,7 +36,7 @@ module Liquid
protected protected
def children def children
@node.respond_to?(:nodelist) ? Array(@node.nodelist) : [] @node.respond_to?(:nodelist) ? Array(@node.nodelist) : Const::EMPTY_ARRAY
end end
end end
end end
+3 -3
View File
@@ -3,8 +3,8 @@
module Liquid module Liquid
class Parser class Parser
def initialize(input) def initialize(input)
l = Lexer.new(input) ss = input.is_a?(StringScanner) ? input : StringScanner.new(input)
@tokens = l.tokenize @tokens = Lexer.tokenize(ss)
@p = 0 # pointer to current location @p = 0 # pointer to current location
end end
@@ -53,7 +53,7 @@ module Liquid
str = consume str = consume
str << variable_lookups str << variable_lookups
when :open_square when :open_square
str = consume str = consume.dup
str << expression str << expression
str << consume(:close_square) str << consume(:close_square)
str << variable_lookups str << variable_lookups
+14 -5
View File
@@ -3,20 +3,29 @@
module Liquid module Liquid
class PartialCache class PartialCache
def self.load(template_name, context:, parse_context:) def self.load(template_name, context:, parse_context:)
cached_partials = (context.registers[:cached_partials] ||= {}) cached_partials = context.registers[:cached_partials]
cached = cached_partials[template_name] cache_key = "#{template_name}:#{parse_context.error_mode}"
cached = cached_partials[cache_key]
return cached if cached return cached if cached
file_system = (context.registers[:file_system] ||= Liquid::Template.file_system) file_system = context.registers[:file_system]
source = file_system.read_template_file(template_name) source = file_system.read_template_file(template_name)
parse_context.partial = true parse_context.partial = true
template_factory = (context.registers[:template_factory] ||= Liquid::TemplateFactory.new) template_factory = context.registers[:template_factory]
template = template_factory.for(template_name) template = template_factory.for(template_name)
begin
partial = template.parse(source, parse_context) partial = template.parse(source, parse_context)
cached_partials[template_name] = partial rescue Liquid::Error => e
e.template_name = template&.name || template_name
raise e
end
partial.name ||= template_name
cached_partials[cache_key] = partial
ensure ensure
parse_context.partial = false parse_context.partial = false
end end
+21 -3
View File
@@ -2,15 +2,27 @@
module Liquid module Liquid
class RangeLookup class RangeLookup
def self.parse(start_markup, end_markup) def self.parse(start_markup, end_markup, string_scanner, cache = nil)
start_obj = Expression.parse(start_markup) start_obj = Expression.parse(start_markup, string_scanner, cache)
end_obj = Expression.parse(end_markup) end_obj = Expression.parse(end_markup, string_scanner, cache)
if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate) if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate)
new(start_obj, end_obj) new(start_obj, end_obj)
else else
begin
start_obj.to_i..end_obj.to_i start_obj.to_i..end_obj.to_i
rescue NoMethodError
invalid_expr = start_markup unless start_obj.respond_to?(:to_i)
invalid_expr ||= end_markup unless end_obj.respond_to?(:to_i)
if invalid_expr
raise Liquid::SyntaxError, "Invalid expression type '#{invalid_expr}' in range expression"
end
raise
end end
end end
end
attr_reader :start_obj, :end_obj
def initialize(start_obj, end_obj) def initialize(start_obj, end_obj)
@start_obj = start_obj @start_obj = start_obj
@@ -35,5 +47,11 @@ module Liquid
Utils.to_integer(input) Utils.to_integer(input)
end end
end end
class ParseTreeVisitor < Liquid::ParseTreeVisitor
def children
[@node.start_obj, @node.end_obj]
end
end
end end
end end
-6
View File
@@ -1,6 +0,0 @@
# frozen_string_literal: true
module Liquid
class Register
end
end
+51
View File
@@ -0,0 +1,51 @@
# frozen_string_literal: true
module Liquid
class Registers
attr_reader :static
def initialize(registers = {})
@static = registers.is_a?(Registers) ? registers.static : registers
@changes = {}
end
def []=(key, value)
@changes[key] = value
end
def [](key)
if @changes.key?(key)
@changes[key]
else
@static[key]
end
end
def delete(key)
@changes.delete(key)
end
UNDEFINED = Object.new
def fetch(key, default = UNDEFINED, &block)
if @changes.key?(key)
@changes.fetch(key)
elsif default != UNDEFINED
if block_given?
@static.fetch(key, &block)
else
@static.fetch(key, default)
end
else
@static.fetch(key, &block)
end
end
def key?(key)
@changes.key?(key) || @static.key?(key)
end
end
# Alias for backwards compatibility
StaticRegisters = Registers
end
File diff suppressed because it is too large Load Diff
-44
View File
@@ -1,44 +0,0 @@
# 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
-36
View File
@@ -1,36 +0,0 @@
# 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
+9
View File
@@ -31,6 +31,15 @@ module Liquid
filter_methods.include?(method.to_s) filter_methods.include?(method.to_s)
end end
def inherited(subclass)
super
subclass.instance_variable_set(:@filter_methods, @filter_methods.dup)
end
def filter_method_names
filter_methods.map(&:to_s).to_a
end
private private
def filter_methods def filter_methods
+58 -1
View File
@@ -1,6 +1,11 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs
# @liquid_type object
# @liquid_name tablerowloop
# @liquid_summary
# Information about a parent [`tablerow` loop](/docs/api/liquid/tags/tablerow).
class TablerowloopDrop < Drop class TablerowloopDrop < Drop
def initialize(length, cols) def initialize(length, cols)
@length = length @length = length
@@ -10,40 +15,92 @@ module Liquid
@index = 0 @index = 0
end end
attr_reader :length, :col, :row # @liquid_public_docs
# @liquid_summary
# The total number of iterations in the loop.
# @liquid_return [number]
attr_reader :length
# @liquid_public_docs
# @liquid_summary
# The 1-based index of the current column.
# @liquid_return [number]
attr_reader :col
# @liquid_public_docs
# @liquid_summary
# The 1-based index of current row.
# @liquid_return [number]
attr_reader :row
# @liquid_public_docs
# @liquid_summary
# The 1-based index of the current iteration.
# @liquid_return [number]
def index def index
@index + 1 @index + 1
end end
# @liquid_public_docs
# @liquid_summary
# The 0-based index of the current iteration.
# @liquid_return [number]
def index0 def index0
@index @index
end end
# @liquid_public_docs
# @liquid_summary
# The 0-based index of the current column.
# @liquid_return [number]
def col0 def col0
@col - 1 @col - 1
end end
# @liquid_public_docs
# @liquid_summary
# The 1-based index of the current iteration, in reverse order.
# @liquid_return [number]
def rindex def rindex
@length - @index @length - @index
end end
# @liquid_public_docs
# @liquid_summary
# The 0-based index of the current iteration, in reverse order.
# @liquid_return [number]
def rindex0 def rindex0
@length - @index - 1 @length - @index - 1
end end
# @liquid_public_docs
# @liquid_summary
# Returns `true` if the current iteration is the first. Returns `false` if not.
# @liquid_return [boolean]
def first def first
@index == 0 @index == 0
end end
# @liquid_public_docs
# @liquid_summary
# Returns `true` if the current iteration is the last. Returns `false` if not.
# @liquid_return [boolean]
def last def last
@index == @length - 1 @index == @length - 1
end end
# @liquid_public_docs
# @liquid_summary
# Returns `true` if the current column is the first in the row. Returns `false` if not.
# @liquid_return [boolean]
def col_first def col_first
@col == 1 @col == 1
end end
# @liquid_public_docs
# @liquid_summary
# Returns `true` if the current column is the last in the row. Returns `false` if not.
# @liquid_return [boolean]
def col_last def col_last
@col == @cols @col == @cols
end end
+13 -3
View File
@@ -1,5 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'liquid/tag/disabler'
require 'liquid/tag/disableable'
module Liquid module Liquid
class Tag class Tag
attr_reader :nodelist, :tag_name, :line_number, :parse_context attr_reader :nodelist, :tag_name, :line_number, :parse_context
@@ -14,12 +17,18 @@ module Liquid
end end
def disable_tags(*tag_names) def disable_tags(*tag_names)
@disabled_tags ||= [] tag_names += disabled_tags
@disabled_tags.concat(tag_names) define_singleton_method(:disabled_tags) { tag_names }
prepend(Disabler) prepend(Disabler)
end end
private :new private :new
protected
def disabled_tags
[]
end
end end
def initialize(tag_name, markup, parse_context) def initialize(tag_name, markup, parse_context)
@@ -48,7 +57,8 @@ module Liquid
# of the `render_to_output_buffer` method will become the default and the `render` # of the `render_to_output_buffer` method will become the default and the `render`
# method will be removed. # method will be removed.
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
output << render(context) render_result = render(context)
output << render_result if render_result
output output
end end
-8
View File
@@ -3,14 +3,6 @@
module Liquid module Liquid
class Tag class Tag
module Disabler module Disabler
module ClassMethods
attr_reader :disabled_tags
end
def self.prepended(base)
base.extend(ClassMethods)
end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
context.with_disabled_tags(self.class.disabled_tags) do context.with_disabled_tags(self.class.disabled_tags) do
super super
+47
View File
@@ -0,0 +1,47 @@
# frozen_string_literal: true
require_relative "tags/table_row"
require_relative "tags/echo"
require_relative "tags/if"
require_relative "tags/break"
require_relative "tags/inline_comment"
require_relative "tags/for"
require_relative "tags/assign"
require_relative "tags/ifchanged"
require_relative "tags/case"
require_relative "tags/include"
require_relative "tags/continue"
require_relative "tags/capture"
require_relative "tags/decrement"
require_relative "tags/unless"
require_relative "tags/increment"
require_relative "tags/comment"
require_relative "tags/raw"
require_relative "tags/render"
require_relative "tags/cycle"
module Liquid
module Tags
STANDARD_TAGS = {
'cycle' => Cycle,
'render' => Render,
'raw' => Raw,
'comment' => Comment,
'increment' => Increment,
'unless' => Unless,
'decrement' => Decrement,
'capture' => Capture,
'continue' => Continue,
'include' => Include,
'case' => Case,
'ifchanged' => Ifchanged,
'assign' => Assign,
'for' => For,
'#' => InlineComment,
'break' => Break,
'if' => If,
'echo' => Echo,
'tablerow' => TableRow,
}.freeze
end
end
+12 -10
View File
@@ -1,14 +1,18 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
# Assign sets a variable in your template. # @liquid_public_docs
# # @liquid_type tag
# {% assign foo = 'monkey' %} # @liquid_category variable
# # @liquid_name assign
# You can then use the variable later in the page. # @liquid_summary
# # Creates a new variable.
# {{ foo }} # @liquid_description
# # You can create variables of any [basic type](/docs/api/liquid/basics#types), [object](/docs/api/liquid/objects), or object property.
# @liquid_syntax
# {% assign variable_name = value %}
# @liquid_syntax_keyword variable_name The name of the variable being created.
# @liquid_syntax_keyword value The value you want to assign to the variable.
class Assign < Tag class Assign < Tag
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
@@ -68,6 +72,4 @@ module Liquid
end end
end end
end end
Template.register_tag('assign', Assign)
end end
+8 -2
View File
@@ -10,6 +10,14 @@ module Liquid
# {% endif %} # {% endif %}
# {% endfor %} # {% endfor %}
# #
# @liquid_public_docs
# @liquid_type tag
# @liquid_category iteration
# @liquid_name break
# @liquid_summary
# Stops a [`for` loop](/docs/api/liquid/tags/for) from iterating.
# @liquid_syntax
# {% break %}
class Break < Tag class Break < Tag
INTERRUPT = BreakInterrupt.new.freeze INTERRUPT = BreakInterrupt.new.freeze
@@ -18,6 +26,4 @@ module Liquid
output output
end end
end end
Template.register_tag('break', Break)
end end
+13 -12
View File
@@ -1,17 +1,20 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
# Capture stores the result of a block into a variable without rendering it inplace. # @liquid_public_docs
# # @liquid_type tag
# {% capture heading %} # @liquid_category variable
# Monkeys! # @liquid_name capture
# @liquid_summary
# Creates a new variable with a string value.
# @liquid_description
# You can create complex strings with Liquid logic and variables.
# @liquid_syntax
# {% capture variable %}
# value
# {% endcapture %} # {% endcapture %}
# ... # @liquid_syntax_keyword variable The name of the variable being created.
# <h1>{{ heading }}</h1> # @liquid_syntax_keyword value The value you want to assign to the variable.
#
# Capture is useful for saving content for use later in your template, such as
# in a sidebar or footer.
#
class Capture < Block class Capture < Block
Syntax = /(#{VariableSignature}+)/o Syntax = /(#{VariableSignature}+)/o
@@ -36,6 +39,4 @@ module Liquid
true true
end end
end end
Template.register_tag('capture', Capture)
end end
+29 -3
View File
@@ -1,6 +1,27 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category conditional
# @liquid_name case
# @liquid_summary
# Renders a specific expression depending on the value of a specific variable.
# @liquid_syntax
# {% case variable %}
# {% when first_value %}
# first_expression
# {% when second_value %}
# second_expression
# {% else %}
# third_expression
# {% endcase %}
# @liquid_syntax_keyword variable The name of the variable you want to base your case statement on.
# @liquid_syntax_keyword first_value A specific value to check for.
# @liquid_syntax_keyword second_value A specific value to check for.
# @liquid_syntax_keyword first_expression An expression to be rendered when the variable's value matches `first_value`.
# @liquid_syntax_keyword second_expression An expression to be rendered when the variable's value matches `second_value`.
# @liquid_syntax_keyword third_expression An expression to be rendered when the variable's value has no match.
class Case < Block class Case < Block
Syntax = /(#{QuotedFragment})/o Syntax = /(#{QuotedFragment})/o
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om
@@ -52,7 +73,14 @@ module Liquid
@blocks.each do |block| @blocks.each do |block|
if block.else? if block.else?
block.attachment.render_to_output_buffer(context, output) if execute_else_block block.attachment.render_to_output_buffer(context, output) if execute_else_block
elsif block.evaluate(context) next
end
result = Liquid::Utils.to_liquid_value(
block.evaluate(context),
)
if result
execute_else_block = false execute_else_block = false
block.attachment.render_to_output_buffer(context, output) block.attachment.render_to_output_buffer(context, output)
end end
@@ -95,6 +123,4 @@ module Liquid
end end
end end
end end
Template.register_tag('case', Case)
end end
+71 -1
View File
@@ -1,6 +1,19 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category syntax
# @liquid_name comment
# @liquid_summary
# Prevents an expression from being rendered or output.
# @liquid_description
# Any text inside `comment` tags won't be output, and any Liquid code will be parsed, but not executed.
# @liquid_syntax
# {% comment %}
# content
# {% endcomment %}
# @liquid_syntax_keyword content The content of the comment.
class Comment < Block class Comment < Block
def render_to_output_buffer(_context, output) def render_to_output_buffer(_context, output)
output output
@@ -12,7 +25,64 @@ module Liquid
def blank? def blank?
true true
end end
private
def parse_body(body, tokenizer)
if parse_context.depth >= MAX_DEPTH
raise StackLevelError, "Nesting too deep"
end end
Template.register_tag('comment', Comment) parse_context.depth += 1
comment_tag_depth = 1
begin
# Consume tokens without creating child nodes.
# The children tag doesn't require to be a valid Liquid except the comment and raw tag.
# The child comment and raw tag must be closed.
while (token = tokenizer.send(:shift))
tag_name = if tokenizer.for_liquid_tag
next if token.empty? || token.match?(BlockBody::WhitespaceOrNothing)
tag_name_match = BlockBody::LiquidTagToken.match(token)
next if tag_name_match.nil?
tag_name_match[1]
else
token =~ BlockBody::FullToken
Regexp.last_match(2)
end
case tag_name
when "raw"
parse_raw_tag_body(tokenizer)
when "comment"
comment_tag_depth += 1
when "endcomment"
comment_tag_depth -= 1
end
if comment_tag_depth.zero?
parse_context.trim_whitespace = (token[-3] == WhitespaceControl) unless tokenizer.for_liquid_tag
return false
end
end
raise_tag_never_closed(block_name)
ensure
parse_context.depth -= 1
end
false
end
def parse_raw_tag_body(tokenizer)
while (token = tokenizer.send(:shift))
return if token =~ BlockBody::FullTokenPossiblyInvalid && "endraw" == Regexp.last_match(2)
end
raise_tag_never_closed("raw")
end
end
end end
+7 -10
View File
@@ -1,15 +1,14 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
# Continue tag to be used to break out of a for loop. # @liquid_public_docs
# # @liquid_type tag
# == Basic Usage: # @liquid_category iteration
# {% for item in collection %} # @liquid_name continue
# {% if item.condition %} # @liquid_summary
# Causes a [`for` loop](/docs/api/liquid/tags/for) to skip to the next iteration.
# @liquid_syntax
# {% continue %} # {% continue %}
# {% endif %}
# {% endfor %}
#
class Continue < Tag class Continue < Tag
INTERRUPT = ContinueInterrupt.new.freeze INTERRUPT = ContinueInterrupt.new.freeze
@@ -18,6 +17,4 @@ module Liquid
output output
end end
end end
Template.register_tag('continue', Continue)
end end
+25 -14
View File
@@ -1,18 +1,19 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
# Cycle is usually used within a loop to alternate between values, like colors or DOM classes. # @liquid_public_docs
# # @liquid_type tag
# {% for item in items %} # @liquid_category iteration
# <div class="{% cycle 'red', 'green', 'blue' %}"> {{ item }} </div> # @liquid_name cycle
# {% end %} # @liquid_summary
# # Loops through a group of strings and outputs them one at a time for each iteration of a [`for` loop](/docs/api/liquid/tags/for).
# <div class="red"> Item one </div> # @liquid_description
# <div class="green"> Item two </div> # The `cycle` tag must be used inside a `for` loop.
# <div class="blue"> Item three </div>
# <div class="red"> Item four </div>
# <div class="green"> Item five</div>
# #
# > Tip:
# > Use the `cycle` tag to output text in a predictable pattern. For example, to apply odd/even classes to rows in a table.
# @liquid_syntax
# {% cycle string, string, ... %}
class Cycle < Tag class Cycle < Tag
SimpleSyntax = /\A#{QuotedFragment}+/o SimpleSyntax = /\A#{QuotedFragment}+/o
NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om
@@ -25,14 +26,20 @@ module Liquid
when NamedSyntax when NamedSyntax
@variables = variables_from_string(Regexp.last_match(2)) @variables = variables_from_string(Regexp.last_match(2))
@name = parse_expression(Regexp.last_match(1)) @name = parse_expression(Regexp.last_match(1))
@is_named = true
when SimpleSyntax when SimpleSyntax
@variables = variables_from_string(markup) @variables = variables_from_string(markup)
@name = @variables.to_s @name = @variables.to_s
@is_named = !@name.match?(/\w+:0x\h{8}/)
else else
raise SyntaxError, options[:locale].t("errors.syntax.cycle") raise SyntaxError, options[:locale].t("errors.syntax.cycle")
end end
end end
def named?
@is_named
end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
context.registers[:cycle] ||= {} context.registers[:cycle] ||= {}
@@ -61,7 +68,13 @@ module Liquid
def variables_from_string(markup) def variables_from_string(markup)
markup.split(',').collect do |var| markup.split(',').collect do |var|
var =~ /\s*(#{QuotedFragment})\s*/o var =~ /\s*(#{QuotedFragment})\s*/o
Regexp.last_match(1) ? parse_expression(Regexp.last_match(1)) : nil next unless Regexp.last_match(1)
# Expression Parser returns cached objects, and we need to dup them to
# start the cycle over for each new cycle call.
# Liquid-C does not have a cache, so we don't need to dup the object.
var = parse_expression(Regexp.last_match(1))
var.is_a?(VariableLookup) ? var.dup : var
end.compact end.compact
end end
@@ -71,6 +84,4 @@ module Liquid
end end
end end
end end
Template.register_tag('cycle', Cycle)
end end
+22 -22
View File
@@ -1,38 +1,38 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
# decrement is used in a place where one needs to insert a counter # @liquid_public_docs
# into a template, and needs the counter to survive across # @liquid_type tag
# multiple instantiations of the template. # @liquid_category variable
# NOTE: decrement is a pre-decrement, --i, # @liquid_name decrement
# while increment is post: i++. # @liquid_summary
# # Creates a new variable, with a default value of -1, that's decreased by 1 with each subsequent call.
# (To achieve the survival, the application must keep the context) # @liquid_description
# # Variables that are declared with `decrement` are unique to the [layout](/themes/architecture/layouts), [template](/themes/architecture/templates),
# if the variable does not exist, it is created with value 0. # or [section](/themes/architecture/sections) file that they're created in. However, the variable is shared across
# [snippets](/themes/architecture#snippets) included in the file.
# Hello: {% decrement variable %}
#
# gives you:
#
# Hello: -1
# Hello: -2
# Hello: -3
# #
# Similarly, variables that are created with `decrement` are independent from those created with [`assign`](/docs/api/liquid/tags/assign)
# and [`capture`](/docs/api/liquid/tags/capture). However, `decrement` and [`increment`](/docs/api/liquid/tags/increment) share
# variables.
# @liquid_syntax
# {% decrement variable_name %}
# @liquid_syntax_keyword variable_name The name of the variable being decremented.
class Decrement < Tag class Decrement < Tag
attr_reader :variable_name
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
@variable = markup.strip @variable_name = markup.strip
end end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
value = context.environments.first[@variable] ||= 0 counter_environment = context.environments.first
value = counter_environment[@variable_name] || 0
value -= 1 value -= 1
context.environments.first[@variable] = value counter_environment[@variable_name] = value
output << value.to_s output << value.to_s
output output
end end
end end
Template.register_tag('decrement', Decrement)
end end
+16 -11
View File
@@ -1,16 +1,23 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
# Echo outputs an expression # @liquid_public_docs
# # @liquid_type tag
# {% echo monkey %} # @liquid_category syntax
# {% echo user.name %} # @liquid_name echo
# # @liquid_summary
# This is identical to variable output syntax, like {{ foo }}, but works # Outputs an expression.
# inside {% liquid %} tags. The full syntax is supported, including filters: # @liquid_description
# # Using the `echo` tag is the same as wrapping an expression in curly brackets (`{{` and `}}`). However, unlike the curly
# {% echo user | link %} # bracket method, you can use the `echo` tag inside [`liquid` tags](/docs/api/liquid/tags/liquid).
# #
# > Tip:
# > You can use [filters](/docs/api/liquid/filters) on expressions inside `echo` tags.
# @liquid_syntax
# {% liquid
# echo expression
# %}
# @liquid_syntax_keyword expression The expression to be output.
class Echo < Tag class Echo < Tag
attr_reader :variable attr_reader :variable
@@ -29,6 +36,4 @@ module Liquid
end end
end end
end end
Template.register_tag('echo', Echo)
end end
+25 -48
View File
@@ -1,50 +1,29 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
# "For" iterates over an array or collection. # @liquid_public_docs
# Several useful variables are available to you within the loop. # @liquid_type tag
# @liquid_category iteration
# @liquid_name for
# @liquid_summary
# Renders an expression for every item in an array.
# @liquid_description
# You can do a maximum of 50 iterations with a `for` loop. If you need to iterate over more than 50 items, then use the
# [`paginate` tag](/docs/api/liquid/tags/paginate) to split the items over multiple pages.
# #
# == Basic usage: # > Tip:
# {% for item in collection %} # > Every `for` loop has an associated [`forloop` object](/docs/api/liquid/objects/forloop) with information about the loop.
# {{ forloop.index }}: {{ item.name }} # @liquid_syntax
# {% for variable in array %}
# expression
# {% endfor %} # {% endfor %}
# # @liquid_syntax_keyword variable The current item in the array.
# == Advanced usage: # @liquid_syntax_keyword array The array to iterate over.
# {% for item in collection %} # @liquid_syntax_keyword expression The expression to render for each iteration.
# <div {% if forloop.first %}class="first"{% endif %}> # @liquid_optional_param limit [number] The number of iterations to perform.
# Item {{ forloop.index }}: {{ item.name }} # @liquid_optional_param offset [number] The 1-based index to start iterating at.
# </div> # @liquid_optional_param range [untyped] A custom numeric range to iterate over.
# {% else %} # @liquid_optional_param reversed [untyped] Iterate in reverse order.
# There is nothing in the collection.
# {% endfor %}
#
# You can also define a limit and offset much like SQL. Remember
# that offset starts at 0 for the first item.
#
# {% for item in collection limit:5 offset:10 %}
# {{ item.name }}
# {% end %}
#
# 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:
#
# forloop.name:: 'item-collection'
# forloop.length:: Length of the loop
# forloop.index:: The current item's position in the collection;
# forloop.index starts at 1.
# This is helpful for non-programmers who start believe
# the first item in an array is 1, not 0.
# forloop.index0:: The current item's position in the collection
# where the first item is 0
# forloop.rindex:: Number of items remaining in the loop
# (length - index) where 1 is the last item.
# forloop.rindex0:: Number of items remaining in the loop
# where 0 is the last item.
# forloop.first:: Returns true if the item is the first item.
# forloop.last:: Returns true if the item is the last item.
# forloop.parentloop:: Provides access to the parent loop, if present.
#
class For < Block class For < Block
Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
@@ -109,7 +88,7 @@ module Liquid
end end
def strict_parse(markup) def strict_parse(markup)
p = Parser.new(markup) p = @parse_context.new_parser(markup)
@variable_name = p.consume(:id) @variable_name = p.consume(:id)
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in') raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in')
@@ -119,11 +98,12 @@ module Liquid
@name = "#{@variable_name}-#{collection_name}" @name = "#{@variable_name}-#{collection_name}"
@reversed = p.id?('reversed') @reversed = p.id?('reversed')
while p.look(:id) && p.look(:colon, 1) while p.look(:comma) || p.look(:id)
p.consume?(:comma)
unless (attribute = p.id?('limit') || p.id?('offset')) unless (attribute = p.id?('limit') || p.id?('offset'))
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_attribute") raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_attribute")
end end
p.consume p.consume(:colon)
set_attribute(attribute, p.expression) set_attribute(attribute, p.expression)
end end
p.consume(:end_of_string) p.consume(:end_of_string)
@@ -198,7 +178,6 @@ module Liquid
case key case key
when 'offset' when 'offset'
@from = if expr == 'continue' @from = if expr == 'continue'
Usage.increment('for_offset_continue')
:continue :continue
else else
parse_expression(expr) parse_expression(expr)
@@ -222,6 +201,4 @@ module Liquid
end end
end end
end end
Template.register_tag('for', For)
end end
+18 -14
View File
@@ -1,16 +1,18 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
# If is the conditional block # @liquid_public_docs
# # @liquid_type tag
# {% if user.admin %} # @liquid_category conditional
# Admin user! # @liquid_name if
# {% else %} # @liquid_summary
# Not admin user # Renders an expression if a specific condition is `true`.
# @liquid_syntax
# {% if condition %}
# expression
# {% endif %} # {% endif %}
# # @liquid_syntax_keyword condition The condition to evaluate.
# There are {% if count < 5 %} less {% else %} more {% endif %} items than you need. # @liquid_syntax_keyword expression The expression to render if the condition is met.
#
class If < Block class If < Block
Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o
ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o
@@ -50,7 +52,11 @@ module Liquid
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
@blocks.each do |block| @blocks.each do |block|
if block.evaluate(context) result = Liquid::Utils.to_liquid_value(
block.evaluate(context),
)
if result
return block.attachment.render_to_output_buffer(context, output) return block.attachment.render_to_output_buffer(context, output)
end end
end end
@@ -96,7 +102,7 @@ module Liquid
end end
def strict_parse(markup) def strict_parse(markup)
p = Parser.new(markup) p = @parse_context.new_parser(markup)
condition = parse_binary_comparisons(p) condition = parse_binary_comparisons(p)
p.consume(:end_of_string) p.consume(:end_of_string)
condition condition
@@ -105,7 +111,7 @@ module Liquid
def parse_binary_comparisons(p) def parse_binary_comparisons(p)
condition = parse_comparison(p) condition = parse_comparison(p)
first_condition = condition first_condition = condition
while (op = (p.id?('and') || p.id?('or'))) while (op = p.id?('and') || p.id?('or'))
child_condition = parse_comparison(p) child_condition = parse_comparison(p)
condition.send(op, child_condition) condition.send(op, child_condition)
condition = child_condition condition = child_condition
@@ -129,6 +135,4 @@ module Liquid
end end
end end
end end
Template.register_tag('if', If)
end end
-2
View File
@@ -14,6 +14,4 @@ module Liquid
output output
end end
end end
Template.register_tag('ifchanged', Ifchanged)
end end
+20 -18
View File
@@ -1,20 +1,22 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
# Include allows templates to relate with other templates # @liquid_public_docs
# # @liquid_type tag
# Simply include another template: # @liquid_category theme
# # @liquid_name include
# {% include 'product' %} # @liquid_summary
# # Renders a [snippet](/themes/architecture#snippets).
# Include a template with a local variable: # @liquid_description
# # Inside the snippet, you can access and alter variables that are [created](/docs/api/liquid/tags/variable-tags) outside of the
# {% include 'product' with products[0] %} # snippet.
# # @liquid_syntax
# Include a template for a collection: # {% include 'filename' %}
# # @liquid_syntax_keyword filename The name of the snippet to render, without the `.liquid` extension.
# {% include 'product' for products %} # @liquid_deprecated
# Deprecated because the way that variables are handled reduces performance and makes code harder to both read and maintain.
# #
# The `include` tag has been replaced by [`render`](/docs/api/liquid/tags/render).
class Include < Tag class Include < Tag
prepend Tag::Disableable prepend Tag::Disableable
@@ -50,12 +52,12 @@ module Liquid
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
template_name = context.evaluate(@template_name_expr) template_name = context.evaluate(@template_name_expr)
raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name.is_a?(String)
partial = PartialCache.load( partial = PartialCache.load(
template_name, template_name,
context: context, context: context,
parse_context: parse_context parse_context: parse_context,
) )
context_variable_name = @alias_name || template_name.split('/').last context_variable_name = @alias_name || template_name.split('/').last
@@ -68,9 +70,11 @@ module Liquid
old_template_name = context.template_name old_template_name = context.template_name
old_partial = context.partial old_partial = context.partial
begin begin
context.template_name = template_name context.template_name = partial.name
context.partial = true context.partial = true
context.stack do context.stack do
@attributes.each do |key, value| @attributes.each do |key, value|
context[key] = context.evaluate(value) context[key] = context.evaluate(value)
@@ -106,6 +110,4 @@ module Liquid
end end
end end
end end
Template.register_tag('include', Include)
end end
+22 -19
View File
@@ -1,35 +1,38 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
# increment is used in a place where one needs to insert a counter # @liquid_public_docs
# into a template, and needs the counter to survive across # @liquid_type tag
# multiple instantiations of the template. # @liquid_category variable
# (To achieve the survival, the application must keep the context) # @liquid_name increment
# # @liquid_summary
# if the variable does not exist, it is created with value 0. # Creates a new variable, with a default value of 0, that's increased by 1 with each subsequent call.
# # @liquid_description
# Hello: {% increment variable %} # Variables that are declared with `increment` are unique to the [layout](/themes/architecture/layouts), [template](/themes/architecture/templates),
# # or [section](/themes/architecture/sections) file that they're created in. However, the variable is shared across
# gives you: # [snippets](/themes/architecture#snippets) included in the file.
#
# Hello: 0
# Hello: 1
# Hello: 2
# #
# Similarly, variables that are created with `increment` are independent from those created with [`assign`](/docs/api/liquid/tags/assign)
# and [`capture`](/docs/api/liquid/tags/capture). However, `increment` and [`decrement`](/docs/api/liquid/tags/decrement) share
# variables.
# @liquid_syntax
# {% increment variable_name %}
# @liquid_syntax_keyword variable_name The name of the variable being incremented.
class Increment < Tag class Increment < Tag
attr_reader :variable_name
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
@variable = markup.strip @variable_name = markup.strip
end end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
value = context.environments.first[@variable] ||= 0 counter_environment = context.environments.first
context.environments.first[@variable] = value + 1 value = counter_environment[@variable_name] || 0
counter_environment[@variable_name] = value + 1
output << value.to_s output << value.to_s
output output
end end
end end
Template.register_tag('increment', Increment)
end end
+28
View File
@@ -0,0 +1,28 @@
# frozen_string_literal: true
module Liquid
class InlineComment < Tag
def initialize(tag_name, markup, options)
super
# Semantically, a comment should only ignore everything after it on the line.
# Currently, this implementation doesn't support mixing a comment with another tag
# but we need to reserve future support for this and prevent the introduction
# of inline comments from being backward incompatible change.
#
# As such, we're forcing users to put a # symbol on every line otherwise this
# tag will throw an error.
if markup.match?(/\n\s*[^#\s]/)
raise SyntaxError, options[:locale].t("errors.syntax.inline_comment_invalid")
end
end
def render_to_output_buffer(_context, output)
output
end
def blank?
true
end
end
end
+13 -4
View File
@@ -1,9 +1,19 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category syntax
# @liquid_name raw
# @liquid_summary
# Outputs any Liquid code as text instead of rendering it.
# @liquid_syntax
# {% raw %}
# expression
# {% endraw %}
# @liquid_syntax_keyword expression The expression to be output without being rendered.
class Raw < Block class Raw < Block
Syntax = /\A\s*\z/ Syntax = /\A\s*\z/
FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
def initialize(tag_name, markup, parse_context) def initialize(tag_name, markup, parse_context)
super super
@@ -14,7 +24,8 @@ module Liquid
def parse(tokens) def parse(tokens)
@body = +'' @body = +''
while (token = tokens.shift) while (token = tokens.shift)
if token =~ FullTokenPossiblyInvalid && block_delimiter == Regexp.last_match(2) if token =~ BlockBody::FullTokenPossiblyInvalid && block_delimiter == Regexp.last_match(2)
parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
@body << Regexp.last_match(1) if Regexp.last_match(1) != "" @body << Regexp.last_match(1) if Regexp.last_match(1) != ""
return return
end end
@@ -45,6 +56,4 @@ module Liquid
end end
end end
end end
Template.register_tag('raw', Raw)
end end
+37 -10
View File
@@ -1,13 +1,37 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category theme
# @liquid_name render
# @liquid_summary
# Renders a [snippet](/themes/architecture#snippets) or [app block](/themes/architecture/sections/section-schema#render-app-blocks).
# @liquid_description
# Inside snippets and app blocks, you can't directly access variables that are [created](/docs/api/liquid/tags/variable-tags) outside
# of the snippet or app block. However, you can [specify variables as parameters](/docs/api/liquid/tags/render#render-passing-variables-to-a-snippet)
# to pass outside variables to snippets.
#
# While you can't directly access created variables, you can access global objects, as well as any objects that are
# directly accessible outside the snippet or app block. For example, a snippet or app block inside the [product template](/themes/architecture/templates/product)
# can access the [`product` object](/docs/api/liquid/objects/product), and a snippet or app block inside a [section](/themes/architecture/sections)
# can access the [`section` object](/docs/api/liquid/objects/section).
#
# Outside a snippet or app block, you can't access variables created inside the snippet or app block.
#
# > Note:
# > When you render a snippet using the `render` tag, you can't use the [`include` tag](/docs/api/liquid/tags/include)
# > inside the snippet.
# @liquid_syntax
# {% render 'filename' %}
# @liquid_syntax_keyword filename The name of the snippet to render, without the `.liquid` extension.
class Render < Tag class Render < Tag
FOR = 'for' FOR = 'for'
SYNTAX = /(#{QuotedString}+)(\s+(with|#{FOR})\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o SYNTAX = /(#{QuotedString}+)(\s+(with|#{FOR})\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o
disable_tags "include" disable_tags "include"
attr_reader :template_name_expr, :attributes attr_reader :template_name_expr, :variable_name_expr, :attributes, :alias_name
def initialize(tag_name, markup, options) def initialize(tag_name, markup, options)
super super
@@ -21,7 +45,7 @@ module Liquid
@alias_name = Regexp.last_match(6) @alias_name = Regexp.last_match(6)
@variable_name_expr = variable_name ? parse_expression(variable_name) : nil @variable_name_expr = variable_name ? parse_expression(variable_name) : nil
@template_name_expr = parse_expression(template_name) @template_name_expr = parse_expression(template_name)
@for = (with_or_for == FOR) @is_for_loop = (with_or_for == FOR)
@attributes = {} @attributes = {}
markup.scan(TagAttributes) do |key, value| markup.scan(TagAttributes) do |key, value|
@@ -29,26 +53,30 @@ module Liquid
end end
end end
def for_loop?
@is_for_loop
end
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
render_tag(context, output) render_tag(context, output)
end end
def render_tag(context, output) def render_tag(context, output)
# Though we evaluate this here we will only ever parse it as a string literal. # The expression should be a String literal, which parses to a String object
template_name = context.evaluate(@template_name_expr) template_name = @template_name_expr
raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name raise ::ArgumentError unless template_name.is_a?(String)
partial = PartialCache.load( partial = PartialCache.load(
template_name, template_name,
context: context, context: context,
parse_context: parse_context parse_context: parse_context,
) )
context_variable_name = @alias_name || template_name.split('/').last context_variable_name = @alias_name || template_name.split('/').last
render_partial_func = ->(var, forloop) { render_partial_func = ->(var, forloop) {
inner_context = context.new_isolated_subcontext inner_context = context.new_isolated_subcontext
inner_context.template_name = template_name inner_context.template_name = partial.name
inner_context.partial = true inner_context.partial = true
inner_context['forloop'] = forloop if forloop inner_context['forloop'] = forloop if forloop
@@ -61,7 +89,7 @@ module Liquid
} }
variable = @variable_name_expr ? context.evaluate(@variable_name_expr) : nil variable = @variable_name_expr ? context.evaluate(@variable_name_expr) : nil
if @for && variable.respond_to?(:each) && variable.respond_to?(:count) if @is_for_loop && variable.respond_to?(:each) && variable.respond_to?(:count)
forloop = Liquid::ForloopDrop.new(template_name, variable.count, nil) forloop = Liquid::ForloopDrop.new(template_name, variable.count, nil)
variable.each { |var| render_partial_func.call(var, forloop) } variable.each { |var| render_partial_func.call(var, forloop) }
else else
@@ -75,10 +103,9 @@ module Liquid
def children def children
[ [
@node.template_name_expr, @node.template_name_expr,
@node.variable_name_expr,
] + @node.attributes.values ] + @node.attributes.values
end end
end end
end end
Template.register_tag('render', Render)
end end
+39 -5
View File
@@ -1,6 +1,28 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category iteration
# @liquid_name tablerow
# @liquid_summary
# Generates HTML table rows for every item in an array.
# @liquid_description
# The `tablerow` tag must be wrapped in HTML `<table>` and `</table>` tags.
#
# > Tip:
# > Every `tablerow` loop has an associated [`tablerowloop` object](/docs/api/liquid/objects/tablerowloop) with information about the loop.
# @liquid_syntax
# {% tablerow variable in array %}
# expression
# {% endtablerow %}
# @liquid_syntax_keyword variable The current item in the array.
# @liquid_syntax_keyword array The array to iterate over.
# @liquid_syntax_keyword expression The expression to render.
# @liquid_optional_param cols [number] The number of columns that the table should have.
# @liquid_optional_param limit [number] The number of iterations to perform.
# @liquid_optional_param offset [number] The 1-based index to start iterating at.
# @liquid_optional_param range [untyped] A custom numeric range to iterate over.
class TableRow < Block class TableRow < Block
Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
@@ -23,13 +45,13 @@ module Liquid
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
(collection = context.evaluate(@collection_name)) || (return '') (collection = context.evaluate(@collection_name)) || (return '')
from = @attributes.key?('offset') ? context.evaluate(@attributes['offset']).to_i : 0 from = @attributes.key?('offset') ? to_integer(context.evaluate(@attributes['offset'])) : 0
to = @attributes.key?('limit') ? from + context.evaluate(@attributes['limit']).to_i : nil to = @attributes.key?('limit') ? from + to_integer(context.evaluate(@attributes['limit'])) : nil
collection = Utils.slice_collection(collection, from, to) collection = Utils.slice_collection(collection, from, to)
length = collection.length length = collection.length
cols = context.evaluate(@attributes['cols']).to_i cols = @attributes.key?('cols') ? to_integer(context.evaluate(@attributes['cols'])) : length
output << "<tr class=\"row1\">\n" output << "<tr class=\"row1\">\n"
context.stack do context.stack do
@@ -43,6 +65,12 @@ module Liquid
super super
output << '</td>' output << '</td>'
# Handle any interrupts if they exist.
if context.interrupt?
interrupt = context.pop_interrupt
break if interrupt.is_a?(BreakInterrupt)
end
if tablerowloop.col_last && !tablerowloop.last if tablerowloop.col_last && !tablerowloop.last
output << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">" output << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">"
end end
@@ -60,7 +88,13 @@ module Liquid
super + @node.attributes.values + [@node.collection_name] super + @node.attributes.values + [@node.collection_name]
end end
end end
end
Template.register_tag('tablerow', TableRow) private
def to_integer(value)
value.to_i
rescue NoMethodError
raise Liquid::ArgumentError, "invalid integer"
end
end
end end
+25 -8
View File
@@ -3,21 +3,40 @@
require_relative 'if' require_relative 'if'
module Liquid module Liquid
# Unless is a conditional just like 'if' but works on the inverse logic. # @liquid_public_docs
# # @liquid_type tag
# {% unless x < 0 %} x is greater than zero {% endunless %} # @liquid_category conditional
# # @liquid_name unless
# @liquid_summary
# Renders an expression unless a specific condition is `true`.
# @liquid_description
# > Tip:
# > Similar to the [`if` tag](/docs/api/liquid/tags/if), you can use `elsif` to add more conditions to an `unless` tag.
# @liquid_syntax
# {% unless condition %}
# expression
# {% endunless %}
# @liquid_syntax_keyword condition The condition to evaluate.
# @liquid_syntax_keyword expression The expression to render unless the condition is met.
class Unless < If class Unless < If
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
# First condition is interpreted backwards ( if not ) # First condition is interpreted backwards ( if not )
first_block = @blocks.first first_block = @blocks.first
unless first_block.evaluate(context) result = Liquid::Utils.to_liquid_value(
first_block.evaluate(context),
)
unless result
return first_block.attachment.render_to_output_buffer(context, output) return first_block.attachment.render_to_output_buffer(context, output)
end end
# After the first condition unless works just like if # After the first condition unless works just like if
@blocks[1..-1].each do |block| @blocks[1..-1].each do |block|
if block.evaluate(context) result = Liquid::Utils.to_liquid_value(
block.evaluate(context),
)
if result
return block.attachment.render_to_output_buffer(context, output) return block.attachment.render_to_output_buffer(context, output)
end end
end end
@@ -25,6 +44,4 @@ module Liquid
output output
end end
end end
Template.register_tag('unless', Unless)
end end
+65 -63
View File
@@ -15,45 +15,9 @@ module Liquid
# template.render('user_name' => 'bob') # template.render('user_name' => 'bob')
# #
class Template class Template
attr_accessor :root attr_accessor :root, :name
attr_reader :resource_limits, :warnings attr_reader :resource_limits, :warnings
class TagRegistry
include Enumerable
def initialize
@tags = {}
@cache = {}
end
def [](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 }
end
def []=(tag_name, klass)
@tags[tag_name] = klass.name
@cache[tag_name] = klass
end
def delete(tag_name)
@tags.delete(tag_name)
@cache.delete(tag_name)
end
def each(&block)
@tags.each(&block)
end
private
def lookup_class(name)
Object.const_get(name)
end
end
attr_reader :profiler attr_reader :profiler
class << self class << self
@@ -61,52 +25,83 @@ module Liquid
# :lax acts like liquid 2.5 and silently ignores malformed tags in most cases. # :lax acts like liquid 2.5 and silently ignores malformed tags in most cases.
# :warn is the default and will give deprecation warnings when invalid syntax is used. # :warn is the default and will give deprecation warnings when invalid syntax is used.
# :strict will enforce correct syntax. # :strict will enforce correct syntax.
attr_accessor :error_mode def error_mode=(mode)
Template.error_mode = :lax Deprecations.warn("Template.error_mode=", "Environment#error_mode=")
Environment.default.error_mode = mode
attr_accessor :default_exception_renderer
Template.default_exception_renderer = lambda do |exception|
exception
end end
attr_accessor :file_system def error_mode
Template.file_system = BlankFileSystem.new Environment.default.error_mode
end
attr_accessor :tags def default_exception_renderer=(renderer)
Template.tags = TagRegistry.new Deprecations.warn("Template.default_exception_renderer=", "Environment#exception_renderer=")
private :tags= Environment.default.exception_renderer = renderer
end
def default_exception_renderer
Environment.default.exception_renderer
end
def file_system=(file_system)
Deprecations.warn("Template.file_system=", "Environment#file_system=")
Environment.default.file_system = file_system
end
def file_system
Environment.default.file_system
end
def tags
Environment.default.tags
end
def register_tag(name, klass) def register_tag(name, klass)
tags[name.to_s] = klass Deprecations.warn("Template.register_tag", "Environment#register_tag")
Environment.default.register_tag(name, klass)
end end
# Pass a module with filter methods which should be available # Pass a module with filter methods which should be available
# to all liquid views. Good for registering the standard library # to all liquid views. Good for registering the standard library
def register_filter(mod) def register_filter(mod)
StrainerFactory.add_global_filter(mod) Deprecations.warn("Template.register_filter", "Environment#register_filter")
Environment.default.register_filter(mod)
end end
attr_accessor :default_resource_limits private def default_resource_limits=(limits)
Template.default_resource_limits = {} Deprecations.warn("Template.default_resource_limits=", "Environment#default_resource_limits=")
private :default_resource_limits= Environment.default.default_resource_limits = limits
end
def default_resource_limits
Environment.default.default_resource_limits
end
# creates a new <tt>Template</tt> object from liquid source code # creates a new <tt>Template</tt> object from liquid source code
# To enable profiling, pass in <tt>profile: true</tt> as an option. # To enable profiling, pass in <tt>profile: true</tt> as an option.
# See Liquid::Profiler for more information # See Liquid::Profiler for more information
def parse(source, options = {}) def parse(source, options = {})
new.parse(source, options) environment = options[:environment] || Environment.default
new(environment: environment).parse(source, options)
end end
end end
def initialize def initialize(environment: Environment.default)
@environment = environment
@rethrow_errors = false @rethrow_errors = false
@resource_limits = ResourceLimits.new(Template.default_resource_limits) @resource_limits = ResourceLimits.new(environment.default_resource_limits)
end end
# Parse source code. # Parse source code.
# Returns self for easy chaining # Returns self for easy chaining
def parse(source, options = {}) def parse(source, options = {})
parse_context = configure_options(options) parse_context = configure_options(options)
source = source.to_s.to_str
unless source.valid_encoding?
raise TemplateEncodingError, parse_context.locale.t("errors.syntax.invalid_template_encoding")
end
tokenizer = parse_context.new_tokenizer(source, start_line_number: @line_numbers && 1) tokenizer = parse_context.new_tokenizer(source, start_line_number: @line_numbers && 1)
@root = Document.parse(tokenizer, parse_context) @root = Document.parse(tokenizer, parse_context)
self self
@@ -156,26 +151,25 @@ module Liquid
c c
when Liquid::Drop when Liquid::Drop
drop = args.shift drop = args.shift
drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits) drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits, {}, @environment)
when Hash when Hash
Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits) Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits, {}, @environment)
when nil when nil
Context.new(assigns, instance_assigns, registers, @rethrow_errors, @resource_limits) Context.new(assigns, instance_assigns, registers, @rethrow_errors, @resource_limits, {}, @environment)
else else
raise ArgumentError, "Expected Hash or Liquid::Context as parameter" raise ArgumentError, "Expected Hash or Liquid::Context as parameter"
end end
output = nil output = nil
context_register = context.registers.is_a?(StaticRegisters) ? context.registers.static : context.registers
case args.last case args.last
when Hash when Hash
options = args.pop options = args.pop
output = options[:output] if options[:output] output = options[:output] if options[:output]
static_registers = context.registers.static
options[:registers]&.each do |key, register| options[:registers]&.each do |key, register|
context_register[key] = register static_registers[key] = register
end end
apply_options_to_context(context, options) apply_options_to_context(context, options)
@@ -190,6 +184,8 @@ module Liquid
@profiler = context.profiler = Liquid::Profiler.new @profiler = context.profiler = Liquid::Profiler.new
end end
context.template_name ||= name
begin begin
# render the nodelist. # render the nodelist.
@root.render_to_output_buffer(context, output || +'') @root.render_to_output_buffer(context, output || +'')
@@ -219,7 +215,13 @@ module Liquid
@options = options @options = options
@profiling = profiling @profiling = profiling
@line_numbers = options[:line_numbers] || @profiling @line_numbers = options[:line_numbers] || @profiling
parse_context = options.is_a?(ParseContext) ? options : ParseContext.new(options) parse_context = if options.is_a?(ParseContext)
options
else
opts = options.key?(:environment) ? options : options.merge(environment: @environment)
ParseContext.new(opts)
end
@warnings = parse_context.warnings @warnings = parse_context.warnings
parse_context parse_context
end end
+126 -10
View File
@@ -1,18 +1,45 @@
# frozen_string_literal: true # frozen_string_literal: true
require "strscan"
module Liquid module Liquid
class Tokenizer class Tokenizer
attr_reader :line_number, :for_liquid_tag attr_reader :line_number, :for_liquid_tag
def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false) TAG_END = /%\}/
@source = source TAG_OR_VARIABLE_START = /\{[\{\%]/
NEWLINE = /\n/
OPEN_CURLEY = "{".ord
CLOSE_CURLEY = "}".ord
PERCENTAGE = "%".ord
def initialize(
source:,
string_scanner:,
line_numbers: false,
line_number: nil,
for_liquid_tag: false
)
@line_number = line_number || (line_numbers ? 1 : nil) @line_number = line_number || (line_numbers ? 1 : nil)
@for_liquid_tag = for_liquid_tag @for_liquid_tag = for_liquid_tag
@tokens = tokenize @source = source.to_s.to_str
@offset = 0
@tokens = []
if @source
@ss = string_scanner
@ss.string = @source
tokenize
end
end end
def shift def shift
(token = @tokens.shift) || return token = @tokens[@offset]
return unless token
@offset += 1
if @line_number if @line_number
@line_number += @for_liquid_tag ? 1 : token.count("\n") @line_number += @for_liquid_tag ? 1 : token.count("\n")
@@ -24,16 +51,105 @@ module Liquid
private private
def tokenize def tokenize
return [] if @source.to_s.empty? if @for_liquid_tag
@tokens = @source.split("\n")
else
@tokens << shift_normal until @ss.eos?
end
return @source.split("\n") if @for_liquid_tag @source = nil
@ss = nil
end
tokens = @source.split(TemplateParser) def shift_normal
token = next_token
# removes the rogue empty element at the beginning of the array return unless token
tokens.shift if tokens[0]&.empty?
tokens token
end
def next_token
# possible states: :text, :tag, :variable
byte_a = @ss.peek_byte
if byte_a == OPEN_CURLEY
@ss.scan_byte
byte_b = @ss.peek_byte
if byte_b == PERCENTAGE
@ss.scan_byte
return next_tag_token
elsif byte_b == OPEN_CURLEY
@ss.scan_byte
return next_variable_token
end
@ss.pos -= 1
end
next_text_token
end
def next_text_token
start = @ss.pos
unless @ss.skip_until(TAG_OR_VARIABLE_START)
token = @ss.rest
@ss.terminate
return token
end
pos = @ss.pos -= 2
@source.byteslice(start, pos - start)
end
def next_variable_token
start = @ss.pos - 2
byte_a = byte_b = @ss.scan_byte
while byte_b
byte_a = @ss.scan_byte while byte_a && (byte_a != CLOSE_CURLEY && byte_a != OPEN_CURLEY)
break unless byte_a
if @ss.eos?
return byte_a == CLOSE_CURLEY ? @source.byteslice(start, @ss.pos - start) : "{{"
end
byte_b = @ss.scan_byte
if byte_a == CLOSE_CURLEY
if byte_b == CLOSE_CURLEY
return @source.byteslice(start, @ss.pos - start)
elsif byte_b != CLOSE_CURLEY
@ss.pos -= 1
return @source.byteslice(start, @ss.pos - start)
end
elsif byte_a == OPEN_CURLEY && byte_b == PERCENTAGE
return next_tag_token_with_start(start)
end
byte_a = byte_b
end
"{{"
end
def next_tag_token
start = @ss.pos - 2
if (len = @ss.skip_until(TAG_END))
@source.byteslice(start, len + 2)
else
"{%"
end
end
def next_tag_token_with_start(start)
@ss.skip_until(TAG_END)
@source.byteslice(start, @ss.pos - start)
end end
end end
end end
+93
View File
@@ -81,5 +81,98 @@ module Liquid
rescue ::ArgumentError rescue ::ArgumentError
nil nil
end end
def self.to_liquid_value(obj)
# Enable "obj" to represent itself as a primitive value like integer, string, or boolean
return obj.to_liquid_value if obj.respond_to?(:to_liquid_value)
# Otherwise return the object itself
obj
end
# if RUBY_VERSION >= '3.4'
def self.to_s(obj, seen = {})
case obj
when Hash
hash_inspect(obj, seen)
when Array
array_inspect(obj, seen)
else
obj.to_s
end
end
def self.inspect(obj, seen = {})
case obj
when Hash
hash_inspect(obj, seen)
when Array
array_inspect(obj, seen)
else
obj.inspect
end
end
# else
# def self.to_s(obj, seen = nil)
# obj.to_s
# end
# def self.inspect(obj, seen = nil)
# obj.inspect
# end
# end
def self.array_inspect(arr, seen = {})
if seen[arr.object_id]
return "[...]"
end
seen[arr.object_id] = true
str = +"["
cursor = 0
len = arr.length
while cursor < len
if cursor > 0
str << ", "
end
item_str = inspect(arr[cursor], seen)
str << item_str
end
str << "]"
str
ensure
seen.delete(arr.object_id)
end
def self.hash_inspect(hash, seen = {})
if seen[hash.object_id]
return "{...}"
end
seen[hash.object_id] = true
str = +"{"
first = true
hash.each do |key, value|
if first
first = false
else
str << ", "
end
key_str = inspect(key, seen)
str << key_str
str << "=>"
value_str = inspect(value, seen)
str << value_str
end
str << "}"
str
ensure
seen.delete(hash.object_id)
end
end end
end end
+19 -13
View File
@@ -47,7 +47,7 @@ module Liquid
name_markup = Regexp.last_match(1) name_markup = Regexp.last_match(1)
filter_markup = Regexp.last_match(2) filter_markup = Regexp.last_match(2)
@name = Expression.parse(name_markup) @name = parse_context.parse_expression(name_markup)
if filter_markup =~ FilterMarkupRegex if filter_markup =~ FilterMarkupRegex
filters = Regexp.last_match(1).scan(FilterParser) filters = Regexp.last_match(1).scan(FilterParser)
filters.each do |f| filters.each do |f|
@@ -61,14 +61,14 @@ module Liquid
def strict_parse(markup) def strict_parse(markup)
@filters = [] @filters = []
p = Parser.new(markup) p = @parse_context.new_parser(markup)
return if p.look(:end_of_string) return if p.look(:end_of_string)
@name = Expression.parse(p.expression) @name = parse_context.parse_expression(p.expression)
while p.consume?(:pipe) while p.consume?(:pipe)
filtername = p.consume(:id) filtername = p.consume(:id)
filterargs = p.consume?(:colon) ? parse_filterargs(p) : [] filterargs = p.consume?(:colon) ? parse_filterargs(p) : Const::EMPTY_ARRAY
@filters << parse_filter_expressions(filtername, filterargs) @filters << parse_filter_expressions(filtername, filterargs)
end end
p.consume(:end_of_string) p.consume(:end_of_string)
@@ -95,15 +95,21 @@ module Liquid
def render_to_output_buffer(context, output) def render_to_output_buffer(context, output)
obj = render(context) obj = render(context)
render_obj_to_output(obj, output)
if obj.is_a?(Array) output
output << obj.join
elsif obj.nil?
else
output << obj.to_s
end end
output def render_obj_to_output(obj, output)
case obj
when NilClass
# Do nothing
when Array
obj.each do |o|
render_obj_to_output(o, output)
end
else
output << Liquid::Utils.to_s(obj)
end
end end
def disabled?(_context) def disabled?(_context)
@@ -122,9 +128,9 @@ module Liquid
unparsed_args.each do |a| unparsed_args.each do |a|
if (matches = a.match(JustTagAttributes)) if (matches = a.match(JustTagAttributes))
keyword_args ||= {} keyword_args ||= {}
keyword_args[matches[1]] = Expression.parse(matches[2]) keyword_args[matches[1]] = parse_context.parse_expression(matches[2])
else else
filter_args << Expression.parse(a) filter_args << parse_context.parse_expression(a)
end end
end end
result = [filter_name, filter_args] result = [filter_name, filter_args]
+23 -9
View File
@@ -2,21 +2,24 @@
module Liquid module Liquid
class VariableLookup class VariableLookup
SQUARE_BRACKETED = /\A\[(.*)\]\z/m
COMMAND_METHODS = ['size', 'first', 'last'].freeze COMMAND_METHODS = ['size', 'first', 'last'].freeze
attr_reader :name, :lookups attr_reader :name, :lookups
def self.parse(markup) def self.parse(markup, string_scanner = StringScanner.new(""), cache = nil)
new(markup) new(markup, string_scanner, cache)
end end
def initialize(markup) def initialize(markup, string_scanner = StringScanner.new(""), cache = nil)
lookups = markup.scan(VariableParser) lookups = markup.scan(VariableParser)
name = lookups.shift name = lookups.shift
if name =~ SQUARE_BRACKETED if name&.start_with?('[') && name&.end_with?(']')
name = Expression.parse(Regexp.last_match(1)) name = Expression.parse(
name[1..-2],
string_scanner,
cache,
)
end end
@name = name @name = name
@@ -25,14 +28,22 @@ module Liquid
@lookups.each_index do |i| @lookups.each_index do |i|
lookup = lookups[i] lookup = lookups[i]
if lookup =~ SQUARE_BRACKETED if lookup&.start_with?('[') && lookup&.end_with?(']')
lookups[i] = Expression.parse(Regexp.last_match(1)) lookups[i] = Expression.parse(
lookup[1..-2],
string_scanner,
cache,
)
elsif COMMAND_METHODS.include?(lookup) elsif COMMAND_METHODS.include?(lookup)
@command_flags |= 1 << i @command_flags |= 1 << i
end end
end end
end end
def lookup_command?(lookup_index)
@command_flags & (1 << lookup_index) != 0
end
def evaluate(context) def evaluate(context)
name = context.evaluate(@name) name = context.evaluate(@name)
object = context.find_variable(name) object = context.find_variable(name)
@@ -40,6 +51,9 @@ module Liquid
@lookups.each_index do |i| @lookups.each_index do |i|
key = context.evaluate(@lookups[i]) key = context.evaluate(@lookups[i])
# Cast "key" to its liquid value to enable it to act as a primitive value
key = Liquid::Utils.to_liquid_value(key)
# If object is a hash- or array-like object we look for the # If object is a hash- or array-like object we look for the
# presence of the key and if its available we return it # presence of the key and if its available we return it
if object.respond_to?(:[]) && if object.respond_to?(:[]) &&
@@ -53,7 +67,7 @@ module Liquid
# Some special cases. If the part wasn't in square brackets and # Some special cases. If the part wasn't in square brackets and
# no key with the same name was found we interpret following calls # no key with the same name was found we interpret following calls
# as commands and call them on the current object # as commands and call them on the current object
elsif @command_flags & (1 << i) != 0 && object.respond_to?(key) elsif lookup_command?(i) && object.respond_to?(key)
object = object.send(key).to_liquid object = object.send(key).to_liquid
# No key was present with the desired value and it wasn't one of the directly supported # No key was present with the desired value and it wasn't one of the directly supported
+1 -1
View File
@@ -2,5 +2,5 @@
# frozen_string_literal: true # frozen_string_literal: true
module Liquid module Liquid
VERSION = "5.0.1" VERSION = "5.6.4"
end end
+5 -3
View File
@@ -13,22 +13,24 @@ Gem::Specification.new do |s|
s.summary = "A secure, non-evaling end user template engine with aesthetic markup." s.summary = "A secure, non-evaling end user template engine with aesthetic markup."
s.authors = ["Tobias Lütke"] s.authors = ["Tobias Lütke"]
s.email = ["[email protected]"] s.email = ["[email protected]"]
s.homepage = "http://www.liquidmarkup.org" s.homepage = "https://shopify.github.io/liquid/"
s.license = "MIT" s.license = "MIT"
# s.description = "A secure, non-evaling end user template engine with aesthetic markup." # s.description = "A secure, non-evaling end user template engine with aesthetic markup."
s.required_ruby_version = ">= 2.5.0" s.required_ruby_version = ">= 3.0.0"
s.required_rubygems_version = ">= 1.3.7" s.required_rubygems_version = ">= 1.3.7"
s.metadata['allowed_push_host'] = 'https://rubygems.org' s.metadata['allowed_push_host'] = 'https://rubygems.org'
s.test_files = Dir.glob("{test}/**/*")
s.files = Dir.glob("{lib}/**/*") + %w(LICENSE README.md) s.files = Dir.glob("{lib}/**/*") + %w(LICENSE README.md)
s.extra_rdoc_files = ["History.md", "README.md"] s.extra_rdoc_files = ["History.md", "README.md"]
s.require_path = "lib" s.require_path = "lib"
s.add_dependency("strscan", ">= 3.1.1")
s.add_dependency("bigdecimal")
s.add_development_dependency('rake', '~> 13.0') s.add_development_dependency('rake', '~> 13.0')
s.add_development_dependency('minitest') s.add_development_dependency('minitest')
end end
+11 -6
View File
@@ -3,18 +3,23 @@
require 'benchmark/ips' require 'benchmark/ips'
require_relative 'theme_runner' require_relative 'theme_runner'
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first RubyVM::YJIT.enable if defined?(RubyVM::YJIT)
Liquid::Environment.default.error_mode = ARGV.first.to_sym if ARGV.first
profiler = ThemeRunner.new profiler = ThemeRunner.new
Benchmark.ips do |x| Benchmark.ips do |x|
x.time = 10 x.time = 20
x.warmup = 5 x.warmup = 10
puts puts
puts "Running benchmark for #{x.time} seconds (with #{x.warmup} seconds warmup)." puts "Running benchmark for #{x.time} seconds (with #{x.warmup} seconds warmup)."
puts puts
x.report("parse:") { profiler.compile } phase = ENV["PHASE"] || "all"
x.report("render:") { profiler.render }
x.report("parse & render:") { profiler.run } x.report("tokenize:") { profiler.tokenize } if phase == "all" || phase == "tokenize"
x.report("parse:") { profiler.compile } if phase == "all" || phase == "parse"
x.report("render:") { profiler.render } if phase == "all" || phase == "render"
x.report("parse & render:") { profiler.run } if phase == "all" || phase == "run"
end end
+9 -6
View File
@@ -3,11 +3,19 @@
require 'yaml' require 'yaml'
module Database module Database
DATABASE_FILE_PATH = "#{__dir__}/vision.database.yml"
# Load the standard vision toolkit database and re-arrage it to be simply exportable # Load the standard vision toolkit database and re-arrage it to be simply exportable
# to liquid as assigns. All this is based on Shopify # to liquid as assigns. All this is based on Shopify
def self.tables def self.tables
@tables ||= begin @tables ||= begin
db = YAML.load_file("#{__dir__}/vision.database.yml") db =
if YAML.respond_to?(:unsafe_load_file) # Only Psych 4+ can use unsafe_load_file
# unsafe_load_file is needed for YAML references
YAML.unsafe_load_file(DATABASE_FILE_PATH)
else
YAML.load_file(DATABASE_FILE_PATH)
end
# From vision source # From vision source
db['products'].each do |product| db['products'].each do |product|
@@ -41,8 +49,3 @@ module Database
end end
end end
end end
if __FILE__ == $PROGRAM_NAME
p(Database.tables['collections']['frontpage'].keys)
# p Database.tables['blog']['articles']
end
+8 -7
View File
@@ -11,11 +11,12 @@ require_relative 'shop_filter'
require_relative 'tag_filter' require_relative 'tag_filter'
require_relative 'weight_filter' require_relative 'weight_filter'
Liquid::Template.register_tag('paginate', Paginate) default_environment = Liquid::Environment.default
Liquid::Template.register_tag('form', CommentForm) default_environment.register_tag('paginate', Paginate)
default_environment.register_tag('form', CommentForm)
Liquid::Template.register_filter(JsonFilter) default_environment.register_filter(JsonFilter)
Liquid::Template.register_filter(MoneyFilter) default_environment.register_filter(MoneyFilter)
Liquid::Template.register_filter(WeightFilter) default_environment.register_filter(WeightFilter)
Liquid::Template.register_filter(ShopFilter) default_environment.register_filter(ShopFilter)
Liquid::Template.register_filter(TagFilter) default_environment.register_filter(TagFilter)
+2 -2
View File
@@ -45,8 +45,8 @@ class Paginate < Liquid::Block
pagination['items'] = collection_size pagination['items'] = collection_size
pagination['pages'] = page_count - 1 pagination['pages'] = page_count - 1
pagination['previous'] = link('&laquo; Previous', current_page - 1) unless 1 >= current_page pagination['previous'] = link('&laquo; Previous', current_page - 1) if 1 < current_page
pagination['next'] = link('Next &raquo;', current_page + 1) unless page_count <= current_page + 1 pagination['next'] = link('Next &raquo;', current_page + 1) if page_count > current_page + 1
pagination['parts'] = [] pagination['parts'] = []
hellip_break = false hellip_break = false
+14 -1
View File
@@ -3,7 +3,7 @@
# This profiler run simulates Shopify. # This profiler run simulates Shopify.
# We are looking in the tests directory for liquid files and render them within the designated layout file. # We are looking in the tests directory for liquid files and render them within the designated layout file.
# We will also export a substantial database to liquid which the templates can render values of. # We will also export a substantial database to liquid which the templates can render values of.
# All this is to make the benchmark as non syntetic as possible. All templates and tests are lifted from # All this is to make the benchmark as non synthetic as possible. All templates and tests are lifted from
# direct real-world usage and the profiler measures code that looks very similar to the way it looks in # direct real-world usage and the profiler measures code that looks very similar to the way it looks in
# Shopify which is likely the biggest user of liquid in the world which something to the tune of several # 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. # million Template#render calls a day.
@@ -48,6 +48,19 @@ class ThemeRunner
end end
end end
# `tokenize` will just test the tokenizen portion of liquid without any templates
def tokenize
ss = StringScanner.new("")
@tests.each do |test_hash|
tokenizer = Liquid::Tokenizer.new(
source: test_hash[:liquid],
string_scanner: ss,
line_numbers: true,
)
while tokenizer.shift; end
end
end
# `run` is called to benchmark rendering and compiling at the same time # `run` is called to benchmark rendering and compiling at the same time
def run def run
each_test do |liquid, layout, assigns, page_template, template_name| each_test do |liquid, layout, assigns, page_template, template_name|
+94
View File
@@ -0,0 +1,94 @@
# frozen_string_literal: true
require "benchmark/ips"
# benchmark liquid lexing
require 'liquid'
RubyVM::YJIT.enable
STRING_MARKUPS = [
"\"foo\"",
"\"fooooooooooo\"",
"\"foooooooooooooooooooooooooooooo\"",
"'foo'",
"'fooooooooooo'",
"'foooooooooooooooooooooooooooooo'",
]
VARIABLE_MARKUPS = [
"article",
"article.title",
"article.title.size",
"very_long_variable_name_2024_11_05",
"very_long_variable_name_2024_11_05.size",
]
NUMBER_MARKUPS = [
"0",
"35",
"1241891024912849",
"3.5",
"3.51214128409128",
"12381902839.123819283910283",
"123.456.789",
"-123",
"-12.33",
"-405.231",
"-0",
"0",
"0.0",
"0.0000000000000000000000",
"0.00000000001",
]
RANGE_MARKUPS = [
"(1..30)",
"(1...30)",
"(1..30..5)",
"(1.0...30.0)",
"(1.........30)",
"(1..foo)",
"(foo..30)",
"(foo..bar)",
"(foo...bar...100)",
"(foo...bar...100.0)",
]
LITERAL_MARKUPS = [
nil,
'nil',
'null',
'',
'true',
'false',
'blank',
'empty',
]
MARKUPS = {
"string" => STRING_MARKUPS,
"literal" => LITERAL_MARKUPS,
"variable" => VARIABLE_MARKUPS,
"number" => NUMBER_MARKUPS,
"range" => RANGE_MARKUPS,
}
Benchmark.ips do |x|
x.config(time: 5, warmup: 5)
MARKUPS.each do |type, markups|
x.report("Liquid::Expression#parse: #{type}") do
markups.each do |markup|
Liquid::Expression.parse(markup)
end
end
end
x.report("Liquid::Expression#parse: all") do
MARKUPS.values.flatten.each do |markup|
Liquid::Expression.parse(markup)
end
end
end
+43
View File
@@ -0,0 +1,43 @@
# frozen_string_literal: true
require "benchmark/ips"
# benchmark liquid lexing
require 'liquid'
RubyVM::YJIT.enable
EXPRESSIONS = [
"foo[1..2].baz",
"12.0",
"foo.bar.based",
"21 - 62",
"foo.bar.baz",
"foo > 12",
"foo < 12",
"foo <= 12",
"foo >= 12",
"foo <> 12",
"foo == 12",
"foo != 12",
"foo contains 12",
"foo contains 'bar'",
"foo != 'bar'",
"'foo' contains 'bar'",
'234089',
"foo | default: -1",
]
Benchmark.ips do |x|
x.config(time: 10, warmup: 5)
x.report("Liquid::Lexer#tokenize") do
EXPRESSIONS.each do |expr|
l = Liquid::Lexer.new(expr)
l.tokenize
end
end
x.compare!
end
+24 -24
View File
@@ -6,51 +6,51 @@ class AssignTest < Minitest::Test
include Liquid include Liquid
def test_assign_with_hyphen_in_variable_name def test_assign_with_hyphen_in_variable_name
template_source = <<-END_TEMPLATE template_source = <<~END_TEMPLATE
{% assign this-thing = 'Print this-thing' %} {% assign this-thing = 'Print this-thing' -%}
{{ this-thing }} {{ this-thing -}}
END_TEMPLATE END_TEMPLATE
template = Template.parse(template_source) assert_template_result("Print this-thing", template_source)
rendered = template.render!
assert_equal("Print this-thing", rendered.strip)
end end
def test_assigned_variable def test_assigned_variable
assert_template_result('.foo.', assert_template_result(
'.foo.',
'{% assign foo = values %}.{{ foo[0] }}.', '{% assign foo = values %}.{{ foo[0] }}.',
'values' => %w(foo bar baz)) { 'values' => %w(foo bar baz) },
)
assert_template_result('.bar.', assert_template_result(
'.bar.',
'{% assign foo = values %}.{{ foo[1] }}.', '{% assign foo = values %}.{{ foo[1] }}.',
'values' => %w(foo bar baz)) { 'values' => %w(foo bar baz) },
)
end end
def test_assign_with_filter def test_assign_with_filter
assert_template_result('.bar.', assert_template_result(
'.bar.',
'{% assign foo = values | split: "," %}.{{ foo[1] }}.', '{% assign foo = values | split: "," %}.{{ foo[1] }}.',
'values' => "foo,bar,baz") { 'values' => "foo,bar,baz" },
)
end end
def test_assign_syntax_error def test_assign_syntax_error
assert_match_syntax_error(/assign/, assert_match_syntax_error(/assign/, '{% assign foo not values %}.')
'{% assign foo not values %}.',
'values' => "foo,bar,baz")
end end
def test_assign_uses_error_mode def test_assign_uses_error_mode
with_error_mode(:strict) do assert_match_syntax_error(
assert_raises(SyntaxError) do "Expected dotdot but found pipe in ",
Template.parse("{% assign foo = ('X' | downcase) %}") "{% assign foo = ('X' | downcase) %}",
end error_mode: :strict,
end )
with_error_mode(:lax) do assert_template_result("", "{% assign foo = ('X' | downcase) %}", error_mode: :lax)
assert(Template.parse("{% assign foo = ('X' | downcase) %}"))
end
end end
def test_expression_with_whitespace_in_square_brackets def test_expression_with_whitespace_in_square_brackets
source = "{% assign r = a[ 'b' ] %}{{ r }}" source = "{% assign r = a[ 'b' ] %}{{ r }}"
assert_template_result('result', source, 'a' => { 'b' => 'result' }) assert_template_result('result', source, { 'a' => { 'b' => 'result' } })
end end
def test_assign_score_exceeding_resource_limit def test_assign_score_exceeding_resource_limit
+20 -13
View File
@@ -9,12 +9,6 @@ class FoobarTag < Liquid::Tag
end end
end end
class BlankTestFileSystem
def read_template_file(template_path)
template_path
end
end
class BlankTest < Minitest::Test class BlankTest < Minitest::Test
include Liquid include Liquid
N = 10 N = 10
@@ -33,7 +27,7 @@ class BlankTest < Minitest::Test
def test_new_tags_are_not_blank_by_default def test_new_tags_are_not_blank_by_default
with_custom_tag('foobar', FoobarTag) do with_custom_tag('foobar', FoobarTag) do
assert_template_result(" " * N, wrap_in_for("{% foobar %}")) assert_equal(" " * N, Liquid::Template.parse(wrap_in_for("{% foobar %}")).render!)
end end
end end
@@ -63,9 +57,11 @@ class BlankTest < Minitest::Test
def test_nested_blocks_are_blank_but_only_if_all_children_are def test_nested_blocks_are_blank_but_only_if_all_children_are
assert_template_result("", wrap(wrap(" "))) assert_template_result("", wrap(wrap(" ")))
assert_template_result("\n but this is not " * (N + 1), assert_template_result(
"\n but this is not " * (N + 1),
wrap('{% if true %} {% comment %} this is blank {% endcomment %} {% endif %} wrap('{% if true %} {% comment %} this is blank {% endcomment %} {% endif %}
{% if true %} but this is not {% endif %}')) {% if true %} but this is not {% endif %}'),
)
end end
def test_assigns_are_blank def test_assigns_are_blank
@@ -95,10 +91,21 @@ class BlankTest < Minitest::Test
end end
def test_include_is_blank def test_include_is_blank
Liquid::Template.file_system = BlankTestFileSystem.new assert_template_result(
assert_template_result("foobar" * (N + 1), wrap("{% include 'foobar' %}")) "foobar" * (N + 1),
assert_template_result(" foobar " * (N + 1), wrap("{% include ' foobar ' %}")) wrap("{% include 'foobar' %}"),
assert_template_result(" " * (N + 1), wrap(" {% include ' ' %} ")) partials: { 'foobar' => 'foobar' },
)
assert_template_result(
" foobar " * (N + 1),
wrap("{% include ' foobar ' %}"),
partials: { ' foobar ' => ' foobar ' },
)
assert_template_result(
" " * (N + 1),
wrap(" {% include ' ' %} "),
partials: { ' ' => ' ' },
)
end end
def test_case_is_blank def test_case_is_blank
+2 -4
View File
@@ -6,10 +6,8 @@ class BlockTest < Minitest::Test
include Liquid include Liquid
def test_unexpected_end_tag def test_unexpected_end_tag
exc = assert_raises(SyntaxError) do source = '{% if true %}{% endunless %}'
Template.parse("{% if true %}{% endunless %}") assert_match_syntax_error("Liquid syntax error (line 1): 'endunless' is not a valid delimiter for if tags. use endif", source)
end
assert_equal(exc.message, "Liquid syntax error: 'endunless' is not a valid delimiter for if tags. use endif")
end end
def test_with_custom_tag def test_with_custom_tag
+23 -29
View File
@@ -10,44 +10,38 @@ class CaptureTest < Minitest::Test
end end
def test_capture_with_hyphen_in_variable_name def test_capture_with_hyphen_in_variable_name
template_source = <<-END_TEMPLATE template_source = <<~END_TEMPLATE
{% capture this-thing %}Print this-thing{% endcapture %} {% capture this-thing %}Print this-thing{% endcapture -%}
{{ this-thing }} {{ this-thing -}}
END_TEMPLATE END_TEMPLATE
template = Template.parse(template_source) assert_template_result("Print this-thing", template_source)
rendered = template.render!
assert_equal("Print this-thing", rendered.strip)
end end
def test_capture_to_variable_from_outer_scope_if_existing def test_capture_to_variable_from_outer_scope_if_existing
template_source = <<-END_TEMPLATE template_source = <<~END_TEMPLATE
{% assign var = '' %} {% assign var = '' -%}
{% if true %} {% if true -%}
{% capture var %}first-block-string{% endcapture %} {% capture var %}first-block-string{% endcapture -%}
{% endif %} {% endif -%}
{% if true %} {% if true -%}
{% capture var %}test-string{% endcapture %} {% capture var %}test-string{% endcapture -%}
{% endif %} {% endif -%}
{{var}} {{var-}}
END_TEMPLATE END_TEMPLATE
template = Template.parse(template_source) assert_template_result("test-string", template_source)
rendered = template.render!
assert_equal("test-string", rendered.gsub(/\s/, ''))
end end
def test_assigning_from_capture def test_assigning_from_capture
template_source = <<-END_TEMPLATE template_source = <<~END_TEMPLATE
{% assign first = '' %} {% assign first = '' -%}
{% assign second = '' %} {% assign second = '' -%}
{% for number in (1..3) %} {% for number in (1..3) -%}
{% capture first %}{{number}}{% endcapture %} {% capture first %}{{number}}{% endcapture -%}
{% assign second = first %} {% assign second = first -%}
{% endfor %} {% endfor -%}
{{ first }}-{{ second }} {{ first }}-{{ second -}}
END_TEMPLATE END_TEMPLATE
template = Template.parse(template_source) assert_template_result("3-3", template_source)
rendered = template.render!
assert_equal("3-3", rendered.gsub(/\s/, ''))
end end
def test_increment_assign_score_by_bytes_not_characters def test_increment_assign_score_by_bytes_not_characters
+165 -98
View File
@@ -24,7 +24,7 @@ class ContextSensitiveDrop < Liquid::Drop
end end
end end
class Category < Liquid::Drop class Category
attr_accessor :name attr_accessor :name
def initialize(name) def initialize(name)
@@ -36,8 +36,27 @@ class Category < Liquid::Drop
end end
end end
class CategoryDrop class ProductsDrop < Liquid::Drop
def initialize(products)
@products = products
end
def size
@products.size
end
def to_liquid
if @context["forloop"]
@products.first(@context["forloop"].length)
else
@products
end
end
end
class CategoryDrop < Liquid::Drop
attr_accessor :category, :context attr_accessor :category, :context
def initialize(category) def initialize(category)
@category = category @category = category
end end
@@ -101,7 +120,7 @@ class ContextTest < Minitest::Test
end end
def test_variables_not_existing def test_variables_not_existing
assert_nil(@context['does_not_exist']) assert_template_result("true", "{% if does_not_exist == nil %}true{% endif %}")
end end
def test_scoping def test_scoping
@@ -120,22 +139,27 @@ class ContextTest < Minitest::Test
end end
def test_length_query def test_length_query
@context['numbers'] = [1, 2, 3, 4] assert_template_result(
"true",
"{% if numbers.size == 4 %}true{% endif %}",
{ "numbers" => [1, 2, 3, 4] },
)
assert_equal(4, @context['numbers.size']) assert_template_result(
"true",
"{% if numbers.size == 4 %}true{% endif %}",
{ "numbers" => { 1 => 1, 2 => 2, 3 => 3, 4 => 4 } },
)
@context['numbers'] = { 1 => 1, 2 => 2, 3 => 3, 4 => 4 } assert_template_result(
"true",
assert_equal(4, @context['numbers.size']) "{% if numbers.size == 1000 %}true{% endif %}",
{ "numbers" => { 1 => 1, 2 => 2, 3 => 3, 4 => 4, 'size' => 1000 } },
@context['numbers'] = { 1 => 1, 2 => 2, 3 => 3, 4 => 4, 'size' => 1000 } )
assert_equal(1000, @context['numbers.size'])
end end
def test_hyphenated_variable def test_hyphenated_variable
@context['oh-my'] = 'godz' assert_template_result("godz", "{{ oh-my }}", { "oh-my" => 'godz' })
assert_equal('godz', @context['oh-my'])
end end
def test_add_filter def test_add_filter
@@ -187,24 +211,24 @@ class ContextTest < Minitest::Test
end end
def test_hierachical_data def test_hierachical_data
@context['hash'] = { "name" => 'tobi' } assigns = { 'hash' => { "name" => 'tobi' } }
assert_equal('tobi', @context['hash.name']) assert_template_result("tobi", "{{ hash.name }}", assigns)
assert_equal('tobi', @context['hash["name"]']) assert_template_result("tobi", '{{ hash["name"] }}', assigns)
end end
def test_keywords def test_keywords
assert_equal(true, @context['true']) assert_template_result("pass", "{% if true == expect %}pass{% endif %}", { "expect" => true })
assert_equal(false, @context['false']) assert_template_result("pass", "{% if false == expect %}pass{% endif %}", { "expect" => false })
end end
def test_digits def test_digits
assert_equal(100, @context['100']) assert_template_result("pass", "{% if 100 == expect %}pass{% endif %}", { "expect" => 100 })
assert_equal(100.00, @context['100.00']) assert_template_result("pass", "{% if 100.00 == expect %}pass{% endif %}", { "expect" => 100.00 })
end end
def test_strings def test_strings
assert_equal("hello!", @context['"hello!"']) assert_template_result("hello!", '{{ "hello!" }}')
assert_equal("hello!", @context["'hello!'"]) assert_template_result("hello!", "{{ 'hello!' }}")
end end
def test_merge def test_merge
@@ -216,99 +240,92 @@ class ContextTest < Minitest::Test
end end
def test_array_notation def test_array_notation
@context['test'] = [1, 2, 3, 4, 5] assigns = { "test" => ["a", "b"] }
assert_template_result("a", "{{ test[0] }}", assigns)
assert_equal(1, @context['test[0]']) assert_template_result("b", "{{ test[1] }}", assigns)
assert_equal(2, @context['test[1]']) assert_template_result("pass", "{% if test[2] == nil %}pass{% endif %}", assigns)
assert_equal(3, @context['test[2]'])
assert_equal(4, @context['test[3]'])
assert_equal(5, @context['test[4]'])
end end
def test_recoursive_array_notation def test_recoursive_array_notation
@context['test'] = { 'test' => [1, 2, 3, 4, 5] } assigns = { "test" => { 'test' => [1, 2, 3, 4, 5] } }
assert_template_result("1", "{{ test.test[0] }}", assigns)
assert_equal(1, @context['test.test[0]']) assigns = { "test" => [{ 'test' => 'worked' }] }
assert_template_result("worked", "{{ test[0].test }}", assigns)
@context['test'] = [{ 'test' => 'worked' }]
assert_equal('worked', @context['test[0].test'])
end end
def test_hash_to_array_transition def test_hash_to_array_transition
@context['colors'] = { assigns = {
'colors' => {
'Blue' => ['003366', '336699', '6699CC', '99CCFF'], 'Blue' => ['003366', '336699', '6699CC', '99CCFF'],
'Green' => ['003300', '336633', '669966', '99CC99'], 'Green' => ['003300', '336633', '669966', '99CC99'],
'Yellow' => ['CC9900', 'FFCC00', 'FFFF99', 'FFFFCC'], 'Yellow' => ['CC9900', 'FFCC00', 'FFFF99', 'FFFFCC'],
'Red' => ['660000', '993333', 'CC6666', 'FF9999'], 'Red' => ['660000', '993333', 'CC6666', 'FF9999'],
},
} }
assert_equal('003366', @context['colors.Blue[0]']) assert_template_result("003366", "{{ colors.Blue[0] }}", assigns)
assert_equal('FF9999', @context['colors.Red[3]']) assert_template_result("FF9999", "{{ colors.Red[3] }}", assigns)
end end
def test_try_first def test_try_first
@context['test'] = [1, 2, 3, 4, 5] assigns = { 'test' => [1, 2, 3, 4, 5] }
assert_template_result("1", "{{ test.first }}", assigns)
assert_template_result("pass", "{% if test.last == 5 %}pass{% endif %}", assigns)
assert_equal(1, @context['test.first']) assigns = { "test" => { "test" => [1, 2, 3, 4, 5] } }
assert_equal(5, @context['test.last']) assert_template_result("1", "{{ test.test.first }}", assigns)
assert_template_result("5", "{{ test.test.last }}", assigns)
@context['test'] = { 'test' => [1, 2, 3, 4, 5] } assigns = { "test" => [1] }
assert_template_result("1", "{{ test.first }}", assigns)
assert_equal(1, @context['test.test.first']) assert_template_result("1", "{{ test.last }}", assigns)
assert_equal(5, @context['test.test.last'])
@context['test'] = [1]
assert_equal(1, @context['test.first'])
assert_equal(1, @context['test.last'])
end end
def test_access_hashes_with_hash_notation def test_access_hashes_with_hash_notation
@context['products'] = { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] } assigns = { 'products' => { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] } }
@context['product'] = { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] } assert_template_result("5", '{{ products["count"] }}', assigns)
assert_template_result("deepsnow", '{{ products["tags"][0] }}', assigns)
assert_template_result("deepsnow", '{{ products["tags"].first }}', assigns)
assert_equal(5, @context['products["count"]']) assigns = { 'product' => { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] } }
assert_equal('deepsnow', @context['products["tags"][0]']) assert_template_result("draft151cm", '{{ product["variants"][0]["title"] }}', assigns)
assert_equal('deepsnow', @context['products["tags"].first']) assert_template_result("element151cm", '{{ product["variants"][1]["title"] }}', assigns)
assert_equal('draft151cm', @context['product["variants"][0]["title"]']) assert_template_result("draft151cm", '{{ product["variants"].first["title"] }}', assigns)
assert_equal('element151cm', @context['product["variants"][1]["title"]']) assert_template_result("element151cm", '{{ product["variants"].last["title"] }}', assigns)
assert_equal('draft151cm', @context['product["variants"][0]["title"]'])
assert_equal('element151cm', @context['product["variants"].last["title"]'])
end end
def test_access_variable_with_hash_notation def test_access_variable_with_hash_notation
@context['foo'] = 'baz' assert_template_result('baz', '{{ ["foo"] }}', { "foo" => "baz" })
@context['bar'] = 'foo' assert_template_result('baz', '{{ [bar] }}', { 'foo' => 'baz', 'bar' => 'foo' })
assert_equal('baz', @context['["foo"]'])
assert_equal('baz', @context['[bar]'])
end end
def test_access_hashes_with_hash_access_variables def test_access_hashes_with_hash_access_variables
@context['var'] = 'tags' assigns = {
@context['nested'] = { 'var' => 'tags' } 'var' => 'tags',
@context['products'] = { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] } 'nested' => { 'var' => 'tags' },
'products' => { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] },
}
assert_equal('deepsnow', @context['products[var].first']) assert_template_result('deepsnow', '{{ products[var].first }}', assigns)
assert_equal('freestyle', @context['products[nested.var].last']) assert_template_result('freestyle', '{{ products[nested.var].last }}', assigns)
end end
def test_hash_notation_only_for_hash_access def test_hash_notation_only_for_hash_access
@context['array'] = [1, 2, 3, 4, 5] assigns = { "array" => [1, 2, 3, 4, 5] }
@context['hash'] = { 'first' => 'Hello' } assert_template_result("1", "{{ array.first }}", assigns)
assert_template_result("pass", '{% if array["first"] == nil %}pass{% endif %}', assigns)
assert_equal(1, @context['array.first']) assert_template_result("Hello", '{{ hash["first"] }}', { "hash" => { "first" => "Hello" } })
assert_nil(@context['array["first"]'])
assert_equal('Hello', @context['hash["first"]'])
end end
def test_first_can_appear_in_middle_of_callchain def test_first_can_appear_in_middle_of_callchain
@context['product'] = { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] } assigns = { "product" => { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] } }
assert_equal('draft151cm', @context['product.variants[0].title']) assert_template_result('draft151cm', '{{ product.variants[0].title }}', assigns)
assert_equal('element151cm', @context['product.variants[1].title']) assert_template_result('element151cm', '{{ product.variants[1].title }}', assigns)
assert_equal('draft151cm', @context['product.variants.first.title']) assert_template_result('draft151cm', '{{ product.variants.first.title }}', assigns)
assert_equal('element151cm', @context['product.variants.last.title']) assert_template_result('element151cm', '{{ product.variants.last.title }}', assigns)
end end
def test_cents def test_cents
@@ -350,10 +367,12 @@ class ContextTest < Minitest::Test
end end
def test_ranges def test_ranges
@context.merge("test" => '5') assert_template_result("1..5", '{{ (1..5) }}')
assert_equal((1..5), @context['(1..5)']) assert_template_result("pass", '{% if (1..5) == expect %}pass{% endif %}', { "expect" => (1..5) })
assert_equal((1..5), @context['(1..test)'])
assert_equal((5..5), @context['(test..test)']) assigns = { "test" => '5' }
assert_template_result("1..5", "{{ (1..test) }}", assigns)
assert_template_result("5..5", "{{ (test..test) }}", assigns)
end end
def test_cents_through_drop_nestedly def test_cents_through_drop_nestedly
@@ -405,8 +424,9 @@ class ContextTest < Minitest::Test
end end
def test_lambda_is_called_once def test_lambda_is_called_once
@global = 0
@context['callcount'] = proc { @context['callcount'] = proc {
@global ||= 0
@global += 1 @global += 1
@global.to_s @global.to_s
} }
@@ -414,36 +434,35 @@ class ContextTest < Minitest::Test
assert_equal('1', @context['callcount']) assert_equal('1', @context['callcount'])
assert_equal('1', @context['callcount']) assert_equal('1', @context['callcount'])
assert_equal('1', @context['callcount']) assert_equal('1', @context['callcount'])
@global = nil
end end
def test_nested_lambda_is_called_once def test_nested_lambda_is_called_once
@context['callcount'] = { "lambda" => proc { @global = 0
@global ||= 0
@context['callcount'] = {
"lambda" => proc {
@global += 1 @global += 1
@global.to_s @global.to_s
} } },
}
assert_equal('1', @context['callcount.lambda']) assert_equal('1', @context['callcount.lambda'])
assert_equal('1', @context['callcount.lambda']) assert_equal('1', @context['callcount.lambda'])
assert_equal('1', @context['callcount.lambda']) assert_equal('1', @context['callcount.lambda'])
@global = nil
end end
def test_lambda_in_array_is_called_once def test_lambda_in_array_is_called_once
@context['callcount'] = [1, 2, proc { @global = 0
@global ||= 0
p = proc {
@global += 1 @global += 1
@global.to_s @global.to_s
}, 4, 5] }
@context['callcount'] = [1, 2, p, 4, 5]
assert_equal('1', @context['callcount[2]']) assert_equal('1', @context['callcount[2]'])
assert_equal('1', @context['callcount[2]']) assert_equal('1', @context['callcount[2]'])
assert_equal('1', @context['callcount[2]']) assert_equal('1', @context['callcount[2]'])
@global = nil
end end
def test_access_to_context_from_proc def test_access_to_context_from_proc
@@ -486,7 +505,7 @@ class ContextTest < Minitest::Test
def test_static_environments_are_read_with_lower_priority_than_environments def test_static_environments_are_read_with_lower_priority_than_environments
context = Context.build( context = Context.build(
static_environments: { 'shadowed' => 'static', 'unshadowed' => 'static' }, static_environments: { 'shadowed' => 'static', 'unshadowed' => 'static' },
environments: { 'shadowed' => 'dynamic' } environments: { 'shadowed' => 'dynamic' },
) )
assert_equal('dynamic', context['shadowed']) assert_equal('dynamic', context['shadowed'])
@@ -531,7 +550,7 @@ class ContextTest < Minitest::Test
registers = { registers = {
my_register: :my_value, my_register: :my_value,
} }
super_context = Context.new({}, {}, StaticRegisters.new(registers)) super_context = Context.new({}, {}, Registers.new(registers))
super_context.registers[:my_register] = :my_alt_value super_context.registers[:my_register] = :my_alt_value
subcontext = super_context.new_isolated_subcontext subcontext = super_context.new_isolated_subcontext
assert_equal(:my_value, subcontext.registers[:my_register]) assert_equal(:my_value, subcontext.registers[:my_register])
@@ -620,6 +639,54 @@ class ContextTest < Minitest::Test
end end
end end
def test_context_always_uses_static_registers
registers = {
my_register: :my_value,
}
c = Context.new({}, {}, registers)
assert_instance_of(Registers, c.registers)
assert_equal(:my_value, c.registers[:my_register])
r = Registers.new(registers)
c = Context.new({}, {}, r)
assert_instance_of(Registers, c.registers)
assert_equal(:my_value, c.registers[:my_register])
end
def test_variable_to_liquid_returns_contextual_drop
context = {
"products" => ProductsDrop.new(["A", "B", "C", "D", "E"]),
}
template = Liquid::Template.parse(<<~LIQUID)
{%- for i in (1..3) -%}
for_loop_products_count: {{ products | size }}
{% endfor %}
unscoped_products_count: {{ products | size }}
LIQUID
result = template.render(context)
assert_includes(result, "for_loop_products_count: 3")
assert_includes(result, "unscoped_products_count: 5")
end
def test_new_isolated_context_inherits_parent_environment
global_environment = Liquid::Environment.build(tags: {})
context = Context.build(environment: global_environment)
subcontext = context.new_isolated_subcontext
assert_equal(global_environment, subcontext.environment)
end
def test_newly_built_context_inherits_parent_environment
global_environment = Liquid::Environment.build(tags: {})
context = Context.build(environment: global_environment)
assert_equal(global_environment, context.environment)
assert(context.environment.tags.each.to_a.empty?)
end
private private
def assert_no_object_allocations def assert_no_object_allocations
+4 -8
View File
@@ -6,16 +6,12 @@ class DocumentTest < Minitest::Test
include Liquid include Liquid
def test_unexpected_outer_tag def test_unexpected_outer_tag
exc = assert_raises(SyntaxError) do source = "{% else %}"
Template.parse("{% else %}") assert_match_syntax_error("Liquid syntax error (line 1): Unexpected outer 'else' tag", source)
end
assert_equal(exc.message, "Liquid syntax error: Unexpected outer 'else' tag")
end end
def test_unknown_tag def test_unknown_tag
exc = assert_raises(SyntaxError) do source = "{% foo %}"
Template.parse("{% foo %}") assert_match_syntax_error("Liquid syntax error (line 1): Unknown tag 'foo'", source)
end
assert_equal(exc.message, "Liquid syntax error: Unknown tag 'foo'")
end end
end end
+121 -42
View File
@@ -63,9 +63,7 @@ class ErrorHandlingTest < Minitest::Test
end end
def test_missing_endtag_parse_time_error def test_missing_endtag_parse_time_error
assert_raises(Liquid::SyntaxError) do assert_match_syntax_error(/: 'for' tag was never closed\z/, ' {% for a in b %} ... ')
Liquid::Template.parse(' {% for a in b %} ... ')
end
end end
def test_unrecognized_operator def test_unrecognized_operator
@@ -84,37 +82,31 @@ class ErrorHandlingTest < Minitest::Test
end end
def test_with_line_numbers_adds_numbers_to_parser_errors def test_with_line_numbers_adds_numbers_to_parser_errors
err = assert_raises(SyntaxError) do source = <<~LIQUID
Liquid::Template.parse('
foobar foobar
{% "cat" | foobar %} {% "cat" | foobar %}
bla bla
', LIQUID
line_numbers: true) assert_match_syntax_error(/Liquid syntax error \(line 3\)/, source)
end
assert_match(/Liquid syntax error \(line 4\)/, err.message)
end end
def test_with_line_numbers_adds_numbers_to_parser_errors_with_whitespace_trim def test_with_line_numbers_adds_numbers_to_parser_errors_with_whitespace_trim
err = assert_raises(SyntaxError) do source = <<~LIQUID
Liquid::Template.parse('
foobar foobar
{%- "cat" | foobar -%} {%- "cat" | foobar -%}
bla bla
', LIQUID
line_numbers: true)
end
assert_match(/Liquid syntax error \(line 4\)/, err.message) assert_match_syntax_error(/Liquid syntax error \(line 3\)/, source)
end end
def test_parsing_warn_with_line_numbers_adds_numbers_to_lexer_errors def test_parsing_warn_with_line_numbers_adds_numbers_to_lexer_errors
template = Liquid::Template.parse(' template = Liquid::Template.parse(
'
foobar foobar
{% if 1 =! 2 %}ok{% endif %} {% if 1 =! 2 %}ok{% endif %}
@@ -122,15 +114,19 @@ class ErrorHandlingTest < Minitest::Test
bla bla
', ',
error_mode: :warn, error_mode: :warn,
line_numbers: true) line_numbers: true,
)
assert_equal(['Liquid syntax error (line 4): Unexpected character = in "1 =! 2"'], assert_equal(
template.warnings.map(&:message)) ['Liquid syntax error (line 4): Unexpected character = in "1 =! 2"'],
template.warnings.map(&:message),
)
end end
def test_parsing_strict_with_line_numbers_adds_numbers_to_lexer_errors def test_parsing_strict_with_line_numbers_adds_numbers_to_lexer_errors
err = assert_raises(SyntaxError) do err = assert_raises(SyntaxError) do
Liquid::Template.parse(' Liquid::Template.parse(
'
foobar foobar
{% if 1 =! 2 %}ok{% endif %} {% if 1 =! 2 %}ok{% endif %}
@@ -138,15 +134,15 @@ class ErrorHandlingTest < Minitest::Test
bla bla
', ',
error_mode: :strict, error_mode: :strict,
line_numbers: true) line_numbers: true,
)
end end
assert_equal('Liquid syntax error (line 4): Unexpected character = in "1 =! 2"', err.message) assert_equal('Liquid syntax error (line 4): Unexpected character = in "1 =! 2"', err.message)
end end
def test_syntax_errors_in_nested_blocks_have_correct_line_number def test_syntax_errors_in_nested_blocks_have_correct_line_number
err = assert_raises(SyntaxError) do source = <<~LIQUID
Liquid::Template.parse('
foobar foobar
{% if 1 != 2 %} {% if 1 != 2 %}
@@ -154,11 +150,9 @@ class ErrorHandlingTest < Minitest::Test
{% endif %} {% endif %}
bla bla
', LIQUID
line_numbers: true)
end
assert_equal("Liquid syntax error (line 5): Unknown tag 'foo'", err.message) assert_match_syntax_error("Liquid syntax error (line 4): Unknown tag 'foo'", source)
end end
def test_strict_error_messages def test_strict_error_messages
@@ -209,20 +203,34 @@ class ErrorHandlingTest < Minitest::Test
end end
def test_setting_default_exception_renderer def test_setting_default_exception_renderer
old_exception_renderer = Liquid::Template.default_exception_renderer
exceptions = [] exceptions = []
Liquid::Template.default_exception_renderer = ->(e) { default_exception_renderer = ->(e) {
exceptions << e exceptions << e
'' ''
} }
template = Liquid::Template.parse('This is a runtime error: {{ errors.argument_error }}')
env = Liquid::Environment.build(exception_renderer: default_exception_renderer)
template = Liquid::Template.parse('This is a runtime error: {{ errors.argument_error }}', environment: env)
output = template.render('errors' => ErrorDrop.new) output = template.render('errors' => ErrorDrop.new)
assert_equal('This is a runtime error: ', output) assert_equal('This is a runtime error: ', output)
assert_equal([Liquid::ArgumentError], template.errors.map(&:class)) assert_equal([Liquid::ArgumentError], template.errors.map(&:class))
ensure end
Liquid::Template.default_exception_renderer = old_exception_renderer if old_exception_renderer
def test_setting_exception_renderer_on_environment
exceptions = []
exception_renderer = ->(e) do
exceptions << e
''
end
environment = Liquid::Environment.build(exception_renderer: exception_renderer)
template = Liquid::Template.parse('This is a runtime error: {{ errors.argument_error }}', environment: environment)
output = template.render('errors' => ErrorDrop.new)
assert_equal('This is a runtime error: ', output)
assert_equal([Liquid::ArgumentError], template.errors.map(&:class))
end end
def test_exception_renderer_exposing_non_liquid_error def test_exception_renderer_exposing_non_liquid_error
@@ -248,16 +256,10 @@ class ErrorHandlingTest < Minitest::Test
end end
def test_included_template_name_with_line_numbers def test_included_template_name_with_line_numbers
old_file_system = Liquid::Template.file_system environment = Liquid::Environment.build(file_system: TestFileSystem.new)
template = Liquid::Template.parse("Argument error:\n{% include 'product' %}", line_numbers: true, environment: environment)
begin
Liquid::Template.file_system = TestFileSystem.new
template = Liquid::Template.parse("Argument error:\n{% include 'product' %}", line_numbers: true)
page = template.render('errors' => ErrorDrop.new) page = template.render('errors' => ErrorDrop.new)
ensure
Liquid::Template.file_system = old_file_system
end
assert_equal("Argument error:\nLiquid error (product line 1): argument error", page) assert_equal("Argument error:\nLiquid error (product line 1): argument error", page)
assert_equal("product", template.errors.first.template_name) assert_equal("product", template.errors.first.template_name)
end end
@@ -269,4 +271,81 @@ class ErrorHandlingTest < Minitest::Test
output = Liquid::Template.parse("{% assign x = 0 %}{% if 1 < '2' %}{% assign x = 3 %}{% endif %}{{ x }}").render output = Liquid::Template.parse("{% assign x = 0 %}{% if 1 < '2' %}{% assign x = 3 %}{% endif %}{{ x }}").render
assert_equal("0", output) assert_equal("0", output)
end end
def test_syntax_error_is_raised_with_template_name
file_system = StubFileSystem.new("snippet" => "1\n2\n{{ 1")
context = Liquid::Context.build(
registers: { file_system: file_system },
)
template = Template.parse(
'{% render "snippet" %}',
line_numbers: true,
)
template.name = "template/index"
assert_equal(
"Liquid syntax error (snippet line 3): Variable '{{' was not properly terminated with regexp: /\\}\\}/",
template.render(context),
)
end
def test_syntax_error_is_raised_with_template_name_from_template_factory
file_system = StubFileSystem.new("snippet" => "1\n2\n{{ 1")
context = Liquid::Context.build(
registers: {
file_system: file_system,
template_factory: StubTemplateFactory.new,
},
)
template = Template.parse(
'{% render "snippet" %}',
line_numbers: true,
)
template.name = "template/index"
assert_equal(
"Liquid syntax error (some/path/snippet line 3): Variable '{{' was not properly terminated with regexp: /\\}\\}/",
template.render(context),
)
end
def test_error_is_raised_during_parse_with_template_name
depth = Liquid::Block::MAX_DEPTH + 1
code = "{% if true %}" * depth + "rendered" + "{% endif %}" * depth
template = Template.parse("{% render 'snippet' %}", line_numbers: true)
context = Liquid::Context.build(
registers: {
file_system: StubFileSystem.new("snippet" => code),
template_factory: StubTemplateFactory.new,
},
)
assert_equal("Liquid error (some/path/snippet line 1): Nesting too deep", template.render(context))
end
def test_internal_error_is_raised_with_template_name
template = Template.new
template.parse(
"{% render 'snippet' %}",
line_numbers: true,
)
template.name = "template/index"
context = Liquid::Context.build(
registers: {
file_system: StubFileSystem.new({}),
},
)
assert_equal(
"Liquid error (template/index line 1): internal",
template.render(context),
)
end
end end
+132 -22
View File
@@ -1,46 +1,156 @@
# frozen_string_literal: true # frozen_string_literal: true
require 'test_helper' require 'test_helper'
require 'lru_redux'
class ExpressionTest < Minitest::Test class ExpressionTest < Minitest::Test
def test_keyword_literals def test_keyword_literals
assert_equal(true, parse_and_eval("true")) assert_template_result("true", "{{ true }}")
assert_equal(true, parse_and_eval(" true ")) assert_expression_result(true, "true")
end end
def test_string def test_string
assert_equal("single quoted", parse_and_eval("'single quoted'")) assert_template_result("single quoted", "{{'single quoted'}}")
assert_equal("double quoted", parse_and_eval('"double quoted"')) assert_template_result("double quoted", '{{"double quoted"}}')
assert_equal("spaced", parse_and_eval(" 'spaced' ")) assert_template_result("spaced", "{{ 'spaced' }}")
assert_equal("spaced2", parse_and_eval(' "spaced2" ')) assert_template_result("spaced2", "{{ 'spaced2' }}")
assert_template_result("emoji🔥", "{{ 'emoji🔥' }}")
end end
def test_int def test_int
assert_equal(123, parse_and_eval("123")) assert_template_result("456", "{{ 456 }}")
assert_equal(456, parse_and_eval(" 456 ")) assert_expression_result(123, "123")
assert_equal(12, parse_and_eval("012")) assert_expression_result(12, "012")
end end
def test_float def test_float
assert_equal(1.5, parse_and_eval("1.5")) assert_template_result("-17.42", "{{ -17.42 }}")
assert_equal(2.5, parse_and_eval(" 2.5 ")) assert_template_result("2.5", "{{ 2.5 }}")
assert_expression_result(0.0, "0.....5")
assert_expression_result(0.0, "-0..1")
assert_expression_result(1.5, "1.5")
# this is a unfortunate quirky behavior of Liquid
result = Expression.parse(".5")
assert_kind_of(Liquid::VariableLookup, result)
result = Expression.parse("-.5")
assert_kind_of(Liquid::VariableLookup, result)
end end
def test_range def test_range
assert_equal(1..2, parse_and_eval("(1..2)")) assert_template_result("3..4", "{{ ( 3 .. 4 ) }}")
assert_equal(3..4, parse_and_eval(" ( 3 .. 4 ) ")) assert_expression_result(1..2, "(1..2)")
assert_match_syntax_error(
"Liquid syntax error (line 1): Invalid expression type 'false' in range expression",
"{{ (false..true) }}",
)
assert_match_syntax_error(
"Liquid syntax error (line 1): Invalid expression type '(1..2)' in range expression",
"{{ ((1..2)..3) }}",
)
end
def test_quirky_negative_sign_expression_markup
result = Expression.parse("-", nil)
assert(result.is_a?(VariableLookup))
assert_equal("-", result.name)
# for this template, the expression markup is "-"
assert_template_result(
"",
"{{ - 'theme.css' - }}",
)
end
def test_expression_cache
skip("Liquid-C does not support Expression caching") if defined?(Liquid::C) && Liquid::C.enabled
cache = {}
template = <<~LIQUID
{% assign x = 1 %}
{{ x }}
{% assign x = 2 %}
{{ x }}
{% assign y = 1 %}
{{ y }}
LIQUID
Liquid::Template.parse(template, expression_cache: cache).render
assert_equal(
["1", "2", "x", "y"],
cache.to_a.map { _1[0] }.sort,
)
end
def test_expression_cache_with_true_boolean
skip("Liquid-C does not support Expression caching") if defined?(Liquid::C) && Liquid::C.enabled
template = <<~LIQUID
{% assign x = 1 %}
{{ x }}
{% assign x = 2 %}
{{ x }}
{% assign y = 1 %}
{{ y }}
LIQUID
parse_context = ParseContext.new(expression_cache: true)
Liquid::Template.parse(template, parse_context).render
cache = parse_context.instance_variable_get(:@expression_cache)
assert_equal(
["1", "2", "x", "y"],
cache.to_a.map { _1[0] }.sort,
)
end
def test_expression_cache_with_lru_redux
skip("Liquid-C does not support Expression caching") if defined?(Liquid::C) && Liquid::C.enabled
cache = LruRedux::Cache.new(10)
template = <<~LIQUID
{% assign x = 1 %}
{{ x }}
{% assign x = 2 %}
{{ x }}
{% assign y = 1 %}
{{ y }}
LIQUID
Liquid::Template.parse(template, expression_cache: cache).render
assert_equal(
["1", "2", "x", "y"],
cache.to_a.map { _1[0] }.sort,
)
end
def test_disable_expression_cache
skip("Liquid-C does not support Expression caching") if defined?(Liquid::C) && Liquid::C.enabled
template = <<~LIQUID
{% assign x = 1 %}
{{ x }}
{% assign x = 2 %}
{{ x }}
{% assign y = 1 %}
{{ y }}
LIQUID
parse_context = Liquid::ParseContext.new(expression_cache: false)
Liquid::Template.parse(template, parse_context).render
assert(parse_context.instance_variable_get(:@expression_cache).nil?)
end end
private private
def parse_and_eval(markup, **assigns) def assert_expression_result(expect, markup, **assigns)
if Liquid::Template.error_mode == :strict liquid = "{% if expect == #{markup} %}pass{% else %}got {{ #{markup} }}{% endif %}"
p = Liquid::Parser.new(markup) assert_template_result("pass", liquid, { "expect" => expect, **assigns })
markup = p.expression
p.consume(:end_of_string)
end
expression = Liquid::Expression.parse(markup)
context = Liquid::Context.new(assigns)
context.evaluate(expression)
end end
end end
+24
View File
@@ -0,0 +1,24 @@
# frozen_string_literal: true
require 'test_helper'
class FilterKwargTest < Minitest::Test
module KwargFilter
def html_tag(_tag, attributes)
attributes
.map { |key, value| "#{key}='#{value}'" }
.join(' ')
end
end
include Liquid
def test_can_parse_data_kwargs
with_global_filter(KwargFilter) do
assert_equal(
"data-src='src' data-widths='100, 200'",
Template.parse("{{ 'img' | html_tag: data-src: 'src', data-widths: '100, 200' }}").render(nil, nil),
)
end
end
end
+45 -42
View File
@@ -59,84 +59,87 @@ class FiltersTest < Minitest::Test
end end
def test_size def test_size
@context['var'] = 'abcd' assert_template_result("4", "{{var | size}}", { "var" => 'abcd' })
@context.add_filters(MoneyFilter)
assert_equal('4', Template.parse("{{var | size}}").render(@context))
end end
def test_join def test_join
@context['var'] = [1, 2, 3, 4] assert_template_result("1 2 3 4", "{{var | join}}", { "var" => [1, 2, 3, 4] })
assert_equal("1 2 3 4", Template.parse("{{var | join}}").render(@context))
end end
def test_sort def test_sort
@context['value'] = 3 assert_template_result("1 2 3 4", "{{numbers | sort | join}}", { "numbers" => [2, 1, 4, 3] })
@context['numbers'] = [2, 1, 4, 3] assert_template_result(
@context['words'] = ['expected', 'as', 'alphabetic'] "alphabetic as expected",
@context['arrays'] = ['flower', 'are'] "{{words | sort | join}}",
@context['case_sensitive'] = ['sensitive', 'Expected', 'case'] { "words" => ['expected', 'as', 'alphabetic'] },
)
assert_equal('1 2 3 4', Template.parse("{{numbers | sort | join}}").render(@context)) assert_template_result("3", "{{value | sort}}", { "value" => 3 })
assert_equal('alphabetic as expected', Template.parse("{{words | sort | join}}").render(@context)) assert_template_result('are flower', "{{arrays | sort | join}}", { 'arrays' => ['flower', 'are'] })
assert_equal('3', Template.parse("{{value | sort}}").render(@context)) assert_template_result(
assert_equal('are flower', Template.parse("{{arrays | sort | join}}").render(@context)) "Expected case sensitive",
assert_equal('Expected case sensitive', Template.parse("{{case_sensitive | sort | join}}").render(@context)) "{{case_sensitive | sort | join}}",
{ "case_sensitive" => ["sensitive", "Expected", "case"] },
)
end end
def test_sort_natural def test_sort_natural
@context['words'] = ['case', 'Assert', 'Insensitive']
@context['hashes'] = [{ 'a' => 'A' }, { 'a' => 'b' }, { 'a' => 'C' }]
@context['objects'] = [TestObject.new('A'), TestObject.new('b'), TestObject.new('C')]
# Test strings # Test strings
assert_equal('Assert case Insensitive', Template.parse("{{words | sort_natural | join}}").render(@context)) assert_template_result(
"Assert case Insensitive",
"{{words | sort_natural | join}}",
{ "words" => ["case", "Assert", "Insensitive"] },
)
# Test hashes # Test hashes
assert_equal('A b C', Template.parse("{{hashes | sort_natural: 'a' | map: 'a' | join}}").render(@context)) assert_template_result(
"A b C",
"{{hashes | sort_natural: 'a' | map: 'a' | join}}",
{ "hashes" => [{ "a" => "A" }, { "a" => "b" }, { "a" => "C" }] },
)
# Test objects # Test objects
@context['objects'] = [TestObject.new('A'), TestObject.new('b'), TestObject.new('C')]
assert_equal('A b C', Template.parse("{{objects | sort_natural: 'a' | map: 'a' | join}}").render(@context)) assert_equal('A b C', Template.parse("{{objects | sort_natural: 'a' | map: 'a' | join}}").render(@context))
end end
def test_compact def test_compact
@context['words'] = ['a', nil, 'b', nil, 'c']
@context['hashes'] = [{ 'a' => 'A' }, { 'a' => nil }, { 'a' => 'C' }]
@context['objects'] = [TestObject.new('A'), TestObject.new(nil), TestObject.new('C')]
# Test strings # Test strings
assert_equal('a b c', Template.parse("{{words | compact | join}}").render(@context)) assert_template_result(
"a b c",
"{{words | compact | join}}",
{ "words" => ['a', nil, 'b', nil, 'c'] },
)
# Test hashes # Test hashes
assert_equal('A C', Template.parse("{{hashes | compact: 'a' | map: 'a' | join}}").render(@context)) assert_template_result(
"A C",
"{{hashes | compact: 'a' | map: 'a' | join}}",
{ "hashes" => [{ "a" => "A" }, { "a" => nil }, { "a" => "C" }] },
)
# Test objects # Test objects
@context['objects'] = [TestObject.new('A'), TestObject.new(nil), TestObject.new('C')]
assert_equal('A C', Template.parse("{{objects | compact: 'a' | map: 'a' | join}}").render(@context)) assert_equal('A C', Template.parse("{{objects | compact: 'a' | map: 'a' | join}}").render(@context))
end end
def test_strip_html def test_strip_html
@context['var'] = "<b>bla blub</a>" assert_template_result("bla blub", "{{ var | strip_html }}", { "var" => "<b>bla blub</a>" })
assert_equal("bla blub", Template.parse("{{ var | strip_html }}").render(@context))
end end
def test_strip_html_ignore_comments_with_html def test_strip_html_ignore_comments_with_html
@context['var'] = "<!-- split and some <ul> tag --><b>bla blub</a>" assert_template_result(
"bla blub",
assert_equal("bla blub", Template.parse("{{ var | strip_html }}").render(@context)) "{{ var | strip_html }}",
{ "var" => "<!-- split and some <ul> tag --><b>bla blub</a>" },
)
end end
def test_capitalize def test_capitalize
@context['var'] = "blub" assert_template_result("Blub", "{{ var | capitalize }}", { "var" => "blub" })
assert_equal("Blub", Template.parse("{{ var | capitalize }}").render(@context))
end end
def test_nonexistent_filter_is_ignored def test_nonexistent_filter_is_ignored
@context['var'] = 1000 assert_template_result("1000", "{{ var | xyzzy }}", { "var" => 1000 })
assert_equal('1000', Template.parse("{{ var | xyzzy }}").render(@context))
end end
def test_filter_with_keyword_arguments def test_filter_with_keyword_arguments
+83
View File
@@ -0,0 +1,83 @@
# frozen_string_literal: true
require 'test_helper'
class HashRenderingTest < Minitest::Test
def test_render_empty_hash
assert_template_result("{}", "{{ my_hash }}", { "my_hash" => {} })
end
def test_render_hash_with_string_keys_and_values
assert_template_result("{\"key1\"=>\"value1\", \"key2\"=>\"value2\"}", "{{ my_hash }}", { "my_hash" => { "key1" => "value1", "key2" => "value2" } })
end
def test_render_hash_with_symbol_keys_and_integer_values
assert_template_result("{:key1=>1, :key2=>2}", "{{ my_hash }}", { "my_hash" => { key1: 1, key2: 2 } })
end
def test_render_nested_hash
assert_template_result("{\"outer\"=>{\"inner\"=>\"value\"}}", "{{ my_hash }}", { "my_hash" => { "outer" => { "inner" => "value" } } })
end
def test_render_hash_with_array_values
assert_template_result("{\"numbers\"=>[1, 2, 3]}", "{{ my_hash }}", { "my_hash" => { "numbers" => [1, 2, 3] } })
end
def test_render_recursive_hash
recursive_hash = { "self" => {} }
recursive_hash["self"]["self"] = recursive_hash
assert_template_result("{\"self\"=>{\"self\"=>{...}}}", "{{ my_hash }}", { "my_hash" => recursive_hash })
end
def test_hash_with_downcase_filter
assert_template_result("{\"key\"=>\"value\", \"anotherkey\"=>\"anothervalue\"}", "{{ my_hash | downcase }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
end
def test_hash_with_upcase_filter
assert_template_result("{\"KEY\"=>\"VALUE\", \"ANOTHERKEY\"=>\"ANOTHERVALUE\"}", "{{ my_hash | upcase }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
end
def test_hash_with_strip_filter
assert_template_result("{\"Key\"=>\"Value\", \"AnotherKey\"=>\"AnotherValue\"}", "{{ my_hash | strip }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
end
def test_hash_with_escape_filter
assert_template_result("{&quot;Key&quot;=&gt;&quot;Value&quot;, &quot;AnotherKey&quot;=&gt;&quot;AnotherValue&quot;}", "{{ my_hash | escape }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
end
def test_hash_with_url_encode_filter
assert_template_result("%7B%22Key%22%3D%3E%22Value%22%2C+%22AnotherKey%22%3D%3E%22AnotherValue%22%7D", "{{ my_hash | url_encode }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
end
def test_hash_with_strip_html_filter
assert_template_result("{\"Key\"=>\"Value\", \"AnotherKey\"=>\"AnotherValue\"}", "{{ my_hash | strip_html }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
end
def test_hash_with_truncate__20_filter
assert_template_result("{\"Key\"=>\"Value\", ...", "{{ my_hash | truncate: 20 }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
end
def test_hash_with_replace___key____replaced_key__filter
assert_template_result("{\"Key\"=>\"Value\", \"AnotherKey\"=>\"AnotherValue\"}", "{{ my_hash | replace: 'key', 'replaced_key' }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
end
def test_hash_with_append____appended_text__filter
assert_template_result("{\"Key\"=>\"Value\", \"AnotherKey\"=>\"AnotherValue\"} appended text", "{{ my_hash | append: ' appended text' }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
end
def test_hash_with_prepend___prepended_text___filter
assert_template_result("prepended text {\"Key\"=>\"Value\", \"AnotherKey\"=>\"AnotherValue\"}", "{{ my_hash | prepend: 'prepended text ' }}", { "my_hash" => { "Key" => "Value", "AnotherKey" => "AnotherValue" } })
end
def test_render_hash_with_array_values_empty
assert_template_result("{\"numbers\"=>[]}", "{{ my_hash }}", { "my_hash" => { "numbers" => [] } })
end
def test_render_hash_with_array_values_hash
assert_template_result("{\"numbers\"=>[{:foo=>42}]}", "{{ my_hash }}", { "my_hash" => { "numbers" => [{ foo: 42 }] } })
end
def test_render_hash_with_hash_key
assert_template_result("{{\"foo\"=>\"bar\"}=>42}", "{{ my_hash }}", { "my_hash" => { Hash["foo" => "bar"] => 42 } })
end
end
+9 -14
View File
@@ -33,31 +33,25 @@ class OutputTest < Minitest::Test
def setup def setup
@assigns = { @assigns = {
'best_cars' => 'bmw',
'car' => { 'bmw' => 'good', 'gm' => 'bad' }, 'car' => { 'bmw' => 'good', 'gm' => 'bad' },
} }
end end
def test_variable def test_variable
text = %( {{best_cars}} ) assert_template_result(" bmw ", " {{best_cars}} ", { "best_cars" => "bmw" })
expected = %( bmw )
assert_equal(expected, Template.parse(text).render!(@assigns))
end end
def test_variable_traversing_with_two_brackets def test_variable_traversing_with_two_brackets
text = %({{ site.data.menu[include.menu][include.locale] }}) source = "{{ site.data.menu[include.menu][include.locale] }}"
assert_equal("it works!", Template.parse(text).render!( assert_template_result("it works!", source, {
"site" => { "data" => { "menu" => { "foo" => { "bar" => "it works!" } } } }, "site" => { "data" => { "menu" => { "foo" => { "bar" => "it works!" } } } },
"include" => { "menu" => "foo", "locale" => "bar" } "include" => { "menu" => "foo", "locale" => "bar" },
)) })
end end
def test_variable_traversing def test_variable_traversing
text = %( {{car.bmw}} {{car.gm}} {{car.bmw}} ) source = " {{car.bmw}} {{car.gm}} {{car.bmw}} "
assert_template_result(" good bad good ", source, @assigns)
expected = %( good bad good )
assert_equal(expected, Template.parse(text).render!(@assigns))
end end
def test_variable_piping def test_variable_piping
@@ -110,10 +104,11 @@ class OutputTest < Minitest::Test
end end
def test_multiple_pipings def test_multiple_pipings
assigns = { 'best_cars' => 'bmw' }
text = %( {{ best_cars | cite_funny | paragraph }} ) text = %( {{ best_cars | cite_funny | paragraph }} )
expected = %( <p>LOL: bmw</p> ) expected = %( <p>LOL: bmw</p> )
assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])) assert_equal(expected, Template.parse(text).render!(assigns, filters: [FunnyFilter]))
end end
def test_link_to def test_link_to
+21 -1
View File
@@ -129,6 +129,26 @@ class ParsingQuirksTest < Minitest::Test
end end
def test_contains_in_id def test_contains_in_id
assert_template_result(' YES ', '{% if containsallshipments == true %} YES {% endif %}', 'containsallshipments' => true) assert_template_result(' YES ', '{% if containsallshipments == true %} YES {% endif %}', { 'containsallshipments' => true })
end
def test_incomplete_expression
with_error_mode(:lax) do
assert_template_result("false", "{{ false - }}")
assert_template_result("false", "{{ false > }}")
assert_template_result("false", "{{ false < }}")
assert_template_result("false", "{{ false = }}")
assert_template_result("false", "{{ false ! }}")
assert_template_result("false", "{{ false 1 }}")
assert_template_result("false", "{{ false a }}")
assert_template_result("false", "{% liquid assign foo = false -\n%}{{ foo }}")
assert_template_result("false", "{% liquid assign foo = false >\n%}{{ foo }}")
assert_template_result("false", "{% liquid assign foo = false <\n%}{{ foo }}")
assert_template_result("false", "{% liquid assign foo = false =\n%}{{ foo }}")
assert_template_result("false", "{% liquid assign foo = false !\n%}{{ foo }}")
assert_template_result("false", "{% liquid assign foo = false 1\n%}{{ foo }}")
assert_template_result("false", "{% liquid assign foo = false a\n%}{{ foo }}")
end
end end
end # ParsingQuirksTest end # ParsingQuirksTest
+34 -7
View File
@@ -3,6 +3,27 @@
require 'test_helper' require 'test_helper'
class ProfilerTest < Minitest::Test class ProfilerTest < Minitest::Test
class TestDrop < Liquid::Drop
def initialize(value)
super()
@value = value
end
def to_s
artificial_execution_time
@value
end
private
# Monotonic clock precision fluctuate based on the operating system
# By introducing a small sleep we ensure ourselves to register a non zero unit of time
def artificial_execution_time
sleep(Process.clock_getres(Process::CLOCK_MONOTONIC))
end
end
include Liquid include Liquid
class ProfilingFileSystem class ProfilingFileSystem
@@ -12,7 +33,7 @@ class ProfilerTest < Minitest::Test
end end
def setup def setup
Liquid::Template.file_system = ProfilingFileSystem.new Liquid::Environment.default.file_system = ProfilingFileSystem.new
end end
def test_template_allows_flagging_profiling def test_template_allows_flagging_profiling
@@ -198,16 +219,22 @@ class ProfilerTest < Minitest::Test
def test_profiling_supports_self_time def test_profiling_supports_self_time
t = Template.parse("{% for item in collection %} {{ item }} {% endfor %}", profile: true) t = Template.parse("{% for item in collection %} {{ item }} {% endfor %}", profile: true)
t.render!("collection" => ["one", "two"]) collection = [
leaf = t.profiler[0].children[0] TestDrop.new("one"),
TestDrop.new("two"),
]
output = t.render!("collection" => collection)
assert_equal(" one two ", output)
assert_operator(leaf.self_time, :>, 0) leaf = t.profiler[0].children[0]
assert_operator(leaf.self_time, :>, 0.0)
end end
def test_profiling_supports_total_time def test_profiling_supports_total_time
t = Template.parse("{% if true %} {% increment test %} {{ test }} {% endif %}", profile: true) t = Template.parse("{% if true %} {{ test }} {% endif %}", profile: true)
t.render! output = t.render!("test" => TestDrop.new("one"))
assert_equal(" one ", output)
assert_operator(t.profiler[0].total_time, :>, 0) assert_operator(t.profiler[0].total_time, :>, 0.0)
end end
end end
+581 -80
View File
@@ -3,10 +3,6 @@
require 'test_helper' require 'test_helper'
class Filters
include Liquid::StandardFilters
end
class TestThing class TestThing
attr_reader :foo attr_reader :foo
@@ -29,8 +25,24 @@ class TestThing
end end
class TestDrop < Liquid::Drop class TestDrop < Liquid::Drop
def test def initialize(value:)
"testfoo" @value = value
end
attr_reader :value
def registers
"{#{@value.inspect}=>#{@context.registers[@value].inspect}}"
end
end
class TestModel
def initialize(value:)
@value = value
end
def to_liquid
TestDrop.new(value: @value)
end end
end end
@@ -42,6 +54,30 @@ class TestEnumerable < Liquid::Drop
end end
end end
class TestDeepEnumerable < Liquid::Drop
include Enumerable
class Product < Liquid::Drop
attr_reader :title, :price, :premium
def initialize(title:, price:, premium: nil)
@title = { "content" => title, "language" => "en" }
@price = { "value" => price, "unit" => "USD" }
@premium = { "category" => premium } if premium
end
end
def each(&block)
[
Product.new(title: "Pro goggles", price: 1299),
Product.new(title: "Thermal gloves", price: 1299),
Product.new(title: "Alpine jacket", price: 3999, premium: 'Basic'),
Product.new(title: "Mountain boots", price: 3899, premium: 'Pro'),
Product.new(title: "Safety helmet", price: 1999)
].each(&block)
end
end
class NumberLikeThing < Liquid::Drop class NumberLikeThing < Liquid::Drop
def initialize(amount) def initialize(amount)
@amount = amount @amount = amount
@@ -53,10 +89,13 @@ class NumberLikeThing < Liquid::Drop
end end
class StandardFiltersTest < Minitest::Test class StandardFiltersTest < Minitest::Test
Filters = Class.new(Liquid::StrainerTemplate)
Filters.add_filter(Liquid::StandardFilters)
include Liquid include Liquid
def setup def setup
@filters = Filters.new @filters = Filters.new(Context.new)
end end
def test_size def test_size
@@ -94,6 +133,10 @@ class StandardFiltersTest < Minitest::Test
assert_raises(Liquid::ArgumentError) do assert_raises(Liquid::ArgumentError) do
@filters.slice('foobar', 0, "") @filters.slice('foobar', 0, "")
end end
assert_equal("", @filters.slice("foobar", 0, -(1 << 64)))
assert_equal("foobar", @filters.slice("foobar", 0, 1 << 63))
assert_equal("", @filters.slice("foobar", 1 << 63, 6))
assert_equal("", @filters.slice("foobar", -(1 << 63), 6))
end end
def test_slice_on_arrays def test_slice_on_arrays
@@ -108,6 +151,10 @@ class StandardFiltersTest < Minitest::Test
assert_equal(%w(r), @filters.slice(input, -1)) assert_equal(%w(r), @filters.slice(input, -1))
assert_equal(%w(), @filters.slice(input, 100, 10)) assert_equal(%w(), @filters.slice(input, 100, 10))
assert_equal(%w(), @filters.slice(input, -100, 10)) assert_equal(%w(), @filters.slice(input, -100, 10))
assert_equal([], @filters.slice(input, 0, -(1 << 64)))
assert_equal(input, @filters.slice(input, 0, 1 << 63))
assert_equal([], @filters.slice(input, 1 << 63, 6))
assert_equal([], @filters.slice(input, -(1 << 63), 6))
end end
def test_truncate def test_truncate
@@ -117,6 +164,8 @@ class StandardFiltersTest < Minitest::Test
assert_equal('1234567890', @filters.truncate('1234567890')) assert_equal('1234567890', @filters.truncate('1234567890'))
assert_equal("测试...", @filters.truncate("测试测试测试测试", 5)) assert_equal("测试...", @filters.truncate("测试测试测试测试", 5))
assert_equal('12341', @filters.truncate("1234567890", 5, 1)) assert_equal('12341', @filters.truncate("1234567890", 5, 1))
assert_equal("foobar", @filters.truncate("foobar", 1 << 63))
assert_equal("...", @filters.truncate("foobar", -(1 << 63)))
end end
def test_split def test_split
@@ -145,6 +194,61 @@ class StandardFiltersTest < Minitest::Test
assert_equal('&lt;strong&gt;Hulk&lt;/strong&gt;', @filters.escape_once('&lt;strong&gt;Hulk</strong>')) assert_equal('&lt;strong&gt;Hulk&lt;/strong&gt;', @filters.escape_once('&lt;strong&gt;Hulk</strong>'))
end end
def test_base64_encode
assert_equal('b25lIHR3byB0aHJlZQ==', @filters.base64_encode('one two three'))
assert_equal('', @filters.base64_encode(nil))
end
def test_base64_decode
decoded = @filters.base64_decode('b25lIHR3byB0aHJlZQ==')
assert_equal('one two three', decoded)
assert_equal(Encoding::UTF_8, decoded.encoding)
decoded = @filters.base64_decode('4pyF')
assert_equal('✅', decoded)
assert_equal(Encoding::UTF_8, decoded.encoding)
decoded = @filters.base64_decode("/w==")
assert_equal(Encoding::ASCII_8BIT, decoded.encoding)
assert_equal((+"\xFF").force_encoding(Encoding::ASCII_8BIT), decoded)
exception = assert_raises(Liquid::ArgumentError) do
@filters.base64_decode("invalidbase64")
end
assert_equal('Liquid error: invalid base64 provided to base64_decode', exception.message)
end
def test_base64_url_safe_encode
assert_equal(
'YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXogQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVogMTIzNDU2Nzg5MCAhQCMkJV4mKigpLT1fKy8_Ljo7W117fVx8',
@filters.base64_url_safe_encode('abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890 !@#$%^&*()-=_+/?.:;[]{}\|'),
)
assert_equal('', @filters.base64_url_safe_encode(nil))
end
def test_base64_url_safe_decode
decoded = @filters.base64_url_safe_decode('YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXogQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVogMTIzNDU2Nzg5MCAhQCMkJV4mKigpLT1fKy8_Ljo7W117fVx8')
assert_equal(
'abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890 !@#$%^&*()-=_+/?.:;[]{}\|',
decoded,
)
assert_equal(Encoding::UTF_8, decoded.encoding)
decoded = @filters.base64_url_safe_decode('4pyF')
assert_equal('✅', decoded)
assert_equal(Encoding::UTF_8, decoded.encoding)
decoded = @filters.base64_url_safe_decode("_w==")
assert_equal(Encoding::ASCII_8BIT, decoded.encoding)
assert_equal((+"\xFF").force_encoding(Encoding::ASCII_8BIT), decoded)
exception = assert_raises(Liquid::ArgumentError) do
@filters.base64_url_safe_decode("invalidbase64")
end
assert_equal('Liquid error: invalid base64 provided to base64_url_safe_decode', exception.message)
end
def test_url_encode def test_url_encode
assert_equal('foo%2B1%40example.com', @filters.url_encode('[email protected]')) assert_equal('foo%2B1%40example.com', @filters.url_encode('[email protected]'))
assert_equal('1', @filters.url_encode(1)) assert_equal('1', @filters.url_encode(1))
@@ -171,13 +275,15 @@ class StandardFiltersTest < Minitest::Test
assert_equal('one two three', @filters.truncatewords('one two three')) assert_equal('one two three', @filters.truncatewords('one two three'))
assert_equal( assert_equal(
'Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221;...', 'Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221;...',
@filters.truncatewords('Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221; x 16&#8221; x 10.5&#8221; high) with cover.', 15) @filters.truncatewords('Two small (13&#8221; x 5.5&#8221; x 10&#8221; high) baskets fit inside one large basket (13&#8221; x 16&#8221; x 10.5&#8221; high) with cover.', 15),
) )
assert_equal("测试测试测试测试", @filters.truncatewords('测试测试测试测试', 5)) assert_equal("测试测试测试测试", @filters.truncatewords('测试测试测试测试', 5))
assert_equal('one two1', @filters.truncatewords("one two three", 2, 1)) assert_equal('one two1', @filters.truncatewords("one two three", 2, 1))
assert_equal('one two three...', @filters.truncatewords("one two\tthree\nfour", 3)) assert_equal('one two three...', @filters.truncatewords("one two\tthree\nfour", 3))
assert_equal('one two...', @filters.truncatewords("one two three four", 2)) assert_equal('one two...', @filters.truncatewords("one two three four", 2))
assert_equal('one...', @filters.truncatewords("one two three four", 0)) assert_equal('one...', @filters.truncatewords("one two three four", 0))
assert_equal('one two three four', @filters.truncatewords("one two three four", 1 << 31))
assert_equal('one...', @filters.truncatewords("one two three four", -(1 << 32)))
end end
def test_strip_html def test_strip_html
@@ -221,8 +327,8 @@ class StandardFiltersTest < Minitest::Test
{ "price" => 1, "handle" => "gamma" }, { "price" => 1, "handle" => "gamma" },
{ "price" => 2, "handle" => "epsilon" }, { "price" => 2, "handle" => "epsilon" },
{ "price" => 4, "handle" => "alpha" }, { "price" => 4, "handle" => "alpha" },
{ "handle" => "delta" },
{ "handle" => "beta" }, { "handle" => "beta" },
{ "handle" => "delta" },
] ]
assert_equal(expectation, @filters.sort(input, "price")) assert_equal(expectation, @filters.sort(input, "price"))
end end
@@ -249,8 +355,8 @@ class StandardFiltersTest < Minitest::Test
{ "price" => "1", "handle" => "gamma" }, { "price" => "1", "handle" => "gamma" },
{ "price" => 2, "handle" => "epsilon" }, { "price" => 2, "handle" => "epsilon" },
{ "price" => "4", "handle" => "alpha" }, { "price" => "4", "handle" => "alpha" },
{ "handle" => "delta" },
{ "handle" => "beta" }, { "handle" => "beta" },
{ "handle" => "delta" },
] ]
assert_equal(expectation, @filters.sort_natural(input, "price")) assert_equal(expectation, @filters.sort_natural(input, "price"))
end end
@@ -310,6 +416,15 @@ class StandardFiltersTest < Minitest::Test
end end
end end
def test_sort_natural_with_deep_enumerables
template = <<~LIQUID
{{- products | sort_natural: 'title.content' | map: 'title.content' | join: ', ' -}}
LIQUID
expected_output = "Alpine jacket, Mountain boots, Pro goggles, Safety helmet, Thermal gloves"
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
end
def test_legacy_sort_hash def test_legacy_sort_hash
assert_equal([{ a: 1, b: 2 }], @filters.sort(a: 1, b: 2)) assert_equal([{ a: 1, b: 2 }], @filters.sort(a: 1, b: 2))
end end
@@ -325,8 +440,9 @@ class StandardFiltersTest < Minitest::Test
assert_equal(["foo"], @filters.uniq("foo")) assert_equal(["foo"], @filters.uniq("foo"))
assert_equal([1, 3, 2, 4], @filters.uniq([1, 1, 3, 2, 3, 1, 4, 3, 2, 1])) assert_equal([1, 3, 2, 4], @filters.uniq([1, 1, 3, 2, 3, 1, 4, 3, 2, 1]))
assert_equal([{ "a" => 1 }, { "a" => 3 }, { "a" => 2 }], @filters.uniq([{ "a" => 1 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a")) assert_equal([{ "a" => 1 }, { "a" => 3 }, { "a" => 2 }], @filters.uniq([{ "a" => 1 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a"))
testdrop = TestDrop.new test_drop = TestDrop.new(value: "test")
assert_equal([testdrop], @filters.uniq([testdrop, TestDrop.new], 'test')) test_drop_alternate = TestDrop.new(value: "test")
assert_equal([test_drop], @filters.uniq([test_drop, test_drop_alternate], 'value'))
end end
def test_uniq_empty_array def test_uniq_empty_array
@@ -345,6 +461,15 @@ class StandardFiltersTest < Minitest::Test
end end
end end
def test_uniq_with_deep_enumerables
template = <<~LIQUID
{{- products | uniq: 'price.value' | map: "title.content" | join: ', ' -}}
LIQUID
expected_output = "Pro goggles, Alpine jacket, Mountain boots, Safety helmet"
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
end
def test_compact_empty_array def test_compact_empty_array
assert_equal([], @filters.compact([], "a")) assert_equal([], @filters.compact([], "a"))
end end
@@ -361,6 +486,15 @@ class StandardFiltersTest < Minitest::Test
end end
end end
def test_compact_with_deep_enumerables
template = <<~LIQUID
{{- products | compact: 'premium.category' | map: 'title.content' | join: ', ' -}}
LIQUID
expected_output = "Alpine jacket, Mountain boots"
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
end
def test_reverse def test_reverse
assert_equal([4, 3, 2, 1], @filters.reverse([1, 2, 3, 4])) assert_equal([4, 3, 2, 1], @filters.reverse([1, 2, 3, 4]))
end end
@@ -371,8 +505,11 @@ class StandardFiltersTest < Minitest::Test
def test_map def test_map
assert_equal([1, 2, 3, 4], @filters.map([{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }], 'a')) assert_equal([1, 2, 3, 4], @filters.map([{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }], 'a'))
assert_template_result('abc', "{{ ary | map:'foo' | map:'bar' }}", assert_template_result(
'ary' => [{ 'foo' => { 'bar' => 'a' } }, { 'foo' => { 'bar' => 'b' } }, { 'foo' => { 'bar' => 'c' } }]) 'abc',
"{{ ary | map:'foo' | map:'bar' }}",
{ 'ary' => [{ 'foo' => { 'bar' => 'a' } }, { 'foo' => { 'bar' => 'b' } }, { 'foo' => { 'bar' => 'c' } }] },
)
end end
def test_map_doesnt_call_arbitrary_stuff def test_map_doesnt_call_arbitrary_stuff
@@ -382,18 +519,31 @@ class StandardFiltersTest < Minitest::Test
def test_map_calls_to_liquid def test_map_calls_to_liquid
t = TestThing.new t = TestThing.new
assert_template_result("woot: 1", '{{ foo | map: "whatever" }}', "foo" => [t]) assert_template_result("woot: 1", '{{ foo | map: "whatever" }}', { "foo" => [t] })
end
def test_map_calls_context=
model = TestModel.new(value: :test)
template = Template.parse('{{ foo | map: "registers" }}')
template.registers[:test] = 1234
template.assigns['foo'] = [model]
assert_template_result("{:test=>1234}", template.render!)
end end
def test_map_on_hashes def test_map_on_hashes
assert_template_result("4217", '{{ thing | map: "foo" | map: "bar" }}', assert_template_result(
"thing" => { "foo" => [{ "bar" => 42 }, { "bar" => 17 }] }) "4217",
'{{ thing | map: "foo" | map: "bar" }}',
{ "thing" => { "foo" => [{ "bar" => 42 }, { "bar" => 17 }] } },
)
end end
def test_legacy_map_on_hashes_with_dynamic_key def test_legacy_map_on_hashes_with_dynamic_key
template = "{% assign key = 'foo' %}{{ thing | map: key | map: 'bar' }}" template = "{% assign key = 'foo' %}{{ thing | map: key | map: 'bar' }}"
hash = { "foo" => { "bar" => 42 } } hash = { "foo" => { "bar" => 42 } }
assert_template_result("42", template, "thing" => hash) assert_template_result("42", template, { "thing" => hash })
end end
def test_sort_calls_to_liquid def test_sort_calls_to_liquid
@@ -403,10 +553,10 @@ class StandardFiltersTest < Minitest::Test
end end
def test_map_over_proc def test_map_over_proc
drop = TestDrop.new drop = TestDrop.new(value: "testfoo")
p = proc { drop } p = proc { drop }
templ = '{{ procs | map: "test" }}' output = Liquid::Template.parse('{{ procs | map: "value" }}').render!({ "procs" => [p] })
assert_template_result("testfoo", templ, "procs" => [p]) assert_equal("testfoo", output)
end end
def test_map_over_drops_returning_procs def test_map_over_drops_returning_procs
@@ -418,12 +568,13 @@ class StandardFiltersTest < Minitest::Test
"proc" => -> { "bar" }, "proc" => -> { "bar" },
}, },
] ]
templ = '{{ drops | map: "proc" }}' output = Liquid::Template.parse('{{ drops | map: "proc" }}').render!({ "drops" => drops })
assert_template_result("foobar", templ, "drops" => drops) assert_equal("foobar", output)
end end
def test_map_works_on_enumerables def test_map_works_on_enumerables
assert_template_result("123", '{{ foo | map: "foo" }}', "foo" => TestEnumerable.new) output = Liquid::Template.parse('{{ foo | map: "foo" }}').render!({ "foo" => TestEnumerable.new })
assert_equal("123", output)
end end
def test_map_returns_empty_on_2d_input_array def test_map_returns_empty_on_2d_input_array
@@ -450,16 +601,25 @@ class StandardFiltersTest < Minitest::Test
end end
def test_sort_works_on_enumerables def test_sort_works_on_enumerables
assert_template_result("213", '{{ foo | sort: "bar" | map: "foo" }}', "foo" => TestEnumerable.new) assert_template_result("213", '{{ foo | sort: "bar" | map: "foo" }}', { "foo" => TestEnumerable.new })
end
def test_sort_with_deep_enumerables
template = <<~LIQUID
{{- products | sort: 'price.value' | map: 'title.content' | join: ', ' -}}
LIQUID
expected_output = "Pro goggles, Thermal gloves, Safety helmet, Mountain boots, Alpine jacket"
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
end end
def test_first_and_last_call_to_liquid def test_first_and_last_call_to_liquid
assert_template_result('foobar', '{{ foo | first }}', 'foo' => [ThingWithToLiquid.new]) assert_template_result('foobar', '{{ foo | first }}', { 'foo' => [ThingWithToLiquid.new] })
assert_template_result('foobar', '{{ foo | last }}', 'foo' => [ThingWithToLiquid.new]) assert_template_result('foobar', '{{ foo | last }}', { 'foo' => [ThingWithToLiquid.new] })
end end
def test_truncate_calls_to_liquid def test_truncate_calls_to_liquid
assert_template_result("wo...", '{{ foo | truncate: 5 }}', "foo" => TestThing.new) assert_template_result("wo...", '{{ foo | truncate: 5 }}', { "foo" => TestThing.new })
end end
def test_date def test_date
@@ -501,19 +661,31 @@ class StandardFiltersTest < Minitest::Test
end end
def test_replace def test_replace
assert_equal('2 2 2 2', @filters.replace('1 1 1 1', '1', 2)) assert_equal('b b b b', @filters.replace('a a a a', 'a', 'b'))
assert_equal('2 2 2 2', @filters.replace('1 1 1 1', 1, 2)) assert_equal('2 2 2 2', @filters.replace('1 1 1 1', 1, 2))
assert_equal('2 1 1 1', @filters.replace_first('1 1 1 1', '1', 2)) assert_equal('1 1 1 1', @filters.replace('1 1 1 1', 2, 3))
assert_template_result('2 2 2 2', "{{ '1 1 1 1' | replace: '1', 2 }}")
assert_equal('b a a a', @filters.replace_first('a a a a', 'a', 'b'))
assert_equal('2 1 1 1', @filters.replace_first('1 1 1 1', 1, 2)) assert_equal('2 1 1 1', @filters.replace_first('1 1 1 1', 1, 2))
assert_equal('1 1 1 1', @filters.replace_first('1 1 1 1', 2, 3))
assert_template_result('2 1 1 1', "{{ '1 1 1 1' | replace_first: '1', 2 }}") assert_template_result('2 1 1 1', "{{ '1 1 1 1' | replace_first: '1', 2 }}")
assert_equal('a a a b', @filters.replace_last('a a a a', 'a', 'b'))
assert_equal('1 1 1 2', @filters.replace_last('1 1 1 1', 1, 2))
assert_equal('1 1 1 1', @filters.replace_last('1 1 1 1', 2, 3))
assert_template_result('1 1 1 2', "{{ '1 1 1 1' | replace_last: '1', 2 }}")
end end
def test_remove def test_remove
assert_equal(' ', @filters.remove("a a a a", 'a')) assert_equal(' ', @filters.remove("a a a a", 'a'))
assert_equal(' ', @filters.remove("1 1 1 1", 1)) assert_template_result(' ', "{{ '1 1 1 1' | remove: 1 }}")
assert_equal('a a a', @filters.remove_first("a a a a", 'a '))
assert_equal(' 1 1 1', @filters.remove_first("1 1 1 1", 1)) assert_equal('b a a', @filters.remove_first("a b a a", 'a '))
assert_template_result('a a a', "{{ 'a a a a' | remove_first: 'a ' }}") assert_template_result(' 1 1 1', "{{ '1 1 1 1' | remove_first: 1 }}")
assert_equal('a a b', @filters.remove_last("a a b a", ' a'))
assert_template_result('1 1 1 ', "{{ '1 1 1 1' | remove_last: 1 }}")
end end
def test_pipes_in_string_arguments def test_pipes_in_string_arguments
@@ -521,42 +693,42 @@ class StandardFiltersTest < Minitest::Test
end end
def test_strip def test_strip
assert_template_result('ab c', "{{ source | strip }}", 'source' => " ab c ") assert_template_result('ab c', "{{ source | strip }}", { 'source' => " ab c " })
assert_template_result('ab c', "{{ source | strip }}", 'source' => " \tab c \n \t") assert_template_result('ab c', "{{ source | strip }}", { 'source' => " \tab c \n \t" })
end end
def test_lstrip def test_lstrip
assert_template_result('ab c ', "{{ source | lstrip }}", 'source' => " ab c ") assert_template_result('ab c ', "{{ source | lstrip }}", { 'source' => " ab c " })
assert_template_result("ab c \n \t", "{{ source | lstrip }}", 'source' => " \tab c \n \t") assert_template_result("ab c \n \t", "{{ source | lstrip }}", { 'source' => " \tab c \n \t" })
end end
def test_rstrip def test_rstrip
assert_template_result(" ab c", "{{ source | rstrip }}", 'source' => " ab c ") assert_template_result(" ab c", "{{ source | rstrip }}", { 'source' => " ab c " })
assert_template_result(" \tab c", "{{ source | rstrip }}", 'source' => " \tab c \n \t") assert_template_result(" \tab c", "{{ source | rstrip }}", { 'source' => " \tab c \n \t" })
end end
def test_strip_newlines def test_strip_newlines
assert_template_result('abc', "{{ source | strip_newlines }}", 'source' => "a\nb\nc") assert_template_result('abc', "{{ source | strip_newlines }}", { 'source' => "a\nb\nc" })
assert_template_result('abc', "{{ source | strip_newlines }}", 'source' => "a\r\nb\nc") assert_template_result('abc', "{{ source | strip_newlines }}", { 'source' => "a\r\nb\nc" })
end end
def test_newlines_to_br def test_newlines_to_br
assert_template_result("a<br />\nb<br />\nc", "{{ source | newline_to_br }}", 'source' => "a\nb\nc") assert_template_result("a<br />\nb<br />\nc", "{{ source | newline_to_br }}", { 'source' => "a\nb\nc" })
assert_template_result("a<br />\nb<br />\nc", "{{ source | newline_to_br }}", 'source' => "a\r\nb\nc") assert_template_result("a<br />\nb<br />\nc", "{{ source | newline_to_br }}", { 'source' => "a\r\nb\nc" })
end end
def test_plus def test_plus
assert_template_result("2", "{{ 1 | plus:1 }}") assert_template_result("2", "{{ 1 | plus:1 }}")
assert_template_result("2.0", "{{ '1' | plus:'1.0' }}") assert_template_result("2.0", "{{ '1' | plus:'1.0' }}")
assert_template_result("5", "{{ price | plus:'2' }}", 'price' => NumberLikeThing.new(3)) assert_template_result("5", "{{ price | plus:'2' }}", { 'price' => NumberLikeThing.new(3) })
end end
def test_minus def test_minus
assert_template_result("4", "{{ input | minus:operand }}", 'input' => 5, 'operand' => 1) assert_template_result("4", "{{ input | minus:operand }}", { 'input' => 5, 'operand' => 1 })
assert_template_result("2.3", "{{ '4.3' | minus:'2' }}") assert_template_result("2.3", "{{ '4.3' | minus:'2' }}")
assert_template_result("5", "{{ price | minus:'2' }}", 'price' => NumberLikeThing.new(7)) assert_template_result("5", "{{ price | minus:'2' }}", { 'price' => NumberLikeThing.new(7) })
end end
def test_abs def test_abs
@@ -579,7 +751,7 @@ class StandardFiltersTest < Minitest::Test
assert_template_result("7.25", "{{ 0.0725 | times:100 }}") assert_template_result("7.25", "{{ 0.0725 | times:100 }}")
assert_template_result("-7.25", '{{ "-0.0725" | times:100 }}') assert_template_result("-7.25", '{{ "-0.0725" | times:100 }}')
assert_template_result("7.25", '{{ "-0.0725" | times: -100 }}') assert_template_result("7.25", '{{ "-0.0725" | times: -100 }}')
assert_template_result("4", "{{ price | times:2 }}", 'price' => NumberLikeThing.new(2)) assert_template_result("4", "{{ price | times:2 }}", { 'price' => NumberLikeThing.new(2) })
end end
def test_divided_by def test_divided_by
@@ -594,7 +766,7 @@ class StandardFiltersTest < Minitest::Test
assert_template_result("4", "{{ 1 | modulo: 0 }}") assert_template_result("4", "{{ 1 | modulo: 0 }}")
end end
assert_template_result("5", "{{ price | divided_by:2 }}", 'price' => NumberLikeThing.new(10)) assert_template_result("5", "{{ price | divided_by:2 }}", { 'price' => NumberLikeThing.new(10) })
end end
def test_modulo def test_modulo
@@ -603,39 +775,39 @@ class StandardFiltersTest < Minitest::Test
assert_template_result("4", "{{ 1 | modulo: 0 }}") assert_template_result("4", "{{ 1 | modulo: 0 }}")
end end
assert_template_result("1", "{{ price | modulo:2 }}", 'price' => NumberLikeThing.new(3)) assert_template_result("1", "{{ price | modulo:2 }}", { 'price' => NumberLikeThing.new(3) })
end end
def test_round def test_round
assert_template_result("5", "{{ input | round }}", 'input' => 4.6) assert_template_result("5", "{{ input | round }}", { 'input' => 4.6 })
assert_template_result("4", "{{ '4.3' | round }}") assert_template_result("4", "{{ '4.3' | round }}")
assert_template_result("4.56", "{{ input | round: 2 }}", 'input' => 4.5612) assert_template_result("4.56", "{{ input | round: 2 }}", { 'input' => 4.5612 })
assert_raises(Liquid::FloatDomainError) do assert_raises(Liquid::FloatDomainError) do
assert_template_result("4", "{{ 1.0 | divided_by: 0.0 | round }}") assert_template_result("4", "{{ 1.0 | divided_by: 0.0 | round }}")
end end
assert_template_result("5", "{{ price | round }}", 'price' => NumberLikeThing.new(4.6)) assert_template_result("5", "{{ price | round }}", { 'price' => NumberLikeThing.new(4.6) })
assert_template_result("4", "{{ price | round }}", 'price' => NumberLikeThing.new(4.3)) assert_template_result("4", "{{ price | round }}", { 'price' => NumberLikeThing.new(4.3) })
end end
def test_ceil def test_ceil
assert_template_result("5", "{{ input | ceil }}", 'input' => 4.6) assert_template_result("5", "{{ input | ceil }}", { 'input' => 4.6 })
assert_template_result("5", "{{ '4.3' | ceil }}") assert_template_result("5", "{{ '4.3' | ceil }}")
assert_raises(Liquid::FloatDomainError) do assert_raises(Liquid::FloatDomainError) do
assert_template_result("4", "{{ 1.0 | divided_by: 0.0 | ceil }}") assert_template_result("4", "{{ 1.0 | divided_by: 0.0 | ceil }}")
end end
assert_template_result("5", "{{ price | ceil }}", 'price' => NumberLikeThing.new(4.6)) assert_template_result("5", "{{ price | ceil }}", { 'price' => NumberLikeThing.new(4.6) })
end end
def test_floor def test_floor
assert_template_result("4", "{{ input | floor }}", 'input' => 4.6) assert_template_result("4", "{{ input | floor }}", { 'input' => 4.6 })
assert_template_result("4", "{{ '4.3' | floor }}") assert_template_result("4", "{{ '4.3' | floor }}")
assert_raises(Liquid::FloatDomainError) do assert_raises(Liquid::FloatDomainError) do
assert_template_result("4", "{{ 1.0 | divided_by: 0.0 | floor }}") assert_template_result("4", "{{ 1.0 | divided_by: 0.0 | floor }}")
end end
assert_template_result("5", "{{ price | floor }}", 'price' => NumberLikeThing.new(5.4)) assert_template_result("5", "{{ price | floor }}", { 'price' => NumberLikeThing.new(5.4) })
end end
def test_at_most def test_at_most
@@ -644,9 +816,9 @@ class StandardFiltersTest < Minitest::Test
assert_template_result("5", "{{ 5 | at_most:6 }}") assert_template_result("5", "{{ 5 | at_most:6 }}")
assert_template_result("4.5", "{{ 4.5 | at_most:5 }}") assert_template_result("4.5", "{{ 4.5 | at_most:5 }}")
assert_template_result("5", "{{ width | at_most:5 }}", 'width' => NumberLikeThing.new(6)) assert_template_result("5", "{{ width | at_most:5 }}", { 'width' => NumberLikeThing.new(6) })
assert_template_result("4", "{{ width | at_most:5 }}", 'width' => NumberLikeThing.new(4)) assert_template_result("4", "{{ width | at_most:5 }}", { 'width' => NumberLikeThing.new(4) })
assert_template_result("4", "{{ 5 | at_most: width }}", 'width' => NumberLikeThing.new(4)) assert_template_result("4", "{{ 5 | at_most: width }}", { 'width' => NumberLikeThing.new(4) })
end end
def test_at_least def test_at_least
@@ -655,9 +827,9 @@ class StandardFiltersTest < Minitest::Test
assert_template_result("6", "{{ 5 | at_least:6 }}") assert_template_result("6", "{{ 5 | at_least:6 }}")
assert_template_result("5", "{{ 4.5 | at_least:5 }}") assert_template_result("5", "{{ 4.5 | at_least:5 }}")
assert_template_result("6", "{{ width | at_least:5 }}", 'width' => NumberLikeThing.new(6)) assert_template_result("6", "{{ width | at_least:5 }}", { 'width' => NumberLikeThing.new(6) })
assert_template_result("5", "{{ width | at_least:5 }}", 'width' => NumberLikeThing.new(4)) assert_template_result("5", "{{ width | at_least:5 }}", { 'width' => NumberLikeThing.new(4) })
assert_template_result("6", "{{ 5 | at_least: width }}", 'width' => NumberLikeThing.new(6)) assert_template_result("6", "{{ 5 | at_least: width }}", { 'width' => NumberLikeThing.new(6) })
end end
def test_append def test_append
@@ -690,6 +862,8 @@ class StandardFiltersTest < Minitest::Test
assert_equal("bar", @filters.default([], "bar")) assert_equal("bar", @filters.default([], "bar"))
assert_equal("bar", @filters.default({}, "bar")) assert_equal("bar", @filters.default({}, "bar"))
assert_template_result('bar', "{{ false | default: 'bar' }}") assert_template_result('bar', "{{ false | default: 'bar' }}")
assert_template_result('bar', "{{ drop | default: 'bar' }}", { 'drop' => BooleanDrop.new(false) })
assert_template_result('Yay', "{{ drop | default: 'bar' }}", { 'drop' => BooleanDrop.new(true) })
end end
def test_default_handle_false def test_default_handle_false
@@ -700,6 +874,8 @@ class StandardFiltersTest < Minitest::Test
assert_equal("bar", @filters.default([], "bar", "allow_false" => true)) assert_equal("bar", @filters.default([], "bar", "allow_false" => true))
assert_equal("bar", @filters.default({}, "bar", "allow_false" => true)) assert_equal("bar", @filters.default({}, "bar", "allow_false" => true))
assert_template_result('false', "{{ false | default: 'bar', allow_false: true }}") assert_template_result('false', "{{ false | default: 'bar', allow_false: true }}")
assert_template_result('Nay', "{{ drop | default: 'bar', allow_false: true }}", { 'drop' => BooleanDrop.new(false) })
assert_template_result('Yay', "{{ drop | default: 'bar', allow_false: true }}", { 'drop' => BooleanDrop.new(true) })
end end
def test_cannot_access_private_methods def test_cannot_access_private_methods
@@ -711,21 +887,231 @@ class StandardFiltersTest < Minitest::Test
assert_template_result('abc', "{{ 'abc' | date: '%D' }}") assert_template_result('abc', "{{ 'abc' | date: '%D' }}")
end end
def test_where def test_reject
input = [ array = [
{ "handle" => "alpha", "ok" => true }, { "handle" => "alpha", "ok" => true },
{ "handle" => "beta", "ok" => false }, { "handle" => "beta", "ok" => false },
{ "handle" => "gamma", "ok" => false }, { "handle" => "gamma", "ok" => false },
{ "handle" => "delta", "ok" => true }, { "handle" => "delta", "ok" => true },
] ]
expectation = [ template = "{{ array | reject: 'ok' | map: 'handle' | join: ' ' }}"
expected_output = "beta gamma"
assert_template_result(expected_output, template, { "array" => array })
end
def test_reject_with_value
array = [
{ "handle" => "alpha", "ok" => true }, { "handle" => "alpha", "ok" => true },
{ "handle" => "beta", "ok" => false },
{ "handle" => "gamma", "ok" => false },
{ "handle" => "delta", "ok" => true }, { "handle" => "delta", "ok" => true },
] ]
assert_equal(expectation, @filters.where(input, "ok", true)) template = "{{ array | reject: 'ok', true | map: 'handle' | join: ' ' }}"
assert_equal(expectation, @filters.where(input, "ok")) expected_output = "beta gamma"
assert_template_result(expected_output, template, { "array" => array })
end
def test_reject_with_false_value
array = [
{ "handle" => "alpha", "ok" => true },
{ "handle" => "beta", "ok" => false },
{ "handle" => "gamma", "ok" => false },
{ "handle" => "delta", "ok" => true },
]
template = "{{ array | reject: 'ok', false | map: 'handle' | join: ' ' }}"
expected_output = "alpha delta"
assert_template_result(expected_output, template, { "array" => array })
end
def test_reject_with_deep_enumerables
template = <<~LIQUID
{{- products | reject: 'title.content', 'Pro goggles' | map: 'price.value' | join: ', ' -}}
LIQUID
expected_output = "1299, 3999, 3899, 1999"
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
end
def test_has
array = [
{ "handle" => "alpha", "ok" => true },
{ "handle" => "beta", "ok" => false },
{ "handle" => "gamma", "ok" => false },
{ "handle" => "delta", "ok" => false },
]
expected_output = "true"
assert_template_result(expected_output, "{{ array | has: 'ok' }}", { "array" => array })
assert_template_result(expected_output, "{{ array | has: 'ok', true }}", { "array" => array })
end
def test_has_when_does_not_have_it
array = [
{ "handle" => "alpha", "ok" => false },
{ "handle" => "beta", "ok" => false },
{ "handle" => "gamma", "ok" => false },
{ "handle" => "delta", "ok" => false },
]
expected_output = "false"
assert_template_result(expected_output, "{{ array | has: 'ok' }}", { "array" => array })
assert_template_result(expected_output, "{{ array | has: 'ok', true }}", { "array" => array })
end
def test_has_with_false_value
array = [
{ "handle" => "alpha", "ok" => true },
{ "handle" => "beta", "ok" => false },
{ "handle" => "gamma", "ok" => false },
{ "handle" => "delta", "ok" => true },
]
template = "{{ array | has: 'ok', false }}"
expected_output = "true"
assert_template_result(expected_output, template, { "array" => array })
end
def test_has_with_false_value_when_does_not_have_it
array = [
{ "handle" => "alpha", "ok" => true },
{ "handle" => "beta", "ok" => true },
{ "handle" => "gamma", "ok" => true },
{ "handle" => "delta", "ok" => true },
]
template = "{{ array | has: 'ok', false }}"
expected_output = "false"
assert_template_result(expected_output, template, { "array" => array })
end
def test_has_with_deep_enumerables
template = <<~LIQUID
{{- products | has: 'title.content', 'Pro goggles' -}},
{{- products | has: 'title.content', 'foo' -}}
LIQUID
expected_output = "true,false"
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
end
def test_find_with_value
products = [
{ "title" => "Pro goggles", "price" => 1299 },
{ "title" => "Thermal gloves", "price" => 1499 },
{ "title" => "Alpine jacket", "price" => 3999 },
{ "title" => "Mountain boots", "price" => 3899 },
{ "title" => "Safety helmet", "price" => 1999 }
]
template = <<~LIQUID
{%- assign product = products | find: 'price', 3999 -%}
{{- product.title -}}
LIQUID
expected_output = "Alpine jacket"
assert_template_result(expected_output, template, { "products" => products })
end
def test_find_with_deep_enumerables
template = <<~LIQUID
{%- assign product = products | find: 'title.content', 'Pro goggles' -%}
{{- product.title.content -}}
LIQUID
expected_output = "Pro goggles"
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
end
def test_find_index_with_value
products = [
{ "title" => "Pro goggles", "price" => 1299 },
{ "title" => "Thermal gloves", "price" => 1499 },
{ "title" => "Alpine jacket", "price" => 3999 },
{ "title" => "Mountain boots", "price" => 3899 },
{ "title" => "Safety helmet", "price" => 1999 }
]
template = <<~LIQUID
{%- assign index = products | find_index: 'price', 3999 -%}
{{- index -}}
LIQUID
expected_output = "2"
assert_template_result(expected_output, template, { "products" => products })
end
def test_find_index_with_deep_enumerables
template = <<~LIQUID
{%- assign index = products | find_index: 'title.content', 'Alpine jacket' -%}
{{- index -}}
LIQUID
expected_output = "2"
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
end
def test_where
array = [
{ "handle" => "alpha", "ok" => true },
{ "handle" => "beta", "ok" => false },
{ "handle" => "gamma", "ok" => false },
{ "handle" => "delta", "ok" => true },
]
template = "{{ array | where: 'ok' | map: 'handle' | join: ' ' }}"
expected_output = "alpha delta"
assert_template_result(expected_output, template, { "array" => array })
end
def test_where_with_value
array = [
{ "handle" => "alpha", "ok" => true },
{ "handle" => "beta", "ok" => false },
{ "handle" => "gamma", "ok" => false },
{ "handle" => "delta", "ok" => true },
]
template = "{{ array | where: 'ok', true | map: 'handle' | join: ' ' }}"
expected_output = "alpha delta"
assert_template_result(expected_output, template, { "array" => array })
end
def test_where_with_false_value
array = [
{ "handle" => "alpha", "ok" => true },
{ "handle" => "beta", "ok" => false },
{ "handle" => "gamma", "ok" => false },
{ "handle" => "delta", "ok" => true },
]
template = "{{ array | where: 'ok', false | map: 'handle' | join: ' ' }}"
expected_output = "beta gamma"
assert_template_result(expected_output, template, { "array" => array })
end
def test_where_string_keys
input = [
"alpha", "beta", "gamma", "delta"
]
expectation = [
"beta",
]
assert_equal(expectation, @filters.where(input, "be"))
end end
def test_where_no_key_set def test_where_no_key_set
@@ -772,8 +1158,17 @@ class StandardFiltersTest < Minitest::Test
assert_nil(@filters.where([nil], "ok")) assert_nil(@filters.where([nil], "ok"))
end end
def test_where_with_deep_enumerables
template = <<~LIQUID
{{- products | where: 'title.content', 'Pro goggles' | map: 'price.value' -}}
LIQUID
expected_output = "1299"
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
end
def test_all_filters_never_raise_non_liquid_exception def test_all_filters_never_raise_non_liquid_exception
test_drop = TestDrop.new test_drop = TestDrop.new(value: "test")
test_drop.context = Context.new test_drop.context = Context.new
test_enum = TestEnumerable.new test_enum = TestEnumerable.new
test_enum.context = Context.new test_enum.context = Context.new
@@ -798,22 +1193,17 @@ class StandardFiltersTest < Minitest::Test
{ 1 => "bar" }, { 1 => "bar" },
["foo", 123, nil, true, false, Drop, ["foo"], { foo: "bar" }], ["foo", 123, nil, true, false, Drop, ["foo"], { foo: "bar" }],
] ]
test_types.each do |first| StandardFilters.public_instance_methods(false).each do |method|
test_types.each do |other|
(@filters.methods - Object.methods).each do |method|
arg_count = @filters.method(method).arity arg_count = @filters.method(method).arity
arg_count *= -1 if arg_count < 0 arg_count *= -1 if arg_count < 0
inputs = [first]
inputs << ([other] * (arg_count - 1)) if arg_count > 1 test_types.repeated_permutation(arg_count) do |args|
begin @filters.send(method, *args)
@filters.send(method, *inputs) rescue Liquid::Error
rescue Liquid::ArgumentError, Liquid::ZeroDivisionError
nil nil
end end
end end
end end
end
end
def test_where_no_target_value def test_where_no_target_value
input = [ input = [
@@ -826,6 +1216,117 @@ class StandardFiltersTest < Minitest::Test
assert_equal([{ "foo" => true }, { "foo" => "for sure" }], @filters.where(input, "foo")) assert_equal([{ "foo" => true }, { "foo" => "for sure" }], @filters.where(input, "foo"))
end end
def test_sum_with_all_numbers
input = [1, 2]
assert_equal(3, @filters.sum(input))
assert_raises(Liquid::ArgumentError, "cannot select the property 'quantity'") do
@filters.sum(input, "quantity")
end
end
def test_sum_with_numeric_strings
input = [1, 2, "3", "4"]
assert_equal(10, @filters.sum(input))
assert_raises(Liquid::ArgumentError, "cannot select the property 'quantity'") do
@filters.sum(input, "quantity")
end
end
def test_sum_with_nested_arrays
input = [1, [2, [3, 4]]]
assert_equal(10, @filters.sum(input))
assert_raises(Liquid::ArgumentError, "cannot select the property 'quantity'") do
@filters.sum(input, "quantity")
end
end
def test_sum_with_indexable_map_values
input = [{ "quantity" => 1 }, { "quantity" => 2, "weight" => 3 }, { "weight" => 4 }]
assert_equal(0, @filters.sum(input))
assert_equal(3, @filters.sum(input, "quantity"))
assert_equal(7, @filters.sum(input, "weight"))
assert_equal(0, @filters.sum(input, "subtotal"))
end
def test_sum_with_indexable_non_map_values
input = [1, [2], "foo", { "quantity" => 3 }]
assert_equal(3, @filters.sum(input))
assert_raises(Liquid::ArgumentError, "cannot select the property 'quantity'") do
@filters.sum(input, "quantity")
end
end
def test_sum_with_unindexable_values
input = [1, true, nil, { "quantity" => 2 }]
assert_equal(1, @filters.sum(input))
assert_raises(Liquid::ArgumentError, "cannot select the property 'quantity'") do
@filters.sum(input, "quantity")
end
end
def test_sum_without_property_calls_to_liquid
t = TestThing.new
Liquid::Template.parse('{{ foo | sum }}').render("foo" => [t])
assert(t.foo > 0)
end
def test_sum_with_property_calls_to_liquid_on_property_values
t = TestThing.new
Liquid::Template.parse('{{ foo | sum: "quantity" }}').render("foo" => [{ "quantity" => t }])
assert(t.foo > 0)
end
def test_sum_of_floats
input = [0.1, 0.2, 0.3]
assert_equal(0.6, @filters.sum(input))
assert_template_result("0.6", "{{ input | sum }}", { "input" => input })
end
def test_sum_of_negative_floats
input = [0.1, 0.2, -0.3]
assert_equal(0.0, @filters.sum(input))
assert_template_result("0.0", "{{ input | sum }}", { "input" => input })
end
def test_sum_with_float_strings
input = [0.1, "0.2", "0.3"]
assert_equal(0.6, @filters.sum(input))
assert_template_result("0.6", "{{ input | sum }}", { "input" => input })
end
def test_sum_resulting_in_negative_float
input = [0.1, -0.2, -0.3]
assert_equal(-0.4, @filters.sum(input))
assert_template_result("-0.4", "{{ input | sum }}", { "input" => input })
end
def test_sum_with_floats_and_indexable_map_values
input = [{ "quantity" => 1 }, { "quantity" => 0.2, "weight" => -0.3 }, { "weight" => 0.4 }]
assert_equal(0.0, @filters.sum(input))
assert_equal(1.2, @filters.sum(input, "quantity"))
assert_equal(0.1, @filters.sum(input, "weight"))
assert_equal(0.0, @filters.sum(input, "subtotal"))
assert_template_result("0", "{{ input | sum }}", { "input" => input })
assert_template_result("1.2", "{{ input | sum: 'quantity' }}", { "input" => input })
assert_template_result("0.1", "{{ input | sum: 'weight' }}", { "input" => input })
assert_template_result("0", "{{ input | sum: 'subtotal' }}", { "input" => input })
end
def test_sum_with_deep_enumerables
template = <<~LIQUID
{{- products | sum: 'price.value' -}}
LIQUID
expected_output = "12495"
assert_template_result(expected_output, template, { "products" => TestDeepEnumerable.new })
end
private private
def with_timezone(tz) def with_timezone(tz)
+2 -2
View File
@@ -9,8 +9,8 @@ class BreakTagTest < Minitest::Test
# block # block
def test_break_with_no_block def test_break_with_no_block
assigns = { 'i' => 1 } assigns = { 'i' => 1 }
markup = '{% break %}' markup = 'before{% break %}after'
expected = '' expected = 'before'
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
end end
+48
View File
@@ -0,0 +1,48 @@
# frozen_string_literal: true
require 'test_helper'
class CycleTagTest < Minitest::Test
def test_simple_cycle
template = <<~LIQUID
{%- cycle '1', '2', '3' -%}
{%- cycle '1', '2', '3' -%}
{%- cycle '1', '2', '3' -%}
LIQUID
assert_template_result("123", template)
end
def test_simple_cycle_inside_for_loop
template = <<~LIQUID
{%- for i in (1..3) -%}
{% cycle '1', '2', '3' %}
{%- endfor -%}
LIQUID
assert_template_result("123", template)
end
def test_cycle_with_variables_inside_for_loop
template = <<~LIQUID
{%- assign a = 1 -%}
{%- assign b = 2 -%}
{%- assign c = 3 -%}
{%- for i in (1..3) -%}
{% cycle a, b, c %}
{%- endfor -%}
LIQUID
assert_template_result("123", template)
end
def test_cycle_tag_always_resets_cycle
template = <<~LIQUID
{%- assign a = "1" -%}
{%- cycle a, "2" -%}
{%- cycle a, "2" -%}
LIQUID
assert_template_result("11", template)
end
end
+1 -1
View File
@@ -6,7 +6,7 @@ class EchoTest < Minitest::Test
include Liquid include Liquid
def test_echo_outputs_its_input def test_echo_outputs_its_input
assert_template_result('BAR', <<~LIQUID, 'variable-name' => 'bar') assert_template_result('BAR', <<~LIQUID, { 'variable-name' => 'bar' })
{%- echo variable-name | upcase -%} {%- echo variable-name | upcase -%}
LIQUID LIQUID
end end
+61 -59
View File
@@ -12,10 +12,10 @@ class ForTagTest < Minitest::Test
include Liquid include Liquid
def test_for def test_for
assert_template_result(' yo yo yo yo ', '{%for item in array%} yo {%endfor%}', 'array' => [1, 2, 3, 4]) assert_template_result(' yo yo yo yo ', '{%for item in array%} yo {%endfor%}', { 'array' => [1, 2, 3, 4] })
assert_template_result('yoyo', '{%for item in array%}yo{%endfor%}', 'array' => [1, 2]) assert_template_result('yoyo', '{%for item in array%}yo{%endfor%}', { 'array' => [1, 2] })
assert_template_result(' yo ', '{%for item in array%} yo {%endfor%}', 'array' => [1]) assert_template_result(' yo ', '{%for item in array%} yo {%endfor%}', { 'array' => [1] })
assert_template_result('', '{%for item in array%}{%endfor%}', 'array' => [1, 2]) assert_template_result('', '{%for item in array%}{%endfor%}', { 'array' => [1, 2] })
expected = <<HERE expected = <<HERE
yo yo
@@ -30,7 +30,7 @@ HERE
yo yo
{%endfor%} {%endfor%}
HERE HERE
assert_template_result(expected, template, 'array' => [1, 2, 3]) assert_template_result(expected, template, { 'array' => [1, 2, 3] })
end end
def test_for_reversed def test_for_reversed
@@ -45,37 +45,39 @@ HERE
Template.parse('{% for i in (a..2) %}{% endfor %}').render!("a" => [1, 2]) Template.parse('{% for i in (a..2) %}{% endfor %}').render!("a" => [1, 2])
end end
assert_template_result(' 0 1 2 3 ', '{% for item in (a..3) %} {{item}} {% endfor %}', "a" => "invalid integer") assert_template_result(' 0 1 2 3 ', '{% for item in (a..3) %} {{item}} {% endfor %}', { "a" => "invalid integer" })
end end
def test_for_with_variable_range def test_for_with_variable_range
assert_template_result(' 1 2 3 ', '{%for item in (1..foobar) %} {{item}} {%endfor%}', "foobar" => 3) assert_template_result(' 1 2 3 ', '{%for item in (1..foobar) %} {{item}} {%endfor%}', { "foobar" => 3 })
end end
def test_for_with_hash_value_range def test_for_with_hash_value_range
foobar = { "value" => 3 } foobar = { "value" => 3 }
assert_template_result(' 1 2 3 ', '{%for item in (1..foobar.value) %} {{item}} {%endfor%}', "foobar" => foobar) assert_template_result(' 1 2 3 ', '{%for item in (1..foobar.value) %} {{item}} {%endfor%}', { "foobar" => foobar })
end end
def test_for_with_drop_value_range def test_for_with_drop_value_range
foobar = ThingWithValue.new foobar = ThingWithValue.new
assert_template_result(' 1 2 3 ', '{%for item in (1..foobar.value) %} {{item}} {%endfor%}', "foobar" => foobar) assert_template_result(' 1 2 3 ', '{%for item in (1..foobar.value) %} {{item}} {%endfor%}', { "foobar" => foobar })
end end
def test_for_with_variable def test_for_with_variable
assert_template_result(' 1 2 3 ', '{%for item in array%} {{item}} {%endfor%}', 'array' => [1, 2, 3]) assert_template_result(' 1 2 3 ', '{%for item in array%} {{item}} {%endfor%}', { 'array' => [1, 2, 3] })
assert_template_result('123', '{%for item in array%}{{item}}{%endfor%}', 'array' => [1, 2, 3]) assert_template_result('123', '{%for item in array%}{{item}}{%endfor%}', { 'array' => [1, 2, 3] })
assert_template_result('123', '{% for item in array %}{{item}}{% endfor %}', 'array' => [1, 2, 3]) assert_template_result('123', '{% for item in array %}{{item}}{% endfor %}', { 'array' => [1, 2, 3] })
assert_template_result('abcd', '{%for item in array%}{{item}}{%endfor%}', 'array' => ['a', 'b', 'c', 'd']) assert_template_result('abcd', '{%for item in array%}{{item}}{%endfor%}', { 'array' => ['a', 'b', 'c', 'd'] })
assert_template_result('a b c', '{%for item in array%}{{item}}{%endfor%}', 'array' => ['a', ' ', 'b', ' ', 'c']) assert_template_result('a b c', '{%for item in array%}{{item}}{%endfor%}', { 'array' => ['a', ' ', 'b', ' ', 'c'] })
assert_template_result('abc', '{%for item in array%}{{item}}{%endfor%}', 'array' => ['a', '', 'b', '', 'c']) assert_template_result('abc', '{%for item in array%}{{item}}{%endfor%}', { 'array' => ['a', '', 'b', '', 'c'] })
end end
def test_for_helpers def test_for_helpers
assigns = { 'array' => [1, 2, 3] } assigns = { 'array' => [1, 2, 3] }
assert_template_result(' 1/3 2/3 3/3 ', assert_template_result(
' 1/3 2/3 3/3 ',
'{%for item in array%} {{forloop.index}}/{{forloop.length}} {%endfor%}', '{%for item in array%} {{forloop.index}}/{{forloop.length}} {%endfor%}',
assigns) assigns,
)
assert_template_result(' 1 2 3 ', '{%for item in array%} {{forloop.index}} {%endfor%}', assigns) assert_template_result(' 1 2 3 ', '{%for item in array%} {{forloop.index}} {%endfor%}', assigns)
assert_template_result(' 0 1 2 ', '{%for item in array%} {{forloop.index0}} {%endfor%}', assigns) assert_template_result(' 0 1 2 ', '{%for item in array%} {{forloop.index0}} {%endfor%}', assigns)
assert_template_result(' 2 1 0 ', '{%for item in array%} {{forloop.rindex0}} {%endfor%}', assigns) assert_template_result(' 2 1 0 ', '{%for item in array%} {{forloop.rindex0}} {%endfor%}', assigns)
@@ -86,15 +88,17 @@ HERE
def test_for_and_if def test_for_and_if
assigns = { 'array' => [1, 2, 3] } assigns = { 'array' => [1, 2, 3] }
assert_template_result('+--', assert_template_result(
'+--',
'{%for item in array%}{% if forloop.first %}+{% else %}-{% endif %}{%endfor%}', '{%for item in array%}{% if forloop.first %}+{% else %}-{% endif %}{%endfor%}',
assigns) assigns,
)
end end
def test_for_else def test_for_else
assert_template_result('+++', '{%for item in array%}+{%else%}-{%endfor%}', 'array' => [1, 2, 3]) assert_template_result('+++', '{%for item in array%}+{%else%}-{%endfor%}', { 'array' => [1, 2, 3] })
assert_template_result('-', '{%for item in array%}+{%else%}-{%endfor%}', 'array' => []) assert_template_result('-', '{%for item in array%}+{%else%}-{%endfor%}', { 'array' => [] })
assert_template_result('-', '{%for item in array%}+{%else%}-{%endfor%}', 'array' => nil) assert_template_result('-', '{%for item in array%}+{%else%}-{%endfor%}', { 'array' => nil })
end end
def test_limiting def test_limiting
@@ -103,6 +107,7 @@ HERE
assert_template_result('1234', '{%for i in array limit:4 %}{{ i }}{%endfor%}', assigns) assert_template_result('1234', '{%for i in array limit:4 %}{{ i }}{%endfor%}', assigns)
assert_template_result('3456', '{%for i in array limit:4 offset:2 %}{{ i }}{%endfor%}', assigns) assert_template_result('3456', '{%for i in array limit:4 offset:2 %}{{ i }}{%endfor%}', assigns)
assert_template_result('3456', '{%for i in array limit: 4 offset: 2 %}{{ i }}{%endfor%}', assigns) assert_template_result('3456', '{%for i in array limit: 4 offset: 2 %}{{ i }}{%endfor%}', assigns)
assert_template_result('3456', '{%for i in array, limit: 4, offset: 2 %}{{ i }}{%endfor%}', assigns)
end end
def test_limiting_with_invalid_limit def test_limiting_with_invalid_limit
@@ -263,6 +268,19 @@ HERE
assert_template_result(expected, markup, assigns) assert_template_result(expected, markup, assigns)
end end
def test_for_with_break_after_nested_loop
source = <<~LIQUID.chomp
{% for i in (1..2) -%}
{% for j in (1..2) -%}
{{ i }}-{{ j }},
{%- endfor -%}
{% break -%}
{% endfor -%}
after
LIQUID
assert_template_result("1-1,1-2,after", source)
end
def test_for_with_continue def test_for_with_continue
assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5] } } assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5] } }
@@ -311,15 +329,20 @@ HERE
# ruby 1.9.3 no longer supports .each on String though we mimic # ruby 1.9.3 no longer supports .each on String though we mimic
# the functionality for backwards compatibility # the functionality for backwards compatibility
assert_template_result('test string', assert_template_result(
'test string',
'{%for val in string%}{{val}}{%endfor%}', '{%for val in string%}{{val}}{%endfor%}',
'string' => "test string") { 'string' => "test string" },
)
assert_template_result('test string', assert_template_result(
'test string',
'{%for val in string limit:1%}{{val}}{%endfor%}', '{%for val in string limit:1%}{{val}}{%endfor%}',
'string' => "test string") { 'string' => "test string" },
)
assert_template_result('val-string-1-1-0-1-0-true-true-test string', assert_template_result(
'val-string-1-1-0-1-0-true-true-test string',
'{%for val in string%}' \ '{%for val in string%}' \
'{{forloop.name}}-' \ '{{forloop.name}}-' \
'{{forloop.index}}-' \ '{{forloop.index}}-' \
@@ -330,23 +353,28 @@ HERE
'{{forloop.first}}-' \ '{{forloop.first}}-' \
'{{forloop.last}}-' \ '{{forloop.last}}-' \
'{{val}}{%endfor%}', '{{val}}{%endfor%}',
'string' => "test string") { 'string' => "test string" },
)
end end
def test_for_parentloop_references_parent_loop def test_for_parentloop_references_parent_loop
assert_template_result('1.1 1.2 1.3 2.1 2.2 2.3 ', assert_template_result(
'1.1 1.2 1.3 2.1 2.2 2.3 ',
'{% for inner in outer %}{% for k in inner %}' \ '{% for inner in outer %}{% for k in inner %}' \
'{{ forloop.parentloop.index }}.{{ forloop.index }} ' \ '{{ forloop.parentloop.index }}.{{ forloop.index }} ' \
'{% endfor %}{% endfor %}', '{% endfor %}{% endfor %}',
'outer' => [[1, 1, 1], [1, 1, 1]]) { 'outer' => [[1, 1, 1], [1, 1, 1]] },
)
end end
def test_for_parentloop_nil_when_not_present def test_for_parentloop_nil_when_not_present
assert_template_result('.1 .2 ', assert_template_result(
'.1 .2 ',
'{% for inner in outer %}' \ '{% for inner in outer %}' \
'{{ forloop.parentloop.index }}.{{ forloop.index }} ' \ '{{ forloop.parentloop.index }}.{{ forloop.index }} ' \
'{% endfor %}', '{% endfor %}',
'outer' => [[1, 1, 1], [1, 1, 1]]) { 'outer' => [[1, 1, 1], [1, 1, 1]] },
)
end end
def test_inner_for_over_empty_input def test_inner_for_over_empty_input
@@ -354,7 +382,7 @@ HERE
end end
def test_blank_string_not_iterable def test_blank_string_not_iterable
assert_template_result('', "{% for char in characters %}I WILL NOT BE OUTPUT{% endfor %}", 'characters' => '') assert_template_result('', "{% for char in characters %}I WILL NOT BE OUTPUT{% endfor %}", { 'characters' => '' })
end end
def test_bad_variable_naming_in_for_loop def test_bad_variable_naming_in_for_loop
@@ -437,30 +465,4 @@ HERE
assert(context.registers[:for_stack].empty?) assert(context.registers[:for_stack].empty?)
end end
def test_instrument_for_offset_continue
assert_usage_increment('for_offset_continue') do
Template.parse('{% for item in items offset:continue %}{{item}}{% endfor %}')
end
assert_usage_increment('for_offset_continue', times: 0) do
Template.parse('{% for item in items offset:2 %}{{item}}{% endfor %}')
end
end
def test_instrument_forloop_drop_name
assigns = { 'items' => [1, 2, 3, 4, 5] }
assert_usage_increment('forloop_drop_name', times: 5) do
Template.parse('{% for item in items %}{{forloop.name}}{% endfor %}').render!(assigns)
end
assert_usage_increment('forloop_drop_name', times: 0) do
Template.parse('{% for item in items %}{{forloop.index}}{% endfor %}').render!(assigns)
end
assert_usage_increment('forloop_drop_name', times: 0) do
Template.parse('{% for item in items %}{{item}}{% endfor %}').render!(assigns)
end
end
end end
+55 -47
View File
@@ -7,8 +7,10 @@ class IfElseTagTest < Minitest::Test
def test_if def test_if
assert_template_result(' ', ' {% if false %} this text should not go into the output {% endif %} ') assert_template_result(' ', ' {% if false %} this text should not go into the output {% endif %} ')
assert_template_result(' this text should go into the output ', assert_template_result(
' {% if true %} this text should go into the output {% endif %} ') ' this text should go into the output ',
' {% if true %} this text should go into the output {% endif %} ',
)
assert_template_result(' you rock ?', '{% if false %} you suck {% endif %} {% if true %} you rock {% endif %}?') assert_template_result(' you rock ?', '{% if false %} you suck {% endif %} {% if true %} you rock {% endif %}?')
end end
@@ -24,23 +26,23 @@ class IfElseTagTest < Minitest::Test
end end
def test_if_boolean def test_if_boolean
assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => true) assert_template_result(' YES ', '{% if var %} YES {% endif %}', { 'var' => true })
end end
def test_if_or def test_if_or
assert_template_result(' YES ', '{% if a or b %} YES {% endif %}', 'a' => true, 'b' => true) assert_template_result(' YES ', '{% if a or b %} YES {% endif %}', { 'a' => true, 'b' => true })
assert_template_result(' YES ', '{% if a or b %} YES {% endif %}', 'a' => true, 'b' => false) assert_template_result(' YES ', '{% if a or b %} YES {% endif %}', { 'a' => true, 'b' => false })
assert_template_result(' YES ', '{% if a or b %} YES {% endif %}', 'a' => false, 'b' => true) assert_template_result(' YES ', '{% if a or b %} YES {% endif %}', { 'a' => false, 'b' => true })
assert_template_result('', '{% if a or b %} YES {% endif %}', 'a' => false, 'b' => false) assert_template_result('', '{% if a or b %} YES {% endif %}', { 'a' => false, 'b' => false })
assert_template_result(' YES ', '{% if a or b or c %} YES {% endif %}', 'a' => false, 'b' => false, 'c' => true) assert_template_result(' YES ', '{% if a or b or c %} YES {% endif %}', { 'a' => false, 'b' => false, 'c' => true })
assert_template_result('', '{% if a or b or c %} YES {% endif %}', 'a' => false, 'b' => false, 'c' => false) assert_template_result('', '{% if a or b or c %} YES {% endif %}', { 'a' => false, 'b' => false, 'c' => false })
end end
def test_if_or_with_operators def test_if_or_with_operators
assert_template_result(' YES ', '{% if a == true or b == true %} YES {% endif %}', 'a' => true, 'b' => true) assert_template_result(' YES ', '{% if a == true or b == true %} YES {% endif %}', { 'a' => true, 'b' => true })
assert_template_result(' YES ', '{% if a == true or b == false %} YES {% endif %}', 'a' => true, 'b' => true) assert_template_result(' YES ', '{% if a == true or b == false %} YES {% endif %}', { 'a' => true, 'b' => true })
assert_template_result('', '{% if a == false or b == false %} YES {% endif %}', 'a' => true, 'b' => true) assert_template_result('', '{% if a == false or b == false %} YES {% endif %}', { 'a' => true, 'b' => true })
end end
def test_comparison_of_strings_containing_and_or_or def test_comparison_of_strings_containing_and_or_or
@@ -51,55 +53,59 @@ class IfElseTagTest < Minitest::Test
def test_comparison_of_expressions_starting_with_and_or_or def test_comparison_of_expressions_starting_with_and_or_or
assigns = { 'order' => { 'items_count' => 0 }, 'android' => { 'name' => 'Roy' } } assigns = { 'order' => { 'items_count' => 0 }, 'android' => { 'name' => 'Roy' } }
assert_template_result("YES", assert_template_result(
"YES",
"{% if android.name == 'Roy' %}YES{% endif %}", "{% if android.name == 'Roy' %}YES{% endif %}",
assigns) assigns,
assert_template_result("YES", )
assert_template_result(
"YES",
"{% if order.items_count == 0 %}YES{% endif %}", "{% if order.items_count == 0 %}YES{% endif %}",
assigns) assigns,
)
end end
def test_if_and def test_if_and
assert_template_result(' YES ', '{% if true and true %} YES {% endif %}') assert_template_result(' YES ', '{% if true and true %} YES {% endif %}')
assert_template_result('', '{% if false and true %} YES {% endif %}') assert_template_result('', '{% if false and true %} YES {% endif %}')
assert_template_result('', '{% if false and true %} YES {% endif %}') assert_template_result('', '{% if true and false %} YES {% endif %}')
end end
def test_hash_miss_generates_false def test_hash_miss_generates_false
assert_template_result('', '{% if foo.bar %} NO {% endif %}', 'foo' => {}) assert_template_result('', '{% if foo.bar %} NO {% endif %}', { 'foo' => {} })
end end
def test_if_from_variable def test_if_from_variable
assert_template_result('', '{% if var %} NO {% endif %}', 'var' => false) assert_template_result('', '{% if var %} NO {% endif %}', { 'var' => false })
assert_template_result('', '{% if var %} NO {% endif %}', 'var' => nil) assert_template_result('', '{% if var %} NO {% endif %}', { 'var' => nil })
assert_template_result('', '{% if foo.bar %} NO {% endif %}', 'foo' => { 'bar' => false }) assert_template_result('', '{% if foo.bar %} NO {% endif %}', { 'foo' => { 'bar' => false } })
assert_template_result('', '{% if foo.bar %} NO {% endif %}', 'foo' => {}) assert_template_result('', '{% if foo.bar %} NO {% endif %}', { 'foo' => {} })
assert_template_result('', '{% if foo.bar %} NO {% endif %}', 'foo' => nil) assert_template_result('', '{% if foo.bar %} NO {% endif %}', { 'foo' => nil })
assert_template_result('', '{% if foo.bar %} NO {% endif %}', 'foo' => true) assert_template_result('', '{% if foo.bar %} NO {% endif %}', { 'foo' => true })
assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => "text") assert_template_result(' YES ', '{% if var %} YES {% endif %}', { 'var' => "text" })
assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => true) assert_template_result(' YES ', '{% if var %} YES {% endif %}', { 'var' => true })
assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => 1) assert_template_result(' YES ', '{% if var %} YES {% endif %}', { 'var' => 1 })
assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => {}) assert_template_result(' YES ', '{% if var %} YES {% endif %}', { 'var' => {} })
assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => []) assert_template_result(' YES ', '{% if var %} YES {% endif %}', { 'var' => [] })
assert_template_result(' YES ', '{% if "foo" %} YES {% endif %}') assert_template_result(' YES ', '{% if "foo" %} YES {% endif %}')
assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => true }) assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', { 'foo' => { 'bar' => true } })
assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => "text" }) assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', { 'foo' => { 'bar' => "text" } })
assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => 1 }) assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', { 'foo' => { 'bar' => 1 } })
assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => {} }) assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', { 'foo' => { 'bar' => {} } })
assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', 'foo' => { 'bar' => [] }) assert_template_result(' YES ', '{% if foo.bar %} YES {% endif %}', { 'foo' => { 'bar' => [] } })
assert_template_result(' YES ', '{% if var %} NO {% else %} YES {% endif %}', 'var' => false) assert_template_result(' YES ', '{% if var %} NO {% else %} YES {% endif %}', { 'var' => false })
assert_template_result(' YES ', '{% if var %} NO {% else %} YES {% endif %}', 'var' => nil) assert_template_result(' YES ', '{% if var %} NO {% else %} YES {% endif %}', { 'var' => nil })
assert_template_result(' YES ', '{% if var %} YES {% else %} NO {% endif %}', 'var' => true) assert_template_result(' YES ', '{% if var %} YES {% else %} NO {% endif %}', { 'var' => true })
assert_template_result(' YES ', '{% if "foo" %} YES {% else %} NO {% endif %}', 'var' => "text") assert_template_result(' YES ', '{% if "foo" %} YES {% else %} NO {% endif %}', { 'var' => "text" })
assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => { 'bar' => false }) assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', { 'foo' => { 'bar' => false } })
assert_template_result(' YES ', '{% if foo.bar %} YES {% else %} NO {% endif %}', 'foo' => { 'bar' => true }) assert_template_result(' YES ', '{% if foo.bar %} YES {% else %} NO {% endif %}', { 'foo' => { 'bar' => true } })
assert_template_result(' YES ', '{% if foo.bar %} YES {% else %} NO {% endif %}', 'foo' => { 'bar' => "text" }) assert_template_result(' YES ', '{% if foo.bar %} YES {% else %} NO {% endif %}', { 'foo' => { 'bar' => "text" } })
assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => { 'notbar' => true }) assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', { 'foo' => { 'notbar' => true } })
assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', 'foo' => {}) assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', { 'foo' => {} })
assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', 'notfoo' => { 'bar' => true }) assert_template_result(' YES ', '{% if foo.bar %} NO {% else %} YES {% endif %}', { 'notfoo' => { 'bar' => true } })
end end
def test_nested_if def test_nested_if
@@ -155,8 +161,10 @@ class IfElseTagTest < Minitest::Test
original_op = Condition.operators['contains'] original_op = Condition.operators['contains']
Condition.operators['contains'] = :[] Condition.operators['contains'] = :[]
assert_template_result('yes', assert_template_result(
%({% if 'gnomeslab-and-or-liquid' contains 'gnomeslab-and-or-liquid' %}yes{% endif %})) 'yes',
%({% if 'gnomeslab-and-or-liquid' contains 'gnomeslab-and-or-liquid' %}yes{% endif %}),
)
ensure ensure
Condition.operators['contains'] = original_op Condition.operators['contains'] = original_op
end end
@@ -184,7 +192,7 @@ class IfElseTagTest < Minitest::Test
tests.each do |vals, expected| tests.each do |vals, expected|
a, b, c = vals a, b, c = vals
assigns = { 'a' => a, 'b' => b, 'c' => c } assigns = { 'a' => a, 'b' => b, 'c' => c }
assert_template_result(expected.to_s, tpl, assigns, assigns.to_s) assert_template_result(expected.to_s, tpl, assigns, message: assigns.to_s)
end end
end end
end end
+208 -100
View File
@@ -3,44 +3,13 @@
require 'test_helper' require 'test_helper'
class TestFileSystem class TestFileSystem
PARTIALS = {
"nested_template" => "{% include 'header' %} {% include 'body' %} {% include 'footer' %}",
"body" => "body {% include 'body_detail' %}",
}
def read_template_file(template_path) def read_template_file(template_path)
case template_path PARTIALS[template_path] || template_path
when "product"
"Product: {{ product.title }} "
when "product_alias"
"Product: {{ product.title }} "
when "locale_variables"
"Locale: {{echo1}} {{echo2}}"
when "variant"
"Variant: {{ variant.title }}"
when "nested_template"
"{% include 'header' %} {% include 'body' %} {% include 'footer' %}"
when "body"
"body {% include 'body_detail' %}"
when "nested_product_template"
"Product: {{ nested_product_template.title }} {%include 'details'%} "
when "recursively_nested_template"
"-{% include 'recursively_nested_template' %}"
when "pick_a_source"
"from TestFileSystem"
when 'assignments'
"{% assign foo = 'bar' %}"
when 'break'
"{% break %}"
else
template_path
end
end end
end end
@@ -80,71 +49,122 @@ end
class IncludeTagTest < Minitest::Test class IncludeTagTest < Minitest::Test
include Liquid include Liquid
def setup
Liquid::Template.file_system = TestFileSystem.new
end
def test_include_tag_looks_for_file_system_in_registers_first def test_include_tag_looks_for_file_system_in_registers_first
assert_equal('from OtherFileSystem', assert_equal(
Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: OtherFileSystem.new })) 'from OtherFileSystem',
Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: OtherFileSystem.new }),
)
end end
def test_include_tag_with def test_include_tag_with
assert_template_result("Product: Draft 151cm ", assert_template_result(
"{% include 'product' with products[0] %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }]) "Product: Draft 151cm ",
"{% include 'product' with products[0] %}",
{ "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] },
partials: { "product" => "Product: {{ product.title }} " },
)
end end
def test_include_tag_with_alias def test_include_tag_with_alias
assert_template_result("Product: Draft 151cm ", assert_template_result(
"{% include 'product_alias' with products[0] as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }]) "Product: Draft 151cm ",
"{% include 'product_alias' with products[0] as product %}",
{ "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] },
partials: { "product_alias" => "Product: {{ product.title }} " },
)
end end
def test_include_tag_for_alias def test_include_tag_for_alias
assert_template_result("Product: Draft 151cm Product: Element 155cm ", assert_template_result(
"{% include 'product_alias' for products as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }]) "Product: Draft 151cm Product: Element 155cm ",
"{% include 'product_alias' for products as product %}",
{ "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] },
partials: { "product_alias" => "Product: {{ product.title }} " },
)
end end
def test_include_tag_with_default_name def test_include_tag_with_default_name
assert_template_result("Product: Draft 151cm ", assert_template_result(
"{% include 'product' %}", "product" => { 'title' => 'Draft 151cm' }) "Product: Draft 151cm ",
"{% include 'product' %}",
{ "product" => { 'title' => 'Draft 151cm' } },
partials: { "product" => "Product: {{ product.title }} " },
)
end end
def test_include_tag_for def test_include_tag_for
assert_template_result("Product: Draft 151cm Product: Element 155cm ", assert_template_result(
"{% include 'product' for products %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }]) "Product: Draft 151cm Product: Element 155cm ",
"{% include 'product' for products %}",
{ "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }] },
partials: { "product" => "Product: {{ product.title }} " },
)
end end
def test_include_tag_with_local_variables def test_include_tag_with_local_variables
assert_template_result("Locale: test123 ", "{% include 'locale_variables' echo1: 'test123' %}") assert_template_result(
"Locale: test123 ",
"{% include 'locale_variables' echo1: 'test123' %}",
partials: { "locale_variables" => "Locale: {{echo1}} {{echo2}}" },
)
end end
def test_include_tag_with_multiple_local_variables def test_include_tag_with_multiple_local_variables
assert_template_result("Locale: test123 test321", assert_template_result(
"{% include 'locale_variables' echo1: 'test123', echo2: 'test321' %}") "Locale: test123 test321",
"{% include 'locale_variables' echo1: 'test123', echo2: 'test321' %}",
partials: { "locale_variables" => "Locale: {{echo1}} {{echo2}}" },
)
end end
def test_include_tag_with_multiple_local_variables_from_context def test_include_tag_with_multiple_local_variables_from_context
assert_template_result("Locale: test123 test321", assert_template_result(
"Locale: test123 test321",
"{% include 'locale_variables' echo1: echo1, echo2: more_echos.echo2 %}", "{% include 'locale_variables' echo1: echo1, echo2: more_echos.echo2 %}",
'echo1' => 'test123', 'more_echos' => { "echo2" => 'test321' }) { 'echo1' => 'test123', 'more_echos' => { "echo2" => 'test321' } },
partials: { "locale_variables" => "Locale: {{echo1}} {{echo2}}" },
)
end end
def test_included_templates_assigns_variables def test_included_templates_assigns_variables
assert_template_result("bar", "{% include 'assignments' %}{{ foo }}") assert_template_result(
"bar",
"{% include 'assignments' %}{{ foo }}",
partials: { 'assignments' => "{% assign foo = 'bar' %}" },
)
end end
def test_nested_include_tag def test_nested_include_tag
assert_template_result("body body_detail", "{% include 'body' %}") partials = { "body" => "body {% include 'body_detail' %}", "body_detail" => "body_detail" }
assert_template_result("body body_detail", "{% include 'body' %}", partials: partials)
assert_template_result("header body body_detail footer", "{% include 'nested_template' %}") partials = partials.merge({
"nested_template" => "{% include 'header' %} {% include 'body' %} {% include 'footer' %}",
"header" => "header",
"footer" => "footer",
})
assert_template_result("header body body_detail footer", "{% include 'nested_template' %}", partials: partials)
end end
def test_nested_include_with_variable def test_nested_include_with_variable
assert_template_result("Product: Draft 151cm details ", partials = {
"{% include 'nested_product_template' with product %}", "product" => { "title" => 'Draft 151cm' }) "nested_product_template" => "Product: {{ nested_product_template.title }} {%include 'details'%} ",
"details" => "details",
}
assert_template_result("Product: Draft 151cm details Product: Element 155cm details ", assert_template_result(
"{% include 'nested_product_template' for products %}", "products" => [{ "title" => 'Draft 151cm' }, { "title" => 'Element 155cm' }]) "Product: Draft 151cm details ",
"{% include 'nested_product_template' with product %}",
{ "product" => { "title" => 'Draft 151cm' } },
partials: partials,
)
assert_template_result(
"Product: Draft 151cm details Product: Element 155cm details ",
"{% include 'nested_product_template' for products %}",
{ "products" => [{ "title" => 'Draft 151cm' }, { "title" => 'Element 155cm' }] },
partials: partials,
)
end end
def test_recursively_included_template_does_not_produce_endless_loop def test_recursively_included_template_does_not_produce_endless_loop
@@ -154,49 +174,77 @@ class IncludeTagTest < Minitest::Test
end end
end end
Liquid::Template.file_system = infinite_file_system.new env = Liquid::Environment.build(file_system: infinite_file_system.new)
assert_raises(Liquid::StackLevelError) do assert_raises(Liquid::StackLevelError) do
Template.parse("{% include 'loop' %}").render! Template.parse("{% include 'loop' %}", environment: env).render!
end end
end end
def test_dynamically_choosen_template def test_dynamically_choosen_template
assert_template_result("Test123", "{% include template %}", "template" => 'Test123') assert_template_result(
assert_template_result("Test321", "{% include template %}", "template" => 'Test321') "Test123",
"{% include template %}",
{ "template" => 'Test123' },
partials: { "Test123" => "Test123" },
)
assert_template_result("Product: Draft 151cm ", "{% include template for product %}", assert_template_result(
"template" => 'product', 'product' => { 'title' => 'Draft 151cm' }) "Test321",
"{% include template %}",
{ "template" => 'Test321' },
partials: { "Test321" => "Test321" },
)
assert_template_result(
"Product: Draft 151cm ",
"{% include template for product %}",
{ "template" => 'product', 'product' => { 'title' => 'Draft 151cm' } },
partials: { "product" => "Product: {{ product.title }} " },
)
end end
def test_include_tag_caches_second_read_of_same_partial def test_include_tag_caches_second_read_of_same_partial
file_system = CountingFileSystem.new file_system = CountingFileSystem.new
assert_equal('from CountingFileSystemfrom CountingFileSystem', environment = Liquid::Environment.build(file_system: file_system)
Template.parse("{% include 'pick_a_source' %}{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system })) assert_equal(
'from CountingFileSystemfrom CountingFileSystem',
Template.parse("{% include 'pick_a_source' %}{% include 'pick_a_source' %}", environment: environment).render!({}, registers: { file_system: file_system }),
)
assert_equal(1, file_system.count) assert_equal(1, file_system.count)
end end
def test_include_tag_doesnt_cache_partials_across_renders def test_include_tag_doesnt_cache_partials_across_renders
file_system = CountingFileSystem.new file_system = CountingFileSystem.new
assert_equal('from CountingFileSystem', assert_equal(
Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system })) 'from CountingFileSystem',
Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system }),
)
assert_equal(1, file_system.count) assert_equal(1, file_system.count)
assert_equal('from CountingFileSystem', assert_equal(
Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system })) 'from CountingFileSystem',
Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system }),
)
assert_equal(2, file_system.count) assert_equal(2, file_system.count)
end end
def test_include_tag_within_if_statement def test_include_tag_within_if_statement
assert_template_result("foo_if_true", "{% if true %}{% include 'foo_if_true' %}{% endif %}") assert_template_result(
"foo_if_true",
"{% if true %}{% include 'foo_if_true' %}{% endif %}",
partials: { "foo_if_true" => "foo_if_true" },
)
end end
def test_custom_include_tag def test_custom_include_tag
original_tag = Liquid::Template.tags['include'] original_tag = Liquid::Template.tags['include']
Liquid::Template.tags['include'] = CustomInclude Liquid::Template.tags['include'] = CustomInclude
begin begin
assert_equal("custom_foo", assert_equal(
Template.parse("{% include 'custom_foo' %}").render!) "custom_foo",
Template.parse("{% include 'custom_foo' %}").render!,
)
ensure ensure
Liquid::Template.tags['include'] = original_tag Liquid::Template.tags['include'] = original_tag
end end
@@ -206,57 +254,92 @@ class IncludeTagTest < Minitest::Test
original_tag = Liquid::Template.tags['include'] original_tag = Liquid::Template.tags['include']
Liquid::Template.tags['include'] = CustomInclude Liquid::Template.tags['include'] = CustomInclude
begin begin
assert_equal("custom_foo_if_true", assert_equal(
Template.parse("{% if true %}{% include 'custom_foo_if_true' %}{% endif %}").render!) "custom_foo_if_true",
Template.parse("{% if true %}{% include 'custom_foo_if_true' %}{% endif %}").render!,
)
ensure ensure
Liquid::Template.tags['include'] = original_tag Liquid::Template.tags['include'] = original_tag
end end
end end
def test_does_not_add_error_in_strict_mode_for_missing_variable def test_does_not_add_error_in_strict_mode_for_missing_variable
Liquid::Template.file_system = TestFileSystem.new env = Liquid::Environment.build(file_system: TestFileSystem.new)
a = Liquid::Template.parse(' {% include "nested_template" %}') a = Liquid::Template.parse(' {% include "nested_template" %}', environment: env)
a.render! a.render!
assert_empty(a.errors) assert_empty(a.errors)
end end
def test_passing_options_to_included_templates def test_passing_options_to_included_templates
env = Liquid::Environment.build(file_system: TestFileSystem.new)
assert_raises(Liquid::SyntaxError) do assert_raises(Liquid::SyntaxError) do
Template.parse("{% include template %}", error_mode: :strict).render!("template" => '{{ "X" || downcase }}') Template.parse("{% include template %}", error_mode: :strict, environment: env).render!("template" => '{{ "X" || downcase }}')
end end
with_error_mode(:lax) do with_error_mode(:lax) do
assert_equal('x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: true).render!("template" => '{{ "X" || downcase }}')) assert_equal('x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: true, environment: env).render!("template" => '{{ "X" || downcase }}'))
end end
assert_raises(Liquid::SyntaxError) do assert_raises(Liquid::SyntaxError) do
Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:locale]).render!("template" => '{{ "X" || downcase }}') Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:locale], environment: env).render!("template" => '{{ "X" || downcase }}')
end end
with_error_mode(:lax) do with_error_mode(:lax) do
assert_equal('x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:error_mode]).render!("template" => '{{ "X" || downcase }}')) assert_equal('x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:error_mode], environment: env).render!("template" => '{{ "X" || downcase }}'))
end end
end end
def test_render_raise_argument_error_when_template_is_undefined def test_render_raise_argument_error_when_template_is_undefined
assert_raises(Liquid::ArgumentError) do assert_template_result(
template = Liquid::Template.parse('{% include undefined_variable %}') "Liquid error (line 1): Argument error in tag 'include' - Illegal template name",
template.render! "{% include undefined_variable %}",
end render_errors: true,
assert_raises(Liquid::ArgumentError) do )
template = Liquid::Template.parse('{% include nil %}')
template.render! assert_template_result(
"Liquid error (line 1): Argument error in tag 'include' - Illegal template name",
"{% include nil %}",
render_errors: true,
)
end end
def test_render_raise_argument_error_when_template_is_not_a_string
assert_template_result(
"Liquid error (line 1): Argument error in tag 'include' - Illegal template name",
"{% include 123 %}",
render_errors: true,
)
end end
def test_including_via_variable_value def test_including_via_variable_value
assert_template_result("from TestFileSystem", "{% assign page = 'pick_a_source' %}{% include page %}") assert_template_result(
"from TestFileSystem",
"{% assign page = 'pick_a_source' %}{% include page %}",
partials: { "pick_a_source" => "from TestFileSystem" },
)
assert_template_result("Product: Draft 151cm ", "{% assign page = 'product' %}{% include page %}", "product" => { 'title' => 'Draft 151cm' }) partials = { "product" => "Product: {{ product.title }} " }
assert_template_result("Product: Draft 151cm ", "{% assign page = 'product' %}{% include page for foo %}", "foo" => { 'title' => 'Draft 151cm' }) assert_template_result(
"Product: Draft 151cm ",
"{% assign page = 'product' %}{% include page %}",
{ "product" => { 'title' => 'Draft 151cm' } },
partials: partials,
)
assert_template_result(
"Product: Draft 151cm ",
"{% assign page = 'product' %}{% include page for foo %}",
{ "foo" => { 'title' => 'Draft 151cm' } },
partials: partials,
)
end end
def test_including_with_strict_variables def test_including_with_strict_variables
template = Liquid::Template.parse("{% include 'simple' %}", error_mode: :warn) env = Liquid::Environment.build(
file_system: StubFileSystem.new('simple' => 'simple'),
)
template = Liquid::Template.parse("{% include 'simple' %}", error_mode: :warn, environment: env)
template.render(nil, strict_variables: true) template.render(nil, strict_variables: true)
assert_equal([], template.errors) assert_equal([], template.errors)
@@ -264,6 +347,31 @@ class IncludeTagTest < Minitest::Test
def test_break_through_include def test_break_through_include
assert_template_result("1", "{% for i in (1..3) %}{{ i }}{% break %}{{ i }}{% endfor %}") assert_template_result("1", "{% for i in (1..3) %}{{ i }}{% break %}{{ i }}{% endfor %}")
assert_template_result("1", "{% for i in (1..3) %}{{ i }}{% include 'break' %}{{ i }}{% endfor %}") assert_template_result(
"1",
"{% for i in (1..3) %}{{ i }}{% include 'break' %}{{ i }}{% endfor %}",
partials: { 'break' => "{% break %}" },
)
end
def test_render_tag_renders_error_with_template_name
assert_template_result(
'Liquid error (foo line 1): standard error',
"{% include 'foo' with errors %}",
{ 'errors' => ErrorDrop.new },
partials: { 'foo' => '{{ foo.standard_error }}' },
render_errors: true,
)
end
def test_render_tag_renders_error_with_template_name_from_template_factory
assert_template_result(
'Liquid error (some/path/foo line 1): standard error',
"{% include 'foo' with errors %}",
{ 'errors' => ErrorDrop.new },
partials: { 'foo' => '{{ foo.standard_error }}' },
template_factory: StubTemplateFactory.new,
render_errors: true,
)
end end
end # IncludeTagTest end # IncludeTagTest
+13 -8
View File
@@ -6,20 +6,25 @@ class IncrementTagTest < Minitest::Test
include Liquid include Liquid
def test_inc def test_inc
assert_template_result('0', '{%increment port %}', {}) assert_template_result('0 1', '{%increment port %} {{ port }}')
assert_template_result('0 1', '{%increment port %} {%increment port%}', {}) assert_template_result(' 0 1 2', '{{port}} {%increment port %} {%increment port%} {{port}}')
assert_template_result('0 0 1 2 1', assert_template_result(
'0 0 1 2 1',
'{%increment port %} {%increment starboard%} ' \ '{%increment port %} {%increment starboard%} ' \
'{%increment port %} {%increment port%} ' \ '{%increment port %} {%increment port%} ' \
'{%increment starboard %}', {}) '{%increment starboard %}',
)
end end
def test_dec def test_dec
assert_template_result('9', '{%decrement port %}', 'port' => 10) assert_template_result('-1 -1', '{%decrement port %} {{ port }}', { 'port' => 10 })
assert_template_result('-1 -2', '{%decrement port %} {%decrement port%}', {}) assert_template_result(' -1 -2 -2', '{{port}} {%decrement port %} {%decrement port%} {{port}}')
assert_template_result('1 5 2 2 5', assert_template_result(
'0 1 2 0 3 1 1 3',
'{%increment starboard %} {%increment starboard%} {%increment starboard%} ' \
'{%increment port %} {%increment starboard%} ' \ '{%increment port %} {%increment starboard%} ' \
'{%increment port %} {%decrement port%} ' \ '{%increment port %} {%decrement port%} ' \
'{%decrement starboard %}', 'port' => 1, 'starboard' => 5) '{%decrement starboard %}',
)
end end
end end
@@ -0,0 +1,69 @@
# frozen_string_literal: true
require 'test_helper'
class InlineCommentTest < Minitest::Test
include Liquid
def test_inline_comment_returns_nothing
assert_template_result('', '{%- # this is an inline comment -%}')
assert_template_result('', '{%-# this is an inline comment -%}')
assert_template_result('', '{% # this is an inline comment %}')
assert_template_result('', '{%# this is an inline comment %}')
end
def test_inline_comment_does_not_require_a_space_after_the_pound_sign
assert_template_result('', '{%#this is an inline comment%}')
end
def test_liquid_inline_comment_returns_nothing
assert_template_result('Hey there, how are you doing today?', <<~LIQUID)
{%- liquid
# This is how you'd write a block comment in a liquid tag.
# It looks a lot like what you'd have in ruby.
# You can use it as inline documentation in your
# liquid blocks to explain why you're doing something.
echo "Hey there, "
# It won't affect the output.
echo "how are you doing today?"
-%}
LIQUID
end
def test_inline_comment_can_be_written_on_multiple_lines
assert_template_result('', <<~LIQUID)
{%-
# That kind of block comment is also allowed.
# It would only be a stylistic difference.
# Much like JavaScript's /* */ comments and their
# leading * on new lines.
-%}
LIQUID
end
def test_inline_comment_multiple_pound_signs
assert_template_result('', <<~LIQUID)
{%- liquid
######################################
# We support comments like this too. #
######################################
-%}
LIQUID
end
def test_inline_comments_require_the_pound_sign_on_every_new_line
assert_match_syntax_error("Each line of comments must be prefixed by the '#' character", <<~LIQUID)
{%-
# some comment
echo 'hello world'
-%}
LIQUID
end
def test_inline_comment_does_not_support_nested_tags
assert_template_result(' -%}', "{%- # {% echo 'hello world' %} -%}")
end
end
+36 -3
View File
@@ -6,13 +6,13 @@ class LiquidTagTest < Minitest::Test
include Liquid include Liquid
def test_liquid_tag def test_liquid_tag
assert_template_result('1 2 3', <<~LIQUID, 'array' => [1, 2, 3]) assert_template_result('1 2 3', <<~LIQUID, { 'array' => [1, 2, 3] })
{%- liquid {%- liquid
echo array | join: " " echo array | join: " "
-%} -%}
LIQUID LIQUID
assert_template_result('1 2 3', <<~LIQUID, 'array' => [1, 2, 3]) assert_template_result('1 2 3', <<~LIQUID, { 'array' => [1, 2, 3] })
{%- liquid {%- liquid
for value in array for value in array
echo value echo value
@@ -23,7 +23,7 @@ class LiquidTagTest < Minitest::Test
-%} -%}
LIQUID LIQUID
assert_template_result('4 8 12 6', <<~LIQUID, 'array' => [1, 2, 3]) assert_template_result('4 8 12 6', <<~LIQUID, { 'array' => [1, 2, 3] })
{%- liquid {%- liquid
for value in array for value in array
assign double_value = value | times: 2 assign double_value = value | times: 2
@@ -113,4 +113,37 @@ class LiquidTagTest < Minitest::Test
{% raw %}{% liquid echo 'test' %}{% endraw %} {% raw %}{% liquid echo 'test' %}{% endraw %}
LIQUID LIQUID
end end
def test_nested_liquid_tags
assert_template_result('good', <<~LIQUID)
{%- liquid
liquid
if true
echo "good"
endif
-%}
LIQUID
end
def test_nested_liquid_tags_on_same_line
assert_template_result('good', <<~LIQUID)
{%- liquid liquid liquid echo "good" -%}
LIQUID
end
def test_nested_liquid_liquid_is_not_skipped_if_used_in_non_tag_position
assert_template_result('liquid', <<~LIQUID, { 'liquid' => 'liquid' })
{%- liquid liquid liquid echo liquid -%}
LIQUID
end
def test_next_liquid_with_unclosed_if_tag
assert_match_syntax_error("Liquid syntax error (line 2): 'if' tag was never closed", <<~LIQUID)
{%- liquid
liquid if true
echo "good"
endif
-%}
LIQUID
end
end end
+8 -3
View File
@@ -6,12 +6,17 @@ class RawTagTest < Minitest::Test
include Liquid include Liquid
def test_tag_in_raw def test_tag_in_raw
assert_template_result('{% comment %} test {% endcomment %}', assert_template_result(
'{% raw %}{% comment %} test {% endcomment %}{% endraw %}') '{% comment %} test {% endcomment %}',
'{% raw %}{% comment %} test {% endcomment %}{% endraw %}',
)
end end
def test_output_in_raw def test_output_in_raw
assert_template_result('{{ test }}', '{% raw %}{{ test }}{% endraw %}') assert_template_result('>{{ test }}<', '> {%- raw -%}{{ test }}{%- endraw -%} <')
assert_template_result("> inner <", "> {%- raw -%} inner {%- endraw %} <")
assert_template_result("> inner <", "> {%- raw -%} inner {%- endraw -%} <")
assert_template_result("{Hello}", "{% raw %}{{% endraw %}Hello{% raw %}}{% endraw %}")
end end
def test_open_tag_in_raw def test_open_tag_in_raw

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