mirror of
https://github.com/Shopify/liquid.git
synced 2026-09-13 07:50:43 -07:00
Compare commits
58
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e37c5e18b | ||
|
|
b3f9639e7d | ||
|
|
fe3da0e17d | ||
|
|
e200c4544b | ||
|
|
7124540563 | ||
|
|
0558bd12c4 | ||
|
|
0639a094a8 | ||
|
|
58777bcd93 | ||
|
|
323951b36f | ||
|
|
a5b91e83ae | ||
|
|
10114b333e | ||
|
|
a07ae90523 | ||
|
|
2e236b0a0e | ||
|
|
bd05dfbd1c | ||
|
|
2a5ecf068f | ||
|
|
0b8e30b819 | ||
|
|
97cada61f9 | ||
|
|
7c9b77e8cf | ||
|
|
d48708ae46 | ||
|
|
42d822bda9 | ||
|
|
2c7d686690 | ||
|
|
313d01706a | ||
|
|
e9f86724f6 | ||
|
|
0a17c15289 | ||
|
|
04bd9dbe90 | ||
|
|
253ec81b56 | ||
|
|
e05719fb82 | ||
|
|
ac374a208f | ||
|
|
91be3dd75e | ||
|
|
40191022e8 | ||
|
|
71506ad54e | ||
|
|
cef64e277e | ||
|
|
7c592c1c00 | ||
|
|
002e4caea7 | ||
|
|
3c16c27ee1 | ||
|
|
eff5c5de8e | ||
|
|
19adfbd863 | ||
|
|
a672f3836c | ||
|
|
92fa334192 | ||
|
|
7c8a269b4d | ||
|
|
0b1dc295ff | ||
|
|
1de6025362 | ||
|
|
8ecb703d4d | ||
|
|
b4667adadf | ||
|
|
94e02d765f | ||
|
|
60701f865d | ||
|
|
0c49dd592f | ||
|
|
c3ac0e0127 | ||
|
|
a0a4307e7d | ||
|
|
fdd8c714b2 | ||
|
|
63583ffe5b | ||
|
|
9a06cedbba | ||
|
|
42b6763546 | ||
|
|
e5d18c83bb | ||
|
|
c77ff68573 | ||
|
|
b0cba0bfd2 | ||
|
|
8d8661349a | ||
|
|
347a2418c4 |
@@ -13,7 +13,8 @@ jobs:
|
||||
entry:
|
||||
- { ruby: 3.0, allowed-failure: false } # minimum supported
|
||||
- { ruby: 3.2, allowed-failure: false }
|
||||
- { ruby: 3.3, allowed-failure: false } # latest
|
||||
- { ruby: 3.3, allowed-failure: false }
|
||||
- { ruby: "3.4.0-rc1", allowed-failure: false } # latest
|
||||
- { ruby: ruby-head, allowed-failure: false }
|
||||
name: Test Ruby ${{ matrix.entry.ruby }}
|
||||
steps:
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
3.3.4
|
||||
3.3.6
|
||||
|
||||
@@ -13,6 +13,7 @@ group :benchmark, :test do
|
||||
gem 'benchmark-ips'
|
||||
gem 'memory_profiler'
|
||||
gem 'terminal-table'
|
||||
gem "lru_redux"
|
||||
|
||||
install_if -> { RUBY_PLATFORM !~ /mingw|mswin|java/ && RUBY_ENGINE != 'truffleruby' } do
|
||||
gem 'stackprof'
|
||||
@@ -23,8 +24,4 @@ group :test do
|
||||
gem 'rubocop', '~> 1.61.0'
|
||||
gem 'rubocop-shopify', '~> 2.12.0', require: false
|
||||
gem 'rubocop-performance', require: false
|
||||
|
||||
platform :mri, :truffleruby do
|
||||
gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'main'
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# Liquid Change Log
|
||||
|
||||
## 5.6.0 (unreleased)
|
||||
|
||||
### Fixes
|
||||
|
||||
* Fix Tokenizer to handle null source value (#1873) [Bahar Pourazar]
|
||||
|
||||
|
||||
## 5.5.0 2024-03-21
|
||||
|
||||
Please reference the GitHub release for more information.
|
||||
|
||||
@@ -91,7 +91,7 @@ Liquid::Template.parse(<<~LIQUID, environment: email_environment)
|
||||
LIQUID
|
||||
```
|
||||
|
||||
By using Environments, you ensure that custom tags and filters are only available in the contexts where they are needed, making your Liquid templates more robust and easier to manage.
|
||||
By using Environments, you ensure that custom tags and filters are only available in the contexts where they are needed, making your Liquid templates more robust and easier to manage. For smaller projects, a global environment is available via `Liquid::Environment.default`.
|
||||
|
||||
### Error Modes
|
||||
|
||||
|
||||
@@ -43,8 +43,6 @@ task :test do
|
||||
Rake::Task['base_test'].invoke
|
||||
|
||||
if RUBY_ENGINE == 'ruby' || RUBY_ENGINE == 'truffleruby'
|
||||
ENV['LIQUID_C'] = '1'
|
||||
|
||||
ENV['LIQUID_PARSER_MODE'] = 'lax'
|
||||
Rake::Task['integration_test'].reenable
|
||||
Rake::Task['integration_test'].invoke
|
||||
@@ -73,7 +71,7 @@ end
|
||||
|
||||
namespace :benchmark do
|
||||
desc "Run the liquid benchmark with lax parsing"
|
||||
task :run do
|
||||
task :lax do
|
||||
ruby "./performance/benchmark.rb lax"
|
||||
end
|
||||
|
||||
@@ -82,11 +80,30 @@ namespace :benchmark do
|
||||
ruby "./performance/benchmark.rb strict"
|
||||
end
|
||||
|
||||
desc "Run the liquid benchmark with both lax and strict parsing"
|
||||
task run: [:lax, :strict]
|
||||
|
||||
desc "Run unit benchmarks"
|
||||
task :unit do
|
||||
Dir["./performance/unit/*_benchmark.rb"].each do |file|
|
||||
puts "🧪 Running #{file}"
|
||||
ruby file
|
||||
namespace :unit do
|
||||
task :all do
|
||||
Dir["./performance/unit/*_benchmark.rb"].each do |file|
|
||||
puts "🧪 Running #{file}"
|
||||
ruby file
|
||||
end
|
||||
end
|
||||
|
||||
task :lexer do
|
||||
Dir["./performance/unit/lexer_benchmark.rb"].each do |file|
|
||||
puts "🧪 Running #{file}"
|
||||
ruby file
|
||||
end
|
||||
end
|
||||
|
||||
task :expression do
|
||||
Dir["./performance/unit/expression_benchmark.rb"].each do |file|
|
||||
puts "🧪 Running #{file}"
|
||||
ruby file
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+4
-5
@@ -21,6 +21,8 @@
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
require "strscan"
|
||||
|
||||
module Liquid
|
||||
FilterSeparator = /\|/
|
||||
ArgumentSeparator = ','
|
||||
@@ -44,9 +46,7 @@ module Liquid
|
||||
VariableParser = /\[(?>[^\[\]]+|\g<0>)*\]|#{VariableSegment}+\??/o
|
||||
|
||||
RAISE_EXCEPTION_LAMBDA = ->(_e) { raise }
|
||||
|
||||
singleton_class.send(:attr_accessor, :cache_classes)
|
||||
self.cache_classes = true
|
||||
HAS_STRING_SCANNER_SCAN_BYTE = StringScanner.instance_methods.include?(:scan_byte)
|
||||
end
|
||||
|
||||
require "liquid/version"
|
||||
@@ -71,8 +71,6 @@ require 'liquid/extensions'
|
||||
require 'liquid/errors'
|
||||
require 'liquid/interrupts'
|
||||
require 'liquid/strainer_template'
|
||||
require 'liquid/strainer_factory'
|
||||
require 'liquid/expression'
|
||||
require 'liquid/context'
|
||||
require 'liquid/tag'
|
||||
require 'liquid/block_body'
|
||||
@@ -81,6 +79,7 @@ require 'liquid/variable'
|
||||
require 'liquid/variable_lookup'
|
||||
require 'liquid/range_lookup'
|
||||
require 'liquid/resource_limits'
|
||||
require 'liquid/expression'
|
||||
require 'liquid/template'
|
||||
require 'liquid/condition'
|
||||
require 'liquid/utils'
|
||||
|
||||
@@ -40,6 +40,10 @@ module Liquid
|
||||
@global_filter = nil
|
||||
@disabled_tags = {}
|
||||
|
||||
# Instead of constructing new StringScanner objects for each Expression parse,
|
||||
# we recycle the same one.
|
||||
@string_scanner = StringScanner.new("")
|
||||
|
||||
@registers.static[:cached_partials] ||= {}
|
||||
@registers.static[:file_system] ||= environment.file_system
|
||||
@registers.static[:template_factory] ||= Liquid::TemplateFactory.new
|
||||
@@ -176,7 +180,7 @@ module Liquid
|
||||
# Example:
|
||||
# products == empty #=> products.empty?
|
||||
def [](expression)
|
||||
evaluate(Expression.parse(expression))
|
||||
evaluate(Expression.parse(expression, @string_scanner))
|
||||
end
|
||||
|
||||
def key?(key)
|
||||
|
||||
+97
-21
@@ -10,37 +10,113 @@ module Liquid
|
||||
'true' => true,
|
||||
'false' => false,
|
||||
'blank' => '',
|
||||
'empty' => ''
|
||||
'empty' => '',
|
||||
# in lax mode, minus sign can be a VariableLookup
|
||||
# For simplicity and performace, we treat it like a literal
|
||||
'-' => VariableLookup.parse("-", nil).freeze,
|
||||
}.freeze
|
||||
|
||||
INTEGERS_REGEX = /\A(-?\d+)\z/
|
||||
FLOATS_REGEX = /\A(-?\d[\d\.]+)\z/
|
||||
DOT = ".".ord
|
||||
ZERO = "0".ord
|
||||
NINE = "9".ord
|
||||
DASH = "-".ord
|
||||
|
||||
# Use an atomic group (?>...) to avoid pathological backtracing from
|
||||
# malicious input as described in https://github.com/Shopify/liquid/issues/1357
|
||||
RANGES_REGEX = /\A\(\s*(?>(\S+)\s*\.\.)\s*(\S+)\s*\)\z/
|
||||
RANGES_REGEX = /\A\(\s*(?>(\S+)\s*\.\.)\s*(\S+)\s*\)\z/
|
||||
INTEGER_REGEX = /\A(-?\d+)\z/
|
||||
FLOAT_REGEX = /\A(-?\d+)\.\d+\z/
|
||||
|
||||
def self.parse(markup)
|
||||
return nil unless markup
|
||||
class << self
|
||||
def parse(markup, ss = StringScanner.new(""), cache = nil)
|
||||
return unless markup
|
||||
|
||||
markup = markup.strip
|
||||
if (markup.start_with?('"') && markup.end_with?('"')) ||
|
||||
(markup.start_with?("'") && markup.end_with?("'"))
|
||||
return markup[1..-2]
|
||||
markup = markup.strip # markup can be a frozen string
|
||||
|
||||
if (markup.start_with?('"') && markup.end_with?('"')) ||
|
||||
(markup.start_with?("'") && markup.end_with?("'"))
|
||||
return markup[1..-2]
|
||||
elsif LITERALS.key?(markup)
|
||||
return LITERALS[markup]
|
||||
end
|
||||
|
||||
# Cache only exists during parsing
|
||||
if cache
|
||||
return cache[markup] if cache.key?(markup)
|
||||
|
||||
cache[markup] = inner_parse(markup, ss, cache).freeze
|
||||
else
|
||||
inner_parse(markup, ss, nil).freeze
|
||||
end
|
||||
end
|
||||
|
||||
case markup
|
||||
when INTEGERS_REGEX
|
||||
Regexp.last_match(1).to_i
|
||||
when RANGES_REGEX
|
||||
RangeLookup.parse(Regexp.last_match(1), Regexp.last_match(2))
|
||||
when FLOATS_REGEX
|
||||
Regexp.last_match(1).to_f
|
||||
else
|
||||
if LITERALS.key?(markup)
|
||||
LITERALS[markup]
|
||||
def inner_parse(markup, ss, cache)
|
||||
if (markup.start_with?("(") && markup.end_with?(")")) && markup =~ RANGES_REGEX
|
||||
return RangeLookup.parse(
|
||||
Regexp.last_match(1),
|
||||
Regexp.last_match(2),
|
||||
ss,
|
||||
cache,
|
||||
)
|
||||
end
|
||||
|
||||
if (num = parse_number(markup, ss))
|
||||
num
|
||||
else
|
||||
VariableLookup.parse(markup)
|
||||
VariableLookup.parse(markup, ss, cache)
|
||||
end
|
||||
end
|
||||
|
||||
def parse_number(markup, ss)
|
||||
# check if the markup is simple integer or float
|
||||
case markup
|
||||
when INTEGER_REGEX
|
||||
return Integer(markup, 10)
|
||||
when FLOAT_REGEX
|
||||
return markup.to_f
|
||||
end
|
||||
|
||||
ss.string = markup
|
||||
# the first byte must be a digit or a dash
|
||||
byte = ss.scan_byte
|
||||
|
||||
return false if byte != DASH && (byte < ZERO || byte > NINE)
|
||||
|
||||
if byte == DASH
|
||||
peek_byte = ss.peek_byte
|
||||
|
||||
# if it starts with a dash, the next byte must be a digit
|
||||
return false if peek_byte.nil? || !(peek_byte >= ZERO && peek_byte <= NINE)
|
||||
end
|
||||
|
||||
# The markup could be a float with multiple dots
|
||||
first_dot_pos = nil
|
||||
num_end_pos = nil
|
||||
|
||||
while (byte = ss.scan_byte)
|
||||
return false if byte != DOT && (byte < ZERO || byte > NINE)
|
||||
|
||||
# we found our number and now we are just scanning the rest of the string
|
||||
next if num_end_pos
|
||||
|
||||
if byte == DOT
|
||||
if first_dot_pos.nil?
|
||||
first_dot_pos = ss.pos
|
||||
else
|
||||
# we found another dot, so we know that the number ends here
|
||||
num_end_pos = ss.pos - 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
num_end_pos = markup.length if ss.eos?
|
||||
|
||||
if num_end_pos
|
||||
# number ends with a number "123.123"
|
||||
markup.byteslice(0, num_end_pos).to_f
|
||||
else
|
||||
# number ends with a dot "123."
|
||||
markup.byteslice(0, first_dot_pos).to_f
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+58
-122
@@ -1,66 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "strscan"
|
||||
|
||||
module Liquid
|
||||
class Lexer1
|
||||
SPECIALS = {
|
||||
'|' => :pipe,
|
||||
'.' => :dot,
|
||||
':' => :colon,
|
||||
',' => :comma,
|
||||
'[' => :open_square,
|
||||
']' => :close_square,
|
||||
'(' => :open_round,
|
||||
')' => :close_round,
|
||||
'?' => :question,
|
||||
'-' => :dash,
|
||||
}.freeze
|
||||
IDENTIFIER = /[a-zA-Z_][\w-]*\??/
|
||||
SINGLE_STRING_LITERAL = /'[^\']*'/
|
||||
DOUBLE_STRING_LITERAL = /"[^\"]*"/
|
||||
STRING_LITERAL = Regexp.union(SINGLE_STRING_LITERAL, DOUBLE_STRING_LITERAL)
|
||||
NUMBER_LITERAL = /-?\d+(\.\d+)?/
|
||||
DOTDOT = /\.\./
|
||||
COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/
|
||||
WHITESPACE_OR_NOTHING = /\s*/
|
||||
|
||||
def initialize(input)
|
||||
@ss = StringScanner.new(input)
|
||||
end
|
||||
|
||||
def tokenize
|
||||
@output = []
|
||||
|
||||
until @ss.eos?
|
||||
@ss.skip(WHITESPACE_OR_NOTHING)
|
||||
break if @ss.eos?
|
||||
tok = if (t = @ss.scan(COMPARISON_OPERATOR))
|
||||
[:comparison, t]
|
||||
elsif (t = @ss.scan(STRING_LITERAL))
|
||||
[:string, t]
|
||||
elsif (t = @ss.scan(NUMBER_LITERAL))
|
||||
[:number, t]
|
||||
elsif (t = @ss.scan(IDENTIFIER))
|
||||
[:id, t]
|
||||
elsif (t = @ss.scan(DOTDOT))
|
||||
[:dotdot, t]
|
||||
else
|
||||
c = @ss.getch
|
||||
if (s = SPECIALS[c])
|
||||
[s, c]
|
||||
else
|
||||
raise SyntaxError, "Unexpected character #{c}"
|
||||
end
|
||||
end
|
||||
@output << tok
|
||||
end
|
||||
|
||||
@output << [:end_of_string]
|
||||
end
|
||||
end
|
||||
|
||||
class Lexer2
|
||||
class Lexer
|
||||
CLOSE_ROUND = [:close_round, ")"].freeze
|
||||
CLOSE_SQUARE = [:close_square, "]"].freeze
|
||||
COLON = [:colon, ":"].freeze
|
||||
@@ -92,6 +33,7 @@ module Liquid
|
||||
SINGLE_COMPARISON_TOKENS = [].tap do |table|
|
||||
table["<".ord] = COMPARISON_LESS_THAN
|
||||
table[">".ord] = COMPARISON_GREATER_THAN
|
||||
table.freeze
|
||||
end
|
||||
|
||||
TWO_CHARS_COMPARISON_JUMP_TABLE = [].tap do |table|
|
||||
@@ -103,18 +45,17 @@ module Liquid
|
||||
sub_table["=".ord] = COMPARISION_NOT_EQUAL
|
||||
sub_table.freeze
|
||||
end
|
||||
table.freeze
|
||||
end
|
||||
|
||||
COMPARISON_JUMP_TABLE = [].tap do |table|
|
||||
table["<".ord] = [].tap do |sub_table|
|
||||
sub_table["=".ord] = COMPARISON_LESS_THAN_OR_EQUAL
|
||||
sub_table[">".ord] = COMPARISON_NOT_EQUAL_ALT
|
||||
RUBY_WHITESPACE.each { |c| sub_table[c.ord] = COMPARISON_LESS_THAN }
|
||||
sub_table.freeze
|
||||
end
|
||||
table[">".ord] = [].tap do |sub_table|
|
||||
sub_table["=".ord] = COMPARISON_GREATER_THAN_OR_EQUAL
|
||||
RUBY_WHITESPACE.each { |c| sub_table[c.ord] = COMPARISON_GREATER_THAN }
|
||||
sub_table.freeze
|
||||
end
|
||||
table.freeze
|
||||
@@ -157,81 +98,76 @@ module Liquid
|
||||
table.freeze
|
||||
end
|
||||
|
||||
def initialize(input)
|
||||
@ss = StringScanner.new(input)
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/BlockNesting
|
||||
def tokenize
|
||||
@output = []
|
||||
class << self
|
||||
def tokenize(ss)
|
||||
output = []
|
||||
|
||||
until @ss.eos?
|
||||
@ss.skip(WHITESPACE_OR_NOTHING)
|
||||
until ss.eos?
|
||||
ss.skip(WHITESPACE_OR_NOTHING)
|
||||
|
||||
break if @ss.eos?
|
||||
break if ss.eos?
|
||||
|
||||
start_pos = @ss.pos
|
||||
peeked = @ss.peek_byte
|
||||
start_pos = ss.pos
|
||||
peeked = ss.peek_byte
|
||||
|
||||
if (special = SPECIAL_TABLE[peeked])
|
||||
@ss.scan_byte
|
||||
# Special case for ".."
|
||||
if special == DOT && @ss.peek_byte == DOT_ORD
|
||||
@ss.scan_byte
|
||||
@output << DOTDOT
|
||||
elsif special == DASH
|
||||
# Special case for negative numbers
|
||||
if (peeked_byte = @ss.peek_byte) && NUMBER_TABLE[peeked_byte]
|
||||
@ss.pos -= 1
|
||||
@output << [:number, @ss.scan(NUMBER_LITERAL)]
|
||||
if (special = SPECIAL_TABLE[peeked])
|
||||
ss.scan_byte
|
||||
# Special case for ".."
|
||||
if special == DOT && ss.peek_byte == DOT_ORD
|
||||
ss.scan_byte
|
||||
output << DOTDOT
|
||||
elsif special == DASH
|
||||
# Special case for negative numbers
|
||||
if (peeked_byte = ss.peek_byte) && NUMBER_TABLE[peeked_byte]
|
||||
ss.pos -= 1
|
||||
output << [:number, ss.scan(NUMBER_LITERAL)]
|
||||
else
|
||||
output << special
|
||||
end
|
||||
else
|
||||
@output << special
|
||||
output << special
|
||||
end
|
||||
elsif (sub_table = TWO_CHARS_COMPARISON_JUMP_TABLE[peeked])
|
||||
ss.scan_byte
|
||||
if (peeked_byte = ss.peek_byte) && (found = sub_table[peeked_byte])
|
||||
output << found
|
||||
ss.scan_byte
|
||||
else
|
||||
raise_syntax_error(start_pos, ss)
|
||||
end
|
||||
elsif (sub_table = COMPARISON_JUMP_TABLE[peeked])
|
||||
ss.scan_byte
|
||||
if (peeked_byte = ss.peek_byte) && (found = sub_table[peeked_byte])
|
||||
output << found
|
||||
ss.scan_byte
|
||||
else
|
||||
output << SINGLE_COMPARISON_TOKENS[peeked]
|
||||
end
|
||||
else
|
||||
@output << special
|
||||
end
|
||||
elsif (sub_table = TWO_CHARS_COMPARISON_JUMP_TABLE[peeked])
|
||||
@ss.scan_byte
|
||||
if (peeked_byte = @ss.peek_byte) && (found = sub_table[peeked_byte])
|
||||
@output << found
|
||||
@ss.scan_byte
|
||||
else
|
||||
raise_syntax_error(start_pos)
|
||||
end
|
||||
elsif (sub_table = COMPARISON_JUMP_TABLE[peeked])
|
||||
@ss.scan_byte
|
||||
if (peeked_byte = @ss.peek_byte) && (found = sub_table[peeked_byte])
|
||||
@output << found
|
||||
@ss.scan_byte
|
||||
else
|
||||
@output << SINGLE_COMPARISON_TOKENS[peeked]
|
||||
end
|
||||
else
|
||||
type, pattern = NEXT_MATCHER_JUMP_TABLE[peeked]
|
||||
type, pattern = NEXT_MATCHER_JUMP_TABLE[peeked]
|
||||
|
||||
if type && (t = @ss.scan(pattern))
|
||||
# Special case for "contains"
|
||||
@output << if type == :id && t == "contains" && @output.last&.first != :dot
|
||||
COMPARISON_CONTAINS
|
||||
if type && (t = ss.scan(pattern))
|
||||
# Special case for "contains"
|
||||
output << if type == :id && t == "contains" && output.last&.first != :dot
|
||||
COMPARISON_CONTAINS
|
||||
else
|
||||
[type, t]
|
||||
end
|
||||
else
|
||||
[type, t]
|
||||
raise_syntax_error(start_pos, ss)
|
||||
end
|
||||
else
|
||||
raise_syntax_error(start_pos)
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics/BlockNesting
|
||||
output << EOS
|
||||
end
|
||||
# rubocop:enable Metrics/BlockNesting
|
||||
|
||||
@output << EOS
|
||||
end
|
||||
|
||||
def raise_syntax_error(start_pos)
|
||||
@ss.pos = start_pos
|
||||
# the character could be a UTF-8 character, use getch to get all the bytes
|
||||
raise SyntaxError, "Unexpected character #{@ss.getch}"
|
||||
def raise_syntax_error(start_pos, ss)
|
||||
ss.pos = start_pos
|
||||
# the character could be a UTF-8 character, use getch to get all the bytes
|
||||
raise SyntaxError, "Unexpected character #{ss.getch}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Lexer = StringScanner.instance_methods.include?(:scan_byte) ? Lexer2 : Lexer1
|
||||
end
|
||||
|
||||
@@ -12,6 +12,18 @@ module Liquid
|
||||
@locale = @template_options[:locale] ||= I18n.new
|
||||
@warnings = []
|
||||
|
||||
# constructing new StringScanner in Lexer, Tokenizer, etc is expensive
|
||||
# This StringScanner will be shared by all of them
|
||||
@string_scanner = StringScanner.new("")
|
||||
|
||||
@expression_cache = if options[:expression_cache].nil?
|
||||
{}
|
||||
elsif options[:expression_cache].respond_to?(:[]) && options[:expression_cache].respond_to?(:[]=)
|
||||
options[:expression_cache]
|
||||
elsif options[:expression_cache]
|
||||
{}
|
||||
end
|
||||
|
||||
self.depth = 0
|
||||
self.partial = false
|
||||
end
|
||||
@@ -24,12 +36,22 @@ module Liquid
|
||||
Liquid::BlockBody.new
|
||||
end
|
||||
|
||||
def new_tokenizer(markup, start_line_number: nil, for_liquid_tag: false)
|
||||
Tokenizer.new(markup, line_number: start_line_number, for_liquid_tag: for_liquid_tag)
|
||||
def new_parser(input)
|
||||
@string_scanner.string = input
|
||||
Parser.new(@string_scanner)
|
||||
end
|
||||
|
||||
def new_tokenizer(source, start_line_number: nil, for_liquid_tag: false)
|
||||
Tokenizer.new(
|
||||
source: source,
|
||||
string_scanner: @string_scanner,
|
||||
line_number: start_line_number,
|
||||
for_liquid_tag: for_liquid_tag,
|
||||
)
|
||||
end
|
||||
|
||||
def parse_expression(markup)
|
||||
Expression.parse(markup)
|
||||
Expression.parse(markup, @string_scanner, @expression_cache)
|
||||
end
|
||||
|
||||
def partial=(value)
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
module Liquid
|
||||
class Parser
|
||||
def initialize(input)
|
||||
l = Lexer.new(input)
|
||||
@tokens = l.tokenize
|
||||
ss = input.is_a?(StringScanner) ? input : StringScanner.new(input)
|
||||
@tokens = Lexer.tokenize(ss)
|
||||
@p = 0 # pointer to current location
|
||||
end
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
module Liquid
|
||||
class RangeLookup
|
||||
def self.parse(start_markup, end_markup)
|
||||
start_obj = Expression.parse(start_markup)
|
||||
end_obj = Expression.parse(end_markup)
|
||||
def self.parse(start_markup, end_markup, string_scanner, cache = nil)
|
||||
start_obj = Expression.parse(start_markup, string_scanner, cache)
|
||||
end_obj = Expression.parse(end_markup, string_scanner, cache)
|
||||
if start_obj.respond_to?(:evaluate) || end_obj.respond_to?(:evaluate)
|
||||
new(start_obj, end_obj)
|
||||
else
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
# StrainerFactory is the factory for the filters system.
|
||||
module StrainerFactory
|
||||
extend self
|
||||
|
||||
def add_global_filter(filter, environment = Environment.default)
|
||||
Deprecations.warn("StrainerFactory.add_global_filter", "Environment#register_filter")
|
||||
environment.register_filter(filter)
|
||||
end
|
||||
|
||||
def create(context, filters = Const::EMPTY_ARRAY, environment = Environment.default)
|
||||
Deprecations.warn("StrainerFactory.create", "StrainerFactory.create_strainer")
|
||||
environment.create_strainer(context, filters)
|
||||
end
|
||||
|
||||
def global_filter_names(environment = Environment.default)
|
||||
Deprecations.warn("StrainerFactory.global_filter_names", "Environment#filter_method_names")
|
||||
Environment.strainer_template.filter_method_names
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -68,7 +68,13 @@ module Liquid
|
||||
def variables_from_string(markup)
|
||||
markup.split(',').collect do |var|
|
||||
var =~ /\s*(#{QuotedFragment})\s*/o
|
||||
Regexp.last_match(1) ? parse_expression(Regexp.last_match(1)) : nil
|
||||
next unless Regexp.last_match(1)
|
||||
|
||||
# Expression Parser returns cached objects, and we need to dup them to
|
||||
# start the cycle over for each new cycle call.
|
||||
# Liquid-C does not have a cache, so we don't need to dup the object.
|
||||
var = parse_expression(Regexp.last_match(1))
|
||||
var.is_a?(VariableLookup) ? var.dup : var
|
||||
end.compact
|
||||
end
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ module Liquid
|
||||
end
|
||||
|
||||
def strict_parse(markup)
|
||||
p = Parser.new(markup)
|
||||
p = @parse_context.new_parser(markup)
|
||||
@variable_name = p.consume(:id)
|
||||
raise SyntaxError, options[:locale].t("errors.syntax.for_invalid_in") unless p.id?('in')
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ module Liquid
|
||||
end
|
||||
|
||||
def strict_parse(markup)
|
||||
p = Parser.new(markup)
|
||||
p = @parse_context.new_parser(markup)
|
||||
condition = parse_binary_comparisons(p)
|
||||
p.consume(:end_of_string)
|
||||
condition
|
||||
|
||||
+126
-16
@@ -1,20 +1,43 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "strscan"
|
||||
|
||||
module Liquid
|
||||
class Tokenizer
|
||||
attr_reader :line_number, :for_liquid_tag
|
||||
|
||||
def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false)
|
||||
@source = source
|
||||
@line_number = line_number || (line_numbers ? 1 : nil)
|
||||
TAG_END = /%\}/
|
||||
TAG_OR_VARIABLE_START = /\{[\{\%]/
|
||||
NEWLINE = /\n/
|
||||
|
||||
OPEN_CURLEY = "{".ord
|
||||
CLOSE_CURLEY = "}".ord
|
||||
PERCENTAGE = "%".ord
|
||||
|
||||
def initialize(
|
||||
source:,
|
||||
string_scanner:,
|
||||
line_numbers: false,
|
||||
line_number: nil,
|
||||
for_liquid_tag: false
|
||||
)
|
||||
@line_number = line_number || (line_numbers ? 1 : nil)
|
||||
@for_liquid_tag = for_liquid_tag
|
||||
@offset = 0
|
||||
@tokens = tokenize
|
||||
@source = source.to_s.to_str
|
||||
@offset = 0
|
||||
@tokens = []
|
||||
|
||||
if @source
|
||||
@ss = string_scanner
|
||||
@ss.string = @source
|
||||
tokenize
|
||||
end
|
||||
end
|
||||
|
||||
def shift
|
||||
token = @tokens[@offset]
|
||||
return nil unless token
|
||||
|
||||
return unless token
|
||||
|
||||
@offset += 1
|
||||
|
||||
@@ -28,18 +51,105 @@ module Liquid
|
||||
private
|
||||
|
||||
def tokenize
|
||||
return [] if @source.empty?
|
||||
|
||||
return @source.split("\n") if @for_liquid_tag
|
||||
|
||||
tokens = @source.split(TemplateParser)
|
||||
|
||||
# removes the rogue empty element at the beginning of the array
|
||||
if tokens[0]&.empty?
|
||||
@offset += 1
|
||||
if @for_liquid_tag
|
||||
@tokens = @source.split("\n")
|
||||
else
|
||||
@tokens << shift_normal until @ss.eos?
|
||||
end
|
||||
|
||||
tokens
|
||||
@source = nil
|
||||
@ss = nil
|
||||
end
|
||||
|
||||
def shift_normal
|
||||
token = next_token
|
||||
|
||||
return unless token
|
||||
|
||||
token
|
||||
end
|
||||
|
||||
def next_token
|
||||
# possible states: :text, :tag, :variable
|
||||
byte_a = @ss.peek_byte
|
||||
|
||||
if byte_a == OPEN_CURLEY
|
||||
@ss.scan_byte
|
||||
|
||||
byte_b = @ss.peek_byte
|
||||
|
||||
if byte_b == PERCENTAGE
|
||||
@ss.scan_byte
|
||||
return next_tag_token
|
||||
elsif byte_b == OPEN_CURLEY
|
||||
@ss.scan_byte
|
||||
return next_variable_token
|
||||
end
|
||||
|
||||
@ss.pos -= 1
|
||||
end
|
||||
|
||||
next_text_token
|
||||
end
|
||||
|
||||
def next_text_token
|
||||
start = @ss.pos
|
||||
|
||||
unless @ss.skip_until(TAG_OR_VARIABLE_START)
|
||||
token = @ss.rest
|
||||
@ss.terminate
|
||||
return token
|
||||
end
|
||||
|
||||
pos = @ss.pos -= 2
|
||||
@source.byteslice(start, pos - start)
|
||||
end
|
||||
|
||||
def next_variable_token
|
||||
start = @ss.pos - 2
|
||||
|
||||
byte_a = byte_b = @ss.scan_byte
|
||||
|
||||
while byte_b
|
||||
byte_a = @ss.scan_byte while byte_a && (byte_a != CLOSE_CURLEY && byte_a != OPEN_CURLEY)
|
||||
|
||||
break unless byte_a
|
||||
|
||||
if @ss.eos?
|
||||
return byte_a == CLOSE_CURLEY ? @source.byteslice(start, @ss.pos - start) : "{{"
|
||||
end
|
||||
|
||||
byte_b = @ss.scan_byte
|
||||
|
||||
if byte_a == CLOSE_CURLEY
|
||||
if byte_b == CLOSE_CURLEY
|
||||
return @source.byteslice(start, @ss.pos - start)
|
||||
elsif byte_b != CLOSE_CURLEY
|
||||
@ss.pos -= 1
|
||||
return @source.byteslice(start, @ss.pos - start)
|
||||
end
|
||||
elsif byte_a == OPEN_CURLEY && byte_b == PERCENTAGE
|
||||
return next_tag_token_with_start(start)
|
||||
end
|
||||
|
||||
byte_a = byte_b
|
||||
end
|
||||
|
||||
"{{"
|
||||
end
|
||||
|
||||
def next_tag_token
|
||||
start = @ss.pos - 2
|
||||
if (len = @ss.skip_until(TAG_END))
|
||||
@source.byteslice(start, len + 2)
|
||||
else
|
||||
"{%"
|
||||
end
|
||||
end
|
||||
|
||||
def next_tag_token_with_start(start)
|
||||
@ss.skip_until(TAG_END)
|
||||
@source.byteslice(start, @ss.pos - start)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+13
-7
@@ -61,7 +61,7 @@ module Liquid
|
||||
|
||||
def strict_parse(markup)
|
||||
@filters = []
|
||||
p = Parser.new(markup)
|
||||
p = @parse_context.new_parser(markup)
|
||||
|
||||
return if p.look(:end_of_string)
|
||||
|
||||
@@ -95,15 +95,21 @@ module Liquid
|
||||
|
||||
def render_to_output_buffer(context, output)
|
||||
obj = render(context)
|
||||
render_obj_to_output(obj, output)
|
||||
output
|
||||
end
|
||||
|
||||
if obj.is_a?(Array)
|
||||
output << obj.join
|
||||
elsif obj.nil?
|
||||
else
|
||||
def render_obj_to_output(obj, output)
|
||||
case obj
|
||||
when NilClass
|
||||
# Do nothing
|
||||
when Array
|
||||
obj.each do |o|
|
||||
render_obj_to_output(o, output)
|
||||
end
|
||||
when
|
||||
output << obj.to_s
|
||||
end
|
||||
|
||||
output
|
||||
end
|
||||
|
||||
def disabled?(_context)
|
||||
|
||||
@@ -6,16 +6,20 @@ module Liquid
|
||||
|
||||
attr_reader :name, :lookups
|
||||
|
||||
def self.parse(markup)
|
||||
new(markup)
|
||||
def self.parse(markup, string_scanner = StringScanner.new(""), cache = nil)
|
||||
new(markup, string_scanner, cache)
|
||||
end
|
||||
|
||||
def initialize(markup)
|
||||
def initialize(markup, string_scanner = StringScanner.new(""), cache = nil)
|
||||
lookups = markup.scan(VariableParser)
|
||||
|
||||
name = lookups.shift
|
||||
if name&.start_with?('[') && name&.end_with?(']')
|
||||
name = Expression.parse(name[1..-2])
|
||||
name = Expression.parse(
|
||||
name[1..-2],
|
||||
string_scanner,
|
||||
cache,
|
||||
)
|
||||
end
|
||||
@name = name
|
||||
|
||||
@@ -25,7 +29,11 @@ module Liquid
|
||||
@lookups.each_index do |i|
|
||||
lookup = lookups[i]
|
||||
if lookup&.start_with?('[') && lookup&.end_with?(']')
|
||||
lookups[i] = Expression.parse(lookup[1..-2])
|
||||
lookups[i] = Expression.parse(
|
||||
lookup[1..-2],
|
||||
string_scanner,
|
||||
cache,
|
||||
)
|
||||
elsif COMMAND_METHODS.include?(lookup)
|
||||
@command_flags |= 1 << i
|
||||
end
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Liquid
|
||||
VERSION = "5.6.0.alpha"
|
||||
VERSION = "5.6.4"
|
||||
end
|
||||
|
||||
+2
-2
@@ -13,7 +13,7 @@ Gem::Specification.new do |s|
|
||||
s.summary = "A secure, non-evaling end user template engine with aesthetic markup."
|
||||
s.authors = ["Tobias Lütke"]
|
||||
s.email = ["[email protected]"]
|
||||
s.homepage = "http://www.liquidmarkup.org"
|
||||
s.homepage = "https://shopify.github.io/liquid/"
|
||||
s.license = "MIT"
|
||||
# s.description = "A secure, non-evaling end user template engine with aesthetic markup."
|
||||
|
||||
@@ -28,7 +28,7 @@ Gem::Specification.new do |s|
|
||||
|
||||
s.require_path = "lib"
|
||||
|
||||
s.add_dependency("strscan")
|
||||
s.add_dependency("strscan", ">= 3.1.1")
|
||||
s.add_dependency("bigdecimal")
|
||||
|
||||
s.add_development_dependency('rake', '~> 13.0')
|
||||
|
||||
@@ -4,18 +4,22 @@ require 'benchmark/ips'
|
||||
require_relative 'theme_runner'
|
||||
|
||||
RubyVM::YJIT.enable if defined?(RubyVM::YJIT)
|
||||
Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
|
||||
Liquid::Environment.default.error_mode = ARGV.first.to_sym if ARGV.first
|
||||
|
||||
profiler = ThemeRunner.new
|
||||
|
||||
Benchmark.ips do |x|
|
||||
x.time = 10
|
||||
x.warmup = 5
|
||||
x.time = 20
|
||||
x.warmup = 10
|
||||
|
||||
puts
|
||||
puts "Running benchmark for #{x.time} seconds (with #{x.warmup} seconds warmup)."
|
||||
puts
|
||||
|
||||
x.report("parse:") { profiler.compile }
|
||||
x.report("render:") { profiler.render }
|
||||
x.report("parse & render:") { profiler.run }
|
||||
phase = ENV["PHASE"] || "all"
|
||||
|
||||
x.report("tokenize:") { profiler.tokenize } if phase == "all" || phase == "tokenize"
|
||||
x.report("parse:") { profiler.compile } if phase == "all" || phase == "parse"
|
||||
x.report("render:") { profiler.render } if phase == "all" || phase == "render"
|
||||
x.report("parse & render:") { profiler.run } if phase == "all" || phase == "run"
|
||||
end
|
||||
|
||||
@@ -11,11 +11,12 @@ require_relative 'shop_filter'
|
||||
require_relative 'tag_filter'
|
||||
require_relative 'weight_filter'
|
||||
|
||||
Liquid::Template.register_tag('paginate', Paginate)
|
||||
Liquid::Template.register_tag('form', CommentForm)
|
||||
default_environment = Liquid::Environment.default
|
||||
default_environment.register_tag('paginate', Paginate)
|
||||
default_environment.register_tag('form', CommentForm)
|
||||
|
||||
Liquid::Template.register_filter(JsonFilter)
|
||||
Liquid::Template.register_filter(MoneyFilter)
|
||||
Liquid::Template.register_filter(WeightFilter)
|
||||
Liquid::Template.register_filter(ShopFilter)
|
||||
Liquid::Template.register_filter(TagFilter)
|
||||
default_environment.register_filter(JsonFilter)
|
||||
default_environment.register_filter(MoneyFilter)
|
||||
default_environment.register_filter(WeightFilter)
|
||||
default_environment.register_filter(ShopFilter)
|
||||
default_environment.register_filter(TagFilter)
|
||||
|
||||
@@ -48,6 +48,19 @@ class ThemeRunner
|
||||
end
|
||||
end
|
||||
|
||||
# `tokenize` will just test the tokenizen portion of liquid without any templates
|
||||
def tokenize
|
||||
ss = StringScanner.new("")
|
||||
@tests.each do |test_hash|
|
||||
tokenizer = Liquid::Tokenizer.new(
|
||||
source: test_hash[:liquid],
|
||||
string_scanner: ss,
|
||||
line_numbers: true,
|
||||
)
|
||||
while tokenizer.shift; end
|
||||
end
|
||||
end
|
||||
|
||||
# `run` is called to benchmark rendering and compiling at the same time
|
||||
def run
|
||||
each_test do |liquid, layout, assigns, page_template, template_name|
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "benchmark/ips"
|
||||
|
||||
# benchmark liquid lexing
|
||||
|
||||
require 'liquid'
|
||||
|
||||
RubyVM::YJIT.enable
|
||||
|
||||
STRING_MARKUPS = [
|
||||
"\"foo\"",
|
||||
"\"fooooooooooo\"",
|
||||
"\"foooooooooooooooooooooooooooooo\"",
|
||||
"'foo'",
|
||||
"'fooooooooooo'",
|
||||
"'foooooooooooooooooooooooooooooo'",
|
||||
]
|
||||
|
||||
VARIABLE_MARKUPS = [
|
||||
"article",
|
||||
"article.title",
|
||||
"article.title.size",
|
||||
"very_long_variable_name_2024_11_05",
|
||||
"very_long_variable_name_2024_11_05.size",
|
||||
]
|
||||
|
||||
NUMBER_MARKUPS = [
|
||||
"0",
|
||||
"35",
|
||||
"1241891024912849",
|
||||
"3.5",
|
||||
"3.51214128409128",
|
||||
"12381902839.123819283910283",
|
||||
"123.456.789",
|
||||
"-123",
|
||||
"-12.33",
|
||||
"-405.231",
|
||||
"-0",
|
||||
"0",
|
||||
"0.0",
|
||||
"0.0000000000000000000000",
|
||||
"0.00000000001",
|
||||
]
|
||||
|
||||
RANGE_MARKUPS = [
|
||||
"(1..30)",
|
||||
"(1...30)",
|
||||
"(1..30..5)",
|
||||
"(1.0...30.0)",
|
||||
"(1.........30)",
|
||||
"(1..foo)",
|
||||
"(foo..30)",
|
||||
"(foo..bar)",
|
||||
"(foo...bar...100)",
|
||||
"(foo...bar...100.0)",
|
||||
]
|
||||
|
||||
LITERAL_MARKUPS = [
|
||||
nil,
|
||||
'nil',
|
||||
'null',
|
||||
'',
|
||||
'true',
|
||||
'false',
|
||||
'blank',
|
||||
'empty',
|
||||
]
|
||||
|
||||
MARKUPS = {
|
||||
"string" => STRING_MARKUPS,
|
||||
"literal" => LITERAL_MARKUPS,
|
||||
"variable" => VARIABLE_MARKUPS,
|
||||
"number" => NUMBER_MARKUPS,
|
||||
"range" => RANGE_MARKUPS,
|
||||
}
|
||||
|
||||
Benchmark.ips do |x|
|
||||
x.config(time: 5, warmup: 5)
|
||||
|
||||
MARKUPS.each do |type, markups|
|
||||
x.report("Liquid::Expression#parse: #{type}") do
|
||||
markups.each do |markup|
|
||||
Liquid::Expression.parse(markup)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
x.report("Liquid::Expression#parse: all") do
|
||||
MARKUPS.values.flatten.each do |markup|
|
||||
Liquid::Expression.parse(markup)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -29,31 +29,12 @@ EXPRESSIONS = [
|
||||
"foo | default: -1",
|
||||
]
|
||||
|
||||
EXPRESSIONS.each do |expr|
|
||||
lexer_1_result = Liquid::Lexer1.new(expr).tokenize
|
||||
lexer_2_result = Liquid::Lexer2.new(expr).tokenize
|
||||
|
||||
next if lexer_1_result == lexer_2_result
|
||||
|
||||
warn "Lexer1 and Lexer2 results are different for expression: #{expr}"
|
||||
warn "expected: #{lexer_1_result}"
|
||||
warn "got: #{lexer_2_result}"
|
||||
abort
|
||||
end
|
||||
|
||||
Benchmark.ips do |x|
|
||||
x.config(time: 10, warmup: 5)
|
||||
|
||||
x.report("Liquid::Lexer1#tokenize") do
|
||||
x.report("Liquid::Lexer#tokenize") do
|
||||
EXPRESSIONS.each do |expr|
|
||||
l = Liquid::Lexer1.new(expr)
|
||||
l.tokenize
|
||||
end
|
||||
end
|
||||
|
||||
x.report("Liquid::Lexer2#tokenize") do
|
||||
EXPRESSIONS.each do |expr|
|
||||
l = Liquid::Lexer2.new(expr)
|
||||
l = Liquid::Lexer.new(expr)
|
||||
l.tokenize
|
||||
end
|
||||
end
|
||||
|
||||
@@ -203,20 +203,19 @@ class ErrorHandlingTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_setting_default_exception_renderer
|
||||
old_exception_renderer = Liquid::Template.default_exception_renderer
|
||||
exceptions = []
|
||||
Liquid::Template.default_exception_renderer = ->(e) {
|
||||
default_exception_renderer = ->(e) {
|
||||
exceptions << e
|
||||
''
|
||||
}
|
||||
template = Liquid::Template.parse('This is a runtime error: {{ errors.argument_error }}')
|
||||
|
||||
env = Liquid::Environment.build(exception_renderer: default_exception_renderer)
|
||||
template = Liquid::Template.parse('This is a runtime error: {{ errors.argument_error }}', environment: env)
|
||||
|
||||
output = template.render('errors' => ErrorDrop.new)
|
||||
|
||||
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_setting_exception_renderer_on_environment
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
require 'lru_redux'
|
||||
|
||||
class ExpressionTest < Minitest::Test
|
||||
def test_keyword_literals
|
||||
@@ -13,6 +14,7 @@ class ExpressionTest < Minitest::Test
|
||||
assert_template_result("double quoted", '{{"double quoted"}}')
|
||||
assert_template_result("spaced", "{{ 'spaced' }}")
|
||||
assert_template_result("spaced2", "{{ 'spaced2' }}")
|
||||
assert_template_result("emoji🔥", "{{ 'emoji🔥' }}")
|
||||
end
|
||||
|
||||
def test_int
|
||||
@@ -22,8 +24,18 @@ class ExpressionTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_float
|
||||
assert_template_result("-17.42", "{{ -17.42 }}")
|
||||
assert_template_result("2.5", "{{ 2.5 }}")
|
||||
assert_expression_result(0.0, "0.....5")
|
||||
assert_expression_result(0.0, "-0..1")
|
||||
assert_expression_result(1.5, "1.5")
|
||||
|
||||
# this is a unfortunate quirky behavior of Liquid
|
||||
result = Expression.parse(".5")
|
||||
assert_kind_of(Liquid::VariableLookup, result)
|
||||
|
||||
result = Expression.parse("-.5")
|
||||
assert_kind_of(Liquid::VariableLookup, result)
|
||||
end
|
||||
|
||||
def test_range
|
||||
@@ -40,6 +52,101 @@ class ExpressionTest < Minitest::Test
|
||||
)
|
||||
end
|
||||
|
||||
def test_quirky_negative_sign_expression_markup
|
||||
result = Expression.parse("-", nil)
|
||||
assert(result.is_a?(VariableLookup))
|
||||
assert_equal("-", result.name)
|
||||
|
||||
# for this template, the expression markup is "-"
|
||||
assert_template_result(
|
||||
"",
|
||||
"{{ - 'theme.css' - }}",
|
||||
)
|
||||
end
|
||||
|
||||
def test_expression_cache
|
||||
skip("Liquid-C does not support Expression caching") if defined?(Liquid::C) && Liquid::C.enabled
|
||||
|
||||
cache = {}
|
||||
template = <<~LIQUID
|
||||
{% assign x = 1 %}
|
||||
{{ x }}
|
||||
{% assign x = 2 %}
|
||||
{{ x }}
|
||||
{% assign y = 1 %}
|
||||
{{ y }}
|
||||
LIQUID
|
||||
|
||||
Liquid::Template.parse(template, expression_cache: cache).render
|
||||
|
||||
assert_equal(
|
||||
["1", "2", "x", "y"],
|
||||
cache.to_a.map { _1[0] }.sort,
|
||||
)
|
||||
end
|
||||
|
||||
def test_expression_cache_with_true_boolean
|
||||
skip("Liquid-C does not support Expression caching") if defined?(Liquid::C) && Liquid::C.enabled
|
||||
|
||||
template = <<~LIQUID
|
||||
{% assign x = 1 %}
|
||||
{{ x }}
|
||||
{% assign x = 2 %}
|
||||
{{ x }}
|
||||
{% assign y = 1 %}
|
||||
{{ y }}
|
||||
LIQUID
|
||||
|
||||
parse_context = ParseContext.new(expression_cache: true)
|
||||
|
||||
Liquid::Template.parse(template, parse_context).render
|
||||
|
||||
cache = parse_context.instance_variable_get(:@expression_cache)
|
||||
|
||||
assert_equal(
|
||||
["1", "2", "x", "y"],
|
||||
cache.to_a.map { _1[0] }.sort,
|
||||
)
|
||||
end
|
||||
|
||||
def test_expression_cache_with_lru_redux
|
||||
skip("Liquid-C does not support Expression caching") if defined?(Liquid::C) && Liquid::C.enabled
|
||||
|
||||
cache = LruRedux::Cache.new(10)
|
||||
template = <<~LIQUID
|
||||
{% assign x = 1 %}
|
||||
{{ x }}
|
||||
{% assign x = 2 %}
|
||||
{{ x }}
|
||||
{% assign y = 1 %}
|
||||
{{ y }}
|
||||
LIQUID
|
||||
|
||||
Liquid::Template.parse(template, expression_cache: cache).render
|
||||
|
||||
assert_equal(
|
||||
["1", "2", "x", "y"],
|
||||
cache.to_a.map { _1[0] }.sort,
|
||||
)
|
||||
end
|
||||
|
||||
def test_disable_expression_cache
|
||||
skip("Liquid-C does not support Expression caching") if defined?(Liquid::C) && Liquid::C.enabled
|
||||
|
||||
template = <<~LIQUID
|
||||
{% assign x = 1 %}
|
||||
{{ x }}
|
||||
{% assign x = 2 %}
|
||||
{{ x }}
|
||||
{% assign y = 1 %}
|
||||
{{ y }}
|
||||
LIQUID
|
||||
|
||||
parse_context = Liquid::ParseContext.new(expression_cache: false)
|
||||
Liquid::Template.parse(template, parse_context).render
|
||||
assert(parse_context.instance_variable_get(:@expression_cache).nil?)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def assert_expression_result(expect, markup, **assigns)
|
||||
|
||||
@@ -134,6 +134,14 @@ class ParsingQuirksTest < Minitest::Test
|
||||
|
||||
def test_incomplete_expression
|
||||
with_error_mode(:lax) do
|
||||
assert_template_result("false", "{{ false - }}")
|
||||
assert_template_result("false", "{{ false > }}")
|
||||
assert_template_result("false", "{{ false < }}")
|
||||
assert_template_result("false", "{{ false = }}")
|
||||
assert_template_result("false", "{{ false ! }}")
|
||||
assert_template_result("false", "{{ false 1 }}")
|
||||
assert_template_result("false", "{{ false a }}")
|
||||
|
||||
assert_template_result("false", "{% liquid assign foo = false -\n%}{{ foo }}")
|
||||
assert_template_result("false", "{% liquid assign foo = false >\n%}{{ foo }}")
|
||||
assert_template_result("false", "{% liquid assign foo = false <\n%}{{ foo }}")
|
||||
|
||||
@@ -33,7 +33,7 @@ class ProfilerTest < Minitest::Test
|
||||
end
|
||||
|
||||
def setup
|
||||
Liquid::Template.file_system = ProfilingFileSystem.new
|
||||
Liquid::Environment.default.file_system = ProfilingFileSystem.new
|
||||
end
|
||||
|
||||
def test_template_allows_flagging_profiling
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class CycleTagTest < Minitest::Test
|
||||
def test_simple_cycle
|
||||
template = <<~LIQUID
|
||||
{%- cycle '1', '2', '3' -%}
|
||||
{%- cycle '1', '2', '3' -%}
|
||||
{%- cycle '1', '2', '3' -%}
|
||||
LIQUID
|
||||
|
||||
assert_template_result("123", template)
|
||||
end
|
||||
|
||||
def test_simple_cycle_inside_for_loop
|
||||
template = <<~LIQUID
|
||||
{%- for i in (1..3) -%}
|
||||
{% cycle '1', '2', '3' %}
|
||||
{%- endfor -%}
|
||||
LIQUID
|
||||
|
||||
assert_template_result("123", template)
|
||||
end
|
||||
|
||||
def test_cycle_with_variables_inside_for_loop
|
||||
template = <<~LIQUID
|
||||
{%- assign a = 1 -%}
|
||||
{%- assign b = 2 -%}
|
||||
{%- assign c = 3 -%}
|
||||
{%- for i in (1..3) -%}
|
||||
{% cycle a, b, c %}
|
||||
{%- endfor -%}
|
||||
LIQUID
|
||||
|
||||
assert_template_result("123", template)
|
||||
end
|
||||
|
||||
def test_cycle_tag_always_resets_cycle
|
||||
template = <<~LIQUID
|
||||
{%- assign a = "1" -%}
|
||||
{%- cycle a, "2" -%}
|
||||
{%- cycle a, "2" -%}
|
||||
LIQUID
|
||||
|
||||
assert_template_result("11", template)
|
||||
end
|
||||
end
|
||||
@@ -174,10 +174,10 @@ class IncludeTagTest < Minitest::Test
|
||||
end
|
||||
end
|
||||
|
||||
Liquid::Template.file_system = infinite_file_system.new
|
||||
env = Liquid::Environment.build(file_system: infinite_file_system.new)
|
||||
|
||||
assert_raises(Liquid::StackLevelError) do
|
||||
Template.parse("{% include 'loop' %}").render!
|
||||
Template.parse("{% include 'loop' %}", environment: env).render!
|
||||
end
|
||||
end
|
||||
|
||||
@@ -264,26 +264,27 @@ class IncludeTagTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_does_not_add_error_in_strict_mode_for_missing_variable
|
||||
Liquid::Template.file_system = TestFileSystem.new
|
||||
env = Liquid::Environment.build(file_system: TestFileSystem.new)
|
||||
|
||||
a = Liquid::Template.parse(' {% include "nested_template" %}')
|
||||
a = Liquid::Template.parse(' {% include "nested_template" %}', environment: env)
|
||||
a.render!
|
||||
assert_empty(a.errors)
|
||||
end
|
||||
|
||||
def test_passing_options_to_included_templates
|
||||
Liquid::Template.file_system = TestFileSystem.new
|
||||
env = Liquid::Environment.build(file_system: TestFileSystem.new)
|
||||
|
||||
assert_raises(Liquid::SyntaxError) do
|
||||
Template.parse("{% include template %}", error_mode: :strict).render!("template" => '{{ "X" || downcase }}')
|
||||
Template.parse("{% include template %}", error_mode: :strict, environment: env).render!("template" => '{{ "X" || downcase }}')
|
||||
end
|
||||
with_error_mode(:lax) do
|
||||
assert_equal('x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: true).render!("template" => '{{ "X" || downcase }}'))
|
||||
assert_equal('x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: true, environment: env).render!("template" => '{{ "X" || downcase }}'))
|
||||
end
|
||||
assert_raises(Liquid::SyntaxError) do
|
||||
Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:locale]).render!("template" => '{{ "X" || downcase }}')
|
||||
Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:locale], environment: env).render!("template" => '{{ "X" || downcase }}')
|
||||
end
|
||||
with_error_mode(:lax) do
|
||||
assert_equal('x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:error_mode]).render!("template" => '{{ "X" || downcase }}'))
|
||||
assert_equal('x', Template.parse("{% include template %}", error_mode: :strict, include_options_blacklist: [:error_mode], environment: env).render!("template" => '{{ "X" || downcase }}'))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -334,8 +335,11 @@ class IncludeTagTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_including_with_strict_variables
|
||||
Liquid::Template.file_system = StubFileSystem.new({ "simple" => "simple" })
|
||||
template = Liquid::Template.parse("{% include 'simple' %}", error_mode: :warn)
|
||||
env = Liquid::Environment.build(
|
||||
file_system: StubFileSystem.new('simple' => 'simple'),
|
||||
)
|
||||
|
||||
template = Liquid::Template.parse("{% include 'simple' %}", error_mode: :warn, environment: env)
|
||||
template.render(nil, strict_variables: true)
|
||||
|
||||
assert_equal([], template.errors)
|
||||
|
||||
@@ -16,6 +16,7 @@ class RawTagTest < Minitest::Test
|
||||
assert_template_result('>{{ test }}<', '> {%- raw -%}{{ test }}{%- endraw -%} <')
|
||||
assert_template_result("> inner <", "> {%- raw -%} inner {%- endraw %} <")
|
||||
assert_template_result("> inner <", "> {%- raw -%} inner {%- endraw -%} <")
|
||||
assert_template_result("{Hello}", "{% raw %}{{% endraw %}Hello{% raw %}}{% endraw %}")
|
||||
end
|
||||
|
||||
def test_open_tag_in_raw
|
||||
|
||||
@@ -82,19 +82,22 @@ class RenderTagTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_recursively_rendered_template_does_not_produce_endless_loop
|
||||
Liquid::Template.file_system = StubFileSystem.new('loop' => '{% render "loop" %}')
|
||||
env = Liquid::Environment.build(
|
||||
file_system: StubFileSystem.new('loop' => '{% render "loop" %}'),
|
||||
)
|
||||
|
||||
assert_raises(Liquid::StackLevelError) do
|
||||
Template.parse('{% render "loop" %}').render!
|
||||
Template.parse('{% render "loop" %}', environment: env).render!
|
||||
end
|
||||
end
|
||||
|
||||
def test_sub_contexts_count_towards_the_same_recursion_limit
|
||||
Liquid::Template.file_system = StubFileSystem.new(
|
||||
'loop_render' => '{% render "loop_render" %}',
|
||||
env = Liquid::Environment.build(
|
||||
file_system: StubFileSystem.new('loop_render' => '{% render "loop_render" %}'),
|
||||
)
|
||||
|
||||
assert_raises(Liquid::StackLevelError) do
|
||||
Template.parse('{% render "loop_render" %}').render!
|
||||
Template.parse('{% render "loop_render" %}', environment: env).render!
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -130,6 +130,10 @@ class VariableTest < Minitest::Test
|
||||
assert_template_result('bar', '{{ foo }}', { 'foo' => :bar })
|
||||
end
|
||||
|
||||
def test_nested_array
|
||||
assert_template_result('', '{{ foo }}', { 'foo' => [[nil]] })
|
||||
end
|
||||
|
||||
def test_dynamic_find_var
|
||||
assert_template_result('bar', '{{ [key] }}', { 'key' => 'foo', 'foo' => 'bar' })
|
||||
end
|
||||
|
||||
+4
-9
@@ -13,12 +13,7 @@ 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
|
||||
Liquid::Environment.default.error_mode = mode
|
||||
|
||||
if Minitest.const_defined?('Test')
|
||||
# We're on Minitest 5+. Nothing to do here.
|
||||
@@ -88,11 +83,11 @@ module Minitest
|
||||
end
|
||||
|
||||
def with_error_mode(mode)
|
||||
old_mode = Liquid::Template.error_mode
|
||||
Liquid::Template.error_mode = mode
|
||||
old_mode = Liquid::Environment.default.error_mode
|
||||
Liquid::Environment.default.error_mode = mode
|
||||
yield
|
||||
ensure
|
||||
Liquid::Template.error_mode = old_mode
|
||||
Liquid::Environment.default.error_mode = old_mode
|
||||
end
|
||||
|
||||
def with_custom_tag(tag_name, tag_class, &block)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
class StrainerFactoryUnitTest < Minitest::Test
|
||||
class EnvironmentFilterTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
module AccessScopeFilters
|
||||
@@ -16,8 +16,6 @@ class StrainerFactoryUnitTest < Minitest::Test
|
||||
private :private_filter
|
||||
end
|
||||
|
||||
StrainerFactory.add_global_filter(AccessScopeFilters)
|
||||
|
||||
module LateAddedFilter
|
||||
def late_added_filter(_input)
|
||||
"filtered"
|
||||
@@ -25,24 +23,28 @@ class StrainerFactoryUnitTest < Minitest::Test
|
||||
end
|
||||
|
||||
def setup
|
||||
@context = Context.build
|
||||
@environment = Liquid::Environment.build do |env|
|
||||
env.register_filter(AccessScopeFilters)
|
||||
end
|
||||
|
||||
@context = Context.build(environment: @environment)
|
||||
end
|
||||
|
||||
def test_strainer
|
||||
strainer = StrainerFactory.create(@context)
|
||||
strainer = @environment.create_strainer(@context)
|
||||
assert_equal(5, strainer.invoke('size', 'input'))
|
||||
assert_equal("public", strainer.invoke("public_filter"))
|
||||
end
|
||||
|
||||
def test_stainer_raises_argument_error
|
||||
strainer = StrainerFactory.create(@context)
|
||||
def test_strainer_raises_argument_error
|
||||
strainer = @environment.create_strainer(@context)
|
||||
assert_raises(Liquid::ArgumentError) do
|
||||
strainer.invoke("public_filter", 1)
|
||||
end
|
||||
end
|
||||
|
||||
def test_stainer_argument_error_contains_backtrace
|
||||
strainer = StrainerFactory.create(@context)
|
||||
def test_strainer_argument_error_contains_backtrace
|
||||
strainer = @environment.create_strainer(@context)
|
||||
|
||||
exception = assert_raises(Liquid::ArgumentError) do
|
||||
strainer.invoke("public_filter", 1)
|
||||
@@ -52,12 +54,13 @@ class StrainerFactoryUnitTest < Minitest::Test
|
||||
/\ALiquid error: wrong number of arguments \((1 for 0|given 1, expected 0)\)\z/,
|
||||
exception.message,
|
||||
)
|
||||
|
||||
source = AccessScopeFilters.instance_method(:public_filter).source_location
|
||||
assert_equal(source.map(&:to_s), exception.backtrace[0].split(':')[0..1])
|
||||
assert_equal(source[0..1].map(&:to_s), exception.backtrace[0].split(':')[0..1])
|
||||
end
|
||||
|
||||
def test_strainer_only_invokes_public_filter_methods
|
||||
strainer = StrainerFactory.create(@context)
|
||||
strainer = @environment.create_strainer(@context)
|
||||
assert_equal(false, strainer.class.invokable?('__test__'))
|
||||
assert_equal(false, strainer.class.invokable?('test'))
|
||||
assert_equal(false, strainer.class.invokable?('instance_eval'))
|
||||
@@ -66,18 +69,18 @@ class StrainerFactoryUnitTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_strainer_returns_nil_if_no_filter_method_found
|
||||
strainer = StrainerFactory.create(@context)
|
||||
strainer = @environment.create_strainer(@context)
|
||||
assert_nil(strainer.invoke("private_filter"))
|
||||
assert_nil(strainer.invoke("undef_the_filter"))
|
||||
end
|
||||
|
||||
def test_strainer_returns_first_argument_if_no_method_and_arguments_given
|
||||
strainer = StrainerFactory.create(@context)
|
||||
strainer = @environment.create_strainer(@context)
|
||||
assert_equal("password", strainer.invoke("undef_the_method", "password"))
|
||||
end
|
||||
|
||||
def test_strainer_only_allows_methods_defined_in_filters
|
||||
strainer = StrainerFactory.create(@context)
|
||||
strainer = @environment.create_strainer(@context)
|
||||
assert_equal("1 + 1", strainer.invoke("instance_eval", "1 + 1"))
|
||||
assert_equal("puts", strainer.invoke("__send__", "puts", "Hi Mom"))
|
||||
assert_equal("has_method?", strainer.invoke("invoke", "has_method?", "invoke"))
|
||||
@@ -86,7 +89,9 @@ class StrainerFactoryUnitTest < Minitest::Test
|
||||
def test_strainer_uses_a_class_cache_to_avoid_method_cache_invalidation
|
||||
a = Module.new
|
||||
b = Module.new
|
||||
strainer = StrainerFactory.create(@context, [a, b])
|
||||
|
||||
strainer = @environment.create_strainer(@context, [a, b])
|
||||
|
||||
assert_kind_of(StrainerTemplate, strainer)
|
||||
assert_kind_of(a, strainer)
|
||||
assert_kind_of(b, strainer)
|
||||
@@ -94,8 +99,10 @@ class StrainerFactoryUnitTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_add_global_filter_clears_cache
|
||||
assert_equal('input', StrainerFactory.create(@context).invoke('late_added_filter', 'input'))
|
||||
StrainerFactory.add_global_filter(LateAddedFilter)
|
||||
assert_equal('filtered', StrainerFactory.create(nil).invoke('late_added_filter', 'input'))
|
||||
assert_equal('input', @environment.create_strainer(@context).invoke('late_added_filter', 'input'))
|
||||
|
||||
@environment.register_filter(LateAddedFilter)
|
||||
|
||||
assert_equal('filtered', @environment.create_strainer(nil).invoke('late_added_filter', 'input'))
|
||||
end
|
||||
end
|
||||
@@ -134,6 +134,6 @@ class LexerUnitTest < Minitest::Test
|
||||
private
|
||||
|
||||
def tokenize(input)
|
||||
Lexer.new(input).tokenize
|
||||
Lexer.tokenize(StringScanner.new(input))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,20 +6,20 @@ class ParserUnitTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_consume
|
||||
p = Parser.new("wat: 7")
|
||||
p = new_parser("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 = new_parser("wat: 7")
|
||||
p.jump(2)
|
||||
assert_equal('7', p.consume(:number))
|
||||
end
|
||||
|
||||
def test_consume?
|
||||
p = Parser.new("wat: 7")
|
||||
p = new_parser("wat: 7")
|
||||
assert_equal('wat', p.consume?(:id))
|
||||
assert_equal(false, p.consume?(:dot))
|
||||
assert_equal(':', p.consume(:colon))
|
||||
@@ -27,7 +27,7 @@ class ParserUnitTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_id?
|
||||
p = Parser.new("wat 6 Peter Hegemon")
|
||||
p = new_parser("wat 6 Peter Hegemon")
|
||||
assert_equal('wat', p.id?('wat'))
|
||||
assert_equal(false, p.id?('endgame'))
|
||||
assert_equal('6', p.consume(:number))
|
||||
@@ -36,7 +36,7 @@ class ParserUnitTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_look
|
||||
p = Parser.new("wat 6 Peter Hegemon")
|
||||
p = new_parser("wat 6 Peter Hegemon")
|
||||
assert_equal(true, p.look(:id))
|
||||
assert_equal('wat', p.consume(:id))
|
||||
assert_equal(false, p.look(:comparison))
|
||||
@@ -46,12 +46,12 @@ class ParserUnitTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_expressions
|
||||
p = Parser.new("hi.there hi?[5].there? hi.there.bob")
|
||||
p = new_parser("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\"")
|
||||
p = new_parser("567 6.0 'lol' \"wut\"")
|
||||
assert_equal('567', p.expression)
|
||||
assert_equal('6.0', p.expression)
|
||||
assert_equal("'lol'", p.expression)
|
||||
@@ -59,7 +59,7 @@ class ParserUnitTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_ranges
|
||||
p = Parser.new("(5..7) (1.5..9.6) (young..old) (hi[5].wat..old)")
|
||||
p = new_parser("(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)
|
||||
@@ -67,7 +67,7 @@ class ParserUnitTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_arguments
|
||||
p = Parser.new("filter: hi.there[5], keyarg: 7")
|
||||
p = new_parser("filter: hi.there[5], keyarg: 7")
|
||||
assert_equal('filter', p.consume(:id))
|
||||
assert_equal(':', p.consume(:colon))
|
||||
assert_equal('hi.there[5]', p.argument)
|
||||
@@ -77,8 +77,14 @@ class ParserUnitTest < Minitest::Test
|
||||
|
||||
def test_invalid_expression
|
||||
assert_raises(SyntaxError) do
|
||||
p = Parser.new("==")
|
||||
p = new_parser("==")
|
||||
p.expression
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def new_parser(str)
|
||||
Parser.new(StringScanner.new(str))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -25,11 +25,13 @@ class StrainerTemplateUnitTest < Minitest::Test
|
||||
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)
|
||||
Liquid::Environment.build do |env|
|
||||
env.register_filter(PublicMethodOverrideFilter)
|
||||
env.register_filter(PrivateMethodOverrideFilter)
|
||||
end
|
||||
end
|
||||
|
||||
assert_equal('Liquid error: Filter overrides registered public methods as non public: public_filter', error.message)
|
||||
end
|
||||
|
||||
@@ -42,11 +44,13 @@ class StrainerTemplateUnitTest < Minitest::Test
|
||||
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)
|
||||
Liquid::Environment.build do |env|
|
||||
env.register_filter(PublicMethodOverrideFilter)
|
||||
env.register_filter(ProtectedMethodOverrideFilter)
|
||||
end
|
||||
end
|
||||
|
||||
assert_equal('Liquid error: Filter overrides registered public methods as non public: public_filter', error.message)
|
||||
end
|
||||
|
||||
|
||||
@@ -6,18 +6,18 @@ class TagUnitTest < Minitest::Test
|
||||
include Liquid
|
||||
|
||||
def test_tag
|
||||
tag = Tag.parse('tag', "", Tokenizer.new(""), ParseContext.new)
|
||||
tag = Tag.parse('tag', "", new_tokenizer, 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)
|
||||
tag = Tag.parse("long_tag", "param1, param2, param3", new_tokenizer, 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)
|
||||
tag = Tag.parse("some_tag", "", new_tokenizer, ParseContext.new)
|
||||
assert_equal('some_tag', tag.tag_name)
|
||||
end
|
||||
|
||||
@@ -26,7 +26,16 @@ class TagUnitTest < Minitest::Test
|
||||
end
|
||||
|
||||
def test_tag_render_to_output_buffer_nil_value
|
||||
custom_tag = CustomTag.parse("some_tag", "", Tokenizer.new(""), ParseContext.new)
|
||||
custom_tag = CustomTag.parse("some_tag", "", new_tokenizer, ParseContext.new)
|
||||
assert_equal('some string', custom_tag.render_to_output_buffer(Context.new, "some string"))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def new_tokenizer
|
||||
Tokenizer.new(
|
||||
source: "",
|
||||
string_scanner: StringScanner.new(""),
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -20,25 +20,6 @@ class TemplateUnitTest < Minitest::Test
|
||||
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)
|
||||
Liquid.cache_classes = original_cache_setting
|
||||
end
|
||||
|
||||
class FakeTag; end
|
||||
|
||||
def test_tags_can_be_looped_over
|
||||
|
||||
@@ -6,6 +6,7 @@ class TokenizerTest < Minitest::Test
|
||||
def test_tokenize_strings
|
||||
assert_equal([' '], tokenize(' '))
|
||||
assert_equal(['hello world'], tokenize('hello world'))
|
||||
assert_equal(['{}'], tokenize('{}'))
|
||||
end
|
||||
|
||||
def test_tokenize_variables
|
||||
@@ -30,6 +31,23 @@ class TokenizerTest < Minitest::Test
|
||||
assert_equal([1, 1, 3], tokenize_line_numbers(" {{\n funk \n}} "))
|
||||
end
|
||||
|
||||
def test_tokenize_with_nil_source_returns_empty_array
|
||||
assert_equal([], tokenize(nil))
|
||||
end
|
||||
|
||||
def test_incomplete_curly_braces
|
||||
assert_equal(["{{.}", " "], tokenize('{{.} '))
|
||||
assert_equal(["{{}", "%}"], tokenize('{{}%}'))
|
||||
assert_equal(["{{}}", "}"], tokenize('{{}}}'))
|
||||
end
|
||||
|
||||
def test_unmatching_start_and_end
|
||||
assert_equal(["{{%}"], tokenize('{{%}'))
|
||||
assert_equal(["{{%%%}}"], tokenize('{{%%%}}'))
|
||||
assert_equal(["{%", "}}"], tokenize('{%}}'))
|
||||
assert_equal(["{%%}", "}"], tokenize('{%%}}'))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def new_tokenizer(source, parse_context: Liquid::ParseContext.new, start_line_number: nil)
|
||||
|
||||
Reference in New Issue
Block a user