Compare commits

..
Author SHA1 Message Date
Michael Go fe2ba1d8da don't create string scanner for liquid tag 2024-11-01 17:31:05 -03:00
Michael Go fa51b8be40 mini opt 2024-11-01 17:30:18 -03:00
Michael Go f77075895a appease rubocop 2024-11-01 17:24:09 -03:00
Michael Go 579b899e24 mini opt 2024-11-01 17:19:50 -03:00
Michael Go 7ac53a3639 use String#split for liquid tag tokenization 2024-11-01 17:11:10 -03:00
Michael Go f49ceed826 mini opt 2024-11-01 17:06:28 -03:00
Michael Go d2b68b81a7 undo mini opt 2024-11-01 17:02:09 -03:00
Michael Go 7d9ffdce03 mini opt 2024-11-01 16:36:25 -03:00
Michael Go 2e4d02f4d1 mini opt 2024-11-01 16:22:01 -03:00
Ian Ker-SeymerandGitHub a0c64e4cbd Update strscan comment 2024-10-31 16:57:52 -04:00
Michael Go b4907f6323 pre-calculate tokens 2024-10-31 15:46:48 -03:00
Michael Go 78ef372878 backward compatible tokenizer 2024-10-30 14:31:55 -03:00
Michael Go 2a829b889e add more quirky lexer parsing unit test 2024-10-30 13:54:01 -03:00
Michael Go 15d117926d more less strict strscan version requirement 2024-10-30 13:54:01 -03:00
Michael Go 936f73dc30 refactor tokenizer to simulate original regex properly 2024-10-30 13:54:01 -03:00
Michael Go fecbc62533 remove unnecessary comparison jump table entries 2024-10-30 13:54:01 -03:00
Michael Go b7b6400985 fix typo 2024-10-30 13:54:01 -03:00
Ian Ker-SeymerandMichael Go be8e329cef more opt 2024-10-30 13:54:01 -03:00
Ian Ker-SeymerandMichael Go d76663f570 more opt 2024-10-30 13:54:01 -03:00
Ian Ker-SeymerandMichael Go f42c6b0608 more opt 2024-10-30 13:54:01 -03:00
Ian Ker-SeymerandMichael Go aa45356133 Avoid method dispatch 2024-10-30 13:54:01 -03:00
Michael Go a406603e9f more micro optimization 2024-10-30 13:54:01 -03:00
Michael Go 54975ddac1 more micro optimization 2024-10-30 13:54:01 -03:00
Michael Go 91e1563e1c more micro optimization 2024-10-30 13:54:01 -03:00
Michael Go 0346337c05 more micro optimization 2024-10-30 13:54:01 -03:00
Michael Go 649b64ffd4 more micro optimization 2024-10-30 13:54:01 -03:00
Michael Go 48d6a03de6 more micro optimization 2024-10-30 13:54:01 -03:00
Michael Go 9e0c5f4747 more micro optimization 2024-10-30 13:54:00 -03:00
Michael Go e06ac1bc9c more micro optimization 2024-10-30 13:54:00 -03:00
Michael Go c2baa4c270 more micro optimization 2024-10-30 13:54:00 -03:00
Michael Go b81d1c8c5d more micro optimization 2024-10-30 13:54:00 -03:00
Michael Go e3c2fd47b2 more micro optimization 2024-10-30 13:54:00 -03:00
Michael Go 1b3927ee1d more micro optimization 2024-10-30 13:54:00 -03:00
Michael Go 6ad7508ce2 more micro optimization 2024-10-30 13:54:00 -03:00
Michael Go ddb969af8b more micro optimization 2024-10-30 13:54:00 -03:00
Michael Go 128935dcef micro optimization 2024-10-30 13:54:00 -03:00
Michael Go b545e3ae6a lazy tokenizer 2024-10-30 13:54:00 -03:00
Michael Go 59699075a2 refactor lexer unit test 2024-10-30 13:53:57 -03:00
Michael Go 12b5a35e26 fix parsing quirky incomplete expressions 2024-10-30 13:53:34 -03:00
Michael GoandGitHub 8e40f8050a Merge pull request #1838 from Shopify/quirky-lexer-parsing
fix parsing quirky incomplete expressions
2024-10-30 13:52:47 -03:00
Michael Go ffce6de8bb avoid using StringScanner eos 2024-10-30 13:45:10 -03:00
Michael Go f00670cb01 refactor lexer unit test 2024-10-30 13:44:36 -03:00
Michael Go f6a3e25e2e fix parsing quirky incomplete expressions 2024-10-30 13:44:35 -03:00
Michael GoandGitHub f6ffc37cf2 Merge pull request #1840 from Shopify/fix-lexer-contains-as-id
fix lexer parsing ID 'contains' as comparison
2024-10-30 13:43:48 -03:00
Michael Go 1375a9e4dc fix lexer parsing ID 'contains' as comparison 2024-10-30 13:39:55 -03:00
Michael GoandGitHub c626dfa1a1 Merge pull request #1839 from Shopify/lexer-parse-error-with-utf8
raise syntax error from lexer parser with UTF-8 character
2024-10-30 13:39:05 -03:00
Michael Go 8a9f33a060 raise syntax error from lexer parser with utf8 character 2024-10-29 22:04:37 -03:00
Michael GoandGitHub 1943441361 Merge pull request #1835 from Shopify/fix-multibyte-variable-parsing
fix parsing Variable blockbody with multibyte character
2024-10-28 19:31:21 -03:00
Michael GoandGitHub 36251e640c Merge pull request #1837 from Shopify/lexer-comparison-fix
fix lexer parsing comparison without whitespaces
2024-10-28 19:31:11 -03:00
Michael Go d94293a464 fix lexer parsing comparison without whitespaces 2024-10-28 19:30:12 -03:00
Michael Go 6c13805a60 fix parsing Variable blockbody with multibyte character 2024-10-28 17:33:55 -03:00
Michael GoandGitHub b4196489c2 Merge pull request #1833 from Shopify/fast-variable-parse
Faster Variable BlockBody Matching
2024-10-28 15:28:04 -03:00
Gray GilmoreandGitHub 6d58c41440 Merge pull request #1831 from Shopify/gg-add-named-params-docs
Update liquid docs for named parameters
2024-10-28 09:20:55 -07:00
Michael Go fb6ac72520 use byteslice to create Variable BlockBody 2024-10-25 15:41:35 -03:00
Michael Go cb16219552 faster BlockBody variable matching 2024-10-25 15:22:56 -03:00
Gray Gilmore 8d7ed706f4 Update liquid docs for named parameters
The YARD liquid gem now supports specifying named parameters. For the
core liquid tags and filters this is the only object I could find that
needed to be updated.
2024-10-24 09:56:50 -07:00
14 changed files with 332 additions and 125 deletions
+2
View File
@@ -28,3 +28,5 @@ group :test do
gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'main'
end
end
gem "strscan", ">= 3.1"
+4 -1
View File
@@ -73,7 +73,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,6 +82,9 @@ 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|
+9 -2
View File
@@ -246,10 +246,17 @@ module Liquid
end
def create_variable(token, parse_context)
if token =~ ContentOfVariable
markup = Regexp.last_match(1)
if token.end_with?("}}")
i = 2
i = 3 if token[i] == "-"
parse_end = token.length - 3
parse_end -= 1 if token[parse_end] == "-"
markup_end = parse_end - i + 1
markup = markup_end <= 0 ? "" : token.slice(i, markup_end)
return Variable.new(markup, parse_context)
end
BlockBody.raise_missing_variable_terminator(token, parse_context)
end
+33 -10
View File
@@ -73,7 +73,6 @@ module Liquid
COMPARISON_LESS_THAN = [:comparison, "<"].freeze
COMPARISON_LESS_THAN_OR_EQUAL = [:comparison, "<="].freeze
COMPARISON_NOT_EQUAL_ALT = [:comparison, "<>"].freeze
CONTAINS = /contains(?=\s)/
DASH = [:dash, "-"].freeze
DOT = [:dot, "."].freeze
DOTDOT = [:dotdot, ".."].freeze
@@ -90,7 +89,13 @@ module Liquid
SINGLE_STRING_LITERAL = /'[^\']*'/
WHITESPACE_OR_NOTHING = /\s*/
COMPARISON_JUMP_TABLE = [].tap do |table|
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|
table["=".ord] = [].tap do |sub_table|
sub_table["=".ord] = COMPARISON_EQUAL
sub_table.freeze
@@ -99,15 +104,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
@@ -163,6 +170,7 @@ module Liquid
break if @ss.eos?
start_pos = @ss.pos
peeked = @ss.peek_byte
if (special = SPECIAL_TABLE[peeked])
@@ -173,7 +181,7 @@ module Liquid
@output << DOTDOT
elsif special == DASH
# Special case for negative numbers
if NUMBER_TABLE[@ss.peek_byte]
if (peeked_byte = @ss.peek_byte) && NUMBER_TABLE[peeked_byte]
@ss.pos -= 1
@output << [:number, @ss.scan(NUMBER_LITERAL)]
else
@@ -182,26 +190,34 @@ module Liquid
else
@output << special
end
elsif (sub_table = COMPARISON_JUMP_TABLE[peeked])
elsif (sub_table = TWO_CHARS_COMPARISON_JUMP_TABLE[peeked])
@ss.scan_byte
if (found = sub_table[@ss.peek_byte])
if (peeked_byte = @ss.peek_byte) && (found = sub_table[peeked_byte])
@output << found
@ss.scan_byte
else
raise SyntaxError, "Unexpected character #{peeked.chr}"
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]
if type && (t = @ss.scan(pattern))
# Special case for "contains"
@output << if type == :id && t == "contains"
@output << if type == :id && t == "contains" && @output.last&.first != :dot
COMPARISON_CONTAINS
else
[type, t]
end
else
raise SyntaxError, "Unexpected character #{peeked.chr}"
raise_syntax_error(start_pos)
end
end
end
@@ -209,7 +225,14 @@ module Liquid
@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}"
end
end
# Remove this once we can depend on strscan >= 3.1.1
Lexer = StringScanner.instance_methods.include?(:scan_byte) ? Lexer2 : Lexer1
end
+1 -1
View File
@@ -877,7 +877,7 @@ module Liquid
# - [`nil`](/docs/api/liquid/basics#nil)
# @liquid_syntax variable | default: variable
# @liquid_return [untyped]
# @liquid_optional_param allow_false [boolean] Whether to use false values instead of the default.
# @liquid_optional_param allow_false: [boolean] Whether to use false values instead of the default.
def default(input, default_value = '', options = {})
options = {} unless options.is_a?(Hash)
false_check = options['allow_false'] ? input.nil? : !Liquid::Utils.to_liquid_value(input)
+126 -82
View File
@@ -3,38 +3,23 @@
require "strscan"
module Liquid
class Tokenizer
class Tokenizer1
attr_reader :line_number, :for_liquid_tag
TAG_END = /%\}/
TAG_OR_VARIABLE_START = /\{[\{\%}]/
NEWLINE = /\n/
OPEN_CURLEY = 123
CLOSE_CURLEY = 125
PERCENTAGE = 37
def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false)
@line_number = line_number || (line_numbers ? 1 : nil)
@source = source
@line_number = line_number || (line_numbers ? 1 : nil)
@for_liquid_tag = for_liquid_tag
@tokens = []
tokenize(source)
end
private def tokenize(source)
ss = StringScanner.new(source)
while token = t_shift(ss)
@tokens.push(token)
end
@offset = 0
@tokens = tokenize
end
def shift
token = @tokens.shift
token = @tokens[@offset]
return nil unless token
@offset += 1
if @line_number
@line_number += @for_liquid_tag ? 1 : token.count("\n")
end
@@ -42,105 +27,164 @@ module Liquid
token
end
private def t_shift(ss)
return nil if ss.eos?
private
@for_liquid_tag ? next_liquid_token(ss) : next_token(ss)
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
end
tokens
end
end
class Tokenizer2
attr_reader :line_number, :for_liquid_tag
TAG_END = /%\}/
TAG_OR_VARIABLE_START = /\{[\{\%]/
NEWLINE = /\n/
OPEN_CURLEY = "{".ord
CLOSE_CURLEY = "}".ord
PERCENTAGE = "%".ord
def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false)
@line_number = line_number || (line_numbers ? 1 : nil)
@for_liquid_tag = for_liquid_tag
@source = source
@offset = 0
@tokens = []
tokenize
end
def shift
token = @tokens[@offset]
return unless token
@offset += 1
if @line_number
@line_number += @for_liquid_tag ? 1 : token.count("\n")
end
token
end
private
def next_liquid_token(ss)
# read until we find a \n
start = ss.pos
if ss.scan_until(NEWLINE).nil?
token = ss.rest
ss.terminate
return token
def tokenize
if @for_liquid_tag
@tokens = @source.split("\n")
else
@ss = StringScanner.new(@source)
@tokens << shift_normal until @ss.eos?
end
ss.string.byteslice(start, ss.pos - start - 1)
@ss = nil
@source = nil
end
def next_token(ss)
if ss.pos == 0
if ss.string.start_with?("{{")
ss.pos = 2
return next_variable_token(ss)
elsif ss.string.start_with?("{%")
ss.pos = 2
return next_tag_token(ss)
else
return next_text_token(ss)
end
end
def shift_normal
token = next_token
return unless token
token
end
def next_token
# possible states: :text, :tag, :variable
byte_a = ss.scan_byte
byte_a = @ss.peek_byte
if byte_a == OPEN_CURLEY
byte_b = ss.scan_byte
@ss.scan_byte
byte_b = @ss.peek_byte
if byte_b == PERCENTAGE
return next_tag_token(ss)
@ss.scan_byte
return next_tag_token
elsif byte_b == OPEN_CURLEY
return next_variable_token(ss)
@ss.scan_byte
return next_variable_token
end
ss.pos -= 1
@ss.pos -= 1
end
ss.pos -= 1
next_text_token(ss)
next_text_token
end
def next_text_token(ss)
start = ss.pos
def next_text_token
start = @ss.pos
unless ss.skip_until(TAG_OR_VARIABLE_START)
token = ss.rest
ss.terminate
unless @ss.skip_until(TAG_OR_VARIABLE_START)
token = @ss.rest
@ss.terminate
return token
end
ss.pos -= 2
ss.string.byteslice(start, ss.pos - start)
pos = @ss.pos -= 2
@source.byteslice(start, pos - start)
end
def next_variable_token(ss)
start = ss.pos - 2
def next_variable_token
start = @ss.pos - 2
# it is possible to see a {% before a }} so we need to check for that
byte_a = ss.scan_byte
byte_a = byte_b = @ss.scan_byte
until ss.eos?
while ss.eos? == false && byte_a != CLOSE_CURLEY && byte_a != OPEN_CURLEY
byte_a = 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
break if ss.eos?
byte_b = @ss.scan_byte
byte_b = ss.scan_byte
if byte_b != CLOSE_CURLEY && byte_b != PERCENTAGE
byte_a = byte_b
next
elsif byte_a == CLOSE_CURLEY && byte_b == CLOSE_CURLEY
return ss.string.byteslice(start, ss.pos - start)
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(ss, start)
return next_tag_token_with_start(start)
end
byte_a = byte_b
end
return "{{"
"{{"
end
def next_tag_token(ss, start = nil)
start ||= ss.pos - 2
def next_tag_token
start = @ss.pos - 2
if (len = @ss.skip_until(TAG_END))
@source.byteslice(start, len + 2)
else
"{%"
end
end
ss.scan_until(TAG_END)
ss.string.byteslice(start, ss.pos - start)
def next_tag_token_with_start(start)
@ss.skip_until(TAG_END)
@source.byteslice(start, @ss.pos - start)
end
end
# Remove this once we can depend on strscan >= 3.1.1
Tokenizer = StringScanner.instance_methods.include?(:scan_byte) ? Tokenizer2 : Tokenizer1
end
+8 -5
View File
@@ -8,14 +8,17 @@ Liquid::Template.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
+8
View File
@@ -48,6 +48,14 @@ class ThemeRunner
end
end
# `tokenize` will just test the tokenizen portion of liquid without any templates
def tokenize
@tests.each do |test_hash|
tokenizer = Liquid::Tokenizer.new(test_hash[:liquid], 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|
+20
View File
@@ -131,4 +131,24 @@ class ParsingQuirksTest < Minitest::Test
def test_contains_in_id
assert_template_result(' YES ', '{% if containsallshipments == true %} YES {% endif %}', { 'containsallshipments' => true })
end
def test_incomplete_expression
with_error_mode(:lax) do
assert_template_result("false", "{{ false - }}")
assert_template_result("false", "{{ false > }}")
assert_template_result("false", "{{ false < }}")
assert_template_result("false", "{{ false = }}")
assert_template_result("false", "{{ false ! }}")
assert_template_result("false", "{{ false 1 }}")
assert_template_result("false", "{{ false a }}")
assert_template_result("false", "{% liquid assign foo = false -\n%}{{ foo }}")
assert_template_result("false", "{% liquid assign foo = false >\n%}{{ foo }}")
assert_template_result("false", "{% liquid assign foo = false <\n%}{{ foo }}")
assert_template_result("false", "{% liquid assign foo = false =\n%}{{ foo }}")
assert_template_result("false", "{% liquid assign foo = false !\n%}{{ foo }}")
assert_template_result("false", "{% liquid assign foo = false 1\n%}{{ foo }}")
assert_template_result("false", "{% liquid assign foo = false a\n%}{{ foo }}")
end
end
end # ParsingQuirksTest
+1
View File
@@ -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
+1 -1
View File
@@ -13,7 +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
Liquid::Environment.default.error_mode = mode
if ENV['LIQUID_C'] == '1'
puts "-- LIQUID C"
+6
View File
@@ -32,6 +32,12 @@ class BlockUnitTest < Minitest::Test
assert_equal(String, template.root.nodelist[2].class)
end
def test_variable_with_multibyte_character
template = Liquid::Template.parse("{{ '❤️' }}")
assert_equal(1, template.root.nodelist.size)
assert_equal(Variable, template.root.nodelist[0].class)
end
def test_variable_many_embedded_fragments
template = Liquid::Template.parse(" {{funk}} {{so}} {{brother}} ")
assert_equal(7, template.root.nodelist.size)
+99 -23
View File
@@ -6,58 +6,134 @@ class LexerUnitTest < Minitest::Test
include Liquid
def test_strings
tokens = Lexer.new(%( 'this is a test""' "wat 'lol'")).tokenize
assert_equal([[:string, %('this is a test""')], [:string, %("wat 'lol'")], [:end_of_string]], tokens)
assert_equal(
[[:string, %('this is a test""')], [:string, %("wat 'lol'")], [:end_of_string]],
tokenize(%( 'this is a test""' "wat 'lol'")),
)
end
def test_integer
tokens = Lexer.new('hi 50').tokenize
assert_equal([[:id, 'hi'], [:number, '50'], [:end_of_string]], tokens)
assert_equal(
[[:id, 'hi'], [:number, '50'], [:end_of_string]],
tokenize('hi 50'),
)
end
def test_float
tokens = Lexer.new('hi 5.0').tokenize
assert_equal([[:id, 'hi'], [:number, '5.0'], [:end_of_string]], tokens)
assert_equal(
[[:id, 'hi'], [:number, '5.0'], [:end_of_string]],
tokenize('hi 5.0'),
)
end
def test_comparison
tokens = Lexer.new('== <> contains ').tokenize
assert_equal([[:comparison, '=='], [:comparison, '<>'], [:comparison, 'contains'], [:end_of_string]], tokens)
assert_equal(
[[:comparison, '=='], [:comparison, '<>'], [:comparison, 'contains'], [:end_of_string]],
tokenize('== <> contains '),
)
end
def test_comparison_without_whitespace
assert_equal(
[[:number, '1'], [:comparison, '>'], [:number, '0'], [:end_of_string]],
tokenize('1>0'),
)
end
def test_comparison_with_negative_number
assert_equal(
[[:number, '1'], [:comparison, '>'], [:number, '-1'], [:end_of_string]],
tokenize('1>-1'),
)
end
def test_raise_for_invalid_comparison
assert_raises(SyntaxError) do
tokenize('1>!1')
end
assert_raises(SyntaxError) do
tokenize('1=<1')
end
assert_raises(SyntaxError) do
tokenize('1!!1')
end
end
def test_specials
tokens = Lexer.new('| .:').tokenize
assert_equal([[:pipe, '|'], [:dot, '.'], [:colon, ':'], [:end_of_string]], tokens)
tokens = Lexer.new('[,]').tokenize
assert_equal([[:open_square, '['], [:comma, ','], [:close_square, ']'], [:end_of_string]], tokens)
assert_equal(
[[:pipe, '|'], [:dot, '.'], [:colon, ':'], [:end_of_string]],
tokenize('| .:'),
)
assert_equal(
[[:open_square, '['], [:comma, ','], [:close_square, ']'], [:end_of_string]],
tokenize('[,]'),
)
end
def test_fancy_identifiers
tokens = Lexer.new('hi five?').tokenize
assert_equal([[:id, 'hi'], [:id, 'five?'], [:end_of_string]], tokens)
assert_equal([[:id, 'hi'], [:id, 'five?'], [:end_of_string]], tokenize('hi five?'))
tokens = Lexer.new('2foo').tokenize
assert_equal([[:number, '2'], [:id, 'foo'], [:end_of_string]], tokens)
assert_equal([[:number, '2'], [:id, 'foo'], [:end_of_string]], tokenize('2foo'))
end
def test_whitespace
tokens = Lexer.new("five|\n\t ==").tokenize
assert_equal([[:id, 'five'], [:pipe, '|'], [:comparison, '=='], [:end_of_string]], tokens)
assert_equal(
[[:id, 'five'], [:pipe, '|'], [:comparison, '=='], [:end_of_string]],
tokenize("five|\n\t =="),
)
end
def test_unexpected_character
assert_raises(SyntaxError) do
Lexer.new("%").tokenize
tokenize("%")
end
end
def test_negative_numbers
tokens = Lexer.new("foo | default: -1").tokenize
assert_equal([[:id, 'foo'], [:pipe, '|'], [:id, 'default'], [:colon, ":"], [:number, '-1'], [:end_of_string]], tokens)
assert_equal(
[[:id, 'foo'], [:pipe, '|'], [:id, 'default'], [:colon, ":"], [:number, '-1'], [:end_of_string]],
tokenize("foo | default: -1"),
)
end
def test_greater_than_two_digits
tokens = Lexer.new("foo > 12").tokenize
assert_equal([[:id, 'foo'], [:comparison, '>'], [:number, '12'], [:end_of_string]], tokens)
assert_equal(
[[:id, 'foo'], [:comparison, '>'], [:number, '12'], [:end_of_string]],
tokenize("foo > 12"),
)
end
def test_error_with_utf8_character
error = assert_raises(SyntaxError) do
tokenize("1 < 1Ø")
end
assert_equal(
'Liquid syntax error: Unexpected character Ø',
error.message,
)
end
def test_contains_as_attribute_name
assert_equal(
[[:id, "a"], [:dot, "."], [:id, "contains"], [:dot, "."], [:id, "b"], [:end_of_string]],
tokenize("a.contains.b"),
)
end
def test_tokenize_incomplete_expression
assert_equal([[:id, "false"], [:dash, "-"], [:end_of_string]], tokenize("false -"))
assert_equal([[:id, "false"], [:comparison, "<"], [:end_of_string]], tokenize("false <"))
assert_equal([[:id, "false"], [:comparison, ">"], [:end_of_string]], tokenize("false >"))
assert_equal([[:id, "false"], [:number, "1"], [:end_of_string]], tokenize("false 1"))
end
private
def tokenize(input)
Lexer.new(input).tokenize
end
end
+14
View File
@@ -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,19 @@ class TokenizerTest < Minitest::Test
assert_equal([1, 1, 3], tokenize_line_numbers(" {{\n funk \n}} "))
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)