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