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,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