diff --git a/lib/rdoc.rb b/lib/rdoc.rb index 8c3559c241..84fe8e1f1e 100644 --- a/lib/rdoc.rb +++ b/lib/rdoc.rb @@ -167,6 +167,7 @@ def self.home autoload :Store, "#{__dir__}/rdoc/store" autoload :Task, "#{__dir__}/rdoc/task" autoload :Text, "#{__dir__}/rdoc/text" + autoload :Server, "#{__dir__}/rdoc/server" autoload :Markdown, "#{__dir__}/rdoc/markdown" autoload :Markup, "#{__dir__}/rdoc/markup" diff --git a/lib/rdoc/generator/darkfish.rb b/lib/rdoc/generator/darkfish.rb index 96bb4fb66f..2321b37041 100644 --- a/lib/rdoc/generator/darkfish.rb +++ b/lib/rdoc/generator/darkfish.rb @@ -238,7 +238,7 @@ def write_style_sheet # Build the initial indices and output objects based on an array of TopLevel # objects containing the extracted information. - def generate + def generate(server_mode: false) setup write_style_sheet @@ -246,11 +246,13 @@ def generate generate_class_files generate_file_files generate_table_of_contents - @json_index.generate - @json_index.generate_gzipped - - copy_static + unless server_mode + # For the server, we only generate the JSON index if requested + @json_index.generate + @json_index.generate_gzipped + copy_static + end rescue => e debug_msg "%s: %s\n %s" % [ e.class.name, e.message, e.backtrace.join("\n ") diff --git a/lib/rdoc/options.rb b/lib/rdoc/options.rb index 2631d57364..fb31405227 100644 --- a/lib/rdoc/options.rb +++ b/lib/rdoc/options.rb @@ -340,6 +340,11 @@ class RDoc::Options attr_reader :visibility + ## + # The server's port to listen on. `nil` means the server is disabled. + + attr_reader :server_port + ## # Indicates if files of test suites should be skipped attr_accessor :skip_tests @@ -392,6 +397,7 @@ def init_ivars # :nodoc: @encoding = Encoding::UTF_8 @charset = @encoding.name @skip_tests = true + @server_port = false end def init_with map # :nodoc: @@ -1116,6 +1122,15 @@ def parse argv opt.separator nil + opt.on( + "--server[=PORT]", + Integer, + "[Experimental] Run WEBrick server with generated documentation.", + "Uses port 4000 by default. Will use ./tmp for file output." + ) do |port| + @server_port = port || 4000 + end + opt.on("--help", "-h", "Display this help") do RDoc::RDoc::GENERATORS.each_key do |generator| setup_generator generator diff --git a/lib/rdoc/rdoc.rb b/lib/rdoc/rdoc.rb index 47108ceee3..7b8d278dda 100644 --- a/lib/rdoc/rdoc.rb +++ b/lib/rdoc/rdoc.rb @@ -463,7 +463,7 @@ def document options exit end - unless @options.coverage_report then + unless @options.coverage_report || @options.server_port @last_modified = setup_output_dir @options.op_dir, @options.force_update end @@ -496,9 +496,16 @@ def document options @generator = gen_klass.new @store, @options - generate + if @options.server_port + start_server + else + generate + end end + # Don't need to run stats for server mode + return if @options.server_port + if @stats and (@options.coverage_report or not @options.quiet) then puts puts @stats.summary.accept RDoc::Markup::ToRdoc.new @@ -507,6 +514,26 @@ def document options exit @stats.fully_documented? if @options.coverage_report end + def start_server + begin + require 'webrick' + rescue LoadError + abort "webrick is not found. You may need to `gem install webrick` to install webrick." + end + + # Change the output directory to tmp so it doesn't overwrite the current documentation + Dir.chdir "tmp" do + server = WEBrick::HTTPServer.new Port: @options.server_port + + server.mount '/', RDoc::Server, self + + trap 'INT' do server.shutdown end + trap 'TERM' do server.shutdown end + + server.start + end + end + ## # Generates documentation for +file_info+ (from #parse_files) into the # output dir using the generator selected diff --git a/lib/rdoc/server.rb b/lib/rdoc/server.rb new file mode 100644 index 0000000000..dfe23346f8 --- /dev/null +++ b/lib/rdoc/server.rb @@ -0,0 +1,175 @@ +# frozen_string_literal: true +require 'erb' +require 'time' +require 'json' + +class RDoc::Server < WEBrick::HTTPServlet::AbstractServlet + ## + # Creates an instance of this servlet that shares cached data between + # requests. + + def self.get_instance server, rdoc # :nodoc: + new server, rdoc + end + + ## + # Creates a new WEBrick servlet. + # + # +server+ is provided automatically by WEBrick when mounting. + # +rdoc+ is the RDoc::RDoc instance to display documentation from. + + def initialize server, rdoc + super server + + @rdoc = rdoc + @generator = rdoc.generator + @generator.file_output = false + + darkfish_dir = File.join(__dir__, 'generator/template/darkfish/') + json_index_dir = File.join(__dir__, 'generator/template/json_index/') + + @asset_dirs = { + :darkfish => darkfish_dir, + :json_index => json_index_dir, + } + end + + ## + # GET request entry point. Fills in +res+ for the path, etc. in +req+. + + def do_GET req, res + req.path.sub!(/\A\//, '') + + case req.path + when '/' + res.body = @generator.generate_servlet_root installed_docs + res.content_type = 'text/html' + when 'js/darkfish.js', 'js/jquery.js', 'js/search.js', %r{^css/}, %r{^images/}, %r{^fonts/} + asset :darkfish, req, res + when 'js/navigation.js', 'js/searcher.js' + asset :json_index, req, res + when 'js/search_index.js' + res.body = "var search_data = #{JSON.dump @generator.json_index.build_index}" + res.content_type = 'application/javascript' + else + show_documentation req, res + end + rescue WEBrick::HTTPStatus::NotFound => e + not_found @generator, req, res, e.message + rescue WEBrick::HTTPStatus::Status + raise + rescue => e + $stderr.puts e.full_message + error e, req, res + end + + private + + def asset generator_name, req, res + asset_dir = @asset_dirs[generator_name] + + asset_path = File.join asset_dir, req.path + + res.body = File.read asset_path + + res.content_type = case req.path + when /\.css\z/ then 'text/css' + when /\.js\z/ then 'application/javascript' + else 'application/octet-stream' + end + end + + def error exception, req, res + backtrace = exception.backtrace.join "\n" + + res.content_type = 'text/html' + res.status = 500 + res.body = <<-BODY + + + + + +Error - #{ERB::Util.html_escape exception.class} + + + + +

Error

+ +

While processing #{ERB::Util.html_escape req.request_uri} the +RDoc (#{ERB::Util.html_escape RDoc::VERSION}) server has encountered a +#{ERB::Util.html_escape exception.class} +exception: + +

#{ERB::Util.html_escape exception.message}
+ +

Please report this to the +RDoc issues tracker. Please +include the RDoc version, the URI above and exception class, message and +backtrace. If you're viewing a gem's documentation, include the gem name and +version. If you're viewing Ruby's documentation, include the version of ruby. + +

Backtrace: + +

#{ERB::Util.html_escape backtrace}
+ + + + BODY + end + + def not_found generator, req, res, message = nil + message ||= "The page #{ERB::Util.h req.path} was not found" + res.body = generator.generate_servlet_not_found message + res.status = 404 + end + + PAGE_NAME_SUB_REGEXP = /_([^_]*)\z/ + + def show_documentation req, res + store = @rdoc.store + # Clear all the previous data + store.classes_hash.clear + store.modules_hash.clear + store.files_hash.clear + + # RDoc instance use last_modified list to avoid reparsing files + # We need to clear it to force reparsing + @rdoc.last_modified.clear + + # Reparse the files + @rdoc.parse_files(@rdoc.options.files) + + # Regenerate the documentation and asserts + @generator.generate(server_mode: true) + + case req.path + when nil, '', 'index.html' + res.body = @generator.generate_index + when 'table_of_contents.html' + res.body = @generator.generate_table_of_contents + else + text_name = req.path.chomp '.html' + name = text_name.gsub '/', '::' + + content = if klass = store.find_class_or_module(name) + @generator.generate_class klass + elsif page = store.find_text_page(name.sub(PAGE_NAME_SUB_REGEXP, '.\1')) + @generator.generate_page page + elsif page = store.find_text_page(text_name.sub(PAGE_NAME_SUB_REGEXP, '.\1')) + @generator.generate_page page + elsif page = store.find_file_named(text_name.sub(PAGE_NAME_SUB_REGEXP, '.\1')) + @generator.generate_page page + end + + if content + res.body = content + else + not_found @generator, req, res + end + end + ensure + res.content_type ||= 'text/html' + end +end diff --git a/rdoc.gemspec b/rdoc.gemspec index 0730f77a22..442d8e70c9 100644 --- a/rdoc.gemspec +++ b/rdoc.gemspec @@ -210,6 +210,7 @@ RDoc includes the +rdoc+ and +ri+ tools for generating and displaying documentat "lib/rdoc/ri/task.rb", "lib/rdoc/ri/servlet.rb", "lib/rdoc/rubygems_hook.rb", + "lib/rdoc/server.rb", "lib/rdoc/single_class.rb", "lib/rdoc/stats.rb", "lib/rdoc/stats/normal.rb", diff --git a/test/rdoc/test_rdoc_options.rb b/test/rdoc/test_rdoc_options.rb index 5e8b1ae3d8..fafc9d4074 100644 --- a/test/rdoc/test_rdoc_options.rb +++ b/test/rdoc/test_rdoc_options.rb @@ -82,6 +82,7 @@ def test_to_yaml 'title' => nil, 'visibility' => :protected, 'webcvs' => nil, + 'server' => false, 'skip_tests' => true, }