Compare commits

..
Author SHA1 Message Date
Marco Concetto Rudilosso ddc32b7bd8 actually test whitespace stripping 2023-02-01 15:52:34 +00:00
Marco Concetto Rudilosso 74e505f6fa Allow raw tag to be used with whitespace control 2023-02-01 13:49:51 +00:00
Michael GoandGitHub 6a888d4564 Merge pull request #1680 from Shopify/recursively-parse-brackets
recursively parse brackets on variable lookup
2023-01-31 16:29:54 -04:00
Michael Go dd257b3d66 add an unit test for double nest variable lookup 2023-01-30 20:36:14 -04:00
Michael Go 1aaf6ed019 recursively parse brackets on variable lookup 2023-01-30 20:19:35 -04:00
Michael GoandGitHub daf93a83c2 Merge pull request #1676 from Shopify/tablerow-nil-params
raise invalid integer argument error from tablerow
2023-01-18 11:08:04 -04:00
Michael Go e889a9da0b use to_i to parse parameters of tablerow tag 2023-01-17 17:48:16 -04:00
Michael Go 0f11c97623 raise invalid integer argument error from tablerow 2023-01-16 18:53:53 -04:00
Dylan Thacker-SmithandGitHub e804f36681 Merge pull request #1674 from Shopify/better-test-to-liquid-value
Improve test coverage of the to_liquid_value feature.
2023-01-16 10:44:41 -05:00
Dylan Thacker-Smith 619ed3fcd7 Add some additional to_liquid_value assertions 2023-01-13 15:46:12 -05:00
Dylan Thacker-Smith cdb5cb06b2 Remove == method from drops from testing to_liquid_value
Since they could cause tests to pass without to_liquid_value being called
on the left side of the equality comparison.
2023-01-13 15:37:27 -05:00
Jean byroot BoussierandGitHub bc153159e6 Merge pull request #1673 from Shopify/initialize-context
Initialize context to nil on the Drop class
2023-01-12 22:19:28 +01:00
Jemma Issroff 128b4e35be Initialize context to nil on the Drop class
We believe this will reduce megamorphic exits in YJIT. There are
currently 241 separate shapes generated with edge_name "@context" in
SFR. Initializing it to nil will significantly reduce this number.
2023-01-12 15:35:22 -05:00
Dylan Thacker-SmithandGitHub c743936a78 Instrument usage of bug with iteration of String with offset or 0 limit (#1667) 2023-01-11 14:05:50 -05:00
Dylan Thacker-SmithandGitHub bf711a0521 Provide another assertion for to_liquid_value unless tag test (#1672)
Since the original one would pass even if to_liquid_value isn't called on
the BooleanDrop object.
2023-01-11 14:05:27 -05:00
Dylan Thacker-SmithandGitHub e8731f27d9 Merge pull request #1671 from ashmaroli/use-builtin-bundle-cache
Use cache built into `ruby/setup-ruby` action
2023-01-11 11:40:11 -05:00
Ashwin Maroli 6a44c1ec77 Do not set ENV["BUNDLE_PATH"] 2023-01-11 22:06:47 +05:30
Ashwin Maroli 1beb87b446 Bump latest Ruby version in CI matrix 2023-01-11 20:58:17 +05:30
Ashwin Maroli b839deb3a8 Use cache built into ruby/setup-ruby action 2023-01-11 20:50:44 +05:30
liamgriffinandGitHub 0b826120c0 Merge pull request #1665 from Shopify/update-description-divided_by-filter
[Liquid reference docs] Adding more content to divided_by filter
2023-01-09 14:12:23 +00:00
liamgriffin 936f803a4e Removed trailing space 2023-01-06 11:36:20 +00:00
liamgriffin 5cd8a83fa6 Adding more content to divided_by filter
Adding more content to divided_by filter, by detailing the behaviour of how different results are generated by different types of divisors.
2023-01-06 11:19:13 +00:00
Dylan Thacker-SmithandGitHub c2c6cb2b15 Allow commas to separate for tag attributes (#1658)
For consistency with tags like the `render` tag, where we actually
prefer to use commas to separate attributes
2022-11-29 09:00:55 -05:00
shainaraskasandGitHub 9ccbf64571 Merge pull request #1656 from Shopify/sr-inline-comment
clarify inline_comment
2022-11-14 10:38:55 -05:00
Shaina Raskas 8b1b9f649a remove separate inline comment tag for IA reasons 2022-11-11 16:21:18 -05:00
Shaina Raskas 474315c6bb edits 2022-11-11 16:03:52 -05:00
Shaina Raskas 667664bb22 clarify inline_comment 2022-11-11 15:34:35 -05:00
Dylan Thacker-SmithandGitHub 1cdd1f0834 refactor: Advance tokenizer array offset instead of using Array#shift (#1653)
Array#shift would move all the remaining elements of the array, which
is slower for larger arrays.
2022-11-09 13:03:33 -05:00
Dylan Thacker-SmithandGitHub 1cd5ec54f0 Merge pull request #1641 from Shopify/assert-template-result-static-env
Use static environment assert_template_result input values
2022-11-02 10:05:52 -04:00
Peter ZhuandGitHub 29732f4305 Merge pull request #1345 from Shopify/pz-fix-tags-in-comment
Fix tags in comment
2022-11-01 13:07:03 -04:00
Peter Zhu abed47547c Fix tags in comment 2022-11-01 13:04:41 -04:00
Dylan Thacker-SmithandGitHub ace3fe15ac Merge pull request #1648 from Shopify/fix-include-internal-error
Prevent an internal error in include tag from non-string template_name
2022-10-28 14:29:03 -04:00
Dylan Thacker-Smith db8e85ab31 Prevent an internal error in include tag from non-string template_name
which would otherwise happen on `template_name.split('/')`
2022-10-28 13:59:00 -04:00
Dylan Thacker-Smith f484b868d0 assert_template_result: Avoid using the BlankFileSystem
Since it doesn't reflect the real liquid usage that we are trying
to test against.
2022-10-28 13:55:33 -04:00
Dylan Thacker-SmithandGitHub 3e8994c258 Have subclasses of a tag inherit superclass's disabled tags (#1646) 2022-10-27 11:13:50 -04:00
Erin MorrisseyandGitHub 7ccac29688 Merge pull request #1635 from Shopify/update-doc-links
Update link paths in docs markup
2022-10-26 11:07:46 -07:00
Dylan Thacker-SmithandGitHub 96b5325f87 Remove Usage.increment calls that have been shown to in fact be used (#1645)
I think these were added for undocumented features to see if they were
actually used and they were being used.
2022-10-26 09:21:43 -04:00
Erin Morrissey b8f05267a9 update link paths in docs markup 2022-10-25 11:53:27 -07:00
Dylan Thacker-SmithandGitHub 6ce4ec1011 tablerow: Avoid accidental special case for constant nil cols (#1644)
It should behave the same as an expression that evaluates to nil
2022-10-25 10:21:03 -04:00
Dylan Thacker-Smith a39422feac Use static environment assert_template_result input values
To match how we use liquid in practice
2022-10-21 11:45:29 -04:00
Dylan Thacker-Smith 2c2f5826d5 Change increment/decrement tests to avoid relying on input
So these can be tested using language tests, without needing support
for counter value input, which isn't needed if input is provided
through the static environment.
2022-10-21 11:43:58 -04:00
Dylan Thacker-SmithandGitHub c99c93255d Merge pull request #1617 from Shopify/better-filter-overflow-handling
Support big integers in truncatewords and slice filters
2022-10-11 12:55:33 -04:00
Dylan Thacker-Smith c0c191cabd Add assertions for truncate filter with large integers 2022-10-11 12:44:33 -04:00
Dylan Thacker-Smith 6765d93938 Avoid internal errors for large arguments to slice filter
Use saturating conversion, which has expected semantics for large integers.
2022-10-11 12:44:32 -04:00
Dylan Thacker-Smith 4f17abfb4a Handle truncatewords word length out of range as if no truncation is needed 2022-10-11 12:44:32 -04:00
Michael GoandGitHub eff2a63204 Merge pull request #1633 from Shopify/fix-tablerow-drop-cols-last-attr
fix tablerow drop's last attribute with missing cols param
2022-10-05 14:15:03 -03:00
Jan Gregor TriebelandGitHub b14bf94c98 Merge pull request #1636 from Shopify/documentation-clarification-for-comment
Clarifying description for {% comment %}
2022-10-05 14:54:29 +02:00
Jan Emge-Triebel 951be6c1f5 Clarified comment's liquid description 2022-09-30 09:07:06 +00:00
35 changed files with 283 additions and 135 deletions
+10 -14
View File
@@ -1,5 +1,10 @@
name: Liquid
on: [push, pull_request]
env:
BUNDLE_JOBS: 4
BUNDLE_RETRY: 3
jobs:
test:
runs-on: ubuntu-latest
@@ -7,22 +12,18 @@ jobs:
matrix:
entry:
- { ruby: 2.7, allowed-failure: false } # minimum supported
- { ruby: 3.1, allowed-failure: false } # latest
- { ruby: 3.2, allowed-failure: false } # latest
- { ruby: ruby-head, allowed-failure: true }
name: test (${{ matrix.entry.ruby }})
name: Test Ruby ${{ matrix.entry.ruby }}
steps:
- uses: actions/checkout@v3
- uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.entry.ruby }}
- uses: actions/cache@v1
with:
path: vendor/bundle
key: ${{ runner.os }}-gems-${{ hashFiles('Gemfile') }}
restore-keys: ${{ runner.os }}-gems-
- run: bundle install --jobs=3 --retry=3 --path=vendor/bundle
bundler-cache: true
- run: bundle exec rake
continue-on-error: ${{ matrix.entry.allowed-failure }}
memory_profile:
runs-on: ubuntu-latest
steps:
@@ -30,10 +31,5 @@ jobs:
- uses: ruby/setup-ruby@v1
with:
ruby-version: 2.7
- uses: actions/cache@v1
with:
path: vendor/bundle
key: ${{ runner.os }}-gems-${{ hashFiles('Gemfile') }}
restore-keys: ${{ runner.os }}-gems-
- run: bundle install --jobs=3 --retry=3 --path=vendor/bundle
bundler-cache: true
- run: bundle exec rake memory_profile:run
+1 -1
View File
@@ -41,7 +41,7 @@ module Liquid
AnyStartingTag = /#{TagStart}|#{VariableStart}/o
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om
VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o
VariableParser = /\[(?:[^\[\]]+|\g<0>)*\]|#{VariableSegment}+\??/o
RAISE_EXCEPTION_LAMBDA = ->(_e) { raise }
+10 -2
View File
@@ -109,14 +109,22 @@ module Liquid
end
end
private def parse_for_document(tokenizer, parse_context)
private def handle_invalid_tag_token(token, parse_context)
if token.end_with?('%}')
yield token, token
else
BlockBody.raise_missing_tag_terminator(token, parse_context)
end
end
private def parse_for_document(tokenizer, parse_context, &block)
while (token = tokenizer.shift)
next if token.empty?
case
when token.start_with?(TAGSTART)
whitespace_handler(token, parse_context)
unless token =~ FullToken
BlockBody.raise_missing_tag_terminator(token, parse_context)
return handle_invalid_tag_token(token, parse_context, &block)
end
tag_name = Regexp.last_match(2)
markup = Regexp.last_match(4)
+4
View File
@@ -25,6 +25,10 @@ module Liquid
class Drop
attr_writer :context
def initialize
@context = nil
end
# Catch all for the method
def liquid_method_missing(method)
return nil unless @context&.strict_variables
+2 -5
View File
@@ -5,7 +5,7 @@ module Liquid
# @liquid_type object
# @liquid_name forloop
# @liquid_summary
# Information about a parent [`for` loop](/api/liquid/tags#for).
# Information about a parent [`for` loop](/api/liquid/tags/for).
class ForloopDrop < Drop
def initialize(name, length, parentloop)
@name = name
@@ -30,10 +30,7 @@ module Liquid
# @liquid_return [forloop]
attr_reader :parentloop
def name
Usage.increment('forloop_drop_name')
@name
end
attr_reader :name
# @liquid_public_docs
# @liquid_summary
+26 -10
View File
@@ -6,7 +6,14 @@ require 'bigdecimal'
module Liquid
module StandardFilters
MAX_INT = (1 << 31) - 1
MAX_I32 = (1 << 31) - 1
private_constant :MAX_I32
MIN_I64 = -(1 << 63)
MAX_I64 = (1 << 63) - 1
I64_RANGE = MIN_I64..MAX_I64
private_constant :MIN_I64, :MAX_I64, :I64_RANGE
HTML_ESCAPE = {
'&' => '&amp;',
'>' => '&gt;',
@@ -186,10 +193,19 @@ module Liquid
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) || ''
begin
if input.is_a?(Array)
input.slice(offset, length) || []
else
input.to_s.slice(offset, length) || ''
end
rescue RangeError
if I64_RANGE.cover?(length) && I64_RANGE.cover?(offset)
raise # unexpected error
end
offset = offset.clamp(I64_RANGE)
length = length.clamp(I64_RANGE)
retry
end
end
@@ -239,9 +255,9 @@ module Liquid
wordlist = begin
input.split(" ", words + 1)
rescue RangeError
raise if words + 1 < MAX_INT
# e.g. integer #{words} too big to convert to `int'
raise Liquid::ArgumentError, "integer #{words} too big for truncatewords"
# integer too big for String#split, but we can semantically assume no truncation is needed
return input if words + 1 > MAX_I32
raise # unexpected error
end
return input if wordlist.length <= words
@@ -599,7 +615,7 @@ module Liquid
# @liquid_description
# > Note:
# > The `concat` filter won't filter out duplicates. If you want to remove duplicates, then you need to use the
# > [`uniq` filter](/api/liquid/filters#uniq).
# > [`uniq` filter](/api/liquid/filters/uniq).
# @liquid_syntax array | concat: array
# @liquid_return [array[untyped]]
def concat(input, array)
@@ -741,7 +757,7 @@ module Liquid
# @liquid_type filter
# @liquid_category math
# @liquid_summary
# Divides a number by a given number.
# Divides a number by a given number. The `divided_by` filter produces a result of the same type as the divisor. This means if you divide by an integer, the result will be an integer, and if you divide by a float, the result will be a float.
# @liquid_syntax number | divided_by: number
# @liquid_return [number]
def divided_by(input, operand)
+1 -1
View File
@@ -5,7 +5,7 @@ module Liquid
# @liquid_type object
# @liquid_name tablerowloop
# @liquid_summary
# Information about a parent [`tablerow` loop](/api/liquid/tags#tablerow).
# Information about a parent [`tablerow` loop](/api/liquid/tags/tablerow).
class TablerowloopDrop < Drop
def initialize(length, cols)
@length = length
+8 -2
View File
@@ -14,12 +14,18 @@ module Liquid
end
def disable_tags(*tag_names)
@disabled_tags ||= []
@disabled_tags.concat(tag_names)
tag_names += disabled_tags
define_singleton_method(:disabled_tags) { tag_names }
prepend(Disabler)
end
private :new
protected
def disabled_tags
[]
end
end
def initialize(tag_name, markup, parse_context)
-8
View File
@@ -3,14 +3,6 @@
module Liquid
class Tag
module Disabler
module ClassMethods
attr_reader :disabled_tags
end
def self.prepended(base)
base.extend(ClassMethods)
end
def render_to_output_buffer(context, output)
context.with_disabled_tags(self.class.disabled_tags) do
super
+1 -1
View File
@@ -15,7 +15,7 @@ module Liquid
# @liquid_category iteration
# @liquid_name break
# @liquid_summary
# Stops a [`for` loop](/api/liquid/tags#for) from iterating.
# Stops a [`for` loop](/api/liquid/tags/for) from iterating.
# @liquid_syntax
# {% break %}
class Break < Tag
+1 -1
View File
@@ -8,7 +8,7 @@ module Liquid
# @liquid_summary
# Prevents an expression from being rendered or output.
# @liquid_description
# Any text inside `comment` tags won't be output, and any Liquid code won't be rendered.
# Any text inside `comment` tags won't be output, and any Liquid code will be parsed, but not executed.
# @liquid_syntax
# {% comment %}
# content
+1 -1
View File
@@ -6,7 +6,7 @@ module Liquid
# @liquid_category iteration
# @liquid_name continue
# @liquid_summary
# Causes a [`for` loop](/api/liquid/tags#for) to skip to the next iteration.
# Causes a [`for` loop](/api/liquid/tags/for) to skip to the next iteration.
# @liquid_syntax
# {% continue %}
class Continue < Tag
+1 -1
View File
@@ -6,7 +6,7 @@ module Liquid
# @liquid_category iteration
# @liquid_name cycle
# @liquid_summary
# Loops through a group of strings and outputs them one at a time for each iteration of a [`for` loop](/api/liquid/tags#for).
# Loops through a group of strings and outputs them one at a time for each iteration of a [`for` loop](/api/liquid/tags/for).
# @liquid_description
# The `cycle` tag must be used inside a `for` loop.
#
+2 -2
View File
@@ -12,8 +12,8 @@ module Liquid
# or [section](/themes/architecture/sections) file that they're created in. However, the variable is shared across
# [snippets](/themes/architecture#snippets) included in the file.
#
# Similarly, variables that are created with `decrement` are independent from those created with [`assign`](/api/liquid/tags#assign)
# and [`capture`](/api/liquid/tags#capture). However, `decrement` and [`increment`](/api/liquid/tags#increment) share
# Similarly, variables that are created with `decrement` are independent from those created with [`assign`](/api/liquid/tags/assign)
# and [`capture`](/api/liquid/tags/capture). However, `decrement` and [`increment`](/api/liquid/tags/increment) share
# variables.
# @liquid_syntax
# {% decrement variable_name %}
+1 -1
View File
@@ -9,7 +9,7 @@ module Liquid
# Outputs an expression.
# @liquid_description
# Using the `echo` tag is the same as wrapping an expression in curly brackets (`{{` and `}}`). However, unlike the curly
# bracket method, you can use the `echo` tag inside [`liquid` tags](/api/liquid/tags#liquid).
# bracket method, you can use the `echo` tag inside [`liquid` tags](/api/liquid/tags/liquid).
#
# > Tip:
# > You can use [filters](/api/liquid/filters) on expressions inside `echo` tags.
+5 -5
View File
@@ -9,10 +9,10 @@ module Liquid
# Renders an expression for every item in an array.
# @liquid_description
# You can do a maximum of 50 iterations with a `for` loop. If you need to iterate over more than 50 items, then use the
# [`paginate` tag](/api/liquid/tags#paginate) to split the items over multiple pages.
# [`paginate` tag](/api/liquid/tags/paginate) to split the items over multiple pages.
#
# > Tip:
# > Every `for` loop has an associated [`forloop` object](/api/liquid/objects#forloop) with information about the loop.
# > Every `for` loop has an associated [`forloop` object](/api/liquid/objects/forloop) with information about the loop.
# @liquid_syntax
# {% for variable in array %}
# expression
@@ -98,11 +98,12 @@ module Liquid
@name = "#{@variable_name}-#{collection_name}"
@reversed = p.id?('reversed')
while p.look(:id) && p.look(:colon, 1)
while p.look(:comma) || p.look(:id)
p.consume?(:comma)
unless (attribute = p.id?('limit') || p.id?('offset'))
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_attribute")
end
p.consume
p.consume(:colon)
set_attribute(attribute, p.expression)
end
p.consume(:end_of_string)
@@ -177,7 +178,6 @@ module Liquid
case key
when 'offset'
@from = if expr == 'continue'
Usage.increment('for_offset_continue')
:continue
else
parse_expression(expr)
+3 -3
View File
@@ -8,7 +8,7 @@ module Liquid
# @liquid_summary
# Renders a [snippet](/themes/architecture#snippets).
# @liquid_description
# Inside the snippet, you can access and alter variables that are [created](/api/liquid/tags#variable-tags) outside of the
# Inside the snippet, you can access and alter variables that are [created](/api/liquid/tags/variable-tags) outside of the
# snippet.
# @liquid_syntax
# {% include 'filename' %}
@@ -16,7 +16,7 @@ module Liquid
# @liquid_deprecated
# Deprecated because the way that variables are handled reduces performance and makes code harder to both read and maintain.
#
# The `include` tag has been replaced by [`render`](/api/liquid/tags#render).
# The `include` tag has been replaced by [`render`](/api/liquid/tags/render).
class Include < Tag
prepend Tag::Disableable
@@ -52,7 +52,7 @@ module Liquid
def render_to_output_buffer(context, output)
template_name = context.evaluate(@template_name_expr)
raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name
raise ArgumentError, options[:locale].t("errors.argument.include") unless template_name.is_a?(String)
partial = PartialCache.load(
template_name,
+2 -2
View File
@@ -12,8 +12,8 @@ module Liquid
# or [section](/themes/architecture/sections) file that they're created in. However, the variable is shared across
# [snippets](/themes/architecture#snippets) included in the file.
#
# Similarly, variables that are created with `increment` are independent from those created with [`assign`](/api/liquid/tags#assign)
# and [`capture`](/api/liquid/tags#capture). However, `increment` and [`decrement`](/api/liquid/tags#decrement) share
# Similarly, variables that are created with `increment` are independent from those created with [`assign`](/api/liquid/tags/assign)
# and [`capture`](/api/liquid/tags/capture). However, `increment` and [`decrement`](/api/liquid/tags/decrement) share
# variables.
# @liquid_syntax
# {% increment variable_name %}
-13
View File
@@ -1,19 +1,6 @@
# frozen_string_literal: true
module Liquid
# @liquid_public_docs
# @liquid_type tag
# @liquid_category syntax
# @liquid_name inline_comment
# @liquid_summary
# Prevents an expression from being rendered or output.
# @liquid_description
# Any text inside an `inline_comment` tag won't be rendered or output.
#
# You can create multi-line inline comments. However, each line must begin with a `#`.
# @liquid_syntax
# {% # content %}
# @liquid_syntax_keyword content The content of the comment.
class InlineComment < Tag
def initialize(tag_name, markup, options)
super
+1 -1
View File
@@ -14,7 +14,7 @@ module Liquid
# @liquid_syntax_keyword expression The expression to be output without being rendered.
class Raw < Block
Syntax = /\A\s*\z/
FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
FullTokenPossiblyInvalid = /\A(.*)#{TagStart}#{WhitespaceControl}?\s*(\w+)\s*(.*)?#{WhitespaceControl}?#{TagEnd}\z/om
def initialize(tag_name, markup, parse_context)
super
+5 -5
View File
@@ -8,19 +8,19 @@ module Liquid
# @liquid_summary
# Renders a [snippet](/themes/architecture#snippets) or [app block](/themes/architecture/sections/section-schema#render-app-blocks).
# @liquid_description
# Inside snippets and app blocks, you can't directly access variables that are [created](/api/liquid/tags#variable-tags) outside
# of the snippet or app block. However, you can [specify variables as parameters](/api/liquid/tags#render-passing-variables-to-snippets)
# Inside snippets and app blocks, you can't directly access variables that are [created](/api/liquid/tags/variable-tags) outside
# of the snippet or app block. However, you can [specify variables as parameters](/api/liquid/tags/render#render-passing-variables-to-a-snippet)
# to pass outside variables to snippets.
#
# While you can't directly access created variables, you can access global objects, as well as any objects that are
# directly accessible outside the snippet or app block. For example, a snippet or app block inside the [product template](/themes/architecture/templates/product)
# can access the [`product` object](/api/liquid/objects#product), and a snippet or app block inside a [section](/themes/architecture/sections)
# can access the [`section` object](/api/liquid/objects#section).
# can access the [`product` object](/api/liquid/objects/product), and a snippet or app block inside a [section](/themes/architecture/sections)
# can access the [`section` object](/api/liquid/objects/section).
#
# Outside a snippet or app block, you can't access variables created inside the snippet or app block.
#
# > Note:
# > When you render a snippet using the `render` tag, you can't use the [`include` tag](/api/liquid/tags#include)
# > When you render a snippet using the `render` tag, you can't use the [`include` tag](/api/liquid/tags/include)
# > inside the snippet.
# @liquid_syntax
# {% render 'filename' %}
+12 -4
View File
@@ -11,7 +11,7 @@ module Liquid
# The `tablerow` tag must be wrapped in HTML `<table>` and `</table>` tags.
#
# > Tip:
# > Every `tablerow` loop has an associated [`tablerowloop` object](/api/liquid/objects#tablerowloop) with information about the loop.
# > Every `tablerow` loop has an associated [`tablerowloop` object](/api/liquid/objects/tablerowloop) with information about the loop.
# @liquid_syntax
# {% tablerow variable in array %}
# expression
@@ -45,13 +45,13 @@ module Liquid
def render_to_output_buffer(context, output)
(collection = context.evaluate(@collection_name)) || (return '')
from = @attributes.key?('offset') ? context.evaluate(@attributes['offset']).to_i : 0
to = @attributes.key?('limit') ? from + context.evaluate(@attributes['limit']).to_i : nil
from = @attributes.key?('offset') ? to_integer(context.evaluate(@attributes['offset'])) : 0
to = @attributes.key?('limit') ? from + to_integer(context.evaluate(@attributes['limit'])) : nil
collection = Utils.slice_collection(collection, from, to)
length = collection.length
cols = @attributes['cols'].nil? ? length : context.evaluate(@attributes['cols']).to_i
cols = @attributes.key?('cols') ? to_integer(context.evaluate(@attributes['cols'])) : length
output << "<tr class=\"row1\">\n"
context.stack do
@@ -82,6 +82,14 @@ module Liquid
super + @node.attributes.values + [@node.collection_name]
end
end
private
def to_integer(value)
value.to_i
rescue NoMethodError
raise Liquid::ArgumentError, "invalid integer"
end
end
Template.register_tag('tablerow', TableRow)
+1 -1
View File
@@ -11,7 +11,7 @@ module Liquid
# Renders an expression unless a specific condition is `true`.
# @liquid_description
# > Tip:
# > Similar to the [`if` tag](/api/liquid/tags#if), you can use `elsif` to add more conditions to an `unless` tag.
# > Similar to the [`if` tag](/api/liquid/tags/if), you can use `elsif` to add more conditions to an `unless` tag.
# @liquid_syntax
# {% unless condition %}
# expression
+8 -2
View File
@@ -8,11 +8,15 @@ module Liquid
@source = source.to_s.to_str
@line_number = line_number || (line_numbers ? 1 : nil)
@for_liquid_tag = for_liquid_tag
@offset = 0
@tokens = tokenize
end
def shift
(token = @tokens.shift) || return
token = @tokens[@offset]
return nil unless token
@offset += 1
if @line_number
@line_number += @for_liquid_tag ? 1 : token.count("\n")
@@ -31,7 +35,9 @@ module Liquid
tokens = @source.split(TemplateParser)
# removes the rogue empty element at the beginning of the array
tokens.shift if tokens[0]&.empty?
if tokens[0]&.empty?
@offset += 1
end
tokens
end
+5 -1
View File
@@ -16,7 +16,11 @@ module Liquid
# Maintains Ruby 1.8.7 String#each behaviour on 1.9
if collection.is_a?(String)
return collection.empty? ? [] : [collection]
return [] if collection.empty?
if from > 0 || to == 0
Usage.increment("string_slice_bug")
end
return [collection]
end
return [] unless collection.respond_to?(:each)
+12 -4
View File
@@ -109,6 +109,10 @@ class StandardFiltersTest < Minitest::Test
assert_raises(Liquid::ArgumentError) do
@filters.slice('foobar', 0, "")
end
assert_equal("", @filters.slice("foobar", 0, -(1 << 64)))
assert_equal("foobar", @filters.slice("foobar", 0, 1 << 63))
assert_equal("", @filters.slice("foobar", 1 << 63, 6))
assert_equal("", @filters.slice("foobar", -(1 << 63), 6))
end
def test_slice_on_arrays
@@ -123,6 +127,10 @@ class StandardFiltersTest < Minitest::Test
assert_equal(%w(r), @filters.slice(input, -1))
assert_equal(%w(), @filters.slice(input, 100, 10))
assert_equal(%w(), @filters.slice(input, -100, 10))
assert_equal([], @filters.slice(input, 0, -(1 << 64)))
assert_equal(input, @filters.slice(input, 0, 1 << 63))
assert_equal([], @filters.slice(input, 1 << 63, 6))
assert_equal([], @filters.slice(input, -(1 << 63), 6))
end
def test_truncate
@@ -132,6 +140,8 @@ class StandardFiltersTest < Minitest::Test
assert_equal('1234567890', @filters.truncate('1234567890'))
assert_equal("测试...", @filters.truncate("测试测试测试测试", 5))
assert_equal('12341', @filters.truncate("1234567890", 5, 1))
assert_equal("foobar", @filters.truncate("foobar", 1 << 63))
assert_equal("...", @filters.truncate("foobar", -(1 << 63)))
end
def test_split
@@ -227,10 +237,8 @@ class StandardFiltersTest < Minitest::Test
assert_equal('one two three...', @filters.truncatewords("one two\tthree\nfour", 3))
assert_equal('one two...', @filters.truncatewords("one two three four", 2))
assert_equal('one...', @filters.truncatewords("one two three four", 0))
exception = assert_raises(Liquid::ArgumentError) do
@filters.truncatewords("one two three four", 1 << 31)
end
assert_equal("Liquid error: integer #{1 << 31} too big for truncatewords", exception.message)
assert_equal('one two three four', @filters.truncatewords("one two three four", 1 << 31))
assert_equal('one...', @filters.truncatewords("one two three four", -(1 << 32)))
end
def test_strip_html
+1 -26
View File
@@ -103,6 +103,7 @@ HERE
assert_template_result('1234', '{%for i in array limit:4 %}{{ i }}{%endfor%}', assigns)
assert_template_result('3456', '{%for i in array limit:4 offset:2 %}{{ i }}{%endfor%}', assigns)
assert_template_result('3456', '{%for i in array limit: 4 offset: 2 %}{{ i }}{%endfor%}', assigns)
assert_template_result('3456', '{%for i in array, limit: 4, offset: 2 %}{{ i }}{%endfor%}', assigns)
end
def test_limiting_with_invalid_limit
@@ -450,30 +451,4 @@ HERE
assert(context.registers[:for_stack].empty?)
end
def test_instrument_for_offset_continue
assert_usage_increment('for_offset_continue') do
Template.parse('{% for item in items offset:continue %}{{item}}{% endfor %}')
end
assert_usage_increment('for_offset_continue', times: 0) do
Template.parse('{% for item in items offset:2 %}{{item}}{% endfor %}')
end
end
def test_instrument_forloop_drop_name
assigns = { 'items' => [1, 2, 3, 4, 5] }
assert_usage_increment('forloop_drop_name', times: 5) do
Template.parse('{% for item in items %}{{forloop.name}}{% endfor %}').render!(assigns)
end
assert_usage_increment('forloop_drop_name', times: 0) do
Template.parse('{% for item in items %}{{forloop.index}}{% endfor %}').render!(assigns)
end
assert_usage_increment('forloop_drop_name', times: 0) do
Template.parse('{% for item in items %}{{item}}{% endfor %}').render!(assigns)
end
end
end
@@ -249,6 +249,11 @@ class IncludeTagTest < Minitest::Test
"{% include nil %}", render_errors: true)
end
def test_render_raise_argument_error_when_template_is_not_a_string
assert_template_result("Liquid error (line 1): Argument error in tag 'include' - Illegal template name",
"{% include 123 %}", render_errors: true)
end
def test_including_via_variable_value
assert_template_result("from TestFileSystem", "{% assign page = 'pick_a_source' %}{% include page %}",
partials: { "pick_a_source" => "from TestFileSystem" })
+8 -7
View File
@@ -6,20 +6,21 @@ 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 1', '{%increment port %} {{ port }}')
assert_template_result(' 0 1 2', '{{port}} {%increment port %} {%increment port%} {{port}}')
assert_template_result('0 0 1 2 1',
'{%increment port %} {%increment starboard%} ' \
'{%increment port %} {%increment port%} ' \
'{%increment starboard %}', {})
'{%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',
assert_template_result('-1 -1', '{%decrement port %} {{ port }}', { 'port' => 10 })
assert_template_result(' -1 -2 -2', '{{port}} {%decrement port %} {%decrement port%} {{port}}')
assert_template_result('0 1 2 0 3 1 1 3',
'{%increment starboard %} {%increment starboard%} {%increment starboard%} ' \
'{%increment port %} {%increment starboard%} ' \
'{%increment port %} {%decrement port%} ' \
'{%decrement starboard %}', { 'port' => 1, 'starboard' => 5 })
'{%decrement starboard %}')
end
end
+2 -1
View File
@@ -11,7 +11,8 @@ class RawTagTest < Minitest::Test
end
def test_output_in_raw
assert_template_result('{{ test }}', '{% raw %}{{ test }}{% endraw %}')
# assert_template_result('{{ test }}', '{% raw %}{{ test }}{% endraw %}')
assert_template_result('>{{ test }}<', '> {%- raw -%}{{ test }}{%- endraw -%} <')
end
def test_open_tag_in_raw
@@ -36,6 +36,8 @@ class StandardTagTest < Minitest::Test
assert_template_result('', '{%comment%}{% endif %}{%endcomment%}')
assert_template_result('', '{% comment %}{% endwhatever %}{% endcomment %}')
assert_template_result('', '{% comment %}{% raw %} {{%%%%}} }} { {% endcomment %} {% comment {% endraw %} {% endcomment %}')
assert_template_result('', '{% comment %}{% " %}{% endcomment %}')
assert_template_result('', '{% comment %}{%%}{% endcomment %}')
assert_template_result('foobar', 'foo{%comment%}comment{%endcomment%}bar')
assert_template_result('foobar', 'foo{% comment %}comment{% endcomment %}bar')
+60
View File
@@ -66,6 +66,46 @@ class TableRowTest < Minitest::Test
{ 'characters' => '' })
end
def test_cols_nil_constant_same_as_evaluated_nil_expression
expect = "<tr class=\"row1\">\n" \
"<td class=\"col1\">false</td>" \
"<td class=\"col2\">false</td>" \
"</tr>\n"
assert_template_result(expect,
"{% tablerow i in (1..2) cols:nil %}{{ tablerowloop.col_last }}{% endtablerow %}")
assert_template_result(expect,
"{% tablerow i in (1..2) cols:var %}{{ tablerowloop.col_last }}{% endtablerow %}",
{ "var" => nil })
end
def test_nil_limit_is_treated_as_zero
expect = "<tr class=\"row1\">\n" \
"</tr>\n"
assert_template_result(expect,
"{% tablerow i in (1..2) limit:nil %}{{ i }}{% endtablerow %}")
assert_template_result(expect,
"{% tablerow i in (1..2) limit:var %}{{ i }}{% endtablerow %}",
{ "var" => nil })
end
def test_nil_offset_is_treated_as_zero
expect = "<tr class=\"row1\">\n" \
"<td class=\"col1\">1:false</td>" \
"<td class=\"col2\">2:true</td>" \
"</tr>\n"
assert_template_result(expect,
"{% tablerow i in (1..2) offset:nil %}{{ i }}:{{ tablerowloop.col_last }}{% endtablerow %}")
assert_template_result(expect,
"{% tablerow i in (1..2) offset:var %}{{ i }}:{{ tablerowloop.col_last }}{% endtablerow %}",
{ "var" => nil })
end
def test_tablerow_loop_drop_attributes
template = <<~LIQUID.chomp
{% tablerow i in (1...2) %}
@@ -117,4 +157,24 @@ class TableRowTest < Minitest::Test
assert_template_result(expected_output, template)
end
def test_table_row_renders_correct_error_message_for_invalid_parameters
assert_template_result(
"Liquid error (line 1): invalid integer",
'{% tablerow n in (1...10) limit:true %} {{n}} {% endtablerow %}',
render_errors: true,
)
assert_template_result(
"Liquid error (line 1): invalid integer",
'{% tablerow n in (1...10) offset:true %} {{n}} {% endtablerow %}',
render_errors: true,
)
assert_template_result(
"Liquid error (line 1): invalid integer",
'{% tablerow n in (1...10) cols:true %} {{n}} {% endtablerow %}',
render_errors: true,
)
end
end
+39
View File
@@ -24,17 +24,22 @@ class VariableTest < Minitest::Test
def test_if_tag_calls_to_liquid_value
assert_template_result('one', '{% if foo == 1 %}one{% endif %}', { 'foo' => IntegerDrop.new('1') })
assert_template_result('one', '{% if foo == eqv %}one{% endif %}', { 'foo' => IntegerDrop.new(1), 'eqv' => IntegerDrop.new(1) })
assert_template_result('one', '{% if 0 < foo %}one{% endif %}', { 'foo' => IntegerDrop.new('1') })
assert_template_result('one', '{% if foo > 0 %}one{% endif %}', { 'foo' => IntegerDrop.new('1') })
assert_template_result('one', '{% if b > a %}one{% endif %}', { 'b' => IntegerDrop.new(1), 'a' => IntegerDrop.new(0) })
assert_template_result('true', '{% if foo == true %}true{% endif %}', { 'foo' => BooleanDrop.new(true) })
assert_template_result('true', '{% if foo %}true{% endif %}', { 'foo' => BooleanDrop.new(true) })
assert_template_result('', '{% if foo %}true{% endif %}', { 'foo' => BooleanDrop.new(false) })
assert_template_result('', '{% if foo == true %}True{% endif %}', { 'foo' => BooleanDrop.new(false) })
assert_template_result('one', '{% if a contains x %}one{% endif %}', { 'a' => [1], 'x' => IntegerDrop.new(1) })
end
def test_unless_tag_calls_to_liquid_value
assert_template_result('', '{% unless foo %}true{% endunless %}', { 'foo' => BooleanDrop.new(true) })
assert_template_result('true', '{% unless foo %}true{% endunless %}', { 'foo' => BooleanDrop.new(false) })
end
def test_case_tag_calls_to_liquid_value
@@ -130,4 +135,38 @@ class VariableTest < Minitest::Test
def test_raw_value_variable
assert_template_result('bar', '{{ [key] }}', { 'key' => 'foo', 'foo' => 'bar' })
end
def test_dynamic_find_var_with_drop
assert_template_result(
'bar',
'{{ [list[settings.zero]] }}',
{
'list' => ['foo'],
'settings' => SettingsDrop.new("zero" => 0),
'foo' => 'bar',
}
)
assert_template_result(
'foo',
'{{ [list[settings.zero]["foo"]] }}',
{
'list' => [{ 'foo' => 'bar' }],
'settings' => SettingsDrop.new("zero" => 0),
'bar' => 'foo',
}
)
end
def test_double_nested_variable_lookup
assert_template_result(
'bar',
'{{ list[list[settings.zero]]["foo"] }}',
{
'list' => [1, { 'foo' => 'bar' }],
'settings' => SettingsDrop.new("zero" => 0),
'bar' => 'foo',
}
)
end
end
+13 -10
View File
@@ -42,9 +42,9 @@ module Minitest
message: nil, partials: nil, error_mode: nil, render_errors: false
)
template = Liquid::Template.parse(template, line_numbers: true, error_mode: error_mode&.to_sym)
file_system = StubFileSystem.new(partials) if partials
file_system = StubFileSystem.new(partials || {})
registers = Liquid::Registers.new(file_system: file_system)
context = Liquid::Context.build(environments: assigns, rethrow_errors: !render_errors, registers: registers)
context = Liquid::Context.build(static_environments: assigns, rethrow_errors: !render_errors, registers: registers)
output = template.render(context)
assert_equal(expected, output, message)
end
@@ -125,16 +125,23 @@ class ThingWithToLiquid
end
end
class SettingsDrop < Liquid::Drop
def initialize(settings)
super()
@settings = settings
end
def liquid_method_missing(key)
@settings[key]
end
end
class IntegerDrop < Liquid::Drop
def initialize(value)
super()
@value = value.to_i
end
def ==(other)
@value == other
end
def to_s
@value.to_s
end
@@ -150,10 +157,6 @@ class BooleanDrop < Liquid::Drop
@value = value
end
def ==(other)
@value == other
end
def to_liquid_value
@value
end
+30
View File
@@ -12,4 +12,34 @@ class ForTagUnitTest < Minitest::Test
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
def test_for_string_slice_bug_usage
template = Liquid::Template.parse("{% for x in str, offset: 1 %}{{ x }},{% endfor %}")
assert_usage("string_slice_bug") do
assert_equal("abc,", template.render({ "str" => "abc" }))
end
end
def test_for_string_0_limit_usage
template = Liquid::Template.parse("{% for x in str, limit: 0 %}{{ x }},{% endfor %}")
assert_usage("string_slice_bug") do
assert_equal("abc,", template.render({ "str" => "abc" }))
end
end
def test_for_string_no_slice_usage
template = Liquid::Template.parse("{% for x in str, offset: 0, limit: 1 %}{{ x }},{% endfor %}")
assert_usage("string_slice_bug", times: 0) do
assert_equal("abc,", template.render({ "str" => "abc" }))
end
end
private
def assert_usage(name, times: 1, &block)
count = 0
result = Liquid::Usage.stub(:increment, ->(n) { count += 1 if n == name }, &block)
assert_equal(times, count)
result
end
end