class Darkroom::Asset
Represents an asset.
Constants
- CSSDelegate
Delegate
for CSS assets.- DEFAULT_QUOTE
- Delegate
Holds information about how to handle a particular asset type.
-
content_type
- HTTP MIME type string. -
import_regex
- Regex to find import statements. Must contain a named component called 'path' (e.g. +/^import (?<path>.*)/+). -
reference_regex
- Regex to find references to other assets. Must contain three named components:-
path
- Path of the asset being referenced. -
entity
- Desired entity (path or content). -
format
- Format to use (seeREFERENCE_FORMATS
).
-
-
validate_reference
- Lambda to call to validate a reference. Should return nil if there are no errors and a string error message if validation fails. Three arguments are passed when called:-
asset
-Asset
object of the asset being referenced. -
match
- MatchData object from the match againstreference_regex
. -
format
- Format of the reference (seeREFERENCE_FORMATS
).
-
-
reference_content
- Lambda to call to get the content for a reference. Should return nil if the default behavior is desired or a string for custom content. Three arguments are passed when called:-
asset
-Asset
object of the asset being referenced. -
match
- MatchData object from the match againstreference_regex
. -
format
- Format of the reference (seeREFERENCE_FORMATS
).
-
-
compile_lib
- Name of a library torequire
that is needed by thecompile
lambda. -
compile
- Lambda to call that will return the compiled version of the asset's content. Two arguments are passed when called:-
path
- Path of the asset being compiled. -
content
- Content to compile.
-
-
minify_lib
- Name of a library torequire
that is needed by theminify
lambda. -
minify
- Lambda to call that will return the minified version of the asset's content. One argument is passed when called:-
content
- Content to minify.
-
-
- EXTENSION_REGEX
- HTMLDelegate
- HTXDelegate
- IMPORT_JOINER
- JavaScriptDelegate
- QUOTED_PATH
- REFERENCE_FORMATS
First item of each set is used as default, so order is important.
- REFERENCE_PATH
Attributes
Public Class Methods
DEPRECATED. Use Darkroom.register
instead.
Defines an asset spec.
-
extensions
- File extensions to associate with this spec. -
content_type
- HTTP MIME type string. -
other
- Optional components of the spec (see Spec struct).
# File lib/darkroom/asset.rb, line 78 def self.add_spec(*extensions, content_type, **other) warn("#{self}.add_spec is deprecated and will be removed soon (use Darkroom.register instead)") params = other.dup params[:content_type] = content_type params[:import_regex] = params.delete(:dependency_regex) if params.key?(:dependency_regex) Darkroom.register(*extensions, params) end
Creates a new instance.
-
file
- Path of file on disk. -
path
- Path this asset will be referenced by (e.g. /js/app.js). -
darkroom
-Darkroom
instance that the asset is a member of. -
prefix
- Prefix to apply to unversioned and versioned paths. -
minify
- Boolean specifying whether or not the asset should be minified when processed. -
internal
- Boolean indicating whether or not the asset is only accessible internally (i.e. as an import or reference).
# File lib/darkroom/asset.rb, line 112 def initialize(path, file, darkroom, prefix: nil, minify: false, internal: false) @path = path @file = file @darkroom = darkroom @prefix = prefix @minify = minify @internal = internal @path_unversioned = "#{@prefix}#{@path}" @extension = File.extname(@path).downcase @delegate = Darkroom.delegate(@extension) or raise(UnrecognizedExtensionError.new(@path)) require_libs clear end
DEPRECATED. Use Darkroom.delegate
instead.
Returns the spec associated with a file extension.
-
extension
- File extension of the desired spec.
# File lib/darkroom/asset.rb, line 95 def self.spec(extension) warn("#{self}.spec is deprecated and will be removed soon (use Darkroom.delegate instead)") Darkroom.delegate(extension) end
Public Instance Methods
Returns boolean indicating whether or not the asset is binary.
# File lib/darkroom/asset.rb, line 166 def binary? return @is_binary if defined?(@is_binary) type, subtype = content_type.split('/') @is_binary = type != 'text' && !subtype.include?('json') && !subtype.include?('xml') end
Returns the HTTP MIME type string.
# File lib/darkroom/asset.rb, line 159 def content_type @delegate.content_type end
Returns boolean indicating whether or not an error was encountered the last time the asset was processed.
# File lib/darkroom/asset.rb, line 229 def error? !!@error end
Returns boolean indicating whether or not the asset is a font.
# File lib/darkroom/asset.rb, line 177 def font? defined?(@is_font) ? @is_font : (@is_font = content_type.start_with?('font/')) end
Returns appropriate HTTP headers.
-
versioned
- Uses Cache-Control header with max-age iftrue
and ETag header iffalse
.
# File lib/darkroom/asset.rb, line 193 def headers(versioned: true) { 'Content-Type' => content_type, 'Cache-Control' => ('public, max-age=31536000' if versioned), 'ETag' => ("\"#{@fingerprint}\"" if !versioned), }.compact! end
Returns boolean indicating whether or not the asset is an image.
# File lib/darkroom/asset.rb, line 184 def image? defined?(@is_image) ? @is_image : (@is_image = content_type.start_with?('image/')) end
Returns high-level object info string.
# File lib/darkroom/asset.rb, line 236 def inspect "#<#{self.class}: "\ "@errors=#{@errors.inspect}, "\ "@extension=#{@extension.inspect}, "\ "@file=#{@file.inspect}, "\ "@fingerprint=#{@fingerprint.inspect}, "\ "@internal=#{@internal.inspect}, "\ "@minify=#{@minify.inspect}, "\ "@mtime=#{@mtime.inspect}, "\ "@path=#{@path.inspect}, "\ "@path_unversioned=#{@path_unversioned.inspect}, "\ "@path_versioned=#{@path_versioned.inspect}, "\ "@prefix=#{@prefix.inspect}"\ '>' end
Returns subresource integrity string.
-
algorithm
- Hash algorithm to use to generate the integrity string (one of :sha256, :sha384, or :sha512).
# File lib/darkroom/asset.rb, line 207 def integrity(algorithm = :sha384) @integrity[algorithm] ||= "#{algorithm}-#{Base64.strict_encode64( case algorithm when :sha256 then Digest::SHA256.digest(@content) when :sha384 then Digest::SHA384.digest(@content) when :sha512 then Digest::SHA512.digest(@content) else raise("Unrecognized integrity algorithm: #{algorithm}") end )}".freeze end
Returns boolean indicating whether or not the asset is marked as internal.
# File lib/darkroom/asset.rb, line 221 def internal? @internal end
Processes the asset if modified (see modified?
for how modification is determined). File is read from disk, references are substituted (if supported), content is compiled (if required), imports are prefixed to its content (if supported), and content is minified (if supported and enabled). Returns true if asset was modified since it was last processed and false otherwise.
# File lib/darkroom/asset.rb, line 134 def process @process_key == @darkroom.process_key ? (return @processed) : (@process_key = @darkroom.process_key) modified? ? (@processed = true) : (return @processed = false) clear read build_imports build_references process_dependencies compile minify @fingerprint = Digest::MD5.hexdigest(@content) @path_versioned = "#{@prefix}#{@path.sub(EXTENSION_REGEX, "-#{@fingerprint}")}" @processed rescue Errno::ENOENT # File was deleted. Do nothing. ensure @error = @errors.empty? ? nil : ProcessingError.new(@errors) end
Protected Instance Methods
Returns all dependencies (including dependencies of dependencies).
-
ignore
- Assets already accounted for as dependency tree is walked (to prevent infinite loops when circular chains are encountered).
# File lib/darkroom/asset.rb, line 276 def dependencies(ignore = Set.new) @dependencies ||= accumulate(:dependencies, ignore) end
Returns all imports (including imports of imports).
-
ignore
- Assets already accounted for as import tree is walked (to prevent infinite loops when circular chains are encountered).
# File lib/darkroom/asset.rb, line 286 def imports(ignore = Set.new) @imports ||= accumulate(:imports, ignore) end
Returns true if the asset or any of its dependencies were modified since last processed, or if an error was recorded during the last processing run.
# File lib/darkroom/asset.rb, line 258 def modified? @modified_key == @darkroom.process_key ? (return @modified) : (@modified_key = @darkroom.process_key) begin @modified = !!@error @modified ||= @mtime != (@mtime = File.mtime(@file)) @modified ||= dependencies.any? { |d| d.modified? } rescue Errno::ENOENT @modified = true end end
Returns the processed content of the asset without dependencies concatenated.
# File lib/darkroom/asset.rb, line 293 def own_content @own_content end
Private Instance Methods
Utility method used by dependencies
and imports
to recursively build arrays.
-
name
- Name of the array to accumulate (:dependencies or :imports). -
ignore
- Set of assets already accumulated which can be ignored (used to avoid infinite loops when circular references are encountered).
# File lib/darkroom/asset.rb, line 479 def accumulate(name, ignore) ignore << self process instance_variable_get(:"@own_#{name}").each_with_object([]) do |asset, assets| next if ignore.include?(asset) assets.push(*asset.send(name, ignore), asset) assets.uniq! assets.delete(self) end end
Builds import info.
# File lib/darkroom/asset.rb, line 359 def build_imports return unless @delegate.import_regex @own_content.scan(@delegate.import_regex) do match = Regexp.last_match path = match[:path] if (asset = @darkroom.manifest(path)) @own_dependencies << asset @own_imports << asset @dependency_matches << [:import, asset, match] else @errors << not_found_error(path, match) end end end
Builds reference info.
# File lib/darkroom/asset.rb, line 328 def build_references return unless @delegate.reference_regex @own_content.scan(@delegate.reference_regex) do match = Regexp.last_match path = match[:path] format = match[:format] format = REFERENCE_FORMATS[match[:entity]].first if format.nil? || format == '' if (asset = @darkroom.manifest(path)) if !REFERENCE_FORMATS[match[:entity]].include?(format) @errors << AssetError.new("Invalid reference format '#{format}' (must be one of "\ "'#{REFERENCE_FORMATS[match[:entity]].join("', '")}')", match[0], @path, line_num(match)) elsif match[:entity] == 'content' && format != 'base64' && asset.binary? @errors << AssetError.new('Base64 encoding is required for binary assets', match[0], @path, line_num(match)) elsif (error = @delegate.validate_reference&.(asset, match, format)) @errors << AssetError.new(error, match[0], @path, line_num(match)) else @own_dependencies << asset @dependency_matches << [:reference, asset, match, format] end else @errors << not_found_error(path, match) end end end
Clears content, dependencies, and errors so asset is ready for (re)processing.
# File lib/darkroom/asset.rb, line 302 def clear @dependencies = nil @imports = nil @error = nil @fingerprint = nil @path_versioned = nil (@own_dependencies ||= []).clear (@own_imports ||= []).clear (@dependency_matches ||= []).clear (@errors ||= []).clear (@content ||= +'').clear (@own_content ||= +'').clear (@integrity ||= {}).clear end
Compiles the asset if compilation is supported for the asset's type and appends the asset's own content to the overall content string.
# File lib/darkroom/asset.rb, line 418 def compile if @delegate.compile begin @own_content = @delegate.compile.(@path, @own_content) rescue => e @errors << e end end @content << @own_content end
Utility method that returns the line number where a regex match was found.
-
match
- MatchData object of the regex.
# File lib/darkroom/asset.rb, line 508 def line_num(match) @own_content[0..match.begin(:path)].count("\n") + 1 end
Minifies the asset if minification is supported for the asset's type, asset is marked as minifiable (i.e. it's not already minified), and the asset is not marked as internal-only.
# File lib/darkroom/asset.rb, line 434 def minify if @delegate.minify && @minify && !@internal begin @content = @delegate.minify.(@content) rescue => e @errors << e end end @content end
Utility method that returns the appropriate error for a dependency that doesn't exist.
-
path
- Path of the asset which cannot be found. -
match
- MatchData object of the regex for the asset that cannot be found.
# File lib/darkroom/asset.rb, line 498 def not_found_error(path, match) klass = Darkroom.delegate(File.extname(path)) ? AssetNotFoundError : UnrecognizedExtensionError klass.new(path, @path, line_num(match)) end
Processes imports and references.
# File lib/darkroom/asset.rb, line 379 def process_dependencies @dependency_matches.sort_by! { |_, __, match| -match.begin(0) }.each do |kind, asset, match, format| if kind == :import @own_content[match.begin(0)...match.end(0)] = '' elsif asset.dependencies.include?(self) @errors << CircularReferenceError.new(match[0], @path, line_num(match)) else value, start, finish = @delegate.reference_content&.(asset, match, format) min_start, max_finish = match.offset(0) start ||= format == 'displace' ? min_start : match.begin(:quoted) finish ||= format == 'displace' ? max_finish : match.end(:quoted) start = [[start, min_start].max, max_finish].min finish = [[finish, max_finish].min, min_start].max @own_content[start...finish] = case "#{match[:entity]}-#{format}" when 'path-versioned' value || asset.path_versioned when 'path-unversioned' value || asset.path_unversioned when 'content-base64' quote = DEFAULT_QUOTE if match[:quote] == '' "#{quote}data:#{asset.content_type};base64,#{Base64.strict_encode64(value || asset.content)}#{quote}" when 'content-utf8' quote = DEFAULT_QUOTE if match[:quote] == '' "#{quote}data:#{asset.content_type};utf8,#{value || asset.content}#{quote}" when 'content-displace' value || asset.content end end end @content << imports.map { |d| d.own_content }.join(IMPORT_JOINER) end
Reads the asset file into memory.
# File lib/darkroom/asset.rb, line 321 def read @own_content = File.read(@file) end
Requires any libraries necessary for compiling and minifying the asset based on its type. Raises a MissingLibraryError
if library cannot be loaded.
Darkroom
does not explicitly depend on any libraries necessary for asset compilation or minification, since not every app will use every kind of asset or use minification. It is instead up to each app using Darkroom
to specify any needed compilation and minification libraries as direct dependencies (e.g. specify +gem('uglifier')+ in the app's Gemfile if JavaScript minification is desired).
# File lib/darkroom/asset.rb, line 455 def require_libs begin require(@delegate.compile_lib) if @delegate.compile_lib rescue LoadError compile_load_error = true end begin require(@delegate.minify_lib) if @delegate.minify_lib && @minify rescue LoadError minify_load_error = true end raise(MissingLibraryError.new(@delegate.compile_lib, 'compile', @extension)) if compile_load_error raise(MissingLibraryError.new(@delegate.minify_lib, 'minify', @extension)) if minify_load_error end