Compare commits

...

30 Commits

Author SHA1 Message Date
Dylan Thacker-Smith
6a9a7ae8d2 Use tokenizer-quoted-strings liquid-c branch. 2015-07-13 22:03:16 -04:00
Dylan Thacker-Smith
4a12fee1f5 Allow variable and tag end characters to be quoted. 2015-07-13 13:19:56 -04:00
Justin Li
b42d35ff36 Merge pull request #620 from Shopify/accept-invalid-range-args
Add param to accept invalid input in to_integer
2015-07-09 13:24:28 -04:00
Justin Li
b4e133e26f Fix regression in range lookup 2015-07-09 13:21:46 -04:00
Justin Li
1f9bd1d809 Add param to accept invalid input in to_integer 2015-07-09 13:18:06 -04:00
Justin Li
e88be60818 Merge pull request #618 from Shopify/move-reraise-for-line-number
Move the syntax error rescue for adding error line numbers.
2015-07-09 11:42:41 -04:00
Dylan Thacker-Smith
14416b3c49 Move the syntax error rescue for adding error line numbers. 2015-07-09 11:25:05 -04:00
Dylan Thacker-Smith
bde14a650d Merge pull request #617 from Shopify/rename-options-iv
Rename options instance variable in Variable and Tag.
2015-07-08 20:50:20 -04:00
Dylan Thacker-Smith
c535af021a Rename options instance variable in Variable and Tag. 2015-07-08 19:59:44 -04:00
Dylan Thacker-Smith
9c9345869b Merge pull request #614 from Shopify/remove-token-class
Implement line numbers without the Liquid::Token class.
2015-07-08 19:48:55 -04:00
Dylan Thacker-Smith
73834a7e52 Use reject rather than dup and delete. 2015-07-08 19:27:24 -04:00
Dylan Thacker-Smith
c45310170b Use parse_context or options instead of @options. 2015-07-08 19:21:59 -04:00
Dylan Thacker-Smith
920e1df643 Rescue and re-raise syntax errors in Template#parse to add line numbers.
This can be done now that the parse context has the line number
information, so it doesn't need to be added on closer to the original
exception.  This has the advantage of not having to rescue and re-raise the
exception multiple times, and simplifies liquid-c which would otherwise
have to rescue the exception in BlockBody#parse.
2015-07-08 19:21:59 -04:00
Dylan Thacker-Smith
cebf75b8d7 Implement line numbers without the Liquid::Token class. 2015-07-08 19:21:59 -04:00
Justin Li
afda01adbb Merge pull request #616 from Shopify/handle-non-int-range-args
Handle non-int range lookup arguments
2015-07-08 17:47:27 -04:00
Justin Li
959cd6d2a2 Temporarily disable rubinius in CI
It takes much longer than the others and is currently broken
2015-07-08 17:47:05 -04:00
Justin Li
4c1b89e20e Add regression test for ranges on non-integer types 2015-07-08 17:41:18 -04:00
Justin Li
83b6dd0268 Use to_integer for range lookup arguments 2015-07-08 17:37:07 -04:00
Justin Li
6fb402e60d Move to_integer, to_date, and to_number to Liquid::Utils 2015-07-08 17:33:05 -04:00
Dylan Thacker-Smith
338287df5e Merge pull request #613 from Shopify/taint-context-warning
Add taint warnings to the context rather than the template.
2015-07-07 16:23:10 -04:00
Dylan Thacker-Smith
c4c398174b Use early returns rather than large if in Variable#taint_check 2015-07-07 15:56:03 -04:00
Dylan Thacker-Smith
80b6ac3bc7 Add taint warnings to the context rather than the template. 2015-07-07 15:53:02 -04:00
Dylan Thacker-Smith
15974d9168 Merge pull request #612 from Shopify/fix-block-body-naming
Use node to refer to objects from the nodelist rather than token.
2015-07-07 15:49:58 -04:00
Dylan Thacker-Smith
f22ab4358b Merge pull request #611 from Shopify/no-escape-rescue
Remove standard exception rescue in escape filter.
2015-07-07 15:49:43 -04:00
Justin Li
9cf0d264e1 Require RuboCop v0.32.0 or later 2015-07-06 15:58:36 -04:00
Justin Li
575e3cae7a Remove class length metric cop 2015-07-06 15:52:11 -04:00
Dylan Thacker-Smith
fad3b8275c Use node to refer to objects from the nodelist rather than token. 2015-07-04 20:57:35 -04:00
Dylan Thacker-Smith
5a071cb7f2 Remove standard exception rescue in escape filter. 2015-07-04 13:48:25 -04:00
Justin Li
8cb2364179 Merge pull request #608 from Shopify/tag-tag_name
Add Liquid::Tag#tag_name
2015-07-02 16:28:37 -04:00
Gaurav Chande
3c23cfc167 Add Liquid::Tag#tag_name 2015-07-02 20:18:09 +00:00
33 changed files with 379 additions and 343 deletions

View File

@@ -13,6 +13,9 @@ Metrics/BlockNesting:
Metrics/ModuleLength:
Enabled: false
Metrics/ClassLength:
Enabled: false
Lint/AssignmentInCondition:
Enabled: false
@@ -118,6 +121,3 @@ Style/PerlBackrefs:
Style/WordArray:
Enabled: false
Style/ModuleLength:
Exclude:
- lib/liquid/standardfilters.rb

View File

@@ -13,11 +13,6 @@ Lint/NestedMethodDefinition:
Metrics/AbcSize:
Max: 58
# Offense count: 16
# Configuration parameters: CountComments.
Metrics/ClassLength:
Max: 314
# Offense count: 12
Metrics/CyclomaticComplexity:
Max: 15
@@ -32,11 +27,6 @@ Metrics/LineLength:
Metrics/MethodLength:
Max: 46
# Offense count: 1
# Configuration parameters: CountComments.
Metrics/ModuleLength:
Max: 235
# Offense count: 6
Metrics/PerceivedComplexity:
Max: 13

View File

@@ -6,7 +6,7 @@ rvm:
- 2.2
- ruby-head
- jruby-head
- rbx-2
# - rbx-2
sudo: false

View File

@@ -6,9 +6,9 @@ gem 'stackprof', platforms: :mri_21
group :test do
gem 'spy', '0.4.1'
gem 'benchmark-ips'
gem 'rubocop'
gem 'rubocop', '>=0.32.0'
platform :mri do
gem 'liquid-c', github: 'Shopify/liquid-c', ref: '35e9aee48d639ae1d3ac9ba77616aca9800eab7d'
gem 'liquid-c', github: 'Shopify/liquid-c', ref: '11d38237d9f491588a58c83dc3d364a7d0d1d55b'
end
end

View File

@@ -35,7 +35,8 @@ module Liquid
QuotedFragment = /#{QuotedString}|(?:[^\s,\|'"]|#{QuotedString})+/o
TagAttributes = /(\w+)\s*\:\s*(#{QuotedFragment})/o
AnyStartingTag = /\{\{|\{\%/
PartialTemplateParser = /#{TagStart}.*?#{TagEnd}|#{VariableStart}.*?#{VariableIncompleteEnd}/om
tag_contents = /(?:#{QuotedString}|.)*?/m
PartialTemplateParser = /#{TagStart}#{tag_contents}#{TagEnd}|#{VariableStart}#{tag_contents}#{VariableIncompleteEnd}/om
TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om
VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o
@@ -69,7 +70,7 @@ require 'liquid/standardfilters'
require 'liquid/condition'
require 'liquid/utils'
require 'liquid/tokenizer'
require 'liquid/token'
require 'liquid/parse_context'
# Load all the tags of the standard library
#

View File

@@ -23,29 +23,17 @@ module Liquid
@body.nodelist
end
# warnings of this block and all sub-tags
def warnings
all_warnings = []
all_warnings.concat(@warnings) if @warnings
(nodelist || []).each do |node|
all_warnings.concat(node.warnings || []) if node.respond_to?(:warnings)
end
all_warnings
end
def unknown_tag(tag, _params, _tokens)
case tag
when 'else'.freeze
raise SyntaxError.new(options[:locale].t("errors.syntax.unexpected_else".freeze,
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_else".freeze,
block_name: block_name))
when 'end'.freeze
raise SyntaxError.new(options[:locale].t("errors.syntax.invalid_delimiter".freeze,
raise SyntaxError.new(parse_context.locale.t("errors.syntax.invalid_delimiter".freeze,
block_name: block_name,
block_delimiter: block_delimiter))
else
raise SyntaxError.new(options[:locale].t("errors.syntax.unknown_tag".freeze, tag: tag))
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unknown_tag".freeze, tag: tag))
end
end
@@ -60,12 +48,12 @@ module Liquid
protected
def parse_body(body, tokens)
body.parse(tokens, options) do |end_tag_name, end_tag_params|
body.parse(tokens, parse_context) do |end_tag_name, end_tag_params|
@blank &&= body.blank?
return false if end_tag_name == block_delimiter
unless end_tag_name
raise SyntaxError.new(@options[:locale].t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
end
# this tag is not registered with the system

View File

@@ -12,44 +12,37 @@ module Liquid
@blank = true
end
def parse(tokens, options)
while token = tokens.shift
begin
unless token.empty?
case
when token.start_with?(TAGSTART)
if token =~ FullToken
tag_name = $1
markup = $2
# fetch the tag from registered blocks
if tag = registered_tags[tag_name]
markup = token.child(markup) if token.is_a?(Token)
new_tag = tag.parse(tag_name, markup, tokens, options)
new_tag.line_number = token.line_number if token.is_a?(Token)
@blank &&= new_tag.blank?
@nodelist << new_tag
else
# end parsing if we reach an unknown tag and let the caller decide
# determine how to proceed
return yield tag_name, markup
end
def parse(tokenizer, parse_context)
parse_context.line_number = tokenizer.line_number
while token = tokenizer.shift
unless token.empty?
case
when token.start_with?(TAGSTART)
if token =~ FullToken
tag_name = $1
markup = $2
# fetch the tag from registered blocks
if tag = registered_tags[tag_name]
new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
@blank &&= new_tag.blank?
@nodelist << new_tag
else
raise_missing_tag_terminator(token, options)
# end parsing if we reach an unknown tag and let the caller decide
# determine how to proceed
return yield tag_name, markup
end
when token.start_with?(VARSTART)
new_var = create_variable(token, options)
new_var.line_number = token.line_number if token.is_a?(Token)
@nodelist << new_var
@blank = false
else
@nodelist << token
@blank &&= !!(token =~ /\A\s*\z/)
raise_missing_tag_terminator(token, parse_context)
end
when token.start_with?(VARSTART)
@nodelist << create_variable(token, parse_context)
@blank = false
else
@nodelist << token
@blank &&= !!(token =~ /\A\s*\z/)
end
rescue SyntaxError => e
e.set_line_number_from_token(token)
raise
end
parse_context.line_number = tokenizer.line_number
end
yield nil, nil
@@ -59,14 +52,6 @@ module Liquid
@blank
end
def warnings
all_warnings = []
nodelist.each do |node|
all_warnings.concat(node.warnings || []) if node.respond_to?(:warnings)
end
all_warnings
end
def render(context)
output = []
context.resource_limits.render_score += @nodelist.length
@@ -84,15 +69,15 @@ module Liquid
break
end
token_output = render_token(token, context)
node_output = render_node(token, context)
unless token.is_a?(Block) && token.blank?
output << token_output
output << node_output
end
rescue MemoryError => e
raise e
rescue ::StandardError => e
output << context.handle_error(e, token)
output << context.handle_error(e, token.line_number)
end
end
@@ -101,31 +86,31 @@ module Liquid
private
def render_token(token, context)
token_output = (token.respond_to?(:render) ? token.render(context) : token)
token_str = token_output.is_a?(Array) ? token_output.join : token_output.to_s
def render_node(node, context)
node_output = (node.respond_to?(:render) ? node.render(context) : node)
node_output = node_output.is_a?(Array) ? node_output.join : node_output.to_s
context.resource_limits.render_length += token_str.length
context.resource_limits.render_length += node_output.length
if context.resource_limits.reached?
raise MemoryError.new("Memory limits exceeded".freeze)
end
token_str
node_output
end
def create_variable(token, options)
def create_variable(token, parse_context)
token.scan(ContentOfVariable) do |content|
markup = token.is_a?(Token) ? token.child(content.first) : content.first
return Variable.new(markup, options)
markup = content.first
return Variable.new(markup, parse_context)
end
raise_missing_variable_terminator(token, options)
raise_missing_variable_terminator(token, parse_context)
end
def raise_missing_tag_terminator(token, options)
raise SyntaxError.new(options[:locale].t("errors.syntax.tag_termination".freeze, token: token, tag_end: TagEnd.inspect))
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, options)
raise SyntaxError.new(options[:locale].t("errors.syntax.variable_termination".freeze, token: token, tag_end: VariableEnd.inspect))
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

View File

@@ -13,13 +13,14 @@ module Liquid
# context['bob'] #=> nil class Context
class Context
attr_reader :scopes, :errors, :registers, :environments, :resource_limits
attr_accessor :exception_handler, :template_name
attr_accessor :exception_handler, :template_name, :partial
def initialize(environments = {}, outer_scope = {}, registers = {}, rethrow_errors = false, resource_limits = nil)
@environments = [environments].flatten
@scopes = [(outer_scope || {})]
@registers = registers
@errors = []
@partial = false
@resource_limits = resource_limits || ResourceLimits.new(Template.default_resource_limits)
squash_instance_assigns_with_environments
@@ -33,6 +34,10 @@ module Liquid
@filters = []
end
def warnings
@warnings ||= []
end
def strainer
@strainer ||= Strainer.create(self, @filters)
end
@@ -62,10 +67,10 @@ module Liquid
@interrupts.pop
end
def handle_error(e, token = nil)
def handle_error(e, line_number = nil)
if e.is_a?(Liquid::Error)
e.template_name = template_name
e.set_line_number_from_token(token)
e.template_name ||= template_name
e.line_number ||= line_number
end
output = nil
@@ -75,7 +80,10 @@ module Liquid
case result
when Exception
e = result
e.set_line_number_from_token(token) if e.is_a?(Liquid::Error)
if e.is_a?(Liquid::Error)
e.template_name ||= template_name
e.line_number ||= line_number
end
when String
output = result
else

View File

@@ -1,27 +1,26 @@
module Liquid
class Document < BlockBody
DEFAULT_OPTIONS = {
locale: I18n.new
}
def self.parse(tokens, options)
def self.parse(tokens, parse_context)
doc = new
doc.parse(tokens, DEFAULT_OPTIONS.merge(options))
doc.parse(tokens, parse_context)
doc
end
def parse(tokens, options)
def parse(tokens, parse_context)
super do |end_tag_name, end_tag_params|
unknown_tag(end_tag_name, options) if end_tag_name
unknown_tag(end_tag_name, parse_context) if end_tag_name
end
rescue SyntaxError => e
e.line_number ||= parse_context.line_number
raise
end
def unknown_tag(tag, options)
def unknown_tag(tag, parse_context)
case tag
when 'else'.freeze, 'end'.freeze
raise SyntaxError.new(options[:locale].t("errors.syntax.unexpected_outer_tag".freeze, tag: tag))
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_outer_tag".freeze, tag: tag))
else
raise SyntaxError.new(options[:locale].t("errors.syntax.unknown_tag".freeze, tag: tag))
raise SyntaxError.new(parse_context.locale.t("errors.syntax.unknown_tag".freeze, tag: tag))
end
end
end

View File

@@ -17,12 +17,6 @@ module Liquid
str
end
def set_line_number_from_token(token)
return unless token.respond_to?(:line_number)
return if line_number
self.line_number = token.line_number
end
def self.render(e)
if e.is_a?(Liquid::Error)
e.to_s

View File

@@ -0,0 +1,37 @@
module Liquid
class ParseContext
attr_accessor :partial, :locale, :line_number
attr_reader :warnings, :error_mode
def initialize(options = {})
@template_options = options ? options.dup : {}
@locale = @template_options[:locale] ||= I18n.new
@warnings = []
self.partial = false
end
def [](option_key)
@options[option_key]
end
def partial=(value)
@partial = value
@options = value ? partial_options : @template_options
@error_mode = @options[:error_mode] || Template.error_mode
value
end
def partial_options
@partial_options ||= begin
dont_pass = @template_options[:include_options_blacklist]
if dont_pass == true
{ locale: locale }
elsif dont_pass.is_a?(Array)
@template_options.reject { |k, v| dont_pass.include?(k) }
else
@template_options
end
end
end
end
end

View File

@@ -1,16 +1,14 @@
module Liquid
module ParserSwitching
def parse_with_selected_parser(markup)
case @options[:error_mode] || Template.error_mode
case parse_context.error_mode
when :strict then strict_parse_with_error_context(markup)
when :lax then lax_parse(markup)
when :warn
begin
return strict_parse_with_error_context(markup)
rescue SyntaxError => e
e.set_line_number_from_token(markup)
@warnings ||= []
@warnings << e
parse_context.warnings << e
return lax_parse(markup)
end
end
@@ -21,6 +19,7 @@ module Liquid
def strict_parse_with_error_context(markup)
strict_parse(markup)
rescue SyntaxError => e
e.line_number = line_number
e.markup_context = markup_context(markup)
raise e
end

View File

@@ -19,7 +19,7 @@ module Liquid
# inside of <tt>{% include %}</tt> tags.
#
# profile.each do |node|
# # Access to the token itself
# # Access to the node itself
# node.code
#
# # Which template and line number of this node.
@@ -46,15 +46,15 @@ module Liquid
class Timing
attr_reader :code, :partial, :line_number, :children
def initialize(token, partial)
@code = token.respond_to?(:raw) ? token.raw : token
def initialize(node, partial)
@code = node.respond_to?(:raw) ? node.raw : node
@partial = partial
@line_number = token.respond_to?(:line_number) ? token.line_number : nil
@line_number = node.respond_to?(:line_number) ? node.line_number : nil
@children = []
end
def self.start(token, partial)
new(token, partial).tap(&:start)
def self.start(node, partial)
new(node, partial).tap(&:start)
end
def start
@@ -70,11 +70,11 @@ module Liquid
end
end
def self.profile_token_render(token)
if Profiler.current_profile && token.respond_to?(:render)
Profiler.current_profile.start_token(token)
def self.profile_node_render(node)
if Profiler.current_profile && node.respond_to?(:render)
Profiler.current_profile.start_node(node)
output = yield
Profiler.current_profile.end_token(token)
Profiler.current_profile.end_node(node)
output
else
yield
@@ -132,11 +132,11 @@ module Liquid
@root_timing.children.length
end
def start_token(token)
@timing_stack.push(Timing.start(token, current_partial))
def start_node(node)
@timing_stack.push(Timing.start(node, current_partial))
end
def end_token(_token)
def end_node(_node)
timing = @timing_stack.pop
timing.finish

View File

@@ -1,13 +1,13 @@
module Liquid
class BlockBody
def render_token_with_profiling(token, context)
Profiler.profile_token_render(token) do
render_token_without_profiling(token, context)
def render_node_with_profiling(node, context)
Profiler.profile_node_render(node) do
render_node_without_profiling(node, context)
end
end
alias_method :render_token_without_profiling, :render_token
alias_method :render_token, :render_token_with_profiling
alias_method :render_node_without_profiling, :render_node
alias_method :render_node, :render_node_with_profiling
end
class Include < Tag

View File

@@ -16,7 +16,22 @@ module Liquid
end
def evaluate(context)
context.evaluate(@start_obj).to_i..context.evaluate(@end_obj).to_i
start_int = to_integer(context.evaluate(@start_obj))
end_int = to_integer(context.evaluate(@end_obj))
start_int..end_int
end
private
def to_integer(input)
case input
when Integer
input
when NilClass, String
input.to_i
else
Utils.to_integer(input)
end
end
end
end

View File

@@ -33,7 +33,7 @@ module Liquid
end
def escape(input)
CGI.escapeHTML(input).untaint rescue input
CGI.escapeHTML(input).untaint
end
alias_method :h, :escape
@@ -46,8 +46,8 @@ module Liquid
end
def slice(input, offset, length = nil)
offset = to_integer(offset)
length = length ? to_integer(length) : 1
offset = Utils.to_integer(offset)
length = length ? Utils.to_integer(length) : 1
if input.is_a?(Array)
input.slice(offset, length) || []
@@ -59,7 +59,7 @@ module Liquid
# Truncate a string down to x characters
def truncate(input, length = 50, truncate_string = "...".freeze)
return if input.nil?
length = to_integer(length)
length = Utils.to_integer(length)
l = length - truncate_string.length
l = 0 if l < 0
input.length > length ? input[0...l] + truncate_string : input
@@ -68,7 +68,7 @@ module Liquid
def truncatewords(input, words = 15, truncate_string = "...".freeze)
return if input.nil?
wordlist = input.to_s.split
words = to_integer(words)
words = Utils.to_integer(words)
l = words - 1
l = 0 if l < 0
wordlist.length > l ? wordlist[0..l].join(" ".freeze) + truncate_string : input
@@ -255,7 +255,7 @@ module Liquid
def date(input, format)
return input if format.to_s.empty?
return input unless date = to_date(input)
return input unless date = Utils.to_date(input)
date.strftime(format.to_s)
end
@@ -307,7 +307,7 @@ module Liquid
end
def round(input, n = 0)
result = to_number(input).round(to_number(n))
result = Utils.to_number(input).round(Utils.to_number(n))
result = result.to_f if result.is_a?(BigDecimal)
result = result.to_i if n == 0
result
@@ -316,13 +316,13 @@ module Liquid
end
def ceil(input)
to_number(input).ceil.to_i
Utils.to_number(input).ceil.to_i
rescue ::FloatDomainError => e
raise Liquid::FloatDomainError, e.message
end
def floor(input)
to_number(input).floor.to_i
Utils.to_number(input).floor.to_i
rescue ::FloatDomainError => e
raise Liquid::FloatDomainError, e.message
end
@@ -334,51 +334,8 @@ module Liquid
private
def to_integer(num)
return num if num.is_a?(Integer)
num = num.to_s
begin
Integer(num)
rescue ::ArgumentError
raise Liquid::ArgumentError, "invalid integer"
end
end
def to_number(obj)
case obj
when Float
BigDecimal.new(obj.to_s)
when Numeric
obj
when String
(obj.strip =~ /\A\d+\.\d+\z/) ? BigDecimal.new(obj) : obj.to_i
else
0
end
end
def to_date(obj)
return obj if obj.respond_to?(:strftime)
if obj.is_a?(String)
return nil if obj.empty?
obj = obj.downcase
end
case obj
when 'now'.freeze, 'today'.freeze
Time.now
when /\A\d+\z/, Integer
Time.at(obj.to_i)
when String
Time.parse(obj)
end
rescue ArgumentError
nil
end
def apply_operation(input, operand, operation)
result = to_number(input).send(operation, to_number(operand))
result = Utils.to_number(input).send(operation, Utils.to_number(operand))
result.is_a?(BigDecimal) ? result.to_f : result
end

View File

@@ -1,23 +1,24 @@
module Liquid
class Tag
attr_accessor :options, :line_number
attr_reader :nodelist, :warnings
attr_reader :nodelist, :tag_name, :line_number, :parse_context
alias_method :options, :parse_context
include ParserSwitching
class << self
def parse(tag_name, markup, tokens, options)
def parse(tag_name, markup, tokenizer, options)
tag = new(tag_name, markup, options)
tag.parse(tokens)
tag.parse(tokenizer)
tag
end
private :new
end
def initialize(tag_name, markup, options)
def initialize(tag_name, markup, parse_context)
@tag_name = tag_name
@markup = markup
@options = options
@parse_context = parse_context
@line_number = parse_context.line_number
end
def parse(_tokens)

View File

@@ -15,7 +15,6 @@ module Liquid
if markup =~ Syntax
@to = $1
@from = Variable.new($2, options)
@from.line_number = line_number
else
raise SyntaxError.new options[:locale].t("errors.syntax.assign".freeze)
end

View File

@@ -53,8 +53,10 @@ module Liquid
end
old_template_name = context.template_name
old_partial = context.partial
begin
context.template_name = template_name
context.partial = true
context.stack do
@attributes.each do |key, value|
context[key] = context.evaluate(value)
@@ -72,11 +74,15 @@ module Liquid
end
ensure
context.template_name = old_template_name
context.partial = old_partial
end
end
private
alias_method :parse_context, :options
private :parse_context
def load_cached_partial(template_name, context)
cached_partials = context.registers[:cached_partials] || {}
@@ -84,7 +90,12 @@ module Liquid
return cached
end
source = read_template_from_file_system(context)
partial = Liquid::Template.parse(source, pass_options)
begin
parse_context.partial = true
partial = Liquid::Template.parse(source, parse_context)
ensure
parse_context.partial = false
end
cached_partials[template_name] = partial
context.registers[:cached_partials] = cached_partials
partial
@@ -95,16 +106,6 @@ module Liquid
file_system.read_template_file(context.evaluate(@template_name_expr))
end
def pass_options
dont_pass = @options[:include_options_blacklist]
return { locale: @options[:locale] } if dont_pass == true
opts = @options.merge(included: true, include_options_blacklist: false)
if dont_pass.is_a?(Array)
dont_pass.each { |o| opts.delete(o) }
end
opts
end
end
Template.register_tag('include'.freeze, Include)

View File

@@ -3,11 +3,11 @@ module Liquid
Syntax = /\A\s*\z/
FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
def initialize(tag_name, markup, options)
def initialize(tag_name, markup, parse_context)
super
unless markup =~ Syntax
raise SyntaxError.new(@options[:locale].t("errors.syntax.tag_unexpected_args".freeze, tag: tag_name))
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_unexpected_args".freeze, tag: tag_name))
end
end
@@ -21,7 +21,7 @@ module Liquid
@body << token unless token.empty?
end
raise SyntaxError.new(@options[:locale].t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
end
def render(_context)

View File

@@ -14,7 +14,7 @@ module Liquid
#
class Template
attr_accessor :root
attr_reader :resource_limits
attr_reader :resource_limits, :warnings
@@file_system = BlankFileSystem.new
@@ -116,16 +116,12 @@ module Liquid
@options = options
@profiling = options[:profile]
@line_numbers = options[:line_numbers] || @profiling
@root = Document.parse(tokenize(source), options)
@warnings = nil
parse_context = options.is_a?(ParseContext) ? options : ParseContext.new(options)
@root = Document.parse(tokenize(source), parse_context)
@warnings = parse_context.warnings
self
end
def warnings
return [] unless @root
@warnings ||= @root.warnings
end
def registers
@registers ||= {}
end
@@ -206,7 +202,7 @@ module Liquid
begin
# render the nodelist.
# for performance reasons we get an array back here. join will make a string out of it.
result = with_profiling do
result = with_profiling(context) do
@root.render(context)
end
result.respond_to?(:join) ? result.join : result
@@ -228,8 +224,8 @@ module Liquid
Tokenizer.new(source, @line_numbers)
end
def with_profiling
if @profiling && !@options[:included]
def with_profiling(context)
if @profiling && !context.partial
raise "Profiler not loaded, require 'liquid/profiler' first" unless defined?(Liquid::Profiler)
@profiler = Profiler.new

View File

@@ -1,18 +0,0 @@
module Liquid
class Token < String
attr_reader :line_number
def initialize(content, line_number)
super(content)
@line_number = line_number
end
def raw
"<raw>"
end
def child(string)
Token.new(string, @line_number)
end
end
end

View File

@@ -1,13 +1,17 @@
module Liquid
class Tokenizer
attr_reader :line_number
def initialize(source, line_numbers = false)
@source = source
@line_numbers = line_numbers
@line_number = 1 if line_numbers
@tokens = tokenize
end
def shift
@tokens.shift
token = @tokens.shift
@line_number += token.count("\n") if @line_number && token
token
end
private
@@ -17,21 +21,11 @@ module Liquid
return [] if @source.to_s.empty?
tokens = @source.split(TemplateParser)
tokens = @line_numbers ? calculate_line_numbers(tokens) : tokens
# removes the rogue empty element at the beginning of the array
tokens.shift if tokens[0] && tokens[0].empty?
tokens
end
def calculate_line_numbers(tokens)
current_line = 1
tokens.map do |token|
Token.new(token, current_line).tap do
current_line += token.count("\n")
end
end
end
end
end

View File

@@ -32,5 +32,48 @@ module Liquid
segments
end
def self.to_integer(num)
return num if num.is_a?(Integer)
num = num.to_s
begin
Integer(num)
rescue ::ArgumentError
raise Liquid::ArgumentError, "invalid integer"
end
end
def self.to_number(obj)
case obj
when Float
BigDecimal.new(obj.to_s)
when Numeric
obj
when String
(obj.strip =~ /\A\d+\.\d+\z/) ? BigDecimal.new(obj) : obj.to_i
else
0
end
end
def self.to_date(obj)
return obj if obj.respond_to?(:strftime)
if obj.is_a?(String)
return nil if obj.empty?
obj = obj.downcase
end
case obj
when 'now'.freeze, 'today'.freeze
Time.now
when /\A\d+\z/, Integer
Time.at(obj.to_i)
when String
Time.parse(obj)
end
rescue ArgumentError
nil
end
end
end

View File

@@ -11,14 +11,16 @@ module Liquid
#
class Variable
FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
attr_accessor :filters, :name, :warnings
attr_accessor :line_number
attr_accessor :filters, :name, :line_number
attr_reader :parse_context
alias_method :options, :parse_context
include ParserSwitching
def initialize(markup, options = {})
def initialize(markup, parse_context)
@markup = markup
@name = nil
@options = options || {}
@parse_context = parse_context
@line_number = parse_context.line_number
parse_with_selected_parser(markup)
end
@@ -74,7 +76,7 @@ module Liquid
@filters.inject(context.evaluate(@name)) do |output, (filter_name, filter_args, filter_kwargs)|
filter_args = evaluate_filter_expressions(context, filter_args, filter_kwargs)
context.invoke(filter_name, output, *filter_args)
end.tap{ |obj| taint_check(obj) }
end.tap{ |obj| taint_check(context, obj) }
end
private
@@ -106,17 +108,22 @@ module Liquid
parsed_args
end
def taint_check(obj)
if obj.tainted?
@markup =~ QuotedFragment
name = Regexp.last_match(0)
case Template.taint_mode
when :warn
@warnings ||= []
@warnings << "variable '#{name}' is tainted and was not escaped"
when :error
raise TaintedError, "Error - variable '#{name}' is tainted and was not escaped"
end
def taint_check(context, obj)
return unless obj.tainted?
return if Template.taint_mode == :lax
@markup =~ QuotedFragment
name = Regexp.last_match(0)
error = TaintedError.new("variable '#{name}' is tainted and was not escaped")
error.line_number = line_number
error.template_name = context.template_name
case Template.taint_mode
when :warn
context.warnings << error
when :error
raise error
end
end
end

View File

@@ -124,8 +124,10 @@ class DropsTest < Minitest::Test
def test_rendering_warns_on_tainted_attr
with_taint_mode(:warn) do
tpl = Liquid::Template.parse('{{ product.user_input }}')
tpl.render!('product' => ProductDrop.new)
assert_match /tainted/, tpl.warnings.first
context = Context.new('product' => ProductDrop.new)
tpl.render!(context)
assert_equal [Liquid::TaintedError], context.warnings.map(&:class)
assert_equal "variable 'product.user_input' is tainted and was not escaped", context.warnings.first.to_s(false)
end
end

View File

@@ -39,13 +39,13 @@ class FiltersTest < Minitest::Test
@context['var'] = 1000
@context.add_filters(MoneyFilter)
assert_equal ' 1000$ ', Variable.new("var | money").render(@context)
assert_equal ' 1000$ ', Template.parse("{{var | money}}").render(@context)
end
def test_underscore_in_filter_name
@context['var'] = 1000
@context.add_filters(MoneyFilter)
assert_equal ' 1000$ ', Variable.new("var | money_with_underscore").render(@context)
assert_equal ' 1000$ ', Template.parse("{{var | money_with_underscore}}").render(@context)
end
def test_second_filter_overwrites_first
@@ -53,20 +53,20 @@ class FiltersTest < Minitest::Test
@context.add_filters(MoneyFilter)
@context.add_filters(CanadianMoneyFilter)
assert_equal ' 1000$ CAD ', Variable.new("var | money").render(@context)
assert_equal ' 1000$ CAD ', Template.parse("{{var | money}}").render(@context)
end
def test_size
@context['var'] = 'abcd'
@context.add_filters(MoneyFilter)
assert_equal 4, Variable.new("var | size").render(@context)
assert_equal '4', Template.parse("{{var | size}}").render(@context)
end
def test_join
@context['var'] = [1, 2, 3, 4]
assert_equal "1 2 3 4", Variable.new("var | join").render(@context)
assert_equal "1 2 3 4", Template.parse("{{var | join}}").render(@context)
end
def test_sort
@@ -76,11 +76,11 @@ class FiltersTest < Minitest::Test
@context['arrays'] = ['flower', 'are']
@context['case_sensitive'] = ['sensitive', 'Expected', 'case']
assert_equal [1, 2, 3, 4], Variable.new("numbers | sort").render(@context)
assert_equal ['alphabetic', 'as', 'expected'], Variable.new("words | sort").render(@context)
assert_equal [3], Variable.new("value | sort").render(@context)
assert_equal ['are', 'flower'], Variable.new("arrays | sort").render(@context)
assert_equal ['Expected', 'case', 'sensitive'], Variable.new("case_sensitive | sort").render(@context)
assert_equal '1 2 3 4', Template.parse("{{numbers | sort | join}}").render(@context)
assert_equal 'alphabetic as expected', Template.parse("{{words | sort | join}}").render(@context)
assert_equal '3', Template.parse("{{value | sort}}").render(@context)
assert_equal 'are flower', Template.parse("{{arrays | sort | join}}").render(@context)
assert_equal 'Expected case sensitive', Template.parse("{{case_sensitive | sort | join}}").render(@context)
end
def test_sort_natural
@@ -89,19 +89,13 @@ class FiltersTest < Minitest::Test
@context['objects'] = [TestObject.new('A'), TestObject.new('b'), TestObject.new('C')]
# Test strings
assert_equal ['Assert', 'case', 'Insensitive'], Variable.new("words | sort_natural").render(@context)
assert_equal 'Assert case Insensitive', Template.parse("{{words | sort_natural | join}}").render(@context)
# Test hashes
sorted = Variable.new("hashes | sort_natural: 'a'").render(@context)
assert_equal sorted[0]['a'], 'A'
assert_equal sorted[1]['a'], 'b'
assert_equal sorted[2]['a'], 'C'
assert_equal 'A b C', Template.parse("{{hashes | sort_natural: 'a' | map: 'a' | join}}").render(@context)
# Test objects
sorted = Variable.new("objects | sort_natural: 'a'").render(@context)
assert_equal sorted[0].a, 'A'
assert_equal sorted[1].a, 'b'
assert_equal sorted[2].a, 'C'
assert_equal 'A b C', Template.parse("{{objects | sort_natural: 'a' | map: 'a' | join}}").render(@context)
end
def test_compact
@@ -110,49 +104,44 @@ class FiltersTest < Minitest::Test
@context['objects'] = [TestObject.new('A'), TestObject.new(nil), TestObject.new('C')]
# Test strings
assert_equal ['a', 'b', 'c'], Variable.new("words | compact").render(@context)
assert_equal 'a b c', Template.parse("{{words | compact | join}}").render(@context)
# Test hashes
sorted = Variable.new("hashes | compact: 'a'").render(@context)
assert_equal sorted[0]['a'], 'A'
assert_equal sorted[1]['a'], 'C'
assert_nil sorted[2]
assert_equal 'A C', Template.parse("{{hashes | compact: 'a' | map: 'a' | join}}").render(@context)
# Test objects
sorted = Variable.new("objects | compact: 'a'").render(@context)
assert_equal sorted[0].a, 'A'
assert_equal sorted[1].a, 'C'
assert_nil sorted[2]
assert_equal 'A C', Template.parse("{{objects | compact: 'a' | map: 'a' | join}}").render(@context)
end
def test_strip_html
@context['var'] = "<b>bla blub</a>"
assert_equal "bla blub", Variable.new("var | strip_html").render(@context)
assert_equal "bla blub", Template.parse("{{ var | strip_html }}").render(@context)
end
def test_strip_html_ignore_comments_with_html
@context['var'] = "<!-- split and some <ul> tag --><b>bla blub</a>"
assert_equal "bla blub", Variable.new("var | strip_html").render(@context)
assert_equal "bla blub", Template.parse("{{ var | strip_html }}").render(@context)
end
def test_capitalize
@context['var'] = "blub"
assert_equal "Blub", Variable.new("var | capitalize").render(@context)
assert_equal "Blub", Template.parse("{{ var | capitalize }}").render(@context)
end
def test_nonexistent_filter_is_ignored
@context['var'] = 1000
assert_equal 1000, Variable.new("var | xyzzy").render(@context)
assert_equal '1000', Template.parse("{{ var | xyzzy }}").render(@context)
end
def test_filter_with_keyword_arguments
@context['surname'] = 'john'
@context['input'] = 'hello %{first_name}, %{last_name}'
@context.add_filters(SubstituteFilter)
output = Variable.new(%( 'hello %{first_name}, %{last_name}' | substitute: first_name: surname, last_name: 'doe' )).render(@context)
output = Template.parse(%({{ input | substitute: first_name: surname, last_name: 'doe' }})).render(@context)
assert_equal 'hello john, doe', output
end
@@ -181,7 +170,7 @@ class FiltersInTemplate < Minitest::Test
end
end # FiltersTest
class TestObject
class TestObject < Liquid::Drop
attr_accessor :a
def initialize(a)
@a = a

View File

@@ -0,0 +1,13 @@
require 'test_helper'
class AssignTagTest < Minitest::Test
include Liquid
def test_assign
assert_template_result('monkey', "{% assign foo = 'monkey' %}{{ foo }}")
end
def test_string_with_end_tag
assert_template_result("{% quoted %}", "{% assign string = '{% quoted %}' %}{{ string }}")
end
end

View File

@@ -38,6 +38,12 @@ HERE
def test_for_with_range
assert_template_result(' 1 2 3 ', '{%for item in (1..3) %} {{item}} {%endfor%}')
assert_raises(Liquid::ArgumentError) do
Template.parse('{% for i in (a..2) %}{% endfor %}').render!("a" => [1, 2])
end
assert_template_result(' 0 1 2 3 ', '{% for item in (a..3) %} {{item}} {% endfor %}', "a" => "invalid integer")
end
def test_for_with_variable_range

View File

@@ -89,4 +89,9 @@ class VariableTest < Minitest::Test
def test_multiline_variable
assert_equal 'worked', Template.parse("{{\ntest\n}}").render!('test' => 'worked')
end
def test_string_with_curly_brackets
json = '{ "key": { "nested": "value" }}'
assert_template_result(json, "{{ '#{json}' }}")
end
end

View File

@@ -4,13 +4,18 @@ class TagUnitTest < Minitest::Test
include Liquid
def test_tag
tag = Tag.parse('tag', [], [], {})
tag = Tag.parse('tag', "", Tokenizer.new(""), 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", [], {})
tag = Tag.parse("long_tag", "param1, param2, param3", Tokenizer.new(""), 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)
assert_equal 'some_tag', tag.tag_name
end
end

View File

@@ -22,20 +22,34 @@ class TokenizerTest < Minitest::Test
end
def test_calculate_line_numbers_per_token_with_profiling
assert_equal [1], tokenize("{{funk}}", true).map(&:line_number)
assert_equal [1, 1, 1], tokenize(" {{funk}} ", true).map(&:line_number)
assert_equal [1, 2, 2], tokenize("\n{{funk}}\n", true).map(&:line_number)
assert_equal [1, 1, 3], tokenize(" {{\n funk \n}} ", true).map(&:line_number)
assert_equal [1], tokenize_line_numbers("{{funk}}")
assert_equal [1, 1, 1], tokenize_line_numbers(" {{funk}} ")
assert_equal [1, 2, 2], tokenize_line_numbers("\n{{funk}}\n")
assert_equal [1, 1, 3], tokenize_line_numbers(" {{\n funk \n}} ")
end
private
def tokenize(source, line_numbers = false)
tokenizer = Liquid::Tokenizer.new(source, line_numbers)
def tokenize(source)
tokenizer = Liquid::Tokenizer.new(source)
tokens = []
while t = tokenizer.shift
tokens << t
end
tokens
end
def tokenize_line_numbers(source)
tokenizer = Liquid::Tokenizer.new(source, true)
line_numbers = []
loop do
line_number = tokenizer.line_number
if tokenizer.shift
line_numbers << line_number
else
break
end
end
line_numbers
end
end

View File

@@ -4,133 +4,133 @@ class VariableUnitTest < Minitest::Test
include Liquid
def test_variable
var = Variable.new('hello')
var = create_variable('hello')
assert_equal VariableLookup.new('hello'), var.name
end
def test_filters
var = Variable.new('hello | textileze')
var = create_variable('hello | textileze')
assert_equal VariableLookup.new('hello'), var.name
assert_equal [['textileze', []]], var.filters
var = Variable.new('hello | textileze | paragraph')
var = create_variable('hello | textileze | paragraph')
assert_equal VariableLookup.new('hello'), var.name
assert_equal [['textileze', []], ['paragraph', []]], var.filters
var = Variable.new(%( hello | strftime: '%Y'))
var = create_variable(%( hello | strftime: '%Y'))
assert_equal VariableLookup.new('hello'), var.name
assert_equal [['strftime', ['%Y']]], var.filters
var = Variable.new(%( 'typo' | link_to: 'Typo', true ))
var = create_variable(%( 'typo' | link_to: 'Typo', true ))
assert_equal 'typo', var.name
assert_equal [['link_to', ['Typo', true]]], var.filters
var = Variable.new(%( 'typo' | link_to: 'Typo', false ))
var = create_variable(%( 'typo' | link_to: 'Typo', false ))
assert_equal 'typo', var.name
assert_equal [['link_to', ['Typo', false]]], var.filters
var = Variable.new(%( 'foo' | repeat: 3 ))
var = create_variable(%( 'foo' | repeat: 3 ))
assert_equal 'foo', var.name
assert_equal [['repeat', [3]]], var.filters
var = Variable.new(%( 'foo' | repeat: 3, 3 ))
var = create_variable(%( 'foo' | repeat: 3, 3 ))
assert_equal 'foo', var.name
assert_equal [['repeat', [3, 3]]], var.filters
var = Variable.new(%( 'foo' | repeat: 3, 3, 3 ))
var = create_variable(%( 'foo' | repeat: 3, 3, 3 ))
assert_equal 'foo', var.name
assert_equal [['repeat', [3, 3, 3]]], var.filters
var = Variable.new(%( hello | strftime: '%Y, okay?'))
var = create_variable(%( hello | strftime: '%Y, okay?'))
assert_equal VariableLookup.new('hello'), var.name
assert_equal [['strftime', ['%Y, okay?']]], var.filters
var = Variable.new(%( hello | things: "%Y, okay?", 'the other one'))
var = create_variable(%( hello | things: "%Y, okay?", 'the other one'))
assert_equal VariableLookup.new('hello'), var.name
assert_equal [['things', ['%Y, okay?', 'the other one']]], var.filters
end
def test_filter_with_date_parameter
var = Variable.new(%( '2006-06-06' | date: "%m/%d/%Y"))
var = create_variable(%( '2006-06-06' | date: "%m/%d/%Y"))
assert_equal '2006-06-06', var.name
assert_equal [['date', ['%m/%d/%Y']]], var.filters
end
def test_filters_without_whitespace
var = Variable.new('hello | textileze | paragraph')
var = create_variable('hello | textileze | paragraph')
assert_equal VariableLookup.new('hello'), var.name
assert_equal [['textileze', []], ['paragraph', []]], var.filters
var = Variable.new('hello|textileze|paragraph')
var = create_variable('hello|textileze|paragraph')
assert_equal VariableLookup.new('hello'), var.name
assert_equal [['textileze', []], ['paragraph', []]], var.filters
var = Variable.new("hello|replace:'foo','bar'|textileze")
var = create_variable("hello|replace:'foo','bar'|textileze")
assert_equal VariableLookup.new('hello'), var.name
assert_equal [['replace', ['foo', 'bar']], ['textileze', []]], var.filters
end
def test_symbol
var = Variable.new("http://disney.com/logo.gif | image: 'med' ", error_mode: :lax)
var = create_variable("http://disney.com/logo.gif | image: 'med' ", error_mode: :lax)
assert_equal VariableLookup.new('http://disney.com/logo.gif'), var.name
assert_equal [['image', ['med']]], var.filters
end
def test_string_to_filter
var = Variable.new("'http://disney.com/logo.gif' | image: 'med' ")
var = create_variable("'http://disney.com/logo.gif' | image: 'med' ")
assert_equal 'http://disney.com/logo.gif', var.name
assert_equal [['image', ['med']]], var.filters
end
def test_string_single_quoted
var = Variable.new(%( "hello" ))
var = create_variable(%( "hello" ))
assert_equal 'hello', var.name
end
def test_string_double_quoted
var = Variable.new(%( 'hello' ))
var = create_variable(%( 'hello' ))
assert_equal 'hello', var.name
end
def test_integer
var = Variable.new(%( 1000 ))
var = create_variable(%( 1000 ))
assert_equal 1000, var.name
end
def test_float
var = Variable.new(%( 1000.01 ))
var = create_variable(%( 1000.01 ))
assert_equal 1000.01, var.name
end
def test_dashes
assert_equal VariableLookup.new('foo-bar'), Variable.new('foo-bar').name
assert_equal VariableLookup.new('foo-bar-2'), Variable.new('foo-bar-2').name
assert_equal VariableLookup.new('foo-bar'), create_variable('foo-bar').name
assert_equal VariableLookup.new('foo-bar-2'), create_variable('foo-bar-2').name
with_error_mode :strict do
assert_raises(Liquid::SyntaxError) { Variable.new('foo - bar') }
assert_raises(Liquid::SyntaxError) { Variable.new('-foo') }
assert_raises(Liquid::SyntaxError) { Variable.new('2foo') }
assert_raises(Liquid::SyntaxError) { create_variable('foo - bar') }
assert_raises(Liquid::SyntaxError) { create_variable('-foo') }
assert_raises(Liquid::SyntaxError) { create_variable('2foo') }
end
end
def test_string_with_special_chars
var = Variable.new(%( 'hello! $!@.;"ddasd" ' ))
var = create_variable(%( 'hello! $!@.;"ddasd" ' ))
assert_equal 'hello! $!@.;"ddasd" ', var.name
end
def test_string_dot
var = Variable.new(%( test.test ))
var = create_variable(%( test.test ))
assert_equal VariableLookup.new('test.test'), var.name
end
def test_filter_with_keyword_arguments
var = Variable.new(%( hello | things: greeting: "world", farewell: 'goodbye'))
var = create_variable(%( hello | things: greeting: "world", farewell: 'goodbye'))
assert_equal VariableLookup.new('hello'), var.name
assert_equal [['things', [], { 'greeting' => 'world', 'farewell' => 'goodbye' }]], var.filters
end
def test_lax_filter_argument_parsing
var = Variable.new(%( number_of_comments | pluralize: 'comment': 'comments' ), error_mode: :lax)
var = create_variable(%( number_of_comments | pluralize: 'comment': 'comments' ), error_mode: :lax)
assert_equal VariableLookup.new('number_of_comments'), var.name
assert_equal [['pluralize', ['comment', 'comments']]], var.filters
end
@@ -138,13 +138,13 @@ class VariableUnitTest < Minitest::Test
def test_strict_filter_argument_parsing
with_error_mode(:strict) do
assert_raises(SyntaxError) do
Variable.new(%( number_of_comments | pluralize: 'comment': 'comments' ))
create_variable(%( number_of_comments | pluralize: 'comment': 'comments' ))
end
end
end
def test_output_raw_source_of_variable
var = Variable.new(%( name_of_variable | upcase ))
var = create_variable(%( name_of_variable | upcase ))
assert_equal " name_of_variable | upcase ", var.raw
end
@@ -153,4 +153,10 @@ class VariableUnitTest < Minitest::Test
assert_equal 'a', lookup.name
assert_equal ['b', 'c'], lookup.lookups
end
private
def create_variable(markup, options = {})
Variable.new(markup, ParseContext.new(options))
end
end