This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
# 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)
|
||||
External.require_with_graceful_fail "jekyll-watch"
|
||||
Jekyll::Watcher.watch(options, site)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,44 @@
|
||||
# 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
|
||||
@@ -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 workarounds 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://github.com/sporkmonger/addressable/blob/0a0e96acb17225f9b1c9cab0bad332b448934c9a/lib/addressable/uri.rb#L103
|
||||
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
|
||||
@@ -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
|
||||
@@ -0,0 +1,168 @@
|
||||
# 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", "< 3"
|
||||
gem "tzinfo-data"
|
||||
end
|
||||
|
||||
# Performance-booster for watching directories on Windows
|
||||
gem "wdm", "~> 0.1", :platforms => [:mingw, :x64_mingw, :mswin]
|
||||
|
||||
# Lock `http_parser.rb` gem to `v0.6.x` on JRuby builds since newer versions of the gem
|
||||
# do not have a Java counterpart.
|
||||
gem "http_parser.rb", "~> 0.6.0", :platforms => [:jruby]
|
||||
RUBY
|
||||
end
|
||||
|
||||
def create_site(new_blog_path)
|
||||
create_sample_files new_blog_path
|
||||
|
||||
File.write(File.expand_path(initialized_post_name, new_blog_path), scaffold_post_content)
|
||||
|
||||
File.write(File.expand_path("Gemfile", new_blog_path), gemfile_contents)
|
||||
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
|
||||
@@ -0,0 +1,39 @@
|
||||
# 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
|
||||
@@ -0,0 +1,372 @@
|
||||
# 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 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["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"]
|
||||
opts["livereload_port"] ||= LIVERELOAD_PORT
|
||||
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
|
||||
|
||||
def register_reload_hooks(opts)
|
||||
require_relative "serve/live_reload_reactor"
|
||||
@reload_reactor = LiveReloadReactor.new
|
||||
|
||||
Jekyll::Hooks.register(:site, :post_render) do |site|
|
||||
@changed_pages = []
|
||||
site.each_site_file do |item|
|
||||
@changed_pages << item if site.regenerator.regenerate?(item)
|
||||
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, 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
|
||||
|
||||
# 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.
|
||||
#
|
||||
# rubocop:disable Security/IoMethods
|
||||
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
|
||||
# rubocop:enable Security/IoMethods
|
||||
|
||||
def webrick_opts(opts)
|
||||
opts = {
|
||||
:JekyllOptions => opts,
|
||||
:DoNotReverseLookup => true,
|
||||
:MimeTypes => mime_types,
|
||||
:MimeTypesCharset => mime_types_charset,
|
||||
: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)
|
||||
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
|
||||
# Detach the process from controlling terminal
|
||||
$stdin.reopen("/dev/null", "r")
|
||||
$stdout.reopen("/dev/null", "w")
|
||||
$stderr.reopen("/dev/null", "w")
|
||||
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."
|
||||
|
||||
# Exit without running `at_exit` inherited by the forked process.
|
||||
Process.exit! 0
|
||||
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 mime_types_charset
|
||||
SafeYAML.load_file(File.expand_path("serve/mime_types_charset.json", __dir__))
|
||||
end
|
||||
|
||||
def read_file(source_dir, file_path)
|
||||
File.read(Jekyll.sanitized_path(source_dir, file_path))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,119 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "em-websocket"
|
||||
|
||||
require_relative "websockets"
|
||||
|
||||
module Jekyll
|
||||
module Commands
|
||||
class Serve
|
||||
class LiveReloadReactor
|
||||
attr_reader :started_event, :stopped_event, :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 URL #{p.url.inspect}"
|
||||
@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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,71 @@
|
||||
{
|
||||
"application/javascript": "UTF-8",
|
||||
"application/json": "UTF-8",
|
||||
"application/manifest+json": "UTF-8",
|
||||
"application/vnd.syncml+xml": "UTF-8",
|
||||
"application/vnd.syncml.dm+wbxml": "UTF-8",
|
||||
"application/vnd.syncml.dm+xml": "UTF-8",
|
||||
"application/vnd.syncml.dmddf+xml": "UTF-8",
|
||||
"application/vnd.wap.wbxml": "UTF-8",
|
||||
"text/cache-manifest": "UTF-8",
|
||||
"text/calendar": "UTF-8",
|
||||
"text/coffeescript": "UTF-8",
|
||||
"text/css": "UTF-8",
|
||||
"text/csv": "UTF-8",
|
||||
"text/html": "UTF-8",
|
||||
"text/jade": "UTF-8",
|
||||
"text/jsx": "UTF-8",
|
||||
"text/less": "UTF-8",
|
||||
"text/markdown": "UTF-8",
|
||||
"text/mathml": "UTF-8",
|
||||
"text/mdx": "UTF-8",
|
||||
"text/n3": "UTF-8",
|
||||
"text/plain": "UTF-8",
|
||||
"text/prs.lines.tag": "UTF-8",
|
||||
"text/richtext": "UTF-8",
|
||||
"text/sgml": "UTF-8",
|
||||
"text/shex": "UTF-8",
|
||||
"text/slim": "UTF-8",
|
||||
"text/spdx": "UTF-8",
|
||||
"text/stylus": "UTF-8",
|
||||
"text/tab-separated-values": "UTF-8",
|
||||
"text/troff": "UTF-8",
|
||||
"text/turtle": "UTF-8",
|
||||
"text/uri-list": "UTF-8",
|
||||
"text/vcard": "UTF-8",
|
||||
"text/vnd.curl": "UTF-8",
|
||||
"text/vnd.curl.dcurl": "UTF-8",
|
||||
"text/vnd.curl.mcurl": "UTF-8",
|
||||
"text/vnd.curl.scurl": "UTF-8",
|
||||
"text/vnd.familysearch.gedcom": "UTF-8",
|
||||
"text/vnd.fly": "UTF-8",
|
||||
"text/vnd.fmi.flexstor": "UTF-8",
|
||||
"text/vnd.graphviz": "UTF-8",
|
||||
"text/vnd.in3d.3dml": "UTF-8",
|
||||
"text/vnd.in3d.spot": "UTF-8",
|
||||
"text/vnd.sun.j2me.app-descriptor": "UTF-8",
|
||||
"text/vnd.wap.wml": "UTF-8",
|
||||
"text/vnd.wap.wmlscript": "UTF-8",
|
||||
"text/vtt": "UTF-8",
|
||||
"text/x-asm": "UTF-8",
|
||||
"text/x-c": "UTF-8",
|
||||
"text/x-component": "UTF-8",
|
||||
"text/x-fortran": "UTF-8",
|
||||
"text/x-handlebars-template": "UTF-8",
|
||||
"text/x-java-source": "UTF-8",
|
||||
"text/x-lua": "UTF-8",
|
||||
"text/x-markdown": "UTF-8",
|
||||
"text/x-nfo": "UTF-8",
|
||||
"text/x-opml": "UTF-8",
|
||||
"text/x-pascal": "UTF-8",
|
||||
"text/x-processing": "UTF-8",
|
||||
"text/x-sass": "UTF-8",
|
||||
"text/x-scss": "UTF-8",
|
||||
"text/x-setext": "UTF-8",
|
||||
"text/x-sfv": "UTF-8",
|
||||
"text/x-suse-ymp": "UTF-8",
|
||||
"text/x-uuencode": "UTF-8",
|
||||
"text/x-vcalendar": "UTF-8",
|
||||
"text/x-vcard": "UTF-8",
|
||||
"text/yaml": "UTF-8"
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
# 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="' + location.protocol + '//' +
|
||||
(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 += "&mindelay=#{@options["livereload_min_delay"]}"
|
||||
end
|
||||
if @options["livereload_max_delay"]
|
||||
src += "&maxdelay=#{@options["livereload_max_delay"]}"
|
||||
end
|
||||
src += "&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]
|
||||
@mime_types_charset = server.config[:MimeTypesCharset]
|
||||
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
|
||||
|
||||
conditionally_inject_charset(res)
|
||||
res.header.merge!(@headers)
|
||||
rtn
|
||||
end
|
||||
# rubocop:enable Naming/MethodName
|
||||
|
||||
private
|
||||
|
||||
# Inject charset based on Jekyll config only if our mime-types database contains
|
||||
# the charset metadata.
|
||||
#
|
||||
# Refer `script/vendor-mimes` in the repository for further details.
|
||||
def conditionally_inject_charset(res)
|
||||
typ = res.header["content-type"]
|
||||
return unless @mime_types_charset.key?(typ)
|
||||
return if %r!;\s*charset=!.match?(typ)
|
||||
|
||||
res.header["content-type"] = "#{typ}; charset=#{@jekyll_opts["encoding"]}"
|
||||
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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user