lib/jekyll_asset_pipeline/pipeline.rb
# frozen_string_literal: true
module JekyllAssetPipeline
# The pipeline itself, the run method is where it all happens
# rubocop:disable Metrics/ClassLength
class Pipeline
# rubocop:enable Metrics/ClassLength
class << self
# Generate hash based on manifest
def hash(source, manifest, options = {})
options = DEFAULTS.merge(options)
begin
Digest::MD5.hexdigest(YAML.safe_load(manifest).map! do |path|
"#{path}#{File.mtime(File.join(source, path)).to_i}"
end.join.concat(options.to_s))
rescue StandardError => e
puts "Failed to generate hash from provided manifest: #{e.message}"
raise e
end
end
# Run the pipeline
# This is called from JekyllAssetPipeline::LiquidBlockExtensions.render
# or, to be more precise, from JekyllAssetPipeline::CssAssetTag.render and
# JekyllAssetPipeline::JavaScriptAssetTag.render
# rubocop:disable Metrics/ParameterLists
def run(manifest, prefix, source, destination, tag, type, config)
# rubocop:enable Metrics/ParameterLists
# Get hash for pipeline
hash = hash(source, manifest, config)
# Check if pipeline has been cached
return cache[hash], true if cache.key?(hash)
begin
puts "Processing '#{tag}' manifest '#{prefix}'"
pipeline = new(manifest, prefix, source, destination, type, config)
process_pipeline(hash, pipeline)
rescue StandardError => e
# Add exception to cache
cache[hash] = e
# Re-raise the exception
raise e
end
end
# Cache processed pipelines
def cache
@cache ||= {}
end
# Empty cache
def clear_cache
@cache = {}
end
# Remove staged assets
def remove_staged_assets(source, config)
config = DEFAULTS.merge(config)
staging_path = File.join(source, config['staging_path'])
FileUtils.rm_rf(staging_path)
end
# Add prefix to output
def puts(message)
$stdout.puts("Asset Pipeline: #{message}")
end
private
def process_pipeline(hash, pipeline)
pipeline.assets.each do |asset|
puts "Saved '#{asset.filename}' to " \
"'#{pipeline.destination}/#{asset.output_path}'"
end
# Add processed pipeline to cache
cache[hash] = pipeline
# Return newly processed pipeline and cached status
[pipeline, false]
end
end
# Initialize new pipeline
# rubocop:disable Metrics/ParameterLists
def initialize(manifest, prefix, source, destination, type, options = {})
# rubocop:enable Metrics/ParameterLists
@manifest = manifest
@prefix = prefix
@source = source
@destination = destination
@type = type
@options = ::JekyllAssetPipeline::DEFAULTS.merge(options)
process
end
attr_reader :assets, :html, :destination
private
# Process the pipeline
def process
collect
convert
bundle if @options['bundle']
compress if @options['compress']
gzip if @options['gzip']
save
markup
end
# Collect assets based on manifest
def collect
@assets = YAML.safe_load(@manifest).map! do |path|
full_path = File.join(@source, path)
File.open(File.join(@source, path)) do |file|
::JekyllAssetPipeline::Asset.new(file.read, File.basename(path),
File.dirname(full_path))
end
end
rescue StandardError => e
puts 'Asset Pipeline: Failed to load assets from provided ' \
"manifest: #{e.message}"
raise e
end
# Convert assets based on the file extension if converter is defined
def convert
@assets.each do |asset|
# Convert asset multiple times if more than one converter is found
finished = false
while finished == false
# Find a converter to use
klass = ::JekyllAssetPipeline::Converter.klass(asset.filename)
# Convert asset if converter is found
if klass.nil?
finished = true
else
convert_asset(klass, asset)
end
end
end
end
# Convert an asset with a given converter class
def convert_asset(klass, asset)
# Convert asset content
converter = klass.new(asset)
# Replace asset content and filename
asset.content = converter.converted
asset.filename = File.basename(asset.filename, '.*')
# Add back the output extension if no extension left
if File.extname(asset.filename) == ''
asset.filename = "#{asset.filename}#{@type}"
end
rescue StandardError => e
puts "Asset Pipeline: Failed to convert '#{asset.filename}' " \
"with '#{klass}': #{e.message}"
raise e
end
# Bundle multiple assets into a single asset
def bundle
content = @assets.map(&:content).join("\n")
hash = ::JekyllAssetPipeline::Pipeline.hash(@source, @manifest, @options)
@assets = [
::JekyllAssetPipeline::Asset.new(content, "#{@prefix}-#{hash}#{@type}")
]
end
# Compress assets if compressor is defined
def compress
@assets.each do |asset|
# Find a compressor to use
klass = ::JekyllAssetPipeline::Compressor.subclasses.select do |c|
c.filetype == @type
end.last
break unless klass
begin
asset.content = klass.new(asset.content).compressed
rescue StandardError => e
puts "Asset Pipeline: Failed to compress '#{asset.filename}' " \
"with '#{klass}': #{e.message}"
raise e
end
end
end
# Create Gzip versions of assets
def gzip
@assets.map! do |asset|
gzip_content = Zlib::Deflate.deflate(asset.content)
[
asset,
::JekyllAssetPipeline::Asset
.new(gzip_content, "#{asset.filename}.gz", asset.dirname)
]
end.flatten!
end
# Save assets to file
def save
output_path = @options['output_path']
staging_path = @options['staging_path']
@assets.each do |asset|
directory = File.join(@source, staging_path, output_path)
write_asset_file(directory, asset)
# Store output path of saved file
asset.output_path = output_path
end
end
# Write asset file to disk
def write_asset_file(directory, asset)
FileUtils.mkpath(directory) unless File.directory?(directory)
begin
# Save file to disk
File.open(File.join(directory, asset.filename), 'w') do |file|
file.write(asset.content)
end
rescue StandardError => e
puts "Asset Pipeline: Failed to save '#{asset.filename}' to " \
"disk: #{e.message}"
raise e
end
end
# Generate html markup pointing to assets
def markup
# Use display_path if defined, otherwise use output_path in url
display_path = @options['display_path'] || @options['output_path']
@html = @assets.map do |asset|
klass = ::JekyllAssetPipeline::Template.klass(asset.filename)
html = klass.new(display_path, asset.filename).html unless klass.nil?
html
end.join
end
end
end