anomaly/jekyll-incremental

View on GitHub
lib/jekyll-incremental.rb

Summary

Maintainability
A
1 hr
Test Coverage
# rubocop:disable Naming/FileName
# Frozen-string-literal: true
# Copyright: 2017 - Apache 2.0 License
# Encoding: utf-8

require "jekyll/cache"

module Jekyll
  # --
  # @note we replace theirs in-place.
  # @example bundle exec jekyll b --incremental
  # A replacement of Jekyll's Regenerator.  That does a
  #   few things a bit differently, most things.
  # --
  module Incremental
    FORCE_KEYS = %w(regenerate force force_regenerate regen).freeze
    CACHE_KEY  = "jekyll:regenerator:metadata"

    def disabled?
      !site.incremental? || (site.config.key?("incremental") &&
          !site.config["incremental"])
    end

    # --
    # {
    #   <Filename> => {
    #     last_modified => Time.now,
    #     forced => true|false
    #     dependencies => [
    #       <FileName>
    #     ]
    #   }
    # }
    # --
    # Determines if a file should be regenerated or not,
    #   this is determined by a few key things, such as whether
    #   you force it by metadata inside of your file, or
    #   whether the file has been modified.
    # --
    def regenerate?(doc)
      out = false unless doc.write?
      out = true if doc.respond_to?(:asset_file?) && doc.asset_file?
      out = true if forced_by_data?(doc)
      out = modified?(doc)

      # --
      # Make sure they know.
      # --
      Jekyll.logger.debug "Incremental" do
        "#{out} for #{doc.path}"
      end
      out
    end

    # --
    # They are one in the same now, there is no reason to
    #   have something that is different.  These still exist
    #   for people who manually call this stuff.
    # --
    alias regenerate_page? regenerate?
    alias regenerate_document? regenerate?
    alias regenerate_doc? regenerate?

    # --
    def forced_by_data?(doc)
      doc.data.values_at(FORCE_KEYS)
        .include?(true)
    end

    # --
    # rubocop:disable Metrics/PerceivedComplexity
    # rubocop:disable Metrics/CyclomaticComplexity
    # rubocop:disable Metrics/LineLength
    # rubocop:disable Metrics/AbcSize
    # --
    def modified?(doc)
      return true if metadata[doc.path]&.[](:forced)
      return true unless File.exist?(site.in_dest_dir(doc.path))
      modified, hash = file_mtime_of(doc.path), metadata[doc.path]
      return modified > hash[:last_modified] if hash && !hash[:dynamic] && hash[:seen_before]
      return hash[:seen_before] = true if hash && !hash[:seen_before]
      return dependencies_modified?(hash) if hash
      add(doc.path).update(seen_before: true)

      true
    end

    # --
    def dependencies_modified?(path)
      path[:dependencies].map { |v| modified?(v) }
        .include?(true)
    end

    # --
    # They are one in the same now, there is no reason to
    #   have something that is different as we will eventually
    #   call this anyways.
    # --
    alias existing_file_modified? modified?
    alias source_modified_or_dest_missing? \
      modified?

    # --
    def file_mtime_of(path)
      File.exist?(path) ? File.mtime(path) : Time.now
    end

    # --
    # seen_before address a logical race that happens
    #   incide of Jekyll.  Dependencies are added before
    #   dependents, which is not good at all.
    # --
    def add(path, forced: false)
      return metadata[path] if metadata.key?(path)
      metadata[path] = {
        seen_before: false,
        dynamic: !File.exist?(site.in_source_dir(path)),
        last_modified: file_mtime_of(path),
        dependencies: Set.new,
        forced: forced,
      }
    end

    # --
    def add_dependency(path, dependency)
      add(path).fetch(:dependencies) << dependency
    end

    # --
    def force(path)
      add(path, {
        force: true,
      })
    end

    # --
    def metadata
      @metadata ||= Jekyll.cache.fetch(CACHE_KEY) do
        {}
      end
    end

    # --
    # They are one in the same now, there is no reason to
    #   have something that is different as we will eventually
    #   write it anyways.  Seems redundant.
    # --
    alias cache metadata

    # --
    def clear
      @metadata = nil
      Jekyll.cache.delete(CACHE_KEY)
      metadata
    end

    # --
    # Write the metadata into the Jekyll cache.
    # @return [nil]
    # --
    def write_metadata
      unless disabled?
        Jekyll.cache.write(CACHE_KEY,
          cache)
      end
    end

    # --
    # Not used anymore, so we just nil them out.
    # If they do then we'll readd them properly someway.
    # These shouldn't cause problems.
    # --
    %i(clear_cache read_metadata).each do |v|
      define_method v do |*|
        nil
      end
    end

    # --
    def metadata_file
      site.in_source_dir(".jekyll-metadata")
    end
  end
end

# --
module Jekyll
  # --
  # Patches Jekyll's own regenerator, and replaces it with
  #   our regenerator, which should in theory be more efficient
  #   than Jekyll's since it does less work.
  # --
  class Regenerator
    prepend Jekyll::Incremental
  end
end