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.
Files changed (103) hide show
  1. checksums.yaml +7 -0
  2. data/History.md +235 -0
  3. data/LICENSE +20 -0
  4. data/README.md +108 -0
  5. data/lib/liquid.rb +80 -0
  6. data/lib/liquid/block.rb +77 -0
  7. data/lib/liquid/block_body.rb +142 -0
  8. data/lib/liquid/condition.rb +151 -0
  9. data/lib/liquid/context.rb +226 -0
  10. data/lib/liquid/document.rb +27 -0
  11. data/lib/liquid/drop.rb +78 -0
  12. data/lib/liquid/errors.rb +56 -0
  13. data/lib/liquid/expression.rb +49 -0
  14. data/lib/liquid/extensions.rb +74 -0
  15. data/lib/liquid/file_system.rb +73 -0
  16. data/lib/liquid/forloop_drop.rb +42 -0
  17. data/lib/liquid/i18n.rb +39 -0
  18. data/lib/liquid/interrupts.rb +16 -0
  19. data/lib/liquid/lexer.rb +55 -0
  20. data/lib/liquid/locales/en.yml +26 -0
  21. data/lib/liquid/parse_context.rb +38 -0
  22. data/lib/liquid/parse_tree_visitor.rb +42 -0
  23. data/lib/liquid/parser.rb +90 -0
  24. data/lib/liquid/parser_switching.rb +31 -0
  25. data/lib/liquid/profiler.rb +158 -0
  26. data/lib/liquid/profiler/hooks.rb +23 -0
  27. data/lib/liquid/range_lookup.rb +37 -0
  28. data/lib/liquid/resource_limits.rb +23 -0
  29. data/lib/liquid/standardfilters.rb +485 -0
  30. data/lib/liquid/strainer.rb +66 -0
  31. data/lib/liquid/tablerowloop_drop.rb +62 -0
  32. data/lib/liquid/tag.rb +43 -0
  33. data/lib/liquid/tags/assign.rb +59 -0
  34. data/lib/liquid/tags/break.rb +18 -0
  35. data/lib/liquid/tags/capture.rb +38 -0
  36. data/lib/liquid/tags/case.rb +94 -0
  37. data/lib/liquid/tags/comment.rb +16 -0
  38. data/lib/liquid/tags/continue.rb +18 -0
  39. data/lib/liquid/tags/cycle.rb +65 -0
  40. data/lib/liquid/tags/decrement.rb +35 -0
  41. data/lib/liquid/tags/for.rb +203 -0
  42. data/lib/liquid/tags/if.rb +122 -0
  43. data/lib/liquid/tags/ifchanged.rb +18 -0
  44. data/lib/liquid/tags/include.rb +124 -0
  45. data/lib/liquid/tags/increment.rb +31 -0
  46. data/lib/liquid/tags/raw.rb +47 -0
  47. data/lib/liquid/tags/table_row.rb +62 -0
  48. data/lib/liquid/tags/unless.rb +30 -0
  49. data/lib/liquid/template.rb +254 -0
  50. data/lib/liquid/tokenizer.rb +31 -0
  51. data/lib/liquid/utils.rb +83 -0
  52. data/lib/liquid/variable.rb +148 -0
  53. data/lib/liquid/variable_lookup.rb +88 -0
  54. data/lib/liquid/version.rb +4 -0
  55. data/test/fixtures/en_locale.yml +9 -0
  56. data/test/integration/assign_test.rb +48 -0
  57. data/test/integration/blank_test.rb +106 -0
  58. data/test/integration/block_test.rb +12 -0
  59. data/test/integration/capture_test.rb +50 -0
  60. data/test/integration/context_test.rb +32 -0
  61. data/test/integration/document_test.rb +19 -0
  62. data/test/integration/drop_test.rb +273 -0
  63. data/test/integration/error_handling_test.rb +260 -0
  64. data/test/integration/filter_test.rb +178 -0
  65. data/test/integration/hash_ordering_test.rb +23 -0
  66. data/test/integration/output_test.rb +123 -0
  67. data/test/integration/parse_tree_visitor_test.rb +247 -0
  68. data/test/integration/parsing_quirks_test.rb +122 -0
  69. data/test/integration/render_profiling_test.rb +154 -0
  70. data/test/integration/security_test.rb +80 -0
  71. data/test/integration/standard_filter_test.rb +698 -0
  72. data/test/integration/tags/break_tag_test.rb +15 -0
  73. data/test/integration/tags/continue_tag_test.rb +15 -0
  74. data/test/integration/tags/for_tag_test.rb +410 -0
  75. data/test/integration/tags/if_else_tag_test.rb +188 -0
  76. data/test/integration/tags/include_tag_test.rb +245 -0
  77. data/test/integration/tags/increment_tag_test.rb +23 -0
  78. data/test/integration/tags/raw_tag_test.rb +31 -0
  79. data/test/integration/tags/standard_tag_test.rb +296 -0
  80. data/test/integration/tags/statements_test.rb +111 -0
  81. data/test/integration/tags/table_row_test.rb +64 -0
  82. data/test/integration/tags/unless_else_tag_test.rb +26 -0
  83. data/test/integration/template_test.rb +332 -0
  84. data/test/integration/trim_mode_test.rb +529 -0
  85. data/test/integration/variable_test.rb +96 -0
  86. data/test/test_helper.rb +116 -0
  87. data/test/unit/block_unit_test.rb +58 -0
  88. data/test/unit/condition_unit_test.rb +166 -0
  89. data/test/unit/context_unit_test.rb +489 -0
  90. data/test/unit/file_system_unit_test.rb +35 -0
  91. data/test/unit/i18n_unit_test.rb +37 -0
  92. data/test/unit/lexer_unit_test.rb +51 -0
  93. data/test/unit/parser_unit_test.rb +82 -0
  94. data/test/unit/regexp_unit_test.rb +44 -0
  95. data/test/unit/strainer_unit_test.rb +164 -0
  96. data/test/unit/tag_unit_test.rb +21 -0
  97. data/test/unit/tags/case_tag_unit_test.rb +10 -0
  98. data/test/unit/tags/for_tag_unit_test.rb +13 -0
  99. data/test/unit/tags/if_tag_unit_test.rb +8 -0
  100. data/test/unit/template_unit_test.rb +78 -0
  101. data/test/unit/tokenizer_unit_test.rb +55 -0
  102. data/test/unit/variable_unit_test.rb +162 -0
  103. 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
@@ -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,16 @@
1
+ module Liquid
2
+ class Comment < Block
3
+ def render(_context)
4
+ ''.freeze
5
+ end
6
+
7
+ def unknown_tag(_tag, _markup, _tokens)
8
+ end
9
+
10
+ def blank?
11
+ true
12
+ end
13
+ end
14
+
15
+ Template.register_tag('comment'.freeze, Comment)
16
+ 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