liquid 2.6.1 → 4.0.3

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 (130) hide show
  1. checksums.yaml +5 -5
  2. data/History.md +194 -29
  3. data/{MIT-LICENSE → LICENSE} +0 -0
  4. data/README.md +60 -2
  5. data/lib/liquid.rb +25 -14
  6. data/lib/liquid/block.rb +47 -96
  7. data/lib/liquid/block_body.rb +143 -0
  8. data/lib/liquid/condition.rb +70 -39
  9. data/lib/liquid/context.rb +116 -157
  10. data/lib/liquid/document.rb +19 -9
  11. data/lib/liquid/drop.rb +31 -14
  12. data/lib/liquid/errors.rb +54 -10
  13. data/lib/liquid/expression.rb +49 -0
  14. data/lib/liquid/extensions.rb +19 -7
  15. data/lib/liquid/file_system.rb +25 -14
  16. data/lib/liquid/forloop_drop.rb +42 -0
  17. data/lib/liquid/i18n.rb +39 -0
  18. data/lib/liquid/interrupts.rb +2 -3
  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 +311 -77
  30. data/lib/liquid/strainer.rb +39 -26
  31. data/lib/liquid/tablerowloop_drop.rb +62 -0
  32. data/lib/liquid/tag.rb +28 -11
  33. data/lib/liquid/tags/assign.rb +34 -10
  34. data/lib/liquid/tags/break.rb +1 -4
  35. data/lib/liquid/tags/capture.rb +11 -9
  36. data/lib/liquid/tags/case.rb +37 -22
  37. data/lib/liquid/tags/comment.rb +10 -3
  38. data/lib/liquid/tags/continue.rb +1 -4
  39. data/lib/liquid/tags/cycle.rb +20 -14
  40. data/lib/liquid/tags/decrement.rb +4 -8
  41. data/lib/liquid/tags/for.rb +121 -60
  42. data/lib/liquid/tags/if.rb +73 -30
  43. data/lib/liquid/tags/ifchanged.rb +3 -5
  44. data/lib/liquid/tags/include.rb +77 -46
  45. data/lib/liquid/tags/increment.rb +4 -8
  46. data/lib/liquid/tags/raw.rb +35 -10
  47. data/lib/liquid/tags/table_row.rb +62 -0
  48. data/lib/liquid/tags/unless.rb +6 -9
  49. data/lib/liquid/template.rb +130 -32
  50. data/lib/liquid/tokenizer.rb +31 -0
  51. data/lib/liquid/truffle.rb +5 -0
  52. data/lib/liquid/utils.rb +57 -4
  53. data/lib/liquid/variable.rb +121 -30
  54. data/lib/liquid/variable_lookup.rb +88 -0
  55. data/lib/liquid/version.rb +2 -1
  56. data/test/fixtures/en_locale.yml +9 -0
  57. data/test/integration/assign_test.rb +48 -0
  58. data/test/integration/blank_test.rb +106 -0
  59. data/test/integration/block_test.rb +12 -0
  60. data/test/{liquid → integration}/capture_test.rb +13 -3
  61. data/test/integration/context_test.rb +32 -0
  62. data/test/integration/document_test.rb +19 -0
  63. data/test/integration/drop_test.rb +273 -0
  64. data/test/integration/error_handling_test.rb +260 -0
  65. data/test/integration/filter_test.rb +178 -0
  66. data/test/integration/hash_ordering_test.rb +23 -0
  67. data/test/integration/output_test.rb +123 -0
  68. data/test/integration/parse_tree_visitor_test.rb +247 -0
  69. data/test/integration/parsing_quirks_test.rb +122 -0
  70. data/test/integration/render_profiling_test.rb +154 -0
  71. data/test/integration/security_test.rb +80 -0
  72. data/test/integration/standard_filter_test.rb +776 -0
  73. data/test/{liquid → integration}/tags/break_tag_test.rb +2 -3
  74. data/test/{liquid → integration}/tags/continue_tag_test.rb +1 -2
  75. data/test/integration/tags/for_tag_test.rb +410 -0
  76. data/test/integration/tags/if_else_tag_test.rb +188 -0
  77. data/test/integration/tags/include_tag_test.rb +253 -0
  78. data/test/integration/tags/increment_tag_test.rb +23 -0
  79. data/test/{liquid → integration}/tags/raw_tag_test.rb +9 -2
  80. data/test/integration/tags/standard_tag_test.rb +296 -0
  81. data/test/integration/tags/statements_test.rb +111 -0
  82. data/test/{liquid/tags/html_tag_test.rb → integration/tags/table_row_test.rb} +25 -24
  83. data/test/integration/tags/unless_else_tag_test.rb +26 -0
  84. data/test/integration/template_test.rb +332 -0
  85. data/test/integration/trim_mode_test.rb +529 -0
  86. data/test/integration/variable_test.rb +96 -0
  87. data/test/test_helper.rb +106 -19
  88. data/test/truffle/truffle_test.rb +9 -0
  89. data/test/{liquid/block_test.rb → unit/block_unit_test.rb} +9 -9
  90. data/test/unit/condition_unit_test.rb +166 -0
  91. data/test/{liquid/context_test.rb → unit/context_unit_test.rb} +85 -74
  92. data/test/unit/file_system_unit_test.rb +35 -0
  93. data/test/unit/i18n_unit_test.rb +37 -0
  94. data/test/unit/lexer_unit_test.rb +51 -0
  95. data/test/unit/parser_unit_test.rb +82 -0
  96. data/test/{liquid/regexp_test.rb → unit/regexp_unit_test.rb} +4 -4
  97. data/test/unit/strainer_unit_test.rb +164 -0
  98. data/test/unit/tag_unit_test.rb +21 -0
  99. data/test/unit/tags/case_tag_unit_test.rb +10 -0
  100. data/test/unit/tags/for_tag_unit_test.rb +13 -0
  101. data/test/unit/tags/if_tag_unit_test.rb +8 -0
  102. data/test/unit/template_unit_test.rb +78 -0
  103. data/test/unit/tokenizer_unit_test.rb +55 -0
  104. data/test/unit/variable_unit_test.rb +162 -0
  105. metadata +157 -77
  106. data/lib/extras/liquid_view.rb +0 -51
  107. data/lib/liquid/htmltags.rb +0 -74
  108. data/lib/liquid/module_ex.rb +0 -62
  109. data/test/liquid/assign_test.rb +0 -21
  110. data/test/liquid/condition_test.rb +0 -127
  111. data/test/liquid/drop_test.rb +0 -180
  112. data/test/liquid/error_handling_test.rb +0 -81
  113. data/test/liquid/file_system_test.rb +0 -29
  114. data/test/liquid/filter_test.rb +0 -125
  115. data/test/liquid/hash_ordering_test.rb +0 -25
  116. data/test/liquid/module_ex_test.rb +0 -87
  117. data/test/liquid/output_test.rb +0 -116
  118. data/test/liquid/parsing_quirks_test.rb +0 -52
  119. data/test/liquid/security_test.rb +0 -64
  120. data/test/liquid/standard_filter_test.rb +0 -251
  121. data/test/liquid/strainer_test.rb +0 -52
  122. data/test/liquid/tags/for_tag_test.rb +0 -297
  123. data/test/liquid/tags/if_else_tag_test.rb +0 -166
  124. data/test/liquid/tags/include_tag_test.rb +0 -166
  125. data/test/liquid/tags/increment_tag_test.rb +0 -24
  126. data/test/liquid/tags/standard_tag_test.rb +0 -295
  127. data/test/liquid/tags/statements_test.rb +0 -134
  128. data/test/liquid/tags/unless_else_tag_test.rb +0 -26
  129. data/test/liquid/template_test.rb +0 -146
  130. data/test/liquid/variable_test.rb +0 -186
@@ -0,0 +1,31 @@
1
+ module Liquid
2
+ class Tokenizer
3
+ attr_reader :line_number
4
+
5
+ def initialize(source, line_numbers = false)
6
+ @source = source
7
+ @line_number = line_numbers ? 1 : nil
8
+ @tokens = tokenize
9
+ end
10
+
11
+ def shift
12
+ token = @tokens.shift
13
+ @line_number += token.count("\n") if @line_number && token
14
+ token
15
+ end
16
+
17
+ private
18
+
19
+ def tokenize
20
+ @source = @source.source if @source.respond_to?(:source)
21
+ return [] if @source.to_s.empty?
22
+
23
+ tokens = @source.split(TemplateParser)
24
+
25
+ # removes the rogue empty element at the beginning of the array
26
+ tokens.shift if tokens[0] && tokens[0].empty?
27
+
28
+ tokens
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,5 @@
1
+ module Liquid
2
+ module Truffle
3
+
4
+ end
5
+ end
@@ -1,14 +1,24 @@
1
1
  module Liquid
2
2
  module Utils
3
+ def self.slice_collection(collection, from, to)
4
+ if (from != 0 || !to.nil?) && collection.respond_to?(:load_slice)
5
+ collection.load_slice(from, to)
6
+ else
7
+ slice_collection_using_each(collection, from, to)
8
+ end
9
+ end
10
+
3
11
  def self.slice_collection_using_each(collection, from, to)
4
12
  segments = []
5
13
  index = 0
6
14
 
7
15
  # Maintains Ruby 1.8.7 String#each behaviour on 1.9
8
- return [collection] if non_blank_string?(collection)
16
+ if collection.is_a?(String)
17
+ return collection.empty? ? [] : [collection]
18
+ end
19
+ return [] unless collection.respond_to?(:each)
9
20
 
10
21
  collection.each do |item|
11
-
12
22
  if to && to <= index
13
23
  break
14
24
  end
@@ -23,8 +33,51 @@ module Liquid
23
33
  segments
24
34
  end
25
35
 
26
- def self.non_blank_string?(collection)
27
- collection.is_a?(String) && collection != ''
36
+ def self.to_integer(num)
37
+ return num if num.is_a?(Integer)
38
+ num = num.to_s
39
+ begin
40
+ Integer(num)
41
+ rescue ::ArgumentError
42
+ raise Liquid::ArgumentError, "invalid integer"
43
+ end
44
+ end
45
+
46
+ def self.to_number(obj)
47
+ case obj
48
+ when Float
49
+ BigDecimal(obj.to_s)
50
+ when Numeric
51
+ obj
52
+ when String
53
+ (obj.strip =~ /\A-?\d+\.\d+\z/) ? BigDecimal(obj) : obj.to_i
54
+ else
55
+ if obj.respond_to?(:to_number)
56
+ obj.to_number
57
+ else
58
+ 0
59
+ end
60
+ end
61
+ end
62
+
63
+ def self.to_date(obj)
64
+ return obj if obj.respond_to?(:strftime)
65
+
66
+ if obj.is_a?(String)
67
+ return nil if obj.empty?
68
+ obj = obj.downcase
69
+ end
70
+
71
+ case obj
72
+ when 'now'.freeze, 'today'.freeze
73
+ Time.now
74
+ when /\A\d+\z/, Integer
75
+ Time.at(obj.to_i)
76
+ when String
77
+ Time.parse(obj)
78
+ end
79
+ rescue ::ArgumentError
80
+ nil
28
81
  end
29
82
  end
30
83
  end
@@ -1,5 +1,4 @@
1
1
  module Liquid
2
-
3
2
  # Holds variables. Variables are only loaded "just in time"
4
3
  # and are not evaluated as part of the render stage
5
4
  #
@@ -11,46 +10,138 @@ module Liquid
11
10
  # {{ user | link }}
12
11
  #
13
12
  class Variable
14
- FilterParser = /(?:#{FilterSeparator}|(?:\s*(?:#{QuotedFragment}|#{ArgumentSeparator})\s*)+)/o
15
- attr_accessor :filters, :name
13
+ FilterMarkupRegex = /#{FilterSeparator}\s*(.*)/om
14
+ FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
15
+ FilterArgsRegex = /(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o
16
+ JustTagAttributes = /\A#{TagAttributes}\z/o
17
+ MarkupWithQuotedFragment = /(#{QuotedFragment})(.*)/om
18
+
19
+ attr_accessor :filters, :name, :line_number
20
+ attr_reader :parse_context
21
+ alias_method :options, :parse_context
22
+
23
+ include ParserSwitching
16
24
 
17
- def initialize(markup)
25
+ def initialize(markup, parse_context)
18
26
  @markup = markup
19
27
  @name = nil
28
+ @parse_context = parse_context
29
+ @line_number = parse_context.line_number
30
+
31
+ parse_with_selected_parser(markup)
32
+ end
33
+
34
+ def raw
35
+ @markup
36
+ end
37
+
38
+ def markup_context(markup)
39
+ "in \"{{#{markup}}}\""
40
+ end
41
+
42
+ def lax_parse(markup)
20
43
  @filters = []
21
- if match = markup.match(/\s*(#{QuotedFragment})(.*)/o)
22
- @name = match[1]
23
- if match[2].match(/#{FilterSeparator}\s*(.*)/o)
24
- filters = Regexp.last_match(1).scan(FilterParser)
25
- filters.each do |f|
26
- if matches = f.match(/\s*(\w+)/)
27
- filtername = matches[1]
28
- filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o).flatten
29
- @filters << [filtername, filterargs]
30
- end
31
- end
44
+ return unless markup =~ MarkupWithQuotedFragment
45
+
46
+ name_markup = $1
47
+ filter_markup = $2
48
+ @name = Expression.parse(name_markup)
49
+ if filter_markup =~ FilterMarkupRegex
50
+ filters = $1.scan(FilterParser)
51
+ filters.each do |f|
52
+ next unless f =~ /\w+/
53
+ filtername = Regexp.last_match(0)
54
+ filterargs = f.scan(FilterArgsRegex).flatten
55
+ @filters << parse_filter_expressions(filtername, filterargs)
32
56
  end
33
57
  end
34
58
  end
35
59
 
60
+ def strict_parse(markup)
61
+ @filters = []
62
+ p = Parser.new(markup)
63
+
64
+ @name = Expression.parse(p.expression)
65
+ while p.consume?(:pipe)
66
+ filtername = p.consume(:id)
67
+ filterargs = p.consume?(:colon) ? parse_filterargs(p) : []
68
+ @filters << parse_filter_expressions(filtername, filterargs)
69
+ end
70
+ p.consume(:end_of_string)
71
+ end
72
+
73
+ def parse_filterargs(p)
74
+ # first argument
75
+ filterargs = [p.argument]
76
+ # followed by comma separated others
77
+ filterargs << p.argument while p.consume?(:comma)
78
+ filterargs
79
+ end
80
+
36
81
  def render(context)
37
- return '' if @name.nil?
38
- @filters.inject(context[@name]) do |output, filter|
39
- filterargs = []
40
- keyword_args = {}
41
- filter[1].to_a.each do |a|
42
- if matches = a.match(/\A#{TagAttributes}\z/o)
43
- keyword_args[matches[1]] = context[matches[2]]
44
- else
45
- filterargs << context[a]
46
- end
82
+ obj = @filters.inject(context.evaluate(@name)) do |output, (filter_name, filter_args, filter_kwargs)|
83
+ filter_args = evaluate_filter_expressions(context, filter_args, filter_kwargs)
84
+ context.invoke(filter_name, output, *filter_args)
85
+ end
86
+
87
+ obj = context.apply_global_filter(obj)
88
+
89
+ taint_check(context, obj)
90
+
91
+ obj
92
+ end
93
+
94
+ private
95
+
96
+ def parse_filter_expressions(filter_name, unparsed_args)
97
+ filter_args = []
98
+ keyword_args = {}
99
+ unparsed_args.each do |a|
100
+ if matches = a.match(JustTagAttributes)
101
+ keyword_args[matches[1]] = Expression.parse(matches[2])
102
+ else
103
+ filter_args << Expression.parse(a)
47
104
  end
48
- filterargs << keyword_args unless keyword_args.empty?
49
- begin
50
- output = context.invoke(filter[0], output, *filterargs)
51
- rescue FilterNotFound
52
- raise FilterNotFound, "Error - filter '#{filter[0]}' in '#{@markup.strip}' could not be found."
105
+ end
106
+ result = [filter_name, filter_args]
107
+ result << keyword_args unless keyword_args.empty?
108
+ result
109
+ end
110
+
111
+ def evaluate_filter_expressions(context, filter_args, filter_kwargs)
112
+ parsed_args = filter_args.map{ |expr| context.evaluate(expr) }
113
+ if filter_kwargs
114
+ parsed_kwargs = {}
115
+ filter_kwargs.each do |key, expr|
116
+ parsed_kwargs[key] = context.evaluate(expr)
53
117
  end
118
+ parsed_args << parsed_kwargs
119
+ end
120
+ parsed_args
121
+ end
122
+
123
+ def taint_check(context, obj)
124
+ return unless obj.tainted?
125
+ return if Template.taint_mode == :lax
126
+
127
+ @markup =~ QuotedFragment
128
+ name = Regexp.last_match(0)
129
+
130
+ error = TaintedError.new("variable '#{name}' is tainted and was not escaped")
131
+ error.line_number = line_number
132
+ error.template_name = context.template_name
133
+
134
+ case Template.taint_mode
135
+ when :warn
136
+ context.warnings << error
137
+ when :error
138
+ raise error
139
+ end
140
+ end
141
+
142
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
143
+ def children
144
+ [@node.name] + @node.filters.flatten
54
145
  end
55
146
  end
56
147
  end
@@ -0,0 +1,88 @@
1
+ module Liquid
2
+ class VariableLookup
3
+ SQUARE_BRACKETED = /\A\[(.*)\]\z/m
4
+ COMMAND_METHODS = ['size'.freeze, 'first'.freeze, 'last'.freeze].freeze
5
+
6
+ attr_reader :name, :lookups
7
+
8
+ def self.parse(markup)
9
+ new(markup)
10
+ end
11
+
12
+ def initialize(markup)
13
+ lookups = markup.scan(VariableParser)
14
+
15
+ name = lookups.shift
16
+ if name =~ SQUARE_BRACKETED
17
+ name = Expression.parse($1)
18
+ end
19
+ @name = name
20
+
21
+ @lookups = lookups
22
+ @command_flags = 0
23
+
24
+ @lookups.each_index do |i|
25
+ lookup = lookups[i]
26
+ if lookup =~ SQUARE_BRACKETED
27
+ lookups[i] = Expression.parse($1)
28
+ elsif COMMAND_METHODS.include?(lookup)
29
+ @command_flags |= 1 << i
30
+ end
31
+ end
32
+ end
33
+
34
+ def evaluate(context)
35
+ name = context.evaluate(@name)
36
+ object = context.find_variable(name)
37
+
38
+ @lookups.each_index do |i|
39
+ key = context.evaluate(@lookups[i])
40
+
41
+ # If object is a hash- or array-like object we look for the
42
+ # presence of the key and if its available we return it
43
+ if object.respond_to?(:[]) &&
44
+ ((object.respond_to?(:key?) && object.key?(key)) ||
45
+ (object.respond_to?(:fetch) && key.is_a?(Integer)))
46
+
47
+ # if its a proc we will replace the entry with the proc
48
+ res = context.lookup_and_evaluate(object, key)
49
+ object = res.to_liquid
50
+
51
+ # Some special cases. If the part wasn't in square brackets and
52
+ # no key with the same name was found we interpret following calls
53
+ # as commands and call them on the current object
54
+ elsif @command_flags & (1 << i) != 0 && object.respond_to?(key)
55
+ object = object.send(key).to_liquid
56
+
57
+ # No key was present with the desired value and it wasn't one of the directly supported
58
+ # keywords either. The only thing we got left is to return nil or
59
+ # raise an exception if `strict_variables` option is set to true
60
+ else
61
+ return nil unless context.strict_variables
62
+ raise Liquid::UndefinedVariable, "undefined variable #{key}"
63
+ end
64
+
65
+ # If we are dealing with a drop here we have to
66
+ object.context = context if object.respond_to?(:context=)
67
+ end
68
+
69
+ object
70
+ end
71
+
72
+ def ==(other)
73
+ self.class == other.class && state == other.state
74
+ end
75
+
76
+ protected
77
+
78
+ def state
79
+ [@name, @lookups, @command_flags]
80
+ end
81
+
82
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
83
+ def children
84
+ @node.lookups
85
+ end
86
+ end
87
+ end
88
+ end
@@ -1,4 +1,5 @@
1
1
  # encoding: utf-8
2
+
2
3
  module Liquid
3
- VERSION = "2.6.1"
4
+ VERSION = "4.0.3".freeze
4
5
  end
@@ -0,0 +1,9 @@
1
+ ---
2
+ simple: "less is more"
3
+ whatever: "something %{something}"
4
+ errors:
5
+ i18n:
6
+ undefined_interpolation: "undefined key %{key}"
7
+ unknown_translation: "translation '%{name}' wasn't found"
8
+ syntax:
9
+ oops: "something wasn't right"
@@ -0,0 +1,48 @@
1
+ require 'test_helper'
2
+
3
+ class AssignTest < Minitest::Test
4
+ include Liquid
5
+
6
+ def test_assign_with_hyphen_in_variable_name
7
+ template_source = <<-END_TEMPLATE
8
+ {% assign this-thing = 'Print this-thing' %}
9
+ {{ this-thing }}
10
+ END_TEMPLATE
11
+ template = Template.parse(template_source)
12
+ rendered = template.render!
13
+ assert_equal "Print this-thing", rendered.strip
14
+ end
15
+
16
+ def test_assigned_variable
17
+ assert_template_result('.foo.',
18
+ '{% assign foo = values %}.{{ foo[0] }}.',
19
+ 'values' => %w(foo bar baz))
20
+
21
+ assert_template_result('.bar.',
22
+ '{% assign foo = values %}.{{ foo[1] }}.',
23
+ 'values' => %w(foo bar baz))
24
+ end
25
+
26
+ def test_assign_with_filter
27
+ assert_template_result('.bar.',
28
+ '{% assign foo = values | split: "," %}.{{ foo[1] }}.',
29
+ 'values' => "foo,bar,baz")
30
+ end
31
+
32
+ def test_assign_syntax_error
33
+ assert_match_syntax_error(/assign/,
34
+ '{% assign foo not values %}.',
35
+ 'values' => "foo,bar,baz")
36
+ end
37
+
38
+ def test_assign_uses_error_mode
39
+ with_error_mode(:strict) do
40
+ assert_raises(SyntaxError) do
41
+ Template.parse("{% assign foo = ('X' | downcase) %}")
42
+ end
43
+ end
44
+ with_error_mode(:lax) do
45
+ assert Template.parse("{% assign foo = ('X' | downcase) %}")
46
+ end
47
+ end
48
+ end # AssignTest