1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137
|
# frozen_string_literal: true
require "listen"
module Jekyll
module Watcher
extend self
# Public: Continuously watch for file changes and rebuild the site
# whenever a change is detected.
#
# If the optional site argument is populated, that site instance will be
# reused and the options Hash ignored. Otherwise, a new site instance will
# be instantiated from the options Hash and used.
#
# options - A Hash containing the site configuration
# site - The current site instance (populated starting with Jekyll 3.2)
# (optional, default: nil)
#
# Returns nothing.
def watch(options, site = nil)
ENV["LISTEN_GEM_DEBUGGING"] ||= "1" if options["verbose"]
site ||= Jekyll::Site.new(options)
listener = build_listener(site, options)
listener.start
Jekyll.logger.info "Auto-regeneration:", "enabled for '#{options["source"]}'"
unless options["serving"]
trap("INT") do
listener.stop
Jekyll.logger.info "", "Halting auto-regeneration."
exit 0
end
sleep_forever
end
rescue ThreadError
# You pressed Ctrl-C, oh my!
end
private
def build_listener(site, options)
Listen.to(
options["source"],
:ignore => listen_ignore_paths(options),
:force_polling => options["force_polling"],
&listen_handler(site)
)
end
def listen_handler(site)
proc do |modified, added, removed|
t = Time.now
c = modified + added + removed
n = c.length
Jekyll.logger.info "Regenerating:",
"#{n} file(s) changed at #{t.strftime("%Y-%m-%d %H:%M:%S")}"
c.each { |path| Jekyll.logger.info "", path["#{site.source}/".length..-1] }
process(site, t)
end
end
def normalize_encoding(obj, desired_encoding)
case obj
when Array
obj.map { |entry| entry.encode!(desired_encoding, entry.encoding) }
when String
obj.encode!(desired_encoding, obj.encoding)
end
end
def custom_excludes(options)
Array(options["exclude"]).map { |e| Jekyll.sanitized_path(options["source"], e) }
end
def config_files(options)
%w(yml yaml toml).map do |ext|
Jekyll.sanitized_path(options["source"], "_config.#{ext}")
end
end
def to_exclude(options)
[
config_files(options),
options["destination"],
custom_excludes(options),
].flatten
end
# Paths to ignore for the watch option
#
# options - A Hash of options passed to the command
#
# Returns a list of relative paths from source that should be ignored
def listen_ignore_paths(options)
source = Pathname.new(options["source"]).expand_path
paths = to_exclude(options)
paths.map do |p|
absolute_path = Pathname.new(normalize_encoding(p, options["source"].encoding)).expand_path
next unless absolute_path.exist?
begin
relative_path = absolute_path.relative_path_from(source).to_s
relative_path = File.join(relative_path, "") if absolute_path.directory?
unless relative_path.start_with?("../")
path_to_ignore = %r!^#{Regexp.escape(relative_path)}!
Jekyll.logger.debug "Watcher:", "Ignoring #{path_to_ignore}"
path_to_ignore
end
rescue ArgumentError
# Could not find a relative path
end
end.compact + [%r!^\.jekyll\-metadata!]
end
def sleep_forever
loop { sleep 1000 }
end
def process(site, time)
begin
site.process
Jekyll.logger.info "", "...done in #{Time.now - time} seconds."
rescue StandardError => e
Jekyll.logger.warn "Error:", e.message
Jekyll.logger.warn "Error:", "Run jekyll build --trace for more information."
end
Jekyll.logger.info ""
end
end
end
|