liquid 4.0.0.rc3 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (123) hide show
  1. checksums.yaml +5 -5
  2. data/History.md +93 -2
  3. data/README.md +8 -0
  4. data/lib/liquid.rb +18 -5
  5. data/lib/liquid/block.rb +47 -20
  6. data/lib/liquid/block_body.rb +190 -76
  7. data/lib/liquid/condition.rb +69 -29
  8. data/lib/liquid/context.rb +122 -76
  9. data/lib/liquid/document.rb +47 -9
  10. data/lib/liquid/drop.rb +4 -2
  11. data/lib/liquid/errors.rb +20 -25
  12. data/lib/liquid/expression.rb +30 -31
  13. data/lib/liquid/extensions.rb +8 -0
  14. data/lib/liquid/file_system.rb +6 -4
  15. data/lib/liquid/forloop_drop.rb +11 -4
  16. data/lib/liquid/i18n.rb +5 -3
  17. data/lib/liquid/interrupts.rb +3 -1
  18. data/lib/liquid/lexer.rb +35 -26
  19. data/lib/liquid/locales/en.yml +4 -2
  20. data/lib/liquid/parse_context.rb +17 -4
  21. data/lib/liquid/parse_tree_visitor.rb +42 -0
  22. data/lib/liquid/parser.rb +30 -18
  23. data/lib/liquid/parser_switching.rb +17 -3
  24. data/lib/liquid/partial_cache.rb +24 -0
  25. data/lib/liquid/profiler.rb +67 -86
  26. data/lib/liquid/profiler/hooks.rb +26 -14
  27. data/lib/liquid/range_lookup.rb +5 -3
  28. data/lib/liquid/register.rb +6 -0
  29. data/lib/liquid/resource_limits.rb +47 -8
  30. data/lib/liquid/standardfilters.rb +171 -57
  31. data/lib/liquid/static_registers.rb +44 -0
  32. data/lib/liquid/strainer_factory.rb +36 -0
  33. data/lib/liquid/strainer_template.rb +53 -0
  34. data/lib/liquid/tablerowloop_drop.rb +6 -4
  35. data/lib/liquid/tag.rb +28 -6
  36. data/lib/liquid/tag/disableable.rb +22 -0
  37. data/lib/liquid/tag/disabler.rb +21 -0
  38. data/lib/liquid/tags/assign.rb +32 -10
  39. data/lib/liquid/tags/break.rb +8 -3
  40. data/lib/liquid/tags/capture.rb +11 -8
  41. data/lib/liquid/tags/case.rb +41 -27
  42. data/lib/liquid/tags/comment.rb +5 -3
  43. data/lib/liquid/tags/continue.rb +8 -3
  44. data/lib/liquid/tags/cycle.rb +35 -16
  45. data/lib/liquid/tags/decrement.rb +6 -3
  46. data/lib/liquid/tags/echo.rb +26 -0
  47. data/lib/liquid/tags/for.rb +79 -47
  48. data/lib/liquid/tags/if.rb +53 -30
  49. data/lib/liquid/tags/ifchanged.rb +11 -10
  50. data/lib/liquid/tags/include.rb +42 -44
  51. data/lib/liquid/tags/increment.rb +7 -3
  52. data/lib/liquid/tags/raw.rb +14 -11
  53. data/lib/liquid/tags/render.rb +84 -0
  54. data/lib/liquid/tags/table_row.rb +32 -20
  55. data/lib/liquid/tags/unless.rb +15 -15
  56. data/lib/liquid/template.rb +60 -71
  57. data/lib/liquid/template_factory.rb +9 -0
  58. data/lib/liquid/tokenizer.rb +17 -9
  59. data/lib/liquid/usage.rb +8 -0
  60. data/lib/liquid/utils.rb +6 -4
  61. data/lib/liquid/variable.rb +55 -38
  62. data/lib/liquid/variable_lookup.rb +14 -6
  63. data/lib/liquid/version.rb +3 -1
  64. data/test/integration/assign_test.rb +74 -5
  65. data/test/integration/blank_test.rb +11 -8
  66. data/test/integration/block_test.rb +58 -0
  67. data/test/integration/capture_test.rb +18 -10
  68. data/test/integration/context_test.rb +608 -5
  69. data/test/integration/document_test.rb +4 -2
  70. data/test/integration/drop_test.rb +67 -83
  71. data/test/integration/error_handling_test.rb +90 -60
  72. data/test/integration/expression_test.rb +46 -0
  73. data/test/integration/filter_test.rb +53 -42
  74. data/test/integration/hash_ordering_test.rb +5 -3
  75. data/test/integration/output_test.rb +26 -24
  76. data/test/integration/parsing_quirks_test.rb +24 -8
  77. data/test/integration/{render_profiling_test.rb → profiler_test.rb} +84 -25
  78. data/test/integration/security_test.rb +41 -18
  79. data/test/integration/standard_filter_test.rb +523 -205
  80. data/test/integration/tag/disableable_test.rb +59 -0
  81. data/test/integration/tag_test.rb +45 -0
  82. data/test/integration/tags/break_tag_test.rb +4 -2
  83. data/test/integration/tags/continue_tag_test.rb +4 -2
  84. data/test/integration/tags/echo_test.rb +13 -0
  85. data/test/integration/tags/for_tag_test.rb +109 -53
  86. data/test/integration/tags/if_else_tag_test.rb +5 -3
  87. data/test/integration/tags/include_tag_test.rb +83 -52
  88. data/test/integration/tags/increment_tag_test.rb +4 -2
  89. data/test/integration/tags/liquid_tag_test.rb +116 -0
  90. data/test/integration/tags/raw_tag_test.rb +14 -11
  91. data/test/integration/tags/render_tag_test.rb +213 -0
  92. data/test/integration/tags/standard_tag_test.rb +38 -31
  93. data/test/integration/tags/statements_test.rb +23 -21
  94. data/test/integration/tags/table_row_test.rb +2 -0
  95. data/test/integration/tags/unless_else_tag_test.rb +4 -2
  96. data/test/integration/template_test.rb +128 -121
  97. data/test/integration/trim_mode_test.rb +82 -44
  98. data/test/integration/variable_test.rb +46 -31
  99. data/test/test_helper.rb +75 -23
  100. data/test/unit/block_unit_test.rb +19 -24
  101. data/test/unit/condition_unit_test.rb +82 -72
  102. data/test/unit/file_system_unit_test.rb +6 -4
  103. data/test/unit/i18n_unit_test.rb +7 -5
  104. data/test/unit/lexer_unit_test.rb +12 -10
  105. data/test/unit/parse_tree_visitor_test.rb +247 -0
  106. data/test/unit/parser_unit_test.rb +37 -35
  107. data/test/unit/partial_cache_unit_test.rb +128 -0
  108. data/test/unit/regexp_unit_test.rb +17 -15
  109. data/test/unit/static_registers_unit_test.rb +156 -0
  110. data/test/unit/strainer_factory_unit_test.rb +100 -0
  111. data/test/unit/strainer_template_unit_test.rb +82 -0
  112. data/test/unit/tag_unit_test.rb +5 -3
  113. data/test/unit/tags/case_tag_unit_test.rb +3 -1
  114. data/test/unit/tags/for_tag_unit_test.rb +4 -2
  115. data/test/unit/tags/if_tag_unit_test.rb +3 -1
  116. data/test/unit/template_factory_unit_test.rb +12 -0
  117. data/test/unit/template_unit_test.rb +19 -10
  118. data/test/unit/tokenizer_unit_test.rb +19 -17
  119. data/test/unit/variable_unit_test.rb +51 -49
  120. metadata +83 -50
  121. data/lib/liquid/strainer.rb +0 -65
  122. data/test/unit/context_unit_test.rb +0 -483
  123. data/test/unit/strainer_unit_test.rb +0 -136
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 3d29364ce9b0e428bfcf9e63df9695f791d8e13e
4
- data.tar.gz: 08cc19791695840bd1d6230ea9643cfa3ee9147c
2
+ SHA256:
3
+ metadata.gz: 0b1c036e6dd6d55418e6364b008551f5d3ba91f6e7dbc1c0ac3be43b235bd957
4
+ data.tar.gz: a6279802ed388bcffc8980c49bcb0034a0f98de7cd6730bf97f603c07bf13dcd
5
5
  SHA512:
6
- metadata.gz: 12c6c427dca1570ce4eee3634d3d6fbfd2f35d231e32678f3d43f19583233a3af8fa2049e04edaf69b41d8df553b75da72a5f5431bfbe2a6d0f1f906b5a97add
7
- data.tar.gz: 318e400a0ea7e6fedd5d066eda0995b438846d925167a2159885f9806747af683859d4fb5d2ca1853efc9c2bbebc9d3528a2a9013d04bc3c28eb223e56fee1d0
6
+ metadata.gz: 0c5094a47d46c8de3ac8ac632dbec8b813c1e47af834c0037ed9b868f5a2dde4da8dfdd143ff34c41505c1425524f1879ca909525676028cadd9900cbd028e63
7
+ data.tar.gz: 7195f154c81283e8b7d99a07b82a2d9976b41be761efc528945f79ac0d77c2ff9c7632b3455f871b6db0471979c35e633f87e1c33900e4f9898012f836fd1496
data/History.md CHANGED
@@ -1,8 +1,96 @@
1
1
  # Liquid Change Log
2
2
 
3
- ## 4.0.0 / not yet released / branch "master"
3
+ ## 5.0.0 / 2021-01-06
4
+
5
+ ### Features
6
+ * Add new `{% render %}` tag (#1122) [Samuel Doiron]
7
+ * Add support for `as` in `{% render %}` and `{% include %}` (#1181) [Mike Angell]
8
+ * Add `{% liquid %}` and `{% echo %}` tags (#1086) [Justin Li]
9
+ * Add [usage tracking](README.md#usage-tracking) [Mike Angell]
10
+ * Add `Tag.disable_tags` for disabling tags that prepend `Tag::Disableable` at render time (#1162, #1274, #1275) [Mike Angell]
11
+ * Support using a profiler for multiple renders (#1365, #1366) [Dylan Thacker-Smith]
12
+
13
+ ### Fixes
14
+ * Fix catastrophic backtracking in `RANGES_REGEX` regular expression (#1357) [Dylan Thacker-Smith]
15
+ * Make sure the for tag's limit and offset are integers (#1094) [David Cornu]
16
+ * Invokable methods for enumerable reject include (#1151) [Thierry Joyal]
17
+ * Allow `default` filter to handle `false` as value (#1144) [Mike Angell]
18
+ * Fix render length resource limit so it doesn't multiply nested output (#1285) [Dylan Thacker-Smith]
19
+ * Fix duplication of text in raw tags (#1304) [Peter Zhu]
20
+ * Fix strict parsing of find variable with a name expression (#1317) [Dylan Thacker-Smith]
21
+ * Use monotonic time to measure durations in Liquid::Profiler (#1362) [Dylan Thacker-Smith]
22
+
23
+ ### Breaking Changes
24
+ * Require Ruby >= 2.5 (#1131, #1310) [Mike Angell, Dylan Thacker-Smith]
25
+ * Remove support for taint checking (#1268) [Dylan Thacker-Smith]
26
+ * Split Strainer class into StrainerFactory and StrainerTemplate (#1208) [Thierry Joyal]
27
+ * Remove handling of a nil context in the Strainer class (#1218) [Thierry Joyal]
28
+ * Handle `BlockBody#blank?` at parse time (#1287) [Dylan Thacker-Smith]
29
+ * Pass the tag markup and tokenizer to `Document#unknown_tag` (#1290) [Dylan Thacker-Smith]
30
+ * And several internal changes
31
+
32
+ ### Performance Improvements
33
+ * Reduce allocations (#1073, #1091, #1115, #1099, #1117, #1141, #1322, #1341) [Richard Monette, Florian Weingarten, Ashwin Maroli]
34
+ * Improve resources limits performance (#1093, #1323) [Florian Weingarten, Dylan Thacker-Smith]
35
+
36
+ ## 4.0.3 / 2019-03-12
37
+
38
+ ### Fixed
39
+ * Fix break and continue tags inside included templates in loops (#1072) [Justin Li]
40
+
41
+ ## 4.0.2 / 2019-03-08
42
+
43
+ ### Changed
44
+ * Add `where` filter (#1026) [Samuel Doiron]
45
+ * Add `ParseTreeVisitor` to iterate the Liquid AST (#1025) [Stephen Paul Weber]
46
+ * Improve `strip_html` performance (#1032) [printercu]
47
+
48
+ ### Fixed
49
+ * Add error checking for invalid combinations of inputs to sort, sort_natural, where, uniq, map, compact filters (#1059) [Garland Zhang]
50
+ * Validate the character encoding in url_decode (#1070) [Clayton Smith]
51
+
52
+ ## 4.0.1 / 2018-10-09
4
53
 
5
54
  ### Changed
55
+ * Add benchmark group in Gemfile (#855) [Jerry Liu]
56
+ * Allow benchmarks to benchmark render by itself (#851) [Jerry Liu]
57
+ * Avoid calling `line_number` on String node when rescuing a render error. (#860) [Dylan Thacker-Smith]
58
+ * Avoid duck typing to detect whether to call render on a node. [Dylan Thacker-Smith]
59
+ * Clarify spelling of `reversed` on `for` block tag (#843) [Mark Crossfield]
60
+ * Replace recursion with loop to avoid potential stack overflow from malicious input (#891, #892) [Dylan Thacker-Smith]
61
+ * Limit block tag nesting to 100 (#894) [Dylan Thacker-Smith]
62
+ * Replace `assert_equal nil` with `assert_nil` (#895) [Dylan Thacker-Smith]
63
+ * Remove Spy Gem (#896) [Dylan Thacker-Smith]
64
+ * Add `collection_name` and `variable_name` reader to `For` block (#909)
65
+ * Symbols render as strings (#920) [Justin Li]
66
+ * Remove default value from Hash objects (#932) [Maxime Bedard]
67
+ * Remove one level of nesting (#944) [Dylan Thacker-Smith]
68
+ * Update Rubocop version (#952) [Justin Li]
69
+ * Add `at_least` and `at_most` filters (#954, #958) [Nithin Bekal]
70
+ * Add a regression test for a liquid-c trim mode bug (#972) [Dylan Thacker-Smith]
71
+ * Use https rather than git protocol to fetch liquid-c [Dylan Thacker-Smith]
72
+ * Add tests against Ruby 2.4 (#963) and 2.5 (#981)
73
+ * Replace RegExp literals with constants (#988) [Ashwin Maroli]
74
+ * Replace unnecessary `#each_with_index` with `#each` (#992) [Ashwin Maroli]
75
+ * Improve the unexpected end delimiter message for block tags. (#1003) [Dylan Thacker-Smith]
76
+ * Refactor and optimize rendering (#1005) [Christopher Aue]
77
+ * Add installation instruction (#1006) [Ben Gift]
78
+ * Remove Circle CI (#1010)
79
+ * Rename deprecated `BigDecimal.new` to `BigDecimal` (#1024) [Koichi ITO]
80
+ * Rename deprecated Rubocop name (#1027) [Justin Li]
81
+
82
+ ### Fixed
83
+ * Handle `join` filter on non String joiners (#857) [Richard Monette]
84
+ * Fix duplicate inclusion condition logic error of `Liquid::Strainer.add_filter` method (#861)
85
+ * Fix `escape`, `url_encode`, `url_decode` not handling non-string values (#898) [Thierry Joyal]
86
+ * Fix raise when variable is defined but nil when using `strict_variables` [Pascal Betz]
87
+ * Fix `sort` and `sort_natural` to handle arrays with nils (#930) [Eric Chan]
88
+
89
+ ## 4.0.0 / 2016-12-14 / branch "4-0-stable"
90
+
91
+ ### Changed
92
+ * Render an opaque internal error by default for non-Liquid::Error (#835) [Dylan Thacker-Smith]
93
+ * Ruby 2.0 support dropped (#832) [Dylan Thacker-Smith]
6
94
  * Add to_number Drop method to allow custom drops to work with number filters (#731)
7
95
  * Add strict_variables and strict_filters options to detect undefined references (#691)
8
96
  * Improve loop performance (#681) [Florian Weingarten]
@@ -18,10 +106,13 @@
18
106
  * Add concat filter to concatenate arrays (#429) [Diogo Beato]
19
107
  * Ruby 1.9 support dropped (#491) [Justin Li]
20
108
  * Liquid::Template.file_system's read_template_file method is no longer passed the context. (#441) [James Reid-Smith]
21
- * Remove support for `liquid_methods`
109
+ * Remove `liquid_methods` (See https://github.com/Shopify/liquid/pull/568 for replacement)
22
110
  * Liquid::Template.register_filter raises when the module overrides registered public methods as private or protected (#705) [Gaurav Chande]
23
111
 
24
112
  ### Fixed
113
+
114
+ * Fix variable names being detected as an operator when starting with contains (#788) [Michael Angell]
115
+ * Fix include tag used with strict_variables (#828) [QuickPay]
25
116
  * Fix map filter when value is a Proc (#672) [Guillaume Malette]
26
117
  * Fix truncate filter when value is not a string (#672) [Guillaume Malette]
27
118
  * Fix behaviour of escape filter when input is nil (#665) [Tanel Jakobsoo]
data/README.md CHANGED
@@ -42,6 +42,8 @@ Liquid is a template engine which was written with very specific requirements:
42
42
 
43
43
  ## How to use Liquid
44
44
 
45
+ Install Liquid by adding `gem 'liquid'` to your gemfile.
46
+
45
47
  Liquid supports a very simple API based around the Liquid::Template class.
46
48
  For standard use you can just pass it the content of a file and call render with a parameters hash.
47
49
 
@@ -104,3 +106,9 @@ template = Liquid::Template.parse("{{x}} {{y}}")
104
106
  template.render!({ 'x' => 1}, { strict_variables: true })
105
107
  #=> Liquid::UndefinedVariable: Liquid error: undefined variable y
106
108
  ```
109
+
110
+ ### Usage tracking
111
+
112
+ To help track usages of a feature or code path in production, we have released opt-in usage tracking. To enable this, we provide an empty `Liquid:: Usage.increment` method which you can customize to your needs. The feature is well suited to https://github.com/Shopify/statsd-instrument. However, the choice of implementation is up to you.
113
+
114
+ Once you have enabled usage tracking, we recommend reporting any events through Github Issues that your system may be logging. It is highly likely this event has been added to consider deprecating or improving code specific to this event, so please raise any concerns.
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Copyright (c) 2005 Tobias Luetke
2
4
  #
3
5
  # Permission is hereby granted, free of charge, to any person obtaining
@@ -21,10 +23,10 @@
21
23
 
22
24
  module Liquid
23
25
  FilterSeparator = /\|/
24
- ArgumentSeparator = ','.freeze
25
- FilterArgumentSeparator = ':'.freeze
26
- VariableAttributeSeparator = '.'.freeze
27
- WhitespaceControl = '-'.freeze
26
+ ArgumentSeparator = ','
27
+ FilterArgumentSeparator = ':'
28
+ VariableAttributeSeparator = '.'
29
+ WhitespaceControl = '-'
28
30
  TagStart = /\{\%/
29
31
  TagEnd = /\%\}/
30
32
  VariableSignature = /\(?[\w\-\.\[\]]\)?/
@@ -40,11 +42,14 @@ module Liquid
40
42
  TemplateParser = /(#{PartialTemplateParser}|#{AnyStartingTag})/om
41
43
  VariableParser = /\[[^\]]+\]|#{VariableSegment}+\??/o
42
44
 
45
+ RAISE_EXCEPTION_LAMBDA = ->(_e) { raise }
46
+
43
47
  singleton_class.send(:attr_accessor, :cache_classes)
44
48
  self.cache_classes = true
45
49
  end
46
50
 
47
51
  require "liquid/version"
52
+ require 'liquid/parse_tree_visitor'
48
53
  require 'liquid/lexer'
49
54
  require 'liquid/parser'
50
55
  require 'liquid/i18n'
@@ -54,11 +59,14 @@ require 'liquid/forloop_drop'
54
59
  require 'liquid/extensions'
55
60
  require 'liquid/errors'
56
61
  require 'liquid/interrupts'
57
- require 'liquid/strainer'
62
+ require 'liquid/strainer_factory'
63
+ require 'liquid/strainer_template'
58
64
  require 'liquid/expression'
59
65
  require 'liquid/context'
60
66
  require 'liquid/parser_switching'
61
67
  require 'liquid/tag'
68
+ require 'liquid/tag/disabler'
69
+ require 'liquid/tag/disableable'
62
70
  require 'liquid/block'
63
71
  require 'liquid/block_body'
64
72
  require 'liquid/document'
@@ -73,6 +81,11 @@ require 'liquid/condition'
73
81
  require 'liquid/utils'
74
82
  require 'liquid/tokenizer'
75
83
  require 'liquid/parse_context'
84
+ require 'liquid/partial_cache'
85
+ require 'liquid/usage'
86
+ require 'liquid/register'
87
+ require 'liquid/static_registers'
88
+ require 'liquid/template_factory'
76
89
 
77
90
  # Load all the tags of the standard library
78
91
  #
@@ -1,16 +1,22 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  class Block < Tag
5
+ MAX_DEPTH = 100
6
+
3
7
  def initialize(tag_name, markup, options)
4
8
  super
5
9
  @blank = true
6
10
  end
7
11
 
8
12
  def parse(tokens)
9
- @body = BlockBody.new
13
+ @body = new_body
10
14
  while parse_body(@body, tokens)
11
15
  end
16
+ @body.freeze
12
17
  end
13
18
 
19
+ # For backwards compatibility
14
20
  def render(context)
15
21
  @body.render(context)
16
22
  end
@@ -23,20 +29,29 @@ module Liquid
23
29
  @body.nodelist
24
30
  end
25
31
 
26
- def unknown_tag(tag, _params, _tokens)
27
- case tag
28
- when 'else'.freeze
29
- raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_else".freeze,
30
- block_name: block_name))
31
- when 'end'.freeze
32
- raise SyntaxError.new(parse_context.locale.t("errors.syntax.invalid_delimiter".freeze,
32
+ def unknown_tag(tag_name, _markup, _tokenizer)
33
+ Block.raise_unknown_tag(tag_name, block_name, block_delimiter, parse_context)
34
+ end
35
+
36
+ # @api private
37
+ def self.raise_unknown_tag(tag, block_name, block_delimiter, parse_context)
38
+ if tag == 'else'
39
+ raise SyntaxError, parse_context.locale.t("errors.syntax.unexpected_else",
40
+ block_name: block_name)
41
+ elsif tag.start_with?('end')
42
+ raise SyntaxError, parse_context.locale.t("errors.syntax.invalid_delimiter",
43
+ tag: tag,
33
44
  block_name: block_name,
34
- block_delimiter: block_delimiter))
45
+ block_delimiter: block_delimiter)
35
46
  else
36
- raise SyntaxError.new(parse_context.locale.t("errors.syntax.unknown_tag".freeze, tag: tag))
47
+ raise SyntaxError, parse_context.locale.t("errors.syntax.unknown_tag", tag: tag)
37
48
  end
38
49
  end
39
50
 
51
+ def raise_tag_never_closed(block_name)
52
+ raise SyntaxError, parse_context.locale.t("errors.syntax.tag_never_closed", block_name: block_name)
53
+ end
54
+
40
55
  def block_name
41
56
  @tag_name
42
57
  end
@@ -45,20 +60,32 @@ module Liquid
45
60
  @block_delimiter ||= "end#{block_name}"
46
61
  end
47
62
 
48
- protected
63
+ private
49
64
 
65
+ # @api public
66
+ def new_body
67
+ parse_context.new_block_body
68
+ end
69
+
70
+ # @api public
50
71
  def parse_body(body, tokens)
51
- body.parse(tokens, parse_context) do |end_tag_name, end_tag_params|
52
- @blank &&= body.blank?
72
+ if parse_context.depth >= MAX_DEPTH
73
+ raise StackLevelError, "Nesting too deep"
74
+ end
75
+ parse_context.depth += 1
76
+ begin
77
+ body.parse(tokens, parse_context) do |end_tag_name, end_tag_params|
78
+ @blank &&= body.blank?
53
79
 
54
- return false if end_tag_name == block_delimiter
55
- unless end_tag_name
56
- raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
57
- end
80
+ return false if end_tag_name == block_delimiter
81
+ raise_tag_never_closed(block_name) unless end_tag_name
58
82
 
59
- # this tag is not registered with the system
60
- # pass it to the current block for special handling or error reporting
61
- unknown_tag(end_tag_name, end_tag_params, tokens)
83
+ # this tag is not registered with the system
84
+ # pass it to the current block for special handling or error reporting
85
+ unknown_tag(end_tag_name, end_tag_params, tokens)
86
+ end
87
+ ensure
88
+ parse_context.depth -= 1
62
89
  end
63
90
 
64
91
  true
@@ -1,52 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'English'
4
+
1
5
  module Liquid
2
6
  class BlockBody
3
- FullToken = /\A#{TagStart}#{WhitespaceControl}?\s*(\w+)\s*(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
4
- ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
5
- TAGSTART = "{%".freeze
6
- VARSTART = "{{".freeze
7
+ LiquidTagToken = /\A\s*(\w+)\s*(.*?)\z/o
8
+ FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(\w+)(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
9
+ ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
10
+ WhitespaceOrNothing = /\A\s*\z/
11
+ TAGSTART = "{%"
12
+ VARSTART = "{{"
7
13
 
8
14
  attr_reader :nodelist
9
15
 
10
16
  def initialize
11
17
  @nodelist = []
12
- @blank = true
18
+ @blank = true
13
19
  end
14
20
 
15
- def parse(tokenizer, parse_context)
21
+ def parse(tokenizer, parse_context, &block)
22
+ raise FrozenError, "can't modify frozen Liquid::BlockBody" if frozen?
23
+
16
24
  parse_context.line_number = tokenizer.line_number
17
- while token = tokenizer.shift
18
- unless token.empty?
19
- case
20
- when token.start_with?(TAGSTART)
21
- whitespace_handler(token, parse_context)
22
- if token =~ FullToken
23
- tag_name = $1
24
- markup = $2
25
- # fetch the tag from registered blocks
26
- if tag = registered_tags[tag_name]
27
- new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
28
- @blank &&= new_tag.blank?
29
- @nodelist << new_tag
30
- else
31
- # end parsing if we reach an unknown tag and let the caller decide
32
- # determine how to proceed
33
- return yield tag_name, markup
34
- end
35
- else
36
- raise_missing_tag_terminator(token, parse_context)
37
- end
38
- when token.start_with?(VARSTART)
39
- whitespace_handler(token, parse_context)
40
- @nodelist << create_variable(token, parse_context)
41
- @blank = false
42
- else
43
- if parse_context.trim_whitespace
44
- token.lstrip!
45
- end
46
- parse_context.trim_whitespace = false
47
- @nodelist << token
48
- @blank &&= !!(token =~ /\A\s*\z/)
25
+
26
+ if tokenizer.for_liquid_tag
27
+ parse_for_liquid_tag(tokenizer, parse_context, &block)
28
+ else
29
+ parse_for_document(tokenizer, parse_context, &block)
30
+ end
31
+ end
32
+
33
+ def freeze
34
+ @nodelist.freeze
35
+ super
36
+ end
37
+
38
+ private def parse_for_liquid_tag(tokenizer, parse_context)
39
+ while (token = tokenizer.shift)
40
+ unless token.empty? || token =~ WhitespaceOrNothing
41
+ unless token =~ LiquidTagToken
42
+ # line isn't empty but didn't match tag syntax, yield and let the
43
+ # caller raise a syntax error
44
+ return yield token, token
49
45
  end
46
+ tag_name = Regexp.last_match(1)
47
+ markup = Regexp.last_match(2)
48
+ unless (tag = registered_tags[tag_name])
49
+ # end parsing if we reach an unknown tag and let the caller decide
50
+ # determine how to proceed
51
+ return yield tag_name, markup
52
+ end
53
+ new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
54
+ @blank &&= new_tag.blank?
55
+ @nodelist << new_tag
56
+ end
57
+ parse_context.line_number = tokenizer.line_number
58
+ end
59
+
60
+ yield nil, nil
61
+ end
62
+
63
+ # @api private
64
+ def self.unknown_tag_in_liquid_tag(tag, parse_context)
65
+ Block.raise_unknown_tag(tag, 'liquid', '%}', parse_context)
66
+ end
67
+
68
+ # @api private
69
+ def self.raise_missing_tag_terminator(token, parse_context)
70
+ raise SyntaxError, parse_context.locale.t("errors.syntax.tag_termination", token: token, tag_end: TagEnd.inspect)
71
+ end
72
+
73
+ # @api private
74
+ def self.raise_missing_variable_terminator(token, parse_context)
75
+ raise SyntaxError, parse_context.locale.t("errors.syntax.variable_termination", token: token, tag_end: VariableEnd.inspect)
76
+ end
77
+
78
+ # @api private
79
+ def self.render_node(context, output, node)
80
+ node.render_to_output_buffer(context, output)
81
+ rescue => exc
82
+ blank_tag = !node.instance_of?(Variable) && node.blank?
83
+ rescue_render_node(context, output, node.line_number, exc, blank_tag)
84
+ end
85
+
86
+ # @api private
87
+ def self.rescue_render_node(context, output, line_number, exc, blank_tag)
88
+ case exc
89
+ when MemoryError
90
+ raise
91
+ when UndefinedVariable, UndefinedDropMethod, UndefinedFilter
92
+ context.handle_error(exc, line_number)
93
+ else
94
+ error_message = context.handle_error(exc, line_number)
95
+ unless blank_tag # conditional for backwards compatibility
96
+ output << error_message
97
+ end
98
+ end
99
+ end
100
+
101
+ private def parse_liquid_tag(markup, parse_context)
102
+ liquid_tag_tokenizer = Tokenizer.new(markup, line_number: parse_context.line_number, for_liquid_tag: true)
103
+ parse_for_liquid_tag(liquid_tag_tokenizer, parse_context) do |end_tag_name, _end_tag_markup|
104
+ if end_tag_name
105
+ BlockBody.unknown_tag_in_liquid_tag(end_tag_name, parse_context)
106
+ end
107
+ end
108
+ end
109
+
110
+ private def parse_for_document(tokenizer, parse_context)
111
+ while (token = tokenizer.shift)
112
+ next if token.empty?
113
+ case
114
+ when token.start_with?(TAGSTART)
115
+ whitespace_handler(token, parse_context)
116
+ unless token =~ FullToken
117
+ BlockBody.raise_missing_tag_terminator(token, parse_context)
118
+ end
119
+ tag_name = Regexp.last_match(2)
120
+ markup = Regexp.last_match(4)
121
+
122
+ if parse_context.line_number
123
+ # newlines inside the tag should increase the line number,
124
+ # particularly important for multiline {% liquid %} tags
125
+ parse_context.line_number += Regexp.last_match(1).count("\n") + Regexp.last_match(3).count("\n")
126
+ end
127
+
128
+ if tag_name == 'liquid'
129
+ parse_liquid_tag(markup, parse_context)
130
+ next
131
+ end
132
+
133
+ unless (tag = registered_tags[tag_name])
134
+ # end parsing if we reach an unknown tag and let the caller decide
135
+ # determine how to proceed
136
+ return yield tag_name, markup
137
+ end
138
+ new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
139
+ @blank &&= new_tag.blank?
140
+ @nodelist << new_tag
141
+ when token.start_with?(VARSTART)
142
+ whitespace_handler(token, parse_context)
143
+ @nodelist << create_variable(token, parse_context)
144
+ @blank = false
145
+ else
146
+ if parse_context.trim_whitespace
147
+ token.lstrip!
148
+ end
149
+ parse_context.trim_whitespace = false
150
+ @nodelist << token
151
+ @blank &&= !!(token =~ WhitespaceOrNothing)
50
152
  end
51
153
  parse_context.line_number = tokenizer.line_number
52
154
  end
@@ -57,8 +159,12 @@ module Liquid
57
159
  def whitespace_handler(token, parse_context)
58
160
  if token[2] == WhitespaceControl
59
161
  previous_token = @nodelist.last
60
- if previous_token.is_a? String
162
+ if previous_token.is_a?(String)
163
+ first_byte = previous_token.getbyte(0)
61
164
  previous_token.rstrip!
165
+ if previous_token.empty? && parse_context[:bug_compatible_whitespace_trimming] && first_byte
166
+ previous_token << first_byte
167
+ end
62
168
  end
63
169
  end
64
170
  parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
@@ -68,52 +174,58 @@ module Liquid
68
174
  @blank
69
175
  end
70
176
 
177
+ # Remove blank strings in the block body for a control flow tag (e.g. `if`, `for`, `case`, `unless`)
178
+ # with a blank body.
179
+ #
180
+ # For example, in a conditional assignment like the following
181
+ #
182
+ # ```
183
+ # {% if size > max_size %}
184
+ # {% assign size = max_size %}
185
+ # {% endif %}
186
+ # ```
187
+ #
188
+ # we assume the intention wasn't to output the blank spaces in the `if` tag's block body, so this method
189
+ # will remove them to reduce the render output size.
190
+ #
191
+ # Note that it is now preferred to use the `liquid` tag for this use case.
192
+ def remove_blank_strings
193
+ raise "remove_blank_strings only support being called on a blank block body" unless @blank
194
+ @nodelist.reject! { |node| node.instance_of?(String) }
195
+ end
196
+
71
197
  def render(context)
72
- output = []
73
- context.resource_limits.render_score += @nodelist.length
198
+ render_to_output_buffer(context, +'')
199
+ end
200
+
201
+ def render_to_output_buffer(context, output)
202
+ freeze unless frozen?
74
203
 
75
- @nodelist.each do |token|
76
- # Break out if we have any unhanded interrupts.
77
- break if context.interrupt?
204
+ context.resource_limits.increment_render_score(@nodelist.length)
78
205
 
79
- begin
206
+ idx = 0
207
+ while (node = @nodelist[idx])
208
+ if node.instance_of?(String)
209
+ output << node
210
+ else
211
+ render_node(context, output, node)
80
212
  # If we get an Interrupt that means the block must stop processing. An
81
213
  # Interrupt is any command that stops block execution such as {% break %}
82
- # or {% continue %}
83
- if token.is_a?(Continue) || token.is_a?(Break)
84
- context.push_interrupt(token.interrupt)
85
- break
86
- end
87
-
88
- node_output = render_node(token, context)
89
-
90
- unless token.is_a?(Block) && token.blank?
91
- output << node_output
92
- end
93
- rescue MemoryError => e
94
- raise e
95
- rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
96
- context.handle_error(e, token.line_number)
97
- output << nil
98
- rescue ::StandardError => e
99
- output << context.handle_error(e, token.line_number)
214
+ # or {% continue %}. These tags may also occur through Block or Include tags.
215
+ break if context.interrupt? # might have happened in a for-block
100
216
  end
217
+ idx += 1
218
+
219
+ context.resource_limits.increment_write_score(output)
101
220
  end
102
221
 
103
- output.join
222
+ output
104
223
  end
105
224
 
106
225
  private
107
226
 
108
- def render_node(node, context)
109
- node_output = (node.respond_to?(:render) ? node.render(context) : node)
110
- node_output = node_output.is_a?(Array) ? node_output.join : node_output.to_s
111
-
112
- context.resource_limits.render_length += node_output.length
113
- if context.resource_limits.reached?
114
- raise MemoryError.new("Memory limits exceeded".freeze)
115
- end
116
- node_output
227
+ def render_node(context, output, node)
228
+ BlockBody.render_node(context, output, node)
117
229
  end
118
230
 
119
231
  def create_variable(token, parse_context)
@@ -121,15 +233,17 @@ module Liquid
121
233
  markup = content.first
122
234
  return Variable.new(markup, parse_context)
123
235
  end
124
- raise_missing_variable_terminator(token, parse_context)
236
+ BlockBody.raise_missing_variable_terminator(token, parse_context)
125
237
  end
126
238
 
239
+ # @deprecated Use {.raise_missing_tag_terminator} instead
127
240
  def raise_missing_tag_terminator(token, parse_context)
128
- raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_termination".freeze, token: token, tag_end: TagEnd.inspect))
241
+ BlockBody.raise_missing_tag_terminator(token, parse_context)
129
242
  end
130
243
 
244
+ # @deprecated Use {.raise_missing_variable_terminator} instead
131
245
  def raise_missing_variable_terminator(token, parse_context)
132
- raise SyntaxError.new(parse_context.locale.t("errors.syntax.variable_termination".freeze, token: token, tag_end: VariableEnd.inspect))
246
+ BlockBody.raise_missing_variable_terminator(token, parse_context)
133
247
  end
134
248
 
135
249
  def registered_tags