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,35 @@
1
+ module Liquid
2
+ # decrement is used in a place where one needs to insert a counter
3
+ # into a template, and needs the counter to survive across
4
+ # multiple instantiations of the template.
5
+ # NOTE: decrement is a pre-decrement, --i,
6
+ # while increment is post: i++.
7
+ #
8
+ # (To achieve the survival, the application must keep the context)
9
+ #
10
+ # if the variable does not exist, it is created with value 0.
11
+
12
+ # Hello: {% decrement variable %}
13
+ #
14
+ # gives you:
15
+ #
16
+ # Hello: -1
17
+ # Hello: -2
18
+ # Hello: -3
19
+ #
20
+ class Decrement < Tag
21
+ def initialize(tag_name, markup, options)
22
+ super
23
+ @variable = markup.strip
24
+ end
25
+
26
+ def render(context)
27
+ value = context.environments.first[@variable] ||= 0
28
+ value -= 1
29
+ context.environments.first[@variable] = value
30
+ value.to_s
31
+ end
32
+ end
33
+
34
+ Template.register_tag('decrement'.freeze, Decrement)
35
+ end
@@ -0,0 +1,203 @@
1
+ module Liquid
2
+ # "For" iterates over an array or collection.
3
+ # Several useful variables are available to you within the loop.
4
+ #
5
+ # == Basic usage:
6
+ # {% for item in collection %}
7
+ # {{ forloop.index }}: {{ item.name }}
8
+ # {% endfor %}
9
+ #
10
+ # == Advanced usage:
11
+ # {% for item in collection %}
12
+ # <div {% if forloop.first %}class="first"{% endif %}>
13
+ # Item {{ forloop.index }}: {{ item.name }}
14
+ # </div>
15
+ # {% else %}
16
+ # There is nothing in the collection.
17
+ # {% endfor %}
18
+ #
19
+ # You can also define a limit and offset much like SQL. Remember
20
+ # that offset starts at 0 for the first item.
21
+ #
22
+ # {% for item in collection limit:5 offset:10 %}
23
+ # {{ item.name }}
24
+ # {% end %}
25
+ #
26
+ # To reverse the for loop simply use {% for item in collection reversed %} (note that the flag's spelling is different to the filter `reverse`)
27
+ #
28
+ # == Available variables:
29
+ #
30
+ # forloop.name:: 'item-collection'
31
+ # forloop.length:: Length of the loop
32
+ # forloop.index:: The current item's position in the collection;
33
+ # forloop.index starts at 1.
34
+ # This is helpful for non-programmers who start believe
35
+ # the first item in an array is 1, not 0.
36
+ # forloop.index0:: The current item's position in the collection
37
+ # where the first item is 0
38
+ # forloop.rindex:: Number of items remaining in the loop
39
+ # (length - index) where 1 is the last item.
40
+ # forloop.rindex0:: Number of items remaining in the loop
41
+ # where 0 is the last item.
42
+ # forloop.first:: Returns true if the item is the first item.
43
+ # forloop.last:: Returns true if the item is the last item.
44
+ # forloop.parentloop:: Provides access to the parent loop, if present.
45
+ #
46
+ class For < Block
47
+ Syntax = /\A(#{VariableSegment}+)\s+in\s+(#{QuotedFragment}+)\s*(reversed)?/o
48
+
49
+ attr_reader :collection_name, :variable_name, :limit, :from
50
+
51
+ def initialize(tag_name, markup, options)
52
+ super
53
+ @from = @limit = nil
54
+ parse_with_selected_parser(markup)
55
+ @for_block = BlockBody.new
56
+ @else_block = nil
57
+ end
58
+
59
+ def parse(tokens)
60
+ return unless parse_body(@for_block, tokens)
61
+ parse_body(@else_block, tokens)
62
+ end
63
+
64
+ def nodelist
65
+ @else_block ? [@for_block, @else_block] : [@for_block]
66
+ end
67
+
68
+ def unknown_tag(tag, markup, tokens)
69
+ return super unless tag == 'else'.freeze
70
+ @else_block = BlockBody.new
71
+ end
72
+
73
+ def render(context)
74
+ segment = collection_segment(context)
75
+
76
+ if segment.empty?
77
+ render_else(context)
78
+ else
79
+ render_segment(context, segment)
80
+ end
81
+ end
82
+
83
+ protected
84
+
85
+ def lax_parse(markup)
86
+ if markup =~ Syntax
87
+ @variable_name = $1
88
+ collection_name = $2
89
+ @reversed = !!$3
90
+ @name = "#{@variable_name}-#{collection_name}"
91
+ @collection_name = Expression.parse(collection_name)
92
+ markup.scan(TagAttributes) do |key, value|
93
+ set_attribute(key, value)
94
+ end
95
+ else
96
+ raise SyntaxError.new(options[:locale].t("errors.syntax.for".freeze))
97
+ end
98
+ end
99
+
100
+ def strict_parse(markup)
101
+ p = Parser.new(markup)
102
+ @variable_name = p.consume(:id)
103
+ raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_in".freeze)) unless p.id?('in'.freeze)
104
+ collection_name = p.expression
105
+ @name = "#{@variable_name}-#{collection_name}"
106
+ @collection_name = Expression.parse(collection_name)
107
+ @reversed = p.id?('reversed'.freeze)
108
+
109
+ while p.look(:id) && p.look(:colon, 1)
110
+ unless attribute = p.id?('limit'.freeze) || p.id?('offset'.freeze)
111
+ raise SyntaxError.new(options[:locale].t("errors.syntax.for_invalid_attribute".freeze))
112
+ end
113
+ p.consume
114
+ set_attribute(attribute, p.expression)
115
+ end
116
+ p.consume(:end_of_string)
117
+ end
118
+
119
+ private
120
+
121
+ def collection_segment(context)
122
+ offsets = context.registers[:for] ||= {}
123
+
124
+ from = if @from == :continue
125
+ offsets[@name].to_i
126
+ else
127
+ context.evaluate(@from).to_i
128
+ end
129
+
130
+ collection = context.evaluate(@collection_name)
131
+ collection = collection.to_a if collection.is_a?(Range)
132
+
133
+ limit = context.evaluate(@limit)
134
+ to = limit ? limit.to_i + from : nil
135
+
136
+ segment = Utils.slice_collection(collection, from, to)
137
+ segment.reverse! if @reversed
138
+
139
+ offsets[@name] = from + segment.length
140
+
141
+ segment
142
+ end
143
+
144
+ def render_segment(context, segment)
145
+ for_stack = context.registers[:for_stack] ||= []
146
+ length = segment.length
147
+
148
+ result = ''
149
+
150
+ context.stack do
151
+ loop_vars = Liquid::ForloopDrop.new(@name, length, for_stack[-1])
152
+
153
+ for_stack.push(loop_vars)
154
+
155
+ begin
156
+ context['forloop'.freeze] = loop_vars
157
+
158
+ segment.each do |item|
159
+ context[@variable_name] = item
160
+ result << @for_block.render(context)
161
+ loop_vars.send(:increment!)
162
+
163
+ # Handle any interrupts if they exist.
164
+ if context.interrupt?
165
+ interrupt = context.pop_interrupt
166
+ break if interrupt.is_a? BreakInterrupt
167
+ next if interrupt.is_a? ContinueInterrupt
168
+ end
169
+ end
170
+ ensure
171
+ for_stack.pop
172
+ end
173
+ end
174
+
175
+ result
176
+ end
177
+
178
+ def set_attribute(key, expr)
179
+ case key
180
+ when 'offset'.freeze
181
+ @from = if expr == 'continue'.freeze
182
+ :continue
183
+ else
184
+ Expression.parse(expr)
185
+ end
186
+ when 'limit'.freeze
187
+ @limit = Expression.parse(expr)
188
+ end
189
+ end
190
+
191
+ def render_else(context)
192
+ @else_block ? @else_block.render(context) : ''.freeze
193
+ end
194
+
195
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
196
+ def children
197
+ (super + [@node.limit, @node.from, @node.collection_name]).compact
198
+ end
199
+ end
200
+ end
201
+
202
+ Template.register_tag('for'.freeze, For)
203
+ end
@@ -0,0 +1,122 @@
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
+ BOOLEAN_OPERATORS = %w(and or)
16
+
17
+ attr_reader :blocks
18
+
19
+ def initialize(tag_name, markup, options)
20
+ super
21
+ @blocks = []
22
+ push_block('if'.freeze, markup)
23
+ end
24
+
25
+ def nodelist
26
+ @blocks.map(&:attachment)
27
+ end
28
+
29
+ def parse(tokens)
30
+ while parse_body(@blocks.last.attachment, tokens)
31
+ end
32
+ end
33
+
34
+ def unknown_tag(tag, markup, tokens)
35
+ if ['elsif'.freeze, 'else'.freeze].include?(tag)
36
+ push_block(tag, markup)
37
+ else
38
+ super
39
+ end
40
+ end
41
+
42
+ def render(context)
43
+ context.stack do
44
+ @blocks.each do |block|
45
+ if block.evaluate(context)
46
+ return block.attachment.render(context)
47
+ end
48
+ end
49
+ ''.freeze
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def push_block(tag, markup)
56
+ block = if tag == 'else'.freeze
57
+ ElseCondition.new
58
+ else
59
+ parse_with_selected_parser(markup)
60
+ end
61
+
62
+ @blocks.push(block)
63
+ block.attach(BlockBody.new)
64
+ end
65
+
66
+ def lax_parse(markup)
67
+ expressions = markup.scan(ExpressionsAndOperators)
68
+ raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop =~ Syntax
69
+
70
+ condition = Condition.new(Expression.parse($1), $2, Expression.parse($3))
71
+
72
+ until expressions.empty?
73
+ operator = expressions.pop.to_s.strip
74
+
75
+ raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless expressions.pop.to_s =~ Syntax
76
+
77
+ new_condition = Condition.new(Expression.parse($1), $2, Expression.parse($3))
78
+ raise(SyntaxError.new(options[:locale].t("errors.syntax.if".freeze))) unless BOOLEAN_OPERATORS.include?(operator)
79
+ new_condition.send(operator, condition)
80
+ condition = new_condition
81
+ end
82
+
83
+ condition
84
+ end
85
+
86
+ def strict_parse(markup)
87
+ p = Parser.new(markup)
88
+ condition = parse_binary_comparisons(p)
89
+ p.consume(:end_of_string)
90
+ condition
91
+ end
92
+
93
+ def parse_binary_comparisons(p)
94
+ condition = parse_comparison(p)
95
+ first_condition = condition
96
+ while op = (p.id?('and'.freeze) || p.id?('or'.freeze))
97
+ child_condition = parse_comparison(p)
98
+ condition.send(op, child_condition)
99
+ condition = child_condition
100
+ end
101
+ first_condition
102
+ end
103
+
104
+ def parse_comparison(p)
105
+ a = Expression.parse(p.expression)
106
+ if op = p.consume?(:comparison)
107
+ b = Expression.parse(p.expression)
108
+ Condition.new(a, op, b)
109
+ else
110
+ Condition.new(a)
111
+ end
112
+ end
113
+
114
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
115
+ def children
116
+ @node.blocks
117
+ end
118
+ end
119
+ end
120
+
121
+ Template.register_tag('if'.freeze, If)
122
+ end
@@ -0,0 +1,18 @@
1
+ module Liquid
2
+ class Ifchanged < Block
3
+ def render(context)
4
+ context.stack do
5
+ output = super
6
+
7
+ if output != context.registers[:ifchanged]
8
+ context.registers[:ifchanged] = output
9
+ output
10
+ else
11
+ ''.freeze
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ Template.register_tag('ifchanged'.freeze, Ifchanged)
18
+ end
@@ -0,0 +1,124 @@
1
+ module Liquid
2
+ # Include allows templates to relate with other templates
3
+ #
4
+ # Simply include another template:
5
+ #
6
+ # {% include 'product' %}
7
+ #
8
+ # Include a template with a local variable:
9
+ #
10
+ # {% include 'product' with products[0] %}
11
+ #
12
+ # Include a template for a collection:
13
+ #
14
+ # {% include 'product' for products %}
15
+ #
16
+ class Include < Tag
17
+ Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/o
18
+
19
+ attr_reader :template_name_expr, :variable_name_expr, :attributes
20
+
21
+ def initialize(tag_name, markup, options)
22
+ super
23
+
24
+ if markup =~ Syntax
25
+
26
+ template_name = $1
27
+ variable_name = $3
28
+
29
+ @variable_name_expr = variable_name ? Expression.parse(variable_name) : nil
30
+ @template_name_expr = Expression.parse(template_name)
31
+ @attributes = {}
32
+
33
+ markup.scan(TagAttributes) do |key, value|
34
+ @attributes[key] = Expression.parse(value)
35
+ end
36
+
37
+ else
38
+ raise SyntaxError.new(options[:locale].t("errors.syntax.include".freeze))
39
+ end
40
+ end
41
+
42
+ def parse(_tokens)
43
+ end
44
+
45
+ def render(context)
46
+ template_name = context.evaluate(@template_name_expr)
47
+ raise ArgumentError.new(options[:locale].t("errors.argument.include")) unless template_name
48
+
49
+ partial = load_cached_partial(template_name, context)
50
+ context_variable_name = template_name.split('/'.freeze).last
51
+
52
+ variable = if @variable_name_expr
53
+ context.evaluate(@variable_name_expr)
54
+ else
55
+ context.find_variable(template_name, raise_on_not_found: false)
56
+ end
57
+
58
+ old_template_name = context.template_name
59
+ old_partial = context.partial
60
+ begin
61
+ context.template_name = template_name
62
+ context.partial = true
63
+ context.stack do
64
+ @attributes.each do |key, value|
65
+ context[key] = context.evaluate(value)
66
+ end
67
+
68
+ if variable.is_a?(Array)
69
+ variable.collect do |var|
70
+ context[context_variable_name] = var
71
+ partial.render(context)
72
+ end
73
+ else
74
+ context[context_variable_name] = variable
75
+ partial.render(context)
76
+ end
77
+ end
78
+ ensure
79
+ context.template_name = old_template_name
80
+ context.partial = old_partial
81
+ end
82
+ end
83
+
84
+ private
85
+
86
+ alias_method :parse_context, :options
87
+ private :parse_context
88
+
89
+ def load_cached_partial(template_name, context)
90
+ cached_partials = context.registers[:cached_partials] || {}
91
+
92
+ if cached = cached_partials[template_name]
93
+ return cached
94
+ end
95
+ source = read_template_from_file_system(context)
96
+ begin
97
+ parse_context.partial = true
98
+ partial = Liquid::Template.parse(source, parse_context)
99
+ ensure
100
+ parse_context.partial = false
101
+ end
102
+ cached_partials[template_name] = partial
103
+ context.registers[:cached_partials] = cached_partials
104
+ partial
105
+ end
106
+
107
+ def read_template_from_file_system(context)
108
+ file_system = context.registers[:file_system] || Liquid::Template.file_system
109
+
110
+ file_system.read_template_file(context.evaluate(@template_name_expr))
111
+ end
112
+
113
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
114
+ def children
115
+ [
116
+ @node.template_name_expr,
117
+ @node.variable_name_expr
118
+ ] + @node.attributes.values
119
+ end
120
+ end
121
+ end
122
+
123
+ Template.register_tag('include'.freeze, Include)
124
+ end