hanami 2.1.0.beta1 → 2.1.0.beta2.1

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 +27 -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 +772 -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_spec.rb +136 -0
  38. data/spec/unit/hanami/helpers/assets_helper/favicon_spec.rb +91 -0
  39. data/spec/unit/hanami/helpers/assets_helper/image_spec.rb +96 -0
  40. data/spec/unit/hanami/helpers/assets_helper/javascript_spec.rb +147 -0
  41. data/spec/unit/hanami/helpers/assets_helper/stylesheet_spec.rb +130 -0
  42. data/spec/unit/hanami/helpers/assets_helper/video_spec.rb +136 -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,772 @@
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(*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
179
+
180
+ # @api public
181
+ # @since 2.1.0
182
+ alias_method :javascript_tag, :javascript
183
+
184
+ # Generate `link` tag for given source(s)
185
+ #
186
+ # It accepts one or more strings representing the name of the asset, if it
187
+ # comes from the application or third party gems. It also accepts strings
188
+ # representing absolute URLs in case of public CDN (eg. Bootstrap CDN).
189
+ #
190
+ # If the "fingerprint mode" is on, `href` is the fingerprinted
191
+ # version of the relative URL.
192
+ #
193
+ # If the "CDN mode" is on, the `href` is an absolute URL of the
194
+ # application CDN.
195
+ #
196
+ # If the "subresource integrity mode" is on, `integriy` is the
197
+ # name of the algorithm, then a hyphen, then the hashed value of the file.
198
+ # If more than one algorithm is used, they"ll be separated by a space.
199
+ #
200
+ # @param sources [Array<String>] one or more assets by name or absolute URL
201
+ #
202
+ # @return [Hanami::View::HTML::SafeString] the markup
203
+ #
204
+ # @raise [Hanami::Assets::MissingManifestAssetError] if `fingerprint` or
205
+ # `subresource_integrity` modes are on and the stylesheet file is missing
206
+ # from the manifest
207
+ #
208
+ # @since 0.1.0
209
+ #
210
+ # @see Hanami::Assets::Helpers#path
211
+ #
212
+ # @example Single Asset
213
+ #
214
+ # <%= css "application" %>
215
+ #
216
+ # # <link href="/assets/application.css" type="text/css" rel="stylesheet">
217
+ #
218
+ # @example Multiple Assets
219
+ #
220
+ # <%= css "application", "dashboard" %>
221
+ #
222
+ # # <link href="/assets/application.css" type="text/css" rel="stylesheet">
223
+ # # <link href="/assets/dashboard.css" type="text/css" rel="stylesheet">
224
+ #
225
+ # @example Subresource Integrity
226
+ #
227
+ # <%= css "application" %>
228
+ #
229
+ # # <link href="/assets/application-28a6b886de2372ee3922fcaf3f78f2d8.css"
230
+ # # type="text/css" integrity="sha384-oqVu...Y8wC" crossorigin="anonymous"></script>
231
+ #
232
+ # @example Subresource Integrity for 3rd Party Assets
233
+ #
234
+ # <%= css "https://example.com/assets/example.css", integrity: "sha384-oqVu...Y8wC" %>
235
+ #
236
+ # # <link href="https://example.com/assets/example.css"
237
+ # # type="text/css" rel="stylesheet" integrity="sha384-oqVu...Y8wC" crossorigin="anonymous"></script>
238
+ #
239
+ # @example Absolute URL
240
+ #
241
+ # <%= css "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" %>
242
+ #
243
+ # # <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"
244
+ # # type="text/css" rel="stylesheet">
245
+ #
246
+ # @example Fingerprint Mode
247
+ #
248
+ # <%= css "application" %>
249
+ #
250
+ # # <link href="/assets/application-28a6b886de2372ee3922fcaf3f78f2d8.css" type="text/css" rel="stylesheet">
251
+ #
252
+ # @example CDN Mode
253
+ #
254
+ # <%= css "application" %>
255
+ #
256
+ # # <link href="https://assets.bookshelf.org/assets/application-28a6b886de2372ee3922fcaf3f78f2d8.css"
257
+ # # type="text/css" rel="stylesheet">
258
+ def stylesheet(*source_paths, **options)
259
+ options = options.reject { |k, _| k.to_sym == :href }
260
+
261
+ _safe_tags(*source_paths) do |source_path|
262
+ attributes = {
263
+ href: _typed_path(source_path, STYLESHEET_EXT),
264
+ type: STYLESHEET_MIME_TYPE,
265
+ rel: STYLESHEET_REL
266
+ }
267
+ attributes.merge!(options)
268
+
269
+ if _context.assets.subresource_integrity? || attributes.include?(:integrity)
270
+ attributes[:integrity] ||= _subresource_integrity_value(source_path, STYLESHEET_EXT)
271
+ attributes[:crossorigin] ||= CROSSORIGIN_ANONYMOUS
272
+ end
273
+
274
+ tag.link(**attributes).to_s
275
+ end
276
+ end
277
+
278
+ # @api public
279
+ # @since 2.1.0
280
+ alias_method :css, :stylesheet
281
+
282
+ # @api public
283
+ # @since 2.1.0
284
+ alias_method :stylesheet_link_tag, :stylesheet
285
+
286
+ # Generate `img` tag for given source
287
+ #
288
+ # It accepts one string representing the name of the asset, if it comes
289
+ # from the application or third party gems. It also accepts string
290
+ # representing absolute URLs in case of public CDN (eg. Bootstrap CDN).
291
+ #
292
+ # `alt` Attribute is auto generated from `src`.
293
+ # You can specify a different value, by passing the `:src` option.
294
+ #
295
+ # If the "fingerprint mode" is on, `src` is the fingerprinted
296
+ # version of the relative URL.
297
+ #
298
+ # If the "CDN mode" is on, the `src` is an absolute URL of the
299
+ # application CDN.
300
+ #
301
+ # @param source [String] asset name or absolute URL
302
+ # @param options [Hash] HTML 5 attributes
303
+ #
304
+ # @return [Hanami::View::HTML::SafeString] the markup
305
+ #
306
+ # @raise [Hanami::Assets::MissingManifestAssetError] if `fingerprint` or
307
+ # `subresource_integrity` modes are on and the image file is missing
308
+ # from the manifest
309
+ #
310
+ # @since 0.1.0
311
+ #
312
+ # @see Hanami::Assets::Helpers#path
313
+ #
314
+ # @example Basic Usage
315
+ #
316
+ # <%= image "logo.png" %>
317
+ #
318
+ # # <img src="/assets/logo.png" alt="Logo">
319
+ #
320
+ # @example Custom alt Attribute
321
+ #
322
+ # <%= image "logo.png", alt: "Application Logo" %>
323
+ #
324
+ # # <img src="/assets/logo.png" alt="Application Logo">
325
+ #
326
+ # @example Custom HTML Attributes
327
+ #
328
+ # <%= image "logo.png", id: "logo", class: "image" %>
329
+ #
330
+ # # <img src="/assets/logo.png" alt="Logo" id="logo" class="image">
331
+ #
332
+ # @example Absolute URL
333
+ #
334
+ # <%= image "https://example-cdn.com/images/logo.png" %>
335
+ #
336
+ # # <img src="https://example-cdn.com/images/logo.png" alt="Logo">
337
+ #
338
+ # @example Fingerprint Mode
339
+ #
340
+ # <%= image "logo.png" %>
341
+ #
342
+ # # <img src="/assets/logo-28a6b886de2372ee3922fcaf3f78f2d8.png" alt="Logo">
343
+ #
344
+ # @example CDN Mode
345
+ #
346
+ # <%= image "logo.png" %>
347
+ #
348
+ # # <img src="https://assets.bookshelf.org/assets/logo-28a6b886de2372ee3922fcaf3f78f2d8.png" alt="Logo">
349
+ def image(source, options = {})
350
+ options = options.reject { |k, _| k.to_sym == :src }
351
+ attributes = {
352
+ src: asset_url(source),
353
+ alt: _context.inflector.humanize(::File.basename(source, WILDCARD_EXT))
354
+ }
355
+ attributes.merge!(options)
356
+
357
+ tag.img(**attributes)
358
+ end
359
+
360
+ # @api public
361
+ # @since 2.1.0
362
+ alias_method :image_tag, :image
363
+
364
+ # Generate `link` tag application favicon.
365
+ #
366
+ # If no argument is given, it assumes `favico.ico` from the application.
367
+ #
368
+ # It accepts one string representing the name of the asset.
369
+ #
370
+ # If the "fingerprint mode" is on, `href` is the fingerprinted version
371
+ # of the relative URL.
372
+ #
373
+ # If the "CDN mode" is on, the `href` is an absolute URL of the
374
+ # application CDN.
375
+ #
376
+ # @param source [String] asset name
377
+ # @param options [Hash] HTML 5 attributes
378
+ #
379
+ # @return [Hanami::View::HTML::SafeString] the markup
380
+ #
381
+ # @raise [Hanami::Assets::MissingManifestAssetError] if `fingerprint` or
382
+ # `subresource_integrity` modes are on and the favicon is file missing
383
+ # from the manifest
384
+ #
385
+ # @since 0.1.0
386
+ #
387
+ # @see Hanami::Assets::Helpers#path
388
+ #
389
+ # @example Basic Usage
390
+ #
391
+ # <%= favicon %>
392
+ #
393
+ # # <link href="/assets/favicon.ico" rel="shortcut icon" type="image/x-icon">
394
+ #
395
+ # @example Custom Path
396
+ #
397
+ # <%= favicon "fav.ico" %>
398
+ #
399
+ # # <link href="/assets/fav.ico" rel="shortcut icon" type="image/x-icon">
400
+ #
401
+ # @example Custom HTML Attributes
402
+ #
403
+ # <%= favicon "favicon.ico", id: "fav" %>
404
+ #
405
+ # # <link id: "fav" href="/assets/favicon.ico" rel="shortcut icon" type="image/x-icon">
406
+ #
407
+ # @example Fingerprint Mode
408
+ #
409
+ # <%= favicon %>
410
+ #
411
+ # # <link href="/assets/favicon-28a6b886de2372ee3922fcaf3f78f2d8.ico" rel="shortcut icon" type="image/x-icon">
412
+ #
413
+ # @example CDN Mode
414
+ #
415
+ # <%= favicon %>
416
+ #
417
+ # # <link href="https://assets.bookshelf.org/assets/favicon-28a6b886de2372ee3922fcaf3f78f2d8.ico"
418
+ # rel="shortcut icon" type="image/x-icon">
419
+ def favicon(source = DEFAULT_FAVICON, options = {})
420
+ options = options.reject { |k, _| k.to_sym == :href }
421
+
422
+ attributes = {
423
+ href: asset_url(source),
424
+ rel: FAVICON_REL,
425
+ type: FAVICON_MIME_TYPE
426
+ }
427
+ attributes.merge!(options)
428
+
429
+ tag.link(**attributes)
430
+ end
431
+
432
+ # @api public
433
+ # @since 2.1.0
434
+ alias_method :favicon_link_tag, :favicon
435
+
436
+ # Generate `video` tag for given source
437
+ #
438
+ # It accepts one string representing the name of the asset, if it comes
439
+ # from the application or third party gems. It also accepts string
440
+ # representing absolute URLs in case of public CDN (eg. Bootstrap CDN).
441
+ #
442
+ # Alternatively, it accepts a block that allows to specify one or more
443
+ # sources via the `source` tag.
444
+ #
445
+ # If the "fingerprint mode" is on, `src` is the fingerprinted
446
+ # version of the relative URL.
447
+ #
448
+ # If the "CDN mode" is on, the `src` is an absolute URL of the
449
+ # application CDN.
450
+ #
451
+ # @param source [String] asset name or absolute URL
452
+ # @param options [Hash] HTML 5 attributes
453
+ #
454
+ # @return [Hanami::View::HTML::SafeString] the markup
455
+ #
456
+ # @raise [Hanami::Assets::MissingManifestAssetError] if `fingerprint` or
457
+ # `subresource_integrity` modes are on and the video file is missing
458
+ # from the manifest
459
+ #
460
+ # @raise [ArgumentError] if source isn"t specified both as argument or
461
+ # tag inside the given block
462
+ #
463
+ # @since 0.1.0
464
+ #
465
+ # @see Hanami::Assets::Helpers#path
466
+ #
467
+ # @example Basic Usage
468
+ #
469
+ # <%= video "movie.mp4" %>
470
+ #
471
+ # # <video src="/assets/movie.mp4"></video>
472
+ #
473
+ # @example Absolute URL
474
+ #
475
+ # <%= video "https://example-cdn.com/assets/movie.mp4" %>
476
+ #
477
+ # # <video src="https://example-cdn.com/assets/movie.mp4"></video>
478
+ #
479
+ # @example Custom HTML Attributes
480
+ #
481
+ # <%= video("movie.mp4", autoplay: true, controls: true) %>
482
+ #
483
+ # # <video src="/assets/movie.mp4" autoplay="autoplay" controls="controls"></video>
484
+ #
485
+ # @example Fallback Content
486
+ #
487
+ # <%=
488
+ # video("movie.mp4") do
489
+ # "Your browser does not support the video tag"
490
+ # end
491
+ # %>
492
+ #
493
+ # # <video src="/assets/movie.mp4">
494
+ # # Your browser does not support the video tag
495
+ # # </video>
496
+ #
497
+ # @example Tracks
498
+ #
499
+ # <%=
500
+ # video("movie.mp4") do
501
+ # tag.track(kind: "captions", src: asset_url("movie.en.vtt"),
502
+ # srclang: "en", label: "English")
503
+ # end
504
+ # %>
505
+ #
506
+ # # <video src="/assets/movie.mp4">
507
+ # # <track kind="captions" src="/assets/movie.en.vtt" srclang="en" label="English">
508
+ # # </video>
509
+ #
510
+ # @example Without Any Argument
511
+ #
512
+ # <%= video %>
513
+ #
514
+ # # ArgumentError
515
+ #
516
+ # @example Without src And Without Block
517
+ #
518
+ # <%= video(content: true) %>
519
+ #
520
+ # # ArgumentError
521
+ #
522
+ # @example Fingerprint Mode
523
+ #
524
+ # <%= video "movie.mp4" %>
525
+ #
526
+ # # <video src="/assets/movie-28a6b886de2372ee3922fcaf3f78f2d8.mp4"></video>
527
+ #
528
+ # @example CDN Mode
529
+ #
530
+ # <%= video "movie.mp4" %>
531
+ #
532
+ # # <video src="https://assets.bookshelf.org/assets/movie-28a6b886de2372ee3922fcaf3f78f2d8.mp4"></video>
533
+ def video(source = nil, options = {}, &blk)
534
+ options = _source_options(source, options, &blk)
535
+ tag.video(**options, &blk)
536
+ end
537
+
538
+ # @api public
539
+ # @since 2.1.0
540
+ alias_method :video_tag, :video
541
+
542
+ # Generate `audio` tag for given source
543
+ #
544
+ # It accepts one string representing the name of the asset, if it comes
545
+ # from the application or third party gems. It also accepts string
546
+ # representing absolute URLs in case of public CDN (eg. Bootstrap CDN).
547
+ #
548
+ # Alternatively, it accepts a block that allows to specify one or more
549
+ # sources via the `source` tag.
550
+ #
551
+ # If the "fingerprint mode" is on, `src` is the fingerprinted
552
+ # version of the relative URL.
553
+ #
554
+ # If the "CDN mode" is on, the `src` is an absolute URL of the
555
+ # application CDN.
556
+ #
557
+ # @param source [String] asset name or absolute URL
558
+ # @param options [Hash] HTML 5 attributes
559
+ #
560
+ # @return [Hanami::View::HTML::SafeString] the markup
561
+ #
562
+ # @raise [Hanami::Assets::MissingManifestAssetError] if `fingerprint` or
563
+ # `subresource_integrity` modes are on and the audio file is missing
564
+ # from the manifest
565
+ #
566
+ # @raise [ArgumentError] if source isn"t specified both as argument or
567
+ # tag inside the given block
568
+ #
569
+ # @since 0.1.0
570
+ #
571
+ # @see Hanami::Assets::Helpers#path
572
+ #
573
+ # @example Basic Usage
574
+ #
575
+ # <%= audio "song.ogg" %>
576
+ #
577
+ # # <audio src="/assets/song.ogg"></audio>
578
+ #
579
+ # @example Absolute URL
580
+ #
581
+ # <%= audio "https://example-cdn.com/assets/song.ogg" %>
582
+ #
583
+ # # <audio src="https://example-cdn.com/assets/song.ogg"></audio>
584
+ #
585
+ # @example Custom HTML Attributes
586
+ #
587
+ # <%= audio("song.ogg", autoplay: true, controls: true) %>
588
+ #
589
+ # # <audio src="/assets/song.ogg" autoplay="autoplay" controls="controls"></audio>
590
+ #
591
+ # @example Fallback Content
592
+ #
593
+ # <%=
594
+ # audio("song.ogg") do
595
+ # "Your browser does not support the audio tag"
596
+ # end
597
+ # %>
598
+ #
599
+ # # <audio src="/assets/song.ogg">
600
+ # # Your browser does not support the audio tag
601
+ # # </audio>
602
+ #
603
+ # @example Tracks
604
+ #
605
+ # <%=
606
+ # audio("song.ogg") do
607
+ # tag.track(kind: "captions", src: asset_url("song.pt-BR.vtt"),
608
+ # srclang: "pt-BR", label: "Portuguese")
609
+ # end
610
+ # %>
611
+ #
612
+ # # <audio src="/assets/song.ogg">
613
+ # # <track kind="captions" src="/assets/song.pt-BR.vtt" srclang="pt-BR" label="Portuguese">
614
+ # # </audio>
615
+ #
616
+ # @example Without Any Argument
617
+ #
618
+ # <%= audio %>
619
+ #
620
+ # # ArgumentError
621
+ #
622
+ # @example Without src And Without Block
623
+ #
624
+ # <%= audio(controls: true) %>
625
+ #
626
+ # # ArgumentError
627
+ #
628
+ # @example Fingerprint Mode
629
+ #
630
+ # <%= audio "song.ogg" %>
631
+ #
632
+ # # <audio src="/assets/song-28a6b886de2372ee3922fcaf3f78f2d8.ogg"></audio>
633
+ #
634
+ # @example CDN Mode
635
+ #
636
+ # <%= audio "song.ogg" %>
637
+ #
638
+ # # <audio src="https://assets.bookshelf.org/assets/song-28a6b886de2372ee3922fcaf3f78f2d8.ogg"></audio>
639
+ def audio(source = nil, options = {}, &blk)
640
+ options = _source_options(source, options, &blk)
641
+ tag.audio(**options, &blk)
642
+ end
643
+
644
+ # @api public
645
+ # @since 2.1.0
646
+ alias_method :audio_tag, :audio
647
+
648
+ # It generates the relative or absolute URL for the given asset.
649
+ # It automatically decides if it has to use the relative or absolute
650
+ # depending on the configuration and current environment.
651
+ #
652
+ # Absolute URLs are returned as they are.
653
+ #
654
+ # It can be the name of the asset, coming from the sources or third party
655
+ # gems.
656
+ #
657
+ # If Fingerprint mode is on, it returns the fingerprinted path of the source
658
+ #
659
+ # If CDN mode is on, it returns the absolute URL of the asset.
660
+ #
661
+ # @param source [String] the asset name
662
+ #
663
+ # @return [String] the asset path
664
+ #
665
+ # @raise [Hanami::Assets::MissingManifestAssetError] if `fingerprint` or
666
+ # `subresource_integrity` modes are on and the asset is missing
667
+ # from the manifest
668
+ #
669
+ # @since 0.1.0
670
+ #
671
+ # @example Basic Usage
672
+ #
673
+ # <%= asset_url "application.js" %>
674
+ #
675
+ # # "/assets/application.js"
676
+ #
677
+ # @example Alias
678
+ #
679
+ # <%= asset_url "application.js" %>
680
+ #
681
+ # # "/assets/application.js"
682
+ #
683
+ # @example Absolute URL
684
+ #
685
+ # <%= asset_url "https://code.jquery.com/jquery-2.1.4.min.js" %>
686
+ #
687
+ # # "https://code.jquery.com/jquery-2.1.4.min.js"
688
+ #
689
+ # @example Fingerprint Mode
690
+ #
691
+ # <%= asset_url "application.js" %>
692
+ #
693
+ # # "/assets/application-28a6b886de2372ee3922fcaf3f78f2d8.js"
694
+ #
695
+ # @example CDN Mode
696
+ #
697
+ # <%= asset_url "application.js" %>
698
+ #
699
+ # # "https://assets.bookshelf.org/assets/application-28a6b886de2372ee3922fcaf3f78f2d8.js"
700
+ def asset_url(source_path)
701
+ return source_path if _absolute_url?(source_path)
702
+
703
+ _context.assets[source_path].url
704
+ end
705
+
706
+ private
707
+
708
+ # @since 0.1.0
709
+ # @api private
710
+ def _safe_tags(*source_paths, &blk)
711
+ ::Hanami::View::HTML::SafeString.new(
712
+ source_paths.map(&blk).join(NEW_LINE_SEPARATOR)
713
+ )
714
+ end
715
+
716
+ # @since 2.1.0
717
+ # @api private
718
+ def _typed_path(source, ext)
719
+ source = "#{source}#{ext}" if _append_extension?(source, ext)
720
+ asset_url(source)
721
+ end
722
+
723
+ # @api private
724
+ def _subresource_integrity_value(source_path, ext)
725
+ return if _absolute_url?(source_path)
726
+
727
+ source_path = "#{source_path}#{ext}" unless /#{Regexp.escape(ext)}\z/.match?(source_path)
728
+ _context.assets[source_path].sri
729
+ end
730
+
731
+ # @since 0.1.0
732
+ # @api private
733
+ def _absolute_url?(source)
734
+ ABSOLUTE_URL_MATCHER.match(source)
735
+ end
736
+
737
+ # @since 1.2.0
738
+ # @api private
739
+ def _crossorigin?(source)
740
+ return false unless _absolute_url?(source)
741
+
742
+ _context.assets.crossorigin?(source)
743
+ end
744
+
745
+ # @since 0.1.0
746
+ # @api private
747
+ def _source_options(src, options, &blk)
748
+ options ||= {}
749
+
750
+ if src.respond_to?(:to_hash)
751
+ options = src.to_hash
752
+ elsif src
753
+ options[:src] = asset_url(src)
754
+ end
755
+
756
+ if !options[:src] && !blk
757
+ raise ArgumentError.new("You should provide a source via `src` option or with a `source` HTML tag")
758
+ end
759
+
760
+ options
761
+ end
762
+
763
+ # @since 1.1.0
764
+ # @api private
765
+ def _append_extension?(source, ext)
766
+ source !~ QUERY_STRING_MATCHER && source !~ /#{Regexp.escape(ext)}\z/
767
+ end
768
+ end
769
+ end
770
+ end
771
+
772
+ # rubocop:enable Metrics/ModuleLength