From 61aca896cec58874517d76893f7aedb45c42335a Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Mon, 5 Aug 2024 18:11:28 +0100 Subject: [PATCH 1/3] Move Servlet under RDoc::RI This class is used as a server for the ri command, not as a rdoc server. So putting it under RDoc::RI will make its purpose clearer. --- Gemfile | 1 + lib/rdoc.rb | 1 - lib/rdoc/ri.rb | 7 ++++--- lib/rdoc/ri/driver.rb | 2 +- lib/rdoc/{ => ri}/servlet.rb | 10 +++++----- rdoc.gemspec | 2 +- test/rdoc/test_rdoc_servlet.rb | 4 ++-- 7 files changed, 14 insertions(+), 13 deletions(-) rename lib/rdoc/{ => ri}/servlet.rb (98%) diff --git a/Gemfile b/Gemfile index 8435662441..4e97336b88 100644 --- a/Gemfile +++ b/Gemfile @@ -11,4 +11,5 @@ group :development do gem 'rubocop', '>= 1.31.0' gem 'gettext' gem 'prism', '>= 0.30.0' + gem 'webrick' end diff --git a/lib/rdoc.rb b/lib/rdoc.rb index 209042ed0e..8c3559c241 100644 --- a/lib/rdoc.rb +++ b/lib/rdoc.rb @@ -162,7 +162,6 @@ def self.home autoload :Generator, "#{__dir__}/rdoc/generator" autoload :Options, "#{__dir__}/rdoc/options" autoload :Parser, "#{__dir__}/rdoc/parser" - autoload :Servlet, "#{__dir__}/rdoc/servlet" autoload :RI, "#{__dir__}/rdoc/ri" autoload :Stats, "#{__dir__}/rdoc/stats" autoload :Store, "#{__dir__}/rdoc/store" diff --git a/lib/rdoc/ri.rb b/lib/rdoc/ri.rb index 0af05f729f..2d187f39d4 100644 --- a/lib/rdoc/ri.rb +++ b/lib/rdoc/ri.rb @@ -13,8 +13,9 @@ module RDoc::RI class Error < RDoc::Error; end - autoload :Driver, "#{__dir__}/ri/driver" - autoload :Paths, "#{__dir__}/ri/paths" - autoload :Store, "#{__dir__}/ri/store" + autoload :Driver, "#{__dir__}/ri/driver" + autoload :Paths, "#{__dir__}/ri/paths" + autoload :Store, "#{__dir__}/ri/store" + autoload :Servlet, "#{__dir__}/ri/servlet" end diff --git a/lib/rdoc/ri/driver.rb b/lib/rdoc/ri/driver.rb index c6fddbac67..de58661361 100644 --- a/lib/rdoc/ri/driver.rb +++ b/lib/rdoc/ri/driver.rb @@ -1506,7 +1506,7 @@ def start_server extra_doc_dirs = @stores.map {|s| s.type == :extra ? s.path : nil}.compact - server.mount '/', RDoc::Servlet, nil, extra_doc_dirs + server.mount '/', RDoc::RI::Servlet, nil, extra_doc_dirs trap 'INT' do server.shutdown end trap 'TERM' do server.shutdown end diff --git a/lib/rdoc/servlet.rb b/lib/rdoc/ri/servlet.rb similarity index 98% rename from lib/rdoc/servlet.rb rename to lib/rdoc/ri/servlet.rb index d05368766a..8c887be402 100644 --- a/lib/rdoc/servlet.rb +++ b/lib/rdoc/ri/servlet.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -require_relative '../rdoc' +require_relative '../../rdoc' require 'erb' require 'time' require 'json' @@ -24,14 +24,14 @@ # # server = WEBrick::HTTPServer.new Port: 8000 # -# server.mount '/', RDoc::Servlet +# server.mount '/', RDoc::RI::Servlet # # If you want to mount the servlet some other place than the root, provide the # base path when mounting: # -# server.mount '/rdoc', RDoc::Servlet, '/rdoc' +# server.mount '/rdoc', RDoc::RI::Servlet, '/rdoc' -class RDoc::Servlet < WEBrick::HTTPServlet::AbstractServlet +class RDoc::RI::Servlet < WEBrick::HTTPServlet::AbstractServlet @server_stores = Hash.new { |hash, server| hash[server] = {} } @cache = Hash.new { |hash, store| hash[store] = {} } @@ -146,7 +146,7 @@ def do_GET req, res # Fills in +res+ with the class, module or page for +req+ from +store+. # # +path+ is relative to the mount_path and is used to determine the class, - # module or page name (/RDoc/Servlet.html becomes RDoc::Servlet). + # module or page name (/RDoc/RI.html becomes RDoc::RI). # +generator+ is used to create the page. def documentation_page store, generator, path, req, res diff --git a/rdoc.gemspec b/rdoc.gemspec index 93a281c8ae..0730f77a22 100644 --- a/rdoc.gemspec +++ b/rdoc.gemspec @@ -208,8 +208,8 @@ RDoc includes the +rdoc+ and +ri+ tools for generating and displaying documentat "lib/rdoc/ri/paths.rb", "lib/rdoc/ri/store.rb", "lib/rdoc/ri/task.rb", + "lib/rdoc/ri/servlet.rb", "lib/rdoc/rubygems_hook.rb", - "lib/rdoc/servlet.rb", "lib/rdoc/single_class.rb", "lib/rdoc/stats.rb", "lib/rdoc/stats/normal.rb", diff --git a/test/rdoc/test_rdoc_servlet.rb b/test/rdoc/test_rdoc_servlet.rb index 7a5b15a6eb..81da17f24a 100644 --- a/test/rdoc/test_rdoc_servlet.rb +++ b/test/rdoc/test_rdoc_servlet.rb @@ -30,7 +30,7 @@ def @server.mount(*) end @extra_dirs = [File.join(@tempdir, 'extra1'), File.join(@tempdir, 'extra2')] - @s = RDoc::Servlet.new @server, @stores, @cache, nil, @extra_dirs + @s = RDoc::RI::Servlet.new @server, @stores, @cache, nil, @extra_dirs @req = WEBrick::HTTPRequest.new :Logger => nil @res = WEBrick::HTTPResponse.new :HTTPVersion => '1.0' @@ -141,7 +141,7 @@ def @req.path() raise 'no' end end def test_do_GET_mount_path - @s = RDoc::Servlet.new @server, @stores, @cache, '/mount/path' + @s = RDoc::RI::Servlet.new @server, @stores, @cache, '/mount/path' temp_dir do FileUtils.mkdir 'css' From f95b96d2c3498af492133226a1221d07b4314ff0 Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Mon, 5 Aug 2024 19:28:40 +0100 Subject: [PATCH 2/3] Add RDoc::Server and --server flag --- lib/rdoc.rb | 1 + lib/rdoc/generator/darkfish.rb | 23 ++++- lib/rdoc/options.rb | 15 +++ lib/rdoc/rdoc.rb | 36 ++++++- lib/rdoc/server.rb | 182 +++++++++++++++++++++++++++++++++ rdoc.gemspec | 1 + test/rdoc/test_rdoc_options.rb | 3 +- 7 files changed, 253 insertions(+), 8 deletions(-) create mode 100644 lib/rdoc/server.rb 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..ca50194677 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 ") @@ -608,12 +610,23 @@ def setup return unless @store + update_data_from_store + end + + def update_data_from_store @classes = @store.all_classes_and_modules.sort @files = @store.all_files.sort @methods = @classes.flat_map { |m| m.method_list }.sort @modsort = get_sorted_module_list @classes end + def clear_data + @classes.clear if @classes + @files.clear if @files + @methods.clear if @methods + @modsort.clear if @modsort + end + ## # Return a string describing the amount of time in the given number of # seconds in terms a human can understand easily. 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..d8abe07f34 100644 --- a/lib/rdoc/rdoc.rb +++ b/lib/rdoc/rdoc.rb @@ -3,6 +3,7 @@ require 'find' require 'fileutils' +require 'tmpdir' require 'pathname' require 'time' @@ -463,7 +464,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 +497,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 +515,30 @@ 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 + + tmp_dir = Dir.mktmpdir + + # Change the output directory to tmp so it doesn't overwrite the current documentation + Dir.chdir tmp_dir 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 + ensure + FileUtils.remove_entry tmp_dir + 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..3043209633 --- /dev/null +++ b/lib/rdoc/server.rb @@ -0,0 +1,182 @@ +# 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 + + # Generator copies data from store to instance variables + # We need to clear it before regenerating the documentation + @generator.clear_data + + # Reparse the files + @rdoc.parse_files(@rdoc.options.files) + + # Update the data from store + @generator.update_data_from_store + + # 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..40549879d8 100644 --- a/test/rdoc/test_rdoc_options.rb +++ b/test/rdoc/test_rdoc_options.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require_relative 'helper' -class TestRDocOptions < RDoc::TestCase +class RDocOptionsTest < RDoc::TestCase def setup super @@ -82,6 +82,7 @@ def test_to_yaml 'title' => nil, 'visibility' => :protected, 'webcvs' => nil, + 'server_port' => false, 'skip_tests' => true, } From 7e9a4744bef49f0f640508fcde5433360f75af0b Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Sat, 31 Aug 2024 21:39:59 +0100 Subject: [PATCH 3/3] Update lib/rdoc/options.rb Co-authored-by: Colby Swandale <996377+colby-swandale@users.noreply.github.com> --- lib/rdoc/options.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rdoc/options.rb b/lib/rdoc/options.rb index fb31405227..d440768a29 100644 --- a/lib/rdoc/options.rb +++ b/lib/rdoc/options.rb @@ -341,7 +341,7 @@ class RDoc::Options attr_reader :visibility ## - # The server's port to listen on. `nil` means the server is disabled. + # The server's port to listen on. `nil` or `false` means the server is disabled. attr_reader :server_port