liquid 2.6.1 → 4.0.3

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 (130) hide show
  1. checksums.yaml +5 -5
  2. data/History.md +194 -29
  3. data/{MIT-LICENSE → LICENSE} +0 -0
  4. data/README.md +60 -2
  5. data/lib/liquid.rb +25 -14
  6. data/lib/liquid/block.rb +47 -96
  7. data/lib/liquid/block_body.rb +143 -0
  8. data/lib/liquid/condition.rb +70 -39
  9. data/lib/liquid/context.rb +116 -157
  10. data/lib/liquid/document.rb +19 -9
  11. data/lib/liquid/drop.rb +31 -14
  12. data/lib/liquid/errors.rb +54 -10
  13. data/lib/liquid/expression.rb +49 -0
  14. data/lib/liquid/extensions.rb +19 -7
  15. data/lib/liquid/file_system.rb +25 -14
  16. data/lib/liquid/forloop_drop.rb +42 -0
  17. data/lib/liquid/i18n.rb +39 -0
  18. data/lib/liquid/interrupts.rb +2 -3
  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 +311 -77
  30. data/lib/liquid/strainer.rb +39 -26
  31. data/lib/liquid/tablerowloop_drop.rb +62 -0
  32. data/lib/liquid/tag.rb +28 -11
  33. data/lib/liquid/tags/assign.rb +34 -10
  34. data/lib/liquid/tags/break.rb +1 -4
  35. data/lib/liquid/tags/capture.rb +11 -9
  36. data/lib/liquid/tags/case.rb +37 -22
  37. data/lib/liquid/tags/comment.rb +10 -3
  38. data/lib/liquid/tags/continue.rb +1 -4
  39. data/lib/liquid/tags/cycle.rb +20 -14
  40. data/lib/liquid/tags/decrement.rb +4 -8
  41. data/lib/liquid/tags/for.rb +121 -60
  42. data/lib/liquid/tags/if.rb +73 -30
  43. data/lib/liquid/tags/ifchanged.rb +3 -5
  44. data/lib/liquid/tags/include.rb +77 -46
  45. data/lib/liquid/tags/increment.rb +4 -8
  46. data/lib/liquid/tags/raw.rb +35 -10
  47. data/lib/liquid/tags/table_row.rb +62 -0
  48. data/lib/liquid/tags/unless.rb +6 -9
  49. data/lib/liquid/template.rb +130 -32
  50. data/lib/liquid/tokenizer.rb +31 -0
  51. data/lib/liquid/truffle.rb +5 -0
  52. data/lib/liquid/utils.rb +57 -4
  53. data/lib/liquid/variable.rb +121 -30
  54. data/lib/liquid/variable_lookup.rb +88 -0
  55. data/lib/liquid/version.rb +2 -1
  56. data/test/fixtures/en_locale.yml +9 -0
  57. data/test/integration/assign_test.rb +48 -0
  58. data/test/integration/blank_test.rb +106 -0
  59. data/test/integration/block_test.rb +12 -0
  60. data/test/{liquid → integration}/capture_test.rb +13 -3
  61. data/test/integration/context_test.rb +32 -0
  62. data/test/integration/document_test.rb +19 -0
  63. data/test/integration/drop_test.rb +273 -0
  64. data/test/integration/error_handling_test.rb +260 -0
  65. data/test/integration/filter_test.rb +178 -0
  66. data/test/integration/hash_ordering_test.rb +23 -0
  67. data/test/integration/output_test.rb +123 -0
  68. data/test/integration/parse_tree_visitor_test.rb +247 -0
  69. data/test/integration/parsing_quirks_test.rb +122 -0
  70. data/test/integration/render_profiling_test.rb +154 -0
  71. data/test/integration/security_test.rb +80 -0
  72. data/test/integration/standard_filter_test.rb +776 -0
  73. data/test/{liquid → integration}/tags/break_tag_test.rb +2 -3
  74. data/test/{liquid → integration}/tags/continue_tag_test.rb +1 -2
  75. data/test/integration/tags/for_tag_test.rb +410 -0
  76. data/test/integration/tags/if_else_tag_test.rb +188 -0
  77. data/test/integration/tags/include_tag_test.rb +253 -0
  78. data/test/integration/tags/increment_tag_test.rb +23 -0
  79. data/test/{liquid → integration}/tags/raw_tag_test.rb +9 -2
  80. data/test/integration/tags/standard_tag_test.rb +296 -0
  81. data/test/integration/tags/statements_test.rb +111 -0
  82. data/test/{liquid/tags/html_tag_test.rb → integration/tags/table_row_test.rb} +25 -24
  83. data/test/integration/tags/unless_else_tag_test.rb +26 -0
  84. data/test/integration/template_test.rb +332 -0
  85. data/test/integration/trim_mode_test.rb +529 -0
  86. data/test/integration/variable_test.rb +96 -0
  87. data/test/test_helper.rb +106 -19
  88. data/test/truffle/truffle_test.rb +9 -0
  89. data/test/{liquid/block_test.rb → unit/block_unit_test.rb} +9 -9
  90. data/test/unit/condition_unit_test.rb +166 -0
  91. data/test/{liquid/context_test.rb → unit/context_unit_test.rb} +85 -74
  92. data/test/unit/file_system_unit_test.rb +35 -0
  93. data/test/unit/i18n_unit_test.rb +37 -0
  94. data/test/unit/lexer_unit_test.rb +51 -0
  95. data/test/unit/parser_unit_test.rb +82 -0
  96. data/test/{liquid/regexp_test.rb → unit/regexp_unit_test.rb} +4 -4
  97. data/test/unit/strainer_unit_test.rb +164 -0
  98. data/test/unit/tag_unit_test.rb +21 -0
  99. data/test/unit/tags/case_tag_unit_test.rb +10 -0
  100. data/test/unit/tags/for_tag_unit_test.rb +13 -0
  101. data/test/unit/tags/if_tag_unit_test.rb +8 -0
  102. data/test/unit/template_unit_test.rb +78 -0
  103. data/test/unit/tokenizer_unit_test.rb +55 -0
  104. data/test/unit/variable_unit_test.rb +162 -0
  105. metadata +157 -77
  106. data/lib/extras/liquid_view.rb +0 -51
  107. data/lib/liquid/htmltags.rb +0 -74
  108. data/lib/liquid/module_ex.rb +0 -62
  109. data/test/liquid/assign_test.rb +0 -21
  110. data/test/liquid/condition_test.rb +0 -127
  111. data/test/liquid/drop_test.rb +0 -180
  112. data/test/liquid/error_handling_test.rb +0 -81
  113. data/test/liquid/file_system_test.rb +0 -29
  114. data/test/liquid/filter_test.rb +0 -125
  115. data/test/liquid/hash_ordering_test.rb +0 -25
  116. data/test/liquid/module_ex_test.rb +0 -87
  117. data/test/liquid/output_test.rb +0 -116
  118. data/test/liquid/parsing_quirks_test.rb +0 -52
  119. data/test/liquid/security_test.rb +0 -64
  120. data/test/liquid/standard_filter_test.rb +0 -251
  121. data/test/liquid/strainer_test.rb +0 -52
  122. data/test/liquid/tags/for_tag_test.rb +0 -297
  123. data/test/liquid/tags/if_else_tag_test.rb +0 -166
  124. data/test/liquid/tags/include_tag_test.rb +0 -166
  125. data/test/liquid/tags/increment_tag_test.rb +0 -24
  126. data/test/liquid/tags/standard_tag_test.rb +0 -295
  127. data/test/liquid/tags/statements_test.rb +0 -134
  128. data/test/liquid/tags/unless_else_tag_test.rb +0 -26
  129. data/test/liquid/template_test.rb +0 -146
  130. data/test/liquid/variable_test.rb +0 -186
@@ -1,20 +1,18 @@
1
1
  module Liquid
2
2
  class Ifchanged < Block
3
-
4
3
  def render(context)
5
4
  context.stack do
6
-
7
- output = render_all(@nodelist, context)
5
+ output = super
8
6
 
9
7
  if output != context.registers[:ifchanged]
10
8
  context.registers[:ifchanged] = output
11
9
  output
12
10
  else
13
- ''
11
+ ''.freeze
14
12
  end
15
13
  end
16
14
  end
17
15
  end
18
16
 
19
- Template.register_tag('ifchanged', Ifchanged)
17
+ Template.register_tag('ifchanged'.freeze, Ifchanged)
20
18
  end
@@ -1,5 +1,4 @@
1
1
  module Liquid
2
-
3
2
  # Include allows templates to relate with other templates
4
3
  #
5
4
  # Simply include another template:
@@ -17,77 +16,109 @@ module Liquid
17
16
  class Include < Tag
18
17
  Syntax = /(#{QuotedFragment}+)(\s+(?:with|for)\s+(#{QuotedFragment}+))?/o
19
18
 
20
- def initialize(tag_name, markup, tokens)
19
+ attr_reader :template_name_expr, :variable_name_expr, :attributes
20
+
21
+ def initialize(tag_name, markup, options)
22
+ super
23
+
21
24
  if markup =~ Syntax
22
25
 
23
- @template_name = $1
24
- @variable_name = $3
25
- @attributes = {}
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 = {}
26
32
 
27
33
  markup.scan(TagAttributes) do |key, value|
28
- @attributes[key] = value
34
+ @attributes[key] = Expression.parse(value)
29
35
  end
30
36
 
31
37
  else
32
- raise SyntaxError.new("Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]")
38
+ raise SyntaxError.new(options[:locale].t("errors.syntax.include".freeze))
33
39
  end
34
-
35
- super
36
40
  end
37
41
 
38
- def parse(tokens)
42
+ def parse(_tokens)
39
43
  end
40
44
 
41
45
  def render(context)
42
- partial = load_cached_partial(context)
43
- variable = context[@variable_name || @template_name[1..-2]]
46
+ template_name = context.evaluate(@template_name_expr)
47
+ raise ArgumentError.new(options[:locale].t("errors.argument.include")) unless template_name
44
48
 
45
- context.stack do
46
- @attributes.each do |key, value|
47
- context[key] = context[value]
48
- end
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
49
57
 
50
- if variable.is_a?(Array)
51
- variable.collect do |var|
52
- context[@template_name[1..-2]] = var
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
53
75
  partial.render(context)
54
76
  end
55
- else
56
- context[@template_name[1..-2]] = variable
57
- partial.render(context)
58
77
  end
78
+ ensure
79
+ context.template_name = old_template_name
80
+ context.partial = old_partial
59
81
  end
60
82
  end
61
83
 
62
84
  private
63
- def load_cached_partial(context)
64
- cached_partials = context.registers[:cached_partials] || {}
65
- template_name = context[@template_name]
66
85
 
67
- if cached = cached_partials[template_name]
68
- return cached
69
- end
70
- source = read_template_from_file_system(context)
71
- partial = Liquid::Template.parse(source)
72
- cached_partials[template_name] = partial
73
- context.registers[:cached_partials] = cached_partials
74
- partial
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
75
101
  end
102
+ cached_partials[template_name] = partial
103
+ context.registers[:cached_partials] = cached_partials
104
+ partial
105
+ end
76
106
 
77
- def read_template_from_file_system(context)
78
- file_system = context.registers[:file_system] || Liquid::Template.file_system
79
-
80
- # make read_template_file call backwards-compatible.
81
- case file_system.method(:read_template_file).arity
82
- when 1
83
- file_system.read_template_file(context[@template_name])
84
- when 2
85
- file_system.read_template_file(context[@template_name], context)
86
- else
87
- raise ArgumentError, "file_system.read_template_file expects two parameters: (template_name, context)"
88
- end
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
89
119
  end
120
+ end
90
121
  end
91
122
 
92
- Template.register_tag('include', Include)
123
+ Template.register_tag('include'.freeze, Include)
93
124
  end
@@ -1,12 +1,11 @@
1
1
  module Liquid
2
-
3
2
  # increment is used in a place where one needs to insert a counter
4
3
  # into a template, and needs the counter to survive across
5
4
  # multiple instantiations of the template.
6
5
  # (To achieve the survival, the application must keep the context)
7
6
  #
8
7
  # if the variable does not exist, it is created with value 0.
9
-
8
+ #
10
9
  # Hello: {% increment variable %}
11
10
  #
12
11
  # gives you:
@@ -16,10 +15,9 @@ module Liquid
16
15
  # Hello: 2
17
16
  #
18
17
  class Increment < Tag
19
- def initialize(tag_name, markup, tokens)
20
- @variable = markup.strip
21
-
18
+ def initialize(tag_name, markup, options)
22
19
  super
20
+ @variable = markup.strip
23
21
  end
24
22
 
25
23
  def render(context)
@@ -27,9 +25,7 @@ module Liquid
27
25
  context.environments.first[@variable] = value + 1
28
26
  value.to_s
29
27
  end
30
-
31
- private
32
28
  end
33
29
 
34
- Template.register_tag('increment', Increment)
30
+ Template.register_tag('increment'.freeze, Increment)
35
31
  end
@@ -1,22 +1,47 @@
1
1
  module Liquid
2
2
  class Raw < Block
3
- FullTokenPossiblyInvalid = /^(.*)#{TagStart}\s*(\w+)\s*(.*)?#{TagEnd}$/o
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
4
11
 
5
12
  def parse(tokens)
6
- @nodelist ||= []
7
- @nodelist.clear
13
+ @body = ''
8
14
  while token = tokens.shift
9
15
  if token =~ FullTokenPossiblyInvalid
10
- @nodelist << $1 if $1 != ""
11
- if block_delimiter == $2
12
- end_tag
13
- return
14
- end
16
+ @body << $1 if $1 != "".freeze
17
+ return if block_delimiter == $2
15
18
  end
16
- @nodelist << token if not token.empty?
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))
17
42
  end
18
43
  end
19
44
  end
20
45
 
21
- Template.register_tag('raw', Raw)
46
+ Template.register_tag('raw'.freeze, Raw)
22
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
@@ -1,33 +1,30 @@
1
- require File.dirname(__FILE__) + '/if'
1
+ require_relative 'if'
2
2
 
3
3
  module Liquid
4
-
5
4
  # Unless is a conditional just like 'if' but works on the inverse logic.
6
5
  #
7
- # {% unless x < 0 %} x is greater than zero {% end %}
6
+ # {% unless x < 0 %} x is greater than zero {% endunless %}
8
7
  #
9
8
  class Unless < If
10
9
  def render(context)
11
10
  context.stack do
12
-
13
11
  # First condition is interpreted backwards ( if not )
14
12
  first_block = @blocks.first
15
13
  unless first_block.evaluate(context)
16
- return render_all(first_block.attachment, context)
14
+ return first_block.attachment.render(context)
17
15
  end
18
16
 
19
17
  # After the first condition unless works just like if
20
18
  @blocks[1..-1].each do |block|
21
19
  if block.evaluate(context)
22
- return render_all(block.attachment, context)
20
+ return block.attachment.render(context)
23
21
  end
24
22
  end
25
23
 
26
- ''
24
+ ''.freeze
27
25
  end
28
26
  end
29
27
  end
30
28
 
31
-
32
- Template.register_tag('unless', Unless)
29
+ Template.register_tag('unless'.freeze, Unless)
33
30
  end
@@ -1,5 +1,4 @@
1
1
  module Liquid
2
-
3
2
  # Templates are central to liquid.
4
3
  # Interpretating templates is a two step process. First you compile the
5
4
  # source code you got. During compile time some extensive error checking is performed.
@@ -14,10 +13,67 @@ module Liquid
14
13
  # template.render('user_name' => 'bob')
15
14
  #
16
15
  class Template
17
- attr_accessor :root, :resource_limits
16
+ attr_accessor :root
17
+ attr_reader :resource_limits, :warnings
18
+
18
19
  @@file_system = BlankFileSystem.new
19
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
+
20
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
+
21
77
  def file_system
22
78
  @@file_system
23
79
  end
@@ -31,7 +87,15 @@ module Liquid
31
87
  end
32
88
 
33
89
  def tags
34
- @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
35
99
  end
36
100
 
37
101
  # Pass a module with filter methods which should be available
@@ -40,23 +104,33 @@ module Liquid
40
104
  Strainer.global_filter(mod)
41
105
  end
42
106
 
107
+ def default_resource_limits
108
+ @default_resource_limits ||= {}
109
+ end
110
+
43
111
  # creates a new <tt>Template</tt> object from liquid source code
44
- def parse(source)
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 = {})
45
115
  template = Template.new
46
- template.parse(source)
47
- template
116
+ template.parse(source, options)
48
117
  end
49
118
  end
50
119
 
51
- # creates a new <tt>Template</tt> from an array of tokens. Use <tt>Template.parse</tt> instead
52
120
  def initialize
53
- @resource_limits = {}
121
+ @rethrow_errors = false
122
+ @resource_limits = ResourceLimits.new(self.class.default_resource_limits)
54
123
  end
55
124
 
56
125
  # Parse source code.
57
126
  # Returns self for easy chaining
58
- def parse(source)
59
- @root = Document.new(tokenize(source))
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
60
134
  self
61
135
  end
62
136
 
@@ -81,6 +155,9 @@ module Liquid
81
155
  # if you use the same filters over and over again consider registering them globally
82
156
  # with <tt>Template.register_filter</tt>
83
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
+ #
84
161
  # Following options can be passed:
85
162
  #
86
163
  # * <tt>filters</tt> : array with local filters
@@ -88,11 +165,17 @@ module Liquid
88
165
  # filters and tags and might be useful to integrate liquid more with its host application
89
166
  #
90
167
  def render(*args)
91
- return '' if @root.nil?
168
+ return ''.freeze if @root.nil?
92
169
 
93
170
  context = case args.first
94
171
  when Liquid::Context
95
- args.shift
172
+ c = args.shift
173
+
174
+ if @rethrow_errors
175
+ c.exception_renderer = ->(e) { raise }
176
+ end
177
+
178
+ c
96
179
  when Liquid::Drop
97
180
  drop = args.shift
98
181
  drop.context = Context.new([drop, assigns], instance_assigns, registers, @rethrow_errors, @resource_limits)
@@ -101,31 +184,29 @@ module Liquid
101
184
  when nil
102
185
  Context.new(assigns, instance_assigns, registers, @rethrow_errors, @resource_limits)
103
186
  else
104
- raise ArgumentError, "Expect Hash or Liquid::Context as parameter"
187
+ raise ArgumentError, "Expected Hash or Liquid::Context as parameter"
105
188
  end
106
189
 
107
190
  case args.last
108
191
  when Hash
109
192
  options = args.pop
110
193
 
111
- if options[:registers].is_a?(Hash)
112
- self.registers.merge!(options[:registers])
113
- end
194
+ registers.merge!(options[:registers]) if options[:registers].is_a?(Hash)
114
195
 
115
- if options[:filters]
116
- context.add_filters(options[:filters])
117
- end
118
-
119
- when Module
120
- context.add_filters(args.pop)
121
- when Array
196
+ apply_options_to_context(context, options)
197
+ when Module, Array
122
198
  context.add_filters(args.pop)
123
199
  end
124
200
 
201
+ # Retrying a render resets resource usage
202
+ context.resource_limits.reset
203
+
125
204
  begin
126
205
  # render the nodelist.
127
206
  # for performance reasons we get an array back here. join will make a string out of it.
128
- result = @root.render(context)
207
+ result = with_profiling(context) do
208
+ @root.render(context)
209
+ end
129
210
  result.respond_to?(:join) ? result.join : result
130
211
  rescue Liquid::MemoryError => e
131
212
  context.handle_error(e)
@@ -135,22 +216,39 @@ module Liquid
135
216
  end
136
217
 
137
218
  def render!(*args)
138
- @rethrow_errors = true; render(*args)
219
+ @rethrow_errors = true
220
+ render(*args)
139
221
  end
140
222
 
141
223
  private
142
224
 
143
- # Uses the <tt>Liquid::TemplateParser</tt> regexp to tokenize the passed source
144
225
  def tokenize(source)
145
- source = source.source if source.respond_to?(:source)
146
- return [] if source.to_s.empty?
147
- tokens = source.split(TemplateParser)
226
+ Tokenizer.new(source, @line_numbers)
227
+ end
148
228
 
149
- # removes the rogue empty element at the beginning of the array
150
- tokens.shift if tokens[0] and tokens[0].empty?
229
+ def with_profiling(context)
230
+ if @profiling && !context.partial
231
+ raise "Profiler not loaded, require 'liquid/profiler' first" unless defined?(Liquid::Profiler)
151
232
 
152
- tokens
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
153
244
  end
154
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
155
253
  end
156
254
  end