1# frozen_string_literal: true 2require 'rdoc' 3require 'erb' 4require 'time' 5require 'json' 6require 'webrick' 7 8## 9# This is a WEBrick servlet that allows you to browse ri documentation. 10# 11# You can show documentation through either `ri --server` or, with RubyGems 12# 2.0 or newer, `gem server`. For ri, the server runs on port 8214 by 13# default. For RubyGems the server runs on port 8808 by default. 14# 15# You can use this servlet in your own project by mounting it on a WEBrick 16# server: 17# 18# require 'webrick' 19# 20# server = WEBrick::HTTPServer.new Port: 8000 21# 22# server.mount '/', RDoc::Servlet 23# 24# If you want to mount the servlet some other place than the root, provide the 25# base path when mounting: 26# 27# server.mount '/rdoc', RDoc::Servlet, '/rdoc' 28 29class RDoc::Servlet < WEBrick::HTTPServlet::AbstractServlet 30 31 @server_stores = Hash.new { |hash, server| hash[server] = {} } 32 @cache = Hash.new { |hash, store| hash[store] = {} } 33 34 ## 35 # Maps an asset type to its path on the filesystem 36 37 attr_reader :asset_dirs 38 39 ## 40 # An RDoc::Options instance used for rendering options 41 42 attr_reader :options 43 44 ## 45 # Creates an instance of this servlet that shares cached data between 46 # requests. 47 48 def self.get_instance server, *options # :nodoc: 49 stores = @server_stores[server] 50 51 new server, stores, @cache, *options 52 end 53 54 ## 55 # Creates a new WEBrick servlet. 56 # 57 # Use +mount_path+ when mounting the servlet somewhere other than /. 58 # 59 # Use +extra_doc_dirs+ for additional documentation directories. 60 # 61 # +server+ is provided automatically by WEBrick when mounting. +stores+ and 62 # +cache+ are provided automatically by the servlet. 63 64 def initialize server, stores, cache, mount_path = nil, extra_doc_dirs = [] 65 super server 66 67 @cache = cache 68 @mount_path = mount_path 69 @extra_doc_dirs = extra_doc_dirs 70 @stores = stores 71 72 @options = RDoc::Options.new 73 @options.op_dir = '.' 74 75 darkfish_dir = nil 76 77 # HACK dup 78 $LOAD_PATH.each do |path| 79 darkfish_dir = File.join path, 'rdoc/generator/template/darkfish/' 80 next unless File.directory? darkfish_dir 81 @options.template_dir = darkfish_dir 82 break 83 end 84 85 @asset_dirs = { 86 :darkfish => darkfish_dir, 87 :json_index => 88 File.expand_path('../generator/template/json_index/', __FILE__), 89 } 90 end 91 92 ## 93 # Serves the asset at the path in +req+ for +generator_name+ via +res+. 94 95 def asset generator_name, req, res 96 asset_dir = @asset_dirs[generator_name] 97 98 asset_path = File.join asset_dir, req.path 99 100 if_modified_since req, res, asset_path 101 102 res.body = File.read asset_path 103 104 res.content_type = case req.path 105 when /css$/ then 'text/css' 106 when /js$/ then 'application/javascript' 107 else 'application/octet-stream' 108 end 109 end 110 111 ## 112 # GET request entry point. Fills in +res+ for the path, etc. in +req+. 113 114 def do_GET req, res 115 req.path.sub!(/^#{Regexp.escape @mount_path}/o, '') if @mount_path 116 117 case req.path 118 when '/' then 119 root req, res 120 when '/js/darkfish.js', '/js/jquery.js', '/js/search.js', 121 %r%^/css/%, %r%^/images/%, %r%^/fonts/% then 122 asset :darkfish, req, res 123 when '/js/navigation.js', '/js/searcher.js' then 124 asset :json_index, req, res 125 when '/js/search_index.js' then 126 root_search req, res 127 else 128 show_documentation req, res 129 end 130 rescue WEBrick::HTTPStatus::NotFound => e 131 generator = generator_for RDoc::Store.new 132 133 not_found generator, req, res, e.message 134 rescue WEBrick::HTTPStatus::Status 135 raise 136 rescue => e 137 error e, req, res 138 end 139 140 ## 141 # Fills in +res+ with the class, module or page for +req+ from +store+. 142 # 143 # +path+ is relative to the mount_path and is used to determine the class, 144 # module or page name (/RDoc/Servlet.html becomes RDoc::Servlet). 145 # +generator+ is used to create the page. 146 147 def documentation_page store, generator, path, req, res 148 name = path.sub(/.html$/, '').gsub '/', '::' 149 150 if klass = store.find_class_or_module(name) then 151 res.body = generator.generate_class klass 152 elsif page = store.find_text_page(name.sub(/_([^_]*)$/, '.\1')) then 153 res.body = generator.generate_page page 154 else 155 not_found generator, req, res 156 end 157 end 158 159 ## 160 # Creates the JSON search index on +res+ for the given +store+. +generator+ 161 # must respond to \#json_index to build. +req+ is ignored. 162 163 def documentation_search store, generator, req, res 164 json_index = @cache[store].fetch :json_index do 165 @cache[store][:json_index] = 166 JSON.dump generator.json_index.build_index 167 end 168 169 res.content_type = 'application/javascript' 170 res.body = "var search_data = #{json_index}" 171 end 172 173 ## 174 # Returns the RDoc::Store and path relative to +mount_path+ for 175 # documentation at +path+. 176 177 def documentation_source path 178 _, source_name, path = path.split '/', 3 179 180 store = @stores[source_name] 181 return store, path if store 182 183 store = store_for source_name 184 185 store.load_all 186 187 @stores[source_name] = store 188 189 return store, path 190 end 191 192 ## 193 # Generates an error page for the +exception+ while handling +req+ on +res+. 194 195 def error exception, req, res 196 backtrace = exception.backtrace.join "\n" 197 198 res.content_type = 'text/html' 199 res.status = 500 200 res.body = <<-BODY 201<!DOCTYPE html> 202<html> 203<head> 204<meta content="text/html; charset=UTF-8" http-equiv="Content-Type"> 205 206<title>Error - #{ERB::Util.html_escape exception.class}</title> 207 208<link type="text/css" media="screen" href="#{@mount_path}/css/rdoc.css" rel="stylesheet"> 209</head> 210<body> 211<h1>Error</h1> 212 213<p>While processing <code>#{ERB::Util.html_escape req.request_uri}</code> the 214RDoc (#{ERB::Util.html_escape RDoc::VERSION}) server has encountered a 215<code>#{ERB::Util.html_escape exception.class}</code> 216exception: 217 218<pre>#{ERB::Util.html_escape exception.message}</pre> 219 220<p>Please report this to the 221<a href="https://github.com/ruby/rdoc/issues">RDoc issues tracker</a>. Please 222include the RDoc version, the URI above and exception class, message and 223backtrace. If you're viewing a gem's documentation, include the gem name and 224version. If you're viewing Ruby's documentation, include the version of ruby. 225 226<p>Backtrace: 227 228<pre>#{ERB::Util.html_escape backtrace}</pre> 229 230</body> 231</html> 232 BODY 233 end 234 235 ## 236 # Instantiates a Darkfish generator for +store+ 237 238 def generator_for store 239 generator = RDoc::Generator::Darkfish.new store, @options 240 generator.file_output = false 241 generator.asset_rel_path = '..' 242 243 rdoc = RDoc::RDoc.new 244 rdoc.store = store 245 rdoc.generator = generator 246 rdoc.options = @options 247 248 @options.main_page = store.main 249 @options.title = store.title 250 251 generator 252 end 253 254 ## 255 # Handles the If-Modified-Since HTTP header on +req+ for +path+. If the 256 # file has not been modified a Not Modified response is returned. If the 257 # file has been modified a Last-Modified header is added to +res+. 258 259 def if_modified_since req, res, path = nil 260 last_modified = File.stat(path).mtime if path 261 262 res['last-modified'] = last_modified.httpdate 263 264 return unless ims = req['if-modified-since'] 265 266 ims = Time.parse ims 267 268 unless ims < last_modified then 269 res.body = '' 270 raise WEBrick::HTTPStatus::NotModified 271 end 272 end 273 274 ## 275 # Returns an Array of installed documentation. 276 # 277 # Each entry contains the documentation name (gem name, 'Ruby 278 # Documentation', etc.), the path relative to the mount point, whether the 279 # documentation exists, the type of documentation (See RDoc::RI::Paths#each) 280 # and the filesystem to the RDoc::Store for the documentation. 281 282 def installed_docs 283 extra_counter = 0 284 ri_paths.map do |path, type| 285 store = RDoc::Store.new path, type 286 exists = File.exist? store.cache_path 287 288 case type 289 when :gem then 290 gem_path = path[%r%/([^/]*)/ri$%, 1] 291 [gem_path, "#{gem_path}/", exists, type, path] 292 when :system then 293 ['Ruby Documentation', 'ruby/', exists, type, path] 294 when :site then 295 ['Site Documentation', 'site/', exists, type, path] 296 when :home then 297 ['Home Documentation', 'home/', exists, type, path] 298 when :extra then 299 extra_counter += 1 300 store.load_cache if exists 301 title = store.title || "Extra Documentation" 302 [title, "extra-#{extra_counter}/", exists, type, path] 303 end 304 end 305 end 306 307 ## 308 # Returns a 404 page built by +generator+ for +req+ on +res+. 309 310 def not_found generator, req, res, message = nil 311 message ||= "The page <kbd>#{ERB::Util.h req.path}</kbd> was not found" 312 res.body = generator.generate_servlet_not_found message 313 res.status = 404 314 end 315 316 ## 317 # Enumerates the ri paths. See RDoc::RI::Paths#each 318 319 def ri_paths &block 320 RDoc::RI::Paths.each true, true, true, :all, *@extra_doc_dirs, &block #TODO: pass extra_dirs 321 end 322 323 ## 324 # Generates the root page on +res+. +req+ is ignored. 325 326 def root req, res 327 generator = RDoc::Generator::Darkfish.new nil, @options 328 329 res.body = generator.generate_servlet_root installed_docs 330 331 res.content_type = 'text/html' 332 end 333 334 ## 335 # Generates a search index for the root page on +res+. +req+ is ignored. 336 337 def root_search req, res 338 search_index = [] 339 info = [] 340 341 installed_docs.map do |name, href, exists, type, path| 342 next unless exists 343 344 search_index << name 345 346 case type 347 when :gem 348 gemspec = path.gsub(%r%/doc/([^/]*?)/ri$%, 349 '/specifications/\1.gemspec') 350 351 spec = Gem::Specification.load gemspec 352 353 path = spec.full_name 354 comment = spec.summary 355 when :system then 356 path = 'ruby' 357 comment = 'Documentation for the Ruby standard library' 358 when :site then 359 path = 'site' 360 comment = 'Documentation for non-gem libraries' 361 when :home then 362 path = 'home' 363 comment = 'Documentation from your home directory' 364 when :extra 365 comment = name 366 end 367 368 info << [name, '', path, '', comment] 369 end 370 371 index = { 372 :index => { 373 :searchIndex => search_index, 374 :longSearchIndex => search_index, 375 :info => info, 376 } 377 } 378 379 res.body = "var search_data = #{JSON.dump index};" 380 res.content_type = 'application/javascript' 381 end 382 383 ## 384 # Displays documentation for +req+ on +res+, whether that be HTML or some 385 # asset. 386 387 def show_documentation req, res 388 store, path = documentation_source req.path 389 390 if_modified_since req, res, store.cache_path 391 392 generator = generator_for store 393 394 case path 395 when nil, '', 'index.html' then 396 res.body = generator.generate_index 397 when 'table_of_contents.html' then 398 res.body = generator.generate_table_of_contents 399 when 'js/search_index.js' then 400 documentation_search store, generator, req, res 401 else 402 documentation_page store, generator, path, req, res 403 end 404 ensure 405 res.content_type ||= 'text/html' 406 end 407 408 ## 409 # Returns an RDoc::Store for the given +source_name+ ('ruby' or a gem name). 410 411 def store_for source_name 412 case source_name 413 when 'home' then 414 RDoc::Store.new RDoc::RI::Paths.home_dir, :home 415 when 'ruby' then 416 RDoc::Store.new RDoc::RI::Paths.system_dir, :system 417 when 'site' then 418 RDoc::Store.new RDoc::RI::Paths.site_dir, :site 419 when /^extra-(\d+)$/ then 420 index = $1.to_i - 1 421 ri_dir = installed_docs[index][4] 422 RDoc::Store.new ri_dir, :extra 423 else 424 ri_dir, type = ri_paths.find do |dir, dir_type| 425 next unless dir_type == :gem 426 427 source_name == dir[%r%/([^/]*)/ri$%, 1] 428 end 429 430 raise WEBrick::HTTPStatus::NotFound, 431 "Could not find gem \"#{ERB::Util.html_escape(source_name)}\". Are you sure you installed it?" unless ri_dir 432 433 store = RDoc::Store.new ri_dir, type 434 435 return store if File.exist? store.cache_path 436 437 raise WEBrick::HTTPStatus::NotFound, 438 "Could not find documentation for \"#{ERB::Util.html_escape(source_name)}\". Please run `gem rdoc --ri gem_name`" 439 440 end 441 end 442 443end 444