mirror of
https://github.com/Shopify/liquid.git
synced 2026-10-02 00:25:12 -07:00
Compare commits
39
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe2ba1d8da | ||
|
|
fa51b8be40 | ||
|
|
f77075895a | ||
|
|
579b899e24 | ||
|
|
7ac53a3639 | ||
|
|
f49ceed826 | ||
|
|
d2b68b81a7 | ||
|
|
7d9ffdce03 | ||
|
|
2e4d02f4d1 | ||
|
|
a0c64e4cbd | ||
|
|
b4907f6323 | ||
|
|
78ef372878 | ||
|
|
2a829b889e | ||
|
|
15d117926d | ||
|
|
936f73dc30 | ||
|
|
fecbc62533 | ||
|
|
b7b6400985 | ||
|
|
be8e329cef | ||
|
|
d76663f570 | ||
|
|
f42c6b0608 | ||
|
|
aa45356133 | ||
|
|
a406603e9f | ||
|
|
54975ddac1 | ||
|
|
91e1563e1c | ||
|
|
0346337c05 | ||
|
|
649b64ffd4 | ||
|
|
48d6a03de6 | ||
|
|
9e0c5f4747 | ||
|
|
e06ac1bc9c | ||
|
|
c2baa4c270 | ||
|
|
b81d1c8c5d | ||
|
|
e3c2fd47b2 | ||
|
|
1b3927ee1d | ||
|
|
6ad7508ce2 | ||
|
|
ddb969af8b | ||
|
|
128935dcef | ||
|
|
b545e3ae6a | ||
|
|
59699075a2 | ||
|
|
12b5a35e26 |
@@ -28,3 +28,5 @@ group :test do
|
|||||||
gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'main'
|
gem 'liquid-c', github: 'Shopify/liquid-c', ref: 'main'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
gem "strscan", ">= 3.1"
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ end
|
|||||||
|
|
||||||
namespace :benchmark do
|
namespace :benchmark do
|
||||||
desc "Run the liquid benchmark with lax parsing"
|
desc "Run the liquid benchmark with lax parsing"
|
||||||
task :run do
|
task :lax do
|
||||||
ruby "./performance/benchmark.rb lax"
|
ruby "./performance/benchmark.rb lax"
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -82,6 +82,9 @@ namespace :benchmark do
|
|||||||
ruby "./performance/benchmark.rb strict"
|
ruby "./performance/benchmark.rb strict"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
desc "Run the liquid benchmark with both lax and strict parsing"
|
||||||
|
task run: [:lax, :strict]
|
||||||
|
|
||||||
desc "Run unit benchmarks"
|
desc "Run unit benchmarks"
|
||||||
task :unit do
|
task :unit do
|
||||||
Dir["./performance/unit/*_benchmark.rb"].each do |file|
|
Dir["./performance/unit/*_benchmark.rb"].each do |file|
|
||||||
|
|||||||
+3
-2
@@ -92,6 +92,7 @@ module Liquid
|
|||||||
SINGLE_COMPARISON_TOKENS = [].tap do |table|
|
SINGLE_COMPARISON_TOKENS = [].tap do |table|
|
||||||
table["<".ord] = COMPARISON_LESS_THAN
|
table["<".ord] = COMPARISON_LESS_THAN
|
||||||
table[">".ord] = COMPARISON_GREATER_THAN
|
table[">".ord] = COMPARISON_GREATER_THAN
|
||||||
|
table.freeze
|
||||||
end
|
end
|
||||||
|
|
||||||
TWO_CHARS_COMPARISON_JUMP_TABLE = [].tap do |table|
|
TWO_CHARS_COMPARISON_JUMP_TABLE = [].tap do |table|
|
||||||
@@ -103,18 +104,17 @@ module Liquid
|
|||||||
sub_table["=".ord] = COMPARISION_NOT_EQUAL
|
sub_table["=".ord] = COMPARISION_NOT_EQUAL
|
||||||
sub_table.freeze
|
sub_table.freeze
|
||||||
end
|
end
|
||||||
|
table.freeze
|
||||||
end
|
end
|
||||||
|
|
||||||
COMPARISON_JUMP_TABLE = [].tap do |table|
|
COMPARISON_JUMP_TABLE = [].tap do |table|
|
||||||
table["<".ord] = [].tap do |sub_table|
|
table["<".ord] = [].tap do |sub_table|
|
||||||
sub_table["=".ord] = COMPARISON_LESS_THAN_OR_EQUAL
|
sub_table["=".ord] = COMPARISON_LESS_THAN_OR_EQUAL
|
||||||
sub_table[">".ord] = COMPARISON_NOT_EQUAL_ALT
|
sub_table[">".ord] = COMPARISON_NOT_EQUAL_ALT
|
||||||
RUBY_WHITESPACE.each { |c| sub_table[c.ord] = COMPARISON_LESS_THAN }
|
|
||||||
sub_table.freeze
|
sub_table.freeze
|
||||||
end
|
end
|
||||||
table[">".ord] = [].tap do |sub_table|
|
table[">".ord] = [].tap do |sub_table|
|
||||||
sub_table["=".ord] = COMPARISON_GREATER_THAN_OR_EQUAL
|
sub_table["=".ord] = COMPARISON_GREATER_THAN_OR_EQUAL
|
||||||
RUBY_WHITESPACE.each { |c| sub_table[c.ord] = COMPARISON_GREATER_THAN }
|
|
||||||
sub_table.freeze
|
sub_table.freeze
|
||||||
end
|
end
|
||||||
table.freeze
|
table.freeze
|
||||||
@@ -233,5 +233,6 @@ module Liquid
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Remove this once we can depend on strscan >= 3.1.1
|
||||||
Lexer = StringScanner.instance_methods.include?(:scan_byte) ? Lexer2 : Lexer1
|
Lexer = StringScanner.instance_methods.include?(:scan_byte) ? Lexer2 : Lexer1
|
||||||
end
|
end
|
||||||
|
|||||||
+146
-1
@@ -1,7 +1,9 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "strscan"
|
||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
class Tokenizer
|
class Tokenizer1
|
||||||
attr_reader :line_number, :for_liquid_tag
|
attr_reader :line_number, :for_liquid_tag
|
||||||
|
|
||||||
def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false)
|
def initialize(source, line_numbers = false, line_number: nil, for_liquid_tag: false)
|
||||||
@@ -42,4 +44,147 @@ module Liquid
|
|||||||
tokens
|
tokens
|
||||||
end
|
end
|
||||||
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 tokenize
|
||||||
|
if @for_liquid_tag
|
||||||
|
@tokens = @source.split("\n")
|
||||||
|
else
|
||||||
|
@ss = StringScanner.new(@source)
|
||||||
|
@tokens << shift_normal until @ss.eos?
|
||||||
|
end
|
||||||
|
|
||||||
|
@ss = nil
|
||||||
|
@source = 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
|
||||||
|
|
||||||
|
# Remove this once we can depend on strscan >= 3.1.1
|
||||||
|
Tokenizer = StringScanner.instance_methods.include?(:scan_byte) ? Tokenizer2 : Tokenizer1
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,14 +8,17 @@ Liquid::Template.error_mode = ARGV.first.to_sym if ARGV.first
|
|||||||
profiler = ThemeRunner.new
|
profiler = ThemeRunner.new
|
||||||
|
|
||||||
Benchmark.ips do |x|
|
Benchmark.ips do |x|
|
||||||
x.time = 10
|
x.time = 20
|
||||||
x.warmup = 5
|
x.warmup = 10
|
||||||
|
|
||||||
puts
|
puts
|
||||||
puts "Running benchmark for #{x.time} seconds (with #{x.warmup} seconds warmup)."
|
puts "Running benchmark for #{x.time} seconds (with #{x.warmup} seconds warmup)."
|
||||||
puts
|
puts
|
||||||
|
|
||||||
x.report("parse:") { profiler.compile }
|
phase = ENV["PHASE"] || "all"
|
||||||
x.report("render:") { profiler.render }
|
|
||||||
x.report("parse & render:") { profiler.run }
|
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
|
end
|
||||||
|
|||||||
@@ -48,6 +48,14 @@ class ThemeRunner
|
|||||||
end
|
end
|
||||||
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
|
# `run` is called to benchmark rendering and compiling at the same time
|
||||||
def run
|
def run
|
||||||
each_test do |liquid, layout, assigns, page_template, template_name|
|
each_test do |liquid, layout, assigns, page_template, template_name|
|
||||||
|
|||||||
@@ -134,6 +134,14 @@ class ParsingQuirksTest < Minitest::Test
|
|||||||
|
|
||||||
def test_incomplete_expression
|
def test_incomplete_expression
|
||||||
with_error_mode(:lax) do
|
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 <\n%}{{ foo }}")
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class RawTagTest < Minitest::Test
|
|||||||
assert_template_result('>{{ test }}<', '> {%- raw -%}{{ test }}{%- endraw -%} <')
|
assert_template_result('>{{ test }}<', '> {%- raw -%}{{ test }}{%- endraw -%} <')
|
||||||
assert_template_result("> inner <", "> {%- raw -%} inner {%- endraw %} <")
|
assert_template_result("> inner <", "> {%- raw -%} inner {%- 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
|
end
|
||||||
|
|
||||||
def test_open_tag_in_raw
|
def test_open_tag_in_raw
|
||||||
|
|||||||
+1
-1
@@ -13,7 +13,7 @@ if (env_mode = ENV['LIQUID_PARSER_MODE'])
|
|||||||
puts "-- #{env_mode.upcase} ERROR MODE"
|
puts "-- #{env_mode.upcase} ERROR MODE"
|
||||||
mode = env_mode.to_sym
|
mode = env_mode.to_sym
|
||||||
end
|
end
|
||||||
Liquid::Template.error_mode = mode
|
Liquid::Environment.default.error_mode = mode
|
||||||
|
|
||||||
if ENV['LIQUID_C'] == '1'
|
if ENV['LIQUID_C'] == '1'
|
||||||
puts "-- LIQUID C"
|
puts "-- LIQUID C"
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ class TokenizerTest < Minitest::Test
|
|||||||
def test_tokenize_strings
|
def test_tokenize_strings
|
||||||
assert_equal([' '], tokenize(' '))
|
assert_equal([' '], tokenize(' '))
|
||||||
assert_equal(['hello world'], tokenize('hello world'))
|
assert_equal(['hello world'], tokenize('hello world'))
|
||||||
|
assert_equal(['{}'], tokenize('{}'))
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_tokenize_variables
|
def test_tokenize_variables
|
||||||
@@ -30,6 +31,19 @@ class TokenizerTest < Minitest::Test
|
|||||||
assert_equal([1, 1, 3], tokenize_line_numbers(" {{\n funk \n}} "))
|
assert_equal([1, 1, 3], tokenize_line_numbers(" {{\n funk \n}} "))
|
||||||
end
|
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
|
private
|
||||||
|
|
||||||
def new_tokenizer(source, parse_context: Liquid::ParseContext.new, start_line_number: nil)
|
def new_tokenizer(source, parse_context: Liquid::ParseContext.new, start_line_number: nil)
|
||||||
|
|||||||
Reference in New Issue
Block a user