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 (see REFERENCE_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 against reference_regex.

    • format - Format of the reference (see REFERENCE_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 against reference_regex.

    • format - Format of the reference (see REFERENCE_FORMATS).

  • compile_lib - Name of a library to require that is needed by the compile 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 to require that is needed by the minify 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

content[R]
error[R]
errors[R]
path[R]
path_unversioned[R]
path_versioned[R]

Public Class Methods

add_spec(*extensions, content_type, **other) click to toggle source

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
new(path, file, darkroom, prefix: nil, minify: false, internal: false) click to toggle source

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
spec(extension) click to toggle source

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

binary?() click to toggle source

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
content_type() click to toggle source

Returns the HTTP MIME type string.

# File lib/darkroom/asset.rb, line 159
def content_type
  @delegate.content_type
end
error?() click to toggle source

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
font?() click to toggle source

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
headers(versioned: true) click to toggle source

Returns appropriate HTTP headers.

  • versioned - Uses Cache-Control header with max-age if true and ETag header if false.

# 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
image?() click to toggle source

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
inspect() click to toggle source

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
integrity(algorithm = :sha384) click to toggle source

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
internal?() click to toggle source

Returns boolean indicating whether or not the asset is marked as internal.

# File lib/darkroom/asset.rb, line 221
def internal?
  @internal
end
process() click to toggle source

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

dependencies(ignore = Set.new) click to toggle source

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
imports(ignore = Set.new) click to toggle source

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
modified?() click to toggle source

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
own_content() click to toggle source

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

accumulate(name, ignore) click to toggle source

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
build_imports() click to toggle source

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
build_references() click to toggle source

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
clear() click to toggle source

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
compile() click to toggle source

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
line_num(match) click to toggle source

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
minify() click to toggle source

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
not_found_error(path, match) click to toggle source

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
process_dependencies() click to toggle source

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
read() click to toggle source

Reads the asset file into memory.

# File lib/darkroom/asset.rb, line 321
def read
  @own_content = File.read(@file)
end
require_libs() click to toggle source

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