liquid-4-0-2 4.0.2

Sign up to get free protection for your applications and to get access to all the features.
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,83 @@
1
+ module Liquid
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
+
11
+ def self.slice_collection_using_each(collection, from, to)
12
+ segments = []
13
+ index = 0
14
+
15
+ # Maintains Ruby 1.8.7 String#each behaviour on 1.9
16
+ if collection.is_a?(String)
17
+ return collection.empty? ? [] : [collection]
18
+ end
19
+ return [] unless collection.respond_to?(:each)
20
+
21
+ collection.each do |item|
22
+ if to && to <= index
23
+ break
24
+ end
25
+
26
+ if from <= index
27
+ segments << item
28
+ end
29
+
30
+ index += 1
31
+ end
32
+
33
+ segments
34
+ end
35
+
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
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,148 @@
1
+ module Liquid
2
+ # Holds variables. Variables are only loaded "just in time"
3
+ # and are not evaluated as part of the render stage
4
+ #
5
+ # {{ monkey }}
6
+ # {{ user.name }}
7
+ #
8
+ # Variables can be combined with filters:
9
+ #
10
+ # {{ user | link }}
11
+ #
12
+ class Variable
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
24
+
25
+ def initialize(markup, parse_context)
26
+ @markup = markup
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)
43
+ @filters = []
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)
56
+ end
57
+ end
58
+ end
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
+
81
+ def render(context)
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)
104
+ end
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)
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
145
+ end
146
+ end
147
+ end
148
+ 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]
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
@@ -0,0 +1,4 @@
1
+ # encoding: utf-8
2
+ module Liquid
3
+ VERSION = "4.0.2"
4
+ 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
@@ -0,0 +1,106 @@
1
+ require 'test_helper'
2
+
3
+ class FoobarTag < Liquid::Tag
4
+ def render(*args)
5
+ " "
6
+ end
7
+
8
+ Liquid::Template.register_tag('foobar', FoobarTag)
9
+ end
10
+
11
+ class BlankTestFileSystem
12
+ def read_template_file(template_path)
13
+ template_path
14
+ end
15
+ end
16
+
17
+ class BlankTest < Minitest::Test
18
+ include Liquid
19
+ N = 10
20
+
21
+ def wrap_in_for(body)
22
+ "{% for i in (1..#{N}) %}#{body}{% endfor %}"
23
+ end
24
+
25
+ def wrap_in_if(body)
26
+ "{% if true %}#{body}{% endif %}"
27
+ end
28
+
29
+ def wrap(body)
30
+ wrap_in_for(body) + wrap_in_if(body)
31
+ end
32
+
33
+ def test_new_tags_are_not_blank_by_default
34
+ assert_template_result(" " * N, wrap_in_for("{% foobar %}"))
35
+ end
36
+
37
+ def test_loops_are_blank
38
+ assert_template_result("", wrap_in_for(" "))
39
+ end
40
+
41
+ def test_if_else_are_blank
42
+ assert_template_result("", "{% if true %} {% elsif false %} {% else %} {% endif %}")
43
+ end
44
+
45
+ def test_unless_is_blank
46
+ assert_template_result("", wrap("{% unless true %} {% endunless %}"))
47
+ end
48
+
49
+ def test_mark_as_blank_only_during_parsing
50
+ assert_template_result(" " * (N + 1), wrap(" {% if false %} this never happens, but still, this block is not blank {% endif %}"))
51
+ end
52
+
53
+ def test_comments_are_blank
54
+ assert_template_result("", wrap(" {% comment %} whatever {% endcomment %} "))
55
+ end
56
+
57
+ def test_captures_are_blank
58
+ assert_template_result("", wrap(" {% capture foo %} whatever {% endcapture %} "))
59
+ end
60
+
61
+ def test_nested_blocks_are_blank_but_only_if_all_children_are
62
+ assert_template_result("", wrap(wrap(" ")))
63
+ assert_template_result("\n but this is not " * (N + 1),
64
+ wrap('{% if true %} {% comment %} this is blank {% endcomment %} {% endif %}
65
+ {% if true %} but this is not {% endif %}'))
66
+ end
67
+
68
+ def test_assigns_are_blank
69
+ assert_template_result("", wrap(' {% assign foo = "bar" %} '))
70
+ end
71
+
72
+ def test_whitespace_is_blank
73
+ assert_template_result("", wrap(" "))
74
+ assert_template_result("", wrap("\t"))
75
+ end
76
+
77
+ def test_whitespace_is_not_blank_if_other_stuff_is_present
78
+ body = " x "
79
+ assert_template_result(body * (N + 1), wrap(body))
80
+ end
81
+
82
+ def test_increment_is_not_blank
83
+ assert_template_result(" 0" * 2 * (N + 1), wrap("{% assign foo = 0 %} {% increment foo %} {% decrement foo %}"))
84
+ end
85
+
86
+ def test_cycle_is_not_blank
87
+ assert_template_result(" " * ((N + 1) / 2) + " ", wrap("{% cycle ' ', ' ' %}"))
88
+ end
89
+
90
+ def test_raw_is_not_blank
91
+ assert_template_result(" " * (N + 1), wrap(" {% raw %} {% endraw %}"))
92
+ end
93
+
94
+ def test_include_is_blank
95
+ Liquid::Template.file_system = BlankTestFileSystem.new
96
+ assert_template_result "foobar" * (N + 1), wrap("{% include 'foobar' %}")
97
+ assert_template_result " foobar " * (N + 1), wrap("{% include ' foobar ' %}")
98
+ assert_template_result " " * (N + 1), wrap(" {% include ' ' %} ")
99
+ end
100
+
101
+ def test_case_is_blank
102
+ assert_template_result("", wrap(" {% assign foo = 'bar' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} {% endcase %} "))
103
+ assert_template_result("", wrap(" {% assign foo = 'else' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} {% endcase %} "))
104
+ assert_template_result(" x " * (N + 1), wrap(" {% assign foo = 'else' %} {% case foo %} {% when 'bar' %} {% when 'whatever' %} {% else %} x {% endcase %} "))
105
+ end
106
+ end