pax_global_header00006660000000000000000000000064142707673010014521gustar00rootroot0000000000000052 comment=af3f8612bfcb7e7e28cde4dcb3bc05d8c3578dda liquid-5.4.0/000077500000000000000000000000001427076730100130165ustar00rootroot00000000000000liquid-5.4.0/.github/000077500000000000000000000000001427076730100143565ustar00rootroot00000000000000liquid-5.4.0/.github/probots.yml000066400000000000000000000000211427076730100165620ustar00rootroot00000000000000enabled: - cla liquid-5.4.0/.github/workflows/000077500000000000000000000000001427076730100164135ustar00rootroot00000000000000liquid-5.4.0/.github/workflows/liquid.yml000066400000000000000000000024321427076730100204260ustar00rootroot00000000000000name: Liquid on: [push, pull_request] jobs: test: runs-on: ubuntu-latest strategy: matrix: entry: - { ruby: 2.7, allowed-failure: false } # minimum supported - { ruby: 3.1, allowed-failure: false } # latest - { ruby: ruby-head, allowed-failure: true } name: test (${{ matrix.entry.ruby }}) steps: - uses: actions/checkout@v3 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.entry.ruby }} - uses: actions/cache@v1 with: path: vendor/bundle key: ${{ runner.os }}-gems-${{ hashFiles('Gemfile') }} restore-keys: ${{ runner.os }}-gems- - run: bundle install --jobs=3 --retry=3 --path=vendor/bundle - run: bundle exec rake continue-on-error: ${{ matrix.entry.allowed-failure }} memory_profile: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: ruby/setup-ruby@v1 with: ruby-version: 2.7 - uses: actions/cache@v1 with: path: vendor/bundle key: ${{ runner.os }}-gems-${{ hashFiles('Gemfile') }} restore-keys: ${{ runner.os }}-gems- - run: bundle install --jobs=3 --retry=3 --path=vendor/bundle - run: bundle exec rake memory_profile:run liquid-5.4.0/.gitignore000066400000000000000000000001231427076730100150020ustar00rootroot00000000000000*~ *.gem *.swp pkg *.rbc .rvmrc .ruby-version Gemfile.lock .bundle .byebug_history liquid-5.4.0/.rubocop.yml000066400000000000000000000006341427076730100152730ustar00rootroot00000000000000inherit_gem: rubocop-shopify: rubocop.yml inherit_from: - .rubocop_todo.yml require: rubocop-performance Performance: Enabled: true AllCops: TargetRubyVersion: 2.7 NewCops: disable Exclude: - 'vendor/bundle/**/*' Naming/MethodName: Exclude: - 'example/server/liquid_servlet.rb' # Backport https://github.com/Shopify/ruby-style-guide/pull/258 Layout/BeginEndAlignment: Enabled: true liquid-5.4.0/.rubocop_todo.yml000066400000000000000000000137701427076730100163250ustar00rootroot00000000000000# This configuration was generated by # `rubocop --auto-gen-config` # 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 # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. # Offense count: 1 # This cop supports safe auto-correction (--auto-correct). # Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation, Include. # Include: **/*.gemspec Gemspec/OrderedDependencies: Exclude: - 'liquid.gemspec' # Offense count: 6 # This cop supports safe auto-correction (--auto-correct). Layout/ClosingHeredocIndentation: Exclude: - 'test/integration/tags/for_tag_test.rb' # 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 # 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: Exclude: - 'lib/liquid/forloop_drop.rb' - 'lib/liquid/tablerowloop_drop.rb' - 'test/integration/assign_test.rb' - 'test/integration/context_test.rb' - 'test/integration/filter_test.rb' - 'test/integration/standard_filter_test.rb' - 'test/integration/tags/for_tag_test.rb' - 'test/integration/tags/table_row_test.rb' # Offense count: 44 Naming/ConstantName: Exclude: - 'lib/liquid.rb' - 'lib/liquid/block_body.rb' - 'lib/liquid/tags/assign.rb' - 'lib/liquid/tags/capture.rb' - 'lib/liquid/tags/case.rb' - 'lib/liquid/tags/cycle.rb' - 'lib/liquid/tags/for.rb' - 'lib/liquid/tags/if.rb' - 'lib/liquid/tags/raw.rb' - 'lib/liquid/tags/table_row.rb' - 'lib/liquid/variable.rb' - 'performance/shopify/comment_form.rb' - 'performance/shopify/paginate.rb' - 'test/integration/tags/include_tag_test.rb' # Offense count: 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 Style/ClassVars: Exclude: - '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 liquid-5.4.0/CONTRIBUTING.md000066400000000000000000000023721427076730100152530ustar00rootroot00000000000000# How to contribute ## Things we will merge * Bugfixes * Performance improvements * Features that are likely to be useful to the majority of Liquid users * Documentation updates that are concise and likely to be useful to the majority of Liquid users ## Things we won't merge * Code that introduces considerable performance degrations * Code that touches performance-critical parts of Liquid and comes without benchmarks * Features that are not important for most people (we want to keep the core Liquid code small and tidy) * Features that can easily be implemented on top of Liquid (for example as a custom filter or custom filesystem) * Code that does not include tests * Code that breaks existing tests * Documentation changes that are verbose, incorrect or not important to most people (we want to keep it simple and easy to understand) ## Workflow * [Sign the CLA](https://cla.shopify.com/) if you haven't already * Fork the Liquid repository * Create a new branch in your fork * For updating [Liquid documentation](https://shopify.github.io/liquid/), create it from `gh-pages` branch. (You can skip tests.) * If it makes sense, add tests for your code and/or run a performance benchmark * Make sure all tests pass (`bundle exec rake`) * Create a pull request liquid-5.4.0/Gemfile000066400000000000000000000011121427076730100143040ustar00rootroot00000000000000# frozen_string_literal: true source 'https://rubygems.org' git_source(:github) do |repo_name| "https://github.com/#{repo_name}.git" end gemspec group :benchmark, :test do gem 'benchmark-ips' gem 'memory_profiler' gem 'terminal-table' install_if -> { RUBY_PLATFORM !~ /mingw|mswin|java/ && RUBY_ENGINE != 'truffleruby' } do gem 'stackprof' end end group :test do gem 'rubocop-shopify', '~> 2.7.0', require: false gem 'rubocop-performance', require: false platform :mri, :truffleruby do gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'master' end end liquid-5.4.0/History.md000066400000000000000000000440771427076730100150150ustar00rootroot00000000000000# Liquid Change Log ## 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 ### Fixes * Add ParseTreeVisitor to Echo tag (#1414) [CP Clermont] * Test with ruby 3.0 as the latest ruby version (#1398) [Dylan Thacker-Smith] * Handle carriage return in newlines_to_br (#1391) [Unending] ### Performance Improvements * Use split limit in truncatewords (#1361) [Dylan Thacker-Smith] ## 5.0.0 / 2021-01-06 ### Features * Add new `{% render %}` tag (#1122) [Samuel Doiron] * Add support for `as` in `{% render %}` and `{% include %}` (#1181) [Mike Angell] * Add `{% liquid %}` and `{% echo %}` tags (#1086) [Justin Li] * Add [usage tracking](README.md#usage-tracking) [Mike Angell] * Add `Tag.disable_tags` for disabling tags that prepend `Tag::Disableable` at render time (#1162, #1274, #1275) [Mike Angell] * Support using a profiler for multiple renders (#1365, #1366) [Dylan Thacker-Smith] ### Fixes * Fix catastrophic backtracking in `RANGES_REGEX` regular expression (#1357) [Dylan Thacker-Smith] * Make sure the for tag's limit and offset are integers (#1094) [David Cornu] * Invokable methods for enumerable reject include (#1151) [Thierry Joyal] * Allow `default` filter to handle `false` as value (#1144) [Mike Angell] * Fix render length resource limit so it doesn't multiply nested output (#1285) [Dylan Thacker-Smith] * Fix duplication of text in raw tags (#1304) [Peter Zhu] * Fix strict parsing of find variable with a name expression (#1317) [Dylan Thacker-Smith] * Use monotonic time to measure durations in Liquid::Profiler (#1362) [Dylan Thacker-Smith] ### Breaking Changes * Require Ruby >= 2.5 (#1131, #1310) [Mike Angell, Dylan Thacker-Smith] * Remove support for taint checking (#1268) [Dylan Thacker-Smith] * Split Strainer class into StrainerFactory and StrainerTemplate (#1208) [Thierry Joyal] * Remove handling of a nil context in the Strainer class (#1218) [Thierry Joyal] * Handle `BlockBody#blank?` at parse time (#1287) [Dylan Thacker-Smith] * Pass the tag markup and tokenizer to `Document#unknown_tag` (#1290) [Dylan Thacker-Smith] * And several internal changes ### Performance Improvements * Reduce allocations (#1073, #1091, #1115, #1099, #1117, #1141, #1322, #1341) [Richard Monette, Florian Weingarten, Ashwin Maroli] * Improve resources limits performance (#1093, #1323) [Florian Weingarten, Dylan Thacker-Smith] ## 4.0.3 / 2019-03-12 ### Fixed * Fix break and continue tags inside included templates in loops (#1072) [Justin Li] ## 4.0.2 / 2019-03-08 ### Changed * Add `where` filter (#1026) [Samuel Doiron] * Add `ParseTreeVisitor` to iterate the Liquid AST (#1025) [Stephen Paul Weber] * Improve `strip_html` performance (#1032) [printercu] ### Fixed * Add error checking for invalid combinations of inputs to sort, sort_natural, where, uniq, map, compact filters (#1059) [Garland Zhang] * Validate the character encoding in url_decode (#1070) [Clayton Smith] ## 4.0.1 / 2018-10-09 ### Changed * Add benchmark group in Gemfile (#855) [Jerry Liu] * Allow benchmarks to benchmark render by itself (#851) [Jerry Liu] * Avoid calling `line_number` on String node when rescuing a render error. (#860) [Dylan Thacker-Smith] * Avoid duck typing to detect whether to call render on a node. [Dylan Thacker-Smith] * Clarify spelling of `reversed` on `for` block tag (#843) [Mark Crossfield] * Replace recursion with loop to avoid potential stack overflow from malicious input (#891, #892) [Dylan Thacker-Smith] * Limit block tag nesting to 100 (#894) [Dylan Thacker-Smith] * Replace `assert_equal nil` with `assert_nil` (#895) [Dylan Thacker-Smith] * Remove Spy Gem (#896) [Dylan Thacker-Smith] * Add `collection_name` and `variable_name` reader to `For` block (#909) * Symbols render as strings (#920) [Justin Li] * Remove default value from Hash objects (#932) [Maxime Bedard] * Remove one level of nesting (#944) [Dylan Thacker-Smith] * Update Rubocop version (#952) [Justin Li] * Add `at_least` and `at_most` filters (#954, #958) [Nithin Bekal] * Add a regression test for a liquid-c trim mode bug (#972) [Dylan Thacker-Smith] * Use https rather than git protocol to fetch liquid-c [Dylan Thacker-Smith] * Add tests against Ruby 2.4 (#963) and 2.5 (#981) * Replace RegExp literals with constants (#988) [Ashwin Maroli] * Replace unnecessary `#each_with_index` with `#each` (#992) [Ashwin Maroli] * Improve the unexpected end delimiter message for block tags. (#1003) [Dylan Thacker-Smith] * Refactor and optimize rendering (#1005) [Christopher Aue] * Add installation instruction (#1006) [Ben Gift] * Remove Circle CI (#1010) * Rename deprecated `BigDecimal.new` to `BigDecimal` (#1024) [Koichi ITO] * Rename deprecated Rubocop name (#1027) [Justin Li] ### Fixed * Handle `join` filter on non String joiners (#857) [Richard Monette] * Fix duplicate inclusion condition logic error of `Liquid::Strainer.add_filter` method (#861) * Fix `escape`, `url_encode`, `url_decode` not handling non-string values (#898) [Thierry Joyal] * Fix raise when variable is defined but nil when using `strict_variables` [Pascal Betz] * Fix `sort` and `sort_natural` to handle arrays with nils (#930) [Eric Chan] ## 4.0.0 / 2016-12-14 / branch "4-0-stable" ### Changed * Render an opaque internal error by default for non-Liquid::Error (#835) [Dylan Thacker-Smith] * Ruby 2.0 support dropped (#832) [Dylan Thacker-Smith] * Add to_number Drop method to allow custom drops to work with number filters (#731) * Add strict_variables and strict_filters options to detect undefined references (#691) * Improve loop performance (#681) [Florian Weingarten] * Rename Drop method `before_method` to `liquid_method_missing` (#661) [Thierry Joyal] * Add url_decode filter to invert url_encode (#645) [Larry Archer] * Add global_filter to apply a filter to all output (#610) [Loren Hale] * Add compact filter (#600) [Carson Reinke] * Rename deprecated "has_key?" and "has_interrupt?" methods (#593) [Florian Weingarten] * Include template name with line numbers in render errors (574) [Dylan Thacker-Smith] * Add sort_natural filter (#554) [Martin Hanzel] * Add forloop.parentloop as a reference to the parent loop (#520) [Justin Li] * Block parsing moved to BlockBody class (#458) [Dylan Thacker-Smith] * Add concat filter to concatenate arrays (#429) [Diogo Beato] * Ruby 1.9 support dropped (#491) [Justin Li] * Liquid::Template.file_system's read_template_file method is no longer passed the context. (#441) [James Reid-Smith] * Remove `liquid_methods` (See https://github.com/Shopify/liquid/pull/568 for replacement) * Liquid::Template.register_filter raises when the module overrides registered public methods as private or protected (#705) [Gaurav Chande] ### Fixed * Fix variable names being detected as an operator when starting with contains (#788) [Michael Angell] * Fix include tag used with strict_variables (#828) [QuickPay] * Fix map filter when value is a Proc (#672) [Guillaume Malette] * Fix truncate filter when value is not a string (#672) [Guillaume Malette] * Fix behaviour of escape filter when input is nil (#665) [Tanel Jakobsoo] * Fix sort filter behaviour with empty array input (#652) [Marcel Cary] * Fix test failure under certain timezones (#631) [Dylan Thacker-Smith] * Fix bug in uniq filter (#595) [Florian Weingarten] * Fix bug when "blank" and "empty" are used as variable names (#592) [Florian Weingarten] * Fix condition parse order in strict mode (#569) [Justin Li] * Fix naming of the "context variable" when dynamically including a template (#559) [Justin Li] * Gracefully accept empty strings in the date filter (#555) [Loren Hale] * Fix capturing into variables with a hyphen in the name (#505) [Florian Weingarten] * Fix case sensitivity regression in date standard filter (#499) [Kelley Reynolds] * Disallow filters with no variable in strict mode (#475) [Justin Li] * Disallow variable names in the strict parser that are not valid in the lax parser (#463) [Justin Li] * Fix BlockBody#warnings taking exponential time to compute (#486) [Justin Li] ## 3.0.5 / 2015-07-23 / branch "3-0-stable" * Fix test failure under certain timezones [Dylan Thacker-Smith] ## 3.0.4 / 2015-07-17 * Fix chained access to multi-dimensional hashes [Florian Weingarten] ## 3.0.3 / 2015-05-28 * Fix condition parse order in strict mode (#569) [Justin Li] ## 3.0.2 / 2015-04-24 * Expose VariableLookup private members (#551) [Justin Li] * Documentation fixes ## 3.0.1 / 2015-01-23 * Remove duplicate `index0` key in TableRow tag (#502) [Alfred Xing] ## 3.0.0 / 2014-11-12 * Removed Block#end_tag. Instead, override parse with `super` followed by your code. See #446 [Dylan Thacker-Smith] * Fixed condition with wrong data types (#423) [Bogdan Gusiev] * Add url_encode to standard filters (#421) [Derrick Reimer] * Add uniq to standard filters [Florian Weingarten] * Add exception_handler feature (#397) and #254 [Bogdan Gusiev, Florian Weingarten] * Optimize variable parsing to avoid repeated regex evaluation during template rendering #383 [Jason Hiltz-Laforge] * Optimize checking for block interrupts to reduce object allocation #380 [Jason Hiltz-Laforge] * Properly set context rethrow_errors on render! #349 [Thierry Joyal] * Fix broken rendering of variables which are equal to false (#345) [Florian Weingarten] * Remove ActionView template handler [Dylan Thacker-Smith] * Freeze lots of string literals for new Ruby 2.1 optimization (#297) [Florian Weingarten] * Allow newlines in tags and variables (#324) [Dylan Thacker-Smith] * Tag#parse is called after initialize, which now takes options instead of tokens as the 3rd argument. See #321 [Dylan Thacker-Smith] * Raise `Liquid::ArgumentError` instead of `::ArgumentError` when filter has wrong number of arguments #309 [Bogdan Gusiev] * Add a to_s default for liquid drops (#306) [Adam Doeler] * Add strip, lstrip, and rstrip to standard filters [Florian Weingarten] * Make if, for & case tags return complete and consistent nodelists (#250) [Nick Jones] * Prevent arbitrary method invocation on condition objects (#274) [Dylan Thacker-Smith] * Don't call to_sym when creating conditions for security reasons (#273) [Bouke van der Bijl] * Fix resource counting bug with respond_to?(:length) (#263) [Florian Weingarten] * Allow specifying custom patterns for template filenames (#284) [Andrei Gladkyi] * Allow drops to optimize loading a slice of elements (#282) [Tom Burns] * Support for passing variables to snippets in subdirs (#271) [Joost Hietbrink] * Add a class cache to avoid runtime extend calls (#249) [James Tucker] * Remove some legacy Ruby 1.8 compatibility code (#276) [Florian Weingarten] * Add default filter to standard filters (#267) [Derrick Reimer] * Add optional strict parsing and warn parsing (#235) [Tristan Hume] * Add I18n syntax error translation (#241) [Simon Hørup Eskildsen, Sirupsen] * Make sort filter work on enumerable drops (#239) [Florian Weingarten] * Fix clashing method names in enumerable drops (#238) [Florian Weingarten] * Make map filter work on enumerable drops (#233) [Florian Weingarten] * Improved whitespace stripping for blank blocks, related to #216 [Florian Weingarten] ## 2.6.3 / 2015-07-23 / branch "2-6-stable" * Fix test failure under certain timezones [Dylan Thacker-Smith] ## 2.6.2 / 2015-01-23 * Remove duplicate hash key [Parker Moore] ## 2.6.1 / 2014-01-10 Security fix, cherry-picked from master (4e14a65): * Don't call to_sym when creating conditions for security reasons (#273) [Bouke van der Bijl] * Prevent arbitrary method invocation on condition objects (#274) [Dylan Thacker-Smith] ## 2.6.0 / 2013-11-25 IMPORTANT: Liquid 2.6 is going to be the last version of Liquid which maintains explicit Ruby 1.8 compatability. The following releases will only be tested against Ruby 1.9 and Ruby 2.0 and are likely to break on Ruby 1.8. * Bugfix for #106: fix example servlet [gnowoel] * Bugfix for #97: strip_html filter supports multi-line tags [Jo Liss] * Bugfix for #114: strip_html filter supports style tags [James Allardice] * Bugfix for #117: 'now' support for date filter in Ruby 1.9 [Notre Dame Webgroup] * Bugfix for #166: truncate filter on UTF-8 strings with Ruby 1.8 [Florian Weingarten] * Bugfix for #204: 'raw' parsing bug [Florian Weingarten] * Bugfix for #150: 'for' parsing bug [Peter Schröder] * Bugfix for #126: Strip CRLF in strip_newline [Peter Schröder] * Bugfix for #174, "can't convert Fixnum into String" for "replace" [jsw0528] * Allow a Liquid::Drop to be passed into Template#render [Daniel Huckstep] * Resource limits [Florian Weingarten] * Add reverse filter [Jay Strybis] * Add utf-8 support * Use array instead of Hash to keep the registered filters [Tasos Stathopoulos] * Cache tokenized partial templates [Tom Burns] * Avoid warnings in Ruby 1.9.3 [Marcus Stollsteimer] * Better documentation for 'include' tag (closes #163) [Peter Schröder] * Use of BigDecimal on filters to have better precision (closes #155) [Arthur Nogueira Neves] ## 2.5.5 / 2014-01-10 / branch "2-5-stable" Security fix, cherry-picked from master (4e14a65): * Don't call to_sym when creating conditions for security reasons (#273) [Bouke van der Bijl] * Prevent arbitrary method invocation on condition objects (#274) [Dylan Thacker-Smith] ## 2.5.4 / 2013-11-11 * Fix "can't convert Fixnum into String" for "replace" (#173), [jsw0528] ## 2.5.3 / 2013-10-09 * #232, #234, #237: Fix map filter bugs [Florian Weingarten] ## 2.5.2 / 2013-09-03 / deleted Yanked from rubygems, as it contained too many changes that broke compatibility. Those changes will be on following major releases. ## 2.5.1 / 2013-07-24 * #230: Fix security issue with map filter, Use invoke_drop in map filter [Florian Weingarten] ## 2.5.0 / 2013-03-06 * Prevent Object methods from being called on drops * Avoid symbol injection from liquid * Added break and continue statements * Fix filter parser for args without space separators * Add support for filter keyword arguments ## 2.4.0 / 2012-08-03 * Performance improvements * Allow filters in `assign` * Add `modulo` filter * Ruby 1.8, 1.9, and Rubinius compatibility fixes * Add support for `quoted['references']` in `tablerow` * Add support for Enumerable to `tablerow` * `strip_html` filter removes html comments ## 2.3.0 / 2011-10-16 * Several speed/memory improvements * Numerous bug fixes * Added support for MRI 1.9, Rubinius, and JRuby * Added support for integer drop parameters * Added epoch support to `date` filter * New `raw` tag that suppresses parsing * Added `else` option to `for` tag * New `increment` tag * New `split` filter ## 2.2.1 / 2010-08-23 * Added support for literal tags ## 2.2.0 / 2010-08-22 * Compatible with Ruby 1.8.7, 1.9.1 and 1.9.2-p0 * Merged some changed made by the community ## 1.9.0 / 2008-03-04 * Fixed gem install rake task * Improve Error encapsulation in liquid by maintaining a own set of exceptions instead of relying on ruby build ins ## Before 1.9.0 * Added If with or / and expressions * Implemented .to_liquid for all objects which can be passed to liquid like Strings Arrays Hashes Numerics and Booleans. To export new objects to liquid just implement .to_liquid on them and return objects which themselves have .to_liquid methods. * Added more tags to standard library * Added include tag ( like partials in rails ) * [...] Gazillion of detail improvements * Added strainers as filter hosts for better security [Tobias Luetke] * Fixed that rails integration would call filter with the wrong "self" [Michael Geary] * Fixed bad error reporting when a filter called a method which doesn't exist. Liquid told you that it couldn't find the filter which was obviously misleading [Tobias Luetke] * Removed count helper from standard lib. use size [Tobias Luetke] * Fixed bug with string filter parameters failing to tolerate commas in strings. [Paul Hammond] * Improved filter parameters. Filter parameters are now context sensitive; Types are resolved according to the rules of the context. Multiple parameters are now separated by the Liquid::ArgumentSeparator: , by default [Paul Hammond] {{ 'Typo' | link_to: 'http://typo.leetsoft.com', 'Typo - a modern weblog engine' }} * Added Liquid::Drop. A base class which you can use for exporting proxy objects to liquid which can acquire more data when used in liquid. [Tobias Luetke] class ProductDrop < Liquid::Drop def top_sales Shop.current.products.find(:all, :order => 'sales', :limit => 10 ) end end t = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {% endfor %} ' ) t.render('product' => ProductDrop.new ) * Added filter parameters support. Example: {{ date | format_date: "%Y" }} [Paul Hammond] liquid-5.4.0/LICENSE000066400000000000000000000020471427076730100140260ustar00rootroot00000000000000Copyright (c) 2005, 2006 Tobias Luetke Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. liquid-5.4.0/README.md000066400000000000000000000127451427076730100143060ustar00rootroot00000000000000[![Build Status](https://api.travis-ci.org/Shopify/liquid.svg?branch=master)](http://travis-ci.org/Shopify/liquid) [![Inline docs](http://inch-ci.org/github/Shopify/liquid.svg?branch=master)](http://inch-ci.org/github/Shopify/liquid) # Liquid template engine * [Contributing guidelines](CONTRIBUTING.md) * [Version history](History.md) * [Liquid documentation from Shopify](https://shopify.dev/api/liquid) * [Liquid Wiki at GitHub](https://github.com/Shopify/liquid/wiki) * [Website](http://liquidmarkup.org/) ## Introduction Liquid is a template engine which was written with very specific requirements: * It has to have beautiful and simple markup. Template engines which don't produce good looking markup are no fun to use. * It needs to be non evaling and secure. Liquid templates are made so that users can edit them. You don't want to run code on your server which your users wrote. * It has to be stateless. Compile and render steps have to be separate so that the expensive parsing and compiling can be done once and later on you can just render it passing in a hash with local variables and objects. ## Why you should use Liquid * You want to allow your users to edit the appearance of your application but don't want them to run **insecure code on your server**. * You want to render templates directly from the database. * You like smarty (PHP) style template engines. * You need a template engine which does HTML just as well as emails. * You don't like the markup of your current templating engine. ## What does it look like? ```html ``` ## How to use Liquid Install Liquid by adding `gem 'liquid'` to your gemfile. Liquid supports a very simple API based around the Liquid::Template class. For standard use you can just pass it the content of a file and call render with a parameters hash. ```ruby @template = Liquid::Template.parse("hi {{name}}") # Parses and compiles the template @template.render('name' => 'tobi') # => "hi tobi" ``` ### Error Modes Setting the error mode of Liquid lets you specify how strictly you want your templates to be interpreted. Normally the parser is very lax and will accept almost anything without error. Unfortunately this can make it very hard to debug and can lead to unexpected behaviour. Liquid also comes with a stricter parser that can be used when editing templates to give better error messages when templates are invalid. You can enable this new parser like this: ```ruby Liquid::Template.error_mode = :strict # Raises a SyntaxError when invalid syntax is used Liquid::Template.error_mode = :warn # Adds strict errors to template.errors but continues as normal Liquid::Template.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`: ```ruby Liquid::Template.parse(source, error_mode: :strict) ``` This is useful for doing things like enabling strict mode only in the theme editor. It is recommended that you enable `:strict` or `:warn` mode on new apps to stop invalid templates from being created. It is also recommended that you use it in the template editors of existing apps to give editors better error messages. ### Undefined variables and filters By default, the renderer doesn't raise or in any other way notify you if some variables or filters are missing, i.e. not passed to the `render` method. You can improve this situation by passing `strict_variables: true` and/or `strict_filters: true` options to the `render` method. When one of these options is set to true, all errors about undefined variables and undefined filters will be stored in `errors` array of a `Liquid::Template` instance. Here are some examples: ```ruby template = Liquid::Template.parse("{{x}} {{y}} {{z.a}} {{z.b}}") template.render({ 'x' => 1, 'z' => { 'a' => 2 } }, { strict_variables: true }) #=> '1 2 ' # when a variable is undefined, it's rendered as nil template.errors #=> [#, #] ``` ```ruby template = Liquid::Template.parse("{{x | filter1 | upcase}}") template.render({ 'x' => 'foo' }, { strict_filters: true }) #=> '' # when at least one filter in the filter chain is undefined, a whole expression is rendered as nil template.errors #=> [#] ``` If you want to raise on a first exception instead of pushing all of them in `errors`, you can use `render!` method: ```ruby template = Liquid::Template.parse("{{x}} {{y}}") template.render!({ 'x' => 1}, { strict_variables: true }) #=> Liquid::UndefinedVariable: Liquid error: undefined variable y ``` ### Usage tracking To help track usages of a feature or code path in production, we have released opt-in usage tracking. To enable this, we provide an empty `Liquid:: Usage.increment` method which you can customize to your needs. The feature is well suited to https://github.com/Shopify/statsd-instrument. However, the choice of implementation is up to you. Once you have enabled usage tracking, we recommend reporting any events through Github Issues that your system may be logging. It is highly likely this event has been added to consider deprecating or improving code specific to this event, so please raise any concerns.liquid-5.4.0/Rakefile000077500000000000000000000050361427076730100144720ustar00rootroot00000000000000# frozen_string_literal: true require 'rake' require 'rake/testtask' $LOAD_PATH.unshift(File.expand_path("../lib", __FILE__)) require "liquid/version" task(default: [:test, :rubocop]) desc('run test suite with default parser') Rake::TestTask.new(:base_test) do |t| t.libs << 'lib' << 'test' t.test_files = FileList['test/{integration,unit}/**/*_test.rb'] t.verbose = false end Rake::TestTask.new(:integration_test) do |t| t.libs << 'lib' << 'test' t.test_files = FileList['test/integration/**/*_test.rb'] t.verbose = false end desc('run test suite with warn error mode') task :warn_test do ENV['LIQUID_PARSER_MODE'] = 'warn' Rake::Task['base_test'].invoke end task :rubocop do if RUBY_ENGINE == 'ruby' require 'rubocop/rake_task' RuboCop::RakeTask.new end end desc('runs test suite with both strict and lax parsers') task :test do ENV['LIQUID_PARSER_MODE'] = 'lax' Rake::Task['base_test'].invoke ENV['LIQUID_PARSER_MODE'] = 'strict' Rake::Task['base_test'].reenable Rake::Task['base_test'].invoke if RUBY_ENGINE == 'ruby' || RUBY_ENGINE == 'truffleruby' ENV['LIQUID_C'] = '1' ENV['LIQUID_PARSER_MODE'] = 'lax' Rake::Task['integration_test'].reenable Rake::Task['integration_test'].invoke ENV['LIQUID_PARSER_MODE'] = 'strict' Rake::Task['integration_test'].reenable Rake::Task['integration_test'].invoke end end task(gem: :build) task :build do system "gem build liquid.gemspec" end task install: :build do system "gem install liquid-#{Liquid::VERSION}.gem" end task release: :build do system "git tag -a v#{Liquid::VERSION} -m 'Tagging #{Liquid::VERSION}'" system "git push --tags" system "gem push liquid-#{Liquid::VERSION}.gem" system "rm liquid-#{Liquid::VERSION}.gem" end namespace :benchmark do desc "Run the liquid benchmark with lax parsing" task :run do ruby "./performance/benchmark.rb lax" end desc "Run the liquid benchmark with strict parsing" task :strict do ruby "./performance/benchmark.rb strict" end end namespace :profile do desc "Run the liquid profile/performance coverage" task :run do ruby "./performance/profile.rb" end desc "Run the liquid profile/performance coverage with strict parsing" task :strict do ruby "./performance/profile.rb strict" end end namespace :memory_profile do desc "Run memory profiler" task :run do ruby "./performance/memory_profile.rb" end end desc("Run example") task :example do ruby "-w -d -Ilib example/server/server.rb" end task :console do exec 'irb -I lib -r liquid' end liquid-5.4.0/example/000077500000000000000000000000001427076730100144515ustar00rootroot00000000000000liquid-5.4.0/example/server/000077500000000000000000000000001427076730100157575ustar00rootroot00000000000000liquid-5.4.0/example/server/example_servlet.rb000066400000000000000000000026721427076730100215120ustar00rootroot00000000000000# frozen_string_literal: true module ProductsFilter def price(integer) format("$%.2d USD", integer / 100.0) end def prettyprint(text) text.gsub(/\*(.*)\*/, '\1') end def count(array) array.size end def paragraph(p) "

#{p}

" end end class Servlet < LiquidServlet def index { 'date' => Time.now } end def products { 'products' => products_list, 'more_products' => more_products_list, 'description' => description, 'section' => 'Snowboards', 'cool_products' => true } end private def products_list [{ 'name' => 'Arbor Draft', 'price' => 39900, 'description' => 'the *arbor draft* is a excellent product' }, { 'name' => 'Arbor Element', 'price' => 40000, 'description' => 'the *arbor element* rocks for freestyling' }, { 'name' => 'Arbor Diamond', 'price' => 59900, 'description' => 'the *arbor diamond* is a made up product because im obsessed with arbor and have no creativity' }] end def more_products_list [{ 'name' => 'Arbor Catalyst', 'price' => 39900, 'description' => 'the *arbor catalyst* is an advanced drop-through for freestyle and flatground performance and versatility' }, { 'name' => 'Arbor Fish', 'price' => 40000, 'description' => 'the *arbor fish* is a compact pin that features an extended wheelbase and time-honored teardrop shape' }] end def description "List of Products ~ This is a list of products with price and description." end end liquid-5.4.0/example/server/liquid_servlet.rb000066400000000000000000000013141427076730100213360ustar00rootroot00000000000000# frozen_string_literal: true class LiquidServlet < WEBrick::HTTPServlet::AbstractServlet def do_GET(req, res) handle(:get, req, res) end def do_POST(req, res) handle(:post, req, res) end private def handle(_type, req, res) @request = req @response = res @request.path_info =~ /(\w+)\z/ @action = Regexp.last_match(1) || 'index' @assigns = send(@action) if respond_to?(@action) @response['Content-Type'] = "text/html" @response.status = 200 @response.body = Liquid::Template.parse(read_template).render(@assigns, filters: [ProductsFilter]) end def read_template(filename = @action) File.read("#{__dir__}/templates/#{filename}.liquid") end end liquid-5.4.0/example/server/server.rb000066400000000000000000000005051427076730100176120ustar00rootroot00000000000000# frozen_string_literal: true require 'webrick' require 'rexml/document' require_relative '../../lib/liquid' require_relative 'liquid_servlet' require_relative 'example_servlet' # Setup webrick server = WEBrick::HTTPServer.new(Port: ARGV[1] || 3000) server.mount('/', Servlet) trap("INT") { server.shutdown } server.start liquid-5.4.0/example/server/templates/000077500000000000000000000000001427076730100177555ustar00rootroot00000000000000liquid-5.4.0/example/server/templates/index.liquid000066400000000000000000000001531427076730100222740ustar00rootroot00000000000000

Hello world!

It is {{date}}

Check out the Products screen

liquid-5.4.0/example/server/templates/products.liquid000066400000000000000000000026271427076730100230400ustar00rootroot00000000000000 products {% assign all_products = products | concat: more_products %}

{{ description | split: '~' | first }}

{{ description | split: '~' | last }}

There are currently {{all_products | count}} products in the {{section}} catalog

{% if cool_products %} Cool products :) {% else %} Uncool products :( {% endif %}
    {% for product in all_products %}
  • {{product.name}}

    Only {{product.price | price }} {{product.description | prettyprint | paragraph }} {{ 'it rocks!' | paragraph }}
  • {% endfor %}
liquid-5.4.0/lib/000077500000000000000000000000001427076730100135645ustar00rootroot00000000000000liquid-5.4.0/lib/liquid.rb000066400000000000000000000066051427076730100154070ustar00rootroot00000000000000# frozen_string_literal: true # Copyright (c) 2005 Tobias Luetke # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. module Liquid FilterSeparator = /\|/ ArgumentSeparator = ',' FilterArgumentSeparator = ':' VariableAttributeSeparator = '.' WhitespaceControl = '-' TagStart = /\{\%/ TagEnd = /\%\}/ TagName = /#|\w+/ VariableSignature = /\(?[\w\-\.\[\]]\)?/ VariableSegment = /[\w\-]/ VariableStart = /\{\{/ VariableEnd = /\}\}/ VariableIncompleteEnd = /\}\}?/ QuotedString = /"[^"]*"|'[^']*'/ QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o TagAttributes = /(\w[\w-]*)\s*\:\s*(#{QuotedFragment})/o AnyStartingTag = /#{TagStart}|#{VariableStart}/o PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o RAISE_EXCEPTION_LAMBDA = ->(_e) { raise } singleton_class.send(:attr_accessor, :cache_classes) self.cache_classes = true end require "liquid/version" require 'liquid/parse_tree_visitor' require 'liquid/lexer' require 'liquid/parser' require 'liquid/i18n' require 'liquid/drop' require 'liquid/tablerowloop_drop' require 'liquid/forloop_drop' require 'liquid/extensions' require 'liquid/errors' require 'liquid/interrupts' require 'liquid/strainer_template' require 'liquid/strainer_factory' require 'liquid/expression' require 'liquid/context' require 'liquid/parser_switching' require 'liquid/tag' require 'liquid/tag/disabler' require 'liquid/tag/disableable' require 'liquid/block' require 'liquid/block_body' require 'liquid/document' require 'liquid/variable' require 'liquid/variable_lookup' require 'liquid/range_lookup' require 'liquid/file_system' require 'liquid/resource_limits' require 'liquid/template' require 'liquid/standardfilters' require 'liquid/condition' require 'liquid/utils' require 'liquid/tokenizer' require 'liquid/parse_context' require 'liquid/partial_cache' require 'liquid/usage' require 'liquid/registers' require 'liquid/template_factory' # Load all the tags of the standard library # Dir["#{__dir__}/liquid/tags/*.rb"].each { |f| require f } liquid-5.4.0/lib/liquid/000077500000000000000000000000001427076730100150535ustar00rootroot00000000000000liquid-5.4.0/lib/liquid/block.rb000066400000000000000000000043621427076730100164770ustar00rootroot00000000000000# frozen_string_literal: true module Liquid class Block < Tag MAX_DEPTH = 100 def initialize(tag_name, markup, options) super @blank = true end def parse(tokens) @body = new_body while parse_body(@body, tokens) end @body.freeze end # For backwards compatibility def render(context) @body.render(context) end def blank? @blank end def nodelist @body.nodelist end def unknown_tag(tag_name, _markup, _tokenizer) Block.raise_unknown_tag(tag_name, block_name, block_delimiter, parse_context) end # @api private def self.raise_unknown_tag(tag, block_name, block_delimiter, parse_context) if tag == 'else' raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_else", block_name: block_name) elsif tag.start_with?('end') raise SyntaxError, parse_context.locale.t("errors.syntax.invalid_delimiter", tag: tag, block_name: block_name, block_delimiter: block_delimiter) else raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag) end end def raise_tag_never_closed(block_name) raise SyntaxError, parse_context.locale.t("errors.syntax.tag_never_closed", block_name: block_name) end def block_name @tag_name end def block_delimiter @block_delimiter ||= "end#{block_name}" end private # @api public def new_body parse_context.new_block_body end # @api public def parse_body(body, tokens) if parse_context.depth >= MAX_DEPTH raise StackLevelError, "Nesting too deep" end parse_context.depth += 1 begin body.parse(tokens, parse_context) do |end_tag_name, end_tag_params| @blank &&= body.blank? return false if end_tag_name == block_delimiter raise_tag_never_closed(block_name) unless end_tag_name # this tag is not registered with the system # pass it to the current block for special handling or error reporting unknown_tag(end_tag_name, end_tag_params, tokens) end ensure parse_context.depth -= 1 end true end end end liquid-5.4.0/lib/liquid/block_body.rb000066400000000000000000000204371427076730100175150ustar00rootroot00000000000000# frozen_string_literal: true require 'English' module Liquid class BlockBody LiquidTagToken = /\A\s*(#{TagName})\s*(.*?)\z/o FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(#{TagName})(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om WhitespaceOrNothing = /\A\s*\z/ TAGSTART = "{%" VARSTART = "{{" attr_reader :nodelist def initialize @nodelist = [] @blank = true end def parse(tokenizer, parse_context, &block) raise FrozenError, "can't modify frozen Liquid::BlockBody" if frozen? parse_context.line_number = tokenizer.line_number if tokenizer.for_liquid_tag parse_for_liquid_tag(tokenizer, parse_context, &block) else parse_for_document(tokenizer, parse_context, &block) end end def freeze @nodelist.freeze super end private def parse_for_liquid_tag(tokenizer, parse_context) while (token = tokenizer.shift) unless token.empty? || token.match?(WhitespaceOrNothing) unless token =~ LiquidTagToken # line isn't empty but didn't match tag syntax, yield and let the # caller raise a syntax error return yield token, token end tag_name = Regexp.last_match(1) markup = Regexp.last_match(2) unless (tag = registered_tags[tag_name]) # end parsing if we reach an unknown tag and let the caller decide # determine how to proceed return yield tag_name, markup end new_tag = tag.parse(tag_name, markup, tokenizer, parse_context) @blank &&= new_tag.blank? @nodelist << new_tag end parse_context.line_number = tokenizer.line_number end yield nil, nil end # @api private def self.unknown_tag_in_liquid_tag(tag, parse_context) Block.raise_unknown_tag(tag, 'liquid', '%}', parse_context) end # @api private def self.raise_missing_tag_terminator(token, parse_context) raise SyntaxError, parse_context.locale.t("errors.syntax.tag_termination", token: token, tag_end: TagEnd.inspect) end # @api private def self.raise_missing_variable_terminator(token, parse_context) raise SyntaxError, parse_context.locale.t("errors.syntax.variable_termination", token: token, tag_end: VariableEnd.inspect) end # @api private def self.render_node(context, output, node) node.render_to_output_buffer(context, output) rescue => exc blank_tag = !node.instance_of?(Variable) && node.blank? rescue_render_node(context, output, node.line_number, exc, blank_tag) end # @api private def self.rescue_render_node(context, output, line_number, exc, blank_tag) case exc when MemoryError raise when UndefinedVariable, UndefinedDropMethod, UndefinedFilter context.handle_error(exc, line_number) else error_message = context.handle_error(exc, line_number) unless blank_tag # conditional for backwards compatibility output << error_message end end end private def parse_liquid_tag(markup, parse_context) liquid_tag_tokenizer = parse_context.new_tokenizer( markup, start_line_number: parse_context.line_number, for_liquid_tag: true ) parse_for_liquid_tag(liquid_tag_tokenizer, parse_context) do |end_tag_name, _end_tag_markup| if end_tag_name BlockBody.unknown_tag_in_liquid_tag(end_tag_name, parse_context) end end end private def parse_for_document(tokenizer, parse_context) while (token = tokenizer.shift) next if token.empty? case when token.start_with?(TAGSTART) whitespace_handler(token, parse_context) unless token =~ FullToken BlockBody.raise_missing_tag_terminator(token, parse_context) end tag_name = Regexp.last_match(2) markup = Regexp.last_match(4) if parse_context.line_number # newlines inside the tag should increase the line number, # particularly important for multiline {% liquid %} tags parse_context.line_number += Regexp.last_match(1).count("\n") + Regexp.last_match(3).count("\n") end if tag_name == 'liquid' parse_liquid_tag(markup, parse_context) next end unless (tag = registered_tags[tag_name]) # end parsing if we reach an unknown tag and let the caller decide # determine how to proceed return yield tag_name, markup end new_tag = tag.parse(tag_name, markup, tokenizer, parse_context) @blank &&= new_tag.blank? @nodelist << new_tag when token.start_with?(VARSTART) whitespace_handler(token, parse_context) @nodelist << create_variable(token, parse_context) @blank = false else if parse_context.trim_whitespace token.lstrip! end parse_context.trim_whitespace = false @nodelist << token @blank &&= token.match?(WhitespaceOrNothing) end parse_context.line_number = tokenizer.line_number end yield nil, nil end def whitespace_handler(token, parse_context) if token[2] == WhitespaceControl previous_token = @nodelist.last if previous_token.is_a?(String) first_byte = previous_token.getbyte(0) previous_token.rstrip! if previous_token.empty? && parse_context[:bug_compatible_whitespace_trimming] && first_byte previous_token << first_byte end end end parse_context.trim_whitespace = (token[-3] == WhitespaceControl) end def blank? @blank end # Remove blank strings in the block body for a control flow tag (e.g. `if`, `for`, `case`, `unless`) # with a blank body. # # For example, in a conditional assignment like the following # # ``` # {% if size > max_size %} # {% assign size = max_size %} # {% endif %} # ``` # # we assume the intention wasn't to output the blank spaces in the `if` tag's block body, so this method # will remove them to reduce the render output size. # # Note that it is now preferred to use the `liquid` tag for this use case. def remove_blank_strings raise "remove_blank_strings only support being called on a blank block body" unless @blank @nodelist.reject! { |node| node.instance_of?(String) } end def render(context) render_to_output_buffer(context, +'') end def render_to_output_buffer(context, output) freeze unless frozen? context.resource_limits.increment_render_score(@nodelist.length) idx = 0 while (node = @nodelist[idx]) if node.instance_of?(String) output << node else render_node(context, output, node) # If we get an Interrupt that means the block must stop processing. An # Interrupt is any command that stops block execution such as {% break %} # or {% continue %}. These tags may also occur through Block or Include tags. break if context.interrupt? # might have happened in a for-block end idx += 1 context.resource_limits.increment_write_score(output) end output end private def render_node(context, output, node) BlockBody.render_node(context, output, node) end def create_variable(token, parse_context) if token =~ ContentOfVariable markup = Regexp.last_match(1) return Variable.new(markup, parse_context) end BlockBody.raise_missing_variable_terminator(token, parse_context) end # @deprecated Use {.raise_missing_tag_terminator} instead def raise_missing_tag_terminator(token, parse_context) BlockBody.raise_missing_tag_terminator(token, parse_context) end # @deprecated Use {.raise_missing_variable_terminator} instead def raise_missing_variable_terminator(token, parse_context) BlockBody.raise_missing_variable_terminator(token, parse_context) end def registered_tags Template.tags end end end liquid-5.4.0/lib/liquid/condition.rb000066400000000000000000000105001427076730100173620ustar00rootroot00000000000000# frozen_string_literal: true module Liquid # Container for liquid nodes which conveniently wraps decision making logic # # Example: # # c = Condition.new(1, '==', 1) # c.evaluate #=> true # class Condition # :nodoc: @@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) }, '<' => :<, '>' => :>, '>=' => :>=, '<=' => :<=, 'contains' => lambda do |_cond, left, right| if left && right && left.respond_to?(:include?) right = right.to_s if left.is_a?(String) left.include?(right) else false end end, } class MethodLiteral attr_reader :method_name, :to_s def initialize(method_name, to_s) @method_name = method_name @to_s = to_s end end @@method_literals = { 'blank' => MethodLiteral.new(:blank?, '').freeze, 'empty' => MethodLiteral.new(:empty?, '').freeze, } def self.operators @@operators end def self.parse_expression(parse_context, markup) @@method_literals[markup] || parse_context.parse_expression(markup) end attr_reader :attachment, :child_condition attr_accessor :left, :operator, :right def initialize(left = nil, operator = nil, right = nil) @left = left @operator = operator @right = right @child_relation = nil @child_condition = nil end def evaluate(context = deprecated_default_context) condition = self result = nil loop do result = interpret_condition(condition.left, condition.right, condition.operator, context) case condition.child_relation when :or break if result when :and break unless result else break end condition = condition.child_condition end result end def or(condition) @child_relation = :or @child_condition = condition end def and(condition) @child_relation = :and @child_condition = condition end def attach(attachment) @attachment = attachment end def else? false end def inspect "#" end protected attr_reader :child_relation private def equal_variables(left, right) if left.is_a?(MethodLiteral) if right.respond_to?(left.method_name) return right.send(left.method_name) else return nil end end if right.is_a?(MethodLiteral) if left.respond_to?(right.method_name) return left.send(right.method_name) else return nil end end left == right end def interpret_condition(left, right, op, context) # If the operator is empty this means that the decision statement is just # a single variable. We can just poll this variable from the context and # return this as the result. return context.evaluate(left) if op.nil? left = Liquid::Utils.to_liquid_value(context.evaluate(left)) right = Liquid::Utils.to_liquid_value(context.evaluate(right)) operation = self.class.operators[op] || raise(Liquid::ArgumentError, "Unknown operator #{op}") if operation.respond_to?(:call) operation.call(self, left, right) elsif left.respond_to?(operation) && right.respond_to?(operation) && !left.is_a?(Hash) && !right.is_a?(Hash) begin left.send(operation, right) rescue ::ArgumentError => e raise Liquid::ArgumentError, e.message 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 def children [ @node.left, @node.right, @node.child_condition, @node.attachment ].compact end end end class ElseCondition < Condition def else? true end def evaluate(_context) true end end end liquid-5.4.0/lib/liquid/context.rb000066400000000000000000000213371427076730100170720ustar00rootroot00000000000000# frozen_string_literal: true module Liquid # Context keeps the variable stack and resolves variables, as well as keywords # # context['variable'] = 'testing' # context['variable'] #=> 'testing' # context['true'] #=> true # context['10.2232'] #=> 10.2232 # # context.stack do # context['bob'] = 'bobsen' # end # # context['bob'] #=> nil class Context class Context 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 # rubocop:disable Metrics/ParameterLists def self.build(environments: {}, outer_scope: {}, registers: {}, rethrow_errors: false, resource_limits: nil, static_environments: {}, &block) new(environments, outer_scope, registers, rethrow_errors, resource_limits, static_environments, &block) end def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil, static_environments = {}) @environments = [environments] @environments.flatten! @static_environments = [static_environments].flat_map(&:freeze).freeze @scopes = [(outer_scope || {})] @registers = registers.is_a?(Registers) ? registers : Registers.new(registers) @errors = [] @partial = false @strict_variables = false @resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits) @base_scope_depth = 0 @interrupts = [] @filters = [] @global_filter = nil @disabled_tags = {} @registers.static[:cached_partials] ||= {} @registers.static[:file_system] ||= Liquid::Template.file_system @registers.static[:template_factory] ||= Liquid::TemplateFactory.new self.exception_renderer = Template.default_exception_renderer if rethrow_errors self.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA end yield self if block_given? # Do this last, since it could result in this object being passed to a Proc in the environment squash_instance_assigns_with_environments end # rubocop:enable Metrics/ParameterLists def warnings @warnings ||= [] end def strainer @strainer ||= StrainerFactory.create(self, @filters) end # Adds filters to this context. # # Note that this does not register the filters with the main Template object. see Template.register_filter # for that def add_filters(filters) filters = [filters].flatten.compact @filters += filters @strainer = nil end def apply_global_filter(obj) global_filter.nil? ? obj : global_filter.call(obj) end # are there any not handled interrupts? def interrupt? !@interrupts.empty? end # push an interrupt to the stack. this interrupt is considered not handled. def push_interrupt(e) @interrupts.push(e) end # pop an interrupt from the stack def pop_interrupt @interrupts.pop end def handle_error(e, line_number = nil) e = internal_error unless e.is_a?(Liquid::Error) e.template_name ||= template_name e.line_number ||= line_number errors.push(e) exception_renderer.call(e).to_s end def invoke(method, *args) strainer.invoke(method, *args).to_liquid end # Push new local scope on the stack. use Context#stack instead def push(new_scope = {}) @scopes.unshift(new_scope) check_overflow end # Merge a hash of variables in the current local scope def merge(new_scopes) @scopes[0].merge!(new_scopes) end # Pop from the stack. use Context#stack instead def pop raise ContextError if @scopes.size == 1 @scopes.shift end # Pushes a new local scope on the stack, pops it at the end of the block # # Example: # context.stack do # context['var'] = 'hi' # end # # context['var'] #=> nil def stack(new_scope = {}) push(new_scope) yield ensure pop end # Creates a new context inheriting resource limits, filters, environment etc., # but with an isolated scope. def new_isolated_subcontext check_overflow self.class.build( resource_limits: resource_limits, static_environments: static_environments, registers: Registers.new(registers) ).tap do |subcontext| subcontext.base_scope_depth = base_scope_depth + 1 subcontext.exception_renderer = exception_renderer subcontext.filters = @filters subcontext.strainer = nil subcontext.errors = errors subcontext.warnings = warnings subcontext.disabled_tags = @disabled_tags end end def clear_instance_assigns @scopes[0] = {} end # Only allow String, Numeric, Hash, Array, Proc, Boolean or Liquid::Drop def []=(key, value) @scopes[0][key] = value end # Look up variable, either resolve directly after considering the name. We can directly handle # Strings, digits, floats and booleans (true,false). # If no match is made we lookup the variable in the current scope and # later move up to the parent blocks to see if we can resolve the variable somewhere up the tree. # Some special keywords return symbols. Those symbols are to be called on the rhs object in expressions # # Example: # products == empty #=> products.empty? def [](expression) evaluate(Expression.parse(expression)) end def key?(key) self[key] != nil end def evaluate(object) object.respond_to?(:evaluate) ? object.evaluate(self) : object end # Fetches an object starting at the local scope and then moving up the hierachy def find_variable(key, raise_on_not_found: true) # This was changed from find() to find_index() because this is a very hot # path and find_index() is optimized in MRI to reduce object allocation index = @scopes.find_index { |s| s.key?(key) } variable = if index lookup_and_evaluate(@scopes[index], key, raise_on_not_found: raise_on_not_found) else try_variable_find_in_environments(key, raise_on_not_found: raise_on_not_found) end variable = variable.to_liquid variable.context = self if variable.respond_to?(:context=) variable end def lookup_and_evaluate(obj, key, raise_on_not_found: true) if @strict_variables && raise_on_not_found && obj.respond_to?(:key?) && !obj.key?(key) raise Liquid::UndefinedVariable, "undefined variable #{key}" end value = obj[key] if value.is_a?(Proc) && obj.respond_to?(:[]=) obj[key] = value.arity == 0 ? value.call : value.call(self) else value end end def with_disabled_tags(tag_names) tag_names.each do |name| @disabled_tags[name] = @disabled_tags.fetch(name, 0) + 1 end yield ensure tag_names.each do |name| @disabled_tags[name] -= 1 end end def tag_disabled?(tag_name) @disabled_tags.fetch(tag_name, 0) > 0 end protected attr_writer :base_scope_depth, :warnings, :errors, :strainer, :filters, :disabled_tags private attr_reader :base_scope_depth def try_variable_find_in_environments(key, raise_on_not_found:) @environments.each do |environment| found_variable = lookup_and_evaluate(environment, key, raise_on_not_found: raise_on_not_found) if !found_variable.nil? || @strict_variables && raise_on_not_found return found_variable end end @static_environments.each do |environment| found_variable = lookup_and_evaluate(environment, key, raise_on_not_found: raise_on_not_found) if !found_variable.nil? || @strict_variables && raise_on_not_found return found_variable end end nil end def check_overflow raise StackLevelError, "Nesting too deep" if overflow? end def overflow? base_scope_depth + @scopes.length > Block::MAX_DEPTH end def internal_error # raise and catch to set backtrace and cause on exception raise Liquid::InternalError, 'internal' rescue Liquid::InternalError => exc exc end def squash_instance_assigns_with_environments @scopes.last.each_key do |k| @environments.each do |env| if env.key?(k) scopes.last[k] = lookup_and_evaluate(env, k) break end end end end # squash_instance_assigns_with_environments end # Context end # Liquid liquid-5.4.0/lib/liquid/document.rb000066400000000000000000000026501427076730100172210ustar00rootroot00000000000000# frozen_string_literal: true module Liquid class Document def self.parse(tokens, parse_context) doc = new(parse_context) doc.parse(tokens, parse_context) doc end attr_reader :parse_context, :body def initialize(parse_context) @parse_context = parse_context @body = new_body end def nodelist @body.nodelist end def parse(tokenizer, parse_context) while parse_body(tokenizer) end @body.freeze rescue SyntaxError => e e.line_number ||= parse_context.line_number raise end def unknown_tag(tag, _markup, _tokenizer) case tag when 'else', 'end' raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_outer_tag", tag: tag) else raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag) end end def render_to_output_buffer(context, output) @body.render_to_output_buffer(context, output) end def render(context) render_to_output_buffer(context, +'') end private def new_body parse_context.new_block_body end def parse_body(tokenizer) @body.parse(tokenizer, parse_context) do |unknown_tag_name, unknown_tag_markup| if unknown_tag_name unknown_tag(unknown_tag_name, unknown_tag_markup, tokenizer) true else false end end end end end liquid-5.4.0/lib/liquid/drop.rb000066400000000000000000000042471427076730100163530ustar00rootroot00000000000000# frozen_string_literal: true require 'set' module Liquid # A drop in liquid is a class which allows you to export DOM like things to liquid. # Methods of drops are callable. # The main use for liquid drops is to implement lazy loaded objects. # If you would like to make data available to the web designers which you don't want loaded unless needed then # a drop is a great way to do that. # # Example: # # class ProductDrop < Liquid::Drop # def top_sales # Shop.current.products.find(:all, :order => 'sales', :limit => 10 ) # end # end # # tmpl = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {%endfor%} ' ) # tmpl.render('product' => ProductDrop.new ) # will invoke top_sales query. # # Your drop can either implement the methods sans any parameters # or implement the liquid_method_missing(name) method which is a catch all. class Drop attr_writer :context # Catch all for the method def liquid_method_missing(method) return nil unless @context&.strict_variables raise Liquid::UndefinedDropMethod, "undefined method #{method}" end # called by liquid to invoke a drop def invoke_drop(method_or_key) if self.class.invokable?(method_or_key) send(method_or_key) else liquid_method_missing(method_or_key) end end def key?(_name) true end def inspect self.class.to_s end def to_liquid self end def to_s self.class.name end alias_method :[], :invoke_drop # Check for method existence without invoking respond_to?, which creates symbols def self.invokable?(method_name) invokable_methods.include?(method_name.to_s) end def self.invokable_methods @invokable_methods ||= begin blacklist = Liquid::Drop.public_instance_methods + [:each] if include?(Enumerable) blacklist += Enumerable.public_instance_methods blacklist -= [:sort, :count, :first, :min, :max] end whitelist = [:to_liquid] + (public_instance_methods - blacklist) Set.new(whitelist.map(&:to_s)) end end end end liquid-5.4.0/lib/liquid/errors.rb000066400000000000000000000025341427076730100167200ustar00rootroot00000000000000# frozen_string_literal: true module Liquid class Error < ::StandardError attr_accessor :line_number attr_accessor :template_name attr_accessor :markup_context def to_s(with_prefix = true) str = +"" str << message_prefix if with_prefix str << super() if markup_context str << " " str << markup_context end str end private def message_prefix str = +"" str << if is_a?(SyntaxError) "Liquid syntax error" else "Liquid error" end if line_number str << " (" str << template_name << " " if template_name str << "line " << line_number.to_s << ")" end str << ": " str end end ArgumentError = Class.new(Error) ContextError = Class.new(Error) FileSystemError = Class.new(Error) StandardError = Class.new(Error) SyntaxError = Class.new(Error) StackLevelError = Class.new(Error) MemoryError = Class.new(Error) ZeroDivisionError = Class.new(Error) FloatDomainError = Class.new(Error) UndefinedVariable = Class.new(Error) UndefinedDropMethod = Class.new(Error) UndefinedFilter = Class.new(Error) MethodOverrideError = Class.new(Error) DisabledError = Class.new(Error) InternalError = Class.new(Error) end liquid-5.4.0/lib/liquid/expression.rb000066400000000000000000000022561427076730100176040ustar00rootroot00000000000000# frozen_string_literal: true module Liquid class Expression LITERALS = { nil => nil, 'nil' => nil, 'null' => nil, '' => nil, 'true' => true, 'false' => false, 'blank' => '', 'empty' => '' }.freeze INTEGERS_REGEX = /\A(-?\d+)\z/ FLOATS_REGEX = /\A(-?\d[\d\.]+)\z/ # Use an atomic group (?>...) to avoid pathological backtracing from # malicious input as described in https://github.com/Shopify/liquid/issues/1357 RANGES_REGEX = /\A\(\s*(?>(\S+)\s*\.\.)\s*(\S+)\s*\)\z/ def self.parse(markup) return nil unless markup markup = markup.strip if (markup.start_with?('"') && markup.end_with?('"')) || (markup.start_with?("'") && markup.end_with?("'")) return markup[1..-2] end case markup when INTEGERS_REGEX Regexp.last_match(1).to_i when RANGES_REGEX RangeLookup.parse(Regexp.last_match(1), Regexp.last_match(2)) when FLOATS_REGEX Regexp.last_match(1).to_f else if LITERALS.key?(markup) LITERALS[markup] else VariableLookup.parse(markup) end end end end end liquid-5.4.0/lib/liquid/extensions.rb000066400000000000000000000014141427076730100175770ustar00rootroot00000000000000# frozen_string_literal: true require 'time' require 'date' class String # :nodoc: def to_liquid self end end class Symbol # :nodoc: def to_liquid to_s end end class Array # :nodoc: def to_liquid self end end class Hash # :nodoc: def to_liquid self end end class Numeric # :nodoc: def to_liquid self end end class Range # :nodoc: def to_liquid self end end class Time # :nodoc: def to_liquid self end end class DateTime < Date # :nodoc: def to_liquid self end end class Date # :nodoc: def to_liquid self end end class TrueClass def to_liquid # :nodoc: self end end class FalseClass def to_liquid # :nodoc: self end end class NilClass def to_liquid # :nodoc: self end end liquid-5.4.0/lib/liquid/file_system.rb000066400000000000000000000053661427076730100177350ustar00rootroot00000000000000# frozen_string_literal: true module Liquid # A Liquid file system is a way to let your templates retrieve other templates for use with the include tag. # # You can implement subclasses that retrieve templates from the database, from the file system using a different # path structure, you can provide them as hard-coded inline strings, or any manner that you see fit. # # You can add additional instance variables, arguments, or methods as needed. # # Example: # # Liquid::Template.file_system = Liquid::LocalFileSystem.new(template_path) # liquid = Liquid::Template.parse(template) # # This will parse the template with a LocalFileSystem implementation rooted at 'template_path'. class BlankFileSystem # Called by Liquid to retrieve a template file def read_template_file(_template_path) raise FileSystemError, "This liquid context does not allow includes." end end # This implements an abstract file system which retrieves template files named in a manner similar to Rails partials, # ie. with the template name prefixed with an underscore. The extension ".liquid" is also added. # # For security reasons, template paths are only allowed to contain letters, numbers, and underscore. # # Example: # # file_system = Liquid::LocalFileSystem.new("/some/path") # # file_system.full_path("mypartial") # => "/some/path/_mypartial.liquid" # file_system.full_path("dir/mypartial") # => "/some/path/dir/_mypartial.liquid" # # Optionally in the second argument you can specify a custom pattern for template filenames. # The Kernel::sprintf format specification is used. # Default pattern is "_%s.liquid". # # Example: # # file_system = Liquid::LocalFileSystem.new("/some/path", "%s.html") # # file_system.full_path("index") # => "/some/path/index.html" # class LocalFileSystem attr_accessor :root def initialize(root, pattern = "_%s.liquid") @root = root @pattern = pattern end def read_template_file(template_path) full_path = full_path(template_path) raise FileSystemError, "No such template '#{template_path}'" unless File.exist?(full_path) File.read(full_path) end def full_path(template_path) raise FileSystemError, "Illegal template name '#{template_path}'" unless %r{\A[^./][a-zA-Z0-9_/]+\z}.match?(template_path) full_path = if template_path.include?('/') File.join(root, File.dirname(template_path), @pattern % File.basename(template_path)) else File.join(root, @pattern % template_path) end raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path).start_with?(File.expand_path(root)) full_path end end end liquid-5.4.0/lib/liquid/forloop_drop.rb000066400000000000000000000041741427076730100201120ustar00rootroot00000000000000# frozen_string_literal: true module Liquid # @liquid_public_docs # @liquid_type object # @liquid_name forloop # @liquid_summary # Information about a parent [`for` loop](/api/liquid/tags#for). class ForloopDrop < Drop def initialize(name, length, parentloop) @name = name @length = length @parentloop = parentloop @index = 0 end # @liquid_public_docs # @liquid_name length # @liquid_summary # The total number of iterations in the loop. # @liquid_return [number] attr_reader :length # @liquid_public_docs # @liquid_name parentloop # @liquid_summary # 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 def name Usage.increment('forloop_drop_name') @name end # @liquid_public_docs # @liquid_summary # The 1-based index of the current iteration. # @liquid_return [number] def index @index + 1 end # @liquid_public_docs # @liquid_summary # The 0-based index of the current iteration. # @liquid_return [number] def index0 @index end # @liquid_public_docs # @liquid_summary # The 1-based index of the current iteration, in reverse order. # @liquid_return [number] def rindex @length - @index end # @liquid_public_docs # @liquid_summary # The 0-based index of the current iteration, in reverse order. # @liquid_return [number] def rindex0 @length - @index - 1 end # @liquid_public_docs # @liquid_summary # Returns `true` if the current iteration is the first. Returns `false` if not. # @liquid_return [boolean] def first @index == 0 end # @liquid_public_docs # @liquid_summary # Returns `true` if the current iteration is the last. Returns `false` if not. # @liquid_return [boolean] def last @index == @length - 1 end protected def increment! @index += 1 end end end liquid-5.4.0/lib/liquid/i18n.rb000066400000000000000000000017251427076730100161640ustar00rootroot00000000000000# frozen_string_literal: true require 'yaml' module Liquid class I18n DEFAULT_LOCALE = File.join(File.expand_path(__dir__), "locales", "en.yml") TranslationError = Class.new(StandardError) attr_reader :path def initialize(path = DEFAULT_LOCALE) @path = path end def translate(name, vars = {}) interpolate(deep_fetch_translation(name), vars) end alias_method :t, :translate def locale @locale ||= YAML.load_file(@path) end private def interpolate(name, vars) name.gsub(/%\{(\w+)\}/) do # raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym] (vars[Regexp.last_match(1).to_sym]).to_s end end def deep_fetch_translation(name) name.split('.').reduce(locale) do |level, cur| level[cur] || raise(TranslationError, "Translation for #{name} does not exist in locale #{path}") end end end end liquid-5.4.0/lib/liquid/interrupts.rb000066400000000000000000000007371427076730100176260ustar00rootroot00000000000000# frozen_string_literal: true module Liquid # An interrupt is any command that breaks processing of a block (ex: a for loop). class Interrupt attr_reader :message def initialize(message = nil) @message = message || "interrupt" end end # Interrupt that is thrown whenever a {% break %} is called. class BreakInterrupt < Interrupt; end # Interrupt that is thrown whenever a {% continue %} is called. class ContinueInterrupt < Interrupt; end end liquid-5.4.0/lib/liquid/lexer.rb000066400000000000000000000030031427076730100165130ustar00rootroot00000000000000# frozen_string_literal: true require "strscan" module Liquid class Lexer SPECIALS = { '|' => :pipe, '.' => :dot, ':' => :colon, ',' => :comma, '[' => :open_square, ']' => :close_square, '(' => :open_round, ')' => :close_round, '?' => :question, '-' => :dash, }.freeze IDENTIFIER = /[a-zA-Z_][\w-]*\??/ SINGLE_STRING_LITERAL = /'[^\']*'/ DOUBLE_STRING_LITERAL = /"[^\"]*"/ NUMBER_LITERAL = /-?\d+(\.\d+)?/ DOTDOT = /\.\./ COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/ WHITESPACE_OR_NOTHING = /\s*/ def initialize(input) @ss = StringScanner.new(input) end def tokenize @output = [] until @ss.eos? @ss.skip(WHITESPACE_OR_NOTHING) break if @ss.eos? tok = if (t = @ss.scan(COMPARISON_OPERATOR)) [:comparison, t] elsif (t = @ss.scan(SINGLE_STRING_LITERAL)) [:string, t] elsif (t = @ss.scan(DOUBLE_STRING_LITERAL)) [:string, t] elsif (t = @ss.scan(NUMBER_LITERAL)) [:number, t] elsif (t = @ss.scan(IDENTIFIER)) [:id, t] elsif (t = @ss.scan(DOTDOT)) [:dotdot, t] else c = @ss.getch if (s = SPECIALS[c]) [s, c] else raise SyntaxError, "Unexpected character #{c}" end end @output << tok end @output << [:end_of_string] end end end liquid-5.4.0/lib/liquid/locales/000077500000000000000000000000001427076730100164755ustar00rootroot00000000000000liquid-5.4.0/lib/liquid/locales/en.yml000066400000000000000000000040161427076730100176230ustar00rootroot00000000000000--- errors: syntax: tag_unexpected_args: "Syntax Error in '%{tag}' - Valid syntax: %{tag}" assign: "Syntax Error in 'assign' - Valid syntax: assign [var] = [source]" capture: "Syntax Error in 'capture' - Valid syntax: capture [var]" case: "Syntax Error in 'case' - Valid syntax: case [condition]" case_invalid_when: "Syntax Error in tag 'case' - Valid when condition: {% when [condition] [or condition2...] %}" case_invalid_else: "Syntax Error in tag 'case' - Valid else condition: {% else %} (no parameters) " cycle: "Syntax Error in 'cycle' - Valid syntax: cycle [name :] var [, var2, var3 ...]" for: "Syntax Error in 'for loop' - Valid syntax: for [item] in [collection]" for_invalid_in: "For loops require an 'in' clause" for_invalid_attribute: "Invalid attribute in for loop. Valid attributes are limit and offset" if: "Syntax Error in tag 'if' - Valid syntax: if [expression]" include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]" 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}" 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_outer_tag: "Unexpected outer '%{tag}' tag" unknown_tag: "Unknown tag '%{tag}'" variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}" argument: include: "Argument error in tag 'include' - Illegal template name" disabled: tag: "usage is not allowed in this context" liquid-5.4.0/lib/liquid/parse_context.rb000066400000000000000000000024611427076730100202610ustar00rootroot00000000000000# frozen_string_literal: true module Liquid class ParseContext attr_accessor :locale, :line_number, :trim_whitespace, :depth attr_reader :partial, :warnings, :error_mode def initialize(options = {}) @template_options = options ? options.dup : {} @locale = @template_options[:locale] ||= I18n.new @warnings = [] self.depth = 0 self.partial = false end def [](option_key) @options[option_key] end def new_block_body Liquid::BlockBody.new end def new_tokenizer(markup, start_line_number: nil, for_liquid_tag: false) Tokenizer.new(markup, line_number: start_line_number, for_liquid_tag: for_liquid_tag) end def parse_expression(markup) Expression.parse(markup) end def partial=(value) @partial = value @options = value ? partial_options : @template_options @error_mode = @options[:error_mode] || Template.error_mode end def partial_options @partial_options ||= begin dont_pass = @template_options[:include_options_blacklist] if dont_pass == true { locale: locale } elsif dont_pass.is_a?(Array) @template_options.reject { |k, _v| dont_pass.include?(k) } else @template_options end end end end end liquid-5.4.0/lib/liquid/parse_tree_visitor.rb000066400000000000000000000017741427076730100213210ustar00rootroot00000000000000# frozen_string_literal: true module Liquid class ParseTreeVisitor def self.for(node, callbacks = Hash.new(proc {})) if defined?(node.class::ParseTreeVisitor) node.class::ParseTreeVisitor else self end.new(node, callbacks) end def initialize(node, callbacks) @node = node @callbacks = callbacks end def add_callback_for(*classes, &block) callback = block callback = ->(node, _) { yield node } if block.arity.abs == 1 callback = ->(_, _) { yield } if block.arity.zero? classes.each { |klass| @callbacks[klass] = callback } self end def visit(context = nil) children.map do |node| item, new_context = @callbacks[node.class].call(node, context) [ item, ParseTreeVisitor.for(node, @callbacks).visit(new_context || context), ] end end protected def children @node.respond_to?(:nodelist) ? Array(@node.nodelist) : [] end end end liquid-5.4.0/lib/liquid/parser.rb000066400000000000000000000043071427076730100167000ustar00rootroot00000000000000# frozen_string_literal: true module Liquid class Parser def initialize(input) l = Lexer.new(input) @tokens = l.tokenize @p = 0 # pointer to current location end def jump(point) @p = point end def consume(type = nil) token = @tokens[@p] if type && token[0] != type raise SyntaxError, "Expected #{type} but found #{@tokens[@p].first}" end @p += 1 token[1] end # Only consumes the token if it matches the type # Returns the token's contents if it was consumed # or false otherwise. def consume?(type) token = @tokens[@p] return false unless token && token[0] == type @p += 1 token[1] end # Like consume? Except for an :id token of a certain name def id?(str) token = @tokens[@p] return false unless token && token[0] == :id return false unless token[1] == str @p += 1 token[1] end def look(type, ahead = 0) tok = @tokens[@p + ahead] return false unless tok tok[0] == type end def expression token = @tokens[@p] case token[0] when :id str = consume str << variable_lookups when :open_square str = consume str << expression str << consume(:close_square) str << variable_lookups when :string, :number consume when :open_round consume first = expression consume(:dotdot) last = expression consume(:close_round) "(#{first}..#{last})" else raise SyntaxError, "#{token} is not a valid expression" end end def argument str = +"" # might be a keyword argument (identifier: expression) if look(:id) && look(:colon, 1) str << consume << consume << ' ' end str << expression str end def variable_lookups str = +"" loop do if look(:open_square) str << consume str << expression str << consume(:close_square) elsif look(:dot) str << consume str << consume(:id) else break end end str end end end liquid-5.4.0/lib/liquid/parser_switching.rb000066400000000000000000000020461427076730100207550ustar00rootroot00000000000000# frozen_string_literal: true module Liquid module ParserSwitching def strict_parse_with_error_mode_fallback(markup) strict_parse_with_error_context(markup) rescue SyntaxError => e case parse_context.error_mode when :strict raise when :warn parse_context.warnings << e end lax_parse(markup) end def parse_with_selected_parser(markup) case parse_context.error_mode when :strict then strict_parse_with_error_context(markup) when :lax then lax_parse(markup) when :warn begin strict_parse_with_error_context(markup) rescue SyntaxError => e parse_context.warnings << e lax_parse(markup) end end end private def strict_parse_with_error_context(markup) strict_parse(markup) rescue SyntaxError => e e.line_number = line_number e.markup_context = markup_context(markup) raise e end def markup_context(markup) "in \"#{markup.strip}\"" end end end liquid-5.4.0/lib/liquid/partial_cache.rb000066400000000000000000000012731427076730100201620ustar00rootroot00000000000000# frozen_string_literal: true module Liquid class PartialCache def self.load(template_name, context:, parse_context:) cached_partials = context.registers[:cached_partials] cached = cached_partials[template_name] return cached if cached file_system = context.registers[:file_system] source = file_system.read_template_file(template_name) parse_context.partial = true template_factory = context.registers[:template_factory] template = template_factory.for(template_name) partial = template.parse(source, parse_context) cached_partials[template_name] = partial ensure parse_context.partial = false end end end liquid-5.4.0/lib/liquid/profiler.rb000066400000000000000000000077341427076730100172350ustar00rootroot00000000000000# frozen_string_literal: true require 'liquid/profiler/hooks' module Liquid # Profiler enables support for profiling template rendering to help track down performance issues. # # To enable profiling, first require 'liquid/profiler'. # Then, to profile a parse/render cycle, pass the profile: true option to Liquid::Template.parse. # After Liquid::Template#render is called, the template object makes available an instance of this # class via the Liquid::Template#profiler method. # # template = Liquid::Template.parse(template_content, profile: true) # output = template.render # profile = template.profiler # # This object contains all profiling information, containing information on what tags were rendered, # where in the templates these tags live, and how long each tag took to render. # # This is a tree structure that is Enumerable all the way down, and keeps track of tags and rendering times # inside of {% include %} tags. # # profile.each do |node| # # Access to the node itself # node.code # # # Which template and line number of this node. # # The top-level template name is `nil` by default, but can be set in the Liquid::Context before rendering. # node.partial # node.line_number # # # Render time in seconds of this node # node.render_time # # # If the template used {% include %}, this node will also have children. # node.children.each do |child2| # # ... # end # end # # Profiler also exposes the total time of the template's render in Liquid::Profiler#total_render_time. # # All render times are in seconds. There is a small performance hit when profiling is enabled. # class Profiler include Enumerable class Timing attr_reader :code, :template_name, :line_number, :children attr_accessor :total_time alias_method :render_time, :total_time alias_method :partial, :template_name def initialize(code: nil, template_name: nil, line_number: nil) @code = code @template_name = template_name @line_number = line_number @children = [] end def self_time @self_time ||= begin total_children_time = 0.0 @children.each do |child| total_children_time += child.total_time end @total_time - total_children_time end end end attr_reader :total_time alias_method :total_render_time, :total_time def initialize @root_children = [] @current_children = nil @total_time = 0.0 end def profile(template_name, &block) # nested renders are done from a tag that already has a timing node return yield if @current_children root_children = @root_children render_idx = root_children.length begin @current_children = root_children profile_node(template_name, &block) ensure @current_children = nil if (timing = root_children[render_idx]) @total_time += timing.total_time end end end def children children = @root_children if children.length == 1 children.first.children else children end end def each(&block) children.each(&block) end def [](idx) children[idx] end def length children.length end def profile_node(template_name, code: nil, line_number: nil) timing = Timing.new(code: code, template_name: template_name, line_number: line_number) parent_children = @current_children start_time = monotonic_time begin @current_children = timing.children yield ensure @current_children = parent_children timing.total_time = monotonic_time - start_time parent_children << timing end end private def monotonic_time Process.clock_gettime(Process::CLOCK_MONOTONIC) end end end liquid-5.4.0/lib/liquid/profiler/000077500000000000000000000000001427076730100166755ustar00rootroot00000000000000liquid-5.4.0/lib/liquid/profiler/hooks.rb000066400000000000000000000015331427076730100203470ustar00rootroot00000000000000# frozen_string_literal: true module Liquid module BlockBodyProfilingHook def render_node(context, output, node) if (profiler = context.profiler) profiler.profile_node(context.template_name, code: node.raw, line_number: node.line_number) do super end else super end end end BlockBody.prepend(BlockBodyProfilingHook) module DocumentProfilingHook def render_to_output_buffer(context, output) return super unless context.profiler context.profiler.profile(context.template_name) { super } end end Document.prepend(DocumentProfilingHook) module ContextProfilingHook attr_accessor :profiler def new_isolated_subcontext new_context = super new_context.profiler = profiler new_context end end Context.prepend(ContextProfilingHook) end liquid-5.4.0/lib/liquid/range_lookup.rb000066400000000000000000000020171427076730100200650ustar00rootroot00000000000000# frozen_string_literal: true module Liquid class RangeLookup def self.parse(start_markup, end_markup) start_obj = Expression.parse(start_markup) end_obj = Expression.parse(end_markup) if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate) new(start_obj, end_obj) else start_obj.to_i..end_obj.to_i end end attr_reader :start_obj, :end_obj def initialize(start_obj, end_obj) @start_obj = start_obj @end_obj = end_obj end def evaluate(context) start_int = to_integer(context.evaluate(@start_obj)) end_int = to_integer(context.evaluate(@end_obj)) start_int..end_int end private def to_integer(input) case input when Integer input when NilClass, String input.to_i else Utils.to_integer(input) end end class ParseTreeVisitor < Liquid::ParseTreeVisitor def children [@node.start_obj, @node.end_obj] end end end end liquid-5.4.0/lib/liquid/registers.rb000066400000000000000000000017051427076730100174120ustar00rootroot00000000000000# 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 liquid-5.4.0/lib/liquid/resource_limits.rb000066400000000000000000000032751427076730100206170ustar00rootroot00000000000000# frozen_string_literal: true module Liquid class ResourceLimits attr_accessor :render_length_limit, :render_score_limit, :assign_score_limit attr_reader :render_score, :assign_score def initialize(limits) @render_length_limit = limits[:render_length_limit] @render_score_limit = limits[:render_score_limit] @assign_score_limit = limits[:assign_score_limit] reset end def increment_render_score(amount) @render_score += amount raise_limits_reached if @render_score_limit && @render_score > @render_score_limit end def increment_assign_score(amount) @assign_score += amount raise_limits_reached if @assign_score_limit && @assign_score > @assign_score_limit end # update either render_length or assign_score based on whether or not the writes are captured def increment_write_score(output) if (last_captured = @last_capture_length) captured = output.bytesize increment = captured - last_captured @last_capture_length = captured increment_assign_score(increment) elsif @render_length_limit && output.bytesize > @render_length_limit raise_limits_reached end end def raise_limits_reached @reached_limit = true raise MemoryError, "Memory limits exceeded" end def reached? @reached_limit end def reset @reached_limit = false @last_capture_length = nil @render_score = @assign_score = 0 end def with_capture old_capture_length = @last_capture_length begin @last_capture_length = 0 yield ensure @last_capture_length = old_capture_length end end end end liquid-5.4.0/lib/liquid/standardfilters.rb000066400000000000000000000674011427076730100206010ustar00rootroot00000000000000# frozen_string_literal: true require 'cgi' require 'base64' require 'bigdecimal' module Liquid module StandardFilters MAX_INT = (1 << 31) - 1 HTML_ESCAPE = { '&' => '&', '>' => '>', '<' => '<', '"' => '"', "'" => ''', }.freeze HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/ STRIP_HTML_BLOCKS = Regexp.union( %r{}m, //m, %r{}m ) STRIP_HTML_TAGS = /<.*?>/m # @liquid_public_docs # @liquid_type filter # @liquid_category array # @liquid_summary # Returns the size of a string or array. # @liquid_description # The size of a string is the number of characters that the string includes. The size of an array is the number of items # in the array. # @liquid_syntax variable | size # @liquid_return [number] def size(input) input.respond_to?(:size) ? input.size : 0 end # @liquid_public_docs # @liquid_type filter # @liquid_category string # @liquid_summary # Converts a string to all lowercase characters. # @liquid_syntax string | downcase # @liquid_return [string] def downcase(input) input.to_s.downcase end # @liquid_public_docs # @liquid_type filter # @liquid_category string # @liquid_summary # Converts a string to all uppercase characters. # @liquid_syntax string | upcase # @liquid_return [string] def upcase(input) input.to_s.upcase end # @liquid_public_docs # @liquid_type filter # @liquid_category string # @liquid_summary # Capitalizes the first word in a string. # @liquid_syntax string | capitalize # @liquid_return [string] def capitalize(input) input.to_s.capitalize end # @liquid_public_docs # @liquid_type filter # @liquid_category string # @liquid_summary # Escapes a string. # @liquid_syntax string | escape # @liquid_return [string] def escape(input) CGI.escapeHTML(input.to_s) unless input.nil? end alias_method :h, :escape # @liquid_public_docs # @liquid_type filter # @liquid_category string # @liquid_summary # Escapes a string without changing characters that have already been escaped. # @liquid_syntax string | escape_once # @liquid_return [string] def escape_once(input) input.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE) end # @liquid_public_docs # @liquid_type filter # @liquid_category string # @liquid_summary # Converts any URL-unsafe characters in a string to the # [percent-encoded](https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding) equivalent. # @liquid_description # > Note: # > Spaces are converted to a `+` character, instead of a percent-encoded character. # @liquid_syntax string | url_encode # @liquid_return [string] def url_encode(input) CGI.escape(input.to_s) unless input.nil? end # @liquid_public_docs # @liquid_type filter # @liquid_category string # @liquid_summary # Decodes any [percent-encoded](https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding) characters # in a string. # @liquid_syntax string | url_decode # @liquid_return [string] def url_decode(input) return if input.nil? result = CGI.unescape(input.to_s) raise Liquid::ArgumentError, "invalid byte sequence in #{result.encoding}" unless result.valid_encoding? result end # @liquid_public_docs # @liquid_type filter # @liquid_category string # @liquid_summary # Encodes a string to [Base64 format](https://developer.mozilla.org/en-US/docs/Glossary/Base64). # @liquid_syntax string | base64_encode # @liquid_return [string] def base64_encode(input) Base64.strict_encode64(input.to_s) end # @liquid_public_docs # @liquid_type filter # @liquid_category string # @liquid_summary # Decodes a string in [Base64 format](https://developer.mozilla.org/en-US/docs/Glossary/Base64). # @liquid_syntax string | base64_decode # @liquid_return [string] def base64_decode(input) Base64.strict_decode64(input.to_s) rescue ::ArgumentError raise Liquid::ArgumentError, "invalid base64 provided to base64_decode" end # @liquid_public_docs # @liquid_type filter # @liquid_category string # @liquid_summary # Encodes a string to URL-safe [Base64 format](https://developer.mozilla.org/en-US/docs/Glossary/Base64). # @liquid_syntax string | base64_url_safe_encode # @liquid_return [string] def base64_url_safe_encode(input) Base64.urlsafe_encode64(input.to_s) end # @liquid_public_docs # @liquid_type filter # @liquid_category string # @liquid_summary # Decodes a string in URL-safe [Base64 format](https://developer.mozilla.org/en-US/docs/Glossary/Base64). # @liquid_syntax string | base64_url_safe_decode # @liquid_return [string] def base64_url_safe_decode(input) Base64.urlsafe_decode64(input.to_s) rescue ::ArgumentError raise Liquid::ArgumentError, "invalid base64 provided to base64_url_safe_decode" end # @liquid_public_docs # @liquid_type filter # @liquid_category string # @liquid_summary # Returns a substring or series of array items, starting at a given 0-based index. # @liquid_description # By default, the substring has a length of one character, and the array series has one array item. However, you can # provide a second parameter to specify the number of characters or array items. # @liquid_syntax string | slice # @liquid_return [string] def slice(input, offset, length = nil) offset = Utils.to_integer(offset) length = length ? Utils.to_integer(length) : 1 if input.is_a?(Array) input.slice(offset, length) || [] else input.to_s.slice(offset, length) || '' end end # @liquid_public_docs # @liquid_type filter # @liquid_category string # @liquid_summary # Truncates a string down to a given number of characters. # @liquid_description # If the specified number of characters is less than the length of the string, then an ellipsis (`...`) is appended to # the truncated string. The ellipsis is included in the character count of the truncated string. # @liquid_syntax string | truncate: number # @liquid_return [string] def truncate(input, length = 50, truncate_string = "...") return if input.nil? input_str = input.to_s length = Utils.to_integer(length) truncate_string_str = truncate_string.to_s l = length - truncate_string_str.length l = 0 if l < 0 input_str.length > length ? input_str[0...l].concat(truncate_string_str) : input_str end # @liquid_public_docs # @liquid_type filter # @liquid_category string # @liquid_summary # Truncates a string down to a given number of words. # @liquid_description # If the specified number of words is less than the number of words in the string, then an ellipsis (`...`) is appended to # the truncated string. # # > Caution: # > HTML tags are treated as words, so you should strip any HTML from truncated content. If you don't strip HTML, then # > closing HTML tags can be removed, which can result in unexpected behavior. # @liquid_syntax string | truncatewords: number # @liquid_return [string] def truncatewords(input, words = 15, truncate_string = "...") return if input.nil? input = input.to_s words = Utils.to_integer(words) words = 1 if words <= 0 wordlist = begin input.split(" ", words + 1) rescue RangeError raise if words + 1 < MAX_INT # e.g. integer #{words} too big to convert to `int' raise Liquid::ArgumentError, "integer #{words} too big for truncatewords" end return input if wordlist.length <= words wordlist.pop wordlist.join(" ").concat(truncate_string.to_s) end # @liquid_public_docs # @liquid_type filter # @liquid_category string # @liquid_summary # Splits a string into an array of substrings based on a given separator. # @liquid_syntax string | split: string # @liquid_return [array[string]] def split(input, pattern) input.to_s.split(pattern.to_s) end # @liquid_public_docs # @liquid_type filter # @liquid_category string # @liquid_summary # Strips all whitespace from the left and right of a string. # @liquid_syntax string | strip # @liquid_return [string] def strip(input) input.to_s.strip end # @liquid_public_docs # @liquid_type filter # @liquid_category string # @liquid_summary # Strips all whitespace from the left of a string. # @liquid_syntax string | lstrip # @liquid_return [string] def lstrip(input) input.to_s.lstrip end # @liquid_public_docs # @liquid_type filter # @liquid_category string # @liquid_summary # Strips all whitespace from the right of a string. # @liquid_syntax string | rstrip # @liquid_return [string] def rstrip(input) input.to_s.rstrip end # @liquid_public_docs # @liquid_type filter # @liquid_category string # @liquid_summary # Strips all HTML tags from a string. # @liquid_syntax string | strip_html # @liquid_return [string] def strip_html(input) empty = '' result = input.to_s.gsub(STRIP_HTML_BLOCKS, empty) result.gsub!(STRIP_HTML_TAGS, empty) result end # @liquid_public_docs # @liquid_type filter # @liquid_category string # @liquid_summary # Strips all newline characters (line breaks) from a string. # @liquid_syntax string | strip_newlines # @liquid_return [string] def strip_newlines(input) input.to_s.gsub(/\r?\n/, '') end # @liquid_public_docs # @liquid_type filter # @liquid_category array # @liquid_summary # Combines all of the items in an array into a single string, separated by a space. # @liquid_syntax array | join # @liquid_return [string] def join(input, glue = ' ') InputIterator.new(input, context).join(glue) end # @liquid_public_docs # @liquid_type filter # @liquid_category array # @liquid_summary # Sorts the items in an array in case-sensitive alphabetical, or numerical, order. # @liquid_syntax array | sort # @liquid_return [array[untyped]] def sort(input, property = nil) ary = InputIterator.new(input, context) return [] if ary.empty? if property.nil? ary.sort do |a, b| nil_safe_compare(a, b) end elsif ary.all? { |el| el.respond_to?(:[]) } begin ary.sort { |a, b| nil_safe_compare(a[property], b[property]) } rescue TypeError raise_property_error(property) end end end # @liquid_public_docs # @liquid_type filter # @liquid_category array # @liquid_summary # Sorts the items in an array in case-insensitive alphabetical order. # @liquid_description # > Caution: # > You shouldn't use the `sort_natural` filter to sort numerical values. When comparing items an array, each item is converted to a # > string, so sorting on numerical values can lead to unexpected results. # @liquid_syntax array | sort_natural # @liquid_return [array[untyped]] def sort_natural(input, property = nil) ary = InputIterator.new(input, context) return [] if ary.empty? if property.nil? ary.sort do |a, b| nil_safe_casecmp(a, b) end elsif ary.all? { |el| el.respond_to?(:[]) } begin ary.sort { |a, b| nil_safe_casecmp(a[property], b[property]) } rescue TypeError raise_property_error(property) end end end # @liquid_public_docs # @liquid_type filter # @liquid_category array # @liquid_summary # Filters an array to include only items with a specific property value. # @liquid_description # This requires you to provide both the property name and the associated value. # @liquid_syntax array | where: string, string # @liquid_return [array[untyped]] def where(input, property, target_value = nil) ary = InputIterator.new(input, context) if ary.empty? [] elsif target_value.nil? ary.select do |item| item[property] rescue TypeError raise_property_error(property) rescue NoMethodError return nil unless item.respond_to?(:[]) raise end else ary.select do |item| item[property] == target_value rescue TypeError raise_property_error(property) rescue NoMethodError return nil unless item.respond_to?(:[]) raise end end end # @liquid_public_docs # @liquid_type filter # @liquid_category array # @liquid_summary # Removes any duplicate items in an array. # @liquid_syntax array | uniq # @liquid_return [array[untyped]] def uniq(input, property = nil) ary = InputIterator.new(input, context) if property.nil? ary.uniq elsif ary.empty? # The next two cases assume a non-empty array. [] else ary.uniq do |item| item[property] rescue TypeError raise_property_error(property) rescue NoMethodError return nil unless item.respond_to?(:[]) raise end end end # @liquid_public_docs # @liquid_type filter # @liquid_category array # @liquid_summary # Reverses the order of the items in an array. # @liquid_syntax array | reverse # @liquid_return [array[untyped]] def reverse(input) ary = InputIterator.new(input, context) ary.reverse end # @liquid_public_docs # @liquid_type filter # @liquid_category array # @liquid_summary # Creates an array of values from a specific property of the items in an array. # @liquid_syntax array | map: string # @liquid_return [array[untyped]] def map(input, property) InputIterator.new(input, context).map do |e| e = e.call if e.is_a?(Proc) if property == "to_liquid" e elsif e.respond_to?(:[]) r = e[property] r.is_a?(Proc) ? r.call : r end end rescue TypeError raise_property_error(property) end # @liquid_public_docs # @liquid_type filter # @liquid_category array # @liquid_summary # Removes any `nil` items from an array. # @liquid_syntax array | compact # @liquid_return [array[untyped]] def compact(input, property = nil) ary = InputIterator.new(input, context) if property.nil? ary.compact elsif ary.empty? # The next two cases assume a non-empty array. [] else ary.reject do |item| item[property].nil? rescue TypeError raise_property_error(property) rescue NoMethodError return nil unless item.respond_to?(:[]) raise end end end # @liquid_public_docs # @liquid_type filter # @liquid_category string # @liquid_summary # Replaces any instance of a substring inside a string with a given string. # @liquid_syntax string | replace: string, string # @liquid_return [string] def replace(input, string, replacement = '') input.to_s.gsub(string.to_s, replacement.to_s) end # @liquid_public_docs # @liquid_type filter # @liquid_category string # @liquid_summary # Replaces the first instance of a substring inside a string with a given string. # @liquid_syntax string | replace_first: string, string # @liquid_return [string] def replace_first(input, string, replacement = '') input.to_s.sub(string.to_s, replacement.to_s) end # @liquid_public_docs # @liquid_type filter # @liquid_category string # @liquid_summary # Replaces the last instance of a substring inside a string with a given string. # @liquid_syntax string | replace_last: string, string # @liquid_return [string] def replace_last(input, string, replacement) input = input.to_s string = string.to_s replacement = replacement.to_s start_index = input.rindex(string) return input unless start_index output = input.dup output[start_index, string.length] = replacement output end # @liquid_public_docs # @liquid_type filter # @liquid_category string # @liquid_summary # Removes any instance of a substring inside a string. # @liquid_syntax string | remove: string # @liquid_return [string] def remove(input, string) replace(input, string, '') end # @liquid_public_docs # @liquid_type filter # @liquid_category string # @liquid_summary # Removes the first instance of a substring inside a string. # @liquid_syntax string | remove_first: string # @liquid_return [string] def remove_first(input, string) replace_first(input, string, '') end # @liquid_public_docs # @liquid_type filter # @liquid_category string # @liquid_summary # Removes the last instance of a substring inside a string. # @liquid_syntax string | remove_last: string # @liquid_return [string] def remove_last(input, string) replace_last(input, string, '') end # @liquid_public_docs # @liquid_type filter # @liquid_category string # @liquid_summary # Adds a given string to the end of a string. # @liquid_syntax string | append: string # @liquid_return [string] def append(input, string) input.to_s + string.to_s end # @liquid_public_docs # @liquid_type filter # @liquid_category array # @liquid_summary # Concatenates (combines) two arrays. # @liquid_description # > Note: # > The `concat` filter won't filter out duplicates. If you want to remove duplicates, then you need to use the # > [`uniq` filter](/api/liquid/filters#uniq). # @liquid_syntax array | concat: array # @liquid_return [array[untyped]] def concat(input, array) unless array.respond_to?(:to_ary) raise ArgumentError, "concat filter requires an array argument" end InputIterator.new(input, context).concat(array) end # @liquid_public_docs # @liquid_type filter # @liquid_category string # @liquid_summary # Adds a given string to the beginning of a string. # @liquid_syntax string | prepend: string # @liquid_return [string] def prepend(input, string) string.to_s + input.to_s end # @liquid_public_docs # @liquid_type filter # @liquid_category string # @liquid_summary # Converts newlines (`\n`) in a string to HTML line breaks (`
`). # @liquid_syntax string | newline_to_br # @liquid_return [string] def newline_to_br(input) input.to_s.gsub(/\r?\n/, "
\n") end # Reformat a date using Ruby's core Time#strftime( string ) -> string # # %a - The abbreviated weekday name (``Sun'') # %A - The full weekday name (``Sunday'') # %b - The abbreviated month name (``Jan'') # %B - The full month name (``January'') # %c - The preferred local date and time representation # %d - Day of the month (01..31) # %H - Hour of the day, 24-hour clock (00..23) # %I - Hour of the day, 12-hour clock (01..12) # %j - Day of the year (001..366) # %m - Month of the year (01..12) # %M - Minute of the hour (00..59) # %p - Meridian indicator (``AM'' or ``PM'') # %s - Number of seconds since 1970-01-01 00:00:00 UTC. # %S - Second of the minute (00..60) # %U - Week number of the current year, # starting with the first Sunday as the first # day of the first week (00..53) # %W - Week number of the current year, # starting with the first Monday as the first # day of the first week (00..53) # %w - Day of the week (Sunday is 0, 0..6) # %x - Preferred representation for the date alone, no time # %X - Preferred representation for the time alone, no date # %y - Year without a century (00..99) # %Y - Year with century # %Z - Time zone name # %% - Literal ``%'' character # # See also: http://www.ruby-doc.org/core/Time.html#method-i-strftime def date(input, format) return input if format.to_s.empty? return input unless (date = Utils.to_date(input)) date.strftime(format.to_s) end # @liquid_public_docs # @liquid_type filter # @liquid_category array # @liquid_summary # Returns the first item in an array. # @liquid_syntax array | first # @liquid_return [untyped] def first(array) array.first if array.respond_to?(:first) end # @liquid_public_docs # @liquid_type filter # @liquid_category array # @liquid_summary # Returns the last item in an array. # @liquid_syntax array | last # @liquid_return [untyped] def last(array) array.last if array.respond_to?(:last) end # @liquid_public_docs # @liquid_type filter # @liquid_category math # @liquid_summary # Returns the absolute value of a number. # @liquid_syntax number | abs # @liquid_return [number] def abs(input) result = Utils.to_number(input).abs result.is_a?(BigDecimal) ? result.to_f : result end # @liquid_public_docs # @liquid_type filter # @liquid_category math # @liquid_summary # Adds two numbers. # @liquid_syntax number | plus: number # @liquid_return [number] def plus(input, operand) apply_operation(input, operand, :+) end # @liquid_public_docs # @liquid_type filter # @liquid_category math # @liquid_summary # Subtracts a given number from another number. # @liquid_syntax number | minus: number # @liquid_return [number] def minus(input, operand) apply_operation(input, operand, :-) end # @liquid_public_docs # @liquid_type filter # @liquid_category math # @liquid_summary # Multiplies a number by a given number. # @liquid_syntax number | times: number # @liquid_return [number] def times(input, operand) apply_operation(input, operand, :*) end # @liquid_public_docs # @liquid_type filter # @liquid_category math # @liquid_summary # Divides a number by a given number. # @liquid_syntax number | divided_by: number # @liquid_return [number] def divided_by(input, operand) apply_operation(input, operand, :/) rescue ::ZeroDivisionError => e raise Liquid::ZeroDivisionError, e.message end # @liquid_public_docs # @liquid_type filter # @liquid_category math # @liquid_summary # Returns the remainder of dividing a number by a given number. # @liquid_syntax number | modulo: number # @liquid_return [number] def modulo(input, operand) apply_operation(input, operand, :%) rescue ::ZeroDivisionError => e raise Liquid::ZeroDivisionError, e.message end # @liquid_public_docs # @liquid_type filter # @liquid_category math # @liquid_summary # Rounds a number to the nearest integer. # @liquid_syntax number | round # @liquid_return [number] def round(input, n = 0) result = Utils.to_number(input).round(Utils.to_number(n)) result = result.to_f if result.is_a?(BigDecimal) result = result.to_i if n == 0 result rescue ::FloatDomainError => e raise Liquid::FloatDomainError, e.message end # @liquid_public_docs # @liquid_type filter # @liquid_category math # @liquid_summary # Rounds a number up to the nearest integer. # @liquid_syntax number | ceil # @liquid_return [number] def ceil(input) Utils.to_number(input).ceil.to_i rescue ::FloatDomainError => e raise Liquid::FloatDomainError, e.message end # @liquid_public_docs # @liquid_type filter # @liquid_category math # @liquid_summary # Rounds a number down to the nearest integer. # @liquid_syntax number | floor # @liquid_return [number] def floor(input) Utils.to_number(input).floor.to_i rescue ::FloatDomainError => e raise Liquid::FloatDomainError, e.message end # @liquid_public_docs # @liquid_type filter # @liquid_category math # @liquid_summary # Limits a number to a minimum value. # @liquid_syntax number | at_least # @liquid_return [number] def at_least(input, n) min_value = Utils.to_number(n) result = Utils.to_number(input) result = min_value if min_value > result result.is_a?(BigDecimal) ? result.to_f : result end # @liquid_public_docs # @liquid_type filter # @liquid_category math # @liquid_summary # Limits a number to a maximum value. # @liquid_syntax number | at_most # @liquid_return [number] def at_most(input, n) max_value = Utils.to_number(n) result = Utils.to_number(input) result = max_value if max_value < result result.is_a?(BigDecimal) ? result.to_f : result end # @liquid_public_docs # @liquid_type filter # @liquid_category default # @liquid_summary # Sets a default value for any variable whose value is one of the following: # # - [`empty`](/api/liquid/basics#empty) # - [`false`](/api/liquid/basics#truthy-and-falsy) # - [`nil`](/api/liquid/basics#nil) # @liquid_syntax variable | default: variable # @liquid_return [untyped] # @liquid_optional_param allow_false [boolean] Whether to use false values instead of the default. def default(input, default_value = '', options = {}) options = {} unless options.is_a?(Hash) false_check = options['allow_false'] ? input.nil? : !Liquid::Utils.to_liquid_value(input) false_check || (input.respond_to?(:empty?) && input.empty?) ? default_value : input end private attr_reader :context def raise_property_error(property) raise Liquid::ArgumentError, "cannot select the property '#{property}'" end def apply_operation(input, operand, operation) result = Utils.to_number(input).send(operation, Utils.to_number(operand)) result.is_a?(BigDecimal) ? result.to_f : result end def nil_safe_compare(a, b) result = a <=> b if result result elsif a.nil? 1 elsif b.nil? -1 else raise Liquid::ArgumentError, "cannot sort values of incompatible types" end end def nil_safe_casecmp(a, b) if !a.nil? && !b.nil? a.to_s.casecmp(b.to_s) else a.nil? ? 1 : -1 end end class InputIterator include Enumerable def initialize(input, context) @context = context @input = if input.is_a?(Array) input.flatten elsif input.is_a?(Hash) [input] elsif input.is_a?(Enumerable) input else Array(input) end end def join(glue) to_a.join(glue.to_s) end def concat(args) to_a.concat(args) end def reverse reverse_each.to_a end def uniq(&block) to_a.uniq(&block) end def compact to_a.compact end def empty? @input.each { return false } true end def each @input.each do |e| e = e.respond_to?(:to_liquid) ? e.to_liquid : e e.context = @context if e.respond_to?(:context=) yield(e) end end end end Template.register_filter(StandardFilters) end liquid-5.4.0/lib/liquid/strainer_factory.rb000066400000000000000000000015221427076730100207560ustar00rootroot00000000000000# 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 GlobalCache.add_filter(filter) end def create(context, filters = []) strainer_from_cache(filters).new(context) end def global_filter_names GlobalCache.filter_method_names end GlobalCache = Class.new(StrainerTemplate) private def strainer_from_cache(filters) if filters.empty? GlobalCache else strainer_class_cache[filters] ||= begin klass = Class.new(GlobalCache) filters.each { |f| klass.add_filter(f) } klass end end end def strainer_class_cache @strainer_class_cache ||= {} end end end liquid-5.4.0/lib/liquid/strainer_template.rb000066400000000000000000000033471427076730100211310ustar00rootroot00000000000000# frozen_string_literal: true require 'set' module Liquid # StrainerTemplate is the computed class for the filters system. # New filters are mixed into the strainer class which is then instantiated for each liquid template render run. # # The Strainer only allows method calls defined in filters given to it via StrainerFactory.add_global_filter, # Context#add_filters or Template.register_filter class StrainerTemplate def initialize(context) @context = context end class << self def add_filter(filter) return if include?(filter) invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) } if invokable_non_public_methods.any? raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}" end include(filter) filter_methods.merge(filter.public_instance_methods.map(&:to_s)) end def invokable?(method) filter_methods.include?(method.to_s) end 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 def filter_methods @filter_methods ||= Set.new end end def invoke(method, *args) if self.class.invokable?(method) send(method, *args) elsif @context.strict_filters raise Liquid::UndefinedFilter, "undefined filter #{method}" else args.first end rescue ::ArgumentError => e raise Liquid::ArgumentError, e.message, e.backtrace end end end liquid-5.4.0/lib/liquid/tablerowloop_drop.rb000066400000000000000000000053061427076730100211410ustar00rootroot00000000000000# frozen_string_literal: true module Liquid # @liquid_public_docs # @liquid_type object # @liquid_name tablerowloop # @liquid_summary # Information about a parent [`tablerow` loop](/api/liquid/tags#tablerow). class TablerowloopDrop < Drop def initialize(length, cols) @length = length @row = 1 @col = 1 @cols = cols @index = 0 end # @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 @index + 1 end # @liquid_public_docs # @liquid_summary # The 0-based index of the current iteration. # @liquid_return [number] def index0 @index end # @liquid_public_docs # @liquid_summary # The 0-based index of the current column. # @liquid_return [number] def col0 @col - 1 end # @liquid_public_docs # @liquid_summary # The 1-based index of the current iteration, in reverse order. # @liquid_return [number] def rindex @length - @index end # @liquid_public_docs # @liquid_summary # The 0-based index of the current iteration, in reverse order. # @liquid_return [number] def rindex0 @length - @index - 1 end # @liquid_public_docs # @liquid_summary # Returns `true` if the current iteration is the first. Returns `false` if not. # @liquid_return [boolean] def first @index == 0 end # @liquid_public_docs # @liquid_summary # Returns `true` if the current iteration is the last. Returns `false` if not. # @liquid_return [boolean] def last @index == @length - 1 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 @col == 1 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 @col == @cols end protected def increment! @index += 1 if @col == @cols @col = 1 @row += 1 else @col += 1 end end end end liquid-5.4.0/lib/liquid/tag.rb000066400000000000000000000025551427076730100161620ustar00rootroot00000000000000# frozen_string_literal: true module Liquid class Tag attr_reader :nodelist, :tag_name, :line_number, :parse_context alias_method :options, :parse_context include ParserSwitching class << self def parse(tag_name, markup, tokenizer, parse_context) tag = new(tag_name, markup, parse_context) tag.parse(tokenizer) tag end def disable_tags(*tag_names) @disabled_tags ||= [] @disabled_tags.concat(tag_names) prepend(Disabler) end private :new end def initialize(tag_name, markup, parse_context) @tag_name = tag_name @markup = markup @parse_context = parse_context @line_number = parse_context.line_number end def parse(_tokens) end def raw "#{@tag_name} #{@markup}" end def name self.class.name.downcase end def render(_context) '' end # For backwards compatibility with custom tags. In a future release, the semantics # of the `render_to_output_buffer` method will become the default and the `render` # method will be removed. def render_to_output_buffer(context, output) output << render(context) output end def blank? false end private def parse_expression(markup) parse_context.parse_expression(markup) end end end liquid-5.4.0/lib/liquid/tag/000077500000000000000000000000001427076730100156265ustar00rootroot00000000000000liquid-5.4.0/lib/liquid/tag/disableable.rb000066400000000000000000000011361427076730100204030ustar00rootroot00000000000000# frozen_string_literal: true module Liquid class Tag module Disableable def render_to_output_buffer(context, output) if context.tag_disabled?(tag_name) output << disabled_error(context) return end super end def disabled_error(context) # raise then rescue the exception so that the Context#exception_renderer can re-raise it raise DisabledError, "#{tag_name} #{parse_context[:locale].t('errors.disabled.tag')}" rescue DisabledError => exc context.handle_error(exc, line_number) end end end end liquid-5.4.0/lib/liquid/tag/disabler.rb000066400000000000000000000006141427076730100177410ustar00rootroot00000000000000# frozen_string_literal: true module Liquid class Tag module Disabler module ClassMethods attr_reader :disabled_tags end def self.prepended(base) base.extend(ClassMethods) end def render_to_output_buffer(context, output) context.with_disabled_tags(self.class.disabled_tags) do super end end end end end liquid-5.4.0/lib/liquid/tags/000077500000000000000000000000001427076730100160115ustar00rootroot00000000000000liquid-5.4.0/lib/liquid/tags/assign.rb000066400000000000000000000037531427076730100176320ustar00rootroot00000000000000# frozen_string_literal: true module Liquid # @liquid_public_docs # @liquid_type tag # @liquid_category variable # @liquid_name assign # @liquid_summary # Creates a new variable. # @liquid_description # You can create variables of any [basic type](/api/liquid/basics#types), [object](/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 Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om # @api private def self.raise_syntax_error(parse_context) raise Liquid::SyntaxError, parse_context.locale.t('errors.syntax.assign') end attr_reader :to, :from def initialize(tag_name, markup, parse_context) super if markup =~ Syntax @to = Regexp.last_match(1) @from = Variable.new(Regexp.last_match(2), parse_context) else self.class.raise_syntax_error(parse_context) end end def render_to_output_buffer(context, output) val = @from.render(context) context.scopes.last[@to] = val context.resource_limits.increment_assign_score(assign_score_of(val)) output end def blank? true end private def assign_score_of(val) if val.instance_of?(String) val.bytesize elsif val.instance_of?(Array) sum = 1 # Uses #each to avoid extra allocations. val.each { |child| sum += assign_score_of(child) } sum elsif val.instance_of?(Hash) sum = 1 val.each do |key, entry_value| sum += assign_score_of(key) sum += assign_score_of(entry_value) end sum else 1 end end class ParseTreeVisitor < Liquid::ParseTreeVisitor def children [@node.from] end end end Template.register_tag('assign', Assign) end liquid-5.4.0/lib/liquid/tags/break.rb000066400000000000000000000013001427076730100174140ustar00rootroot00000000000000# frozen_string_literal: true module Liquid # Break tag to be used to break out of a for loop. # # == Basic Usage: # {% for item in collection %} # {% if item.condition %} # {% break %} # {% endif %} # {% endfor %} # # @liquid_public_docs # @liquid_type tag # @liquid_category iteration # @liquid_name break # @liquid_summary # Stops a [`for` loop](/api/liquid/tags#for) from iterating. # @liquid_syntax # {% break %} class Break < Tag INTERRUPT = BreakInterrupt.new.freeze def render_to_output_buffer(context, output) context.push_interrupt(INTERRUPT) output end end Template.register_tag('break', Break) end liquid-5.4.0/lib/liquid/tags/capture.rb000066400000000000000000000021651427076730100200050ustar00rootroot00000000000000# frozen_string_literal: true module Liquid # @liquid_public_docs # @liquid_type tag # @liquid_category variable # @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 %} # @liquid_syntax_keyword variable The name of the variable being created. # @liquid_syntax_keyword value The value you want to assign to the variable. class Capture < Block Syntax = /(#{VariableSignature}+)/o def initialize(tag_name, markup, options) super if markup =~ Syntax @to = Regexp.last_match(1) else raise SyntaxError, options[:locale].t("errors.syntax.capture") end end def render_to_output_buffer(context, output) context.resource_limits.with_capture do capture_output = render(context) context.scopes.last[@to] = capture_output end output end def blank? true end end Template.register_tag('capture', Capture) end liquid-5.4.0/lib/liquid/tags/case.rb000066400000000000000000000067111427076730100172560ustar00rootroot00000000000000# frozen_string_literal: true 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 Syntax = /(#{QuotedFragment})/o WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om attr_reader :blocks, :left def initialize(tag_name, markup, options) super @blocks = [] if markup =~ Syntax @left = parse_expression(Regexp.last_match(1)) else raise SyntaxError, options[:locale].t("errors.syntax.case") end end def parse(tokens) body = case_body = new_body body = @blocks.last.attachment while parse_body(body, tokens) @blocks.reverse_each do |condition| body = condition.attachment unless body.frozen? body.remove_blank_strings if blank? body.freeze end end case_body.freeze end def nodelist @blocks.map(&:attachment) end def unknown_tag(tag, markup, tokens) case tag when 'when' record_when_condition(markup) when 'else' record_else_condition(markup) else super end end def render_to_output_buffer(context, output) execute_else_block = true @blocks.each do |block| if block.else? block.attachment.render_to_output_buffer(context, output) if execute_else_block next end result = Liquid::Utils.to_liquid_value( block.evaluate(context) ) if result execute_else_block = false block.attachment.render_to_output_buffer(context, output) end end output end private def record_when_condition(markup) body = new_body while markup unless markup =~ WhenSyntax raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_when") end markup = Regexp.last_match(2) block = Condition.new(@left, '==', Condition.parse_expression(parse_context, Regexp.last_match(1))) block.attach(body) @blocks << block end end def record_else_condition(markup) unless markup.strip.empty? raise SyntaxError, options[:locale].t("errors.syntax.case_invalid_else") end block = ElseCondition.new block.attach(new_body) @blocks << block end class ParseTreeVisitor < Liquid::ParseTreeVisitor def children [@node.left] + @node.blocks end end end Template.register_tag('case', Case) end liquid-5.4.0/lib/liquid/tags/comment.rb000066400000000000000000000013171427076730100200020ustar00rootroot00000000000000# frozen_string_literal: true 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 won't be rendered. # @liquid_syntax # {% comment %} # content # {% endcomment %} # @liquid_syntax_keyword content The content of the comment. class Comment < Block def render_to_output_buffer(_context, output) output end def unknown_tag(_tag, _markup, _tokens) end def blank? true end end Template.register_tag('comment', Comment) end liquid-5.4.0/lib/liquid/tags/continue.rb000066400000000000000000000010141427076730100201560ustar00rootroot00000000000000# frozen_string_literal: true module Liquid # @liquid_public_docs # @liquid_type tag # @liquid_category iteration # @liquid_name continue # @liquid_summary # Causes a [`for` loop](/api/liquid/tags#for) to skip to the next iteration. # @liquid_syntax # {% continue %} class Continue < Tag INTERRUPT = ContinueInterrupt.new.freeze def render_to_output_buffer(context, output) context.push_interrupt(INTERRUPT) output end end Template.register_tag('continue', Continue) end liquid-5.4.0/lib/liquid/tags/cycle.rb000066400000000000000000000040531427076730100174370ustar00rootroot00000000000000# frozen_string_literal: true module Liquid # @liquid_public_docs # @liquid_type tag # @liquid_category iteration # @liquid_name cycle # @liquid_summary # Loops through a group of strings and outputs them one at a time for each iteration of a [`for` loop](/api/liquid/tags#for). # @liquid_description # The `cycle` tag must be used inside a `for` loop. # # > 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 SimpleSyntax = /\A#{QuotedFragment}+/o NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om attr_reader :variables def initialize(tag_name, markup, options) super case markup when NamedSyntax @variables = variables_from_string(Regexp.last_match(2)) @name = parse_expression(Regexp.last_match(1)) when SimpleSyntax @variables = variables_from_string(markup) @name = @variables.to_s else raise SyntaxError, options[:locale].t("errors.syntax.cycle") end end def render_to_output_buffer(context, output) context.registers[:cycle] ||= {} key = context.evaluate(@name) iteration = context.registers[:cycle][key].to_i val = context.evaluate(@variables[iteration]) if val.is_a?(Array) val = val.join elsif !val.is_a?(String) val = val.to_s end output << val iteration += 1 iteration = 0 if iteration >= @variables.size context.registers[:cycle][key] = iteration output end private def variables_from_string(markup) markup.split(',').collect do |var| var =~ /\s*(#{QuotedFragment})\s*/o Regexp.last_match(1) ? parse_expression(Regexp.last_match(1)) : nil end.compact end class ParseTreeVisitor < Liquid::ParseTreeVisitor def children Array(@node.variables) end end end Template.register_tag('cycle', Cycle) end liquid-5.4.0/lib/liquid/tags/decrement.rb000066400000000000000000000026711427076730100203120ustar00rootroot00000000000000# frozen_string_literal: true module Liquid # @liquid_public_docs # @liquid_type tag # @liquid_category variable # @liquid_name decrement # @liquid_summary # Creates a new variable, with a default value of -1, that's decreased by 1 with each subsequent call. # @liquid_description # Variables that are declared with `decrement` 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 # [snippets](/themes/architecture#snippets) included in the file. # # Similarly, variables that are created with `decrement` are independent from those created with [`assign`](/api/liquid/tags#assign) # and [`capture`](/api/liquid/tags#capture). However, `decrement` and [`increment`](/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 def initialize(tag_name, markup, options) super @variable = markup.strip end def render_to_output_buffer(context, output) value = context.environments.first[@variable] ||= 0 value -= 1 context.environments.first[@variable] = value output << value.to_s output end end Template.register_tag('decrement', Decrement) end liquid-5.4.0/lib/liquid/tags/echo.rb000066400000000000000000000021141427076730100172520ustar00rootroot00000000000000# frozen_string_literal: true module Liquid # @liquid_public_docs # @liquid_type tag # @liquid_category syntax # @liquid_name echo # @liquid_summary # Outputs an expression. # @liquid_description # Using the `echo` tag is the same as wrapping an expression in curly brackets (`{{` and `}}`). However, unlike the curly # bracket method, you can use the `echo` tag inside [`liquid` tags](/api/liquid/tags#liquid). # # > Tip: # > You can use [filters](/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 attr_reader :variable def initialize(tag_name, markup, parse_context) super @variable = Variable.new(markup, parse_context) end def render(context) @variable.render_to_output_buffer(context, +'') end class ParseTreeVisitor < Liquid::ParseTreeVisitor def children [@node.variable] end end end Template.register_tag('echo', Echo) end liquid-5.4.0/lib/liquid/tags/for.rb000066400000000000000000000133421427076730100171270ustar00rootroot00000000000000# frozen_string_literal: true module Liquid # @liquid_public_docs # @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](/api/liquid/tags#paginate) to split the items over multiple pages. # # > Tip: # > Every `for` loop has an associated [`forloop` object](/api/liquid/objects#forloop) with information about the loop. # @liquid_syntax # {% for variable in array %} # expression # {% endfor %} # @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 for each iteration. # @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. # @liquid_optional_param reversed [untyped] Iterate in reverse order. class For < Block Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o attr_reader :collection_name, :variable_name, :limit, :from def initialize(tag_name, markup, options) super @from = @limit = nil parse_with_selected_parser(markup) @for_block = new_body @else_block = nil end def parse(tokens) if parse_body(@for_block, tokens) parse_body(@else_block, tokens) end if blank? @else_block&.remove_blank_strings @for_block.remove_blank_strings end @else_block&.freeze @for_block.freeze end def nodelist @else_block ? [@for_block, @else_block] : [@for_block] end def unknown_tag(tag, markup, tokens) return super unless tag == 'else' @else_block = new_body end def render_to_output_buffer(context, output) segment = collection_segment(context) if segment.empty? render_else(context, output) else render_segment(context, output, segment) end output end protected def lax_parse(markup) if markup =~ Syntax @variable_name = Regexp.last_match(1) collection_name = Regexp.last_match(2) @reversed = !!Regexp.last_match(3) @name = "#{@variable_name}-#{collection_name}" @collection_name = parse_expression(collection_name) markup.scan(TagAttributes) do |key, value| set_attribute(key, value) end else raise SyntaxError, options[:locale].t("errors.syntax.for") end end def strict_parse(markup) p = Parser.new(markup) @variable_name = p.consume(:id) raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in') collection_name = p.expression @collection_name = parse_expression(collection_name) @name = "#{@variable_name}-#{collection_name}" @reversed = p.id?('reversed') while p.look(:id) && p.look(:colon, 1) unless (attribute = p.id?('limit') || p.id?('offset')) raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_attribute") end p.consume set_attribute(attribute, p.expression) end p.consume(:end_of_string) end private def collection_segment(context) offsets = context.registers[:for] ||= {} from = if @from == :continue offsets[@name].to_i else from_value = context.evaluate(@from) if from_value.nil? 0 else Utils.to_integer(from_value) end end collection = context.evaluate(@collection_name) collection = collection.to_a if collection.is_a?(Range) limit_value = context.evaluate(@limit) to = if limit_value.nil? nil else Utils.to_integer(limit_value) + from end segment = Utils.slice_collection(collection, from, to) segment.reverse! if @reversed offsets[@name] = from + segment.length segment end def render_segment(context, output, segment) for_stack = context.registers[:for_stack] ||= [] length = segment.length context.stack do loop_vars = Liquid::ForloopDrop.new(@name, length, for_stack[-1]) for_stack.push(loop_vars) begin context['forloop'] = loop_vars segment.each do |item| context[@variable_name] = item @for_block.render_to_output_buffer(context, output) loop_vars.send(:increment!) # Handle any interrupts if they exist. next unless context.interrupt? interrupt = context.pop_interrupt break if interrupt.is_a?(BreakInterrupt) next if interrupt.is_a?(ContinueInterrupt) end ensure for_stack.pop end end output end def set_attribute(key, expr) case key when 'offset' @from = if expr == 'continue' Usage.increment('for_offset_continue') :continue else parse_expression(expr) end when 'limit' @limit = parse_expression(expr) end end def render_else(context, output) if @else_block @else_block.render_to_output_buffer(context, output) else output end end class ParseTreeVisitor < Liquid::ParseTreeVisitor def children (super + [@node.limit, @node.from, @node.collection_name]).compact end end end Template.register_tag('for', For) end liquid-5.4.0/lib/liquid/tags/if.rb000066400000000000000000000073421427076730100167420ustar00rootroot00000000000000# frozen_string_literal: true module Liquid # @liquid_public_docs # @liquid_type tag # @liquid_category conditional # @liquid_name if # @liquid_summary # Renders an expression if a specific condition is `true`. # @liquid_syntax # {% if condition %} # expression # {% endif %} # @liquid_syntax_keyword condition The condition to evaluate. # @liquid_syntax_keyword expression The expression to render if the condition is met. class If < Block Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o BOOLEAN_OPERATORS = %w(and or).freeze attr_reader :blocks def initialize(tag_name, markup, options) super @blocks = [] push_block('if', markup) end def nodelist @blocks.map(&:attachment) end def parse(tokens) while parse_body(@blocks.last.attachment, tokens) end @blocks.reverse_each do |block| block.attachment.remove_blank_strings if blank? block.attachment.freeze end end ELSE_TAG_NAMES = ['elsif', 'else'].freeze private_constant :ELSE_TAG_NAMES def unknown_tag(tag, markup, tokens) if ELSE_TAG_NAMES.include?(tag) push_block(tag, markup) else super end end def render_to_output_buffer(context, output) @blocks.each do |block| result = Liquid::Utils.to_liquid_value( block.evaluate(context) ) if result return block.attachment.render_to_output_buffer(context, output) end end output end private def push_block(tag, markup) block = if tag == 'else' ElseCondition.new else parse_with_selected_parser(markup) end @blocks.push(block) block.attach(new_body) end def parse_expression(markup) Condition.parse_expression(parse_context, markup) end def lax_parse(markup) expressions = markup.scan(ExpressionsAndOperators) raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop =~ Syntax condition = Condition.new(parse_expression(Regexp.last_match(1)), Regexp.last_match(2), parse_expression(Regexp.last_match(3))) until expressions.empty? operator = expressions.pop.to_s.strip raise SyntaxError, options[:locale].t("errors.syntax.if") unless expressions.pop.to_s =~ Syntax new_condition = Condition.new(parse_expression(Regexp.last_match(1)), Regexp.last_match(2), parse_expression(Regexp.last_match(3))) raise SyntaxError, options[:locale].t("errors.syntax.if") unless BOOLEAN_OPERATORS.include?(operator) new_condition.send(operator, condition) condition = new_condition end condition end def strict_parse(markup) p = Parser.new(markup) condition = parse_binary_comparisons(p) p.consume(:end_of_string) condition end def parse_binary_comparisons(p) condition = parse_comparison(p) first_condition = condition while (op = (p.id?('and') || p.id?('or'))) child_condition = parse_comparison(p) condition.send(op, child_condition) condition = child_condition end first_condition end def parse_comparison(p) a = parse_expression(p.expression) if (op = p.consume?(:comparison)) b = parse_expression(p.expression) Condition.new(a, op, b) else Condition.new(a) end end class ParseTreeVisitor < Liquid::ParseTreeVisitor def children @node.blocks end end end Template.register_tag('if', If) end liquid-5.4.0/lib/liquid/tags/ifchanged.rb000066400000000000000000000006331427076730100202500ustar00rootroot00000000000000# frozen_string_literal: true module Liquid class Ifchanged < Block def render_to_output_buffer(context, output) block_output = +'' super(context, block_output) if block_output != context.registers[:ifchanged] context.registers[:ifchanged] = block_output output << block_output end output end end Template.register_tag('ifchanged', Ifchanged) end liquid-5.4.0/lib/liquid/tags/include.rb000066400000000000000000000065511427076730100177700ustar00rootroot00000000000000# frozen_string_literal: true module Liquid # @liquid_public_docs # @liquid_type tag # @liquid_category theme # @liquid_name include # @liquid_summary # Renders a [snippet](/themes/architecture#snippets). # @liquid_description # Inside the snippet, you can access and alter variables that are [created](/api/liquid/tags#variable-tags) outside of the # snippet. # @liquid_syntax # {% include 'filename' %} # @liquid_syntax_keyword filename The name of the snippet to render, without the `.liquid` extension. # @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`](/api/liquid/tags#render). class Include < Tag prepend Tag::Disableable SYNTAX = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o Syntax = SYNTAX attr_reader :template_name_expr, :variable_name_expr, :attributes def initialize(tag_name, markup, options) super if markup =~ SYNTAX template_name = Regexp.last_match(1) variable_name = Regexp.last_match(3) @alias_name = Regexp.last_match(5) @variable_name_expr = variable_name ? parse_expression(variable_name) : nil @template_name_expr = parse_expression(template_name) @attributes = {} markup.scan(TagAttributes) do |key, value| @attributes[key] = parse_expression(value) end else raise SyntaxError, options[:locale].t("errors.syntax.include") end end def parse(_tokens) end def render_to_output_buffer(context, output) template_name = context.evaluate(@template_name_expr) raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name partial = PartialCache.load( template_name, context: context, parse_context: parse_context ) context_variable_name = @alias_name || template_name.split('/').last variable = if @variable_name_expr context.evaluate(@variable_name_expr) else context.find_variable(template_name, raise_on_not_found: false) end old_template_name = context.template_name old_partial = context.partial begin context.template_name = template_name context.partial = true context.stack do @attributes.each do |key, value| context[key] = context.evaluate(value) end if variable.is_a?(Array) variable.each do |var| context[context_variable_name] = var partial.render_to_output_buffer(context, output) end else context[context_variable_name] = variable partial.render_to_output_buffer(context, output) end end ensure context.template_name = old_template_name context.partial = old_partial end output end alias_method :parse_context, :options private :parse_context class ParseTreeVisitor < Liquid::ParseTreeVisitor def children [ @node.template_name_expr, @node.variable_name_expr, ] + @node.attributes.values end end end Template.register_tag('include', Include) end liquid-5.4.0/lib/liquid/tags/increment.rb000066400000000000000000000026541427076730100203310ustar00rootroot00000000000000# frozen_string_literal: true module Liquid # @liquid_public_docs # @liquid_type tag # @liquid_category variable # @liquid_name increment # @liquid_summary # Creates a new variable, with a default value of 0, that's increased by 1 with each subsequent call. # @liquid_description # 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 # [snippets](/themes/architecture#snippets) included in the file. # # Similarly, variables that are created with `increment` are independent from those created with [`assign`](/api/liquid/tags#assign) # and [`capture`](/api/liquid/tags#capture). However, `increment` and [`decrement`](/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 def initialize(tag_name, markup, options) super @variable = markup.strip end def render_to_output_buffer(context, output) value = context.environments.first[@variable] ||= 0 context.environments.first[@variable] = value + 1 output << value.to_s output end end Template.register_tag('increment', Increment) end liquid-5.4.0/lib/liquid/tags/inline_comment.rb000066400000000000000000000025311427076730100213370ustar00rootroot00000000000000# frozen_string_literal: true module Liquid # @liquid_public_docs # @liquid_type tag # @liquid_category syntax # @liquid_name inline_comment # @liquid_summary # Prevents an expression from being rendered or output. # @liquid_description # Any text inside an `inline_comment` tag won't be rendered or output. # # You can create multi-line inline comments. However, each line must begin with a `#`. # @liquid_syntax # {% # content %} # @liquid_syntax_keyword content The content of the comment. 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 Template.register_tag('#', InlineComment) end liquid-5.4.0/lib/liquid/tags/raw.rb000066400000000000000000000026671427076730100171420ustar00rootroot00000000000000# frozen_string_literal: true 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 Syntax = /\A\s*\z/ FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om def initialize(tag_name, markup, parse_context) super ensure_valid_markup(tag_name, markup, parse_context) end def parse(tokens) @body = +'' while (token = tokens.shift) if token =~ FullTokenPossiblyInvalid && block_delimiter == Regexp.last_match(2) @body << Regexp.last_match(1) if Regexp.last_match(1) != "" return end @body << token unless token.empty? end raise_tag_never_closed(block_name) end def render_to_output_buffer(_context, output) output << @body output end def nodelist [@body] end def blank? @body.empty? end protected def ensure_valid_markup(tag_name, markup, parse_context) unless Syntax.match?(markup) raise SyntaxError, parse_context.locale.t("errors.syntax.tag_unexpected_args", tag: tag_name) end end end Template.register_tag('raw', Raw) end liquid-5.4.0/lib/liquid/tags/render.rb000066400000000000000000000101531427076730100176150ustar00rootroot00000000000000# frozen_string_literal: true 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](/api/liquid/tags#variable-tags) outside # of the snippet or app block. However, you can [specify variables as parameters](/api/liquid/tags#render-passing-variables-to-snippets) # 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](/api/liquid/objects#product), and a snippet or app block inside a [section](/themes/architecture/sections) # can access the [`section` object](/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](/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 FOR = 'for' SYNTAX = /(#{QuotedString}+)(\s+(with|#{FOR})\s+(#{QuotedFragment}+))?(\s+(?:as)\s+(#{VariableSegment}+))?/o disable_tags "include" attr_reader :template_name_expr, :variable_name_expr, :attributes def initialize(tag_name, markup, options) super raise SyntaxError, options[:locale].t("errors.syntax.render") unless markup =~ SYNTAX template_name = Regexp.last_match(1) with_or_for = Regexp.last_match(3) variable_name = Regexp.last_match(4) @alias_name = Regexp.last_match(6) @variable_name_expr = variable_name ? parse_expression(variable_name) : nil @template_name_expr = parse_expression(template_name) @for = (with_or_for == FOR) @attributes = {} markup.scan(TagAttributes) do |key, value| @attributes[key] = parse_expression(value) end end def render_to_output_buffer(context, output) render_tag(context, output) end def render_tag(context, output) # The expression should be a String literal, which parses to a String object template_name = @template_name_expr raise ::ArgumentError unless template_name.is_a?(String) partial = PartialCache.load( template_name, context: context, parse_context: parse_context ) context_variable_name = @alias_name || template_name.split('/').last render_partial_func = ->(var, forloop) { inner_context = context.new_isolated_subcontext inner_context.template_name = template_name inner_context.partial = true inner_context['forloop'] = forloop if forloop @attributes.each do |key, value| inner_context[key] = context.evaluate(value) end inner_context[context_variable_name] = var unless var.nil? partial.render_to_output_buffer(inner_context, output) forloop&.send(:increment!) } variable = @variable_name_expr ? context.evaluate(@variable_name_expr) : nil if @for && variable.respond_to?(:each) && variable.respond_to?(:count) forloop = Liquid::ForloopDrop.new(template_name, variable.count, nil) variable.each { |var| render_partial_func.call(var, forloop) } else render_partial_func.call(variable, nil) end output end class ParseTreeVisitor < Liquid::ParseTreeVisitor def children [ @node.template_name_expr, @node.variable_name_expr, ] + @node.attributes.values end end end Template.register_tag('render', Render) end liquid-5.4.0/lib/liquid/tags/table_row.rb000066400000000000000000000056721427076730100203260ustar00rootroot00000000000000# frozen_string_literal: true 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 `` and `
` tags. # # > Tip: # > Every `tablerow` loop has an associated [`tablerowloop` object](/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 Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o attr_reader :variable_name, :collection_name, :attributes def initialize(tag_name, markup, options) super if markup =~ Syntax @variable_name = Regexp.last_match(1) @collection_name = parse_expression(Regexp.last_match(2)) @attributes = {} markup.scan(TagAttributes) do |key, value| @attributes[key] = parse_expression(value) end else raise SyntaxError, options[:locale].t("errors.syntax.table_row") end end def render_to_output_buffer(context, output) (collection = context.evaluate(@collection_name)) || (return '') from = @attributes.key?('offset') ? context.evaluate(@attributes['offset']).to_i : 0 to = @attributes.key?('limit') ? from + context.evaluate(@attributes['limit']).to_i : nil collection = Utils.slice_collection(collection, from, to) length = collection.length cols = context.evaluate(@attributes['cols']).to_i output << "\n" context.stack do tablerowloop = Liquid::TablerowloopDrop.new(length, cols) context['tablerowloop'] = tablerowloop collection.each do |item| context[@variable_name] = item output << "" super output << '' if tablerowloop.col_last && !tablerowloop.last output << "\n" end tablerowloop.send(:increment!) end end output << "\n" output end class ParseTreeVisitor < Liquid::ParseTreeVisitor def children super + @node.attributes.values + [@node.collection_name] end end end Template.register_tag('tablerow', TableRow) end liquid-5.4.0/lib/liquid/tags/unless.rb000066400000000000000000000025741427076730100176570ustar00rootroot00000000000000# frozen_string_literal: true require_relative 'if' module Liquid # @liquid_public_docs # @liquid_type tag # @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](/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 def render_to_output_buffer(context, output) # First condition is interpreted backwards ( if not ) first_block = @blocks.first result = Liquid::Utils.to_liquid_value( first_block.evaluate(context) ) unless result return first_block.attachment.render_to_output_buffer(context, output) end # After the first condition unless works just like if @blocks[1..-1].each do |block| result = Liquid::Utils.to_liquid_value( block.evaluate(context) ) if result return block.attachment.render_to_output_buffer(context, output) end end output end end Template.register_tag('unless', Unless) end liquid-5.4.0/lib/liquid/template.rb000066400000000000000000000153171427076730100172220ustar00rootroot00000000000000# frozen_string_literal: true module Liquid # Templates are central to liquid. # Interpretating templates is a two step process. First you compile the # source code you got. During compile time some extensive error checking is performed. # your code should expect to get some SyntaxErrors. # # After you have a compiled template you can then render it. # You can use a compiled template over and over again and keep it cached. # # Example: # # template = Liquid::Template.parse(source) # template.render('user_name' => 'bob') # class Template attr_accessor :root 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 class << self # Sets how strict the parser should be. # :lax acts like liquid 2.5 and silently ignores malformed tags in most cases. # :warn is the default and will give deprecation warnings when invalid syntax is used. # :strict will enforce correct syntax. attr_accessor :error_mode Template.error_mode = :lax attr_accessor :default_exception_renderer Template.default_exception_renderer = lambda do |exception| exception end attr_accessor :file_system Template.file_system = BlankFileSystem.new attr_accessor :tags Template.tags = TagRegistry.new private :tags= def register_tag(name, klass) tags[name.to_s] = klass end # Pass a module with filter methods which should be available # to all liquid views. Good for registering the standard library def register_filter(mod) StrainerFactory.add_global_filter(mod) end attr_accessor :default_resource_limits Template.default_resource_limits = {} private :default_resource_limits= # creates a new Template object from liquid source code # To enable profiling, pass in profile: true as an option. # See Liquid::Profiler for more information def parse(source, options = {}) new.parse(source, options) end end def initialize @rethrow_errors = false @resource_limits = ResourceLimits.new(Template.default_resource_limits) end # Parse source code. # Returns self for easy chaining def parse(source, options = {}) parse_context = configure_options(options) tokenizer = parse_context.new_tokenizer(source, start_line_number: @line_numbers && 1) @root = Document.parse(tokenizer, parse_context) self end def registers @registers ||= {} end def assigns @assigns ||= {} end def instance_assigns @instance_assigns ||= {} end def errors @errors ||= [] end # Render takes a hash with local variables. # # if you use the same filters over and over again consider registering them globally # with Template.register_filter # # if profiling was enabled in Template#parse then the resulting profiling information # will be available via Template#profiler # # Following options can be passed: # # * filters : array with local filters # * registers : hash with register variables. Those can be accessed from # filters and tags and might be useful to integrate liquid more with its host application # def render(*args) return '' if @root.nil? context = case args.first when Liquid::Context c = args.shift if @rethrow_errors c.exception_renderer = Liquid::RAISE_EXCEPTION_LAMBDA end c when Liquid::Drop drop = args.shift drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits) when Hash Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits) when nil Context.new(assigns, instance_assigns, registers, @rethrow_errors, @resource_limits) else raise ArgumentError, "Expected Hash or Liquid::Context as parameter" end output = nil case args.last when Hash options = args.pop output = options[:output] if options[:output] static_registers = context.registers.static options[:registers]&.each do |key, register| static_registers[key] = register end apply_options_to_context(context, options) when Module, Array context.add_filters(args.pop) end # Retrying a render resets resource usage context.resource_limits.reset if @profiling && context.profiler.nil? @profiler = context.profiler = Liquid::Profiler.new end begin # render the nodelist. @root.render_to_output_buffer(context, output || +'') rescue Liquid::MemoryError => e context.handle_error(e) ensure @errors = context.errors end end def render!(*args) @rethrow_errors = true render(*args) end def render_to_output_buffer(context, output) render(context, output: output) end private def configure_options(options) if (profiling = options[:profile]) raise "Profiler not loaded, require 'liquid/profiler' first" unless defined?(Liquid::Profiler) end @options = options @profiling = profiling @line_numbers = options[:line_numbers] || @profiling parse_context = options.is_a?(ParseContext) ? options : ParseContext.new(options) @warnings = parse_context.warnings parse_context end def apply_options_to_context(context, options) context.add_filters(options[:filters]) if options[:filters] context.global_filter = options[:global_filter] if options[:global_filter] context.exception_renderer = options[:exception_renderer] if options[:exception_renderer] context.strict_variables = options[:strict_variables] if options[:strict_variables] context.strict_filters = options[:strict_filters] if options[:strict_filters] end end end liquid-5.4.0/lib/liquid/template_factory.rb000066400000000000000000000002161427076730100207410ustar00rootroot00000000000000# frozen_string_literal: true module Liquid class TemplateFactory def for(_template_name) Liquid::Template.new end end end liquid-5.4.0/lib/liquid/tokenizer.rb000066400000000000000000000015431427076730100174150ustar00rootroot00000000000000# frozen_string_literal: true module Liquid class Tokenizer attr_reader :line_number, :for_liquid_tag def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false) @source = source.to_s.to_str @line_number = line_number || (line_numbers ? 1 : nil) @for_liquid_tag = for_liquid_tag @tokens = tokenize end def shift (token = @tokens.shift) || return if @line_number @line_number += @for_liquid_tag ? 1 : token.count("\n") end token end private def tokenize return [] if @source.empty? return @source.split("\n") if @for_liquid_tag tokens = @source.split(TemplateParser) # removes the rogue empty element at the beginning of the array tokens.shift if tokens[0]&.empty? tokens end end end liquid-5.4.0/lib/liquid/usage.rb000066400000000000000000000001531427076730100165030ustar00rootroot00000000000000# frozen_string_literal: true module Liquid module Usage def self.increment(name) end end end liquid-5.4.0/lib/liquid/utils.rb000066400000000000000000000040051427076730100165370ustar00rootroot00000000000000# frozen_string_literal: true module Liquid module Utils def self.slice_collection(collection, from, to) if (from != 0 || !to.nil?) && collection.respond_to?(:load_slice) collection.load_slice(from, to) else slice_collection_using_each(collection, from, to) end end def self.slice_collection_using_each(collection, from, to) segments = [] index = 0 # Maintains Ruby 1.8.7 String#each behaviour on 1.9 if collection.is_a?(String) return collection.empty? ? [] : [collection] end return [] unless collection.respond_to?(:each) collection.each do |item| if to && to <= index break end if from <= index segments << item end index += 1 end segments end def self.to_integer(num) return num if num.is_a?(Integer) num = num.to_s begin Integer(num) rescue ::ArgumentError raise Liquid::ArgumentError, "invalid integer" end end def self.to_number(obj) case obj when Float BigDecimal(obj.to_s) when Numeric obj when String /\A-?\d+\.\d+\z/.match?(obj.strip) ? BigDecimal(obj) : obj.to_i else if obj.respond_to?(:to_number) obj.to_number else 0 end end end def self.to_date(obj) return obj if obj.respond_to?(:strftime) if obj.is_a?(String) return nil if obj.empty? obj = obj.downcase end case obj when 'now', 'today' Time.now when /\A\d+\z/, Integer Time.at(obj.to_i) when String Time.parse(obj) end rescue ::ArgumentError nil end def self.to_liquid_value(obj) # Enable "obj" to represent itself as a primitive value like integer, string, or boolean return obj.to_liquid_value if obj.respond_to?(:to_liquid_value) # Otherwise return the object itself obj end end end liquid-5.4.0/lib/liquid/variable.rb000066400000000000000000000100521427076730100171630ustar00rootroot00000000000000# frozen_string_literal: true module Liquid # Holds variables. Variables are only loaded "just in time" # and are not evaluated as part of the render stage # # {{ monkey }} # {{ user.name }} # # Variables can be combined with filters: # # {{ user | link }} # class Variable FilterMarkupRegex = /#{FilterSeparator}\s*(.*)/om FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o FilterArgsRegex = /(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o JustTagAttributes = /\A#{TagAttributes}\z/o MarkupWithQuotedFragment = /(#{QuotedFragment})(.*)/om attr_accessor :filters, :name, :line_number attr_reader :parse_context alias_method :options, :parse_context include ParserSwitching def initialize(markup, parse_context) @markup = markup @name = nil @parse_context = parse_context @line_number = parse_context.line_number strict_parse_with_error_mode_fallback(markup) end def raw @markup end def markup_context(markup) "in \"{{#{markup}}}\"" end def lax_parse(markup) @filters = [] return unless markup =~ MarkupWithQuotedFragment name_markup = Regexp.last_match(1) filter_markup = Regexp.last_match(2) @name = parse_context.parse_expression(name_markup) if filter_markup =~ FilterMarkupRegex filters = Regexp.last_match(1).scan(FilterParser) filters.each do |f| next unless f =~ /\w+/ filtername = Regexp.last_match(0) filterargs = f.scan(FilterArgsRegex).flatten @filters << parse_filter_expressions(filtername, filterargs) end end end def strict_parse(markup) @filters = [] p = Parser.new(markup) return if p.look(:end_of_string) @name = parse_context.parse_expression(p.expression) while p.consume?(:pipe) filtername = p.consume(:id) filterargs = p.consume?(:colon) ? parse_filterargs(p) : [] @filters << parse_filter_expressions(filtername, filterargs) end p.consume(:end_of_string) end def parse_filterargs(p) # first argument filterargs = [p.argument] # followed by comma separated others filterargs << p.argument while p.consume?(:comma) filterargs end def render(context) obj = context.evaluate(@name) @filters.each do |filter_name, filter_args, filter_kwargs| filter_args = evaluate_filter_expressions(context, filter_args, filter_kwargs) obj = context.invoke(filter_name, obj, *filter_args) end context.apply_global_filter(obj) end def render_to_output_buffer(context, output) obj = render(context) if obj.is_a?(Array) output << obj.join elsif obj.nil? else output << obj.to_s end output end def disabled?(_context) false end def disabled_tags [] end private def parse_filter_expressions(filter_name, unparsed_args) filter_args = [] keyword_args = nil unparsed_args.each do |a| if (matches = a.match(JustTagAttributes)) keyword_args ||= {} keyword_args[matches[1]] = parse_context.parse_expression(matches[2]) else filter_args << parse_context.parse_expression(a) end end result = [filter_name, filter_args] result << keyword_args if keyword_args result end def evaluate_filter_expressions(context, filter_args, filter_kwargs) parsed_args = filter_args.map { |expr| context.evaluate(expr) } if filter_kwargs parsed_kwargs = {} filter_kwargs.each do |key, expr| parsed_kwargs[key] = context.evaluate(expr) end parsed_args << parsed_kwargs end parsed_args end class ParseTreeVisitor < Liquid::ParseTreeVisitor def children [@node.name] + @node.filters.flatten end end end end liquid-5.4.0/lib/liquid/variable_lookup.rb000066400000000000000000000054031427076730100205600ustar00rootroot00000000000000# frozen_string_literal: true module Liquid class VariableLookup COMMAND_METHODS = ['size', 'first', 'last'].freeze attr_reader :name, :lookups def self.parse(markup) new(markup) end def initialize(markup) lookups = markup.scan(VariableParser) name = lookups.shift if name&.start_with?('[') && name&.end_with?(']') name = Expression.parse(name[1..-2]) end @name = name @lookups = lookups @command_flags = 0 @lookups.each_index do |i| lookup = lookups[i] if lookup&.start_with?('[') && lookup&.end_with?(']') lookups[i] = Expression.parse(lookup[1..-2]) elsif COMMAND_METHODS.include?(lookup) @command_flags |= 1 << i end end end def lookup_command?(lookup_index) @command_flags & (1 << lookup_index) != 0 end def evaluate(context) name = context.evaluate(@name) object = context.find_variable(name) @lookups.each_index do |i| key = context.evaluate(@lookups[i]) # Cast "key" to its liquid value to enable it to act as a primitive value key = Liquid::Utils.to_liquid_value(key) # If object is a hash- or array-like object we look for the # presence of the key and if its available we return it if object.respond_to?(:[]) && ((object.respond_to?(:key?) && object.key?(key)) || (object.respond_to?(:fetch) && key.is_a?(Integer))) # if its a proc we will replace the entry with the proc res = context.lookup_and_evaluate(object, key) object = res.to_liquid # Some special cases. If the part wasn't in square brackets and # no key with the same name was found we interpret following calls # as commands and call them on the current object elsif lookup_command?(i) && object.respond_to?(key) object = object.send(key).to_liquid # No key was present with the desired value and it wasn't one of the directly supported # keywords either. The only thing we got left is to return nil or # raise an exception if `strict_variables` option is set to true else return nil unless context.strict_variables raise Liquid::UndefinedVariable, "undefined variable #{key}" end # If we are dealing with a drop here we have to object.context = context if object.respond_to?(:context=) end object end def ==(other) self.class == other.class && state == other.state end protected def state [@name, @lookups, @command_flags] end class ParseTreeVisitor < Liquid::ParseTreeVisitor def children @node.lookups end end end end liquid-5.4.0/lib/liquid/version.rb000066400000000000000000000001271427076730100170650ustar00rootroot00000000000000# encoding: utf-8 # frozen_string_literal: true module Liquid VERSION = "5.4.0" end liquid-5.4.0/liquid.gemspec000066400000000000000000000017721427076730100156610ustar00rootroot00000000000000# encoding: utf-8 # frozen_string_literal: true lib = File.expand_path('../lib/', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require "liquid/version" Gem::Specification.new do |s| s.name = "liquid" s.version = Liquid::VERSION s.platform = Gem::Platform::RUBY s.summary = "A secure, non-evaling end user template engine with aesthetic markup." s.authors = ["Tobias Lütke"] s.email = ["tobi@leetsoft.com"] s.homepage = "http://www.liquidmarkup.org" s.license = "MIT" # s.description = "A secure, non-evaling end user template engine with aesthetic markup." s.required_ruby_version = ">= 2.7.0" s.required_rubygems_version = ">= 1.3.7" s.metadata['allowed_push_host'] = 'https://rubygems.org' s.files = Dir.glob("{lib}/**/*") + %w(LICENSE README.md) s.extra_rdoc_files = ["History.md", "README.md"] s.require_path = "lib" s.add_development_dependency('rake', '~> 13.0') s.add_development_dependency('minitest') end liquid-5.4.0/performance/000077500000000000000000000000001427076730100153175ustar00rootroot00000000000000liquid-5.4.0/performance/benchmark.rb000066400000000000000000000007211427076730100175760ustar00rootroot00000000000000# frozen_string_literal: true require 'benchmark/ips' require_relative 'theme_runner' Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first profiler = ThemeRunner.new Benchmark.ips do |x| x.time = 10 x.warmup = 5 puts puts "Running benchmark for #{x.time} seconds (with #{x.warmup} seconds warmup)." puts x.report("parse:") { profiler.compile } x.report("render:") { profiler.render } x.report("parse & render:") { profiler.run } end liquid-5.4.0/performance/memory_profile.rb000066400000000000000000000031061427076730100206740ustar00rootroot00000000000000# frozen_string_literal: true require 'benchmark/ips' require 'memory_profiler' require 'terminal-table' require_relative 'theme_runner' class Profiler LOG_LABEL = "Profiling: ".rjust(14).freeze REPORTS_DIR = File.expand_path('.memprof', __dir__).freeze def self.run puts yield new end def initialize @allocated = [] @retained = [] @headings = [] end def profile(phase, &block) print(LOG_LABEL) print("#{phase}.. ".ljust(10)) report = MemoryProfiler.report(&block) puts 'Done.' @headings << phase.capitalize @allocated << "#{report.scale_bytes(report.total_allocated_memsize)} (#{report.total_allocated} objects)" @retained << "#{report.scale_bytes(report.total_retained_memsize)} (#{report.total_retained} objects)" return if ENV['CI'] require 'fileutils' report_file = File.join(REPORTS_DIR, "#{sanitize(phase)}.txt") FileUtils.mkdir_p(REPORTS_DIR) report.pretty_print(to_file: report_file, scale_bytes: true) end def tabulate table = Terminal::Table.new(headings: @headings.unshift('Phase')) do |t| t << @allocated.unshift('Total allocated') t << @retained.unshift('Total retained') end puts puts table puts "\nDetailed report(s) saved to #{REPORTS_DIR}/" unless ENV['CI'] end def sanitize(string) string.downcase.gsub(/[\W]/, '-').squeeze('-') end end Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first runner = ThemeRunner.new Profiler.run do |x| x.profile('parse') { runner.compile } x.profile('render') { runner.render } x.tabulate end liquid-5.4.0/performance/profile.rb000066400000000000000000000013141427076730100173030ustar00rootroot00000000000000# frozen_string_literal: true require 'stackprof' require_relative 'theme_runner' Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first profiler = ThemeRunner.new profiler.run [:cpu, :object].each do |profile_type| puts "Profiling in #{profile_type} mode..." results = StackProf.run(mode: profile_type) do 200.times do profiler.run end end if profile_type == :cpu && (graph_filename = ENV['GRAPH_FILENAME']) File.open(graph_filename, 'w') do |f| StackProf::Report.new(results).print_graphviz(nil, f) end end StackProf::Report.new(results).print_text(false, 20) File.write(ENV['FILENAME'] + "." + profile_type.to_s, Marshal.dump(results)) if ENV['FILENAME'] end liquid-5.4.0/performance/shopify/000077500000000000000000000000001427076730100170005ustar00rootroot00000000000000liquid-5.4.0/performance/shopify/comment_form.rb000066400000000000000000000020311427076730100220060ustar00rootroot00000000000000# frozen_string_literal: true class CommentForm < Liquid::Block Syntax = /(#{Liquid::VariableSignature}+)/ def initialize(tag_name, markup, options) super if markup =~ Syntax @variable_name = Regexp.last_match(1) @attributes = {} else raise SyntaxError, "Syntax Error in 'comment_form' - Valid syntax: comment_form [article]" end end def render_to_output_buffer(context, output) article = context[@variable_name] context.stack do context['form'] = { 'posted_successfully?' => context.registers[:posted_successfully], 'errors' => context['comment.errors'], 'author' => context['comment.author'], 'email' => context['comment.email'], 'body' => context['comment.body'], } output << wrap_in_form(article, render_all(@nodelist, context, output)) output end end def wrap_in_form(article, input) %(
\n#{input}\n
) end end liquid-5.4.0/performance/shopify/database.rb000066400000000000000000000035231427076730100210740ustar00rootroot00000000000000# frozen_string_literal: true require 'yaml' module Database DATABASE_FILE_PATH = "#{__dir__}/vision.database.yml" # Load the standard vision toolkit database and re-arrage it to be simply exportable # to liquid as assigns. All this is based on Shopify def self.tables @tables ||= begin db = if YAML.respond_to?(:unsafe_load_file) # Only Psych 4+ can use unsafe_load_file # unsafe_load_file is needed for YAML references YAML.unsafe_load_file(DATABASE_FILE_PATH) else YAML.load_file(DATABASE_FILE_PATH) end # From vision source db['products'].each do |product| collections = db['collections'].find_all do |collection| collection['products'].any? { |p| p['id'].to_i == product['id'].to_i } end product['collections'] = collections end # key the tables by handles, as this is how liquid expects it. db = db.each_with_object({}) do |(key, values), assigns| assigns[key] = values.each_with_object({}) do |v, h| h[v['handle']] = v end end # Some standard direct accessors so that the specialized templates # render correctly db['collection'] = db['collections'].values.first db['product'] = db['products'].values.first db['blog'] = db['blogs'].values.first db['article'] = db['blog']['articles'].first db['cart'] = { 'total_price' => db['line_items'].values.inject(0) { |sum, item| sum + item['line_price'] * item['quantity'] }, 'item_count' => db['line_items'].values.inject(0) { |sum, item| sum + item['quantity'] }, 'items' => db['line_items'].values, } db end end end if __FILE__ == $PROGRAM_NAME p(Database.tables['collections']['frontpage'].keys) # p Database.tables['blog']['articles'] end liquid-5.4.0/performance/shopify/json_filter.rb000066400000000000000000000002321427076730100216400ustar00rootroot00000000000000# frozen_string_literal: true require 'json' module JsonFilter def json(object) JSON.dump(object.reject { |k, _v| k == "collections" }) end end liquid-5.4.0/performance/shopify/liquid.rb000066400000000000000000000012241427076730100206130ustar00rootroot00000000000000# frozen_string_literal: true $LOAD_PATH.unshift(__dir__ + '/../../lib') require_relative '../../lib/liquid' require_relative 'comment_form' require_relative 'paginate' require_relative 'json_filter' require_relative 'money_filter' require_relative 'shop_filter' require_relative 'tag_filter' require_relative 'weight_filter' Liquid::Template.register_tag('paginate', Paginate) Liquid::Template.register_tag('form', CommentForm) Liquid::Template.register_filter(JsonFilter) Liquid::Template.register_filter(MoneyFilter) Liquid::Template.register_filter(WeightFilter) Liquid::Template.register_filter(ShopFilter) Liquid::Template.register_filter(TagFilter) liquid-5.4.0/performance/shopify/money_filter.rb000066400000000000000000000004661427076730100220270ustar00rootroot00000000000000# frozen_string_literal: true module MoneyFilter def money_with_currency(money) return '' if money.nil? format("$ %.2f USD", money / 100.0) end def money(money) return '' if money.nil? format("$ %.2f", money / 100.0) end private def currency ShopDrop.new.currency end end liquid-5.4.0/performance/shopify/paginate.rb000066400000000000000000000050071427076730100211170ustar00rootroot00000000000000# frozen_string_literal: true class Paginate < Liquid::Block Syntax = /(#{Liquid::QuotedFragment})\s*(by\s*(\d+))?/ def initialize(tag_name, markup, options) super if markup =~ Syntax @collection_name = Regexp.last_match(1) @page_size = if Regexp.last_match(2) Regexp.last_match(3).to_i else 20 end @attributes = { 'window_size' => 3 } markup.scan(Liquid::TagAttributes) do |key, value| @attributes[key] = value end else raise SyntaxError, "Syntax Error in tag 'paginate' - Valid syntax: paginate [collection] by number" end end def render_to_output_buffer(context, output) @context = context context.stack do current_page = context['current_page'].to_i pagination = { 'page_size' => @page_size, 'current_page' => 5, 'current_offset' => @page_size * 5, } context['paginate'] = pagination collection_size = context[@collection_name].size raise ArgumentError, "Cannot paginate array '#{@collection_name}'. Not found." if collection_size.nil? page_count = (collection_size.to_f / @page_size.to_f).to_f.ceil + 1 pagination['items'] = collection_size pagination['pages'] = page_count - 1 pagination['previous'] = link('« Previous', current_page - 1) unless 1 >= current_page pagination['next'] = link('Next »', current_page + 1) unless page_count <= current_page + 1 pagination['parts'] = [] hellip_break = false if page_count > 2 1.upto(page_count - 1) do |page| if current_page == page pagination['parts'] << no_link(page) elsif page == 1 pagination['parts'] << link(page, page) elsif page == page_count - 1 pagination['parts'] << link(page, page) elsif page <= current_page - @attributes['window_size'] || page >= current_page + @attributes['window_size'] next if hellip_break pagination['parts'] << no_link('…') hellip_break = true next else pagination['parts'] << link(page, page) end hellip_break = false end end super end end private def no_link(title) { 'title' => title, 'is_link' => false } end def link(title, page) { 'title' => title, 'url' => current_url + "?page=#{page}", 'is_link' => true } end def current_url "/collections/frontpage" end end liquid-5.4.0/performance/shopify/shop_filter.rb000066400000000000000000000054461427076730100216540ustar00rootroot00000000000000# frozen_string_literal: true module ShopFilter def asset_url(input) "/files/1/[shop_id]/[shop_id]/assets/#{input}" end def global_asset_url(input) "/global/#{input}" end def shopify_asset_url(input) "/shopify/#{input}" end def script_tag(url) %() end def stylesheet_tag(url, media = "all") %() end def link_to(link, url, title = "") %(#{link}) end def img_tag(url, alt = "") %(#{alt}) end def link_to_vendor(vendor) if vendor link_to(vendor, url_for_vendor(vendor), vendor) else 'Unknown Vendor' end end def link_to_type(type) if type link_to(type, url_for_type(type), type) else 'Unknown Vendor' end end def url_for_vendor(vendor_title) "/collections/#{to_handle(vendor_title)}" end def url_for_type(type_title) "/collections/#{to_handle(type_title)}" end def product_img_url(url, style = 'small') unless url =~ %r{\Aproducts/([\w\-\_]+)\.(\w{2,4})} raise ArgumentError, 'filter "size" can only be called on product images' end case style when 'original' '/files/shops/random_number/' + url when 'grande', 'large', 'medium', 'compact', 'small', 'thumb', 'icon' "/files/shops/random_number/products/#{Regexp.last_match(1)}_#{style}.#{Regexp.last_match(2)}" else raise ArgumentError, 'valid parameters for filter "size" are: original, grande, large, medium, compact, small, thumb and icon ' end end def default_pagination(paginate) html = [] html << %(#{link_to(paginate['previous']['title'], paginate['previous']['url'])}) if paginate['previous'] paginate['parts'].each do |part| html << if part['is_link'] %(#{link_to(part['title'], part['url'])}) elsif part['title'].to_i == paginate['current_page'].to_i %(#{part['title']}) else %(#{part['title']}) end end html << %(#{link_to(paginate['next']['title'], paginate['next']['url'])}) if paginate['next'] html.join(' ') end # Accepts a number, and two words - one for singular, one for plural # Returns the singular word if input equals 1, otherwise plural def pluralize(input, singular, plural) input == 1 ? singular : plural end private def to_handle(str) result = str.dup result.downcase! result.delete!("'\"()[]") result.gsub!(/\W+/, '-') result.gsub!(/-+\z/, '') if result[-1] == '-' result.gsub!(/\A-+/, '') if result[0] == '-' result end end liquid-5.4.0/performance/shopify/tag_filter.rb000066400000000000000000000014131427076730100214440ustar00rootroot00000000000000# frozen_string_literal: true module TagFilter def link_to_tag(label, tag) "#{label}" end def highlight_active_tag(tag, css_class = 'active') if @context['current_tags'].include?(tag) "#{tag}" else tag end end def link_to_add_tag(label, tag) tags = (@context['current_tags'] + [tag]).uniq "#{label}" end def link_to_remove_tag(label, tag) tags = (@context['current_tags'] - [tag]).uniq "#{label}" end end liquid-5.4.0/performance/shopify/vision.database.yml000066400000000000000000000720071427076730100226030ustar00rootroot00000000000000# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- # Variants # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- product_variants: - &product-1-var-1 id: 1 title: 151cm / Normal price: 19900 weight: 1000 compare_at_price: 49900 available: true inventory_quantity: 5 option1: 151cm option2: Normal option3: - &product-1-var-2 id: 2 title: 155cm / Normal price: 31900 weight: 1000 compare_at_price: 50900 available: true inventory_quantity: 2 option1: 155cm option2: Normal option3: - &product-2-var-1 id: 3 title: 162cm price: 29900 weight: 1000 compare_at_price: 52900 available: true inventory_quantity: 3 option1: 162cm option2: option3: - &product-3-var-1 id: 4 title: 159cm price: 19900 weight: 1000 compare_at_price: available: true inventory_quantity: 4 option1: 159cm option2: option3: - &product-4-var-1 id: 5 title: 159cm price: 19900 weight: 1000 compare_at_price: 32900 available: true inventory_quantity: 6 option1: 159cm option2: option3: - &product-1-var-3 id: 6 title: 158cm / Wide price: 23900 weight: 1000 compare_at_price: 99900 available: false inventory_quantity: 0 option1: 158cm option2: Wide option3: - &product-3-var-2 id: 7 title: 162cm price: 19900 weight: 1000 compare_at_price: available: false inventory_quantity: 0 option1: 162cm option2: option3: - &product-3-var-3 id: 8 title: 165cm price: 22900 weight: 1000 compare_at_price: available: true inventory_quantity: 4 option1: 165cm option2: option3: - &product-5-var-1 id: 9 title: black / 42 price: 11900 weight: 500 compare_at_price: 22900 available: true inventory_quantity: 1 option1: black option2: 42 option3: - &product-5-var-2 id: 10 title: beige / 42 price: 11900 weight: 500 compare_at_price: 22900 available: true inventory_quantity: 3 option1: beige option2: 42 option3: - &product-5-var-3 id: 11 title: white / 42 price: 13900 weight: 500 compare_at_price: 24900 available: true inventory_quantity: 1 option1: white option2: 42 option3: - &product-5-var-4 id: 12 title: black / 44 price: 11900 weight: 500 compare_at_price: 22900 available: true inventory_quantity: 2 option1: black option2: 44 option3: - &product-5-var-5 id: 13 title: beige / 44 price: 11900 weight: 500 compare_at_price: 22900 available: false inventory_quantity: 0 option1: beige option2: 44 option3: - &product-5-var-6 id: 14 title: white / 44 price: 13900 weight: 500 compare_at_price: 24900 available: false inventory_quantity: 0 option1: white option2: 44 option3: - &product-6-var-1 id: 15 title: red price: 2179500 weight: 200000 compare_at_price: available: true inventory_quantity: 0 option1: red option2: option3: - &product-7-var-1 id: 16 title: black / small price: 1900 weight: 200 compare_at_price: available: true inventory_quantity: 20 option1: black option2: small option3: - &product-7-var-2 id: 17 title: black / medium price: 1900 weight: 200 compare_at_price: available: false inventory_quantity: 0 option1: black option2: medium option3: - &product-7-var-3 id: 18 title: black / large price: 1900 weight: 200 compare_at_price: available: true inventory_quantity: 10 option1: black option2: large option3: - &product-7-var-4 id: 19 title: black / extra large price: 1900 weight: 200 compare_at_price: available: false inventory_quantity: 0 option1: black option2: extra large option3: - &product-8-var-1 id: 20 title: brown / small price: 5900 weight: 400 compare_at_price: 6900 available: true inventory_quantity: 5 option1: brown option2: small option3: - &product-8-var-2 id: 21 title: brown / medium price: 5900 weight: 400 compare_at_price: 6900 available: false inventory_quantity: 0 option1: brown option2: medium option3: - &product-8-var-3 id: 22 title: brown / large price: 5900 weight: 400 compare_at_price: 6900 available: true inventory_quantity: 10 option1: brown option2: large option3: - &product-8-var-4 id: 23 title: black / small price: 5900 weight: 400 compare_at_price: 6900 available: true inventory_quantity: 10 option1: black option2: small option3: - &product-8-var-5 id: 24 title: black / medium price: 5900 weight: 400 compare_at_price: 6900 available: true inventory_quantity: 10 option1: black option2: medium option3: - &product-8-var-6 id: 25 title: black / large price: 5900 weight: 400 compare_at_price: 6900 available: false inventory_quantity: 0 option1: black option2: large option3: - &product-9-var-1 id: 26 title: Body Only price: 499995 weight: 2000 compare_at_price: available: true inventory_quantity: 3 option1: Body Only option2: option3: - &product-9-var-2 id: 27 title: Kit with 18-55mm VR lens price: 523995 weight: 2000 compare_at_price: available: true inventory_quantity: 2 option1: Kit with 18-55mm VR lens option2: option3: - &product-9-var-3 id: 28 title: Kit with 18-200 VR lens price: 552500 weight: 2000 compare_at_price: available: true inventory_quantity: 3 option1: Kit with 18-200 VR lens option2: option3: # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- # Products # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- products: - &product-1 id: 1 title: Arbor Draft handle: arbor-draft type: Snowboards vendor: Arbor price: 23900 price_max: 31900 price_min: 23900 price_varies: true available: true tags: - season2005 - pro - intermediate - wooden - freestyle options: - Length - Style compare_at_price: 49900 compare_at_price_max: 50900 compare_at_price_min: 49900 compare_at_price_varies: true url: /products/arbor-draft featured_image: products/arbor_draft.jpg images: - products/arbor_draft.jpg description: The Arbor Draft snowboard wouldn't exist if Polynesians hadn't figured out how to surf hundreds of years ago. But the Draft does exist, and it's here to bring your urban and park riding to a new level. The board's freaky Tiki design pays homage to culture that inspired snowboarding. It's designed to spin with ease, land smoothly, lock hook-free onto rails, and take the abuse of a pavement pounding or twelve. The Draft will pop off kickers with authority and carve solidly across the pipe. The Draft features targeted Koa wood die cuts inlayed into the deck that enhance the flex pattern. Now bow down to riding's ancestors. variants: - *product-1-var-1 - *product-1-var-2 - *product-1-var-3 - &product-2 id: 2 title: Arbor Element handle: arbor-element type: Snowboards vendor: Arbor price: 29900 price_max: 29900 price_min: 29900 price_varies: false available: true tags: - season2005 - pro - wooden - freestyle options: - Length compare_at_price: 52900 compare_at_price_max: 52900 compare_at_price_min: 52900 compare_at_price_varies: false url: /products/arbor-element featured_image: products/element58.jpg images: - products/element58.jpg description: The Element is a technically advanced all-mountain board for riders who readily transition from one terrain, snow condition, or riding style to another. Its balanced design provides the versatility needed for the true ride-it-all experience. The Element is exceedingly lively, freely initiates, and holds a tight edge at speed. Its structural real-wood topsheet is made with book-matched Koa. variants: - *product-2-var-1 - &product-3 id: 3 title: Comic ~ Pastel handle: comic-pastel type: Snowboards vendor: Technine price: 19900 price_max: 22900 price_min: 19900 tags: - season2006 - beginner - intermediate - freestyle - purple options: - Length price_varies: true available: true compare_at_price: compare_at_price_max: 0 compare_at_price_min: 0 compare_at_price_varies: false url: /products/comic-pastel featured_image: products/technine1.jpg images: - products/technine1.jpg - products/technine2.jpg - products/technine_detail.jpg description: 2005 Technine Comic Series Description The Comic series was developed to be the ultimate progressive freestyle board in the Technine line. Dependable edge control and a perfect flex pattern for jumping in the park or out of bounds. Landins and progression will come easy with this board and it will help your riding progress to the next level. Street rails, park jibs, backcountry booters and park jumps, this board will do it all. variants: - *product-3-var-1 - *product-3-var-2 - *product-3-var-3 - &product-4 id: 4 title: Comic ~ Orange handle: comic-orange type: Snowboards vendor: Technine price: 19900 price_max: 19900 price_min: 19900 price_varies: false available: true tags: - season2006 - beginner - intermediate - freestyle - orange options: - Length compare_at_price: 32900 compare_at_price_max: 32900 compare_at_price_min: 32900 compare_at_price_varies: false url: /products/comic-orange featured_image: products/technine3.jpg images: - products/technine3.jpg - products/technine4.jpg description: 2005 Technine Comic Series Description The Comic series was developed to be the ultimate progressive freestyle board in the Technine line. Dependable edge control and a perfect flex pattern for jumping in the park or out of bounds. Landins and progression will come easy with this board and it will help your riding progress to the next level. Street rails, park jibs, backcountry booters and park jumps, this board will do it all. variants: - *product-4-var-1 - &product-5 id: 5 title: Burton Boots handle: burton-boots type: Boots vendor: Burton price: 11900 price_max: 11900 price_min: 11900 price_varies: false available: true tags: - season2006 - beginner - intermediate - boots options: - Color - Shoe Size compare_at_price: 22900 compare_at_price_max: 22900 compare_at_price_min: 22900 compare_at_price_varies: false url: /products/burton-boots featured_image: products/burton.jpg images: - products/burton.jpg description: The Burton boots are particularly well on snowboards. The very best thing about them is that the according picture is cubic. This makes testing in a Vision testing environment very easy. variants: - *product-5-var-1 - *product-5-var-2 - *product-5-var-3 - *product-5-var-4 - *product-5-var-5 - *product-5-var-6 - &product-6 id: 6 title: Superbike 1198 S handle: superbike type: Superbike vendor: Ducati price: 2179500 price_max: 2179500 price_min: 2179500 price_varies: false available: true tags: - ducati - superbike - bike - street - racing - performance options: - Color compare_at_price: compare_at_price_max: 0 compare_at_price_min: 0 compare_at_price_varies: false url: /products/superbike featured_image: products/ducati.jpg images: - products/ducati.jpg description:

‘S’ PERFORMANCE

Producing 170hp (125kW) and with a dry weight of just 169kg (372.6lb), the new 1198 S now incorporates more World Superbike technology than ever before by taking the 1198 motor and adding top-of-the-range suspension, lightweight chassis components and a true racing-style traction control system designed for road use.

The high performance, fully adjustable 43mm Öhlins forks, which sport low friction titanium nitride-treated fork sliders, respond effortlessly to every imperfection in the tarmac. Beyond their advanced engineering solutions, one of the most important characteristics of Öhlins forks is their ability to communicate the condition and quality of the tyre-to-road contact patch, a feature that puts every rider in superior control. The suspension set-up at the rear is complemented with a fully adjustable Öhlins rear shock equipped with a ride enhancing top-out spring and mounted to a single-sided swingarm for outstanding drive and traction. The front-to-rear Öhlins package is completed with a control-enhancing adjustable steering damper.

variants: - *product-6-var-1 - &product-7 id: 7 title: Shopify Shirt handle: shopify-shirt type: Shirt vendor: Shopify price: 1900 price_max: 1900 price_min: 1900 price_varies: false available: true tags: - shopify - shirt - apparel - tshirt - clothing options: - Color - Size compare_at_price: compare_at_price_max: 0 compare_at_price_min: 0 compare_at_price_varies: false url: /products/shopify-shirt featured_image: products/shopify_shirt.png images: - products/shopify_shirt.png description:

High Quality Shopify Shirt. Wear your e-commerce solution with pride and attract attention anywhere you go.

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

variants: - *product-7-var-1 - *product-7-var-2 - *product-7-var-3 - *product-7-var-4 - &product-8 id: 8 title: Hooded Sweater handle: hooded-sweater type: Sweater vendor: Stormtech price: 5900 price_max: 5900 price_min: 5900 price_varies: false available: true tags: - sweater - hooded - apparel - clothing options: - Color - Size compare_at_price: 6900 compare_at_price_max: 6900 compare_at_price_min: 6900 compare_at_price_varies: false url: /products/hooded-sweater featured_image: products/hooded-sweater.jpg images: - products/hooded-sweater.jpg - products/hooded-sweater-b.jpg description:

Extra comfortable zip up sweater. Durable quality, ideal for any outdoor activities.

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

variants: - *product-8-var-1 - *product-8-var-2 - *product-8-var-3 - *product-8-var-4 - *product-8-var-5 - *product-8-var-6 - &product-9 id: 9 title: D3 Digital SLR Camera handle: d3 type: SLR vendor: Nikon price: 499995 price_max: 552500 price_min: 499995 price_varies: true available: true tags: - camera - slr - nikon - professional options: - Bundle compare_at_price: compare_at_price_max: 0 compare_at_price_min: 0 compare_at_price_varies: false url: /products/d3 featured_image: products/d3.jpg images: - products/d3.jpg - products/d3_2.jpg - products/d3_3.jpg description:

Flagship pro D-SLR with a 12.1-MP FX-format CMOS sensor, blazing 9 fps shooting at full FX resolution and low-noise performance up to 6400 ISO.

Nikon's original 12.1-megapixel FX-format (23.9 x 36mm) CMOS sensor: Couple Nikon's exclusive digital image processing system with the 12.1-megapixel FX-format and you'll get breathtakingly rich images while also reducing noise to unprecedented levels with even higher ISOs.

Continuous shooting at up to 9 frames per second: At full FX resolution and up to 11fps in the DX crop mode, the D3 offers uncompromised shooting speeds for fast-action and sports photography.

variants: - *product-9-var-1 - *product-9-var-2 - *product-9-var-3 # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- # Line Items # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- line_items: - &line_item-1 id: 1 title: 'Arbor Draft' subtitle: '151cm' price: 29900 line_price: 29900 quantity: 1 variant: *product-1-var-1 product: *product-1 - &line_item-2 id: 2 title: 'Comic ~ Orange' subtitle: '159cm' price: 19900 line_price: 39800 quantity: 2 variant: *product-4-var-1 product: *product-4 # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- # Link Lists # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- links: - &link-1 id: 1 title: Our Sale url: /collections/sale - &link-2 id: 2 title: Arbor Stuff url: /collections/arbor - &link-3 id: 3 title: All our Snowboards url: /collections/snowboards - &link-4 id: 4 title: Powered by Shopify url: 'http://shopify.com' - &link-5 id: 5 title: About Us url: /pages/about-us - &link-6 id: 6 title: Policies url: /pages/shipping - &link-7 id: 7 title: Contact Us url: /pages/contact - &link-8 id: 8 title: Our blog url: /blogs/bigcheese-blog - &link-9 id: 9 title: New Boots url: /products/burton-boots - &link-10 id: 10 title: Paginated Sale url: /collections/paginated-sale - &link-11 id: 11 title: Our Paginated blog url: /blogs/paginated-blog - &link-12 id: 12 title: Catalog url: /collections/all # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- # Link Lists # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- link_lists: - &link-list-1 id: 1 title: 'Main Menu' handle: 'main-menu' links: - *link-12 - *link-5 - *link-7 - *link-8 - &link-list-2 id: 1 title: 'Footer Menu' handle: 'footer' links: - *link-5 - *link-6 # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- # Collections # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- collections: - &collection-1 id: 1 title: Frontpage handle: frontpage url: /collections/frontpage products: - *product-7 - *product-8 - *product-9 - &collection-2 id: 2 title: Arbor handle: arbor url: /collections/arbor products: - *product-1 - *product-2 - &collection-3 id: 3 title: Snowboards handle: snowboards url: /collections/snowboards description:

This is a description for my Snowboards collection.

products: - *product-1 - *product-2 - *product-3 - *product-4 - &collection-4 id: 4 title: Items On Sale handle: sale url: /collections/sale products: - *product-1 - &collection-5 id: 5 title: Paginated Sale handle: 'paginated-sale' url: '/collections/paginated-sale' products: - *product-1 - *product-2 - *product-3 - *product-4 products_count: 210 - &collection-6 id: 6 title: All products handle: 'all' url: '/collections/all' products: - *product-7 - *product-8 - *product-9 - *product-6 - *product-1 - *product-2 - *product-3 - *product-4 - *product-5 # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- # Pages and Blogs # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=- pages: - &page-2 id: 1 title: Contact Us handle: contact url: /pages/contact author: Tobi content: "

You can contact us via phone under (555) 567-2222.

Our retail store is located at Rue d'Avignon 32, Avignon (Provence).

Opening Hours:
Monday through Friday: 9am - 6pm
Saturday: 10am - 3pm
Sunday: closed

" created_at: 2005-04-04 12:00 - &page-3 id: 2 title: About Us handle: about-us url: /pages/about-us author: Tobi content: "

Our company was founded in 1894 and we are since operating out of Avignon from the beautiful Provence.

We offer the highest quality products and are proud to serve our customers to their heart's content.

" created_at: 2005-04-04 12:00 - &page-4 id: 3 title: Shopping Cart handle: shopping-cart url: /pages/shopping-cart author: Tobi content: "
  • Your order is safe with us. Our checkout uses industry standard security to protect your information.
  • Your order will be billed immediately upon checkout.
  • ALL SALES ARE FINAL: Defective or damaged product will be exchanged
  • All orders are processed expediently: usually in under 24 hours.
" created_at: 2005-04-04 12:00 - &page-5 id: 4 title: Shipping and Handling handle: shipping url: /pages/shipping author: Tobi content:

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

created_at: 2005-04-04 12:00 - &page-6 id: 5 title: Frontpage handle: frontpage url: /pages/frontpage author: Tobi content:

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

created_at: 2005-04-04 12:00 blogs: - id: 1 handle: news title: News url: /blogs/news articles: - id: 3 title: 'Welcome to the new Foo Shop' author: Daniel content:

Welcome to your Shopify store! The jaded Pixel crew is really glad you decided to take Shopify for a spin.

To help you get you started with Shopify, here are a couple of tips regarding what you see on this page.

The text you see here is an article. To edit this article, create new articles or create new pages you can go to the Blogs & Pages tab of the administration menu.

The Shopify t-shirt above is a product and selling products is what Shopify is all about. To edit this product, or create new products you can go to the Products Tab in of the administration menu.

While you're looking around be sure to check out the Collections and Navigations tabs and soon you will be well on your way to populating your site.

And of course don't forget to browse the theme gallery to pick a new look for your shop!

Shopify is in beta
If you would like to make comments or suggestions please visit us in the Shopify Forums or drop us an email.

created_at: 2005-04-04 16:00 - id: 4 title: 'Breaking News: Restock on all sales products' author: Tobi content: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. created_at: 2005-04-04 12:00 articles_count: 2 - id: 2 handle: bigcheese-blog title: Bigcheese blog url: /blogs/bigcheese-blog articles: - id: 1 title: 'One thing you probably did not know yet...' author: Justin content: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. created_at: 2005-04-04 16:00 comments: - id: 1 author: John Smith email: john@smith.com content: Wow...great article man. status: published created_at: 2009-01-01 12:00 updated_at: 2009-02-01 12:00 url: "" - id: 2 author: John Jones email: john@jones.com content: I really enjoyed this article. And I love your shop! It's awesome. Shopify rocks! status: published created_at: 2009-03-01 12:00 updated_at: 2009-02-01 12:00 url: "http://somesite.com/" - id: 2 title: Fascinating author: Tobi content: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. created_at: 2005-04-06 12:00 comments: articles_count: 2 comments_enabled?: true comment_post_url: "" comments_count: 2 moderated?: true - id: 3 handle: paginated-blog title: Paginated blog url: /blogs/paginated-blog articles: - id: 6 title: 'One thing you probably did not know yet...' author: Justin content: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. created_at: 2005-04-04 16:00 - id: 7 title: Fascinating author: Tobi content: Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. created_at: 2005-04-06 12:00 articles_count: 200 liquid-5.4.0/performance/shopify/weight_filter.rb000066400000000000000000000002611427076730100221600ustar00rootroot00000000000000# frozen_string_literal: true module WeightFilter def weight(grams) format("%.2f", grams / 1000) end def weight_with_unit(grams) "#{weight(grams)} kg" end end liquid-5.4.0/performance/tests/000077500000000000000000000000001427076730100164615ustar00rootroot00000000000000liquid-5.4.0/performance/tests/dropify/000077500000000000000000000000001427076730100201355ustar00rootroot00000000000000liquid-5.4.0/performance/tests/dropify/article.liquid000066400000000000000000000053051427076730100227740ustar00rootroot00000000000000

{{ article.title }}

posted {{ article.created_at | date: "%Y %h" }} by

{{ article.content }}
{% if blog.comments_enabled? %}

Comments

    {% for comment in article.comments %}
  • {{ comment.author }} said on {{ comment.created_at | date: "%B %d, %Y" }}:
    {{ comment.content }}
  • {% endfor %}
{% form article %}

Leave a comment

{% if form.posted_successfully? %} {% if blog.moderated? %}
Successfully posted your comment.
It will have to be approved by the blog owner first before showing up.
{% else %}
Successfully posted your comment.
{% endif %} {% endif %} {% if form.errors %}
Not all the fields have been filled out correctly!
{% endif %}
{% if blog.moderated? %}

comments have to be approved before showing up

{% endif %} {% endform %}
{% endif %} liquid-5.4.0/performance/tests/dropify/blog.liquid000066400000000000000000000014651427076730100222770ustar00rootroot00000000000000

{{page.title}}

{% paginate blog.articles by 20 %} {% for article in blog.articles %}

{{ article.title }}

Posted on {{ article.created_at | date: "%B %d, '%y" }} by {{ article.author }}.

{{ article.content | strip_html | truncate: 250 }}
{% if blog.comments_enabled? %}

{{ article.comments_count }} comments

{% endif %}
{% endfor %} {% endpaginate %}
liquid-5.4.0/performance/tests/dropify/cart.liquid000066400000000000000000000044721427076730100223060ustar00rootroot00000000000000
{% if cart.item_count == 0 %}

Your shopping cart is looking rather empty...

{% else %}

You have {{ cart.item_count }} {{ cart.item_count | pluralize: 'product', 'products' }} in here!

{% if additional_checkout_buttons %}

- or -

{{ content_for_additional_checkout_buttons }}
{% endif %}
{% endif %}
liquid-5.4.0/performance/tests/dropify/collection.liquid000066400000000000000000000014001427076730100234740ustar00rootroot00000000000000{% paginate collection.products by 20 %}
    {% for product in collection.products %}
  • {{product.title}}

    {{ product.description | strip_html | truncatewords: 35 }}

    {{ product.price_min | money }}{% if product.price_varies %} - {{ product.price_max | money }}{% endif %}

  • {% endfor %}
{% endpaginate %} liquid-5.4.0/performance/tests/dropify/index.liquid000066400000000000000000000033701427076730100224600ustar00rootroot00000000000000

Featured Items

{% for product in collections.frontpage.products limit:1 offset:0 %}
{{ product.title | escape }}

{{ product.title }}

{{ product.description | strip_html | truncatewords: 18 }}

{{ product.price_min | money }}

{% endfor %} {% for product in collections.frontpage.products offset:1 %}
{{ product.title | escape }}

{{ product.title }}

{{ product.price_min | money }}

{% endfor %}
{% assign article = pages.frontpage %} {% if article.content != "" %}

{{ article.title }}

{{ article.content }}
{% else %}
In Admin > Blogs & Pages, create a page with the handle frontpage and it will show up here.
{{ "Learn more about handles" | link_to: "http://wiki.shopify.com/Handle" }}
{% endif %}

{% for article in blogs.news.articles offset:1 %}

{{ article.title }}

{{ article.content }}
{% endfor %}
liquid-5.4.0/performance/tests/dropify/page.liquid000066400000000000000000000001611427076730100222600ustar00rootroot00000000000000

{{page.title}}

{{page.content}}
liquid-5.4.0/performance/tests/dropify/product.liquid000066400000000000000000000052231427076730100230300ustar00rootroot00000000000000
{% for image in product.images %} {% if forloop.first %} {{product.title | escape }} {% else %} {{product.title | escape }} {% endif %} {% endfor %}

{{ product.title }}

  • Vendor: {{ product.vendor | link_to_vendor }}
  • Type: {{ product.type | link_to_type }}
{{ product.price_min | money }}{% if product.price_varies %} - {{ product.price_max | money }}{% endif %}
{{ product.description }}
liquid-5.4.0/performance/tests/dropify/theme.liquid000066400000000000000000000074771427076730100224670ustar00rootroot00000000000000 {{shop.name}} - {{page_title}} {{ 'textile.css' | global_asset_url | stylesheet_tag }} {{ 'lightbox/v204/lightbox.css' | global_asset_url | stylesheet_tag }} {{ 'prototype/1.6/prototype.js' | global_asset_url | script_tag }} {{ 'scriptaculous/1.8.2/scriptaculous.js' | global_asset_url | script_tag }} {{ 'lightbox/v204/lightbox.js' | global_asset_url | script_tag }} {{ 'option_selection.js' | shopify_asset_url | script_tag }} {{ 'layout.css' | asset_url | stylesheet_tag }} {{ 'shop.js' | asset_url | script_tag }} {{ content_for_header }}

Skip to navigation.

{% if cart.item_count > 0 %} {% endif %}

{{ content_for_layout }}

{% if tags %} {% endif %}


liquid-5.4.0/performance/tests/ripen/000077500000000000000000000000001427076730100175765ustar00rootroot00000000000000liquid-5.4.0/performance/tests/ripen/article.liquid000066400000000000000000000052031427076730100224320ustar00rootroot00000000000000

{{ article.title }}

posted {{ article.created_at | date: "%Y %h" }} by

{{ article.content }}
{% if blog.comments_enabled? %}

Comments

    {% for comment in article.comments %}
  • {{ comment.content }}
    Posted by {{ comment.author }} on {{ comment.created_at | date: "%B %d, %Y" }}
  • {% endfor %}
{% form article %}

Leave a comment

{% if form.posted_successfully? %} {% if blog.moderated? %}
Successfully posted your comment.
It will have to be approved by the blog owner first before showing up.
{% else %}
Successfully posted your comment.
{% endif %} {% endif %} {% if form.errors %}
Not all the fields have been filled out correctly!
{% endif %}
{% if blog.moderated? %}

comments have to be approved before showing up

{% endif %} {% endform %}
{% endif %} liquid-5.4.0/performance/tests/ripen/blog.liquid000066400000000000000000000006651427076730100217410ustar00rootroot00000000000000

{{page.title}}

{% for article in blog.articles %}

{{ article.created_at | date: '%d %b' }} {{ article.title }}

{{ article.content }} {% if blog.comments_enabled? %}

{{ article.comments_count }} comments

{% endif %} {% endfor %}
liquid-5.4.0/performance/tests/ripen/cart.liquid000066400000000000000000000040351427076730100217420ustar00rootroot00000000000000
{% if cart.item_count == 0 %}

Your shopping cart is empty...

Continue shopping

{% else %}

{% for item in cart.items %} {% endfor %}
Product Qty Price Total Remove
{{ item.product.featured_image | product_img_url: 'thumb' | img_tag }} {{ item.title }} {{ item.price | money }} {{item.line_price | money }} Remove

Subtotal: {{cart.total_price | money_with_currency }}

{% if additional_checkout_buttons %}

- or -

{{ content_for_additional_checkout_buttons }}
{% endif %}
{% endif %}
liquid-5.4.0/performance/tests/ripen/collection.liquid000066400000000000000000000017311427076730100231440ustar00rootroot00000000000000
{% if collection.description %}
{{ collection.description }}
{% endif %} {% paginate collection.products by 20 %}
    {% for product in collection.products %}
  • {{ product.title | escape }}

    {{product.title}}

    {{ product.description | strip_html | truncatewords: 35 }}

    {{ product.price_min | money }}{% if product.price_varies %} - {{ product.price_max | money }}{% endif %}

  • {% endfor %}
{% endpaginate %}
liquid-5.4.0/performance/tests/ripen/index.liquid000066400000000000000000000025031427076730100221160ustar00rootroot00000000000000

Featured products...

{% assign article = pages.frontpage %} {% if article.content != "" %}

{{ article.title }}

{{ article.content }} {% else %} In Admin > Blogs & Pages, create a page with the handle frontpage and it will show up here.
{{ "Learn more about handles" | link_to: "http://wiki.shopify.com/Handle" }} {% endif %}
liquid-5.4.0/performance/tests/ripen/page.liquid000066400000000000000000000001421427076730100217200ustar00rootroot00000000000000

{{page.title}}

{{ page.content }}
liquid-5.4.0/performance/tests/ripen/product.liquid000066400000000000000000000053771427076730100225030ustar00rootroot00000000000000

{{ product.title }}

{% for image in product.images %} {% if forloop.first %} {{product.title | escape }} {% else %} {{product.title | escape }} {% endif %} {% endfor %}
  • Vendor: {{ product.vendor | link_to_vendor }}
  • Type: {{ product.type | link_to_type }}
{{ product.price_min | money }}{% if product.price_varies %} - {{ product.price_max | money }}{% endif %}
{% if product.available %}
{% else %} Sold Out! {% endif %}
{{ product.description }}
liquid-5.4.0/performance/tests/ripen/theme.liquid000066400000000000000000000047671427076730100221270ustar00rootroot00000000000000 {{shop.name}} - {{page_title}} {{ 'main.css' | asset_url | stylesheet_tag }} {{ 'shop.js' | asset_url | script_tag }} {{ 'mootools.js' | asset_url | script_tag }} {{ 'slimbox.js' | asset_url | script_tag }} {{ 'option_selection.js' | shopify_asset_url | script_tag }} {{ 'slimbox.css' | asset_url | stylesheet_tag }} {{ content_for_header }}

Skip to navigation.

{{ content_for_layout }}
{% if template != 'cart' %}
Shopping Cart
{% if cart.item_count != 0 %} {{ cart.item_count }} {{ cart.item_count | pluralize: 'item', 'items' }} in your cart {% else %} Your cart is empty {% endif %}
{% endif %}
liquid-5.4.0/performance/tests/tribble/000077500000000000000000000000001427076730100201045ustar00rootroot00000000000000liquid-5.4.0/performance/tests/tribble/404.liquid000066400000000000000000000041161427076730100216260ustar00rootroot00000000000000

Oh no!

Seems like you are looking for something that just isn't here. Try heading back to our main page. Or you can checkout some of our featured products below.

Featured Products

    {% for product in collections.frontpage.products %}
  • {{product.title}}

    {{ product.description | truncatewords: 15 }}

    {{ product.title | escape }}

    View Details {% if product.compare_at_price %} {% if product.price_min != product.compare_at_price %} {{product.compare_at_price | money}} - {% endif %} {% endif %} {{product.price_min | money}}

  • {% endfor %}
liquid-5.4.0/performance/tests/tribble/article.liquid000066400000000000000000000062731427076730100227500ustar00rootroot00000000000000

{{article.title}}

{{ article.created_at | date: "%b %d" }}
{{ article.content }}
{% if blog.comments_enabled? %}

Comments

    {% for comment in article.comments %}
  • {{ comment.content }}
    Posted by {{ comment.author }} on {{ comment.created_at | date: "%B %d, %Y" }}
  • {% endfor %}
{% form article %}

Leave a comment

{% if form.posted_successfully? %} {% if blog.moderated? %}
Successfully posted your comment.
It will have to be approved by the blog owner first before showing up.
{% else %}
Successfully posted your comment.
{% endif %} {% endif %} {% if form.errors %}
Not all the fields have been filled out correctly!
{% endif %}
{% if blog.moderated? %}

comments have to be approved before showing up

{% endif %} {% endform %}
{% endif %}

Why Shop With Us?

  • 24 Hours

    We're always here to help.

  • No Spam

    We'll never share your info.

  • Secure Servers

    Checkout is 256bit encrypted.

liquid-5.4.0/performance/tests/tribble/blog.liquid000066400000000000000000000021551427076730100222430ustar00rootroot00000000000000

Post from our blog...

{% paginate blog.articles by 20 %} {% for article in blog.articles %}

{{ article.title }}

{{ article.created_at | date: "%b %d" }}
{{ article.content }}
{% endfor %}
{{ paginate | default_pagination }}
{% endpaginate %}

Why Shop With Us?

  • 24 Hours

    We're always here to help.

  • No Spam

    We'll never share your info.

  • Secure Servers

    Checkout is 256bit encrypted.

liquid-5.4.0/performance/tests/tribble/cart.liquid000066400000000000000000000115721427076730100222540ustar00rootroot00000000000000
. {% if cart.item_count == 0 %}

Your cart is currently empty.

{% else %}

Your Cart ({{ cart.item_count }} {{ cart.item_count | pluralize: 'item', 'items' }}, {{cart.total_price | money_with_currency }} total)

{% for item in cart.items %} {% endfor %}
{{ item.product.featured_image | product_img_url: 'thumb' | img_tag }}

{{ item.title }}

{{item.line_price | money }} Remove
{{ pages.shopping-cart.content }}

Order Total: {{cart.total_price | money_with_currency }}

{% if additional_checkout_buttons %}

- or -

{{ content_for_additional_checkout_buttons }}
{% endif %}
{% endif %}

Other Products You Might Enjoy

    {% for product in collections.frontpage.products limit:2 %}
  • {{product.title}}

    {{ product.description | truncatewords: 15 }}

    {{ product.title | escape }}

    View Details {% if product.compare_at_price %} {% if product.price_min != product.compare_at_price %} {{product.compare_at_price | money}} - {% endif %} {% endif %} {{product.price_min | money}}

  • {% endfor %}

Why Shop With Us?

  • 24 Hours

    We're always here to help.

  • No Spam

    We'll never share your info.

  • Secure Servers

    Checkout is 256bit encrypted.

liquid-5.4.0/performance/tests/tribble/collection.liquid000066400000000000000000000047131427076730100234550ustar00rootroot00000000000000

{{ collection.title }}

{% if collection.description.size > 0 %}
{{ collection.description }}
{% endif %} {% paginate collection.products by 8 %}
    {% for product in collection.products %}
  • {{product.title}}

    {{ product.description | truncatewords: 15 }}

    {{ product.title | escape }}

    View Details {% if product.compare_at_price %} {% if product.price_min != product.compare_at_price %} {{product.compare_at_price | money}} - {% endif %} {% endif %} {{product.price_min | money}}

  • {% endfor %}
{{ paginate | default_pagination }}

Why Shop With Us?

  • 24 Hours

    We're always here to help.

  • No Spam

    We'll never share your info.

  • Secure Servers

    Checkout is 256bit encrypted.

{% endpaginate %} liquid-5.4.0/performance/tests/tribble/index.liquid000066400000000000000000000060721427076730100224310ustar00rootroot00000000000000

Three Great Reasons You Should Shop With Us...

  • Free Shipping

    On all orders over $25

  • Top Quality

    Hand made in our shop

  • 100% Guarantee

    Any time, any reason

{{pages.alert.content}}
    {% for product in collections.frontpage.products %}
  • {{product.title}}

    {{ product.description | truncatewords: 15 }}

    {{ product.title | escape }}

    View Details {% if product.compare_at_price %} {% if product.price_min != product.compare_at_price %} {{product.compare_at_price | money}} - {% endif %} {% endif %} {{product.price_min | money}}

  • {% endfor %}

Why Shop With Us?

  • 24 Hours

    We're always here to help.

  • No Spam

    We'll never share your info.

  • Save Energy

    We're green, all the way.

  • Secure Servers

    Checkout is 256bits encrypted.

Our Company

{{pages.about-us.content | truncatewords: 49}} read more

liquid-5.4.0/performance/tests/tribble/page.liquid000066400000000000000000000036741427076730100222430ustar00rootroot00000000000000

{{page.title}}

{{page.content}}

Featured Products

    {% for product in collections.frontpage.products %}
  • {{product.title}}

    {{ product.description | truncatewords: 15 }}

    {{ product.title | escape }}

    View Details {% if product.compare_at_price %} {% if product.price_min != product.compare_at_price %} {{product.compare_at_price | money}} - {% endif %} {% endif %} {{product.price_min | money}}

  • {% endfor %}
liquid-5.4.0/performance/tests/tribble/product.liquid000066400000000000000000000070641427076730100230040ustar00rootroot00000000000000

{{ collection.title }} {{ product.title }}

Product Tags: {% for tag in product.tags %} {{ tag }} | {% endfor %}

{{ product.title }}

{{ product.description }}

{% if product.available %}

Product Options:

{% else %}

Sold out!

Sorry, we're all out of this product. Check back often and order when it returns

{% endif %}
{% for image in product.images %} {% if forloop.first %}
{{product.title | escape }}
{% else %} {% endif %} {% endfor %}
    {% for image in product.images %} {% if forloop.first %} {% else %}
  • {{product.title | escape }}
  • {% endif %} {% endfor %}

Why Shop With Us?

  • 24 Hours

    We're always here to help.

  • No Spam

    We'll never share your info.

  • Secure Servers

    Checkout is 256bit encrypted.

liquid-5.4.0/performance/tests/tribble/search.liquid000066400000000000000000000024121427076730100225610ustar00rootroot00000000000000

Search Results

{% if search.performed %} {% paginate search.results by 10 %} {% if search.results == empty %}
Your search for "{{search.terms | escape}}" did not yield any results
{% else %}
    {% for item in search.results %}
  • {{ item.title | link_to: item.url }}

    {{ item.content | strip_html | truncatewords: 65 | highlight: search.terms }} ... view this item

  • {% endfor %}
{% endif %}
{{ paginate | default_pagination }}

Why Shop With Us?

  • 24 Hours

    We're always here to help.

  • No Spam

    We'll never share your info.

  • Secure Servers

    Checkout is 256bit encrypted.

{% endpaginate %} {% endif %} liquid-5.4.0/performance/tests/tribble/theme.liquid000066400000000000000000000056201427076730100224220ustar00rootroot00000000000000 {{shop.name}} - {{page_title}} {{ 'reset.css' | asset_url | stylesheet_tag }} {{ 'style.css' | asset_url | stylesheet_tag }} {{ 'lightbox.css' | asset_url | stylesheet_tag }} {{ 'prototype/1.6/prototype.js' | global_asset_url | script_tag }} {{ 'scriptaculous/1.8.2/scriptaculous.js' | global_asset_url | script_tag }} {{ 'lightbox.js' | asset_url | script_tag }} {{ 'option_selection.js' | shopify_asset_url | script_tag }} {{ content_for_header }}

Shopping Cart

{% if cart.item_count == 0 %} Your cart is currently empty {% else %} {{ cart.item_count }} {{ cart.item_count | pluralize: 'item', 'items' }} - Total: {{cart.total_price | money_with_currency }} - View Cart {% endif %}

{{shop.name}}

Tribble: A Shopify Theme

{{ content_for_layout }}
liquid-5.4.0/performance/tests/vogue/000077500000000000000000000000001427076730100176065ustar00rootroot00000000000000liquid-5.4.0/performance/tests/vogue/article.liquid000066400000000000000000000046331427076730100224500ustar00rootroot00000000000000

{{ article.title }}

posted {{ article.created_at | date: "%Y %h" }} by {{ article.author }}

{{ article.content }}
{% if blog.comments_enabled? %}

Comments

    {% for comment in article.comments %}
  • {{ comment.content }}
    Posted by {{ comment.author }} on {{ comment.created_at | date: "%B %d, %Y" }}
  • {% endfor %}
{% form article %}

Leave a comment

{% if form.posted_successfully? %} {% if blog.moderated? %}
Successfully posted your comment.
It will have to be approved by the blog owner first before showing up.
{% else %}
Successfully posted your comment.
{% endif %} {% endif %} {% if form.errors %}
Not all the fields have been filled out correctly!
{% endif %}
{% if blog.moderated? %}

comments have to be approved before showing up

{% endif %} {% endform %}
{% endif %} liquid-5.4.0/performance/tests/vogue/blog.liquid000066400000000000000000000014161427076730100217440ustar00rootroot00000000000000

{{page.title}}

{% paginate blog.articles by 20 %} {% for article in blog.articles %}

{{ article.title }}

{% if blog.comments_enabled? %} {{ article.comments_count }} comments — {% endif %} posted {{ article.created_at | date: "%Y %h" }} by {{ article.author }}

{{ article.content }}
{% endfor %} {% endpaginate %}
liquid-5.4.0/performance/tests/vogue/cart.liquid000066400000000000000000000057651427076730100217650ustar00rootroot00000000000000

Shopping Cart

{% if cart.item_count == 0 %}

Your shopping basket is empty. Perhaps a featured item below is of interest...

{% tablerow product in collections.frontpage.products cols: 3 limit: 12 %} {% endtablerow %} {% else %}
{% for item in cart.items %} {% endfor %}
Item Description Price Qty Delete Total
{{ item.title | escape }}

{{ item.title }}

{{ item.product.description | strip_html | truncate: 120 }}
{{ item.price | money }}{% if item.variant.compare_at_price > item.price %}
{{ item.variant.compare_at_price | money }}{% endif %}
Remove {{ item.line_price | money }}

Subtotal {{ cart.total_price | money }}

{% if additional_checkout_buttons %}

- or -

{{ content_for_additional_checkout_buttons }}
{% endif %}
{% endif %} liquid-5.4.0/performance/tests/vogue/collection.liquid000066400000000000000000000020711427076730100231520ustar00rootroot00000000000000{% paginate collection.products by 12 %}{% if collection.products.size == 0 %} No products found in this collection.{% else %}

{{ collection.title }}

{{ collection.description }} {% tablerow product in collection.products cols: 3 %} {% endtablerow %} {% if paginate.pages > 1 %}
{{ paginate | default_pagination }}
{% endif %}{% endif %} {% endpaginate %} liquid-5.4.0/performance/tests/vogue/index.liquid000066400000000000000000000023011427076730100221220ustar00rootroot00000000000000
{% assign article = pages.frontpage %} {% if article.content != "" %}

{{ article.title }}

{{ article.content }} {% else %} In Admin > Blogs & Pages, create a page with the handle frontpage and it will show up here.
{{ "Learn more about handles" | link_to: "http://wiki.shopify.com/Handle" }} {% endif %}
{% tablerow product in collections.frontpage.products cols: 3 limit: 12 %} {% endtablerow %} liquid-5.4.0/performance/tests/vogue/page.liquid000066400000000000000000000000621427076730100217310ustar00rootroot00000000000000

{{ page.title }}

{{ page.content }} liquid-5.4.0/performance/tests/vogue/product.liquid000066400000000000000000000050431427076730100225010ustar00rootroot00000000000000
{% for image in product.images %}{% if forloop.first %}
{{ product.title | escape }}
{% else %}
{{ product.title | escape }}
{% endif %}{% endfor %}

{{ product.title }}

{{ product.description }} {% if product.available %}
{% else %}

This product is temporarily unavailable

{% endif %}
Continue Shopping
Browse more {{ product.type | link_to_type }} or additional {{ product.vendor | link_to_vendor }} products.
liquid-5.4.0/performance/tests/vogue/theme.liquid000066400000000000000000000114141427076730100221220ustar00rootroot00000000000000 {{ shop.name }} — {{ page_title }} {{ 'stylesheet.css' | asset_url | stylesheet_tag }} {{ 'mootools.js' | global_asset_url | script_tag }} {{ 'slimbox.js' | global_asset_url | script_tag }} {{ 'option_selection.js' | shopify_asset_url | script_tag }} {{ content_for_header }}
{% if template == "search" %}

Search Results

{% endif %} {{ content_for_layout }}
{% if template != "cart" %}{% if template != "product" %}
{% if template == "index" %} {% if blogs.news.articles.size > 1 %} Subscribe

More news

{% endif %} {% endif %} {% if template == "collection" %}

Collection Tags

{% if collection.tags.size == 0 %} No tags found.{% else %} {% for tag in collection.tags %}{% if current_tags contains tag %} {{ tag | highlight_active_tag | link_to_remove_tag: tag }}{% else %} {{ tag | highlight_active_tag | link_to_add_tag: tag }}{% endif %}{% unless forloop.last %}, {% endunless %}{% endfor %}{% endif %}
{% endif %}

Navigation

{% if template != "page" %}

Featured Products

{% endif %}
{% endif %}{% endif %}
liquid-5.4.0/performance/theme_runner.rb000066400000000000000000000104131427076730100203360ustar00rootroot00000000000000# frozen_string_literal: true # This profiler run simulates Shopify. # We are looking in the tests directory for liquid files and render them within the designated layout file. # We will also export a substantial database to liquid which the templates can render values of. # 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 # Shopify which is likely the biggest user of liquid in the world which something to the tune of several # million Template#render calls a day. require_relative 'shopify/liquid' require_relative 'shopify/database' class ThemeRunner class FileSystem def initialize(path) @path = path end # Called by Liquid to retrieve a template file def read_template_file(template_path) File.read(@path + '/' + template_path + '.liquid') end end # Initialize a new liquid ThemeRunner instance # Will load all templates into memory, do this now so that we don't profile IO. def initialize @tests = Dir[__dir__ + '/tests/**/*.liquid'].collect do |test| next if File.basename(test) == 'theme.liquid' theme_path = File.dirname(test) + '/theme.liquid' { liquid: File.read(test), layout: (File.file?(theme_path) ? File.read(theme_path) : nil), template_name: test, } end.compact compile_all_tests end # `compile` will test just the compilation portion of liquid without any templates def compile @tests.each do |test_hash| Liquid::Template.new.parse(test_hash[:liquid]) Liquid::Template.new.parse(test_hash[:layout]) end end # `run` is called to benchmark rendering and compiling at the same time def run each_test do |liquid, layout, assigns, page_template, template_name| compile_and_render(liquid, layout, assigns, page_template, template_name) end end # `render` is called to benchmark just the render portion of liquid def render @compiled_tests.each do |test| tmpl = test[:tmpl] assigns = test[:assigns] layout = test[:layout] if layout assigns['content_for_layout'] = tmpl.render!(assigns) layout.render!(assigns) else tmpl.render!(assigns) end end end private def render_layout(template, layout, assigns) assigns['content_for_layout'] = template.render!(assigns) layout&.render!(assigns) end def compile_and_render(template, layout, assigns, page_template, template_file) compiled_test = compile_test(template, layout, assigns, page_template, template_file) render_layout(compiled_test[:tmpl], compiled_test[:layout], compiled_test[:assigns]) end def compile_all_tests @compiled_tests = [] each_test do |liquid, layout, assigns, page_template, template_name| @compiled_tests << compile_test(liquid, layout, assigns, page_template, template_name) end @compiled_tests end def compile_test(template, layout, assigns, page_template, template_file) tmpl = init_template(page_template, template_file) parsed_template = tmpl.parse(template).dup if layout parsed_layout = tmpl.parse(layout) { tmpl: parsed_template, assigns: assigns, layout: parsed_layout } else { tmpl: parsed_template, assigns: assigns } end end # utility method with similar functionality needed in `compile_all_tests` and `run` def each_test # Dup assigns because will make some changes to them assigns = Database.tables.dup @tests.each do |test_hash| # Compute page_template outside of profiler run, uninteresting to profiler page_template = File.basename(test_hash[:template_name], File.extname(test_hash[:template_name])) yield(test_hash[:liquid], test_hash[:layout], assigns, page_template, test_hash[:template_name]) end end # set up a new Liquid::Template object for use in `compile_and_render` and `compile_test` def init_template(page_template, template_file) tmpl = Liquid::Template.new tmpl.assigns['page_title'] = 'Page title' tmpl.assigns['template'] = page_template tmpl.registers[:file_system] = ThemeRunner::FileSystem.new(File.dirname(template_file)) tmpl end end liquid-5.4.0/test/000077500000000000000000000000001427076730100137755ustar00rootroot00000000000000liquid-5.4.0/test/fixtures/000077500000000000000000000000001427076730100156465ustar00rootroot00000000000000liquid-5.4.0/test/fixtures/en_locale.yml000066400000000000000000000003751427076730100203170ustar00rootroot00000000000000--- simple: "less is more" whatever: "something %{something}" errors: i18n: undefined_interpolation: "undefined key %{key}" unknown_translation: "translation '%{name}' wasn't found" syntax: oops: "something wasn't right" liquid-5.4.0/test/integration/000077500000000000000000000000001427076730100163205ustar00rootroot00000000000000liquid-5.4.0/test/integration/assign_test.rb000066400000000000000000000063261427076730100211770ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class AssignTest < Minitest::Test include Liquid def test_assign_with_hyphen_in_variable_name template_source = <<-END_TEMPLATE {% assign this-thing = 'Print this-thing' %} {{ this-thing }} END_TEMPLATE template = Template.parse(template_source) rendered = template.render! assert_equal("Print this-thing", rendered.strip) end def test_assigned_variable assert_template_result('.foo.', '{% assign foo = values %}.{{ foo[0] }}.', 'values' => %w(foo bar baz)) assert_template_result('.bar.', '{% assign foo = values %}.{{ foo[1] }}.', 'values' => %w(foo bar baz)) end def test_assign_with_filter assert_template_result('.bar.', '{% assign foo = values | split: "," %}.{{ foo[1] }}.', 'values' => "foo,bar,baz") end def test_assign_syntax_error assert_match_syntax_error(/assign/, '{% assign foo not values %}.', 'values' => "foo,bar,baz") end def test_assign_uses_error_mode with_error_mode(:strict) do assert_raises(SyntaxError) do Template.parse("{% assign foo = ('X' | downcase) %}") end end with_error_mode(:lax) do assert(Template.parse("{% assign foo = ('X' | downcase) %}")) end end def test_expression_with_whitespace_in_square_brackets source = "{% assign r = a[ 'b' ] %}{{ r }}" assert_template_result('result', source, 'a' => { 'b' => 'result' }) end def test_assign_score_exceeding_resource_limit t = Template.parse("{% assign foo = 42 %}{% assign bar = 23 %}") t.resource_limits.assign_score_limit = 1 assert_equal("Liquid error: Memory limits exceeded", t.render) assert(t.resource_limits.reached?) t.resource_limits.assign_score_limit = 2 assert_equal("", t.render!) refute_nil(t.resource_limits.assign_score) end def test_assign_score_exceeding_limit_from_composite_object t = Template.parse("{% assign foo = 'aaaa' | reverse %}") t.resource_limits.assign_score_limit = 3 assert_equal("Liquid error: Memory limits exceeded", t.render) assert(t.resource_limits.reached?) t.resource_limits.assign_score_limit = 5 assert_equal("", t.render!) end def test_assign_score_of_int assert_equal(1, assign_score_of(123)) end def test_assign_score_of_string_counts_bytes assert_equal(3, assign_score_of('123')) assert_equal(5, assign_score_of('12345')) assert_equal(9, assign_score_of('すごい')) end def test_assign_score_of_array assert_equal(1, assign_score_of([])) assert_equal(2, assign_score_of([123])) assert_equal(6, assign_score_of([123, 'abcd'])) end def test_assign_score_of_hash assert_equal(1, assign_score_of({})) assert_equal(5, assign_score_of('int' => 123)) assert_equal(12, assign_score_of('int' => 123, 'str' => 'abcd')) end private class ObjectWrapperDrop < Liquid::Drop def initialize(obj) @obj = obj end def value @obj end end def assign_score_of(obj) context = Liquid::Context.new('drop' => ObjectWrapperDrop.new(obj)) Liquid::Template.parse('{% assign obj = drop.value %}').render!(context) context.resource_limits.assign_score end end liquid-5.4.0/test/integration/blank_test.rb000066400000000000000000000064361427076730100210040ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class FoobarTag < Liquid::Tag def render_to_output_buffer(_context, output) output << ' ' output end end class BlankTestFileSystem def read_template_file(template_path) template_path end end class BlankTest < Minitest::Test include Liquid N = 10 def wrap_in_for(body) "{% for i in (1..#{N}) %}#{body}{% endfor %}" end def wrap_in_if(body) "{% if true %}#{body}{% endif %}" end def wrap(body) wrap_in_for(body) + wrap_in_if(body) end def test_new_tags_are_not_blank_by_default with_custom_tag('foobar', FoobarTag) do assert_template_result(" " * N, wrap_in_for("{% foobar %}")) end end def test_loops_are_blank assert_template_result("", wrap_in_for(" ")) end def test_if_else_are_blank assert_template_result("", "{% if true %} {% elsif false %} {% else %} {% endif %}") end def test_unless_is_blank assert_template_result("", wrap("{% unless true %} {% endunless %}")) end def test_mark_as_blank_only_during_parsing assert_template_result(" " * (N + 1), wrap(" {% if false %} this never happens, but still, this block is not blank {% endif %}")) end def test_comments_are_blank assert_template_result("", wrap(" {% comment %} whatever {% endcomment %} ")) end def test_captures_are_blank assert_template_result("", wrap(" {% capture foo %} whatever {% endcapture %} ")) end def test_nested_blocks_are_blank_but_only_if_all_children_are assert_template_result("", wrap(wrap(" "))) assert_template_result("\n but this is not " * (N + 1), wrap('{% if true %} {% comment %} this is blank {% endcomment %} {% endif %} {% if true %} but this is not {% endif %}')) end def test_assigns_are_blank assert_template_result("", wrap(' {% assign foo = "bar" %} ')) end def test_whitespace_is_blank assert_template_result("", wrap(" ")) assert_template_result("", wrap("\t")) end def test_whitespace_is_not_blank_if_other_stuff_is_present body = " x " assert_template_result(body * (N + 1), wrap(body)) end def test_increment_is_not_blank assert_template_result(" 0" * 2 * (N + 1), wrap("{% assign foo = 0 %} {% increment foo %} {% decrement foo %}")) end def test_cycle_is_not_blank assert_template_result(" " * ((N + 1) / 2) + " ", wrap("{% cycle ' ', ' ' %}")) end def test_raw_is_not_blank assert_template_result(" " * (N + 1), wrap(" {% raw %} {% endraw %}")) end def test_include_is_blank Liquid::Template.file_system = BlankTestFileSystem.new assert_template_result("foobar" * (N + 1), wrap("{% include 'foobar' %}")) assert_template_result(" foobar " * (N + 1), wrap("{% include ' foobar ' %}")) assert_template_result(" " * (N + 1), wrap(" {% include ' ' %} ")) end def test_case_is_blank assert_template_result("", wrap(" {% assign foo = 'bar' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} {% endcase %} ")) assert_template_result("", wrap(" {% assign foo = 'else' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} {% endcase %} ")) assert_template_result(" x " * (N + 1), wrap(" {% assign foo = 'else' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} x {% endcase %} ")) end end liquid-5.4.0/test/integration/block_test.rb000066400000000000000000000030221427076730100207730ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class BlockTest < Minitest::Test include Liquid def test_unexpected_end_tag exc = assert_raises(SyntaxError) do Template.parse("{% if true %}{% endunless %}") end assert_equal(exc.message, "Liquid syntax error: 'endunless' is not a valid delimiter for if tags. use endif") end def test_with_custom_tag with_custom_tag('testtag', Block) do assert(Liquid::Template.parse("{% testtag %} {% endtesttag %}")) end end def test_custom_block_tags_have_a_default_render_to_output_buffer_method_for_backwards_compatibility klass1 = Class.new(Block) do def render(*) 'hello' end end with_custom_tag('blabla', klass1) do template = Liquid::Template.parse("{% blabla %} bla {% endblabla %}") assert_equal('hello', template.render) buf = +'' output = template.render({}, output: buf) assert_equal('hello', output) assert_equal('hello', buf) assert_equal(buf.object_id, output.object_id) end klass2 = Class.new(klass1) do def render(*) 'foo' + super + 'bar' end end with_custom_tag('blabla', klass2) do template = Liquid::Template.parse("{% blabla %} foo {% endblabla %}") assert_equal('foohellobar', template.render) buf = +'' output = template.render({}, output: buf) assert_equal('foohellobar', output) assert_equal('foohellobar', buf) assert_equal(buf.object_id, output.object_id) end end end liquid-5.4.0/test/integration/capture_test.rb000066400000000000000000000033151427076730100213510ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class CaptureTest < Minitest::Test include Liquid def test_captures_block_content_in_variable assert_template_result("test string", "{% capture 'var' %}test string{% endcapture %}{{var}}", {}) end def test_capture_with_hyphen_in_variable_name template_source = <<-END_TEMPLATE {% capture this-thing %}Print this-thing{% endcapture %} {{ this-thing }} END_TEMPLATE template = Template.parse(template_source) rendered = template.render! assert_equal("Print this-thing", rendered.strip) end def test_capture_to_variable_from_outer_scope_if_existing template_source = <<-END_TEMPLATE {% assign var = '' %} {% if true %} {% capture var %}first-block-string{% endcapture %} {% endif %} {% if true %} {% capture var %}test-string{% endcapture %} {% endif %} {{var}} END_TEMPLATE template = Template.parse(template_source) rendered = template.render! assert_equal("test-string", rendered.gsub(/\s/, '')) end def test_assigning_from_capture template_source = <<-END_TEMPLATE {% assign first = '' %} {% assign second = '' %} {% for number in (1..3) %} {% capture first %}{{number}}{% endcapture %} {% assign second = first %} {% endfor %} {{ first }}-{{ second }} END_TEMPLATE template = Template.parse(template_source) rendered = template.render! assert_equal("3-3", rendered.gsub(/\s/, '')) end def test_increment_assign_score_by_bytes_not_characters t = Template.parse("{% capture foo %}すごい{% endcapture %}") t.render! assert_equal(9, t.resource_limits.assign_score) end end liquid-5.4.0/test/integration/context_test.rb000066400000000000000000000432331427076730100213750ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class HundredCentes def to_liquid 100 end end class CentsDrop < Liquid::Drop def amount HundredCentes.new end def non_zero? true end end class ContextSensitiveDrop < Liquid::Drop def test @context['test'] end end class Category attr_accessor :name def initialize(name) @name = name end def to_liquid CategoryDrop.new(self) end end class CategoryDrop < Liquid::Drop attr_accessor :category, :context def initialize(category) @category = category end end class CounterDrop < Liquid::Drop def count @count ||= 0 @count += 1 end end class ArrayLike def fetch(index) end def [](index) @counts ||= [] @counts[index] ||= 0 @counts[index] += 1 end def to_liquid self end end class ContextTest < Minitest::Test include Liquid def setup @context = Liquid::Context.new end def test_variables @context['string'] = 'string' assert_equal('string', @context['string']) @context['num'] = 5 assert_equal(5, @context['num']) @context['time'] = Time.parse('2006-06-06 12:00:00') assert_equal(Time.parse('2006-06-06 12:00:00'), @context['time']) @context['date'] = Date.today assert_equal(Date.today, @context['date']) now = Time.now @context['datetime'] = now assert_equal(now, @context['datetime']) @context['bool'] = true assert_equal(true, @context['bool']) @context['bool'] = false assert_equal(false, @context['bool']) @context['nil'] = nil assert_nil(@context['nil']) assert_nil(@context['nil']) end def test_variables_not_existing assert_nil(@context['does_not_exist']) end def test_scoping @context.push @context.pop assert_raises(Liquid::ContextError) do @context.pop end assert_raises(Liquid::ContextError) do @context.push @context.pop @context.pop end end def test_length_query @context['numbers'] = [1, 2, 3, 4] assert_equal(4, @context['numbers.size']) @context['numbers'] = { 1 => 1, 2 => 2, 3 => 3, 4 => 4 } assert_equal(4, @context['numbers.size']) @context['numbers'] = { 1 => 1, 2 => 2, 3 => 3, 4 => 4, 'size' => 1000 } assert_equal(1000, @context['numbers.size']) end def test_hyphenated_variable @context['oh-my'] = 'godz' assert_equal('godz', @context['oh-my']) end def test_add_filter filter = Module.new do def hi(output) output + ' hi!' end end context = Context.new context.add_filters(filter) assert_equal('hi? hi!', context.invoke(:hi, 'hi?')) context = Context.new assert_equal('hi?', context.invoke(:hi, 'hi?')) context.add_filters(filter) assert_equal('hi? hi!', context.invoke(:hi, 'hi?')) end def test_only_intended_filters_make_it_there filter = Module.new do def hi(output) output + ' hi!' end end context = Context.new assert_equal("Wookie", context.invoke("hi", "Wookie")) context.add_filters(filter) assert_equal("Wookie hi!", context.invoke("hi", "Wookie")) end def test_add_item_in_outer_scope @context['test'] = 'test' @context.push assert_equal('test', @context['test']) @context.pop assert_equal('test', @context['test']) end def test_add_item_in_inner_scope @context.push @context['test'] = 'test' assert_equal('test', @context['test']) @context.pop assert_nil(@context['test']) end def test_hierachical_data @context['hash'] = { "name" => 'tobi' } assert_equal('tobi', @context['hash.name']) assert_equal('tobi', @context['hash["name"]']) end def test_keywords assert_equal(true, @context['true']) assert_equal(false, @context['false']) end def test_digits assert_equal(100, @context['100']) assert_equal(100.00, @context['100.00']) end def test_strings assert_equal("hello!", @context['"hello!"']) assert_equal("hello!", @context["'hello!'"]) end def test_merge @context.merge("test" => "test") assert_equal('test', @context['test']) @context.merge("test" => "newvalue", "foo" => "bar") assert_equal('newvalue', @context['test']) assert_equal('bar', @context['foo']) end def test_array_notation @context['test'] = [1, 2, 3, 4, 5] assert_equal(1, @context['test[0]']) assert_equal(2, @context['test[1]']) assert_equal(3, @context['test[2]']) assert_equal(4, @context['test[3]']) assert_equal(5, @context['test[4]']) end def test_recoursive_array_notation @context['test'] = { 'test' => [1, 2, 3, 4, 5] } assert_equal(1, @context['test.test[0]']) @context['test'] = [{ 'test' => 'worked' }] assert_equal('worked', @context['test[0].test']) end def test_hash_to_array_transition @context['colors'] = { 'Blue' => ['003366', '336699', '6699CC', '99CCFF'], 'Green' => ['003300', '336633', '669966', '99CC99'], 'Yellow' => ['CC9900', 'FFCC00', 'FFFF99', 'FFFFCC'], 'Red' => ['660000', '993333', 'CC6666', 'FF9999'], } assert_equal('003366', @context['colors.Blue[0]']) assert_equal('FF9999', @context['colors.Red[3]']) end def test_try_first @context['test'] = [1, 2, 3, 4, 5] assert_equal(1, @context['test.first']) assert_equal(5, @context['test.last']) @context['test'] = { 'test' => [1, 2, 3, 4, 5] } assert_equal(1, @context['test.test.first']) assert_equal(5, @context['test.test.last']) @context['test'] = [1] assert_equal(1, @context['test.first']) assert_equal(1, @context['test.last']) end def test_access_hashes_with_hash_notation @context['products'] = { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] } @context['product'] = { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] } assert_equal(5, @context['products["count"]']) assert_equal('deepsnow', @context['products["tags"][0]']) assert_equal('deepsnow', @context['products["tags"].first']) assert_equal('draft151cm', @context['product["variants"][0]["title"]']) assert_equal('element151cm', @context['product["variants"][1]["title"]']) assert_equal('draft151cm', @context['product["variants"][0]["title"]']) assert_equal('element151cm', @context['product["variants"].last["title"]']) end def test_access_variable_with_hash_notation @context['foo'] = 'baz' @context['bar'] = 'foo' assert_equal('baz', @context['["foo"]']) assert_equal('baz', @context['[bar]']) end def test_access_hashes_with_hash_access_variables @context['var'] = 'tags' @context['nested'] = { 'var' => 'tags' } @context['products'] = { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] } assert_equal('deepsnow', @context['products[var].first']) assert_equal('freestyle', @context['products[nested.var].last']) end def test_hash_notation_only_for_hash_access @context['array'] = [1, 2, 3, 4, 5] @context['hash'] = { 'first' => 'Hello' } assert_equal(1, @context['array.first']) assert_nil(@context['array["first"]']) assert_equal('Hello', @context['hash["first"]']) end def test_first_can_appear_in_middle_of_callchain @context['product'] = { 'variants' => [{ 'title' => 'draft151cm' }, { 'title' => 'element151cm' }] } assert_equal('draft151cm', @context['product.variants[0].title']) assert_equal('element151cm', @context['product.variants[1].title']) assert_equal('draft151cm', @context['product.variants.first.title']) assert_equal('element151cm', @context['product.variants.last.title']) end def test_cents @context.merge("cents" => HundredCentes.new) assert_equal(100, @context['cents']) end def test_nested_cents @context.merge("cents" => { 'amount' => HundredCentes.new }) assert_equal(100, @context['cents.amount']) @context.merge("cents" => { 'cents' => { 'amount' => HundredCentes.new } }) assert_equal(100, @context['cents.cents.amount']) end def test_cents_through_drop @context.merge("cents" => CentsDrop.new) assert_equal(100, @context['cents.amount']) end def test_nested_cents_through_drop @context.merge("vars" => { "cents" => CentsDrop.new }) assert_equal(100, @context['vars.cents.amount']) end def test_drop_methods_with_question_marks @context.merge("cents" => CentsDrop.new) assert(@context['cents.non_zero?']) end def test_context_from_within_drop @context.merge("test" => '123', "vars" => ContextSensitiveDrop.new) assert_equal('123', @context['vars.test']) end def test_nested_context_from_within_drop @context.merge("test" => '123', "vars" => { "local" => ContextSensitiveDrop.new }) assert_equal('123', @context['vars.local.test']) end def test_ranges @context.merge("test" => '5') assert_equal((1..5), @context['(1..5)']) assert_equal((1..5), @context['(1..test)']) assert_equal((5..5), @context['(test..test)']) end def test_cents_through_drop_nestedly @context.merge("cents" => { "cents" => CentsDrop.new }) assert_equal(100, @context['cents.cents.amount']) @context.merge("cents" => { "cents" => { "cents" => CentsDrop.new } }) assert_equal(100, @context['cents.cents.cents.amount']) end def test_drop_with_variable_called_only_once @context['counter'] = CounterDrop.new assert_equal(1, @context['counter.count']) assert_equal(2, @context['counter.count']) assert_equal(3, @context['counter.count']) end def test_drop_with_key_called_only_once @context['counter'] = CounterDrop.new assert_equal(1, @context['counter["count"]']) assert_equal(2, @context['counter["count"]']) assert_equal(3, @context['counter["count"]']) end def test_proc_as_variable @context['dynamic'] = proc { 'Hello' } assert_equal('Hello', @context['dynamic']) end def test_lambda_as_variable @context['dynamic'] = proc { 'Hello' } assert_equal('Hello', @context['dynamic']) end def test_nested_lambda_as_variable @context['dynamic'] = { "lambda" => proc { 'Hello' } } assert_equal('Hello', @context['dynamic.lambda']) end def test_array_containing_lambda_as_variable @context['dynamic'] = [1, 2, proc { 'Hello' }, 4, 5] assert_equal('Hello', @context['dynamic[2]']) end def test_lambda_is_called_once @global = 0 @context['callcount'] = proc { @global += 1 @global.to_s } assert_equal('1', @context['callcount']) assert_equal('1', @context['callcount']) assert_equal('1', @context['callcount']) end def test_nested_lambda_is_called_once @global = 0 @context['callcount'] = { "lambda" => proc { @global += 1 @global.to_s } } assert_equal('1', @context['callcount.lambda']) assert_equal('1', @context['callcount.lambda']) assert_equal('1', @context['callcount.lambda']) end def test_lambda_in_array_is_called_once @global = 0 @context['callcount'] = [1, 2, proc { @global += 1 @global.to_s }, 4, 5] assert_equal('1', @context['callcount[2]']) assert_equal('1', @context['callcount[2]']) assert_equal('1', @context['callcount[2]']) end def test_access_to_context_from_proc @context.registers[:magic] = 345392 @context['magic'] = proc { @context.registers[:magic] } assert_equal(345392, @context['magic']) end def test_to_liquid_and_context_at_first_level @context['category'] = Category.new("foobar") assert_kind_of(CategoryDrop, @context['category']) assert_equal(@context, @context['category'].context) end def test_interrupt_avoids_object_allocations @context.interrupt? # ruby 3.0.0 allocates on the first call assert_no_object_allocations do @context.interrupt? end end def test_context_initialization_with_a_proc_in_environment contx = Context.new([test: ->(c) { c['poutine'] }], test: :foo) assert(contx) assert_nil(contx['poutine']) end def test_apply_global_filter global_filter_proc = ->(output) { "#{output} filtered" } context = Context.new context.global_filter = global_filter_proc assert_equal('hi filtered', context.apply_global_filter('hi')) end def test_static_environments_are_read_with_lower_priority_than_environments context = Context.build( static_environments: { 'shadowed' => 'static', 'unshadowed' => 'static' }, environments: { 'shadowed' => 'dynamic' } ) assert_equal('dynamic', context['shadowed']) assert_equal('static', context['unshadowed']) end def test_apply_global_filter_when_no_global_filter_exist context = Context.new assert_equal('hi', context.apply_global_filter('hi')) end def test_new_isolated_subcontext_does_not_inherit_variables super_context = Context.new super_context['my_variable'] = 'some value' subcontext = super_context.new_isolated_subcontext assert_nil(subcontext['my_variable']) end def test_new_isolated_subcontext_inherits_static_environment super_context = Context.build(static_environments: { 'my_environment_value' => 'my value' }) subcontext = super_context.new_isolated_subcontext assert_equal('my value', subcontext['my_environment_value']) end def test_new_isolated_subcontext_inherits_resource_limits resource_limits = ResourceLimits.new({}) super_context = Context.new({}, {}, {}, false, resource_limits) subcontext = super_context.new_isolated_subcontext assert_equal(resource_limits, subcontext.resource_limits) end def test_new_isolated_subcontext_inherits_exception_renderer super_context = Context.new super_context.exception_renderer = ->(_e) { 'my exception message' } subcontext = super_context.new_isolated_subcontext assert_equal('my exception message', subcontext.handle_error(Liquid::Error.new)) end def test_new_isolated_subcontext_does_not_inherit_non_static_registers registers = { my_register: :my_value, } super_context = Context.new({}, {}, Registers.new(registers)) super_context.registers[:my_register] = :my_alt_value subcontext = super_context.new_isolated_subcontext assert_equal(:my_value, subcontext.registers[:my_register]) end def test_new_isolated_subcontext_inherits_static_registers super_context = Context.build(registers: { my_register: :my_value }) subcontext = super_context.new_isolated_subcontext assert_equal(:my_value, subcontext.registers[:my_register]) end def test_new_isolated_subcontext_registers_do_not_pollute_context super_context = Context.build(registers: { my_register: :my_value }) subcontext = super_context.new_isolated_subcontext subcontext.registers[:my_register] = :my_alt_value assert_equal(:my_value, super_context.registers[:my_register]) end def test_new_isolated_subcontext_inherits_filters my_filter = Module.new do def my_filter(*) 'my filter result' end end super_context = Context.new super_context.add_filters([my_filter]) subcontext = super_context.new_isolated_subcontext template = Template.parse('{{ 123 | my_filter }}') assert_equal('my filter result', template.render(subcontext)) end def test_disables_tag_specified context = Context.new context.with_disabled_tags(%w(foo bar)) do assert_equal(true, context.tag_disabled?("foo")) assert_equal(true, context.tag_disabled?("bar")) assert_equal(false, context.tag_disabled?("unknown")) end end def test_disables_nested_tags context = Context.new context.with_disabled_tags(["foo"]) do context.with_disabled_tags(["foo"]) do assert_equal(true, context.tag_disabled?("foo")) assert_equal(false, context.tag_disabled?("bar")) end context.with_disabled_tags(["bar"]) do assert_equal(true, context.tag_disabled?("foo")) assert_equal(true, context.tag_disabled?("bar")) context.with_disabled_tags(["foo"]) do assert_equal(true, context.tag_disabled?("foo")) assert_equal(true, context.tag_disabled?("bar")) end end assert_equal(true, context.tag_disabled?("foo")) assert_equal(false, context.tag_disabled?("bar")) end end def test_override_global_filter global = Module.new do def notice(output) "Global #{output}" end end local = Module.new do def notice(output) "Local #{output}" end end with_global_filter(global) do assert_equal('Global test', Template.parse("{{'test' | notice }}").render!) assert_equal('Local test', Template.parse("{{'test' | notice }}").render!({}, filters: [local])) end end def test_has_key_will_not_add_an_error_for_missing_keys with_error_mode(:strict) do context = Context.new context.key?('unknown') assert_empty(context.errors) 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 private def assert_no_object_allocations unless RUBY_ENGINE == 'ruby' skip("stackprof needed to count object allocations") end require 'stackprof' profile = StackProf.run(mode: :object) do yield end assert_equal(0, profile[:samples]) end end # ContextTest liquid-5.4.0/test/integration/document_test.rb000066400000000000000000000007631427076730100215300ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class DocumentTest < Minitest::Test include Liquid def test_unexpected_outer_tag exc = assert_raises(SyntaxError) do Template.parse("{% else %}") end assert_equal(exc.message, "Liquid syntax error: Unexpected outer 'else' tag") end def test_unknown_tag exc = assert_raises(SyntaxError) do Template.parse("{% foo %}") end assert_equal(exc.message, "Liquid syntax error: Unknown tag 'foo'") end end liquid-5.4.0/test/integration/drop_test.rb000066400000000000000000000236001427076730100206510ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class ContextDrop < Liquid::Drop def scopes @context.scopes.size end def scopes_as_array (1..@context.scopes.size).to_a end def loop_pos @context['forloop.index'] end def liquid_method_missing(method) @context[method] end end class ProductDrop < Liquid::Drop class TextDrop < Liquid::Drop def array ['text1', 'text2'] end def text 'text1' end end class CatchallDrop < Liquid::Drop def liquid_method_missing(method) "catchall_method: #{method}" end end def texts TextDrop.new end def catchall CatchallDrop.new end def context ContextDrop.new end protected def callmenot "protected" end end class EnumerableDrop < Liquid::Drop def liquid_method_missing(method) method end def size 3 end def first 1 end def count 3 end def min 1 end def max 3 end def each yield 1 yield 2 yield 3 end end class RealEnumerableDrop < Liquid::Drop include Enumerable def liquid_method_missing(method) method end def each yield 1 yield 2 yield 3 end end class DropsTest < Minitest::Test include Liquid def test_product_drop tpl = Liquid::Template.parse(' ') assert_equal(' ', tpl.render!('product' => ProductDrop.new)) end def test_drop_does_only_respond_to_whitelisted_methods assert_equal("", Liquid::Template.parse("{{ product.inspect }}").render!('product' => ProductDrop.new)) assert_equal("", Liquid::Template.parse("{{ product.pretty_inspect }}").render!('product' => ProductDrop.new)) assert_equal("", Liquid::Template.parse("{{ product.whatever }}").render!('product' => ProductDrop.new)) assert_equal("", Liquid::Template.parse('{{ product | map: "inspect" }}').render!('product' => ProductDrop.new)) assert_equal("", Liquid::Template.parse('{{ product | map: "pretty_inspect" }}').render!('product' => ProductDrop.new)) assert_equal("", Liquid::Template.parse('{{ product | map: "whatever" }}').render!('product' => ProductDrop.new)) end def test_drops_respond_to_to_liquid assert_equal("text1", Liquid::Template.parse("{{ product.to_liquid.texts.text }}").render!('product' => ProductDrop.new)) assert_equal("text1", Liquid::Template.parse('{{ product | map: "to_liquid" | map: "texts" | map: "text" }}').render!('product' => ProductDrop.new)) end def test_text_drop output = Liquid::Template.parse(' {{ product.texts.text }} ').render!('product' => ProductDrop.new) assert_equal(' text1 ', output) end def test_catchall_unknown_method output = Liquid::Template.parse(' {{ product.catchall.unknown }} ').render!('product' => ProductDrop.new) assert_equal(' catchall_method: unknown ', output) end def test_catchall_integer_argument_drop output = Liquid::Template.parse(' {{ product.catchall[8] }} ').render!('product' => ProductDrop.new) assert_equal(' catchall_method: 8 ', output) end def test_text_array_drop output = Liquid::Template.parse('{% for text in product.texts.array %} {{text}} {% endfor %}').render!('product' => ProductDrop.new) assert_equal(' text1 text2 ', output) end def test_context_drop output = Liquid::Template.parse(' {{ context.bar }} ').render!('context' => ContextDrop.new, 'bar' => "carrot") assert_equal(' carrot ', output) end def test_context_drop_array_with_map output = Liquid::Template.parse(' {{ contexts | map: "bar" }} ').render!('contexts' => [ContextDrop.new, ContextDrop.new], 'bar' => "carrot") assert_equal(' carrotcarrot ', output) end def test_nested_context_drop output = Liquid::Template.parse(' {{ product.context.foo }} ').render!('product' => ProductDrop.new, 'foo' => "monkey") assert_equal(' monkey ', output) end def test_protected output = Liquid::Template.parse(' {{ product.callmenot }} ').render!('product' => ProductDrop.new) assert_equal(' ', output) end def test_object_methods_not_allowed [:dup, :clone, :singleton_class, :eval, :class_eval, :inspect].each do |method| output = Liquid::Template.parse(" {{ product.#{method} }} ").render!('product' => ProductDrop.new) assert_equal(' ', output) end end def test_scope assert_equal('1', Liquid::Template.parse('{{ context.scopes }}').render!('context' => ContextDrop.new)) assert_equal('2', Liquid::Template.parse('{%for i in dummy%}{{ context.scopes }}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1])) assert_equal('3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ context.scopes }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1])) end def test_scope_though_proc assert_equal('1', Liquid::Template.parse('{{ s }}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] })) assert_equal('2', Liquid::Template.parse('{%for i in dummy%}{{ s }}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }, 'dummy' => [1])) assert_equal('3', Liquid::Template.parse('{%for i in dummy%}{%for i in dummy%}{{ s }}{%endfor%}{%endfor%}').render!('context' => ContextDrop.new, 's' => proc { |c| c['context.scopes'] }, 'dummy' => [1])) end def test_scope_with_assigns assert_equal('variable', Liquid::Template.parse('{% assign a = "variable"%}{{a}}').render!('context' => ContextDrop.new)) assert_equal('variable', Liquid::Template.parse('{% assign a = "variable"%}{%for i in dummy%}{{a}}{%endfor%}').render!('context' => ContextDrop.new, 'dummy' => [1])) assert_equal('test', Liquid::Template.parse('{% assign header_gif = "test"%}{{header_gif}}').render!('context' => ContextDrop.new)) assert_equal('test', Liquid::Template.parse("{% assign header_gif = 'test'%}{{header_gif}}").render!('context' => ContextDrop.new)) end def test_scope_from_tags assert_equal('1', Liquid::Template.parse('{% for i in context.scopes_as_array %}{{i}}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1])) assert_equal('12', Liquid::Template.parse('{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1])) assert_equal('123', Liquid::Template.parse('{%for a in dummy%}{%for a in dummy%}{% for i in context.scopes_as_array %}{{i}}{% endfor %}{% endfor %}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1])) end def test_access_context_from_drop assert_equal('123', Liquid::Template.parse('{%for a in dummy%}{{ context.loop_pos }}{% endfor %}').render!('context' => ContextDrop.new, 'dummy' => [1, 2, 3])) end def test_enumerable_drop assert_equal('123', Liquid::Template.parse('{% for c in collection %}{{c}}{% endfor %}').render!('collection' => EnumerableDrop.new)) end def test_enumerable_drop_size assert_equal('3', Liquid::Template.parse('{{collection.size}}').render!('collection' => EnumerableDrop.new)) end def test_enumerable_drop_will_invoke_liquid_method_missing_for_clashing_method_names ["select", "each", "map", "cycle"].each do |method| assert_equal(method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)) assert_equal(method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)) assert_equal(method.to_s, Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new)) assert_equal(method.to_s, Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new)) end end def test_some_enumerable_methods_still_get_invoked [:count, :max].each do |method| assert_equal("3", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new)) assert_equal("3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new)) assert_equal("3", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)) assert_equal("3", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)) end assert_equal("yes", Liquid::Template.parse("{% if collection contains 3 %}yes{% endif %}").render!('collection' => RealEnumerableDrop.new)) [:min, :first].each do |method| assert_equal("1", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => RealEnumerableDrop.new)) assert_equal("1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => RealEnumerableDrop.new)) assert_equal("1", Liquid::Template.parse("{{collection.#{method}}}").render!('collection' => EnumerableDrop.new)) assert_equal("1", Liquid::Template.parse("{{collection[\"#{method}\"]}}").render!('collection' => EnumerableDrop.new)) end end def test_empty_string_value_access assert_equal('', Liquid::Template.parse('{{ product[value] }}').render!('product' => ProductDrop.new, 'value' => '')) end def test_nil_value_access assert_equal('', Liquid::Template.parse('{{ product[value] }}').render!('product' => ProductDrop.new, 'value' => nil)) end def test_default_to_s_on_drops assert_equal('ProductDrop', Liquid::Template.parse("{{ product }}").render!('product' => ProductDrop.new)) assert_equal('EnumerableDrop', Liquid::Template.parse('{{ collection }}').render!('collection' => EnumerableDrop.new)) end def test_invokable_methods assert_equal(%w(to_liquid catchall context texts).to_set, ProductDrop.invokable_methods) assert_equal(%w(to_liquid scopes_as_array loop_pos scopes).to_set, ContextDrop.invokable_methods) assert_equal(%w(to_liquid size max min first count).to_set, EnumerableDrop.invokable_methods) assert_equal(%w(to_liquid max min sort count first).to_set, RealEnumerableDrop.invokable_methods) end end # DropsTest liquid-5.4.0/test/integration/error_handling_test.rb000066400000000000000000000213651427076730100227100ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class ErrorHandlingTest < Minitest::Test include Liquid def test_templates_parsed_with_line_numbers_renders_them_in_errors template = <<-LIQUID Hello, {{ errors.standard_error }} will raise a standard error. Bla bla test. {{ errors.syntax_error }} will raise a syntax error. This is an argument error: {{ errors.argument_error }} Bla. LIQUID expected = <<-TEXT Hello, Liquid error (line 3): standard error will raise a standard error. Bla bla test. Liquid syntax error (line 7): syntax error will raise a syntax error. This is an argument error: Liquid error (line 9): argument error Bla. TEXT output = Liquid::Template.parse(template, line_numbers: true).render('errors' => ErrorDrop.new) assert_equal(expected, output) end def test_standard_error template = Liquid::Template.parse(' {{ errors.standard_error }} ') assert_equal(' Liquid error: standard error ', template.render('errors' => ErrorDrop.new)) assert_equal(1, template.errors.size) assert_equal(StandardError, template.errors.first.class) end def test_syntax template = Liquid::Template.parse(' {{ errors.syntax_error }} ') assert_equal(' Liquid syntax error: syntax error ', template.render('errors' => ErrorDrop.new)) assert_equal(1, template.errors.size) assert_equal(SyntaxError, template.errors.first.class) end def test_argument template = Liquid::Template.parse(' {{ errors.argument_error }} ') assert_equal(' Liquid error: argument error ', template.render('errors' => ErrorDrop.new)) assert_equal(1, template.errors.size) assert_equal(ArgumentError, template.errors.first.class) end def test_missing_endtag_parse_time_error assert_raises(Liquid::SyntaxError) do Liquid::Template.parse(' {% for a in b %} ... ') end end def test_unrecognized_operator with_error_mode(:strict) do assert_raises(SyntaxError) do Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ') end end end def test_lax_unrecognized_operator template = Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', error_mode: :lax) assert_equal(' Liquid error: Unknown operator =! ', template.render) assert_equal(1, template.errors.size) assert_equal(Liquid::ArgumentError, template.errors.first.class) end def test_with_line_numbers_adds_numbers_to_parser_errors err = assert_raises(SyntaxError) do Liquid::Template.parse(' foobar {% "cat" | foobar %} bla ', line_numbers: true) end assert_match(/Liquid syntax error \(line 4\)/, err.message) end def test_with_line_numbers_adds_numbers_to_parser_errors_with_whitespace_trim err = assert_raises(SyntaxError) do Liquid::Template.parse(' foobar {%- "cat" | foobar -%} bla ', line_numbers: true) end assert_match(/Liquid syntax error \(line 4\)/, err.message) end def test_parsing_warn_with_line_numbers_adds_numbers_to_lexer_errors template = Liquid::Template.parse(' foobar {% if 1 =! 2 %}ok{% endif %} bla ', error_mode: :warn, line_numbers: true) assert_equal(['Liquid syntax error (line 4): Unexpected character = in "1 =! 2"'], template.warnings.map(&:message)) end def test_parsing_strict_with_line_numbers_adds_numbers_to_lexer_errors err = assert_raises(SyntaxError) do Liquid::Template.parse(' foobar {% if 1 =! 2 %}ok{% endif %} bla ', error_mode: :strict, line_numbers: true) end assert_equal('Liquid syntax error (line 4): Unexpected character = in "1 =! 2"', err.message) end def test_syntax_errors_in_nested_blocks_have_correct_line_number err = assert_raises(SyntaxError) do Liquid::Template.parse(' foobar {% if 1 != 2 %} {% foo %} {% endif %} bla ', line_numbers: true) end assert_equal("Liquid syntax error (line 5): Unknown tag 'foo'", err.message) end def test_strict_error_messages err = assert_raises(SyntaxError) do Liquid::Template.parse(' {% if 1 =! 2 %}ok{% endif %} ', error_mode: :strict) end assert_equal('Liquid syntax error: Unexpected character = in "1 =! 2"', err.message) err = assert_raises(SyntaxError) do Liquid::Template.parse('{{%%%}}', error_mode: :strict) end assert_equal('Liquid syntax error: Unexpected character % in "{{%%%}}"', err.message) end def test_warnings template = Liquid::Template.parse('{% if ~~~ %}{{%%%}}{% else %}{{ hello. }}{% endif %}', error_mode: :warn) assert_equal(3, template.warnings.size) assert_equal('Unexpected character ~ in "~~~"', template.warnings[0].to_s(false)) assert_equal('Unexpected character % in "{{%%%}}"', template.warnings[1].to_s(false)) assert_equal('Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].to_s(false)) assert_equal('', template.render) end def test_warning_line_numbers template = Liquid::Template.parse("{% if ~~~ %}\n{{%%%}}{% else %}\n{{ hello. }}{% endif %}", error_mode: :warn, line_numbers: true) assert_equal('Liquid syntax error (line 1): Unexpected character ~ in "~~~"', template.warnings[0].message) assert_equal('Liquid syntax error (line 2): Unexpected character % in "{{%%%}}"', template.warnings[1].message) assert_equal('Liquid syntax error (line 3): Expected id but found end_of_string in "{{ hello. }}"', template.warnings[2].message) assert_equal(3, template.warnings.size) assert_equal([1, 2, 3], template.warnings.map(&:line_number)) end # Liquid should not catch Exceptions that are not subclasses of StandardError, like Interrupt and NoMemoryError def test_exceptions_propagate assert_raises(Exception) do template = Liquid::Template.parse('{{ errors.exception }}') template.render('errors' => ErrorDrop.new) end end def test_default_exception_renderer_with_internal_error template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true) output = template.render('errors' => ErrorDrop.new) assert_equal('This is a runtime error: Liquid error (line 1): internal', output) assert_equal([Liquid::InternalError], template.errors.map(&:class)) end def test_setting_default_exception_renderer old_exception_renderer = Liquid::Template.default_exception_renderer exceptions = [] Liquid::Template.default_exception_renderer = ->(e) { exceptions << e '' } template = Liquid::Template.parse('This is a runtime error: {{ errors.argument_error }}') output = template.render('errors' => ErrorDrop.new) assert_equal('This is a runtime error: ', output) assert_equal([Liquid::ArgumentError], template.errors.map(&:class)) ensure Liquid::Template.default_exception_renderer = old_exception_renderer if old_exception_renderer end def test_exception_renderer_exposing_non_liquid_error template = Liquid::Template.parse('This is a runtime error: {{ errors.runtime_error }}', line_numbers: true) exceptions = [] handler = ->(e) { exceptions << e e.cause } output = template.render({ 'errors' => ErrorDrop.new }, exception_renderer: handler) assert_equal('This is a runtime error: runtime error', output) assert_equal([Liquid::InternalError], exceptions.map(&:class)) assert_equal(exceptions, template.errors) assert_equal('#', exceptions.first.cause.inspect) end class TestFileSystem def read_template_file(_template_path) "{{ errors.argument_error }}" end end def test_included_template_name_with_line_numbers old_file_system = Liquid::Template.file_system 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) ensure Liquid::Template.file_system = old_file_system end assert_equal("Argument error:\nLiquid error (product line 1): argument error", page) assert_equal("product", template.errors.first.template_name) end def test_bug_compatible_silencing_of_errors_in_blank_nodes output = Liquid::Template.parse("{% assign x = 0 %}{% if 1 < '2' %}not blank{% assign x = 3 %}{% endif %}{{ x }}").render assert_equal("Liquid error: comparison of Integer with String failed0", output) output = Liquid::Template.parse("{% assign x = 0 %}{% if 1 < '2' %}{% assign x = 3 %}{% endif %}{{ x }}").render assert_equal("0", output) end end liquid-5.4.0/test/integration/expression_test.rb000066400000000000000000000023321427076730100221030ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class ExpressionTest < Minitest::Test def test_keyword_literals assert_equal(true, parse_and_eval("true")) assert_equal(true, parse_and_eval(" true ")) end def test_string assert_equal("single quoted", parse_and_eval("'single quoted'")) assert_equal("double quoted", parse_and_eval('"double quoted"')) assert_equal("spaced", parse_and_eval(" 'spaced' ")) assert_equal("spaced2", parse_and_eval(' "spaced2" ')) end def test_int assert_equal(123, parse_and_eval("123")) assert_equal(456, parse_and_eval(" 456 ")) assert_equal(12, parse_and_eval("012")) end def test_float assert_equal(1.5, parse_and_eval("1.5")) assert_equal(2.5, parse_and_eval(" 2.5 ")) end def test_range assert_equal(1..2, parse_and_eval("(1..2)")) assert_equal(3..4, parse_and_eval(" ( 3 .. 4 ) ")) end private def parse_and_eval(markup, **assigns) if Liquid::Template.error_mode == :strict p = Liquid::Parser.new(markup) markup = p.expression p.consume(:end_of_string) end expression = Liquid::Expression.parse(markup) context = Liquid::Context.new(assigns) context.evaluate(expression) end end liquid-5.4.0/test/integration/filter_kwarg_test.rb000066400000000000000000000010371427076730100223650ustar00rootroot00000000000000# 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 liquid-5.4.0/test/integration/filter_test.rb000066400000000000000000000136071427076730100212000ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' module MoneyFilter def money(input) format(' %d$ ', input) end def money_with_underscore(input) format(' %d$ ', input) end end module CanadianMoneyFilter def money(input) format(' %d$ CAD ', input) end end module SubstituteFilter def substitute(input, params = {}) input.gsub(/%\{(\w+)\}/) { |_match| params[Regexp.last_match(1)] } end end class FiltersTest < Minitest::Test include Liquid module OverrideObjectMethodFilter def tap(_input) "tap overridden" end end def setup @context = Context.new end def test_local_filter @context['var'] = 1000 @context.add_filters(MoneyFilter) assert_equal(' 1000$ ', Template.parse("{{var | money}}").render(@context)) end def test_underscore_in_filter_name @context['var'] = 1000 @context.add_filters(MoneyFilter) assert_equal(' 1000$ ', Template.parse("{{var | money_with_underscore}}").render(@context)) end def test_second_filter_overwrites_first @context['var'] = 1000 @context.add_filters(MoneyFilter) @context.add_filters(CanadianMoneyFilter) assert_equal(' 1000$ CAD ', Template.parse("{{var | money}}").render(@context)) end def test_size @context['var'] = 'abcd' @context.add_filters(MoneyFilter) assert_equal('4', Template.parse("{{var | size}}").render(@context)) end def test_join @context['var'] = [1, 2, 3, 4] assert_equal("1 2 3 4", Template.parse("{{var | join}}").render(@context)) end def test_sort @context['value'] = 3 @context['numbers'] = [2, 1, 4, 3] @context['words'] = ['expected', 'as', 'alphabetic'] @context['arrays'] = ['flower', 'are'] @context['case_sensitive'] = ['sensitive', 'Expected', 'case'] assert_equal('1 2 3 4', Template.parse("{{numbers | sort | join}}").render(@context)) assert_equal('alphabetic as expected', Template.parse("{{words | sort | join}}").render(@context)) assert_equal('3', Template.parse("{{value | sort}}").render(@context)) assert_equal('are flower', Template.parse("{{arrays | sort | join}}").render(@context)) assert_equal('Expected case sensitive', Template.parse("{{case_sensitive | sort | join}}").render(@context)) end 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 assert_equal('Assert case Insensitive', Template.parse("{{words | sort_natural | join}}").render(@context)) # Test hashes assert_equal('A b C', Template.parse("{{hashes | sort_natural: 'a' | map: 'a' | join}}").render(@context)) # Test objects assert_equal('A b C', Template.parse("{{objects | sort_natural: 'a' | map: 'a' | join}}").render(@context)) end 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 assert_equal('a b c', Template.parse("{{words | compact | join}}").render(@context)) # Test hashes assert_equal('A C', Template.parse("{{hashes | compact: 'a' | map: 'a' | join}}").render(@context)) # Test objects assert_equal('A C', Template.parse("{{objects | compact: 'a' | map: 'a' | join}}").render(@context)) end def test_strip_html @context['var'] = "bla blub" assert_equal("bla blub", Template.parse("{{ var | strip_html }}").render(@context)) end def test_strip_html_ignore_comments_with_html @context['var'] = "bla blub" assert_equal("bla blub", Template.parse("{{ var | strip_html }}").render(@context)) end def test_capitalize @context['var'] = "blub" assert_equal("Blub", Template.parse("{{ var | capitalize }}").render(@context)) end def test_nonexistent_filter_is_ignored @context['var'] = 1000 assert_equal('1000', Template.parse("{{ var | xyzzy }}").render(@context)) end def test_filter_with_keyword_arguments @context['surname'] = 'john' @context['input'] = 'hello %{first_name}, %{last_name}' @context.add_filters(SubstituteFilter) output = Template.parse(%({{ input | substitute: first_name: surname, last_name: 'doe' }})).render(@context) assert_equal('hello john, doe', output) end def test_override_object_method_in_filter assert_equal("tap overridden", Template.parse("{{var | tap}}").render!({ 'var' => 1000 }, filters: [OverrideObjectMethodFilter])) # tap still treated as a non-existent filter assert_equal("1000", Template.parse("{{var | tap}}").render!('var' => 1000)) end def test_liquid_argument_error source = "{{ '' | size: 'too many args' }}" exc = assert_raises(Liquid::ArgumentError) do Template.parse(source).render! end assert_match(/\ALiquid error: wrong number of arguments /, exc.message) assert_equal(exc.message, Template.parse(source).render) end end class FiltersInTemplate < Minitest::Test include Liquid def test_local_global with_global_filter(MoneyFilter) do assert_equal(" 1000$ ", Template.parse("{{1000 | money}}").render!(nil, nil)) assert_equal(" 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, filters: CanadianMoneyFilter)) assert_equal(" 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, filters: [CanadianMoneyFilter])) end end def test_local_filter_with_deprecated_syntax assert_equal(" 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, CanadianMoneyFilter)) assert_equal(" 1000$ CAD ", Template.parse("{{1000 | money}}").render!(nil, [CanadianMoneyFilter])) end end # FiltersTest class TestObject < Liquid::Drop attr_accessor :a def initialize(a) @a = a end end liquid-5.4.0/test/integration/hash_ordering_test.rb000066400000000000000000000007561427076730100225300ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class HashOrderingTest < Minitest::Test module MoneyFilter def money(input) format(' %d$ ', input) end end module CanadianMoneyFilter def money(input) format(' %d$ CAD ', input) end end include Liquid def test_global_register_order with_global_filter(MoneyFilter, CanadianMoneyFilter) do assert_equal(" 1000$ CAD ", Template.parse("{{1000 | money}}").render(nil, nil)) end end end liquid-5.4.0/test/integration/output_test.rb000066400000000000000000000064671427076730100212610ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' module FunnyFilter def make_funny(_input) 'LOL' end def cite_funny(input) "LOL: #{input}" end def add_smiley(input, smiley = ":-)") "#{input} #{smiley}" end def add_tag(input, tag = "p", id = "foo") %(<#{tag} id="#{id}">#{input}) end def paragraph(input) "

#{input}

" end def link_to(name, url) %(#{name}) end end class OutputTest < Minitest::Test include Liquid def setup @assigns = { 'best_cars' => 'bmw', 'car' => { 'bmw' => 'good', 'gm' => 'bad' }, } end def test_variable text = %( {{best_cars}} ) expected = %( bmw ) assert_equal(expected, Template.parse(text).render!(@assigns)) end def test_variable_traversing_with_two_brackets text = %({{ site.data.menu[include.menu][include.locale] }}) assert_equal("it works!", Template.parse(text).render!( "site" => { "data" => { "menu" => { "foo" => { "bar" => "it works!" } } } }, "include" => { "menu" => "foo", "locale" => "bar" } )) end def test_variable_traversing text = %( {{car.bmw}} {{car.gm}} {{car.bmw}} ) expected = %( good bad good ) assert_equal(expected, Template.parse(text).render!(@assigns)) end def test_variable_piping text = %( {{ car.gm | make_funny }} ) expected = %( LOL ) assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])) end def test_variable_piping_with_input text = %( {{ car.gm | cite_funny }} ) expected = %( LOL: bad ) assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])) end def test_variable_piping_with_args text = %! {{ car.gm | add_smiley : ':-(' }} ! expected = %| bad :-( | assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])) end def test_variable_piping_with_no_args text = %( {{ car.gm | add_smiley }} ) expected = %| bad :-) | assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])) end def test_multiple_variable_piping_with_args text = %! {{ car.gm | add_smiley : ':-(' | add_smiley : ':-('}} ! expected = %| bad :-( :-( | assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])) end def test_variable_piping_with_multiple_args text = %( {{ car.gm | add_tag : 'span', 'bar'}} ) expected = %( bad ) assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])) end def test_variable_piping_with_variable_args text = %( {{ car.gm | add_tag : 'span', car.bmw}} ) expected = %( bad ) assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])) end def test_multiple_pipings text = %( {{ best_cars | cite_funny | paragraph }} ) expected = %(

LOL: bmw

) assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])) end def test_link_to text = %( {{ 'Typo' | link_to: 'http://typo.leetsoft.com' }} ) expected = %( Typo ) assert_equal(expected, Template.parse(text).render!(@assigns, filters: [FunnyFilter])) end end # OutputTest liquid-5.4.0/test/integration/parsing_quirks_test.rb000066400000000000000000000076301427076730100227530ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class ParsingQuirksTest < Minitest::Test include Liquid def test_parsing_css text = " div { font-weight: bold; } " assert_equal(text, Template.parse(text).render!) end def test_raise_on_single_close_bracet assert_raises(SyntaxError) do Template.parse("text {{method} oh nos!") end end def test_raise_on_label_and_no_close_bracets assert_raises(SyntaxError) do Template.parse("TEST {{ ") end end def test_raise_on_label_and_no_close_bracets_percent assert_raises(SyntaxError) do Template.parse("TEST {% ") end end def test_error_on_empty_filter assert(Template.parse("{{test}}")) with_error_mode(:lax) do assert(Template.parse("{{|test}}")) end with_error_mode(:strict) do assert_raises(SyntaxError) { Template.parse("{{|test}}") } assert_raises(SyntaxError) { Template.parse("{{test |a|b|}}") } end end def test_meaningless_parens_error with_error_mode(:strict) do assert_raises(SyntaxError) do markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false" Template.parse("{% if #{markup} %} YES {% endif %}") end end end def test_unexpected_characters_syntax_error with_error_mode(:strict) do assert_raises(SyntaxError) do markup = "true && false" Template.parse("{% if #{markup} %} YES {% endif %}") end assert_raises(SyntaxError) do markup = "false || true" Template.parse("{% if #{markup} %} YES {% endif %}") end end end def test_no_error_on_lax_empty_filter assert(Template.parse("{{test |a|b|}}", error_mode: :lax)) assert(Template.parse("{{test}}", error_mode: :lax)) assert(Template.parse("{{|test|}}", error_mode: :lax)) end def test_meaningless_parens_lax with_error_mode(:lax) do assigns = { 'b' => 'bar', 'c' => 'baz' } markup = "a == 'foo' or (b == 'bar' and c == 'baz') or false" assert_template_result(' YES ', "{% if #{markup} %} YES {% endif %}", assigns) end end def test_unexpected_characters_silently_eat_logic_lax with_error_mode(:lax) do markup = "true && false" assert_template_result(' YES ', "{% if #{markup} %} YES {% endif %}") markup = "false || true" assert_template_result('', "{% if #{markup} %} YES {% endif %}") end end def test_raise_on_invalid_tag_delimiter assert_raises(Liquid::SyntaxError) do Template.new.parse('{% end %}') end end def test_unanchored_filter_arguments with_error_mode(:lax) do assert_template_result('hi', "{{ 'hi there' | split$$$:' ' | first }}") assert_template_result('x', "{{ 'X' | downcase) }}") # After the messed up quotes a filter without parameters (reverse) should work # but one with parameters (remove) shouldn't be detected. assert_template_result('here', "{{ 'hi there' | split:\"t\"\" | reverse | first}}") assert_template_result('hi ', "{{ 'hi there' | split:\"t\"\" | remove:\"i\" | first}}") end end def test_invalid_variables_work with_error_mode(:lax) do assert_template_result('bar', "{% assign 123foo = 'bar' %}{{ 123foo }}") assert_template_result('123', "{% assign 123 = 'bar' %}{{ 123 }}") end end def test_extra_dots_in_ranges with_error_mode(:lax) do assert_template_result('12345', "{% for i in (1...5) %}{{ i }}{% endfor %}") end end def test_blank_variable_markup assert_template_result('', "{{}}") end def test_lookup_on_var_with_literal_name assigns = { "blank" => { "x" => "result" } } assert_template_result('result', "{{ blank.x }}", assigns) assert_template_result('result', "{{ blank['x'] }}", assigns) end def test_contains_in_id assert_template_result(' YES ', '{% if containsallshipments == true %} YES {% endif %}', 'containsallshipments' => true) end end # ParsingQuirksTest liquid-5.4.0/test/integration/profiler_test.rb000066400000000000000000000154471427076730100215410ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class ProfilerTest < Minitest::Test class TestDrop < Liquid::Drop def initialize(value) super() @value = value end def to_s artificial_execution_time @value end private # Monotonic clock precision fluctuate based on the operating system # By introducing a small sleep we ensure ourselves to register a non zero unit of time def artificial_execution_time sleep(Process.clock_getres(Process::CLOCK_MONOTONIC)) end end include Liquid class ProfilingFileSystem def read_template_file(template_path) "Rendering template {% assign template_name = '#{template_path}'%}\n{{ template_name }}" end end def setup Liquid::Template.file_system = ProfilingFileSystem.new end def test_template_allows_flagging_profiling t = Template.parse("{{ 'a string' | upcase }}") t.render! assert_nil(t.profiler) end def test_parse_makes_available_simple_profiling t = Template.parse("{{ 'a string' | upcase }}", profile: true) t.render! assert_equal(1, t.profiler.length) node = t.profiler[0] assert_equal(" 'a string' | upcase ", node.code) end def test_render_ignores_raw_strings_when_profiling t = Template.parse("This is raw string\nstuff\nNewline", profile: true) t.render! assert_equal(0, t.profiler.length) end def test_profiling_includes_line_numbers_of_liquid_nodes t = Template.parse("{{ 'a string' | upcase }}\n{% increment test %}", profile: true) t.render! assert_equal(2, t.profiler.length) # {{ 'a string' | upcase }} assert_equal(1, t.profiler[0].line_number) # {{ increment test }} assert_equal(2, t.profiler[1].line_number) end def test_profiling_includes_line_numbers_of_included_partials t = Template.parse("{% include 'a_template' %}", profile: true) t.render! included_children = t.profiler[0].children # {% assign template_name = 'a_template' %} assert_equal(1, included_children[0].line_number) # {{ template_name }} assert_equal(2, included_children[1].line_number) end def test_profiling_render_tag t = Template.parse("{% render 'a_template' %}", profile: true) t.render! render_children = t.profiler[0].children render_children.each do |timing| assert_equal('a_template', timing.partial) end assert_equal([1, 2], render_children.map(&:line_number)) end def test_profiling_times_the_rendering_of_tokens t = Template.parse("{% include 'a_template' %}", profile: true) t.render! node = t.profiler[0] refute_nil(node.render_time) end def test_profiling_times_the_entire_render t = Template.parse("{% include 'a_template' %}", profile: true) t.render! assert(t.profiler.total_render_time >= 0, "Total render time was not calculated") end class SleepTag < Liquid::Tag def initialize(tag_name, markup, parse_context) super @duration = Float(markup) end def render_to_output_buffer(_context, _output) sleep(@duration) end end def test_profiling_multiple_renders with_custom_tag('sleep', SleepTag) do context = Liquid::Context.new t = Liquid::Template.parse("{% sleep 0.001 %}", profile: true) context.template_name = 'index' t.render!(context) context.template_name = 'layout' first_render_time = context.profiler.total_time t.render!(context) profiler = context.profiler children = profiler.children assert_operator(first_render_time, :>=, 0.001) assert_operator(profiler.total_time, :>=, 0.001 + first_render_time) assert_equal(["index", "layout"], children.map(&:template_name)) assert_equal([nil, nil], children.map(&:code)) assert_equal(profiler.total_time, children.map(&:total_time).reduce(&:+)) end end def test_profiling_uses_include_to_mark_children t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}", profile: true) t.render! include_node = t.profiler[1] assert_equal(2, include_node.children.length) end def test_profiling_marks_children_with_the_name_of_included_partial t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}", profile: true) t.render! include_node = t.profiler[1] include_node.children.each do |child| assert_equal("a_template", child.partial) end end def test_profiling_supports_multiple_templates t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}\n{% include 'b_template' %}", profile: true) t.render! a_template = t.profiler[1] a_template.children.each do |child| assert_equal("a_template", child.partial) end b_template = t.profiler[2] b_template.children.each do |child| assert_equal("b_template", child.partial) end end def test_profiling_supports_rendering_the_same_partial_multiple_times t = Template.parse("{{ 'a string' | upcase }}\n{% include 'a_template' %}\n{% include 'a_template' %}", profile: true) t.render! a_template1 = t.profiler[1] a_template1.children.each do |child| assert_equal("a_template", child.partial) end a_template2 = t.profiler[2] a_template2.children.each do |child| assert_equal("a_template", child.partial) end end def test_can_iterate_over_each_profiling_entry t = Template.parse("{{ 'a string' | upcase }}\n{% increment test %}", profile: true) t.render! timing_count = 0 t.profiler.each do |_timing| timing_count += 1 end assert_equal(2, timing_count) end def test_profiling_marks_children_of_if_blocks t = Template.parse("{% if true %} {% increment test %} {{ test }} {% endif %}", profile: true) t.render! assert_equal(1, t.profiler.length) assert_equal(2, t.profiler[0].children.length) end def test_profiling_marks_children_of_for_blocks t = Template.parse("{% for item in collection %} {{ item }} {% endfor %}", profile: true) t.render!("collection" => ["one", "two"]) assert_equal(1, t.profiler.length) # Will profile each invocation of the for block assert_equal(2, t.profiler[0].children.length) end def test_profiling_supports_self_time t = Template.parse("{% for item in collection %} {{ item }} {% endfor %}", profile: true) collection = [ TestDrop.new("one"), TestDrop.new("two"), ] output = t.render!("collection" => collection) assert_equal(" one two ", output) leaf = t.profiler[0].children[0] assert_operator(leaf.self_time, :>, 0.0) end def test_profiling_supports_total_time t = Template.parse("{% if true %} {{ test }} {% endif %}", profile: true) output = t.render!("test" => TestDrop.new("one")) assert_equal(" one ", output) assert_operator(t.profiler[0].total_time, :>, 0.0) end end liquid-5.4.0/test/integration/security_test.rb000066400000000000000000000051611427076730100215560ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' module SecurityFilter def add_one(input) "#{input} + 1" end end class SecurityTest < Minitest::Test include Liquid def setup @assigns = {} end def test_no_instance_eval text = %( {{ '1+1' | instance_eval }} ) expected = %( 1+1 ) assert_equal(expected, Template.parse(text).render!(@assigns)) end def test_no_existing_instance_eval text = %( {{ '1+1' | __instance_eval__ }} ) expected = %( 1+1 ) assert_equal(expected, Template.parse(text).render!(@assigns)) end def test_no_instance_eval_after_mixing_in_new_filter text = %( {{ '1+1' | instance_eval }} ) expected = %( 1+1 ) assert_equal(expected, Template.parse(text).render!(@assigns)) end def test_no_instance_eval_later_in_chain text = %( {{ '1+1' | add_one | instance_eval }} ) expected = %( 1+1 + 1 ) assert_equal(expected, Template.parse(text).render!(@assigns, filters: SecurityFilter)) end def test_does_not_permanently_add_filters_to_symbol_table current_symbols = Symbol.all_symbols # MRI imprecisely marks objects found on the C stack, which can result # in uninitialized memory being marked. This can even result in the test failing # deterministically for a given compilation of ruby. Using a separate thread will # keep these writes of the symbol pointer on a separate stack that will be garbage # collected after Thread#join. Thread.new do test = %( {{ "some_string" | a_bad_filter }} ) Template.parse(test).render! nil end.join GC.start assert_equal([], (Symbol.all_symbols - current_symbols)) end def test_does_not_add_drop_methods_to_symbol_table current_symbols = Symbol.all_symbols assigns = { 'drop' => Drop.new } assert_equal("", Template.parse("{{ drop.custom_method_1 }}", assigns).render!) assert_equal("", Template.parse("{{ drop.custom_method_2 }}", assigns).render!) assert_equal("", Template.parse("{{ drop.custom_method_3 }}", assigns).render!) assert_equal([], (Symbol.all_symbols - current_symbols)) end def test_max_depth_nested_blocks_does_not_raise_exception depth = Liquid::Block::MAX_DEPTH code = "{% if true %}" * depth + "rendered" + "{% endif %}" * depth assert_equal("rendered", Template.parse(code).render!) end def test_more_than_max_depth_nested_blocks_raises_exception depth = Liquid::Block::MAX_DEPTH + 1 code = "{% if true %}" * depth + "rendered" + "{% endif %}" * depth assert_raises(Liquid::StackLevelError) do Template.parse(code).render! end end end # SecurityTest liquid-5.4.0/test/integration/standard_filter_test.rb000066400000000000000000000761411427076730100230620ustar00rootroot00000000000000# encoding: utf-8 # frozen_string_literal: true require 'test_helper' class TestThing attr_reader :foo def initialize @foo = 0 end def to_s "woot: #{@foo}" end def [](_whatever) to_s end def to_liquid @foo += 1 self end end class TestDrop < Liquid::Drop def initialize(value:) @value = value end attr_reader :value def registers { @value => @context.registers[@value] } end end class TestModel def initialize(value:) @value = value end def to_liquid TestDrop.new(value: @value) end end class TestEnumerable < Liquid::Drop include Enumerable def each(&block) [{ "foo" => 1, "bar" => 2 }, { "foo" => 2, "bar" => 1 }, { "foo" => 3, "bar" => 3 }].each(&block) end end class NumberLikeThing < Liquid::Drop def initialize(amount) @amount = amount end def to_number @amount end end class StandardFiltersTest < Minitest::Test Filters = Class.new(Liquid::StrainerTemplate) Filters.add_filter(Liquid::StandardFilters) include Liquid def setup @filters = Filters.new(Context.new) end def test_size assert_equal(3, @filters.size([1, 2, 3])) assert_equal(0, @filters.size([])) assert_equal(0, @filters.size(nil)) end def test_downcase assert_equal('testing', @filters.downcase("Testing")) assert_equal('', @filters.downcase(nil)) end def test_upcase assert_equal('TESTING', @filters.upcase("Testing")) assert_equal('', @filters.upcase(nil)) end def test_slice assert_equal('oob', @filters.slice('foobar', 1, 3)) assert_equal('oobar', @filters.slice('foobar', 1, 1000)) assert_equal('', @filters.slice('foobar', 1, 0)) assert_equal('o', @filters.slice('foobar', 1, 1)) assert_equal('bar', @filters.slice('foobar', 3, 3)) assert_equal('ar', @filters.slice('foobar', -2, 2)) assert_equal('ar', @filters.slice('foobar', -2, 1000)) assert_equal('r', @filters.slice('foobar', -1)) assert_equal('', @filters.slice(nil, 0)) assert_equal('', @filters.slice('foobar', 100, 10)) assert_equal('', @filters.slice('foobar', -100, 10)) assert_equal('oob', @filters.slice('foobar', '1', '3')) assert_raises(Liquid::ArgumentError) do @filters.slice('foobar', nil) end assert_raises(Liquid::ArgumentError) do @filters.slice('foobar', 0, "") end end def test_slice_on_arrays input = 'foobar'.split(//) assert_equal(%w(o o b), @filters.slice(input, 1, 3)) assert_equal(%w(o o b a r), @filters.slice(input, 1, 1000)) assert_equal(%w(), @filters.slice(input, 1, 0)) assert_equal(%w(o), @filters.slice(input, 1, 1)) assert_equal(%w(b a r), @filters.slice(input, 3, 3)) assert_equal(%w(a r), @filters.slice(input, -2, 2)) assert_equal(%w(a r), @filters.slice(input, -2, 1000)) assert_equal(%w(r), @filters.slice(input, -1)) assert_equal(%w(), @filters.slice(input, 100, 10)) assert_equal(%w(), @filters.slice(input, -100, 10)) end def test_truncate assert_equal('1234...', @filters.truncate('1234567890', 7)) assert_equal('1234567890', @filters.truncate('1234567890', 20)) assert_equal('...', @filters.truncate('1234567890', 0)) assert_equal('1234567890', @filters.truncate('1234567890')) assert_equal("测试...", @filters.truncate("测试测试测试测试", 5)) assert_equal('12341', @filters.truncate("1234567890", 5, 1)) end def test_split assert_equal(['12', '34'], @filters.split('12~34', '~')) assert_equal(['A? ', ' ,Z'], @filters.split('A? ~ ~ ~ ,Z', '~ ~ ~')) assert_equal(['A?Z'], @filters.split('A?Z', '~')) assert_equal([], @filters.split(nil, ' ')) assert_equal(['A', 'Z'], @filters.split('A1Z', 1)) end def test_escape assert_equal('<strong>', @filters.escape('')) assert_equal('1', @filters.escape(1)) assert_equal('2001-02-03', @filters.escape(Date.new(2001, 2, 3))) assert_nil(@filters.escape(nil)) end def test_h assert_equal('<strong>', @filters.h('')) assert_equal('1', @filters.h(1)) assert_equal('2001-02-03', @filters.h(Date.new(2001, 2, 3))) assert_nil(@filters.h(nil)) end def test_escape_once assert_equal('<strong>Hulk</strong>', @filters.escape_once('<strong>Hulk')) end def test_base64_encode assert_equal('b25lIHR3byB0aHJlZQ==', @filters.base64_encode('one two three')) assert_equal('', @filters.base64_encode(nil)) end def test_base64_decode assert_equal('one two three', @filters.base64_decode('b25lIHR3byB0aHJlZQ==')) exception = assert_raises(Liquid::ArgumentError) do @filters.base64_decode("invalidbase64") end assert_equal('Liquid error: invalid base64 provided to base64_decode', exception.message) end def test_base64_url_safe_encode assert_equal( 'YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXogQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVogMTIzNDU2Nzg5MCAhQCMkJV4mKigpLT1fKy8_Ljo7W117fVx8', @filters.base64_url_safe_encode('abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890 !@#$%^&*()-=_+/?.:;[]{}\|') ) assert_equal('', @filters.base64_url_safe_encode(nil)) end def test_base64_url_safe_decode assert_equal( 'abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890 !@#$%^&*()-=_+/?.:;[]{}\|', @filters.base64_url_safe_decode('YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXogQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVogMTIzNDU2Nzg5MCAhQCMkJV4mKigpLT1fKy8_Ljo7W117fVx8') ) exception = assert_raises(Liquid::ArgumentError) do @filters.base64_url_safe_decode("invalidbase64") end assert_equal('Liquid error: invalid base64 provided to base64_url_safe_decode', exception.message) end def test_url_encode assert_equal('foo%2B1%40example.com', @filters.url_encode('foo+1@example.com')) assert_equal('1', @filters.url_encode(1)) assert_equal('2001-02-03', @filters.url_encode(Date.new(2001, 2, 3))) assert_nil(@filters.url_encode(nil)) end def test_url_decode assert_equal('foo bar', @filters.url_decode('foo+bar')) assert_equal('foo bar', @filters.url_decode('foo%20bar')) assert_equal('foo+1@example.com', @filters.url_decode('foo%2B1%40example.com')) assert_equal('1', @filters.url_decode(1)) assert_equal('2001-02-03', @filters.url_decode(Date.new(2001, 2, 3))) assert_nil(@filters.url_decode(nil)) exception = assert_raises(Liquid::ArgumentError) do @filters.url_decode('%ff') end assert_equal('Liquid error: invalid byte sequence in UTF-8', exception.message) end def test_truncatewords assert_equal('one two three', @filters.truncatewords('one two three', 4)) assert_equal('one two...', @filters.truncatewords('one two three', 2)) assert_equal('one two three', @filters.truncatewords('one two three')) assert_equal( 'Two small (13” x 5.5” x 10” high) baskets fit inside one large basket (13”...', @filters.truncatewords('Two small (13” x 5.5” x 10” high) baskets fit inside one large basket (13” x 16” x 10.5” high) with cover.', 15) ) assert_equal("测试测试测试测试", @filters.truncatewords('测试测试测试测试', 5)) assert_equal('one two1', @filters.truncatewords("one two three", 2, 1)) assert_equal('one two three...', @filters.truncatewords("one two\tthree\nfour", 3)) assert_equal('one two...', @filters.truncatewords("one two three four", 2)) assert_equal('one...', @filters.truncatewords("one two three four", 0)) exception = assert_raises(Liquid::ArgumentError) do @filters.truncatewords("one two three four", 1 << 31) end assert_equal("Liquid error: integer #{1 << 31} too big for truncatewords", exception.message) end def test_strip_html assert_equal('test', @filters.strip_html("
test
")) assert_equal('test', @filters.strip_html("
test
")) assert_equal('', @filters.strip_html("")) assert_equal('', @filters.strip_html("")) assert_equal('test', @filters.strip_html("test")) assert_equal('test', @filters.strip_html("test")) assert_equal('', @filters.strip_html(nil)) # Quirk of the existing implementation assert_equal('foo;', @filters.strip_html("<<")) end def test_join assert_equal('1 2 3 4', @filters.join([1, 2, 3, 4])) assert_equal('1 - 2 - 3 - 4', @filters.join([1, 2, 3, 4], ' - ')) assert_equal('1121314', @filters.join([1, 2, 3, 4], 1)) end def test_sort assert_equal([1, 2, 3, 4], @filters.sort([4, 3, 2, 1])) assert_equal([{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }], @filters.sort([{ "a" => 4 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a")) end def test_sort_with_nils assert_equal([1, 2, 3, 4, nil], @filters.sort([nil, 4, 3, 2, 1])) assert_equal([{ "a" => 1 }, { "a" => 2 }, { "a" => 3 }, { "a" => 4 }, {}], @filters.sort([{ "a" => 4 }, { "a" => 3 }, {}, { "a" => 1 }, { "a" => 2 }], "a")) end def test_sort_when_property_is_sometimes_missing_puts_nils_last input = [ { "price" => 4, "handle" => "alpha" }, { "handle" => "beta" }, { "price" => 1, "handle" => "gamma" }, { "handle" => "delta" }, { "price" => 2, "handle" => "epsilon" }, ] expectation = [ { "price" => 1, "handle" => "gamma" }, { "price" => 2, "handle" => "epsilon" }, { "price" => 4, "handle" => "alpha" }, { "handle" => "beta" }, { "handle" => "delta" }, ] assert_equal(expectation, @filters.sort(input, "price")) end def test_sort_natural assert_equal(["a", "B", "c", "D"], @filters.sort_natural(["c", "D", "a", "B"])) assert_equal([{ "a" => "a" }, { "a" => "B" }, { "a" => "c" }, { "a" => "D" }], @filters.sort_natural([{ "a" => "D" }, { "a" => "c" }, { "a" => "a" }, { "a" => "B" }], "a")) end def test_sort_natural_with_nils assert_equal(["a", "B", "c", "D", nil], @filters.sort_natural([nil, "c", "D", "a", "B"])) assert_equal([{ "a" => "a" }, { "a" => "B" }, { "a" => "c" }, { "a" => "D" }, {}], @filters.sort_natural([{ "a" => "D" }, { "a" => "c" }, {}, { "a" => "a" }, { "a" => "B" }], "a")) end def test_sort_natural_when_property_is_sometimes_missing_puts_nils_last input = [ { "price" => "4", "handle" => "alpha" }, { "handle" => "beta" }, { "price" => "1", "handle" => "gamma" }, { "handle" => "delta" }, { "price" => 2, "handle" => "epsilon" }, ] expectation = [ { "price" => "1", "handle" => "gamma" }, { "price" => 2, "handle" => "epsilon" }, { "price" => "4", "handle" => "alpha" }, { "handle" => "delta" }, { "handle" => "beta" }, ] assert_equal(expectation, @filters.sort_natural(input, "price")) end def test_sort_natural_case_check input = [ { "key" => "X" }, { "key" => "Y" }, { "key" => "Z" }, { "fake" => "t" }, { "key" => "a" }, { "key" => "b" }, { "key" => "c" }, ] expectation = [ { "key" => "a" }, { "key" => "b" }, { "key" => "c" }, { "key" => "X" }, { "key" => "Y" }, { "key" => "Z" }, { "fake" => "t" }, ] assert_equal(expectation, @filters.sort_natural(input, "key")) assert_equal(["a", "b", "c", "X", "Y", "Z"], @filters.sort_natural(["X", "Y", "Z", "a", "b", "c"])) end def test_sort_empty_array assert_equal([], @filters.sort([], "a")) end def test_sort_invalid_property foo = [ [1], [2], [3], ] assert_raises(Liquid::ArgumentError) do @filters.sort(foo, "bar") end end def test_sort_natural_empty_array assert_equal([], @filters.sort_natural([], "a")) end def test_sort_natural_invalid_property foo = [ [1], [2], [3], ] assert_raises(Liquid::ArgumentError) do @filters.sort_natural(foo, "bar") end end def test_legacy_sort_hash assert_equal([{ a: 1, b: 2 }], @filters.sort(a: 1, b: 2)) end def test_numerical_vs_lexicographical_sort assert_equal([2, 10], @filters.sort([10, 2])) assert_equal([{ "a" => 2 }, { "a" => 10 }], @filters.sort([{ "a" => 10 }, { "a" => 2 }], "a")) assert_equal(["10", "2"], @filters.sort(["10", "2"])) assert_equal([{ "a" => "10" }, { "a" => "2" }], @filters.sort([{ "a" => "10" }, { "a" => "2" }], "a")) end def test_uniq assert_equal(["foo"], @filters.uniq("foo")) assert_equal([1, 3, 2, 4], @filters.uniq([1, 1, 3, 2, 3, 1, 4, 3, 2, 1])) assert_equal([{ "a" => 1 }, { "a" => 3 }, { "a" => 2 }], @filters.uniq([{ "a" => 1 }, { "a" => 3 }, { "a" => 1 }, { "a" => 2 }], "a")) test_drop = TestDrop.new(value: "test") test_drop_alternate = TestDrop.new(value: "test") assert_equal([test_drop], @filters.uniq([test_drop, test_drop_alternate], 'value')) end def test_uniq_empty_array assert_equal([], @filters.uniq([], "a")) end def test_uniq_invalid_property foo = [ [1], [2], [3], ] assert_raises(Liquid::ArgumentError) do @filters.uniq(foo, "bar") end end def test_compact_empty_array assert_equal([], @filters.compact([], "a")) end def test_compact_invalid_property foo = [ [1], [2], [3], ] assert_raises(Liquid::ArgumentError) do @filters.compact(foo, "bar") end end def test_reverse assert_equal([4, 3, 2, 1], @filters.reverse([1, 2, 3, 4])) end def test_legacy_reverse_hash assert_equal([{ a: 1, b: 2 }], @filters.reverse(a: 1, b: 2)) end def test_map 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' }}", 'ary' => [{ 'foo' => { 'bar' => 'a' } }, { 'foo' => { 'bar' => 'b' } }, { 'foo' => { 'bar' => 'c' } }]) end def test_map_doesnt_call_arbitrary_stuff assert_template_result("", '{{ "foo" | map: "__id__" }}') assert_template_result("", '{{ "foo" | map: "inspect" }}') end def test_map_calls_to_liquid t = TestThing.new assert_template_result("woot: 1", '{{ foo | map: "whatever" }}', "foo" => [t]) end def test_map_calls_context= model = TestModel.new(value: :test) template = Template.parse('{{ foo | map: "registers" }}') template.registers[:test] = 1234 template.assigns['foo'] = [model] assert_template_result("{:test=>1234}", template.render!) end def test_map_on_hashes assert_template_result("4217", '{{ thing | map: "foo" | map: "bar" }}', "thing" => { "foo" => [{ "bar" => 42 }, { "bar" => 17 }] }) end def test_legacy_map_on_hashes_with_dynamic_key template = "{% assign key = 'foo' %}{{ thing | map: key | map: 'bar' }}" hash = { "foo" => { "bar" => 42 } } assert_template_result("42", template, "thing" => hash) end def test_sort_calls_to_liquid t = TestThing.new Liquid::Template.parse('{{ foo | sort: "whatever" }}').render("foo" => [t]) assert(t.foo > 0) end def test_map_over_proc drop = TestDrop.new(value: "testfoo") p = proc { drop } templ = '{{ procs | map: "value" }}' assert_template_result("testfoo", templ, "procs" => [p]) end def test_map_over_drops_returning_procs drops = [ { "proc" => -> { "foo" }, }, { "proc" => -> { "bar" }, }, ] templ = '{{ drops | map: "proc" }}' assert_template_result("foobar", templ, "drops" => drops) end def test_map_works_on_enumerables assert_template_result("123", '{{ foo | map: "foo" }}', "foo" => TestEnumerable.new) end def test_map_returns_empty_on_2d_input_array foo = [ [1], [2], [3], ] assert_raises(Liquid::ArgumentError) do @filters.map(foo, "bar") end end def test_map_returns_empty_with_no_property foo = [ [1], [2], [3], ] assert_raises(Liquid::ArgumentError) do @filters.map(foo, nil) end end def test_sort_works_on_enumerables assert_template_result("213", '{{ foo | sort: "bar" | map: "foo" }}', "foo" => TestEnumerable.new) end def test_first_and_last_call_to_liquid assert_template_result('foobar', '{{ foo | first }}', 'foo' => [ThingWithToLiquid.new]) assert_template_result('foobar', '{{ foo | last }}', 'foo' => [ThingWithToLiquid.new]) end def test_truncate_calls_to_liquid assert_template_result("wo...", '{{ foo | truncate: 5 }}', "foo" => TestThing.new) end def test_date assert_equal('May', @filters.date(Time.parse("2006-05-05 10:00:00"), "%B")) assert_equal('June', @filters.date(Time.parse("2006-06-05 10:00:00"), "%B")) assert_equal('July', @filters.date(Time.parse("2006-07-05 10:00:00"), "%B")) assert_equal('May', @filters.date("2006-05-05 10:00:00", "%B")) assert_equal('June', @filters.date("2006-06-05 10:00:00", "%B")) assert_equal('July', @filters.date("2006-07-05 10:00:00", "%B")) assert_equal('2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", "")) assert_equal('2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", "")) assert_equal('2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", "")) assert_equal('2006-07-05 10:00:00', @filters.date("2006-07-05 10:00:00", nil)) assert_equal('07/05/2006', @filters.date("2006-07-05 10:00:00", "%m/%d/%Y")) assert_equal("07/16/2004", @filters.date("Fri Jul 16 01:00:00 2004", "%m/%d/%Y")) assert_equal(Date.today.year.to_s, @filters.date('now', '%Y')) assert_equal(Date.today.year.to_s, @filters.date('today', '%Y')) assert_equal(Date.today.year.to_s, @filters.date('Today', '%Y')) assert_nil(@filters.date(nil, "%B")) assert_equal('', @filters.date('', "%B")) with_timezone("UTC") do assert_equal("07/05/2006", @filters.date(1152098955, "%m/%d/%Y")) assert_equal("07/05/2006", @filters.date("1152098955", "%m/%d/%Y")) end end def test_first_last assert_equal(1, @filters.first([1, 2, 3])) assert_equal(3, @filters.last([1, 2, 3])) assert_nil(@filters.first([])) assert_nil(@filters.last([])) end def test_replace 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('1 1 1 1', @filters.replace('1 1 1 1', 2, 3)) assert_template_result('2 2 2 2', "{{ '1 1 1 1' | replace: '1', 2 }}") assert_equal('b a a a', @filters.replace_first('a a a a', 'a', 'b')) assert_equal('2 1 1 1', @filters.replace_first('1 1 1 1', 1, 2)) assert_equal('1 1 1 1', @filters.replace_first('1 1 1 1', 2, 3)) assert_template_result('2 1 1 1', "{{ '1 1 1 1' | replace_first: '1', 2 }}") assert_equal('a a a b', @filters.replace_last('a a a a', 'a', 'b')) assert_equal('1 1 1 2', @filters.replace_last('1 1 1 1', 1, 2)) assert_equal('1 1 1 1', @filters.replace_last('1 1 1 1', 2, 3)) assert_template_result('1 1 1 2', "{{ '1 1 1 1' | replace_last: '1', 2 }}") end def test_remove assert_equal(' ', @filters.remove("a a a a", 'a')) assert_template_result(' ', "{{ '1 1 1 1' | remove: 1 }}") assert_equal('b a a', @filters.remove_first("a b a a", 'a ')) assert_template_result(' 1 1 1', "{{ '1 1 1 1' | remove_first: 1 }}") assert_equal('a a b', @filters.remove_last("a a b a", ' a')) assert_template_result('1 1 1 ', "{{ '1 1 1 1' | remove_last: 1 }}") end def test_pipes_in_string_arguments assert_template_result('foobar', "{{ 'foo|bar' | remove: '|' }}") end def test_strip assert_template_result('ab c', "{{ source | strip }}", 'source' => " ab c ") assert_template_result('ab c', "{{ source | strip }}", 'source' => " \tab c \n \t") end def test_lstrip assert_template_result('ab c ', "{{ source | lstrip }}", 'source' => " ab c ") assert_template_result("ab c \n \t", "{{ source | lstrip }}", 'source' => " \tab c \n \t") end def test_rstrip assert_template_result(" ab c", "{{ source | rstrip }}", 'source' => " ab c ") assert_template_result(" \tab c", "{{ source | rstrip }}", 'source' => " \tab c \n \t") end def test_strip_newlines assert_template_result('abc', "{{ source | strip_newlines }}", 'source' => "a\nb\nc") assert_template_result('abc', "{{ source | strip_newlines }}", 'source' => "a\r\nb\nc") end def test_newlines_to_br assert_template_result("a
\nb
\nc", "{{ source | newline_to_br }}", 'source' => "a\nb\nc") assert_template_result("a
\nb
\nc", "{{ source | newline_to_br }}", 'source' => "a\r\nb\nc") end def test_plus assert_template_result("2", "{{ 1 | plus:1 }}") assert_template_result("2.0", "{{ '1' | plus:'1.0' }}") assert_template_result("5", "{{ price | plus:'2' }}", 'price' => NumberLikeThing.new(3)) end def test_minus assert_template_result("4", "{{ input | minus:operand }}", 'input' => 5, 'operand' => 1) assert_template_result("2.3", "{{ '4.3' | minus:'2' }}") assert_template_result("5", "{{ price | minus:'2' }}", 'price' => NumberLikeThing.new(7)) end def test_abs assert_template_result("17", "{{ 17 | abs }}") assert_template_result("17", "{{ -17 | abs }}") assert_template_result("17", "{{ '17' | abs }}") assert_template_result("17", "{{ '-17' | abs }}") assert_template_result("0", "{{ 0 | abs }}") assert_template_result("0", "{{ '0' | abs }}") assert_template_result("17.42", "{{ 17.42 | abs }}") assert_template_result("17.42", "{{ -17.42 | abs }}") assert_template_result("17.42", "{{ '17.42' | abs }}") assert_template_result("17.42", "{{ '-17.42' | abs }}") end def test_times assert_template_result("12", "{{ 3 | times:4 }}") assert_template_result("0", "{{ 'foo' | times:4 }}") assert_template_result("6", "{{ '2.1' | times:3 | replace: '.','-' | plus:0}}") assert_template_result("7.25", "{{ 0.0725 | times:100 }}") assert_template_result("-7.25", '{{ "-0.0725" | times:100 }}') assert_template_result("7.25", '{{ "-0.0725" | times: -100 }}') assert_template_result("4", "{{ price | times:2 }}", 'price' => NumberLikeThing.new(2)) end def test_divided_by assert_template_result("4", "{{ 12 | divided_by:3 }}") assert_template_result("4", "{{ 14 | divided_by:3 }}") assert_template_result("5", "{{ 15 | divided_by:3 }}") assert_equal("Liquid error: divided by 0", Template.parse("{{ 5 | divided_by:0 }}").render) assert_template_result("0.5", "{{ 2.0 | divided_by:4 }}") assert_raises(Liquid::ZeroDivisionError) do assert_template_result("4", "{{ 1 | modulo: 0 }}") end assert_template_result("5", "{{ price | divided_by:2 }}", 'price' => NumberLikeThing.new(10)) end def test_modulo assert_template_result("1", "{{ 3 | modulo:2 }}") assert_raises(Liquid::ZeroDivisionError) do assert_template_result("4", "{{ 1 | modulo: 0 }}") end assert_template_result("1", "{{ price | modulo:2 }}", 'price' => NumberLikeThing.new(3)) end def test_round assert_template_result("5", "{{ input | round }}", 'input' => 4.6) assert_template_result("4", "{{ '4.3' | round }}") assert_template_result("4.56", "{{ input | round: 2 }}", 'input' => 4.5612) assert_raises(Liquid::FloatDomainError) do assert_template_result("4", "{{ 1.0 | divided_by: 0.0 | round }}") end assert_template_result("5", "{{ price | round }}", 'price' => NumberLikeThing.new(4.6)) assert_template_result("4", "{{ price | round }}", 'price' => NumberLikeThing.new(4.3)) end def test_ceil assert_template_result("5", "{{ input | ceil }}", 'input' => 4.6) assert_template_result("5", "{{ '4.3' | ceil }}") assert_raises(Liquid::FloatDomainError) do assert_template_result("4", "{{ 1.0 | divided_by: 0.0 | ceil }}") end assert_template_result("5", "{{ price | ceil }}", 'price' => NumberLikeThing.new(4.6)) end def test_floor assert_template_result("4", "{{ input | floor }}", 'input' => 4.6) assert_template_result("4", "{{ '4.3' | floor }}") assert_raises(Liquid::FloatDomainError) do assert_template_result("4", "{{ 1.0 | divided_by: 0.0 | floor }}") end assert_template_result("5", "{{ price | floor }}", 'price' => NumberLikeThing.new(5.4)) end def test_at_most assert_template_result("4", "{{ 5 | at_most:4 }}") assert_template_result("5", "{{ 5 | at_most:5 }}") assert_template_result("5", "{{ 5 | at_most:6 }}") 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("4", "{{ width | at_most:5 }}", 'width' => NumberLikeThing.new(4)) assert_template_result("4", "{{ 5 | at_most: width }}", 'width' => NumberLikeThing.new(4)) end def test_at_least assert_template_result("5", "{{ 5 | at_least:4 }}") assert_template_result("5", "{{ 5 | at_least:5 }}") assert_template_result("6", "{{ 5 | at_least:6 }}") assert_template_result("5", "{{ 4.5 | at_least:5 }}") 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("6", "{{ 5 | at_least: width }}", 'width' => NumberLikeThing.new(6)) end def test_append assigns = { 'a' => 'bc', 'b' => 'd' } assert_template_result('bcd', "{{ a | append: 'd'}}", assigns) assert_template_result('bcd', "{{ a | append: b}}", assigns) end def test_concat assert_equal([1, 2, 3, 4], @filters.concat([1, 2], [3, 4])) assert_equal([1, 2, 'a'], @filters.concat([1, 2], ['a'])) assert_equal([1, 2, 10], @filters.concat([1, 2], [10])) assert_raises(Liquid::ArgumentError, "concat filter requires an array argument") do @filters.concat([1, 2], 10) end end def test_prepend assigns = { 'a' => 'bc', 'b' => 'a' } assert_template_result('abc', "{{ a | prepend: 'a'}}", assigns) assert_template_result('abc', "{{ a | prepend: b}}", assigns) end def test_default assert_equal("foo", @filters.default("foo", "bar")) assert_equal("bar", @filters.default(nil, "bar")) assert_equal("bar", @filters.default("", "bar")) assert_equal("bar", @filters.default(false, "bar")) assert_equal("bar", @filters.default([], "bar")) assert_equal("bar", @filters.default({}, "bar")) assert_template_result('bar', "{{ false | default: 'bar' }}") assert_template_result('bar', "{{ drop | default: 'bar' }}", 'drop' => BooleanDrop.new(false)) assert_template_result('Yay', "{{ drop | default: 'bar' }}", 'drop' => BooleanDrop.new(true)) end def test_default_handle_false assert_equal("foo", @filters.default("foo", "bar", "allow_false" => true)) assert_equal("bar", @filters.default(nil, "bar", "allow_false" => true)) assert_equal("bar", @filters.default("", "bar", "allow_false" => true)) assert_equal(false, @filters.default(false, "bar", "allow_false" => true)) assert_equal("bar", @filters.default([], "bar", "allow_false" => true)) assert_equal("bar", @filters.default({}, "bar", "allow_false" => true)) assert_template_result('false', "{{ false | default: 'bar', allow_false: true }}") assert_template_result('Nay', "{{ drop | default: 'bar', allow_false: true }}", 'drop' => BooleanDrop.new(false)) assert_template_result('Yay', "{{ drop | default: 'bar', allow_false: true }}", 'drop' => BooleanDrop.new(true)) end def test_cannot_access_private_methods assert_template_result('a', "{{ 'a' | to_number }}") end def test_date_raises_nothing assert_template_result('', "{{ '' | date: '%D' }}") assert_template_result('abc', "{{ 'abc' | date: '%D' }}") end def test_where input = [ { "handle" => "alpha", "ok" => true }, { "handle" => "beta", "ok" => false }, { "handle" => "gamma", "ok" => false }, { "handle" => "delta", "ok" => true }, ] expectation = [ { "handle" => "alpha", "ok" => true }, { "handle" => "delta", "ok" => true }, ] assert_equal(expectation, @filters.where(input, "ok", true)) assert_equal(expectation, @filters.where(input, "ok")) end def test_where_string_keys input = [ "alpha", "beta", "gamma", "delta" ] expectation = [ "beta", ] assert_equal(expectation, @filters.where(input, "be")) end def test_where_no_key_set input = [ { "handle" => "alpha", "ok" => true }, { "handle" => "beta" }, { "handle" => "gamma" }, { "handle" => "delta", "ok" => true }, ] expectation = [ { "handle" => "alpha", "ok" => true }, { "handle" => "delta", "ok" => true }, ] assert_equal(expectation, @filters.where(input, "ok", true)) assert_equal(expectation, @filters.where(input, "ok")) end def test_where_non_array_map_input assert_equal([{ "a" => "ok" }], @filters.where({ "a" => "ok" }, "a", "ok")) assert_equal([], @filters.where({ "a" => "not ok" }, "a", "ok")) end def test_where_indexable_but_non_map_value assert_raises(Liquid::ArgumentError) { @filters.where(1, "ok", true) } assert_raises(Liquid::ArgumentError) { @filters.where(1, "ok") } end def test_where_non_boolean_value input = [ { "message" => "Bonjour!", "language" => "French" }, { "message" => "Hello!", "language" => "English" }, { "message" => "Hallo!", "language" => "German" }, ] assert_equal([{ "message" => "Bonjour!", "language" => "French" }], @filters.where(input, "language", "French")) assert_equal([{ "message" => "Hallo!", "language" => "German" }], @filters.where(input, "language", "German")) assert_equal([{ "message" => "Hello!", "language" => "English" }], @filters.where(input, "language", "English")) end def test_where_array_of_only_unindexable_values assert_nil(@filters.where([nil], "ok", true)) assert_nil(@filters.where([nil], "ok")) end def test_all_filters_never_raise_non_liquid_exception test_drop = TestDrop.new(value: "test") test_drop.context = Context.new test_enum = TestEnumerable.new test_enum.context = Context.new test_types = [ "foo", 123, 0, 0.0, -1234.003030303, -99999999, 1234.38383000383830003838300, nil, true, false, TestThing.new, test_drop, test_enum, ["foo", "bar"], { "foo" => "bar" }, { foo: "bar" }, [{ "foo" => "bar" }, { "foo" => 123 }, { "foo" => nil }, { "foo" => true }, { "foo" => ["foo", "bar"] }], { 1 => "bar" }, ["foo", 123, nil, true, false, Drop, ["foo"], { foo: "bar" }], ] StandardFilters.public_instance_methods(false).each do |method| arg_count = @filters.method(method).arity arg_count *= -1 if arg_count < 0 test_types.repeated_permutation(arg_count) do |args| @filters.send(method, *args) rescue Liquid::Error nil end end end def test_where_no_target_value input = [ { "foo" => false }, { "foo" => true }, { "foo" => "for sure" }, { "bar" => true }, ] assert_equal([{ "foo" => true }, { "foo" => "for sure" }], @filters.where(input, "foo")) end private def with_timezone(tz) old_tz = ENV['TZ'] ENV['TZ'] = tz yield ensure ENV['TZ'] = old_tz end end # StandardFiltersTest liquid-5.4.0/test/integration/tag/000077500000000000000000000000001427076730100170735ustar00rootroot00000000000000liquid-5.4.0/test/integration/tag/disableable_test.rb000066400000000000000000000026161427076730100227130ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TagDisableableTest < Minitest::Test include Liquid module RenderTagName def render(_context) tag_name end end class Custom < Tag prepend Liquid::Tag::Disableable include RenderTagName end class Custom2 < Tag prepend Liquid::Tag::Disableable include RenderTagName end class DisableCustom < Block disable_tags "custom" end class DisableBoth < Block disable_tags "custom", "custom2" end def test_block_tag_disabling_nested_tag with_disableable_tags do with_custom_tag('disable', DisableCustom) do output = Template.parse('{% disable %}{% custom %};{% custom2 %}{% enddisable %}').render assert_equal('Liquid error: custom usage is not allowed in this context;custom2', output) end end end def test_block_tag_disabling_multiple_nested_tags with_disableable_tags do with_custom_tag('disable', DisableBoth) do output = Template.parse('{% disable %}{% custom %};{% custom2 %}{% enddisable %}').render assert_equal('Liquid error: custom usage is not allowed in this context;Liquid error: custom2 usage is not allowed in this context', output) end end end private def with_disableable_tags with_custom_tag('custom', Custom) do with_custom_tag('custom2', Custom2) do yield end end end end liquid-5.4.0/test/integration/tag_test.rb000066400000000000000000000021121427076730100204530ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TagTest < Minitest::Test include Liquid def test_custom_tags_have_a_default_render_to_output_buffer_method_for_backwards_compatibility klass1 = Class.new(Tag) do def render(*) 'hello' end end with_custom_tag('blabla', klass1) do template = Liquid::Template.parse("{% blabla %}") assert_equal('hello', template.render) buf = +'' output = template.render({}, output: buf) assert_equal('hello', output) assert_equal('hello', buf) assert_equal(buf.object_id, output.object_id) end klass2 = Class.new(klass1) do def render(*) 'foo' + super + 'bar' end end with_custom_tag('blabla', klass2) do template = Liquid::Template.parse("{% blabla %}") assert_equal('foohellobar', template.render) buf = +'' output = template.render({}, output: buf) assert_equal('foohellobar', output) assert_equal('foohellobar', buf) assert_equal(buf.object_id, output.object_id) end end end liquid-5.4.0/test/integration/tags/000077500000000000000000000000001427076730100172565ustar00rootroot00000000000000liquid-5.4.0/test/integration/tags/break_tag_test.rb000066400000000000000000000005531427076730100225640ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class BreakTagTest < Minitest::Test include Liquid # tests that no weird errors are raised if break is called outside of a # block def test_break_with_no_block assigns = { 'i' => 1 } markup = '{% break %}' expected = '' assert_template_result(expected, markup, assigns) end end liquid-5.4.0/test/integration/tags/continue_tag_test.rb000066400000000000000000000005551427076730100233260ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class ContinueTagTest < Minitest::Test include Liquid # tests that no weird errors are raised if continue is called outside of a # block def test_continue_with_no_block assigns = {} markup = '{% continue %}' expected = '' assert_template_result(expected, markup, assigns) end end liquid-5.4.0/test/integration/tags/echo_test.rb000066400000000000000000000004201427076730100215540ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class EchoTest < Minitest::Test include Liquid def test_echo_outputs_its_input assert_template_result('BAR', <<~LIQUID, 'variable-name' => 'bar') {%- echo variable-name | upcase -%} LIQUID end end liquid-5.4.0/test/integration/tags/for_tag_test.rb000066400000000000000000000376361427076730100223020ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class ThingWithValue < Liquid::Drop def value 3 end end class ForTagTest < Minitest::Test include Liquid def test_for 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(' yo ', '{%for item in array%} yo {%endfor%}', 'array' => [1]) assert_template_result('', '{%for item in array%}{%endfor%}', 'array' => [1, 2]) expected = < [1, 2, 3]) end def test_for_reversed assigns = { 'array' => [1, 2, 3] } assert_template_result('321', '{%for item in array reversed %}{{item}}{%endfor%}', assigns) end def test_for_with_range assert_template_result(' 1 2 3 ', '{%for item in (1..3) %} {{item}} {%endfor%}') assert_raises(Liquid::ArgumentError) do Template.parse('{% for i in (a..2) %}{% endfor %}').render!("a" => [1, 2]) end assert_template_result(' 0 1 2 3 ', '{% for item in (a..3) %} {{item}} {% endfor %}', "a" => "invalid integer") end def test_for_with_variable_range assert_template_result(' 1 2 3 ', '{%for item in (1..foobar) %} {{item}} {%endfor%}', "foobar" => 3) end def test_for_with_hash_value_range foobar = { "value" => 3 } assert_template_result(' 1 2 3 ', '{%for item in (1..foobar.value) %} {{item}} {%endfor%}', "foobar" => foobar) end def test_for_with_drop_value_range foobar = ThingWithValue.new assert_template_result(' 1 2 3 ', '{%for item in (1..foobar.value) %} {{item}} {%endfor%}', "foobar" => foobar) end def test_for_with_variable 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('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('abc', '{%for item in array%}{{item}}{%endfor%}', 'array' => ['a', '', 'b', '', 'c']) end def test_for_helpers assigns = { 'array' => [1, 2, 3] } assert_template_result(' 1/3 2/3 3/3 ', '{%for item in array%} {{forloop.index}}/{{forloop.length}} {%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(' 2 1 0 ', '{%for item in array%} {{forloop.rindex0}} {%endfor%}', assigns) assert_template_result(' 3 2 1 ', '{%for item in array%} {{forloop.rindex}} {%endfor%}', assigns) assert_template_result(' true false false ', '{%for item in array%} {{forloop.first}} {%endfor%}', assigns) assert_template_result(' false false true ', '{%for item in array%} {{forloop.last}} {%endfor%}', assigns) end def test_for_and_if assigns = { 'array' => [1, 2, 3] } assert_template_result('+--', '{%for item in array%}{% if forloop.first %}+{% else %}-{% endif %}{%endfor%}', assigns) end 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' => []) assert_template_result('-', '{%for item in array%}+{%else%}-{%endfor%}', 'array' => nil) end def test_limiting assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } assert_template_result('12', '{%for i in array limit:2 %}{{ 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) end def test_limiting_with_invalid_limit assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } template = <<-MKUP {% for i in array limit: true offset: 1 %} {{ i }} {% endfor %} MKUP exception = assert_raises(Liquid::ArgumentError) do Template.parse(template).render!(assigns) end assert_equal("Liquid error: invalid integer", exception.message) end def test_limiting_with_invalid_offset assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } template = <<-MKUP {% for i in array limit: 1 offset: true %} {{ i }} {% endfor %} MKUP exception = assert_raises(Liquid::ArgumentError) do Template.parse(template).render!(assigns) end assert_equal("Liquid error: invalid integer", exception.message) end def test_dynamic_variable_limiting assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } assigns['limit'] = 2 assigns['offset'] = 2 assert_template_result('34', '{%for i in array limit: limit offset: offset %}{{ i }}{%endfor%}', assigns) end def test_nested_for assigns = { 'array' => [[1, 2], [3, 4], [5, 6]] } assert_template_result('123456', '{%for item in array%}{%for i in item%}{{ i }}{%endfor%}{%endfor%}', assigns) end def test_offset_only assigns = { 'array' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } assert_template_result('890', '{%for i in array offset:7 %}{{ i }}{%endfor%}', assigns) end def test_pause_resume assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } } markup = <<-MKUP {%for i in array.items limit: 3 %}{{i}}{%endfor%} next {%for i in array.items offset:continue limit: 3 %}{{i}}{%endfor%} next {%for i in array.items offset:continue limit: 3 %}{{i}}{%endfor%} MKUP expected = <<-XPCTD 123 next 456 next 789 XPCTD assert_template_result(expected, markup, assigns) end def test_pause_resume_limit assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } } markup = <<-MKUP {%for i in array.items limit:3 %}{{i}}{%endfor%} next {%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%} next {%for i in array.items offset:continue limit:1 %}{{i}}{%endfor%} MKUP expected = <<-XPCTD 123 next 456 next 7 XPCTD assert_template_result(expected, markup, assigns) end def test_pause_resume_big_limit assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } } markup = <<-MKUP {%for i in array.items limit:3 %}{{i}}{%endfor%} next {%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%} next {%for i in array.items offset:continue limit:1000 %}{{i}}{%endfor%} MKUP expected = <<-XPCTD 123 next 456 next 7890 XPCTD assert_template_result(expected, markup, assigns) end def test_pause_resume_big_offset assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 0] } } markup = '{%for i in array.items limit:3 %}{{i}}{%endfor%} next {%for i in array.items offset:continue limit:3 %}{{i}}{%endfor%} next {%for i in array.items offset:continue limit:3 offset:1000 %}{{i}}{%endfor%}' expected = '123 next 456 next ' assert_template_result(expected, markup, assigns) end def test_for_with_break assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] } } markup = '{% for i in array.items %}{% break %}{% endfor %}' expected = "" assert_template_result(expected, markup, assigns) markup = '{% for i in array.items %}{{ i }}{% break %}{% endfor %}' expected = "1" assert_template_result(expected, markup, assigns) markup = '{% for i in array.items %}{% break %}{{ i }}{% endfor %}' expected = "" assert_template_result(expected, markup, assigns) markup = '{% for i in array.items %}{{ i }}{% if i > 3 %}{% break %}{% endif %}{% endfor %}' expected = "1234" assert_template_result(expected, markup, assigns) # tests to ensure it only breaks out of the local for loop # and not all of them. assigns = { 'array' => [[1, 2], [3, 4], [5, 6]] } markup = '{% for item in array %}' \ '{% for i in item %}' \ '{% if i == 1 %}' \ '{% break %}' \ '{% endif %}' \ '{{ i }}' \ '{% endfor %}' \ '{% endfor %}' expected = '3456' assert_template_result(expected, markup, assigns) # test break does nothing when unreached assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5] } } markup = '{% for i in array.items %}{% if i == 9999 %}{% break %}{% endif %}{{ i }}{% endfor %}' expected = '12345' assert_template_result(expected, markup, assigns) end def test_for_with_continue assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5] } } markup = '{% for i in array.items %}{% continue %}{% endfor %}' expected = "" assert_template_result(expected, markup, assigns) markup = '{% for i in array.items %}{{ i }}{% continue %}{% endfor %}' expected = "12345" assert_template_result(expected, markup, assigns) markup = '{% for i in array.items %}{% continue %}{{ i }}{% endfor %}' expected = "" assert_template_result(expected, markup, assigns) markup = '{% for i in array.items %}{% if i > 3 %}{% continue %}{% endif %}{{ i }}{% endfor %}' expected = "123" assert_template_result(expected, markup, assigns) markup = '{% for i in array.items %}{% if i == 3 %}{% continue %}{% else %}{{ i }}{% endif %}{% endfor %}' expected = "1245" assert_template_result(expected, markup, assigns) # tests to ensure it only continues the local for loop and not all of them. assigns = { 'array' => [[1, 2], [3, 4], [5, 6]] } markup = '{% for item in array %}' \ '{% for i in item %}' \ '{% if i == 1 %}' \ '{% continue %}' \ '{% endif %}' \ '{{ i }}' \ '{% endfor %}' \ '{% endfor %}' expected = '23456' assert_template_result(expected, markup, assigns) # test continue does nothing when unreached assigns = { 'array' => { 'items' => [1, 2, 3, 4, 5] } } markup = '{% for i in array.items %}{% if i == 9999 %}{% continue %}{% endif %}{{ i }}{% endfor %}' expected = '12345' assert_template_result(expected, markup, assigns) end def test_for_tag_string # ruby 1.8.7 "String".each => Enumerator with single "String" element. # ruby 1.9.3 no longer supports .each on String though we mimic # the functionality for backwards compatibility assert_template_result('test string', '{%for val in string%}{{val}}{%endfor%}', 'string' => "test string") assert_template_result('test string', '{%for val in string limit:1%}{{val}}{%endfor%}', 'string' => "test string") assert_template_result('val-string-1-1-0-1-0-true-true-test string', '{%for val in string%}' \ '{{forloop.name}}-' \ '{{forloop.index}}-' \ '{{forloop.length}}-' \ '{{forloop.index0}}-' \ '{{forloop.rindex}}-' \ '{{forloop.rindex0}}-' \ '{{forloop.first}}-' \ '{{forloop.last}}-' \ '{{val}}{%endfor%}', 'string' => "test string") end def test_for_parentloop_references_parent_loop assert_template_result('1.1 1.2 1.3 2.1 2.2 2.3 ', '{% for inner in outer %}{% for k in inner %}' \ '{{ forloop.parentloop.index }}.{{ forloop.index }} ' \ '{% endfor %}{% endfor %}', 'outer' => [[1, 1, 1], [1, 1, 1]]) end def test_for_parentloop_nil_when_not_present assert_template_result('.1 .2 ', '{% for inner in outer %}' \ '{{ forloop.parentloop.index }}.{{ forloop.index }} ' \ '{% endfor %}', 'outer' => [[1, 1, 1], [1, 1, 1]]) end def test_inner_for_over_empty_input assert_template_result('oo', '{% for a in (1..2) %}o{% for b in empty %}{% endfor %}{% endfor %}') end def test_blank_string_not_iterable assert_template_result('', "{% for char in characters %}I WILL NOT BE OUTPUT{% endfor %}", 'characters' => '') end def test_bad_variable_naming_in_for_loop assert_raises(Liquid::SyntaxError) do Liquid::Template.parse('{% for a/b in x %}{% endfor %}') end end def test_spacing_with_variable_naming_in_for_loop expected = '12345' template = '{% for item in items %}{{item}}{% endfor %}' assigns = { 'items' => [1, 2, 3, 4, 5] } assert_template_result(expected, template, assigns) end class LoaderDrop < Liquid::Drop attr_accessor :each_called, :load_slice_called def initialize(data) @data = data end def each @each_called = true @data.each { |el| yield el } end def load_slice(from, to) @load_slice_called = true @data[(from..to - 1)] end end def test_iterate_with_each_when_no_limit_applied loader = LoaderDrop.new([1, 2, 3, 4, 5]) assigns = { 'items' => loader } expected = '12345' template = '{% for item in items %}{{item}}{% endfor %}' assert_template_result(expected, template, assigns) assert(loader.each_called) assert(!loader.load_slice_called) end def test_iterate_with_load_slice_when_limit_applied loader = LoaderDrop.new([1, 2, 3, 4, 5]) assigns = { 'items' => loader } expected = '1' template = '{% for item in items limit:1 %}{{item}}{% endfor %}' assert_template_result(expected, template, assigns) assert(!loader.each_called) assert(loader.load_slice_called) end def test_iterate_with_load_slice_when_limit_and_offset_applied loader = LoaderDrop.new([1, 2, 3, 4, 5]) assigns = { 'items' => loader } expected = '34' template = '{% for item in items offset:2 limit:2 %}{{item}}{% endfor %}' assert_template_result(expected, template, assigns) assert(!loader.each_called) assert(loader.load_slice_called) end def test_iterate_with_load_slice_returns_same_results_as_without loader = LoaderDrop.new([1, 2, 3, 4, 5]) loader_assigns = { 'items' => loader } array_assigns = { 'items' => [1, 2, 3, 4, 5] } expected = '34' template = '{% for item in items offset:2 limit:2 %}{{item}}{% endfor %}' assert_template_result(expected, template, loader_assigns) assert_template_result(expected, template, array_assigns) end def test_for_cleans_up_registers context = Context.new(ErrorDrop.new) assert_raises(StandardError) do Liquid::Template.parse('{% for i in (1..2) %}{{ standard_error }}{% endfor %}').render!(context) end assert(context.registers[:for_stack].empty?) end def test_instrument_for_offset_continue assert_usage_increment('for_offset_continue') do Template.parse('{% for item in items offset:continue %}{{item}}{% endfor %}') end assert_usage_increment('for_offset_continue', times: 0) do Template.parse('{% for item in items offset:2 %}{{item}}{% endfor %}') end end def test_instrument_forloop_drop_name assigns = { 'items' => [1, 2, 3, 4, 5] } assert_usage_increment('forloop_drop_name', times: 5) do Template.parse('{% for item in items %}{{forloop.name}}{% endfor %}').render!(assigns) end assert_usage_increment('forloop_drop_name', times: 0) do Template.parse('{% for item in items %}{{forloop.index}}{% endfor %}').render!(assigns) end assert_usage_increment('forloop_drop_name', times: 0) do Template.parse('{% for item in items %}{{item}}{% endfor %}').render!(assigns) end end end liquid-5.4.0/test/integration/tags/if_else_tag_test.rb000066400000000000000000000221471427076730100231110ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class IfElseTagTest < Minitest::Test include Liquid def test_if assert_template_result(' ', ' {% if false %} this text should not go into the output {% endif %} ') assert_template_result(' 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 %}?') end def test_literal_comparisons assert_template_result(' NO ', '{% assign v = false %}{% if v %} YES {% else %} NO {% endif %}') assert_template_result(' YES ', '{% assign v = nil %}{% if v == nil %} YES {% else %} NO {% endif %}') end def test_if_else assert_template_result(' YES ', '{% if false %} NO {% else %} YES {% endif %}') assert_template_result(' YES ', '{% if true %} YES {% else %} NO {% endif %}') assert_template_result(' YES ', '{% if "foo" %} YES {% else %} NO {% endif %}') end def test_if_boolean assert_template_result(' YES ', '{% if var %} YES {% endif %}', 'var' => true) end 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' => false) 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(' 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) end 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 == false %} YES {% endif %}', 'a' => true, 'b' => true) assert_template_result('', '{% if a == false or b == false %} YES {% endif %}', 'a' => true, 'b' => true) end def test_comparison_of_strings_containing_and_or_or awful_markup = "a == 'and' and b == 'or' and c == 'foo and bar' and d == 'bar or baz' and e == 'foo' and foo and bar" assigns = { 'a' => 'and', 'b' => 'or', 'c' => 'foo and bar', 'd' => 'bar or baz', 'e' => 'foo', 'foo' => true, 'bar' => true } assert_template_result(' YES ', "{% if #{awful_markup} %} YES {% endif %}", assigns) end def test_comparison_of_expressions_starting_with_and_or_or assigns = { 'order' => { 'items_count' => 0 }, 'android' => { 'name' => 'Roy' } } assert_template_result("YES", "{% if android.name == 'Roy' %}YES{% endif %}", assigns) assert_template_result("YES", "{% if order.items_count == 0 %}YES{% endif %}", assigns) end def test_if_and 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 %}') end def test_hash_miss_generates_false assert_template_result('', '{% if foo.bar %} NO {% endif %}', 'foo' => {}) end def test_if_from_variable assert_template_result('', '{% if var %} NO {% endif %}', 'var' => false) 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' => {}) assert_template_result('', '{% if foo.bar %} NO {% endif %}', 'foo' => nil) 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' => true) 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 "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' => "text" }) 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 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 %} YES {% else %} NO {% endif %}', 'var' => true) 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 %} 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 %} 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 %}', 'notfoo' => { 'bar' => true }) end def test_nested_if assert_template_result('', '{% if false %}{% if false %} NO {% endif %}{% endif %}') assert_template_result('', '{% if false %}{% if true %} NO {% endif %}{% endif %}') assert_template_result('', '{% if true %}{% if false %} NO {% endif %}{% endif %}') assert_template_result(' YES ', '{% if true %}{% if true %} YES {% endif %}{% endif %}') assert_template_result(' YES ', '{% if true %}{% if true %} YES {% else %} NO {% endif %}{% else %} NO {% endif %}') assert_template_result(' YES ', '{% if true %}{% if false %} NO {% else %} YES {% endif %}{% else %} NO {% endif %}') assert_template_result(' YES ', '{% if false %}{% if true %} NO {% else %} NONO {% endif %}{% else %} YES {% endif %}') end def test_comparisons_on_null assert_template_result('', '{% if null < 10 %} NO {% endif %}') assert_template_result('', '{% if null <= 10 %} NO {% endif %}') assert_template_result('', '{% if null >= 10 %} NO {% endif %}') assert_template_result('', '{% if null > 10 %} NO {% endif %}') assert_template_result('', '{% if 10 < null %} NO {% endif %}') assert_template_result('', '{% if 10 <= null %} NO {% endif %}') assert_template_result('', '{% if 10 >= null %} NO {% endif %}') assert_template_result('', '{% if 10 > null %} NO {% endif %}') end def test_else_if assert_template_result('0', '{% if 0 == 0 %}0{% elsif 1 == 1%}1{% else %}2{% endif %}') assert_template_result('1', '{% if 0 != 0 %}0{% elsif 1 == 1%}1{% else %}2{% endif %}') assert_template_result('2', '{% if 0 != 0 %}0{% elsif 1 != 1%}1{% else %}2{% endif %}') assert_template_result('elsif', '{% if false %}if{% elsif true %}elsif{% endif %}') end def test_syntax_error_no_variable assert_raises(SyntaxError) { assert_template_result('', '{% if jerry == 1 %}') } end def test_syntax_error_no_expression assert_raises(SyntaxError) { assert_template_result('', '{% if %}') } end def test_if_with_custom_condition original_op = Condition.operators['contains'] Condition.operators['contains'] = :[] assert_template_result('yes', %({% if 'bob' contains 'o' %}yes{% endif %})) assert_template_result('no', %({% if 'bob' contains 'f' %}yes{% else %}no{% endif %})) ensure Condition.operators['contains'] = original_op end def test_operators_are_ignored_unless_isolated original_op = Condition.operators['contains'] Condition.operators['contains'] = :[] assert_template_result('yes', %({% if 'gnomeslab-and-or-liquid' contains 'gnomeslab-and-or-liquid' %}yes{% endif %})) ensure Condition.operators['contains'] = original_op end def test_operators_are_whitelisted assert_raises(SyntaxError) do assert_template_result('', %({% if 1 or throw or or 1 %}yes{% endif %})) end end def test_multiple_conditions tpl = "{% if a or b and c %}true{% else %}false{% endif %}" tests = { [true, true, true] => true, [true, true, false] => true, [true, false, true] => true, [true, false, false] => true, [false, true, true] => true, [false, true, false] => false, [false, false, true] => false, [false, false, false] => false, } tests.each do |vals, expected| a, b, c = vals assigns = { 'a' => a, 'b' => b, 'c' => c } assert_template_result(expected.to_s, tpl, assigns, assigns.to_s) end end end liquid-5.4.0/test/integration/tags/include_tag_test.rb000066400000000000000000000221121427076730100231160ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TestFileSystem def read_template_file(template_path) case 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 class OtherFileSystem def read_template_file(_template_path) 'from OtherFileSystem' end end class CountingFileSystem attr_reader :count def read_template_file(_template_path) @count ||= 0 @count += 1 'from CountingFileSystem' end end class CustomInclude < Liquid::Tag Syntax = /(#{Liquid::QuotedFragment}+)(\s+(?:with|for)\s+(#{Liquid::QuotedFragment}+))?/o def initialize(tag_name, markup, tokens) markup =~ Syntax @template_name = Regexp.last_match(1) super end def parse(tokens) end def render_to_output_buffer(_context, output) output << @template_name[1..-2] output end end class IncludeTagTest < Minitest::Test include Liquid def setup Liquid::Template.file_system = TestFileSystem.new end def test_include_tag_looks_for_file_system_in_registers_first assert_equal('from OtherFileSystem', Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: OtherFileSystem.new })) end def test_include_tag_with assert_template_result("Product: Draft 151cm ", "{% include 'product' with products[0] %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }]) end def test_include_tag_with_alias assert_template_result("Product: Draft 151cm ", "{% include 'product_alias' with products[0] as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }]) end def test_include_tag_for_alias assert_template_result("Product: Draft 151cm Product: Element 155cm ", "{% include 'product_alias' for products as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }]) end def test_include_tag_with_default_name assert_template_result("Product: Draft 151cm ", "{% include 'product' %}", "product" => { 'title' => 'Draft 151cm' }) end def test_include_tag_for assert_template_result("Product: Draft 151cm Product: Element 155cm ", "{% include 'product' for products %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }]) end def test_include_tag_with_local_variables assert_template_result("Locale: test123 ", "{% include 'locale_variables' echo1: 'test123' %}") end def test_include_tag_with_multiple_local_variables assert_template_result("Locale: test123 test321", "{% include 'locale_variables' echo1: 'test123', echo2: 'test321' %}") end def test_include_tag_with_multiple_local_variables_from_context assert_template_result("Locale: test123 test321", "{% include 'locale_variables' echo1: echo1, echo2: more_echos.echo2 %}", 'echo1' => 'test123', 'more_echos' => { "echo2" => 'test321' }) end def test_included_templates_assigns_variables assert_template_result("bar", "{% include 'assignments' %}{{ foo }}") end def test_nested_include_tag assert_template_result("body body_detail", "{% include 'body' %}") assert_template_result("header body body_detail footer", "{% include 'nested_template' %}") end def test_nested_include_with_variable assert_template_result("Product: Draft 151cm details ", "{% include 'nested_product_template' with product %}", "product" => { "title" => 'Draft 151cm' }) assert_template_result("Product: Draft 151cm details Product: Element 155cm details ", "{% include 'nested_product_template' for products %}", "products" => [{ "title" => 'Draft 151cm' }, { "title" => 'Element 155cm' }]) end def test_recursively_included_template_does_not_produce_endless_loop infinite_file_system = Class.new do def read_template_file(_template_path) "-{% include 'loop' %}" end end Liquid::Template.file_system = infinite_file_system.new assert_raises(Liquid::StackLevelError) do Template.parse("{% include 'loop' %}").render! end end def test_dynamically_choosen_template assert_template_result("Test123", "{% include template %}", "template" => 'Test123') assert_template_result("Test321", "{% include template %}", "template" => 'Test321') assert_template_result("Product: Draft 151cm ", "{% include template for product %}", "template" => 'product', 'product' => { 'title' => 'Draft 151cm' }) end def test_include_tag_caches_second_read_of_same_partial file_system = CountingFileSystem.new assert_equal('from CountingFileSystemfrom CountingFileSystem', Template.parse("{% include 'pick_a_source' %}{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system })) assert_equal(1, file_system.count) end def test_include_tag_doesnt_cache_partials_across_renders file_system = CountingFileSystem.new assert_equal('from CountingFileSystem', Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system })) assert_equal(1, file_system.count) assert_equal('from CountingFileSystem', Template.parse("{% include 'pick_a_source' %}").render!({}, registers: { file_system: file_system })) assert_equal(2, file_system.count) end def test_include_tag_within_if_statement assert_template_result("foo_if_true", "{% if true %}{% include 'foo_if_true' %}{% endif %}") end def test_custom_include_tag original_tag = Liquid::Template.tags['include'] Liquid::Template.tags['include'] = CustomInclude begin assert_equal("custom_foo", Template.parse("{% include 'custom_foo' %}").render!) ensure Liquid::Template.tags['include'] = original_tag end end def test_custom_include_tag_within_if_statement original_tag = Liquid::Template.tags['include'] Liquid::Template.tags['include'] = CustomInclude begin assert_equal("custom_foo_if_true", Template.parse("{% if true %}{% include 'custom_foo_if_true' %}{% endif %}").render!) ensure Liquid::Template.tags['include'] = original_tag end end def test_does_not_add_error_in_strict_mode_for_missing_variable Liquid::Template.file_system = TestFileSystem.new a = Liquid::Template.parse(' {% include "nested_template" %}') a.render! assert_empty(a.errors) end def test_passing_options_to_included_templates assert_raises(Liquid::SyntaxError) do Template.parse("{% include template %}", error_mode: :strict).render!("template" => '{{ "X" || downcase }}') end with_error_mode(:lax) do assert_equal('x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: true).render!("template" => '{{ "X" || downcase }}')) end assert_raises(Liquid::SyntaxError) do Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:locale]).render!("template" => '{{ "X" || downcase }}') end with_error_mode(:lax) do assert_equal('x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:error_mode]).render!("template" => '{{ "X" || downcase }}')) end end def test_render_raise_argument_error_when_template_is_undefined assert_raises(Liquid::ArgumentError) do template = Liquid::Template.parse('{% include undefined_variable %}') template.render! end assert_raises(Liquid::ArgumentError) do template = Liquid::Template.parse('{% include nil %}') template.render! end end def test_including_via_variable_value assert_template_result("from TestFileSystem", "{% assign page = 'pick_a_source' %}{% include page %}") assert_template_result("Product: Draft 151cm ", "{% assign page = 'product' %}{% include page %}", "product" => { 'title' => 'Draft 151cm' }) assert_template_result("Product: Draft 151cm ", "{% assign page = 'product' %}{% include page for foo %}", "foo" => { 'title' => 'Draft 151cm' }) end def test_including_with_strict_variables template = Liquid::Template.parse("{% include 'simple' %}", error_mode: :warn) template.render(nil, strict_variables: true) assert_equal([], template.errors) end 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 }}{% include 'break' %}{{ i }}{% endfor %}") end end # IncludeTagTest liquid-5.4.0/test/integration/tags/increment_tag_test.rb000066400000000000000000000015121427076730100234600ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class IncrementTagTest < Minitest::Test include Liquid def test_inc assert_template_result('0', '{%increment port %}', {}) assert_template_result('0 1', '{%increment port %} {%increment port%}', {}) assert_template_result('0 0 1 2 1', '{%increment port %} {%increment starboard%} ' \ '{%increment port %} {%increment port%} ' \ '{%increment starboard %}', {}) end def test_dec assert_template_result('9', '{%decrement port %}', 'port' => 10) assert_template_result('-1 -2', '{%decrement port %} {%decrement port%}', {}) assert_template_result('1 5 2 2 5', '{%increment port %} {%increment starboard%} ' \ '{%increment port %} {%decrement port%} ' \ '{%decrement starboard %}', 'port' => 1, 'starboard' => 5) end end liquid-5.4.0/test/integration/tags/inline_comment_test.rb000066400000000000000000000040761427076730100236510ustar00rootroot00000000000000# 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 liquid-5.4.0/test/integration/tags/liquid_tag_test.rb000066400000000000000000000055351427076730100227740ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class LiquidTagTest < Minitest::Test include Liquid def test_liquid_tag assert_template_result('1 2 3', <<~LIQUID, 'array' => [1, 2, 3]) {%- liquid echo array | join: " " -%} LIQUID assert_template_result('1 2 3', <<~LIQUID, 'array' => [1, 2, 3]) {%- liquid for value in array echo value unless forloop.last echo " " endunless endfor -%} LIQUID assert_template_result('4 8 12 6', <<~LIQUID, 'array' => [1, 2, 3]) {%- liquid for value in array assign double_value = value | times: 2 echo double_value | times: 2 unless forloop.last echo " " endunless endfor echo " " echo double_value -%} LIQUID assert_template_result('abc', <<~LIQUID) {%- liquid echo "a" -%} b {%- liquid echo "c" -%} LIQUID end def test_liquid_tag_errors assert_match_syntax_error("syntax error (line 1): Unknown tag 'error'", <<~LIQUID) {%- liquid error no such tag -%} LIQUID assert_match_syntax_error("syntax error (line 7): Unknown tag 'error'", <<~LIQUID) {{ test }} {%- liquid for value in array error no such tag endfor -%} LIQUID assert_match_syntax_error("syntax error (line 2): Unknown tag '!!! the guards are vigilant'", <<~LIQUID) {%- liquid !!! the guards are vigilant -%} LIQUID assert_match_syntax_error("syntax error (line 4): 'for' tag was never closed", <<~LIQUID) {%- liquid for value in array echo 'forgot to close the for tag' -%} LIQUID end def test_line_number_is_correct_after_a_blank_token assert_match_syntax_error("syntax error (line 3): Unknown tag 'error'", "{% liquid echo ''\n\n error %}") assert_match_syntax_error("syntax error (line 3): Unknown tag 'error'", "{% liquid echo ''\n \n error %}") end def test_nested_liquid_tag assert_template_result('good', <<~LIQUID) {%- if true %} {%- liquid echo "good" %} {%- endif -%} LIQUID end def test_cannot_open_blocks_living_past_a_liquid_tag assert_match_syntax_error("syntax error (line 3): 'if' tag was never closed", <<~LIQUID) {%- liquid if true -%} {%- endif -%} LIQUID end def test_cannot_close_blocks_created_before_a_liquid_tag assert_match_syntax_error("syntax error (line 3): 'endif' is not a valid delimiter for liquid tags. use %}", <<~LIQUID) {%- if true -%} 42 {%- liquid endif -%} LIQUID end def test_liquid_tag_in_raw assert_template_result("{% liquid echo 'test' %}\n", <<~LIQUID) {% raw %}{% liquid echo 'test' %}{% endraw %} LIQUID end end liquid-5.4.0/test/integration/tags/raw_tag_test.rb000066400000000000000000000030411427076730100222640ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class RawTagTest < Minitest::Test include Liquid def test_tag_in_raw assert_template_result('{% comment %} test {% endcomment %}', '{% raw %}{% comment %} test {% endcomment %}{% endraw %}') end def test_output_in_raw assert_template_result('{{ test }}', '{% raw %}{{ test }}{% endraw %}') end def test_open_tag_in_raw assert_template_result(' Foobar {% invalid ', '{% raw %} Foobar {% invalid {% endraw %}') assert_template_result(' Foobar invalid %} ', '{% raw %} Foobar invalid %} {% endraw %}') assert_template_result(' Foobar {{ invalid ', '{% raw %} Foobar {{ invalid {% endraw %}') assert_template_result(' Foobar invalid }} ', '{% raw %} Foobar invalid }} {% endraw %}') assert_template_result(' Foobar {% invalid {% {% endraw ', '{% raw %} Foobar {% invalid {% {% endraw {% endraw %}') assert_template_result(' Foobar {% {% {% ', '{% raw %} Foobar {% {% {% {% endraw %}') assert_template_result(' test {% raw %} {% endraw %}', '{% raw %} test {% raw %} {% {% endraw %}endraw %}') assert_template_result(' Foobar {{ invalid 1', '{% raw %} Foobar {{ invalid {% endraw %}{{ 1 }}') assert_template_result(' Foobar {% foo {% bar %}', '{% raw %} Foobar {% foo {% bar %}{% endraw %}') end def test_invalid_raw assert_match_syntax_error(/tag was never closed/, '{% raw %} foo') assert_match_syntax_error(/Valid syntax/, '{% raw } foo {% endraw %}') assert_match_syntax_error(/Valid syntax/, '{% raw } foo %}{% endraw %}') end end liquid-5.4.0/test/integration/tags/render_tag_test.rb000066400000000000000000000205571427076730100227650ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class RenderTagTest < Minitest::Test include Liquid def test_render_with_no_arguments Liquid::Template.file_system = StubFileSystem.new('source' => 'rendered content') assert_template_result('rendered content', '{% render "source" %}') end def test_render_tag_looks_for_file_system_in_registers_first file_system = StubFileSystem.new('pick_a_source' => 'from register file system') assert_equal('from register file system', Template.parse('{% render "pick_a_source" %}').render!({}, registers: { file_system: file_system })) end def test_render_passes_named_arguments_into_inner_scope Liquid::Template.file_system = StubFileSystem.new('product' => '{{ inner_product.title }}') assert_template_result('My Product', '{% render "product", inner_product: outer_product %}', 'outer_product' => { 'title' => 'My Product' }) end def test_render_accepts_literals_as_arguments Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ price }}') assert_template_result('123', '{% render "snippet", price: 123 %}') end def test_render_accepts_multiple_named_arguments Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ one }} {{ two }}') assert_template_result('1 2', '{% render "snippet", one: 1, two: 2 %}') end def test_render_does_not_inherit_parent_scope_variables Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ outer_variable }}') assert_template_result('', '{% assign outer_variable = "should not be visible" %}{% render "snippet" %}') end def test_render_does_not_inherit_variable_with_same_name_as_snippet Liquid::Template.file_system = StubFileSystem.new('snippet' => '{{ snippet }}') assert_template_result('', "{% assign snippet = 'should not be visible' %}{% render 'snippet' %}") end def test_render_does_not_mutate_parent_scope Liquid::Template.file_system = StubFileSystem.new('snippet' => '{% assign inner = 1 %}') assert_template_result('', "{% render 'snippet' %}{{ inner }}") end def test_nested_render_tag Liquid::Template.file_system = StubFileSystem.new( 'one' => "one {% render 'two' %}", 'two' => 'two' ) assert_template_result('one two', "{% render 'one' %}") end def test_recursively_rendered_template_does_not_produce_endless_loop Liquid::Template.file_system = StubFileSystem.new('loop' => '{% render "loop" %}') assert_raises(Liquid::StackLevelError) do Template.parse('{% render "loop" %}').render! end end def test_sub_contexts_count_towards_the_same_recursion_limit Liquid::Template.file_system = StubFileSystem.new( 'loop_render' => '{% render "loop_render" %}', ) assert_raises(Liquid::StackLevelError) do Template.parse('{% render "loop_render" %}').render! end end def test_dynamically_choosen_templates_are_not_allowed Liquid::Template.file_system = StubFileSystem.new('snippet' => 'should not be rendered') assert_raises(Liquid::SyntaxError) do Liquid::Template.parse("{% assign name = 'snippet' %}{% render name %}") end end def test_include_tag_caches_second_read_of_same_partial file_system = StubFileSystem.new('snippet' => 'echo') assert_equal('echoecho', Template.parse('{% render "snippet" %}{% render "snippet" %}') .render!({}, registers: { file_system: file_system })) assert_equal(1, file_system.file_read_count) end def test_render_tag_doesnt_cache_partials_across_renders file_system = StubFileSystem.new('snippet' => 'my message') assert_equal('my message', Template.parse('{% include "snippet" %}').render!({}, registers: { file_system: file_system })) assert_equal(1, file_system.file_read_count) assert_equal('my message', Template.parse('{% include "snippet" %}').render!({}, registers: { file_system: file_system })) assert_equal(2, file_system.file_read_count) end def test_render_tag_within_if_statement Liquid::Template.file_system = StubFileSystem.new('snippet' => 'my message') assert_template_result('my message', '{% if true %}{% render "snippet" %}{% endif %}') end def test_break_through_render Liquid::Template.file_system = StubFileSystem.new('break' => '{% break %}') assert_template_result('1', '{% for i in (1..3) %}{{ i }}{% break %}{{ i }}{% endfor %}') assert_template_result('112233', '{% for i in (1..3) %}{{ i }}{% render "break" %}{{ i }}{% endfor %}') end def test_increment_is_isolated_between_renders Liquid::Template.file_system = StubFileSystem.new('incr' => '{% increment %}') assert_template_result('010', '{% increment %}{% increment %}{% render "incr" %}') end def test_decrement_is_isolated_between_renders Liquid::Template.file_system = StubFileSystem.new('decr' => '{% decrement %}') assert_template_result('-1-2-1', '{% decrement %}{% decrement %}{% render "decr" %}') end def test_includes_will_not_render_inside_render_tag Liquid::Template.file_system = StubFileSystem.new( 'foo' => 'bar', 'test_include' => '{% include "foo" %}' ) exc = assert_raises(Liquid::DisabledError) do Liquid::Template.parse('{% render "test_include" %}').render! end assert_equal('Liquid error: include usage is not allowed in this context', exc.message) end def test_includes_will_not_render_inside_nested_sibling_tags Liquid::Template.file_system = StubFileSystem.new( 'foo' => 'bar', 'nested_render_with_sibling_include' => '{% render "test_include" %}{% include "foo" %}', 'test_include' => '{% include "foo" %}' ) output = Liquid::Template.parse('{% render "nested_render_with_sibling_include" %}').render assert_equal('Liquid error: include usage is not allowed in this contextLiquid error: include usage is not allowed in this context', output) end def test_render_tag_with Liquid::Template.file_system = StubFileSystem.new( 'product' => "Product: {{ product.title }} ", 'product_alias' => "Product: {{ product.title }} ", ) assert_template_result("Product: Draft 151cm ", "{% render 'product' with products[0] %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }]) end def test_render_tag_with_alias Liquid::Template.file_system = StubFileSystem.new( 'product' => "Product: {{ product.title }} ", 'product_alias' => "Product: {{ product.title }} ", ) assert_template_result("Product: Draft 151cm ", "{% render 'product_alias' with products[0] as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }]) end def test_render_tag_for_alias Liquid::Template.file_system = StubFileSystem.new( 'product' => "Product: {{ product.title }} ", 'product_alias' => "Product: {{ product.title }} ", ) assert_template_result("Product: Draft 151cm Product: Element 155cm ", "{% render 'product_alias' for products as product %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }]) end def test_render_tag_for Liquid::Template.file_system = StubFileSystem.new( 'product' => "Product: {{ product.title }} ", 'product_alias' => "Product: {{ product.title }} ", ) assert_template_result("Product: Draft 151cm Product: Element 155cm ", "{% render 'product' for products %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }]) end def test_render_tag_forloop Liquid::Template.file_system = StubFileSystem.new( 'product' => "Product: {{ product.title }} {% if forloop.first %}first{% endif %} {% if forloop.last %}last{% endif %} index:{{ forloop.index }} ", ) assert_template_result("Product: Draft 151cm first index:1 Product: Element 155cm last index:2 ", "{% render 'product' for products %}", "products" => [{ 'title' => 'Draft 151cm' }, { 'title' => 'Element 155cm' }]) end def test_render_tag_for_drop Liquid::Template.file_system = StubFileSystem.new( 'loop' => "{{ value.foo }}", ) assert_template_result("123", "{% render 'loop' for loop as value %}", "loop" => TestEnumerable.new) end def test_render_tag_with_drop Liquid::Template.file_system = StubFileSystem.new( 'loop' => "{{ value }}", ) assert_template_result("TestEnumerable", "{% render 'loop' with loop as value %}", "loop" => TestEnumerable.new) end end liquid-5.4.0/test/integration/tags/standard_tag_test.rb000066400000000000000000000312661427076730100233050ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StandardTagTest < Minitest::Test include Liquid def test_no_transform assert_template_result('this text should come out of the template without change...', 'this text should come out of the template without change...') assert_template_result('blah', 'blah') assert_template_result('', '') assert_template_result('|,.:', '|,.:') assert_template_result('', '') text = %(this shouldnt see any transformation either but has multiple lines as you can clearly see here ...) assert_template_result(text, text) end def test_has_a_block_which_does_nothing assert_template_result(%(the comment block should be removed .. right?), %(the comment block should be removed {%comment%} be gone.. {%endcomment%} .. right?)) assert_template_result('', '{%comment%}{%endcomment%}') assert_template_result('', '{%comment%}{% endcomment %}') assert_template_result('', '{% comment %}{%endcomment%}') assert_template_result('', '{% comment %}{% endcomment %}') assert_template_result('', '{%comment%}comment{%endcomment%}') assert_template_result('', '{% comment %}comment{% endcomment %}') assert_template_result('', '{% comment %} 1 {% comment %} 2 {% endcomment %} 3 {% endcomment %}') assert_template_result('', '{%comment%}{%blabla%}{%endcomment%}') assert_template_result('', '{% comment %}{% blabla %}{% endcomment %}') assert_template_result('', '{%comment%}{% endif %}{%endcomment%}') assert_template_result('', '{% comment %}{% endwhatever %}{% endcomment %}') assert_template_result('', '{% comment %}{% raw %} {{%%%%}} }} { {% endcomment %} {% comment {% endraw %} {% endcomment %}') assert_template_result('foobar', 'foo{%comment%}comment{%endcomment%}bar') assert_template_result('foobar', 'foo{% comment %}comment{% endcomment %}bar') assert_template_result('foobar', 'foo{%comment%} comment {%endcomment%}bar') assert_template_result('foobar', 'foo{% comment %} comment {% endcomment %}bar') assert_template_result('foo bar', 'foo {%comment%} {%endcomment%} bar') assert_template_result('foo bar', 'foo {%comment%}comment{%endcomment%} bar') assert_template_result('foo bar', 'foo {%comment%} comment {%endcomment%} bar') assert_template_result('foobar', 'foo{%comment%} {%endcomment%}bar') end def test_hyphenated_assign assigns = { 'a-b' => '1' } assert_template_result('a-b:1 a-b:2', 'a-b:{{a-b}} {%assign a-b = 2 %}a-b:{{a-b}}', assigns) end def test_assign_with_colon_and_spaces assigns = { 'var' => { 'a:b c' => { 'paged' => '1' } } } assert_template_result('var2: 1', '{%assign var2 = var["a:b c"].paged %}var2: {{var2}}', assigns) end def test_capture assigns = { 'var' => 'content' } assert_template_result('content foo content foo ', '{{ var2 }}{% capture var2 %}{{ var }} foo {% endcapture %}{{ var2 }}{{ var2 }}', assigns) end def test_capture_detects_bad_syntax assert_raises(SyntaxError) do assert_template_result('content foo content foo ', '{{ var2 }}{% capture %}{{ var }} foo {% endcapture %}{{ var2 }}{{ var2 }}', 'var' => 'content') end end def test_case assigns = { 'condition' => 2 } assert_template_result(' its 2 ', '{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}', assigns) assigns = { 'condition' => 1 } assert_template_result(' its 1 ', '{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}', assigns) assigns = { 'condition' => 3 } assert_template_result('', '{% case condition %}{% when 1 %} its 1 {% when 2 %} its 2 {% endcase %}', assigns) assigns = { 'condition' => "string here" } assert_template_result(' hit ', '{% case condition %}{% when "string here" %} hit {% endcase %}', assigns) assigns = { 'condition' => "bad string here" } assert_template_result('', '{% case condition %}{% when "string here" %} hit {% endcase %}',\ assigns) end def test_case_with_else assigns = { 'condition' => 5 } assert_template_result(' hit ', '{% case condition %}{% when 5 %} hit {% else %} else {% endcase %}', assigns) assigns = { 'condition' => 6 } assert_template_result(' else ', '{% case condition %}{% when 5 %} hit {% else %} else {% endcase %}', assigns) assigns = { 'condition' => 6 } assert_template_result(' else ', '{% case condition %} {% when 5 %} hit {% else %} else {% endcase %}', assigns) end def test_case_on_size assert_template_result('', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => []) assert_template_result('1', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1]) assert_template_result('2', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1, 1]) assert_template_result('', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1, 1, 1]) assert_template_result('', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1, 1, 1, 1]) assert_template_result('', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% endcase %}', 'a' => [1, 1, 1, 1, 1]) end def test_case_on_size_with_else assert_template_result('else', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', 'a' => []) assert_template_result('1', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', 'a' => [1]) assert_template_result('2', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', 'a' => [1, 1]) assert_template_result('else', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', 'a' => [1, 1, 1]) assert_template_result('else', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', 'a' => [1, 1, 1, 1]) assert_template_result('else', '{% case a.size %}{% when 1 %}1{% when 2 %}2{% else %}else{% endcase %}', 'a' => [1, 1, 1, 1, 1]) end def test_case_on_length_with_else assert_template_result('else', '{% case a.empty? %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', {}) assert_template_result('false', '{% case false %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', {}) assert_template_result('true', '{% case true %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', {}) assert_template_result('else', '{% case NULL %}{% when true %}true{% when false %}false{% else %}else{% endcase %}', {}) end def test_assign_from_case # Example from the shopify forums code = "{% case collection.handle %}{% when 'menswear-jackets' %}{% assign ptitle = 'menswear' %}{% when 'menswear-t-shirts' %}{% assign ptitle = 'menswear' %}{% else %}{% assign ptitle = 'womenswear' %}{% endcase %}{{ ptitle }}" template = Liquid::Template.parse(code) assert_equal("menswear", template.render!("collection" => { 'handle' => 'menswear-jackets' })) assert_equal("menswear", template.render!("collection" => { 'handle' => 'menswear-t-shirts' })) assert_equal("womenswear", template.render!("collection" => { 'handle' => 'x' })) assert_equal("womenswear", template.render!("collection" => { 'handle' => 'y' })) assert_equal("womenswear", template.render!("collection" => { 'handle' => 'z' })) end def test_case_when_or code = '{% case condition %}{% when 1 or 2 or 3 %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 1) assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 2) assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 3) assert_template_result(' its 4 ', code, 'condition' => 4) assert_template_result('', code, 'condition' => 5) code = '{% case condition %}{% when 1 or "string" or null %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 1) assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 'string') assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => nil) assert_template_result('', code, 'condition' => 'something else') end def test_case_when_comma code = '{% case condition %}{% when 1, 2, 3 %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 1) assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 2) assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 3) assert_template_result(' its 4 ', code, 'condition' => 4) assert_template_result('', code, 'condition' => 5) code = '{% case condition %}{% when 1, "string", null %} its 1 or 2 or 3 {% when 4 %} its 4 {% endcase %}' assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 1) assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => 'string') assert_template_result(' its 1 or 2 or 3 ', code, 'condition' => nil) assert_template_result('', code, 'condition' => 'something else') end def test_case_when_comma_and_blank_body code = '{% case condition %}{% when 1, 2 %} {% assign r = "result" %} {% endcase %}{{ r }}' assert_template_result('result', code, 'condition' => 2) end def test_assign assert_template_result('variable', '{% assign a = "variable"%}{{a}}') end def test_assign_unassigned assigns = { 'var' => 'content' } assert_template_result('var2: var2:content', 'var2:{{var2}} {%assign var2 = var%} var2:{{var2}}', assigns) end def test_assign_an_empty_string assert_template_result('', '{% assign a = ""%}{{a}}') end def test_assign_is_global assert_template_result('variable', '{%for i in (1..2) %}{% assign a = "variable"%}{% endfor %}{{a}}') end def test_case_detects_bad_syntax assert_raises(SyntaxError) do assert_template_result('', '{% case false %}{% when %}true{% endcase %}', {}) end assert_raises(SyntaxError) do assert_template_result('', '{% case false %}{% huh %}true{% endcase %}', {}) end end def test_cycle assert_template_result('one', '{%cycle "one", "two"%}') assert_template_result('one two', '{%cycle "one", "two"%} {%cycle "one", "two"%}') assert_template_result(' two', '{%cycle "", "two"%} {%cycle "", "two"%}') assert_template_result('one two one', '{%cycle "one", "two"%} {%cycle "one", "two"%} {%cycle "one", "two"%}') assert_template_result('text-align: left text-align: right', '{%cycle "text-align: left", "text-align: right" %} {%cycle "text-align: left", "text-align: right"%}') end def test_multiple_cycles assert_template_result('1 2 1 1 2 3 1', '{%cycle 1,2%} {%cycle 1,2%} {%cycle 1,2%} {%cycle 1,2,3%} {%cycle 1,2,3%} {%cycle 1,2,3%} {%cycle 1,2,3%}') end def test_multiple_named_cycles assert_template_result('one one two two one one', '{%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %} {%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %} {%cycle 1: "one", "two" %} {%cycle 2: "one", "two" %}') end def test_multiple_named_cycles_with_names_from_context assigns = { "var1" => 1, "var2" => 2 } assert_template_result('one one two two one one', '{%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %} {%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %} {%cycle var1: "one", "two" %} {%cycle var2: "one", "two" %}', assigns) end def test_size_of_array assigns = { "array" => [1, 2, 3, 4] } assert_template_result('array has 4 elements', "array has {{ array.size }} elements", assigns) end def test_size_of_hash assigns = { "hash" => { a: 1, b: 2, c: 3, d: 4 } } assert_template_result('hash has 4 elements', "hash has {{ hash.size }} elements", assigns) end def test_illegal_symbols assert_template_result('', '{% if true == empty %}?{% endif %}', {}) assert_template_result('', '{% if true == null %}?{% endif %}', {}) assert_template_result('', '{% if empty == true %}?{% endif %}', {}) assert_template_result('', '{% if null == true %}?{% endif %}', {}) end def test_ifchanged assigns = { 'array' => [1, 1, 2, 2, 3, 3] } assert_template_result('123', '{%for item in array%}{%ifchanged%}{{item}}{% endifchanged %}{%endfor%}', assigns) assigns = { 'array' => [1, 1, 1, 1] } assert_template_result('1', '{%for item in array%}{%ifchanged%}{{item}}{% endifchanged %}{%endfor%}', assigns) end def test_multiline_tag assert_template_result('0 1 2 3', "0{%\nfor i in (1..3)\n%} {{\ni\n}}{%\nendfor\n%}") end end # StandardTagTest liquid-5.4.0/test/integration/tags/statements_test.rb000066400000000000000000000071161427076730100230360ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StatementsTest < Minitest::Test include Liquid def test_true_eql_true text = ' {% if true == true %} true {% else %} false {% endif %} ' assert_template_result(' true ', text) end def test_true_not_eql_true text = ' {% if true != true %} true {% else %} false {% endif %} ' assert_template_result(' false ', text) end def test_true_lq_true text = ' {% if 0 > 0 %} true {% else %} false {% endif %} ' assert_template_result(' false ', text) end def test_one_lq_zero text = ' {% if 1 > 0 %} true {% else %} false {% endif %} ' assert_template_result(' true ', text) end def test_zero_lq_one text = ' {% if 0 < 1 %} true {% else %} false {% endif %} ' assert_template_result(' true ', text) end def test_zero_lq_or_equal_one text = ' {% if 0 <= 0 %} true {% else %} false {% endif %} ' assert_template_result(' true ', text) end def test_zero_lq_or_equal_one_involving_nil text = ' {% if null <= 0 %} true {% else %} false {% endif %} ' assert_template_result(' false ', text) text = ' {% if 0 <= null %} true {% else %} false {% endif %} ' assert_template_result(' false ', text) end def test_zero_lqq_or_equal_one text = ' {% if 0 >= 0 %} true {% else %} false {% endif %} ' assert_template_result(' true ', text) end def test_strings text = " {% if 'test' == 'test' %} true {% else %} false {% endif %} " assert_template_result(' true ', text) end def test_strings_not_equal text = " {% if 'test' != 'test' %} true {% else %} false {% endif %} " assert_template_result(' false ', text) end def test_var_strings_equal text = ' {% if var == "hello there!" %} true {% else %} false {% endif %} ' assert_template_result(' true ', text, 'var' => 'hello there!') end def test_var_strings_are_not_equal text = ' {% if "hello there!" == var %} true {% else %} false {% endif %} ' assert_template_result(' true ', text, 'var' => 'hello there!') end def test_var_and_long_string_are_equal text = " {% if var == 'hello there!' %} true {% else %} false {% endif %} " assert_template_result(' true ', text, 'var' => 'hello there!') end def test_var_and_long_string_are_equal_backwards text = " {% if 'hello there!' == var %} true {% else %} false {% endif %} " assert_template_result(' true ', text, 'var' => 'hello there!') end # def test_is_nil # text = %| {% if var != nil %} true {% else %} false {% end %} | # @template.assigns = { 'var' => 'hello there!'} # expected = %| true | # assert_equal expected, @template.parse(text) # end def test_is_collection_empty text = ' {% if array == empty %} true {% else %} false {% endif %} ' assert_template_result(' true ', text, 'array' => []) end def test_is_not_collection_empty text = ' {% if array == empty %} true {% else %} false {% endif %} ' assert_template_result(' false ', text, 'array' => [1, 2, 3]) end def test_nil text = ' {% if var == nil %} true {% else %} false {% endif %} ' assert_template_result(' true ', text, 'var' => nil) text = ' {% if var == null %} true {% else %} false {% endif %} ' assert_template_result(' true ', text, 'var' => nil) end def test_not_nil text = ' {% if var != nil %} true {% else %} false {% endif %} ' assert_template_result(' true ', text, 'var' => 1) text = ' {% if var != null %} true {% else %} false {% endif %} ' assert_template_result(' true ', text, 'var' => 1) end end # StatementsTest liquid-5.4.0/test/integration/tags/table_row_test.rb000066400000000000000000000066141427076730100226270ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TableRowTest < Minitest::Test include Liquid class ArrayDrop < Liquid::Drop include Enumerable def initialize(array) @array = array end def each(&block) @array.each(&block) end end def test_table_row assert_template_result("\n 1 2 3 \n 4 5 6 \n", '{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}', 'numbers' => [1, 2, 3, 4, 5, 6]) assert_template_result("\n\n", '{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}', 'numbers' => []) end def test_table_row_with_different_cols assert_template_result("\n 1 2 3 4 5 \n 6 \n", '{% tablerow n in numbers cols:5%} {{n}} {% endtablerow %}', 'numbers' => [1, 2, 3, 4, 5, 6]) end def test_table_col_counter assert_template_result("\n12\n12\n12\n", '{% tablerow n in numbers cols:2%}{{tablerowloop.col}}{% endtablerow %}', 'numbers' => [1, 2, 3, 4, 5, 6]) end def test_quoted_fragment assert_template_result("\n 1 2 3 \n 4 5 6 \n", "{% tablerow n in collections.frontpage cols:3%} {{n}} {% endtablerow %}", 'collections' => { 'frontpage' => [1, 2, 3, 4, 5, 6] }) assert_template_result("\n 1 2 3 \n 4 5 6 \n", "{% tablerow n in collections['frontpage'] cols:3%} {{n}} {% endtablerow %}", 'collections' => { 'frontpage' => [1, 2, 3, 4, 5, 6] }) end def test_enumerable_drop assert_template_result("\n 1 2 3 \n 4 5 6 \n", '{% tablerow n in numbers cols:3%} {{n}} {% endtablerow %}', 'numbers' => ArrayDrop.new([1, 2, 3, 4, 5, 6])) end def test_offset_and_limit assert_template_result("\n 1 2 3 \n 4 5 6 \n", '{% tablerow n in numbers cols:3 offset:1 limit:6%} {{n}} {% endtablerow %}', 'numbers' => [0, 1, 2, 3, 4, 5, 6, 7]) end def test_blank_string_not_iterable assert_template_result("\n\n", "{% tablerow char in characters cols:3 %}I WILL NOT BE OUTPUT{% endtablerow %}", 'characters' => '') end end liquid-5.4.0/test/integration/tags/unless_else_tag_test.rb000066400000000000000000000023521427076730100240200ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class UnlessElseTagTest < Minitest::Test include Liquid def test_unless assert_template_result(' ', ' {% unless true %} this text should not go into the output {% endunless %} ') assert_template_result(' this text should go into the output ', ' {% unless false %} this text should go into the output {% endunless %} ') assert_template_result(' you rock ?', '{% unless true %} you suck {% endunless %} {% unless false %} you rock {% endunless %}?') end def test_unless_else assert_template_result(' YES ', '{% unless true %} NO {% else %} YES {% endunless %}') assert_template_result(' YES ', '{% unless false %} YES {% else %} NO {% endunless %}') assert_template_result(' YES ', '{% unless "foo" %} NO {% else %} YES {% endunless %}') end def test_unless_in_loop assert_template_result('23', '{% for i in choices %}{% unless i %}{{ forloop.index }}{% endunless %}{% endfor %}', 'choices' => [1, nil, false]) end def test_unless_else_in_loop assert_template_result(' TRUE 2 3 ', '{% for i in choices %}{% unless i %} {{ forloop.index }} {% else %} TRUE {% endunless %}{% endfor %}', 'choices' => [1, nil, false]) end end # UnlessElseTest liquid-5.4.0/test/integration/template_test.rb000066400000000000000000000267701427076730100215330ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' require 'timeout' class TemplateContextDrop < Liquid::Drop def liquid_method_missing(method) method end def foo 'fizzbuzz' end def baz @context.registers['lulz'] end end class SomethingWithLength < Liquid::Drop def length nil end end class ErroneousDrop < Liquid::Drop def bad_method raise 'ruby error in drop' end end class DropWithUndefinedMethod < Liquid::Drop def foo 'foo' end end class TemplateTest < Minitest::Test include Liquid def test_instance_assigns_persist_on_same_template_object_between_parses t = Template.new assert_equal('from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render!) assert_equal('from instance assigns', t.parse("{{ foo }}").render!) end def test_warnings_is_not_exponential_time str = "false" 100.times do str = "{% if true %}true{% else %}#{str}{% endif %}" end t = Template.parse(str) assert_equal([], Timeout.timeout(1) { t.warnings }) end def test_instance_assigns_persist_on_same_template_parsing_between_renders t = Template.new.parse("{{ foo }}{% assign foo = 'foo' %}{{ foo }}") assert_equal('foo', t.render!) assert_equal('foofoo', t.render!) end def test_custom_assigns_do_not_persist_on_same_template t = Template.new assert_equal('from custom assigns', t.parse("{{ foo }}").render!('foo' => 'from custom assigns')) assert_equal('', t.parse("{{ foo }}").render!) end def test_custom_assigns_squash_instance_assigns t = Template.new assert_equal('from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render!) assert_equal('from custom assigns', t.parse("{{ foo }}").render!('foo' => 'from custom assigns')) end def test_persistent_assigns_squash_instance_assigns t = Template.new assert_equal('from instance assigns', t.parse("{% assign foo = 'from instance assigns' %}{{ foo }}").render!) t.assigns['foo'] = 'from persistent assigns' assert_equal('from persistent assigns', t.parse("{{ foo }}").render!) end def test_lambda_is_called_once_from_persistent_assigns_over_multiple_parses_and_renders t = Template.new t.assigns['number'] = -> { @global ||= 0 @global += 1 } assert_equal('1', t.parse("{{number}}").render!) assert_equal('1', t.parse("{{number}}").render!) assert_equal('1', t.render!) @global = nil end def test_lambda_is_called_once_from_custom_assigns_over_multiple_parses_and_renders t = Template.new assigns = { 'number' => -> { @global ||= 0 @global += 1 } } assert_equal('1', t.parse("{{number}}").render!(assigns)) assert_equal('1', t.parse("{{number}}").render!(assigns)) assert_equal('1', t.render!(assigns)) @global = nil end def test_resource_limits_works_with_custom_length_method t = Template.parse("{% assign foo = bar %}") t.resource_limits.render_length_limit = 42 assert_equal("", t.render!("bar" => SomethingWithLength.new)) end def test_resource_limits_render_length t = Template.parse("0123456789") t.resource_limits.render_length_limit = 9 assert_equal("Liquid error: Memory limits exceeded", t.render) assert(t.resource_limits.reached?) t.resource_limits.render_length_limit = 10 assert_equal("0123456789", t.render!) end def test_resource_limits_render_score t = Template.parse("{% for a in (1..10) %} {% for a in (1..10) %} foo {% endfor %} {% endfor %}") t.resource_limits.render_score_limit = 50 assert_equal("Liquid error: Memory limits exceeded", t.render) assert(t.resource_limits.reached?) t = Template.parse("{% for a in (1..100) %} foo {% endfor %}") t.resource_limits.render_score_limit = 50 assert_equal("Liquid error: Memory limits exceeded", t.render) assert(t.resource_limits.reached?) t.resource_limits.render_score_limit = 200 assert_equal((" foo " * 100), t.render!) refute_nil(t.resource_limits.render_score) end def test_resource_limits_aborts_rendering_after_first_error t = Template.parse("{% for a in (1..100) %} foo1 {% endfor %} bar {% for a in (1..100) %} foo2 {% endfor %}") t.resource_limits.render_score_limit = 50 assert_equal("Liquid error: Memory limits exceeded", t.render) assert(t.resource_limits.reached?) end def test_resource_limits_hash_in_template_gets_updated_even_if_no_limits_are_set t = Template.parse("{% for a in (1..100) %}x{% assign foo = 1 %} {% endfor %}") t.render! assert(t.resource_limits.assign_score > 0) assert(t.resource_limits.render_score > 0) end def test_render_length_persists_between_blocks t = Template.parse("{% if true %}aaaa{% endif %}") t.resource_limits.render_length_limit = 3 assert_equal("Liquid error: Memory limits exceeded", t.render) t.resource_limits.render_length_limit = 4 assert_equal("aaaa", t.render) t = Template.parse("{% if true %}aaaa{% endif %}{% if true %}bbb{% endif %}") t.resource_limits.render_length_limit = 6 assert_equal("Liquid error: Memory limits exceeded", t.render) t.resource_limits.render_length_limit = 7 assert_equal("aaaabbb", t.render) t = Template.parse("{% if true %}a{% endif %}{% if true %}b{% endif %}{% if true %}a{% endif %}{% if true %}b{% endif %}{% if true %}a{% endif %}{% if true %}b{% endif %}") t.resource_limits.render_length_limit = 5 assert_equal("Liquid error: Memory limits exceeded", t.render) t.resource_limits.render_length_limit = 6 assert_equal("ababab", t.render) end def test_render_length_uses_number_of_bytes_not_characters t = Template.parse("{% if true %}すごい{% endif %}") t.resource_limits.render_length_limit = 8 assert_equal("Liquid error: Memory limits exceeded", t.render) t.resource_limits.render_length_limit = 9 assert_equal("すごい", t.render) end def test_default_resource_limits_unaffected_by_render_with_context context = Context.new t = Template.parse("{% for a in (1..100) %}x{% assign foo = 1 %} {% endfor %}") t.render!(context) assert(context.resource_limits.assign_score > 0) assert(context.resource_limits.render_score > 0) end def test_can_use_drop_as_context t = Template.new t.registers['lulz'] = 'haha' drop = TemplateContextDrop.new assert_equal('fizzbuzz', t.parse('{{foo}}').render!(drop)) assert_equal('bar', t.parse('{{bar}}').render!(drop)) assert_equal('haha', t.parse("{{baz}}").render!(drop)) end def test_render_bang_force_rethrow_errors_on_passed_context context = Context.new('drop' => ErroneousDrop.new) t = Template.new.parse('{{ drop.bad_method }}') e = assert_raises(RuntimeError) do t.render!(context) end assert_equal('ruby error in drop', e.message) end def test_exception_renderer_that_returns_string exception = nil handler = ->(e) { exception = e '' } output = Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_renderer: handler) assert(exception.is_a?(Liquid::ZeroDivisionError)) assert_equal('', output) end def test_exception_renderer_that_raises exception = nil assert_raises(Liquid::ZeroDivisionError) do Template.parse("{{ 1 | divided_by: 0 }}").render({}, exception_renderer: ->(e) { exception = e raise }) end assert(exception.is_a?(Liquid::ZeroDivisionError)) end def test_global_filter_option_on_render global_filter_proc = ->(output) { "#{output} filtered" } rendered_template = Template.parse("{{name}}").render({ "name" => "bob" }, global_filter: global_filter_proc) assert_equal('bob filtered', rendered_template) end def test_global_filter_option_when_native_filters_exist global_filter_proc = ->(output) { "#{output} filtered" } rendered_template = Template.parse("{{name | upcase}}").render({ "name" => "bob" }, global_filter: global_filter_proc) assert_equal('BOB filtered', rendered_template) end def test_undefined_variables t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}") result = t.render({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, strict_variables: true) assert_equal('33 32 ', result) assert_equal(3, t.errors.count) assert_instance_of(Liquid::UndefinedVariable, t.errors[0]) assert_equal('Liquid error: undefined variable y', t.errors[0].message) assert_instance_of(Liquid::UndefinedVariable, t.errors[1]) assert_equal('Liquid error: undefined variable b', t.errors[1].message) assert_instance_of(Liquid::UndefinedVariable, t.errors[2]) assert_equal('Liquid error: undefined variable d', t.errors[2].message) end def test_nil_value_does_not_raise t = Template.parse("some{{x}}thing", error_mode: :strict) result = t.render!({ 'x' => nil }, strict_variables: true) assert_equal(0, t.errors.count) assert_equal('something', result) end def test_undefined_variables_raise t = Template.parse("{{x}} {{y}} {{z.a}} {{z.b}} {{z.c.d}}") assert_raises(UndefinedVariable) do t.render!({ 'x' => 33, 'z' => { 'a' => 32, 'c' => { 'e' => 31 } } }, strict_variables: true) end end def test_undefined_drop_methods d = DropWithUndefinedMethod.new t = Template.new.parse('{{ foo }} {{ woot }}') result = t.render(d, strict_variables: true) assert_equal('foo ', result) assert_equal(1, t.errors.count) assert_instance_of(Liquid::UndefinedDropMethod, t.errors[0]) end def test_undefined_drop_methods_raise d = DropWithUndefinedMethod.new t = Template.new.parse('{{ foo }} {{ woot }}') assert_raises(UndefinedDropMethod) do t.render!(d, strict_variables: true) end end def test_undefined_filters t = Template.parse("{{a}} {{x | upcase | somefilter1 | somefilter2 | somefilter3}}") filters = Module.new do def somefilter3(v) "-#{v}-" end end result = t.render({ 'a' => 123, 'x' => 'foo' }, filters: [filters], strict_filters: true) assert_equal('123 ', result) assert_equal(1, t.errors.count) assert_instance_of(Liquid::UndefinedFilter, t.errors[0]) assert_equal('Liquid error: undefined filter somefilter1', t.errors[0].message) end def test_undefined_filters_raise t = Template.parse("{{x | somefilter1 | upcase | somefilter2}}") assert_raises(UndefinedFilter) do t.render!({ 'x' => 'foo' }, strict_filters: true) end end def test_using_range_literal_works_as_expected t = Template.parse("{% assign foo = (x..y) %}{{ foo }}") result = t.render('x' => 1, 'y' => 5) assert_equal('1..5', result) t = Template.parse("{% assign nums = (x..y) %}{% for num in nums %}{{ num }}{% endfor %}") result = t.render('x' => 1, 'y' => 5) assert_equal('12345', result) end def test_source_string_subclass string_subclass = Class.new(String) do # E.g. ActiveSupport::SafeBuffer does this, so don't just rely on to_s to return a String def to_s self end end source = string_subclass.new("{% assign x = 2 -%} x= {{- x }}") assert_instance_of(string_subclass, source) output = Template.parse(source).render! assert_equal("x=2", output) assert_instance_of(String, output) end end liquid-5.4.0/test/integration/trim_mode_test.rb000066400000000000000000000266571427076730100217030ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TrimModeTest < Minitest::Test include Liquid # Make sure the trim isn't applied to standard output def test_standard_output text = <<-END_TEMPLATE

{{ 'John' }}

END_TEMPLATE expected = <<-END_EXPECTED

John

END_EXPECTED assert_template_result(expected, text) end def test_variable_output_with_multiple_blank_lines text = <<-END_TEMPLATE

{{- 'John' -}}

END_TEMPLATE expected = <<-END_EXPECTED

John

END_EXPECTED assert_template_result(expected, text) end def test_tag_output_with_multiple_blank_lines text = <<-END_TEMPLATE

{%- if true -%} yes {%- endif -%}

END_TEMPLATE expected = <<-END_EXPECTED

yes

END_EXPECTED assert_template_result(expected, text) end # Make sure the trim isn't applied to standard tags def test_standard_tags whitespace = ' ' text = <<-END_TEMPLATE

{% if true %} yes {% endif %}

END_TEMPLATE expected = <<~END_EXPECTED

#{whitespace} yes #{whitespace}

END_EXPECTED assert_template_result(expected, text) text = <<-END_TEMPLATE

{% if false %} no {% endif %}

END_TEMPLATE expected = <<~END_EXPECTED

#{whitespace}

END_EXPECTED assert_template_result(expected, text) end # Make sure the trim isn't too agressive def test_no_trim_output text = '

{{- \'John\' -}}

' expected = '

John

' assert_template_result(expected, text) end # Make sure the trim isn't too agressive def test_no_trim_tags text = '

{%- if true -%}yes{%- endif -%}

' expected = '

yes

' assert_template_result(expected, text) text = '

{%- if false -%}no{%- endif -%}

' expected = '

' assert_template_result(expected, text) end def test_single_line_outer_tag text = '

{%- if true %} yes {% endif -%}

' expected = '

yes

' assert_template_result(expected, text) text = '

{%- if false %} no {% endif -%}

' expected = '

' assert_template_result(expected, text) end def test_single_line_inner_tag text = '

{% if true -%} yes {%- endif %}

' expected = '

yes

' assert_template_result(expected, text) text = '

{% if false -%} no {%- endif %}

' expected = '

' assert_template_result(expected, text) end def test_single_line_post_tag text = '

{% if true -%} yes {% endif -%}

' expected = '

yes

' assert_template_result(expected, text) text = '

{% if false -%} no {% endif -%}

' expected = '

' assert_template_result(expected, text) end def test_single_line_pre_tag text = '

{%- if true %} yes {%- endif %}

' expected = '

yes

' assert_template_result(expected, text) text = '

{%- if false %} no {%- endif %}

' expected = '

' assert_template_result(expected, text) end def test_pre_trim_output text = <<-END_TEMPLATE

{{- 'John' }}

END_TEMPLATE expected = <<-END_EXPECTED

John

END_EXPECTED assert_template_result(expected, text) end def test_pre_trim_tags text = <<-END_TEMPLATE

{%- if true %} yes {%- endif %}

END_TEMPLATE expected = <<-END_EXPECTED

yes

END_EXPECTED assert_template_result(expected, text) text = <<-END_TEMPLATE

{%- if false %} no {%- endif %}

END_TEMPLATE expected = <<-END_EXPECTED

END_EXPECTED assert_template_result(expected, text) end def test_post_trim_output text = <<-END_TEMPLATE

{{ 'John' -}}

END_TEMPLATE expected = <<-END_EXPECTED

John

END_EXPECTED assert_template_result(expected, text) end def test_post_trim_tags text = <<-END_TEMPLATE

{% if true -%} yes {% endif -%}

END_TEMPLATE expected = <<-END_EXPECTED

yes

END_EXPECTED assert_template_result(expected, text) text = <<-END_TEMPLATE

{% if false -%} no {% endif -%}

END_TEMPLATE expected = <<-END_EXPECTED

END_EXPECTED assert_template_result(expected, text) end def test_pre_and_post_trim_tags text = <<-END_TEMPLATE

{%- if true %} yes {% endif -%}

END_TEMPLATE expected = <<-END_EXPECTED

yes

END_EXPECTED assert_template_result(expected, text) text = <<-END_TEMPLATE

{%- if false %} no {% endif -%}

END_TEMPLATE expected = <<-END_EXPECTED

END_EXPECTED assert_template_result(expected, text) end def test_post_and_pre_trim_tags text = <<-END_TEMPLATE

{% if true -%} yes {%- endif %}

END_TEMPLATE expected = <<-END_EXPECTED

yes

END_EXPECTED assert_template_result(expected, text) whitespace = ' ' text = <<-END_TEMPLATE

{% if false -%} no {%- endif %}

END_TEMPLATE expected = <<~END_EXPECTED

#{whitespace}

END_EXPECTED assert_template_result(expected, text) end def test_trim_output text = <<-END_TEMPLATE

{{- 'John' -}}

END_TEMPLATE expected = <<-END_EXPECTED

John

END_EXPECTED assert_template_result(expected, text) end def test_trim_tags text = <<-END_TEMPLATE

{%- if true -%} yes {%- endif -%}

END_TEMPLATE expected = <<-END_EXPECTED

yes

END_EXPECTED assert_template_result(expected, text) text = <<-END_TEMPLATE

{%- if false -%} no {%- endif -%}

END_TEMPLATE expected = <<-END_EXPECTED

END_EXPECTED assert_template_result(expected, text) end def test_whitespace_trim_output text = <<-END_TEMPLATE

{{- 'John' -}}, {{- '30' -}}

END_TEMPLATE expected = <<-END_EXPECTED

John,30

END_EXPECTED assert_template_result(expected, text) end def test_whitespace_trim_tags text = <<-END_TEMPLATE

{%- if true -%} yes {%- endif -%}

END_TEMPLATE expected = <<-END_EXPECTED

yes

END_EXPECTED assert_template_result(expected, text) text = <<-END_TEMPLATE

{%- if false -%} no {%- endif -%}

END_TEMPLATE expected = <<-END_EXPECTED

END_EXPECTED assert_template_result(expected, text) end def test_complex_trim_output text = <<-END_TEMPLATE

{{- 'John' -}} {{- '30' -}}

{{ 'John' -}} {{- '30' }} {{- 'John' }} {{ '30' -}}
END_TEMPLATE expected = <<-END_EXPECTED

John30

John30 John 30
END_EXPECTED assert_template_result(expected, text) end def test_complex_trim text = <<-END_TEMPLATE
{%- if true -%} {%- if true -%}

{{- 'John' -}}

{%- endif -%} {%- endif -%}
END_TEMPLATE expected = <<-END_EXPECTED

John

END_EXPECTED assert_template_result(expected, text) end def test_right_trim_followed_by_tag assert_template_result('ab c', '{{ "a" -}}{{ "b" }} c') end def test_raw_output whitespace = ' ' text = <<-END_TEMPLATE
{% raw %} {%- if true -%}

{{- 'John' -}}

{%- endif -%} {% endraw %}
END_TEMPLATE expected = <<~END_EXPECTED
#{whitespace} {%- if true -%}

{{- 'John' -}}

{%- endif -%} #{whitespace}
END_EXPECTED assert_template_result(expected, text) end def test_pre_trim_blank_preceding_text template = Liquid::Template.parse("\n{%- raw %}{% endraw %}") assert_equal("", template.render) template = Liquid::Template.parse("\n{%- if true %}{% endif %}") assert_equal("", template.render) template = Liquid::Template.parse("{{ 'B' }} \n{%- if true %}C{% endif %}") assert_equal("BC", template.render) end def test_bug_compatible_pre_trim template = Liquid::Template.parse("\n {%- raw %}{% endraw %}", bug_compatible_whitespace_trimming: true) assert_equal("\n", template.render) template = Liquid::Template.parse("\n {%- if true %}{% endif %}", bug_compatible_whitespace_trimming: true) assert_equal("\n", template.render) template = Liquid::Template.parse("{{ 'B' }} \n{%- if true %}C{% endif %}", bug_compatible_whitespace_trimming: true) assert_equal("B C", template.render) template = Liquid::Template.parse("B\n {%- raw %}{% endraw %}", bug_compatible_whitespace_trimming: true) assert_equal("B", template.render) template = Liquid::Template.parse("B\n {%- if true %}{% endif %}", bug_compatible_whitespace_trimming: true) assert_equal("B", template.render) end def test_trim_blank assert_template_result('foobar', 'foo {{-}} bar') end end # TrimModeTest liquid-5.4.0/test/integration/variable_test.rb000066400000000000000000000126631427076730100215010ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class VariableTest < Minitest::Test include Liquid def test_simple_variable template = Template.parse(%({{test}})) assert_equal('worked', template.render!('test' => 'worked')) assert_equal('worked wonderfully', template.render!('test' => 'worked wonderfully')) end def test_variable_render_calls_to_liquid assert_template_result('foobar', '{{ foo }}', 'foo' => ThingWithToLiquid.new) end def test_variable_lookup_calls_to_liquid_value assert_template_result('1', '{{ foo }}', 'foo' => IntegerDrop.new('1')) assert_template_result('2', '{{ list[foo] }}', 'foo' => IntegerDrop.new('1'), 'list' => [1, 2, 3]) assert_template_result('one', '{{ list[foo] }}', 'foo' => IntegerDrop.new('1'), 'list' => { 1 => 'one' }) assert_template_result('Yay', '{{ foo }}', 'foo' => BooleanDrop.new(true)) assert_template_result('YAY', '{{ foo | upcase }}', 'foo' => BooleanDrop.new(true)) end def test_if_tag_calls_to_liquid_value assert_template_result('one', '{% if foo == 1 %}one{% endif %}', 'foo' => IntegerDrop.new('1')) assert_template_result('one', '{% if 0 < foo %}one{% endif %}', 'foo' => IntegerDrop.new('1')) assert_template_result('one', '{% if foo > 0 %}one{% endif %}', 'foo' => IntegerDrop.new('1')) assert_template_result('true', '{% if foo == true %}true{% endif %}', 'foo' => BooleanDrop.new(true)) assert_template_result('true', '{% if foo %}true{% endif %}', 'foo' => BooleanDrop.new(true)) assert_template_result('', '{% if foo %}true{% endif %}', 'foo' => BooleanDrop.new(false)) assert_template_result('', '{% if foo == true %}True{% endif %}', 'foo' => BooleanDrop.new(false)) end def test_unless_tag_calls_to_liquid_value assert_template_result('', '{% unless foo %}true{% endunless %}', 'foo' => BooleanDrop.new(true)) end def test_case_tag_calls_to_liquid_value assert_template_result('One', '{% case foo %}{% when 1 %}One{% endcase %}', 'foo' => IntegerDrop.new('1')) end def test_simple_with_whitespaces template = Template.parse(%( {{ test }} )) assert_equal(' worked ', template.render!('test' => 'worked')) assert_equal(' worked wonderfully ', template.render!('test' => 'worked wonderfully')) end def test_expression_with_whitespace_in_square_brackets assert_template_result('result', "{{ a[ 'b' ] }}", 'a' => { 'b' => 'result' }) assert_template_result('result', "{{ a[ [ 'b' ] ] }}", 'b' => 'c', 'a' => { 'c' => 'result' }) end def test_ignore_unknown template = Template.parse(%({{ test }})) assert_equal('', template.render!) end def test_using_blank_as_variable_name template = Template.parse("{% assign foo = blank %}{{ foo }}") assert_equal('', template.render!) end def test_using_empty_as_variable_name template = Template.parse("{% assign foo = empty %}{{ foo }}") assert_equal('', template.render!) end def test_hash_scoping assert_template_result('worked', "{{ test.test }}", 'test' => { 'test' => 'worked' }) assert_template_result('worked', "{{ test . test }}", 'test' => { 'test' => 'worked' }) end def test_false_renders_as_false assert_equal('false', Template.parse("{{ foo }}").render!('foo' => false)) assert_equal('false', Template.parse("{{ false }}").render!) end def test_nil_renders_as_empty_string assert_equal('', Template.parse("{{ nil }}").render!) assert_equal('cat', Template.parse("{{ nil | append: 'cat' }}").render!) end def test_preset_assigns template = Template.parse(%({{ test }})) template.assigns['test'] = 'worked' assert_equal('worked', template.render!) end def test_reuse_parsed_template template = Template.parse(%({{ greeting }} {{ name }})) template.assigns['greeting'] = 'Goodbye' assert_equal('Hello Tobi', template.render!('greeting' => 'Hello', 'name' => 'Tobi')) assert_equal('Hello ', template.render!('greeting' => 'Hello', 'unknown' => 'Tobi')) assert_equal('Hello Brian', template.render!('greeting' => 'Hello', 'name' => 'Brian')) assert_equal('Goodbye Brian', template.render!('name' => 'Brian')) assert_equal({ 'greeting' => 'Goodbye' }, template.assigns) end def test_assigns_not_polluted_from_template template = Template.parse(%({{ test }}{% assign test = 'bar' %}{{ test }})) template.assigns['test'] = 'baz' assert_equal('bazbar', template.render!) assert_equal('bazbar', template.render!) assert_equal('foobar', template.render!('test' => 'foo')) assert_equal('bazbar', template.render!) end def test_hash_with_default_proc template = Template.parse(%(Hello {{ test }})) assigns = Hash.new { |_h, k| raise "Unknown variable '#{k}'" } assigns['test'] = 'Tobi' assert_equal('Hello Tobi', template.render!(assigns)) assigns.delete('test') e = assert_raises(RuntimeError) do template.render!(assigns) end assert_equal("Unknown variable 'test'", e.message) end def test_multiline_variable assert_equal('worked', Template.parse("{{\ntest\n}}").render!('test' => 'worked')) end def test_render_symbol assert_template_result('bar', '{{ foo }}', 'foo' => :bar) end def test_dynamic_find_var assert_template_result('bar', '{{ [key] }}', 'key' => 'foo', 'foo' => 'bar') end def test_raw_value_variable assert_template_result('bar', '{{ [key] }}', 'key' => 'foo', 'foo' => 'bar') end end liquid-5.4.0/test/test_helper.rb000077500000000000000000000112031427076730100166400ustar00rootroot00000000000000#!/usr/bin/env ruby # frozen_string_literal: true ENV["MT_NO_EXPECTATIONS"] = "1" require 'minitest/autorun' $LOAD_PATH.unshift(File.join(File.expand_path(__dir__), '..', 'lib')) require 'liquid.rb' require 'liquid/profiler' mode = :strict if (env_mode = ENV['LIQUID_PARSER_MODE']) puts "-- #{env_mode.upcase} ERROR MODE" mode = env_mode.to_sym end Liquid::Template.error_mode = mode if ENV['LIQUID_C'] == '1' puts "-- LIQUID C" require 'liquid/c' end if Minitest.const_defined?('Test') # We're on Minitest 5+. Nothing to do here. else # Minitest 4 doesn't have Minitest::Test yet. Minitest::Test = MiniTest::Unit::TestCase end module Minitest class Test def fixture(name) File.join(File.expand_path(__dir__), "fixtures", name) end end module Assertions include Liquid def assert_template_result(expected, template, assigns = {}, message = nil) assert_equal(expected, Template.parse(template, line_numbers: true).render!(assigns), message) end def assert_template_result_matches(expected, template, assigns = {}, message = nil) return assert_template_result(expected, template, assigns, message) unless expected.is_a?(Regexp) assert_match(expected, Template.parse(template, line_numbers: true).render!(assigns), message) end def assert_match_syntax_error(match, template, assigns = {}) exception = assert_raises(Liquid::SyntaxError) do Template.parse(template, line_numbers: true).render(assigns) end assert_match(match, exception.message) end def assert_usage_increment(name, times: 1) old_method = Liquid::Usage.method(:increment) calls = 0 begin Liquid::Usage.singleton_class.send(:remove_method, :increment) Liquid::Usage.define_singleton_method(:increment) do |got_name| calls += 1 if got_name == name old_method.call(got_name) end yield ensure Liquid::Usage.singleton_class.send(:remove_method, :increment) Liquid::Usage.define_singleton_method(:increment, old_method) end assert_equal(times, calls, "Number of calls to Usage.increment with #{name.inspect}") end def with_global_filter(*globals) original_global_cache = Liquid::StrainerFactory::GlobalCache Liquid::StrainerFactory.send(:remove_const, :GlobalCache) Liquid::StrainerFactory.const_set(:GlobalCache, Class.new(Liquid::StrainerTemplate)) globals.each do |global| Liquid::Template.register_filter(global) end Liquid::StrainerFactory.send(:strainer_class_cache).clear begin yield ensure Liquid::StrainerFactory.send(:remove_const, :GlobalCache) Liquid::StrainerFactory.const_set(:GlobalCache, original_global_cache) Liquid::StrainerFactory.send(:strainer_class_cache).clear end end def with_error_mode(mode) old_mode = Liquid::Template.error_mode Liquid::Template.error_mode = mode yield ensure Liquid::Template.error_mode = old_mode end def with_custom_tag(tag_name, tag_class) old_tag = Liquid::Template.tags[tag_name] begin Liquid::Template.register_tag(tag_name, tag_class) yield ensure if old_tag Liquid::Template.tags[tag_name] = old_tag else Liquid::Template.tags.delete(tag_name) end end end end end class ThingWithToLiquid def to_liquid 'foobar' end end class IntegerDrop < Liquid::Drop def initialize(value) super() @value = value.to_i end def ==(other) @value == other end def to_s @value.to_s end def to_liquid_value @value end end class BooleanDrop < Liquid::Drop def initialize(value) super() @value = value end def ==(other) @value == other end def to_liquid_value @value end def to_s @value ? "Yay" : "Nay" end end class ErrorDrop < Liquid::Drop def standard_error raise Liquid::StandardError, 'standard error' end def argument_error raise Liquid::ArgumentError, 'argument error' end def syntax_error raise Liquid::SyntaxError, 'syntax error' end def runtime_error raise 'runtime error' end def exception raise Exception, 'exception' end end class StubFileSystem attr_reader :file_read_count def initialize(values) @file_read_count = 0 @values = values end def read_template_file(template_path) @file_read_count += 1 @values.fetch(template_path) end end class StubTemplateFactory attr_reader :count def initialize @count = 0 end def for(_template_name) @count += 1 Liquid::Template.new end end liquid-5.4.0/test/unit/000077500000000000000000000000001427076730100147545ustar00rootroot00000000000000liquid-5.4.0/test/unit/block_unit_test.rb000066400000000000000000000031731427076730100204750ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class BlockUnitTest < Minitest::Test include Liquid def test_blankspace template = Liquid::Template.parse(" ") assert_equal([" "], template.root.nodelist) end def test_variable_beginning template = Liquid::Template.parse("{{funk}} ") assert_equal(2, template.root.nodelist.size) assert_equal(Variable, template.root.nodelist[0].class) assert_equal(String, template.root.nodelist[1].class) end def test_variable_end template = Liquid::Template.parse(" {{funk}}") assert_equal(2, template.root.nodelist.size) assert_equal(String, template.root.nodelist[0].class) assert_equal(Variable, template.root.nodelist[1].class) end def test_variable_middle template = Liquid::Template.parse(" {{funk}} ") assert_equal(3, template.root.nodelist.size) assert_equal(String, template.root.nodelist[0].class) assert_equal(Variable, template.root.nodelist[1].class) assert_equal(String, template.root.nodelist[2].class) end def test_variable_many_embedded_fragments template = Liquid::Template.parse(" {{funk}} {{so}} {{brother}} ") assert_equal(7, template.root.nodelist.size) assert_equal([String, Variable, String, Variable, String, Variable, String], block_types(template.root.nodelist)) end def test_with_block template = Liquid::Template.parse(" {% comment %} {% endcomment %} ") assert_equal([String, Comment, String], block_types(template.root.nodelist)) assert_equal(3, template.root.nodelist.size) end private def block_types(nodelist) nodelist.collect(&:class) end end liquid-5.4.0/test/unit/condition_unit_test.rb000066400000000000000000000135161427076730100213730ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class ConditionUnitTest < Minitest::Test include Liquid def setup @context = Liquid::Context.new end def test_basic_condition assert_equal(false, Condition.new(1, '==', 2).evaluate(Context.new)) assert_equal(true, Condition.new(1, '==', 1).evaluate(Context.new)) end def test_default_operators_evalute_true assert_evaluates_true(1, '==', 1) assert_evaluates_true(1, '!=', 2) assert_evaluates_true(1, '<>', 2) assert_evaluates_true(1, '<', 2) assert_evaluates_true(2, '>', 1) assert_evaluates_true(1, '>=', 1) assert_evaluates_true(2, '>=', 1) assert_evaluates_true(1, '<=', 2) assert_evaluates_true(1, '<=', 1) # negative numbers assert_evaluates_true(1, '>', -1) assert_evaluates_true(-1, '<', 1) assert_evaluates_true(1.0, '>', -1.0) assert_evaluates_true(-1.0, '<', 1.0) end def test_default_operators_evalute_false assert_evaluates_false(1, '==', 2) assert_evaluates_false(1, '!=', 1) assert_evaluates_false(1, '<>', 1) assert_evaluates_false(1, '<', 0) assert_evaluates_false(2, '>', 4) assert_evaluates_false(1, '>=', 3) assert_evaluates_false(2, '>=', 4) assert_evaluates_false(1, '<=', 0) assert_evaluates_false(1, '<=', 0) end def test_contains_works_on_strings assert_evaluates_true('bob', 'contains', 'o') assert_evaluates_true('bob', 'contains', 'b') assert_evaluates_true('bob', 'contains', 'bo') assert_evaluates_true('bob', 'contains', 'ob') assert_evaluates_true('bob', 'contains', 'bob') assert_evaluates_false('bob', 'contains', 'bob2') assert_evaluates_false('bob', 'contains', 'a') assert_evaluates_false('bob', 'contains', '---') end def test_invalid_comparation_operator assert_evaluates_argument_error(1, '~~', 0) end def test_comparation_of_int_and_str assert_evaluates_argument_error('1', '>', 0) assert_evaluates_argument_error('1', '<', 0) assert_evaluates_argument_error('1', '>=', 0) assert_evaluates_argument_error('1', '<=', 0) end def test_hash_compare_backwards_compatibility assert_nil(Condition.new({}, '>', 2).evaluate(Context.new)) assert_nil(Condition.new(2, '>', {}).evaluate(Context.new)) assert_equal(false, Condition.new({}, '==', 2).evaluate(Context.new)) assert_equal(true, Condition.new({ 'a' => 1 }, '==', 'a' => 1).evaluate(Context.new)) assert_equal(true, Condition.new({ 'a' => 2 }, 'contains', 'a').evaluate(Context.new)) end def test_contains_works_on_arrays @context = Liquid::Context.new @context['array'] = [1, 2, 3, 4, 5] array_expr = VariableLookup.new("array") assert_evaluates_false(array_expr, 'contains', 0) assert_evaluates_true(array_expr, 'contains', 1) assert_evaluates_true(array_expr, 'contains', 2) assert_evaluates_true(array_expr, 'contains', 3) assert_evaluates_true(array_expr, 'contains', 4) assert_evaluates_true(array_expr, 'contains', 5) assert_evaluates_false(array_expr, 'contains', 6) assert_evaluates_false(array_expr, 'contains', "1") end def test_contains_returns_false_for_nil_operands @context = Liquid::Context.new assert_evaluates_false(VariableLookup.new('not_assigned'), 'contains', '0') assert_evaluates_false(0, 'contains', VariableLookup.new('not_assigned')) end def test_contains_return_false_on_wrong_data_type assert_evaluates_false(1, 'contains', 0) end def test_contains_with_string_left_operand_coerces_right_operand_to_string assert_evaluates_true(' 1 ', 'contains', 1) assert_evaluates_false(' 1 ', 'contains', 2) end def test_or_condition condition = Condition.new(1, '==', 2) assert_equal(false, condition.evaluate(Context.new)) condition.or(Condition.new(2, '==', 1)) assert_equal(false, condition.evaluate(Context.new)) condition.or(Condition.new(1, '==', 1)) assert_equal(true, condition.evaluate(Context.new)) end def test_and_condition condition = Condition.new(1, '==', 1) assert_equal(true, condition.evaluate(Context.new)) condition.and(Condition.new(2, '==', 2)) assert_equal(true, condition.evaluate(Context.new)) condition.and(Condition.new(2, '==', 1)) assert_equal(false, condition.evaluate(Context.new)) end def test_should_allow_custom_proc_operator Condition.operators['starts_with'] = proc { |_cond, left, right| left =~ /^#{right}/ } assert_evaluates_true('bob', 'starts_with', 'b') assert_evaluates_false('bob', 'starts_with', 'o') ensure Condition.operators.delete('starts_with') end def test_left_or_right_may_contain_operators @context = Liquid::Context.new @context['one'] = @context['another'] = "gnomeslab-and-or-liquid" assert_evaluates_true(VariableLookup.new("one"), '==', VariableLookup.new("another")) end def test_default_context_is_deprecated if Gem::Version.new(Liquid::VERSION) >= Gem::Version.new('6.0.0') flunk("Condition#evaluate without a context argument is to be removed") end _out, err = capture_io do assert_equal(true, Condition.new(1, '==', 1).evaluate) end expected = "DEPRECATION WARNING: Condition#evaluate without a context argument is deprecated" \ " and will be removed from Liquid 6.0.0." assert_includes(err.lines.map(&:strip), expected) end private def assert_evaluates_true(left, op, right) assert(Condition.new(left, op, right).evaluate(@context), "Evaluated false: #{left} #{op} #{right}") end def assert_evaluates_false(left, op, right) assert(!Condition.new(left, op, right).evaluate(@context), "Evaluated true: #{left} #{op} #{right}") end def assert_evaluates_argument_error(left, op, right) assert_raises(Liquid::ArgumentError) do Condition.new(left, op, right).evaluate(@context) end end end # ConditionTest liquid-5.4.0/test/unit/file_system_unit_test.rb000066400000000000000000000021211427076730100217160ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class FileSystemUnitTest < Minitest::Test include Liquid def test_default assert_raises(FileSystemError) do BlankFileSystem.new.read_template_file("dummy") end end def test_local file_system = Liquid::LocalFileSystem.new("/some/path") assert_equal("/some/path/_mypartial.liquid", file_system.full_path("mypartial")) assert_equal("/some/path/dir/_mypartial.liquid", file_system.full_path("dir/mypartial")) assert_raises(FileSystemError) do file_system.full_path("../dir/mypartial") end assert_raises(FileSystemError) do file_system.full_path("/dir/../../dir/mypartial") end assert_raises(FileSystemError) do file_system.full_path("/etc/passwd") end end def test_custom_template_filename_patterns file_system = Liquid::LocalFileSystem.new("/some/path", "%s.html") assert_equal("/some/path/mypartial.html", file_system.full_path("mypartial")) assert_equal("/some/path/dir/mypartial.html", file_system.full_path("dir/mypartial")) end end # FileSystemTest liquid-5.4.0/test/unit/i18n_unit_test.rb000066400000000000000000000017051427076730100201610ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class I18nUnitTest < Minitest::Test include Liquid def setup @i18n = I18n.new(fixture("en_locale.yml")) end def test_simple_translate_string assert_equal("less is more", @i18n.translate("simple")) end def test_nested_translate_string assert_equal("something wasn't right", @i18n.translate("errors.syntax.oops")) end def test_single_string_interpolation assert_equal("something different", @i18n.translate("whatever", something: "different")) end # def test_raises_translation_error_on_undefined_interpolation_key # assert_raises I18n::TranslationError do # @i18n.translate("whatever", :oopstypos => "yes") # end # end def test_raises_unknown_translation assert_raises(I18n::TranslationError) do @i18n.translate("doesnt_exist") end end def test_sets_default_path_to_en assert_equal(I18n::DEFAULT_LOCALE, I18n.new.path) end end liquid-5.4.0/test/unit/lexer_unit_test.rb000066400000000000000000000031471427076730100205230ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class LexerUnitTest < Minitest::Test include Liquid def test_strings tokens = Lexer.new(%( 'this is a test""' "wat 'lol'")).tokenize assert_equal([[:string, %('this is a test""')], [:string, %("wat 'lol'")], [:end_of_string]], tokens) end def test_integer tokens = Lexer.new('hi 50').tokenize assert_equal([[:id, 'hi'], [:number, '50'], [:end_of_string]], tokens) end def test_float tokens = Lexer.new('hi 5.0').tokenize assert_equal([[:id, 'hi'], [:number, '5.0'], [:end_of_string]], tokens) end def test_comparison tokens = Lexer.new('== <> contains ').tokenize assert_equal([[:comparison, '=='], [:comparison, '<>'], [:comparison, 'contains'], [:end_of_string]], tokens) end def test_specials tokens = Lexer.new('| .:').tokenize assert_equal([[:pipe, '|'], [:dot, '.'], [:colon, ':'], [:end_of_string]], tokens) tokens = Lexer.new('[,]').tokenize assert_equal([[:open_square, '['], [:comma, ','], [:close_square, ']'], [:end_of_string]], tokens) end def test_fancy_identifiers tokens = Lexer.new('hi five?').tokenize assert_equal([[:id, 'hi'], [:id, 'five?'], [:end_of_string]], tokens) tokens = Lexer.new('2foo').tokenize assert_equal([[:number, '2'], [:id, 'foo'], [:end_of_string]], tokens) end def test_whitespace tokens = Lexer.new("five|\n\t ==").tokenize assert_equal([[:id, 'five'], [:pipe, '|'], [:comparison, '=='], [:end_of_string]], tokens) end def test_unexpected_character assert_raises(SyntaxError) do Lexer.new("%").tokenize end end end liquid-5.4.0/test/unit/parse_tree_visitor_test.rb000066400000000000000000000116301427076730100222510ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class ParseTreeVisitorTest < Minitest::Test include Liquid def test_variable assert_equal( ["test"], visit(%({{ test }})) ) end def test_varible_with_filter assert_equal( ["test", "infilter"], visit(%({{ test | split: infilter }})) ) end def test_dynamic_variable assert_equal( ["test", "inlookup"], visit(%({{ test[inlookup] }})) ) end def test_echo assert_equal( ["test"], visit(%({% echo test %})) ) end def test_if_condition assert_equal( ["test"], visit(%({% if test %}{% endif %})) ) end def test_complex_if_condition assert_equal( ["test"], visit(%({% if 1 == 1 and 2 == test %}{% endif %})) ) end def test_if_body assert_equal( ["test"], visit(%({% if 1 == 1 %}{{ test }}{% endif %})) ) end def test_unless_condition assert_equal( ["test"], visit(%({% unless test %}{% endunless %})) ) end def test_complex_unless_condition assert_equal( ["test"], visit(%({% unless 1 == 1 and 2 == test %}{% endunless %})) ) end def test_unless_body assert_equal( ["test"], visit(%({% unless 1 == 1 %}{{ test }}{% endunless %})) ) end def test_elsif_condition assert_equal( ["test"], visit(%({% if 1 == 1 %}{% elsif test %}{% endif %})) ) end def test_complex_elsif_condition assert_equal( ["test"], visit(%({% if 1 == 1 %}{% elsif 1 == 1 and 2 == test %}{% endif %})) ) end def test_elsif_body assert_equal( ["test"], visit(%({% if 1 == 1 %}{% elsif 2 == 2 %}{{ test }}{% endif %})) ) end def test_else_body assert_equal( ["test"], visit(%({% if 1 == 1 %}{% else %}{{ test }}{% endif %})) ) end def test_case_left assert_equal( ["test"], visit(%({% case test %}{% endcase %})) ) end def test_case_condition assert_equal( ["test"], visit(%({% case 1 %}{% when test %}{% endcase %})) ) end def test_case_when_body assert_equal( ["test"], visit(%({% case 1 %}{% when 2 %}{{ test }}{% endcase %})) ) end def test_case_else_body assert_equal( ["test"], visit(%({% case 1 %}{% else %}{{ test }}{% endcase %})) ) end def test_for_in assert_equal( ["test"], visit(%({% for x in test %}{% endfor %})) ) end def test_for_limit assert_equal( ["test"], visit(%({% for x in (1..5) limit: test %}{% endfor %})) ) end def test_for_offset assert_equal( ["test"], visit(%({% for x in (1..5) offset: test %}{% endfor %})) ) end def test_for_body assert_equal( ["test"], visit(%({% for x in (1..5) %}{{ test }}{% endfor %})) ) end def test_for_range assert_equal( ["test"], visit(%({% for x in (1..test) %}{% endfor %})) ) end def test_tablerow_in assert_equal( ["test"], visit(%({% tablerow x in test %}{% endtablerow %})) ) end def test_tablerow_limit assert_equal( ["test"], visit(%({% tablerow x in (1..5) limit: test %}{% endtablerow %})) ) end def test_tablerow_offset assert_equal( ["test"], visit(%({% tablerow x in (1..5) offset: test %}{% endtablerow %})) ) end def test_tablerow_body assert_equal( ["test"], visit(%({% tablerow x in (1..5) %}{{ test }}{% endtablerow %})) ) end def test_cycle assert_equal( ["test"], visit(%({% cycle test %})) ) end def test_assign assert_equal( ["test"], visit(%({% assign x = test %})) ) end def test_capture assert_equal( ["test"], visit(%({% capture x %}{{ test }}{% endcapture %})) ) end def test_include assert_equal( ["test"], visit(%({% include test %})) ) end def test_include_with assert_equal( ["test"], visit(%({% include "hai" with test %})) ) end def test_include_for assert_equal( ["test"], visit(%({% include "hai" for test %})) ) end def test_render_with assert_equal( ["test"], visit(%({% render "hai" with test %})) ) end def test_render_for assert_equal( ["test"], visit(%({% render "hai" for test %})) ) end def test_preserve_tree_structure assert_equal( [[nil, [ [nil, [[nil, [["other", []]]]]], ["test", []], ["xs", []], ]]], traversal(%({% for x in xs offset: test %}{{ other }}{% endfor %})).visit ) end private def traversal(template) ParseTreeVisitor .for(Template.parse(template).root) .add_callback_for(VariableLookup) { |node| node.name } # rubocop:disable Style/SymbolProc end def visit(template) traversal(template).visit.flatten.compact end end liquid-5.4.0/test/unit/parser_unit_test.rb000066400000000000000000000044251427076730100207000ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class ParserUnitTest < Minitest::Test include Liquid def test_consume p = Parser.new("wat: 7") assert_equal('wat', p.consume(:id)) assert_equal(':', p.consume(:colon)) assert_equal('7', p.consume(:number)) end def test_jump p = Parser.new("wat: 7") p.jump(2) assert_equal('7', p.consume(:number)) end def test_consume? p = Parser.new("wat: 7") assert_equal('wat', p.consume?(:id)) assert_equal(false, p.consume?(:dot)) assert_equal(':', p.consume(:colon)) assert_equal('7', p.consume?(:number)) end def test_id? p = Parser.new("wat 6 Peter Hegemon") assert_equal('wat', p.id?('wat')) assert_equal(false, p.id?('endgame')) assert_equal('6', p.consume(:number)) assert_equal('Peter', p.id?('Peter')) assert_equal(false, p.id?('Achilles')) end def test_look p = Parser.new("wat 6 Peter Hegemon") assert_equal(true, p.look(:id)) assert_equal('wat', p.consume(:id)) assert_equal(false, p.look(:comparison)) assert_equal(true, p.look(:number)) assert_equal(true, p.look(:id, 1)) assert_equal(false, p.look(:number, 1)) end def test_expressions p = Parser.new("hi.there hi?[5].there? hi.there.bob") assert_equal('hi.there', p.expression) assert_equal('hi?[5].there?', p.expression) assert_equal('hi.there.bob', p.expression) p = Parser.new("567 6.0 'lol' \"wut\"") assert_equal('567', p.expression) assert_equal('6.0', p.expression) assert_equal("'lol'", p.expression) assert_equal('"wut"', p.expression) end def test_ranges p = Parser.new("(5..7) (1.5..9.6) (young..old) (hi[5].wat..old)") assert_equal('(5..7)', p.expression) assert_equal('(1.5..9.6)', p.expression) assert_equal('(young..old)', p.expression) assert_equal('(hi[5].wat..old)', p.expression) end def test_arguments p = Parser.new("filter: hi.there[5], keyarg: 7") assert_equal('filter', p.consume(:id)) assert_equal(':', p.consume(:colon)) assert_equal('hi.there[5]', p.argument) assert_equal(',', p.consume(:comma)) assert_equal('keyarg: 7', p.argument) end def test_invalid_expression assert_raises(SyntaxError) do p = Parser.new("==") p.expression end end end liquid-5.4.0/test/unit/partial_cache_unit_test.rb000066400000000000000000000101531427076730100221560ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class PartialCacheUnitTest < Minitest::Test def test_uses_the_file_system_register_if_present context = Liquid::Context.build( registers: { file_system: StubFileSystem.new('my_partial' => 'my partial body'), } ) partial = Liquid::PartialCache.load( 'my_partial', context: context, parse_context: Liquid::ParseContext.new ) assert_equal('my partial body', partial.render) end def test_reads_from_the_file_system_only_once_per_file file_system = StubFileSystem.new('my_partial' => 'some partial body') context = Liquid::Context.build( registers: { file_system: file_system } ) 2.times do Liquid::PartialCache.load( 'my_partial', context: context, parse_context: Liquid::ParseContext.new ) end assert_equal(1, file_system.file_read_count) end def test_cache_state_is_stored_per_context parse_context = Liquid::ParseContext.new shared_file_system = StubFileSystem.new( 'my_partial' => 'my shared value' ) context_one = Liquid::Context.build( registers: { file_system: shared_file_system, } ) context_two = Liquid::Context.build( registers: { file_system: shared_file_system, } ) 2.times do Liquid::PartialCache.load( 'my_partial', context: context_one, parse_context: parse_context ) end Liquid::PartialCache.load( 'my_partial', context: context_two, parse_context: parse_context ) assert_equal(2, shared_file_system.file_read_count) end def test_cache_is_not_broken_when_a_different_parse_context_is_used file_system = StubFileSystem.new('my_partial' => 'some partial body') context = Liquid::Context.build( registers: { file_system: file_system } ) Liquid::PartialCache.load( 'my_partial', context: context, parse_context: Liquid::ParseContext.new(my_key: 'value one') ) Liquid::PartialCache.load( 'my_partial', context: context, parse_context: Liquid::ParseContext.new(my_key: 'value two') ) # Technically what we care about is that the file was parsed twice, # but measuring file reads is an OK proxy for this. assert_equal(1, file_system.file_read_count) end def test_uses_default_template_factory_when_no_template_factory_found_in_register context = Liquid::Context.build( registers: { file_system: StubFileSystem.new('my_partial' => 'my partial body'), } ) partial = Liquid::PartialCache.load( 'my_partial', context: context, parse_context: Liquid::ParseContext.new ) assert_equal('my partial body', partial.render) end def test_uses_template_factory_register_if_present template_factory = StubTemplateFactory.new context = Liquid::Context.build( registers: { file_system: StubFileSystem.new('my_partial' => 'my partial body'), template_factory: template_factory, } ) partial = Liquid::PartialCache.load( 'my_partial', context: context, parse_context: Liquid::ParseContext.new ) assert_equal('my partial body', partial.render) assert_equal(1, template_factory.count) end def test_cache_state_is_shared_for_subcontexts parse_context = Liquid::ParseContext.new shared_file_system = StubFileSystem.new( 'my_partial' => 'my shared value' ) context = Liquid::Context.build( registers: Liquid::Registers.new( file_system: shared_file_system, ) ) subcontext = context.new_isolated_subcontext assert_equal(subcontext.registers[:cached_partials].object_id, context.registers[:cached_partials].object_id) 2.times do Liquid::PartialCache.load( 'my_partial', context: context, parse_context: parse_context ) Liquid::PartialCache.load( 'my_partial', context: subcontext, parse_context: parse_context ) end assert_equal(1, shared_file_system.file_read_count) end end liquid-5.4.0/test/unit/regexp_unit_test.rb000066400000000000000000000031611427076730100206720ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class RegexpUnitTest < Minitest::Test include Liquid def test_empty assert_equal([], ''.scan(QuotedFragment)) end def test_quote assert_equal(['"arg 1"'], '"arg 1"'.scan(QuotedFragment)) end def test_words assert_equal(['arg1', 'arg2'], 'arg1 arg2'.scan(QuotedFragment)) end def test_tags assert_equal(['', ''], ' '.scan(QuotedFragment)) assert_equal([''], ''.scan(QuotedFragment)) assert_equal(['', ''], %().scan(QuotedFragment)) end def test_double_quoted_words assert_equal(['arg1', 'arg2', '"arg 3"'], 'arg1 arg2 "arg 3"'.scan(QuotedFragment)) end def test_single_quoted_words assert_equal(['arg1', 'arg2', "'arg 3'"], 'arg1 arg2 \'arg 3\''.scan(QuotedFragment)) end def test_quoted_words_in_the_middle assert_equal(['arg1', 'arg2', '"arg 3"', 'arg4'], 'arg1 arg2 "arg 3" arg4 '.scan(QuotedFragment)) end def test_variable_parser assert_equal(['var'], 'var'.scan(VariableParser)) assert_equal(['var', 'method'], 'var.method'.scan(VariableParser)) assert_equal(['var', '[method]'], 'var[method]'.scan(VariableParser)) assert_equal(['var', '[method]', '[0]'], 'var[method][0]'.scan(VariableParser)) assert_equal(['var', '["method"]', '[0]'], 'var["method"][0]'.scan(VariableParser)) assert_equal(['var', '[method]', '[0]', 'method'], 'var[method][0].method'.scan(VariableParser)) end end # RegexpTest liquid-5.4.0/test/unit/registers_unit_test.rb000066400000000000000000000103571427076730100214140ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class RegistersUnitTest < Minitest::Test include Liquid def test_set static_register = Registers.new(a: 1, b: 2) static_register[:b] = 22 static_register[:c] = 33 assert_equal(1, static_register[:a]) assert_equal(22, static_register[:b]) assert_equal(33, static_register[:c]) end def test_get_missing_key static_register = Registers.new assert_nil(static_register[:missing]) end def test_delete static_register = Registers.new(a: 1, b: 2) static_register[:b] = 22 static_register[:c] = 33 assert_nil(static_register.delete(:a)) assert_equal(22, static_register.delete(:b)) assert_equal(33, static_register.delete(:c)) assert_nil(static_register[:c]) assert_nil(static_register.delete(:d)) end def test_fetch static_register = Registers.new(a: 1, b: 2) static_register[:b] = 22 static_register[:c] = 33 assert_equal(1, static_register.fetch(:a)) assert_equal(1, static_register.fetch(:a, "default")) assert_equal(22, static_register.fetch(:b)) assert_equal(22, static_register.fetch(:b, "default")) assert_equal(33, static_register.fetch(:c)) assert_equal(33, static_register.fetch(:c, "default")) assert_raises(KeyError) do static_register.fetch(:d) end assert_equal("default", static_register.fetch(:d, "default")) result = static_register.fetch(:d) { "default" } assert_equal("default", result) result = static_register.fetch(:d, "default 1") { "default 2" } assert_equal("default 2", result) end def test_key static_register = Registers.new(a: 1, b: 2) static_register[:b] = 22 static_register[:c] = 33 assert_equal(true, static_register.key?(:a)) assert_equal(true, static_register.key?(:b)) assert_equal(true, static_register.key?(:c)) assert_equal(false, static_register.key?(:d)) end def test_static_register_can_be_frozen static_register = Registers.new(a: 1) static_register.static.freeze assert_raises(RuntimeError) do static_register.static[:a] = "foo" end assert_raises(RuntimeError) do static_register.static[:b] = "foo" end assert_raises(RuntimeError) do static_register.static.delete(:a) end assert_raises(RuntimeError) do static_register.static.delete(:c) end end def test_new_static_retains_static static_register = Registers.new(a: 1, b: 2) static_register[:b] = 22 static_register[:c] = 33 new_static_register = Registers.new(static_register) new_static_register[:b] = 222 newest_static_register = Registers.new(new_static_register) newest_static_register[:c] = 333 assert_equal(1, static_register[:a]) assert_equal(22, static_register[:b]) assert_equal(33, static_register[:c]) assert_equal(1, new_static_register[:a]) assert_equal(222, new_static_register[:b]) assert_nil(new_static_register[:c]) assert_equal(1, newest_static_register[:a]) assert_equal(2, newest_static_register[:b]) assert_equal(333, newest_static_register[:c]) end def test_multiple_instances_are_unique static_register_1 = Registers.new(a: 1, b: 2) static_register_1[:b] = 22 static_register_1[:c] = 33 static_register_2 = Registers.new(a: 10, b: 20) static_register_2[:b] = 220 static_register_2[:c] = 330 assert_equal({ a: 1, b: 2 }, static_register_1.static) assert_equal(1, static_register_1[:a]) assert_equal(22, static_register_1[:b]) assert_equal(33, static_register_1[:c]) assert_equal({ a: 10, b: 20 }, static_register_2.static) assert_equal(10, static_register_2[:a]) assert_equal(220, static_register_2[:b]) assert_equal(330, static_register_2[:c]) end def test_initialization_reused_static_same_memory_object static_register_1 = Registers.new(a: 1, b: 2) static_register_1[:b] = 22 static_register_1[:c] = 33 static_register_2 = Registers.new(static_register_1) assert_equal(1, static_register_2[:a]) assert_equal(2, static_register_2[:b]) assert_nil(static_register_2[:c]) static_register_1.static[:b] = 222 static_register_1.static[:c] = 333 assert_same(static_register_1.static, static_register_2.static) end end liquid-5.4.0/test/unit/strainer_factory_unit_test.rb000066400000000000000000000062101427076730100227540ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StrainerFactoryUnitTest < Minitest::Test include Liquid module AccessScopeFilters def public_filter "public" end def private_filter "private" end private :private_filter end StrainerFactory.add_global_filter(AccessScopeFilters) module LateAddedFilter def late_added_filter(_input) "filtered" end end def setup @context = Context.build end def test_strainer strainer = StrainerFactory.create(@context) assert_equal(5, strainer.invoke('size', 'input')) assert_equal("public", strainer.invoke("public_filter")) end def test_stainer_raises_argument_error strainer = StrainerFactory.create(@context) assert_raises(Liquid::ArgumentError) do strainer.invoke("public_filter", 1) end end def test_stainer_argument_error_contains_backtrace strainer = StrainerFactory.create(@context) exception = assert_raises(Liquid::ArgumentError) do strainer.invoke("public_filter", 1) end assert_match( /\ALiquid error: wrong number of arguments \((1 for 0|given 1, expected 0)\)\z/, exception.message ) source = AccessScopeFilters.instance_method(:public_filter).source_location assert_equal(source.map(&:to_s), exception.backtrace[0].split(':')[0..1]) end def test_strainer_only_invokes_public_filter_methods strainer = StrainerFactory.create(@context) assert_equal(false, strainer.class.invokable?('__test__')) assert_equal(false, strainer.class.invokable?('test')) assert_equal(false, strainer.class.invokable?('instance_eval')) assert_equal(false, strainer.class.invokable?('__send__')) assert_equal(true, strainer.class.invokable?('size')) # from the standard lib end def test_strainer_returns_nil_if_no_filter_method_found strainer = StrainerFactory.create(@context) assert_nil(strainer.invoke("private_filter")) assert_nil(strainer.invoke("undef_the_filter")) end def test_strainer_returns_first_argument_if_no_method_and_arguments_given strainer = StrainerFactory.create(@context) assert_equal("password", strainer.invoke("undef_the_method", "password")) end def test_strainer_only_allows_methods_defined_in_filters strainer = StrainerFactory.create(@context) assert_equal("1 + 1", strainer.invoke("instance_eval", "1 + 1")) assert_equal("puts", strainer.invoke("__send__", "puts", "Hi Mom")) assert_equal("has_method?", strainer.invoke("invoke", "has_method?", "invoke")) end def test_strainer_uses_a_class_cache_to_avoid_method_cache_invalidation a = Module.new b = Module.new strainer = StrainerFactory.create(@context, [a, b]) assert_kind_of(StrainerTemplate, strainer) assert_kind_of(a, strainer) assert_kind_of(b, strainer) assert_kind_of(Liquid::StandardFilters, strainer) end def test_add_global_filter_clears_cache assert_equal('input', StrainerFactory.create(@context).invoke('late_added_filter', 'input')) StrainerFactory.add_global_filter(LateAddedFilter) assert_equal('filtered', StrainerFactory.create(nil).invoke('late_added_filter', 'input')) end end liquid-5.4.0/test/unit/strainer_template_unit_test.rb000066400000000000000000000042741427076730100231300ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class StrainerTemplateUnitTest < Minitest::Test include Liquid def test_add_filter_when_wrong_filter_class c = Context.new s = c.strainer wrong_filter = ->(v) { v.reverse } exception = assert_raises(TypeError) do s.class.add_filter(wrong_filter) end assert_equal(exception.message, "wrong argument type Proc (expected Module)") end module PrivateMethodOverrideFilter private def public_filter "overriden as private" end end def test_add_filter_raises_when_module_privately_overrides_registered_public_methods strainer = Context.new.strainer error = assert_raises(Liquid::MethodOverrideError) do strainer.class.add_filter(PrivateMethodOverrideFilter) end assert_equal('Liquid error: Filter overrides registered public methods as non public: public_filter', error.message) end module ProtectedMethodOverrideFilter protected def public_filter "overriden as protected" end end def test_add_filter_raises_when_module_overrides_registered_public_method_as_protected strainer = Context.new.strainer error = assert_raises(Liquid::MethodOverrideError) do strainer.class.add_filter(ProtectedMethodOverrideFilter) end assert_equal('Liquid error: Filter overrides registered public methods as non public: public_filter', error.message) end module PublicMethodOverrideFilter def public_filter "public" end end def test_add_filter_does_not_raise_when_module_overrides_previously_registered_method with_global_filter do strainer = Context.new.strainer strainer.class.add_filter(PublicMethodOverrideFilter) assert(strainer.class.send(:filter_methods).include?('public_filter')) end end def test_add_filter_does_not_include_already_included_module mod = Module.new do class << self attr_accessor :include_count def included(_mod) self.include_count += 1 end end self.include_count = 0 end strainer = Context.new.strainer strainer.class.add_filter(mod) strainer.class.add_filter(mod) assert_equal(1, mod.include_count) end end liquid-5.4.0/test/unit/tag_unit_test.rb000066400000000000000000000012221427076730100201470ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TagUnitTest < Minitest::Test include Liquid def test_tag tag = Tag.parse('tag', "", Tokenizer.new(""), ParseContext.new) assert_equal('liquid::tag', tag.name) assert_equal('', tag.render(Context.new)) end def test_return_raw_text_of_tag tag = Tag.parse("long_tag", "param1, param2, param3", Tokenizer.new(""), ParseContext.new) assert_equal("long_tag param1, param2, param3", tag.raw) end def test_tag_name_should_return_name_of_the_tag tag = Tag.parse("some_tag", "", Tokenizer.new(""), ParseContext.new) assert_equal('some_tag', tag.tag_name) end end liquid-5.4.0/test/unit/tags/000077500000000000000000000000001427076730100157125ustar00rootroot00000000000000liquid-5.4.0/test/unit/tags/case_tag_unit_test.rb000066400000000000000000000005271427076730100221070ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class CaseTagUnitTest < Minitest::Test include Liquid def test_case_nodelist template = Liquid::Template.parse('{% case var %}{% when true %}WHEN{% else %}ELSE{% endcase %}') assert_equal(['WHEN', 'ELSE'], template.root.nodelist[0].nodelist.map(&:nodelist).flatten) end end liquid-5.4.0/test/unit/tags/for_tag_unit_test.rb000066400000000000000000000010041427076730100217510ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class ForTagUnitTest < Minitest::Test def test_for_nodelist template = Liquid::Template.parse('{% for item in items %}FOR{% endfor %}') assert_equal(['FOR'], template.root.nodelist[0].nodelist.map(&:nodelist).flatten) end def test_for_else_nodelist template = Liquid::Template.parse('{% for item in items %}FOR{% else %}ELSE{% endfor %}') assert_equal(['FOR', 'ELSE'], template.root.nodelist[0].nodelist.map(&:nodelist).flatten) end end liquid-5.4.0/test/unit/tags/if_tag_unit_test.rb000066400000000000000000000004531427076730100215700ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class IfTagUnitTest < Minitest::Test def test_if_nodelist template = Liquid::Template.parse('{% if true %}IF{% else %}ELSE{% endif %}') assert_equal(['IF', 'ELSE'], template.root.nodelist[0].nodelist.map(&:nodelist).flatten) end end liquid-5.4.0/test/unit/template_factory_unit_test.rb000066400000000000000000000004271427076730100227440ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TemplateFactoryUnitTest < Minitest::Test include Liquid def test_for_returns_liquid_template_instance template = TemplateFactory.new.for("anything") assert_instance_of(Liquid::Template, template) end end liquid-5.4.0/test/unit/template_unit_test.rb000066400000000000000000000046261427076730100212220ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TemplateUnitTest < Minitest::Test include Liquid def test_sets_default_localization_in_document t = Template.new t.parse('{%comment%}{%endcomment%}') assert_instance_of(I18n, t.root.nodelist[0].options[:locale]) end def test_sets_default_localization_in_context_with_quick_initialization t = Template.new t.parse('{%comment%}{%endcomment%}', locale: I18n.new(fixture("en_locale.yml"))) locale = t.root.nodelist[0].options[:locale] assert_instance_of(I18n, locale) assert_equal(fixture("en_locale.yml"), locale.path) end def test_with_cache_classes_tags_returns_the_same_class original_cache_setting = Liquid.cache_classes Liquid.cache_classes = true original_klass = Class.new Object.send(:const_set, :CustomTag, original_klass) Template.register_tag('custom', CustomTag) Object.send(:remove_const, :CustomTag) new_klass = Class.new Object.send(:const_set, :CustomTag, new_klass) assert(Template.tags['custom'].equal?(original_klass)) ensure Object.send(:remove_const, :CustomTag) Template.tags.delete('custom') Liquid.cache_classes = original_cache_setting end def test_without_cache_classes_tags_reloads_the_class original_cache_setting = Liquid.cache_classes Liquid.cache_classes = false original_klass = Class.new Object.send(:const_set, :CustomTag, original_klass) Template.register_tag('custom', CustomTag) Object.send(:remove_const, :CustomTag) new_klass = Class.new Object.send(:const_set, :CustomTag, new_klass) assert(Template.tags['custom'].equal?(new_klass)) ensure Object.send(:remove_const, :CustomTag) Template.tags.delete('custom') Liquid.cache_classes = original_cache_setting end class FakeTag; end def test_tags_delete Template.register_tag('fake', FakeTag) assert_equal(FakeTag, Template.tags['fake']) Template.tags.delete('fake') assert_nil(Template.tags['fake']) end def test_tags_can_be_looped_over Template.register_tag('fake', FakeTag) result = Template.tags.map { |name, klass| [name, klass] } assert(result.include?(["fake", "TemplateUnitTest::FakeTag"])) ensure Template.tags.delete('fake') end class TemplateSubclass < Liquid::Template end def test_template_inheritance assert_equal("foo", TemplateSubclass.parse("foo").render) end end liquid-5.4.0/test/unit/tokenizer_unit_test.rb000066400000000000000000000037731427076730100214230ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class TokenizerTest < Minitest::Test def test_tokenize_strings assert_equal([' '], tokenize(' ')) assert_equal(['hello world'], tokenize('hello world')) end def test_tokenize_variables assert_equal(['{{funk}}'], tokenize('{{funk}}')) assert_equal([' ', '{{funk}}', ' '], tokenize(' {{funk}} ')) assert_equal([' ', '{{funk}}', ' ', '{{so}}', ' ', '{{brother}}', ' '], tokenize(' {{funk}} {{so}} {{brother}} ')) assert_equal([' ', '{{ funk }}', ' '], tokenize(' {{ funk }} ')) end def test_tokenize_blocks assert_equal(['{%comment%}'], tokenize('{%comment%}')) assert_equal([' ', '{%comment%}', ' '], tokenize(' {%comment%} ')) assert_equal([' ', '{%comment%}', ' ', '{%endcomment%}', ' '], tokenize(' {%comment%} {%endcomment%} ')) assert_equal([' ', '{% comment %}', ' ', '{% endcomment %}', ' '], tokenize(" {% comment %} {% endcomment %} ")) end def test_calculate_line_numbers_per_token_with_profiling assert_equal([1], tokenize_line_numbers("{{funk}}")) assert_equal([1, 1, 1], tokenize_line_numbers(" {{funk}} ")) assert_equal([1, 2, 2], tokenize_line_numbers("\n{{funk}}\n")) assert_equal([1, 1, 3], tokenize_line_numbers(" {{\n funk \n}} ")) end private def new_tokenizer(source, parse_context: Liquid::ParseContext.new, start_line_number: nil) parse_context.new_tokenizer(source, start_line_number: start_line_number) end def tokenize(source) tokenizer = new_tokenizer(source) tokens = [] # shift is private in Liquid::C::Tokenizer, since it is only for unit testing while (t = tokenizer.send(:shift)) tokens << t end tokens end def tokenize_line_numbers(source) tokenizer = new_tokenizer(source, start_line_number: 1) line_numbers = [] loop do line_number = tokenizer.line_number if tokenizer.send(:shift) line_numbers << line_number else break end end line_numbers end end liquid-5.4.0/test/unit/variable_unit_test.rb000066400000000000000000000127001427076730100211640ustar00rootroot00000000000000# frozen_string_literal: true require 'test_helper' class VariableUnitTest < Minitest::Test include Liquid def test_variable var = create_variable('hello') assert_equal(VariableLookup.new('hello'), var.name) end def test_filters var = create_variable('hello | textileze') assert_equal(VariableLookup.new('hello'), var.name) assert_equal([['textileze', []]], var.filters) var = create_variable('hello | textileze | paragraph') assert_equal(VariableLookup.new('hello'), var.name) assert_equal([['textileze', []], ['paragraph', []]], var.filters) var = create_variable(%( hello | strftime: '%Y')) assert_equal(VariableLookup.new('hello'), var.name) assert_equal([['strftime', ['%Y']]], var.filters) var = create_variable(%( 'typo' | link_to: 'Typo', true )) assert_equal('typo', var.name) assert_equal([['link_to', ['Typo', true]]], var.filters) var = create_variable(%( 'typo' | link_to: 'Typo', false )) assert_equal('typo', var.name) assert_equal([['link_to', ['Typo', false]]], var.filters) var = create_variable(%( 'foo' | repeat: 3 )) assert_equal('foo', var.name) assert_equal([['repeat', [3]]], var.filters) var = create_variable(%( 'foo' | repeat: 3, 3 )) assert_equal('foo', var.name) assert_equal([['repeat', [3, 3]]], var.filters) var = create_variable(%( 'foo' | repeat: 3, 3, 3 )) assert_equal('foo', var.name) assert_equal([['repeat', [3, 3, 3]]], var.filters) var = create_variable(%( hello | strftime: '%Y, okay?')) assert_equal(VariableLookup.new('hello'), var.name) assert_equal([['strftime', ['%Y, okay?']]], var.filters) var = create_variable(%( hello | things: "%Y, okay?", 'the other one')) assert_equal(VariableLookup.new('hello'), var.name) assert_equal([['things', ['%Y, okay?', 'the other one']]], var.filters) end def test_filter_with_date_parameter var = create_variable(%( '2006-06-06' | date: "%m/%d/%Y")) assert_equal('2006-06-06', var.name) assert_equal([['date', ['%m/%d/%Y']]], var.filters) end def test_filters_without_whitespace var = create_variable('hello | textileze | paragraph') assert_equal(VariableLookup.new('hello'), var.name) assert_equal([['textileze', []], ['paragraph', []]], var.filters) var = create_variable('hello|textileze|paragraph') assert_equal(VariableLookup.new('hello'), var.name) assert_equal([['textileze', []], ['paragraph', []]], var.filters) var = create_variable("hello|replace:'foo','bar'|textileze") assert_equal(VariableLookup.new('hello'), var.name) assert_equal([['replace', ['foo', 'bar']], ['textileze', []]], var.filters) end def test_symbol var = create_variable("http://disney.com/logo.gif | image: 'med' ", error_mode: :lax) assert_equal(VariableLookup.new('http://disney.com/logo.gif'), var.name) assert_equal([['image', ['med']]], var.filters) end def test_string_to_filter var = create_variable("'http://disney.com/logo.gif' | image: 'med' ") assert_equal('http://disney.com/logo.gif', var.name) assert_equal([['image', ['med']]], var.filters) end def test_string_single_quoted var = create_variable(%( "hello" )) assert_equal('hello', var.name) end def test_string_double_quoted var = create_variable(%( 'hello' )) assert_equal('hello', var.name) end def test_integer var = create_variable(%( 1000 )) assert_equal(1000, var.name) end def test_float var = create_variable(%( 1000.01 )) assert_equal(1000.01, var.name) end def test_dashes assert_equal(VariableLookup.new('foo-bar'), create_variable('foo-bar').name) assert_equal(VariableLookup.new('foo-bar-2'), create_variable('foo-bar-2').name) with_error_mode(:strict) do assert_raises(Liquid::SyntaxError) { create_variable('foo - bar') } assert_raises(Liquid::SyntaxError) { create_variable('-foo') } assert_raises(Liquid::SyntaxError) { create_variable('2foo') } end end def test_string_with_special_chars var = create_variable(%( 'hello! $!@.;"ddasd" ' )) assert_equal('hello! $!@.;"ddasd" ', var.name) end def test_string_dot var = create_variable(%( test.test )) assert_equal(VariableLookup.new('test.test'), var.name) end def test_filter_with_keyword_arguments var = create_variable(%( hello | things: greeting: "world", farewell: 'goodbye')) assert_equal(VariableLookup.new('hello'), var.name) assert_equal([['things', [], { 'greeting' => 'world', 'farewell' => 'goodbye' }]], var.filters) end def test_lax_filter_argument_parsing var = create_variable(%( number_of_comments | pluralize: 'comment': 'comments' ), error_mode: :lax) assert_equal(VariableLookup.new('number_of_comments'), var.name) assert_equal([['pluralize', ['comment', 'comments']]], var.filters) end def test_strict_filter_argument_parsing with_error_mode(:strict) do assert_raises(SyntaxError) do create_variable(%( number_of_comments | pluralize: 'comment': 'comments' )) end end end def test_output_raw_source_of_variable var = create_variable(%( name_of_variable | upcase )) assert_equal(" name_of_variable | upcase ", var.raw) end def test_variable_lookup_interface lookup = VariableLookup.new('a.b.c') assert_equal('a', lookup.name) assert_equal(['b', 'c'], lookup.lookups) end private def create_variable(markup, options = {}) Variable.new(markup, ParseContext.new(options)) end end