1
0
mirror of https://github.com/bitwarden/help synced 2025-12-24 04:04:27 +00:00

Promote to Master (#748)

* initial commit

* adding quotes for the array error

* Create Gemfile

* Create Gemfile.lock

* add .nvmrc and .node-version

* removed /article from URL

* update links to work with netlify

* more fixed links

* link fixes

* update bad links

* Update netlify.toml

toml test for redirects

* article redirect

* link fixes

* Update index.html

* Update netlify.toml

* Update _config.yml

* Update netlify.toml

* Update netlify.toml

* Update netlify.toml

* Update netlify.toml

* Update netlify.toml

* add article back into URL for launch

* Update netlify.toml

* Update netlify.toml

* add order to categories front matter

* Update netlify.toml

* update

* sidemenu update

* Revert "sidemenu update"

This reverts commit 5441c3d35c.

* update order prop

* Navbar updates per Gary and compiler warnings

* font/style tweaks

* Update sidebar.html

* Stage Release Documentation (#739)

* initial drafts

* rewrite Custom Fields article to prioritize new context-menu option & better organize ancillary information

* edit

* edit

* Custom Field Context Menu & CAPTCHA item in release notes

* SSO relink event

* update rn

* small edits

* improve release notes titles

* fix side menu

* Edits courtest of mportune!

* update order

* link fixes

* link cleanup

* image updates and a link

* fix trailing slash

Co-authored-by: DanHillesheim <79476558+DanHillesheim@users.noreply.github.com>
This commit is contained in:
fred_the_tech_writer
2021-09-21 13:21:11 -04:00
committed by GitHub
parent 63f78e8979
commit 906e2ca0dd
3304 changed files with 386714 additions and 8864 deletions

View File

@@ -0,0 +1,190 @@
# frozen_string_literal: true
require "digest"
module Jekyll
class Cache
# class-wide base cache
@base_cache = {}
# class-wide directive to write cache to disk is enabled by default
@disk_cache_enabled = true
class << self
# class-wide cache location
attr_accessor :cache_dir
# class-wide directive to write cache to disk
attr_reader :disk_cache_enabled
# class-wide base cache reader
attr_reader :base_cache
# Disable Marshaling cached items to disk
def disable_disk_cache!
@disk_cache_enabled = false
end
# Clear all caches
def clear
delete_cache_files
base_cache.each_value(&:clear)
end
# Compare the current config to the cached config
# If they are different, clear all caches
#
# Returns nothing.
def clear_if_config_changed(config)
config = config.inspect
cache = Jekyll::Cache.new "Jekyll::Cache"
return if cache.key?("config") && cache["config"] == config
clear
cache = Jekyll::Cache.new "Jekyll::Cache"
cache["config"] = config
nil
end
private
# Delete all cached items from all caches
#
# Returns nothing.
def delete_cache_files
FileUtils.rm_rf(@cache_dir) if disk_cache_enabled
end
end
#
# Get an existing named cache, or create a new one if none exists
#
# name - name of the cache
#
# Returns nothing.
def initialize(name)
@cache = Jekyll::Cache.base_cache[name] ||= {}
@name = name.gsub(%r![^\w\s-]!, "-")
end
# Clear this particular cache
def clear
delete_cache_files
@cache.clear
end
# Retrieve a cached item
# Raises if key does not exist in cache
#
# Returns cached value
def [](key)
return @cache[key] if @cache.key?(key)
path = path_to(hash(key))
if disk_cache_enabled? && File.file?(path) && File.readable?(path)
@cache[key] = load(path)
else
raise
end
end
# Add an item to cache
#
# Returns nothing.
def []=(key, value)
@cache[key] = value
return unless disk_cache_enabled?
path = path_to(hash(key))
value = new Hash(value) if value.is_a?(Hash) && !value.default.nil?
dump(path, value)
rescue TypeError
Jekyll.logger.debug "Cache:", "Cannot dump object #{key}"
end
# If an item already exists in the cache, retrieve it.
# Else execute code block, and add the result to the cache, and return that result.
def getset(key)
self[key]
rescue StandardError
value = yield
self[key] = value
value
end
# Remove one particular item from the cache
#
# Returns nothing.
def delete(key)
@cache.delete(key)
File.delete(path_to(hash(key))) if disk_cache_enabled?
end
# Check if `key` already exists in this cache
#
# Returns true if key exists in the cache, false otherwise
def key?(key)
# First, check if item is already cached in memory
return true if @cache.key?(key)
# Otherwise, it might be cached on disk
# but we should not consider the disk cache if it is disabled
return false unless disk_cache_enabled?
path = path_to(hash(key))
File.file?(path) && File.readable?(path)
end
def disk_cache_enabled?
!!Jekyll::Cache.disk_cache_enabled
end
private
# Given a hashed key, return the path to where this item would be saved on disk.
def path_to(hash = nil)
@base_dir ||= File.join(Jekyll::Cache.cache_dir, @name)
return @base_dir if hash.nil?
File.join(@base_dir, hash[0..1], hash[2..-1]).freeze
end
# Given a key, return a SHA2 hash that can be used for caching this item to disk.
def hash(key)
Digest::SHA2.hexdigest(key).freeze
end
# Remove all this caches items from disk
#
# Returns nothing.
def delete_cache_files
FileUtils.rm_rf(path_to) if disk_cache_enabled?
end
# Load `path` from disk and return the result.
# This MUST NEVER be called in Safe Mode
# rubocop:disable Security/MarshalLoad
def load(path)
raise unless disk_cache_enabled?
cached_file = File.open(path, "rb")
value = Marshal.load(cached_file)
cached_file.close
value
end
# rubocop:enable Security/MarshalLoad
# Given a path and a value, save value to disk at path.
# This should NEVER be called in Safe Mode
#
# Returns nothing.
def dump(path, value)
return unless disk_cache_enabled?
FileUtils.mkdir_p(File.dirname(path))
File.open(path, "wb") do |cached_file|
Marshal.dump(value, cached_file)
end
end
end
end

View File

@@ -0,0 +1,111 @@
# frozen_string_literal: true
module Jekyll
# Handles the cleanup of a site's destination before it is built.
class Cleaner
HIDDEN_FILE_REGEX = %r!/\.{1,2}$!.freeze
attr_reader :site
def initialize(site)
@site = site
end
# Cleans up the site's destination directory
def cleanup!
FileUtils.rm_rf(obsolete_files)
FileUtils.rm_rf(metadata_file) unless @site.incremental?
end
private
# Private: The list of files and directories to be deleted during cleanup process
#
# Returns an Array of the file and directory paths
def obsolete_files
out = (existing_files - new_files - new_dirs + replaced_files).to_a
Jekyll::Hooks.trigger :clean, :on_obsolete, out
out
end
# Private: The metadata file storing dependency tree and build history
#
# Returns an Array with the metdata file as the only item
def metadata_file
[site.regenerator.metadata_file]
end
# Private: The list of existing files, apart from those included in
# keep_files and hidden files.
#
# Returns a Set with the file paths
def existing_files
files = Set.new
regex = keep_file_regex
dirs = keep_dirs
Utils.safe_glob(site.in_dest_dir, ["**", "*"], File::FNM_DOTMATCH).each do |file|
next if HIDDEN_FILE_REGEX.match?(file) || regex.match?(file) || dirs.include?(file)
files << file
end
files
end
# Private: The list of files to be created when site is built.
#
# Returns a Set with the file paths
def new_files
@new_files ||= Set.new.tap do |files|
site.each_site_file { |item| files << item.destination(site.dest) }
end
end
# Private: The list of directories to be created when site is built.
# These are the parent directories of the files in #new_files.
#
# Returns a Set with the directory paths
def new_dirs
@new_dirs ||= new_files.flat_map { |file| parent_dirs(file) }.to_set
end
# Private: The list of parent directories of a given file
#
# Returns an Array with the directory paths
def parent_dirs(file)
parent_dir = File.dirname(file)
if parent_dir == site.dest
[]
else
parent_dirs(parent_dir).unshift(parent_dir)
end
end
# Private: The list of existing files that will be replaced by a directory
# during build
#
# Returns a Set with the file paths
def replaced_files
new_dirs.select { |dir| File.file?(dir) }.to_set
end
# Private: The list of directories that need to be kept because they are
# parent directories of files specified in keep_files
#
# Returns a Set with the directory paths
def keep_dirs
site.keep_files.flat_map { |file| parent_dirs(site.in_dest_dir(file)) }.to_set
end
# Private: Creates a regular expression from the config's keep_files array
#
# Examples
# ['.git','.svn'] with site.dest "/myblog/_site" creates
# the following regex: /\A\/myblog\/_site\/(\.git|\/.svn)/
#
# Returns the regular expression
def keep_file_regex
%r!\A#{Regexp.quote(site.dest)}/(#{Regexp.union(site.keep_files).source})!
end
end
end

View File

@@ -0,0 +1,309 @@
# frozen_string_literal: true
module Jekyll
class Collection
attr_reader :site, :label, :metadata
attr_writer :docs
# Create a new Collection.
#
# site - the site to which this collection belongs.
# label - the name of the collection
#
# Returns nothing.
def initialize(site, label)
@site = site
@label = sanitize_label(label)
@metadata = extract_metadata
end
# Fetch the Documents in this collection.
# Defaults to an empty array if no documents have been read in.
#
# Returns an array of Jekyll::Document objects.
def docs
@docs ||= []
end
# Override of normal respond_to? to match method_missing's logic for
# looking in @data.
def respond_to_missing?(method, include_private = false)
docs.respond_to?(method.to_sym, include_private) || super
end
# Override of method_missing to check in @data for the key.
def method_missing(method, *args, &blck)
if docs.respond_to?(method.to_sym)
Jekyll.logger.warn "Deprecation:",
"#{label}.#{method} should be changed to #{label}.docs.#{method}."
Jekyll.logger.warn "", "Called by #{caller(0..0)}."
docs.public_send(method.to_sym, *args, &blck)
else
super
end
end
# Fetch the static files in this collection.
# Defaults to an empty array if no static files have been read in.
#
# Returns an array of Jekyll::StaticFile objects.
def files
@files ||= []
end
# Read the allowed documents into the collection's array of docs.
#
# Returns the sorted array of docs.
def read
filtered_entries.each do |file_path|
full_path = collection_dir(file_path)
next if File.directory?(full_path)
if Utils.has_yaml_header? full_path
read_document(full_path)
else
read_static_file(file_path, full_path)
end
end
sort_docs!
end
# All the entries in this collection.
#
# Returns an Array of file paths to the documents in this collection
# relative to the collection's directory
def entries
return [] unless exists?
@entries ||= begin
collection_dir_slash = "#{collection_dir}/"
Utils.safe_glob(collection_dir, ["**", "*"], File::FNM_DOTMATCH).map do |entry|
entry[collection_dir_slash] = ""
entry
end
end
end
# Filtered version of the entries in this collection.
# See `Jekyll::EntryFilter#filter` for more information.
#
# Returns a list of filtered entry paths.
def filtered_entries
return [] unless exists?
@filtered_entries ||=
Dir.chdir(directory) do
entry_filter.filter(entries).reject do |f|
path = collection_dir(f)
File.directory?(path) || entry_filter.symlink?(f)
end
end
end
# The directory for this Collection, relative to the site source or the directory
# containing the collection.
#
# Returns a String containing the directory name where the collection
# is stored on the filesystem.
def relative_directory
@relative_directory ||= "_#{label}"
end
# The full path to the directory containing the collection.
#
# Returns a String containing th directory name where the collection
# is stored on the filesystem.
def directory
@directory ||= site.in_source_dir(
File.join(container, relative_directory)
)
end
# The full path to the directory containing the collection, with
# optional subpaths.
#
# *files - (optional) any other path pieces relative to the
# directory to append to the path
#
# Returns a String containing th directory name where the collection
# is stored on the filesystem.
def collection_dir(*files)
return directory if files.empty?
site.in_source_dir(container, relative_directory, *files)
end
# Checks whether the directory "exists" for this collection.
# The directory must exist on the filesystem and must not be a symlink
# if in safe mode.
#
# Returns false if the directory doesn't exist or if it's a symlink
# and we're in safe mode.
def exists?
File.directory?(directory) && !entry_filter.symlink?(directory)
end
# The entry filter for this collection.
# Creates an instance of Jekyll::EntryFilter.
#
# Returns the instance of Jekyll::EntryFilter for this collection.
def entry_filter
@entry_filter ||= Jekyll::EntryFilter.new(site, relative_directory)
end
# An inspect string.
#
# Returns the inspect string
def inspect
"#<#{self.class} @label=#{label} docs=#{docs}>"
end
# Produce a sanitized label name
# Label names may not contain anything but alphanumeric characters,
# underscores, and hyphens.
#
# label - the possibly-unsafe label
#
# Returns a sanitized version of the label.
def sanitize_label(label)
label.gsub(%r![^a-z0-9_\-.]!i, "")
end
# Produce a representation of this Collection for use in Liquid.
# Exposes two attributes:
# - label
# - docs
#
# Returns a representation of this collection for use in Liquid.
def to_liquid
Drops::CollectionDrop.new self
end
# Whether the collection's documents ought to be written as individual
# files in the output.
#
# Returns true if the 'write' metadata is true, false otherwise.
def write?
!!metadata.fetch("output", false)
end
# The URL template to render collection's documents at.
#
# Returns the URL template to render collection's documents at.
def url_template
@url_template ||= metadata.fetch("permalink") do
Utils.add_permalink_suffix("/:collection/:path", site.permalink_style)
end
end
# Extract options for this collection from the site configuration.
#
# Returns the metadata for this collection
def extract_metadata
if site.config["collections"].is_a?(Hash)
site.config["collections"][label] || {}
else
{}
end
end
private
def container
@container ||= site.config["collections_dir"]
end
def read_document(full_path)
doc = Document.new(full_path, :site => site, :collection => self)
doc.read
docs << doc if site.unpublished || doc.published?
end
def sort_docs!
if metadata["order"].is_a?(Array)
rearrange_docs!
elsif metadata["sort_by"].is_a?(String)
sort_docs_by_key!
else
docs.sort!
end
end
# A custom sort function based on Schwartzian transform
# Refer https://byparker.com/blog/2017/schwartzian-transform-faster-sorting/ for details
def sort_docs_by_key!
meta_key = metadata["sort_by"]
# Modify `docs` array to cache document's property along with the Document instance
docs.map! { |doc| [doc.data[meta_key], doc] }.sort! do |apples, olives|
order = determine_sort_order(meta_key, apples, olives)
# Fall back to `Document#<=>` if the properties were equal or were non-sortable
# Otherwise continue with current sort-order
if order.nil? || order.zero?
apples[-1] <=> olives[-1]
else
order
end
# Finally restore the `docs` array with just the Document objects themselves
end.map!(&:last)
end
def determine_sort_order(sort_key, apples, olives)
apple_property, apple_document = apples
olive_property, olive_document = olives
if apple_property.nil? && !olive_property.nil?
order_with_warning(sort_key, apple_document, 1)
elsif !apple_property.nil? && olive_property.nil?
order_with_warning(sort_key, olive_document, -1)
else
apple_property <=> olive_property
end
end
def order_with_warning(sort_key, document, order)
Jekyll.logger.warn "Sort warning:", "'#{sort_key}' not defined in #{document.relative_path}"
order
end
# Rearrange documents within the `docs` array as listed in the `metadata["order"]` array.
#
# Involves converting the two arrays into hashes based on relative_paths as keys first, then
# merging them to remove duplicates and finally retrieving the Document instances from the
# merged array.
def rearrange_docs!
docs_table = {}
custom_order = {}
# pre-sort to normalize default array across platforms and then proceed to create a Hash
# from that sorted array.
docs.sort.each do |doc|
docs_table[doc.relative_path] = doc
end
metadata["order"].each do |entry|
custom_order[File.join(relative_directory, entry)] = nil
end
result = Jekyll::Utils.deep_merge_hashes(custom_order, docs_table).values
result.compact!
self.docs = result
end
def read_static_file(file_path, full_path)
relative_dir = Jekyll.sanitized_path(
relative_directory,
File.dirname(file_path)
).chomp("/.")
files << StaticFile.new(
site,
site.source,
relative_dir,
File.basename(full_path),
self
)
end
end
end

View File

@@ -0,0 +1,105 @@
# frozen_string_literal: true
module Jekyll
class Command
class << self
# A list of subclasses of Jekyll::Command
def subclasses
@subclasses ||= []
end
# Keep a list of subclasses of Jekyll::Command every time it's inherited
# Called automatically.
#
# base - the subclass
#
# Returns nothing
def inherited(base)
subclasses << base
super(base)
end
# Run Site#process and catch errors
#
# site - the Jekyll::Site object
#
# Returns nothing
def process_site(site)
site.process
rescue Jekyll::Errors::FatalException => e
Jekyll.logger.error "ERROR:", "YOUR SITE COULD NOT BE BUILT:"
Jekyll.logger.error "", "------------------------------------"
Jekyll.logger.error "", e.message
exit(1)
end
# Create a full Jekyll configuration with the options passed in as overrides
#
# options - the configuration overrides
#
# Returns a full Jekyll configuration
def configuration_from_options(options)
return options if options.is_a?(Jekyll::Configuration)
Jekyll.configuration(options)
end
# Add common options to a command for building configuration
#
# cmd - the Jekyll::Command to add these options to
#
# Returns nothing
# rubocop:disable Metrics/MethodLength
def add_build_options(cmd)
cmd.option "config", "--config CONFIG_FILE[,CONFIG_FILE2,...]",
Array, "Custom configuration file"
cmd.option "destination", "-d", "--destination DESTINATION",
"The current folder will be generated into DESTINATION"
cmd.option "source", "-s", "--source SOURCE", "Custom source directory"
cmd.option "future", "--future", "Publishes posts with a future date"
cmd.option "limit_posts", "--limit_posts MAX_POSTS", Integer,
"Limits the number of posts to parse and publish"
cmd.option "watch", "-w", "--[no-]watch", "Watch for changes and rebuild"
cmd.option "baseurl", "-b", "--baseurl URL",
"Serve the website from the given base URL"
cmd.option "force_polling", "--force_polling", "Force watch to use polling"
cmd.option "lsi", "--lsi", "Use LSI for improved related posts"
cmd.option "show_drafts", "-D", "--drafts", "Render posts in the _drafts folder"
cmd.option "unpublished", "--unpublished",
"Render posts that were marked as unpublished"
cmd.option "disable_disk_cache", "--disable-disk-cache",
"Disable caching to disk in non-safe mode"
cmd.option "quiet", "-q", "--quiet", "Silence output."
cmd.option "verbose", "-V", "--verbose", "Print verbose output."
cmd.option "incremental", "-I", "--incremental", "Enable incremental rebuild."
cmd.option "strict_front_matter", "--strict_front_matter",
"Fail if errors are present in front matter"
end
# rubocop:enable Metrics/MethodLength
# Run ::process method in a given set of Jekyll::Command subclasses and suggest
# re-running the associated command with --trace switch to obtain any additional
# information or backtrace regarding the encountered Exception.
#
# cmd - the Jekyll::Command to be handled
# options - configuration overrides
# klass - an array of Jekyll::Command subclasses associated with the command
#
# Note that all exceptions are rescued..
# rubocop: disable Lint/RescueException
def process_with_graceful_fail(cmd, options, *klass)
klass.each { |k| k.process(options) if k.respond_to?(:process) }
rescue Exception => e
raise e if cmd.trace
msg = " Please append `--trace` to the `#{cmd.name}` command "
dashes = "-" * msg.length
Jekyll.logger.error "", dashes
Jekyll.logger.error "Jekyll #{Jekyll::VERSION} ", msg
Jekyll.logger.error "", " for any additional information or backtrace. "
Jekyll.logger.abort_with "", dashes
end
# rubocop: enable Lint/RescueException
end
end
end

View File

@@ -0,0 +1,93 @@
# frozen_string_literal: true
module Jekyll
module Commands
class Build < Command
class << self
# Create the Mercenary command for the Jekyll CLI for this Command
def init_with_program(prog)
prog.command(:build) do |c|
c.syntax "build [options]"
c.description "Build your site"
c.alias :b
add_build_options(c)
c.action do |_, options|
options["serving"] = false
process_with_graceful_fail(c, options, self)
end
end
end
# Build your jekyll site
# Continuously watch if `watch` is set to true in the config.
def process(options)
# Adjust verbosity quickly
Jekyll.logger.adjust_verbosity(options)
options = configuration_from_options(options)
site = Jekyll::Site.new(options)
if options.fetch("skip_initial_build", false)
Jekyll.logger.warn "Build Warning:", "Skipping the initial build." \
" This may result in an out-of-date site."
else
build(site, options)
end
if options.fetch("detach", false)
Jekyll.logger.info "Auto-regeneration:",
"disabled when running server detached."
elsif options.fetch("watch", false)
watch(site, options)
else
Jekyll.logger.info "Auto-regeneration:", "disabled. Use --watch to enable."
end
end
# Build your Jekyll site.
#
# site - the Jekyll::Site instance to build
# options - A Hash of options passed to the command
#
# Returns nothing.
def build(site, options)
t = Time.now
source = File.expand_path(options["source"])
destination = File.expand_path(options["destination"])
incremental = options["incremental"]
Jekyll.logger.info "Source:", source
Jekyll.logger.info "Destination:", destination
Jekyll.logger.info "Incremental build:",
(incremental ? "enabled" : "disabled. Enable with --incremental")
Jekyll.logger.info "Generating..."
process_site(site)
Jekyll.logger.info "", "done in #{(Time.now - t).round(3)} seconds."
end
# Private: Watch for file changes and rebuild the site.
#
# site - A Jekyll::Site instance
# options - A Hash of options passed to the command
#
# Returns nothing.
def watch(site, options)
# Warn Windows users that they might need to upgrade.
if Utils::Platforms.bash_on_windows?
Jekyll.logger.warn "",
"Auto-regeneration may not work on some Windows versions."
Jekyll.logger.warn "",
"Please see: https://github.com/Microsoft/BashOnWindows/issues/216"
Jekyll.logger.warn "",
"If it does not work, please upgrade Bash on Windows or "\
"run Jekyll with --no-watch."
end
External.require_with_graceful_fail "jekyll-watch"
Jekyll::Watcher.watch(options, site)
end
end
end
end
end

View File

@@ -0,0 +1,45 @@
# frozen_string_literal: true
module Jekyll
module Commands
class Clean < Command
class << self
def init_with_program(prog)
prog.command(:clean) do |c|
c.syntax "clean [subcommand]"
c.description "Clean the site " \
"(removes site output and metadata file) without building."
add_build_options(c)
c.action do |_, options|
Jekyll::Commands::Clean.process(options)
end
end
end
def process(options)
options = configuration_from_options(options)
destination = options["destination"]
metadata_file = File.join(options["source"], ".jekyll-metadata")
cache_dir = File.join(options["source"], options["cache_dir"])
sass_cache = ".sass-cache"
remove(destination, :checker_func => :directory?)
remove(metadata_file, :checker_func => :file?)
remove(cache_dir, :checker_func => :directory?)
remove(sass_cache, :checker_func => :directory?)
end
def remove(filename, checker_func: :file?)
if File.public_send(checker_func, filename)
Jekyll.logger.info "Cleaner:", "Removing #{filename}..."
FileUtils.rm_rf(filename)
else
Jekyll.logger.info "Cleaner:", "Nothing to do for #{filename}."
end
end
end
end
end
end

View File

@@ -0,0 +1,177 @@
# frozen_string_literal: true
module Jekyll
module Commands
class Doctor < Command
class << self
def init_with_program(prog)
prog.command(:doctor) do |c|
c.syntax "doctor"
c.description "Search site and print specific deprecation warnings"
c.alias(:hyde)
c.option "config", "--config CONFIG_FILE[,CONFIG_FILE2,...]", Array,
"Custom configuration file"
c.action do |_, options|
Jekyll::Commands::Doctor.process(options)
end
end
end
def process(options)
site = Jekyll::Site.new(configuration_from_options(options))
site.reset
site.read
site.generate
if healthy?(site)
Jekyll.logger.info "Your test results", "are in. Everything looks fine."
else
abort
end
end
def healthy?(site)
[
fsnotify_buggy?(site),
!deprecated_relative_permalinks(site),
!conflicting_urls(site),
!urls_only_differ_by_case(site),
proper_site_url?(site),
properly_gathered_posts?(site),
].all?
end
def properly_gathered_posts?(site)
return true if site.config["collections_dir"].empty?
posts_at_root = site.in_source_dir("_posts")
return true unless File.directory?(posts_at_root)
Jekyll.logger.warn "Warning:",
"Detected '_posts' directory outside custom `collections_dir`!"
Jekyll.logger.warn "",
"Please move '#{posts_at_root}' into the custom directory at " \
"'#{site.in_source_dir(site.config["collections_dir"])}'"
false
end
def deprecated_relative_permalinks(site)
if site.config["relative_permalinks"]
Jekyll::Deprecator.deprecation_message "Your site still uses relative permalinks," \
" which was removed in Jekyll v3.0.0."
true
end
end
def conflicting_urls(site)
conflicting_urls = false
destination_map(site).each do |dest, paths|
next unless paths.size > 1
conflicting_urls = true
Jekyll.logger.warn "Conflict:",
"The following destination is shared by multiple files."
Jekyll.logger.warn "", "The written file may end up with unexpected contents."
Jekyll.logger.warn "", dest.to_s.cyan
paths.each { |path| Jekyll.logger.warn "", " - #{path}" }
Jekyll.logger.warn ""
end
conflicting_urls
end
def fsnotify_buggy?(_site)
return true unless Utils::Platforms.osx?
if Dir.pwd != `pwd`.strip
Jekyll.logger.error <<~STR
We have detected that there might be trouble using fsevent on your
operating system, you can read https://github.com/thibaudgg/rb-fsevent/wiki/no-fsevents-fired-(OSX-bug)
for possible work arounds or you can work around it immediately
with `--force-polling`.
STR
false
end
true
end
def urls_only_differ_by_case(site)
urls_only_differ_by_case = false
urls = case_insensitive_urls(site.pages + site.docs_to_write, site.dest)
urls.each_value do |real_urls|
next unless real_urls.uniq.size > 1
urls_only_differ_by_case = true
Jekyll.logger.warn "Warning:", "The following URLs only differ" \
" by case. On a case-insensitive file system one of the URLs" \
" will be overwritten by the other: #{real_urls.join(", ")}"
end
urls_only_differ_by_case
end
def proper_site_url?(site)
url = site.config["url"]
[
url_exists?(url),
url_valid?(url),
url_absolute(url),
].all?
end
private
def destination_map(site)
{}.tap do |result|
site.each_site_file do |thing|
next if allow_used_permalink?(thing)
dest_path = thing.destination(site.dest)
(result[dest_path] ||= []) << thing.path
end
end
end
def allow_used_permalink?(item)
defined?(JekyllRedirectFrom) && item.is_a?(JekyllRedirectFrom::RedirectPage)
end
def case_insensitive_urls(things, destination)
things.each_with_object({}) do |thing, memo|
dest = thing.destination(destination)
(memo[dest.downcase] ||= []) << dest
end
end
def url_exists?(url)
return true unless url.nil? || url.empty?
Jekyll.logger.warn "Warning:", "You didn't set an URL in the config file, "\
"you may encounter problems with some plugins."
false
end
def url_valid?(url)
Addressable::URI.parse(url)
true
# Addressable::URI#parse only raises a TypeError
# https://git.io/vFfbx
rescue TypeError
Jekyll.logger.warn "Warning:", "The site URL does not seem to be valid, "\
"check the value of `url` in your config file."
false
end
def url_absolute(url)
return true if url.is_a?(String) && Addressable::URI.parse(url).absolute?
Jekyll.logger.warn "Warning:", "Your site URL does not seem to be absolute, "\
"check the value of `url` in your config file."
false
end
end
end
end
end

View File

@@ -0,0 +1,34 @@
# frozen_string_literal: true
module Jekyll
module Commands
class Help < Command
class << self
def init_with_program(prog)
prog.command(:help) do |c|
c.syntax "help [subcommand]"
c.description "Show the help message, optionally for a given subcommand."
c.action do |args, _|
cmd = (args.first || "").to_sym
if args.empty?
Jekyll.logger.info prog.to_s
elsif prog.has_command? cmd
Jekyll.logger.info prog.commands[cmd].to_s
else
invalid_command(prog, cmd)
abort
end
end
end
end
def invalid_command(prog, cmd)
Jekyll.logger.error "Error:",
"Hmm... we don't know what the '#{cmd}' command is."
Jekyll.logger.info "Valid commands:", prog.commands.keys.join(", ")
end
end
end
end
end

View File

@@ -0,0 +1,169 @@
# frozen_string_literal: true
require "erb"
module Jekyll
module Commands
class New < Command
class << self
def init_with_program(prog)
prog.command(:new) do |c|
c.syntax "new PATH"
c.description "Creates a new Jekyll site scaffold in PATH"
c.option "force", "--force", "Force creation even if PATH already exists"
c.option "blank", "--blank", "Creates scaffolding but with empty files"
c.option "skip-bundle", "--skip-bundle", "Skip 'bundle install'"
c.action do |args, options|
Jekyll::Commands::New.process(args, options)
end
end
end
def process(args, options = {})
raise ArgumentError, "You must specify a path." if args.empty?
new_blog_path = File.expand_path(args.join(" "), Dir.pwd)
FileUtils.mkdir_p new_blog_path
if preserve_source_location?(new_blog_path, options)
Jekyll.logger.error "Conflict:", "#{new_blog_path} exists and is not empty."
Jekyll.logger.abort_with "", "Ensure #{new_blog_path} is empty or else " \
"try again with `--force` to proceed and overwrite any files."
end
if options["blank"]
create_blank_site new_blog_path
else
create_site new_blog_path
end
after_install(new_blog_path, options)
end
def blank_template
File.expand_path("../../blank_template", __dir__)
end
def create_blank_site(path)
FileUtils.cp_r blank_template + "/.", path
FileUtils.chmod_R "u+w", path
Dir.chdir(path) do
FileUtils.mkdir(%w(_data _drafts _includes _posts))
end
end
def scaffold_post_content
ERB.new(File.read(File.expand_path(scaffold_path, site_template))).result
end
# Internal: Gets the filename of the sample post to be created
#
# Returns the filename of the sample post, as a String
def initialized_post_name
"_posts/#{Time.now.strftime("%Y-%m-%d")}-welcome-to-jekyll.markdown"
end
private
def gemfile_contents
<<~RUBY
source "https://rubygems.org"
# Hello! This is where you manage which Jekyll version is used to run.
# When you want to use a different version, change it below, save the
# file and run `bundle install`. Run Jekyll with `bundle exec`, like so:
#
# bundle exec jekyll serve
#
# This will help ensure the proper Jekyll version is running.
# Happy Jekylling!
gem "jekyll", "~> #{Jekyll::VERSION}"
# This is the default theme for new Jekyll sites. You may change this to anything you like.
gem "minima", "~> 2.5"
# If you want to use GitHub Pages, remove the "gem "jekyll"" above and
# uncomment the line below. To upgrade, run `bundle update github-pages`.
# gem "github-pages", group: :jekyll_plugins
# If you have any plugins, put them here!
group :jekyll_plugins do
gem "jekyll-feed", "~> 0.12"
end
# Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem
# and associated library.
platforms :mingw, :x64_mingw, :mswin, :jruby do
gem "tzinfo", "~> 1.2"
gem "tzinfo-data"
end
# Performance-booster for watching directories on Windows
gem "wdm", "~> 0.1.1", :platforms => [:mingw, :x64_mingw, :mswin]
RUBY
end
def create_site(new_blog_path)
create_sample_files new_blog_path
File.open(File.expand_path(initialized_post_name, new_blog_path), "w") do |f|
f.write(scaffold_post_content)
end
File.open(File.expand_path("Gemfile", new_blog_path), "w") do |f|
f.write(gemfile_contents)
end
end
def preserve_source_location?(path, options)
!options["force"] && !Dir["#{path}/**/*"].empty?
end
def create_sample_files(path)
FileUtils.cp_r site_template + "/.", path
FileUtils.chmod_R "u+w", path
FileUtils.rm File.expand_path(scaffold_path, path)
end
def site_template
File.expand_path("../../site_template", __dir__)
end
def scaffold_path
"_posts/0000-00-00-welcome-to-jekyll.markdown.erb"
end
# After a new blog has been created, print a success notification and
# then automatically execute bundle install from within the new blog dir
# unless the user opts to generate a blank blog or skip 'bundle install'.
def after_install(path, options = {})
unless options["blank"] || options["skip-bundle"]
begin
require "bundler"
bundle_install path
rescue LoadError
Jekyll.logger.info "Could not load Bundler. Bundle install skipped."
end
end
Jekyll.logger.info "New jekyll site installed in #{path.cyan}."
Jekyll.logger.info "Bundle install skipped." if options["skip-bundle"]
end
def bundle_install(path)
Jekyll.logger.info "Running bundle install in #{path.cyan}..."
Dir.chdir(path) do
exe = Gem.bin_path("bundler", "bundle")
process, output = Jekyll::Utils::Exec.run("ruby", exe, "install")
output.to_s.each_line do |line|
Jekyll.logger.info("Bundler:".green, line.strip) unless line.to_s.empty?
end
raise SystemExit unless process.success?
end
end
end
end
end
end

View File

@@ -0,0 +1,40 @@
# frozen_string_literal: true
require "erb"
module Jekyll
module Commands
class NewTheme < Jekyll::Command
class << self
def init_with_program(prog)
prog.command(:"new-theme") do |c|
c.syntax "new-theme NAME"
c.description "Creates a new Jekyll theme scaffold"
c.option "code_of_conduct", \
"-c", "--code-of-conduct", \
"Include a Code of Conduct. (defaults to false)"
c.action do |args, opts|
Jekyll::Commands::NewTheme.process(args, opts)
end
end
end
def process(args, opts)
if !args || args.empty?
raise Jekyll::Errors::InvalidThemeName, "You must specify a theme name."
end
new_theme_name = args.join("_")
theme = Jekyll::ThemeBuilder.new(new_theme_name, opts)
Jekyll.logger.abort_with "Conflict:", "#{theme.path} already exists." if theme.path.exist?
theme.create!
Jekyll.logger.info "Your new Jekyll theme, #{theme.name.cyan}," \
" is ready for you in #{theme.path.to_s.cyan}!"
Jekyll.logger.info "For help getting started, read #{theme.path}/README.md."
end
end
end
end
end

View File

@@ -0,0 +1,365 @@
# frozen_string_literal: true
module Jekyll
module Commands
class Serve < Command
# Similar to the pattern in Utils::ThreadEvent except we are maintaining the
# state of @running instead of just signaling an event. We have to maintain this
# state since Serve is just called via class methods instead of an instance
# being created each time.
@mutex = Mutex.new
@run_cond = ConditionVariable.new
@running = false
class << self
COMMAND_OPTIONS = {
"ssl_cert" => ["--ssl-cert [CERT]", "X.509 (SSL) certificate."],
"host" => ["host", "-H", "--host [HOST]", "Host to bind to"],
"open_url" => ["-o", "--open-url", "Launch your site in a browser"],
"detach" => ["-B", "--detach",
"Run the server in the background",],
"ssl_key" => ["--ssl-key [KEY]", "X.509 (SSL) Private Key."],
"port" => ["-P", "--port [PORT]", "Port to listen on"],
"show_dir_listing" => ["--show-dir-listing",
"Show a directory listing instead of loading" \
" your index file.",],
"skip_initial_build" => ["skip_initial_build", "--skip-initial-build",
"Skips the initial site build which occurs before" \
" the server is started.",],
"livereload" => ["-l", "--livereload",
"Use LiveReload to automatically refresh browsers",],
"livereload_ignore" => ["--livereload-ignore ignore GLOB1[,GLOB2[,...]]",
Array,
"Files for LiveReload to ignore. " \
"Remember to quote the values so your shell " \
"won't expand them",],
"livereload_min_delay" => ["--livereload-min-delay [SECONDS]",
"Minimum reload delay",],
"livereload_max_delay" => ["--livereload-max-delay [SECONDS]",
"Maximum reload delay",],
"livereload_port" => ["--livereload-port [PORT]", Integer,
"Port for LiveReload to listen on",],
}.freeze
DIRECTORY_INDEX = %w(
index.htm
index.html
index.rhtml
index.xht
index.xhtml
index.cgi
index.xml
index.json
).freeze
LIVERELOAD_PORT = 35_729
LIVERELOAD_DIR = File.join(__dir__, "serve", "livereload_assets")
attr_reader :mutex, :run_cond, :running
alias_method :running?, :running
def init_with_program(prog)
prog.command(:serve) do |cmd|
cmd.description "Serve your site locally"
cmd.syntax "serve [options]"
cmd.alias :server
cmd.alias :s
add_build_options(cmd)
COMMAND_OPTIONS.each do |key, val|
cmd.option key, *val
end
cmd.action do |_, opts|
opts["livereload_port"] ||= LIVERELOAD_PORT
opts["serving"] = true
opts["watch"] = true unless opts.key?("watch")
# Set the reactor to nil so any old reactor will be GCed.
# We can't unregister a hook so while running tests we don't want to
# inadvertently keep using a reactor created by a previous test.
@reload_reactor = nil
config = configuration_from_options(opts)
config["url"] = default_url(config) if Jekyll.env == "development"
process_with_graceful_fail(cmd, config, Build, Serve)
end
end
end
#
def process(opts)
opts = configuration_from_options(opts)
destination = opts["destination"]
if opts["livereload"]
validate_options(opts)
register_reload_hooks(opts)
end
setup(destination)
start_up_webrick(opts, destination)
end
def shutdown
@server.shutdown if running?
end
# Perform logical validation of CLI options
private
def validate_options(opts)
if opts["livereload"]
if opts["detach"]
Jekyll.logger.warn "Warning:", "--detach and --livereload are mutually exclusive." \
" Choosing --livereload"
opts["detach"] = false
end
if opts["ssl_cert"] || opts["ssl_key"]
# This is not technically true. LiveReload works fine over SSL, but
# EventMachine's SSL support in Windows requires building the gem's
# native extensions against OpenSSL and that proved to be a process
# so tedious that expecting users to do it is a non-starter.
Jekyll.logger.abort_with "Error:", "LiveReload does not support SSL"
end
unless opts["watch"]
# Using livereload logically implies you want to watch the files
opts["watch"] = true
end
elsif %w(livereload_min_delay
livereload_max_delay
livereload_ignore
livereload_port).any? { |o| opts[o] }
Jekyll.logger.abort_with "--livereload-min-delay, "\
"--livereload-max-delay, --livereload-ignore, and "\
"--livereload-port require the --livereload option."
end
end
# rubocop:disable Metrics/AbcSize
def register_reload_hooks(opts)
require_relative "serve/live_reload_reactor"
@reload_reactor = LiveReloadReactor.new
Jekyll::Hooks.register(:site, :post_render) do |site|
regenerator = Jekyll::Regenerator.new(site)
@changed_pages = site.pages.select do |p|
regenerator.regenerate?(p)
end
end
# A note on ignoring files: LiveReload errs on the side of reloading when it
# comes to the message it gets. If, for example, a page is ignored but a CSS
# file linked in the page isn't, the page will still be reloaded if the CSS
# file is contained in the message sent to LiveReload. Additionally, the
# path matching is very loose so that a message to reload "/" will always
# lead the page to reload since every page starts with "/".
Jekyll::Hooks.register(:site, :post_write) do
if @changed_pages && @reload_reactor && @reload_reactor.running?
ignore, @changed_pages = @changed_pages.partition do |p|
Array(opts["livereload_ignore"]).any? do |filter|
File.fnmatch(filter, Jekyll.sanitized_path(p.relative_path))
end
end
Jekyll.logger.debug "LiveReload:", "Ignoring #{ignore.map(&:relative_path)}"
@reload_reactor.reload(@changed_pages)
end
@changed_pages = nil
end
end
# rubocop:enable Metrics/AbcSize
# Do a base pre-setup of WEBRick so that everything is in place
# when we get ready to party, checking for an setting up an error page
# and making sure our destination exists.
def setup(destination)
require_relative "serve/servlet"
FileUtils.mkdir_p(destination)
if File.exist?(File.join(destination, "404.html"))
WEBrick::HTTPResponse.class_eval do
def create_error_page
@header["Content-Type"] = "text/html; charset=UTF-8"
@body = IO.read(File.join(@config[:DocumentRoot], "404.html"))
end
end
end
end
def webrick_opts(opts)
opts = {
:JekyllOptions => opts,
:DoNotReverseLookup => true,
:MimeTypes => mime_types,
:DocumentRoot => opts["destination"],
:StartCallback => start_callback(opts["detach"]),
:StopCallback => stop_callback(opts["detach"]),
:BindAddress => opts["host"],
:Port => opts["port"],
:DirectoryIndex => DIRECTORY_INDEX,
}
opts[:DirectoryIndex] = [] if opts[:JekyllOptions]["show_dir_listing"]
enable_ssl(opts)
enable_logging(opts)
opts
end
def start_up_webrick(opts, destination)
@reload_reactor.start(opts) if opts["livereload"]
@server = WEBrick::HTTPServer.new(webrick_opts(opts)).tap { |o| o.unmount("") }
@server.mount(opts["baseurl"].to_s, Servlet, destination, file_handler_opts)
Jekyll.logger.info "Server address:", server_address(@server, opts)
launch_browser @server, opts if opts["open_url"]
boot_or_detach @server, opts
end
# Recreate NondisclosureName under utf-8 circumstance
def file_handler_opts
WEBrick::Config::FileHandler.merge(
:FancyIndexing => true,
:NondisclosureName => [
".ht*", "~*",
]
)
end
def server_address(server, options = {})
format_url(
server.config[:SSLEnable],
server.config[:BindAddress],
server.config[:Port],
options["baseurl"]
)
end
def format_url(ssl_enabled, address, port, baseurl = nil)
format("%<prefix>s://%<address>s:%<port>i%<baseurl>s",
:prefix => ssl_enabled ? "https" : "http",
:address => address,
:port => port,
:baseurl => baseurl ? "#{baseurl}/" : "")
end
def default_url(opts)
config = configuration_from_options(opts)
auth = config.values_at("host", "port").join(":")
return config["url"] if auth == "127.0.0.1:4000"
format_url(
config["ssl_cert"] && config["ssl_key"],
config["host"] == "127.0.0.1" ? "localhost" : config["host"],
config["port"]
)
end
def launch_browser(server, opts)
address = server_address(server, opts)
return system "start", address if Utils::Platforms.windows?
return system "xdg-open", address if Utils::Platforms.linux?
return system "open", address if Utils::Platforms.osx?
Jekyll.logger.error "Refusing to launch browser; " \
"Platform launcher unknown."
end
# Keep in our area with a thread or detach the server as requested
# by the user. This method determines what we do based on what you
# ask us to do.
def boot_or_detach(server, opts)
if opts["detach"]
pid = Process.fork do
server.start
end
Process.detach(pid)
Jekyll.logger.info "Server detached with pid '#{pid}'.", \
"Run `pkill -f jekyll' or `kill -9 #{pid}'" \
" to stop the server."
else
t = Thread.new { server.start }
trap("INT") { server.shutdown }
t.join
end
end
# Make the stack verbose if the user requests it.
def enable_logging(opts)
opts[:AccessLog] = []
level = WEBrick::Log.const_get(opts[:JekyllOptions]["verbose"] ? :DEBUG : :WARN)
opts[:Logger] = WEBrick::Log.new($stdout, level)
end
# Add SSL to the stack if the user triggers --enable-ssl and they
# provide both types of certificates commonly needed. Raise if they
# forget to add one of the certificates.
def enable_ssl(opts)
cert, key, src =
opts[:JekyllOptions].values_at("ssl_cert", "ssl_key", "source")
return if cert.nil? && key.nil?
raise "Missing --ssl_cert or --ssl_key. Both are required." unless cert && key
require "openssl"
require "webrick/https"
opts[:SSLCertificate] = OpenSSL::X509::Certificate.new(read_file(src, cert))
begin
opts[:SSLPrivateKey] = OpenSSL::PKey::RSA.new(read_file(src, key))
rescue StandardError
if defined?(OpenSSL::PKey::EC)
opts[:SSLPrivateKey] = OpenSSL::PKey::EC.new(read_file(src, key))
else
raise
end
end
opts[:SSLEnable] = true
end
def start_callback(detached)
unless detached
proc do
mutex.synchronize do
# Block until EventMachine reactor starts
@reload_reactor&.started_event&.wait
@running = true
Jekyll.logger.info("Server running...", "press ctrl-c to stop.")
@run_cond.broadcast
end
end
end
end
def stop_callback(detached)
unless detached
proc do
mutex.synchronize do
unless @reload_reactor.nil?
@reload_reactor.stop
@reload_reactor.stopped_event.wait
end
@running = false
@run_cond.broadcast
end
end
end
end
def mime_types
file = File.expand_path("../mime.types", __dir__)
WEBrick::HTTPUtils.load_mime_types(file)
end
def read_file(source_dir, file_path)
File.read(Jekyll.sanitized_path(source_dir, file_path))
end
end
end
end
end

View File

@@ -0,0 +1,122 @@
# frozen_string_literal: true
require "em-websocket"
require_relative "websockets"
module Jekyll
module Commands
class Serve
class LiveReloadReactor
attr_reader :started_event
attr_reader :stopped_event
attr_reader :thread
def initialize
@websockets = []
@connections_count = 0
@started_event = Utils::ThreadEvent.new
@stopped_event = Utils::ThreadEvent.new
end
def stop
# There is only one EventMachine instance per Ruby process so stopping
# it here will stop the reactor thread we have running.
EM.stop if EM.reactor_running?
Jekyll.logger.debug "LiveReload Server:", "halted"
end
def running?
EM.reactor_running?
end
def handle_websockets_event(websocket)
websocket.onopen { |handshake| connect(websocket, handshake) }
websocket.onclose { disconnect(websocket) }
websocket.onmessage { |msg| print_message(msg) }
websocket.onerror { |error| log_error(error) }
end
def start(opts)
@thread = Thread.new do
# Use epoll if the kernel supports it
EM.epoll
EM.run do
EM.error_handler { |e| log_error(e) }
EM.start_server(
opts["host"],
opts["livereload_port"],
HttpAwareConnection,
opts
) do |ws|
handle_websockets_event(ws)
end
# Notify blocked threads that EventMachine has started or shutdown
EM.schedule { @started_event.set }
EM.add_shutdown_hook { @stopped_event.set }
Jekyll.logger.info "LiveReload address:",
"http://#{opts["host"]}:#{opts["livereload_port"]}"
end
end
@thread.abort_on_exception = true
end
# For a description of the protocol see
# http://feedback.livereload.com/knowledgebase/articles/86174-livereload-protocol
def reload(pages)
pages.each do |p|
json_message = JSON.dump(
:command => "reload",
:path => p.url,
:liveCSS => true
)
Jekyll.logger.debug "LiveReload:", "Reloading #{p.url}"
Jekyll.logger.debug "", json_message
@websockets.each { |ws| ws.send(json_message) }
end
end
private
def connect(websocket, handshake)
@connections_count += 1
if @connections_count == 1
message = "Browser connected"
message += " over SSL/TLS" if handshake.secure?
Jekyll.logger.info "LiveReload:", message
end
websocket.send(
JSON.dump(
:command => "hello",
:protocols => ["http://livereload.com/protocols/official-7"],
:serverName => "jekyll"
)
)
@websockets << websocket
end
def disconnect(websocket)
@websockets.delete(websocket)
end
def print_message(json_message)
msg = JSON.parse(json_message)
# Not sure what the 'url' command even does in LiveReload. The spec is silent
# on its purpose.
Jekyll.logger.info "LiveReload:", "Browser URL: #{msg["url"]}" if msg["command"] == "url"
end
def log_error(error)
Jekyll.logger.error "LiveReload experienced an error. " \
"Run with --trace for more information."
raise error
end
end
end
end
end

View File

@@ -0,0 +1,202 @@
# frozen_string_literal: true
require "webrick"
module Jekyll
module Commands
class Serve
# This class is used to determine if the Servlet should modify a served file
# to insert the LiveReload script tags
class SkipAnalyzer
BAD_USER_AGENTS = [%r!MSIE!].freeze
def self.skip_processing?(request, response, options)
new(request, response, options).skip_processing?
end
def initialize(request, response, options)
@options = options
@request = request
@response = response
end
def skip_processing?
!html? || chunked? || inline? || bad_browser?
end
def chunked?
@response["Transfer-Encoding"] == "chunked"
end
def inline?
@response["Content-Disposition"].to_s.start_with?("inline")
end
def bad_browser?
BAD_USER_AGENTS.any? { |pattern| pattern.match?(@request["User-Agent"]) }
end
def html?
@response["Content-Type"].to_s.include?("text/html")
end
end
# This class inserts the LiveReload script tags into HTML as it is served
class BodyProcessor
HEAD_TAG_REGEX = %r!<head>|<head[^(er)][^<]*>!.freeze
attr_reader :content_length, :new_body, :livereload_added
def initialize(body, options)
@body = body
@options = options
@processed = false
end
def processed?
@processed
end
# rubocop:disable Metrics/MethodLength
def process!
@new_body = []
# @body will usually be a File object but Strings occur in rare cases
if @body.respond_to?(:each)
begin
@body.each { |line| @new_body << line.to_s }
ensure
@body.close
end
else
@new_body = @body.lines
end
@content_length = 0
@livereload_added = false
@new_body.each do |line|
if !@livereload_added && line["<head"]
line.gsub!(HEAD_TAG_REGEX) do |match|
%(#{match}#{template.result(binding)})
end
@livereload_added = true
end
@content_length += line.bytesize
@processed = true
end
@new_body = @new_body.join
end
# rubocop:enable Metrics/MethodLength
def template
# Unclear what "snipver" does. Doc at
# https://github.com/livereload/livereload-js states that the recommended
# setting is 1.
# Complicated JavaScript to ensure that livereload.js is loaded from the
# same origin as the page. Mostly useful for dealing with the browser's
# distinction between 'localhost' and 127.0.0.1
@template ||= ERB.new(<<~TEMPLATE)
<script>
document.write(
'<script src="http://' +
(location.host || 'localhost').split(':')[0] +
':<%=@options["livereload_port"] %>/livereload.js?snipver=1<%= livereload_args %>"' +
'></' +
'script>');
</script>
TEMPLATE
end
def livereload_args
# XHTML standard requires ampersands to be encoded as entities when in
# attributes. See http://stackoverflow.com/a/2190292
src = ""
if @options["livereload_min_delay"]
src += "&amp;mindelay=#{@options["livereload_min_delay"]}"
end
if @options["livereload_max_delay"]
src += "&amp;maxdelay=#{@options["livereload_max_delay"]}"
end
src += "&amp;port=#{@options["livereload_port"]}" if @options["livereload_port"]
src
end
end
class Servlet < WEBrick::HTTPServlet::FileHandler
DEFAULTS = {
"Cache-Control" => "private, max-age=0, proxy-revalidate, " \
"no-store, no-cache, must-revalidate",
}.freeze
def initialize(server, root, callbacks)
# So we can access them easily.
@jekyll_opts = server.config[:JekyllOptions]
set_defaults
super
end
def search_index_file(req, res)
super ||
search_file(req, res, ".html") ||
search_file(req, res, ".xhtml")
end
# Add the ability to tap file.html the same way that Nginx does on our
# Docker images (or on GitHub Pages.) The difference is that we might end
# up with a different preference on which comes first.
def search_file(req, res, basename)
# /file.* > /file/index.html > /file.html
super ||
super(req, res, "#{basename}.html") ||
super(req, res, "#{basename}.xhtml")
end
# rubocop:disable Naming/MethodName
def do_GET(req, res)
rtn = super
if @jekyll_opts["livereload"]
return rtn if SkipAnalyzer.skip_processing?(req, res, @jekyll_opts)
processor = BodyProcessor.new(res.body, @jekyll_opts)
processor.process!
res.body = processor.new_body
res.content_length = processor.content_length.to_s
if processor.livereload_added
# Add a header to indicate that the page content has been modified
res["X-Rack-LiveReload"] = "1"
end
end
validate_and_ensure_charset(req, res)
res.header.merge!(@headers)
rtn
end
# rubocop:enable Naming/MethodName
private
def validate_and_ensure_charset(_req, res)
key = res.header.keys.grep(%r!content-type!i).first
typ = res.header[key]
unless %r!;\s*charset=!.match?(typ)
res.header[key] = "#{typ}; charset=#{@jekyll_opts["encoding"]}"
end
end
def set_defaults
hash_ = @jekyll_opts.fetch("webrick", {}).fetch("headers", {})
DEFAULTS.each_with_object(@headers = hash_) do |(key, val), hash|
hash[key] = val unless hash.key?(key)
end
end
end
end
end
end

View File

@@ -0,0 +1,81 @@
# frozen_string_literal: true
require "http/parser"
module Jekyll
module Commands
class Serve
# The LiveReload protocol requires the server to serve livereload.js over HTTP
# despite the fact that the protocol itself uses WebSockets. This custom connection
# class addresses the dual protocols that the server needs to understand.
class HttpAwareConnection < EventMachine::WebSocket::Connection
attr_reader :reload_body, :reload_size
def initialize(_opts)
# If EventMachine SSL support on Windows ever gets better, the code below will
# set up the reactor to handle SSL
#
# @ssl_enabled = opts["ssl_cert"] && opts["ssl_key"]
# if @ssl_enabled
# em_opts[:tls_options] = {
# :private_key_file => Jekyll.sanitized_path(opts["source"], opts["ssl_key"]),
# :cert_chain_file => Jekyll.sanitized_path(opts["source"], opts["ssl_cert"])
# }
# em_opts[:secure] = true
# end
# This is too noisy even for --verbose, but uncomment if you need it for
# a specific WebSockets issue. Adding ?LR-verbose=true onto the URL will
# enable logging on the client side.
# em_opts[:debug] = true
em_opts = {}
super(em_opts)
reload_file = File.join(Serve.singleton_class::LIVERELOAD_DIR, "livereload.js")
@reload_body = File.read(reload_file)
@reload_size = @reload_body.bytesize
end
# rubocop:disable Metrics/MethodLength
def dispatch(data)
parser = Http::Parser.new
parser << data
# WebSockets requests will have a Connection: Upgrade header
if parser.http_method != "GET" || parser.upgrade?
super
elsif parser.request_url.start_with?("/livereload.js")
headers = [
"HTTP/1.1 200 OK",
"Content-Type: application/javascript",
"Content-Length: #{reload_size}",
"",
"",
].join("\r\n")
send_data(headers)
# stream_file_data would free us from keeping livereload.js in memory
# but JRuby blocks on that call and never returns
send_data(reload_body)
close_connection_after_writing
else
body = "This port only serves livereload.js over HTTP.\n"
headers = [
"HTTP/1.1 400 Bad Request",
"Content-Type: text/plain",
"Content-Length: #{body.bytesize}",
"",
"",
].join("\r\n")
send_data(headers)
send_data(body)
close_connection_after_writing
end
end
# rubocop:enable Metrics/MethodLength
end
end
end
end

View File

@@ -0,0 +1,313 @@
# frozen_string_literal: true
module Jekyll
class Configuration < Hash
# Default options. Overridden by values in _config.yml.
# Strings rather than symbols are used for compatibility with YAML.
DEFAULTS = {
# Where things are
"source" => Dir.pwd,
"destination" => File.join(Dir.pwd, "_site"),
"collections_dir" => "",
"cache_dir" => ".jekyll-cache",
"plugins_dir" => "_plugins",
"layouts_dir" => "_layouts",
"data_dir" => "_data",
"includes_dir" => "_includes",
"collections" => {},
# Handling Reading
"safe" => false,
"include" => [".htaccess"],
"exclude" => [],
"keep_files" => [".git", ".svn"],
"encoding" => "utf-8",
"markdown_ext" => "markdown,mkdown,mkdn,mkd,md",
"strict_front_matter" => false,
# Filtering Content
"show_drafts" => nil,
"limit_posts" => 0,
"future" => false,
"unpublished" => false,
# Plugins
"whitelist" => [],
"plugins" => [],
# Conversion
"markdown" => "kramdown",
"highlighter" => "rouge",
"lsi" => false,
"excerpt_separator" => "\n\n",
"incremental" => false,
# Serving
"detach" => false, # default to not detaching the server
"port" => "4000",
"host" => "127.0.0.1",
"baseurl" => nil, # this mounts at /, i.e. no subdirectory
"show_dir_listing" => false,
# Output Configuration
"permalink" => "date",
"paginate_path" => "/page:num",
"timezone" => nil, # use the local timezone
"quiet" => false,
"verbose" => false,
"defaults" => [],
"liquid" => {
"error_mode" => "warn",
"strict_filters" => false,
"strict_variables" => false,
},
"kramdown" => {
"auto_ids" => true,
"toc_levels" => (1..6).to_a,
"entity_output" => "as_char",
"smart_quotes" => "lsquo,rsquo,ldquo,rdquo",
"input" => "GFM",
"hard_wrap" => false,
"guess_lang" => true,
"footnote_nr" => 1,
"show_warnings" => false,
},
}.each_with_object(Configuration.new) { |(k, v), hsh| hsh[k] = v.freeze }.freeze
class << self
# Static: Produce a Configuration ready for use in a Site.
# It takes the input, fills in the defaults where values do not exist.
#
# user_config - a Hash or Configuration of overrides.
#
# Returns a Configuration filled with defaults.
def from(user_config)
Utils.deep_merge_hashes(DEFAULTS, Configuration[user_config].stringify_keys)
.add_default_collections.add_default_excludes
end
end
# Public: Turn all keys into string
#
# Return a copy of the hash where all its keys are strings
def stringify_keys
each_with_object({}) { |(k, v), hsh| hsh[k.to_s] = v }
end
def get_config_value_with_override(config_key, override)
override[config_key] || self[config_key] || DEFAULTS[config_key]
end
# Public: Directory of the Jekyll source folder
#
# override - the command-line options hash
#
# Returns the path to the Jekyll source directory
def source(override)
get_config_value_with_override("source", override)
end
def quiet(override = {})
get_config_value_with_override("quiet", override)
end
alias_method :quiet?, :quiet
def verbose(override = {})
get_config_value_with_override("verbose", override)
end
alias_method :verbose?, :verbose
def safe_load_file(filename)
case File.extname(filename)
when %r!\.toml!i
Jekyll::External.require_with_graceful_fail("tomlrb") unless defined?(Tomlrb)
Tomlrb.load_file(filename)
when %r!\.ya?ml!i
SafeYAML.load_file(filename) || {}
else
raise ArgumentError,
"No parser for '#{filename}' is available. Use a .y(a)ml or .toml file instead."
end
end
# Public: Generate list of configuration files from the override
#
# override - the command-line options hash
#
# Returns an Array of config files
def config_files(override)
# Adjust verbosity quickly
Jekyll.logger.adjust_verbosity(
:quiet => quiet?(override),
:verbose => verbose?(override)
)
# Get configuration from <source>/_config.yml or <source>/<config_file>
config_files = override["config"]
if config_files.to_s.empty?
default = %w(yml yaml toml).find(-> { "yml" }) do |ext|
File.exist?(Jekyll.sanitized_path(source(override), "_config.#{ext}"))
end
config_files = Jekyll.sanitized_path(source(override), "_config.#{default}")
@default_config_file = true
end
Array(config_files)
end
# Public: Read configuration and return merged Hash
#
# file - the path to the YAML file to be read in
#
# Returns this configuration, overridden by the values in the file
def read_config_file(file)
file = File.expand_path(file)
next_config = safe_load_file(file)
unless next_config.is_a?(Hash)
raise ArgumentError, "Configuration file: (INVALID) #{file}".yellow
end
Jekyll.logger.info "Configuration file:", file
next_config
rescue SystemCallError
if @default_config_file ||= nil
Jekyll.logger.warn "Configuration file:", "none"
{}
else
Jekyll.logger.error "Fatal:", "The configuration file '#{file}' could not be found."
raise LoadError, "The Configuration file '#{file}' could not be found."
end
end
# Public: Read in a list of configuration files and merge with this hash
#
# files - the list of configuration file paths
#
# Returns the full configuration, with the defaults overridden by the values in the
# configuration files
def read_config_files(files)
configuration = clone
begin
files.each do |config_file|
next if config_file.nil? || config_file.empty?
new_config = read_config_file(config_file)
configuration = Utils.deep_merge_hashes(configuration, new_config)
end
rescue ArgumentError => e
Jekyll.logger.warn "WARNING:", "Error reading configuration. Using defaults (and options)."
warn e
end
configuration.validate.add_default_collections
end
# Public: Split a CSV string into an array containing its values
#
# csv - the string of comma-separated values
#
# Returns an array of the values contained in the CSV
def csv_to_array(csv)
csv.split(",").map(&:strip)
end
# Public: Ensure the proper options are set in the configuration
#
# Returns the configuration Hash
def validate
config = clone
check_plugins(config)
check_include_exclude(config)
config
end
def add_default_collections
config = clone
# It defaults to `{}`, so this is only if someone sets it to null manually.
return config if config["collections"].nil?
# Ensure we have a hash.
if config["collections"].is_a?(Array)
config["collections"] = config["collections"].each_with_object({}) do |collection, hash|
hash[collection] = {}
end
end
config["collections"] = Utils.deep_merge_hashes(
{ "posts" => {} }, config["collections"]
).tap do |collections|
collections["posts"]["output"] = true
if config["permalink"]
collections["posts"]["permalink"] ||= style_to_permalink(config["permalink"])
end
end
config
end
DEFAULT_EXCLUDES = %w(
.sass-cache .jekyll-cache
gemfiles Gemfile Gemfile.lock
node_modules
vendor/bundle/ vendor/cache/ vendor/gems/ vendor/ruby/
).freeze
def add_default_excludes
config = clone
return config if config["exclude"].nil?
config["exclude"].concat(DEFAULT_EXCLUDES).uniq!
config
end
private
STYLE_TO_PERMALINK = {
:none => "/:categories/:title:output_ext",
:date => "/:categories/:year/:month/:day/:title:output_ext",
:ordinal => "/:categories/:year/:y_day/:title:output_ext",
:pretty => "/:categories/:year/:month/:day/:title/",
:weekdate => "/:categories/:year/W:week/:short_day/:title:output_ext",
}.freeze
private_constant :STYLE_TO_PERMALINK
def style_to_permalink(permalink_style)
STYLE_TO_PERMALINK[permalink_style.to_sym] || permalink_style.to_s
end
def check_include_exclude(config)
%w(include exclude).each do |option|
next unless config.key?(option)
next if config[option].is_a?(Array)
raise Jekyll::Errors::InvalidConfigurationError,
"'#{option}' should be set as an array, but was: #{config[option].inspect}."
end
end
# Private: Checks if the `plugins` config is a String
#
# config - the config hash
#
# Raises a Jekyll::Errors::InvalidConfigurationError if the config `plugins`
# is not an Array.
def check_plugins(config)
return unless config.key?("plugins")
return if config["plugins"].is_a?(Array)
Jekyll.logger.error "'plugins' should be set as an array of gem-names, but was: " \
"#{config["plugins"].inspect}. Use 'plugins_dir' instead to set the directory " \
"for your non-gemified Ruby plugins."
raise Jekyll::Errors::InvalidConfigurationError,
"'plugins' should be set as an array, but was: #{config["plugins"].inspect}."
end
end
end

View File

@@ -0,0 +1,54 @@
# frozen_string_literal: true
module Jekyll
class Converter < Plugin
# Public: Get or set the highlighter prefix. When an argument is specified,
# the prefix will be set. If no argument is specified, the current prefix
# will be returned.
#
# highlighter_prefix - The String prefix (default: nil).
#
# Returns the String prefix.
def self.highlighter_prefix(highlighter_prefix = nil)
unless defined?(@highlighter_prefix) && highlighter_prefix.nil?
@highlighter_prefix = highlighter_prefix
end
@highlighter_prefix
end
# Public: Get or set the highlighter suffix. When an argument is specified,
# the suffix will be set. If no argument is specified, the current suffix
# will be returned.
#
# highlighter_suffix - The String suffix (default: nil).
#
# Returns the String suffix.
def self.highlighter_suffix(highlighter_suffix = nil)
unless defined?(@highlighter_suffix) && highlighter_suffix.nil?
@highlighter_suffix = highlighter_suffix
end
@highlighter_suffix
end
# Initialize the converter.
#
# Returns an initialized Converter.
def initialize(config = {})
@config = config
end
# Get the highlighter prefix.
#
# Returns the String prefix.
def highlighter_prefix
self.class.highlighter_prefix
end
# Get the highlighter suffix.
#
# Returns the String suffix.
def highlighter_suffix
self.class.highlighter_suffix
end
end
end

View File

@@ -0,0 +1,41 @@
# frozen_string_literal: true
module Jekyll
module Converters
# Identity converter. Returns same content as given.
# For more info on converters see https://jekyllrb.com/docs/plugins/converters/
class Identity < Converter
safe true
priority :lowest
# Public: Does the given extension match this converter's list of acceptable extensions?
# Takes one argument: the file's extension (including the dot).
#
# _ext - The String extension to check (not relevant here)
#
# Returns true since it always matches.
def matches(_ext)
true
end
# Public: The extension to be given to the output file (including the dot).
#
# ext - The String extension or original file.
#
# Returns The String output file extension.
def output_ext(ext)
ext
end
# Logic to do the content conversion.
#
# content - String content of file (without front matter).
#
# Returns a String of the converted content.
def convert(content)
content
end
end
end
end

View File

@@ -0,0 +1,113 @@
# frozen_string_literal: true
module Jekyll
module Converters
# Markdown converter.
# For more info on converters see https://jekyllrb.com/docs/plugins/converters/
class Markdown < Converter
highlighter_prefix "\n"
highlighter_suffix "\n"
safe true
def setup
return if @setup ||= false
unless (@parser = get_processor)
if @config["safe"]
Jekyll.logger.warn "Build Warning:", "Custom processors are not loaded in safe mode"
end
Jekyll.logger.error "Markdown processor:",
"#{@config["markdown"].inspect} is not a valid Markdown processor."
Jekyll.logger.error "", "Available processors are: #{valid_processors.join(", ")}"
Jekyll.logger.error ""
raise Errors::FatalException, "Invalid Markdown processor given: #{@config["markdown"]}"
end
@cache = Jekyll::Cache.new("Jekyll::Converters::Markdown")
@setup = true
end
# RuboCop does not allow reader methods to have names starting with `get_`
# To ensure compatibility, this check has been disabled on this method
#
# rubocop:disable Naming/AccessorMethodName
def get_processor
case @config["markdown"].downcase
when "kramdown" then KramdownParser.new(@config)
else
custom_processor
end
end
# rubocop:enable Naming/AccessorMethodName
# Public: Provides you with a list of processors comprised of the ones we support internally
# and the ones that you have provided to us (if they're whitelisted for use in safe mode).
#
# Returns an array of symbols.
def valid_processors
[:kramdown] + third_party_processors
end
# Public: A list of processors that you provide via plugins.
#
# Returns an array of symbols
def third_party_processors
self.class.constants - [:KramdownParser, :PRIORITIES]
end
# Does the given extension match this converter's list of acceptable extensions?
# Takes one argument: the file's extension (including the dot).
#
# ext - The String extension to check.
#
# Returns true if it matches, false otherwise.
def matches(ext)
extname_list.include?(ext.downcase)
end
# Public: The extension to be given to the output file (including the dot).
#
# ext - The String extension or original file.
#
# Returns The String output file extension.
def output_ext(_ext)
".html"
end
# Logic to do the content conversion.
#
# content - String content of file (without front matter).
#
# Returns a String of the converted content.
def convert(content)
setup
@cache.getset(content) do
@parser.convert(content)
end
end
def extname_list
@extname_list ||= @config["markdown_ext"].split(",").map! { |e| ".#{e.downcase}" }
end
private
def custom_processor
converter_name = @config["markdown"]
self.class.const_get(converter_name).new(@config) if custom_class_allowed?(converter_name)
end
# Private: Determine whether a class name is an allowed custom
# markdown class name.
#
# parser_name - the name of the parser class
#
# Returns true if the parser name contains only alphanumeric characters and is defined
# within Jekyll::Converters::Markdown
def custom_class_allowed?(parser_name)
parser_name !~ %r![^A-Za-z0-9_]! && self.class.constants.include?(parser_name.to_sym)
end
end
end
end

View File

@@ -0,0 +1,199 @@
# Frozen-string-literal: true
module Kramdown
# A Kramdown::Document subclass meant to optimize memory usage from initializing
# a kramdown document for parsing.
#
# The optimization is by using the same options Hash (and its derivatives) for
# converting all Markdown documents in a Jekyll site.
class JekyllDocument < Document
class << self
attr_reader :options, :parser
# The implementation is basically the core logic in +Kramdown::Document#initialize+
#
# rubocop:disable Naming/MemoizedInstanceVariableName
def setup(options)
@cache ||= {}
# reset variables on a subsequent set up with a different options Hash
unless @cache[:id] == options.hash
@options = @parser = nil
@cache[:id] = options.hash
end
@options ||= Options.merge(options).freeze
@parser ||= begin
parser_name = (@options[:input] || "kramdown").to_s
parser_name = parser_name[0..0].upcase + parser_name[1..-1]
try_require("parser", parser_name)
if Parser.const_defined?(parser_name)
Parser.const_get(parser_name)
else
raise Kramdown::Error, "kramdown has no parser to handle the specified " \
"input format: #{@options[:input]}"
end
end
end
# rubocop:enable Naming/MemoizedInstanceVariableName
private
def try_require(type, name)
require "kramdown/#{type}/#{Utils.snake_case(name)}"
rescue LoadError
false
end
end
def initialize(source, options = {})
JekyllDocument.setup(options)
@options = JekyllDocument.options
@root, @warnings = JekyllDocument.parser.parse(source, @options)
end
# Use Kramdown::Converter::Html class to convert this document into HTML.
#
# The implementation is basically an optimized version of core logic in
# +Kramdown::Document#method_missing+ from kramdown-2.1.0.
def to_html
output, warnings = Kramdown::Converter::Html.convert(@root, @options)
@warnings.concat(warnings)
output
end
end
end
#
module Jekyll
module Converters
class Markdown
class KramdownParser
CODERAY_DEFAULTS = {
"css" => "style",
"bold_every" => 10,
"line_numbers" => "inline",
"line_number_start" => 1,
"tab_width" => 4,
"wrap" => "div",
}.freeze
def initialize(config)
@main_fallback_highlighter = config["highlighter"] || "rouge"
@config = config["kramdown"] || {}
@highlighter = nil
setup
load_dependencies
end
# Setup and normalize the configuration:
# * Create Kramdown if it doesn't exist.
# * Set syntax_highlighter, detecting enable_coderay and merging
# highlighter if none.
# * Merge kramdown[coderay] into syntax_highlighter_opts stripping coderay_.
# * Make sure `syntax_highlighter_opts` exists.
def setup
@config["syntax_highlighter"] ||= highlighter
@config["syntax_highlighter_opts"] ||= {}
@config["syntax_highlighter_opts"]["default_lang"] ||= "plaintext"
@config["syntax_highlighter_opts"]["guess_lang"] = @config["guess_lang"]
@config["coderay"] ||= {} # XXX: Legacy.
modernize_coderay_config
end
def convert(content)
document = Kramdown::JekyllDocument.new(content, @config)
html_output = document.to_html
if @config["show_warnings"]
document.warnings.each do |warning|
Jekyll.logger.warn "Kramdown warning:", warning
end
end
html_output
end
private
def load_dependencies
require "kramdown-parser-gfm" if @config["input"] == "GFM"
if highlighter == "coderay"
Jekyll::External.require_with_graceful_fail("kramdown-syntax-coderay")
end
# `mathjax` emgine is bundled within kramdown-2.x and will be handled by
# kramdown itself.
if (math_engine = @config["math_engine"]) && math_engine != "mathjax"
Jekyll::External.require_with_graceful_fail("kramdown-math-#{math_engine}")
end
end
# config[kramdown][syntax_higlighter] >
# config[kramdown][enable_coderay] >
# config[highlighter]
# Where `enable_coderay` is now deprecated because Kramdown
# supports Rouge now too.
def highlighter
return @highlighter if @highlighter
if @config["syntax_highlighter"]
return @highlighter = @config[
"syntax_highlighter"
]
end
@highlighter = begin
if @config.key?("enable_coderay") && @config["enable_coderay"]
Jekyll::Deprecator.deprecation_message(
"You are using 'enable_coderay', " \
"use syntax_highlighter: coderay in your configuration file."
)
"coderay"
else
@main_fallback_highlighter
end
end
end
def strip_coderay_prefix(hash)
hash.each_with_object({}) do |(key, val), hsh|
cleaned_key = key.to_s.gsub(%r!\Acoderay_!, "")
if key != cleaned_key
Jekyll::Deprecator.deprecation_message(
"You are using '#{key}'. Normalizing to #{cleaned_key}."
)
end
hsh[cleaned_key] = val
end
end
# If our highlighter is CodeRay we go in to merge the CodeRay defaults
# with your "coderay" key if it's there, deprecating it in the
# process of you using it.
def modernize_coderay_config
unless @config["coderay"].empty?
Jekyll::Deprecator.deprecation_message(
"You are using 'kramdown.coderay' in your configuration, " \
"please use 'syntax_highlighter_opts' instead."
)
@config["syntax_highlighter_opts"] = begin
strip_coderay_prefix(
@config["syntax_highlighter_opts"] \
.merge(CODERAY_DEFAULTS) \
.merge(@config["coderay"])
)
end
end
end
end
end
end
end

View File

@@ -0,0 +1,70 @@
# frozen_string_literal: true
module Kramdown
module Parser
class SmartyPants < Kramdown::Parser::Kramdown
def initialize(source, options)
super
@block_parsers = [:block_html, :content]
@span_parsers = [:smart_quotes, :html_entity, :typographic_syms, :span_html]
end
def parse_content
add_text @src.scan(%r!\A.*\n!)
end
define_parser(:content, %r!\A!)
end
end
end
module Jekyll
module Converters
# SmartyPants converter.
# For more info on converters see https://jekyllrb.com/docs/plugins/converters/
class SmartyPants < Converter
safe true
priority :low
def initialize(config)
Jekyll::External.require_with_graceful_fail "kramdown" unless defined?(Kramdown)
@config = config["kramdown"].dup || {}
@config[:input] = :SmartyPants
end
# Does the given extension match this converter's list of acceptable extensions?
# Takes one argument: the file's extension (including the dot).
#
# ext - The String extension to check.
#
# Returns true if it matches, false otherwise.
def matches(_ext)
false
end
# Public: The extension to be given to the output file (including the dot).
#
# ext - The String extension or original file.
#
# Returns The String output file extension.
def output_ext(_ext)
nil
end
# Logic to do the content conversion.
#
# content - String content of file (without front matter).
#
# Returns a String of the converted content.
def convert(content)
document = Kramdown::Document.new(content, @config)
html_output = document.to_html.chomp
if @config["show_warnings"]
document.warnings.each do |warning|
Jekyll.logger.warn "Kramdown warning:", warning.sub(%r!^Warning:\s+!, "")
end
end
html_output
end
end
end
end

View File

@@ -0,0 +1,260 @@
# frozen_string_literal: true
# Convertible provides methods for converting a pagelike item
# from a certain type of markup into actual content
#
# Requires
# self.site -> Jekyll::Site
# self.content
# self.content=
# self.data=
# self.ext=
# self.output=
# self.name
# self.path
# self.type -> :page, :post or :draft
module Jekyll
module Convertible
# Returns the contents as a String.
def to_s
content || ""
end
# Whether the file is published or not, as indicated in YAML front-matter
def published?
!(data.key?("published") && data["published"] == false)
end
# Read the YAML frontmatter.
#
# base - The String path to the dir containing the file.
# name - The String filename of the file.
# opts - optional parameter to File.read, default at site configs
#
# Returns nothing.
# rubocop:disable Metrics/AbcSize
def read_yaml(base, name, opts = {})
filename = @path || site.in_source_dir(base, name)
Jekyll.logger.debug "Reading:", relative_path
begin
self.content = File.read(filename, **Utils.merged_file_read_opts(site, opts))
if content =~ Document::YAML_FRONT_MATTER_REGEXP
self.content = Regexp.last_match.post_match
self.data = SafeYAML.load(Regexp.last_match(1))
end
rescue Psych::SyntaxError => e
Jekyll.logger.warn "YAML Exception reading #{filename}: #{e.message}"
raise e if site.config["strict_front_matter"]
rescue StandardError => e
Jekyll.logger.warn "Error reading file #{filename}: #{e.message}"
raise e if site.config["strict_front_matter"]
end
self.data ||= {}
validate_data! filename
validate_permalink! filename
self.data
end
# rubocop:enable Metrics/AbcSize
def validate_data!(filename)
unless self.data.is_a?(Hash)
raise Errors::InvalidYAMLFrontMatterError,
"Invalid YAML front matter in #{filename}"
end
end
def validate_permalink!(filename)
if self.data["permalink"] == ""
raise Errors::InvalidPermalinkError, "Invalid permalink in #{filename}"
end
end
# Transform the contents based on the content type.
#
# Returns the transformed contents.
def transform
renderer.convert(content)
end
# Determine the extension depending on content_type.
#
# Returns the String extension for the output file.
# e.g. ".html" for an HTML output file.
def output_ext
renderer.output_ext
end
# Determine which converter to use based on this convertible's
# extension.
#
# Returns the Converter instance.
def converters
renderer.converters
end
# Render Liquid in the content
#
# content - the raw Liquid content to render
# payload - the payload for Liquid
# info - the info for Liquid
#
# Returns the converted content
def render_liquid(content, payload, info, path)
renderer.render_liquid(content, payload, info, path)
end
# Convert this Convertible's data to a Hash suitable for use by Liquid.
#
# Returns the Hash representation of this Convertible.
def to_liquid(attrs = nil)
further_data = attribute_hash(attrs || self.class::ATTRIBUTES_FOR_LIQUID)
Utils.deep_merge_hashes defaults, Utils.deep_merge_hashes(data, further_data)
end
# The type of a document,
# i.e., its classname downcase'd and to_sym'd.
#
# Returns the type of self.
def type
:pages if is_a?(Page)
end
# returns the owner symbol for hook triggering
def hook_owner
:pages if is_a?(Page)
end
# Determine whether the document is an asset file.
# Asset files include CoffeeScript files and Sass/SCSS files.
#
# Returns true if the extname belongs to the set of extensions
# that asset files use.
def asset_file?
sass_file? || coffeescript_file?
end
# Determine whether the document is a Sass file.
#
# Returns true if extname == .sass or .scss, false otherwise.
def sass_file?
Jekyll::Document::SASS_FILE_EXTS.include?(ext)
end
# Determine whether the document is a CoffeeScript file.
#
# Returns true if extname == .coffee, false otherwise.
def coffeescript_file?
ext == ".coffee"
end
# Determine whether the file should be rendered with Liquid.
#
# Returns true if the file has Liquid Tags or Variables, false otherwise.
def render_with_liquid?
return false if data["render_with_liquid"] == false
Jekyll::Utils.has_liquid_construct?(content)
end
# Determine whether the file should be placed into layouts.
#
# Returns false if the document is an asset file or if the front matter
# specifies `layout: none`
def place_in_layout?
!(asset_file? || no_layout?)
end
# Checks if the layout specified in the document actually exists
#
# layout - the layout to check
#
# Returns true if the layout is invalid, false if otherwise
def invalid_layout?(layout)
!data["layout"].nil? && layout.nil? && !(is_a? Jekyll::Excerpt)
end
# Recursively render layouts
#
# layouts - a list of the layouts
# payload - the payload for Liquid
# info - the info for Liquid
#
# Returns nothing
def render_all_layouts(layouts, payload, info)
renderer.layouts = layouts
self.output = renderer.place_in_layouts(output, payload, info)
ensure
@renderer = nil # this will allow the modifications above to disappear
end
# Add any necessary layouts to this convertible document.
#
# payload - The site payload Drop or Hash.
# layouts - A Hash of {"name" => "layout"}.
#
# Returns nothing.
def do_layout(payload, layouts)
self.output = renderer.tap do |doc_renderer|
doc_renderer.layouts = layouts
doc_renderer.payload = payload
end.run
Jekyll.logger.debug "Post-Render Hooks:", relative_path
Jekyll::Hooks.trigger hook_owner, :post_render, self
ensure
@renderer = nil # this will allow the modifications above to disappear
end
# Write the generated page file to the destination directory.
#
# dest - The String path to the destination dir.
#
# Returns nothing.
def write(dest)
path = destination(dest)
FileUtils.mkdir_p(File.dirname(path))
Jekyll.logger.debug "Writing:", path
File.write(path, output, :mode => "wb")
Jekyll::Hooks.trigger hook_owner, :post_write, self
end
# Accessor for data properties by Liquid.
#
# property - The String name of the property to retrieve.
#
# Returns the String value or nil if the property isn't included.
def [](property)
if self.class::ATTRIBUTES_FOR_LIQUID.include?(property)
send(property)
else
data[property]
end
end
def renderer
@renderer ||= Jekyll::Renderer.new(site, self)
end
private
def defaults
@defaults ||= site.frontmatter_defaults.all(relative_path, type)
end
def attribute_hash(attrs)
@attribute_hash ||= {}
@attribute_hash[attrs] ||= attrs.each_with_object({}) do |attribute, hsh|
hsh[attribute] = send(attribute)
end
end
def no_layout?
data["layout"] == "none"
end
end
end

View File

@@ -0,0 +1,50 @@
# frozen_string_literal: true
module Jekyll
module Deprecator
extend self
def process(args)
arg_is_present? args, "--server", "The --server command has been replaced by the \
'serve' subcommand."
arg_is_present? args, "--serve", "The --serve command has been replaced by the \
'serve' subcommand."
arg_is_present? args, "--no-server", "To build Jekyll without launching a server, \
use the 'build' subcommand."
arg_is_present? args, "--auto", "The switch '--auto' has been replaced with \
'--watch'."
arg_is_present? args, "--no-auto", "To disable auto-replication, simply leave off \
the '--watch' switch."
arg_is_present? args, "--pygments", "The 'pygments'settings has been removed in \
favour of 'highlighter'."
arg_is_present? args, "--paginate", "The 'paginate' setting can only be set in \
your config files."
arg_is_present? args, "--url", "The 'url' setting can only be set in your \
config files."
no_subcommand(args)
end
def no_subcommand(args)
unless args.empty? ||
args.first !~ %r(!/^--/!) || %w(--help --version).include?(args.first)
deprecation_message "Jekyll now uses subcommands instead of just switches. \
Run `jekyll help` to find out more."
abort
end
end
def arg_is_present?(args, deprecated_argument, message)
deprecation_message(message) if args.include?(deprecated_argument)
end
def deprecation_message(message)
Jekyll.logger.warn "Deprecation:", message
end
def defaults_deprecate_type(old, current)
Jekyll.logger.warn "Defaults:", "The '#{old}' type has become '#{current}'."
Jekyll.logger.warn "Defaults:", "Please update your front-matter defaults to use \
'type: #{current}'."
end
end
end

View File

@@ -0,0 +1,544 @@
# frozen_string_literal: true
module Jekyll
class Document
include Comparable
extend Forwardable
attr_reader :path, :site, :extname, :collection, :type
attr_accessor :content, :output
def_delegator :self, :read_post_data, :post_read
YAML_FRONT_MATTER_REGEXP = %r!\A(---\s*\n.*?\n?)^((---|\.\.\.)\s*$\n?)!m.freeze
DATELESS_FILENAME_MATCHER = %r!^(?:.+/)*(.*)(\.[^.]+)$!.freeze
DATE_FILENAME_MATCHER = %r!^(?>.+/)*?(\d{2,4}-\d{1,2}-\d{1,2})-([^/]*)(\.[^.]+)$!.freeze
SASS_FILE_EXTS = %w(.sass .scss).freeze
YAML_FILE_EXTS = %w(.yaml .yml).freeze
#
# Class-wide cache to stash and retrieve regexp to detect "super-directories"
# of a particular Jekyll::Document object.
#
# dirname - The *special directory* for the Document.
# e.g. "_posts" or "_drafts" for Documents from the `site.posts` collection.
def self.superdirs_regex(dirname)
@superdirs_regex ||= {}
@superdirs_regex[dirname] ||= %r!#{dirname}.*!
end
#
# Create a new Document.
#
# path - the path to the file
# relations - a hash with keys :site and :collection, the values of which
# are the Jekyll::Site and Jekyll::Collection to which this
# Document belong.
#
# Returns nothing.
def initialize(path, relations = {})
@site = relations[:site]
@path = path
@extname = File.extname(path)
@collection = relations[:collection]
@type = @collection.label.to_sym
@has_yaml_header = nil
if draft?
categories_from_path("_drafts")
else
categories_from_path(collection.relative_directory)
end
data.default_proc = proc do |_, key|
site.frontmatter_defaults.find(relative_path, type, key)
end
trigger_hooks(:post_init)
end
# Fetch the Document's data.
#
# Returns a Hash containing the data. An empty hash is returned if
# no data was read.
def data
@data ||= {}
end
# Merge some data in with this document's data.
#
# Returns the merged data.
def merge_data!(other, source: "YAML front matter")
merge_categories!(other)
Utils.deep_merge_hashes!(data, other)
merge_date!(source)
data
end
# Returns the document date. If metadata is not present then calculates it
# based on Jekyll::Site#time or the document file modification time.
#
# Return document date string.
def date
data["date"] ||= (draft? ? source_file_mtime : site.time)
end
# Return document file modification time in the form of a Time object.
#
# Return document file modification Time object.
def source_file_mtime
File.mtime(path)
end
# Returns whether the document is a draft. This is only the case if
# the document is in the 'posts' collection but in a different
# directory than '_posts'.
#
# Returns whether the document is a draft.
def draft?
data["draft"] ||= relative_path.index(collection.relative_directory).nil? &&
collection.label == "posts"
end
# The path to the document, relative to the collections_dir.
#
# Returns a String path which represents the relative path from the collections_dir
# to this document.
def relative_path
@relative_path ||= path.sub("#{site.collections_path}/", "")
end
# The output extension of the document.
#
# Returns the output extension
def output_ext
renderer.output_ext
end
# The base filename of the document, without the file extname.
#
# Returns the basename without the file extname.
def basename_without_ext
@basename_without_ext ||= File.basename(path, ".*")
end
# The base filename of the document.
#
# Returns the base filename of the document.
def basename
@basename ||= File.basename(path)
end
def renderer
@renderer ||= Jekyll::Renderer.new(site, self)
end
# Produces a "cleaned" relative path.
# The "cleaned" relative path is the relative path without the extname
# and with the collection's directory removed as well.
# This method is useful when building the URL of the document.
#
# NOTE: `String#gsub` removes all trailing periods (in comparison to `String#chomp`)
#
# Examples:
# When relative_path is "_methods/site/generate...md":
# cleaned_relative_path
# # => "/site/generate"
#
# Returns the cleaned relative path of the document.
def cleaned_relative_path
@cleaned_relative_path ||=
relative_path[0..-extname.length - 1]
.sub(collection.relative_directory, "")
.gsub(%r!\.*\z!, "")
end
# Determine whether the document is a YAML file.
#
# Returns true if the extname is either .yml or .yaml, false otherwise.
def yaml_file?
YAML_FILE_EXTS.include?(extname)
end
# Determine whether the document is an asset file.
# Asset files include CoffeeScript files and Sass/SCSS files.
#
# Returns true if the extname belongs to the set of extensions
# that asset files use.
def asset_file?
sass_file? || coffeescript_file?
end
# Determine whether the document is a Sass file.
#
# Returns true if extname == .sass or .scss, false otherwise.
def sass_file?
SASS_FILE_EXTS.include?(extname)
end
# Determine whether the document is a CoffeeScript file.
#
# Returns true if extname == .coffee, false otherwise.
def coffeescript_file?
extname == ".coffee"
end
# Determine whether the file should be rendered with Liquid.
#
# Returns false if the document is either an asset file or a yaml file,
# or if the document doesn't contain any Liquid Tags or Variables,
# true otherwise.
def render_with_liquid?
return false if data["render_with_liquid"] == false
!(coffeescript_file? || yaml_file? || !Utils.has_liquid_construct?(content))
end
# Determine whether the file should be rendered with a layout.
#
# Returns true if the Front Matter specifies that `layout` is set to `none`.
def no_layout?
data["layout"] == "none"
end
# Determine whether the file should be placed into layouts.
#
# Returns false if the document is set to `layouts: none`, or is either an
# asset file or a yaml file. Returns true otherwise.
def place_in_layout?
!(asset_file? || yaml_file? || no_layout?)
end
# The URL template where the document would be accessible.
#
# Returns the URL template for the document.
def url_template
collection.url_template
end
# Construct a Hash of key-value pairs which contain a mapping between
# a key in the URL template and the corresponding value for this document.
#
# Returns the Hash of key-value pairs for replacement in the URL.
def url_placeholders
@url_placeholders ||= Drops::UrlDrop.new(self)
end
# The permalink for this Document.
# Permalink is set via the data Hash.
#
# Returns the permalink or nil if no permalink was set in the data.
def permalink
data && data.is_a?(Hash) && data["permalink"]
end
# The computed URL for the document. See `Jekyll::URL#to_s` for more details.
#
# Returns the computed URL for the document.
def url
@url ||= URL.new(
:template => url_template,
:placeholders => url_placeholders,
:permalink => permalink
).to_s
end
def [](key)
data[key]
end
# The full path to the output file.
#
# base_directory - the base path of the output directory
#
# Returns the full path to the output file of this document.
def destination(base_directory)
@destination ||= {}
@destination[base_directory] ||= begin
path = site.in_dest_dir(base_directory, URL.unescape_path(url))
if url.end_with? "/"
path = File.join(path, "index.html")
else
path << output_ext unless path.end_with? output_ext
end
path
end
end
# Write the generated Document file to the destination directory.
#
# dest - The String path to the destination dir.
#
# Returns nothing.
def write(dest)
path = destination(dest)
FileUtils.mkdir_p(File.dirname(path))
Jekyll.logger.debug "Writing:", path
File.write(path, output, :mode => "wb")
trigger_hooks(:post_write)
end
# Whether the file is published or not, as indicated in YAML front-matter
#
# Returns 'false' if the 'published' key is specified in the
# YAML front-matter and is 'false'. Otherwise returns 'true'.
def published?
!(data.key?("published") && data["published"] == false)
end
# Read in the file and assign the content and data based on the file contents.
# Merge the frontmatter of the file with the frontmatter default
# values
#
# Returns nothing.
def read(opts = {})
Jekyll.logger.debug "Reading:", relative_path
if yaml_file?
@data = SafeYAML.load_file(path)
else
begin
merge_defaults
read_content(**opts)
read_post_data
rescue StandardError => e
handle_read_error(e)
end
end
end
# Create a Liquid-understandable version of this Document.
#
# Returns a Hash representing this Document's data.
def to_liquid
@to_liquid ||= Drops::DocumentDrop.new(self)
end
# The inspect string for this document.
# Includes the relative path and the collection label.
#
# Returns the inspect string for this document.
def inspect
"#<#{self.class} #{relative_path} collection=#{collection.label}>"
end
# The string representation for this document.
#
# Returns the content of the document
def to_s
output || content || "NO CONTENT"
end
# Compare this document against another document.
# Comparison is a comparison between the 2 paths of the documents.
#
# Returns -1, 0, +1 or nil depending on whether this doc's path is less than,
# equal or greater than the other doc's path. See String#<=> for more details.
def <=>(other)
return nil unless other.respond_to?(:data)
cmp = data["date"] <=> other.data["date"]
cmp = path <=> other.path if cmp.nil? || cmp.zero?
cmp
end
# Determine whether this document should be written.
# Based on the Collection to which it belongs.
#
# True if the document has a collection and if that collection's #write?
# method returns true, and if the site's Publisher will publish the document.
# False otherwise.
#
# rubocop:disable Naming/MemoizedInstanceVariableName
def write?
return @write_p if defined?(@write_p)
@write_p = collection&.write? && site.publisher.publish?(self)
end
# rubocop:enable Naming/MemoizedInstanceVariableName
# The Document excerpt_separator, from the YAML Front-Matter or site
# default excerpt_separator value
#
# Returns the document excerpt_separator
def excerpt_separator
@excerpt_separator ||= (data["excerpt_separator"] || site.config["excerpt_separator"]).to_s
end
# Whether to generate an excerpt
#
# Returns true if the excerpt separator is configured.
def generate_excerpt?
!excerpt_separator.empty?
end
def next_doc
pos = collection.docs.index { |post| post.equal?(self) }
collection.docs[pos + 1] if pos && pos < collection.docs.length - 1
end
def previous_doc
pos = collection.docs.index { |post| post.equal?(self) }
collection.docs[pos - 1] if pos && pos.positive?
end
def trigger_hooks(hook_name, *args)
Jekyll::Hooks.trigger collection.label.to_sym, hook_name, self, *args if collection
Jekyll::Hooks.trigger :documents, hook_name, self, *args
end
def id
@id ||= File.join(File.dirname(url), (data["slug"] || basename_without_ext).to_s)
end
# Calculate related posts.
#
# Returns an Array of related Posts.
def related_posts
@related_posts ||= Jekyll::RelatedPosts.new(self).build
end
# Override of method_missing to check in @data for the key.
def method_missing(method, *args, &blck)
if data.key?(method.to_s)
Jekyll::Deprecator.deprecation_message "Document##{method} is now a key "\
"in the #data hash."
Jekyll::Deprecator.deprecation_message "Called by #{caller(0..0)}."
data[method.to_s]
else
super
end
end
def respond_to_missing?(method, *)
data.key?(method.to_s) || super
end
# Add superdirectories of the special_dir to categories.
# In the case of es/_posts, 'es' is added as a category.
# In the case of _posts/es, 'es' is NOT added as a category.
#
# Returns nothing.
def categories_from_path(special_dir)
if relative_path.start_with?(special_dir)
superdirs = []
else
superdirs = relative_path.sub(Document.superdirs_regex(special_dir), "")
superdirs = superdirs.split(File::SEPARATOR)
superdirs.reject! { |c| c.empty? || c == special_dir || c == basename }
end
merge_data!({ "categories" => superdirs }, :source => "file path")
end
def populate_categories
categories = Array(data["categories"]) + Utils.pluralized_array_from_hash(
data, "category", "categories"
)
categories.map!(&:to_s)
categories.flatten!
categories.uniq!
merge_data!({ "categories" => categories })
end
def populate_tags
tags = Utils.pluralized_array_from_hash(data, "tag", "tags")
tags.flatten!
merge_data!({ "tags" => tags })
end
private
def merge_categories!(other)
if other.key?("categories") && !other["categories"].nil?
other["categories"] = other["categories"].split if other["categories"].is_a?(String)
if data["categories"].is_a?(Array)
other["categories"] = data["categories"] | other["categories"]
end
end
end
def merge_date!(source)
if data.key?("date")
data["date"] = Utils.parse_date(
data["date"].to_s,
"Document '#{relative_path}' does not have a valid date in the #{source}."
)
end
end
def merge_defaults
defaults = @site.frontmatter_defaults.all(relative_path, type)
merge_data!(defaults, :source => "front matter defaults") unless defaults.empty?
end
def read_content(**opts)
self.content = File.read(path, **Utils.merged_file_read_opts(site, opts))
if content =~ YAML_FRONT_MATTER_REGEXP
self.content = Regexp.last_match.post_match
data_file = SafeYAML.load(Regexp.last_match(1))
merge_data!(data_file, :source => "YAML front matter") if data_file
end
end
def read_post_data
populate_title
populate_categories
populate_tags
generate_excerpt
end
def handle_read_error(error)
if error.is_a? Psych::SyntaxError
Jekyll.logger.error "Error:", "YAML Exception reading #{path}: #{error.message}"
else
Jekyll.logger.error "Error:", "could not read file #{path}: #{error.message}"
end
if site.config["strict_front_matter"] || error.is_a?(Jekyll::Errors::FatalException)
raise error
end
end
def populate_title
if relative_path =~ DATE_FILENAME_MATCHER
date, slug, ext = Regexp.last_match.captures
modify_date(date)
elsif relative_path =~ DATELESS_FILENAME_MATCHER
slug, ext = Regexp.last_match.captures
end
# `slug` will be nil for documents without an extension since the regex patterns
# above tests for an extension as well.
# In such cases, assign `basename_without_ext` as the slug.
slug ||= basename_without_ext
# slugs shouldn't end with a period
# `String#gsub!` removes all trailing periods (in comparison to `String#chomp!`)
slug.gsub!(%r!\.*\z!, "")
# Try to ensure the user gets a title.
data["title"] ||= Utils.titleize_slug(slug)
# Only overwrite slug & ext if they aren't specified.
data["slug"] ||= slug
data["ext"] ||= ext
end
def modify_date(date)
if !data["date"] || data["date"].to_i == site.time.to_i
merge_data!({ "date" => date }, :source => "filename")
end
end
def generate_excerpt
data["excerpt"] ||= Jekyll::Excerpt.new(self) if generate_excerpt?
end
end
end

View File

@@ -0,0 +1,20 @@
# frozen_string_literal: true
module Jekyll
module Drops
class CollectionDrop < Drop
extend Forwardable
mutable false
delegate_method_as :write?, :output
delegate_methods :label, :docs, :files, :directory, :relative_directory
private delegate_method_as :metadata, :fallback_data
def to_s
docs.to_s
end
end
end
end

View File

@@ -0,0 +1,70 @@
# frozen_string_literal: true
module Jekyll
module Drops
class DocumentDrop < Drop
extend Forwardable
NESTED_OBJECT_FIELD_BLACKLIST = %w(
content output excerpt next previous
).freeze
mutable false
delegate_method_as :relative_path, :path
private delegate_method_as :data, :fallback_data
delegate_methods :id, :output, :content, :to_s, :relative_path, :url, :date
data_delegators "title", "categories", "tags"
def collection
@obj.collection.label
end
def excerpt
fallback_data["excerpt"].to_s
end
def <=>(other)
return nil unless other.is_a? DocumentDrop
cmp = self["date"] <=> other["date"]
cmp = self["path"] <=> other["path"] if cmp.nil? || cmp.zero?
cmp
end
def previous
@obj.previous_doc.to_liquid
end
def next
@obj.next_doc.to_liquid
end
# Generate a Hash for use in generating JSON.
# This is useful if fields need to be cleared before the JSON can generate.
#
# state - the JSON::State object which determines the state of current processing.
#
# Returns a Hash ready for JSON generation.
def hash_for_json(state = nil)
to_h.tap do |hash|
if state && state.depth >= 2
hash["previous"] = collapse_document(hash["previous"]) if hash["previous"]
hash["next"] = collapse_document(hash["next"]) if hash["next"]
end
end
end
# Generate a Hash which breaks the recursive chain.
# Certain fields which are normally available are omitted.
#
# Returns a Hash with only non-recursive fields present.
def collapse_document(doc)
doc.keys.each_with_object({}) do |(key, _), result|
result[key] = doc[key] unless NESTED_OBJECT_FIELD_BLACKLIST.include?(key)
end
end
end
end
end

View File

@@ -0,0 +1,293 @@
# frozen_string_literal: true
module Jekyll
module Drops
class Drop < Liquid::Drop
include Enumerable
NON_CONTENT_METHODS = [:fallback_data, :collapse_document].freeze
NON_CONTENT_METHOD_NAMES = NON_CONTENT_METHODS.map(&:to_s).freeze
private_constant :NON_CONTENT_METHOD_NAMES
# A private stash to avoid repeatedly generating the setter method name string for
# a call to `Drops::Drop#[]=`.
# The keys of the stash below have a very high probability of being called upon during
# the course of various `Jekyll::Renderer#run` calls.
SETTER_KEYS_STASH = {
"content" => "content=",
"layout" => "layout=",
"page" => "page=",
"paginator" => "paginator=",
"highlighter_prefix" => "highlighter_prefix=",
"highlighter_suffix" => "highlighter_suffix=",
}.freeze
private_constant :SETTER_KEYS_STASH
class << self
# Get or set whether the drop class is mutable.
# Mutability determines whether or not pre-defined fields may be
# overwritten.
#
# is_mutable - Boolean set mutability of the class (default: nil)
#
# Returns the mutability of the class
def mutable(is_mutable = nil)
@is_mutable = is_mutable || false
end
def mutable?
@is_mutable
end
# public delegation helper methods that calls onto Drop's instance
# variable `@obj`.
# Generate private Drop instance_methods for each symbol in the given list.
#
# Returns nothing.
def private_delegate_methods(*symbols)
symbols.each { |symbol| private delegate_method(symbol) }
nil
end
# Generate public Drop instance_methods for each symbol in the given list.
#
# Returns nothing.
def delegate_methods(*symbols)
symbols.each { |symbol| delegate_method(symbol) }
nil
end
# Generate public Drop instance_method for given symbol that calls `@obj.<sym>`.
#
# Returns delegated method symbol.
def delegate_method(symbol)
define_method(symbol) { @obj.send(symbol) }
end
# Generate public Drop instance_method named `delegate` that calls `@obj.<original>`.
#
# Returns delegated method symbol.
def delegate_method_as(original, delegate)
define_method(delegate) { @obj.send(original) }
end
# Generate public Drop instance_methods for each string entry in the given list.
# The generated method(s) access(es) `@obj`'s data hash.
#
# Returns nothing.
def data_delegators(*strings)
strings.each do |key|
data_delegator(key) if key.is_a?(String)
end
nil
end
# Generate public Drop instance_methods for given string `key`.
# The generated method access(es) `@obj`'s data hash.
#
# Returns method symbol.
def data_delegator(key)
define_method(key.to_sym) { @obj.data[key] }
end
# Array of stringified instance methods that do not end with the assignment operator.
#
# (<klass>.instance_methods always generates a new Array object so it can be mutated)
#
# Returns array of strings.
def getter_method_names
@getter_method_names ||= instance_methods.map!(&:to_s).tap do |list|
list.reject! { |item| item.end_with?("=") }
end
end
end
# Create a new Drop
#
# obj - the Jekyll Site, Collection, or Document required by the
# drop.
#
# Returns nothing
def initialize(obj)
@obj = obj
end
# Access a method in the Drop or a field in the underlying hash data.
# If mutable, checks the mutations first. Then checks the methods,
# and finally check the underlying hash (e.g. document front matter)
# if all the previous places didn't match.
#
# key - the string key whose value to fetch
#
# Returns the value for the given key, or nil if none exists
def [](key)
if self.class.mutable? && mutations.key?(key)
mutations[key]
elsif self.class.invokable? key
public_send key
else
fallback_data[key]
end
end
alias_method :invoke_drop, :[]
# Set a field in the Drop. If mutable, sets in the mutations and
# returns. If not mutable, checks first if it's trying to override a
# Drop method and raises a DropMutationException if so. If not
# mutable and the key is not a method on the Drop, then it sets the
# key to the value in the underlying hash (e.g. document front
# matter)
#
# key - the String key whose value to set
# val - the Object to set the key's value to
#
# Returns the value the key was set to unless the Drop is not mutable
# and the key matches a method in which case it raises a
# DropMutationException.
def []=(key, val)
setter = SETTER_KEYS_STASH[key] || "#{key}="
if respond_to?(setter)
public_send(setter, val)
elsif respond_to?(key.to_s)
if self.class.mutable?
mutations[key] = val
else
raise Errors::DropMutationException, "Key #{key} cannot be set in the drop."
end
else
fallback_data[key] = val
end
end
# Generates a list of strings which correspond to content getter
# methods.
#
# Returns an Array of strings which represent method-specific keys.
def content_methods
@content_methods ||= \
self.class.getter_method_names \
- Jekyll::Drops::Drop.getter_method_names \
- NON_CONTENT_METHOD_NAMES
end
# Check if key exists in Drop
#
# key - the string key whose value to fetch
#
# Returns true if the given key is present
def key?(key)
return false if key.nil?
return true if self.class.mutable? && mutations.key?(key)
respond_to?(key) || fallback_data.key?(key)
end
# Generates a list of keys with user content as their values.
# This gathers up the Drop methods and keys of the mutations and
# underlying data hashes and performs a set union to ensure a list
# of unique keys for the Drop.
#
# Returns an Array of unique keys for content for the Drop.
def keys
(content_methods |
mutations.keys |
fallback_data.keys).flatten
end
# Generate a Hash representation of the Drop by resolving each key's
# value. It includes Drop methods, mutations, and the underlying object's
# data. See the documentation for Drop#keys for more.
#
# Returns a Hash with all the keys and values resolved.
def to_h
keys.each_with_object({}) do |(key, _), result|
result[key] = self[key]
end
end
alias_method :to_hash, :to_h
# Inspect the drop's keys and values through a JSON representation
# of its keys and values.
#
# Returns a pretty generation of the hash representation of the Drop.
def inspect
JSON.pretty_generate to_h
end
# Generate a Hash for use in generating JSON.
# This is useful if fields need to be cleared before the JSON can generate.
#
# Returns a Hash ready for JSON generation.
def hash_for_json(*)
to_h
end
# Generate a JSON representation of the Drop.
#
# state - the JSON::State object which determines the state of current processing.
#
# Returns a JSON representation of the Drop in a String.
def to_json(state = nil)
JSON.generate(hash_for_json(state), state)
end
# Collects all the keys and passes each to the block in turn.
#
# block - a block which accepts one argument, the key
#
# Returns nothing.
def each_key(&block)
keys.each(&block)
end
def each
each_key.each do |key|
yield key, self[key]
end
end
def merge(other, &block)
dup.tap do |me|
if block.nil?
me.merge!(other)
else
me.merge!(other, block)
end
end
end
def merge!(other)
other.each_key do |key|
if block_given?
self[key] = yield key, self[key], other[key]
else
if Utils.mergable?(self[key]) && Utils.mergable?(other[key])
self[key] = Utils.deep_merge_hashes(self[key], other[key])
next
end
self[key] = other[key] unless other[key].nil?
end
end
end
# Imitate Hash.fetch method in Drop
#
# Returns value if key is present in Drop, otherwise returns default value
# KeyError is raised if key is not present and no default value given
def fetch(key, default = nil, &block)
return self[key] if key?(key)
raise KeyError, %(key not found: "#{key}") if default.nil? && block.nil?
return yield(key) unless block.nil?
return default unless default.nil?
end
private
def mutations
@mutations ||= {}
end
end
end
end

View File

@@ -0,0 +1,19 @@
# frozen_string_literal: true
module Jekyll
module Drops
class ExcerptDrop < DocumentDrop
def layout
@obj.doc.data["layout"]
end
def date
@obj.doc.date
end
def excerpt
nil
end
end
end
end

View File

@@ -0,0 +1,32 @@
# frozen_string_literal: true
module Jekyll
module Drops
class JekyllDrop < Liquid::Drop
class << self
def global
@global ||= JekyllDrop.new
end
end
def version
Jekyll::VERSION
end
def environment
Jekyll.env
end
def to_h
@to_h ||= {
"version" => version,
"environment" => environment,
}
end
def to_json(state = nil)
JSON.generate(to_h, state)
end
end
end
end

View File

@@ -0,0 +1,66 @@
# frozen_string_literal: true
module Jekyll
module Drops
class SiteDrop < Drop
extend Forwardable
mutable false
delegate_method_as :site_data, :data
delegate_methods :time, :pages, :static_files, :tags, :categories
private delegate_method_as :config, :fallback_data
def [](key)
if key != "posts" && @obj.collections.key?(key)
@obj.collections[key].docs
else
super(key)
end
end
def key?(key)
(key != "posts" && @obj.collections.key?(key)) || super
end
def posts
@site_posts ||= @obj.posts.docs.sort { |a, b| b <=> a }
end
def html_pages
@site_html_pages ||= @obj.pages.select do |page|
page.html? || page.url.end_with?("/")
end
end
def collections
@site_collections ||= @obj.collections.values.sort_by(&:label).map(&:to_liquid)
end
# `Site#documents` cannot be memoized so that `Site#docs_to_write` can access the
# latest state of the attribute.
#
# Since this method will be called after `Site#pre_render` hook, the `Site#documents`
# array shouldn't thereafter change and can therefore be safely memoized to prevent
# additional computation of `Site#documents`.
def documents
@documents ||= @obj.documents
end
# `{{ site.related_posts }}` is how posts can get posts related to
# them, either through LSI if it's enabled, or through the most
# recent posts.
# We should remove this in 4.0 and switch to `{{ post.related_posts }}`.
def related_posts
return nil unless @current_document.is_a?(Jekyll::Document)
@current_document.related_posts
end
attr_writer :current_document
# return nil for `{{ site.config }}` even if --config was passed via CLI
def config; end
end
end
end

View File

@@ -0,0 +1,14 @@
# frozen_string_literal: true
module Jekyll
module Drops
class StaticFileDrop < Drop
extend Forwardable
delegate_methods :name, :extname, :modified_time, :basename
delegate_method_as :relative_path, :path
delegate_method_as :type, :collection
private delegate_method_as :data, :fallback_data
end
end
end

View File

@@ -0,0 +1,26 @@
# frozen_string_literal: true
module Jekyll
module Drops
class UnifiedPayloadDrop < Drop
mutable true
attr_accessor :page, :layout, :content, :paginator
attr_accessor :highlighter_prefix, :highlighter_suffix
def jekyll
JekyllDrop.global
end
def site
@site_drop ||= SiteDrop.new(@obj)
end
private
def fallback_data
@fallback_data ||= {}
end
end
end
end

View File

@@ -0,0 +1,140 @@
# frozen_string_literal: true
module Jekyll
module Drops
class UrlDrop < Drop
extend Forwardable
mutable false
delegate_method :output_ext
delegate_method_as :cleaned_relative_path, :path
def collection
@obj.collection.label
end
def name
Utils.slugify(@obj.basename_without_ext)
end
def title
Utils.slugify(@obj.data["slug"], :mode => "pretty", :cased => true) ||
Utils.slugify(@obj.basename_without_ext, :mode => "pretty", :cased => true)
end
def slug
Utils.slugify(@obj.data["slug"]) || Utils.slugify(@obj.basename_without_ext)
end
def categories
category_set = Set.new
Array(@obj.data["categories"]).each do |category|
category_set << category.to_s.downcase
end
category_set.to_a.join("/")
end
# Similar to output from #categories, but each category will be downcased and
# all non-alphanumeric characters of the category replaced with a hyphen.
def slugified_categories
Array(@obj.data["categories"]).each_with_object(Set.new) do |category, set|
set << Utils.slugify(category.to_s)
end.to_a.join("/")
end
# CCYY
def year
@obj.date.strftime("%Y")
end
# MM: 01..12
def month
@obj.date.strftime("%m")
end
# DD: 01..31
def day
@obj.date.strftime("%d")
end
# hh: 00..23
def hour
@obj.date.strftime("%H")
end
# mm: 00..59
def minute
@obj.date.strftime("%M")
end
# ss: 00..59
def second
@obj.date.strftime("%S")
end
# D: 1..31
def i_day
@obj.date.strftime("%-d")
end
# M: 1..12
def i_month
@obj.date.strftime("%-m")
end
# MMM: Jan..Dec
def short_month
@obj.date.strftime("%b")
end
# MMMM: January..December
def long_month
@obj.date.strftime("%B")
end
# YY: 00..99
def short_year
@obj.date.strftime("%y")
end
# CCYYw, ISO week year
# may differ from CCYY for the first days of January and last days of December
def w_year
@obj.date.strftime("%G")
end
# WW: 01..53
# %W and %U do not comply with ISO 8601-1
def week
@obj.date.strftime("%V")
end
# d: 1..7 (Monday..Sunday)
def w_day
@obj.date.strftime("%u")
end
# dd: Mon..Sun
def short_day
@obj.date.strftime("%a")
end
# ddd: Monday..Sunday
def long_day
@obj.date.strftime("%A")
end
# DDD: 001..366
def y_day
@obj.date.strftime("%j")
end
private
def fallback_data
@fallback_data ||= {}
end
end
end
end

View File

@@ -0,0 +1,121 @@
# frozen_string_literal: true
module Jekyll
class EntryFilter
attr_reader :site
SPECIAL_LEADING_CHAR_REGEX = %r!\A#{Regexp.union([".", "_", "#", "~"])}!o.freeze
def initialize(site, base_directory = nil)
@site = site
@base_directory = derive_base_directory(
@site, base_directory.to_s.dup
)
end
def base_directory
@base_directory.to_s
end
def derive_base_directory(site, base_dir)
base_dir[site.source] = "" if base_dir.start_with?(site.source)
base_dir
end
def relative_to_source(entry)
File.join(
base_directory, entry
)
end
def filter(entries)
entries.reject do |e|
# Reject this entry if it is just a "dot" representation.
# e.g.: '.', '..', '_movies/.', 'music/..', etc
next true if e.end_with?(".")
# Check if the current entry is explicitly included and cache the result
included = included?(e)
# Reject current entry if it is excluded but not explicitly included as well.
next true if excluded?(e) && !included
# Reject current entry if it is a symlink.
next true if symlink?(e)
# Do not reject current entry if it is explicitly included.
next false if included
# Reject current entry if it is special or a backup file.
special?(e) || backup?(e)
end
end
def included?(entry)
glob_include?(site.include, entry) ||
glob_include?(site.include, File.basename(entry))
end
def special?(entry)
SPECIAL_LEADING_CHAR_REGEX.match?(entry) ||
SPECIAL_LEADING_CHAR_REGEX.match?(File.basename(entry))
end
def backup?(entry)
entry.end_with?("~")
end
def excluded?(entry)
glob_include?(site.exclude - site.include, relative_to_source(entry)).tap do |excluded|
if excluded
Jekyll.logger.debug(
"EntryFilter:",
"excluded #{relative_to_source(entry)}"
)
end
end
end
# --
# Check if a file is a symlink.
# NOTE: This can be converted to allowing even in safe,
# since we use Pathutil#in_path? now.
# --
def symlink?(entry)
site.safe && File.symlink?(entry) && symlink_outside_site_source?(entry)
end
# --
# NOTE: Pathutil#in_path? gets the realpath.
# @param [<Anything>] entry the entry you want to validate.
# Check if a path is outside of our given root.
# --
def symlink_outside_site_source?(entry)
!Pathutil.new(entry).in_path?(
site.in_source_dir
)
end
# Check if an entry matches a specific pattern.
# Returns true if path matches against any glob pattern, else false.
def glob_include?(enumerator, entry)
entry_with_source = PathManager.join(site.source, entry)
entry_is_directory = File.directory?(entry_with_source)
enumerator.any? do |pattern|
case pattern
when String
pattern_with_source = PathManager.join(site.source, pattern)
File.fnmatch?(pattern_with_source, entry_with_source) ||
entry_with_source.start_with?(pattern_with_source) ||
(pattern_with_source == "#{entry_with_source}/" if entry_is_directory)
when Regexp
pattern.match?(entry_with_source)
else
false
end
end
end
end
end

View File

@@ -0,0 +1,20 @@
# frozen_string_literal: true
module Jekyll
module Errors
FatalException = Class.new(::RuntimeError)
InvalidThemeName = Class.new(FatalException)
DropMutationException = Class.new(FatalException)
InvalidPermalinkError = Class.new(FatalException)
InvalidYAMLFrontMatterError = Class.new(FatalException)
MissingDependencyException = Class.new(FatalException)
InvalidDateError = Class.new(FatalException)
InvalidPostNameError = Class.new(FatalException)
PostURLError = Class.new(FatalException)
InvalidURLError = Class.new(FatalException)
InvalidConfigurationError = Class.new(FatalException)
end
end

View File

@@ -0,0 +1,201 @@
# frozen_string_literal: true
module Jekyll
class Excerpt
extend Forwardable
attr_accessor :doc
attr_accessor :content, :ext
attr_writer :output
def_delegators :@doc,
:site, :name, :ext, :extname,
:collection, :related_posts, :type,
:coffeescript_file?, :yaml_file?,
:url, :next_doc, :previous_doc
private :coffeescript_file?, :yaml_file?
# Initialize this Excerpt instance.
#
# doc - The Document.
#
# Returns the new Excerpt.
def initialize(doc)
self.doc = doc
self.content = extract_excerpt(doc.content)
end
# Fetch YAML front-matter data from related doc, without layout key
#
# Returns Hash of doc data
def data
@data ||= doc.data.dup
@data.delete("layout")
@data.delete("excerpt")
@data
end
def trigger_hooks(*); end
# 'Path' of the excerpt.
#
# Returns the path for the doc this excerpt belongs to with #excerpt appended
def path
File.join(doc.path, "#excerpt")
end
# 'Relative Path' of the excerpt.
#
# Returns the relative_path for the doc this excerpt belongs to with #excerpt appended
def relative_path
@relative_path ||= File.join(doc.relative_path, "#excerpt")
end
# Check if excerpt includes a string
#
# Returns true if the string passed in
def include?(something)
output&.include?(something) || content.include?(something)
end
# The UID for this doc (useful in feeds).
# e.g. /2008/11/05/my-awesome-doc
#
# Returns the String UID.
def id
"#{doc.id}#excerpt"
end
def to_s
output || content
end
def to_liquid
Jekyll::Drops::ExcerptDrop.new(self)
end
# Returns the shorthand String identifier of this doc.
def inspect
"<#{self.class} id=#{id}>"
end
def output
@output ||= Renderer.new(doc.site, self, site.site_payload).run
end
def place_in_layout?
false
end
def render_with_liquid?
return false if data["render_with_liquid"] == false
!(coffeescript_file? || yaml_file? || !Utils.has_liquid_construct?(content))
end
protected
# Internal: Extract excerpt from the content
#
# By default excerpt is your first paragraph of a doc: everything before
# the first two new lines:
#
# ---
# title: Example
# ---
#
# First paragraph with [link][1].
#
# Second paragraph.
#
# [1]: http://example.com/
#
# This is fairly good option for Markdown and Textile files. But might cause
# problems for HTML docs (which is quite unusual for Jekyll). If default
# excerpt delimiter is not good for you, you might want to set your own via
# configuration option `excerpt_separator`. For example, following is a good
# alternative for HTML docs:
#
# # file: _config.yml
# excerpt_separator: "<!-- more -->"
#
# Notice that all markdown-style link references will be appended to the
# excerpt. So the example doc above will have this excerpt source:
#
# First paragraph with [link][1].
#
# [1]: http://example.com/
#
# Excerpts are rendered same time as content is rendered.
#
# Returns excerpt String
LIQUID_TAG_REGEX = %r!{%-?\s*(\w+)\s*.*?-?%}!m.freeze
MKDWN_LINK_REF_REGEX = %r!^ {0,3}(?:(\[[^\]]+\])(:.+))$!.freeze
def extract_excerpt(doc_content)
head, _, tail = doc_content.to_s.partition(doc.excerpt_separator)
return head if tail.empty?
head = sanctify_liquid_tags(head) if head.include?("{%")
definitions = extract_markdown_link_reference_defintions(head, tail)
return head if definitions.empty?
head << "\n\n" << definitions.join("\n")
end
private
# append appropriate closing tag(s) (for each Liquid block), to the `head` if the
# partitioning resulted in leaving the closing tag somewhere in the `tail` partition.
def sanctify_liquid_tags(head)
modified = false
tag_names = head.scan(LIQUID_TAG_REGEX)
tag_names.flatten!
tag_names.reverse_each do |tag_name|
next unless liquid_block?(tag_name)
next if endtag_regex_stash(tag_name).match?(head)
modified = true
head << "\n{% end#{tag_name} %}"
end
print_build_warning if modified
head
end
def extract_markdown_link_reference_defintions(head, tail)
[].tap do |definitions|
tail.scan(MKDWN_LINK_REF_REGEX).each do |segments|
definitions << segments.join if head.include?(segments[0])
end
end
end
def endtag_regex_stash(tag_name)
@endtag_regex_stash ||= {}
@endtag_regex_stash[tag_name] ||= %r!{%-?\s*end#{tag_name}.*?\s*-?%}!m
end
def liquid_block?(tag_name)
return false unless tag_name.is_a?(String)
return false unless Liquid::Template.tags[tag_name]
Liquid::Template.tags[tag_name].ancestors.include?(Liquid::Block)
rescue NoMethodError
Jekyll.logger.error "Error:",
"A Liquid tag in the excerpt of #{doc.relative_path} couldn't be parsed."
raise
end
def print_build_warning
Jekyll.logger.warn "Warning:", "Excerpt modified in #{doc.relative_path}!"
Jekyll.logger.warn "", "Found a Liquid block containing the excerpt separator" \
" #{doc.excerpt_separator.inspect}. "
Jekyll.logger.warn "", "The block has been modified with the appropriate closing tag."
Jekyll.logger.warn "", "Feel free to define a custom excerpt or excerpt_separator in the"
Jekyll.logger.warn "", "document's Front Matter if the generated excerpt is unsatisfactory."
end
end
end

View File

@@ -0,0 +1,79 @@
# frozen_string_literal: true
module Jekyll
module External
class << self
#
# Gems that, if installed, should be loaded.
# Usually contain subcommands.
#
def blessed_gems
%w(
jekyll-compose
jekyll-docs
jekyll-import
)
end
#
# Require a gem or file if it's present, otherwise silently fail.
#
# names - a string gem name or array of gem names
#
def require_if_present(names)
Array(names).each do |name|
begin
require name
rescue LoadError
Jekyll.logger.debug "Couldn't load #{name}. Skipping."
yield(name, version_constraint(name)) if block_given?
false
end
end
end
#
# The version constraint required to activate a given gem.
# Usually the gem version requirement is "> 0," because any version
# will do. In the case of jekyll-docs, however, we require the exact
# same version as Jekyll.
#
# Returns a String version constraint in a parseable form for
# RubyGems.
def version_constraint(gem_name)
return "= #{Jekyll::VERSION}" if gem_name.to_s.eql?("jekyll-docs")
"> 0"
end
#
# Require a gem or gems. If it's not present, show a very nice error
# message that explains everything and is much more helpful than the
# normal LoadError.
#
# names - a string gem name or array of gem names
#
def require_with_graceful_fail(names)
Array(names).each do |name|
begin
Jekyll.logger.debug "Requiring:", name.to_s
require name
rescue LoadError => e
Jekyll.logger.error "Dependency Error:", <<~MSG
Yikes! It looks like you don't have #{name} or one of its dependencies installed.
In order to use Jekyll as currently configured, you'll need to install this gem.
If you've run Jekyll with `bundle exec`, ensure that you have included the #{name}
gem in your Gemfile as well.
The full error message from Ruby is: '#{e.message}'
If you run into trouble, you can find helpful resources at https://jekyllrb.com/help/!
MSG
raise Jekyll::Errors::MissingDependencyException, name
end
end
end
end
end
end

View File

@@ -0,0 +1,535 @@
# frozen_string_literal: true
require_all "jekyll/filters"
module Jekyll
module Filters
include URLFilters
include GroupingFilters
include DateFilters
# Convert a Markdown string into HTML output.
#
# input - The Markdown String to convert.
#
# Returns the HTML formatted String.
def markdownify(input)
@context.registers[:site].find_converter_instance(
Jekyll::Converters::Markdown
).convert(input.to_s)
end
# Convert quotes into smart quotes.
#
# input - The String to convert.
#
# Returns the smart-quotified String.
def smartify(input)
@context.registers[:site].find_converter_instance(
Jekyll::Converters::SmartyPants
).convert(input.to_s)
end
# Convert a Sass string into CSS output.
#
# input - The Sass String to convert.
#
# Returns the CSS formatted String.
def sassify(input)
@context.registers[:site].find_converter_instance(
Jekyll::Converters::Sass
).convert(input)
end
# Convert a Scss string into CSS output.
#
# input - The Scss String to convert.
#
# Returns the CSS formatted String.
def scssify(input)
@context.registers[:site].find_converter_instance(
Jekyll::Converters::Scss
).convert(input)
end
# Slugify a filename or title.
#
# input - The filename or title to slugify.
# mode - how string is slugified
#
# Returns the given filename or title as a lowercase URL String.
# See Utils.slugify for more detail.
def slugify(input, mode = nil)
Utils.slugify(input, :mode => mode)
end
# XML escape a string for use. Replaces any special characters with
# appropriate HTML entity replacements.
#
# input - The String to escape.
#
# Examples
#
# xml_escape('foo "bar" <baz>')
# # => "foo &quot;bar&quot; &lt;baz&gt;"
#
# Returns the escaped String.
def xml_escape(input)
input.to_s.encode(:xml => :attr).gsub(%r!\A"|"\Z!, "")
end
# CGI escape a string for use in a URL. Replaces any special characters
# with appropriate %XX replacements.
#
# input - The String to escape.
#
# Examples
#
# cgi_escape('foo,bar;baz?')
# # => "foo%2Cbar%3Bbaz%3F"
#
# Returns the escaped String.
def cgi_escape(input)
CGI.escape(input)
end
# URI escape a string.
#
# input - The String to escape.
#
# Examples
#
# uri_escape('foo, bar \\baz?')
# # => "foo,%20bar%20%5Cbaz?"
#
# Returns the escaped String.
def uri_escape(input)
Addressable::URI.normalize_component(input)
end
# Replace any whitespace in the input string with a single space
#
# input - The String on which to operate.
#
# Returns the formatted String
def normalize_whitespace(input)
input.to_s.gsub(%r!\s+!, " ").tap(&:strip!)
end
# Count the number of words in the input string.
#
# input - The String on which to operate.
#
# Returns the Integer word count.
def number_of_words(input, mode = nil)
cjk_charset = '\p{Han}\p{Katakana}\p{Hiragana}\p{Hangul}'
cjk_regex = %r![#{cjk_charset}]!o
word_regex = %r![^#{cjk_charset}\s]+!o
case mode
when "cjk"
input.scan(cjk_regex).length + input.scan(word_regex).length
when "auto"
cjk_count = input.scan(cjk_regex).length
cjk_count.zero? ? input.split.length : cjk_count + input.scan(word_regex).length
else
input.split.length
end
end
# Join an array of things into a string by separating with commas and the
# word "and" for the last one.
#
# array - The Array of Strings to join.
# connector - Word used to connect the last 2 items in the array
#
# Examples
#
# array_to_sentence_string(["apples", "oranges", "grapes"])
# # => "apples, oranges, and grapes"
#
# Returns the formatted String.
def array_to_sentence_string(array, connector = "and")
case array.length
when 0
""
when 1
array[0].to_s
when 2
"#{array[0]} #{connector} #{array[1]}"
else
"#{array[0...-1].join(", ")}, #{connector} #{array[-1]}"
end
end
# Convert the input into json string
#
# input - The Array or Hash to be converted
#
# Returns the converted json string
def jsonify(input)
as_liquid(input).to_json
end
# Filter an array of objects
#
# input - the object array.
# property - the property within each object to filter by.
# value - the desired value.
# Cannot be an instance of Array nor Hash since calling #to_s on them returns
# their `#inspect` string object.
#
# Returns the filtered array of objects
def where(input, property, value)
return input if !property || value.is_a?(Array) || value.is_a?(Hash)
return input unless input.respond_to?(:select)
input = input.values if input.is_a?(Hash)
input_id = input.hash
# implement a hash based on method parameters to cache the end-result
# for given parameters.
@where_filter_cache ||= {}
@where_filter_cache[input_id] ||= {}
@where_filter_cache[input_id][property] ||= {}
# stash or retrive results to return
@where_filter_cache[input_id][property][value] ||= begin
input.select do |object|
compare_property_vs_target(item_property(object, property), value)
end.to_a
end
end
# Filters an array of objects against an expression
#
# input - the object array
# variable - the variable to assign each item to in the expression
# expression - a Liquid comparison expression passed in as a string
#
# Returns the filtered array of objects
def where_exp(input, variable, expression)
return input unless input.respond_to?(:select)
input = input.values if input.is_a?(Hash) # FIXME
condition = parse_condition(expression)
@context.stack do
input.select do |object|
@context[variable] = object
condition.evaluate(@context)
end
end || []
end
# Search an array of objects and returns the first object that has the queried attribute
# with the given value or returns nil otherwise.
#
# input - the object array.
# property - the property within each object to search by.
# value - the desired value.
# Cannot be an instance of Array nor Hash since calling #to_s on them returns
# their `#inspect` string object.
#
# Returns the found object or nil
#
# rubocop:disable Metrics/CyclomaticComplexity
def find(input, property, value)
return input if !property || value.is_a?(Array) || value.is_a?(Hash)
return input unless input.respond_to?(:find)
input = input.values if input.is_a?(Hash)
input_id = input.hash
# implement a hash based on method parameters to cache the end-result for given parameters.
@find_filter_cache ||= {}
@find_filter_cache[input_id] ||= {}
@find_filter_cache[input_id][property] ||= {}
# stash or retrive results to return
# Since `enum.find` can return nil or false, we use a placeholder string "<__NO MATCH__>"
# to validate caching.
result = @find_filter_cache[input_id][property][value] ||= begin
input.find do |object|
compare_property_vs_target(item_property(object, property), value)
end || "<__NO MATCH__>"
end
return nil if result == "<__NO MATCH__>"
result
end
# rubocop:enable Metrics/CyclomaticComplexity
# Searches an array of objects against an expression and returns the first object for which
# the expression evaluates to true, or returns nil otherwise.
#
# input - the object array
# variable - the variable to assign each item to in the expression
# expression - a Liquid comparison expression passed in as a string
#
# Returns the found object or nil
def find_exp(input, variable, expression)
return input unless input.respond_to?(:find)
input = input.values if input.is_a?(Hash)
condition = parse_condition(expression)
@context.stack do
input.find do |object|
@context[variable] = object
condition.evaluate(@context)
end
end
end
# Convert the input into integer
#
# input - the object string
#
# Returns the integer value
def to_integer(input)
return 1 if input == true
return 0 if input == false
input.to_i
end
# Sort an array of objects
#
# input - the object array
# property - property within each object to filter by
# nils ('first' | 'last') - nils appear before or after non-nil values
#
# Returns the filtered array of objects
def sort(input, property = nil, nils = "first")
raise ArgumentError, "Cannot sort a null object." if input.nil?
if property.nil?
input.sort
else
case nils
when "first"
order = - 1
when "last"
order = + 1
else
raise ArgumentError, "Invalid nils order: " \
"'#{nils}' is not a valid nils order. It must be 'first' or 'last'."
end
sort_input(input, property, order)
end
end
def pop(array, num = 1)
return array unless array.is_a?(Array)
num = Liquid::Utils.to_integer(num)
new_ary = array.dup
new_ary.pop(num)
new_ary
end
def push(array, input)
return array unless array.is_a?(Array)
new_ary = array.dup
new_ary.push(input)
new_ary
end
def shift(array, num = 1)
return array unless array.is_a?(Array)
num = Liquid::Utils.to_integer(num)
new_ary = array.dup
new_ary.shift(num)
new_ary
end
def unshift(array, input)
return array unless array.is_a?(Array)
new_ary = array.dup
new_ary.unshift(input)
new_ary
end
def sample(input, num = 1)
return input unless input.respond_to?(:sample)
num = Liquid::Utils.to_integer(num) rescue 1
if num == 1
input.sample
else
input.sample(num)
end
end
# Convert an object into its String representation for debugging
#
# input - The Object to be converted
#
# Returns a String representation of the object.
def inspect(input)
xml_escape(input.inspect)
end
private
# Sort the input Enumerable by the given property.
# If the property doesn't exist, return the sort order respective of
# which item doesn't have the property.
# We also utilize the Schwartzian transform to make this more efficient.
def sort_input(input, property, order)
input.map { |item| [item_property(item, property), item] }
.sort! do |a_info, b_info|
a_property = a_info.first
b_property = b_info.first
if !a_property.nil? && b_property.nil?
- order
elsif a_property.nil? && !b_property.nil?
+ order
else
a_property <=> b_property || a_property.to_s <=> b_property.to_s
end
end
.map!(&:last)
end
# `where` filter helper
#
def compare_property_vs_target(property, target)
case target
when NilClass
return true if property.nil?
when Liquid::Expression::MethodLiteral # `empty` or `blank`
target = target.to_s
return true if property == target || Array(property).join == target
else
target = target.to_s
if property.is_a? String
return true if property == target
else
Array(property).each do |prop|
return true if prop.to_s == target
end
end
end
false
end
def item_property(item, property)
@item_property_cache ||= @context.registers[:site].filter_cache[:item_property] ||= {}
@item_property_cache[property] ||= {}
@item_property_cache[property][item] ||= begin
property = property.to_s
property = if item.respond_to?(:to_liquid)
read_liquid_attribute(item.to_liquid, property)
elsif item.respond_to?(:data)
item.data[property]
else
item[property]
end
parse_sort_input(property)
end
end
def read_liquid_attribute(liquid_data, property)
return liquid_data[property] unless property.include?(".")
property.split(".").reduce(liquid_data) do |data, key|
data.respond_to?(:[]) && data[key]
end
end
FLOAT_LIKE = %r!\A\s*-?(?:\d+\.?\d*|\.\d+)\s*\Z!.freeze
INTEGER_LIKE = %r!\A\s*-?\d+\s*\Z!.freeze
private_constant :FLOAT_LIKE, :INTEGER_LIKE
# return numeric values as numbers for proper sorting
def parse_sort_input(property)
stringified = property.to_s
return property.to_i if INTEGER_LIKE.match?(stringified)
return property.to_f if FLOAT_LIKE.match?(stringified)
property
end
def as_liquid(item)
case item
when Hash
item.each_with_object({}) { |(k, v), result| result[as_liquid(k)] = as_liquid(v) }
when Array
item.map { |i| as_liquid(i) }
else
if item.respond_to?(:to_liquid)
liquidated = item.to_liquid
# prevent infinite recursion for simple types (which return `self`)
if liquidated == item
item
else
as_liquid(liquidated)
end
else
item
end
end
end
# ----------- The following set of code was *adapted* from Liquid::If
# ----------- ref: https://git.io/vp6K6
# Parse a string to a Liquid Condition
def parse_condition(exp)
parser = Liquid::Parser.new(exp)
condition = parse_binary_comparison(parser)
parser.consume(:end_of_string)
condition
end
# Generate a Liquid::Condition object from a Liquid::Parser object additionally processing
# the parsed expression based on whether the expression consists of binary operations with
# Liquid operators `and` or `or`
#
# - parser: an instance of Liquid::Parser
#
# Returns an instance of Liquid::Condition
def parse_binary_comparison(parser)
condition = parse_comparison(parser)
first_condition = condition
while (binary_operator = parser.id?("and") || parser.id?("or"))
child_condition = parse_comparison(parser)
condition.send(binary_operator, child_condition)
condition = child_condition
end
first_condition
end
# Generates a Liquid::Condition object from a Liquid::Parser object based on whether the parsed
# expression involves a "comparison" operator (e.g. <, ==, >, !=, etc)
#
# - parser: an instance of Liquid::Parser
#
# Returns an instance of Liquid::Condition
def parse_comparison(parser)
left_operand = Liquid::Expression.parse(parser.expression)
operator = parser.consume?(:comparison)
# No comparison-operator detected. Initialize a Liquid::Condition using only left operand
return Liquid::Condition.new(left_operand) unless operator
# Parse what remained after extracting the left operand and the `:comparison` operator
# and initialize a Liquid::Condition object using the operands and the comparison-operator
Liquid::Condition.new(left_operand, operator, Liquid::Expression.parse(parser.expression))
end
end
end
Liquid::Template.register_filter(
Jekyll::Filters
)

View File

@@ -0,0 +1,110 @@
# frozen_string_literal: true
module Jekyll
module Filters
module DateFilters
# Format a date in short format e.g. "27 Jan 2011".
# Ordinal format is also supported, in both the UK
# (e.g. "27th Jan 2011") and US ("e.g. Jan 27th, 2011") formats.
# UK format is the default.
#
# date - the Time to format.
# type - if "ordinal" the returned String will be in ordinal format
# style - if "US" the returned String will be in US format.
# Otherwise it will be in UK format.
#
# Returns the formatting String.
def date_to_string(date, type = nil, style = nil)
stringify_date(date, "%b", type, style)
end
# Format a date in long format e.g. "27 January 2011".
# Ordinal format is also supported, in both the UK
# (e.g. "27th January 2011") and US ("e.g. January 27th, 2011") formats.
# UK format is the default.
#
# date - the Time to format.
# type - if "ordinal" the returned String will be in ordinal format
# style - if "US" the returned String will be in US format.
# Otherwise it will be in UK format.
#
# Returns the formatted String.
def date_to_long_string(date, type = nil, style = nil)
stringify_date(date, "%B", type, style)
end
# Format a date for use in XML.
#
# date - The Time to format.
#
# Examples
#
# date_to_xmlschema(Time.now)
# # => "2011-04-24T20:34:46+08:00"
#
# Returns the formatted String.
def date_to_xmlschema(date)
return date if date.to_s.empty?
time(date).xmlschema
end
# Format a date according to RFC-822
#
# date - The Time to format.
#
# Examples
#
# date_to_rfc822(Time.now)
# # => "Sun, 24 Apr 2011 12:34:46 +0000"
#
# Returns the formatted String.
def date_to_rfc822(date)
return date if date.to_s.empty?
time(date).rfc822
end
private
# month_type: Notations that evaluate to 'Month' via `Time#strftime` ("%b", "%B")
# type: nil (default) or "ordinal"
# style: nil (default) or "US"
#
# Returns a stringified date or the empty input.
def stringify_date(date, month_type, type = nil, style = nil)
return date if date.to_s.empty?
time = time(date)
if type == "ordinal"
day = time.day
ordinal_day = "#{day}#{ordinal(day)}"
return time.strftime("#{month_type} #{ordinal_day}, %Y") if style == "US"
return time.strftime("#{ordinal_day} #{month_type} %Y")
end
time.strftime("%d #{month_type} %Y")
end
def ordinal(number)
return "th" if (11..13).cover?(number)
case number % 10
when 1 then "st"
when 2 then "nd"
when 3 then "rd"
else "th"
end
end
def time(input)
date = Liquid::Utils.to_date(input)
unless date.respond_to?(:to_time)
raise Errors::InvalidDateError,
"Invalid Date: '#{input.inspect}' is not a valid datetime."
end
date.to_time.dup.localtime
end
end
end
end

View File

@@ -0,0 +1,64 @@
# frozen_string_literal: true
module Jekyll
module Filters
module GroupingFilters
# Group an array of items by a property
#
# input - the inputted Enumerable
# property - the property
#
# Returns an array of Hashes, each looking something like this:
# {"name" => "larry"
# "items" => [...] } # all the items where `property` == "larry"
def group_by(input, property)
if groupable?(input)
groups = input.group_by { |item| item_property(item, property).to_s }
grouped_array(groups)
else
input
end
end
# Group an array of items by an expression
#
# input - the object array
# variable - the variable to assign each item to in the expression
# expression -a Liquid comparison expression passed in as a string
#
# Returns the filtered array of objects
def group_by_exp(input, variable, expression)
return input unless groupable?(input)
parsed_expr = parse_expression(expression)
@context.stack do
groups = input.group_by do |item|
@context[variable] = item
parsed_expr.render(@context)
end
grouped_array(groups)
end
end
private
def parse_expression(str)
Liquid::Variable.new(str, Liquid::ParseContext.new)
end
def groupable?(element)
element.respond_to?(:group_by)
end
def grouped_array(groups)
groups.each_with_object([]) do |item, array|
array << {
"name" => item.first,
"items" => item.last,
"size" => item.last.size,
}
end
end
end
end
end

View File

@@ -0,0 +1,98 @@
# frozen_string_literal: true
module Jekyll
module Filters
module URLFilters
# Produces an absolute URL based on site.url and site.baseurl.
#
# input - the URL to make absolute.
#
# Returns the absolute URL as a String.
def absolute_url(input)
return if input.nil?
cache = if input.is_a?(String)
(@context.registers[:site].filter_cache[:absolute_url] ||= {})
else
(@context.registers[:cached_absolute_url] ||= {})
end
cache[input] ||= compute_absolute_url(input)
# Duplicate cached string so that the cached value is never mutated by
# a subsequent filter.
cache[input].dup
end
# Produces a URL relative to the domain root based on site.baseurl
# unless it is already an absolute url with an authority (host).
#
# input - the URL to make relative to the domain root
#
# Returns a URL relative to the domain root as a String.
def relative_url(input)
return if input.nil?
cache = if input.is_a?(String)
(@context.registers[:site].filter_cache[:relative_url] ||= {})
else
(@context.registers[:cached_relative_url] ||= {})
end
cache[input] ||= compute_relative_url(input)
# Duplicate cached string so that the cached value is never mutated by
# a subsequent filter.
cache[input].dup
end
# Strips trailing `/index.html` from URLs to create pretty permalinks
#
# input - the URL with a possible `/index.html`
#
# Returns a URL with the trailing `/index.html` removed
def strip_index(input)
return if input.nil? || input.to_s.empty?
input.sub(%r!/index\.html?$!, "/")
end
private
def compute_absolute_url(input)
input = input.url if input.respond_to?(:url)
return input if Addressable::URI.parse(input.to_s).absolute?
site = @context.registers[:site]
site_url = site.config["url"]
return relative_url(input) if site_url.nil? || site_url == ""
Addressable::URI.parse(
site_url.to_s + relative_url(input)
).normalize.to_s
end
def compute_relative_url(input)
input = input.url if input.respond_to?(:url)
return input if Addressable::URI.parse(input.to_s).absolute?
parts = [sanitized_baseurl, input]
Addressable::URI.parse(
parts.map! { |part| ensure_leading_slash(part.to_s) }.join
).normalize.to_s
end
def sanitized_baseurl
site = @context.registers[:site]
baseurl = site.config["baseurl"]
return "" if baseurl.nil?
baseurl.to_s.chomp("/")
end
def ensure_leading_slash(input)
return input if input.nil? || input.empty? || input.start_with?("/")
"/#{input}"
end
end
end
end

View File

@@ -0,0 +1,240 @@
# frozen_string_literal: true
module Jekyll
# This class handles custom defaults for YAML frontmatter settings.
# These are set in _config.yml and apply both to internal use (e.g. layout)
# and the data available to liquid.
#
# It is exposed via the frontmatter_defaults method on the site class.
class FrontmatterDefaults
# Initializes a new instance.
def initialize(site)
@site = site
end
def reset
@glob_cache = {} if @glob_cache
end
def update_deprecated_types(set)
return set unless set.key?("scope") && set["scope"].key?("type")
set["scope"]["type"] =
case set["scope"]["type"]
when "page"
Deprecator.defaults_deprecate_type("page", "pages")
"pages"
when "post"
Deprecator.defaults_deprecate_type("post", "posts")
"posts"
when "draft"
Deprecator.defaults_deprecate_type("draft", "drafts")
"drafts"
else
set["scope"]["type"]
end
set
end
def ensure_time!(set)
return set unless set.key?("values") && set["values"].key?("date")
return set if set["values"]["date"].is_a?(Time)
set["values"]["date"] = Utils.parse_date(
set["values"]["date"],
"An invalid date format was found in a front-matter default set: #{set}"
)
set
end
# Finds a default value for a given setting, filtered by path and type
#
# path - the path (relative to the source) of the page,
# post or :draft the default is used in
# type - a symbol indicating whether a :page,
# a :post or a :draft calls this method
#
# Returns the default value or nil if none was found
def find(path, type, setting)
value = nil
old_scope = nil
matching_sets(path, type).each do |set|
if set["values"].key?(setting) && has_precedence?(old_scope, set["scope"])
value = set["values"][setting]
old_scope = set["scope"]
end
end
value
end
# Collects a hash with all default values for a page or post
#
# path - the relative path of the page or post
# type - a symbol indicating the type (:post, :page or :draft)
#
# Returns a hash with all default values (an empty hash if there are none)
def all(path, type)
defaults = {}
old_scope = nil
matching_sets(path, type).each do |set|
if has_precedence?(old_scope, set["scope"])
defaults = Utils.deep_merge_hashes(defaults, set["values"])
old_scope = set["scope"]
else
defaults = Utils.deep_merge_hashes(set["values"], defaults)
end
end
defaults
end
private
# Checks if a given default setting scope matches the given path and type
#
# scope - the hash indicating the scope, as defined in _config.yml
# path - the path to check for
# type - the type (:post, :page or :draft) to check for
#
# Returns true if the scope applies to the given type and path
def applies?(scope, path, type)
applies_type?(scope, type) && applies_path?(scope, path)
end
def applies_path?(scope, path)
rel_scope_path = scope["path"]
return true if !rel_scope_path.is_a?(String) || rel_scope_path.empty?
sanitized_path = sanitize_path(path)
if rel_scope_path.include?("*")
glob_scope(sanitized_path, rel_scope_path)
else
path_is_subpath?(sanitized_path, strip_collections_dir(rel_scope_path))
end
end
def glob_scope(sanitized_path, rel_scope_path)
site_source = Pathname.new(@site.source)
abs_scope_path = site_source.join(rel_scope_path).to_s
glob_cache(abs_scope_path).each do |scope_path|
scope_path = Pathname.new(scope_path).relative_path_from(site_source).to_s
scope_path = strip_collections_dir(scope_path)
Jekyll.logger.debug "Globbed Scope Path:", scope_path
return true if path_is_subpath?(sanitized_path, scope_path)
end
false
end
def glob_cache(path)
@glob_cache ||= {}
@glob_cache[path] ||= Dir.glob(path)
end
def path_is_subpath?(path, parent_path)
path.start_with?(parent_path)
end
def strip_collections_dir(path)
collections_dir = @site.config["collections_dir"]
slashed_coll_dir = collections_dir.empty? ? "/" : "#{collections_dir}/"
return path if collections_dir.empty? || !path.to_s.start_with?(slashed_coll_dir)
path.sub(slashed_coll_dir, "")
end
# Determines whether the scope applies to type.
# The scope applies to the type if:
# 1. no 'type' is specified
# 2. the 'type' in the scope is the same as the type asked about
#
# scope - the Hash defaults set being asked about application
# type - the type of the document being processed / asked about
# its defaults.
#
# Returns true if either of the above conditions are satisfied,
# otherwise returns false
def applies_type?(scope, type)
!scope.key?("type") || type&.to_sym.eql?(scope["type"].to_sym)
end
# Checks if a given set of default values is valid
#
# set - the default value hash, as defined in _config.yml
#
# Returns true if the set is valid and can be used in this class
def valid?(set)
set.is_a?(Hash) && set["values"].is_a?(Hash)
end
# Determines if a new scope has precedence over an old one
#
# old_scope - the old scope hash, or nil if there's none
# new_scope - the new scope hash
#
# Returns true if the new scope has precedence over the older
# rubocop: disable Naming/PredicateName
def has_precedence?(old_scope, new_scope)
return true if old_scope.nil?
new_path = sanitize_path(new_scope["path"])
old_path = sanitize_path(old_scope["path"])
if new_path.length != old_path.length
new_path.length >= old_path.length
elsif new_scope.key?("type")
true
else
!old_scope.key? "type"
end
end
# rubocop: enable Naming/PredicateName
# Collects a list of sets that match the given path and type
#
# Returns an array of hashes
def matching_sets(path, type)
@matched_set_cache ||= {}
@matched_set_cache[path] ||= {}
@matched_set_cache[path][type] ||= begin
valid_sets.select do |set|
!set.key?("scope") || applies?(set["scope"], path, type)
end
end
end
# Returns a list of valid sets
#
# This is not cached to allow plugins to modify the configuration
# and have their changes take effect
#
# Returns an array of hashes
def valid_sets
sets = @site.config["defaults"]
return [] unless sets.is_a?(Array)
sets.map do |set|
if valid?(set)
ensure_time!(update_deprecated_types(set))
else
Jekyll.logger.warn "Defaults:", "An invalid front-matter default set was found:"
Jekyll.logger.warn set.to_s
nil
end
end.tap(&:compact!)
end
# Sanitizes the given path by removing a leading slash
def sanitize_path(path)
if path.nil? || path.empty?
""
elsif path.start_with?("/")
path.gsub(%r!\A/|(?<=[^/])\z!, "")
else
path
end
end
end
end

View File

@@ -0,0 +1,5 @@
# frozen_string_literal: true
module Jekyll
Generator = Class.new(Plugin)
end

View File

@@ -0,0 +1,107 @@
# frozen_string_literal: true
module Jekyll
module Hooks
DEFAULT_PRIORITY = 20
# compatibility layer for octopress-hooks users
PRIORITY_MAP = {
:low => 10,
:normal => 20,
:high => 30,
}.freeze
# initial empty hooks
@registry = {
:site => {
:after_init => [],
:after_reset => [],
:post_read => [],
:pre_render => [],
:post_render => [],
:post_write => [],
},
:pages => {
:post_init => [],
:pre_render => [],
:post_convert => [],
:post_render => [],
:post_write => [],
},
:posts => {
:post_init => [],
:pre_render => [],
:post_convert => [],
:post_render => [],
:post_write => [],
},
:documents => {
:post_init => [],
:pre_render => [],
:post_convert => [],
:post_render => [],
:post_write => [],
},
:clean => {
:on_obsolete => [],
},
}
# map of all hooks and their priorities
@hook_priority = {}
NotAvailable = Class.new(RuntimeError)
Uncallable = Class.new(RuntimeError)
# register hook(s) to be called later, public API
def self.register(owners, event, priority: DEFAULT_PRIORITY, &block)
Array(owners).each do |owner|
register_one(owner, event, priority_value(priority), &block)
end
end
# Ensure the priority is a Fixnum
def self.priority_value(priority)
return priority if priority.is_a?(Integer)
PRIORITY_MAP[priority] || DEFAULT_PRIORITY
end
# register a single hook to be called later, internal API
def self.register_one(owner, event, priority, &block)
@registry[owner] ||= {
:post_init => [],
:pre_render => [],
:post_convert => [],
:post_render => [],
:post_write => [],
}
unless @registry[owner][event]
raise NotAvailable, "Invalid hook. #{owner} supports only the " \
"following hooks #{@registry[owner].keys.inspect}"
end
raise Uncallable, "Hooks must respond to :call" unless block.respond_to? :call
insert_hook owner, event, priority, &block
end
def self.insert_hook(owner, event, priority, &block)
@hook_priority[block] = [-priority, @hook_priority.size]
@registry[owner][event] << block
end
# interface for Jekyll core components to trigger hooks
def self.trigger(owner, event, *args)
# proceed only if there are hooks to call
hooks = @registry.dig(owner, event)
return if hooks.nil? || hooks.empty?
# sort and call hooks according to priority and load order
hooks.sort_by { |h| @hook_priority[h] }.each do |hook|
hook.call(*args)
end
end
end
end

View File

@@ -0,0 +1,32 @@
# frozen_string_literal: true
module Jekyll
class Inclusion
attr_reader :site, :name, :path
private :site
def initialize(site, base, name)
@site = site
@name = name
@path = PathManager.join(base, name)
end
def render(context)
@template ||= site.liquid_renderer.file(path).parse(content)
@template.render!(context)
rescue Liquid::Error => e
e.template_name = path
e.markup_context = "included " if e.markup_context.nil?
raise e
end
def content
@content ||= File.read(path, **site.file_read_opts)
end
def inspect
"#{self.class} #{path.inspect}"
end
alias_method :to_s, :inspect
end
end

View File

@@ -0,0 +1,67 @@
# frozen_string_literal: true
module Jekyll
class Layout
include Convertible
# Gets the Site object.
attr_reader :site
# Gets the name of this layout.
attr_reader :name
# Gets the path to this layout.
attr_reader :path
# Gets the path to this layout relative to its base
attr_reader :relative_path
# Gets/Sets the extension of this layout.
attr_accessor :ext
# Gets/Sets the Hash that holds the metadata for this layout.
attr_accessor :data
# Gets/Sets the content of this layout.
attr_accessor :content
# Initialize a new Layout.
#
# site - The Site.
# base - The String path to the source.
# name - The String filename of the post file.
def initialize(site, base, name)
@site = site
@base = base
@name = name
if site.theme && site.theme.layouts_path.eql?(base)
@base_dir = site.theme.root
@path = site.in_theme_dir(base, name)
else
@base_dir = site.source
@path = site.in_source_dir(base, name)
end
@relative_path = @path.sub(@base_dir, "")
self.data = {}
process(name)
read_yaml(base, name)
end
# Extract information from the layout filename.
#
# name - The String filename of the layout file.
#
# Returns nothing.
def process(name)
self.ext = File.extname(name)
end
# Returns the object as a debug String.
def inspect
"#<#{self.class} @path=#{@path.inspect}>"
end
end
end

View File

@@ -0,0 +1,22 @@
# frozen_string_literal: true
module Jekyll
module LiquidExtensions
# Lookup a Liquid variable in the given context.
#
# context - the Liquid context in question.
# variable - the variable name, as a string.
#
# Returns the value of the variable in the context
# or the variable name if not found.
def lookup_variable(context, variable)
lookup = context
variable.split(".").each do |value|
lookup = lookup[value]
end
lookup || variable
end
end
end

View File

@@ -0,0 +1,80 @@
# frozen_string_literal: true
require_relative "liquid_renderer/file"
require_relative "liquid_renderer/table"
module Jekyll
class LiquidRenderer
def initialize(site)
@site = site
Liquid::Template.error_mode = @site.config["liquid"]["error_mode"].to_sym
reset
end
def reset
@stats = {}
@cache = {}
end
def file(filename)
filename = normalize_path(filename)
LiquidRenderer::File.new(self, filename).tap do
@stats[filename] ||= new_profile_hash
end
end
def increment_bytes(filename, bytes)
@stats[filename][:bytes] += bytes
end
def increment_time(filename, time)
@stats[filename][:time] += time
end
def increment_count(filename)
@stats[filename][:count] += 1
end
def stats_table(num_of_rows = 50)
LiquidRenderer::Table.new(@stats).to_s(num_of_rows)
end
def self.format_error(error, path)
"#{error.message} in #{path}"
end
# A persistent cache to store and retrieve parsed templates based on the filename
# via `LiquidRenderer::File#parse`
#
# It is emptied when `self.reset` is called.
def cache
@cache ||= {}
end
private
def normalize_path(filename)
@normalize_path ||= {}
@normalize_path[filename] ||= begin
theme_dir = @site.theme&.root
case filename
when %r!\A(#{Regexp.escape(@site.source)}/)(?<rest>.*)!io
Regexp.last_match(:rest)
when %r!(/gems/.*)*/gems/(?<dirname>[^/]+)(?<rest>.*)!,
%r!(?<dirname>[^/]+/lib)(?<rest>.*)!
"#{Regexp.last_match(:dirname)}#{Regexp.last_match(:rest)}"
when theme_dir && %r!\A#{Regexp.escape(theme_dir)}/(?<rest>.*)!io
PathManager.join(@site.theme.basename, Regexp.last_match(:rest))
when %r!\A/(.*)!
Regexp.last_match(1)
else
filename
end
end
end
def new_profile_hash
Hash.new { |hash, key| hash[key] = 0 }
end
end
end

View File

@@ -0,0 +1,77 @@
# frozen_string_literal: true
module Jekyll
class LiquidRenderer
class File
def initialize(renderer, filename)
@renderer = renderer
@filename = filename
end
def parse(content)
measure_time do
@renderer.cache[@filename] ||= Liquid::Template.parse(content, :line_numbers => true)
end
@template = @renderer.cache[@filename]
self
end
def render(*args)
reset_template_assigns
measure_time do
measure_bytes do
measure_counts do
@template.render(*args)
end
end
end
end
# This method simply 'rethrows any error' before attempting to render the template.
def render!(*args)
reset_template_assigns
measure_time do
measure_bytes do
measure_counts do
@template.render!(*args)
end
end
end
end
def warnings
@template.warnings
end
private
# clear assigns to `Liquid::Template` instance prior to rendering since
# `Liquid::Template` instances are cached in Jekyll 4.
def reset_template_assigns
@template.instance_assigns.clear
end
def measure_counts
@renderer.increment_count(@filename)
yield
end
def measure_bytes
yield.tap do |str|
@renderer.increment_bytes(@filename, str.bytesize)
end
end
def measure_time
before = Time.now
yield
ensure
after = Time.now
@renderer.increment_time(@filename, after - before)
end
end
end
end

View File

@@ -0,0 +1,55 @@
# frozen_string_literal: true
module Jekyll
class LiquidRenderer
class Table
GAUGES = [:count, :bytes, :time].freeze
def initialize(stats)
@stats = stats
end
def to_s(num_of_rows = 50)
Jekyll::Profiler.tabulate(data_for_table(num_of_rows))
end
private
# rubocop:disable Metrics/AbcSize
def data_for_table(num_of_rows)
sorted = @stats.sort_by { |_, file_stats| -file_stats[:time] }
sorted = sorted.slice(0, num_of_rows)
table = [header_labels]
totals = Hash.new { |hash, key| hash[key] = 0 }
sorted.each do |filename, file_stats|
GAUGES.each { |gauge| totals[gauge] += file_stats[gauge] }
row = []
row << filename
row << file_stats[:count].to_s
row << format_bytes(file_stats[:bytes])
row << format("%.3f", file_stats[:time])
table << row
end
footer = []
footer << "TOTAL (for #{sorted.size} files)"
footer << totals[:count].to_s
footer << format_bytes(totals[:bytes])
footer << format("%.3f", totals[:time])
table << footer
end
# rubocop:enable Metrics/AbcSize
def header_labels
GAUGES.map { |gauge| gauge.to_s.capitalize }.unshift("Filename")
end
def format_bytes(bytes)
bytes /= 1024.0
format("%.2fK", bytes)
end
end
end
end

View File

@@ -0,0 +1,151 @@
# frozen_string_literal: true
module Jekyll
class LogAdapter
attr_reader :writer, :messages, :level
LOG_LEVELS = {
:debug => ::Logger::DEBUG,
:info => ::Logger::INFO,
:warn => ::Logger::WARN,
:error => ::Logger::ERROR,
}.freeze
# Public: Create a new instance of a log writer
#
# writer - Logger compatible instance
# log_level - (optional, symbol) the log level
#
# Returns nothing
def initialize(writer, level = :info)
@messages = []
@writer = writer
self.log_level = level
end
# Public: Set the log level on the writer
#
# level - (symbol) the log level
#
# Returns nothing
def log_level=(level)
writer.level = level if level.is_a?(Integer) && level.between?(0, 3)
writer.level = LOG_LEVELS[level] ||
raise(ArgumentError, "unknown log level")
@level = level
end
def adjust_verbosity(options = {})
# Quiet always wins.
if options[:quiet]
self.log_level = :error
elsif options[:verbose]
self.log_level = :debug
end
debug "Logging at level:", LOG_LEVELS.key(writer.level).to_s
debug "Jekyll Version:", Jekyll::VERSION
end
# Public: Print a debug message
#
# topic - the topic of the message, e.g. "Configuration file", "Deprecation", etc.
# message - the message detail
#
# Returns nothing
def debug(topic, message = nil, &block)
write(:debug, topic, message, &block)
end
# Public: Print a message
#
# topic - the topic of the message, e.g. "Configuration file", "Deprecation", etc.
# message - the message detail
#
# Returns nothing
def info(topic, message = nil, &block)
write(:info, topic, message, &block)
end
# Public: Print a message
#
# topic - the topic of the message, e.g. "Configuration file", "Deprecation", etc.
# message - the message detail
#
# Returns nothing
def warn(topic, message = nil, &block)
write(:warn, topic, message, &block)
end
# Public: Print an error message
#
# topic - the topic of the message, e.g. "Configuration file", "Deprecation", etc.
# message - the message detail
#
# Returns nothing
def error(topic, message = nil, &block)
write(:error, topic, message, &block)
end
# Public: Print an error message and immediately abort the process
#
# topic - the topic of the message, e.g. "Configuration file", "Deprecation", etc.
# message - the message detail (can be omitted)
#
# Returns nothing
def abort_with(topic, message = nil, &block)
error(topic, message, &block)
abort
end
# Internal: Build a topic method
#
# topic - the topic of the message, e.g. "Configuration file", "Deprecation", etc.
# message - the message detail
#
# Returns the formatted message
def message(topic, message = nil)
raise ArgumentError, "block or message, not both" if block_given? && message
message = yield if block_given?
message = message.to_s.gsub(%r!\s+!, " ")
topic = formatted_topic(topic, block_given?)
out = topic + message
messages << out
out
end
# Internal: Format the topic
#
# topic - the topic of the message, e.g. "Configuration file", "Deprecation", etc.
# colon -
#
# Returns the formatted topic statement
def formatted_topic(topic, colon = false)
"#{topic}#{colon ? ": " : " "}".rjust(20)
end
# Internal: Check if the message should be written given the log level.
#
# level_of_message - the Symbol level of message, one of :debug, :info, :warn, :error
#
# Returns whether the message should be written.
def write_message?(level_of_message)
LOG_LEVELS.fetch(level) <= LOG_LEVELS.fetch(level_of_message)
end
# Internal: Log a message.
#
# level_of_message - the Symbol level of message, one of :debug, :info, :warn, :error
# topic - the String topic or full message
# message - the String message (optional)
# block - a block containing the message (optional)
#
# Returns false if the message was not written, otherwise returns the value of calling
# the appropriate writer method, e.g. writer.info.
def write(level_of_message, topic, message = nil, &block)
return false unless write_message?(level_of_message)
writer.public_send(level_of_message, message(topic, message, &block))
end
end
end

View File

@@ -0,0 +1,867 @@
# Woah there. Do not edit this file directly.
# This file is generated automatically by script/vendor-mimes.
application/andrew-inset ez
application/applixware aw
application/atom+xml atom
application/atomcat+xml atomcat
application/atomsvc+xml atomsvc
application/bdoc bdoc
application/ccxml+xml ccxml
application/cdmi-capability cdmia
application/cdmi-container cdmic
application/cdmi-domain cdmid
application/cdmi-object cdmio
application/cdmi-queue cdmiq
application/cu-seeme cu
application/dash+xml mpd
application/davmount+xml davmount
application/docbook+xml dbk
application/dssc+der dssc
application/dssc+xml xdssc
application/ecmascript ecma es
application/emma+xml emma
application/epub+zip epub
application/exi exi
application/font-tdpfr pfr
application/geo+json geojson
application/gml+xml gml
application/gpx+xml gpx
application/gxf gxf
application/gzip gz
application/hjson hjson
application/hyperstudio stk
application/inkml+xml ink inkml
application/ipfix ipfix
application/java-archive jar war ear
application/java-serialized-object ser
application/java-vm class
application/javascript js mjs
application/json json map
application/json5 json5
application/jsonml+json jsonml
application/ld+json jsonld
application/lost+xml lostxml
application/mac-binhex40 hqx
application/mac-compactpro cpt
application/mads+xml mads
application/manifest+json webmanifest
application/marc mrc
application/marcxml+xml mrcx
application/mathematica ma nb mb
application/mathml+xml mathml
application/mbox mbox
application/mediaservercontrol+xml mscml
application/metalink+xml metalink
application/metalink4+xml meta4
application/mets+xml mets
application/mods+xml mods
application/mp21 m21 mp21
application/mp4 mp4s m4p
application/msword doc dot
application/mxf mxf
application/n-quads nq
application/n-triples nt
application/octet-stream bin dms lrf mar so dist distz pkg bpk dump elc deploy exe dll deb dmg iso img msi msp msm buffer
application/oda oda
application/oebps-package+xml opf
application/ogg ogx
application/omdoc+xml omdoc
application/onenote onetoc onetoc2 onetmp onepkg
application/oxps oxps
application/patch-ops-error+xml xer
application/pdf pdf
application/pgp-encrypted pgp
application/pgp-signature asc sig
application/pics-rules prf
application/pkcs10 p10
application/pkcs7-mime p7m p7c
application/pkcs7-signature p7s
application/pkcs8 p8
application/pkix-attr-cert ac
application/pkix-cert cer
application/pkix-crl crl
application/pkix-pkipath pkipath
application/pkixcmp pki
application/pls+xml pls
application/postscript ai eps ps
application/prs.cww cww
application/pskc+xml pskcxml
application/raml+yaml raml
application/rdf+xml rdf owl
application/reginfo+xml rif
application/relax-ng-compact-syntax rnc
application/resource-lists+xml rl
application/resource-lists-diff+xml rld
application/rls-services+xml rs
application/rpki-ghostbusters gbr
application/rpki-manifest mft
application/rpki-roa roa
application/rsd+xml rsd
application/rss+xml rss
application/rtf rtf
application/sbml+xml sbml
application/scvp-cv-request scq
application/scvp-cv-response scs
application/scvp-vp-request spq
application/scvp-vp-response spp
application/sdp sdp
application/set-payment-initiation setpay
application/set-registration-initiation setreg
application/shf+xml shf
application/sieve siv sieve
application/smil+xml smi smil
application/sparql-query rq
application/sparql-results+xml srx
application/srgs gram
application/srgs+xml grxml
application/sru+xml sru
application/ssdl+xml ssdl
application/ssml+xml ssml
application/tei+xml tei teicorpus
application/thraud+xml tfi
application/timestamped-data tsd
application/vnd.3gpp.pic-bw-large plb
application/vnd.3gpp.pic-bw-small psb
application/vnd.3gpp.pic-bw-var pvb
application/vnd.3gpp2.tcap tcap
application/vnd.3m.post-it-notes pwn
application/vnd.accpac.simply.aso aso
application/vnd.accpac.simply.imp imp
application/vnd.acucobol acu
application/vnd.acucorp atc acutc
application/vnd.adobe.air-application-installer-package+zip air
application/vnd.adobe.formscentral.fcdt fcdt
application/vnd.adobe.fxp fxp fxpl
application/vnd.adobe.xdp+xml xdp
application/vnd.adobe.xfdf xfdf
application/vnd.ahead.space ahead
application/vnd.airzip.filesecure.azf azf
application/vnd.airzip.filesecure.azs azs
application/vnd.amazon.ebook azw
application/vnd.americandynamics.acc acc
application/vnd.amiga.ami ami
application/vnd.android.package-archive apk
application/vnd.anser-web-certificate-issue-initiation cii
application/vnd.anser-web-funds-transfer-initiation fti
application/vnd.antix.game-component atx
application/vnd.apple.installer+xml mpkg
application/vnd.apple.keynote keynote
application/vnd.apple.mpegurl m3u8
application/vnd.apple.numbers numbers
application/vnd.apple.pages pages
application/vnd.apple.pkpass pkpass
application/vnd.aristanetworks.swi swi
application/vnd.astraea-software.iota iota
application/vnd.audiograph aep
application/vnd.blueice.multipass mpm
application/vnd.bmi bmi
application/vnd.businessobjects rep
application/vnd.chemdraw+xml cdxml
application/vnd.chipnuts.karaoke-mmd mmd
application/vnd.cinderella cdy
application/vnd.citationstyles.style+xml csl
application/vnd.claymore cla
application/vnd.cloanto.rp9 rp9
application/vnd.clonk.c4group c4g c4d c4f c4p c4u
application/vnd.cluetrust.cartomobile-config c11amc
application/vnd.cluetrust.cartomobile-config-pkg c11amz
application/vnd.commonspace csp
application/vnd.contact.cmsg cdbcmsg
application/vnd.cosmocaller cmc
application/vnd.crick.clicker clkx
application/vnd.crick.clicker.keyboard clkk
application/vnd.crick.clicker.palette clkp
application/vnd.crick.clicker.template clkt
application/vnd.crick.clicker.wordbank clkw
application/vnd.criticaltools.wbs+xml wbs
application/vnd.ctc-posml pml
application/vnd.cups-ppd ppd
application/vnd.curl.car car
application/vnd.curl.pcurl pcurl
application/vnd.dart dart
application/vnd.data-vision.rdz rdz
application/vnd.dece.data uvf uvvf uvd uvvd
application/vnd.dece.ttml+xml uvt uvvt
application/vnd.dece.unspecified uvx uvvx
application/vnd.dece.zip uvz uvvz
application/vnd.denovo.fcselayout-link fe_launch
application/vnd.dna dna
application/vnd.dolby.mlp mlp
application/vnd.dpgraph dpg
application/vnd.dreamfactory dfac
application/vnd.ds-keypoint kpxx
application/vnd.dvb.ait ait
application/vnd.dvb.service svc
application/vnd.dynageo geo
application/vnd.ecowin.chart mag
application/vnd.enliven nml
application/vnd.epson.esf esf
application/vnd.epson.msf msf
application/vnd.epson.quickanime qam
application/vnd.epson.salt slt
application/vnd.epson.ssf ssf
application/vnd.eszigno3+xml es3 et3
application/vnd.ezpix-album ez2
application/vnd.ezpix-package ez3
application/vnd.fdf fdf
application/vnd.fdsn.mseed mseed
application/vnd.fdsn.seed seed dataless
application/vnd.flographit gph
application/vnd.fluxtime.clip ftc
application/vnd.framemaker fm frame maker book
application/vnd.frogans.fnc fnc
application/vnd.frogans.ltf ltf
application/vnd.fsc.weblaunch fsc
application/vnd.fujitsu.oasys oas
application/vnd.fujitsu.oasys2 oa2
application/vnd.fujitsu.oasys3 oa3
application/vnd.fujitsu.oasysgp fg5
application/vnd.fujitsu.oasysprs bh2
application/vnd.fujixerox.ddd ddd
application/vnd.fujixerox.docuworks xdw
application/vnd.fujixerox.docuworks.binder xbd
application/vnd.fuzzysheet fzs
application/vnd.genomatix.tuxedo txd
application/vnd.geogebra.file ggb
application/vnd.geogebra.tool ggt
application/vnd.geometry-explorer gex gre
application/vnd.geonext gxt
application/vnd.geoplan g2w
application/vnd.geospace g3w
application/vnd.gmx gmx
application/vnd.google-apps.document gdoc
application/vnd.google-apps.presentation gslides
application/vnd.google-apps.spreadsheet gsheet
application/vnd.google-earth.kml+xml kml
application/vnd.google-earth.kmz kmz
application/vnd.grafeq gqf gqs
application/vnd.groove-account gac
application/vnd.groove-help ghf
application/vnd.groove-identity-message gim
application/vnd.groove-injector grv
application/vnd.groove-tool-message gtm
application/vnd.groove-tool-template tpl
application/vnd.groove-vcard vcg
application/vnd.hal+xml hal
application/vnd.handheld-entertainment+xml zmm
application/vnd.hbci hbci
application/vnd.hhe.lesson-player les
application/vnd.hp-hpgl hpgl
application/vnd.hp-hpid hpid
application/vnd.hp-hps hps
application/vnd.hp-jlyt jlt
application/vnd.hp-pcl pcl
application/vnd.hp-pclxl pclxl
application/vnd.hydrostatix.sof-data sfd-hdstx
application/vnd.ibm.minipay mpy
application/vnd.ibm.modcap afp listafp list3820
application/vnd.ibm.rights-management irm
application/vnd.ibm.secure-container sc
application/vnd.iccprofile icc icm
application/vnd.igloader igl
application/vnd.immervision-ivp ivp
application/vnd.immervision-ivu ivu
application/vnd.insors.igm igm
application/vnd.intercon.formnet xpw xpx
application/vnd.intergeo i2g
application/vnd.intu.qbo qbo
application/vnd.intu.qfx qfx
application/vnd.ipunplugged.rcprofile rcprofile
application/vnd.irepository.package+xml irp
application/vnd.is-xpr xpr
application/vnd.isac.fcs fcs
application/vnd.jam jam
application/vnd.jcp.javame.midlet-rms rms
application/vnd.jisp jisp
application/vnd.joost.joda-archive joda
application/vnd.kahootz ktz ktr
application/vnd.kde.karbon karbon
application/vnd.kde.kchart chrt
application/vnd.kde.kformula kfo
application/vnd.kde.kivio flw
application/vnd.kde.kontour kon
application/vnd.kde.kpresenter kpr kpt
application/vnd.kde.kspread ksp
application/vnd.kde.kword kwd kwt
application/vnd.kenameaapp htke
application/vnd.kidspiration kia
application/vnd.kinar kne knp
application/vnd.koan skp skd skt skm
application/vnd.kodak-descriptor sse
application/vnd.las.las+xml lasxml
application/vnd.llamagraphics.life-balance.desktop lbd
application/vnd.llamagraphics.life-balance.exchange+xml lbe
application/vnd.lotus-1-2-3 123
application/vnd.lotus-approach apr
application/vnd.lotus-freelance pre
application/vnd.lotus-notes nsf
application/vnd.lotus-organizer org
application/vnd.lotus-screencam scm
application/vnd.lotus-wordpro lwp
application/vnd.macports.portpkg portpkg
application/vnd.mcd mcd
application/vnd.medcalcdata mc1
application/vnd.mediastation.cdkey cdkey
application/vnd.mfer mwf
application/vnd.mfmp mfm
application/vnd.micrografx.flo flo
application/vnd.micrografx.igx igx
application/vnd.mif mif
application/vnd.mobius.daf daf
application/vnd.mobius.dis dis
application/vnd.mobius.mbk mbk
application/vnd.mobius.mqy mqy
application/vnd.mobius.msl msl
application/vnd.mobius.plc plc
application/vnd.mobius.txf txf
application/vnd.mophun.application mpn
application/vnd.mophun.certificate mpc
application/vnd.mozilla.xul+xml xul
application/vnd.ms-artgalry cil
application/vnd.ms-cab-compressed cab
application/vnd.ms-excel xls xlm xla xlc xlt xlw
application/vnd.ms-excel.addin.macroenabled.12 xlam
application/vnd.ms-excel.sheet.binary.macroenabled.12 xlsb
application/vnd.ms-excel.sheet.macroenabled.12 xlsm
application/vnd.ms-excel.template.macroenabled.12 xltm
application/vnd.ms-fontobject eot
application/vnd.ms-htmlhelp chm
application/vnd.ms-ims ims
application/vnd.ms-lrm lrm
application/vnd.ms-officetheme thmx
application/vnd.ms-outlook msg
application/vnd.ms-pki.seccat cat
application/vnd.ms-pki.stl stl
application/vnd.ms-powerpoint ppt pps pot
application/vnd.ms-powerpoint.addin.macroenabled.12 ppam
application/vnd.ms-powerpoint.presentation.macroenabled.12 pptm
application/vnd.ms-powerpoint.slide.macroenabled.12 sldm
application/vnd.ms-powerpoint.slideshow.macroenabled.12 ppsm
application/vnd.ms-powerpoint.template.macroenabled.12 potm
application/vnd.ms-project mpp mpt
application/vnd.ms-word.document.macroenabled.12 docm
application/vnd.ms-word.template.macroenabled.12 dotm
application/vnd.ms-works wps wks wcm wdb
application/vnd.ms-wpl wpl
application/vnd.ms-xpsdocument xps
application/vnd.mseq mseq
application/vnd.musician mus
application/vnd.muvee.style msty
application/vnd.mynfc taglet
application/vnd.neurolanguage.nlu nlu
application/vnd.nitf ntf nitf
application/vnd.noblenet-directory nnd
application/vnd.noblenet-sealer nns
application/vnd.noblenet-web nnw
application/vnd.nokia.n-gage.data ngdat
application/vnd.nokia.n-gage.symbian.install n-gage
application/vnd.nokia.radio-preset rpst
application/vnd.nokia.radio-presets rpss
application/vnd.novadigm.edm edm
application/vnd.novadigm.edx edx
application/vnd.novadigm.ext ext
application/vnd.oasis.opendocument.chart odc
application/vnd.oasis.opendocument.chart-template otc
application/vnd.oasis.opendocument.database odb
application/vnd.oasis.opendocument.formula odf
application/vnd.oasis.opendocument.formula-template odft
application/vnd.oasis.opendocument.graphics odg
application/vnd.oasis.opendocument.graphics-template otg
application/vnd.oasis.opendocument.image odi
application/vnd.oasis.opendocument.image-template oti
application/vnd.oasis.opendocument.presentation odp
application/vnd.oasis.opendocument.presentation-template otp
application/vnd.oasis.opendocument.spreadsheet ods
application/vnd.oasis.opendocument.spreadsheet-template ots
application/vnd.oasis.opendocument.text odt
application/vnd.oasis.opendocument.text-master odm
application/vnd.oasis.opendocument.text-template ott
application/vnd.oasis.opendocument.text-web oth
application/vnd.olpc-sugar xo
application/vnd.oma.dd2+xml dd2
application/vnd.openofficeorg.extension oxt
application/vnd.openxmlformats-officedocument.presentationml.presentation pptx
application/vnd.openxmlformats-officedocument.presentationml.slide sldx
application/vnd.openxmlformats-officedocument.presentationml.slideshow ppsx
application/vnd.openxmlformats-officedocument.presentationml.template potx
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet xlsx
application/vnd.openxmlformats-officedocument.spreadsheetml.template xltx
application/vnd.openxmlformats-officedocument.wordprocessingml.document docx
application/vnd.openxmlformats-officedocument.wordprocessingml.template dotx
application/vnd.osgeo.mapguide.package mgp
application/vnd.osgi.dp dp
application/vnd.osgi.subsystem esa
application/vnd.palm pdb pqa oprc
application/vnd.pawaafile paw
application/vnd.pg.format str
application/vnd.pg.osasli ei6
application/vnd.picsel efif
application/vnd.pmi.widget wg
application/vnd.pocketlearn plf
application/vnd.powerbuilder6 pbd
application/vnd.previewsystems.box box
application/vnd.proteus.magazine mgz
application/vnd.publishare-delta-tree qps
application/vnd.pvi.ptid1 ptid
application/vnd.quark.quarkxpress qxd qxt qwd qwt qxl qxb
application/vnd.realvnc.bed bed
application/vnd.recordare.musicxml mxl
application/vnd.recordare.musicxml+xml musicxml
application/vnd.rig.cryptonote cryptonote
application/vnd.rim.cod cod
application/vnd.rn-realmedia rm
application/vnd.rn-realmedia-vbr rmvb
application/vnd.route66.link66+xml link66
application/vnd.sailingtracker.track st
application/vnd.seemail see
application/vnd.sema sema
application/vnd.semd semd
application/vnd.semf semf
application/vnd.shana.informed.formdata ifm
application/vnd.shana.informed.formtemplate itp
application/vnd.shana.informed.interchange iif
application/vnd.shana.informed.package ipk
application/vnd.simtech-mindmapper twd twds
application/vnd.smaf mmf
application/vnd.smart.teacher teacher
application/vnd.solent.sdkm+xml sdkm sdkd
application/vnd.spotfire.dxp dxp
application/vnd.spotfire.sfs sfs
application/vnd.stardivision.calc sdc
application/vnd.stardivision.draw sda
application/vnd.stardivision.impress sdd
application/vnd.stardivision.math smf
application/vnd.stardivision.writer sdw vor
application/vnd.stardivision.writer-global sgl
application/vnd.stepmania.package smzip
application/vnd.stepmania.stepchart sm
application/vnd.sun.wadl+xml wadl
application/vnd.sun.xml.calc sxc
application/vnd.sun.xml.calc.template stc
application/vnd.sun.xml.draw sxd
application/vnd.sun.xml.draw.template std
application/vnd.sun.xml.impress sxi
application/vnd.sun.xml.impress.template sti
application/vnd.sun.xml.math sxm
application/vnd.sun.xml.writer sxw
application/vnd.sun.xml.writer.global sxg
application/vnd.sun.xml.writer.template stw
application/vnd.sus-calendar sus susp
application/vnd.svd svd
application/vnd.symbian.install sis sisx
application/vnd.syncml+xml xsm
application/vnd.syncml.dm+wbxml bdm
application/vnd.syncml.dm+xml xdm
application/vnd.tao.intent-module-archive tao
application/vnd.tcpdump.pcap pcap cap dmp
application/vnd.tmobile-livetv tmo
application/vnd.trid.tpt tpt
application/vnd.triscape.mxs mxs
application/vnd.trueapp tra
application/vnd.ufdl ufd ufdl
application/vnd.uiq.theme utz
application/vnd.umajin umj
application/vnd.unity unityweb
application/vnd.uoml+xml uoml
application/vnd.vcx vcx
application/vnd.visio vsd vst vss vsw
application/vnd.visionary vis
application/vnd.vsf vsf
application/vnd.wap.wbxml wbxml
application/vnd.wap.wmlc wmlc
application/vnd.wap.wmlscriptc wmlsc
application/vnd.webturbo wtb
application/vnd.wolfram.player nbp
application/vnd.wordperfect wpd
application/vnd.wqd wqd
application/vnd.wt.stf stf
application/vnd.xara xar
application/vnd.xfdl xfdl
application/vnd.yamaha.hv-dic hvd
application/vnd.yamaha.hv-script hvs
application/vnd.yamaha.hv-voice hvp
application/vnd.yamaha.openscoreformat osf
application/vnd.yamaha.openscoreformat.osfpvg+xml osfpvg
application/vnd.yamaha.smaf-audio saf
application/vnd.yamaha.smaf-phrase spf
application/vnd.yellowriver-custom-menu cmp
application/vnd.zul zir zirz
application/vnd.zzazz.deck+xml zaz
application/voicexml+xml vxml
application/wasm wasm
application/widget wgt
application/winhlp hlp
application/wsdl+xml wsdl
application/wspolicy+xml wspolicy
application/x-7z-compressed 7z
application/x-abiword abw
application/x-ace-compressed ace
application/x-arj arj
application/x-authorware-bin aab x32 u32 vox
application/x-authorware-map aam
application/x-authorware-seg aas
application/x-bcpio bcpio
application/x-bittorrent torrent
application/x-blorb blb blorb
application/x-bzip bz
application/x-bzip2 bz2 boz
application/x-cbr cbr cba cbt cbz cb7
application/x-cdlink vcd
application/x-cfs-compressed cfs
application/x-chat chat
application/x-chess-pgn pgn
application/x-chrome-extension crx
application/x-cocoa cco
application/x-conference nsc
application/x-cpio cpio
application/x-csh csh
application/x-debian-package udeb
application/x-dgc-compressed dgc
application/x-director dir dcr dxr cst cct cxt w3d fgd swa
application/x-doom wad
application/x-dtbncx+xml ncx
application/x-dtbook+xml dtb
application/x-dtbresource+xml res
application/x-dvi dvi
application/x-envoy evy
application/x-eva eva
application/x-font-bdf bdf
application/x-font-ghostscript gsf
application/x-font-linux-psf psf
application/x-font-pcf pcf
application/x-font-snf snf
application/x-font-type1 pfa pfb pfm afm
application/x-freearc arc
application/x-futuresplash spl
application/x-gca-compressed gca
application/x-glulx ulx
application/x-gnumeric gnumeric
application/x-gramps-xml gramps
application/x-gtar gtar
application/x-hdf hdf
application/x-httpd-php php
application/x-install-instructions install
application/x-java-archive-diff jardiff
application/x-java-jnlp-file jnlp
application/x-latex latex
application/x-lua-bytecode luac
application/x-lzh-compressed lzh lha
application/x-makeself run
application/x-mie mie
application/x-mobipocket-ebook prc mobi
application/x-ms-application application
application/x-ms-shortcut lnk
application/x-ms-wmd wmd
application/x-ms-wmz wmz
application/x-ms-xbap xbap
application/x-msaccess mdb
application/x-msbinder obd
application/x-mscardfile crd
application/x-msclip clp
application/x-msdownload com bat
application/x-msmediaview mvb m13 m14
application/x-msmetafile wmf emf emz
application/x-msmoney mny
application/x-mspublisher pub
application/x-msschedule scd
application/x-msterminal trm
application/x-mswrite wri
application/x-netcdf nc cdf
application/x-ns-proxy-autoconfig pac
application/x-nzb nzb
application/x-perl pl pm
application/x-pkcs12 p12 pfx
application/x-pkcs7-certificates p7b spc
application/x-pkcs7-certreqresp p7r
application/x-rar-compressed rar
application/x-redhat-package-manager rpm
application/x-research-info-systems ris
application/x-sea sea
application/x-sh sh
application/x-shar shar
application/x-shockwave-flash swf
application/x-silverlight-app xap
application/x-sql sql
application/x-stuffit sit
application/x-stuffitx sitx
application/x-subrip srt
application/x-sv4cpio sv4cpio
application/x-sv4crc sv4crc
application/x-t3vm-image t3
application/x-tads gam
application/x-tar tar
application/x-tcl tcl tk
application/x-tex tex
application/x-tex-tfm tfm
application/x-texinfo texinfo texi
application/x-tgif obj
application/x-ustar ustar
application/x-virtualbox-hdd hdd
application/x-virtualbox-ova ova
application/x-virtualbox-ovf ovf
application/x-virtualbox-vbox vbox
application/x-virtualbox-vbox-extpack vbox-extpack
application/x-virtualbox-vdi vdi
application/x-virtualbox-vhd vhd
application/x-virtualbox-vmdk vmdk
application/x-wais-source src
application/x-web-app-manifest+json webapp
application/x-x509-ca-cert der crt pem
application/x-xfig fig
application/x-xliff+xml xlf
application/x-xpinstall xpi
application/x-xz xz
application/x-zmachine z1 z2 z3 z4 z5 z6 z7 z8
application/xaml+xml xaml
application/xcap-diff+xml xdf
application/xenc+xml xenc
application/xhtml+xml xhtml xht
application/xml xml xsl xsd rng
application/xml-dtd dtd
application/xop+xml xop
application/xproc+xml xpl
application/xslt+xml xslt
application/xspf+xml xspf
application/xv+xml mxml xhvml xvml xvm
application/yang yang
application/yin+xml yin
application/zip zip
audio/3gpp 3gpp
audio/adpcm adp
audio/basic au snd
audio/midi mid midi kar rmi
audio/mp3 mp3
audio/mp4 m4a mp4a
audio/mpeg mpga mp2 mp2a m2a m3a
audio/ogg oga ogg spx
audio/s3m s3m
audio/silk sil
audio/vnd.dece.audio uva uvva
audio/vnd.digital-winds eol
audio/vnd.dra dra
audio/vnd.dts dts
audio/vnd.dts.hd dtshd
audio/vnd.lucent.voice lvp
audio/vnd.ms-playready.media.pya pya
audio/vnd.nuera.ecelp4800 ecelp4800
audio/vnd.nuera.ecelp7470 ecelp7470
audio/vnd.nuera.ecelp9600 ecelp9600
audio/vnd.rip rip
audio/wav wav
audio/webm weba
audio/x-aac aac
audio/x-aiff aif aiff aifc
audio/x-caf caf
audio/x-flac flac
audio/x-matroska mka
audio/x-mpegurl m3u
audio/x-ms-wax wax
audio/x-ms-wma wma
audio/x-pn-realaudio ram ra
audio/x-pn-realaudio-plugin rmp
audio/xm xm
chemical/x-cdx cdx
chemical/x-cif cif
chemical/x-cmdf cmdf
chemical/x-cml cml
chemical/x-csml csml
chemical/x-xyz xyz
font/collection ttc
font/otf otf
font/ttf ttf
font/woff woff
font/woff2 woff2
image/aces exr
image/apng apng
image/bmp bmp
image/cgm cgm
image/dicom-rle drle
image/fits fits
image/g3fax g3
image/gif gif
image/heic heic
image/heic-sequence heics
image/heif heif
image/heif-sequence heifs
image/ief ief
image/jls jls
image/jp2 jp2 jpg2
image/jpeg jpeg jpg jpe
image/jpm jpm
image/jpx jpx jpf
image/jxr jxr
image/ktx ktx
image/png png
image/prs.btif btif
image/prs.pti pti
image/sgi sgi
image/svg+xml svg svgz
image/t38 t38
image/tiff tif tiff
image/tiff-fx tfx
image/vnd.adobe.photoshop psd
image/vnd.airzip.accelerator.azv azv
image/vnd.dece.graphic uvi uvvi uvg uvvg
image/vnd.djvu djvu djv
image/vnd.dvb.subtitle sub
image/vnd.dwg dwg
image/vnd.dxf dxf
image/vnd.fastbidsheet fbs
image/vnd.fpx fpx
image/vnd.fst fst
image/vnd.fujixerox.edmics-mmr mmr
image/vnd.fujixerox.edmics-rlc rlc
image/vnd.microsoft.icon ico
image/vnd.ms-modi mdi
image/vnd.ms-photo wdp
image/vnd.net-fpx npx
image/vnd.tencent.tap tap
image/vnd.valve.source.texture vtf
image/vnd.wap.wbmp wbmp
image/vnd.xiff xif
image/vnd.zbrush.pcx pcx
image/webp webp
image/x-3ds 3ds
image/x-cmu-raster ras
image/x-cmx cmx
image/x-freehand fh fhc fh4 fh5 fh7
image/x-jng jng
image/x-mrsid-image sid
image/x-pict pic pct
image/x-portable-anymap pnm
image/x-portable-bitmap pbm
image/x-portable-graymap pgm
image/x-portable-pixmap ppm
image/x-rgb rgb
image/x-tga tga
image/x-xbitmap xbm
image/x-xpixmap xpm
image/x-xwindowdump xwd
message/disposition-notification disposition-notification
message/global u8msg
message/global-delivery-status u8dsn
message/global-disposition-notification u8mdn
message/global-headers u8hdr
message/rfc822 eml mime
message/vnd.wfa.wsc wsc
model/3mf 3mf
model/gltf+json gltf
model/gltf-binary glb
model/iges igs iges
model/mesh msh mesh silo
model/vnd.collada+xml dae
model/vnd.dwf dwf
model/vnd.gdl gdl
model/vnd.gtw gtw
model/vnd.mts mts
model/vnd.opengex ogex
model/vnd.parasolid.transmit.binary x_b
model/vnd.parasolid.transmit.text x_t
model/vnd.usdz+zip usdz
model/vnd.valve.source.compiled-map bsp
model/vnd.vtu vtu
model/vrml wrl vrml
model/x3d+binary x3db x3dbz
model/x3d+vrml x3dv x3dvz
model/x3d+xml x3d x3dz
text/cache-manifest appcache manifest
text/calendar ics ifb
text/coffeescript coffee litcoffee
text/css css
text/csv csv
text/html html htm shtml
text/jade jade
text/jsx jsx
text/less less
text/markdown markdown md
text/mathml mml
text/mdx mdx
text/n3 n3
text/plain txt text conf def list log in ini
text/prs.lines.tag dsc
text/richtext rtx
text/sgml sgml sgm
text/shex shex
text/slim slim slm
text/stylus stylus styl
text/tab-separated-values tsv
text/troff t tr roff man me ms
text/turtle ttl
text/uri-list uri uris urls
text/vcard vcard
text/vnd.curl curl
text/vnd.curl.dcurl dcurl
text/vnd.curl.mcurl mcurl
text/vnd.curl.scurl scurl
text/vnd.fly fly
text/vnd.fmi.flexstor flx
text/vnd.graphviz gv
text/vnd.in3d.3dml 3dml
text/vnd.in3d.spot spot
text/vnd.sun.j2me.app-descriptor jad
text/vnd.wap.wml wml
text/vnd.wap.wmlscript wmls
text/vtt vtt
text/x-asm s asm
text/x-c c cc cxx cpp h hh dic
text/x-component htc
text/x-fortran f for f77 f90
text/x-handlebars-template hbs
text/x-java-source java
text/x-lua lua
text/x-markdown mkd
text/x-nfo nfo
text/x-opml opml
text/x-pascal p pas
text/x-processing pde
text/x-sass sass
text/x-scss scss
text/x-setext etx
text/x-sfv sfv
text/x-suse-ymp ymp
text/x-uuencode uu
text/x-vcalendar vcs
text/x-vcard vcf
text/yaml yaml yml
video/3gpp 3gp
video/3gpp2 3g2
video/h261 h261
video/h263 h263
video/h264 h264
video/jpeg jpgv
video/jpm jpgm
video/mj2 mj2 mjp2
video/mp2t ts
video/mp4 mp4 mp4v mpg4
video/mpeg mpeg mpg mpe m1v m2v
video/ogg ogv
video/quicktime qt mov
video/vnd.dece.hd uvh uvvh
video/vnd.dece.mobile uvm uvvm
video/vnd.dece.pd uvp uvvp
video/vnd.dece.sd uvs uvvs
video/vnd.dece.video uvv uvvv
video/vnd.dvb.file dvb
video/vnd.fvt fvt
video/vnd.mpegurl mxu m4u
video/vnd.ms-playready.media.pyv pyv
video/vnd.uvvu.mp4 uvu uvvu
video/vnd.vivo viv
video/webm webm
video/x-f4v f4v
video/x-fli fli
video/x-flv flv
video/x-m4v m4v
video/x-matroska mkv mk3d mks
video/x-mng mng
video/x-ms-asf asf asx
video/x-ms-vob vob
video/x-ms-wm wm
video/x-ms-wmv wmv
video/x-ms-wmx wmx
video/x-ms-wvx wvx
video/x-msvideo avi
video/x-sgi-movie movie
video/x-smv smv
x-conference/x-cooltalk ice

View File

@@ -0,0 +1,217 @@
# frozen_string_literal: true
module Jekyll
class Page
include Convertible
attr_writer :dir
attr_accessor :site, :pager
attr_accessor :name, :ext, :basename
attr_accessor :data, :content, :output
alias_method :extname, :ext
# Attributes for Liquid templates
ATTRIBUTES_FOR_LIQUID = %w(
content
dir
excerpt
name
path
url
).freeze
# A set of extensions that are considered HTML or HTML-like so we
# should not alter them, this includes .xhtml through XHTM5.
HTML_EXTENSIONS = %w(
.html
.xhtml
.htm
).freeze
# Initialize a new Page.
#
# site - The Site object.
# base - The String path to the source.
# dir - The String path between the source and the file.
# name - The String filename of the file.
def initialize(site, base, dir, name)
@site = site
@base = base
@dir = dir
@name = name
@path = if site.in_theme_dir(base) == base # we're in a theme
site.in_theme_dir(base, dir, name)
else
site.in_source_dir(base, dir, name)
end
process(name)
read_yaml(PathManager.join(base, dir), name)
generate_excerpt if site.config["page_excerpts"]
data.default_proc = proc do |_, key|
site.frontmatter_defaults.find(relative_path, type, key)
end
Jekyll::Hooks.trigger :pages, :post_init, self
end
# The generated directory into which the page will be placed
# upon generation. This is derived from the permalink or, if
# permalink is absent, will be '/'
#
# Returns the String destination directory.
def dir
url.end_with?("/") ? url : url_dir
end
# The full path and filename of the post. Defined in the YAML of the post
# body.
#
# Returns the String permalink or nil if none has been set.
def permalink
data.nil? ? nil : data["permalink"]
end
# The template of the permalink.
#
# Returns the template String.
def template
if !html?
"/:path/:basename:output_ext"
elsif index?
"/:path/"
else
Utils.add_permalink_suffix("/:path/:basename", site.permalink_style)
end
end
# The generated relative url of this page. e.g. /about.html.
#
# Returns the String url.
def url
@url ||= URL.new(
:template => template,
:placeholders => url_placeholders,
:permalink => permalink
).to_s
end
# Returns a hash of URL placeholder names (as symbols) mapping to the
# desired placeholder replacements. For details see "url.rb"
def url_placeholders
{
:path => @dir,
:basename => basename,
:output_ext => output_ext,
}
end
# Extract information from the page filename.
#
# name - The String filename of the page file.
#
# NOTE: `String#gsub` removes all trailing periods (in comparison to `String#chomp`)
# Returns nothing.
def process(name)
return unless name
self.ext = File.extname(name)
self.basename = name[0..-ext.length - 1].gsub(%r!\.*\z!, "")
end
# Add any necessary layouts to this post
#
# layouts - The Hash of {"name" => "layout"}.
# site_payload - The site payload Hash.
#
# Returns String rendered page.
def render(layouts, site_payload)
site_payload["page"] = to_liquid
site_payload["paginator"] = pager.to_liquid
do_layout(site_payload, layouts)
end
# The path to the source file
#
# Returns the path to the source file
def path
data.fetch("path") { relative_path }
end
# The path to the page source file, relative to the site source
def relative_path
@relative_path ||= PathManager.join(@dir, @name).sub(%r!\A/!, "")
end
# Obtain destination path.
#
# dest - The String path to the destination dir.
#
# Returns the destination file path String.
def destination(dest)
@destination ||= {}
@destination[dest] ||= begin
path = site.in_dest_dir(dest, URL.unescape_path(url))
path = File.join(path, "index") if url.end_with?("/")
path << output_ext unless path.end_with? output_ext
path
end
end
# Returns the object as a debug String.
def inspect
"#<#{self.class} @relative_path=#{relative_path.inspect}>"
end
# Returns the Boolean of whether this Page is HTML or not.
def html?
HTML_EXTENSIONS.include?(output_ext)
end
# Returns the Boolean of whether this Page is an index file or not.
def index?
basename == "index"
end
def trigger_hooks(hook_name, *args)
Jekyll::Hooks.trigger :pages, hook_name, self, *args
end
def write?
true
end
def excerpt_separator
@excerpt_separator ||= (data["excerpt_separator"] || site.config["excerpt_separator"]).to_s
end
def excerpt
return @excerpt if defined?(@excerpt)
@excerpt = data["excerpt"] ? data["excerpt"].to_s : nil
end
def generate_excerpt?
!excerpt_separator.empty? && instance_of?(Jekyll::Page) && html?
end
private
def generate_excerpt
return unless generate_excerpt?
data["excerpt"] ||= Jekyll::PageExcerpt.new(self)
end
def url_dir
@url_dir ||= begin
value = File.dirname(url)
value.end_with?("/") ? value : "#{value}/"
end
end
end
end

View File

@@ -0,0 +1,25 @@
# frozen_string_literal: true
module Jekyll
class PageExcerpt < Excerpt
attr_reader :doc
alias_method :id, :relative_path
EXCERPT_ATTRIBUTES = (Page::ATTRIBUTES_FOR_LIQUID - %w(excerpt)).freeze
private_constant :EXCERPT_ATTRIBUTES
def to_liquid
@to_liquid ||= doc.to_liquid(EXCERPT_ATTRIBUTES)
end
def render_with_liquid?
return false if data["render_with_liquid"] == false
Jekyll::Utils.has_liquid_construct?(content)
end
def inspect
"#<#{self.class} id=#{id.inspect}>"
end
end
end

View File

@@ -0,0 +1,14 @@
# frozen_string_literal: true
module Jekyll
# A Jekyll::Page subclass to handle processing files without reading it to
# determine the page-data and page-content based on Front Matter delimiters.
#
# The class instance is basically just a bare-bones entity with just
# attributes "dir", "name", "path", "url" defined on it.
class PageWithoutAFile < Page
def read_yaml(*)
@data ||= {}
end
end
end

View File

@@ -0,0 +1,74 @@
# frozen_string_literal: true
module Jekyll
# A singleton class that caches frozen instances of path strings returned from its methods.
#
# NOTE:
# This class exists because `File.join` allocates an Array and returns a new String on every
# call using **the same arguments**. Caching the result means reduced memory usage.
# However, the caches are never flushed so that they can be used even when a site is
# regenerating. The results are frozen to deter mutation of the cached string.
#
# Therefore, employ this class only for situations where caching the result is necessary
# for performance reasons.
#
class PathManager
# This class cannot be initialized from outside
private_class_method :new
class << self
# Wraps `File.join` to cache the frozen result.
# Reassigns `nil`, empty strings and empty arrays to a frozen empty string beforehand.
#
# Returns a frozen string.
def join(base, item)
base = "" if base.nil? || base.empty?
item = "" if item.nil? || item.empty?
@join ||= {}
@join[base] ||= {}
@join[base][item] ||= File.join(base, item).freeze
end
# Ensures the questionable path is prefixed with the base directory
# and prepends the questionable path with the base directory if false.
#
# Returns a frozen string.
def sanitized_path(base_directory, questionable_path)
@sanitized_path ||= {}
@sanitized_path[base_directory] ||= {}
@sanitized_path[base_directory][questionable_path] ||= begin
if questionable_path.nil?
base_directory.freeze
else
sanitize_and_join(base_directory, questionable_path).freeze
end
end
end
private
def sanitize_and_join(base_directory, questionable_path)
clean_path = if questionable_path.start_with?("~")
questionable_path.dup.insert(0, "/")
else
questionable_path
end
clean_path = File.expand_path(clean_path, "/")
return clean_path if clean_path.eql?(base_directory)
# remove any remaining extra leading slashes not stripped away by calling
# `File.expand_path` above.
clean_path.squeeze!("/")
return clean_path if clean_path.start_with?(slashed_dir_cache(base_directory))
clean_path.sub!(%r!\A\w:/!, "/")
join(base_directory, clean_path)
end
def slashed_dir_cache(base_directory)
@slashed_dir_cache ||= {}
@slashed_dir_cache[base_directory] ||= base_directory.sub(%r!\z!, "/")
end
end
end
end

View File

@@ -0,0 +1,92 @@
# frozen_string_literal: true
module Jekyll
class Plugin
PRIORITIES = {
:low => -10,
:highest => 100,
:lowest => -100,
:normal => 0,
:high => 10,
}.freeze
#
def self.inherited(const)
catch_inheritance(const) do |const_|
catch_inheritance(const_)
end
end
#
def self.catch_inheritance(const)
const.define_singleton_method :inherited do |const_|
(@children ||= Set.new).add const_
yield const_ if block_given?
end
end
#
def self.descendants
@children ||= Set.new
out = @children.map(&:descendants)
out << self unless superclass == Plugin
Set.new(out).flatten
end
# Get or set the priority of this plugin. When called without an
# argument it returns the priority. When an argument is given, it will
# set the priority.
#
# priority - The Symbol priority (default: nil). Valid options are:
# :lowest, :low, :normal, :high, :highest
#
# Returns the Symbol priority.
def self.priority(priority = nil)
@priority ||= nil
@priority = priority if priority && PRIORITIES.key?(priority)
@priority || :normal
end
# Get or set the safety of this plugin. When called without an argument
# it returns the safety. When an argument is given, it will set the
# safety.
#
# safe - The Boolean safety (default: nil).
#
# Returns the safety Boolean.
def self.safe(safe = nil)
@safe = safe unless defined?(@safe) && safe.nil?
@safe || false
end
# Spaceship is priority [higher -> lower]
#
# other - The class to be compared.
#
# Returns -1, 0, 1.
def self.<=>(other)
PRIORITIES[other.priority] <=> PRIORITIES[priority]
end
# Spaceship is priority [higher -> lower]
#
# other - The class to be compared.
#
# Returns -1, 0, 1.
def <=>(other)
self.class <=> other.class
end
# Initialize a new plugin. This should be overridden by the subclass.
#
# config - The Hash of configuration options.
#
# Returns a new instance.
def initialize(config = {})
# no-op for default
end
end
end

View File

@@ -0,0 +1,115 @@
# frozen_string_literal: true
module Jekyll
class PluginManager
attr_reader :site
# Create an instance of this class.
#
# site - the instance of Jekyll::Site we're concerned with
#
# Returns nothing
def initialize(site)
@site = site
end
# Require all the plugins which are allowed.
#
# Returns nothing
def conscientious_require
require_theme_deps if site.theme
require_plugin_files
require_gems
deprecation_checks
end
# Require each of the gem plugins specified.
#
# Returns nothing.
def require_gems
Jekyll::External.require_with_graceful_fail(
site.gems.select { |plugin| plugin_allowed?(plugin) }
)
end
# Require each of the runtime_dependencies specified by the theme's gemspec.
#
# Returns false only if no dependencies have been specified, otherwise nothing.
def require_theme_deps
return false unless site.theme.runtime_dependencies
site.theme.runtime_dependencies.each do |dep|
next if dep.name == "jekyll"
External.require_with_graceful_fail(dep.name) if plugin_allowed?(dep.name)
end
end
def self.require_from_bundler
if !ENV["JEKYLL_NO_BUNDLER_REQUIRE"] && File.file?("Gemfile")
require "bundler"
Bundler.setup
required_gems = Bundler.require(:jekyll_plugins)
message = "Required #{required_gems.map(&:name).join(", ")}"
Jekyll.logger.debug("PluginManager:", message)
ENV["JEKYLL_NO_BUNDLER_REQUIRE"] = "true"
true
else
false
end
end
# Check whether a gem plugin is allowed to be used during this build.
#
# plugin_name - the name of the plugin
#
# Returns true if the plugin name is in the whitelist or if the site is not
# in safe mode.
def plugin_allowed?(plugin_name)
!site.safe || whitelist.include?(plugin_name)
end
# Build an array of allowed plugin gem names.
#
# Returns an array of strings, each string being the name of a gem name
# that is allowed to be used.
def whitelist
@whitelist ||= Array[site.config["whitelist"]].flatten
end
# Require all .rb files if safe mode is off
#
# Returns nothing.
def require_plugin_files
unless site.safe
plugins_path.each do |plugin_search_path|
plugin_files = Utils.safe_glob(plugin_search_path, File.join("**", "*.rb"))
Jekyll::External.require_with_graceful_fail(plugin_files)
end
end
end
# Public: Setup the plugin search path
#
# Returns an Array of plugin search paths
def plugins_path
if site.config["plugins_dir"].eql? Jekyll::Configuration::DEFAULTS["plugins_dir"]
[site.in_source_dir(site.config["plugins_dir"])]
else
Array(site.config["plugins_dir"]).map { |d| File.expand_path(d) }
end
end
def deprecation_checks
pagination_included = (site.config["plugins"] || []).include?("jekyll-paginate") ||
defined?(Jekyll::Paginate)
if site.config["paginate"] && !pagination_included
Jekyll::Deprecator.deprecation_message "You appear to have pagination " \
"turned on, but you haven't included the `jekyll-paginate` gem. " \
"Ensure you have `plugins: [jekyll-paginate]` in your configuration file."
end
end
end
end

View File

@@ -0,0 +1,58 @@
# frozen_string_literal: true
module Jekyll
class Profiler
TERMINAL_TABLE_STYLES = {
:alignment => :right,
:border_top => false,
:border_bottom => false,
}.freeze
private_constant :TERMINAL_TABLE_STYLES
def self.tabulate(table_rows)
require "terminal-table"
rows = table_rows.dup
header = rows.shift
footer = rows.pop
output = +"\n"
table = Terminal::Table.new do |t|
t << header
t << :separator
rows.each { |row| t << row }
t << :separator
t << footer
t.style = TERMINAL_TABLE_STYLES
t.align_column(0, :left)
end
output << table.to_s << "\n"
end
def initialize(site)
@site = site
end
def profile_process
profile_data = { "PHASE" => "TIME" }
total_time = 0
[:reset, :read, :generate, :render, :cleanup, :write].each do |method|
start_time = Time.now
@site.send(method)
end_time = (Time.now - start_time).round(4)
profile_data[method.to_s.upcase] = format("%.4f", end_time)
total_time += end_time
end
profile_data["TOTAL TIME"] = format("%.4f", total_time)
Jekyll.logger.info "\nBuild Process Summary:"
Jekyll.logger.info Profiler.tabulate(Array(profile_data))
Jekyll.logger.info "\nSite Render Stats:"
@site.print_stats
end
end
end

View File

@@ -0,0 +1,23 @@
# frozen_string_literal: true
module Jekyll
class Publisher
def initialize(site)
@site = site
end
def publish?(thing)
can_be_published?(thing) && !hidden_in_the_future?(thing)
end
def hidden_in_the_future?(thing)
thing.respond_to?(:date) && !@site.future && thing.date.to_i > @site.time.to_i
end
private
def can_be_published?(thing)
thing.data.fetch("published", true) || @site.unpublished
end
end
end

View File

@@ -0,0 +1,192 @@
# frozen_string_literal: true
module Jekyll
class Reader
attr_reader :site
def initialize(site)
@site = site
end
# Read Site data from disk and load it into internal data structures.
#
# Returns nothing.
def read
@site.layouts = LayoutReader.new(site).read
read_directories
read_included_excludes
sort_files!
@site.data = DataReader.new(site).read(site.config["data_dir"])
CollectionReader.new(site).read
ThemeAssetsReader.new(site).read
end
# Sorts posts, pages, and static files.
def sort_files!
site.collections.each_value { |c| c.docs.sort! }
site.pages.sort_by!(&:name)
site.static_files.sort_by!(&:relative_path)
end
# Recursively traverse directories to find pages and static files
# that will become part of the site according to the rules in
# filter_entries.
#
# dir - The String relative path of the directory to read. Default: ''.
#
# Returns nothing.
def read_directories(dir = "")
base = site.in_source_dir(dir)
return unless File.directory?(base)
dot_dirs = []
dot_pages = []
dot_static_files = []
dot = Dir.chdir(base) { filter_entries(Dir.entries("."), base) }
dot.each do |entry|
file_path = @site.in_source_dir(base, entry)
if File.directory?(file_path)
dot_dirs << entry
elsif Utils.has_yaml_header?(file_path)
dot_pages << entry
else
dot_static_files << entry
end
end
retrieve_posts(dir)
retrieve_dirs(base, dir, dot_dirs)
retrieve_pages(dir, dot_pages)
retrieve_static_files(dir, dot_static_files)
end
# Retrieves all the posts(posts/drafts) from the given directory
# and add them to the site and sort them.
#
# dir - The String representing the directory to retrieve the posts from.
#
# Returns nothing.
def retrieve_posts(dir)
return if outside_configured_directory?(dir)
site.posts.docs.concat(post_reader.read_posts(dir))
site.posts.docs.concat(post_reader.read_drafts(dir)) if site.show_drafts
end
# Recursively traverse directories with the read_directories function.
#
# base - The String representing the site's base directory.
# dir - The String representing the directory to traverse down.
# dot_dirs - The Array of subdirectories in the dir.
#
# Returns nothing.
def retrieve_dirs(_base, dir, dot_dirs)
dot_dirs.each do |file|
dir_path = site.in_source_dir(dir, file)
rel_path = PathManager.join(dir, file)
@site.reader.read_directories(rel_path) unless @site.dest.chomp("/") == dir_path
end
end
# Retrieve all the pages from the current directory,
# add them to the site and sort them.
#
# dir - The String representing the directory retrieve the pages from.
# dot_pages - The Array of pages in the dir.
#
# Returns nothing.
def retrieve_pages(dir, dot_pages)
site.pages.concat(PageReader.new(site, dir).read(dot_pages))
end
# Retrieve all the static files from the current directory,
# add them to the site and sort them.
#
# dir - The directory retrieve the static files from.
# dot_static_files - The static files in the dir.
#
# Returns nothing.
def retrieve_static_files(dir, dot_static_files)
site.static_files.concat(StaticFileReader.new(site, dir).read(dot_static_files))
end
# Filter out any files/directories that are hidden or backup files (start
# with "." or "#" or end with "~"), or contain site content (start with "_"),
# or are excluded in the site configuration, unless they are web server
# files such as '.htaccess'.
#
# entries - The Array of String file/directory entries to filter.
# base_directory - The string representing the optional base directory.
#
# Returns the Array of filtered entries.
def filter_entries(entries, base_directory = nil)
EntryFilter.new(site, base_directory).filter(entries)
end
# Read the entries from a particular directory for processing
#
# dir - The String representing the relative path of the directory to read.
# subfolder - The String representing the directory to read.
#
# Returns the list of entries to process
def get_entries(dir, subfolder)
base = site.in_source_dir(dir, subfolder)
return [] unless File.exist?(base)
entries = Dir.chdir(base) { filter_entries(Dir["**/*"], base) }
entries.delete_if { |e| File.directory?(site.in_source_dir(base, e)) }
end
private
# Internal
#
# Determine if the directory is supposed to contain posts and drafts.
# If the user has defined a custom collections_dir, then attempt to read
# posts and drafts only from within that directory.
#
# Returns true if a custom collections_dir has been set but current directory lies
# outside that directory.
def outside_configured_directory?(dir)
collections_dir = site.config["collections_dir"]
!collections_dir.empty? && !dir.start_with?("/#{collections_dir}")
end
# Create a single PostReader instance to retrieve drafts and posts from all valid
# directories in current site.
def post_reader
@post_reader ||= PostReader.new(site)
end
def read_included_excludes
entry_filter = EntryFilter.new(site)
site.include.each do |entry|
entry_path = site.in_source_dir(entry)
next if File.directory?(entry_path)
next if entry_filter.symlink?(entry_path)
read_included_file(entry_path) if File.file?(entry_path)
end
end
def read_included_file(entry_path)
if Utils.has_yaml_header?(entry_path)
conditionally_generate_entry(entry_path, site.pages, PageReader)
else
conditionally_generate_entry(entry_path, site.static_files, StaticFileReader)
end
end
def conditionally_generate_entry(entry_path, container, reader)
return if container.find { |item| site.in_source_dir(item.relative_path) == entry_path }
dir, files = File.split(entry_path)
dir.sub!(site.source, "")
files = Array(files)
container.concat(reader.new(site, dir).read(files))
end
end
end

View File

@@ -0,0 +1,23 @@
# frozen_string_literal: true
module Jekyll
class CollectionReader
SPECIAL_COLLECTIONS = %w(posts data).freeze
attr_reader :site, :content
def initialize(site)
@site = site
@content = {}
end
# Read in all collections specified in the configuration
#
# Returns nothing.
def read
site.collections.each_value do |collection|
collection.read unless SPECIAL_COLLECTIONS.include?(collection.label)
end
end
end
end

View File

@@ -0,0 +1,79 @@
# frozen_string_literal: true
module Jekyll
class DataReader
attr_reader :site, :content
def initialize(site)
@site = site
@content = {}
@entry_filter = EntryFilter.new(site)
@source_dir = site.in_source_dir("/")
end
# Read all the files in <dir> and adds them to @content
#
# dir - The String relative path of the directory to read.
#
# Returns @content, a Hash of the .yaml, .yml,
# .json, and .csv files in the base directory
def read(dir)
base = site.in_source_dir(dir)
read_data_to(base, @content)
@content
end
# Read and parse all .yaml, .yml, .json, .csv and .tsv
# files under <dir> and add them to the <data> variable.
#
# dir - The string absolute path of the directory to read.
# data - The variable to which data will be added.
#
# Returns nothing
def read_data_to(dir, data)
return unless File.directory?(dir) && !@entry_filter.symlink?(dir)
entries = Dir.chdir(dir) do
Dir["*.{yaml,yml,json,csv,tsv}"] + Dir["*"].select { |fn| File.directory?(fn) }
end
entries.each do |entry|
path = @site.in_source_dir(dir, entry)
next if @entry_filter.symlink?(path)
if File.directory?(path)
read_data_to(path, data[sanitize_filename(entry)] = {})
else
key = sanitize_filename(File.basename(entry, ".*"))
data[key] = read_data_file(path)
end
end
end
# Determines how to read a data file.
#
# Returns the contents of the data file.
def read_data_file(path)
Jekyll.logger.debug "Reading:", path.sub(@source_dir, "")
case File.extname(path).downcase
when ".csv"
CSV.read(path,
:headers => true,
:encoding => site.config["encoding"]).map(&:to_hash)
when ".tsv"
CSV.read(path,
:col_sep => "\t",
:headers => true,
:encoding => site.config["encoding"]).map(&:to_hash)
else
SafeYAML.load_file(path)
end
end
def sanitize_filename(name)
name.gsub(%r![^\w\s-]+|(?<=^|\b\s)\s+(?=$|\s?\b)!, "")
.gsub(%r!\s+!, "_")
end
end
end

View File

@@ -0,0 +1,62 @@
# frozen_string_literal: true
module Jekyll
class LayoutReader
attr_reader :site
def initialize(site)
@site = site
@layouts = {}
end
def read
layout_entries.each do |layout_file|
@layouts[layout_name(layout_file)] = \
Layout.new(site, layout_directory, layout_file)
end
theme_layout_entries.each do |layout_file|
@layouts[layout_name(layout_file)] ||= \
Layout.new(site, theme_layout_directory, layout_file)
end
@layouts
end
def layout_directory
@layout_directory ||= site.in_source_dir(site.config["layouts_dir"])
end
def theme_layout_directory
@theme_layout_directory ||= site.theme.layouts_path if site.theme
end
private
def layout_entries
entries_in layout_directory
end
def theme_layout_entries
theme_layout_directory ? entries_in(theme_layout_directory) : []
end
def entries_in(dir)
entries = []
within(dir) do
entries = EntryFilter.new(site).filter(Dir["**/*.*"])
end
entries
end
def layout_name(file)
file.split(".")[0..-2].join(".")
end
def within(directory)
return unless File.exist?(directory)
Dir.chdir(directory) { yield }
end
end
end

View File

@@ -0,0 +1,25 @@
# frozen_string_literal: true
module Jekyll
class PageReader
attr_reader :site, :dir, :unfiltered_content
def initialize(site, dir)
@site = site
@dir = dir
@unfiltered_content = []
end
# Create a new `Jekyll::Page` object for each entry in a given array.
#
# files - An array of file names inside `@dir`
#
# Returns an array of publishable `Jekyll::Page` objects.
def read(files)
files.each do |page|
@unfiltered_content << Page.new(@site, @site.source, @dir, page)
end
@unfiltered_content.select { |page| site.publisher.publish?(page) }
end
end
end

View File

@@ -0,0 +1,85 @@
# frozen_string_literal: true
module Jekyll
class PostReader
attr_reader :site, :unfiltered_content
def initialize(site)
@site = site
end
# Read all the files in <source>/<dir>/_drafts and create a new
# Document object with each one.
#
# dir - The String relative path of the directory to read.
#
# Returns nothing.
def read_drafts(dir)
read_publishable(dir, "_drafts", Document::DATELESS_FILENAME_MATCHER)
end
# Read all the files in <source>/<dir>/_posts and create a new Document
# object with each one.
#
# dir - The String relative path of the directory to read.
#
# Returns nothing.
def read_posts(dir)
read_publishable(dir, "_posts", Document::DATE_FILENAME_MATCHER)
end
# Read all the files in <source>/<dir>/<magic_dir> and create a new
# Document object with each one insofar as it matches the regexp matcher.
#
# dir - The String relative path of the directory to read.
#
# Returns nothing.
def read_publishable(dir, magic_dir, matcher)
read_content(dir, magic_dir, matcher)
.tap { |docs| docs.each(&:read) }
.select { |doc| processable?(doc) }
end
# Read all the content files from <source>/<dir>/magic_dir
# and return them with the type klass.
#
# dir - The String relative path of the directory to read.
# magic_dir - The String relative directory to <dir>,
# looks for content here.
# klass - The return type of the content.
#
# Returns klass type of content files
def read_content(dir, magic_dir, matcher)
@site.reader.get_entries(dir, magic_dir).map do |entry|
next unless matcher.match?(entry)
path = @site.in_source_dir(File.join(dir, magic_dir, entry))
Document.new(path,
:site => @site,
:collection => @site.posts)
end.tap(&:compact!)
end
private
def processable?(doc)
if doc.content.nil?
Jekyll.logger.debug "Skipping:", "Content in #{doc.relative_path} is nil"
false
elsif !doc.content.valid_encoding?
Jekyll.logger.debug "Skipping:", "#{doc.relative_path} is not valid UTF-8"
false
else
publishable?(doc)
end
end
def publishable?(doc)
site.publisher.publish?(doc).tap do |will_publish|
if !will_publish && site.publisher.hidden_in_the_future?(doc)
Jekyll.logger.warn "Skipping:", "#{doc.relative_path} has a future date"
end
end
end
end
end

View File

@@ -0,0 +1,25 @@
# frozen_string_literal: true
module Jekyll
class StaticFileReader
attr_reader :site, :dir, :unfiltered_content
def initialize(site, dir)
@site = site
@dir = dir
@unfiltered_content = []
end
# Create a new StaticFile object for every entry in a given list of basenames.
#
# files - an array of file basenames.
#
# Returns an array of static files.
def read(files)
files.each do |file|
@unfiltered_content << StaticFile.new(@site, @site.source, @dir, file)
end
@unfiltered_content
end
end
end

View File

@@ -0,0 +1,52 @@
# frozen_string_literal: true
module Jekyll
class ThemeAssetsReader
attr_reader :site
def initialize(site)
@site = site
end
def read
return unless site.theme&.assets_path
Find.find(site.theme.assets_path) do |path|
next if File.directory?(path)
if File.symlink?(path)
Jekyll.logger.warn "Theme reader:", "Ignored symlinked asset: #{path}"
else
read_theme_asset(path)
end
end
end
private
def read_theme_asset(path)
base = site.theme.root
dir = File.dirname(path.sub("#{site.theme.root}/", ""))
name = File.basename(path)
if Utils.has_yaml_header?(path)
append_unless_exists site.pages,
Jekyll::Page.new(site, base, dir, name)
else
append_unless_exists site.static_files,
Jekyll::StaticFile.new(site, base, "/#{dir}", name)
end
end
def append_unless_exists(haystack, new_item)
if haystack.any? { |file| file.relative_path == new_item.relative_path }
Jekyll.logger.debug "Theme:",
"Ignoring #{new_item.relative_path} in theme due to existing file " \
"with that path in site."
return
end
haystack << new_item
end
end
end

View File

@@ -0,0 +1,195 @@
# frozen_string_literal: true
module Jekyll
class Regenerator
attr_reader :site, :metadata, :cache
attr_accessor :disabled
private :disabled, :disabled=
def initialize(site)
@site = site
# Read metadata from file
read_metadata
# Initialize cache to an empty hash
clear_cache
end
# Checks if a renderable object needs to be regenerated
#
# Returns a boolean.
def regenerate?(document)
return true if disabled
case document
when Page
regenerate_page?(document)
when Document
regenerate_document?(document)
else
source_path = document.respond_to?(:path) ? document.path : nil
dest_path = document.destination(@site.dest) if document.respond_to?(:destination)
source_modified_or_dest_missing?(source_path, dest_path)
end
end
# Add a path to the metadata
#
# Returns true, also on failure.
def add(path)
return true unless File.exist?(path)
metadata[path] = {
"mtime" => File.mtime(path),
"deps" => [],
}
cache[path] = true
end
# Force a path to regenerate
#
# Returns true.
def force(path)
cache[path] = true
end
# Clear the metadata and cache
#
# Returns nothing
def clear
@metadata = {}
clear_cache
end
# Clear just the cache
#
# Returns nothing
def clear_cache
@cache = {}
end
# Checks if the source has been modified or the
# destination is missing
#
# returns a boolean
def source_modified_or_dest_missing?(source_path, dest_path)
modified?(source_path) || (dest_path && !File.exist?(dest_path))
end
# Checks if a path's (or one of its dependencies)
# mtime has changed
#
# Returns a boolean.
def modified?(path)
return true if disabled?
# objects that don't have a path are always regenerated
return true if path.nil?
# Check for path in cache
return cache[path] if cache.key? path
if metadata[path]
# If we have seen this file before,
# check if it or one of its dependencies has been modified
existing_file_modified?(path)
else
# If we have not seen this file before, add it to the metadata and regenerate it
add(path)
end
end
# Add a dependency of a path
#
# Returns nothing.
def add_dependency(path, dependency)
return if metadata[path].nil? || disabled
unless metadata[path]["deps"].include? dependency
metadata[path]["deps"] << dependency
add(dependency) unless metadata.include?(dependency)
end
regenerate? dependency
end
# Write the metadata to disk
#
# Returns nothing.
def write_metadata
unless disabled?
Jekyll.logger.debug "Writing Metadata:", ".jekyll-metadata"
File.binwrite(metadata_file, Marshal.dump(metadata))
end
end
# Produce the absolute path of the metadata file
#
# Returns the String path of the file.
def metadata_file
@metadata_file ||= site.in_source_dir(".jekyll-metadata")
end
# Check if metadata has been disabled
#
# Returns a Boolean (true for disabled, false for enabled).
def disabled?
self.disabled = !site.incremental? if disabled.nil?
disabled
end
private
# Read metadata from the metadata file, if no file is found,
# initialize with an empty hash
#
# Returns the read metadata.
def read_metadata
@metadata =
if !disabled? && File.file?(metadata_file)
content = File.binread(metadata_file)
begin
Marshal.load(content)
rescue TypeError
SafeYAML.load(content)
rescue ArgumentError => e
Jekyll.logger.warn("Failed to load #{metadata_file}: #{e}")
{}
end
else
{}
end
end
def regenerate_page?(document)
document.asset_file? || document.data["regenerate"] ||
source_modified_or_dest_missing?(
site.in_source_dir(document.relative_path), document.destination(@site.dest)
)
end
def regenerate_document?(document)
!document.write? || document.data["regenerate"] ||
source_modified_or_dest_missing?(
document.path, document.destination(@site.dest)
)
end
def existing_file_modified?(path)
# If one of this file dependencies have been modified,
# set the regeneration bit for both the dependency and the file to true
metadata[path]["deps"].each do |dependency|
return cache[dependency] = cache[path] = true if modified?(dependency)
end
if File.exist?(path) && metadata[path]["mtime"].eql?(File.mtime(path))
# If this file has not been modified, set the regeneration bit to false
cache[path] = false
else
# If it has been modified, set it to true
add(path)
end
end
end
end

View File

@@ -0,0 +1,52 @@
# frozen_string_literal: true
module Jekyll
class RelatedPosts
class << self
attr_accessor :lsi
end
attr_reader :post, :site
def initialize(post)
@post = post
@site = post.site
Jekyll::External.require_with_graceful_fail("classifier-reborn") if site.lsi
end
def build
return [] unless site.posts.docs.size > 1
if site.lsi
build_index
lsi_related_posts
else
most_recent_posts
end
end
def build_index
self.class.lsi ||= begin
lsi = ClassifierReborn::LSI.new(:auto_rebuild => false)
Jekyll.logger.info("Populating LSI...")
site.posts.docs.each do |x|
lsi.add_item(x)
end
Jekyll.logger.info("Rebuilding index...")
lsi.build_index
Jekyll.logger.info("")
lsi
end
end
def lsi_related_posts
self.class.lsi.find_related(post, 11)
end
def most_recent_posts
@most_recent_posts ||= (site.posts.docs.last(11).reverse! - [post]).first(10)
end
end
end

View File

@@ -0,0 +1,265 @@
# frozen_string_literal: true
module Jekyll
class Renderer
attr_reader :document, :site
attr_writer :layouts, :payload
def initialize(site, document, site_payload = nil)
@site = site
@document = document
@payload = site_payload
@layouts = nil
end
# Fetches the payload used in Liquid rendering.
# It can be written with #payload=(new_payload)
# Falls back to site.site_payload if no payload is set.
#
# Returns a Jekyll::Drops::UnifiedPayloadDrop
def payload
@payload ||= site.site_payload
end
# The list of layouts registered for this Renderer.
# It can be written with #layouts=(new_layouts)
# Falls back to site.layouts if no layouts are registered.
#
# Returns a Hash of String => Jekyll::Layout identified
# as basename without the extension name.
def layouts
@layouts || site.layouts
end
# Determine which converters to use based on this document's
# extension.
#
# Returns Array of Converter instances.
def converters
@converters ||= site.converters.select { |c| c.matches(document.extname) }.tap(&:sort!)
end
# Determine the extname the outputted file should have
#
# Returns String the output extname including the leading period.
def output_ext
@output_ext ||= (permalink_ext || converter_output_ext)
end
# Prepare payload and render the document
#
# Returns String rendered document output
def run
Jekyll.logger.debug "Rendering:", document.relative_path
assign_pages!
assign_current_document!
assign_highlighter_options!
assign_layout_data!
Jekyll.logger.debug "Pre-Render Hooks:", document.relative_path
document.trigger_hooks(:pre_render, payload)
render_document
end
# Render the document.
#
# Returns String rendered document output
# rubocop: disable Metrics/AbcSize, Metrics/MethodLength
def render_document
info = {
:registers => { :site => site, :page => payload["page"] },
:strict_filters => liquid_options["strict_filters"],
:strict_variables => liquid_options["strict_variables"],
}
output = document.content
if document.render_with_liquid?
Jekyll.logger.debug "Rendering Liquid:", document.relative_path
output = render_liquid(output, payload, info, document.path)
end
Jekyll.logger.debug "Rendering Markup:", document.relative_path
output = convert(output.to_s)
document.content = output
Jekyll.logger.debug "Post-Convert Hooks:", document.relative_path
document.trigger_hooks(:post_convert)
output = document.content
if document.place_in_layout?
Jekyll.logger.debug "Rendering Layout:", document.relative_path
output = place_in_layouts(output, payload, info)
end
output
end
# rubocop: enable Metrics/AbcSize, Metrics/MethodLength
# Convert the document using the converters which match this renderer's document.
#
# Returns String the converted content.
def convert(content)
converters.reduce(content) do |output, converter|
begin
converter.convert output
rescue StandardError => e
Jekyll.logger.error "Conversion error:",
"#{converter.class} encountered an error while "\
"converting '#{document.relative_path}':"
Jekyll.logger.error("", e.to_s)
raise e
end
end
end
# Render the given content with the payload and info
#
# content -
# payload -
# info -
# path - (optional) the path to the file, for use in ex
#
# Returns String the content, rendered by Liquid.
def render_liquid(content, payload, info, path = nil)
template = site.liquid_renderer.file(path).parse(content)
template.warnings.each do |e|
Jekyll.logger.warn "Liquid Warning:",
LiquidRenderer.format_error(e, path || document.relative_path)
end
template.render!(payload, info)
# rubocop: disable Lint/RescueException
rescue Exception => e
Jekyll.logger.error "Liquid Exception:",
LiquidRenderer.format_error(e, path || document.relative_path)
raise e
end
# rubocop: enable Lint/RescueException
# Checks if the layout specified in the document actually exists
#
# layout - the layout to check
#
# Returns Boolean true if the layout is invalid, false if otherwise
def invalid_layout?(layout)
!document.data["layout"].nil? && layout.nil? && !(document.is_a? Jekyll::Excerpt)
end
# Render layouts and place document content inside.
#
# Returns String rendered content
def place_in_layouts(content, payload, info)
output = content.dup
layout = layouts[document.data["layout"].to_s]
validate_layout(layout)
used = Set.new([layout])
# Reset the payload layout data to ensure it starts fresh for each page.
payload["layout"] = nil
while layout
output = render_layout(output, layout, info)
add_regenerator_dependencies(layout)
next unless (layout = site.layouts[layout.data["layout"]])
break if used.include?(layout)
used << layout
end
output
end
private
# Checks if the layout specified in the document actually exists
#
# layout - the layout to check
# Returns nothing
def validate_layout(layout)
return unless invalid_layout?(layout)
Jekyll.logger.warn "Build Warning:", "Layout '#{document.data["layout"]}' requested " \
"in #{document.relative_path} does not exist."
end
# Render layout content into document.output
#
# Returns String rendered content
def render_layout(output, layout, info)
payload["content"] = output
payload["layout"] = Utils.deep_merge_hashes(layout.data, payload["layout"] || {})
render_liquid(
layout.content,
payload,
info,
layout.path
)
end
def add_regenerator_dependencies(layout)
return unless document.write?
site.regenerator.add_dependency(
site.in_source_dir(document.path),
layout.path
)
end
# Set page content to payload and assign pager if document has one.
#
# Returns nothing
def assign_pages!
payload["page"] = document.to_liquid
payload["paginator"] = (document.pager.to_liquid if document.respond_to?(:pager))
end
# Set related posts to payload if document is a post.
#
# Returns nothing
def assign_current_document!
payload["site"].current_document = document
end
# Set highlighter prefix and suffix
#
# Returns nothing
def assign_highlighter_options!
payload["highlighter_prefix"] = converters.first.highlighter_prefix
payload["highlighter_suffix"] = converters.first.highlighter_suffix
end
def assign_layout_data!
layout = layouts[document.data["layout"]]
payload["layout"] = Utils.deep_merge_hashes(layout.data, payload["layout"] || {}) if layout
end
def permalink_ext
document_permalink = document.permalink
if document_permalink && !document_permalink.end_with?("/")
permalink_ext = File.extname(document_permalink)
permalink_ext unless permalink_ext.empty?
end
end
def converter_output_ext
if output_exts.size == 1
output_exts.last
else
output_exts[-2]
end
end
def output_exts
@output_exts ||= converters.map do |c|
c.output_ext(document.extname)
end.tap(&:compact!)
end
def liquid_options
@liquid_options ||= site.config["liquid"]
end
end
end

View File

@@ -0,0 +1,551 @@
# frozen_string_literal: true
module Jekyll
class Site
attr_reader :source, :dest, :cache_dir, :config
attr_accessor :layouts, :pages, :static_files, :drafts, :inclusions,
:exclude, :include, :lsi, :highlighter, :permalink_style,
:time, :future, :unpublished, :safe, :plugins, :limit_posts,
:show_drafts, :keep_files, :baseurl, :data, :file_read_opts,
:gems, :plugin_manager, :theme
attr_accessor :converters, :generators, :reader
attr_reader :regenerator, :liquid_renderer, :includes_load_paths, :filter_cache, :profiler
# Public: Initialize a new Site.
#
# config - A Hash containing site configuration details.
def initialize(config)
# Source and destination may not be changed after the site has been created.
@source = File.expand_path(config["source"]).freeze
@dest = File.expand_path(config["destination"]).freeze
self.config = config
@cache_dir = in_source_dir(config["cache_dir"])
@filter_cache = {}
@reader = Reader.new(self)
@profiler = Profiler.new(self)
@regenerator = Regenerator.new(self)
@liquid_renderer = LiquidRenderer.new(self)
Jekyll.sites << self
reset
setup
Jekyll::Hooks.trigger :site, :after_init, self
end
# Public: Set the site's configuration. This handles side-effects caused by
# changing values in the configuration.
#
# config - a Jekyll::Configuration, containing the new configuration.
#
# Returns the new configuration.
def config=(config)
@config = config.clone
%w(safe lsi highlighter baseurl exclude include future unpublished
show_drafts limit_posts keep_files).each do |opt|
send("#{opt}=", config[opt])
end
# keep using `gems` to avoid breaking change
self.gems = config["plugins"]
configure_cache
configure_plugins
configure_theme
configure_include_paths
configure_file_read_opts
self.permalink_style = config["permalink"].to_sym
# Read in a _config.yml from the current theme-gem at the very end.
@config = load_theme_configuration(config) if theme
@config
end
# Public: Read, process, and write this Site to output.
#
# Returns nothing.
def process
return profiler.profile_process if config["profile"]
reset
read
generate
render
cleanup
write
end
def print_stats
Jekyll.logger.info @liquid_renderer.stats_table
end
# rubocop:disable Metrics/AbcSize
# rubocop:disable Metrics/MethodLength
#
# Reset Site details.
#
# Returns nothing
def reset
self.time = if config["time"]
Utils.parse_date(config["time"].to_s, "Invalid time in _config.yml.")
else
Time.now
end
self.layouts = {}
self.inclusions = {}
self.pages = []
self.static_files = []
self.data = {}
@post_attr_hash = {}
@site_data = nil
@collections = nil
@documents = nil
@docs_to_write = nil
@regenerator.clear_cache
@liquid_renderer.reset
@site_cleaner = nil
frontmatter_defaults.reset
raise ArgumentError, "limit_posts must be a non-negative number" if limit_posts.negative?
Jekyll::Cache.clear_if_config_changed config
Jekyll::Hooks.trigger :site, :after_reset, self
nil
end
# rubocop:enable Metrics/MethodLength
# rubocop:enable Metrics/AbcSize
# Load necessary libraries, plugins, converters, and generators.
#
# Returns nothing.
def setup
ensure_not_in_dest
plugin_manager.conscientious_require
self.converters = instantiate_subclasses(Jekyll::Converter)
self.generators = instantiate_subclasses(Jekyll::Generator)
end
# Check that the destination dir isn't the source dir or a directory
# parent to the source dir.
def ensure_not_in_dest
dest_pathname = Pathname.new(dest)
Pathname.new(source).ascend do |path|
if path == dest_pathname
raise Errors::FatalException,
"Destination directory cannot be or contain the Source directory."
end
end
end
# The list of collections and their corresponding Jekyll::Collection instances.
# If config['collections'] is set, a new instance is created
# for each item in the collection, a new hash is returned otherwise.
#
# Returns a Hash containing collection name-to-instance pairs.
def collections
@collections ||= collection_names.each_with_object({}) do |name, hsh|
hsh[name] = Jekyll::Collection.new(self, name)
end
end
# The list of collection names.
#
# Returns an array of collection names from the configuration,
# or an empty array if the `collections` key is not set.
def collection_names
case config["collections"]
when Hash
config["collections"].keys
when Array
config["collections"]
when nil
[]
else
raise ArgumentError, "Your `collections` key must be a hash or an array."
end
end
# Read Site data from disk and load it into internal data structures.
#
# Returns nothing.
def read
reader.read
limit_posts!
Jekyll::Hooks.trigger :site, :post_read, self
nil
end
# Run each of the Generators.
#
# Returns nothing.
def generate
generators.each do |generator|
start = Time.now
generator.generate(self)
Jekyll.logger.debug "Generating:",
"#{generator.class} finished in #{Time.now - start} seconds."
end
nil
end
# Render the site to the destination.
#
# Returns nothing.
def render
relative_permalinks_are_deprecated
payload = site_payload
Jekyll::Hooks.trigger :site, :pre_render, self, payload
render_docs(payload)
render_pages(payload)
Jekyll::Hooks.trigger :site, :post_render, self, payload
nil
end
# Remove orphaned files and empty directories in destination.
#
# Returns nothing.
def cleanup
site_cleaner.cleanup!
nil
end
# Write static files, pages, and posts.
#
# Returns nothing.
def write
Jekyll::Commands::Doctor.conflicting_urls(self)
each_site_file do |item|
item.write(dest) if regenerator.regenerate?(item)
end
regenerator.write_metadata
Jekyll::Hooks.trigger :site, :post_write, self
nil
end
def posts
collections["posts"] ||= Collection.new(self, "posts")
end
# Construct a Hash of Posts indexed by the specified Post attribute.
#
# post_attr - The String name of the Post attribute.
#
# Examples
#
# post_attr_hash('categories')
# # => { 'tech' => [<Post A>, <Post B>],
# # 'ruby' => [<Post B>] }
#
# Returns the Hash: { attr => posts } where
# attr - One of the values for the requested attribute.
# posts - The Array of Posts with the given attr value.
def post_attr_hash(post_attr)
# Build a hash map based on the specified post attribute ( post attr =>
# array of posts ) then sort each array in reverse order.
@post_attr_hash[post_attr] ||= begin
hash = Hash.new { |h, key| h[key] = [] }
posts.docs.each do |p|
p.data[post_attr]&.each { |t| hash[t] << p }
end
hash.each_value { |posts| posts.sort!.reverse! }
hash
end
end
def tags
post_attr_hash("tags")
end
def categories
post_attr_hash("categories")
end
# Prepare site data for site payload. The method maintains backward compatibility
# if the key 'data' is already used in _config.yml.
#
# Returns the Hash to be hooked to site.data.
def site_data
@site_data ||= (config["data"] || data)
end
# The Hash payload containing site-wide data.
#
# Returns the Hash: { "site" => data } where data is a Hash with keys:
# "time" - The Time as specified in the configuration or the
# current time if none was specified.
# "posts" - The Array of Posts, sorted chronologically by post date
# and then title.
# "pages" - The Array of all Pages.
# "html_pages" - The Array of HTML Pages.
# "categories" - The Hash of category values and Posts.
# See Site#post_attr_hash for type info.
# "tags" - The Hash of tag values and Posts.
# See Site#post_attr_hash for type info.
def site_payload
Drops::UnifiedPayloadDrop.new self
end
alias_method :to_liquid, :site_payload
# Get the implementation class for the given Converter.
# Returns the Converter instance implementing the given Converter.
# klass - The Class of the Converter to fetch.
def find_converter_instance(klass)
@find_converter_instance ||= {}
@find_converter_instance[klass] ||= begin
converters.find { |converter| converter.instance_of?(klass) } || \
raise("No Converters found for #{klass}")
end
end
# klass - class or module containing the subclasses.
# Returns array of instances of subclasses of parameter.
# Create array of instances of the subclasses of the class or module
# passed in as argument.
def instantiate_subclasses(klass)
klass.descendants.select { |c| !safe || c.safe }.tap do |result|
result.sort!
result.map! { |c| c.new(config) }
end
end
# Warns the user if permanent links are relative to the parent
# directory. As this is a deprecated function of Jekyll.
#
# Returns
def relative_permalinks_are_deprecated
if config["relative_permalinks"]
Jekyll.logger.abort_with "Since v3.0, permalinks for pages" \
" in subfolders must be relative to the" \
" site source directory, not the parent" \
" directory. Check https://jekyllrb.com/docs/upgrading/"\
" for more info."
end
end
# Get the to be written documents
#
# Returns an Array of Documents which should be written
def docs_to_write
documents.select(&:write?)
end
# Get all the documents
#
# Returns an Array of all Documents
def documents
collections.each_with_object(Set.new) do |(_, collection), set|
set.merge(collection.docs).merge(collection.files)
end.to_a
end
def each_site_file
%w(pages static_files docs_to_write).each do |type|
send(type).each do |item|
yield item
end
end
end
# Returns the FrontmatterDefaults or creates a new FrontmatterDefaults
# if it doesn't already exist.
#
# Returns The FrontmatterDefaults
def frontmatter_defaults
@frontmatter_defaults ||= FrontmatterDefaults.new(self)
end
# Whether to perform a full rebuild without incremental regeneration
#
# Returns a Boolean: true for a full rebuild, false for normal build
def incremental?(override = {})
override["incremental"] || config["incremental"]
end
# Returns the publisher or creates a new publisher if it doesn't
# already exist.
#
# Returns The Publisher
def publisher
@publisher ||= Publisher.new(self)
end
# Public: Prefix a given path with the source directory.
#
# paths - (optional) path elements to a file or directory within the
# source directory
#
# Returns a path which is prefixed with the source directory.
def in_source_dir(*paths)
paths.reduce(source) do |base, path|
Jekyll.sanitized_path(base, path)
end
end
# Public: Prefix a given path with the theme directory.
#
# paths - (optional) path elements to a file or directory within the
# theme directory
#
# Returns a path which is prefixed with the theme root directory.
def in_theme_dir(*paths)
return nil unless theme
paths.reduce(theme.root) do |base, path|
Jekyll.sanitized_path(base, path)
end
end
# Public: Prefix a given path with the destination directory.
#
# paths - (optional) path elements to a file or directory within the
# destination directory
#
# Returns a path which is prefixed with the destination directory.
def in_dest_dir(*paths)
paths.reduce(dest) do |base, path|
Jekyll.sanitized_path(base, path)
end
end
# Public: Prefix a given path with the cache directory.
#
# paths - (optional) path elements to a file or directory within the
# cache directory
#
# Returns a path which is prefixed with the cache directory.
def in_cache_dir(*paths)
paths.reduce(cache_dir) do |base, path|
Jekyll.sanitized_path(base, path)
end
end
# Public: The full path to the directory that houses all the collections registered
# with the current site.
#
# Returns the source directory or the absolute path to the custom collections_dir
def collections_path
dir_str = config["collections_dir"]
@collections_path ||= dir_str.empty? ? source : in_source_dir(dir_str)
end
# Public
#
# Returns the object as a debug String.
def inspect
"#<#{self.class} @source=#{@source}>"
end
private
def load_theme_configuration(config)
return config if config["ignore_theme_config"] == true
theme_config_file = in_theme_dir("_config.yml")
return config unless File.exist?(theme_config_file)
# Bail out if the theme_config_file is a symlink file irrespective of safe mode
return config if File.symlink?(theme_config_file)
theme_config = SafeYAML.load_file(theme_config_file)
return config unless theme_config.is_a?(Hash)
Jekyll.logger.info "Theme Config file:", theme_config_file
# theme_config should not be overriding Jekyll's defaults
theme_config.delete_if { |key, _| Configuration::DEFAULTS.key?(key) }
# Override theme_config with existing config and return the result.
Utils.deep_merge_hashes(theme_config, config)
end
# Limits the current posts; removes the posts which exceed the limit_posts
#
# Returns nothing
def limit_posts!
if limit_posts.positive?
limit = posts.docs.length < limit_posts ? posts.docs.length : limit_posts
posts.docs = posts.docs[-limit, limit]
end
end
# Returns the Cleaner or creates a new Cleaner if it doesn't
# already exist.
#
# Returns The Cleaner
def site_cleaner
@site_cleaner ||= Cleaner.new(self)
end
# Disable Marshaling cache to disk in Safe Mode
def configure_cache
Jekyll::Cache.cache_dir = in_source_dir(config["cache_dir"], "Jekyll/Cache")
Jekyll::Cache.disable_disk_cache! if safe || config["disable_disk_cache"]
end
def configure_plugins
self.plugin_manager = Jekyll::PluginManager.new(self)
self.plugins = plugin_manager.plugins_path
end
def configure_theme
self.theme = nil
return if config["theme"].nil?
self.theme =
if config["theme"].is_a?(String)
Jekyll::Theme.new(config["theme"])
else
Jekyll.logger.warn "Theme:", "value of 'theme' in config should be " \
"String to use gem-based themes, but got #{config["theme"].class}"
nil
end
end
def configure_include_paths
@includes_load_paths = Array(in_source_dir(config["includes_dir"].to_s))
@includes_load_paths << theme.includes_path if theme&.includes_path
end
def configure_file_read_opts
self.file_read_opts = {}
file_read_opts[:encoding] = config["encoding"] if config["encoding"]
self.file_read_opts = Jekyll::Utils.merged_file_read_opts(self, {})
end
def render_docs(payload)
collections.each_value do |collection|
collection.docs.each do |document|
render_regenerated(document, payload)
end
end
end
def render_pages(payload)
pages.each do |page|
render_regenerated(page, payload)
end
end
def render_regenerated(document, payload)
return unless regenerator.regenerate?(document)
document.renderer.payload = payload
document.output = document.renderer.run
document.trigger_hooks(:post_render)
end
end
end

View File

@@ -0,0 +1,208 @@
# frozen_string_literal: true
module Jekyll
class StaticFile
extend Forwardable
attr_reader :relative_path, :extname, :name
def_delegator :to_liquid, :to_json, :to_json
class << self
# The cache of last modification times [path] -> mtime.
def mtimes
@mtimes ||= {}
end
def reset_cache
@mtimes = nil
end
end
# Initialize a new StaticFile.
#
# site - The Site.
# base - The String path to the <source>.
# dir - The String path between <source> and the file.
# name - The String filename of the file.
# rubocop: disable Metrics/ParameterLists
def initialize(site, base, dir, name, collection = nil)
@site = site
@base = base
@dir = dir
@name = name
@collection = collection
@relative_path = File.join(*[@dir, @name].compact)
@extname = File.extname(@name)
end
# rubocop: enable Metrics/ParameterLists
# Returns source file path.
def path
@path ||= begin
# Static file is from a collection inside custom collections directory
if !@collection.nil? && !@site.config["collections_dir"].empty?
File.join(*[@base, @site.config["collections_dir"], @dir, @name].compact)
else
File.join(*[@base, @dir, @name].compact)
end
end
end
# Obtain destination path.
#
# dest - The String path to the destination dir.
#
# Returns destination file path.
def destination(dest)
@destination ||= {}
@destination[dest] ||= @site.in_dest_dir(dest, Jekyll::URL.unescape_path(url))
end
def destination_rel_dir
if @collection
File.dirname(url)
else
@dir
end
end
def modified_time
@modified_time ||= File.stat(path).mtime
end
# Returns last modification time for this file.
def mtime
modified_time.to_i
end
# Is source path modified?
#
# Returns true if modified since last write.
def modified?
self.class.mtimes[path] != mtime
end
# Whether to write the file to the filesystem
#
# Returns true unless the defaults for the destination path from
# _config.yml contain `published: false`.
def write?
publishable = defaults.fetch("published", true)
return publishable unless @collection
publishable && @collection.write?
end
# Write the static file to the destination directory (if modified).
#
# dest - The String path to the destination dir.
#
# Returns false if the file was not modified since last time (no-op).
def write(dest)
dest_path = destination(dest)
return false if File.exist?(dest_path) && !modified?
self.class.mtimes[path] = mtime
FileUtils.mkdir_p(File.dirname(dest_path))
FileUtils.rm(dest_path) if File.exist?(dest_path)
copy_file(dest_path)
true
end
def data
@data ||= @site.frontmatter_defaults.all(relative_path, type)
end
def to_liquid
@to_liquid ||= Drops::StaticFileDrop.new(self)
end
# Generate "basename without extension" and strip away any trailing periods.
# NOTE: `String#gsub` removes all trailing periods (in comparison to `String#chomp`)
def basename
@basename ||= File.basename(name, extname).gsub(%r!\.*\z!, "")
end
def placeholders
{
:collection => @collection.label,
:path => cleaned_relative_path,
:output_ext => "",
:name => basename,
:title => "",
}
end
# Similar to Jekyll::Document#cleaned_relative_path.
# Generates a relative path with the collection's directory removed when applicable
# and additionally removes any multiple periods in the string.
#
# NOTE: `String#gsub!` removes all trailing periods (in comparison to `String#chomp!`)
#
# Examples:
# When `relative_path` is "_methods/site/my-cool-avatar...png":
# cleaned_relative_path
# # => "/site/my-cool-avatar"
#
# Returns the cleaned relative path of the static file.
def cleaned_relative_path
@cleaned_relative_path ||= begin
cleaned = relative_path[0..-extname.length - 1]
cleaned.gsub!(%r!\.*\z!, "")
cleaned.sub!(@collection.relative_directory, "") if @collection
cleaned
end
end
# Applies a similar URL-building technique as Jekyll::Document that takes
# the collection's URL template into account. The default URL template can
# be overriden in the collection's configuration in _config.yml.
def url
@url ||= begin
base = if @collection.nil?
cleaned_relative_path
else
Jekyll::URL.new(
:template => @collection.url_template,
:placeholders => placeholders
)
end.to_s.chomp("/")
base << extname
end
end
# Returns the type of the collection if present, nil otherwise.
def type
@type ||= @collection.nil? ? nil : @collection.label.to_sym
end
# Returns the front matter defaults defined for the file's URL and/or type
# as defined in _config.yml.
def defaults
@defaults ||= @site.frontmatter_defaults.all url, type
end
# Returns a debug string on inspecting the static file.
# Includes only the relative path of the object.
def inspect
"#<#{self.class} @relative_path=#{relative_path.inspect}>"
end
private
def copy_file(dest_path)
if @site.safe || Jekyll.env == "production"
FileUtils.cp(path, dest_path)
else
FileUtils.copy_entry(path, dest_path)
end
unless File.symlink?(dest_path)
File.utime(self.class.mtimes[path], self.class.mtimes[path], dest_path)
end
end
end
end

View File

@@ -0,0 +1,60 @@
# frozen_string_literal: true
module Jekyll
class Stevenson < ::Logger
def initialize
@progname = nil
@level = DEBUG
@default_formatter = Formatter.new
@logdev = $stdout
@formatter = proc do |_, _, _, msg|
msg.to_s
end
end
def add(severity, message = nil, progname = nil)
severity ||= UNKNOWN
@logdev = logdevice(severity)
return true if @logdev.nil? || severity < @level
progname ||= @progname
if message.nil?
if block_given?
message = yield
else
message = progname
progname = @progname
end
end
@logdev.puts(
format_message(format_severity(severity), Time.now, progname, message)
)
true
end
# Log a +WARN+ message
def warn(progname = nil, &block)
add(WARN, nil, progname.yellow, &block)
end
# Log an +ERROR+ message
def error(progname = nil, &block)
add(ERROR, nil, progname.red, &block)
end
def close
# No LogDevice in use
end
private
def logdevice(severity)
if severity > INFO
$stderr
else
$stdout
end
end
end
end

View File

@@ -0,0 +1,110 @@
# frozen_string_literal: true
module Jekyll
module Tags
class HighlightBlock < Liquid::Block
include Liquid::StandardFilters
# The regular expression syntax checker. Start with the language specifier.
# Follow that by zero or more space separated options that take one of three
# forms: name, name=value, or name="<quoted list>"
#
# <quoted list> is a space-separated list of numbers
SYNTAX = %r!^([a-zA-Z0-9.+#_-]+)((\s+\w+(=(\w+|"([0-9]+\s)*[0-9]+"))?)*)$!.freeze
def initialize(tag_name, markup, tokens)
super
if markup.strip =~ SYNTAX
@lang = Regexp.last_match(1).downcase
@highlight_options = parse_options(Regexp.last_match(2))
else
raise SyntaxError, <<~MSG
Syntax Error in tag 'highlight' while parsing the following markup:
#{markup}
Valid syntax: highlight <lang> [linenos]
MSG
end
end
LEADING_OR_TRAILING_LINE_TERMINATORS = %r!\A(\n|\r)+|(\n|\r)+\z!.freeze
def render(context)
prefix = context["highlighter_prefix"] || ""
suffix = context["highlighter_suffix"] || ""
code = super.to_s.gsub(LEADING_OR_TRAILING_LINE_TERMINATORS, "")
output =
case context.registers[:site].highlighter
when "rouge"
render_rouge(code)
when "pygments"
render_pygments(code, context)
else
render_codehighlighter(code)
end
rendered_output = add_code_tag(output)
prefix + rendered_output + suffix
end
private
OPTIONS_REGEX = %r!(?:\w="[^"]*"|\w=\w|\w)+!.freeze
def parse_options(input)
options = {}
return options if input.empty?
# Split along 3 possible forms -- key="<quoted list>", key=value, or key
input.scan(OPTIONS_REGEX) do |opt|
key, value = opt.split("=")
# If a quoted list, convert to array
if value&.include?('"')
value.delete!('"')
value = value.split
end
options[key.to_sym] = value || true
end
options[:linenos] = "inline" if options[:linenos] == true
options
end
def render_pygments(code, _context)
Jekyll.logger.warn "Warning:", "Highlight Tag no longer supports rendering with Pygments."
Jekyll.logger.warn "", "Using the default highlighter, Rouge, instead."
render_rouge(code)
end
def render_rouge(code)
require "rouge"
formatter = ::Rouge::Formatters::HTMLLegacy.new(
:line_numbers => @highlight_options[:linenos],
:wrap => false,
:css_class => "highlight",
:gutter_class => "gutter",
:code_class => "code"
)
lexer = ::Rouge::Lexer.find_fancy(@lang, code) || Rouge::Lexers::PlainText
formatter.format(lexer.lex(code))
end
def render_codehighlighter(code)
h(code).strip
end
def add_code_tag(code)
code_attributes = [
"class=\"language-#{@lang.to_s.tr("+", "-")}\"",
"data-lang=\"#{@lang}\"",
].join(" ")
"<figure class=\"highlight\"><pre><code #{code_attributes}>"\
"#{code.chomp}</code></pre></figure>"
end
end
end
end
Liquid::Template.register_tag("highlight", Jekyll::Tags::HighlightBlock)

View File

@@ -0,0 +1,270 @@
# frozen_string_literal: true
module Jekyll
module Tags
class IncludeTag < Liquid::Tag
VALID_SYNTAX = %r!
([\w-]+)\s*=\s*
(?:"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)'|([\w.-]+))
!x.freeze
VARIABLE_SYNTAX = %r!
(?<variable>[^{]*(\{\{\s*[\w\-.]+\s*(\|.*)?\}\}[^\s{}]*)+)
(?<params>.*)
!mx.freeze
FULL_VALID_SYNTAX = %r!\A\s*(?:#{VALID_SYNTAX}(?=\s|\z)\s*)*\z!.freeze
VALID_FILENAME_CHARS = %r!^[\w/.-]+$!.freeze
INVALID_SEQUENCES = %r![./]{2,}!.freeze
def initialize(tag_name, markup, tokens)
super
markup = markup.strip
matched = markup.match(VARIABLE_SYNTAX)
if matched
@file = matched["variable"].strip
@params = matched["params"].strip
else
@file, @params = markup.split(%r!\s+!, 2)
end
validate_params if @params
@tag_name = tag_name
end
def syntax_example
"{% #{@tag_name} file.ext param='value' param2='value' %}"
end
def parse_params(context)
params = {}
@params.scan(VALID_SYNTAX) do |key, d_quoted, s_quoted, variable|
value = if d_quoted
d_quoted.include?('\\"') ? d_quoted.gsub('\\"', '"') : d_quoted
elsif s_quoted
s_quoted.include?("\\'") ? s_quoted.gsub("\\'", "'") : s_quoted
elsif variable
context[variable]
end
params[key] = value
end
params
end
def validate_file_name(file)
if INVALID_SEQUENCES.match?(file) || !VALID_FILENAME_CHARS.match?(file)
raise ArgumentError, <<~MSG
Invalid syntax for include tag. File contains invalid characters or sequences:
#{file}
Valid syntax:
#{syntax_example}
MSG
end
end
def validate_params
unless FULL_VALID_SYNTAX.match?(@params)
raise ArgumentError, <<~MSG
Invalid syntax for include tag:
#{@params}
Valid syntax:
#{syntax_example}
MSG
end
end
# Grab file read opts in the context
def file_read_opts(context)
context.registers[:site].file_read_opts
end
# Render the variable if required
def render_variable(context)
Liquid::Template.parse(@file).render(context) if VARIABLE_SYNTAX.match?(@file)
end
def tag_includes_dirs(context)
context.registers[:site].includes_load_paths.freeze
end
def locate_include_file(context, file, safe)
includes_dirs = tag_includes_dirs(context)
includes_dirs.each do |dir|
path = PathManager.join(dir, file)
return path if valid_include_file?(path, dir.to_s, safe)
end
raise IOError, could_not_locate_message(file, includes_dirs, safe)
end
def render(context)
site = context.registers[:site]
file = render_variable(context) || @file
validate_file_name(file)
path = locate_include_file(context, file, site.safe)
return unless path
add_include_to_dependency(site, path, context)
partial = load_cached_partial(path, context)
context.stack do
context["include"] = parse_params(context) if @params
begin
partial.render!(context)
rescue Liquid::Error => e
e.template_name = path
e.markup_context = "included " if e.markup_context.nil?
raise e
end
end
end
def add_include_to_dependency(site, path, context)
if context.registers[:page]&.key?("path")
site.regenerator.add_dependency(
site.in_source_dir(context.registers[:page]["path"]),
path
)
end
end
def load_cached_partial(path, context)
context.registers[:cached_partials] ||= {}
cached_partial = context.registers[:cached_partials]
if cached_partial.key?(path)
cached_partial[path]
else
unparsed_file = context.registers[:site]
.liquid_renderer
.file(path)
begin
cached_partial[path] = unparsed_file.parse(read_file(path, context))
rescue Liquid::Error => e
e.template_name = path
e.markup_context = "included " if e.markup_context.nil?
raise e
end
end
end
def valid_include_file?(path, dir, safe)
!outside_site_source?(path, dir, safe) && File.file?(path)
end
def outside_site_source?(path, dir, safe)
safe && !realpath_prefixed_with?(path, dir)
end
def realpath_prefixed_with?(path, dir)
File.exist?(path) && File.realpath(path).start_with?(dir)
rescue StandardError
false
end
# This method allows to modify the file content by inheriting from the class.
def read_file(file, context)
File.read(file, **file_read_opts(context))
end
private
def could_not_locate_message(file, includes_dirs, safe)
message = "Could not locate the included file '#{file}' in any of "\
"#{includes_dirs}. Ensure it exists in one of those directories and"
message + if safe
" is not a symlink as those are not allowed in safe mode."
else
", if it is a symlink, does not point outside your site source."
end
end
end
# Do not inherit from this class.
# TODO: Merge into the `Jekyll::Tags::IncludeTag` in v5.0
class OptimizedIncludeTag < IncludeTag
def render(context)
@site ||= context.registers[:site]
file = render_variable(context) || @file
validate_file_name(file)
@site.inclusions[file] ||= locate_include_file(file)
inclusion = @site.inclusions[file]
add_include_to_dependency(inclusion, context) if @site.config["incremental"]
context.stack do
context["include"] = parse_params(context) if @params
inclusion.render(context)
end
end
private
def locate_include_file(file)
@site.includes_load_paths.each do |dir|
path = PathManager.join(dir, file)
return Inclusion.new(@site, dir, file) if valid_include_file?(path, dir)
end
raise IOError, could_not_locate_message(file, @site.includes_load_paths, @site.safe)
end
def valid_include_file?(path, dir)
File.file?(path) && !outside_scope?(path, dir)
end
def outside_scope?(path, dir)
@site.safe && !realpath_prefixed_with?(path, dir)
end
def realpath_prefixed_with?(path, dir)
File.realpath(path).start_with?(dir)
rescue StandardError
false
end
def add_include_to_dependency(inclusion, context)
return unless context.registers[:page]&.key?("path")
@site.regenerator.add_dependency(
@site.in_source_dir(context.registers[:page]["path"]),
inclusion.path
)
end
end
class IncludeRelativeTag < IncludeTag
def tag_includes_dirs(context)
Array(page_path(context)).freeze
end
def page_path(context)
page, site = context.registers.values_at(:page, :site)
return site.source unless page
site.in_source_dir File.dirname(resource_path(page, site))
end
private
def resource_path(page, site)
path = page["path"]
path = File.join(site.config["collections_dir"], path) if page["collection"]
path.sub(%r!/#excerpt\z!, "")
end
end
end
end
Liquid::Template.register_tag("include", Jekyll::Tags::OptimizedIncludeTag)
Liquid::Template.register_tag("include_relative", Jekyll::Tags::IncludeRelativeTag)

View File

@@ -0,0 +1,42 @@
# frozen_string_literal: true
module Jekyll
module Tags
class Link < Liquid::Tag
include Jekyll::Filters::URLFilters
class << self
def tag_name
name.split("::").last.downcase
end
end
def initialize(tag_name, relative_path, tokens)
super
@relative_path = relative_path.strip
end
def render(context)
@context = context
site = context.registers[:site]
relative_path = Liquid::Template.parse(@relative_path).render(context)
relative_path_with_leading_slash = PathManager.join("", relative_path)
site.each_site_file do |item|
return relative_url(item) if item.relative_path == relative_path
# This takes care of the case for static files that have a leading /
return relative_url(item) if item.relative_path == relative_path_with_leading_slash
end
raise ArgumentError, <<~MSG
Could not find document '#{relative_path}' in tag '#{self.class.tag_name}'.
Make sure the document exists and the path is correct.
MSG
end
end
end
end
Liquid::Template.register_tag(Jekyll::Tags::Link.tag_name, Jekyll::Tags::Link)

View File

@@ -0,0 +1,106 @@
# frozen_string_literal: true
module Jekyll
module Tags
class PostComparer
MATCHER = %r!^(.+/)*(\d+-\d+-\d+)-(.*)$!.freeze
attr_reader :path, :date, :slug, :name
def initialize(name)
@name = name
all, @path, @date, @slug = *name.sub(%r!^/!, "").match(MATCHER)
unless all
raise Jekyll::Errors::InvalidPostNameError,
"'#{name}' does not contain valid date and/or title."
end
basename_pattern = "#{date}-#{Regexp.escape(slug)}\\.[^.]+"
@name_regex = %r!^_posts/#{path}#{basename_pattern}|^#{path}_posts/?#{basename_pattern}!
end
def post_date
@post_date ||= Utils.parse_date(
date,
"'#{date}' does not contain valid date and/or title."
)
end
def ==(other)
other.relative_path.match(@name_regex)
end
def deprecated_equality(other)
slug == post_slug(other) &&
post_date.year == other.date.year &&
post_date.month == other.date.month &&
post_date.day == other.date.day
end
private
# Construct the directory-aware post slug for a Jekyll::Post
#
# other - the Jekyll::Post
#
# Returns the post slug with the subdirectory (relative to _posts)
def post_slug(other)
path = other.basename.split("/")[0...-1].join("/")
if path.nil? || path == ""
other.data["slug"]
else
"#{path}/#{other.data["slug"]}"
end
end
end
class PostUrl < Liquid::Tag
include Jekyll::Filters::URLFilters
def initialize(tag_name, post, tokens)
super
@orig_post = post.strip
begin
@post = PostComparer.new(@orig_post)
rescue StandardError => e
raise Jekyll::Errors::PostURLError, <<~MSG
Could not parse name of post "#{@orig_post}" in tag 'post_url'.
Make sure the post exists and the name is correct.
#{e.class}: #{e.message}
MSG
end
end
def render(context)
@context = context
site = context.registers[:site]
site.posts.docs.each do |document|
return relative_url(document) if @post == document
end
# New matching method did not match, fall back to old method
# with deprecation warning if this matches
site.posts.docs.each do |document|
next unless @post.deprecated_equality document
Jekyll::Deprecator.deprecation_message "A call to "\
"'{% post_url #{@post.name} %}' did not match " \
"a post using the new matching method of checking name " \
"(path-date-slug) equality. Please make sure that you " \
"change this tag to match the post's name exactly."
return relative_url(document)
end
raise Jekyll::Errors::PostURLError, <<~MSG
Could not find post "#{@orig_post}" in tag 'post_url'.
Make sure the post exists and the name is correct.
MSG
end
end
end
end
Liquid::Template.register_tag("post_url", Jekyll::Tags::PostUrl)

View File

@@ -0,0 +1,86 @@
# frozen_string_literal: true
module Jekyll
class Theme
extend Forwardable
attr_reader :name
def_delegator :gemspec, :version, :version
def initialize(name)
@name = name.downcase.strip
Jekyll.logger.debug "Theme:", name
Jekyll.logger.debug "Theme source:", root
end
def root
# Must use File.realpath to resolve symlinks created by rbenv
# Otherwise, Jekyll.sanitized path with prepend the unresolved root
@root ||= File.realpath(gemspec.full_gem_path)
rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP
raise "Path #{gemspec.full_gem_path} does not exist, is not accessible "\
"or includes a symbolic link loop"
end
# The name of theme directory
def basename
@basename ||= File.basename(root)
end
def includes_path
@includes_path ||= path_for "_includes"
end
def layouts_path
@layouts_path ||= path_for "_layouts"
end
def sass_path
@sass_path ||= path_for "_sass"
end
def assets_path
@assets_path ||= path_for "assets"
end
def runtime_dependencies
gemspec.runtime_dependencies
end
private
def path_for(folder)
path = realpath_for(folder)
path if path && File.directory?(path)
end
def realpath_for(folder)
# This resolves all symlinks for the theme subfolder and then ensures that the directory
# remains inside the theme root. This prevents the use of symlinks for theme subfolders to
# escape the theme root.
# However, symlinks are allowed to point to other directories within the theme.
Jekyll.sanitized_path(root, File.realpath(Jekyll.sanitized_path(root, folder.to_s)))
rescue Errno::ENOENT, Errno::EACCES, Errno::ELOOP => e
log_realpath_exception(e, folder)
nil
end
def log_realpath_exception(err, folder)
return if err.is_a?(Errno::ENOENT)
case err
when Errno::EACCES
Jekyll.logger.error "Theme error:", "Directory '#{folder}' is not accessible."
when Errno::ELOOP
Jekyll.logger.error "Theme error:", "Directory '#{folder}' includes a symbolic link loop."
end
end
def gemspec
@gemspec ||= Gem::Specification.find_by_name(name)
rescue Gem::LoadError
raise Jekyll::Errors::MissingDependencyException,
"The #{name} theme could not be found."
end
end
end

View File

@@ -0,0 +1,121 @@
# frozen_string_literal: true
module Jekyll
class ThemeBuilder
SCAFFOLD_DIRECTORIES = %w(
assets _layouts _includes _sass
).freeze
attr_reader :name, :path, :code_of_conduct
def initialize(theme_name, opts)
@name = theme_name.to_s.tr(" ", "_").squeeze("_")
@path = Pathname.new(File.expand_path(name, Dir.pwd))
@code_of_conduct = !!opts["code_of_conduct"]
end
def create!
create_directories
create_starter_files
create_gemspec
create_accessories
initialize_git_repo
end
def user_name
@user_name ||= `git config user.name`.chomp
end
def user_email
@user_email ||= `git config user.email`.chomp
end
private
def root
@root ||= Pathname.new(File.expand_path("../", __dir__))
end
def template_file(filename)
[
root.join("theme_template", "#{filename}.erb"),
root.join("theme_template", filename.to_s),
].find(&:exist?)
end
def template(filename)
erb.render(template_file(filename).read)
end
def erb
@erb ||= ERBRenderer.new(self)
end
def mkdir_p(directories)
Array(directories).each do |directory|
full_path = path.join(directory)
Jekyll.logger.info "create", full_path.to_s
FileUtils.mkdir_p(full_path)
end
end
def write_file(filename, contents)
full_path = path.join(filename)
Jekyll.logger.info "create", full_path.to_s
File.write(full_path, contents)
end
def create_directories
mkdir_p(SCAFFOLD_DIRECTORIES)
end
def create_starter_files
%w(page post default).each do |layout|
write_file("_layouts/#{layout}.html", template("_layouts/#{layout}.html"))
end
end
def create_gemspec
write_file("Gemfile", template("Gemfile"))
write_file("#{name}.gemspec", template("theme.gemspec"))
end
def create_accessories
accessories = %w(README.md LICENSE.txt)
accessories << "CODE_OF_CONDUCT.md" if code_of_conduct
accessories.each do |filename|
write_file(filename, template(filename))
end
end
def initialize_git_repo
Jekyll.logger.info "initialize", path.join(".git").to_s
Dir.chdir(path.to_s) { `git init` }
write_file(".gitignore", template("gitignore"))
end
class ERBRenderer
extend Forwardable
def_delegator :@theme_builder, :name, :theme_name
def_delegator :@theme_builder, :user_name, :user_name
def_delegator :@theme_builder, :user_email, :user_email
def initialize(theme_builder)
@theme_builder = theme_builder
end
def jekyll_version_with_minor
Jekyll::VERSION.split(".").take(2).join(".")
end
def theme_directories
SCAFFOLD_DIRECTORIES
end
def render(contents)
ERB.new(contents).result binding
end
end
end
end

View File

@@ -0,0 +1,167 @@
# frozen_string_literal: true
# Public: Methods that generate a URL for a resource such as a Post or a Page.
#
# Examples
#
# URL.new({
# :template => /:categories/:title.html",
# :placeholders => {:categories => "ruby", :title => "something"}
# }).to_s
#
module Jekyll
class URL
# options - One of :permalink or :template must be supplied.
# :template - The String used as template for URL generation,
# for example "/:path/:basename:output_ext", where
# a placeholder is prefixed with a colon.
# :placeholders - A hash containing the placeholders which will be
# replaced when used inside the template. E.g.
# { "year" => Time.now.strftime("%Y") } would replace
# the placeholder ":year" with the current year.
# :permalink - If supplied, no URL will be generated from the
# template. Instead, the given permalink will be
# used as URL.
def initialize(options)
@template = options[:template]
@placeholders = options[:placeholders] || {}
@permalink = options[:permalink]
if (@template || @permalink).nil?
raise ArgumentError, "One of :template or :permalink must be supplied."
end
end
# The generated relative URL of the resource
#
# Returns the String URL
def to_s
sanitize_url(generated_permalink || generated_url)
end
# Generates a URL from the permalink
#
# Returns the _unsanitized String URL
def generated_permalink
(@generated_permalink ||= generate_url(@permalink)) if @permalink
end
# Generates a URL from the template
#
# Returns the unsanitized String URL
def generated_url
@generated_url ||= generate_url(@template)
end
# Internal: Generate the URL by replacing all placeholders with their
# respective values in the given template
#
# Returns the unsanitized String URL
def generate_url(template)
if @placeholders.is_a? Drops::UrlDrop
generate_url_from_drop(template)
else
generate_url_from_hash(template)
end
end
def generate_url_from_hash(template)
@placeholders.inject(template) do |result, token|
break result if result.index(":").nil?
if token.last.nil?
# Remove leading "/" to avoid generating urls with `//`
result.gsub("/:#{token.first}", "")
else
result.gsub(":#{token.first}", self.class.escape_path(token.last))
end
end
end
# We include underscores in keys to allow for 'i_month' and so forth.
# This poses a problem for keys which are followed by an underscore
# but the underscore is not part of the key, e.g. '/:month_:day'.
# That should be :month and :day, but our key extraction regexp isn't
# smart enough to know that so we have to make it an explicit
# possibility.
def possible_keys(key)
if key.end_with?("_")
[key, key.chomp("_")]
else
[key]
end
end
def generate_url_from_drop(template)
template.gsub(%r!:([a-z_]+)!) do |match|
name = Regexp.last_match(1)
pool = name.end_with?("_") ? [name, name.chomp!("_")] : [name]
winner = pool.find { |key| @placeholders.key?(key) }
if winner.nil?
raise NoMethodError,
"The URL template doesn't have #{pool.join(" or ")} keys. "\
"Check your permalink template!"
end
value = @placeholders[winner]
value = "" if value.nil?
replacement = self.class.escape_path(value)
match.sub!(":#{winner}", replacement)
end
end
# Returns a sanitized String URL, stripping "../../" and multiples of "/",
# as well as the beginning "/" so we can enforce and ensure it.
def sanitize_url(str)
"/#{str}".gsub("..", "/").tap do |result|
result.gsub!("./", "")
result.squeeze!("/")
end
end
# Escapes a path to be a valid URL path segment
#
# path - The path to be escaped.
#
# Examples:
#
# URL.escape_path("/a b")
# # => "/a%20b"
#
# Returns the escaped path.
def self.escape_path(path)
return path if path.empty? || %r!^[a-zA-Z0-9./-]+$!.match?(path)
# Because URI.escape doesn't escape "?", "[" and "]" by default,
# specify unsafe string (except unreserved, sub-delims, ":", "@" and "/").
#
# URI path segment is defined in RFC 3986 as follows:
# segment = *pchar
# pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
# unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
# pct-encoded = "%" HEXDIG HEXDIG
# sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
# / "*" / "+" / "," / ";" / "="
Addressable::URI.encode(path).encode("utf-8").sub("#", "%23")
end
# Unescapes a URL path segment
#
# path - The path to be unescaped.
#
# Examples:
#
# URL.unescape_path("/a%20b")
# # => "/a b"
#
# Returns the unescaped path.
def self.unescape_path(path)
path = path.encode("utf-8")
return path unless path.include?("%")
Addressable::URI.unencode(path)
end
end
end

View File

@@ -0,0 +1,367 @@
# frozen_string_literal: true
module Jekyll
module Utils
extend self
autoload :Ansi, "jekyll/utils/ansi"
autoload :Exec, "jekyll/utils/exec"
autoload :Internet, "jekyll/utils/internet"
autoload :Platforms, "jekyll/utils/platforms"
autoload :ThreadEvent, "jekyll/utils/thread_event"
autoload :WinTZ, "jekyll/utils/win_tz"
# Constants for use in #slugify
SLUGIFY_MODES = %w(raw default pretty ascii latin).freeze
SLUGIFY_RAW_REGEXP = Regexp.new('\\s+').freeze
SLUGIFY_DEFAULT_REGEXP = Regexp.new("[^\\p{M}\\p{L}\\p{Nd}]+").freeze
SLUGIFY_PRETTY_REGEXP = Regexp.new("[^\\p{M}\\p{L}\\p{Nd}._~!$&'()+,;=@]+").freeze
SLUGIFY_ASCII_REGEXP = Regexp.new("[^[A-Za-z0-9]]+").freeze
# Takes a slug and turns it into a simple title.
def titleize_slug(slug)
slug.split("-").map!(&:capitalize).join(" ")
end
# Non-destructive version of deep_merge_hashes! See that method.
#
# Returns the merged hashes.
def deep_merge_hashes(master_hash, other_hash)
deep_merge_hashes!(master_hash.dup, other_hash)
end
# Merges a master hash with another hash, recursively.
#
# master_hash - the "parent" hash whose values will be overridden
# other_hash - the other hash whose values will be persisted after the merge
#
# This code was lovingly stolen from some random gem:
# http://gemjack.com/gems/tartan-0.1.1/classes/Hash.html
#
# Thanks to whoever made it.
def deep_merge_hashes!(target, overwrite)
merge_values(target, overwrite)
merge_default_proc(target, overwrite)
duplicate_frozen_values(target)
target
end
def mergable?(value)
value.is_a?(Hash) || value.is_a?(Drops::Drop)
end
def duplicable?(obj)
case obj
when nil, false, true, Symbol, Numeric
false
else
true
end
end
# Read array from the supplied hash favouring the singular key
# and then the plural key, and handling any nil entries.
#
# hash - the hash to read from
# singular_key - the singular key
# plural_key - the plural key
#
# Returns an array
def pluralized_array_from_hash(hash, singular_key, plural_key)
array = []
value = value_from_singular_key(hash, singular_key)
value ||= value_from_plural_key(hash, plural_key)
array << value
array.flatten!
array.compact!
array
end
def value_from_singular_key(hash, key)
hash[key] if hash.key?(key) || (hash.default_proc && hash[key])
end
def value_from_plural_key(hash, key)
if hash.key?(key) || (hash.default_proc && hash[key])
val = hash[key]
case val
when String
val.split
when Array
val.compact
end
end
end
def transform_keys(hash)
result = {}
hash.each_key do |key|
result[yield(key)] = hash[key]
end
result
end
# Apply #to_sym to all keys in the hash
#
# hash - the hash to which to apply this transformation
#
# Returns a new hash with symbolized keys
def symbolize_hash_keys(hash)
transform_keys(hash) { |key| key.to_sym rescue key }
end
# Apply #to_s to all keys in the Hash
#
# hash - the hash to which to apply this transformation
#
# Returns a new hash with stringified keys
def stringify_hash_keys(hash)
transform_keys(hash) { |key| key.to_s rescue key }
end
# Parse a date/time and throw an error if invalid
#
# input - the date/time to parse
# msg - (optional) the error message to show the user
#
# Returns the parsed date if successful, throws a FatalException
# if not
def parse_date(input, msg = "Input could not be parsed.")
Time.parse(input).localtime
rescue ArgumentError
raise Errors::InvalidDateError, "Invalid date '#{input}': #{msg}"
end
# Determines whether a given file has
#
# Returns true if the YAML front matter is present.
# rubocop: disable Naming/PredicateName
def has_yaml_header?(file)
File.open(file, "rb", &:readline).match? %r!\A---\s*\r?\n!
rescue EOFError
false
end
# Determine whether the given content string contains Liquid Tags or Vaiables
#
# Returns true is the string contains sequences of `{%` or `{{`
def has_liquid_construct?(content)
return false if content.nil? || content.empty?
content.include?("{%") || content.include?("{{")
end
# rubocop: enable Naming/PredicateName
# Slugify a filename or title.
#
# string - the filename or title to slugify
# mode - how string is slugified
# cased - whether to replace all uppercase letters with their
# lowercase counterparts
#
# When mode is "none", return the given string.
#
# When mode is "raw", return the given string,
# with every sequence of spaces characters replaced with a hyphen.
#
# When mode is "default" or nil, non-alphabetic characters are
# replaced with a hyphen too.
#
# When mode is "pretty", some non-alphabetic characters (._~!$&'()+,;=@)
# are not replaced with hyphen.
#
# When mode is "ascii", some everything else except ASCII characters
# a-z (lowercase), A-Z (uppercase) and 0-9 (numbers) are not replaced with hyphen.
#
# When mode is "latin", the input string is first preprocessed so that
# any letters with accents are replaced with the plain letter. Afterwards,
# it follows the "default" mode of operation.
#
# If cased is true, all uppercase letters in the result string are
# replaced with their lowercase counterparts.
#
# Examples:
# slugify("The _config.yml file")
# # => "the-config-yml-file"
#
# slugify("The _config.yml file", "pretty")
# # => "the-_config.yml-file"
#
# slugify("The _config.yml file", "pretty", true)
# # => "The-_config.yml file"
#
# slugify("The _config.yml file", "ascii")
# # => "the-config-yml-file"
#
# slugify("The _config.yml file", "latin")
# # => "the-config-yml-file"
#
# Returns the slugified string.
def slugify(string, mode: nil, cased: false)
mode ||= "default"
return nil if string.nil?
unless SLUGIFY_MODES.include?(mode)
return cased ? string : string.downcase
end
# Drop accent marks from latin characters. Everything else turns to ?
if mode == "latin"
I18n.config.available_locales = :en if I18n.config.available_locales.empty?
string = I18n.transliterate(string)
end
slug = replace_character_sequence_with_hyphen(string, :mode => mode)
# Remove leading/trailing hyphen
slug.gsub!(%r!^-|-$!i, "")
slug.downcase! unless cased
Jekyll.logger.warn("Warning:", "Empty `slug` generated for '#{string}'.") if slug.empty?
slug
end
# Add an appropriate suffix to template so that it matches the specified
# permalink style.
#
# template - permalink template without trailing slash or file extension
# permalink_style - permalink style, either built-in or custom
#
# The returned permalink template will use the same ending style as
# specified in permalink_style. For example, if permalink_style contains a
# trailing slash (or is :pretty, which indirectly has a trailing slash),
# then so will the returned template. If permalink_style has a trailing
# ":output_ext" (or is :none, :date, or :ordinal) then so will the returned
# template. Otherwise, template will be returned without modification.
#
# Examples:
# add_permalink_suffix("/:basename", :pretty)
# # => "/:basename/"
#
# add_permalink_suffix("/:basename", :date)
# # => "/:basename:output_ext"
#
# add_permalink_suffix("/:basename", "/:year/:month/:title/")
# # => "/:basename/"
#
# add_permalink_suffix("/:basename", "/:year/:month/:title")
# # => "/:basename"
#
# Returns the updated permalink template
def add_permalink_suffix(template, permalink_style)
template = template.dup
case permalink_style
when :pretty
template << "/"
when :date, :ordinal, :none
template << ":output_ext"
else
template << "/" if permalink_style.to_s.end_with?("/")
template << ":output_ext" if permalink_style.to_s.end_with?(":output_ext")
end
template
end
# Work the same way as Dir.glob but seperating the input into two parts
# ('dir' + '/' + 'pattern') to make sure the first part('dir') does not act
# as a pattern.
#
# For example, Dir.glob("path[/*") always returns an empty array,
# because the method fails to find the closing pattern to '[' which is ']'
#
# Examples:
# safe_glob("path[", "*")
# # => ["path[/file1", "path[/file2"]
#
# safe_glob("path", "*", File::FNM_DOTMATCH)
# # => ["path/.", "path/..", "path/file1"]
#
# safe_glob("path", ["**", "*"])
# # => ["path[/file1", "path[/folder/file2"]
#
# dir - the dir where glob will be executed under
# (the dir will be included to each result)
# patterns - the patterns (or the pattern) which will be applied under the dir
# flags - the flags which will be applied to the pattern
#
# Returns matched pathes
def safe_glob(dir, patterns, flags = 0)
return [] unless Dir.exist?(dir)
pattern = File.join(Array(patterns))
return [dir] if pattern.empty?
Dir.chdir(dir) do
Dir.glob(pattern, flags).map { |f| File.join(dir, f) }
end
end
# Returns merged option hash for File.read of self.site (if exists)
# and a given param
def merged_file_read_opts(site, opts)
merged = (site ? site.file_read_opts : {}).merge(opts)
if merged[:encoding] && !merged[:encoding].start_with?("bom|")
merged[:encoding] = "bom|#{merged[:encoding]}"
end
if merged["encoding"] && !merged["encoding"].start_with?("bom|")
merged["encoding"] = "bom|#{merged["encoding"]}"
end
merged
end
private
def merge_values(target, overwrite)
target.merge!(overwrite) do |_key, old_val, new_val|
if new_val.nil?
old_val
elsif mergable?(old_val) && mergable?(new_val)
deep_merge_hashes(old_val, new_val)
else
new_val
end
end
end
def merge_default_proc(target, overwrite)
if target.is_a?(Hash) && overwrite.is_a?(Hash) && target.default_proc.nil?
target.default_proc = overwrite.default_proc
end
end
def duplicate_frozen_values(target)
target.each do |key, val|
target[key] = val.dup if val.frozen? && duplicable?(val)
end
end
# Replace each character sequence with a hyphen.
#
# See Utils#slugify for a description of the character sequence specified
# by each mode.
def replace_character_sequence_with_hyphen(string, mode: "default")
replaceable_char =
case mode
when "raw"
SLUGIFY_RAW_REGEXP
when "pretty"
# "._~!$&'()+,;=@" is human readable (not URI-escaped) in URL
# and is allowed in both extN and NTFS.
SLUGIFY_PRETTY_REGEXP
when "ascii"
# For web servers not being able to handle Unicode, the safe
# method is to ditch anything else but latin letters and numeric
# digits.
SLUGIFY_ASCII_REGEXP
else
SLUGIFY_DEFAULT_REGEXP
end
# Strip according to the mode
string.gsub(replaceable_char, "-")
end
end
end

View File

@@ -0,0 +1,57 @@
# Frozen-string-literal: true
module Jekyll
module Utils
module Ansi
extend self
ESCAPE = format("%c", 27)
MATCH = %r!#{ESCAPE}\[(?:\d+)(?:;\d+)*(j|k|m|s|u|A|B|G)|\e\(B\e\[m!ix.freeze
COLORS = {
:red => 31,
:green => 32,
:black => 30,
:magenta => 35,
:yellow => 33,
:white => 37,
:blue => 34,
:cyan => 36,
}.freeze
# Strip ANSI from the current string. It also strips cursor stuff,
# well some of it, and it also strips some other stuff that a lot of
# the other ANSI strippers don't.
def strip(str)
str.gsub MATCH, ""
end
#
def has?(str)
!!(str =~ MATCH)
end
# Reset the color back to the default color so that you do not leak any
# colors when you move onto the next line. This is probably normally
# used as part of a wrapper so that we don't leak colors.
def reset(str = "")
@ansi_reset ||= format("%c[0m", 27)
"#{@ansi_reset}#{str}"
end
# SEE: `self::COLORS` for a list of methods. They are mostly
# standard base colors supported by pretty much any xterm-color, we do
# not need more than the base colors so we do not include them.
# Actually... if I'm honest we don't even need most of the
# base colors.
COLORS.each do |color, num|
define_method color do |str|
"#{format("%c", 27)}[#{num}m#{str}#{reset}"
end
end
end
end
end

View File

@@ -0,0 +1,26 @@
# frozen_string_literal: true
require "open3"
module Jekyll
module Utils
module Exec
extend self
# Runs a program in a sub-shell.
#
# *args - a list of strings containing the program name and arguments
#
# Returns a Process::Status and a String of output in an array in
# that order.
def run(*args)
stdin, stdout, stderr, process = Open3.popen3(*args)
out = stdout.read.strip
err = stderr.read.strip
[stdin, stdout, stderr].each(&:close)
[process.value, out + err]
end
end
end
end

View File

@@ -0,0 +1,37 @@
# frozen_string_literal: true
module Jekyll
module Utils
module Internet
# Public: Determine whether the present device has a connection to
# the Internet. This allows plugin writers which require the outside
# world to have a neat fallback mechanism for offline building.
#
# Example:
# if Internet.connected?
# Typhoeus.get("https://pages.github.com/versions.json")
# else
# Jekyll.logger.warn "Warning:", "Version check has been disabled."
# Jekyll.logger.warn "", "Connect to the Internet to enable it."
# nil
# end
#
# Returns true if a DNS call can successfully be made, or false if not.
module_function
def connected?
!dns("example.com").nil?
end
def dns(domain)
require "resolv"
Resolv::DNS.open do |resolver|
resolver.getaddress(domain)
end
rescue Resolv::ResolvError, Resolv::ResolvTimeout
nil
end
end
end
end

View File

@@ -0,0 +1,67 @@
# frozen_string_literal: true
module Jekyll
module Utils
module Platforms
extend self
def jruby?
RUBY_ENGINE == "jruby"
end
def mri?
RUBY_ENGINE == "ruby"
end
def windows?
vanilla_windows? || bash_on_windows?
end
# Not a Windows Subsystem for Linux (WSL)
def vanilla_windows?
rbconfig_host.match?(%r!mswin|mingw|cygwin!) && proc_version.empty?
end
alias_method :really_windows?, :vanilla_windows?
# Determine if Windows Subsystem for Linux (WSL)
def bash_on_windows?
linux_os? && microsoft_proc_version?
end
def linux?
linux_os? && !microsoft_proc_version?
end
def osx?
rbconfig_host.match?(%r!darwin|mac os!)
end
def unix?
rbconfig_host.match?(%r!solaris|bsd!)
end
private
def proc_version
@proc_version ||= \
begin
File.read("/proc/version").downcase
rescue Errno::ENOENT, Errno::EACCES
""
end
end
def rbconfig_host
@rbconfig_host ||= RbConfig::CONFIG["host_os"].downcase
end
def linux_os?
rbconfig_host.include?("linux")
end
def microsoft_proc_version?
proc_version.include?("microsoft")
end
end
end
end

View File

@@ -0,0 +1,31 @@
# frozen_string_literal: true
module Jekyll
module Utils
# Based on the pattern and code from
# https://emptysqua.re/blog/an-event-synchronization-primitive-for-ruby/
class ThreadEvent
attr_reader :flag
def initialize
@lock = Mutex.new
@cond = ConditionVariable.new
@flag = false
end
def set
@lock.synchronize do
yield if block_given?
@flag = true
@cond.broadcast
end
end
def wait
@lock.synchronize do
@cond.wait(@lock) unless @flag
end
end
end
end
end

View File

@@ -0,0 +1,75 @@
# frozen_string_literal: true
module Jekyll
module Utils
module WinTZ
extend self
# Public: Calculate the Timezone for Windows when the config file has a defined
# 'timezone' key.
#
# timezone - the IANA Time Zone specified in "_config.yml"
#
# Returns a string that ultimately re-defines ENV["TZ"] in Windows
def calculate(timezone)
External.require_with_graceful_fail("tzinfo") unless defined?(TZInfo)
tz = TZInfo::Timezone.get(timezone)
difference = Time.now.to_i - tz.now.to_i
#
# POSIX style definition reverses the offset sign.
# e.g. Eastern Standard Time (EST) that is 5Hrs. to the 'west' of Prime Meridian
# is denoted as:
# EST+5 (or) EST+05:00
# Reference: http://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html
sign = difference.negative? ? "-" : "+"
offset = sign == "-" ? "+" : "-" unless difference.zero?
#
# convert the difference (in seconds) to hours, as a rational number, and perform
# a modulo operation on it.
modulo = modulo_of(rational_hour(difference))
#
# Format the hour as a two-digit number.
# Establish the minutes based on modulo expression.
hh = format("%<hour>02d", :hour => absolute_hour(difference).ceil)
mm = modulo.zero? ? "00" : "30"
Jekyll.logger.debug "Timezone:", "#{timezone} #{offset}#{hh}:#{mm}"
#
# Note: The 3-letter-word below doesn't have a particular significance.
"WTZ#{sign}#{hh}:#{mm}"
end
private
# Private: Convert given seconds to an hour as a rational number.
#
# seconds - supplied as an integer, it is converted to a rational number.
# 3600 - no. of seconds in an hour.
#
# Returns a rational number.
def rational_hour(seconds)
seconds.to_r / 3600
end
# Private: Convert given seconds to an hour as an absolute number.
#
# seconds - supplied as an integer, it is converted to its absolute.
# 3600 - no. of seconds in an hour.
#
# Returns an integer.
def absolute_hour(seconds)
seconds.abs / 3600
end
# Private: Perform a modulo operation on a given fraction.
#
# fraction - supplied as a rational number, its numerator is divided
# by its denominator and the remainder returned.
#
# Returns an integer.
def modulo_of(fraction)
fraction.numerator % fraction.denominator
end
end
end
end

View File

@@ -0,0 +1,5 @@
# frozen_string_literal: true
module Jekyll
VERSION = "4.2.0"
end