liquid 1.7.0

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 (61) hide show
  1. data/CHANGELOG +38 -0
  2. data/MIT-LICENSE +20 -0
  3. data/Manifest.txt +60 -0
  4. data/README +38 -0
  5. data/Rakefile +24 -0
  6. data/example/server/example_servlet.rb +37 -0
  7. data/example/server/liquid_servlet.rb +28 -0
  8. data/example/server/server.rb +12 -0
  9. data/example/server/templates/index.liquid +6 -0
  10. data/example/server/templates/products.liquid +45 -0
  11. data/init.rb +6 -0
  12. data/lib/extras/liquid_view.rb +27 -0
  13. data/lib/liquid.rb +66 -0
  14. data/lib/liquid/block.rb +101 -0
  15. data/lib/liquid/condition.rb +91 -0
  16. data/lib/liquid/context.rb +216 -0
  17. data/lib/liquid/document.rb +17 -0
  18. data/lib/liquid/drop.rb +48 -0
  19. data/lib/liquid/errors.rb +7 -0
  20. data/lib/liquid/extensions.rb +56 -0
  21. data/lib/liquid/file_system.rb +62 -0
  22. data/lib/liquid/htmltags.rb +64 -0
  23. data/lib/liquid/standardfilters.rb +125 -0
  24. data/lib/liquid/strainer.rb +43 -0
  25. data/lib/liquid/tag.rb +25 -0
  26. data/lib/liquid/tags/assign.rb +22 -0
  27. data/lib/liquid/tags/capture.rb +22 -0
  28. data/lib/liquid/tags/case.rb +68 -0
  29. data/lib/liquid/tags/comment.rb +9 -0
  30. data/lib/liquid/tags/cycle.rb +46 -0
  31. data/lib/liquid/tags/for.rb +81 -0
  32. data/lib/liquid/tags/if.rb +51 -0
  33. data/lib/liquid/tags/ifchanged.rb +20 -0
  34. data/lib/liquid/tags/include.rb +56 -0
  35. data/lib/liquid/tags/unless.rb +29 -0
  36. data/lib/liquid/template.rb +150 -0
  37. data/lib/liquid/variable.rb +39 -0
  38. data/test/block_test.rb +50 -0
  39. data/test/context_test.rb +340 -0
  40. data/test/drop_test.rb +139 -0
  41. data/test/error_handling_test.rb +65 -0
  42. data/test/extra/breakpoint.rb +547 -0
  43. data/test/extra/caller.rb +80 -0
  44. data/test/file_system_test.rb +30 -0
  45. data/test/filter_test.rb +98 -0
  46. data/test/helper.rb +20 -0
  47. data/test/html_tag_test.rb +24 -0
  48. data/test/if_else_test.rb +95 -0
  49. data/test/include_tag_test.rb +91 -0
  50. data/test/output_test.rb +121 -0
  51. data/test/parsing_quirks_test.rb +14 -0
  52. data/test/regexp_test.rb +39 -0
  53. data/test/security_test.rb +41 -0
  54. data/test/standard_filter_test.rb +101 -0
  55. data/test/standard_tag_test.rb +336 -0
  56. data/test/statements_test.rb +137 -0
  57. data/test/strainer_test.rb +16 -0
  58. data/test/template_test.rb +26 -0
  59. data/test/unless_else_test.rb +19 -0
  60. data/test/variable_test.rb +135 -0
  61. metadata +114 -0
@@ -0,0 +1,64 @@
1
+ module Liquid
2
+ class TableRow < Block
3
+ Syntax = /(\w+)\s+in\s+(#{VariableSignature}+)/
4
+
5
+ def initialize(markup, tokens)
6
+ super
7
+
8
+ if markup =~ Syntax
9
+ @variable_name = $1
10
+ @collection_name = $2
11
+ @attributes = {}
12
+ markup.scan(TagAttributes) do |key, value|
13
+ @attributes[key] = value
14
+ end
15
+ else
16
+ raise SyntaxError.new("Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3")
17
+ end
18
+ end
19
+
20
+ def render(context)
21
+ collection = context[@collection_name] or return ''
22
+
23
+ if @attributes['limit'] or @attributes['offset']
24
+ limit = context[@attributes['limit']] || -1
25
+ offset = context[@attributes['offset']] || 0
26
+ collection = collection[offset.to_i..(limit.to_i + offset.to_i - 1)]
27
+ end
28
+
29
+ length = collection.length
30
+
31
+ cols = context[@attributes['cols']].to_i
32
+
33
+ row = 1
34
+ col = 0
35
+
36
+ result = ["<tr class=\"row1\">\n"]
37
+ context.stack do
38
+
39
+ collection.each_with_index do |item, index|
40
+ context[@variable_name] = item
41
+ context['tablerowloop'] = {
42
+ 'length' => length,
43
+ 'index' => index + 1,
44
+ 'index0' => index,
45
+ 'rindex' => length - index,
46
+ 'rindex0' => length - index -1,
47
+ 'first' => (index == 0),
48
+ 'last' => (index == length - 1) }
49
+
50
+ result << ["<td class=\"col#{col += 1}\">"] + render_all(@nodelist, context) + ['</td>']
51
+
52
+ if col == cols and not (index == length - 1)
53
+ col = 0
54
+ result << ["</tr>\n<tr class=\"row#{row += 1}\">"]
55
+ end
56
+
57
+ end
58
+ end
59
+ result + ["</tr>\n"]
60
+ end
61
+ end
62
+
63
+ Template.register_tag('tablerow', TableRow)
64
+ end
@@ -0,0 +1,125 @@
1
+ module Liquid
2
+
3
+ module StandardFilters
4
+
5
+ # Return the size of an array or of an string
6
+ def size(input)
7
+
8
+ input.respond_to?(:size) ? input.size : 0
9
+ end
10
+
11
+ # convert a input string to DOWNCASE
12
+ def downcase(input)
13
+ input.to_s.downcase
14
+ end
15
+
16
+ # convert a input string to UPCASE
17
+ def upcase(input)
18
+ input.to_s.upcase
19
+ end
20
+
21
+ # capitalize words in the input centence
22
+ def capitalize(input)
23
+ input.to_s.capitalize
24
+ end
25
+
26
+ # Truncate a string down to x characters
27
+ def truncate(input, length = 50, truncate_string = "...")
28
+ if input.nil? then return end
29
+ l = length.to_i - truncate_string.length
30
+ l = 0 if l < 0
31
+ input.length > length.to_i ? input[0...l] + truncate_string : input
32
+ end
33
+
34
+ def truncatewords(input, words = 15, truncate_string = "...")
35
+ if input.nil? then return end
36
+ wordlist = input.to_s.split
37
+ l = words.to_i - 1
38
+ l = 0 if l < 0
39
+ wordlist.length > l ? wordlist[0..l].join(" ") + truncate_string : input
40
+ end
41
+
42
+ def strip_html(input)
43
+ input.to_s.gsub(/<.*?>/, '')
44
+ end
45
+
46
+ # Join elements of the array with certain character between them
47
+ def join(input, glue = ' ')
48
+ [input].flatten.join(glue)
49
+ end
50
+
51
+ # Sort elements of the array
52
+ def sort(input)
53
+ [input].flatten.sort
54
+ end
55
+
56
+ # Reformat a date
57
+ #
58
+ # %a - The abbreviated weekday name (``Sun'')
59
+ # %A - The full weekday name (``Sunday'')
60
+ # %b - The abbreviated month name (``Jan'')
61
+ # %B - The full month name (``January'')
62
+ # %c - The preferred local date and time representation
63
+ # %d - Day of the month (01..31)
64
+ # %H - Hour of the day, 24-hour clock (00..23)
65
+ # %I - Hour of the day, 12-hour clock (01..12)
66
+ # %j - Day of the year (001..366)
67
+ # %m - Month of the year (01..12)
68
+ # %M - Minute of the hour (00..59)
69
+ # %p - Meridian indicator (``AM'' or ``PM'')
70
+ # %S - Second of the minute (00..60)
71
+ # %U - Week number of the current year,
72
+ # starting with the first Sunday as the first
73
+ # day of the first week (00..53)
74
+ # %W - Week number of the current year,
75
+ # starting with the first Monday as the first
76
+ # day of the first week (00..53)
77
+ # %w - Day of the week (Sunday is 0, 0..6)
78
+ # %x - Preferred representation for the date alone, no time
79
+ # %X - Preferred representation for the time alone, no date
80
+ # %y - Year without a century (00..99)
81
+ # %Y - Year with century
82
+ # %Z - Time zone name
83
+ # %% - Literal ``%'' character
84
+ def date(input, format)
85
+
86
+ if format.to_s.empty?
87
+ return input.to_s
88
+ end
89
+
90
+ date = case input
91
+ when String
92
+ Time.parse(input)
93
+ when Date, Time, DateTime
94
+ input
95
+ else
96
+ return input
97
+ end
98
+
99
+ date.strftime(format.to_s)
100
+ rescue => e
101
+ input
102
+ end
103
+
104
+ # Get the first element of the passed in array
105
+ #
106
+ # Example:
107
+ # {{ product.images | first | to_img }}
108
+ #
109
+ def first(array)
110
+ array.first if array.respond_to?(:first)
111
+ end
112
+
113
+ # Get the last element of the passed in array
114
+ #
115
+ # Example:
116
+ # {{ product.images | last | to_img }}
117
+ #
118
+ def last(array)
119
+ array.last if array.respond_to?(:last)
120
+ end
121
+
122
+ end
123
+
124
+ Template.register_filter(StandardFilters)
125
+ end
@@ -0,0 +1,43 @@
1
+ module Liquid
2
+
3
+ # Strainer is the parent class for the filters system.
4
+ # New filters are mixed into the strainer class which is then instanciated for each liquid template render run.
5
+ #
6
+ # 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"]
10
+
11
+ @@filters = []
12
+
13
+ def initialize(context)
14
+ @context = context
15
+ end
16
+
17
+ def self.global_filter(filter)
18
+ raise StandardError, "Passed filter is not a module" unless filter.is_a?(Module)
19
+ @@filters << filter
20
+ end
21
+
22
+ def self.create(context)
23
+ strainer = Strainer.new(context)
24
+ @@filters.each { |m| strainer.extend(m) }
25
+ strainer
26
+ end
27
+
28
+ def respond_to?(method)
29
+ method_name = method.to_s
30
+ return false if method_name =~ /^__/
31
+ return false if @@required_methods.include?(method_name)
32
+ super
33
+ end
34
+
35
+ # remove all standard methods from the bucket so circumvent security
36
+ # problems
37
+ instance_methods.each do |m|
38
+ unless @@required_methods.include?(m)
39
+ undef_method m
40
+ end
41
+ end
42
+ end
43
+ end
data/lib/liquid/tag.rb ADDED
@@ -0,0 +1,25 @@
1
+ module Liquid
2
+
3
+ class Tag
4
+ attr_accessor :nodelist
5
+
6
+ def initialize(markup, tokens)
7
+ @markup = markup
8
+ parse(tokens)
9
+ end
10
+
11
+ def parse(tokens)
12
+ end
13
+
14
+ def name
15
+ self.class.name.downcase
16
+ end
17
+
18
+ def render(context)
19
+ ''
20
+ end
21
+ end
22
+
23
+
24
+ end
25
+
@@ -0,0 +1,22 @@
1
+ module Liquid
2
+ class Assign < Tag
3
+ Syntax = /(\w+)\s*=\s*(#{QuotedFragment}+)/
4
+
5
+ def initialize(markup, tokens)
6
+ if markup =~ Syntax
7
+ @to = $1
8
+ @from = $2
9
+ else
10
+ raise SyntaxError.new("Syntax Error in 'assign' - Valid syntax: assign [var] = [source]")
11
+ end
12
+ end
13
+
14
+ def render(context)
15
+ context[@to] = context[@from]
16
+ ''
17
+ end
18
+
19
+ end
20
+
21
+ Template.register_tag('assign', Assign)
22
+ end
@@ -0,0 +1,22 @@
1
+ module Liquid
2
+ class Capture < Block
3
+ Syntax = /(\w+)/
4
+
5
+ def initialize(markup, tokens)
6
+ if markup =~ Syntax
7
+ @to = $1
8
+ super
9
+ else
10
+ raise SyntaxError.new("Syntax Error in 'capture' - Valid syntax: capture [var]")
11
+ end
12
+ end
13
+
14
+ def render(context)
15
+ output = super
16
+ context[@to] = output.to_s
17
+ ''
18
+ end
19
+ end
20
+
21
+ Template.register_tag('capture', Capture)
22
+ end
@@ -0,0 +1,68 @@
1
+ module Liquid
2
+ class Case < Block
3
+ Syntax = /(#{QuotedFragment})/
4
+ WhenSyntax = /(#{QuotedFragment})/
5
+
6
+ def initialize(markup, tokens)
7
+ @blocks = []
8
+
9
+ if markup =~ Syntax
10
+ @left = $1
11
+ else
12
+ raise SyntaxError.new("Syntax Error in tag 'case' - Valid syntax: case [condition]")
13
+ end
14
+
15
+ push_block('case', markup)
16
+
17
+ super
18
+ end
19
+
20
+ def unknown_tag(tag, markup, tokens)
21
+ if ['when', 'else'].include?(tag)
22
+ push_block(tag, markup)
23
+ else
24
+ super
25
+ end
26
+ end
27
+
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
+
43
+
44
+ output
45
+ end.join
46
+ end
47
+
48
+ private
49
+
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] ")
58
+ end
59
+
60
+ @blocks.push(block)
61
+ @nodelist = block.attach(Array.new)
62
+ end
63
+
64
+
65
+ end
66
+
67
+ Template.register_tag('case', Case)
68
+ end
@@ -0,0 +1,9 @@
1
+ module Liquid
2
+ class Comment < Block
3
+ def render(context)
4
+ ''
5
+ end
6
+ end
7
+
8
+ Template.register_tag('comment', Comment)
9
+ end
@@ -0,0 +1,46 @@
1
+ module Liquid
2
+ class Cycle < Tag
3
+ SimpleSyntax = /#{QuotedFragment}/
4
+ NamedSyntax = /(#{QuotedFragment})\s*\:\s*(.*)/
5
+
6
+ def initialize(markup, tokens)
7
+ case markup
8
+ when NamedSyntax
9
+ @variables = variables_from_string($2)
10
+ @name = $1
11
+ when SimpleSyntax
12
+ @variables = variables_from_string(markup)
13
+ @name = "'#{@variables.to_s}'"
14
+ else
15
+ raise SyntaxError.new("Syntax Error in 'cycle' - Valid syntax: cycle [name :] var [, var2, var3 ...]")
16
+ end
17
+
18
+ end
19
+
20
+ def render(context)
21
+ context.registers[:cycle] ||= Hash.new(0)
22
+
23
+ context.stack do
24
+ key = context[@name]
25
+ iteration = context.registers[:cycle][key]
26
+ result = context[@variables[iteration]]
27
+ iteration += 1
28
+ iteration = 0 if iteration >= @variables.size
29
+ context.registers[:cycle][key] = iteration
30
+ result
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def variables_from_string(markup)
37
+ markup.split(',').collect do |var|
38
+ var =~ /\s*(#{QuotedFragment})\s*/
39
+ $1 ? $1 : nil
40
+ end.compact
41
+ end
42
+
43
+ end
44
+
45
+ Template.register_tag('cycle', Cycle)
46
+ end
@@ -0,0 +1,81 @@
1
+ module Liquid
2
+ class For < Block
3
+ Syntax = /(\w+)\s+in\s+(#{VariableSignature}+)/
4
+
5
+ def initialize(markup, tokens)
6
+ super
7
+
8
+ if markup =~ Syntax
9
+ @variable_name = $1
10
+ @collection_name = $2
11
+ @name = "#{$1}-#{$2}"
12
+ @attributes = {}
13
+ markup.scan(TagAttributes) do |key, value|
14
+ @attributes[key] = value
15
+ end
16
+ else
17
+ raise SyntaxError.new("Syntax Error in 'for loop' - Valid syntax: for [item] in [collection]")
18
+ end
19
+ end
20
+
21
+ def render(context)
22
+ context.registers[:for] ||= Hash.new(0)
23
+
24
+ collection = context[@collection_name]
25
+
26
+ return '' if collection.nil? or collection.empty?
27
+
28
+ range = (0..collection.length)
29
+
30
+ if @attributes['limit'] or @attributes['offset']
31
+
32
+
33
+ offset = 0
34
+ if @attributes['offset'] == 'continue'
35
+ offset = context.registers[:for][@name]
36
+ else
37
+ offset = context[@attributes['offset']] || 0
38
+ end
39
+
40
+ limit = context[@attributes['limit']]
41
+
42
+ range_end = limit ? offset + limit : collection.length
43
+
44
+ range = (offset..range_end-1)
45
+
46
+ # Save the range end in the registers so that future calls to
47
+ # offset:continue have something to pick up
48
+ context.registers[:for][@name] = range_end
49
+ end
50
+
51
+ result = []
52
+ segment = collection[range]
53
+ return '' if segment.nil?
54
+
55
+ context.stack do
56
+ length = segment.length
57
+
58
+ segment.each_with_index do |item, index|
59
+ context[@variable_name] = item
60
+ context['forloop'] = {
61
+ 'name' => @name,
62
+ 'length' => length,
63
+ 'index' => index + 1,
64
+ 'index0' => index,
65
+ 'rindex' => length - index,
66
+ 'rindex0' => length - index -1,
67
+ 'first' => (index == 0),
68
+ 'last' => (index == length - 1) }
69
+
70
+ result << render_all(@nodelist, context)
71
+ end
72
+ end
73
+
74
+ # Store position of last element we rendered. This allows us to do
75
+
76
+ result
77
+ end
78
+ end
79
+
80
+ Template.register_tag('for', For)
81
+ end