liquid 1.7.0 → 1.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. data/CHANGELOG +17 -15
  2. data/History.txt +44 -0
  3. data/MIT-LICENSE +2 -2
  4. data/Manifest.txt +6 -1
  5. data/{README → README.txt} +0 -0
  6. data/Rakefile +3 -3
  7. data/init.rb +5 -3
  8. data/lib/liquid.rb +8 -6
  9. data/lib/liquid/block.rb +6 -9
  10. data/lib/liquid/condition.rb +49 -17
  11. data/lib/liquid/context.rb +67 -41
  12. data/lib/liquid/errors.rb +8 -5
  13. data/lib/liquid/htmltags.rb +17 -7
  14. data/lib/liquid/module_ex.rb +62 -0
  15. data/lib/liquid/standardfilters.rb +39 -0
  16. data/lib/liquid/strainer.rb +20 -11
  17. data/lib/liquid/tag.rb +4 -3
  18. data/lib/liquid/tags/assign.rb +15 -4
  19. data/lib/liquid/tags/capture.rb +15 -2
  20. data/lib/liquid/tags/case.rb +51 -36
  21. data/lib/liquid/tags/cycle.rb +16 -2
  22. data/lib/liquid/tags/for.rb +45 -8
  23. data/lib/liquid/tags/if.rb +35 -7
  24. data/lib/liquid/tags/include.rb +2 -3
  25. data/lib/liquid/tags/unless.rb +6 -2
  26. data/lib/liquid/template.rb +13 -18
  27. data/lib/liquid/variable.rb +25 -12
  28. data/test/block_test.rb +8 -0
  29. data/test/condition_test.rb +109 -0
  30. data/test/context_test.rb +88 -10
  31. data/test/drop_test.rb +3 -1
  32. data/test/error_handling_test.rb +16 -3
  33. data/test/extra/breakpoint.rb +0 -0
  34. data/test/extra/caller.rb +0 -0
  35. data/test/filter_test.rb +3 -3
  36. data/test/html_tag_test.rb +7 -0
  37. data/test/if_else_test.rb +32 -0
  38. data/test/include_tag_test.rb +24 -1
  39. data/test/module_ex_test.rb +89 -0
  40. data/test/parsing_quirks_test.rb +15 -0
  41. data/test/regexp_test.rb +4 -3
  42. data/test/standard_filter_test.rb +27 -2
  43. data/test/standard_tag_test.rb +67 -20
  44. data/test/test_helper.rb +20 -0
  45. data/test/unless_else_test.rb +8 -0
  46. metadata +60 -46
data/lib/liquid/errors.rb CHANGED
@@ -1,7 +1,10 @@
1
1
  module Liquid
2
- class FilterNotFound < StandardError
3
- end
4
-
5
- class FileSystemError < StandardError
6
- end
2
+ class Error < ::StandardError; end
3
+
4
+ class ArgumentError < Error; end
5
+ class ContextError < Error; end
6
+ class FilterNotFound < Error; end
7
+ class FileSystemError < Error; end
8
+ class StandardError < Error; end
9
+ class SyntaxError < Error; end
7
10
  end
@@ -2,9 +2,7 @@ module Liquid
2
2
  class TableRow < Block
3
3
  Syntax = /(\w+)\s+in\s+(#{VariableSignature}+)/
4
4
 
5
- def initialize(markup, tokens)
6
- super
7
-
5
+ def initialize(tag_name, markup, tokens)
8
6
  if markup =~ Syntax
9
7
  @variable_name = $1
10
8
  @collection_name = $2
@@ -15,6 +13,8 @@ module Liquid
15
13
  else
16
14
  raise SyntaxError.new("Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3")
17
15
  end
16
+
17
+ super
18
18
  end
19
19
 
20
20
  def render(context)
@@ -42,16 +42,26 @@ module Liquid
42
42
  'length' => length,
43
43
  'index' => index + 1,
44
44
  'index0' => index,
45
+ 'col' => col + 1,
46
+ 'col0' => col,
47
+ 'index0' => index,
45
48
  'rindex' => length - index,
46
49
  'rindex0' => length - index -1,
47
50
  'first' => (index == 0),
48
- 'last' => (index == length - 1) }
51
+ 'last' => (index == length - 1),
52
+ 'col_first' => (col == 0),
53
+ 'col_last' => (col == cols - 1)
54
+ }
55
+
56
+
57
+ col += 1
49
58
 
50
- result << ["<td class=\"col#{col += 1}\">"] + render_all(@nodelist, context) + ['</td>']
59
+ result << ["<td class=\"col#{col}\">"] + render_all(@nodelist, context) + ['</td>']
51
60
 
52
- if col == cols and not (index == length - 1)
61
+ if col == cols and not (index == length - 1)
53
62
  col = 0
54
- result << ["</tr>\n<tr class=\"row#{row += 1}\">"]
63
+ row += 1
64
+ result << ["</tr>\n<tr class=\"row#{row}\">"]
55
65
  end
56
66
 
57
67
  end
@@ -0,0 +1,62 @@
1
+ # Copyright 2007 by Domizio Demichelis
2
+ # This library is free software. It may be used, redistributed and/or modified
3
+ # under the same terms as Ruby itself
4
+ #
5
+ # This extension is usesd in order to expose the object of the implementing class
6
+ # to liquid as it were a Drop. It also limits the liquid-callable methods of the instance
7
+ # to the allowed method passed with the liquid_methods call
8
+ # Example:
9
+ #
10
+ # class SomeClass
11
+ # liquid_methods :an_allowed_method
12
+ #
13
+ # def an_allowed_method
14
+ # 'this comes from an allowed method'
15
+ # end
16
+ # def unallowed_method
17
+ # 'this will never be an output'
18
+ # end
19
+ # end
20
+ #
21
+ # if you want to extend the drop to other methods you can defines more methods
22
+ # in the class <YourClass>::LiquidDropClass
23
+ #
24
+ # class SomeClass::LiquidDropClass
25
+ # def another_allowed_method
26
+ # 'and this from another allowed method'
27
+ # end
28
+ # end
29
+ # end
30
+ #
31
+ # usage:
32
+ # @something = SomeClass.new
33
+ #
34
+ # template:
35
+ # {{something.an_allowed_method}}{{something.unallowed_method}} {{something.another_allowed_method}}
36
+ #
37
+ # output:
38
+ # 'this comes from an allowed method and this from another allowed method'
39
+ #
40
+ # You can also chain associations, by adding the liquid_method call in the
41
+ # association models.
42
+ #
43
+ class Module
44
+
45
+ def liquid_methods(*allowed_methods)
46
+ drop_class = eval "class #{self.to_s}::LiquidDropClass < Liquid::Drop; self; end"
47
+ define_method :to_liquid do
48
+ drop_class.new(self)
49
+ end
50
+ drop_class.class_eval do
51
+ def initialize(object)
52
+ @object = object
53
+ end
54
+ allowed_methods.each do |sym|
55
+ define_method sym do
56
+ @object.send sym
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ end
@@ -1,3 +1,5 @@
1
+ require 'cgi'
2
+
1
3
  module Liquid
2
4
 
3
5
  module StandardFilters
@@ -22,6 +24,12 @@ module Liquid
22
24
  def capitalize(input)
23
25
  input.to_s.capitalize
24
26
  end
27
+
28
+ def escape(input)
29
+ CGI.escapeHTML(input) rescue input
30
+ end
31
+
32
+ alias_method :h, :escape
25
33
 
26
34
  # Truncate a string down to x characters
27
35
  def truncate(input, length = 50, truncate_string = "...")
@@ -41,8 +49,14 @@ module Liquid
41
49
 
42
50
  def strip_html(input)
43
51
  input.to_s.gsub(/<.*?>/, '')
52
+ end
53
+
54
+ # Remove all newlines from the string
55
+ def strip_newlines(input)
56
+ input.to_s.gsub(/\n/, '')
44
57
  end
45
58
 
59
+
46
60
  # Join elements of the array with certain character between them
47
61
  def join(input, glue = ' ')
48
62
  [input].flatten.join(glue)
@@ -51,6 +65,31 @@ module Liquid
51
65
  # Sort elements of the array
52
66
  def sort(input)
53
67
  [input].flatten.sort
68
+ end
69
+
70
+ # Replace occurrences of a string with another
71
+ def replace(input, string, replacement = '')
72
+ input.to_s.gsub(string, replacement)
73
+ end
74
+
75
+ # Replace the first occurrences of a string with another
76
+ def replace_first(input, string, replacement = '')
77
+ input.to_s.sub(string, replacement)
78
+ end
79
+
80
+ # remove a substring
81
+ def remove(input, string)
82
+ input.to_s.gsub(string, '')
83
+ end
84
+
85
+ # remove the first occurrences of a substring
86
+ def remove_first(input, string)
87
+ input.to_s.sub(string, '')
88
+ end
89
+
90
+ # Add <br /> tags in front of all newlines in input string
91
+ def newline_to_br(input)
92
+ input.to_s.gsub(/\n/, "<br />\n")
54
93
  end
55
94
 
56
95
  # Reformat a date
@@ -1,33 +1,42 @@
1
+ require 'set'
2
+
1
3
  module Liquid
4
+
5
+
6
+ parent_object = if defined? BlankObject
7
+ BlankObject
8
+ else
9
+ Object
10
+ end
2
11
 
3
12
  # Strainer is the parent class for the filters system.
4
13
  # New filters are mixed into the strainer class which is then instanciated for each liquid template render run.
5
14
  #
6
15
  # One of the strainer's responsibilities is to keep malicious method calls out
7
- class Strainer
8
-
9
- @@required_methods = ["__send__", "__id__", "respond_to?", "extend", "methods"]
16
+ class Strainer < parent_object #:nodoc:
17
+ INTERNAL_METHOD = /^__/
18
+ @@required_methods = Set.new([:__send__, :__id__, :respond_to?, :extend, :methods, :class])
10
19
 
11
- @@filters = []
20
+ @@filters = {}
12
21
 
13
22
  def initialize(context)
14
23
  @context = context
15
24
  end
16
25
 
17
26
  def self.global_filter(filter)
18
- raise StandardError, "Passed filter is not a module" unless filter.is_a?(Module)
19
- @@filters << filter
27
+ raise ArgumentError, "Passed filter is not a module" unless filter.is_a?(Module)
28
+ @@filters[filter.name] = filter
20
29
  end
21
30
 
22
31
  def self.create(context)
23
32
  strainer = Strainer.new(context)
24
- @@filters.each { |m| strainer.extend(m) }
33
+ @@filters.each { |k,m| strainer.extend(m) }
25
34
  strainer
26
35
  end
27
36
 
28
37
  def respond_to?(method)
29
38
  method_name = method.to_s
30
- return false if method_name =~ /^__/
39
+ return false if method_name =~ INTERNAL_METHOD
31
40
  return false if @@required_methods.include?(method_name)
32
41
  super
33
42
  end
@@ -35,9 +44,9 @@ module Liquid
35
44
  # remove all standard methods from the bucket so circumvent security
36
45
  # problems
37
46
  instance_methods.each do |m|
38
- unless @@required_methods.include?(m)
39
- undef_method m
47
+ unless @@required_methods.include?(m.to_sym)
48
+ undef_method m
40
49
  end
41
50
  end
42
51
  end
43
- end
52
+ end
data/lib/liquid/tag.rb CHANGED
@@ -3,8 +3,9 @@ module Liquid
3
3
  class Tag
4
4
  attr_accessor :nodelist
5
5
 
6
- def initialize(markup, tokens)
7
- @markup = markup
6
+ def initialize(tag_name, markup, tokens)
7
+ @tag_name = tag_name
8
+ @markup = markup
8
9
  parse(tokens)
9
10
  end
10
11
 
@@ -14,7 +15,7 @@ module Liquid
14
15
  def name
15
16
  self.class.name.downcase
16
17
  end
17
-
18
+
18
19
  def render(context)
19
20
  ''
20
21
  end
@@ -1,19 +1,30 @@
1
1
  module Liquid
2
+
3
+ # Assign sets a variable in your template.
4
+ #
5
+ # {% assign foo = 'monkey' %}
6
+ #
7
+ # You can then use the variable later in the page.
8
+ #
9
+ # {{ monkey }}
10
+ #
2
11
  class Assign < Tag
3
- Syntax = /(\w+)\s*=\s*(#{QuotedFragment}+)/
12
+ Syntax = /(#{VariableSignature}+)\s*=\s*(#{QuotedFragment}+)/
4
13
 
5
- def initialize(markup, tokens)
14
+ def initialize(tag_name, markup, tokens)
6
15
  if markup =~ Syntax
7
16
  @to = $1
8
17
  @from = $2
9
18
  else
10
19
  raise SyntaxError.new("Syntax Error in 'assign' - Valid syntax: assign [var] = [source]")
11
20
  end
21
+
22
+ super
12
23
  end
13
24
 
14
25
  def render(context)
15
- context[@to] = context[@from]
16
- ''
26
+ context.scopes.last[@to.to_s] = context[@from]
27
+ ''
17
28
  end
18
29
 
19
30
  end
@@ -1,14 +1,27 @@
1
1
  module Liquid
2
+
3
+ # Capture stores the result of a block into a variable without rendering it inplace.
4
+ #
5
+ # {% capture heading %}
6
+ # Monkeys!
7
+ # {% endcapture %}
8
+ # ...
9
+ # <h1>{{ monkeys }}</h1>
10
+ #
11
+ # Capture is useful for saving content for use later in your template, such as
12
+ # in a sidebar or footer.
13
+ #
2
14
  class Capture < Block
3
15
  Syntax = /(\w+)/
4
16
 
5
- def initialize(markup, tokens)
17
+ def initialize(tag_name, markup, tokens)
6
18
  if markup =~ Syntax
7
19
  @to = $1
8
- super
9
20
  else
10
21
  raise SyntaxError.new("Syntax Error in 'capture' - Valid syntax: capture [var]")
11
22
  end
23
+
24
+ super
12
25
  end
13
26
 
14
27
  def render(context)
@@ -1,9 +1,9 @@
1
1
  module Liquid
2
2
  class Case < Block
3
3
  Syntax = /(#{QuotedFragment})/
4
- WhenSyntax = /(#{QuotedFragment})/
4
+ WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/
5
5
 
6
- def initialize(markup, tokens)
6
+ def initialize(tag_name, markup, tokens)
7
7
  @blocks = []
8
8
 
9
9
  if markup =~ Syntax
@@ -11,58 +11,73 @@ module Liquid
11
11
  else
12
12
  raise SyntaxError.new("Syntax Error in tag 'case' - Valid syntax: case [condition]")
13
13
  end
14
-
15
- push_block('case', markup)
16
-
14
+
17
15
  super
18
16
  end
19
17
 
20
18
  def unknown_tag(tag, markup, tokens)
21
- if ['when', 'else'].include?(tag)
22
- push_block(tag, markup)
19
+ @nodelist = []
20
+ case tag
21
+ when 'when'
22
+ record_when_condition(markup)
23
+ when 'else'
24
+ record_else_condition(markup)
23
25
  else
24
26
  super
25
27
  end
26
28
  end
27
29
 
28
- def render(context)
29
- @blocks.inject([]) do |output, block|
30
-
31
- if block.else?
32
- return render_all(block.attachment, context) if output.empty? || output.join !~ /\S/
33
- else
34
-
35
- if block.evaluate(context)
36
- context.stack do
37
- output += render_all(block.attachment, context)
38
- end
39
- end
40
-
41
- end
42
-
30
+ def render(context)
31
+ context.stack do
32
+ execute_else_block = true
43
33
 
44
- output
45
- end.join
34
+ @blocks.inject([]) do |output, block|
35
+
36
+ if block.else?
37
+
38
+ return render_all(block.attachment, context) if execute_else_block
39
+
40
+ elsif block.evaluate(context)
41
+
42
+ execute_else_block = false
43
+ output += render_all(block.attachment, context)
44
+ end
45
+
46
+ output
47
+ end
48
+ end
46
49
  end
47
50
 
48
51
  private
49
52
 
50
- def push_block(tag, markup)
51
-
52
- block = if tag == 'else'
53
- ElseCondition.new
54
- elsif markup =~ WhenSyntax
55
- Condition.new(@left, '==', $1)
56
- else
57
- raise SyntaxError.new("Syntax Error in tag 'case' - Valid when condition: when [condition] ")
53
+ def record_when_condition(markup)
54
+ while markup
55
+ # Create a new nodelist and assign it to the new block
56
+ if not markup =~ WhenSyntax
57
+ raise SyntaxError.new("Syntax Error in tag 'case' - Valid when condition: {% when [condition] [or condition2...] %} ")
58
+ end
59
+
60
+ markup = $2
61
+
62
+ block = Condition.new(@left, '==', $1)
63
+ block.attach(@nodelist)
64
+ @blocks.push(block)
58
65
  end
59
-
60
- @blocks.push(block)
61
- @nodelist = block.attach(Array.new)
66
+ end
67
+
68
+ def record_else_condition(markup)
69
+
70
+ if not markup.strip.empty?
71
+ raise SyntaxError.new("Syntax Error in tag 'case' - Valid else condition: {% else %} (no parameters) ")
72
+ end
73
+
74
+ block = ElseCondition.new
75
+ block.attach(@nodelist)
76
+ @blocks << block
62
77
  end
63
78
 
64
79
 
65
80
  end
66
81
 
67
82
  Template.register_tag('case', Case)
68
- end
83
+ end