liquid 4.0.1 → 5.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +142 -0
  3. data/README.md +10 -4
  4. data/lib/liquid/block.rb +31 -14
  5. data/lib/liquid/block_body.rb +169 -56
  6. data/lib/liquid/condition.rb +59 -23
  7. data/lib/liquid/context.rb +111 -52
  8. data/lib/liquid/document.rb +47 -9
  9. data/lib/liquid/drop.rb +4 -2
  10. data/lib/liquid/errors.rb +20 -18
  11. data/lib/liquid/expression.rb +29 -33
  12. data/lib/liquid/extensions.rb +2 -0
  13. data/lib/liquid/file_system.rb +6 -4
  14. data/lib/liquid/forloop_drop.rb +54 -4
  15. data/lib/liquid/i18n.rb +5 -3
  16. data/lib/liquid/interrupts.rb +3 -1
  17. data/lib/liquid/lexer.rb +31 -24
  18. data/lib/liquid/locales/en.yml +8 -5
  19. data/lib/liquid/parse_context.rb +20 -4
  20. data/lib/liquid/parse_tree_visitor.rb +42 -0
  21. data/lib/liquid/parser.rb +30 -18
  22. data/lib/liquid/parser_switching.rb +17 -3
  23. data/lib/liquid/partial_cache.rb +24 -0
  24. data/lib/liquid/profiler/hooks.rb +26 -14
  25. data/lib/liquid/profiler.rb +67 -86
  26. data/lib/liquid/range_lookup.rb +13 -3
  27. data/lib/liquid/registers.rb +51 -0
  28. data/lib/liquid/resource_limits.rb +47 -8
  29. data/lib/liquid/standardfilters.rb +616 -129
  30. data/lib/liquid/strainer_factory.rb +41 -0
  31. data/lib/liquid/strainer_template.rb +62 -0
  32. data/lib/liquid/tablerowloop_drop.rb +64 -5
  33. data/lib/liquid/tag/disableable.rb +22 -0
  34. data/lib/liquid/tag/disabler.rb +21 -0
  35. data/lib/liquid/tag.rb +28 -6
  36. data/lib/liquid/tags/assign.rb +44 -18
  37. data/lib/liquid/tags/break.rb +16 -3
  38. data/lib/liquid/tags/capture.rb +24 -18
  39. data/lib/liquid/tags/case.rb +69 -27
  40. data/lib/liquid/tags/comment.rb +18 -3
  41. data/lib/liquid/tags/continue.rb +16 -12
  42. data/lib/liquid/tags/cycle.rb +45 -25
  43. data/lib/liquid/tags/decrement.rb +22 -20
  44. data/lib/liquid/tags/echo.rb +41 -0
  45. data/lib/liquid/tags/for.rb +97 -89
  46. data/lib/liquid/tags/if.rb +61 -35
  47. data/lib/liquid/tags/ifchanged.rb +11 -10
  48. data/lib/liquid/tags/include.rb +56 -56
  49. data/lib/liquid/tags/increment.rb +23 -17
  50. data/lib/liquid/tags/inline_comment.rb +43 -0
  51. data/lib/liquid/tags/raw.rb +25 -11
  52. data/lib/liquid/tags/render.rb +109 -0
  53. data/lib/liquid/tags/table_row.rb +53 -19
  54. data/lib/liquid/tags/unless.rb +38 -19
  55. data/lib/liquid/template.rb +52 -72
  56. data/lib/liquid/template_factory.rb +9 -0
  57. data/lib/liquid/tokenizer.rb +18 -10
  58. data/lib/liquid/usage.rb +8 -0
  59. data/lib/liquid/utils.rb +13 -3
  60. data/lib/liquid/variable.rb +52 -41
  61. data/lib/liquid/variable_lookup.rb +24 -10
  62. data/lib/liquid/version.rb +3 -1
  63. data/lib/liquid.rb +19 -6
  64. metadata +21 -104
  65. data/lib/liquid/strainer.rb +0 -66
  66. data/test/fixtures/en_locale.yml +0 -9
  67. data/test/integration/assign_test.rb +0 -48
  68. data/test/integration/blank_test.rb +0 -106
  69. data/test/integration/block_test.rb +0 -12
  70. data/test/integration/capture_test.rb +0 -50
  71. data/test/integration/context_test.rb +0 -32
  72. data/test/integration/document_test.rb +0 -19
  73. data/test/integration/drop_test.rb +0 -273
  74. data/test/integration/error_handling_test.rb +0 -260
  75. data/test/integration/filter_test.rb +0 -178
  76. data/test/integration/hash_ordering_test.rb +0 -23
  77. data/test/integration/output_test.rb +0 -123
  78. data/test/integration/parsing_quirks_test.rb +0 -122
  79. data/test/integration/render_profiling_test.rb +0 -154
  80. data/test/integration/security_test.rb +0 -80
  81. data/test/integration/standard_filter_test.rb +0 -626
  82. data/test/integration/tags/break_tag_test.rb +0 -15
  83. data/test/integration/tags/continue_tag_test.rb +0 -15
  84. data/test/integration/tags/for_tag_test.rb +0 -410
  85. data/test/integration/tags/if_else_tag_test.rb +0 -188
  86. data/test/integration/tags/include_tag_test.rb +0 -245
  87. data/test/integration/tags/increment_tag_test.rb +0 -23
  88. data/test/integration/tags/raw_tag_test.rb +0 -31
  89. data/test/integration/tags/standard_tag_test.rb +0 -296
  90. data/test/integration/tags/statements_test.rb +0 -111
  91. data/test/integration/tags/table_row_test.rb +0 -64
  92. data/test/integration/tags/unless_else_tag_test.rb +0 -26
  93. data/test/integration/template_test.rb +0 -332
  94. data/test/integration/trim_mode_test.rb +0 -529
  95. data/test/integration/variable_test.rb +0 -96
  96. data/test/test_helper.rb +0 -116
  97. data/test/unit/block_unit_test.rb +0 -58
  98. data/test/unit/condition_unit_test.rb +0 -166
  99. data/test/unit/context_unit_test.rb +0 -489
  100. data/test/unit/file_system_unit_test.rb +0 -35
  101. data/test/unit/i18n_unit_test.rb +0 -37
  102. data/test/unit/lexer_unit_test.rb +0 -51
  103. data/test/unit/parser_unit_test.rb +0 -82
  104. data/test/unit/regexp_unit_test.rb +0 -44
  105. data/test/unit/strainer_unit_test.rb +0 -164
  106. data/test/unit/tag_unit_test.rb +0 -21
  107. data/test/unit/tags/case_tag_unit_test.rb +0 -10
  108. data/test/unit/tags/for_tag_unit_test.rb +0 -13
  109. data/test/unit/tags/if_tag_unit_test.rb +0 -8
  110. data/test/unit/template_unit_test.rb +0 -78
  111. data/test/unit/tokenizer_unit_test.rb +0 -55
  112. data/test/unit/variable_unit_test.rb +0 -162
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 55d4aa3aeeef9a99c5601c5c8bbaab62b0c909c9aa3df0ac181e01373a6ed069
4
- data.tar.gz: 1e3eddbb13e867eca4f90efd32fbbc2609e4235b77d005efaa93711f8e476c40
3
+ metadata.gz: c8408df245a1cc22ee1154fe5387e3e41eb3c740f7277e7d3736c1863d387e47
4
+ data.tar.gz: 64549a58828fd7e9e0eb7310cb4371e75e64a7c3249fe9ebd021039e3334bba1
5
5
  SHA512:
6
- metadata.gz: 349a44c983e69443a0350d3aa7de2fffed15bf67356a6ce490583413d60dd926cc32907b1fca4d04ca075dd92e17434176f94552237ec5ae549477ccbbff4042
7
- data.tar.gz: 31d9c4fbe841ad2c3a6549e1e6d02f580b31a538186e83140bd297fb47caf23387795a48468886e82562f661231890cb42637ecbe369dc4583d560055e94ca77
6
+ metadata.gz: 29aaff16e3bc464712cdcac3aa205df943132198ef9568d46f4a123d327849f7ead7bd669a095eae0425581eb59ea08874ac38664bf9d8df00b0aa99917c7768
7
+ data.tar.gz: 9f6070c39733b7f4064f70676b1f886fc61070f3ba8921360b9c5fcdb09c217b3e58c3d59d8293b24b6c18e55899ed17552a2f867ea8eb95e7dee9a7deb29ae5
data/History.md CHANGED
@@ -1,5 +1,147 @@
1
1
  # Liquid Change Log
2
2
 
3
+ ## 5.4.0 2022-07-29
4
+
5
+ ### Breaking Changes
6
+ * Drop support for end-of-life Ruby versions (2.5 and 2.6) (#1578) [Andy Waite]
7
+
8
+ ### Features
9
+ * Allow `#` to be used as an inline comment tag (#1498) [CP Clermont]
10
+
11
+ ### Fixes
12
+ * `PartialCache` now shares snippet cache with subcontexts by default (#1553) [Chris AtLee]
13
+ * Hash registers no longer leak into subcontexts as static registers (#1564) [Chris AtLee]
14
+ * Fix `ParseTreeVisitor` for `with` variable expressions in `Render` tag (#1596) [CP Clermont]
15
+
16
+ ### Changed
17
+ * Liquid::Context#registers now always returns a Liquid::Registers object, though supports the most used Hash functions for compatibility (#1553)
18
+
19
+ ## 5.3.0 2022-03-22
20
+
21
+ ### Fixes
22
+ * StandardFilter: Fix missing @context on iterations (#1525) [Thierry Joyal]
23
+ * Fix warning about block and default value in `static_registers.rb` (#1531) [Peter Zhu]
24
+
25
+ ### Deprecation
26
+ * Condition#evaluate to require mandatory context argument in Liquid 6.0.0 (#1527) [Thierry Joyal]
27
+
28
+ ## 5.2.0 2022-03-01
29
+
30
+ ### Features
31
+ * Add `remove_last`, and `replace_last` filters (#1422) [Anders Hagbard]
32
+ * Eagerly cache global filters (#1524) [Jean Boussier]
33
+
34
+ ### Fixes
35
+ * Fix some internal errors in filters from invalid input (#1476) [Dylan Thacker-Smith]
36
+ * Allow dash in filter kwarg name for consistency with Liquid::C (#1518) [CP Clermont]
37
+
38
+ ## 5.1.0 / 2021-09-09
39
+
40
+ ### Features
41
+ * Add `base64_encode`, `base64_decode`, `base64_url_safe_encode`, and `base64_url_safe_decode` filters (#1450) [Daniel Insley]
42
+ * Introduce `to_liquid_value` in `Liquid::Drop` (#1441) [Michael Go]
43
+
44
+ ### Fixes
45
+ * Fix support for using a String subclass for the liquid source (#1421) [Dylan Thacker-Smith]
46
+ * Add `ParseTreeVisitor` to `RangeLookup` (#1470) [CP Clermont]
47
+ * Translate `RangeError` to `Liquid::Error` for `truncatewords` with large int (#1431) [Dylan Thacker-Smith]
48
+
49
+ ## 5.0.1 / 2021-03-24
50
+
51
+ ### Fixes
52
+ * Add ParseTreeVisitor to Echo tag (#1414) [CP Clermont]
53
+ * Test with ruby 3.0 as the latest ruby version (#1398) [Dylan Thacker-Smith]
54
+ * Handle carriage return in newlines_to_br (#1391) [Unending]
55
+
56
+ ### Performance Improvements
57
+ * Use split limit in truncatewords (#1361) [Dylan Thacker-Smith]
58
+
59
+ ## 5.0.0 / 2021-01-06
60
+
61
+ ### Features
62
+ * Add new `{% render %}` tag (#1122) [Samuel Doiron]
63
+ * Add support for `as` in `{% render %}` and `{% include %}` (#1181) [Mike Angell]
64
+ * Add `{% liquid %}` and `{% echo %}` tags (#1086) [Justin Li]
65
+ * Add [usage tracking](README.md#usage-tracking) [Mike Angell]
66
+ * Add `Tag.disable_tags` for disabling tags that prepend `Tag::Disableable` at render time (#1162, #1274, #1275) [Mike Angell]
67
+ * Support using a profiler for multiple renders (#1365, #1366) [Dylan Thacker-Smith]
68
+
69
+ ### Fixes
70
+ * Fix catastrophic backtracking in `RANGES_REGEX` regular expression (#1357) [Dylan Thacker-Smith]
71
+ * Make sure the for tag's limit and offset are integers (#1094) [David Cornu]
72
+ * Invokable methods for enumerable reject include (#1151) [Thierry Joyal]
73
+ * Allow `default` filter to handle `false` as value (#1144) [Mike Angell]
74
+ * Fix render length resource limit so it doesn't multiply nested output (#1285) [Dylan Thacker-Smith]
75
+ * Fix duplication of text in raw tags (#1304) [Peter Zhu]
76
+ * Fix strict parsing of find variable with a name expression (#1317) [Dylan Thacker-Smith]
77
+ * Use monotonic time to measure durations in Liquid::Profiler (#1362) [Dylan Thacker-Smith]
78
+
79
+ ### Breaking Changes
80
+ * Require Ruby >= 2.5 (#1131, #1310) [Mike Angell, Dylan Thacker-Smith]
81
+ * Remove support for taint checking (#1268) [Dylan Thacker-Smith]
82
+ * Split Strainer class into StrainerFactory and StrainerTemplate (#1208) [Thierry Joyal]
83
+ * Remove handling of a nil context in the Strainer class (#1218) [Thierry Joyal]
84
+ * Handle `BlockBody#blank?` at parse time (#1287) [Dylan Thacker-Smith]
85
+ * Pass the tag markup and tokenizer to `Document#unknown_tag` (#1290) [Dylan Thacker-Smith]
86
+ * And several internal changes
87
+
88
+ ### Performance Improvements
89
+ * Reduce allocations (#1073, #1091, #1115, #1099, #1117, #1141, #1322, #1341) [Richard Monette, Florian Weingarten, Ashwin Maroli]
90
+ * Improve resources limits performance (#1093, #1323) [Florian Weingarten, Dylan Thacker-Smith]
91
+
92
+ ## 4.0.3 / 2019-03-12
93
+
94
+ ### Fixed
95
+ * Fix break and continue tags inside included templates in loops (#1072) [Justin Li]
96
+
97
+ ## 4.0.2 / 2019-03-08
98
+
99
+ ### Changed
100
+ * Add `where` filter (#1026) [Samuel Doiron]
101
+ * Add `ParseTreeVisitor` to iterate the Liquid AST (#1025) [Stephen Paul Weber]
102
+ * Improve `strip_html` performance (#1032) [printercu]
103
+
104
+ ### Fixed
105
+ * Add error checking for invalid combinations of inputs to sort, sort_natural, where, uniq, map, compact filters (#1059) [Garland Zhang]
106
+ * Validate the character encoding in url_decode (#1070) [Clayton Smith]
107
+
108
+ ## 4.0.1 / 2018-10-09
109
+
110
+ ### Changed
111
+ * Add benchmark group in Gemfile (#855) [Jerry Liu]
112
+ * Allow benchmarks to benchmark render by itself (#851) [Jerry Liu]
113
+ * Avoid calling `line_number` on String node when rescuing a render error. (#860) [Dylan Thacker-Smith]
114
+ * Avoid duck typing to detect whether to call render on a node. [Dylan Thacker-Smith]
115
+ * Clarify spelling of `reversed` on `for` block tag (#843) [Mark Crossfield]
116
+ * Replace recursion with loop to avoid potential stack overflow from malicious input (#891, #892) [Dylan Thacker-Smith]
117
+ * Limit block tag nesting to 100 (#894) [Dylan Thacker-Smith]
118
+ * Replace `assert_equal nil` with `assert_nil` (#895) [Dylan Thacker-Smith]
119
+ * Remove Spy Gem (#896) [Dylan Thacker-Smith]
120
+ * Add `collection_name` and `variable_name` reader to `For` block (#909)
121
+ * Symbols render as strings (#920) [Justin Li]
122
+ * Remove default value from Hash objects (#932) [Maxime Bedard]
123
+ * Remove one level of nesting (#944) [Dylan Thacker-Smith]
124
+ * Update Rubocop version (#952) [Justin Li]
125
+ * Add `at_least` and `at_most` filters (#954, #958) [Nithin Bekal]
126
+ * Add a regression test for a liquid-c trim mode bug (#972) [Dylan Thacker-Smith]
127
+ * Use https rather than git protocol to fetch liquid-c [Dylan Thacker-Smith]
128
+ * Add tests against Ruby 2.4 (#963) and 2.5 (#981)
129
+ * Replace RegExp literals with constants (#988) [Ashwin Maroli]
130
+ * Replace unnecessary `#each_with_index` with `#each` (#992) [Ashwin Maroli]
131
+ * Improve the unexpected end delimiter message for block tags. (#1003) [Dylan Thacker-Smith]
132
+ * Refactor and optimize rendering (#1005) [Christopher Aue]
133
+ * Add installation instruction (#1006) [Ben Gift]
134
+ * Remove Circle CI (#1010)
135
+ * Rename deprecated `BigDecimal.new` to `BigDecimal` (#1024) [Koichi ITO]
136
+ * Rename deprecated Rubocop name (#1027) [Justin Li]
137
+
138
+ ### Fixed
139
+ * Handle `join` filter on non String joiners (#857) [Richard Monette]
140
+ * Fix duplicate inclusion condition logic error of `Liquid::Strainer.add_filter` method (#861)
141
+ * Fix `escape`, `url_encode`, `url_decode` not handling non-string values (#898) [Thierry Joyal]
142
+ * Fix raise when variable is defined but nil when using `strict_variables` [Pascal Betz]
143
+ * Fix `sort` and `sort_natural` to handle arrays with nils (#930) [Eric Chan]
144
+
3
145
  ## 4.0.0 / 2016-12-14 / branch "4-0-stable"
4
146
 
5
147
  ### Changed
data/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  * [Contributing guidelines](CONTRIBUTING.md)
7
7
  * [Version history](History.md)
8
- * [Liquid documentation from Shopify](http://docs.shopify.com/themes/liquid-basics)
8
+ * [Liquid documentation from Shopify](https://shopify.dev/api/liquid)
9
9
  * [Liquid Wiki at GitHub](https://github.com/Shopify/liquid/wiki)
10
10
  * [Website](http://liquidmarkup.org/)
11
11
 
@@ -56,20 +56,20 @@ For standard use you can just pass it the content of a file and call render with
56
56
 
57
57
  Setting the error mode of Liquid lets you specify how strictly you want your templates to be interpreted.
58
58
  Normally the parser is very lax and will accept almost anything without error. Unfortunately this can make
59
- it very hard to debug and can lead to unexpected behaviour.
59
+ it very hard to debug and can lead to unexpected behaviour.
60
60
 
61
61
  Liquid also comes with a stricter parser that can be used when editing templates to give better error messages
62
62
  when templates are invalid. You can enable this new parser like this:
63
63
 
64
64
  ```ruby
65
65
  Liquid::Template.error_mode = :strict # Raises a SyntaxError when invalid syntax is used
66
- Liquid::Template.error_mode = :warn # Adds errors to template.errors but continues as normal
66
+ Liquid::Template.error_mode = :warn # Adds strict errors to template.errors but continues as normal
67
67
  Liquid::Template.error_mode = :lax # The default mode, accepts almost anything.
68
68
  ```
69
69
 
70
70
  If you want to set the error mode only on specific templates you can pass `:error_mode` as an option to `parse`:
71
71
  ```ruby
72
- Liquid::Template.parse(source, :error_mode => :strict)
72
+ Liquid::Template.parse(source, error_mode: :strict)
73
73
  ```
74
74
  This is useful for doing things like enabling strict mode only in the theme editor.
75
75
 
@@ -106,3 +106,9 @@ template = Liquid::Template.parse("{{x}} {{y}}")
106
106
  template.render!({ 'x' => 1}, { strict_variables: true })
107
107
  #=> Liquid::UndefinedVariable: Liquid error: undefined variable y
108
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.
data/lib/liquid/block.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Liquid
2
4
  class Block < Tag
3
5
  MAX_DEPTH = 100
@@ -8,11 +10,13 @@ module Liquid
8
10
  end
9
11
 
10
12
  def parse(tokens)
11
- @body = BlockBody.new
13
+ @body = new_body
12
14
  while parse_body(@body, tokens)
13
15
  end
16
+ @body.freeze
14
17
  end
15
18
 
19
+ # For backwards compatibility
16
20
  def render(context)
17
21
  @body.render(context)
18
22
  end
@@ -25,20 +29,29 @@ module Liquid
25
29
  @body.nodelist
26
30
  end
27
31
 
28
- def unknown_tag(tag, _params, _tokens)
29
- if tag == 'else'.freeze
30
- raise SyntaxError.new(parse_context.locale.t("errors.syntax.unexpected_else".freeze,
31
- block_name: block_name))
32
- elsif tag.start_with?('end'.freeze)
33
- 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",
34
43
  tag: tag,
35
44
  block_name: block_name,
36
- block_delimiter: block_delimiter))
45
+ block_delimiter: block_delimiter)
37
46
  else
38
- 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)
39
48
  end
40
49
  end
41
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
+
42
55
  def block_name
43
56
  @tag_name
44
57
  end
@@ -47,11 +60,17 @@ module Liquid
47
60
  @block_delimiter ||= "end#{block_name}"
48
61
  end
49
62
 
50
- protected
63
+ private
64
+
65
+ # @api public
66
+ def new_body
67
+ parse_context.new_block_body
68
+ end
51
69
 
70
+ # @api public
52
71
  def parse_body(body, tokens)
53
72
  if parse_context.depth >= MAX_DEPTH
54
- raise StackLevelError, "Nesting too deep".freeze
73
+ raise StackLevelError, "Nesting too deep"
55
74
  end
56
75
  parse_context.depth += 1
57
76
  begin
@@ -59,9 +78,7 @@ module Liquid
59
78
  @blank &&= body.blank?
60
79
 
61
80
  return false if end_tag_name == block_delimiter
62
- unless end_tag_name
63
- raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_never_closed".freeze, block_name: block_name))
64
- end
81
+ raise_tag_never_closed(block_name) unless end_tag_name
65
82
 
66
83
  # this tag is not registered with the system
67
84
  # pass it to the current block for special handling or error reporting
@@ -1,32 +1,138 @@
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
7
+ LiquidTagToken = /\A\s*(#{TagName})\s*(.*?)\z/o
8
+ FullToken = /\A#{TagStart}#{WhitespaceControl}?(\s*)(#{TagName})(\s*)(.*?)#{WhitespaceControl}?#{TagEnd}\z/om
9
+ ContentOfVariable = /\A#{VariableStart}#{WhitespaceControl}?(.*?)#{WhitespaceControl}?#{VariableEnd}\z/om
5
10
  WhitespaceOrNothing = /\A\s*\z/
6
- TAGSTART = "{%".freeze
7
- VARSTART = "{{".freeze
11
+ TAGSTART = "{%"
12
+ VARSTART = "{{"
8
13
 
9
14
  attr_reader :nodelist
10
15
 
11
16
  def initialize
12
17
  @nodelist = []
13
- @blank = true
18
+ @blank = true
14
19
  end
15
20
 
16
- def parse(tokenizer, parse_context)
21
+ def parse(tokenizer, parse_context, &block)
22
+ raise FrozenError, "can't modify frozen Liquid::BlockBody" if frozen?
23
+
17
24
  parse_context.line_number = tokenizer.line_number
18
- while token = tokenizer.shift
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.match?(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
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 = parse_context.new_tokenizer(
103
+ markup, start_line_number: parse_context.line_number, for_liquid_tag: true
104
+ )
105
+ parse_for_liquid_tag(liquid_tag_tokenizer, parse_context) do |end_tag_name, _end_tag_markup|
106
+ if end_tag_name
107
+ BlockBody.unknown_tag_in_liquid_tag(end_tag_name, parse_context)
108
+ end
109
+ end
110
+ end
111
+
112
+ private def parse_for_document(tokenizer, parse_context)
113
+ while (token = tokenizer.shift)
19
114
  next if token.empty?
20
115
  case
21
116
  when token.start_with?(TAGSTART)
22
117
  whitespace_handler(token, parse_context)
23
118
  unless token =~ FullToken
24
- raise_missing_tag_terminator(token, parse_context)
119
+ BlockBody.raise_missing_tag_terminator(token, parse_context)
120
+ end
121
+ tag_name = Regexp.last_match(2)
122
+ markup = Regexp.last_match(4)
123
+
124
+ if parse_context.line_number
125
+ # newlines inside the tag should increase the line number,
126
+ # particularly important for multiline {% liquid %} tags
127
+ parse_context.line_number += Regexp.last_match(1).count("\n") + Regexp.last_match(3).count("\n")
128
+ end
129
+
130
+ if tag_name == 'liquid'
131
+ parse_liquid_tag(markup, parse_context)
132
+ next
25
133
  end
26
- tag_name = $1
27
- markup = $2
28
- # fetch the tag from registered blocks
29
- unless tag = registered_tags[tag_name]
134
+
135
+ unless (tag = registered_tags[tag_name])
30
136
  # end parsing if we reach an unknown tag and let the caller decide
31
137
  # determine how to proceed
32
138
  return yield tag_name, markup
@@ -44,7 +150,7 @@ module Liquid
44
150
  end
45
151
  parse_context.trim_whitespace = false
46
152
  @nodelist << token
47
- @blank &&= !!(token =~ WhitespaceOrNothing)
153
+ @blank &&= token.match?(WhitespaceOrNothing)
48
154
  end
49
155
  parse_context.line_number = tokenizer.line_number
50
156
  end
@@ -55,8 +161,12 @@ module Liquid
55
161
  def whitespace_handler(token, parse_context)
56
162
  if token[2] == WhitespaceControl
57
163
  previous_token = @nodelist.last
58
- if previous_token.is_a? String
164
+ if previous_token.is_a?(String)
165
+ first_byte = previous_token.getbyte(0)
59
166
  previous_token.rstrip!
167
+ if previous_token.empty? && parse_context[:bug_compatible_whitespace_trimming] && first_byte
168
+ previous_token << first_byte
169
+ end
60
170
  end
61
171
  end
62
172
  parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
@@ -66,73 +176,76 @@ module Liquid
66
176
  @blank
67
177
  end
68
178
 
179
+ # Remove blank strings in the block body for a control flow tag (e.g. `if`, `for`, `case`, `unless`)
180
+ # with a blank body.
181
+ #
182
+ # For example, in a conditional assignment like the following
183
+ #
184
+ # ```
185
+ # {% if size > max_size %}
186
+ # {% assign size = max_size %}
187
+ # {% endif %}
188
+ # ```
189
+ #
190
+ # we assume the intention wasn't to output the blank spaces in the `if` tag's block body, so this method
191
+ # will remove them to reduce the render output size.
192
+ #
193
+ # Note that it is now preferred to use the `liquid` tag for this use case.
194
+ def remove_blank_strings
195
+ raise "remove_blank_strings only support being called on a blank block body" unless @blank
196
+ @nodelist.reject! { |node| node.instance_of?(String) }
197
+ end
198
+
69
199
  def render(context)
70
- output = []
71
- context.resource_limits.render_score += @nodelist.length
200
+ render_to_output_buffer(context, +'')
201
+ end
202
+
203
+ def render_to_output_buffer(context, output)
204
+ freeze unless frozen?
205
+
206
+ context.resource_limits.increment_render_score(@nodelist.length)
72
207
 
73
208
  idx = 0
74
- while node = @nodelist[idx]
75
- case node
76
- when String
77
- check_resources(context, node)
209
+ while (node = @nodelist[idx])
210
+ if node.instance_of?(String)
78
211
  output << node
79
- when Variable
80
- render_node_to_output(node, output, context)
81
- when Block
82
- render_node_to_output(node, output, context, node.blank?)
83
- break if context.interrupt? # might have happened in a for-block
84
- when Continue, Break
212
+ else
213
+ render_node(context, output, node)
85
214
  # If we get an Interrupt that means the block must stop processing. An
86
215
  # Interrupt is any command that stops block execution such as {% break %}
87
- # or {% continue %}
88
- context.push_interrupt(node.interrupt)
89
- break
90
- else # Other non-Block tags
91
- render_node_to_output(node, output, context)
216
+ # or {% continue %}. These tags may also occur through Block or Include tags.
217
+ break if context.interrupt? # might have happened in a for-block
92
218
  end
93
219
  idx += 1
220
+
221
+ context.resource_limits.increment_write_score(output)
94
222
  end
95
223
 
96
- output.join
224
+ output
97
225
  end
98
226
 
99
227
  private
100
228
 
101
- def render_node_to_output(node, output, context, skip_output = false)
102
- node_output = node.render(context)
103
- node_output = node_output.is_a?(Array) ? node_output.join : node_output.to_s
104
- check_resources(context, node_output)
105
- output << node_output unless skip_output
106
- rescue MemoryError => e
107
- raise e
108
- rescue UndefinedVariable, UndefinedDropMethod, UndefinedFilter => e
109
- context.handle_error(e, node.line_number)
110
- output << nil
111
- rescue ::StandardError => e
112
- line_number = node.is_a?(String) ? nil : node.line_number
113
- output << context.handle_error(e, line_number)
114
- end
115
-
116
- def check_resources(context, node_output)
117
- context.resource_limits.render_length += node_output.length
118
- return unless context.resource_limits.reached?
119
- raise MemoryError.new("Memory limits exceeded".freeze)
229
+ def render_node(context, output, node)
230
+ BlockBody.render_node(context, output, node)
120
231
  end
121
232
 
122
233
  def create_variable(token, parse_context)
123
- token.scan(ContentOfVariable) do |content|
124
- markup = content.first
234
+ if token =~ ContentOfVariable
235
+ markup = Regexp.last_match(1)
125
236
  return Variable.new(markup, parse_context)
126
237
  end
127
- raise_missing_variable_terminator(token, parse_context)
238
+ BlockBody.raise_missing_variable_terminator(token, parse_context)
128
239
  end
129
240
 
241
+ # @deprecated Use {.raise_missing_tag_terminator} instead
130
242
  def raise_missing_tag_terminator(token, parse_context)
131
- raise SyntaxError.new(parse_context.locale.t("errors.syntax.tag_termination".freeze, token: token, tag_end: TagEnd.inspect))
243
+ BlockBody.raise_missing_tag_terminator(token, parse_context)
132
244
  end
133
245
 
246
+ # @deprecated Use {.raise_missing_variable_terminator} instead
134
247
  def raise_missing_variable_terminator(token, parse_context)
135
- raise SyntaxError.new(parse_context.locale.t("errors.syntax.variable_termination".freeze, token: token, tag_end: VariableEnd.inspect))
248
+ BlockBody.raise_missing_variable_terminator(token, parse_context)
136
249
  end
137
250
 
138
251
  def registered_tags