locomotivecms-liquid 2.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/History.md +108 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +75 -0
  5. data/lib/extras/liquid_view.rb +51 -0
  6. data/lib/liquid/block.rb +160 -0
  7. data/lib/liquid/condition.rb +120 -0
  8. data/lib/liquid/context.rb +268 -0
  9. data/lib/liquid/document.rb +18 -0
  10. data/lib/liquid/drop.rb +74 -0
  11. data/lib/liquid/errors.rb +21 -0
  12. data/lib/liquid/extensions.rb +62 -0
  13. data/lib/liquid/file_system.rb +62 -0
  14. data/lib/liquid/htmltags.rb +74 -0
  15. data/lib/liquid/i18n.rb +39 -0
  16. data/lib/liquid/interrupts.rb +17 -0
  17. data/lib/liquid/lexer.rb +51 -0
  18. data/lib/liquid/locales/en.yml +25 -0
  19. data/lib/liquid/module_ex.rb +62 -0
  20. data/lib/liquid/parser.rb +89 -0
  21. data/lib/liquid/standardfilters.rb +285 -0
  22. data/lib/liquid/strainer.rb +53 -0
  23. data/lib/liquid/tag.rb +61 -0
  24. data/lib/liquid/tags/assign.rb +36 -0
  25. data/lib/liquid/tags/break.rb +21 -0
  26. data/lib/liquid/tags/capture.rb +40 -0
  27. data/lib/liquid/tags/case.rb +77 -0
  28. data/lib/liquid/tags/comment.rb +16 -0
  29. data/lib/liquid/tags/continue.rb +21 -0
  30. data/lib/liquid/tags/cycle.rb +61 -0
  31. data/lib/liquid/tags/decrement.rb +39 -0
  32. data/lib/liquid/tags/default_content.rb +21 -0
  33. data/lib/liquid/tags/extends.rb +79 -0
  34. data/lib/liquid/tags/for.rb +167 -0
  35. data/lib/liquid/tags/if.rb +100 -0
  36. data/lib/liquid/tags/ifchanged.rb +20 -0
  37. data/lib/liquid/tags/include.rb +97 -0
  38. data/lib/liquid/tags/increment.rb +36 -0
  39. data/lib/liquid/tags/inherited_block.rb +101 -0
  40. data/lib/liquid/tags/raw.rb +22 -0
  41. data/lib/liquid/tags/unless.rb +33 -0
  42. data/lib/liquid/template.rb +213 -0
  43. data/lib/liquid/utils.rb +30 -0
  44. data/lib/liquid/variable.rb +109 -0
  45. data/lib/liquid/version.rb +4 -0
  46. data/lib/liquid.rb +72 -0
  47. data/lib/locomotivecms-liquid.rb +1 -0
  48. metadata +94 -0
@@ -0,0 +1,77 @@
1
+ module Liquid
2
+ class Case < Block
3
+ Syntax = /(#{QuotedFragment})/o
4
+ WhenSyntax = /(#{QuotedFragment})(?:(?:\s+or\s+|\s*\,\s*)(#{QuotedFragment}.*))?/o
5
+
6
+
7
+ def initialize(tag_name, markup, tokens, options)
8
+ @blocks = []
9
+
10
+ if markup =~ Syntax
11
+ @left = $1
12
+ else
13
+ raise SyntaxError.new(options[:locale].t("errors.syntax.case"), options[:line])
14
+ end
15
+
16
+ super
17
+ end
18
+
19
+ def unknown_tag(tag, markup, tokens)
20
+ @nodelist = []
21
+ case tag
22
+ when 'when'
23
+ record_when_condition(markup)
24
+ when 'else'
25
+ record_else_condition(markup)
26
+ else
27
+ super
28
+ end
29
+ end
30
+
31
+ def render(context)
32
+ context.stack do
33
+ execute_else_block = true
34
+
35
+ output = ''
36
+ @blocks.each do |block|
37
+ if block.else?
38
+ return render_all(block.attachment, context) if execute_else_block
39
+ elsif block.evaluate(context)
40
+ execute_else_block = false
41
+ output << render_all(block.attachment, context)
42
+ end
43
+ end
44
+ output
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def record_when_condition(markup)
51
+ while markup
52
+ # Create a new nodelist and assign it to the new block
53
+ if not markup =~ WhenSyntax
54
+ raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_when"), line)
55
+ end
56
+
57
+ markup = $2
58
+
59
+ block = Condition.new(@left, '==', $1)
60
+ block.attach(@nodelist)
61
+ @blocks.push(block)
62
+ end
63
+ end
64
+
65
+ def record_else_condition(markup)
66
+ if not markup.strip.empty?
67
+ raise SyntaxError.new(options[:locale].t("errors.syntax.case_invalid_else"), line)
68
+ end
69
+
70
+ block = ElseCondition.new
71
+ block.attach(@nodelist)
72
+ @blocks << block
73
+ end
74
+ end
75
+
76
+ Template.register_tag('case', Case)
77
+ end
@@ -0,0 +1,16 @@
1
+ module Liquid
2
+ class Comment < Block
3
+ def render(context)
4
+ ''
5
+ end
6
+
7
+ def unknown_tag(tag, markup, tokens)
8
+ end
9
+
10
+ def blank?
11
+ true
12
+ end
13
+ end
14
+
15
+ Template.register_tag('comment', Comment)
16
+ end
@@ -0,0 +1,21 @@
1
+ module Liquid
2
+
3
+ # Continue tag to be used to break out of a for loop.
4
+ #
5
+ # == Basic Usage:
6
+ # {% for item in collection %}
7
+ # {% if item.condition %}
8
+ # {% continue %}
9
+ # {% endif %}
10
+ # {% endfor %}
11
+ #
12
+ class Continue < Tag
13
+
14
+ def interrupt
15
+ ContinueInterrupt.new
16
+ end
17
+
18
+ end
19
+
20
+ Template.register_tag('continue', Continue)
21
+ end
@@ -0,0 +1,61 @@
1
+ module Liquid
2
+ # Cycle is usually used within a loop to alternate between values, like colors or DOM classes.
3
+ #
4
+ # {% for item in items %}
5
+ # <div class="{% cycle 'red', 'green', 'blue' %}"> {{ item }} </div>
6
+ # {% end %}
7
+ #
8
+ # <div class="red"> Item one </div>
9
+ # <div class="green"> Item two </div>
10
+ # <div class="blue"> Item three </div>
11
+ # <div class="red"> Item four </div>
12
+ # <div class="green"> Item five</div>
13
+ #
14
+ class Cycle < Tag
15
+ SimpleSyntax = /^#{QuotedFragment}+/o
16
+ NamedSyntax = /^(#{QuotedFragment})\s*\:\s*(.*)/o
17
+
18
+ def initialize(tag_name, markup, tokens, options)
19
+ case markup
20
+ when NamedSyntax
21
+ @variables = variables_from_string($2)
22
+ @name = $1
23
+ when SimpleSyntax
24
+ @variables = variables_from_string(markup)
25
+ @name = "'#{@variables.to_s}'"
26
+ else
27
+ raise SyntaxError.new(options[:locale].t("errors.syntax.cycle"), options[:line])
28
+ end
29
+ super
30
+ end
31
+
32
+ def render(context)
33
+ context.registers[:cycle] ||= Hash.new(0)
34
+
35
+ context.stack do
36
+ key = context[@name]
37
+ iteration = context.registers[:cycle][key]
38
+ result = context[@variables[iteration]]
39
+ iteration += 1
40
+ iteration = 0 if iteration >= @variables.size
41
+ context.registers[:cycle][key] = iteration
42
+ result
43
+ end
44
+ end
45
+
46
+ def blank?
47
+ false
48
+ end
49
+
50
+ private
51
+
52
+ def variables_from_string(markup)
53
+ markup.split(',').collect do |var|
54
+ var =~ /\s*(#{QuotedFragment})\s*/o
55
+ $1 ? $1 : nil
56
+ end.compact
57
+ end
58
+ end
59
+
60
+ Template.register_tag('cycle', Cycle)
61
+ end
@@ -0,0 +1,39 @@
1
+ module Liquid
2
+
3
+ # decrement is used in a place where one needs to insert a counter
4
+ # into a template, and needs the counter to survive across
5
+ # multiple instantiations of the template.
6
+ # NOTE: decrement is a pre-decrement, --i,
7
+ # while increment is post: i++.
8
+ #
9
+ # (To achieve the survival, the application must keep the context)
10
+ #
11
+ # if the variable does not exist, it is created with value 0.
12
+
13
+ # Hello: {% decrement variable %}
14
+ #
15
+ # gives you:
16
+ #
17
+ # Hello: -1
18
+ # Hello: -2
19
+ # Hello: -3
20
+ #
21
+ class Decrement < Tag
22
+ def initialize(tag_name, markup, tokens, options)
23
+ @variable = markup.strip
24
+
25
+ super
26
+ end
27
+
28
+ def render(context)
29
+ value = context.environments.first[@variable] ||= 0
30
+ value = value - 1
31
+ context.environments.first[@variable] = value
32
+ value.to_s
33
+ end
34
+
35
+ private
36
+ end
37
+
38
+ Template.register_tag('decrement', Decrement)
39
+ end
@@ -0,0 +1,21 @@
1
+ module Liquid
2
+
3
+ # InheritedContent pulls out the content from child templates that isnt defined in blocks
4
+ #
5
+ # {% defaultcontent %}
6
+ #
7
+ class DefaultContent < Tag
8
+ def initialize(tag_name, markup, tokens, context)
9
+ super
10
+ end
11
+
12
+ def render(context)
13
+ context.stack do
14
+ "HELLO"
15
+ end
16
+ end
17
+ end
18
+
19
+
20
+ Template.register_tag('defaultcontent', DefaultContent)
21
+ end
@@ -0,0 +1,79 @@
1
+ module Liquid
2
+
3
+ # Extends allows designer to use template inheritance
4
+ #
5
+ # {% extends home %}
6
+ # {% block content }Hello world{% endblock %}
7
+ #
8
+ class Extends < Block
9
+ Syntax = /(#{QuotedFragment}+)/
10
+
11
+ def initialize(tag_name, markup, tokens, options)
12
+ if markup =~ Syntax
13
+ @template_name = $1.gsub(/["']/o, '').strip
14
+ else
15
+ raise(SyntaxError.new(options[:locale].t("errors.syntax.extends")), options[:line])
16
+ end
17
+
18
+ @options = options
19
+
20
+ @parent_template = parse_parent_template
21
+
22
+ prepare_parsing
23
+
24
+ super
25
+
26
+ end_tag
27
+ end
28
+
29
+ def prepare_parsing
30
+ @options.merge!(:blocks => self.find_blocks(@parent_template.root.nodelist))
31
+ end
32
+
33
+ def end_tag
34
+ # replace the nodelist by the new one
35
+ @nodelist = @parent_template.root.nodelist.clone
36
+
37
+ @parent_template = nil # no need to keep it
38
+ end
39
+
40
+ def blank?
41
+ false
42
+ end
43
+
44
+ protected
45
+
46
+ def find_blocks(nodelist, blocks = {})
47
+ if nodelist && nodelist.any?
48
+ 0.upto(nodelist.size - 1).each do |index|
49
+ node = nodelist[index]
50
+
51
+ if node.respond_to?(:call_super) # inherited block !
52
+ new_node = node.class.clone_block(node)
53
+
54
+ nodelist.insert(index, new_node)
55
+ nodelist.delete_at(index + 1)
56
+
57
+ blocks[node.name] = new_node
58
+ end
59
+ if node.respond_to?(:nodelist)
60
+ self.find_blocks(node.nodelist, blocks) # FIXME: find nested blocks too
61
+ end
62
+ end
63
+ end
64
+ blocks
65
+ end
66
+
67
+ private
68
+
69
+ def parse_parent_template
70
+ source = Template.file_system.read_template_file(@template_name)
71
+ Template.parse(source)
72
+ end
73
+
74
+ def assert_missing_delimitation!
75
+ end
76
+ end
77
+
78
+ Template.register_tag('extends', Extends)
79
+ end
@@ -0,0 +1,167 @@
1
+ module Liquid
2
+
3
+ # "For" iterates over an array or collection.
4
+ # Several useful variables are available to you within the loop.
5
+ #
6
+ # == Basic usage:
7
+ # {% for item in collection %}
8
+ # {{ forloop.index }}: {{ item.name }}
9
+ # {% endfor %}
10
+ #
11
+ # == Advanced usage:
12
+ # {% for item in collection %}
13
+ # <div {% if forloop.first %}class="first"{% endif %}>
14
+ # Item {{ forloop.index }}: {{ item.name }}
15
+ # </div>
16
+ # {% else %}
17
+ # There is nothing in the collection.
18
+ # {% endfor %}
19
+ #
20
+ # You can also define a limit and offset much like SQL. Remember
21
+ # that offset starts at 0 for the first item.
22
+ #
23
+ # {% for item in collection limit:5 offset:10 %}
24
+ # {{ item.name }}
25
+ # {% end %}
26
+ #
27
+ # To reverse the for loop simply use {% for item in collection reversed %}
28
+ #
29
+ # == Available variables:
30
+ #
31
+ # forloop.name:: 'item-collection'
32
+ # forloop.length:: Length of the loop
33
+ # forloop.index:: The current item's position in the collection;
34
+ # forloop.index starts at 1.
35
+ # This is helpful for non-programmers who start believe
36
+ # the first item in an array is 1, not 0.
37
+ # forloop.index0:: The current item's position in the collection
38
+ # where the first item is 0
39
+ # forloop.rindex:: Number of items remaining in the loop
40
+ # (length - index) where 1 is the last item.
41
+ # forloop.rindex0:: Number of items remaining in the loop
42
+ # where 0 is the last item.
43
+ # forloop.first:: Returns true if the item is the first item.
44
+ # forloop.last:: Returns true if the item is the last item.
45
+ #
46
+ class For < Block
47
+ Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
48
+
49
+ def initialize(tag_name, markup, tokens, options)
50
+ parse_with_selected_parser(markup)
51
+ @nodelist = @for_block = []
52
+ super
53
+ end
54
+
55
+ def unknown_tag(tag, markup, tokens)
56
+ return super unless tag == 'else'
57
+ @nodelist = @else_block = []
58
+ end
59
+
60
+ def render(context)
61
+ context.registers[:for] ||= Hash.new(0)
62
+
63
+ collection = context[@collection_name]
64
+ collection = collection.to_a if collection.is_a?(Range)
65
+
66
+ # Maintains Ruby 1.8.7 String#each behaviour on 1.9
67
+ return render_else(context) unless iterable?(collection)
68
+
69
+ from = if @attributes['offset'] == 'continue'
70
+ context.registers[:for][@name].to_i
71
+ else
72
+ context[@attributes['offset']].to_i
73
+ end
74
+
75
+ limit = context[@attributes['limit']]
76
+ to = limit ? limit.to_i + from : nil
77
+
78
+
79
+ segment = Utils.slice_collection_using_each(collection, from, to)
80
+
81
+ return render_else(context) if segment.empty?
82
+
83
+ segment.reverse! if @reversed
84
+
85
+ result = ''
86
+
87
+ length = segment.length
88
+
89
+ # Store our progress through the collection for the continue flag
90
+ context.registers[:for][@name] = from + segment.length
91
+
92
+ context.stack do
93
+ segment.each_with_index do |item, index|
94
+ context[@variable_name] = item
95
+ context['forloop'] = {
96
+ 'name' => @name,
97
+ 'length' => length,
98
+ 'index' => index + 1,
99
+ 'index0' => index,
100
+ 'rindex' => length - index,
101
+ 'rindex0' => length - index - 1,
102
+ 'first' => (index == 0),
103
+ 'last' => (index == length - 1) }
104
+
105
+ result << render_all(@for_block, context)
106
+
107
+ # Handle any interrupts if they exist.
108
+ if context.has_interrupt?
109
+ interrupt = context.pop_interrupt
110
+ break if interrupt.is_a? BreakInterrupt
111
+ next if interrupt.is_a? ContinueInterrupt
112
+ end
113
+ end
114
+ end
115
+ result
116
+ end
117
+
118
+ protected
119
+
120
+ def lax_parse(markup)
121
+ if markup =~ Syntax
122
+ @variable_name = $1
123
+ @collection_name = $2
124
+ @name = "#{$1}-#{$2}"
125
+ @reversed = $3
126
+ @attributes = {}
127
+ markup.scan(TagAttributes) do |key, value|
128
+ @attributes[key] = value
129
+ end
130
+ else
131
+ raise SyntaxError.new(options[:locale].t("errors.syntax.for"), line)
132
+ end
133
+ end
134
+
135
+ def strict_parse(markup)
136
+ p = Parser.new(markup)
137
+ @variable_name = p.consume(:id)
138
+ raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_in"), line) unless p.id?('in')
139
+ @collection_name = p.expression
140
+ @name = "#{@variable_name}-#{@collection_name}"
141
+ @reversed = p.id?('reversed')
142
+
143
+ @attributes = {}
144
+ while p.look(:id) && p.look(:colon, 1)
145
+ unless attribute = p.id?('limit') || p.id?('offset')
146
+ raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_attribute"), line)
147
+ end
148
+ p.consume
149
+ val = p.expression
150
+ @attributes[attribute] = val
151
+ end
152
+ p.consume(:end_of_string)
153
+ end
154
+
155
+ private
156
+
157
+ def render_else(context)
158
+ return @else_block ? [render_all(@else_block, context)] : ''
159
+ end
160
+
161
+ def iterable?(collection)
162
+ collection.respond_to?(:each) || Utils.non_blank_string?(collection)
163
+ end
164
+ end
165
+
166
+ Template.register_tag('for', For)
167
+ end
@@ -0,0 +1,100 @@
1
+ module Liquid
2
+ # If is the conditional block
3
+ #
4
+ # {% if user.admin %}
5
+ # Admin user!
6
+ # {% else %}
7
+ # Not admin user
8
+ # {% endif %}
9
+ #
10
+ # There are {% if count < 5 %} less {% else %} more {% endif %} items than you need.
11
+ #
12
+ class If < Block
13
+ Syntax = /(#{QuotedFragment})\s*([=!<>a-z_]+)?\s*(#{QuotedFragment})?/o
14
+ ExpressionsAndOperators = /(?:\b(?:\s?and\s?|\s?or\s?)\b|(?:\s*(?!\b(?:\s?and\s?|\s?or\s?)\b)(?:#{QuotedFragment}|\S+)\s*)+)/o
15
+
16
+ def initialize(tag_name, markup, tokens, options)
17
+ @blocks = []
18
+ push_block('if', markup)
19
+ super
20
+ end
21
+
22
+ def unknown_tag(tag, markup, tokens)
23
+ if ['elsif', 'else'].include?(tag)
24
+ push_block(tag, markup)
25
+ else
26
+ super
27
+ end
28
+ end
29
+
30
+ def render(context)
31
+ context.stack do
32
+ @blocks.each do |block|
33
+ if block.evaluate(context)
34
+ return render_all(block.attachment, context)
35
+ end
36
+ end
37
+ ''
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def push_block(tag, markup)
44
+ block = if tag == 'else'
45
+ ElseCondition.new
46
+ else
47
+ parse_with_selected_parser(markup)
48
+ end
49
+
50
+ @blocks.push(block)
51
+ @nodelist = block.attach(Array.new)
52
+ end
53
+
54
+ def lax_parse(markup)
55
+ expressions = markup.scan(ExpressionsAndOperators).reverse
56
+ raise(SyntaxError.new(options[:locale].t("errors.syntax.if")), line) unless expressions.shift =~ Syntax
57
+
58
+ condition = Condition.new($1, $2, $3)
59
+
60
+ while not expressions.empty?
61
+ operator = (expressions.shift).to_s.strip
62
+
63
+ raise(SyntaxError.new(options[:locale].t("errors.syntax.if")), line) unless expressions.shift.to_s =~ Syntax
64
+
65
+ new_condition = Condition.new($1, $2, $3)
66
+ new_condition.send(operator.to_sym, condition)
67
+ condition = new_condition
68
+ end
69
+
70
+ condition
71
+ end
72
+
73
+ def strict_parse(markup)
74
+ p = Parser.new(markup)
75
+
76
+ condition = parse_comparison(p)
77
+
78
+ while op = (p.id?('and') || p.id?('or'))
79
+ new_cond = parse_comparison(p)
80
+ new_cond.send(op, condition)
81
+ condition = new_cond
82
+ end
83
+ p.consume(:end_of_string)
84
+
85
+ condition
86
+ end
87
+
88
+ def parse_comparison(p)
89
+ a = p.expression
90
+ if op = p.consume?(:comparison)
91
+ b = p.expression
92
+ Condition.new(a, op, b)
93
+ else
94
+ Condition.new(a)
95
+ end
96
+ end
97
+ end
98
+
99
+ Template.register_tag('if', If)
100
+ end
@@ -0,0 +1,20 @@
1
+ module Liquid
2
+ class Ifchanged < Block
3
+
4
+ def render(context)
5
+ context.stack do
6
+
7
+ output = render_all(@nodelist, context)
8
+
9
+ if output != context.registers[:ifchanged]
10
+ context.registers[:ifchanged] = output
11
+ output
12
+ else
13
+ ''
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ Template.register_tag('ifchanged', Ifchanged)
20
+ end
@@ -0,0 +1,97 @@
1
+ module Liquid
2
+
3
+ # Include allows templates to relate with other templates
4
+ #
5
+ # Simply include another template:
6
+ #
7
+ # {% include 'product' %}
8
+ #
9
+ # Include a template with a local variable:
10
+ #
11
+ # {% include 'product' with products[0] %}
12
+ #
13
+ # Include a template for a collection:
14
+ #
15
+ # {% include 'product' for products %}
16
+ #
17
+ class Include < Tag
18
+ Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/o
19
+
20
+ def initialize(tag_name, markup, tokens, options)
21
+ if markup =~ Syntax
22
+
23
+ @template_name = $1
24
+ @variable_name = $3
25
+ @attributes = {}
26
+
27
+ markup.scan(TagAttributes) do |key, value|
28
+ @attributes[key] = value
29
+ end
30
+
31
+ else
32
+ raise SyntaxError.new(options[:locale].t("errors.syntax.include"), options[:line])
33
+ end
34
+
35
+ super
36
+ end
37
+
38
+ def parse(tokens)
39
+ end
40
+
41
+ def blank?
42
+ false
43
+ end
44
+
45
+ def render(context)
46
+ partial = load_cached_partial(context)
47
+ variable = context[@variable_name || @template_name[1..-2]]
48
+
49
+ context.stack do
50
+ @attributes.each do |key, value|
51
+ context[key] = context[value]
52
+ end
53
+
54
+ if variable.is_a?(Array)
55
+ variable.collect do |var|
56
+ context[@template_name[1..-2]] = var
57
+ partial.render(context)
58
+ end
59
+ else
60
+ context[@template_name[1..-2]] = variable
61
+ partial.render(context)
62
+ end
63
+ end
64
+ end
65
+
66
+ private
67
+ def load_cached_partial(context)
68
+ cached_partials = context.registers[:cached_partials] || {}
69
+ template_name = context[@template_name]
70
+
71
+ if cached = cached_partials[template_name]
72
+ return cached
73
+ end
74
+ source = read_template_from_file_system(context)
75
+ partial = Liquid::Template.parse(source)
76
+ cached_partials[template_name] = partial
77
+ context.registers[:cached_partials] = cached_partials
78
+ partial
79
+ end
80
+
81
+ def read_template_from_file_system(context)
82
+ file_system = context.registers[:file_system] || Liquid::Template.file_system
83
+
84
+ # make read_template_file call backwards-compatible.
85
+ case file_system.method(:read_template_file).arity
86
+ when 1
87
+ file_system.read_template_file(context[@template_name])
88
+ when 2
89
+ file_system.read_template_file(context[@template_name], context)
90
+ else
91
+ raise ArgumentError, "file_system.read_template_file expects two parameters: (template_name, context)"
92
+ end
93
+ end
94
+ end
95
+
96
+ Template.register_tag('include', Include)
97
+ end