liquid-4-0-2 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/History.md +235 -0
- data/LICENSE +20 -0
- data/README.md +108 -0
- data/lib/liquid.rb +80 -0
- data/lib/liquid/block.rb +77 -0
- data/lib/liquid/block_body.rb +142 -0
- data/lib/liquid/condition.rb +151 -0
- data/lib/liquid/context.rb +226 -0
- data/lib/liquid/document.rb +27 -0
- data/lib/liquid/drop.rb +78 -0
- data/lib/liquid/errors.rb +56 -0
- data/lib/liquid/expression.rb +49 -0
- data/lib/liquid/extensions.rb +74 -0
- data/lib/liquid/file_system.rb +73 -0
- data/lib/liquid/forloop_drop.rb +42 -0
- data/lib/liquid/i18n.rb +39 -0
- data/lib/liquid/interrupts.rb +16 -0
- data/lib/liquid/lexer.rb +55 -0
- data/lib/liquid/locales/en.yml +26 -0
- data/lib/liquid/parse_context.rb +38 -0
- data/lib/liquid/parse_tree_visitor.rb +42 -0
- data/lib/liquid/parser.rb +90 -0
- data/lib/liquid/parser_switching.rb +31 -0
- data/lib/liquid/profiler.rb +158 -0
- data/lib/liquid/profiler/hooks.rb +23 -0
- data/lib/liquid/range_lookup.rb +37 -0
- data/lib/liquid/resource_limits.rb +23 -0
- data/lib/liquid/standardfilters.rb +485 -0
- data/lib/liquid/strainer.rb +66 -0
- data/lib/liquid/tablerowloop_drop.rb +62 -0
- data/lib/liquid/tag.rb +43 -0
- data/lib/liquid/tags/assign.rb +59 -0
- data/lib/liquid/tags/break.rb +18 -0
- data/lib/liquid/tags/capture.rb +38 -0
- data/lib/liquid/tags/case.rb +94 -0
- data/lib/liquid/tags/comment.rb +16 -0
- data/lib/liquid/tags/continue.rb +18 -0
- data/lib/liquid/tags/cycle.rb +65 -0
- data/lib/liquid/tags/decrement.rb +35 -0
- data/lib/liquid/tags/for.rb +203 -0
- data/lib/liquid/tags/if.rb +122 -0
- data/lib/liquid/tags/ifchanged.rb +18 -0
- data/lib/liquid/tags/include.rb +124 -0
- data/lib/liquid/tags/increment.rb +31 -0
- data/lib/liquid/tags/raw.rb +47 -0
- data/lib/liquid/tags/table_row.rb +62 -0
- data/lib/liquid/tags/unless.rb +30 -0
- data/lib/liquid/template.rb +254 -0
- data/lib/liquid/tokenizer.rb +31 -0
- data/lib/liquid/utils.rb +83 -0
- data/lib/liquid/variable.rb +148 -0
- data/lib/liquid/variable_lookup.rb +88 -0
- data/lib/liquid/version.rb +4 -0
- data/test/fixtures/en_locale.yml +9 -0
- data/test/integration/assign_test.rb +48 -0
- data/test/integration/blank_test.rb +106 -0
- data/test/integration/block_test.rb +12 -0
- data/test/integration/capture_test.rb +50 -0
- data/test/integration/context_test.rb +32 -0
- data/test/integration/document_test.rb +19 -0
- data/test/integration/drop_test.rb +273 -0
- data/test/integration/error_handling_test.rb +260 -0
- data/test/integration/filter_test.rb +178 -0
- data/test/integration/hash_ordering_test.rb +23 -0
- data/test/integration/output_test.rb +123 -0
- data/test/integration/parse_tree_visitor_test.rb +247 -0
- data/test/integration/parsing_quirks_test.rb +122 -0
- data/test/integration/render_profiling_test.rb +154 -0
- data/test/integration/security_test.rb +80 -0
- data/test/integration/standard_filter_test.rb +698 -0
- data/test/integration/tags/break_tag_test.rb +15 -0
- data/test/integration/tags/continue_tag_test.rb +15 -0
- data/test/integration/tags/for_tag_test.rb +410 -0
- data/test/integration/tags/if_else_tag_test.rb +188 -0
- data/test/integration/tags/include_tag_test.rb +245 -0
- data/test/integration/tags/increment_tag_test.rb +23 -0
- data/test/integration/tags/raw_tag_test.rb +31 -0
- data/test/integration/tags/standard_tag_test.rb +296 -0
- data/test/integration/tags/statements_test.rb +111 -0
- data/test/integration/tags/table_row_test.rb +64 -0
- data/test/integration/tags/unless_else_tag_test.rb +26 -0
- data/test/integration/template_test.rb +332 -0
- data/test/integration/trim_mode_test.rb +529 -0
- data/test/integration/variable_test.rb +96 -0
- data/test/test_helper.rb +116 -0
- data/test/unit/block_unit_test.rb +58 -0
- data/test/unit/condition_unit_test.rb +166 -0
- data/test/unit/context_unit_test.rb +489 -0
- data/test/unit/file_system_unit_test.rb +35 -0
- data/test/unit/i18n_unit_test.rb +37 -0
- data/test/unit/lexer_unit_test.rb +51 -0
- data/test/unit/parser_unit_test.rb +82 -0
- data/test/unit/regexp_unit_test.rb +44 -0
- data/test/unit/strainer_unit_test.rb +164 -0
- data/test/unit/tag_unit_test.rb +21 -0
- data/test/unit/tags/case_tag_unit_test.rb +10 -0
- data/test/unit/tags/for_tag_unit_test.rb +13 -0
- data/test/unit/tags/if_tag_unit_test.rb +8 -0
- data/test/unit/template_unit_test.rb +78 -0
- data/test/unit/tokenizer_unit_test.rb +55 -0
- data/test/unit/variable_unit_test.rb +162 -0
- metadata +224 -0
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Liquid
|
4
|
+
# Strainer is the parent class for the filters system.
|
5
|
+
# New filters are mixed into the strainer class which is then instantiated for each liquid template render run.
|
6
|
+
#
|
7
|
+
# The Strainer only allows method calls defined in filters given to it via Strainer.global_filter,
|
8
|
+
# Context#add_filters or Template.register_filter
|
9
|
+
class Strainer #:nodoc:
|
10
|
+
@@global_strainer = Class.new(Strainer) do
|
11
|
+
@filter_methods = Set.new
|
12
|
+
end
|
13
|
+
@@strainer_class_cache = Hash.new do |hash, filters|
|
14
|
+
hash[filters] = Class.new(@@global_strainer) do
|
15
|
+
@filter_methods = @@global_strainer.filter_methods.dup
|
16
|
+
filters.each { |f| add_filter(f) }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def initialize(context)
|
21
|
+
@context = context
|
22
|
+
end
|
23
|
+
|
24
|
+
class << self
|
25
|
+
attr_reader :filter_methods
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.add_filter(filter)
|
29
|
+
raise ArgumentError, "Expected module but got: #{filter.class}" unless filter.is_a?(Module)
|
30
|
+
unless self.include?(filter)
|
31
|
+
invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }
|
32
|
+
if invokable_non_public_methods.any?
|
33
|
+
raise MethodOverrideError, "Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}"
|
34
|
+
else
|
35
|
+
send(:include, filter)
|
36
|
+
@filter_methods.merge(filter.public_instance_methods.map(&:to_s))
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.global_filter(filter)
|
42
|
+
@@strainer_class_cache.clear
|
43
|
+
@@global_strainer.add_filter(filter)
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.invokable?(method)
|
47
|
+
@filter_methods.include?(method.to_s)
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.create(context, filters = [])
|
51
|
+
@@strainer_class_cache[filters].new(context)
|
52
|
+
end
|
53
|
+
|
54
|
+
def invoke(method, *args)
|
55
|
+
if self.class.invokable?(method)
|
56
|
+
send(method, *args)
|
57
|
+
elsif @context && @context.strict_filters
|
58
|
+
raise Liquid::UndefinedFilter, "undefined filter #{method}"
|
59
|
+
else
|
60
|
+
args.first
|
61
|
+
end
|
62
|
+
rescue ::ArgumentError => e
|
63
|
+
raise Liquid::ArgumentError, e.message, e.backtrace
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Liquid
|
2
|
+
class TablerowloopDrop < Drop
|
3
|
+
def initialize(length, cols)
|
4
|
+
@length = length
|
5
|
+
@row = 1
|
6
|
+
@col = 1
|
7
|
+
@cols = cols
|
8
|
+
@index = 0
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :length, :col, :row
|
12
|
+
|
13
|
+
def index
|
14
|
+
@index + 1
|
15
|
+
end
|
16
|
+
|
17
|
+
def index0
|
18
|
+
@index
|
19
|
+
end
|
20
|
+
|
21
|
+
def col0
|
22
|
+
@col - 1
|
23
|
+
end
|
24
|
+
|
25
|
+
def rindex
|
26
|
+
@length - @index
|
27
|
+
end
|
28
|
+
|
29
|
+
def rindex0
|
30
|
+
@length - @index - 1
|
31
|
+
end
|
32
|
+
|
33
|
+
def first
|
34
|
+
@index == 0
|
35
|
+
end
|
36
|
+
|
37
|
+
def last
|
38
|
+
@index == @length - 1
|
39
|
+
end
|
40
|
+
|
41
|
+
def col_first
|
42
|
+
@col == 1
|
43
|
+
end
|
44
|
+
|
45
|
+
def col_last
|
46
|
+
@col == @cols
|
47
|
+
end
|
48
|
+
|
49
|
+
protected
|
50
|
+
|
51
|
+
def increment!
|
52
|
+
@index += 1
|
53
|
+
|
54
|
+
if @col == @cols
|
55
|
+
@col = 1
|
56
|
+
@row += 1
|
57
|
+
else
|
58
|
+
@col += 1
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
data/lib/liquid/tag.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
module Liquid
|
2
|
+
class Tag
|
3
|
+
attr_reader :nodelist, :tag_name, :line_number, :parse_context
|
4
|
+
alias_method :options, :parse_context
|
5
|
+
include ParserSwitching
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def parse(tag_name, markup, tokenizer, options)
|
9
|
+
tag = new(tag_name, markup, options)
|
10
|
+
tag.parse(tokenizer)
|
11
|
+
tag
|
12
|
+
end
|
13
|
+
|
14
|
+
private :new
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(tag_name, markup, parse_context)
|
18
|
+
@tag_name = tag_name
|
19
|
+
@markup = markup
|
20
|
+
@parse_context = parse_context
|
21
|
+
@line_number = parse_context.line_number
|
22
|
+
end
|
23
|
+
|
24
|
+
def parse(_tokens)
|
25
|
+
end
|
26
|
+
|
27
|
+
def raw
|
28
|
+
"#{@tag_name} #{@markup}"
|
29
|
+
end
|
30
|
+
|
31
|
+
def name
|
32
|
+
self.class.name.downcase
|
33
|
+
end
|
34
|
+
|
35
|
+
def render(_context)
|
36
|
+
''.freeze
|
37
|
+
end
|
38
|
+
|
39
|
+
def blank?
|
40
|
+
false
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Liquid
|
2
|
+
# Assign sets a variable in your template.
|
3
|
+
#
|
4
|
+
# {% assign foo = 'monkey' %}
|
5
|
+
#
|
6
|
+
# You can then use the variable later in the page.
|
7
|
+
#
|
8
|
+
# {{ foo }}
|
9
|
+
#
|
10
|
+
class Assign < Tag
|
11
|
+
Syntax = /(#{VariableSignature}+)\s*=\s*(.*)\s*/om
|
12
|
+
|
13
|
+
attr_reader :to, :from
|
14
|
+
|
15
|
+
def initialize(tag_name, markup, options)
|
16
|
+
super
|
17
|
+
if markup =~ Syntax
|
18
|
+
@to = $1
|
19
|
+
@from = Variable.new($2, options)
|
20
|
+
else
|
21
|
+
raise SyntaxError.new options[:locale].t("errors.syntax.assign".freeze)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def render(context)
|
26
|
+
val = @from.render(context)
|
27
|
+
context.scopes.last[@to] = val
|
28
|
+
context.resource_limits.assign_score += assign_score_of(val)
|
29
|
+
''.freeze
|
30
|
+
end
|
31
|
+
|
32
|
+
def blank?
|
33
|
+
true
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def assign_score_of(val)
|
39
|
+
if val.instance_of?(String)
|
40
|
+
val.length
|
41
|
+
elsif val.instance_of?(Array) || val.instance_of?(Hash)
|
42
|
+
sum = 1
|
43
|
+
# Uses #each to avoid extra allocations.
|
44
|
+
val.each { |child| sum += assign_score_of(child) }
|
45
|
+
sum
|
46
|
+
else
|
47
|
+
1
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
52
|
+
def children
|
53
|
+
[@node.from]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
Template.register_tag('assign'.freeze, Assign)
|
59
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Liquid
|
2
|
+
# Break tag to be used to break out of a for loop.
|
3
|
+
#
|
4
|
+
# == Basic Usage:
|
5
|
+
# {% for item in collection %}
|
6
|
+
# {% if item.condition %}
|
7
|
+
# {% break %}
|
8
|
+
# {% endif %}
|
9
|
+
# {% endfor %}
|
10
|
+
#
|
11
|
+
class Break < Tag
|
12
|
+
def interrupt
|
13
|
+
BreakInterrupt.new
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
Template.register_tag('break'.freeze, Break)
|
18
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Liquid
|
2
|
+
# Capture stores the result of a block into a variable without rendering it inplace.
|
3
|
+
#
|
4
|
+
# {% capture heading %}
|
5
|
+
# Monkeys!
|
6
|
+
# {% endcapture %}
|
7
|
+
# ...
|
8
|
+
# <h1>{{ heading }}</h1>
|
9
|
+
#
|
10
|
+
# Capture is useful for saving content for use later in your template, such as
|
11
|
+
# in a sidebar or footer.
|
12
|
+
#
|
13
|
+
class Capture < Block
|
14
|
+
Syntax = /(#{VariableSignature}+)/o
|
15
|
+
|
16
|
+
def initialize(tag_name, markup, options)
|
17
|
+
super
|
18
|
+
if markup =~ Syntax
|
19
|
+
@to = $1
|
20
|
+
else
|
21
|
+
raise SyntaxError.new(options[:locale].t("errors.syntax.capture"))
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def render(context)
|
26
|
+
output = super
|
27
|
+
context.scopes.last[@to] = output
|
28
|
+
context.resource_limits.assign_score += output.length
|
29
|
+
''.freeze
|
30
|
+
end
|
31
|
+
|
32
|
+
def blank?
|
33
|
+
true
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
Template.register_tag('capture'.freeze, Capture)
|
38
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module Liquid
|
2
|
+
class Case < Block
|
3
|
+
Syntax = /(#{QuotedFragment})/o
|
4
|
+
WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/om
|
5
|
+
|
6
|
+
attr_reader :blocks, :left
|
7
|
+
|
8
|
+
def initialize(tag_name, markup, options)
|
9
|
+
super
|
10
|
+
@blocks = []
|
11
|
+
|
12
|
+
if markup =~ Syntax
|
13
|
+
@left = Expression.parse($1)
|
14
|
+
else
|
15
|
+
raise SyntaxError.new(options[:locale].t("errors.syntax.case".freeze))
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def parse(tokens)
|
20
|
+
body = BlockBody.new
|
21
|
+
while parse_body(body, tokens)
|
22
|
+
body = @blocks.last.attachment
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def nodelist
|
27
|
+
@blocks.map(&:attachment)
|
28
|
+
end
|
29
|
+
|
30
|
+
def unknown_tag(tag, markup, tokens)
|
31
|
+
case tag
|
32
|
+
when 'when'.freeze
|
33
|
+
record_when_condition(markup)
|
34
|
+
when 'else'.freeze
|
35
|
+
record_else_condition(markup)
|
36
|
+
else
|
37
|
+
super
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def render(context)
|
42
|
+
context.stack do
|
43
|
+
execute_else_block = true
|
44
|
+
|
45
|
+
output = ''
|
46
|
+
@blocks.each do |block|
|
47
|
+
if block.else?
|
48
|
+
return block.attachment.render(context) if execute_else_block
|
49
|
+
elsif block.evaluate(context)
|
50
|
+
execute_else_block = false
|
51
|
+
output << block.attachment.render(context)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
output
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def record_when_condition(markup)
|
61
|
+
body = BlockBody.new
|
62
|
+
|
63
|
+
while markup
|
64
|
+
unless markup =~ WhenSyntax
|
65
|
+
raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_when".freeze))
|
66
|
+
end
|
67
|
+
|
68
|
+
markup = $2
|
69
|
+
|
70
|
+
block = Condition.new(@left, '=='.freeze, Expression.parse($1))
|
71
|
+
block.attach(body)
|
72
|
+
@blocks << block
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def record_else_condition(markup)
|
77
|
+
unless markup.strip.empty?
|
78
|
+
raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_else".freeze))
|
79
|
+
end
|
80
|
+
|
81
|
+
block = ElseCondition.new
|
82
|
+
block.attach(BlockBody.new)
|
83
|
+
@blocks << block
|
84
|
+
end
|
85
|
+
|
86
|
+
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
87
|
+
def children
|
88
|
+
[@node.left] + @node.blocks
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
Template.register_tag('case'.freeze, Case)
|
94
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Liquid
|
2
|
+
# Continue tag to be used to break out of a for loop.
|
3
|
+
#
|
4
|
+
# == Basic Usage:
|
5
|
+
# {% for item in collection %}
|
6
|
+
# {% if item.condition %}
|
7
|
+
# {% continue %}
|
8
|
+
# {% endif %}
|
9
|
+
# {% endfor %}
|
10
|
+
#
|
11
|
+
class Continue < Tag
|
12
|
+
def interrupt
|
13
|
+
ContinueInterrupt.new
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
Template.register_tag('continue'.freeze, Continue)
|
18
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Liquid
|
2
|
+
# Cycle is usually used within a loop to alternate between values, like colors or DOM classes.
|
3
|
+
#
|
4
|
+
# {% for item in items %}
|
5
|
+
# <div class="{% cycle 'red', 'green', 'blue' %}"> {{ item }} </div>
|
6
|
+
# {% end %}
|
7
|
+
#
|
8
|
+
# <div class="red"> Item one </div>
|
9
|
+
# <div class="green"> Item two </div>
|
10
|
+
# <div class="blue"> Item three </div>
|
11
|
+
# <div class="red"> Item four </div>
|
12
|
+
# <div class="green"> Item five</div>
|
13
|
+
#
|
14
|
+
class Cycle < Tag
|
15
|
+
SimpleSyntax = /\A#{QuotedFragment}+/o
|
16
|
+
NamedSyntax = /\A(#{QuotedFragment})\s*\:\s*(.*)/om
|
17
|
+
|
18
|
+
attr_reader :variables
|
19
|
+
|
20
|
+
def initialize(tag_name, markup, options)
|
21
|
+
super
|
22
|
+
case markup
|
23
|
+
when NamedSyntax
|
24
|
+
@variables = variables_from_string($2)
|
25
|
+
@name = Expression.parse($1)
|
26
|
+
when SimpleSyntax
|
27
|
+
@variables = variables_from_string(markup)
|
28
|
+
@name = @variables.to_s
|
29
|
+
else
|
30
|
+
raise SyntaxError.new(options[:locale].t("errors.syntax.cycle".freeze))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def render(context)
|
35
|
+
context.registers[:cycle] ||= {}
|
36
|
+
|
37
|
+
context.stack do
|
38
|
+
key = context.evaluate(@name)
|
39
|
+
iteration = context.registers[:cycle][key].to_i
|
40
|
+
result = context.evaluate(@variables[iteration])
|
41
|
+
iteration += 1
|
42
|
+
iteration = 0 if iteration >= @variables.size
|
43
|
+
context.registers[:cycle][key] = iteration
|
44
|
+
result
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def variables_from_string(markup)
|
51
|
+
markup.split(',').collect do |var|
|
52
|
+
var =~ /\s*(#{QuotedFragment})\s*/o
|
53
|
+
$1 ? Expression.parse($1) : nil
|
54
|
+
end.compact
|
55
|
+
end
|
56
|
+
|
57
|
+
class ParseTreeVisitor < Liquid::ParseTreeVisitor
|
58
|
+
def children
|
59
|
+
Array(@node.variables)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
Template.register_tag('cycle', Cycle)
|
65
|
+
end
|