From e746b3bd5f331044703435bd2c9965daf5ccd763 Mon Sep 17 00:00:00 2001 From: Parker Moore Date: Thu, 13 Mar 2014 14:07:05 -0400 Subject: [PATCH] Initialize each command in its own class so we can be *magical*. --- bin/jekyll | 92 ++---------------------- lib/jekyll/command.rb | 92 ++++++++++++++++++------ lib/jekyll/commands/build.rb | 130 ++++++++++++++++++++-------------- lib/jekyll/commands/docs.rb | 30 ++++++++ lib/jekyll/commands/doctor.rb | 19 ++++- lib/jekyll/commands/new.rb | 25 +++++-- lib/jekyll/commands/serve.rb | 129 ++++++++++++++++++++------------- 7 files changed, 299 insertions(+), 218 deletions(-) create mode 100644 lib/jekyll/commands/docs.rb diff --git a/bin/jekyll b/bin/jekyll index c073b755..9a34ae1f 100755 --- a/bin/jekyll +++ b/bin/jekyll @@ -8,17 +8,6 @@ require 'mercenary' Jekyll::Deprecator.process(ARGV) -def add_build_options(c) - c.option 'config', '--config CONFIG_FILE[,CONFIG_FILE2,...]', Array, 'Custom configuration file' - c.option 'future', '--future', 'Publishes posts with a future date' - c.option 'limit_posts', '--limit_posts MAX_POSTS', Integer, 'Limits the number of posts to parse and publish' - c.option 'watch', '-w', '--watch', 'Watch for changes and rebuild' - c.option 'lsi', '--lsi', 'Use LSI for improved related posts' - c.option 'show_drafts', '-D', '--drafts', 'Render posts in the _drafts folder' - c.option 'quiet', '-q', '--quiet', 'Silence output.' - c.option 'verbose', '-V', '--verbose', 'Print verbose output.' -end - Mercenary.program(:jekyll) do |p| p.version Jekyll::VERSION p.description 'Jekyll is a blog-aware, static site generator in Ruby' @@ -30,9 +19,11 @@ Mercenary.program(:jekyll) do |p| p.option 'plugins', '-p', '--plugins PLUGINS_DIR1[,PLUGINS_DIR2[,...]]', Array, 'Plugins directory (defaults to ./_plugins)' p.option 'layouts', '--layouts DIR', String, 'Layouts directory (defaults to ./_layouts)' + Jekyll::Command.subclasses.each { |c| c.init_with_program(p) } + p.action do |args, options| if args.empty? - p.go(["-h"]) + puts p else unless p.has_command?(args.first) Jekyll.logger.abort_with "Invalid command. Use --help for more information" @@ -40,83 +31,8 @@ Mercenary.program(:jekyll) do |p| end end - p.command(:new) do |c| - c.syntax 'jekyll 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.action do |args, options| - Jekyll::Commands::New.process(args) - end - end - - p.command(:build) do |c| - c.syntax 'jekyll build [options]' - c.description 'Build your site' - - add_build_options(c) - - c.action do |args, options| - options["serving"] = false - config = Jekyll.configuration(options) - Jekyll::Commands::Build.process(config) - end - end - - p.command(:serve) do |c| - c.syntax 'jekyll serve [options]' - c.description 'Serve your site locally' - c.alias :server - - add_build_options(c) - - c.option 'detach', '-B', '--detach', 'Run the server in the background (detach)' - c.option 'port', '-P', '--port [PORT]', 'Port to listen on' - c.option 'host', '-H', '--host [HOST]', 'Host to bind to' - c.option 'baseurl', '-b', '--baseurl [URL]', 'Base URL' - - c.action do |args, options| - options["serving"] ||= true - options = Jekyll.configuration(options) - Jekyll::Commands::Build.process(options) - Jekyll::Commands::Serve.process(options) - end - end - - p.command(:doctor) do |c| - c.syntax 'jekyll doctor' - c.description 'Search site and print specific deprecation warnings' - c.alias(:hyde) - - c.option '--config CONFIG_FILE[,CONFIG_FILE2,...]', Array, 'Custom configuration file' - - c.action do |args, options| - options = Jekyll.configuration(options) - Jekyll::Commands::Doctor.process(options) - end - end - - p.command(:docs) do |c| - c.syntax 'jekyll docs' - c.description "Launch local server with docs for Jekyll v#{Jekyll::VERSION}" - - c.option 'port', '-P', '--port [PORT]', 'Port to listen on' - c.option 'host', '-H', '--host [HOST]', 'Host to bind to' - - c.action do |args, options| - options = Jekyll.configuration(options.merge!({ - 'source' => File.expand_path("../site", File.dirname(__FILE__)), - 'destination' => File.expand_path("../site/_site", File.dirname(__FILE__)) - })) - Jekyll::Commands::Build.process(options) - Jekyll::Commands::Serve.process(options) - end - end - p.command(:import) do |c| - c.syntax 'jekyll import [options]' + c.syntax 'import [options]' c.description 'Import your old blog to Jekyll' importers = [] diff --git a/lib/jekyll/command.rb b/lib/jekyll/command.rb index 911d85b0..7bdf19ca 100644 --- a/lib/jekyll/command.rb +++ b/lib/jekyll/command.rb @@ -1,27 +1,79 @@ module Jekyll class Command - def self.globs(source, destination) - Dir.chdir(source) do - dirs = Dir['*'].select { |x| File.directory?(x) } - dirs -= [destination, File.expand_path(destination), File.basename(destination)] - dirs = dirs.map { |x| "#{x}/**/*" } - dirs += ['*'] + + 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 + + # Listing of all directories (globbed to include subfiles and folders) + # + # source - the source path + # destination - the destination path + # + # Returns an Array of directory globs in the source, excluding the destination + def globs(source, destination) + Dir.chdir(source) do + dirs = Dir['*'].select { |x| File.directory?(x) } + dirs -= [destination, File.expand_path(destination), File.basename(destination)] + dirs = dirs.map { |x| "#{x}/**/*" } + dirs += ['*'] + end + end + + # Run Site#process and catch errors + # + # site - the Jekyll::Site object + # + # Returns nothing + def process_site(site) + site.process + rescue Jekyll::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) + Jekyll.configuration(options) + end + + # Add common options to a command for building configuration + # + # c - the Jekyll::Command to add these options to + # + # Returns nothing + def add_build_options(c) + c.option 'config', '--config CONFIG_FILE[,CONFIG_FILE2,...]', Array, 'Custom configuration file' + c.option 'future', '--future', 'Publishes posts with a future date' + c.option 'limit_posts', '--limit_posts MAX_POSTS', Integer, 'Limits the number of posts to parse and publish' + c.option 'watch', '-w', '--watch', 'Watch for changes and rebuild' + c.option 'lsi', '--lsi', 'Use LSI for improved related posts' + c.option 'show_drafts', '-D', '--drafts', 'Render posts in the _drafts folder' + c.option 'quiet', '-q', '--quiet', 'Silence output.' + c.option 'verbose', '-V', '--verbose', 'Print verbose output.' + end + end - # Static: Run Site#process and catch errors - # - # site - the Jekyll::Site object - # - # Returns nothing - def self.process_site(site) - site.process - rescue Jekyll::FatalException => e - puts - Jekyll.logger.error "ERROR:", "YOUR SITE COULD NOT BE BUILT:" - Jekyll.logger.error "", "------------------------------------" - Jekyll.logger.error "", e.message - exit(1) - end end end diff --git a/lib/jekyll/commands/build.rb b/lib/jekyll/commands/build.rb index 42a4d136..bdd475cf 100644 --- a/lib/jekyll/commands/build.rb +++ b/lib/jekyll/commands/build.rb @@ -1,72 +1,96 @@ module Jekyll module Commands class Build < Command - def self.process(options) - site = Jekyll::Site.new(options) - Jekyll.logger.log_level = Jekyll::Stevenson::ERROR if options['quiet'] + class << self - build(site, options) - watch(site, options) if options['watch'] - end + # 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' - # Private: Build the site from source into destination. - # - # site - A Jekyll::Site instance - # options - A Hash of options passed to the command - # - # Returns nothing. - def self.build(site, options) - source = options['source'] - destination = options['destination'] - Jekyll.logger.info "Source:", source - Jekyll.logger.info "Destination:", destination - print Jekyll.logger.formatted_topic "Generating..." - process_site(site) - puts "done." - end + add_build_options(c) - # 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 self.watch(site, options) - require 'listen' - - source = options['source'] - destination = options['destination'] - - begin - dest = Pathname.new(destination).relative_path_from(Pathname.new(source)).to_s - ignored = Regexp.new(Regexp.escape(dest)) - rescue ArgumentError - # Destination is outside the source, no need to ignore it. - ignored = nil + c.action do |args, options| + options["serving"] = false + Jekyll::Commands::Build.process(options) + end + end end - Jekyll.logger.info "Auto-regeneration:", "enabled" + # Build your jekyll site + # Continuously watch if `watch` is set to true in the config. + def process(options) + options = configuration_from_options(options) + site = Jekyll::Site.new(options) - listener = Listen.to(source, :ignore => ignored) do |modified, added, removed| - t = Time.now.strftime("%Y-%m-%d %H:%M:%S") - n = modified.length + added.length + removed.length - print Jekyll.logger.formatted_topic("Regenerating:") + "#{n} files at #{t} " + Jekyll.logger.log_level = Jekyll::Stevenson::ERROR if options['quiet'] + + build(site, options) + watch(site, options) if options['watch'] + end + + # Build your Jekyll site. + # + # site - the Jekyll::Site instance to build + # options - the + # + # Returns nothing. + def build(site, options) + source = options['source'] + destination = options['destination'] + Jekyll.logger.info "Source:", source + Jekyll.logger.info "Destination:", destination + Jekyll.logger.info "Generating..." process_site(site) - puts "...done." + Jekyll.logger.info "", "done." end - listener.start - unless options['serving'] - trap("INT") do - listener.stop - puts " Halting auto-regeneration." - exit 0 + # 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) + require 'listen' + + source = options['source'] + destination = options['destination'] + + begin + dest = Pathname.new(destination).relative_path_from(Pathname.new(source)).to_s + ignored = Regexp.new(Regexp.escape(dest)) + rescue ArgumentError + # Destination is outside the source, no need to ignore it. + ignored = nil end - loop { sleep 1000 } + Jekyll.logger.info "Auto-regeneration:", "enabled" + + listener = Listen.to(source, :ignore => ignored) do |modified, added, removed| + t = Time.now.strftime("%Y-%m-%d %H:%M:%S") + n = modified.length + added.length + removed.length + print Jekyll.logger.formatted_topic("Regenerating:") + "#{n} files at #{t} " + process_site(site) + puts "...done." + end + listener.start + + unless options['serving'] + trap("INT") do + listener.stop + puts " Halting auto-regeneration." + exit 0 + end + + loop { sleep 1000 } + end end - end + + end # end of class << self + end end end diff --git a/lib/jekyll/commands/docs.rb b/lib/jekyll/commands/docs.rb new file mode 100644 index 00000000..71c10160 --- /dev/null +++ b/lib/jekyll/commands/docs.rb @@ -0,0 +1,30 @@ +module Jekyll + module Commands + class Docs < Command + + class << self + + def init_with_program(prog) + prog.command(:docs) do |c| + c.syntax 'docs' + c.description "Launch local server with docs for Jekyll v#{Jekyll::VERSION}" + + c.option 'port', '-P', '--port [PORT]', 'Port to listen on' + c.option 'host', '-H', '--host [HOST]', 'Host to bind to' + + c.action do |args, options| + options.merge!({ + 'source' => File.expand_path("../../../site", File.dirname(__FILE__)), + 'destination' => File.expand_path("../../../site/_site", File.dirname(__FILE__)) + }) + Jekyll::Commands::Build.process(options) + Jekyll::Commands::Serve.process(options) + end + end + end + + end + + end + end +end diff --git a/lib/jekyll/commands/doctor.rb b/lib/jekyll/commands/doctor.rb index 7ebab903..3e4ad75c 100644 --- a/lib/jekyll/commands/doctor.rb +++ b/lib/jekyll/commands/doctor.rb @@ -2,8 +2,23 @@ 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_FILE[,CONFIG_FILE2,...]', Array, 'Custom configuration file' + + c.action do |args, options| + Jekyll::Commands::Doctor.process(options) + end + end + end + def process(options) - site = Jekyll::Site.new(options) + site = Jekyll::Site.new(configuration_from_options(options)) site.read if healthy?(site) @@ -61,7 +76,9 @@ module Jekyll end urls end + end + end end end diff --git a/lib/jekyll/commands/new.rb b/lib/jekyll/commands/new.rb index 00b08c20..d348315a 100644 --- a/lib/jekyll/commands/new.rb +++ b/lib/jekyll/commands/new.rb @@ -3,23 +3,36 @@ require 'erb' module Jekyll module Commands class New < Command + def self.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.action do |args, options| + Jekyll::Commands::New.process(args, options) + end + end + end + def self.process(args, options = {}) raise ArgumentError.new('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." - exit(1) + Jekyll.logger.abort_with "Conflict:", "#{new_blog_path} exists and is not empty." end - if options[:blank] + if options["blank"] create_blank_site new_blog_path else create_sample_files new_blog_path - File.open(File.expand_path(self.initialized_post_name, new_blog_path), "w") do |f| - f.write(self.scaffold_post_content) + File.open(File.expand_path(initialized_post_name, new_blog_path), "w") do |f| + f.write(scaffold_post_content) end end @@ -47,7 +60,7 @@ module Jekyll private def self.preserve_source_location?(path, options) - !options[:force] && !Dir["#{path}/**/*"].empty? + !options["force"] && !Dir["#{path}/**/*"].empty? end def self.create_sample_files(path) diff --git a/lib/jekyll/commands/serve.rb b/lib/jekyll/commands/serve.rb index ed7fb59f..f884a08a 100644 --- a/lib/jekyll/commands/serve.rb +++ b/lib/jekyll/commands/serve.rb @@ -2,76 +2,105 @@ module Jekyll module Commands class Serve < Command - def self.process(options) - require 'webrick' - include WEBrick - destination = options['destination'] + class << self - FileUtils.mkdir_p(destination) + def init_with_program(prog) + prog.command(:serve) do |c| + c.syntax 'serve [options]' + c.description 'Serve your site locally' + c.alias :server - # monkey patch WEBrick using custom 404 page (/404.html) - if File.exists?(File.join(destination, '404.html')) - WEBrick::HTTPResponse.class_eval do - def create_error_page - @body = IO.read(File.join(@config[:DocumentRoot], '404.html')) + add_build_options(c) + + c.option 'detach', '-B', '--detach', 'Run the server in the background (detach)' + c.option 'port', '-P', '--port [PORT]', 'Port to listen on' + c.option 'host', '-H', '--host [HOST]', 'Host to bind to' + c.option 'baseurl', '-b', '--baseurl [URL]', 'Base URL' + + c.action do |args, options| + options["serving"] ||= true + Jekyll::Commands::Build.process(options) + Jekyll::Commands::Serve.process(options) end end end - # recreate NondisclosureName under utf-8 circumstance - fh_option = WEBrick::Config::FileHandler - fh_option[:NondisclosureName] = ['.ht*','~*'] + # Boot up a WEBrick server which points to the compiled site's root. + def process(options) + options = configuration_from_options(options) - s = HTTPServer.new(webrick_options(options)) + require 'webrick' - s.config.store(:DirectoryIndex, s.config[:DirectoryIndex] << "index.xml") + destination = options['destination'] - s.mount(options['baseurl'], HTTPServlet::FileHandler, destination, fh_option) + FileUtils.mkdir_p(destination) - Jekyll.logger.info "Server address:", "http://#{s.config[:BindAddress]}:#{s.config[:Port]}" + # monkey patch WEBrick using custom 404 page (/404.html) + if File.exists?(File.join(destination, '404.html')) + WEBrick::HTTPResponse.class_eval do + def create_error_page + @body = IO.read(File.join(@config[:DocumentRoot], '404.html')) + end + end + end - if options['detach'] # detach the server - pid = Process.fork { s.start } - Process.detach(pid) - Jekyll.logger.info "Server detached with pid '#{pid}'.", "Run `kill -9 #{pid}' to stop the server." - else # create a new server thread, then join it with current terminal - t = Thread.new { s.start } - trap("INT") { s.shutdown } - t.join() - end - end + # recreate NondisclosureName under utf-8 circumstance + fh_option = WEBrick::Config::FileHandler + fh_option[:NondisclosureName] = ['.ht*','~*'] - def self.webrick_options(config) - opts = { - :DocumentRoot => config['destination'], - :Port => config['port'], - :BindAddress => config['host'], - :MimeTypes => self.mime_types, - :DoNotReverseLookup => true, - :StartCallback => start_callback(config['detach']) - } + s = WEBrick::HTTPServer.new(webrick_options(options)) - if !config['verbose'] - opts.merge!({ - :AccessLog => [], - :Logger => Log::new([], Log::WARN) - }) + s.config.store(:DirectoryIndex, s.config[:DirectoryIndex] << "index.xml") + + s.mount(options['baseurl'], WEBrick::HTTPServlet::FileHandler, destination, fh_option) + + Jekyll.logger.info "Server address:", "http://#{s.config[:BindAddress]}:#{s.config[:Port]}" + + if options['detach'] # detach the server + pid = Process.fork { s.start } + Process.detach(pid) + Jekyll.logger.info "Server detached with pid '#{pid}'.", "Run `kill -9 #{pid}' to stop the server." + else # create a new server thread, then join it with current terminal + t = Thread.new { s.start } + trap("INT") { s.shutdown } + t.join + end end - opts - end + def webrick_options(config) + opts = { + :DocumentRoot => config['destination'], + :Port => config['port'], + :BindAddress => config['host'], + :MimeTypes => mime_types, + :DoNotReverseLookup => true, + :StartCallback => start_callback(config['detach']) + } - def self.start_callback(detached) - unless detached - Proc.new { Jekyll.logger.info "Server running...", "press ctrl-c to stop." } + if !config['verbose'] + opts.merge!({ + :AccessLog => [], + :Logger => WEBrick::Log.new([], WEBrick::Log::WARN) + }) + end + + opts end + + def start_callback(detached) + unless detached + Proc.new { Jekyll.logger.info "Server running...", "press ctrl-c to stop." } + end + end + + def mime_types + mime_types_file = File.expand_path('../mime.types', File.dirname(__FILE__)) + WEBrick::HTTPUtils::load_mime_types(mime_types_file) + end + end - def self.mime_types - mime_types_file = File.expand_path('../mime.types', File.dirname(__FILE__)) - WEBrick::HTTPUtils::load_mime_types(mime_types_file) - end end end end