hanami 2.1.0.beta1 → 2.1.0.beta2

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +20 -4
  3. data/README.md +1 -1
  4. data/lib/hanami/app.rb +5 -0
  5. data/lib/hanami/config/actions.rb +4 -7
  6. data/lib/hanami/config/assets.rb +84 -0
  7. data/lib/hanami/config/null_config.rb +3 -0
  8. data/lib/hanami/config.rb +17 -5
  9. data/lib/hanami/extensions/action.rb +4 -2
  10. data/lib/hanami/extensions/view/standard_helpers.rb +4 -0
  11. data/lib/hanami/helpers/assets_helper.rb +752 -0
  12. data/lib/hanami/middleware/assets.rb +21 -0
  13. data/lib/hanami/middleware/render_errors.rb +4 -7
  14. data/lib/hanami/providers/assets.rb +44 -0
  15. data/lib/hanami/rake_tasks.rb +19 -18
  16. data/lib/hanami/settings.rb +1 -1
  17. data/lib/hanami/slice.rb +25 -4
  18. data/lib/hanami/version.rb +1 -1
  19. data/lib/hanami.rb +2 -2
  20. data/spec/integration/assets/assets_spec.rb +101 -0
  21. data/spec/integration/assets/serve_static_assets_spec.rb +152 -0
  22. data/spec/integration/logging/exception_logging_spec.rb +115 -0
  23. data/spec/integration/logging/notifications_spec.rb +68 -0
  24. data/spec/integration/logging/request_logging_spec.rb +128 -0
  25. data/spec/integration/rack_app/middleware_spec.rb +4 -4
  26. data/spec/integration/rack_app/rack_app_spec.rb +0 -221
  27. data/spec/integration/rake_tasks_spec.rb +107 -0
  28. data/spec/integration/view/context/assets_spec.rb +3 -9
  29. data/spec/integration/web/render_detailed_errors_spec.rb +17 -0
  30. data/spec/integration/web/render_errors_spec.rb +6 -4
  31. data/spec/support/app_integration.rb +46 -2
  32. data/spec/unit/hanami/config/actions/content_security_policy_spec.rb +24 -36
  33. data/spec/unit/hanami/config/actions/csrf_protection_spec.rb +4 -3
  34. data/spec/unit/hanami/config/actions/default_values_spec.rb +3 -2
  35. data/spec/unit/hanami/env_spec.rb +11 -25
  36. data/spec/unit/hanami/helpers/assets_helper/asset_url_spec.rb +109 -0
  37. data/spec/unit/hanami/helpers/assets_helper/audio_tag_spec.rb +132 -0
  38. data/spec/unit/hanami/helpers/assets_helper/favicon_link_tag_spec.rb +91 -0
  39. data/spec/unit/hanami/helpers/assets_helper/image_tag_spec.rb +92 -0
  40. data/spec/unit/hanami/helpers/assets_helper/javascript_tag_spec.rb +143 -0
  41. data/spec/unit/hanami/helpers/assets_helper/stylesheet_link_tag_spec.rb +126 -0
  42. data/spec/unit/hanami/helpers/assets_helper/video_tag_spec.rb +132 -0
  43. data/spec/unit/hanami/version_spec.rb +1 -1
  44. metadata +32 -4
  45. data/lib/hanami/assets/app_config.rb +0 -61
  46. data/lib/hanami/assets/config.rb +0 -53
@@ -0,0 +1,752 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+ require "hanami/view"
5
+
6
+ # rubocop:disable Metrics/ModuleLength
7
+
8
+ module Hanami
9
+ module Helpers
10
+ # HTML assets helpers
11
+ #
12
+ # Inject these helpers in a view
13
+ #
14
+ # @since 0.1.0
15
+ #
16
+ # @see http://www.rubydoc.info/gems/hanami-helpers/Hanami/Helpers/HtmlHelper
17
+ module AssetsHelper
18
+ # @since 0.1.0
19
+ # @api private
20
+ NEW_LINE_SEPARATOR = "\n"
21
+
22
+ # @since 0.1.0
23
+ # @api private
24
+ WILDCARD_EXT = ".*"
25
+
26
+ # @since 0.1.0
27
+ # @api private
28
+ JAVASCRIPT_EXT = ".js"
29
+
30
+ # @since 0.1.0
31
+ # @api private
32
+ STYLESHEET_EXT = ".css"
33
+
34
+ # @since 0.1.0
35
+ # @api private
36
+ JAVASCRIPT_MIME_TYPE = "text/javascript"
37
+
38
+ # @since 0.1.0
39
+ # @api private
40
+ STYLESHEET_MIME_TYPE = "text/css"
41
+
42
+ # @since 0.1.0
43
+ # @api private
44
+ FAVICON_MIME_TYPE = "image/x-icon"
45
+
46
+ # @since 0.1.0
47
+ # @api private
48
+ STYLESHEET_REL = "stylesheet"
49
+
50
+ # @since 0.1.0
51
+ # @api private
52
+ FAVICON_REL = "shortcut icon"
53
+
54
+ # @since 0.1.0
55
+ # @api private
56
+ DEFAULT_FAVICON = "favicon.ico"
57
+
58
+ # @since 0.3.0
59
+ # @api private
60
+ CROSSORIGIN_ANONYMOUS = "anonymous"
61
+
62
+ # @since 0.3.0
63
+ # @api private
64
+ ABSOLUTE_URL_MATCHER = URI::DEFAULT_PARSER.make_regexp
65
+
66
+ # @since 1.1.0
67
+ # @api private
68
+ QUERY_STRING_MATCHER = /\?/
69
+
70
+ include Hanami::View::Helpers::TagHelper
71
+
72
+ # Generate `script` tag for given source(s)
73
+ #
74
+ # It accepts one or more strings representing the name of the asset, if it
75
+ # comes from the application or third party gems. It also accepts strings
76
+ # representing absolute URLs in case of public CDN (eg. jQuery CDN).
77
+ #
78
+ # If the "fingerprint mode" is on, `src` is the fingerprinted
79
+ # version of the relative URL.
80
+ #
81
+ # If the "CDN mode" is on, the `src` is an absolute URL of the
82
+ # application CDN.
83
+ #
84
+ # If the "subresource integrity mode" is on, `integriy` is the
85
+ # name of the algorithm, then a hyphen, then the hash value of the file.
86
+ # If more than one algorithm is used, they"ll be separated by a space.
87
+ #
88
+ # @param sources [Array<String>] one or more assets by name or absolute URL
89
+ #
90
+ # @return [Hanami::View::HTML::SafeString] the markup
91
+ #
92
+ # @raise [Hanami::Assets::MissingManifestAssetError] if `fingerprint` or
93
+ # `subresource_integrity` modes are on and the javascript file is missing
94
+ # from the manifest
95
+ #
96
+ # @since 0.1.0
97
+ #
98
+ # @see Hanami::Assets::Helpers#path
99
+ #
100
+ # @example Single Asset
101
+ #
102
+ # <%= js "application" %>
103
+ #
104
+ # # <script src="/assets/application.js" type="text/javascript"></script>
105
+ #
106
+ # @example Multiple Assets
107
+ #
108
+ # <%= js "application", "dashboard" %>
109
+ #
110
+ # # <script src="/assets/application.js" type="text/javascript"></script>
111
+ # # <script src="/assets/dashboard.js" type="text/javascript"></script>
112
+ #
113
+ # @example Asynchronous Execution
114
+ #
115
+ # <%= js "application", async: true %>
116
+ #
117
+ # # <script src="/assets/application.js" type="text/javascript" async="async"></script>
118
+ #
119
+ # @example Subresource Integrity
120
+ #
121
+ # <%= js "application" %>
122
+ #
123
+ # # <script src="/assets/application-28a6b886de2372ee3922fcaf3f78f2d8.js"
124
+ # # type="text/javascript" integrity="sha384-oqVu...Y8wC" crossorigin="anonymous"></script>
125
+ #
126
+ # @example Subresource Integrity for 3rd Party Scripts
127
+ #
128
+ # <%= js "https://example.com/assets/example.js", integrity: "sha384-oqVu...Y8wC" %>
129
+ #
130
+ # # <script src="https://example.com/assets/example.js" type="text/javascript"
131
+ # # integrity="sha384-oqVu...Y8wC" crossorigin="anonymous"></script>
132
+ #
133
+ # @example Deferred Execution
134
+ #
135
+ # <%= js "application", defer: true %>
136
+ #
137
+ # # <script src="/assets/application.js" type="text/javascript" defer="defer"></script>
138
+ #
139
+ # @example Absolute URL
140
+ #
141
+ # <%= js "https://code.jquery.com/jquery-2.1.4.min.js" %>
142
+ #
143
+ # # <script src="https://code.jquery.com/jquery-2.1.4.min.js" type="text/javascript"></script>
144
+ #
145
+ # @example Fingerprint Mode
146
+ #
147
+ # <%= js "application" %>
148
+ #
149
+ # # <script src="/assets/application-28a6b886de2372ee3922fcaf3f78f2d8.js" type="text/javascript"></script>
150
+ #
151
+ # @example CDN Mode
152
+ #
153
+ # <%= js "application" %>
154
+ #
155
+ # # <script src="https://assets.bookshelf.org/assets/application-28a6b886de2372ee3922fcaf3f78f2d8.js"
156
+ # # type="text/javascript"></script>
157
+ def javascript_tag(*source_paths, **options)
158
+ options = options.reject { |k, _| k.to_sym == :src }
159
+
160
+ _safe_tags(*source_paths) do |source|
161
+ attributes = {
162
+ src: _typed_path(source, JAVASCRIPT_EXT),
163
+ type: JAVASCRIPT_MIME_TYPE
164
+ }
165
+ attributes.merge!(options)
166
+
167
+ if _context.assets.subresource_integrity? || attributes.include?(:integrity)
168
+ attributes[:integrity] ||= _subresource_integrity_value(source, JAVASCRIPT_EXT)
169
+ attributes[:crossorigin] ||= CROSSORIGIN_ANONYMOUS
170
+ end
171
+
172
+ tag.script(**attributes).to_s
173
+ end
174
+ end
175
+
176
+ # @api public
177
+ # @since 2.1.0
178
+ alias_method :js, :javascript_tag
179
+
180
+ # Generate `link` tag for given source(s)
181
+ #
182
+ # It accepts one or more strings representing the name of the asset, if it
183
+ # comes from the application or third party gems. It also accepts strings
184
+ # representing absolute URLs in case of public CDN (eg. Bootstrap CDN).
185
+ #
186
+ # If the "fingerprint mode" is on, `href` is the fingerprinted
187
+ # version of the relative URL.
188
+ #
189
+ # If the "CDN mode" is on, the `href` is an absolute URL of the
190
+ # application CDN.
191
+ #
192
+ # If the "subresource integrity mode" is on, `integriy` is the
193
+ # name of the algorithm, then a hyphen, then the hashed value of the file.
194
+ # If more than one algorithm is used, they"ll be separated by a space.
195
+ #
196
+ # @param sources [Array<String>] one or more assets by name or absolute URL
197
+ #
198
+ # @return [Hanami::View::HTML::SafeString] the markup
199
+ #
200
+ # @raise [Hanami::Assets::MissingManifestAssetError] if `fingerprint` or
201
+ # `subresource_integrity` modes are on and the stylesheet file is missing
202
+ # from the manifest
203
+ #
204
+ # @since 0.1.0
205
+ #
206
+ # @see Hanami::Assets::Helpers#path
207
+ #
208
+ # @example Single Asset
209
+ #
210
+ # <%= css "application" %>
211
+ #
212
+ # # <link href="/assets/application.css" type="text/css" rel="stylesheet">
213
+ #
214
+ # @example Multiple Assets
215
+ #
216
+ # <%= css "application", "dashboard" %>
217
+ #
218
+ # # <link href="/assets/application.css" type="text/css" rel="stylesheet">
219
+ # # <link href="/assets/dashboard.css" type="text/css" rel="stylesheet">
220
+ #
221
+ # @example Subresource Integrity
222
+ #
223
+ # <%= css "application" %>
224
+ #
225
+ # # <link href="/assets/application-28a6b886de2372ee3922fcaf3f78f2d8.css"
226
+ # # type="text/css" integrity="sha384-oqVu...Y8wC" crossorigin="anonymous"></script>
227
+ #
228
+ # @example Subresource Integrity for 3rd Party Assets
229
+ #
230
+ # <%= css "https://example.com/assets/example.css", integrity: "sha384-oqVu...Y8wC" %>
231
+ #
232
+ # # <link href="https://example.com/assets/example.css"
233
+ # # type="text/css" rel="stylesheet" integrity="sha384-oqVu...Y8wC" crossorigin="anonymous"></script>
234
+ #
235
+ # @example Absolute URL
236
+ #
237
+ # <%= css "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" %>
238
+ #
239
+ # # <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"
240
+ # # type="text/css" rel="stylesheet">
241
+ #
242
+ # @example Fingerprint Mode
243
+ #
244
+ # <%= css "application" %>
245
+ #
246
+ # # <link href="/assets/application-28a6b886de2372ee3922fcaf3f78f2d8.css" type="text/css" rel="stylesheet">
247
+ #
248
+ # @example CDN Mode
249
+ #
250
+ # <%= css "application" %>
251
+ #
252
+ # # <link href="https://assets.bookshelf.org/assets/application-28a6b886de2372ee3922fcaf3f78f2d8.css"
253
+ # # type="text/css" rel="stylesheet">
254
+ def stylesheet_link_tag(*source_paths, **options)
255
+ options = options.reject { |k, _| k.to_sym == :href }
256
+
257
+ _safe_tags(*source_paths) do |source_path|
258
+ attributes = {
259
+ href: _typed_path(source_path, STYLESHEET_EXT),
260
+ type: STYLESHEET_MIME_TYPE,
261
+ rel: STYLESHEET_REL
262
+ }
263
+ attributes.merge!(options)
264
+
265
+ if _context.assets.subresource_integrity? || attributes.include?(:integrity)
266
+ attributes[:integrity] ||= _subresource_integrity_value(source_path, STYLESHEET_EXT)
267
+ attributes[:crossorigin] ||= CROSSORIGIN_ANONYMOUS
268
+ end
269
+
270
+ tag.link(**attributes).to_s
271
+ end
272
+ end
273
+
274
+ # @api public
275
+ # @since 2.1.0
276
+ alias_method :css, :stylesheet_link_tag
277
+
278
+ # Generate `img` tag for given source
279
+ #
280
+ # It accepts one string representing the name of the asset, if it comes
281
+ # from the application or third party gems. It also accepts string
282
+ # representing absolute URLs in case of public CDN (eg. Bootstrap CDN).
283
+ #
284
+ # `alt` Attribute is auto generated from `src`.
285
+ # You can specify a different value, by passing the `:src` option.
286
+ #
287
+ # If the "fingerprint mode" is on, `src` is the fingerprinted
288
+ # version of the relative URL.
289
+ #
290
+ # If the "CDN mode" is on, the `src` is an absolute URL of the
291
+ # application CDN.
292
+ #
293
+ # @param source [String] asset name or absolute URL
294
+ # @param options [Hash] HTML 5 attributes
295
+ #
296
+ # @return [Hanami::View::HTML::SafeString] the markup
297
+ #
298
+ # @raise [Hanami::Assets::MissingManifestAssetError] if `fingerprint` or
299
+ # `subresource_integrity` modes are on and the image file is missing
300
+ # from the manifest
301
+ #
302
+ # @since 0.1.0
303
+ #
304
+ # @see Hanami::Assets::Helpers#path
305
+ #
306
+ # @example Basic Usage
307
+ #
308
+ # <%= image_tag "logo.png" %>
309
+ #
310
+ # # <img src="/assets/logo.png" alt="Logo">
311
+ #
312
+ # @example Custom alt Attribute
313
+ #
314
+ # <%= image_tag "logo.png", alt: "Application Logo" %>
315
+ #
316
+ # # <img src="/assets/logo.png" alt="Application Logo">
317
+ #
318
+ # @example Custom HTML Attributes
319
+ #
320
+ # <%= image_tag "logo.png", id: "logo", class: "image" %>
321
+ #
322
+ # # <img src="/assets/logo.png" alt="Logo" id="logo" class="image">
323
+ #
324
+ # @example Absolute URL
325
+ #
326
+ # <%= image_tag "https://example-cdn.com/images/logo.png" %>
327
+ #
328
+ # # <img src="https://example-cdn.com/images/logo.png" alt="Logo">
329
+ #
330
+ # @example Fingerprint Mode
331
+ #
332
+ # <%= image_tag "logo.png" %>
333
+ #
334
+ # # <img src="/assets/logo-28a6b886de2372ee3922fcaf3f78f2d8.png" alt="Logo">
335
+ #
336
+ # @example CDN Mode
337
+ #
338
+ # <%= image_tag "logo.png" %>
339
+ #
340
+ # # <img src="https://assets.bookshelf.org/assets/logo-28a6b886de2372ee3922fcaf3f78f2d8.png" alt="Logo">
341
+ def image_tag(source, options = {})
342
+ options = options.reject { |k, _| k.to_sym == :src }
343
+ attributes = {
344
+ src: asset_url(source),
345
+ alt: _context.inflector.humanize(::File.basename(source, WILDCARD_EXT))
346
+ }
347
+ attributes.merge!(options)
348
+
349
+ tag.img(**attributes)
350
+ end
351
+
352
+ # Generate `link` tag application favicon.
353
+ #
354
+ # If no argument is given, it assumes `favico.ico` from the application.
355
+ #
356
+ # It accepts one string representing the name of the asset.
357
+ #
358
+ # If the "fingerprint mode" is on, `href` is the fingerprinted version
359
+ # of the relative URL.
360
+ #
361
+ # If the "CDN mode" is on, the `href` is an absolute URL of the
362
+ # application CDN.
363
+ #
364
+ # @param source [String] asset name
365
+ # @param options [Hash] HTML 5 attributes
366
+ #
367
+ # @return [Hanami::View::HTML::SafeString] the markup
368
+ #
369
+ # @raise [Hanami::Assets::MissingManifestAssetError] if `fingerprint` or
370
+ # `subresource_integrity` modes are on and the favicon is file missing
371
+ # from the manifest
372
+ #
373
+ # @since 0.1.0
374
+ #
375
+ # @see Hanami::Assets::Helpers#path
376
+ #
377
+ # @example Basic Usage
378
+ #
379
+ # <%= favicon_link_tag %>
380
+ #
381
+ # # <link href="/assets/favicon.ico" rel="shortcut icon" type="image/x-icon">
382
+ #
383
+ # @example Custom Path
384
+ #
385
+ # <%= favicon_link_tag "fav.ico" %>
386
+ #
387
+ # # <link href="/assets/fav.ico" rel="shortcut icon" type="image/x-icon">
388
+ #
389
+ # @example Custom HTML Attributes
390
+ #
391
+ # <%= favicon_link_tag "favicon.ico", id: "fav" %>
392
+ #
393
+ # # <link id: "fav" href="/assets/favicon.ico" rel="shortcut icon" type="image/x-icon">
394
+ #
395
+ # @example Fingerprint Mode
396
+ #
397
+ # <%= favicon_link_tag %>
398
+ #
399
+ # # <link href="/assets/favicon-28a6b886de2372ee3922fcaf3f78f2d8.ico" rel="shortcut icon" type="image/x-icon">
400
+ #
401
+ # @example CDN Mode
402
+ #
403
+ # <%= favicon_link_tag %>
404
+ #
405
+ # # <link href="https://assets.bookshelf.org/assets/favicon-28a6b886de2372ee3922fcaf3f78f2d8.ico"
406
+ # rel="shortcut icon" type="image/x-icon">
407
+ def favicon_link_tag(source = DEFAULT_FAVICON, options = {})
408
+ options = options.reject { |k, _| k.to_sym == :href }
409
+
410
+ attributes = {
411
+ href: asset_url(source),
412
+ rel: FAVICON_REL,
413
+ type: FAVICON_MIME_TYPE
414
+ }
415
+ attributes.merge!(options)
416
+
417
+ tag.link(**attributes)
418
+ end
419
+
420
+ # @api public
421
+ # @since 2.1.0
422
+ alias_method :favicon, :favicon_link_tag
423
+
424
+ # Generate `video` tag for given source
425
+ #
426
+ # It accepts one string representing the name of the asset, if it comes
427
+ # from the application or third party gems. It also accepts string
428
+ # representing absolute URLs in case of public CDN (eg. Bootstrap CDN).
429
+ #
430
+ # Alternatively, it accepts a block that allows to specify one or more
431
+ # sources via the `source` tag.
432
+ #
433
+ # If the "fingerprint mode" is on, `src` is the fingerprinted
434
+ # version of the relative URL.
435
+ #
436
+ # If the "CDN mode" is on, the `src` is an absolute URL of the
437
+ # application CDN.
438
+ #
439
+ # @param source [String] asset name or absolute URL
440
+ # @param options [Hash] HTML 5 attributes
441
+ #
442
+ # @return [Hanami::View::HTML::SafeString] the markup
443
+ #
444
+ # @raise [Hanami::Assets::MissingManifestAssetError] if `fingerprint` or
445
+ # `subresource_integrity` modes are on and the video file is missing
446
+ # from the manifest
447
+ #
448
+ # @raise [ArgumentError] if source isn"t specified both as argument or
449
+ # tag inside the given block
450
+ #
451
+ # @since 0.1.0
452
+ #
453
+ # @see Hanami::Assets::Helpers#path
454
+ #
455
+ # @example Basic Usage
456
+ #
457
+ # <%= video_tag "movie.mp4" %>
458
+ #
459
+ # # <video src="/assets/movie.mp4"></video>
460
+ #
461
+ # @example Absolute URL
462
+ #
463
+ # <%= video_tag "https://example-cdn.com/assets/movie.mp4" %>
464
+ #
465
+ # # <video src="https://example-cdn.com/assets/movie.mp4"></video>
466
+ #
467
+ # @example Custom HTML Attributes
468
+ #
469
+ # <%= video_tag("movie.mp4", autoplay: true, controls: true) %>
470
+ #
471
+ # # <video src="/assets/movie.mp4" autoplay="autoplay" controls="controls"></video>
472
+ #
473
+ # @example Fallback Content
474
+ #
475
+ # <%=
476
+ # video_tag("movie.mp4") do
477
+ # "Your browser does not support the video tag"
478
+ # end
479
+ # %>
480
+ #
481
+ # # <video src="/assets/movie.mp4">
482
+ # # Your browser does not support the video tag
483
+ # # </video>
484
+ #
485
+ # @example Tracks
486
+ #
487
+ # <%=
488
+ # video_tag("movie.mp4") do
489
+ # tag.track(kind: "captions", src: asset_url("movie.en.vtt"),
490
+ # srclang: "en", label: "English")
491
+ # end
492
+ # %>
493
+ #
494
+ # # <video src="/assets/movie.mp4">
495
+ # # <track kind="captions" src="/assets/movie.en.vtt" srclang="en" label="English">
496
+ # # </video>
497
+ #
498
+ # @example Without Any Argument
499
+ #
500
+ # <%= video_tag %>
501
+ #
502
+ # # ArgumentError
503
+ #
504
+ # @example Without src And Without Block
505
+ #
506
+ # <%= video_tag(content: true) %>
507
+ #
508
+ # # ArgumentError
509
+ #
510
+ # @example Fingerprint Mode
511
+ #
512
+ # <%= video_tag "movie.mp4" %>
513
+ #
514
+ # # <video src="/assets/movie-28a6b886de2372ee3922fcaf3f78f2d8.mp4"></video>
515
+ #
516
+ # @example CDN Mode
517
+ #
518
+ # <%= video_tag "movie.mp4" %>
519
+ #
520
+ # # <video src="https://assets.bookshelf.org/assets/movie-28a6b886de2372ee3922fcaf3f78f2d8.mp4"></video>
521
+ def video_tag(source = nil, options = {}, &blk)
522
+ options = _source_options(source, options, &blk)
523
+ tag.video(**options, &blk)
524
+ end
525
+
526
+ # Generate `audio` tag for given source
527
+ #
528
+ # It accepts one string representing the name of the asset, if it comes
529
+ # from the application or third party gems. It also accepts string
530
+ # representing absolute URLs in case of public CDN (eg. Bootstrap CDN).
531
+ #
532
+ # Alternatively, it accepts a block that allows to specify one or more
533
+ # sources via the `source` tag.
534
+ #
535
+ # If the "fingerprint mode" is on, `src` is the fingerprinted
536
+ # version of the relative URL.
537
+ #
538
+ # If the "CDN mode" is on, the `src` is an absolute URL of the
539
+ # application CDN.
540
+ #
541
+ # @param source [String] asset name or absolute URL
542
+ # @param options [Hash] HTML 5 attributes
543
+ #
544
+ # @return [Hanami::View::HTML::SafeString] the markup
545
+ #
546
+ # @raise [Hanami::Assets::MissingManifestAssetError] if `fingerprint` or
547
+ # `subresource_integrity` modes are on and the audio file is missing
548
+ # from the manifest
549
+ #
550
+ # @raise [ArgumentError] if source isn"t specified both as argument or
551
+ # tag inside the given block
552
+ #
553
+ # @since 0.1.0
554
+ #
555
+ # @see Hanami::Assets::Helpers#path
556
+ #
557
+ # @example Basic Usage
558
+ #
559
+ # <%= audio_tag "song.ogg" %>
560
+ #
561
+ # # <audio src="/assets/song.ogg"></audio>
562
+ #
563
+ # @example Absolute URL
564
+ #
565
+ # <%= audio_tag "https://example-cdn.com/assets/song.ogg" %>
566
+ #
567
+ # # <audio src="https://example-cdn.com/assets/song.ogg"></audio>
568
+ #
569
+ # @example Custom HTML Attributes
570
+ #
571
+ # <%= audio_tag("song.ogg", autoplay: true, controls: true) %>
572
+ #
573
+ # # <audio src="/assets/song.ogg" autoplay="autoplay" controls="controls"></audio>
574
+ #
575
+ # @example Fallback Content
576
+ #
577
+ # <%=
578
+ # audio_tag("song.ogg") do
579
+ # "Your browser does not support the audio tag"
580
+ # end
581
+ # %>
582
+ #
583
+ # # <audio src="/assets/song.ogg">
584
+ # # Your browser does not support the audio tag
585
+ # # </audio>
586
+ #
587
+ # @example Tracks
588
+ #
589
+ # <%=
590
+ # audio_tag("song.ogg") do
591
+ # tag.track(kind: "captions", src: asset_url("song.pt-BR.vtt"),
592
+ # srclang: "pt-BR", label: "Portuguese")
593
+ # end
594
+ # %>
595
+ #
596
+ # # <audio src="/assets/song.ogg">
597
+ # # <track kind="captions" src="/assets/song.pt-BR.vtt" srclang="pt-BR" label="Portuguese">
598
+ # # </audio>
599
+ #
600
+ # @example Without Any Argument
601
+ #
602
+ # <%= audio_tag %>
603
+ #
604
+ # # ArgumentError
605
+ #
606
+ # @example Without src And Without Block
607
+ #
608
+ # <%= audio_tag(controls: true) %>
609
+ #
610
+ # # ArgumentError
611
+ #
612
+ # @example Fingerprint Mode
613
+ #
614
+ # <%= audio_tag "song.ogg" %>
615
+ #
616
+ # # <audio src="/assets/song-28a6b886de2372ee3922fcaf3f78f2d8.ogg"></audio>
617
+ #
618
+ # @example CDN Mode
619
+ #
620
+ # <%= audio_tag "song.ogg" %>
621
+ #
622
+ # # <audio src="https://assets.bookshelf.org/assets/song-28a6b886de2372ee3922fcaf3f78f2d8.ogg"></audio>
623
+ def audio_tag(source = nil, options = {}, &blk)
624
+ options = _source_options(source, options, &blk)
625
+ tag.audio(**options, &blk)
626
+ end
627
+
628
+ # It generates the relative or absolute URL for the given asset.
629
+ # It automatically decides if it has to use the relative or absolute
630
+ # depending on the configuration and current environment.
631
+ #
632
+ # Absolute URLs are returned as they are.
633
+ #
634
+ # It can be the name of the asset, coming from the sources or third party
635
+ # gems.
636
+ #
637
+ # If Fingerprint mode is on, it returns the fingerprinted path of the source
638
+ #
639
+ # If CDN mode is on, it returns the absolute URL of the asset.
640
+ #
641
+ # @param source [String] the asset name
642
+ #
643
+ # @return [String] the asset path
644
+ #
645
+ # @raise [Hanami::Assets::MissingManifestAssetError] if `fingerprint` or
646
+ # `subresource_integrity` modes are on and the asset is missing
647
+ # from the manifest
648
+ #
649
+ # @since 0.1.0
650
+ #
651
+ # @example Basic Usage
652
+ #
653
+ # <%= asset_url "application.js" %>
654
+ #
655
+ # # "/assets/application.js"
656
+ #
657
+ # @example Alias
658
+ #
659
+ # <%= asset_url "application.js" %>
660
+ #
661
+ # # "/assets/application.js"
662
+ #
663
+ # @example Absolute URL
664
+ #
665
+ # <%= asset_url "https://code.jquery.com/jquery-2.1.4.min.js" %>
666
+ #
667
+ # # "https://code.jquery.com/jquery-2.1.4.min.js"
668
+ #
669
+ # @example Fingerprint Mode
670
+ #
671
+ # <%= asset_url "application.js" %>
672
+ #
673
+ # # "/assets/application-28a6b886de2372ee3922fcaf3f78f2d8.js"
674
+ #
675
+ # @example CDN Mode
676
+ #
677
+ # <%= asset_url "application.js" %>
678
+ #
679
+ # # "https://assets.bookshelf.org/assets/application-28a6b886de2372ee3922fcaf3f78f2d8.js"
680
+ def asset_url(source_path)
681
+ return source_path if _absolute_url?(source_path)
682
+
683
+ _context.assets[source_path].url
684
+ end
685
+
686
+ private
687
+
688
+ # @since 0.1.0
689
+ # @api private
690
+ def _safe_tags(*source_paths, &blk)
691
+ ::Hanami::View::HTML::SafeString.new(
692
+ source_paths.map(&blk).join(NEW_LINE_SEPARATOR)
693
+ )
694
+ end
695
+
696
+ # @since 2.1.0
697
+ # @api private
698
+ def _typed_path(source, ext)
699
+ source = "#{source}#{ext}" if _append_extension?(source, ext)
700
+ asset_url(source)
701
+ end
702
+
703
+ # @api private
704
+ def _subresource_integrity_value(source_path, ext)
705
+ return if _absolute_url?(source_path)
706
+
707
+ source_path = "#{source_path}#{ext}" unless /#{Regexp.escape(ext)}\z/.match?(source_path)
708
+ _context.assets[source_path].sri
709
+ end
710
+
711
+ # @since 0.1.0
712
+ # @api private
713
+ def _absolute_url?(source)
714
+ ABSOLUTE_URL_MATCHER.match(source)
715
+ end
716
+
717
+ # @since 1.2.0
718
+ # @api private
719
+ def _crossorigin?(source)
720
+ return false unless _absolute_url?(source)
721
+
722
+ _context.assets.crossorigin?(source)
723
+ end
724
+
725
+ # @since 0.1.0
726
+ # @api private
727
+ def _source_options(src, options, &blk)
728
+ options ||= {}
729
+
730
+ if src.respond_to?(:to_hash)
731
+ options = src.to_hash
732
+ elsif src
733
+ options[:src] = asset_url(src)
734
+ end
735
+
736
+ if !options[:src] && !blk
737
+ raise ArgumentError.new("You should provide a source via `src` option or with a `source` HTML tag")
738
+ end
739
+
740
+ options
741
+ end
742
+
743
+ # @since 1.1.0
744
+ # @api private
745
+ def _append_extension?(source, ext)
746
+ source !~ QUERY_STRING_MATCHER && source !~ /#{Regexp.escape(ext)}\z/
747
+ end
748
+ end
749
+ end
750
+ end
751
+
752
+ # rubocop:enable Metrics/ModuleLength