liquid 2.6.1 → 4.0.3

Sign up to get free protection for your applications and to get access to all the features.
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