herb 0.8.9-arm-linux-gnu → 0.9.0-arm-linux-gnu

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. checksums.yaml +4 -4
  2. data/Makefile +33 -11
  3. data/README.md +64 -34
  4. data/Rakefile +48 -40
  5. data/config.yml +323 -33
  6. data/ext/herb/error_helpers.c +384 -132
  7. data/ext/herb/error_helpers.h +1 -0
  8. data/ext/herb/extconf.rb +67 -28
  9. data/ext/herb/extension.c +317 -51
  10. data/ext/herb/extension.h +1 -0
  11. data/ext/herb/extension_helpers.c +23 -14
  12. data/ext/herb/extension_helpers.h +2 -2
  13. data/ext/herb/nodes.c +537 -270
  14. data/ext/herb/nodes.h +1 -0
  15. data/herb.gemspec +3 -2
  16. data/lib/herb/3.0/herb.so +0 -0
  17. data/lib/herb/3.1/herb.so +0 -0
  18. data/lib/herb/3.2/herb.so +0 -0
  19. data/lib/herb/3.3/herb.so +0 -0
  20. data/lib/herb/3.4/herb.so +0 -0
  21. data/lib/herb/4.0/herb.so +0 -0
  22. data/lib/herb/ast/helpers.rb +3 -3
  23. data/lib/herb/ast/node.rb +15 -2
  24. data/lib/herb/ast/nodes.rb +1132 -157
  25. data/lib/herb/bootstrap.rb +87 -0
  26. data/lib/herb/cli.rb +341 -31
  27. data/lib/herb/configuration.rb +248 -0
  28. data/lib/herb/defaults.yml +32 -0
  29. data/lib/herb/engine/compiler.rb +83 -14
  30. data/lib/herb/engine/debug_visitor.rb +51 -6
  31. data/lib/herb/engine/error_formatter.rb +13 -9
  32. data/lib/herb/engine/parser_error_overlay.rb +10 -6
  33. data/lib/herb/engine/validator.rb +8 -3
  34. data/lib/herb/engine/validators/nesting_validator.rb +2 -2
  35. data/lib/herb/engine.rb +92 -33
  36. data/lib/herb/errors.rb +582 -87
  37. data/lib/herb/lex_result.rb +1 -0
  38. data/lib/herb/location.rb +7 -3
  39. data/lib/herb/parse_result.rb +12 -2
  40. data/lib/herb/parser_options.rb +57 -0
  41. data/lib/herb/position.rb +1 -0
  42. data/lib/herb/prism_inspect.rb +116 -0
  43. data/lib/herb/project.rb +923 -331
  44. data/lib/herb/range.rb +1 -0
  45. data/lib/herb/token.rb +7 -1
  46. data/lib/herb/version.rb +1 -1
  47. data/lib/herb/visitor.rb +37 -2
  48. data/lib/herb/warnings.rb +6 -1
  49. data/lib/herb.rb +35 -3
  50. data/sig/herb/ast/helpers.rbs +2 -2
  51. data/sig/herb/ast/node.rbs +12 -2
  52. data/sig/herb/ast/nodes.rbs +641 -128
  53. data/sig/herb/bootstrap.rbs +31 -0
  54. data/sig/herb/configuration.rbs +89 -0
  55. data/sig/herb/engine/compiler.rbs +9 -1
  56. data/sig/herb/engine/debug_visitor.rbs +8 -0
  57. data/sig/herb/engine/validator.rbs +5 -1
  58. data/sig/herb/engine.rbs +18 -2
  59. data/sig/herb/errors.rbs +268 -63
  60. data/sig/herb/location.rbs +4 -0
  61. data/sig/herb/parse_result.rbs +4 -2
  62. data/sig/herb/parser_options.rbs +42 -0
  63. data/sig/herb/position.rbs +1 -0
  64. data/sig/herb/prism_inspect.rbs +28 -0
  65. data/sig/herb/range.rbs +1 -0
  66. data/sig/herb/token.rbs +6 -0
  67. data/sig/herb/visitor.rbs +25 -4
  68. data/sig/herb/warnings.rbs +6 -1
  69. data/sig/herb.rbs +14 -0
  70. data/sig/herb_c_extension.rbs +5 -2
  71. data/sig/serialized_ast_errors.rbs +57 -6
  72. data/sig/serialized_ast_nodes.rbs +60 -6
  73. data/src/analyze/action_view/attribute_extraction_helpers.c +290 -0
  74. data/src/analyze/action_view/content_tag.c +70 -0
  75. data/src/analyze/action_view/link_to.c +143 -0
  76. data/src/analyze/action_view/registry.c +60 -0
  77. data/src/analyze/action_view/tag.c +64 -0
  78. data/src/analyze/action_view/tag_helper_node_builders.c +305 -0
  79. data/src/analyze/action_view/tag_helpers.c +748 -0
  80. data/src/analyze/action_view/turbo_frame_tag.c +88 -0
  81. data/src/analyze/analyze.c +882 -0
  82. data/src/{analyzed_ruby.c → analyze/analyzed_ruby.c} +13 -11
  83. data/src/analyze/builders.c +343 -0
  84. data/src/analyze/conditional_elements.c +594 -0
  85. data/src/analyze/conditional_open_tags.c +640 -0
  86. data/src/analyze/control_type.c +250 -0
  87. data/src/{analyze_helpers.c → analyze/helpers.c} +79 -31
  88. data/src/analyze/invalid_structures.c +193 -0
  89. data/src/{analyze_missing_end.c → analyze/missing_end.c} +33 -22
  90. data/src/analyze/parse_errors.c +84 -0
  91. data/src/analyze/prism_annotate.c +397 -0
  92. data/src/{analyze_transform.c → analyze/transform.c} +17 -3
  93. data/src/ast_node.c +17 -7
  94. data/src/ast_nodes.c +662 -387
  95. data/src/ast_pretty_print.c +190 -6
  96. data/src/errors.c +1099 -506
  97. data/src/extract.c +148 -49
  98. data/src/herb.c +52 -34
  99. data/src/html_util.c +241 -12
  100. data/src/include/analyze/action_view/attribute_extraction_helpers.h +36 -0
  101. data/src/include/analyze/action_view/tag_helper_handler.h +41 -0
  102. data/src/include/analyze/action_view/tag_helper_node_builders.h +70 -0
  103. data/src/include/analyze/action_view/tag_helpers.h +38 -0
  104. data/src/include/{analyze.h → analyze/analyze.h} +14 -4
  105. data/src/include/{analyzed_ruby.h → analyze/analyzed_ruby.h} +3 -3
  106. data/src/include/analyze/builders.h +27 -0
  107. data/src/include/analyze/conditional_elements.h +9 -0
  108. data/src/include/analyze/conditional_open_tags.h +9 -0
  109. data/src/include/analyze/control_type.h +14 -0
  110. data/src/include/{analyze_helpers.h → analyze/helpers.h} +22 -17
  111. data/src/include/analyze/invalid_structures.h +11 -0
  112. data/src/include/analyze/prism_annotate.h +16 -0
  113. data/src/include/ast_node.h +11 -5
  114. data/src/include/ast_nodes.h +117 -38
  115. data/src/include/ast_pretty_print.h +5 -0
  116. data/src/include/element_source.h +3 -8
  117. data/src/include/errors.h +154 -53
  118. data/src/include/extract.h +21 -5
  119. data/src/include/herb.h +18 -6
  120. data/src/include/herb_prism_node.h +13 -0
  121. data/src/include/html_util.h +7 -2
  122. data/src/include/io.h +3 -1
  123. data/src/include/lex_helpers.h +29 -0
  124. data/src/include/lexer.h +1 -1
  125. data/src/include/lexer_peek_helpers.h +87 -13
  126. data/src/include/lexer_struct.h +2 -0
  127. data/src/include/location.h +2 -1
  128. data/src/include/parser.h +27 -2
  129. data/src/include/parser_helpers.h +19 -3
  130. data/src/include/pretty_print.h +10 -5
  131. data/src/include/prism_context.h +45 -0
  132. data/src/include/prism_helpers.h +10 -7
  133. data/src/include/prism_serialized.h +12 -0
  134. data/src/include/token.h +16 -4
  135. data/src/include/token_struct.h +10 -3
  136. data/src/include/utf8.h +2 -1
  137. data/src/include/util/hb_allocator.h +78 -0
  138. data/src/include/util/hb_arena.h +6 -1
  139. data/src/include/util/hb_arena_debug.h +12 -1
  140. data/src/include/util/hb_array.h +7 -3
  141. data/src/include/util/hb_buffer.h +6 -4
  142. data/src/include/util/hb_foreach.h +79 -0
  143. data/src/include/util/hb_narray.h +8 -4
  144. data/src/include/util/hb_string.h +56 -9
  145. data/src/include/util/string.h +11 -0
  146. data/src/include/util.h +6 -3
  147. data/src/include/version.h +1 -1
  148. data/src/io.c +3 -2
  149. data/src/lexer.c +42 -30
  150. data/src/lexer_peek_helpers.c +12 -74
  151. data/src/location.c +2 -2
  152. data/src/main.c +79 -66
  153. data/src/parser.c +784 -247
  154. data/src/parser_helpers.c +110 -23
  155. data/src/parser_match_tags.c +109 -48
  156. data/src/pretty_print.c +29 -24
  157. data/src/prism_helpers.c +30 -27
  158. data/src/ruby_parser.c +2 -0
  159. data/src/token.c +151 -66
  160. data/src/token_matchers.c +0 -1
  161. data/src/utf8.c +7 -6
  162. data/src/util/hb_allocator.c +341 -0
  163. data/src/util/hb_arena.c +81 -56
  164. data/src/util/hb_arena_debug.c +32 -17
  165. data/src/util/hb_array.c +30 -15
  166. data/src/util/hb_buffer.c +17 -21
  167. data/src/util/hb_narray.c +22 -7
  168. data/src/util/hb_string.c +49 -35
  169. data/src/util.c +21 -11
  170. data/src/visitor.c +47 -0
  171. data/templates/ext/herb/error_helpers.c.erb +24 -11
  172. data/templates/ext/herb/error_helpers.h.erb +1 -0
  173. data/templates/ext/herb/nodes.c.erb +50 -16
  174. data/templates/ext/herb/nodes.h.erb +1 -0
  175. data/templates/java/error_helpers.c.erb +1 -1
  176. data/templates/java/nodes.c.erb +30 -8
  177. data/templates/java/org/herb/ast/Errors.java.erb +24 -1
  178. data/templates/java/org/herb/ast/Nodes.java.erb +80 -21
  179. data/templates/javascript/packages/core/src/errors.ts.erb +16 -3
  180. data/templates/javascript/packages/core/src/node-type-guards.ts.erb +3 -1
  181. data/templates/javascript/packages/core/src/nodes.ts.erb +109 -32
  182. data/templates/javascript/packages/node/extension/error_helpers.cpp.erb +13 -4
  183. data/templates/javascript/packages/node/extension/nodes.cpp.erb +43 -4
  184. data/templates/lib/herb/ast/nodes.rb.erb +88 -31
  185. data/templates/lib/herb/errors.rb.erb +15 -3
  186. data/templates/lib/herb/visitor.rb.erb +2 -2
  187. data/templates/rust/src/ast/nodes.rs.erb +97 -44
  188. data/templates/rust/src/errors.rs.erb +2 -1
  189. data/templates/rust/src/nodes.rs.erb +167 -15
  190. data/templates/rust/src/union_types.rs.erb +60 -0
  191. data/templates/rust/src/visitor.rs.erb +81 -0
  192. data/templates/src/{analyze_missing_end.c.erb → analyze/missing_end.c.erb} +9 -6
  193. data/templates/src/{analyze_transform.c.erb → analyze/transform.c.erb} +2 -2
  194. data/templates/src/ast_nodes.c.erb +34 -26
  195. data/templates/src/ast_pretty_print.c.erb +24 -5
  196. data/templates/src/errors.c.erb +60 -54
  197. data/templates/src/include/ast_nodes.h.erb +6 -2
  198. data/templates/src/include/ast_pretty_print.h.erb +5 -0
  199. data/templates/src/include/errors.h.erb +15 -11
  200. data/templates/src/include/util/hb_foreach.h.erb +20 -0
  201. data/templates/src/parser_match_tags.c.erb +10 -4
  202. data/templates/src/visitor.c.erb +2 -2
  203. data/templates/template.rb +204 -29
  204. data/templates/wasm/error_helpers.cpp.erb +9 -5
  205. data/templates/wasm/nodes.cpp.erb +41 -4
  206. data/vendor/prism/config.yml +4 -4
  207. data/vendor/prism/include/prism/ast.h +4 -4
  208. data/vendor/prism/include/prism/version.h +2 -2
  209. data/vendor/prism/src/prism.c +1 -1
  210. data/vendor/prism/templates/java/org/prism/Loader.java.erb +1 -1
  211. data/vendor/prism/templates/javascript/src/deserialize.js.erb +1 -1
  212. data/vendor/prism/templates/lib/prism/node.rb.erb +23 -15
  213. data/vendor/prism/templates/lib/prism/serialize.rb.erb +1 -1
  214. data/vendor/prism/templates/rbi/prism/node.rbi.erb +3 -0
  215. data/vendor/prism/templates/sig/prism/node.rbs.erb +3 -0
  216. data/vendor/prism/templates/sig/prism.rbs.erb +9 -10
  217. metadata +58 -16
  218. data/src/analyze.c +0 -1594
  219. data/src/element_source.c +0 -12
  220. data/src/include/util/hb_system.h +0 -9
  221. data/src/util/hb_system.c +0 -30
@@ -3,14 +3,19 @@
3
3
  module Herb
4
4
  class Engine
5
5
  class Validator < Herb::Visitor
6
- attr_reader :diagnostics
6
+ attr_reader :diagnostics, :enabled
7
7
 
8
- def initialize
9
- super
8
+ def initialize(enabled: true)
9
+ super()
10
10
 
11
+ @enabled = enabled
11
12
  @diagnostics = []
12
13
  end
13
14
 
15
+ def enabled?
16
+ @enabled
17
+ end
18
+
14
19
  def validate(node)
15
20
  visit(node)
16
21
  end
@@ -31,7 +31,7 @@ module Herb
31
31
  end
32
32
 
33
33
  def validate_no_block_elements_in_paragraph(node)
34
- block_elements = %w[div section article header footer nav aside p h1 h2 h3 h4 h5 h6 ul ol dl table form]
34
+ block_elements = ["div", "section", "article", "header", "footer", "nav", "aside", "p", "h1", "h2", "h3", "h4", "h5", "h6", "ul", "ol", "dl", "table", "form"]
35
35
 
36
36
  node.body.each do |child|
37
37
  next unless child.is_a?(Herb::AST::HTMLElementNode)
@@ -58,7 +58,7 @@ module Herb
58
58
  end
59
59
 
60
60
  def validate_no_interactive_in_button(node)
61
- interactive_elements = %w[a button input select textarea]
61
+ interactive_elements = ["a", "button", "input", "select", "textarea"]
62
62
 
63
63
  node.body.each do |child|
64
64
  next unless child.is_a?(Herb::AST::HTMLElementNode)
data/lib/herb/engine.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ # typed: false
2
3
 
3
4
  require "json"
4
5
  require "time"
@@ -17,7 +18,7 @@ require_relative "engine/validators/accessibility_validator"
17
18
  module Herb
18
19
  class Engine
19
20
  attr_reader :src, :filename, :project_path, :relative_file_path, :bufvar, :debug, :content_for_head,
20
- :validation_error_template, :visitors
21
+ :validation_error_template, :visitors, :enabled_validators
21
22
 
22
23
  ESCAPE_TABLE = {
23
24
  "&" => "&amp;",
@@ -30,6 +31,16 @@ module Herb
30
31
  class CompilationError < StandardError
31
32
  end
32
33
 
34
+ class InvalidRubyError < CompilationError
35
+ attr_reader :compiled_source
36
+
37
+ def initialize(message, compiled_source: nil)
38
+ @compiled_source = compiled_source
39
+
40
+ super(message)
41
+ end
42
+ end
43
+
33
44
  def initialize(input, properties = {})
34
45
  @filename = properties[:filename] ? ::Pathname.new(properties[:filename]) : nil
35
46
  @project_path = ::Pathname.new(properties[:project_path] || Dir.pwd)
@@ -51,6 +62,8 @@ module Herb
51
62
  @content_for_head = properties[:content_for_head]
52
63
  @validation_error_template = nil
53
64
  @validation_mode = properties.fetch(:validation_mode, :raise)
65
+ @enabled_validators = Herb.configuration.enabled_validators(properties[:validators] || {})
66
+ @strict = properties.fetch(:strict, true)
54
67
  @visitors = properties.fetch(:visitors, default_visitors)
55
68
 
56
69
  if @debug && @visitors.empty?
@@ -75,6 +88,8 @@ module Herb
75
88
  preamble = properties[:preamble] || "#{@bufvar} = #{bufval};"
76
89
  postamble = properties[:postamble] || "#{@bufvar}.to_s\n"
77
90
 
91
+ preamble = "#{preamble}; " unless preamble.empty? || preamble.end_with?(";", " ", "\n")
92
+
78
93
  @src << "# frozen_string_literal: true\n" if @freeze
79
94
 
80
95
  if properties[:ensure]
@@ -89,7 +104,7 @@ module Herb
89
104
  @src << "__herb = ::Herb::Engine; " if @escape && @escapefunc == "__herb.h"
90
105
  @src << preamble
91
106
 
92
- parse_result = ::Herb.parse(input, track_whitespace: true)
107
+ parse_result = ::Herb.parse(input, track_whitespace: true, strict: @strict)
93
108
  ast = parse_result.value
94
109
  parser_errors = parse_result.errors
95
110
 
@@ -104,12 +119,12 @@ module Herb
104
119
  # Skip both errors and compilation, but still need minimal Ruby code
105
120
  end
106
121
  else
107
- validation_errors = run_validation(ast) unless @validation_mode == :none
108
- all_errors = parser_errors + (validation_errors || [])
109
-
110
- handle_validation_errors(all_errors, input) if @validation_mode == :raise && all_errors.any?
122
+ validators = run_validation(ast) unless @validation_mode == :none
111
123
 
112
- add_validation_overlay(validation_errors, input) if @validation_mode == :overlay && validation_errors&.any?
124
+ if validators
125
+ handle_validation_errors(validators, input) if @validation_mode == :raise
126
+ add_validation_overlay(validators, input) if @validation_mode == :overlay
127
+ end
113
128
 
114
129
  @visitors.each do |visitor|
115
130
  ast.accept(visitor)
@@ -132,6 +147,18 @@ module Herb
132
147
 
133
148
  @src << "; ensure\n #{@bufvar} = __original_outvar\nend\n" if properties[:ensure]
134
149
 
150
+ if properties.fetch(:validate_ruby, false)
151
+ require "prism"
152
+
153
+ prism_result = Prism.parse(@src)
154
+ syntax_errors = prism_result.errors.reject { |e| e.type == :invalid_yield }
155
+
156
+ if syntax_errors.any?
157
+ details = syntax_errors.map { |e| " - #{e.message} (line #{e.location.start_line})" }.join("\n")
158
+ raise InvalidRubyError.new("Compiled template produced invalid Ruby:\n#{details}", compiled_source: @src)
159
+ end
160
+ end
161
+
135
162
  @src.freeze
136
163
  freeze
137
164
  end
@@ -172,6 +199,14 @@ module Herb
172
199
  end
173
200
  end
174
201
 
202
+ def self.comment?(code)
203
+ code.include?("#")
204
+ end
205
+
206
+ def self.heredoc?(code)
207
+ code.match?(/<<[~-]?\s*['"`]?\w/)
208
+ end
209
+
175
210
  protected
176
211
 
177
212
  def add_text(text)
@@ -193,8 +228,8 @@ module Herb
193
228
  @src << " " << code
194
229
 
195
230
  # TODO: rework and check for Prism::InlineComment as soon as we expose the Prism Nodes in the Herb AST
196
- if code.include?("#")
197
- @src << "\n"
231
+ if self.class.comment?(code) || self.class.heredoc?(code)
232
+ @src << "\n" unless code[-1] == "\n"
198
233
  else
199
234
  @src << ";" unless code[-1] == "\n"
200
235
  end
@@ -212,11 +247,15 @@ module Herb
212
247
  end
213
248
 
214
249
  def add_expression_result(code)
215
- with_buffer { @src << " << (" << code << ").to_s" }
250
+ with_buffer {
251
+ @src << " << (" << code << trailing_newline(code) << ").to_s"
252
+ }
216
253
  end
217
254
 
218
255
  def add_expression_result_escaped(code)
219
- with_buffer { @src << " << " << @escapefunc << "((" << code << "))" }
256
+ with_buffer {
257
+ @src << " << " << @escapefunc << "((" << code << trailing_newline(code) << "))"
258
+ }
220
259
  end
221
260
 
222
261
  def add_expression_block(indicator, code)
@@ -228,11 +267,38 @@ module Herb
228
267
  end
229
268
 
230
269
  def add_expression_block_result(code)
231
- with_buffer { @src << " << " << code }
270
+ with_buffer {
271
+ @src << " << (" << code << trailing_newline(code)
272
+ }
232
273
  end
233
274
 
234
275
  def add_expression_block_result_escaped(code)
235
- with_buffer { @src << " << " << @escapefunc << "(" << code << ")" }
276
+ with_buffer {
277
+ @src << " << " << @escapefunc << "((" << code << trailing_newline(code)
278
+ }
279
+ end
280
+
281
+ def add_expression_block_end(code, escaped: false)
282
+ terminate_expression
283
+
284
+ trailing_newline = code.end_with?("\n")
285
+ code_stripped = code.chomp
286
+
287
+ @src.chomp! if @src.end_with?("\n") && code_stripped.start_with?(" ")
288
+
289
+ @src << " " << code_stripped
290
+ @src << "\n" if self.class.comment?(code_stripped)
291
+ @src << (escaped ? "))" : ")")
292
+ @src << (trailing_newline ? "\n" : ";")
293
+
294
+ @buffer_on_stack = false
295
+ end
296
+
297
+ def trailing_newline(code)
298
+ return "\n" if self.class.comment?(code)
299
+ return "\n" if self.class.heredoc?(code)
300
+
301
+ ""
236
302
  end
237
303
 
238
304
  def add_postamble(postamble)
@@ -260,19 +326,16 @@ module Herb
260
326
 
261
327
  def run_validation(ast)
262
328
  validators = [
263
- Validators::SecurityValidator.new,
264
- Validators::NestingValidator.new,
265
- Validators::AccessibilityValidator.new
329
+ Validators::SecurityValidator.new(enabled: @enabled_validators[:security]),
330
+ Validators::NestingValidator.new(enabled: @enabled_validators[:nesting]),
331
+ Validators::AccessibilityValidator.new(enabled: @enabled_validators[:accessibility])
266
332
  ]
267
333
 
268
- errors = [] #: Array[untyped]
269
-
270
- validators.each do |validator|
334
+ validators.select(&:enabled?).each do |validator|
271
335
  ast.accept(validator)
272
- errors.concat(validator.errors)
273
336
  end
274
337
 
275
- errors
338
+ validators
276
339
  end
277
340
 
278
341
  def handle_parser_errors(parser_errors, input, _ast)
@@ -292,24 +355,19 @@ module Herb
292
355
  end
293
356
  end
294
357
 
295
- def handle_validation_errors(errors, input)
358
+ def handle_validation_errors(validators, input)
359
+ errors = validators.select(&:enabled?).flat_map(&:errors)
296
360
  return unless errors.any?
297
361
 
298
- security_error = errors.find { |error|
299
- error.is_a?(Hash) && error[:source] == "SecurityValidator"
300
- }
362
+ security_error = errors.find { |error| error[:source] == "SecurityValidator" }
301
363
 
302
364
  if security_error
303
- line = security_error[:location]&.start&.line
304
- column = security_error[:location]&.start&.column
305
- suggestion = security_error[:suggestion]
306
-
307
365
  raise SecurityError.new(
308
366
  security_error[:message],
309
- line: line,
310
- column: column,
367
+ line: security_error[:location]&.start&.line,
368
+ column: security_error[:location]&.start&.column,
311
369
  filename: @filename,
312
- suggestion: suggestion
370
+ suggestion: security_error[:suggestion]
313
371
  )
314
372
  end
315
373
 
@@ -318,7 +376,8 @@ module Herb
318
376
  raise CompilationError, "\n#{message}"
319
377
  end
320
378
 
321
- def add_validation_overlay(errors, input = nil)
379
+ def add_validation_overlay(validators, input = nil)
380
+ errors = validators.select(&:enabled?).flat_map(&:errors)
322
381
  return unless errors.any?
323
382
 
324
383
  templates = errors.map { |error|