mirror of
https://github.com/Shopify/liquid.git
synced 2026-09-16 09:20:42 -07:00
Measures:
1) A while loop is faster than iterating with #each.
2) Check string, variable and block tokens first. They are far more
frequent than interrupt tokens. In their case, checking for an
interrupt can be avoided.
3) String tokens just map to themselves and don't need the special
treatment of BlockBody#render_node (except the resource limit
check).
Benchmark
=========
$ bundle exec rake benchmark:run
Before
------
Run 1)
parse: 41.630 (± 0.0%) i/s - 420.000 in 10.089309s
render: 75.962 (± 3.9%) i/s - 763.000 in 10.066823s
parse & render: 25.497 (± 0.0%) i/s - 256.000 in 10.040862s
Run 2)
parse: 42.130 (± 0.0%) i/s - 424.000 in 10.064738s
render: 77.003 (± 1.3%) i/s - 777.000 in 10.093524s
parse & render: 25.739 (± 0.0%) i/s - 258.000 in 10.024581s
Run 3)
parse: 41.976 (± 2.4%) i/s - 420.000 in 10.021406s
render: 76.184 (± 1.3%) i/s - 763.000 in 10.018104s
parse & render: 25.641 (± 0.0%) i/s - 258.000 in 10.062549s
After
-----
Run 1)
parse: 42.283 (± 0.0%) i/s - 424.000 in 10.028306s
render: 83.158 (± 2.4%) i/s - 832.000 in 10.009201s
parse & render: 26.417 (± 0.0%) i/s - 266.000 in 10.069718s
Run 2)
parse: 41.159 (± 4.9%) i/s - 412.000 in 10.031297s
render: 81.591 (± 3.7%) i/s - 816.000 in 10.018225s
parse & render: 25.924 (± 3.9%) i/s - 260.000 in 10.035653s
Run 3)
parse: 42.418 (± 2.4%) i/s - 424.000 in 10.003100s
render: 84.183 (± 2.4%) i/s - 847.000 in 10.069781s
parse & render: 26.726 (± 0.0%) i/s - 268.000 in 10.029857s
143 lines
4.6 KiB
Ruby
143 lines
4.6 KiB
Ruby
module Liquid
|
|
class BlockBody
|
|
FullToken = /\A#{TagStart}#{WhitespaceControl}?\s*(\w+)\s*(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
|
|
ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
|
|
WhitespaceOrNothing = /\A\s*\z/
|
|
TAGSTART = "{%".freeze
|
|
VARSTART = "{{".freeze
|
|
|
|
attr_reader :nodelist
|
|
|
|
def initialize
|
|
@nodelist = []
|
|
@blank = true
|
|
end
|
|
|
|
def parse(tokenizer, parse_context)
|
|
parse_context.line_number = tokenizer.line_number
|
|
while token = tokenizer.shift
|
|
next if token.empty?
|
|
case
|
|
when token.start_with?(TAGSTART)
|
|
whitespace_handler(token, parse_context)
|
|
unless token =~ FullToken
|
|
raise_missing_tag_terminator(token, parse_context)
|
|
end
|
|
tag_name = $1
|
|
markup = $2
|
|
# fetch the tag from registered blocks
|
|
unless tag = registered_tags[tag_name]
|
|
# end parsing if we reach an unknown tag and let the caller decide
|
|
# determine how to proceed
|
|
return yield tag_name, markup
|
|
end
|
|
new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
|
|
@blank &&= new_tag.blank?
|
|
@nodelist << new_tag
|
|
when token.start_with?(VARSTART)
|
|
whitespace_handler(token, parse_context)
|
|
@nodelist << create_variable(token, parse_context)
|
|
@blank = false
|
|
else
|
|
if parse_context.trim_whitespace
|
|
token.lstrip!
|
|
end
|
|
parse_context.trim_whitespace = false
|
|
@nodelist << token
|
|
@blank &&= !!(token =~ WhitespaceOrNothing)
|
|
end
|
|
parse_context.line_number = tokenizer.line_number
|
|
end
|
|
|
|
yield nil, nil
|
|
end
|
|
|
|
def whitespace_handler(token, parse_context)
|
|
if token[2] == WhitespaceControl
|
|
previous_token = @nodelist.last
|
|
if previous_token.is_a? String
|
|
previous_token.rstrip!
|
|
end
|
|
end
|
|
parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
|
|
end
|
|
|
|
def blank?
|
|
@blank
|
|
end
|
|
|
|
def render(context)
|
|
output = []
|
|
context.resource_limits.render_score += @nodelist.length
|
|
|
|
idx = 0
|
|
while node = @nodelist[idx]
|
|
case node
|
|
when String
|
|
check_resources(context, node)
|
|
output << node
|
|
when Variable
|
|
render_node_to_output(node, output, context)
|
|
when Block
|
|
render_node_to_output(node, output, context, node.blank?)
|
|
break if context.interrupt? # might have happened in a for-block
|
|
when Continue, Break
|
|
# If we get an Interrupt that means the block must stop processing. An
|
|
# Interrupt is any command that stops block execution such as {% break %}
|
|
# or {% continue %}
|
|
context.push_interrupt(node.interrupt)
|
|
break
|
|
else # Other non-Block tags
|
|
render_node_to_output(node, output, context)
|
|
end
|
|
idx += 1
|
|
end
|
|
|
|
output.join
|
|
end
|
|
|
|
private
|
|
|
|
def render_node_to_output(node, output, context, skip_output = false)
|
|
node_output = node.render(context)
|
|
node_output = node_output.is_a?(Array) ? node_output.join : node_output.to_s
|
|
check_resources(context, node_output)
|
|
output << node_output unless skip_output
|
|
rescue MemoryError => e
|
|
raise e
|
|
rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
|
|
context.handle_error(e, node.line_number)
|
|
output << nil
|
|
rescue ::StandardError => e
|
|
line_number = node.is_a?(String) ? nil : node.line_number
|
|
output << context.handle_error(e, line_number)
|
|
end
|
|
|
|
def check_resources(context, node_output)
|
|
context.resource_limits.render_length += node_output.length
|
|
return unless context.resource_limits.reached?
|
|
raise MemoryError.new("Memory limits exceeded".freeze)
|
|
end
|
|
|
|
def create_variable(token, parse_context)
|
|
token.scan(ContentOfVariable) do |content|
|
|
markup = content.first
|
|
return Variable.new(markup, parse_context)
|
|
end
|
|
raise_missing_variable_terminator(token, parse_context)
|
|
end
|
|
|
|
def raise_missing_tag_terminator(token, parse_context)
|
|
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_termination".freeze, token: token, tag_end: TagEnd.inspect))
|
|
end
|
|
|
|
def raise_missing_variable_terminator(token, parse_context)
|
|
raise SyntaxError.new(parse_context.locale.t("errors.syntax.variable_termination".freeze, token: token, tag_end: VariableEnd.inspect))
|
|
end
|
|
|
|
def registered_tags
|
|
Template.tags
|
|
end
|
|
end
|
|
end
|