1# frozen_string_literal: true 2# -*- mode: ruby; ruby-indent-level: 2; tab-width: 2 -*- 3 4require 'erb' 5require 'fileutils' 6require 'pathname' 7require 'rdoc/generator/markup' 8 9## 10# Darkfish RDoc HTML Generator 11# 12# $Id: darkfish.rb 52 2009-01-07 02:08:11Z deveiant $ 13# 14# == Author/s 15# * Michael Granger (ged@FaerieMUD.org) 16# 17# == Contributors 18# * Mahlon E. Smith (mahlon@martini.nu) 19# * Eric Hodel (drbrain@segment7.net) 20# 21# == License 22# 23# Copyright (c) 2007, 2008, Michael Granger. All rights reserved. 24# 25# Redistribution and use in source and binary forms, with or without 26# modification, are permitted provided that the following conditions are met: 27# 28# * Redistributions of source code must retain the above copyright notice, 29# this list of conditions and the following disclaimer. 30# 31# * Redistributions in binary form must reproduce the above copyright notice, 32# this list of conditions and the following disclaimer in the documentation 33# and/or other materials provided with the distribution. 34# 35# * Neither the name of the author/s, nor the names of the project's 36# contributors may be used to endorse or promote products derived from this 37# software without specific prior written permission. 38# 39# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 40# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 41# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 42# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 43# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 44# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 45# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 46# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 47# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 48# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 49# 50# == Attributions 51# 52# Darkfish uses the {Silk Icons}[http://www.famfamfam.com/lab/icons/silk/] set 53# by Mark James. 54 55class RDoc::Generator::Darkfish 56 57 RDoc::RDoc.add_generator self 58 59 include ERB::Util 60 61 ## 62 # Stylesheets, fonts, etc. that are included in RDoc. 63 64 BUILTIN_STYLE_ITEMS = # :nodoc: 65 %w[ 66 css/fonts.css 67 fonts/Lato-Light.ttf 68 fonts/Lato-LightItalic.ttf 69 fonts/Lato-Regular.ttf 70 fonts/Lato-RegularItalic.ttf 71 fonts/SourceCodePro-Bold.ttf 72 fonts/SourceCodePro-Regular.ttf 73 css/rdoc.css 74 ] 75 76 ## 77 # Path to this file's parent directory. Used to find templates and other 78 # resources. 79 80 GENERATOR_DIR = File.join 'rdoc', 'generator' 81 82 ## 83 # Release Version 84 85 VERSION = '3' 86 87 ## 88 # Description of this generator 89 90 DESCRIPTION = 'HTML generator, written by Michael Granger' 91 92 ## 93 # The relative path to style sheets and javascript. By default this is set 94 # the same as the rel_prefix. 95 96 attr_accessor :asset_rel_path 97 98 ## 99 # The path to generate files into, combined with <tt>--op</tt> from the 100 # options for a full path. 101 102 attr_reader :base_dir 103 104 ## 105 # Classes and modules to be used by this generator, not necessarily 106 # displayed. See also #modsort 107 108 attr_reader :classes 109 110 ## 111 # No files will be written when dry_run is true. 112 113 attr_accessor :dry_run 114 115 ## 116 # When false the generate methods return a String instead of writing to a 117 # file. The default is true. 118 119 attr_accessor :file_output 120 121 ## 122 # Files to be displayed by this generator 123 124 attr_reader :files 125 126 ## 127 # The JSON index generator for this Darkfish generator 128 129 attr_reader :json_index 130 131 ## 132 # Methods to be displayed by this generator 133 134 attr_reader :methods 135 136 ## 137 # Sorted list of classes and modules to be displayed by this generator 138 139 attr_reader :modsort 140 141 ## 142 # The RDoc::Store that is the source of the generated content 143 144 attr_reader :store 145 146 ## 147 # The directory where the template files live 148 149 attr_reader :template_dir # :nodoc: 150 151 ## 152 # The output directory 153 154 attr_reader :outputdir 155 156 ## 157 # Initialize a few instance variables before we start 158 159 def initialize store, options 160 @store = store 161 @options = options 162 163 @asset_rel_path = '' 164 @base_dir = Pathname.pwd.expand_path 165 @dry_run = @options.dry_run 166 @file_output = true 167 @template_dir = Pathname.new options.template_dir 168 @template_cache = {} 169 170 @classes = nil 171 @context = nil 172 @files = nil 173 @methods = nil 174 @modsort = nil 175 176 @json_index = RDoc::Generator::JsonIndex.new self, options 177 end 178 179 ## 180 # Output progress information if debugging is enabled 181 182 def debug_msg *msg 183 return unless $DEBUG_RDOC 184 $stderr.puts(*msg) 185 end 186 187 ## 188 # Directory where generated class HTML files live relative to the output 189 # dir. 190 191 def class_dir 192 nil 193 end 194 195 ## 196 # Directory where generated class HTML files live relative to the output 197 # dir. 198 199 def file_dir 200 nil 201 end 202 203 ## 204 # Create the directories the generated docs will live in if they don't 205 # already exist. 206 207 def gen_sub_directories 208 @outputdir.mkpath 209 end 210 211 ## 212 # Copy over the stylesheet into the appropriate place in the output 213 # directory. 214 215 def write_style_sheet 216 debug_msg "Copying static files" 217 options = { :verbose => $DEBUG_RDOC, :noop => @dry_run } 218 219 BUILTIN_STYLE_ITEMS.each do |item| 220 install_rdoc_static_file @template_dir + item, "./#{item}", options 221 end 222 223 @options.template_stylesheets.each do |stylesheet| 224 FileUtils.cp stylesheet, '.', options 225 end 226 227 Dir[(@template_dir + "{js,images}/**/*").to_s].each do |path| 228 next if File.directory? path 229 next if File.basename(path) =~ /^\./ 230 231 dst = Pathname.new(path).relative_path_from @template_dir 232 233 install_rdoc_static_file @template_dir + path, dst, options 234 end 235 end 236 237 ## 238 # Build the initial indices and output objects based on an array of TopLevel 239 # objects containing the extracted information. 240 241 def generate 242 setup 243 244 write_style_sheet 245 generate_index 246 generate_class_files 247 generate_file_files 248 generate_table_of_contents 249 @json_index.generate 250 @json_index.generate_gzipped 251 252 copy_static 253 254 rescue => e 255 debug_msg "%s: %s\n %s" % [ 256 e.class.name, e.message, e.backtrace.join("\n ") 257 ] 258 259 raise 260 end 261 262 ## 263 # Copies static files from the static_path into the output directory 264 265 def copy_static 266 return if @options.static_path.empty? 267 268 fu_options = { :verbose => $DEBUG_RDOC, :noop => @dry_run } 269 270 @options.static_path.each do |path| 271 unless File.directory? path then 272 FileUtils.install path, @outputdir, fu_options.merge(:mode => 0644) 273 next 274 end 275 276 Dir.chdir path do 277 Dir[File.join('**', '*')].each do |entry| 278 dest_file = @outputdir + entry 279 280 if File.directory? entry then 281 FileUtils.mkdir_p entry, fu_options 282 else 283 FileUtils.install entry, dest_file, fu_options.merge(:mode => 0644) 284 end 285 end 286 end 287 end 288 end 289 290 ## 291 # Return a list of the documented modules sorted by salience first, then 292 # by name. 293 294 def get_sorted_module_list classes 295 classes.select do |klass| 296 klass.display? 297 end.sort 298 end 299 300 ## 301 # Generate an index page which lists all the classes which are documented. 302 303 def generate_index 304 setup 305 306 template_file = @template_dir + 'index.rhtml' 307 return unless template_file.exist? 308 309 debug_msg "Rendering the index page..." 310 311 out_file = @base_dir + @options.op_dir + 'index.html' 312 rel_prefix = @outputdir.relative_path_from out_file.dirname 313 search_index_rel_prefix = rel_prefix 314 search_index_rel_prefix += @asset_rel_path if @file_output 315 316 asset_rel_prefix = rel_prefix + @asset_rel_path 317 318 @title = @options.title 319 320 render_template template_file, out_file do |io| 321 here = binding 322 # suppress 1.9.3 warning 323 here.local_variable_set(:asset_rel_prefix, asset_rel_prefix) 324 here 325 end 326 rescue => e 327 error = RDoc::Error.new \ 328 "error generating index.html: #{e.message} (#{e.class})" 329 error.set_backtrace e.backtrace 330 331 raise error 332 end 333 334 ## 335 # Generates a class file for +klass+ 336 337 def generate_class klass, template_file = nil 338 setup 339 340 current = klass 341 342 template_file ||= @template_dir + 'class.rhtml' 343 344 debug_msg " working on %s (%s)" % [klass.full_name, klass.path] 345 out_file = @outputdir + klass.path 346 rel_prefix = @outputdir.relative_path_from out_file.dirname 347 search_index_rel_prefix = rel_prefix 348 search_index_rel_prefix += @asset_rel_path if @file_output 349 350 asset_rel_prefix = rel_prefix + @asset_rel_path 351 svninfo = get_svninfo(current) 352 353 @title = "#{klass.type} #{klass.full_name} - #{@options.title}" 354 355 debug_msg " rendering #{out_file}" 356 render_template template_file, out_file do |io| 357 here = binding 358 # suppress 1.9.3 warning 359 here.local_variable_set(:asset_rel_prefix, asset_rel_prefix) 360 here.local_variable_set(:svninfo, svninfo) 361 here 362 end 363 end 364 365 ## 366 # Generate a documentation file for each class and module 367 368 def generate_class_files 369 setup 370 371 template_file = @template_dir + 'class.rhtml' 372 template_file = @template_dir + 'classpage.rhtml' unless 373 template_file.exist? 374 return unless template_file.exist? 375 debug_msg "Generating class documentation in #{@outputdir}" 376 377 current = nil 378 379 @classes.each do |klass| 380 current = klass 381 382 generate_class klass, template_file 383 end 384 rescue => e 385 error = RDoc::Error.new \ 386 "error generating #{current.path}: #{e.message} (#{e.class})" 387 error.set_backtrace e.backtrace 388 389 raise error 390 end 391 392 ## 393 # Generate a documentation file for each file 394 395 def generate_file_files 396 setup 397 398 page_file = @template_dir + 'page.rhtml' 399 fileinfo_file = @template_dir + 'fileinfo.rhtml' 400 401 # for legacy templates 402 filepage_file = @template_dir + 'filepage.rhtml' unless 403 page_file.exist? or fileinfo_file.exist? 404 405 return unless 406 page_file.exist? or fileinfo_file.exist? or filepage_file.exist? 407 408 debug_msg "Generating file documentation in #{@outputdir}" 409 410 out_file = nil 411 current = nil 412 413 @files.each do |file| 414 current = file 415 416 if file.text? and page_file.exist? then 417 generate_page file 418 next 419 end 420 421 template_file = nil 422 out_file = @outputdir + file.path 423 debug_msg " working on %s (%s)" % [file.full_name, out_file] 424 rel_prefix = @outputdir.relative_path_from out_file.dirname 425 search_index_rel_prefix = rel_prefix 426 search_index_rel_prefix += @asset_rel_path if @file_output 427 428 asset_rel_prefix = rel_prefix + @asset_rel_path 429 430 unless filepage_file then 431 if file.text? then 432 next unless page_file.exist? 433 template_file = page_file 434 @title = file.page_name 435 else 436 next unless fileinfo_file.exist? 437 template_file = fileinfo_file 438 @title = "File: #{file.base_name}" 439 end 440 end 441 442 @title += " - #{@options.title}" 443 template_file ||= filepage_file 444 445 render_template template_file, out_file do |io| 446 here = binding 447 # suppress 1.9.3 warning 448 here.local_variable_set(:asset_rel_prefix, asset_rel_prefix) 449 here.local_variable_set(:current, current) 450 here 451 end 452 end 453 rescue => e 454 error = 455 RDoc::Error.new "error generating #{out_file}: #{e.message} (#{e.class})" 456 error.set_backtrace e.backtrace 457 458 raise error 459 end 460 461 ## 462 # Generate a page file for +file+ 463 464 def generate_page file 465 setup 466 467 template_file = @template_dir + 'page.rhtml' 468 469 out_file = @outputdir + file.path 470 debug_msg " working on %s (%s)" % [file.full_name, out_file] 471 rel_prefix = @outputdir.relative_path_from out_file.dirname 472 search_index_rel_prefix = rel_prefix 473 search_index_rel_prefix += @asset_rel_path if @file_output 474 475 current = file 476 asset_rel_prefix = rel_prefix + @asset_rel_path 477 478 @title = "#{file.page_name} - #{@options.title}" 479 480 debug_msg " rendering #{out_file}" 481 render_template template_file, out_file do |io| 482 here = binding 483 # suppress 1.9.3 warning 484 here.local_variable_set(:current, current) 485 here.local_variable_set(:asset_rel_prefix, asset_rel_prefix) 486 here 487 end 488 end 489 490 ## 491 # Generates the 404 page for the RDoc servlet 492 493 def generate_servlet_not_found message 494 setup 495 496 template_file = @template_dir + 'servlet_not_found.rhtml' 497 return unless template_file.exist? 498 499 debug_msg "Rendering the servlet 404 Not Found page..." 500 501 rel_prefix = rel_prefix = '' 502 search_index_rel_prefix = rel_prefix 503 search_index_rel_prefix += @asset_rel_path if @file_output 504 505 asset_rel_prefix = '' 506 507 @title = 'Not Found' 508 509 render_template template_file do |io| 510 here = binding 511 # suppress 1.9.3 warning 512 here.local_variable_set(:asset_rel_prefix, asset_rel_prefix) 513 here 514 end 515 rescue => e 516 error = RDoc::Error.new \ 517 "error generating servlet_not_found: #{e.message} (#{e.class})" 518 error.set_backtrace e.backtrace 519 520 raise error 521 end 522 523 ## 524 # Generates the servlet root page for the RDoc servlet 525 526 def generate_servlet_root installed 527 setup 528 529 template_file = @template_dir + 'servlet_root.rhtml' 530 return unless template_file.exist? 531 532 debug_msg 'Rendering the servlet root page...' 533 534 rel_prefix = '.' 535 asset_rel_prefix = rel_prefix 536 search_index_rel_prefix = asset_rel_prefix 537 search_index_rel_prefix += @asset_rel_path if @file_output 538 539 @title = 'Local RDoc Documentation' 540 541 render_template template_file do |io| binding end 542 rescue => e 543 error = RDoc::Error.new \ 544 "error generating servlet_root: #{e.message} (#{e.class})" 545 error.set_backtrace e.backtrace 546 547 raise error 548 end 549 550 ## 551 # Generate an index page which lists all the classes which are documented. 552 553 def generate_table_of_contents 554 setup 555 556 template_file = @template_dir + 'table_of_contents.rhtml' 557 return unless template_file.exist? 558 559 debug_msg "Rendering the Table of Contents..." 560 561 out_file = @outputdir + 'table_of_contents.html' 562 rel_prefix = @outputdir.relative_path_from out_file.dirname 563 search_index_rel_prefix = rel_prefix 564 search_index_rel_prefix += @asset_rel_path if @file_output 565 566 asset_rel_prefix = rel_prefix + @asset_rel_path 567 568 @title = "Table of Contents - #{@options.title}" 569 570 render_template template_file, out_file do |io| 571 here = binding 572 # suppress 1.9.3 warning 573 here.local_variable_set(:asset_rel_prefix, asset_rel_prefix) 574 here 575 end 576 rescue => e 577 error = RDoc::Error.new \ 578 "error generating table_of_contents.html: #{e.message} (#{e.class})" 579 error.set_backtrace e.backtrace 580 581 raise error 582 end 583 584 def install_rdoc_static_file source, destination, options # :nodoc: 585 return unless source.exist? 586 587 begin 588 FileUtils.mkdir_p File.dirname(destination), options 589 590 begin 591 FileUtils.ln source, destination, options 592 rescue Errno::EEXIST 593 FileUtils.rm destination 594 retry 595 end 596 rescue 597 FileUtils.cp source, destination, options 598 end 599 end 600 601 ## 602 # Prepares for generation of output from the current directory 603 604 def setup 605 return if instance_variable_defined? :@outputdir 606 607 @outputdir = Pathname.new(@options.op_dir).expand_path @base_dir 608 609 return unless @store 610 611 @classes = @store.all_classes_and_modules.sort 612 @files = @store.all_files.sort 613 @methods = @classes.map { |m| m.method_list }.flatten.sort 614 @modsort = get_sorted_module_list @classes 615 end 616 617 ## 618 # Return a string describing the amount of time in the given number of 619 # seconds in terms a human can understand easily. 620 621 def time_delta_string seconds 622 return 'less than a minute' if seconds < 60 623 return "#{seconds / 60} minute#{seconds / 60 == 1 ? '' : 's'}" if 624 seconds < 3000 # 50 minutes 625 return 'about one hour' if seconds < 5400 # 90 minutes 626 return "#{seconds / 3600} hours" if seconds < 64800 # 18 hours 627 return 'one day' if seconds < 86400 # 1 day 628 return 'about one day' if seconds < 172800 # 2 days 629 return "#{seconds / 86400} days" if seconds < 604800 # 1 week 630 return 'about one week' if seconds < 1209600 # 2 week 631 return "#{seconds / 604800} weeks" if seconds < 7257600 # 3 months 632 return "#{seconds / 2419200} months" if seconds < 31536000 # 1 year 633 return "#{seconds / 31536000} years" 634 end 635 636 # %q$Id: darkfish.rb 52 2009-01-07 02:08:11Z deveiant $" 637 SVNID_PATTERN = / 638 \$Id:\s 639 (\S+)\s # filename 640 (\d+)\s # rev 641 (\d{4}-\d{2}-\d{2})\s # Date (YYYY-MM-DD) 642 (\d{2}:\d{2}:\d{2}Z)\s # Time (HH:MM:SSZ) 643 (\w+)\s # committer 644 \$$ 645 /x 646 647 ## 648 # Try to extract Subversion information out of the first constant whose 649 # value looks like a subversion Id tag. If no matching constant is found, 650 # and empty hash is returned. 651 652 def get_svninfo klass 653 constants = klass.constants or return {} 654 655 constants.find { |c| c.value =~ SVNID_PATTERN } or return {} 656 657 filename, rev, date, time, committer = $~.captures 658 commitdate = Time.parse "#{date} #{time}" 659 660 return { 661 :filename => filename, 662 :rev => Integer(rev), 663 :commitdate => commitdate, 664 :commitdelta => time_delta_string(Time.now - commitdate), 665 :committer => committer, 666 } 667 end 668 669 ## 670 # Creates a template from its components and the +body_file+. 671 # 672 # For backwards compatibility, if +body_file+ contains "<html" the body is 673 # used directly. 674 675 def assemble_template body_file 676 body = body_file.read 677 return body if body =~ /<html/ 678 679 head_file = @template_dir + '_head.rhtml' 680 footer_file = @template_dir + '_footer.rhtml' 681 682 <<-TEMPLATE 683<!DOCTYPE html> 684 685<html> 686<head> 687#{head_file.read} 688 689#{body} 690 691#{footer_file.read} 692 TEMPLATE 693 end 694 695 ## 696 # Renders the ERb contained in +file_name+ relative to the template 697 # directory and returns the result based on the current context. 698 699 def render file_name 700 template_file = @template_dir + file_name 701 702 template = template_for template_file, false, RDoc::ERBPartial 703 704 template.filename = template_file.to_s 705 706 template.result @context 707 end 708 709 ## 710 # Load and render the erb template in the given +template_file+ and write 711 # it out to +out_file+. 712 # 713 # Both +template_file+ and +out_file+ should be Pathname-like objects. 714 # 715 # An io will be yielded which must be captured by binding in the caller. 716 717 def render_template template_file, out_file = nil # :yield: io 718 io_output = out_file && !@dry_run && @file_output 719 erb_klass = io_output ? RDoc::ERBIO : ERB 720 721 template = template_for template_file, true, erb_klass 722 723 if io_output then 724 debug_msg "Outputting to %s" % [out_file.expand_path] 725 726 out_file.dirname.mkpath 727 out_file.open 'w', 0644 do |io| 728 io.set_encoding @options.encoding 729 730 @context = yield io 731 732 template_result template, @context, template_file 733 end 734 else 735 @context = yield nil 736 737 output = template_result template, @context, template_file 738 739 debug_msg " would have written %d characters to %s" % [ 740 output.length, out_file.expand_path 741 ] if @dry_run 742 743 output 744 end 745 end 746 747 ## 748 # Creates the result for +template+ with +context+. If an error is raised a 749 # Pathname +template_file+ will indicate the file where the error occurred. 750 751 def template_result template, context, template_file 752 template.filename = template_file.to_s 753 template.result context 754 rescue NoMethodError => e 755 raise RDoc::Error, "Error while evaluating %s: %s" % [ 756 template_file.expand_path, 757 e.message, 758 ], e.backtrace 759 end 760 761 ## 762 # Retrieves a cache template for +file+, if present, or fills the cache. 763 764 def template_for file, page = true, klass = ERB 765 template = @template_cache[file] 766 767 return template if template 768 769 if page then 770 template = assemble_template file 771 erbout = 'io' 772 else 773 template = file.read 774 template = template.encode @options.encoding 775 776 file_var = File.basename(file).sub(/\..*/, '') 777 778 erbout = "_erbout_#{file_var}" 779 end 780 781 if RUBY_VERSION >= '2.6' 782 template = klass.new template, trim_mode: '<>', eoutvar: erbout 783 else 784 template = klass.new template, nil, '<>', erbout 785 end 786 @template_cache[file] = template 787 template 788 end 789 790end 791