locomotivecms-liquid 2.6.0 → 4.0.0.alpha

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 +4 -4
  2. data/History.md +62 -5
  3. data/README.md +4 -4
  4. data/lib/liquid.rb +16 -12
  5. data/lib/liquid/block.rb +37 -118
  6. data/lib/liquid/block_body.rb +131 -0
  7. data/lib/liquid/condition.rb +28 -17
  8. data/lib/liquid/context.rb +94 -146
  9. data/lib/liquid/document.rb +16 -10
  10. data/lib/liquid/drop.rb +8 -5
  11. data/lib/liquid/drops/inherited_block_drop.rb +24 -0
  12. data/lib/liquid/errors.rb +44 -5
  13. data/lib/liquid/expression.rb +33 -0
  14. data/lib/liquid/file_system.rb +17 -6
  15. data/lib/liquid/i18n.rb +2 -2
  16. data/lib/liquid/interrupts.rb +1 -1
  17. data/lib/liquid/lexer.rb +11 -9
  18. data/lib/liquid/locales/en.yml +2 -4
  19. data/lib/liquid/parser.rb +2 -1
  20. data/lib/liquid/parser_switching.rb +31 -0
  21. data/lib/liquid/profiler.rb +162 -0
  22. data/lib/liquid/profiler/hooks.rb +23 -0
  23. data/lib/liquid/range_lookup.rb +22 -0
  24. data/lib/liquid/resource_limits.rb +23 -0
  25. data/lib/liquid/standardfilters.rb +142 -67
  26. data/lib/liquid/strainer.rb +14 -4
  27. data/lib/liquid/tag.rb +22 -41
  28. data/lib/liquid/tags/assign.rb +15 -10
  29. data/lib/liquid/tags/break.rb +1 -1
  30. data/lib/liquid/tags/capture.rb +7 -9
  31. data/lib/liquid/tags/case.rb +28 -19
  32. data/lib/liquid/tags/comment.rb +2 -2
  33. data/lib/liquid/tags/continue.rb +1 -4
  34. data/lib/liquid/tags/cycle.rb +10 -14
  35. data/lib/liquid/tags/decrement.rb +3 -4
  36. data/lib/liquid/tags/extends.rb +28 -44
  37. data/lib/liquid/tags/for.rb +64 -42
  38. data/lib/liquid/tags/if.rb +30 -19
  39. data/lib/liquid/tags/ifchanged.rb +4 -4
  40. data/lib/liquid/tags/include.rb +30 -20
  41. data/lib/liquid/tags/increment.rb +3 -8
  42. data/lib/liquid/tags/inherited_block.rb +54 -56
  43. data/lib/liquid/tags/raw.rb +18 -10
  44. data/lib/liquid/tags/table_row.rb +72 -0
  45. data/lib/liquid/tags/unless.rb +5 -7
  46. data/lib/liquid/template.rb +113 -53
  47. data/lib/liquid/token.rb +18 -0
  48. data/lib/liquid/utils.rb +13 -4
  49. data/lib/liquid/variable.rb +68 -50
  50. data/lib/liquid/variable_lookup.rb +78 -0
  51. data/lib/liquid/version.rb +1 -1
  52. data/test/fixtures/en_locale.yml +9 -0
  53. data/test/integration/assign_test.rb +48 -0
  54. data/test/integration/blank_test.rb +106 -0
  55. data/test/integration/capture_test.rb +50 -0
  56. data/test/integration/context_test.rb +32 -0
  57. data/test/integration/document_test.rb +19 -0
  58. data/test/integration/drop_test.rb +271 -0
  59. data/test/integration/error_handling_test.rb +207 -0
  60. data/test/integration/filter_test.rb +125 -0
  61. data/test/integration/hash_ordering_test.rb +23 -0
  62. data/test/integration/output_test.rb +116 -0
  63. data/test/integration/parsing_quirks_test.rb +119 -0
  64. data/test/integration/render_profiling_test.rb +154 -0
  65. data/test/integration/security_test.rb +64 -0
  66. data/test/integration/standard_filter_test.rb +379 -0
  67. data/test/integration/tags/break_tag_test.rb +16 -0
  68. data/test/integration/tags/continue_tag_test.rb +16 -0
  69. data/test/integration/tags/extends_tag_test.rb +104 -0
  70. data/test/integration/tags/for_tag_test.rb +375 -0
  71. data/test/integration/tags/if_else_tag_test.rb +169 -0
  72. data/test/integration/tags/include_tag_test.rb +234 -0
  73. data/test/integration/tags/increment_tag_test.rb +24 -0
  74. data/test/integration/tags/raw_tag_test.rb +25 -0
  75. data/test/integration/tags/standard_tag_test.rb +297 -0
  76. data/test/integration/tags/statements_test.rb +113 -0
  77. data/test/integration/tags/table_row_test.rb +63 -0
  78. data/test/integration/tags/unless_else_tag_test.rb +26 -0
  79. data/test/integration/template_test.rb +216 -0
  80. data/test/integration/variable_test.rb +82 -0
  81. data/test/test_helper.rb +83 -0
  82. data/test/unit/block_unit_test.rb +55 -0
  83. data/test/unit/condition_unit_test.rb +149 -0
  84. data/test/unit/context_unit_test.rb +482 -0
  85. data/test/unit/file_system_unit_test.rb +35 -0
  86. data/test/unit/i18n_unit_test.rb +37 -0
  87. data/test/unit/lexer_unit_test.rb +51 -0
  88. data/test/unit/module_ex_unit_test.rb +87 -0
  89. data/test/unit/parser_unit_test.rb +82 -0
  90. data/test/unit/regexp_unit_test.rb +44 -0
  91. data/test/unit/strainer_unit_test.rb +71 -0
  92. data/test/unit/tag_unit_test.rb +16 -0
  93. data/test/unit/tags/case_tag_unit_test.rb +10 -0
  94. data/test/unit/tags/for_tag_unit_test.rb +13 -0
  95. data/test/unit/tags/if_tag_unit_test.rb +8 -0
  96. data/test/unit/template_unit_test.rb +70 -0
  97. data/test/unit/tokenizer_unit_test.rb +38 -0
  98. data/test/unit/variable_unit_test.rb +150 -0
  99. metadata +144 -15
  100. data/lib/extras/liquid_view.rb +0 -51
  101. data/lib/liquid/htmltags.rb +0 -74
  102. data/lib/liquid/tags/default_content.rb +0 -21
  103. data/lib/locomotivecms-liquid.rb +0 -1
@@ -11,41 +11,40 @@ module Liquid
11
11
  # {{ user | link }}
12
12
  #
13
13
  class Variable
14
- FilterParser = /(?:#{FilterSeparator}|(?:\s*(?:#{QuotedFragment}|#{ArgumentSeparator})\s*)+)/o
15
- EasyParse = /^ *(\w+(?:\.\w+)*) *$/
14
+ FilterParser = /(?:\s+|#{QuotedFragment}|#{ArgumentSeparator})+/o
16
15
  attr_accessor :filters, :name, :warnings
16
+ attr_accessor :line_number
17
+ include ParserSwitching
17
18
 
18
19
  def initialize(markup, options = {})
19
20
  @markup = markup
20
21
  @name = nil
21
22
  @options = options || {}
22
-
23
23
 
24
- case @options[:error_mode] || Template.error_mode
25
- when :strict then strict_parse(markup)
26
- when :lax then lax_parse(markup)
27
- when :warn
28
- begin
29
- strict_parse(markup)
30
- rescue SyntaxError => e
31
- @warnings ||= []
32
- @warnings << e
33
- lax_parse(markup)
34
- end
35
- end
24
+ parse_with_selected_parser(markup)
25
+ end
26
+
27
+ def raw
28
+ @markup
29
+ end
30
+
31
+ def markup_context(markup)
32
+ "in \"{{#{markup}}}\""
36
33
  end
37
34
 
38
35
  def lax_parse(markup)
39
36
  @filters = []
40
- if match = markup.match(/\s*(#{QuotedFragment})(.*)/o)
41
- @name = match[1]
42
- if match[2].match(/#{FilterSeparator}\s*(.*)/o)
43
- filters = Regexp.last_match(1).scan(FilterParser)
37
+ if markup =~ /(#{QuotedFragment})(.*)/om
38
+ name_markup = $1
39
+ filter_markup = $2
40
+ @name = Expression.parse(name_markup)
41
+ if filter_markup =~ /#{FilterSeparator}\s*(.*)/om
42
+ filters = $1.scan(FilterParser)
44
43
  filters.each do |f|
45
- if matches = f.match(/\s*(\w+)/)
46
- filtername = matches[1]
44
+ if f =~ /\w+/
45
+ filtername = Regexp.last_match(0)
47
46
  filterargs = f.scan(/(?:#{FilterArgumentSeparator}|#{ArgumentSeparator})\s*((?:\w+\s*\:\s*)?#{QuotedFragment})/o).flatten
48
- @filters << [filtername, filterargs]
47
+ @filters << parse_filter_expressions(filtername, filterargs)
49
48
  end
50
49
  end
51
50
  end
@@ -53,26 +52,16 @@ module Liquid
53
52
  end
54
53
 
55
54
  def strict_parse(markup)
56
- # Very simple valid cases
57
- if markup =~ EasyParse
58
- @name = $1
59
- @filters = []
60
- return
61
- end
62
-
63
55
  @filters = []
64
56
  p = Parser.new(markup)
65
- # Could be just filters with no input
66
- @name = p.look(:pipe) ? '' : p.expression
57
+
58
+ @name = Expression.parse(p.expression)
67
59
  while p.consume?(:pipe)
68
60
  filtername = p.consume(:id)
69
61
  filterargs = p.consume?(:colon) ? parse_filterargs(p) : []
70
- @filters << [filtername, filterargs]
62
+ @filters << parse_filter_expressions(filtername, filterargs)
71
63
  end
72
64
  p.consume(:end_of_string)
73
- rescue SyntaxError => e
74
- e.message << " in \"{{#{markup}}}\""
75
- raise e
76
65
  end
77
66
 
78
67
  def parse_filterargs(p)
@@ -86,22 +75,51 @@ module Liquid
86
75
  end
87
76
 
88
77
  def render(context)
89
- return '' if @name.nil?
90
- @filters.inject(context[@name]) do |output, filter|
91
- filterargs = []
92
- keyword_args = {}
93
- filter[1].to_a.each do |a|
94
- if matches = a.match(/\A#{TagAttributes}\z/o)
95
- keyword_args[matches[1]] = context[matches[2]]
96
- else
97
- filterargs << context[a]
98
- end
78
+ @filters.inject(context.evaluate(@name)) do |output, (filter_name, filter_args, filter_kwargs)|
79
+ filter_args = evaluate_filter_expressions(context, filter_args, filter_kwargs)
80
+ output = context.invoke(filter_name, output, *filter_args)
81
+ end.tap{ |obj| taint_check(obj) }
82
+ end
83
+
84
+ private
85
+
86
+ def parse_filter_expressions(filter_name, unparsed_args)
87
+ filter_args = []
88
+ keyword_args = {}
89
+ unparsed_args.each do |a|
90
+ if matches = a.match(/\A#{TagAttributes}\z/o)
91
+ keyword_args[matches[1]] = Expression.parse(matches[2])
92
+ else
93
+ filter_args << Expression.parse(a)
99
94
  end
100
- filterargs << keyword_args unless keyword_args.empty?
101
- begin
102
- output = context.invoke(filter[0], output, *filterargs)
103
- rescue FilterNotFound
104
- raise FilterNotFound, "Error - filter '#{filter[0]}' in '#{@markup.strip}' could not be found."
95
+ end
96
+ result = [filter_name, filter_args]
97
+ result << keyword_args unless keyword_args.empty?
98
+ result
99
+ end
100
+
101
+ def evaluate_filter_expressions(context, filter_args, filter_kwargs)
102
+ parsed_args = filter_args.map{ |expr| context.evaluate(expr) }
103
+ if filter_kwargs
104
+ parsed_kwargs = {}
105
+ filter_kwargs.each do |key, expr|
106
+ parsed_kwargs[key] = context.evaluate(expr)
107
+ end
108
+ parsed_args << parsed_kwargs
109
+ end
110
+ parsed_args
111
+ end
112
+
113
+ def taint_check(obj)
114
+ if obj.tainted?
115
+ @markup =~ QuotedFragment
116
+ name = Regexp.last_match(0)
117
+ case Template.taint_mode
118
+ when :warn
119
+ @warnings ||= []
120
+ @warnings << "variable '#{name}' is tainted and was not escaped"
121
+ when :error
122
+ raise TaintedError, "Error - variable '#{name}' is tainted and was not escaped"
105
123
  end
106
124
  end
107
125
  end
@@ -0,0 +1,78 @@
1
+ module Liquid
2
+ class VariableLookup
3
+ SQUARE_BRACKETED = /\A\[(.*)\]\z/m
4
+ COMMAND_METHODS = ['size'.freeze, 'first'.freeze, 'last'.freeze]
5
+
6
+ def self.parse(markup)
7
+ new(markup)
8
+ end
9
+
10
+ def initialize(markup)
11
+ lookups = markup.scan(VariableParser)
12
+
13
+ name = lookups.shift
14
+ if name =~ SQUARE_BRACKETED
15
+ name = Expression.parse($1)
16
+ end
17
+ @name = name
18
+
19
+ @lookups = lookups
20
+ @command_flags = 0
21
+
22
+ @lookups.each_index do |i|
23
+ lookup = lookups[i]
24
+ if lookup =~ SQUARE_BRACKETED
25
+ lookups[i] = Expression.parse($1)
26
+ elsif COMMAND_METHODS.include?(lookup)
27
+ @command_flags |= 1 << i
28
+ end
29
+ end
30
+ end
31
+
32
+ def evaluate(context)
33
+ name = context.evaluate(@name)
34
+ object = context.find_variable(name)
35
+
36
+ @lookups.each_index do |i|
37
+ key = context.evaluate(@lookups[i])
38
+
39
+ # If object is a hash- or array-like object we look for the
40
+ # presence of the key and if its available we return it
41
+ if object.respond_to?(:[]) &&
42
+ ((object.respond_to?(:has_key?) && object.has_key?(key)) ||
43
+ (object.respond_to?(:fetch) && key.is_a?(Integer)))
44
+
45
+ # if its a proc we will replace the entry with the proc
46
+ res = context.lookup_and_evaluate(object, key)
47
+ object = res.to_liquid
48
+
49
+ # Some special cases. If the part wasn't in square brackets and
50
+ # no key with the same name was found we interpret following calls
51
+ # as commands and call them on the current object
52
+ elsif @command_flags & (1 << i) != 0 && object.respond_to?(key)
53
+ object = object.send(key).to_liquid
54
+
55
+ # No key was present with the desired value and it wasn't one of the directly supported
56
+ # keywords either. The only thing we got left is to return nil
57
+ else
58
+ return nil
59
+ end
60
+
61
+ # If we are dealing with a drop here we have to
62
+ object.context = context if object.respond_to?(:context=)
63
+ end
64
+
65
+ object
66
+ end
67
+
68
+ def ==(other)
69
+ self.class == other.class && self.state == other.state
70
+ end
71
+
72
+ protected
73
+
74
+ def state
75
+ [@name, @lookups, @command_flags]
76
+ end
77
+ end
78
+ end
@@ -1,4 +1,4 @@
1
1
  # encoding: utf-8
2
2
  module Liquid
3
- VERSION = "2.6.0"
3
+ VERSION = "4.0.0.alpha"
4
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, context)
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(%q{{% 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
@@ -0,0 +1,50 @@
1
+ require 'test_helper'
2
+
3
+ class CaptureTest < Minitest::Test
4
+ include Liquid
5
+
6
+ def test_captures_block_content_in_variable
7
+ assert_template_result("test string", "{% capture 'var' %}test string{% endcapture %}{{var}}", {})
8
+ end
9
+
10
+ def test_capture_with_hyphen_in_variable_name
11
+ template_source = <<-END_TEMPLATE
12
+ {% capture this-thing %}Print this-thing{% endcapture %}
13
+ {{ this-thing }}
14
+ END_TEMPLATE
15
+ template = Template.parse(template_source)
16
+ rendered = template.render!
17
+ assert_equal "Print this-thing", rendered.strip
18
+ end
19
+
20
+ def test_capture_to_variable_from_outer_scope_if_existing
21
+ template_source = <<-END_TEMPLATE
22
+ {% assign var = '' %}
23
+ {% if true %}
24
+ {% capture var %}first-block-string{% endcapture %}
25
+ {% endif %}
26
+ {% if true %}
27
+ {% capture var %}test-string{% endcapture %}
28
+ {% endif %}
29
+ {{var}}
30
+ END_TEMPLATE
31
+ template = Template.parse(template_source)
32
+ rendered = template.render!
33
+ assert_equal "test-string", rendered.gsub(/\s/, '')
34
+ end
35
+
36
+ def test_assigning_from_capture
37
+ template_source = <<-END_TEMPLATE
38
+ {% assign first = '' %}
39
+ {% assign second = '' %}
40
+ {% for number in (1..3) %}
41
+ {% capture first %}{{number}}{% endcapture %}
42
+ {% assign second = first %}
43
+ {% endfor %}
44
+ {{ first }}-{{ second }}
45
+ END_TEMPLATE
46
+ template = Template.parse(template_source)
47
+ rendered = template.render!
48
+ assert_equal "3-3", rendered.gsub(/\s/, '')
49
+ end
50
+ end # CaptureTest