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,11 +1,10 @@
1
1
  module Liquid
2
-
3
2
  # An interrupt is any command that breaks processing of a block (ex: a for loop).
4
3
  class Interrupt
5
4
  attr_reader :message
6
5
 
7
- def initialize(message=nil)
8
- @message = message || "interrupt"
6
+ def initialize(message = nil)
7
+ @message = message || "interrupt".freeze
9
8
  end
10
9
  end
11
10
 
@@ -0,0 +1,55 @@
1
+ require "strscan"
2
+ module Liquid
3
+ class Lexer
4
+ SPECIALS = {
5
+ '|'.freeze => :pipe,
6
+ '.'.freeze => :dot,
7
+ ':'.freeze => :colon,
8
+ ','.freeze => :comma,
9
+ '['.freeze => :open_square,
10
+ ']'.freeze => :close_square,
11
+ '('.freeze => :open_round,
12
+ ')'.freeze => :close_round,
13
+ '?'.freeze => :question,
14
+ '-'.freeze => :dash
15
+ }.freeze
16
+ IDENTIFIER = /[a-zA-Z_][\w-]*\??/
17
+ SINGLE_STRING_LITERAL = /'[^\']*'/
18
+ DOUBLE_STRING_LITERAL = /"[^\"]*"/
19
+ NUMBER_LITERAL = /-?\d+(\.\d+)?/
20
+ DOTDOT = /\.\./
21
+ COMPARISON_OPERATOR = /==|!=|<>|<=?|>=?|contains(?=\s)/
22
+ WHITESPACE_OR_NOTHING = /\s*/
23
+
24
+ def initialize(input)
25
+ @ss = StringScanner.new(input)
26
+ end
27
+
28
+ def tokenize
29
+ @output = []
30
+
31
+ until @ss.eos?
32
+ @ss.skip(WHITESPACE_OR_NOTHING)
33
+ break if @ss.eos?
34
+ tok = case
35
+ when t = @ss.scan(COMPARISON_OPERATOR) then [:comparison, t]
36
+ when t = @ss.scan(SINGLE_STRING_LITERAL) then [:string, t]
37
+ when t = @ss.scan(DOUBLE_STRING_LITERAL) then [:string, t]
38
+ when t = @ss.scan(NUMBER_LITERAL) then [:number, t]
39
+ when t = @ss.scan(IDENTIFIER) then [:id, t]
40
+ when t = @ss.scan(DOTDOT) then [:dotdot, t]
41
+ else
42
+ c = @ss.getch
43
+ if s = SPECIALS[c]
44
+ [s, c]
45
+ else
46
+ raise SyntaxError, "Unexpected character #{c}"
47
+ end
48
+ end
49
+ @output << tok
50
+ end
51
+
52
+ @output << [:end_of_string]
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,26 @@
1
+ ---
2
+ errors:
3
+ syntax:
4
+ tag_unexpected_args: "Syntax Error in '%{tag}' - Valid syntax: %{tag}"
5
+ assign: "Syntax Error in 'assign' - Valid syntax: assign [var] = [source]"
6
+ capture: "Syntax Error in 'capture' - Valid syntax: capture [var]"
7
+ case: "Syntax Error in 'case' - Valid syntax: case [condition]"
8
+ case_invalid_when: "Syntax Error in tag 'case' - Valid when condition: {% when [condition] [or condition2...] %}"
9
+ case_invalid_else: "Syntax Error in tag 'case' - Valid else condition: {% else %} (no parameters) "
10
+ cycle: "Syntax Error in 'cycle' - Valid syntax: cycle [name :] var [, var2, var3 ...]"
11
+ for: "Syntax Error in 'for loop' - Valid syntax: for [item] in [collection]"
12
+ for_invalid_in: "For loops require an 'in' clause"
13
+ for_invalid_attribute: "Invalid attribute in for loop. Valid attributes are limit and offset"
14
+ if: "Syntax Error in tag 'if' - Valid syntax: if [expression]"
15
+ include: "Error in tag 'include' - Valid syntax: include '[template]' (with|for) [object|collection]"
16
+ unknown_tag: "Unknown tag '%{tag}'"
17
+ invalid_delimiter: "'%{tag}' is not a valid delimiter for %{block_name} tags. use %{block_delimiter}"
18
+ unexpected_else: "%{block_name} tag does not expect 'else' tag"
19
+ unexpected_outer_tag: "Unexpected outer '%{tag}' tag"
20
+ tag_termination: "Tag '%{token}' was not properly terminated with regexp: %{tag_end}"
21
+ variable_termination: "Variable '%{token}' was not properly terminated with regexp: %{tag_end}"
22
+ tag_never_closed: "'%{block_name}' tag was never closed"
23
+ meta_syntax_error: "Liquid syntax error: #{e.message}"
24
+ table_row: "Syntax Error in 'table_row loop' - Valid syntax: table_row [item] in [collection] cols=3"
25
+ argument:
26
+ include: "Argument error in tag 'include' - Illegal template name"
@@ -0,0 +1,38 @@
1
+ module Liquid
2
+ class ParseContext
3
+ attr_accessor :locale, :line_number, :trim_whitespace, :depth
4
+ attr_reader :partial, :warnings, :error_mode
5
+
6
+ def initialize(options = {})
7
+ @template_options = options ? options.dup : {}
8
+ @locale = @template_options[:locale] ||= I18n.new
9
+ @warnings = []
10
+ self.depth = 0
11
+ self.partial = false
12
+ end
13
+
14
+ def [](option_key)
15
+ @options[option_key]
16
+ end
17
+
18
+ def partial=(value)
19
+ @partial = value
20
+ @options = value ? partial_options : @template_options
21
+ @error_mode = @options[:error_mode] || Template.error_mode
22
+ value
23
+ end
24
+
25
+ def partial_options
26
+ @partial_options ||= begin
27
+ dont_pass = @template_options[:include_options_blacklist]
28
+ if dont_pass == true
29
+ { locale: locale }
30
+ elsif dont_pass.is_a?(Array)
31
+ @template_options.reject { |k, v| dont_pass.include?(k) }
32
+ else
33
+ @template_options
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Liquid
4
+ class ParseTreeVisitor
5
+ def self.for(node, callbacks = Hash.new(proc {}))
6
+ if defined?(node.class::ParseTreeVisitor)
7
+ node.class::ParseTreeVisitor
8
+ else
9
+ self
10
+ end.new(node, callbacks)
11
+ end
12
+
13
+ def initialize(node, callbacks)
14
+ @node = node
15
+ @callbacks = callbacks
16
+ end
17
+
18
+ def add_callback_for(*classes, &block)
19
+ callback = block
20
+ callback = ->(node, _) { yield node } if block.arity.abs == 1
21
+ callback = ->(_, _) { yield } if block.arity.zero?
22
+ classes.each { |klass| @callbacks[klass] = callback }
23
+ self
24
+ end
25
+
26
+ def visit(context = nil)
27
+ children.map do |node|
28
+ item, new_context = @callbacks[node.class].call(node, context)
29
+ [
30
+ item,
31
+ ParseTreeVisitor.for(node, @callbacks).visit(new_context || context)
32
+ ]
33
+ end
34
+ end
35
+
36
+ protected
37
+
38
+ def children
39
+ @node.respond_to?(:nodelist) ? Array(@node.nodelist) : []
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,90 @@
1
+ module Liquid
2
+ class Parser
3
+ def initialize(input)
4
+ l = Lexer.new(input)
5
+ @tokens = l.tokenize
6
+ @p = 0 # pointer to current location
7
+ end
8
+
9
+ def jump(point)
10
+ @p = point
11
+ end
12
+
13
+ def consume(type = nil)
14
+ token = @tokens[@p]
15
+ if type && token[0] != type
16
+ raise SyntaxError, "Expected #{type} but found #{@tokens[@p].first}"
17
+ end
18
+ @p += 1
19
+ token[1]
20
+ end
21
+
22
+ # Only consumes the token if it matches the type
23
+ # Returns the token's contents if it was consumed
24
+ # or false otherwise.
25
+ def consume?(type)
26
+ token = @tokens[@p]
27
+ return false unless token && token[0] == type
28
+ @p += 1
29
+ token[1]
30
+ end
31
+
32
+ # Like consume? Except for an :id token of a certain name
33
+ def id?(str)
34
+ token = @tokens[@p]
35
+ return false unless token && token[0] == :id
36
+ return false unless token[1] == str
37
+ @p += 1
38
+ token[1]
39
+ end
40
+
41
+ def look(type, ahead = 0)
42
+ tok = @tokens[@p + ahead]
43
+ return false unless tok
44
+ tok[0] == type
45
+ end
46
+
47
+ def expression
48
+ token = @tokens[@p]
49
+ if token[0] == :id
50
+ variable_signature
51
+ elsif [:string, :number].include? token[0]
52
+ consume
53
+ elsif token.first == :open_round
54
+ consume
55
+ first = expression
56
+ consume(:dotdot)
57
+ last = expression
58
+ consume(:close_round)
59
+ "(#{first}..#{last})"
60
+ else
61
+ raise SyntaxError, "#{token} is not a valid expression"
62
+ end
63
+ end
64
+
65
+ def argument
66
+ str = ""
67
+ # might be a keyword argument (identifier: expression)
68
+ if look(:id) && look(:colon, 1)
69
+ str << consume << consume << ' '.freeze
70
+ end
71
+
72
+ str << expression
73
+ str
74
+ end
75
+
76
+ def variable_signature
77
+ str = consume(:id)
78
+ while look(:open_square)
79
+ str << consume
80
+ str << expression
81
+ str << consume(:close_square)
82
+ end
83
+ if look(:dot)
84
+ str << consume
85
+ str << variable_signature
86
+ end
87
+ str
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,31 @@
1
+ module Liquid
2
+ module ParserSwitching
3
+ def parse_with_selected_parser(markup)
4
+ case parse_context.error_mode
5
+ when :strict then strict_parse_with_error_context(markup)
6
+ when :lax then lax_parse(markup)
7
+ when :warn
8
+ begin
9
+ return strict_parse_with_error_context(markup)
10
+ rescue SyntaxError => e
11
+ parse_context.warnings << e
12
+ return lax_parse(markup)
13
+ end
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def strict_parse_with_error_context(markup)
20
+ strict_parse(markup)
21
+ rescue SyntaxError => e
22
+ e.line_number = line_number
23
+ e.markup_context = markup_context(markup)
24
+ raise e
25
+ end
26
+
27
+ def markup_context(markup)
28
+ "in \"#{markup.strip}\""
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,158 @@
1
+ require 'liquid/profiler/hooks'
2
+
3
+ module Liquid
4
+ # Profiler enables support for profiling template rendering to help track down performance issues.
5
+ #
6
+ # To enable profiling, first require 'liquid/profiler'.
7
+ # Then, to profile a parse/render cycle, pass the <tt>profile: true</tt> option to <tt>Liquid::Template.parse</tt>.
8
+ # After <tt>Liquid::Template#render</tt> is called, the template object makes available an instance of this
9
+ # class via the <tt>Liquid::Template#profiler</tt> method.
10
+ #
11
+ # template = Liquid::Template.parse(template_content, profile: true)
12
+ # output = template.render
13
+ # profile = template.profiler
14
+ #
15
+ # This object contains all profiling information, containing information on what tags were rendered,
16
+ # where in the templates these tags live, and how long each tag took to render.
17
+ #
18
+ # This is a tree structure that is Enumerable all the way down, and keeps track of tags and rendering times
19
+ # inside of <tt>{% include %}</tt> tags.
20
+ #
21
+ # profile.each do |node|
22
+ # # Access to the node itself
23
+ # node.code
24
+ #
25
+ # # Which template and line number of this node.
26
+ # # If top level, this will be "<root>".
27
+ # node.partial
28
+ # node.line_number
29
+ #
30
+ # # Render time in seconds of this node
31
+ # node.render_time
32
+ #
33
+ # # If the template used {% include %}, this node will also have children.
34
+ # node.children.each do |child2|
35
+ # # ...
36
+ # end
37
+ # end
38
+ #
39
+ # Profiler also exposes the total time of the template's render in <tt>Liquid::Profiler#total_render_time</tt>.
40
+ #
41
+ # All render times are in seconds. There is a small performance hit when profiling is enabled.
42
+ #
43
+ class Profiler
44
+ include Enumerable
45
+
46
+ class Timing
47
+ attr_reader :code, :partial, :line_number, :children
48
+
49
+ def initialize(node, partial)
50
+ @code = node.respond_to?(:raw) ? node.raw : node
51
+ @partial = partial
52
+ @line_number = node.respond_to?(:line_number) ? node.line_number : nil
53
+ @children = []
54
+ end
55
+
56
+ def self.start(node, partial)
57
+ new(node, partial).tap(&:start)
58
+ end
59
+
60
+ def start
61
+ @start_time = Time.now
62
+ end
63
+
64
+ def finish
65
+ @end_time = Time.now
66
+ end
67
+
68
+ def render_time
69
+ @end_time - @start_time
70
+ end
71
+ end
72
+
73
+ def self.profile_node_render(node)
74
+ if Profiler.current_profile && node.respond_to?(:render)
75
+ Profiler.current_profile.start_node(node)
76
+ output = yield
77
+ Profiler.current_profile.end_node(node)
78
+ output
79
+ else
80
+ yield
81
+ end
82
+ end
83
+
84
+ def self.profile_children(template_name)
85
+ if Profiler.current_profile
86
+ Profiler.current_profile.push_partial(template_name)
87
+ output = yield
88
+ Profiler.current_profile.pop_partial
89
+ output
90
+ else
91
+ yield
92
+ end
93
+ end
94
+
95
+ def self.current_profile
96
+ Thread.current[:liquid_profiler]
97
+ end
98
+
99
+ def initialize
100
+ @partial_stack = ["<root>"]
101
+
102
+ @root_timing = Timing.new("", current_partial)
103
+ @timing_stack = [@root_timing]
104
+
105
+ @render_start_at = Time.now
106
+ @render_end_at = @render_start_at
107
+ end
108
+
109
+ def start
110
+ Thread.current[:liquid_profiler] = self
111
+ @render_start_at = Time.now
112
+ end
113
+
114
+ def stop
115
+ Thread.current[:liquid_profiler] = nil
116
+ @render_end_at = Time.now
117
+ end
118
+
119
+ def total_render_time
120
+ @render_end_at - @render_start_at
121
+ end
122
+
123
+ def each(&block)
124
+ @root_timing.children.each(&block)
125
+ end
126
+
127
+ def [](idx)
128
+ @root_timing.children[idx]
129
+ end
130
+
131
+ def length
132
+ @root_timing.children.length
133
+ end
134
+
135
+ def start_node(node)
136
+ @timing_stack.push(Timing.start(node, current_partial))
137
+ end
138
+
139
+ def end_node(_node)
140
+ timing = @timing_stack.pop
141
+ timing.finish
142
+
143
+ @timing_stack.last.children << timing
144
+ end
145
+
146
+ def current_partial
147
+ @partial_stack.last
148
+ end
149
+
150
+ def push_partial(partial_name)
151
+ @partial_stack.push(partial_name)
152
+ end
153
+
154
+ def pop_partial
155
+ @partial_stack.pop
156
+ end
157
+ end
158
+ end