lib/hanami/assets/helpers.rb in hanami-assets-1.1.1 vs lib/hanami/assets/helpers.rb in hanami-assets-1.2.0.beta1
- old
+ new
@@ -1,7 +1,6 @@
require 'uri'
-require 'set'
require 'hanami/helpers/html_helper'
require 'hanami/utils/escape'
module Hanami
module Assets
@@ -95,11 +94,15 @@
#
# If the "subresource integrity mode" is on, <tt>integriy</tt> is the
# name of the algorithm, then a hyphen, then the hash value of the file.
# If more than one algorithm is used, they'll be separated by a space.
#
+ # It makes the script(s) eligible for HTTP/2 Push Promise/Early Hints.
+ # You can opt-out with inline option: `push: false`.
+ #
# @param sources [Array<String>] one or more assets by name or absolute URL
+ # @param push [TrueClass, FalseClass] HTTP/2 Push Promise/Early Hints flag
#
# @return [Hanami::Utils::Escape::SafeString] the markup
#
# @raise [Hanami::Assets::MissingManifestAssetError] if `fingerprint` or
# `subresource_integrity` modes are on and the javascript file is missing
@@ -163,16 +166,21 @@
# @example CDN Mode
#
# <%= javascript 'application' %>
#
# # <script src="https://assets.bookshelf.org/assets/application-28a6b886de2372ee3922fcaf3f78f2d8.js" type="text/javascript"></script>
- def javascript(*sources, **options) # rubocop:disable Metrics/MethodLength
+ #
+ # @example Disable Push Promise/Early Hints
+ #
+ # <%= javascript 'application', push: false %>
+ # <%= javascript 'http://cdn.example.test/jquery.js', 'dashboard', push: false %>
+ def javascript(*sources, push: true, **options) # rubocop:disable Metrics/MethodLength
options = options.reject { |k, _| k.to_sym == :src }
_safe_tags(*sources) do |source|
attributes = {
- src: _typed_asset_path(source, JAVASCRIPT_EXT),
+ src: _typed_asset_path(source, JAVASCRIPT_EXT, push: push, as: :script),
type: JAVASCRIPT_MIME_TYPE
}
attributes.merge!(options)
if _subresource_integrity? || attributes.include?(:integrity)
@@ -197,11 +205,16 @@
# application CDN.
#
# If the "subresource integrity mode" is on, <tt>integriy</tt> is the
# name of the algorithm, then a hyphen, then the hashed value of the file.
# If more than one algorithm is used, they'll be separated by a space.
+ #
+ # It makes the script(s) eligible for HTTP/2 Push Promise/Early Hints.
+ # You can opt-out with inline option: `push: false`.
+ #
# @param sources [Array<String>] one or more assets by name or absolute URL
+ # @param push [TrueClass, FalseClass] HTTP/2 Push Promise/Early Hints flag
#
# @return [Hanami::Utils::Escape::SafeString] the markup
#
# @raise [Hanami::Assets::MissingManifestAssetError] if `fingerprint` or
# `subresource_integrity` modes are on and the stylesheet file is missing
@@ -254,16 +267,21 @@
# @example CDN Mode
#
# <%= stylesheet 'application' %>
#
# # <link href="https://assets.bookshelf.org/assets/application-28a6b886de2372ee3922fcaf3f78f2d8.css" type="text/css" rel="stylesheet">
- def stylesheet(*sources, **options) # rubocop:disable Metrics/MethodLength
+ #
+ # @example Disable Push Promise/Early Hints
+ #
+ # <%= stylesheet 'application', push: false %>
+ # <%= stylesheet 'http://cdn.example.test/bootstrap.css', 'dashboard', push: false %>
+ def stylesheet(*sources, push: true, **options) # rubocop:disable Metrics/MethodLength
options = options.reject { |k, _| k.to_sym == :href }
_safe_tags(*sources) do |source|
attributes = {
- href: _typed_asset_path(source, STYLESHEET_EXT),
+ href: _typed_asset_path(source, STYLESHEET_EXT, push: push, as: :style),
type: STYLESHEET_MIME_TYPE,
rel: STYLESHEET_REL
}
attributes.merge!(options)
@@ -290,10 +308,12 @@
#
# If the "CDN mode" is on, the <tt>src</tt> is an absolute URL of the
# application CDN.
#
# @param source [String] asset name or absolute URL
+ # @param options [Hash] HTML 5 attributes
+ # @option options [TrueClass, FalseClass] :push HTTP/2 Push Promise/Early Hints flag
#
# @return [Hanami::Utils::Helpers::HtmlBuilder] the builder
#
# @raise [Hanami::Assets::MissingManifestAssetError] if `fingerprint` or
# `subresource_integrity` modes are on and the image file is missing
@@ -339,15 +359,18 @@
# @example CDN Mode
#
# <%= image 'logo.png' %>
#
# # <img src="https://assets.bookshelf.org/assets/logo-28a6b886de2372ee3922fcaf3f78f2d8.png" alt="Logo">
+ #
+ # @example Enable Push Promise/Early Hints
+ #
+ # <%= image 'logo.png', push: true %>
def image(source, options = {})
options = options.reject { |k, _| k.to_sym == :src }
-
attributes = {
- src: asset_path(source),
+ src: asset_path(source, push: options.delete(:push) || false, as: :image),
alt: Utils::String.titleize(::File.basename(source, WILDCARD_EXT))
}
attributes.merge!(options)
html.img(attributes)
@@ -364,10 +387,12 @@
#
# If the "CDN mode" is on, the <tt>href</tt> is an absolute URL of the
# application CDN.
#
# @param source [String] asset name
+ # @param options [Hash] HTML 5 attributes
+ # @option options [TrueClass, FalseClass] :push HTTP/2 Push Promise/Early Hints flag
#
# @return [Hanami::Utils::Helpers::HtmlBuilder] the builder
#
# @raise [Hanami::Assets::MissingManifestAssetError] if `fingerprint` or
# `subresource_integrity` modes are on and the favicon is file missing
@@ -391,11 +416,11 @@
#
# # <link href="/assets/fav.ico" rel="shortcut icon" type="image/x-icon">
#
# @example Custom HTML Attributes
#
- # <%= favicon id: 'fav' %>
+ # <%= favicon "favicon.ico", id: "fav" %>
#
# # <link id: "fav" href="/assets/favicon.ico" rel="shortcut icon" type="image/x-icon">
#
# @example Fingerprint Mode
#
@@ -406,15 +431,19 @@
# @example CDN Mode
#
# <%= favicon %>
#
# # <link href="https://assets.bookshelf.org/assets/favicon-28a6b886de2372ee3922fcaf3f78f2d8.ico" rel="shortcut icon" type="image/x-icon">
+ #
+ # @example Enable Push Promise/Early Hints
+ #
+ # <%= favicon 'favicon.ico', push: true %>
def favicon(source = DEFAULT_FAVICON, options = {})
options = options.reject { |k, _| k.to_sym == :href }
attributes = {
- href: asset_path(source),
+ href: asset_path(source, push: options.delete(:push) || false, as: :image),
rel: FAVICON_REL,
type: FAVICON_MIME_TYPE
}
attributes.merge!(options)
@@ -435,10 +464,12 @@
#
# If the "CDN mode" is on, the <tt>src</tt> is an absolute URL of the
# application CDN.
#
# @param source [String] asset name or absolute URL
+ # @param options [Hash] HTML 5 attributes
+ # @option options [TrueClass, FalseClass] :push HTTP/2 Push Promise/Early Hints flag
#
# @return [Hanami::Utils::Helpers::HtmlBuilder] the builder
#
# @raise [Hanami::Assets::MissingManifestAssetError] if `fingerprint` or
# `subresource_integrity` modes are on and the video file is missing
@@ -533,12 +564,24 @@
# @example CDN Mode
#
# <%= video 'movie.mp4' %>
#
# # <video src="https://assets.bookshelf.org/assets/movie-28a6b886de2372ee3922fcaf3f78f2d8.mp4"></video>
+ #
+ # @example Enable Push Promise/Early Hints
+ #
+ # <%= video 'movie.mp4', push: true %>
+ #
+ # <%=
+ # video do
+ # text "Your browser does not support the video tag"
+ # source src: asset_path("movie.mp4", push: :video), type: "video/mp4"
+ # source src: asset_path("movie.ogg"), type: "video/ogg"
+ # end
+ # %>
def video(source = nil, options = {}, &blk)
- options = _source_options(source, options, &blk)
+ options = _source_options(source, options, as: :video, &blk)
html.video(blk, options)
end
# Generate <tt>audio</tt> tag for given source
#
@@ -554,10 +597,12 @@
#
# If the "CDN mode" is on, the <tt>src</tt> is an absolute URL of the
# application CDN.
#
# @param source [String] asset name or absolute URL
+ # @param options [Hash] HTML 5 attributes
+ # @option options [TrueClass, FalseClass] :push HTTP/2 Push Promise/Early Hints flag
#
# @return [Hanami::Utils::Helpers::HtmlBuilder] the builder
#
# @raise [Hanami::Assets::MissingManifestAssetError] if `fingerprint` or
# `subresource_integrity` modes are on and the audio file is missing
@@ -652,12 +697,24 @@
# @example CDN Mode
#
# <%= audio 'song.ogg' %>
#
# # <audio src="https://assets.bookshelf.org/assets/song-28a6b886de2372ee3922fcaf3f78f2d8.ogg"></audio>
+ #
+ # @example Enable Push Promise/Early Hints
+ #
+ # <%= audio 'movie.mp4', push: true %>
+ #
+ # <%=
+ # audio do
+ # text "Your browser does not support the audio tag"
+ # source src: asset_path("song.ogg", push: :audio), type: "audio/ogg"
+ # source src: asset_path("song.wav"), type: "audio/wav"
+ # end
+ # %>
def audio(source = nil, options = {}, &blk)
- options = _source_options(source, options, &blk)
+ options = _source_options(source, options, as: :audio, &blk)
html.audio(blk, options)
end
# It generates the relative URL for the given source.
#
@@ -669,10 +726,12 @@
# If Fingerprint mode is on, it returns the fingerprinted path of the source
#
# If CDN mode is on, it returns the absolute URL of the asset.
#
# @param source [String] the asset name
+ # @param push [TrueClass, FalseClass, Symbol] HTTP/2 Push Promise/Early Hints flag, or type
+ # @param as [Symbol] HTTP/2 Push Promise / Early Hints flag type
#
# @return [String] the asset path
#
# @raise [Hanami::Assets::MissingManifestAssetError] if `fingerprint` or
# `subresource_integrity` modes are on and the asset is missing
@@ -701,12 +760,16 @@
# @example CDN Mode
#
# <%= asset_path 'application.js' %>
#
# # "https://assets.bookshelf.org/assets/application-28a6b886de2372ee3922fcaf3f78f2d8.js"
- def asset_path(source)
- _asset_url(source) { _relative_url(source) }
+ #
+ # @example Enable Push Promise/Early Hints
+ #
+ # <%= asset_path "application.js", push: :script %>
+ def asset_path(source, push: false, as: nil)
+ _asset_url(source, push: push, as: as) { _relative_url(source) }
end
# It generates the absolute URL for the given source.
#
# It can be the name of the asset, coming from the sources or third party
@@ -717,10 +780,12 @@
# If Fingerprint mode is on, it returns the fingerprint URL of the source
#
# If CDN mode is on, it returns the absolute URL of the asset.
#
# @param source [String] the asset name
+ # @param push [TrueClass, FalseClass, Symbol] HTTP/2 Push Promise/Early Hints flag, or type
+ # @param as [Symbol] HTTP/2 Push Promise / Early Hints flag type
#
# @return [String] the asset URL
#
# @raise [Hanami::Assets::MissingManifestAssetError] if `fingerprint` or
# `subresource_integrity` modes are on and the asset is missing
@@ -749,12 +814,16 @@
# @example CDN Mode
#
# <%= asset_url 'application.js' %>
#
# # "https://assets.bookshelf.org/assets/application-28a6b886de2372ee3922fcaf3f78f2d8.js"
- def asset_url(source)
- _asset_url(source) { _absolute_url(source) }
+ #
+ # @example Enable Push Promise/Early Hints
+ #
+ # <%= asset_url "application.js", push: :script %>
+ def asset_url(source, push: false, as: nil)
+ _asset_url(source, push: push, as: as) { _absolute_url(source) }
end
private
# @since 0.1.0
@@ -767,22 +836,28 @@
)
end
# @since 0.1.0
# @api private
- def _asset_url(source)
- _push_promise(
- _absolute_url?(source) ? # rubocop:disable Style/MultilineTernaryOperator
- source : yield
- )
+ def _asset_url(source, push:, as:)
+ url = _absolute_url?(source) ? source : yield
+
+ case push
+ when Symbol
+ _push_promise(url, as: push)
+ when TrueClass
+ _push_promise(url, as: as)
+ end
+
+ url
end
# @since 0.1.0
# @api private
- def _typed_asset_path(source, ext)
+ def _typed_asset_path(source, ext, push: false, as: nil)
source = "#{source}#{ext}" if _append_extension?(source, ext)
- asset_path(source)
+ asset_path(source, push: push, as: as)
end
# @api private
def _subresource_integrity?
!!self.class.assets_configuration.subresource_integrity # rubocop:disable Style/DoubleNegation
@@ -798,10 +873,17 @@
# @api private
def _absolute_url?(source)
ABSOLUTE_URL_MATCHER.match(source)
end
+ # @since 1.2.0
+ # @api private
+ def _crossorigin?(source)
+ return false unless _absolute_url?(source)
+ self.class.assets_configuration.crossorigin?(source)
+ end
+
# @since 0.1.0
# @api private
def _relative_url(source)
self.class.assets_configuration.asset_path(source)
end
@@ -812,17 +894,17 @@
self.class.assets_configuration.asset_url(source)
end
# @since 0.1.0
# @api private
- def _source_options(src, options, &_blk)
+ def _source_options(src, options, as:, &_blk)
options ||= {}
if src.respond_to?(:to_hash)
options = src.to_hash
elsif src
- options[:src] = asset_path(src)
+ options[:src] = asset_path(src, push: options.delete(:push) || false, as: as)
end
if !options[:src] && !block_given?
raise ArgumentError.new('You should provide a source via `src` option or with a `source` HTML tag')
end
@@ -830,14 +912,12 @@
options
end
# @since 0.1.0
# @api private
- def _push_promise(url)
- Mutex.new.synchronize do
- Thread.current[:__hanami_assets] ||= Set.new
- Thread.current[:__hanami_assets].add(url.to_s)
- end
+ def _push_promise(url, as: nil)
+ Thread.current[:__hanami_assets] ||= {}
+ Thread.current[:__hanami_assets][url.to_s] = { as: as, crossorigin: _crossorigin?(url) }
url
end
# @since 1.1.0