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,31 @@
1
+ module Liquid
2
+ # increment 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
+ # (To achieve the survival, the application must keep the context)
6
+ #
7
+ # if the variable does not exist, it is created with value 0.
8
+ #
9
+ # Hello: {% increment variable %}
10
+ #
11
+ # gives you:
12
+ #
13
+ # Hello: 0
14
+ # Hello: 1
15
+ # Hello: 2
16
+ #
17
+ class Increment < Tag
18
+ def initialize(tag_name, markup, options)
19
+ super
20
+ @variable = markup.strip
21
+ end
22
+
23
+ def render(context)
24
+ value = context.environments.first[@variable] ||= 0
25
+ context.environments.first[@variable] = value + 1
26
+ value.to_s
27
+ end
28
+ end
29
+
30
+ Template.register_tag('increment'.freeze, Increment)
31
+ end
@@ -0,0 +1,47 @@
1
+ module Liquid
2
+ class Raw < Block
3
+ Syntax = /\A\s*\z/
4
+ FullTokenPossiblyInvalid = /\A(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}\z/om
5
+
6
+ def initialize(tag_name, markup, parse_context)
7
+ super
8
+
9
+ ensure_valid_markup(tag_name, markup, parse_context)
10
+ end
11
+
12
+ def parse(tokens)
13
+ @body = ''
14
+ while token = tokens.shift
15
+ if token =~ FullTokenPossiblyInvalid
16
+ @body << $1 if $1 != "".freeze
17
+ return if block_delimiter == $2
18
+ end
19
+ @body << token unless token.empty?
20
+ end
21
+
22
+ raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
23
+ end
24
+
25
+ def render(_context)
26
+ @body
27
+ end
28
+
29
+ def nodelist
30
+ [@body]
31
+ end
32
+
33
+ def blank?
34
+ @body.empty?
35
+ end
36
+
37
+ protected
38
+
39
+ def ensure_valid_markup(tag_name, markup, parse_context)
40
+ unless markup =~ Syntax
41
+ raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_unexpected_args".freeze, tag: tag_name))
42
+ end
43
+ end
44
+ end
45
+
46
+ Template.register_tag('raw'.freeze, Raw)
47
+ end
@@ -0,0 +1,62 @@
1
+ module Liquid
2
+ class TableRow < Block
3
+ Syntax = /(\w+)\s+in\s+(#{QuotedFragment}+)/o
4
+
5
+ attr_reader :variable_name, :collection_name, :attributes
6
+
7
+ def initialize(tag_name, markup, options)
8
+ super
9
+ if markup =~ Syntax
10
+ @variable_name = $1
11
+ @collection_name = Expression.parse($2)
12
+ @attributes = {}
13
+ markup.scan(TagAttributes) do |key, value|
14
+ @attributes[key] = Expression.parse(value)
15
+ end
16
+ else
17
+ raise SyntaxError.new(options[:locale].t("errors.syntax.table_row".freeze))
18
+ end
19
+ end
20
+
21
+ def render(context)
22
+ collection = context.evaluate(@collection_name) or return ''.freeze
23
+
24
+ from = @attributes.key?('offset'.freeze) ? context.evaluate(@attributes['offset'.freeze]).to_i : 0
25
+ to = @attributes.key?('limit'.freeze) ? from + context.evaluate(@attributes['limit'.freeze]).to_i : nil
26
+
27
+ collection = Utils.slice_collection(collection, from, to)
28
+
29
+ length = collection.length
30
+
31
+ cols = context.evaluate(@attributes['cols'.freeze]).to_i
32
+
33
+ result = "<tr class=\"row1\">\n"
34
+ context.stack do
35
+ tablerowloop = Liquid::TablerowloopDrop.new(length, cols)
36
+ context['tablerowloop'.freeze] = tablerowloop
37
+
38
+ collection.each do |item|
39
+ context[@variable_name] = item
40
+
41
+ result << "<td class=\"col#{tablerowloop.col}\">" << super << '</td>'
42
+
43
+ if tablerowloop.col_last && !tablerowloop.last
44
+ result << "</tr>\n<tr class=\"row#{tablerowloop.row + 1}\">"
45
+ end
46
+
47
+ tablerowloop.send(:increment!)
48
+ end
49
+ end
50
+ result << "</tr>\n"
51
+ result
52
+ end
53
+
54
+ class ParseTreeVisitor < Liquid::ParseTreeVisitor
55
+ def children
56
+ super + @node.attributes.values + [@node.collection_name]
57
+ end
58
+ end
59
+ end
60
+
61
+ Template.register_tag('tablerow'.freeze, TableRow)
62
+ end
@@ -0,0 +1,30 @@
1
+ require_relative 'if'
2
+
3
+ module Liquid
4
+ # Unless is a conditional just like 'if' but works on the inverse logic.
5
+ #
6
+ # {% unless x < 0 %} x is greater than zero {% endunless %}
7
+ #
8
+ class Unless < If
9
+ def render(context)
10
+ context.stack do
11
+ # First condition is interpreted backwards ( if not )
12
+ first_block = @blocks.first
13
+ unless first_block.evaluate(context)
14
+ return first_block.attachment.render(context)
15
+ end
16
+
17
+ # After the first condition unless works just like if
18
+ @blocks[1..-1].each do |block|
19
+ if block.evaluate(context)
20
+ return block.attachment.render(context)
21
+ end
22
+ end
23
+
24
+ ''.freeze
25
+ end
26
+ end
27
+ end
28
+
29
+ Template.register_tag('unless'.freeze, Unless)
30
+ end
@@ -0,0 +1,254 @@
1
+ module Liquid
2
+ # Templates are central to liquid.
3
+ # Interpretating templates is a two step process. First you compile the
4
+ # source code you got. During compile time some extensive error checking is performed.
5
+ # your code should expect to get some SyntaxErrors.
6
+ #
7
+ # After you have a compiled template you can then <tt>render</tt> it.
8
+ # You can use a compiled template over and over again and keep it cached.
9
+ #
10
+ # Example:
11
+ #
12
+ # template = Liquid::Template.parse(source)
13
+ # template.render('user_name' => 'bob')
14
+ #
15
+ class Template
16
+ attr_accessor :root
17
+ attr_reader :resource_limits, :warnings
18
+
19
+ @@file_system = BlankFileSystem.new
20
+
21
+ class TagRegistry
22
+ include Enumerable
23
+
24
+ def initialize
25
+ @tags = {}
26
+ @cache = {}
27
+ end
28
+
29
+ def [](tag_name)
30
+ return nil unless @tags.key?(tag_name)
31
+ return @cache[tag_name] if Liquid.cache_classes
32
+
33
+ lookup_class(@tags[tag_name]).tap { |o| @cache[tag_name] = o }
34
+ end
35
+
36
+ def []=(tag_name, klass)
37
+ @tags[tag_name] = klass.name
38
+ @cache[tag_name] = klass
39
+ end
40
+
41
+ def delete(tag_name)
42
+ @tags.delete(tag_name)
43
+ @cache.delete(tag_name)
44
+ end
45
+
46
+ def each(&block)
47
+ @tags.each(&block)
48
+ end
49
+
50
+ private
51
+
52
+ def lookup_class(name)
53
+ name.split("::").reject(&:empty?).reduce(Object) { |scope, const| scope.const_get(const) }
54
+ end
55
+ end
56
+
57
+ attr_reader :profiler
58
+
59
+ class << self
60
+ # Sets how strict the parser should be.
61
+ # :lax acts like liquid 2.5 and silently ignores malformed tags in most cases.
62
+ # :warn is the default and will give deprecation warnings when invalid syntax is used.
63
+ # :strict will enforce correct syntax.
64
+ attr_writer :error_mode
65
+
66
+ # Sets how strict the taint checker should be.
67
+ # :lax is the default, and ignores the taint flag completely
68
+ # :warn adds a warning, but does not interrupt the rendering
69
+ # :error raises an error when tainted output is used
70
+ attr_writer :taint_mode
71
+
72
+ attr_accessor :default_exception_renderer
73
+ Template.default_exception_renderer = lambda do |exception|
74
+ exception
75
+ end
76
+
77
+ def file_system
78
+ @@file_system
79
+ end
80
+
81
+ def file_system=(obj)
82
+ @@file_system = obj
83
+ end
84
+
85
+ def register_tag(name, klass)
86
+ tags[name.to_s] = klass
87
+ end
88
+
89
+ def tags
90
+ @tags ||= TagRegistry.new
91
+ end
92
+
93
+ def error_mode
94
+ @error_mode ||= :lax
95
+ end
96
+
97
+ def taint_mode
98
+ @taint_mode ||= :lax
99
+ end
100
+
101
+ # Pass a module with filter methods which should be available
102
+ # to all liquid views. Good for registering the standard library
103
+ def register_filter(mod)
104
+ Strainer.global_filter(mod)
105
+ end
106
+
107
+ def default_resource_limits
108
+ @default_resource_limits ||= {}
109
+ end
110
+
111
+ # creates a new <tt>Template</tt> object from liquid source code
112
+ # To enable profiling, pass in <tt>profile: true</tt> as an option.
113
+ # See Liquid::Profiler for more information
114
+ def parse(source, options = {})
115
+ template = Template.new
116
+ template.parse(source, options)
117
+ end
118
+ end
119
+
120
+ def initialize
121
+ @rethrow_errors = false
122
+ @resource_limits = ResourceLimits.new(self.class.default_resource_limits)
123
+ end
124
+
125
+ # Parse source code.
126
+ # Returns self for easy chaining
127
+ def parse(source, options = {})
128
+ @options = options
129
+ @profiling = options[:profile]
130
+ @line_numbers = options[:line_numbers] || @profiling
131
+ parse_context = options.is_a?(ParseContext) ? options : ParseContext.new(options)
132
+ @root = Document.parse(tokenize(source), parse_context)
133
+ @warnings = parse_context.warnings
134
+ self
135
+ end
136
+
137
+ def registers
138
+ @registers ||= {}
139
+ end
140
+
141
+ def assigns
142
+ @assigns ||= {}
143
+ end
144
+
145
+ def instance_assigns
146
+ @instance_assigns ||= {}
147
+ end
148
+
149
+ def errors
150
+ @errors ||= []
151
+ end
152
+
153
+ # Render takes a hash with local variables.
154
+ #
155
+ # if you use the same filters over and over again consider registering them globally
156
+ # with <tt>Template.register_filter</tt>
157
+ #
158
+ # if profiling was enabled in <tt>Template#parse</tt> then the resulting profiling information
159
+ # will be available via <tt>Template#profiler</tt>
160
+ #
161
+ # Following options can be passed:
162
+ #
163
+ # * <tt>filters</tt> : array with local filters
164
+ # * <tt>registers</tt> : hash with register variables. Those can be accessed from
165
+ # filters and tags and might be useful to integrate liquid more with its host application
166
+ #
167
+ def render(*args)
168
+ return ''.freeze if @root.nil?
169
+
170
+ context = case args.first
171
+ when Liquid::Context
172
+ c = args.shift
173
+
174
+ if @rethrow_errors
175
+ c.exception_renderer = ->(e) { raise }
176
+ end
177
+
178
+ c
179
+ when Liquid::Drop
180
+ drop = args.shift
181
+ drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
182
+ when Hash
183
+ Context.new([args.shift, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
184
+ when nil
185
+ Context.new(assigns, instance_assigns, registers, @rethrow_errors, @resource_limits)
186
+ else
187
+ raise ArgumentError, "Expected Hash or Liquid::Context as parameter"
188
+ end
189
+
190
+ case args.last
191
+ when Hash
192
+ options = args.pop
193
+
194
+ registers.merge!(options[:registers]) if options[:registers].is_a?(Hash)
195
+
196
+ apply_options_to_context(context, options)
197
+ when Module, Array
198
+ context.add_filters(args.pop)
199
+ end
200
+
201
+ # Retrying a render resets resource usage
202
+ context.resource_limits.reset
203
+
204
+ begin
205
+ # render the nodelist.
206
+ # for performance reasons we get an array back here. join will make a string out of it.
207
+ result = with_profiling(context) do
208
+ @root.render(context)
209
+ end
210
+ result.respond_to?(:join) ? result.join : result
211
+ rescue Liquid::MemoryError => e
212
+ context.handle_error(e)
213
+ ensure
214
+ @errors = context.errors
215
+ end
216
+ end
217
+
218
+ def render!(*args)
219
+ @rethrow_errors = true
220
+ render(*args)
221
+ end
222
+
223
+ private
224
+
225
+ def tokenize(source)
226
+ Tokenizer.new(source, @line_numbers)
227
+ end
228
+
229
+ def with_profiling(context)
230
+ if @profiling && !context.partial
231
+ raise "Profiler not loaded, require 'liquid/profiler' first" unless defined?(Liquid::Profiler)
232
+
233
+ @profiler = Profiler.new
234
+ @profiler.start
235
+
236
+ begin
237
+ yield
238
+ ensure
239
+ @profiler.stop
240
+ end
241
+ else
242
+ yield
243
+ end
244
+ end
245
+
246
+ def apply_options_to_context(context, options)
247
+ context.add_filters(options[:filters]) if options[:filters]
248
+ context.global_filter = options[:global_filter] if options[:global_filter]
249
+ context.exception_renderer = options[:exception_renderer] if options[:exception_renderer]
250
+ context.strict_variables = options[:strict_variables] if options[:strict_variables]
251
+ context.strict_filters = options[:strict_filters] if options[:strict_filters]
252
+ end
253
+ end
254
+ end
@@ -0,0 +1,31 @@
1
+ module Liquid
2
+ class Tokenizer
3
+ attr_reader :line_number
4
+
5
+ def initialize(source, line_numbers = false)
6
+ @source = source
7
+ @line_number = line_numbers ? 1 : nil
8
+ @tokens = tokenize
9
+ end
10
+
11
+ def shift
12
+ token = @tokens.shift
13
+ @line_number += token.count("\n") if @line_number && token
14
+ token
15
+ end
16
+
17
+ private
18
+
19
+ def tokenize
20
+ @source = @source.source if @source.respond_to?(:source)
21
+ return [] if @source.to_s.empty?
22
+
23
+ tokens = @source.split(TemplateParser)
24
+
25
+ # removes the rogue empty element at the beginning of the array
26
+ tokens.shift if tokens[0] && tokens[0].empty?
27
+
28
+ tokens
29
+ end
30
+ end
31
+ end