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,17 +1,27 @@
1
1
  module Liquid
2
- class Document < Block
3
- # we don't need markup to open this block
4
- def initialize(tokens)
5
- parse(tokens)
2
+ class Document < BlockBody
3
+ def self.parse(tokens, parse_context)
4
+ doc = new
5
+ doc.parse(tokens, parse_context)
6
+ doc
6
7
  end
7
8
 
8
- # There isn't a real delimiter
9
- def block_delimiter
10
- []
9
+ def parse(tokens, parse_context)
10
+ super do |end_tag_name, end_tag_params|
11
+ unknown_tag(end_tag_name, parse_context) if end_tag_name
12
+ end
13
+ rescue SyntaxError => e
14
+ e.line_number ||= parse_context.line_number
15
+ raise
11
16
  end
12
17
 
13
- # Document blocks don't need to be terminated since they are not actually opened
14
- def assert_missing_delimitation!
18
+ def unknown_tag(tag, parse_context)
19
+ case tag
20
+ when 'else'.freeze, 'end'.freeze
21
+ raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_outer_tag".freeze, tag: tag))
22
+ else
23
+ raise SyntaxError.new(parse_context.locale.t("errors.syntax.unknown_tag".freeze, tag: tag))
24
+ end
15
25
  end
16
26
  end
17
27
  end
@@ -1,7 +1,6 @@
1
1
  require 'set'
2
2
 
3
3
  module Liquid
4
-
5
4
  # A drop in liquid is a class which allows you to export DOM like things to liquid.
6
5
  # Methods of drops are callable.
7
6
  # The main use for liquid drops is to implement lazy loaded objects.
@@ -19,43 +18,61 @@ module Liquid
19
18
  # tmpl = Liquid::Template.parse( ' {% for product in product.top_sales %} {{ product.name }} {%endfor%} ' )
20
19
  # tmpl.render('product' => ProductDrop.new ) # will invoke top_sales query.
21
20
  #
22
- # Your drop can either implement the methods sans any parameters or implement the before_method(name) method which is a
23
- # catch all.
21
+ # Your drop can either implement the methods sans any parameters
22
+ # or implement the liquid_method_missing(name) method which is a catch all.
24
23
  class Drop
25
24
  attr_writer :context
26
25
 
27
- EMPTY_STRING = ''.freeze
28
-
29
26
  # Catch all for the method
30
- def before_method(method)
31
- nil
27
+ def liquid_method_missing(method)
28
+ return nil unless @context && @context.strict_variables
29
+ raise Liquid::UndefinedDropMethod, "undefined method #{method}"
32
30
  end
33
31
 
34
32
  # called by liquid to invoke a drop
35
33
  def invoke_drop(method_or_key)
36
- if method_or_key && method_or_key != EMPTY_STRING && self.class.invokable?(method_or_key)
34
+ if self.class.invokable?(method_or_key)
37
35
  send(method_or_key)
38
36
  else
39
- before_method(method_or_key)
37
+ liquid_method_missing(method_or_key)
40
38
  end
41
39
  end
42
40
 
43
- def has_key?(name)
41
+ def key?(_name)
44
42
  true
45
43
  end
46
44
 
45
+ def inspect
46
+ self.class.to_s
47
+ end
48
+
47
49
  def to_liquid
48
50
  self
49
51
  end
50
52
 
51
- alias :[] :invoke_drop
53
+ def to_s
54
+ self.class.name
55
+ end
52
56
 
53
- private
57
+ alias_method :[], :invoke_drop
54
58
 
55
59
  # Check for method existence without invoking respond_to?, which creates symbols
56
60
  def self.invokable?(method_name)
57
- @invokable_methods ||= Set.new(["to_liquid"] + (public_instance_methods - Liquid::Drop.public_instance_methods).map(&:to_s))
58
- @invokable_methods.include?(method_name.to_s)
61
+ invokable_methods.include?(method_name.to_s)
62
+ end
63
+
64
+ def self.invokable_methods
65
+ @invokable_methods ||= begin
66
+ blacklist = Liquid::Drop.public_instance_methods + [:each]
67
+
68
+ if include?(Enumerable)
69
+ blacklist += Enumerable.public_instance_methods
70
+ blacklist -= [:sort, :count, :first, :min, :max, :include?]
71
+ end
72
+
73
+ whitelist = [:to_liquid] + (public_instance_methods - blacklist)
74
+ Set.new(whitelist.map(&:to_s))
75
+ end
59
76
  end
60
77
  end
61
78
  end
@@ -1,12 +1,56 @@
1
1
  module Liquid
2
- class Error < ::StandardError; end
3
-
4
- class ArgumentError < Error; end
5
- class ContextError < Error; end
6
- class FilterNotFound < Error; end
7
- class FileSystemError < Error; end
8
- class StandardError < Error; end
9
- class SyntaxError < Error; end
10
- class StackLevelError < Error; end
11
- class MemoryError < Error; end
2
+ class Error < ::StandardError
3
+ attr_accessor :line_number
4
+ attr_accessor :template_name
5
+ attr_accessor :markup_context
6
+
7
+ def to_s(with_prefix = true)
8
+ str = ""
9
+ str << message_prefix if with_prefix
10
+ str << super()
11
+
12
+ if markup_context
13
+ str << " "
14
+ str << markup_context
15
+ end
16
+
17
+ str
18
+ end
19
+
20
+ private
21
+
22
+ def message_prefix
23
+ str = ""
24
+ if is_a?(SyntaxError)
25
+ str << "Liquid syntax error"
26
+ else
27
+ str << "Liquid error"
28
+ end
29
+
30
+ if line_number
31
+ str << " ("
32
+ str << template_name << " " if template_name
33
+ str << "line " << line_number.to_s << ")"
34
+ end
35
+
36
+ str << ": "
37
+ str
38
+ end
39
+ end
40
+
41
+ ArgumentError = Class.new(Error)
42
+ ContextError = Class.new(Error)
43
+ FileSystemError = Class.new(Error)
44
+ StandardError = Class.new(Error)
45
+ SyntaxError = Class.new(Error)
46
+ StackLevelError = Class.new(Error)
47
+ TaintedError = Class.new(Error)
48
+ MemoryError = Class.new(Error)
49
+ ZeroDivisionError = Class.new(Error)
50
+ FloatDomainError = Class.new(Error)
51
+ UndefinedVariable = Class.new(Error)
52
+ UndefinedDropMethod = Class.new(Error)
53
+ UndefinedFilter = Class.new(Error)
54
+ MethodOverrideError = Class.new(Error)
55
+ InternalError = Class.new(Error)
12
56
  end
@@ -0,0 +1,49 @@
1
+ module Liquid
2
+ class Expression
3
+ class MethodLiteral
4
+ attr_reader :method_name, :to_s
5
+
6
+ def initialize(method_name, to_s)
7
+ @method_name = method_name
8
+ @to_s = to_s
9
+ end
10
+
11
+ def to_liquid
12
+ to_s
13
+ end
14
+ end
15
+
16
+ LITERALS = {
17
+ nil => nil, 'nil'.freeze => nil, 'null'.freeze => nil, ''.freeze => nil,
18
+ 'true'.freeze => true,
19
+ 'false'.freeze => false,
20
+ 'blank'.freeze => MethodLiteral.new(:blank?, '').freeze,
21
+ 'empty'.freeze => MethodLiteral.new(:empty?, '').freeze
22
+ }.freeze
23
+
24
+ SINGLE_QUOTED_STRING = /\A'(.*)'\z/m
25
+ DOUBLE_QUOTED_STRING = /\A"(.*)"\z/m
26
+ INTEGERS_REGEX = /\A(-?\d+)\z/
27
+ FLOATS_REGEX = /\A(-?\d[\d\.]+)\z/
28
+ RANGES_REGEX = /\A\((\S+)\.\.(\S+)\)\z/
29
+
30
+ def self.parse(markup)
31
+ if LITERALS.key?(markup)
32
+ LITERALS[markup]
33
+ else
34
+ case markup
35
+ when SINGLE_QUOTED_STRING, DOUBLE_QUOTED_STRING
36
+ $1
37
+ when INTEGERS_REGEX
38
+ $1.to_i
39
+ when RANGES_REGEX
40
+ RangeLookup.parse($1, $2)
41
+ when FLOATS_REGEX
42
+ $1.to_f
43
+ else
44
+ VariableLookup.parse(markup)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -7,44 +7,56 @@ class String # :nodoc:
7
7
  end
8
8
  end
9
9
 
10
- class Array # :nodoc:
10
+ class Symbol # :nodoc:
11
+ def to_liquid
12
+ to_s
13
+ end
14
+ end
15
+
16
+ class Array # :nodoc:
17
+ def to_liquid
18
+ self
19
+ end
20
+ end
21
+
22
+ class Hash # :nodoc:
11
23
  def to_liquid
12
24
  self
13
25
  end
14
26
  end
15
27
 
16
- class Hash # :nodoc:
28
+ class Numeric # :nodoc:
17
29
  def to_liquid
18
30
  self
19
31
  end
20
32
  end
21
33
 
22
- class Numeric # :nodoc:
34
+ class Range # :nodoc:
23
35
  def to_liquid
24
36
  self
25
37
  end
26
38
  end
27
39
 
28
- class Time # :nodoc:
40
+ class Time # :nodoc:
29
41
  def to_liquid
30
42
  self
31
43
  end
32
44
  end
33
45
 
34
- class DateTime < Date # :nodoc:
46
+ class DateTime < Date # :nodoc:
35
47
  def to_liquid
36
48
  self
37
49
  end
38
50
  end
39
51
 
40
- class Date # :nodoc:
52
+ class Date # :nodoc:
41
53
  def to_liquid
42
54
  self
43
55
  end
44
56
  end
45
57
 
46
58
  class TrueClass
47
- def to_liquid # :nodoc:
59
+ def to_liquid # :nodoc:
48
60
  self
49
61
  end
50
62
  end
@@ -8,13 +8,13 @@ module Liquid
8
8
  #
9
9
  # Example:
10
10
  #
11
- # Liquid::Template.file_system = Liquid::LocalFileSystem.new(template_path)
12
- # liquid = Liquid::Template.parse(template)
11
+ # Liquid::Template.file_system = Liquid::LocalFileSystem.new(template_path)
12
+ # liquid = Liquid::Template.parse(template)
13
13
  #
14
14
  # This will parse the template with a LocalFileSystem implementation rooted at 'template_path'.
15
15
  class BlankFileSystem
16
16
  # Called by Liquid to retrieve a template file
17
- def read_template_file(template_path, context)
17
+ def read_template_file(_template_path)
18
18
  raise FileSystemError, "This liquid context does not allow includes."
19
19
  end
20
20
  end
@@ -26,35 +26,46 @@ module Liquid
26
26
  #
27
27
  # Example:
28
28
  #
29
- # file_system = Liquid::LocalFileSystem.new("/some/path")
29
+ # file_system = Liquid::LocalFileSystem.new("/some/path")
30
30
  #
31
- # file_system.full_path("mypartial") # => "/some/path/_mypartial.liquid"
32
- # file_system.full_path("dir/mypartial") # => "/some/path/dir/_mypartial.liquid"
31
+ # file_system.full_path("mypartial") # => "/some/path/_mypartial.liquid"
32
+ # file_system.full_path("dir/mypartial") # => "/some/path/dir/_mypartial.liquid"
33
+ #
34
+ # Optionally in the second argument you can specify a custom pattern for template filenames.
35
+ # The Kernel::sprintf format specification is used.
36
+ # Default pattern is "_%s.liquid".
37
+ #
38
+ # Example:
39
+ #
40
+ # file_system = Liquid::LocalFileSystem.new("/some/path", "%s.html")
41
+ #
42
+ # file_system.full_path("index") # => "/some/path/index.html"
33
43
  #
34
44
  class LocalFileSystem
35
45
  attr_accessor :root
36
46
 
37
- def initialize(root)
47
+ def initialize(root, pattern = "_%s.liquid".freeze)
38
48
  @root = root
49
+ @pattern = pattern
39
50
  end
40
51
 
41
- def read_template_file(template_path, context)
52
+ def read_template_file(template_path)
42
53
  full_path = full_path(template_path)
43
- raise FileSystemError, "No such template '#{template_path}'" unless File.exists?(full_path)
54
+ raise FileSystemError, "No such template '#{template_path}'" unless File.exist?(full_path)
44
55
 
45
56
  File.read(full_path)
46
57
  end
47
58
 
48
59
  def full_path(template_path)
49
- raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ /^[^.\/][a-zA-Z0-9_\/]+$/
60
+ raise FileSystemError, "Illegal template name '#{template_path}'" unless template_path =~ /\A[^.\/][a-zA-Z0-9_\/]+\z/
50
61
 
51
- full_path = if template_path.include?('/')
52
- File.join(root, File.dirname(template_path), "_#{File.basename(template_path)}.liquid")
62
+ full_path = if template_path.include?('/'.freeze)
63
+ File.join(root, File.dirname(template_path), @pattern % File.basename(template_path))
53
64
  else
54
- File.join(root, "_#{template_path}.liquid")
65
+ File.join(root, @pattern % template_path)
55
66
  end
56
67
 
57
- raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path) =~ /^#{File.expand_path(root)}/
68
+ raise FileSystemError, "Illegal template path '#{File.expand_path(full_path)}'" unless File.expand_path(full_path).start_with?(File.expand_path(root))
58
69
 
59
70
  full_path
60
71
  end
@@ -0,0 +1,42 @@
1
+ module Liquid
2
+ class ForloopDrop < Drop
3
+ def initialize(name, length, parentloop)
4
+ @name = name
5
+ @length = length
6
+ @parentloop = parentloop
7
+ @index = 0
8
+ end
9
+
10
+ attr_reader :name, :length, :parentloop
11
+
12
+ def index
13
+ @index + 1
14
+ end
15
+
16
+ def index0
17
+ @index
18
+ end
19
+
20
+ def rindex
21
+ @length - @index
22
+ end
23
+
24
+ def rindex0
25
+ @length - @index - 1
26
+ end
27
+
28
+ def first
29
+ @index == 0
30
+ end
31
+
32
+ def last
33
+ @index == @length - 1
34
+ end
35
+
36
+ protected
37
+
38
+ def increment!
39
+ @index += 1
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,39 @@
1
+ require 'yaml'
2
+
3
+ module Liquid
4
+ class I18n
5
+ DEFAULT_LOCALE = File.join(File.expand_path(__dir__), "locales", "en.yml")
6
+
7
+ TranslationError = Class.new(StandardError)
8
+
9
+ attr_reader :path
10
+
11
+ def initialize(path = DEFAULT_LOCALE)
12
+ @path = path
13
+ end
14
+
15
+ def translate(name, vars = {})
16
+ interpolate(deep_fetch_translation(name), vars)
17
+ end
18
+ alias_method :t, :translate
19
+
20
+ def locale
21
+ @locale ||= YAML.load_file(@path)
22
+ end
23
+
24
+ private
25
+
26
+ def interpolate(name, vars)
27
+ name.gsub(/%\{(\w+)\}/) do
28
+ # raise TranslationError, "Undefined key #{$1} for interpolation in translation #{name}" unless vars[$1.to_sym]
29
+ (vars[$1.to_sym]).to_s
30
+ end
31
+ end
32
+
33
+ def deep_fetch_translation(name)
34
+ name.split('.'.freeze).reduce(locale) do |level, cur|
35
+ level[cur] or raise TranslationError, "Translation for #{name} does not exist in locale #{path}"
36
+ end
37
+ end
38
+ end
39
+ end