Skip to content
Draft

WIP2 #2020

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions History.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

## 6.0.0

### Architectural changes

### Features
* (TODO) Add support for boolean expressions everywhere
* As variable output `{{ a or b }}`
Expand All @@ -21,16 +19,28 @@
- (TODO) Add support for parenthesized expressions
* e.g. `(a or b) and c`

### Architectural changes
* `parse_expression` and `safe_parse_expression` have been removed from `Tag` and `ParseContext`
* `Parser` methods now produce AST nodes instead of strings
* `Parser#expression` produces a value,
* `Parser#string` produces a string,
* etc.

### Breaking changes
* We are removing the Environment's `error_mode` option.
* The Environment's `error_mode` option has been removed.
* `:warn` is no longer supported
* `:lax` and `lax_parse` is no longer supported
* `:strict` and `strict_parse` is no longer supported
* `strict2_parse` is renamed to `parse_markup`
* The `warnings` system has been removed.
* `Parser#expression` is renamed to `Parser#expression_string`
* `safe_parse_expression` methods are replaced by `Parser#expression`
* `parse_expression` methods are replaced by `Parser#unsafe_parse_expression`

### Migrating from `^5.11.0`
- In custom tags that include `ParserSwitching`, rename `strict2_parse` to `parse_markup`
- Remove code depending on `:error_mode`
- Replace `safe_parse_expression` calls with `Parser#expression`

## 5.11.0
* Revert the Inline Snippets tag (#2001), treat its inclusion in the latest Liquid release as a bug, and allow for feedback on RFC#1916 to better support Liquid developers [Guilherme Carreiro]
Expand Down
5 changes: 3 additions & 2 deletions lib/liquid/condition.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,9 @@ def self.operators
@@operators
end

def self.parse_expression(parse_context, markup, safe: false)
@@method_literals[markup] || parse_context.parse_expression(markup, safe: safe)
def self.parse_expression(parser)
markup = parser.expression_string
@@method_literals[markup] || parser.unsafe_parse_expression(markup)
end

attr_reader :attachment, :child_condition
Expand Down
10 changes: 3 additions & 7 deletions lib/liquid/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,6 @@ def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_erro
end
# rubocop:enable Metrics/ParameterLists

def warnings
@warnings ||= []
end

def strainer
@strainer ||= @environment.create_strainer(self, @filters)
end
Expand Down Expand Up @@ -157,7 +153,6 @@ def new_isolated_subcontext
subcontext.filters = @filters
subcontext.strainer = nil
subcontext.errors = errors
subcontext.warnings = warnings
subcontext.disabled_tags = @disabled_tags
end
end
Expand All @@ -180,7 +175,8 @@ def []=(key, value)
# Example:
# products == empty #=> products.empty?
def [](expression)
evaluate(Expression.parse(expression, @string_scanner))
@string_scanner.string = expression
evaluate(Parser.new(@string_scanner).expression)
end

def key?(key)
Expand Down Expand Up @@ -244,7 +240,7 @@ def tag_disabled?(tag_name)

protected

attr_writer :base_scope_depth, :warnings, :errors, :strainer, :filters, :disabled_tags
attr_writer :base_scope_depth, :errors, :strainer, :filters, :disabled_tags

private

Expand Down
71 changes: 15 additions & 56 deletions lib/liquid/expression.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,6 @@ class Expression
FLOAT_REGEX = /\A(-?\d+)\.\d+\z/

class << self
def safe_parse(parser, ss = StringScanner.new(""), cache = nil)
parse(parser.expression, ss, cache)
end

def parse(markup, ss = StringScanner.new(""), cache = nil)
return unless markup

Expand All @@ -52,72 +48,35 @@ def parse(markup, ss = StringScanner.new(""), cache = nil)
end

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,
if (markup.start_with?("(") && markup.end_with?(")")) && markup =~ RANGES_REGEX
start_markup = Regexp.last_match(1)
end_markup = Regexp.last_match(2)
start_obj = parse(start_markup, ss, cache)
end_obj = parse(end_markup, ss, cache)
return RangeLookup.create(
start_obj,
end_obj,
start_markup,
end_markup,
)
end

if (num = parse_number(markup, ss))
if (num = parse_number(markup))
num
else
VariableLookup.parse(markup, ss, cache)
end
end

def parse_number(markup, ss)
def parse_number(markup)
# check if the markup is simple integer or float
case markup
when INTEGER_REGEX
return Integer(markup, 10)
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
markup.to_f
else
# number ends with a dot "123."
markup.byteslice(0, first_dot_pos).to_f
false
end
end
end
Expand Down
22 changes: 3 additions & 19 deletions lib/liquid/parse_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@
module Liquid
class ParseContext
attr_accessor :locale, :line_number, :trim_whitespace, :depth
attr_reader :partial, :warnings, :environment
attr_reader :partial, :environment

def initialize(options = Const::EMPTY_HASH)
@environment = options.fetch(:environment, Environment.default)
@template_options = options ? options.dup : {}

@locale = @template_options[:locale] ||= I18n.new
@warnings = []
@locale = @template_options[:locale] ||= I18n.new

# constructing new StringScanner in Lexer, Tokenizer, etc is expensive
# This StringScanner will be shared by all of them
Expand Down Expand Up @@ -38,7 +37,7 @@ def new_block_body

def new_parser(input)
@string_scanner.string = input
Parser.new(@string_scanner)
Parser.new(@string_scanner, @expression_cache)
end

def new_tokenizer(source, start_line_number: nil, for_liquid_tag: false)
Expand All @@ -50,21 +49,6 @@ def new_tokenizer(source, start_line_number: nil, for_liquid_tag: false)
)
end

def safe_parse_expression(parser)
Expression.safe_parse(parser, @string_scanner, @expression_cache)
end

def parse_expression(markup, safe: false)
# markup MUST come from a string returned by the parser
# (e.g., parser.expression). We're not calling the parser here to
# prevent redundant parser overhead. The `safe` opt-in
# exists to ensure it is not accidentally still called with
# the result of a regex.
raise Liquid::InternalError, "unsafe parse_expression cannot be used" unless safe

Expression.parse(markup, @string_scanner, @expression_cache)
end

def partial=(value)
@partial = value
@options = value ? partial_options : @template_options
Expand Down
Loading
Loading