pax_global_header00006660000000000000000000000064135140726170014520gustar00rootroot0000000000000052 comment=8e9b5d9c087de461dd8dd1d8b261e715b4d9526c ruby-liquid-4.0.3/000077500000000000000000000000001351407261700137725ustar00rootroot00000000000000ruby-liquid-4.0.3/History.md000066400000000000000000000340411351407261700157570ustar00rootroot00000000000000# Liquid Change Log ## 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] ruby-liquid-4.0.3/LICENSE000066400000000000000000000020471351407261700150020ustar00rootroot00000000000000Copyright (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. ruby-liquid-4.0.3/README.md000066400000000000000000000115731351407261700152600ustar00rootroot00000000000000[![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](http://docs.shopify.com/themes/liquid-basics) * [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 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 ``` ruby-liquid-4.0.3/lib/000077500000000000000000000000001351407261700145405ustar00rootroot00000000000000ruby-liquid-4.0.3/lib/liquid.rb000066400000000000000000000061131351407261700163550ustar00rootroot00000000000000# 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 = ','.freeze FilterArgumentSeparator = ':'.freeze VariableAttributeSeparator = '.'.freeze WhitespaceControl = '-'.freeze TagStart = /\{\%/ TagEnd = /\%\}/ VariableSignature = /\(?[\w\-\.\[\]]\)?/ VariableSegment = /[\w\-]/ VariableStart = /\{\{/ VariableEnd = /\}\}/ VariableIncompleteEnd = /\}\}?/ QuotedString = /"[^"]*"|'[^']*'/ QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/o AnyStartingTag = /#{TagStart}|#{VariableStart}/o PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o singleton_class.send(:attr_accessor, :cache_classes) self.cache_classes = true end require "liquid/version" require 'liquid/parse_tree_visitor' require 'liquid/lexer' require 'liquid/parser' require 'liquid/i18n' require 'liquid/drop' require 'liquid/tablerowloop_drop' require 'liquid/forloop_drop' require 'liquid/extensions' require 'liquid/errors' require 'liquid/interrupts' require 'liquid/strainer' require 'liquid/expression' require 'liquid/context' require 'liquid/parser_switching' require 'liquid/tag' 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' # Load all the tags of the standard library # Dir["#{__dir__}/liquid/tags/*.rb"].each { |f| require f } ruby-liquid-4.0.3/lib/liquid/000077500000000000000000000000001351407261700160275ustar00rootroot00000000000000ruby-liquid-4.0.3/lib/liquid/block.rb000066400000000000000000000035721351407261700174550ustar00rootroot00000000000000module Liquid class Block < Tag MAX_DEPTH = 100 def initialize(tag_name, markup, options) super @blank = true end def parse(tokens) @body = BlockBody.new while parse_body(@body, tokens) end end def render(context) @body.render(context) end def blank? @blank end def nodelist @body.nodelist end def unknown_tag(tag, _params, _tokens) if tag == 'else'.freeze raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_else".freeze, block_name: block_name)) elsif tag.start_with?('end'.freeze) raise SyntaxError.new(parse_context.locale.t("errors.syntax.invalid_delimiter".freeze, tag: tag, block_name: block_name, block_delimiter: block_delimiter)) else raise SyntaxError.new(parse_context.locale.t("errors.syntax.unknown_tag".freeze, tag: tag)) end end def block_name @tag_name end def block_delimiter @block_delimiter ||= "end#{block_name}" end protected def parse_body(body, tokens) if parse_context.depth >= MAX_DEPTH raise StackLevelError, "Nesting too deep".freeze end parse_context.depth += 1 begin body.parse(tokens, parse_context) do |end_tag_name, end_tag_params| @blank &&= body.blank? return false if end_tag_name == block_delimiter unless end_tag_name raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name)) end # this tag is not registered with the system # pass it to the current block for special handling or error reporting unknown_tag(end_tag_name, end_tag_params, tokens) end ensure parse_context.depth -= 1 end true end end end ruby-liquid-4.0.3/lib/liquid/block_body.rb000066400000000000000000000112261351407261700204650ustar00rootroot00000000000000module Liquid class BlockBody FullToken = /\A#{TagStart}#{WhitespaceControl}?\s*(\w+)\s*(.*?)#{WhitespaceControl}?#{TagEnd}\z/om ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om WhitespaceOrNothing = /\A\s*\z/ TAGSTART = "{%".freeze VARSTART = "{{".freeze attr_reader :nodelist def initialize @nodelist = [] @blank = true end def parse(tokenizer, parse_context) parse_context.line_number = tokenizer.line_number while token = tokenizer.shift next if token.empty? case when token.start_with?(TAGSTART) whitespace_handler(token, parse_context) unless token =~ FullToken raise_missing_tag_terminator(token, parse_context) end tag_name = $1 markup = $2 # fetch the tag from registered blocks unless tag = registered_tags[tag_name] # end parsing if we reach an unknown tag and let the caller decide # determine how to proceed return yield tag_name, markup end new_tag = tag.parse(tag_name, markup, tokenizer, parse_context) @blank &&= new_tag.blank? @nodelist << new_tag when token.start_with?(VARSTART) whitespace_handler(token, parse_context) @nodelist << create_variable(token, parse_context) @blank = false else if parse_context.trim_whitespace token.lstrip! end parse_context.trim_whitespace = false @nodelist << token @blank &&= !!(token =~ WhitespaceOrNothing) end parse_context.line_number = tokenizer.line_number end yield nil, nil end def whitespace_handler(token, parse_context) if token[2] == WhitespaceControl previous_token = @nodelist.last if previous_token.is_a? String previous_token.rstrip! end end parse_context.trim_whitespace = (token[-3] == WhitespaceControl) end def blank? @blank end def render(context) output = [] context.resource_limits.render_score += @nodelist.length idx = 0 while node = @nodelist[idx] case node when String check_resources(context, node) output << node when Variable render_node_to_output(node, output, context) when Block render_node_to_output(node, output, context, node.blank?) break if context.interrupt? # might have happened in a for-block when Continue, Break # If we get an Interrupt that means the block must stop processing. An # Interrupt is any command that stops block execution such as {% break %} # or {% continue %} context.push_interrupt(node.interrupt) break else # Other non-Block tags render_node_to_output(node, output, context) break if context.interrupt? # might have happened through an include end idx += 1 end output.join end private def render_node_to_output(node, output, context, skip_output = false) node_output = node.render(context) node_output = node_output.is_a?(Array) ? node_output.join : node_output.to_s check_resources(context, node_output) output << node_output unless skip_output rescue MemoryError => e raise e rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e context.handle_error(e, node.line_number) output << nil rescue ::StandardError => e line_number = node.is_a?(String) ? nil : node.line_number output << context.handle_error(e, line_number) end def check_resources(context, node_output) context.resource_limits.render_length += node_output.length return unless context.resource_limits.reached? raise MemoryError.new("Memory limits exceeded".freeze) end def create_variable(token, parse_context) token.scan(ContentOfVariable) do |content| markup = content.first return Variable.new(markup, parse_context) end raise_missing_variable_terminator(token, parse_context) end def raise_missing_tag_terminator(token, parse_context) raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_termination".freeze, token: token, tag_end: TagEnd.inspect)) end def raise_missing_variable_terminator(token, parse_context) raise SyntaxError.new(parse_context.locale.t("errors.syntax.variable_termination".freeze, token: token, tag_end: VariableEnd.inspect)) end def registered_tags Template.tags end end end ruby-liquid-4.0.3/lib/liquid/condition.rb000066400000000000000000000072331351407261700203470ustar00rootroot00000000000000module Liquid # Container for liquid nodes which conveniently wraps decision making logic # # Example: # # c = Condition.new(1, '==', 1) # c.evaluate #=> true # class Condition #:nodoc: @@operators = { '=='.freeze => ->(cond, left, right) { cond.send(:equal_variables, left, right) }, '!='.freeze => ->(cond, left, right) { !cond.send(:equal_variables, left, right) }, '<>'.freeze => ->(cond, left, right) { !cond.send(:equal_variables, left, right) }, '<'.freeze => :<, '>'.freeze => :>, '>='.freeze => :>=, '<='.freeze => :<=, 'contains'.freeze => 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 } def self.operators @@operators 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 = Context.new) 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?(Liquid::Expression::MethodLiteral) if right.respond_to?(left.method_name) return right.send(left.method_name) else return nil end end if right.is_a?(Liquid::Expression::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 = context.evaluate(left) right = context.evaluate(right) operation = self.class.operators[op] || raise(Liquid::ArgumentError.new("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.new(e.message) end end end class ParseTreeVisitor < Liquid::ParseTreeVisitor def children [ @node.left, @node.right, @node.child_condition, @node.attachment ].compact end end end class ElseCondition < Condition def else? true end def evaluate(_context) true end end end ruby-liquid-4.0.3/lib/liquid/context.rb000066400000000000000000000150251351407261700200430ustar00rootroot00000000000000module 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 attr_accessor :exception_renderer, :template_name, :partial, :global_filter, :strict_variables, :strict_filters def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil) @environments = [environments].flatten @scopes = [(outer_scope || {})] @registers = registers @errors = [] @partial = false @strict_variables = false @resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits) squash_instance_assigns_with_environments @this_stack_used = false self.exception_renderer = Template.default_exception_renderer if rethrow_errors self.exception_renderer = ->(e) { raise } end @interrupts = [] @filters = [] @global_filter = nil end def warnings @warnings ||= [] end def strainer @strainer ||= Strainer.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) raise StackLevelError, "Nesting too deep".freeze if @scopes.length > Block::MAX_DEPTH 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 = nil) old_stack_used = @this_stack_used if new_scope push(new_scope) @this_stack_used = true else @this_stack_used = false end yield ensure pop if @this_stack_used @this_stack_used = old_stack_used end def clear_instance_assigns @scopes[0] = {} end # Only allow String, Numeric, Hash, Array, Proc, Boolean or Liquid::Drop def []=(key, value) unless @this_stack_used @this_stack_used = true push({}) end @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) } scope = @scopes[index] if index variable = nil if scope.nil? @environments.each do |e| variable = lookup_and_evaluate(e, key, raise_on_not_found: raise_on_not_found) # When lookup returned a value OR there is no value but the lookup also did not raise # then it is the value we are looking for. if !variable.nil? || @strict_variables && raise_on_not_found scope = e break end end end scope ||= @environments.last || @scopes.last variable ||= lookup_and_evaluate(scope, key, raise_on_not_found: raise_on_not_found) 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 private 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 ruby-liquid-4.0.3/lib/liquid/document.rb000066400000000000000000000013661351407261700202000ustar00rootroot00000000000000module Liquid class Document < BlockBody def self.parse(tokens, parse_context) doc = new doc.parse(tokens, parse_context) doc end def parse(tokens, parse_context) super do |end_tag_name, end_tag_params| unknown_tag(end_tag_name, parse_context) if end_tag_name end rescue SyntaxError => e e.line_number ||= parse_context.line_number raise end def unknown_tag(tag, parse_context) case tag when 'else'.freeze, 'end'.freeze raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_outer_tag".freeze, tag: tag)) else raise SyntaxError.new(parse_context.locale.t("errors.syntax.unknown_tag".freeze, tag: tag)) end end end end ruby-liquid-4.0.3/lib/liquid/drop.rb000066400000000000000000000042361351407261700173250ustar00rootroot00000000000000require '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 && @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, :include?] end whitelist = [:to_liquid] + (public_instance_methods - blacklist) Set.new(whitelist.map(&:to_s)) end end end end ruby-liquid-4.0.3/lib/liquid/errors.rb000066400000000000000000000023771351407261700177010ustar00rootroot00000000000000module 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 = "" if is_a?(SyntaxError) str << "Liquid syntax error" else str << "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) TaintedError = 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) InternalError = Class.new(Error) end ruby-liquid-4.0.3/lib/liquid/expression.rb000066400000000000000000000022611351407261700205540ustar00rootroot00000000000000module Liquid class Expression class MethodLiteral attr_reader :method_name, :to_s def initialize(method_name, to_s) @method_name = method_name @to_s = to_s end def to_liquid to_s end end LITERALS = { nil => nil, 'nil'.freeze => nil, 'null'.freeze => nil, ''.freeze => nil, 'true'.freeze => true, 'false'.freeze => false, 'blank'.freeze => MethodLiteral.new(:blank?, '').freeze, 'empty'.freeze => MethodLiteral.new(:empty?, '').freeze }.freeze SINGLE_QUOTED_STRING = /\A'(.*)'\z/m DOUBLE_QUOTED_STRING = /\A"(.*)"\z/m INTEGERS_REGEX = /\A(-?\d+)\z/ FLOATS_REGEX = /\A(-?\d[\d\.]+)\z/ RANGES_REGEX = /\A\((\S+)\.\.(\S+)\)\z/ def self.parse(markup) if LITERALS.key?(markup) LITERALS[markup] else case markup when SINGLE_QUOTED_STRING, DOUBLE_QUOTED_STRING $1 when INTEGERS_REGEX $1.to_i when RANGES_REGEX RangeLookup.parse($1, $2) when FLOATS_REGEX $1.to_f else VariableLookup.parse(markup) end end end end end ruby-liquid-4.0.3/lib/liquid/extensions.rb000066400000000000000000000013551351407261700205570ustar00rootroot00000000000000require '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 ruby-liquid-4.0.3/lib/liquid/file_system.rb000066400000000000000000000053351351407261700207050ustar00rootroot00000000000000module 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".freeze) @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 template_path =~ /\A[^.\/][a-zA-Z0-9_\/]+\z/ full_path = if template_path.include?('/'.freeze) 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 ruby-liquid-4.0.3/lib/liquid/forloop_drop.rb000066400000000000000000000010671351407261700210640ustar00rootroot00000000000000module Liquid class ForloopDrop < Drop def initialize(name, length, parentloop) @name = name @length = length @parentloop = parentloop @index = 0 end attr_reader :name, :length, :parentloop def index @index + 1 end def index0 @index end def rindex @length - @index end def rindex0 @length - @index - 1 end def first @index == 0 end def last @index == @length - 1 end protected def increment! @index += 1 end end end ruby-liquid-4.0.3/lib/liquid/i18n.rb000066400000000000000000000016521351407261700171370ustar00rootroot00000000000000require '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[$1.to_sym]).to_s end end def deep_fetch_translation(name) name.split('.'.freeze).reduce(locale) do |level, cur| level[cur] or raise TranslationError, "Translation for #{name} does not exist in locale #{path}" end end end end ruby-liquid-4.0.3/lib/liquid/interrupts.rb000066400000000000000000000007071351407261700205770ustar00rootroot00000000000000module Liquid # An interrupt is any command that breaks processing of a block (ex: a for loop). class Interrupt attr_reader :message def initialize(message = nil) @message = message || "interrupt".freeze 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 ruby-liquid-4.0.3/lib/liquid/lexer.rb000066400000000000000000000027331351407261700175000ustar00rootroot00000000000000require "strscan" module Liquid class Lexer SPECIALS = { '|'.freeze => :pipe, '.'.freeze => :dot, ':'.freeze => :colon, ','.freeze => :comma, '['.freeze => :open_square, ']'.freeze => :close_square, '('.freeze => :open_round, ')'.freeze => :close_round, '?'.freeze => :question, '-'.freeze => :dash }.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 = case when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t] when t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t] when t = @ss.scan(DOUBLE_STRING_LITERAL) then [:string, t] when t = @ss.scan(NUMBER_LITERAL) then [:number, t] when t = @ss.scan(IDENTIFIER) then [:id, t] when t = @ss.scan(DOTDOT) then [:dotdot, t] 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 ruby-liquid-4.0.3/lib/liquid/locales/000077500000000000000000000000001351407261700174515ustar00rootroot00000000000000ruby-liquid-4.0.3/lib/liquid/locales/en.yml000066400000000000000000000035001351407261700205740ustar00rootroot00000000000000--- 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]" unknown_tag: "Unknown tag '%{tag}'" invalid_delimiter: "'%{tag}' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}" unexpected_else: "%{block_name} tag does not expect 'else' tag" unexpected_outer_tag: "Unexpected outer '%{tag}' tag" tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}" variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}" tag_never_closed: "'%{block_name}' tag was never closed" meta_syntax_error: "Liquid syntax error: #{e.message}" table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3" argument: include: "Argument error in tag 'include' - Illegal template name" ruby-liquid-4.0.3/lib/liquid/parse_context.rb000066400000000000000000000017371351407261700212420ustar00rootroot00000000000000module 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 partial=(value) @partial = value @options = value ? partial_options : @template_options @error_mode = @options[:error_mode] || Template.error_mode value 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 ruby-liquid-4.0.3/lib/liquid/parse_tree_visitor.rb000066400000000000000000000017661351407261700222760ustar00rootroot00000000000000# 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 ruby-liquid-4.0.3/lib/liquid/parser.rb000066400000000000000000000037531351407261700176600ustar00rootroot00000000000000module 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] if token[0] == :id variable_signature elsif [:string, :number].include? token[0] consume elsif token.first == :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 << ' '.freeze end str << expression str end def variable_signature str = consume(:id) while look(:open_square) str << consume str << expression str << consume(:close_square) end if look(:dot) str << consume str << variable_signature end str end end end ruby-liquid-4.0.3/lib/liquid/parser_switching.rb000066400000000000000000000013551351407261700217330ustar00rootroot00000000000000module Liquid module ParserSwitching 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 return strict_parse_with_error_context(markup) rescue SyntaxError => e parse_context.warnings << e return 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 ruby-liquid-4.0.3/lib/liquid/profiler.rb000066400000000000000000000077571351407261700202160ustar00rootroot00000000000000require '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. # # If top level, this will be "". # 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, :partial, :line_number, :children def initialize(node, partial) @code = node.respond_to?(:raw) ? node.raw : node @partial = partial @line_number = node.respond_to?(:line_number) ? node.line_number : nil @children = [] end def self.start(node, partial) new(node, partial).tap(&:start) end def start @start_time = Time.now end def finish @end_time = Time.now end def render_time @end_time - @start_time end end def self.profile_node_render(node) if Profiler.current_profile && node.respond_to?(:render) Profiler.current_profile.start_node(node) output = yield Profiler.current_profile.end_node(node) output else yield end end def self.profile_children(template_name) if Profiler.current_profile Profiler.current_profile.push_partial(template_name) output = yield Profiler.current_profile.pop_partial output else yield end end def self.current_profile Thread.current[:liquid_profiler] end def initialize @partial_stack = [""] @root_timing = Timing.new("", current_partial) @timing_stack = [@root_timing] @render_start_at = Time.now @render_end_at = @render_start_at end def start Thread.current[:liquid_profiler] = self @render_start_at = Time.now end def stop Thread.current[:liquid_profiler] = nil @render_end_at = Time.now end def total_render_time @render_end_at - @render_start_at end def each(&block) @root_timing.children.each(&block) end def [](idx) @root_timing.children[idx] end def length @root_timing.children.length end def start_node(node) @timing_stack.push(Timing.start(node, current_partial)) end def end_node(_node) timing = @timing_stack.pop timing.finish @timing_stack.last.children << timing end def current_partial @partial_stack.last end def push_partial(partial_name) @partial_stack.push(partial_name) end def pop_partial @partial_stack.pop end end end ruby-liquid-4.0.3/lib/liquid/profiler/000077500000000000000000000000001351407261700176515ustar00rootroot00000000000000ruby-liquid-4.0.3/lib/liquid/profiler/hooks.rb000066400000000000000000000013041351407261700213170ustar00rootroot00000000000000module Liquid class BlockBody def render_node_with_profiling(node, output, context, skip_output = false) Profiler.profile_node_render(node) do render_node_without_profiling(node, output, context, skip_output) end end alias_method :render_node_without_profiling, :render_node_to_output alias_method :render_node_to_output, :render_node_with_profiling end class Include < Tag def render_with_profiling(context) Profiler.profile_children(context.evaluate(@template_name_expr).to_s) do render_without_profiling(context) end end alias_method :render_without_profiling, :render alias_method :render, :render_with_profiling end end ruby-liquid-4.0.3/lib/liquid/range_lookup.rb000066400000000000000000000014771351407261700210520ustar00rootroot00000000000000module 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 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 end end ruby-liquid-4.0.3/lib/liquid/resource_limits.rb000066400000000000000000000013161351407261700215650ustar00rootroot00000000000000module Liquid class ResourceLimits attr_accessor :render_length, :render_score, :assign_score, :render_length_limit, :render_score_limit, :assign_score_limit 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 reached? (@render_length_limit && @render_length > @render_length_limit) || (@render_score_limit && @render_score > @render_score_limit) || (@assign_score_limit && @assign_score > @assign_score_limit) end def reset @render_length = @render_score = @assign_score = 0 end end end ruby-liquid-4.0.3/lib/liquid/standardfilters.rb000066400000000000000000000317641351407261700215600ustar00rootroot00000000000000require 'cgi' require 'bigdecimal' module Liquid module StandardFilters HTML_ESCAPE = { '&'.freeze => '&'.freeze, '>'.freeze => '>'.freeze, '<'.freeze => '<'.freeze, '"'.freeze => '"'.freeze, "'".freeze => '''.freeze }.freeze HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/ STRIP_HTML_BLOCKS = Regexp.union( //m, //m, //m ) STRIP_HTML_TAGS = /<.*?>/m # Return the size of an array or of an string def size(input) input.respond_to?(:size) ? input.size : 0 end # convert an input string to DOWNCASE def downcase(input) input.to_s.downcase end # convert an input string to UPCASE def upcase(input) input.to_s.upcase end # capitalize words in the input centence def capitalize(input) input.to_s.capitalize end def escape(input) CGI.escapeHTML(input.to_s).untaint unless input.nil? end alias_method :h, :escape def escape_once(input) input.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE) end def url_encode(input) CGI.escape(input.to_s) unless input.nil? end def url_decode(input) return if input.nil? result = CGI.unescape(input.to_s) raise Liquid::ArgumentError, "invalid byte sequence in #{result.encoding}" unless result.valid_encoding? result end def slice(input, offset, length = nil) offset = Utils.to_integer(offset) length = length ? Utils.to_integer(length) : 1 if input.is_a?(Array) input.slice(offset, length) || [] else input.to_s.slice(offset, length) || '' end end # Truncate a string down to x characters def truncate(input, length = 50, truncate_string = "...".freeze) 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] + truncate_string_str : input_str end def truncatewords(input, words = 15, truncate_string = "...".freeze) return if input.nil? wordlist = input.to_s.split words = Utils.to_integer(words) l = words - 1 l = 0 if l < 0 wordlist.length > l ? wordlist[0..l].join(" ".freeze) + truncate_string.to_s : input end # Split input string into an array of substrings separated by given pattern. # # Example: #
{{ post | split '//' | first }}
# def split(input, pattern) input.to_s.split(pattern.to_s) end def strip(input) input.to_s.strip end def lstrip(input) input.to_s.lstrip end def rstrip(input) input.to_s.rstrip end def strip_html(input) empty = ''.freeze result = input.to_s.gsub(STRIP_HTML_BLOCKS, empty) result.gsub!(STRIP_HTML_TAGS, empty) result end # Remove all newlines from the string def strip_newlines(input) input.to_s.gsub(/\r?\n/, ''.freeze) end # Join elements of the array with certain character between them def join(input, glue = ' '.freeze) InputIterator.new(input).join(glue) end # Sort elements of the array # provide optional property with which to sort an array of hashes or drops def sort(input, property = nil) ary = InputIterator.new(input) 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 # Sort elements of an array ignoring case if strings # provide optional property with which to sort an array of hashes or drops def sort_natural(input, property = nil) ary = InputIterator.new(input) 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 # Filter the elements of an array to those with a certain property value. # By default the target is any truthy value. def where(input, property, target_value = nil) ary = InputIterator.new(input) if ary.empty? [] elsif ary.first.respond_to?(:[]) && target_value.nil? begin ary.select { |item| item[property] } rescue TypeError raise_property_error(property) end elsif ary.first.respond_to?(:[]) begin ary.select { |item| item[property] == target_value } rescue TypeError raise_property_error(property) end end end # Remove duplicate elements from an array # provide optional property with which to determine uniqueness def uniq(input, property = nil) ary = InputIterator.new(input) if property.nil? ary.uniq elsif ary.empty? # The next two cases assume a non-empty array. [] elsif ary.first.respond_to?(:[]) begin ary.uniq { |a| a[property] } rescue TypeError raise_property_error(property) end end end # Reverse the elements of an array def reverse(input) ary = InputIterator.new(input) ary.reverse end # map/collect on a given property def map(input, property) InputIterator.new(input).map do |e| e = e.call if e.is_a?(Proc) if property == "to_liquid".freeze e elsif e.respond_to?(:[]) r = e[property] r.is_a?(Proc) ? r.call : r end end rescue TypeError raise_property_error(property) end # Remove nils within an array # provide optional property with which to check for nil def compact(input, property = nil) ary = InputIterator.new(input) if property.nil? ary.compact elsif ary.empty? # The next two cases assume a non-empty array. [] elsif ary.first.respond_to?(:[]) begin ary.reject { |a| a[property].nil? } rescue TypeError raise_property_error(property) end end end # Replace occurrences of a string with another def replace(input, string, replacement = ''.freeze) input.to_s.gsub(string.to_s, replacement.to_s) end # Replace the first occurrences of a string with another def replace_first(input, string, replacement = ''.freeze) input.to_s.sub(string.to_s, replacement.to_s) end # remove a substring def remove(input, string) input.to_s.gsub(string.to_s, ''.freeze) end # remove the first occurrences of a substring def remove_first(input, string) input.to_s.sub(string.to_s, ''.freeze) end # add one string to another def append(input, string) input.to_s + string.to_s end def concat(input, array) unless array.respond_to?(:to_ary) raise ArgumentError.new("concat filter requires an array argument") end InputIterator.new(input).concat(array) end # prepend a string to another def prepend(input, string) string.to_s + input.to_s end # Add
tags in front of all newlines in input string def newline_to_br(input) input.to_s.gsub(/\n/, "
\n".freeze) 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 # Get the first element of the passed in array # # Example: # {{ product.images | first | to_img }} # def first(array) array.first if array.respond_to?(:first) end # Get the last element of the passed in array # # Example: # {{ product.images | last | to_img }} # def last(array) array.last if array.respond_to?(:last) end # absolute value def abs(input) result = Utils.to_number(input).abs result.is_a?(BigDecimal) ? result.to_f : result end # addition def plus(input, operand) apply_operation(input, operand, :+) end # subtraction def minus(input, operand) apply_operation(input, operand, :-) end # multiplication def times(input, operand) apply_operation(input, operand, :*) end # division def divided_by(input, operand) apply_operation(input, operand, :/) rescue ::ZeroDivisionError => e raise Liquid::ZeroDivisionError, e.message end def modulo(input, operand) apply_operation(input, operand, :%) rescue ::ZeroDivisionError => e raise Liquid::ZeroDivisionError, e.message end def round(input, n = 0) result = Utils.to_number(input).round(Utils.to_number(n)) result = result.to_f if result.is_a?(BigDecimal) result = result.to_i if n == 0 result rescue ::FloatDomainError => e raise Liquid::FloatDomainError, e.message end def ceil(input) Utils.to_number(input).ceil.to_i rescue ::FloatDomainError => e raise Liquid::FloatDomainError, e.message end def floor(input) Utils.to_number(input).floor.to_i rescue ::FloatDomainError => e raise Liquid::FloatDomainError, e.message end def at_least(input, n) min_value = Utils.to_number(n) result = Utils.to_number(input) result = min_value if min_value > result result.is_a?(BigDecimal) ? result.to_f : result end def at_most(input, n) max_value = Utils.to_number(n) result = Utils.to_number(input) result = max_value if max_value < result result.is_a?(BigDecimal) ? result.to_f : result end def default(input, default_value = ''.freeze) if !input || input.respond_to?(:empty?) && input.empty? default_value else input end end private def raise_property_error(property) raise Liquid::ArgumentError.new("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) if !a.nil? && !b.nil? a <=> b else a.nil? ? 1 : -1 end end def nil_safe_casecmp(a, b) if !a.nil? && !b.nil? a.to_s.casecmp(b.to_s) else a.nil? ? 1 : -1 end end class InputIterator include Enumerable def initialize(input) @input = if input.is_a?(Array) 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| yield(e.respond_to?(:to_liquid) ? e.to_liquid : e) end end end end Template.register_filter(StandardFilters) end ruby-liquid-4.0.3/lib/liquid/strainer.rb000066400000000000000000000041351351407261700202060ustar00rootroot00000000000000require 'set' module Liquid # Strainer is the parent class for the filters system. # New filters are mixed into the strainer class which is then instantiated for each liquid template render run. # # The Strainer only allows method calls defined in filters given to it via Strainer.global_filter, # Context#add_filters or Template.register_filter class Strainer #:nodoc: @@global_strainer = Class.new(Strainer) do @filter_methods = Set.new end @@strainer_class_cache = Hash.new do |hash, filters| hash[filters] = Class.new(@@global_strainer) do @filter_methods = @@global_strainer.filter_methods.dup filters.each { |f| add_filter(f) } end end def initialize(context) @context = context end class << self attr_reader :filter_methods end def self.add_filter(filter) raise ArgumentError, "Expected module but got: #{filter.class}" unless filter.is_a?(Module) unless self.include?(filter) invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) } if invokable_non_public_methods.any? raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}" else send(:include, filter) @filter_methods.merge(filter.public_instance_methods.map(&:to_s)) end end end def self.global_filter(filter) @@strainer_class_cache.clear @@global_strainer.add_filter(filter) end def self.invokable?(method) @filter_methods.include?(method.to_s) end def self.create(context, filters = []) @@strainer_class_cache[filters].new(context) end def invoke(method, *args) if self.class.invokable?(method) send(method, *args) elsif @context && @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 ruby-liquid-4.0.3/lib/liquid/tablerowloop_drop.rb000066400000000000000000000014071351407261700221130ustar00rootroot00000000000000module Liquid class TablerowloopDrop < Drop def initialize(length, cols) @length = length @row = 1 @col = 1 @cols = cols @index = 0 end attr_reader :length, :col, :row def index @index + 1 end def index0 @index end def col0 @col - 1 end def rindex @length - @index end def rindex0 @length - @index - 1 end def first @index == 0 end def last @index == @length - 1 end def col_first @col == 1 end def col_last @col == @cols end protected def increment! @index += 1 if @col == @cols @col = 1 @row += 1 else @col += 1 end end end end ruby-liquid-4.0.3/lib/liquid/tag.rb000066400000000000000000000014331351407261700171300ustar00rootroot00000000000000module 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, options) tag = new(tag_name, markup, options) tag.parse(tokenizer) tag 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) ''.freeze end def blank? false end end end ruby-liquid-4.0.3/lib/liquid/tags/000077500000000000000000000000001351407261700167655ustar00rootroot00000000000000ruby-liquid-4.0.3/lib/liquid/tags/assign.rb000066400000000000000000000023561351407261700206040ustar00rootroot00000000000000module Liquid # Assign sets a variable in your template. # # {% assign foo = 'monkey' %} # # You can then use the variable later in the page. # # {{ foo }} # class Assign < Tag Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om attr_reader :to, :from def initialize(tag_name, markup, options) super if markup =~ Syntax @to = $1 @from = Variable.new($2, options) else raise SyntaxError.new options[:locale].t("errors.syntax.assign".freeze) end end def render(context) val = @from.render(context) context.scopes.last[@to] = val context.resource_limits.assign_score += assign_score_of(val) ''.freeze end def blank? true end private def assign_score_of(val) if val.instance_of?(String) val.length elsif val.instance_of?(Array) || val.instance_of?(Hash) sum = 1 # Uses #each to avoid extra allocations. val.each { |child| sum += assign_score_of(child) } sum else 1 end end class ParseTreeVisitor < Liquid::ParseTreeVisitor def children [@node.from] end end end Template.register_tag('assign'.freeze, Assign) end ruby-liquid-4.0.3/lib/liquid/tags/break.rb000066400000000000000000000005451351407261700204020ustar00rootroot00000000000000module 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 %} # class Break < Tag def interrupt BreakInterrupt.new end end Template.register_tag('break'.freeze, Break) end ruby-liquid-4.0.3/lib/liquid/tags/capture.rb000066400000000000000000000015421351407261700207570ustar00rootroot00000000000000module Liquid # Capture stores the result of a block into a variable without rendering it inplace. # # {% capture heading %} # Monkeys! # {% endcapture %} # ... #

{{ heading }}

# # Capture is useful for saving content for use later in your template, such as # in a sidebar or footer. # class Capture < Block Syntax = /(#{VariableSignature}+)/o def initialize(tag_name, markup, options) super if markup =~ Syntax @to = $1 else raise SyntaxError.new(options[:locale].t("errors.syntax.capture")) end end def render(context) output = super context.scopes.last[@to] = output context.resource_limits.assign_score += output.length ''.freeze end def blank? true end end Template.register_tag('capture'.freeze, Capture) end ruby-liquid-4.0.3/lib/liquid/tags/case.rb000066400000000000000000000041611351407261700202270ustar00rootroot00000000000000module Liquid class Case < Block Syntax = /(#{QuotedFragment})/o WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om attr_reader :blocks, :left def initialize(tag_name, markup, options) super @blocks = [] if markup =~ Syntax @left = Expression.parse($1) else raise SyntaxError.new(options[:locale].t("errors.syntax.case".freeze)) end end def parse(tokens) body = BlockBody.new while parse_body(body, tokens) body = @blocks.last.attachment end end def nodelist @blocks.map(&:attachment) end def unknown_tag(tag, markup, tokens) case tag when 'when'.freeze record_when_condition(markup) when 'else'.freeze record_else_condition(markup) else super end end def render(context) context.stack do execute_else_block = true output = '' @blocks.each do |block| if block.else? return block.attachment.render(context) if execute_else_block elsif block.evaluate(context) execute_else_block = false output << block.attachment.render(context) end end output end end private def record_when_condition(markup) body = BlockBody.new while markup unless markup =~ WhenSyntax raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_when".freeze)) end markup = $2 block = Condition.new(@left, '=='.freeze, Expression.parse($1)) block.attach(body) @blocks << block end end def record_else_condition(markup) unless markup.strip.empty? raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_else".freeze)) end block = ElseCondition.new block.attach(BlockBody.new) @blocks << block end class ParseTreeVisitor < Liquid::ParseTreeVisitor def children [@node.left] + @node.blocks end end end Template.register_tag('case'.freeze, Case) end ruby-liquid-4.0.3/lib/liquid/tags/comment.rb000066400000000000000000000003551351407261700207570ustar00rootroot00000000000000module Liquid class Comment < Block def render(_context) ''.freeze end def unknown_tag(_tag, _markup, _tokens) end def blank? true end end Template.register_tag('comment'.freeze, Comment) end ruby-liquid-4.0.3/lib/liquid/tags/continue.rb000066400000000000000000000005671351407261700211460ustar00rootroot00000000000000module Liquid # Continue tag to be used to break out of a for loop. # # == Basic Usage: # {% for item in collection %} # {% if item.condition %} # {% continue %} # {% endif %} # {% endfor %} # class Continue < Tag def interrupt ContinueInterrupt.new end end Template.register_tag('continue'.freeze, Continue) end ruby-liquid-4.0.3/lib/liquid/tags/cycle.rb000066400000000000000000000033671351407261700204220ustar00rootroot00000000000000module Liquid # Cycle is usually used within a loop to alternate between values, like colors or DOM classes. # # {% for item in items %} #
{{ item }}
# {% end %} # #
Item one
#
Item two
#
Item three
#
Item four
#
Item five
# 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($2) @name = Expression.parse($1) when SimpleSyntax @variables = variables_from_string(markup) @name = @variables.to_s else raise SyntaxError.new(options[:locale].t("errors.syntax.cycle".freeze)) end end def render(context) context.registers[:cycle] ||= {} context.stack do key = context.evaluate(@name) iteration = context.registers[:cycle][key].to_i result = context.evaluate(@variables[iteration]) iteration += 1 iteration = 0 if iteration >= @variables.size context.registers[:cycle][key] = iteration result end end private def variables_from_string(markup) markup.split(',').collect do |var| var =~ /\s*(#{QuotedFragment})\s*/o $1 ? Expression.parse($1) : nil end.compact end class ParseTreeVisitor < Liquid::ParseTreeVisitor def children Array(@node.variables) end end end Template.register_tag('cycle', Cycle) end ruby-liquid-4.0.3/lib/liquid/tags/decrement.rb000066400000000000000000000016351351407261700212650ustar00rootroot00000000000000module Liquid # decrement is used in a place where one needs to insert a counter # into a template, and needs the counter to survive across # multiple instantiations of the template. # NOTE: decrement is a pre-decrement, --i, # while increment is post: i++. # # (To achieve the survival, the application must keep the context) # # if the variable does not exist, it is created with value 0. # Hello: {% decrement variable %} # # gives you: # # Hello: -1 # Hello: -2 # Hello: -3 # class Decrement < Tag def initialize(tag_name, markup, options) super @variable = markup.strip end def render(context) value = context.environments.first[@variable] ||= 0 value -= 1 context.environments.first[@variable] = value value.to_s end end Template.register_tag('decrement'.freeze, Decrement) end ruby-liquid-4.0.3/lib/liquid/tags/for.rb000066400000000000000000000135271351407261700201100ustar00rootroot00000000000000module Liquid # "For" iterates over an array or collection. # Several useful variables are available to you within the loop. # # == Basic usage: # {% for item in collection %} # {{ forloop.index }}: {{ item.name }} # {% endfor %} # # == Advanced usage: # {% for item in collection %} #
# Item {{ forloop.index }}: {{ item.name }} #
# {% else %} # There is nothing in the collection. # {% endfor %} # # You can also define a limit and offset much like SQL. Remember # that offset starts at 0 for the first item. # # {% for item in collection limit:5 offset:10 %} # {{ item.name }} # {% end %} # # To reverse the for loop simply use {% for item in collection reversed %} (note that the flag's spelling is different to the filter `reverse`) # # == Available variables: # # forloop.name:: 'item-collection' # forloop.length:: Length of the loop # forloop.index:: The current item's position in the collection; # forloop.index starts at 1. # This is helpful for non-programmers who start believe # the first item in an array is 1, not 0. # forloop.index0:: The current item's position in the collection # where the first item is 0 # forloop.rindex:: Number of items remaining in the loop # (length - index) where 1 is the last item. # forloop.rindex0:: Number of items remaining in the loop # where 0 is the last item. # forloop.first:: Returns true if the item is the first item. # forloop.last:: Returns true if the item is the last item. # forloop.parentloop:: Provides access to the parent loop, if present. # class For < Block Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o attr_reader :collection_name, :variable_name, :limit, :from def initialize(tag_name, markup, options) super @from = @limit = nil parse_with_selected_parser(markup) @for_block = BlockBody.new @else_block = nil end def parse(tokens) return unless parse_body(@for_block, tokens) parse_body(@else_block, tokens) end def nodelist @else_block ? [@for_block, @else_block] : [@for_block] end def unknown_tag(tag, markup, tokens) return super unless tag == 'else'.freeze @else_block = BlockBody.new end def render(context) segment = collection_segment(context) if segment.empty? render_else(context) else render_segment(context, segment) end end protected def lax_parse(markup) if markup =~ Syntax @variable_name = $1 collection_name = $2 @reversed = !!$3 @name = "#{@variable_name}-#{collection_name}" @collection_name = Expression.parse(collection_name) markup.scan(TagAttributes) do |key, value| set_attribute(key, value) end else raise SyntaxError.new(options[:locale].t("errors.syntax.for".freeze)) end end def strict_parse(markup) p = Parser.new(markup) @variable_name = p.consume(:id) raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_in".freeze)) unless p.id?('in'.freeze) collection_name = p.expression @name = "#{@variable_name}-#{collection_name}" @collection_name = Expression.parse(collection_name) @reversed = p.id?('reversed'.freeze) while p.look(:id) && p.look(:colon, 1) unless attribute = p.id?('limit'.freeze) || p.id?('offset'.freeze) raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_attribute".freeze)) 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 context.evaluate(@from).to_i end collection = context.evaluate(@collection_name) collection = collection.to_a if collection.is_a?(Range) limit = context.evaluate(@limit) to = limit ? limit.to_i + from : nil segment = Utils.slice_collection(collection, from, to) segment.reverse! if @reversed offsets[@name] = from + segment.length segment end def render_segment(context, segment) for_stack = context.registers[:for_stack] ||= [] length = segment.length result = '' context.stack do loop_vars = Liquid::ForloopDrop.new(@name, length, for_stack[-1]) for_stack.push(loop_vars) begin context['forloop'.freeze] = loop_vars segment.each do |item| context[@variable_name] = item result << @for_block.render(context) loop_vars.send(:increment!) # Handle any interrupts if they exist. if context.interrupt? interrupt = context.pop_interrupt break if interrupt.is_a? BreakInterrupt next if interrupt.is_a? ContinueInterrupt end end ensure for_stack.pop end end result end def set_attribute(key, expr) case key when 'offset'.freeze @from = if expr == 'continue'.freeze :continue else Expression.parse(expr) end when 'limit'.freeze @limit = Expression.parse(expr) end end def render_else(context) @else_block ? @else_block.render(context) : ''.freeze end class ParseTreeVisitor < Liquid::ParseTreeVisitor def children (super + [@node.limit, @node.from, @node.collection_name]).compact end end end Template.register_tag('for'.freeze, For) end ruby-liquid-4.0.3/lib/liquid/tags/if.rb000066400000000000000000000061341351407261700177140ustar00rootroot00000000000000module Liquid # If is the conditional block # # {% if user.admin %} # Admin user! # {% else %} # Not admin user # {% endif %} # # There are {% if count < 5 %} less {% else %} more {% endif %} items than you need. # 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'.freeze, markup) end def nodelist @blocks.map(&:attachment) end def parse(tokens) while parse_body(@blocks.last.attachment, tokens) end end def unknown_tag(tag, markup, tokens) if ['elsif'.freeze, 'else'.freeze].include?(tag) push_block(tag, markup) else super end end def render(context) context.stack do @blocks.each do |block| if block.evaluate(context) return block.attachment.render(context) end end ''.freeze end end private def push_block(tag, markup) block = if tag == 'else'.freeze ElseCondition.new else parse_with_selected_parser(markup) end @blocks.push(block) block.attach(BlockBody.new) end def lax_parse(markup) expressions = markup.scan(ExpressionsAndOperators) raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop =~ Syntax condition = Condition.new(Expression.parse($1), $2, Expression.parse($3)) until expressions.empty? operator = expressions.pop.to_s.strip raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop.to_s =~ Syntax new_condition = Condition.new(Expression.parse($1), $2, Expression.parse($3)) raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless BOOLEAN_OPERATORS.include?(operator) new_condition.send(operator, condition) condition = new_condition end 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'.freeze) || p.id?('or'.freeze)) child_condition = parse_comparison(p) condition.send(op, child_condition) condition = child_condition end first_condition end def parse_comparison(p) a = Expression.parse(p.expression) if op = p.consume?(:comparison) b = Expression.parse(p.expression) Condition.new(a, op, b) else Condition.new(a) end end class ParseTreeVisitor < Liquid::ParseTreeVisitor def children @node.blocks end end end Template.register_tag('if'.freeze, If) end ruby-liquid-4.0.3/lib/liquid/tags/ifchanged.rb000066400000000000000000000005451351407261700212260ustar00rootroot00000000000000module Liquid class Ifchanged < Block def render(context) context.stack do output = super if output != context.registers[:ifchanged] context.registers[:ifchanged] = output output else ''.freeze end end end end Template.register_tag('ifchanged'.freeze, Ifchanged) end ruby-liquid-4.0.3/lib/liquid/tags/include.rb000066400000000000000000000065711351407261700207460ustar00rootroot00000000000000module Liquid # Include allows templates to relate with other templates # # Simply include another template: # # {% include 'product' %} # # Include a template with a local variable: # # {% include 'product' with products[0] %} # # Include a template for a collection: # # {% include 'product' for products %} # class Include < Tag Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/o attr_reader :template_name_expr, :variable_name_expr, :attributes def initialize(tag_name, markup, options) super if markup =~ Syntax template_name = $1 variable_name = $3 @variable_name_expr = variable_name ? Expression.parse(variable_name) : nil @template_name_expr = Expression.parse(template_name) @attributes = {} markup.scan(TagAttributes) do |key, value| @attributes[key] = Expression.parse(value) end else raise SyntaxError.new(options[:locale].t("errors.syntax.include".freeze)) end end def parse(_tokens) end def render(context) template_name = context.evaluate(@template_name_expr) raise ArgumentError.new(options[:locale].t("errors.argument.include")) unless template_name partial = load_cached_partial(template_name, context) context_variable_name = template_name.split('/'.freeze).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.collect do |var| context[context_variable_name] = var partial.render(context) end else context[context_variable_name] = variable partial.render(context) end end ensure context.template_name = old_template_name context.partial = old_partial end end private alias_method :parse_context, :options private :parse_context def load_cached_partial(template_name, context) cached_partials = context.registers[:cached_partials] || {} if cached = cached_partials[template_name] return cached end source = read_template_from_file_system(context) begin parse_context.partial = true partial = Liquid::Template.parse(source, parse_context) ensure parse_context.partial = false end cached_partials[template_name] = partial context.registers[:cached_partials] = cached_partials partial end def read_template_from_file_system(context) file_system = context.registers[:file_system] || Liquid::Template.file_system file_system.read_template_file(context.evaluate(@template_name_expr)) end class ParseTreeVisitor < Liquid::ParseTreeVisitor def children [ @node.template_name_expr, @node.variable_name_expr ] + @node.attributes.values end end end Template.register_tag('include'.freeze, Include) end ruby-liquid-4.0.3/lib/liquid/tags/increment.rb000066400000000000000000000014521351407261700213000ustar00rootroot00000000000000module Liquid # increment is used in a place where one needs to insert a counter # into a template, and needs the counter to survive across # multiple instantiations of the template. # (To achieve the survival, the application must keep the context) # # if the variable does not exist, it is created with value 0. # # Hello: {% increment variable %} # # gives you: # # Hello: 0 # Hello: 1 # Hello: 2 # class Increment < Tag def initialize(tag_name, markup, options) super @variable = markup.strip end def render(context) value = context.environments.first[@variable] ||= 0 context.environments.first[@variable] = value + 1 value.to_s end end Template.register_tag('increment'.freeze, Increment) end ruby-liquid-4.0.3/lib/liquid/tags/raw.rb000066400000000000000000000020731351407261700201050ustar00rootroot00000000000000module Liquid 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 @body << $1 if $1 != "".freeze return if block_delimiter == $2 end @body << token unless token.empty? end raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name)) end def render(_context) @body end def nodelist [@body] end def blank? @body.empty? end protected def ensure_valid_markup(tag_name, markup, parse_context) unless markup =~ Syntax raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_unexpected_args".freeze, tag: tag_name)) end end end Template.register_tag('raw'.freeze, Raw) end ruby-liquid-4.0.3/lib/liquid/tags/table_row.rb000066400000000000000000000035071351407261700212750ustar00rootroot00000000000000module Liquid class TableRow < Block Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o attr_reader :variable_name, :collection_name, :attributes def initialize(tag_name, markup, options) super if markup =~ Syntax @variable_name = $1 @collection_name = Expression.parse($2) @attributes = {} markup.scan(TagAttributes) do |key, value| @attributes[key] = Expression.parse(value) end else raise SyntaxError.new(options[:locale].t("errors.syntax.table_row".freeze)) end end def render(context) collection = context.evaluate(@collection_name) or return ''.freeze from = @attributes.key?('offset'.freeze) ? context.evaluate(@attributes['offset'.freeze]).to_i : 0 to = @attributes.key?('limit'.freeze) ? from + context.evaluate(@attributes['limit'.freeze]).to_i : nil collection = Utils.slice_collection(collection, from, to) length = collection.length cols = context.evaluate(@attributes['cols'.freeze]).to_i result = "\n" context.stack do tablerowloop = Liquid::TablerowloopDrop.new(length, cols) context['tablerowloop'.freeze] = tablerowloop collection.each do |item| context[@variable_name] = item result << "" << super << '' if tablerowloop.col_last && !tablerowloop.last result << "\n" end tablerowloop.send(:increment!) end end result << "\n" result end class ParseTreeVisitor < Liquid::ParseTreeVisitor def children super + @node.attributes.values + [@node.collection_name] end end end Template.register_tag('tablerow'.freeze, TableRow) end ruby-liquid-4.0.3/lib/liquid/tags/unless.rb000066400000000000000000000014101351407261700206170ustar00rootroot00000000000000require_relative 'if' module Liquid # Unless is a conditional just like 'if' but works on the inverse logic. # # {% unless x < 0 %} x is greater than zero {% endunless %} # class Unless < If def render(context) context.stack do # First condition is interpreted backwards ( if not ) first_block = @blocks.first unless first_block.evaluate(context) return first_block.attachment.render(context) end # After the first condition unless works just like if @blocks[1..-1].each do |block| if block.evaluate(context) return block.attachment.render(context) end end ''.freeze end end end Template.register_tag('unless'.freeze, Unless) end ruby-liquid-4.0.3/lib/liquid/template.rb000066400000000000000000000157601351407261700202000ustar00rootroot00000000000000module 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 @@file_system = BlankFileSystem.new 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) name.split("::").reject(&:empty?).reduce(Object) { |scope, const| scope.const_get(const) } 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_writer :error_mode # Sets how strict the taint checker should be. # :lax is the default, and ignores the taint flag completely # :warn adds a warning, but does not interrupt the rendering # :error raises an error when tainted output is used attr_writer :taint_mode attr_accessor :default_exception_renderer Template.default_exception_renderer = lambda do |exception| exception end def file_system @@file_system end def file_system=(obj) @@file_system = obj end def register_tag(name, klass) tags[name.to_s] = klass end def tags @tags ||= TagRegistry.new end def error_mode @error_mode ||= :lax end def taint_mode @taint_mode ||= :lax end # Pass a module with filter methods which should be available # to all liquid views. Good for registering the standard library def register_filter(mod) Strainer.global_filter(mod) end def default_resource_limits @default_resource_limits ||= {} end # 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 = {}) template = Template.new template.parse(source, options) end end def initialize @rethrow_errors = false @resource_limits = ResourceLimits.new(self.class.default_resource_limits) end # Parse source code. # Returns self for easy chaining def parse(source, options = {}) @options = options @profiling = options[:profile] @line_numbers = options[:line_numbers] || @profiling parse_context = options.is_a?(ParseContext) ? options : ParseContext.new(options) @root = Document.parse(tokenize(source), parse_context) @warnings = parse_context.warnings 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 ''.freeze if @root.nil? context = case args.first when Liquid::Context c = args.shift if @rethrow_errors c.exception_renderer = ->(e) { raise } 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 case args.last when Hash options = args.pop registers.merge!(options[:registers]) if options[:registers].is_a?(Hash) 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 begin # render the nodelist. # for performance reasons we get an array back here. join will make a string out of it. result = with_profiling(context) do @root.render(context) end result.respond_to?(:join) ? result.join : result rescue Liquid::MemoryError => e context.handle_error(e) ensure @errors = context.errors end end def render!(*args) @rethrow_errors = true render(*args) end private def tokenize(source) Tokenizer.new(source, @line_numbers) end def with_profiling(context) if @profiling && !context.partial raise "Profiler not loaded, require 'liquid/profiler' first" unless defined?(Liquid::Profiler) @profiler = Profiler.new @profiler.start begin yield ensure @profiler.stop end else yield end end 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 ruby-liquid-4.0.3/lib/liquid/tokenizer.rb000066400000000000000000000012411351407261700203640ustar00rootroot00000000000000module Liquid class Tokenizer attr_reader :line_number def initialize(source, line_numbers = false) @source = source @line_number = line_numbers ? 1 : nil @tokens = tokenize end def shift token = @tokens.shift @line_number += token.count("\n") if @line_number && token token end private def tokenize @source = @source.source if @source.respond_to?(:source) return [] if @source.to_s.empty? tokens = @source.split(TemplateParser) # removes the rogue empty element at the beginning of the array tokens.shift if tokens[0] && tokens[0].empty? tokens end end end ruby-liquid-4.0.3/lib/liquid/truffle.rb000066400000000000000000000000521351407261700200200ustar00rootroot00000000000000module Liquid module Truffle end end ruby-liquid-4.0.3/lib/liquid/utils.rb000066400000000000000000000033501351407261700175150ustar00rootroot00000000000000module 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 (obj.strip =~ /\A-?\d+\.\d+\z/) ? 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'.freeze, 'today'.freeze Time.now when /\A\d+\z/, Integer Time.at(obj.to_i) when String Time.parse(obj) end rescue ::ArgumentError nil end end end ruby-liquid-4.0.3/lib/liquid/variable.rb000066400000000000000000000077651351407261700201600ustar00rootroot00000000000000module 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 parse_with_selected_parser(markup) end def raw @markup end def markup_context(markup) "in \"{{#{markup}}}\"" end def lax_parse(markup) @filters = [] return unless markup =~ MarkupWithQuotedFragment name_markup = $1 filter_markup = $2 @name = Expression.parse(name_markup) if filter_markup =~ FilterMarkupRegex filters = $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) @name = Expression.parse(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 = @filters.inject(context.evaluate(@name)) do |output, (filter_name, filter_args, filter_kwargs)| filter_args = evaluate_filter_expressions(context, filter_args, filter_kwargs) context.invoke(filter_name, output, *filter_args) end obj = context.apply_global_filter(obj) taint_check(context, obj) obj end private def parse_filter_expressions(filter_name, unparsed_args) filter_args = [] keyword_args = {} unparsed_args.each do |a| if matches = a.match(JustTagAttributes) keyword_args[matches[1]] = Expression.parse(matches[2]) else filter_args << Expression.parse(a) end end result = [filter_name, filter_args] result << keyword_args unless keyword_args.empty? result 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 def taint_check(context, obj) return unless obj.tainted? return if Template.taint_mode == :lax @markup =~ QuotedFragment name = Regexp.last_match(0) error = TaintedError.new("variable '#{name}' is tainted and was not escaped") error.line_number = line_number error.template_name = context.template_name case Template.taint_mode when :warn context.warnings << error when :error raise error end end class ParseTreeVisitor < Liquid::ParseTreeVisitor def children [@node.name] + @node.filters.flatten end end end end ruby-liquid-4.0.3/lib/liquid/variable_lookup.rb000066400000000000000000000047741351407261700215460ustar00rootroot00000000000000module Liquid class VariableLookup SQUARE_BRACKETED = /\A\[(.*)\]\z/m COMMAND_METHODS = ['size'.freeze, 'first'.freeze, 'last'.freeze].freeze attr_reader :name, :lookups def self.parse(markup) new(markup) end def initialize(markup) lookups = markup.scan(VariableParser) name = lookups.shift if name =~ SQUARE_BRACKETED name = Expression.parse($1) end @name = name @lookups = lookups @command_flags = 0 @lookups.each_index do |i| lookup = lookups[i] if lookup =~ SQUARE_BRACKETED lookups[i] = Expression.parse($1) elsif COMMAND_METHODS.include?(lookup) @command_flags |= 1 << i end end end def evaluate(context) name = context.evaluate(@name) object = context.find_variable(name) @lookups.each_index do |i| key = context.evaluate(@lookups[i]) # 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 @command_flags & (1 << i) != 0 && 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 ruby-liquid-4.0.3/lib/liquid/version.rb000066400000000000000000000001001351407261700200300ustar00rootroot00000000000000# encoding: utf-8 module Liquid VERSION = "4.0.3".freeze end ruby-liquid-4.0.3/liquid.gemspec000066400000000000000000000170541351407261700166350ustar00rootroot00000000000000######################################################### # This file has been automatically generated by gem2tgz # ######################################################### # -*- encoding: utf-8 -*- # stub: liquid 4.0.3 ruby lib Gem::Specification.new do |s| s.name = "liquid".freeze s.version = "4.0.3" s.required_rubygems_version = Gem::Requirement.new(">= 1.3.7".freeze) if s.respond_to? :required_rubygems_version= s.require_paths = ["lib".freeze] s.authors = ["Tobias L\u{fc}tke".freeze] s.date = "2019-03-12" s.email = ["tobi@leetsoft.com".freeze] s.extra_rdoc_files = ["History.md".freeze, "README.md".freeze] s.files = ["History.md".freeze, "LICENSE".freeze, "README.md".freeze, "lib/liquid.rb".freeze, "lib/liquid/block.rb".freeze, "lib/liquid/block_body.rb".freeze, "lib/liquid/condition.rb".freeze, "lib/liquid/context.rb".freeze, "lib/liquid/document.rb".freeze, "lib/liquid/drop.rb".freeze, "lib/liquid/errors.rb".freeze, "lib/liquid/expression.rb".freeze, "lib/liquid/extensions.rb".freeze, "lib/liquid/file_system.rb".freeze, "lib/liquid/forloop_drop.rb".freeze, "lib/liquid/i18n.rb".freeze, "lib/liquid/interrupts.rb".freeze, "lib/liquid/lexer.rb".freeze, "lib/liquid/locales/en.yml".freeze, "lib/liquid/parse_context.rb".freeze, "lib/liquid/parse_tree_visitor.rb".freeze, "lib/liquid/parser.rb".freeze, "lib/liquid/parser_switching.rb".freeze, "lib/liquid/profiler.rb".freeze, "lib/liquid/profiler/hooks.rb".freeze, "lib/liquid/range_lookup.rb".freeze, "lib/liquid/resource_limits.rb".freeze, "lib/liquid/standardfilters.rb".freeze, "lib/liquid/strainer.rb".freeze, "lib/liquid/tablerowloop_drop.rb".freeze, "lib/liquid/tag.rb".freeze, "lib/liquid/tags/assign.rb".freeze, "lib/liquid/tags/break.rb".freeze, "lib/liquid/tags/capture.rb".freeze, "lib/liquid/tags/case.rb".freeze, "lib/liquid/tags/comment.rb".freeze, "lib/liquid/tags/continue.rb".freeze, "lib/liquid/tags/cycle.rb".freeze, "lib/liquid/tags/decrement.rb".freeze, "lib/liquid/tags/for.rb".freeze, "lib/liquid/tags/if.rb".freeze, "lib/liquid/tags/ifchanged.rb".freeze, "lib/liquid/tags/include.rb".freeze, "lib/liquid/tags/increment.rb".freeze, "lib/liquid/tags/raw.rb".freeze, "lib/liquid/tags/table_row.rb".freeze, "lib/liquid/tags/unless.rb".freeze, "lib/liquid/template.rb".freeze, "lib/liquid/tokenizer.rb".freeze, "lib/liquid/truffle.rb".freeze, "lib/liquid/utils.rb".freeze, "lib/liquid/variable.rb".freeze, "lib/liquid/variable_lookup.rb".freeze, "lib/liquid/version.rb".freeze, "test/fixtures/en_locale.yml".freeze, "test/integration/assign_test.rb".freeze, "test/integration/blank_test.rb".freeze, "test/integration/block_test.rb".freeze, "test/integration/capture_test.rb".freeze, "test/integration/context_test.rb".freeze, "test/integration/document_test.rb".freeze, "test/integration/drop_test.rb".freeze, "test/integration/error_handling_test.rb".freeze, "test/integration/filter_test.rb".freeze, "test/integration/hash_ordering_test.rb".freeze, "test/integration/output_test.rb".freeze, "test/integration/parse_tree_visitor_test.rb".freeze, "test/integration/parsing_quirks_test.rb".freeze, "test/integration/render_profiling_test.rb".freeze, "test/integration/security_test.rb".freeze, "test/integration/standard_filter_test.rb".freeze, "test/integration/tags/break_tag_test.rb".freeze, "test/integration/tags/continue_tag_test.rb".freeze, "test/integration/tags/for_tag_test.rb".freeze, "test/integration/tags/if_else_tag_test.rb".freeze, "test/integration/tags/include_tag_test.rb".freeze, "test/integration/tags/increment_tag_test.rb".freeze, "test/integration/tags/raw_tag_test.rb".freeze, "test/integration/tags/standard_tag_test.rb".freeze, "test/integration/tags/statements_test.rb".freeze, "test/integration/tags/table_row_test.rb".freeze, "test/integration/tags/unless_else_tag_test.rb".freeze, "test/integration/template_test.rb".freeze, "test/integration/trim_mode_test.rb".freeze, "test/integration/variable_test.rb".freeze, "test/test_helper.rb".freeze, "test/truffle/truffle_test.rb".freeze, "test/unit/block_unit_test.rb".freeze, "test/unit/condition_unit_test.rb".freeze, "test/unit/context_unit_test.rb".freeze, "test/unit/file_system_unit_test.rb".freeze, "test/unit/i18n_unit_test.rb".freeze, "test/unit/lexer_unit_test.rb".freeze, "test/unit/parser_unit_test.rb".freeze, "test/unit/regexp_unit_test.rb".freeze, "test/unit/strainer_unit_test.rb".freeze, "test/unit/tag_unit_test.rb".freeze, "test/unit/tags/case_tag_unit_test.rb".freeze, "test/unit/tags/for_tag_unit_test.rb".freeze, "test/unit/tags/if_tag_unit_test.rb".freeze, "test/unit/template_unit_test.rb".freeze, "test/unit/tokenizer_unit_test.rb".freeze, "test/unit/variable_unit_test.rb".freeze] s.homepage = "http://www.liquidmarkup.org".freeze s.licenses = ["MIT".freeze] s.required_ruby_version = Gem::Requirement.new(">= 2.1.0".freeze) s.rubygems_version = "2.5.2.1".freeze s.summary = "A secure, non-evaling end user template engine with aesthetic markup.".freeze s.test_files = ["test/fixtures/en_locale.yml".freeze, "test/integration/assign_test.rb".freeze, "test/integration/blank_test.rb".freeze, "test/integration/block_test.rb".freeze, "test/integration/capture_test.rb".freeze, "test/integration/context_test.rb".freeze, "test/integration/document_test.rb".freeze, "test/integration/drop_test.rb".freeze, "test/integration/error_handling_test.rb".freeze, "test/integration/filter_test.rb".freeze, "test/integration/hash_ordering_test.rb".freeze, "test/integration/output_test.rb".freeze, "test/integration/parse_tree_visitor_test.rb".freeze, "test/integration/parsing_quirks_test.rb".freeze, "test/integration/render_profiling_test.rb".freeze, "test/integration/security_test.rb".freeze, "test/integration/standard_filter_test.rb".freeze, "test/integration/tags/break_tag_test.rb".freeze, "test/integration/tags/continue_tag_test.rb".freeze, "test/integration/tags/for_tag_test.rb".freeze, "test/integration/tags/if_else_tag_test.rb".freeze, "test/integration/tags/include_tag_test.rb".freeze, "test/integration/tags/increment_tag_test.rb".freeze, "test/integration/tags/raw_tag_test.rb".freeze, "test/integration/tags/standard_tag_test.rb".freeze, "test/integration/tags/statements_test.rb".freeze, "test/integration/tags/table_row_test.rb".freeze, "test/integration/tags/unless_else_tag_test.rb".freeze, "test/integration/template_test.rb".freeze, "test/integration/trim_mode_test.rb".freeze, "test/integration/variable_test.rb".freeze, "test/test_helper.rb".freeze, "test/truffle/truffle_test.rb".freeze, "test/unit/block_unit_test.rb".freeze, "test/unit/condition_unit_test.rb".freeze, "test/unit/context_unit_test.rb".freeze, "test/unit/file_system_unit_test.rb".freeze, "test/unit/i18n_unit_test.rb".freeze, "test/unit/lexer_unit_test.rb".freeze, "test/unit/parser_unit_test.rb".freeze, "test/unit/regexp_unit_test.rb".freeze, "test/unit/strainer_unit_test.rb".freeze, "test/unit/tag_unit_test.rb".freeze, "test/unit/tags/case_tag_unit_test.rb".freeze, "test/unit/tags/for_tag_unit_test.rb".freeze, "test/unit/tags/if_tag_unit_test.rb".freeze, "test/unit/template_unit_test.rb".freeze, "test/unit/tokenizer_unit_test.rb".freeze, "test/unit/variable_unit_test.rb".freeze] if s.respond_to? :specification_version then s.specification_version = 4 if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then s.add_development_dependency(%q.freeze, [">= 0"]) s.add_development_dependency(%q.freeze, ["~> 11.3"]) else s.add_dependency(%q.freeze, [">= 0"]) s.add_dependency(%q.freeze, ["~> 11.3"]) end else s.add_dependency(%q.freeze, [">= 0"]) s.add_dependency(%q.freeze, ["~> 11.3"]) end end ruby-liquid-4.0.3/test/000077500000000000000000000000001351407261700147515ustar00rootroot00000000000000ruby-liquid-4.0.3/test/fixtures/000077500000000000000000000000001351407261700166225ustar00rootroot00000000000000ruby-liquid-4.0.3/test/fixtures/en_locale.yml000066400000000000000000000003751351407261700212730ustar00rootroot00000000000000--- 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" ruby-liquid-4.0.3/test/integration/000077500000000000000000000000001351407261700172745ustar00rootroot00000000000000ruby-liquid-4.0.3/test/integration/assign_test.rb000066400000000000000000000024101351407261700221410ustar00rootroot00000000000000require '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 end # AssignTest ruby-liquid-4.0.3/test/integration/blank_test.rb000066400000000000000000000063131351407261700217520ustar00rootroot00000000000000require 'test_helper' class FoobarTag < Liquid::Tag def render(*args) " " end Liquid::Template.register_tag('foobar', FoobarTag) 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 assert_template_result(" " * N, wrap_in_for("{% foobar %}")) 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 ruby-liquid-4.0.3/test/integration/block_test.rb000066400000000000000000000005101351407261700217460ustar00rootroot00000000000000require '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 end ruby-liquid-4.0.3/test/integration/capture_test.rb000066400000000000000000000027071351407261700223310ustar00rootroot00000000000000require '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 end # CaptureTest ruby-liquid-4.0.3/test/integration/context_test.rb000066400000000000000000000013371351407261700223500ustar00rootroot00000000000000require 'test_helper' class ContextTest < Minitest::Test include Liquid 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 end ruby-liquid-4.0.3/test/integration/document_test.rb000066400000000000000000000007221351407261700224770ustar00rootroot00000000000000require '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 ruby-liquid-4.0.3/test/integration/drop_test.rb000066400000000000000000000241101351407261700216220ustar00rootroot00000000000000require '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.to_s end end def texts TextDrop.new end def catchall CatchallDrop.new end def context ContextDrop.new end def user_input "foo".taint 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_rendering_raises_on_tainted_attr with_taint_mode(:error) do tpl = Liquid::Template.parse('{{ product.user_input }}') assert_raises TaintedError do tpl.render!('product' => ProductDrop.new) end end end def test_rendering_warns_on_tainted_attr with_taint_mode(:warn) do tpl = Liquid::Template.parse('{{ product.user_input }}') context = Context.new('product' => ProductDrop.new) tpl.render!(context) assert_equal [Liquid::TaintedError], context.warnings.map(&:class) assert_equal "variable 'product.user_input' is tainted and was not escaped", context.warnings.first.to_s(false) end end def test_rendering_doesnt_raise_on_escaped_tainted_attr with_taint_mode(:error) do tpl = Liquid::Template.parse('{{ product.user_input | escape }}') tpl.render!('product' => ProductDrop.new) end end def test_drop_does_only_respond_to_whitelisted_methods 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_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 end # DropsTest ruby-liquid-4.0.3/test/integration/error_handling_test.rb000066400000000000000000000204111351407261700236530ustar00rootroot00000000000000require '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(%q( 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(%q( 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 end ruby-liquid-4.0.3/test/integration/filter_test.rb000066400000000000000000000127451351407261700221560ustar00rootroot00000000000000require 'test_helper' module MoneyFilter def money(input) sprintf(' %d$ ', input) end def money_with_underscore(input) sprintf(' %d$ ', input) end end module CanadianMoneyFilter def money(input) sprintf(' %d$ CAD ', input) end end module SubstituteFilter def substitute(input, params = {}) input.gsub(/%\{(\w+)\}/) { |match| params[$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 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 ruby-liquid-4.0.3/test/integration/hash_ordering_test.rb000066400000000000000000000007201351407261700234730ustar00rootroot00000000000000require 'test_helper' class HashOrderingTest < Minitest::Test module MoneyFilter def money(input) sprintf(' %d$ ', input) end end module CanadianMoneyFilter def money(input) sprintf(' %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 ruby-liquid-4.0.3/test/integration/output_test.rb000066400000000000000000000063461351407261700222310ustar00rootroot00000000000000require '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 ruby-liquid-4.0.3/test/integration/parse_tree_visitor_test.rb000066400000000000000000000106531351407261700245750ustar00rootroot00000000000000# 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_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_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_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, &:name) end def visit(template) traversal(template).visit.flatten.compact end end ruby-liquid-4.0.3/test/integration/parsing_quirks_test.rb000066400000000000000000000070771351407261700237340ustar00rootroot00000000000000require '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_contains_in_id assert_template_result(' YES ', '{% if containsallshipments == true %} YES {% endif %}', 'containsallshipments' => true) end end # ParsingQuirksTest ruby-liquid-4.0.3/test/integration/render_profiling_test.rb000066400000000000000000000105551351407261700242160ustar00rootroot00000000000000require 'test_helper' class RenderProfilingTest < Minitest::Test 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_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 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 end ruby-liquid-4.0.3/test/integration/security_test.rb000066400000000000000000000043241351407261700225320ustar00rootroot00000000000000require '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_add_filters_to_symbol_table current_symbols = Symbol.all_symbols test = %( {{ "some_string" | a_bad_filter }} ) template = Template.parse(test) assert_equal [], (Symbol.all_symbols - current_symbols) template.render! 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 ruby-liquid-4.0.3/test/integration/standard_filter_test.rb000066400000000000000000000625271351407261700240410ustar00rootroot00000000000000# encoding: utf-8 require 'test_helper' class Filters include Liquid::StandardFilters end 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 test "testfoo" 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 include Liquid def setup @filters = Filters.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_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) 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" => "delta" }, { "handle" => "beta" } ] 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") testdrop = TestDrop.new assert_equal [testdrop], @filters.uniq([testdrop, TestDrop.new], 'test') 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_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 p = proc{ drop } templ = '{{ procs | map: "test" }}' 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 '2 2 2 2', @filters.replace('1 1 1 1', '1', 2) assert_equal '2 2 2 2', @filters.replace('1 1 1 1', 1, 2) assert_equal '2 1 1 1', @filters.replace_first('1 1 1 1', '1', 2) assert_equal '2 1 1 1', @filters.replace_first('1 1 1 1', 1, 2) assert_template_result '2 1 1 1', "{{ '1 1 1 1' | replace_first: '1', 2 }}" end def test_remove assert_equal ' ', @filters.remove("a a a a", 'a') assert_equal ' ', @filters.remove("1 1 1 1", 1) assert_equal 'a a a', @filters.remove_first("a a a a", 'a ') assert_equal ' 1 1 1', @filters.remove_first("1 1 1 1", 1) assert_template_result 'a a a', "{{ 'a a a a' | remove_first: 'a ' }}" 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" 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") 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_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_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 ruby-liquid-4.0.3/test/integration/tags/000077500000000000000000000000001351407261700202325ustar00rootroot00000000000000ruby-liquid-4.0.3/test/integration/tags/break_tag_test.rb000066400000000000000000000005111351407261700235320ustar00rootroot00000000000000require '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 ruby-liquid-4.0.3/test/integration/tags/continue_tag_test.rb000066400000000000000000000005131351407261700242740ustar00rootroot00000000000000require '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 ruby-liquid-4.0.3/test/integration/tags/for_tag_test.rb000066400000000000000000000341161351407261700232440ustar00rootroot00000000000000require '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_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 end ruby-liquid-4.0.3/test/integration/tags/if_else_tag_test.rb000066400000000000000000000221011351407261700240530ustar00rootroot00000000000000require '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 ruby-liquid-4.0.3/test/integration/tags/include_tag_test.rb000066400000000000000000000206331351407261700241000ustar00rootroot00000000000000require 'test_helper' class TestFileSystem def read_template_file(template_path) case template_path when "product" "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 = $1 super end def parse(tokens) end def render(context) @template_name[1..-2] 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_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 ruby-liquid-4.0.3/test/integration/tags/increment_tag_test.rb000066400000000000000000000014631351407261700244410ustar00rootroot00000000000000require '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 ruby-liquid-4.0.3/test/integration/tags/raw_tag_test.rb000066400000000000000000000026201351407261700232420ustar00rootroot00000000000000require '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 }}' 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 ruby-liquid-4.0.3/test/integration/tags/standard_tag_test.rb000066400000000000000000000310121351407261700242460ustar00rootroot00000000000000require '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_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 ruby-liquid-4.0.3/test/integration/tags/statements_test.rb000066400000000000000000000070321351407261700240070ustar00rootroot00000000000000require '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 ruby-liquid-4.0.3/test/integration/tags/table_row_test.rb000066400000000000000000000065551351407261700236070ustar00rootroot00000000000000require '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 ruby-liquid-4.0.3/test/integration/tags/unless_else_tag_test.rb000066400000000000000000000023111351407261700247670ustar00rootroot00000000000000require '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 ruby-liquid-4.0.3/test/integration/template_test.rb000066400000000000000000000264501351407261700225020ustar00rootroot00000000000000require '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 = 5 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! refute_nil t.resource_limits.render_length 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_assign_score t = Template.parse("{% assign foo = 42 %}{% assign bar = 23 %}") t.resource_limits.assign_score_limit = 1 assert_equal "Liquid error: Memory limits exceeded", t.render assert t.resource_limits.reached? t.resource_limits.assign_score_limit = 2 assert_equal "", t.render! refute_nil t.resource_limits.assign_score end def test_resource_limits_assign_score_nested t = Template.parse("{% assign foo = 'aaaa' | reverse %}") t.resource_limits.assign_score_limit = 3 assert_equal "Liquid error: Memory limits exceeded", t.render assert t.resource_limits.reached? t.resource_limits.assign_score_limit = 5 assert_equal "", t.render! end def test_resource_limits_aborts_rendering_after_first_error 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) %} {% assign foo = 1 %} {% endfor %}") t.render! assert t.resource_limits.assign_score > 0 assert t.resource_limits.render_score > 0 assert t.resource_limits.render_length > 0 end def test_render_length_persists_between_blocks t = Template.parse("{% if true %}aaaa{% endif %}") t.resource_limits.render_length_limit = 7 assert_equal "Liquid error: Memory limits exceeded", t.render t.resource_limits.render_length_limit = 8 assert_equal "aaaa", t.render t = Template.parse("{% if true %}aaaa{% endif %}{% if true %}bbb{% endif %}") t.resource_limits.render_length_limit = 13 assert_equal "Liquid error: Memory limits exceeded", t.render t.resource_limits.render_length_limit = 14 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 = 11 assert_equal "Liquid error: Memory limits exceeded", t.render t.resource_limits.render_length_limit = 12 assert_equal "ababab", t.render end def test_default_resource_limits_unaffected_by_render_with_context context = Context.new t = Template.parse("{% for a in (1..100) %} {% assign foo = 1 %} {% endfor %}") t.render!(context) assert context.resource_limits.assign_score > 0 assert context.resource_limits.render_score > 0 assert context.resource_limits.render_length > 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 Liquid::Template.error_mode = :strict t = Template.parse("some{{x}}thing") 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 end ruby-liquid-4.0.3/test/integration/trim_mode_test.rb000066400000000000000000000236731351407261700226520ustar00rootroot00000000000000require '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 end # TrimModeTest ruby-liquid-4.0.3/test/integration/variable_test.rb000066400000000000000000000063741351407261700224570ustar00rootroot00000000000000require '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_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_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 template = Template.parse(%({{ test.test }})) assert_equal 'worked', template.render!('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 end ruby-liquid-4.0.3/test/test_helper.rb000077500000000000000000000055461351407261700176310ustar00rootroot00000000000000#!/usr/bin/env ruby 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).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).render!(assigns), message end def assert_match_syntax_error(match, template, assigns = {}) exception = assert_raises(Liquid::SyntaxError) do Template.parse(template).render(assigns) end assert_match match, exception.message end def with_global_filter(*globals) original_global_strainer = Liquid::Strainer.class_variable_get(:@@global_strainer) Liquid::Strainer.class_variable_set(:@@global_strainer, Class.new(Liquid::Strainer) do @filter_methods = Set.new end) Liquid::Strainer.class_variable_get(:@@strainer_class_cache).clear globals.each do |global| Liquid::Template.register_filter(global) end yield ensure Liquid::Strainer.class_variable_get(:@@strainer_class_cache).clear Liquid::Strainer.class_variable_set(:@@global_strainer, original_global_strainer) end def with_taint_mode(mode) old_mode = Liquid::Template.taint_mode Liquid::Template.taint_mode = mode yield ensure Liquid::Template.taint_mode = old_mode end 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 end end class ThingWithToLiquid def to_liquid 'foobar' 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 ruby-liquid-4.0.3/test/truffle/000077500000000000000000000000001351407261700164205ustar00rootroot00000000000000ruby-liquid-4.0.3/test/truffle/truffle_test.rb000066400000000000000000000001601351407261700214500ustar00rootroot00000000000000require 'test_helper' class TruffleTest < Minitest::Test include Liquid def test_truffle_works end end ruby-liquid-4.0.3/test/unit/000077500000000000000000000000001351407261700157305ustar00rootroot00000000000000ruby-liquid-4.0.3/test/unit/block_unit_test.rb000066400000000000000000000034531351407261700214520ustar00rootroot00000000000000require '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 def test_with_custom_tag Liquid::Template.register_tag("testtag", Block) assert Liquid::Template.parse("{% testtag %} {% endtesttag %}") ensure Liquid::Template.tags.delete('testtag') end private def block_types(nodelist) nodelist.collect(&:class) end end # VariableTest ruby-liquid-4.0.3/test/unit/condition_unit_test.rb000066400000000000000000000120631351407261700223430ustar00rootroot00000000000000require '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 assert_equal true, Condition.new(1, '==', 1).evaluate 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 assert_nil Condition.new(2, '>', {}).evaluate assert_equal false, Condition.new({}, '==', 2).evaluate assert_equal true, Condition.new({ 'a' => 1 }, '==', { 'a' => 1 }).evaluate assert_equal true, Condition.new({ 'a' => 2 }, 'contains', 'a').evaluate 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 condition.or Condition.new(2, '==', 1) assert_equal false, condition.evaluate condition.or Condition.new(1, '==', 1) assert_equal true, condition.evaluate end def test_and_condition condition = Condition.new(1, '==', 1) assert_equal true, condition.evaluate condition.and Condition.new(2, '==', 2) assert_equal true, condition.evaluate condition.and Condition.new(2, '==', 1) assert_equal false, condition.evaluate end def test_should_allow_custom_proc_operator Condition.operators['starts_with'] = proc { |cond, left, right| left =~ %r{^#{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 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 ruby-liquid-4.0.3/test/unit/context_unit_test.rb000066400000000000000000000305041351407261700220410ustar00rootroot00000000000000require 'test_helper' class HundredCentes def to_liquid 100 end end class CentsDrop < Liquid::Drop def amount HundredCentes.new end def non_zero? true end end class ContextSensitiveDrop < Liquid::Drop def test @context['test'] end end class Category < Liquid::Drop attr_accessor :name def initialize(name) @name = name end def to_liquid CategoryDrop.new(self) end end class CategoryDrop attr_accessor :category, :context def initialize(category) @category = category end end class CounterDrop < Liquid::Drop def count @count ||= 0 @count += 1 end end class ArrayLike def fetch(index) end def [](index) @counts ||= [] @counts[index] ||= 0 @counts[index] += 1 end def to_liquid self end end class ContextUnitTest < 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 = DateTime.now @context['datetime'] = now assert_equal now, @context['datetime'] @context['bool'] = true assert_equal true, @context['bool'] @context['bool'] = false assert_equal false, @context['bool'] @context['nil'] = nil assert_nil @context['nil'] assert_nil @context['nil'] end def test_variables_not_existing assert_nil @context['does_not_exist'] end def test_scoping @context.push @context.pop assert_raises(Liquid::ContextError) do @context.pop end assert_raises(Liquid::ContextError) do @context.push @context.pop @context.pop end end def test_length_query @context['numbers'] = [1, 2, 3, 4] assert_equal 4, @context['numbers.size'] @context['numbers'] = { 1 => 1, 2 => 2, 3 => 3, 4 => 4 } assert_equal 4, @context['numbers.size'] @context['numbers'] = { 1 => 1, 2 => 2, 3 => 3, 4 => 4, 'size' => 1000 } assert_equal 1000, @context['numbers.size'] end def test_hyphenated_variable @context['oh-my'] = 'godz' assert_equal 'godz', @context['oh-my'] end def test_add_filter filter = Module.new do def hi(output) output + ' hi!' end end context = Context.new context.add_filters(filter) assert_equal 'hi? hi!', context.invoke(:hi, 'hi?') context = Context.new assert_equal 'hi?', context.invoke(:hi, 'hi?') context.add_filters(filter) assert_equal 'hi? hi!', context.invoke(:hi, 'hi?') end def test_only_intended_filters_make_it_there filter = Module.new do def hi(output) output + ' hi!' end end context = Context.new assert_equal "Wookie", context.invoke("hi", "Wookie") context.add_filters(filter) assert_equal "Wookie hi!", context.invoke("hi", "Wookie") end def test_add_item_in_outer_scope @context['test'] = 'test' @context.push assert_equal 'test', @context['test'] @context.pop assert_equal 'test', @context['test'] end def test_add_item_in_inner_scope @context.push @context['test'] = 'test' assert_equal 'test', @context['test'] @context.pop assert_nil @context['test'] end def test_hierachical_data @context['hash'] = { "name" => 'tobi' } assert_equal 'tobi', @context['hash.name'] assert_equal 'tobi', @context['hash["name"]'] end def test_keywords assert_equal true, @context['true'] assert_equal false, @context['false'] end def test_digits assert_equal 100, @context['100'] assert_equal 100.00, @context['100.00'] end def test_strings assert_equal "hello!", @context['"hello!"'] assert_equal "hello!", @context["'hello!'"] end def test_merge @context.merge({ "test" => "test" }) assert_equal 'test', @context['test'] @context.merge({ "test" => "newvalue", "foo" => "bar" }) assert_equal 'newvalue', @context['test'] assert_equal 'bar', @context['foo'] end def test_array_notation @context['test'] = [1, 2, 3, 4, 5] assert_equal 1, @context['test[0]'] assert_equal 2, @context['test[1]'] assert_equal 3, @context['test[2]'] assert_equal 4, @context['test[3]'] assert_equal 5, @context['test[4]'] end def test_recoursive_array_notation @context['test'] = { 'test' => [1, 2, 3, 4, 5] } assert_equal 1, @context['test.test[0]'] @context['test'] = [{ 'test' => 'worked' }] assert_equal 'worked', @context['test[0].test'] end def test_hash_to_array_transition @context['colors'] = { 'Blue' => ['003366', '336699', '6699CC', '99CCFF'], 'Green' => ['003300', '336633', '669966', '99CC99'], 'Yellow' => ['CC9900', 'FFCC00', 'FFFF99', 'FFFFCC'], 'Red' => ['660000', '993333', 'CC6666', 'FF9999'] } assert_equal '003366', @context['colors.Blue[0]'] assert_equal 'FF9999', @context['colors.Red[3]'] end def test_try_first @context['test'] = [1, 2, 3, 4, 5] assert_equal 1, @context['test.first'] assert_equal 5, @context['test.last'] @context['test'] = { 'test' => [1, 2, 3, 4, 5] } assert_equal 1, @context['test.test.first'] assert_equal 5, @context['test.test.last'] @context['test'] = [1] assert_equal 1, @context['test.first'] assert_equal 1, @context['test.last'] end def test_access_hashes_with_hash_notation @context['products'] = { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] } @context['product'] = { 'variants' => [ { 'title' => 'draft151cm' }, { 'title' => 'element151cm' } ] } assert_equal 5, @context['products["count"]'] assert_equal 'deepsnow', @context['products["tags"][0]'] assert_equal 'deepsnow', @context['products["tags"].first'] assert_equal 'draft151cm', @context['product["variants"][0]["title"]'] assert_equal 'element151cm', @context['product["variants"][1]["title"]'] assert_equal 'draft151cm', @context['product["variants"][0]["title"]'] assert_equal 'element151cm', @context['product["variants"].last["title"]'] end def test_access_variable_with_hash_notation @context['foo'] = 'baz' @context['bar'] = 'foo' assert_equal 'baz', @context['["foo"]'] assert_equal 'baz', @context['[bar]'] end def test_access_hashes_with_hash_access_variables @context['var'] = 'tags' @context['nested'] = { 'var' => 'tags' } @context['products'] = { 'count' => 5, 'tags' => ['deepsnow', 'freestyle'] } assert_equal 'deepsnow', @context['products[var].first'] assert_equal 'freestyle', @context['products[nested.var].last'] end def test_hash_notation_only_for_hash_access @context['array'] = [1, 2, 3, 4, 5] @context['hash'] = { 'first' => 'Hello' } assert_equal 1, @context['array.first'] assert_nil @context['array["first"]'] assert_equal 'Hello', @context['hash["first"]'] end def test_first_can_appear_in_middle_of_callchain @context['product'] = { 'variants' => [ { 'title' => 'draft151cm' }, { 'title' => 'element151cm' } ] } assert_equal 'draft151cm', @context['product.variants[0].title'] assert_equal 'element151cm', @context['product.variants[1].title'] assert_equal 'draft151cm', @context['product.variants.first.title'] assert_equal 'element151cm', @context['product.variants.last.title'] end def test_cents @context.merge("cents" => HundredCentes.new) assert_equal 100, @context['cents'] end def test_nested_cents @context.merge("cents" => { 'amount' => HundredCentes.new }) assert_equal 100, @context['cents.amount'] @context.merge("cents" => { 'cents' => { 'amount' => HundredCentes.new } }) assert_equal 100, @context['cents.cents.amount'] end def test_cents_through_drop @context.merge("cents" => CentsDrop.new) assert_equal 100, @context['cents.amount'] end def test_nested_cents_through_drop @context.merge("vars" => { "cents" => CentsDrop.new }) assert_equal 100, @context['vars.cents.amount'] end def test_drop_methods_with_question_marks @context.merge("cents" => CentsDrop.new) assert @context['cents.non_zero?'] end def test_context_from_within_drop @context.merge("test" => '123', "vars" => ContextSensitiveDrop.new) assert_equal '123', @context['vars.test'] end def test_nested_context_from_within_drop @context.merge("test" => '123', "vars" => { "local" => ContextSensitiveDrop.new }) assert_equal '123', @context['vars.local.test'] end def test_ranges @context.merge("test" => '5') assert_equal (1..5), @context['(1..5)'] assert_equal (1..5), @context['(1..test)'] assert_equal (5..5), @context['(test..test)'] end def test_cents_through_drop_nestedly @context.merge("cents" => { "cents" => CentsDrop.new }) assert_equal 100, @context['cents.cents.amount'] @context.merge("cents" => { "cents" => { "cents" => CentsDrop.new } }) assert_equal 100, @context['cents.cents.cents.amount'] end def test_drop_with_variable_called_only_once @context['counter'] = CounterDrop.new assert_equal 1, @context['counter.count'] assert_equal 2, @context['counter.count'] assert_equal 3, @context['counter.count'] end def test_drop_with_key_called_only_once @context['counter'] = CounterDrop.new assert_equal 1, @context['counter["count"]'] assert_equal 2, @context['counter["count"]'] assert_equal 3, @context['counter["count"]'] end def test_proc_as_variable @context['dynamic'] = proc { 'Hello' } assert_equal 'Hello', @context['dynamic'] end def test_lambda_as_variable @context['dynamic'] = proc { 'Hello' } assert_equal 'Hello', @context['dynamic'] end def test_nested_lambda_as_variable @context['dynamic'] = { "lambda" => proc { 'Hello' } } assert_equal 'Hello', @context['dynamic.lambda'] end def test_array_containing_lambda_as_variable @context['dynamic'] = [1, 2, proc { 'Hello' }, 4, 5] assert_equal 'Hello', @context['dynamic[2]'] end def test_lambda_is_called_once @context['callcount'] = proc { @global ||= 0; @global += 1; @global.to_s } assert_equal '1', @context['callcount'] assert_equal '1', @context['callcount'] assert_equal '1', @context['callcount'] @global = nil end def test_nested_lambda_is_called_once @context['callcount'] = { "lambda" => proc { @global ||= 0; @global += 1; @global.to_s } } assert_equal '1', @context['callcount.lambda'] assert_equal '1', @context['callcount.lambda'] assert_equal '1', @context['callcount.lambda'] @global = nil end def test_lambda_in_array_is_called_once @context['callcount'] = [1, 2, proc { @global ||= 0; @global += 1; @global.to_s }, 4, 5] assert_equal '1', @context['callcount[2]'] assert_equal '1', @context['callcount[2]'] assert_equal '1', @context['callcount[2]'] @global = nil end def test_access_to_context_from_proc @context.registers[:magic] = 345392 @context['magic'] = proc { @context.registers[:magic] } assert_equal 345392, @context['magic'] end def test_to_liquid_and_context_at_first_level @context['category'] = Category.new("foobar") assert_kind_of CategoryDrop, @context['category'] assert_equal @context, @context['category'].context end def test_interrupt_avoids_object_allocations assert_no_object_allocations do @context.interrupt? end end def test_context_initialization_with_a_proc_in_environment contx = Context.new([test: ->(c) { c['poutine'] }], { test: :foo }) assert contx assert_nil contx['poutine'] end def test_apply_global_filter global_filter_proc = ->(output) { "#{output} filtered" } context = Context.new context.global_filter = global_filter_proc assert_equal 'hi filtered', context.apply_global_filter('hi') end def test_apply_global_filter_when_no_global_filter_exist context = Context.new assert_equal 'hi', context.apply_global_filter('hi') 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 ruby-liquid-4.0.3/test/unit/file_system_unit_test.rb000066400000000000000000000020561351407261700227010ustar00rootroot00000000000000require '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 ruby-liquid-4.0.3/test/unit/i18n_unit_test.rb000066400000000000000000000016411351407261700211340ustar00rootroot00000000000000require '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 ruby-liquid-4.0.3/test/unit/lexer_unit_test.rb000066400000000000000000000030771351407261700215010ustar00rootroot00000000000000require '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 ruby-liquid-4.0.3/test/unit/parser_unit_test.rb000066400000000000000000000043231351407261700216510ustar00rootroot00000000000000require '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 ruby-liquid-4.0.3/test/unit/regexp_unit_test.rb000066400000000000000000000031031351407261700216420ustar00rootroot00000000000000require '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 ruby-liquid-4.0.3/test/unit/strainer_unit_test.rb000066400000000000000000000113251351407261700222040ustar00rootroot00000000000000require 'test_helper' class StrainerUnitTest < Minitest::Test include Liquid module AccessScopeFilters def public_filter "public" end def private_filter "private" end private :private_filter end Strainer.global_filter(AccessScopeFilters) def test_strainer strainer = Strainer.create(nil) assert_equal 5, strainer.invoke('size', 'input') assert_equal "public", strainer.invoke("public_filter") end def test_stainer_raises_argument_error strainer = Strainer.create(nil) assert_raises(Liquid::ArgumentError) do strainer.invoke("public_filter", 1) end end def test_stainer_argument_error_contains_backtrace strainer = Strainer.create(nil) begin strainer.invoke("public_filter", 1) rescue Liquid::ArgumentError => e assert_match( /\ALiquid error: wrong number of arguments \((1 for 0|given 1, expected 0)\)\z/, e.message) assert_equal e.backtrace[0].split(':')[0], __FILE__ end end def test_strainer_only_invokes_public_filter_methods strainer = Strainer.create(nil) 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 = Strainer.create(nil) 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 = Strainer.create(nil) assert_equal "password", strainer.invoke("undef_the_method", "password") end def test_strainer_only_allows_methods_defined_in_filters strainer = Strainer.create(nil) 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 = Strainer.create(nil, [a, b]) assert_kind_of Strainer, strainer assert_kind_of a, strainer assert_kind_of b, strainer assert_kind_of Liquid::StandardFilters, strainer end def test_add_filter_when_wrong_filter_class c = Context.new s = c.strainer wrong_filter = ->(v) { v.reverse } assert_raises ArgumentError do s.class.add_filter(wrong_filter) end 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 strainer = Context.new.strainer strainer.class.add_filter(PublicMethodOverrideFilter) assert strainer.class.filter_methods.include?('public_filter') end module LateAddedFilter def late_added_filter(input) "filtered" end end def test_global_filter_clears_cache assert_equal 'input', Strainer.create(nil).invoke('late_added_filter', 'input') Strainer.global_filter(LateAddedFilter) assert_equal 'filtered', Strainer.create(nil).invoke('late_added_filter', 'input') 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 # StrainerTest ruby-liquid-4.0.3/test/unit/tag_unit_test.rb000066400000000000000000000011601351407261700211240ustar00rootroot00000000000000require '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 ruby-liquid-4.0.3/test/unit/tags/000077500000000000000000000000001351407261700166665ustar00rootroot00000000000000ruby-liquid-4.0.3/test/unit/tags/case_tag_unit_test.rb000066400000000000000000000004671351407261700230660ustar00rootroot00000000000000require '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 ruby-liquid-4.0.3/test/unit/tags/for_tag_unit_test.rb000066400000000000000000000007431351407261700227360ustar00rootroot00000000000000require '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 ruby-liquid-4.0.3/test/unit/tags/if_tag_unit_test.rb000066400000000000000000000004131351407261700225400ustar00rootroot00000000000000require '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 ruby-liquid-4.0.3/test/unit/template_unit_test.rb000066400000000000000000000043231351407261700221700ustar00rootroot00000000000000require '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 end ruby-liquid-4.0.3/test/unit/tokenizer_unit_test.rb000066400000000000000000000032641351407261700223720ustar00rootroot00000000000000require '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 tokenize(source) tokenizer = Liquid::Tokenizer.new(source) tokens = [] while t = tokenizer.shift tokens << t end tokens end def tokenize_line_numbers(source) tokenizer = Liquid::Tokenizer.new(source, true) line_numbers = [] loop do line_number = tokenizer.line_number if tokenizer.shift line_numbers << line_number else break end end line_numbers end end ruby-liquid-4.0.3/test/unit/variable_unit_test.rb000066400000000000000000000125601351407261700221440ustar00rootroot00000000000000require '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