liquid 4.0.0 → 5.0.1

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 +101 -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 +192 -76
  7. data/lib/liquid/condition.rb +69 -29
  8. data/lib/liquid/context.rb +110 -53
  9. data/lib/liquid/document.rb +47 -9
  10. data/lib/liquid/drop.rb +4 -2
  11. data/lib/liquid/errors.rb +20 -18
  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 +21 -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 +170 -63
  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 +34 -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 +53 -72
  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 +609 -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 +73 -61
  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 +513 -210
  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 +123 -120
  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 +254 -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 +26 -19
  119. data/test/unit/variable_unit_test.rb +51 -49
  120. metadata +79 -46
  121. data/lib/liquid/strainer.rb +0 -66
  122. data/test/unit/context_unit_test.rb +0 -483
  123. data/test/unit/strainer_unit_test.rb +0 -148
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 68c0e92a0c24e19463a4ef68801093b2be0211bb
4
- data.tar.gz: d80ceddfb328477c4fd8ed52faa893088bfd86ef
2
+ SHA256:
3
+ metadata.gz: 98bb40e18a2bb905a917743968edb79732f903b3beb1f51ab9551e1aaee3f91a
4
+ data.tar.gz: 264b8ccd158a79f2b43a0308a055d38b018b0f4132f316007f3589e778f5d9dd
5
5
  SHA512:
6
- metadata.gz: 2232e4b2053dbcbad922fa89b53c79366aad0ad2f649a9d8e0dec5d6100c572c88bc8ec3a941df8063138cb02ffb9f68269dac3581d780c695b2c96e940cb366
7
- data.tar.gz: 3695492ac392acc500f6ee10758dc5d15991289f0ca5fd16e0c770056cf0a4db928b2d7d29af1f7bb9c1eb4951c7e42854ab48409d373d6780ddd7ce2c27f357
6
+ metadata.gz: 19b30524fc6b7828de10e3eecc4e28b5595436144eacedad06c36d6472b740d01e971d315f7aa911ebad80dac5c075b75649c0b506f588e50ff8c977f08ed365
7
+ data.tar.gz: 53726f629d8a76ebcc4e134bc12abe981df9526fb5baa4d9312b68bc967e691af329bfcabadad86af65b8149034bd94479a2f98a93a9056a090fd28ba2901597
data/History.md CHANGED
@@ -1,6 +1,102 @@
1
1
  # Liquid Change Log
2
2
 
3
- ## 4.0.0 / not yet released / branch "master"
3
+ ## 5.0.1 / 2021-03-24
4
+
5
+ ### Fixes
6
+ * Add ParseTreeVisitor to Echo tag (#1414) [CP Clermont]
7
+ * Test with ruby 3.0 as the latest ruby version (#1398) [Dylan Thacker-Smith]
8
+ * Handle carriage return in newlines_to_br (#1391) [Unending]
9
+
10
+ ### Performance Improvements
11
+ * Use split limit in truncatewords (#1361) [Dylan Thacker-Smith]
12
+
13
+ ## 5.0.0 / 2021-01-06
14
+
15
+ ### Features
16
+ * Add new `{% render %}` tag (#1122) [Samuel Doiron]
17
+ * Add support for `as` in `{% render %}` and `{% include %}` (#1181) [Mike Angell]
18
+ * Add `{% liquid %}` and `{% echo %}` tags (#1086) [Justin Li]
19
+ * Add [usage tracking](README.md#usage-tracking) [Mike Angell]
20
+ * Add `Tag.disable_tags` for disabling tags that prepend `Tag::Disableable` at render time (#1162, #1274, #1275) [Mike Angell]
21
+ * Support using a profiler for multiple renders (#1365, #1366) [Dylan Thacker-Smith]
22
+
23
+ ### Fixes
24
+ * Fix catastrophic backtracking in `RANGES_REGEX` regular expression (#1357) [Dylan Thacker-Smith]
25
+ * Make sure the for tag's limit and offset are integers (#1094) [David Cornu]
26
+ * Invokable methods for enumerable reject include (#1151) [Thierry Joyal]
27
+ * Allow `default` filter to handle `false` as value (#1144) [Mike Angell]
28
+ * Fix render length resource limit so it doesn't multiply nested output (#1285) [Dylan Thacker-Smith]
29
+ * Fix duplication of text in raw tags (#1304) [Peter Zhu]
30
+ * Fix strict parsing of find variable with a name expression (#1317) [Dylan Thacker-Smith]
31
+ * Use monotonic time to measure durations in Liquid::Profiler (#1362) [Dylan Thacker-Smith]
32
+
33
+ ### Breaking Changes
34
+ * Require Ruby >= 2.5 (#1131, #1310) [Mike Angell, Dylan Thacker-Smith]
35
+ * Remove support for taint checking (#1268) [Dylan Thacker-Smith]
36
+ * Split Strainer class into StrainerFactory and StrainerTemplate (#1208) [Thierry Joyal]
37
+ * Remove handling of a nil context in the Strainer class (#1218) [Thierry Joyal]
38
+ * Handle `BlockBody#blank?` at parse time (#1287) [Dylan Thacker-Smith]
39
+ * Pass the tag markup and tokenizer to `Document#unknown_tag` (#1290) [Dylan Thacker-Smith]
40
+ * And several internal changes
41
+
42
+ ### Performance Improvements
43
+ * Reduce allocations (#1073, #1091, #1115, #1099, #1117, #1141, #1322, #1341) [Richard Monette, Florian Weingarten, Ashwin Maroli]
44
+ * Improve resources limits performance (#1093, #1323) [Florian Weingarten, Dylan Thacker-Smith]
45
+
46
+ ## 4.0.3 / 2019-03-12
47
+
48
+ ### Fixed
49
+ * Fix break and continue tags inside included templates in loops (#1072) [Justin Li]
50
+
51
+ ## 4.0.2 / 2019-03-08
52
+
53
+ ### Changed
54
+ * Add `where` filter (#1026) [Samuel Doiron]
55
+ * Add `ParseTreeVisitor` to iterate the Liquid AST (#1025) [Stephen Paul Weber]
56
+ * Improve `strip_html` performance (#1032) [printercu]
57
+
58
+ ### Fixed
59
+ * Add error checking for invalid combinations of inputs to sort, sort_natural, where, uniq, map, compact filters (#1059) [Garland Zhang]
60
+ * Validate the character encoding in url_decode (#1070) [Clayton Smith]
61
+
62
+ ## 4.0.1 / 2018-10-09
63
+
64
+ ### Changed
65
+ * Add benchmark group in Gemfile (#855) [Jerry Liu]
66
+ * Allow benchmarks to benchmark render by itself (#851) [Jerry Liu]
67
+ * Avoid calling `line_number` on String node when rescuing a render error. (#860) [Dylan Thacker-Smith]
68
+ * Avoid duck typing to detect whether to call render on a node. [Dylan Thacker-Smith]
69
+ * Clarify spelling of `reversed` on `for` block tag (#843) [Mark Crossfield]
70
+ * Replace recursion with loop to avoid potential stack overflow from malicious input (#891, #892) [Dylan Thacker-Smith]
71
+ * Limit block tag nesting to 100 (#894) [Dylan Thacker-Smith]
72
+ * Replace `assert_equal nil` with `assert_nil` (#895) [Dylan Thacker-Smith]
73
+ * Remove Spy Gem (#896) [Dylan Thacker-Smith]
74
+ * Add `collection_name` and `variable_name` reader to `For` block (#909)
75
+ * Symbols render as strings (#920) [Justin Li]
76
+ * Remove default value from Hash objects (#932) [Maxime Bedard]
77
+ * Remove one level of nesting (#944) [Dylan Thacker-Smith]
78
+ * Update Rubocop version (#952) [Justin Li]
79
+ * Add `at_least` and `at_most` filters (#954, #958) [Nithin Bekal]
80
+ * Add a regression test for a liquid-c trim mode bug (#972) [Dylan Thacker-Smith]
81
+ * Use https rather than git protocol to fetch liquid-c [Dylan Thacker-Smith]
82
+ * Add tests against Ruby 2.4 (#963) and 2.5 (#981)
83
+ * Replace RegExp literals with constants (#988) [Ashwin Maroli]
84
+ * Replace unnecessary `#each_with_index` with `#each` (#992) [Ashwin Maroli]
85
+ * Improve the unexpected end delimiter message for block tags. (#1003) [Dylan Thacker-Smith]
86
+ * Refactor and optimize rendering (#1005) [Christopher Aue]
87
+ * Add installation instruction (#1006) [Ben Gift]
88
+ * Remove Circle CI (#1010)
89
+ * Rename deprecated `BigDecimal.new` to `BigDecimal` (#1024) [Koichi ITO]
90
+ * Rename deprecated Rubocop name (#1027) [Justin Li]
91
+
92
+ ### Fixed
93
+ * Handle `join` filter on non String joiners (#857) [Richard Monette]
94
+ * Fix duplicate inclusion condition logic error of `Liquid::Strainer.add_filter` method (#861)
95
+ * Fix `escape`, `url_encode`, `url_decode` not handling non-string values (#898) [Thierry Joyal]
96
+ * Fix raise when variable is defined but nil when using `strict_variables` [Pascal Betz]
97
+ * Fix `sort` and `sort_natural` to handle arrays with nils (#930) [Eric Chan]
98
+
99
+ ## 4.0.0 / 2016-12-14 / branch "4-0-stable"
4
100
 
5
101
  ### Changed
6
102
  * Render an opaque internal error by default for non-Liquid::Error (#835) [Dylan Thacker-Smith]
@@ -20,10 +116,13 @@
20
116
  * Add concat filter to concatenate arrays (#429) [Diogo Beato]
21
117
  * Ruby 1.9 support dropped (#491) [Justin Li]
22
118
  * Liquid::Template.file_system's read_template_file method is no longer passed the context. (#441) [James Reid-Smith]
23
- * Remove support for `liquid_methods`
119
+ * Remove `liquid_methods` (See https://github.com/Shopify/liquid/pull/568 for replacement)
24
120
  * Liquid::Template.register_filter raises when the module overrides registered public methods as private or protected (#705) [Gaurav Chande]
25
121
 
26
122
  ### Fixed
123
+
124
+ * Fix variable names being detected as an operator when starting with contains (#788) [Michael Angell]
125
+ * Fix include tag used with strict_variables (#828) [QuickPay]
27
126
  * Fix map filter when value is a Proc (#672) [Guillaume Malette]
28
127
  * Fix truncate filter when value is not a string (#672) [Guillaume Malette]
29
128
  * 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.
data/lib/liquid.rb CHANGED
@@ -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
  #
data/lib/liquid/block.rb CHANGED
@@ -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,156 @@
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 = 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)
114
+ next if token.empty?
115
+ case
116
+ when token.start_with?(TAGSTART)
117
+ whitespace_handler(token, parse_context)
118
+ unless token =~ FullToken
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
133
+ end
134
+
135
+ unless (tag = registered_tags[tag_name])
136
+ # end parsing if we reach an unknown tag and let the caller decide
137
+ # determine how to proceed
138
+ return yield tag_name, markup
139
+ end
140
+ new_tag = tag.parse(tag_name, markup, tokenizer, parse_context)
141
+ @blank &&= new_tag.blank?
142
+ @nodelist << new_tag
143
+ when token.start_with?(VARSTART)
144
+ whitespace_handler(token, parse_context)
145
+ @nodelist << create_variable(token, parse_context)
146
+ @blank = false
147
+ else
148
+ if parse_context.trim_whitespace
149
+ token.lstrip!
150
+ end
151
+ parse_context.trim_whitespace = false
152
+ @nodelist << token
153
+ @blank &&= !!(token =~ WhitespaceOrNothing)
50
154
  end
51
155
  parse_context.line_number = tokenizer.line_number
52
156
  end
@@ -57,8 +161,12 @@ module Liquid
57
161
  def whitespace_handler(token, parse_context)
58
162
  if token[2] == WhitespaceControl
59
163
  previous_token = @nodelist.last
60
- if previous_token.is_a? String
164
+ if previous_token.is_a?(String)
165
+ first_byte = previous_token.getbyte(0)
61
166
  previous_token.rstrip!
167
+ if previous_token.empty? && parse_context[:bug_compatible_whitespace_trimming] && first_byte
168
+ previous_token << first_byte
169
+ end
62
170
  end
63
171
  end
64
172
  parse_context.trim_whitespace = (token[-3] == WhitespaceControl)
@@ -68,52 +176,58 @@ module Liquid
68
176
  @blank
69
177
  end
70
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
+
71
199
  def render(context)
72
- output = []
73
- 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?
74
205
 
75
- @nodelist.each do |token|
76
- # Break out if we have any unhanded interrupts.
77
- break if context.interrupt?
206
+ context.resource_limits.increment_render_score(@nodelist.length)
78
207
 
79
- begin
208
+ idx = 0
209
+ while (node = @nodelist[idx])
210
+ if node.instance_of?(String)
211
+ output << node
212
+ else
213
+ render_node(context, output, node)
80
214
  # If we get an Interrupt that means the block must stop processing. An
81
215
  # 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, token.raw)
97
- output << nil
98
- rescue ::StandardError => e
99
- output << context.handle_error(e, token.line_number, token.raw)
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
100
218
  end
219
+ idx += 1
220
+
221
+ context.resource_limits.increment_write_score(output)
101
222
  end
102
223
 
103
- output.join
224
+ output
104
225
  end
105
226
 
106
227
  private
107
228
 
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
229
+ def render_node(context, output, node)
230
+ BlockBody.render_node(context, output, node)
117
231
  end
118
232
 
119
233
  def create_variable(token, parse_context)
@@ -121,15 +235,17 @@ module Liquid
121
235
  markup = content.first
122
236
  return Variable.new(markup, parse_context)
123
237
  end
124
- raise_missing_variable_terminator(token, parse_context)
238
+ BlockBody.raise_missing_variable_terminator(token, parse_context)
125
239
  end
126
240
 
241
+ # @deprecated Use {.raise_missing_tag_terminator} instead
127
242
  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))
243
+ BlockBody.raise_missing_tag_terminator(token, parse_context)
129
244
  end
130
245
 
246
+ # @deprecated Use {.raise_missing_variable_terminator} instead
131
247
  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))
248
+ BlockBody.raise_missing_variable_terminator(token, parse_context)
133
249
  end
134
250
 
135
251
  def registered_tags