diff --git a/.rdoc_options b/.rdoc_options index ab54556487..983bb7512c 100644 --- a/.rdoc_options +++ b/.rdoc_options @@ -11,6 +11,11 @@ exclude: - CLAUDE.md - lib/rdoc/markdown.kpeg +hide: +- CVE-2013-0256.rdoc +- LEGAL.rdoc +- LICENSE.rdoc + footer_content: DOCUMENTATION: Home: index.html diff --git a/lib/rdoc/code_object/top_level.rb b/lib/rdoc/code_object/top_level.rb index c1c003130e..fcabcb8af5 100644 --- a/lib/rdoc/code_object/top_level.rb +++ b/lib/rdoc/code_object/top_level.rb @@ -38,6 +38,11 @@ class RDoc::TopLevel < RDoc::Context attr_reader :parser + ## + # Is this page hidden from the sidebar listing? + + attr_accessor :hidden + ## # Creates a new TopLevel for the file at +absolute_name+. If documentation # is being generated outside the source dir +relative_name+ is relative to @@ -49,6 +54,7 @@ def initialize(absolute_name, relative_name = absolute_name) @absolute_name = absolute_name @relative_name = relative_name @parser = nil + @hidden = false if relative_name @base_name = File.basename(relative_name) diff --git a/lib/rdoc/generator/json_index.rb b/lib/rdoc/generator/json_index.rb index 065caa47ea..3203dd95f2 100644 --- a/lib/rdoc/generator/json_index.rb +++ b/lib/rdoc/generator/json_index.rb @@ -250,7 +250,7 @@ def index_pages debug_msg " generating pages search index" pages = @files.select do |file| - file.text? + file.text? && !file.hidden end pages.each do |page| diff --git a/lib/rdoc/generator/template/aliki/_sidebar_pages.rhtml b/lib/rdoc/generator/template/aliki/_sidebar_pages.rhtml index 2ab60e8552..c16476d645 100644 --- a/lib/rdoc/generator/template/aliki/_sidebar_pages.rhtml +++ b/lib/rdoc/generator/template/aliki/_sidebar_pages.rhtml @@ -1,4 +1,4 @@ -<%- simple_files = @files.select { |f| f.text? } %> +<%- simple_files = @files.select { |f| f.text? && !f.hidden } %> <%- if defined?(current) && current.respond_to?(:page_name) %> <%- dir = current.full_name[%r{\A[^/]+(?=/)}] || current.page_name %> <%- end %> diff --git a/lib/rdoc/options.rb b/lib/rdoc/options.rb index 557993a263..edaf01ba37 100644 --- a/lib/rdoc/options.rb +++ b/lib/rdoc/options.rb @@ -168,6 +168,11 @@ class RDoc::Options attr_writer :exclude + ## + # Pages matching this pattern will be hidden from listings but still generated + + attr_writer :hide + ## # The list of files to be processed @@ -409,6 +414,7 @@ def init_ivars # :nodoc: @embed_mixins = false @exclude = [] @files = nil + @hide = [] @force_output = false @force_update = true @generator_name = "darkfish" @@ -461,6 +467,7 @@ def init_with(map) # :nodoc: @charset = map['charset'] @embed_mixins = map['embed_mixins'] @exclude = map['exclude'] + @hide = map['hide'] @generator_name = map['generator_name'] @hyperlink_all = map['hyperlink_all'] @line_numbers = map['line_numbers'] @@ -497,6 +504,7 @@ def override(map) # :nodoc: @charset = map['charset'] if map.has_key?('charset') @embed_mixins = map['embed_mixins'] if map.has_key?('embed_mixins') @exclude = map['exclude'] if map.has_key?('exclude') + @hide = map['hide'] if map.has_key?('hide') @generator_name = map['generator_name'] if map.has_key?('generator_name') @hyperlink_all = map['hyperlink_all'] if map.has_key?('hyperlink_all') @line_numbers = map['line_numbers'] if map.has_key?('line_numbers') @@ -628,6 +636,19 @@ def exclude end end + ## + # Create a regexp for #hide + + def hide + if @hide.nil? || @hide.is_a?(Regexp) + @hide + elsif @hide.empty? + nil + else + Regexp.new(@hide.join("|")) + end + end + ## # Completes any unfinished option setup business such as filtering for # existent files, creating a regexp for #exclude and setting a default @@ -647,6 +668,7 @@ def finish end @exclude = self.exclude + @hide = self.hide finish_page_dir @@ -865,6 +887,15 @@ def parse(argv) opt.separator nil + opt.on("--hide=PATTERN", "-H", Regexp, + "Hide pages matching PATTERN from the", + "sidebar listing. Pages are still", + "generated and linkable.") do |value| + @hide << value + end + + opt.separator nil + opt.on("--no-skipping-tests", nil, "Don't skip generating documentation for test and spec files") do |value| @skip_tests = false diff --git a/lib/rdoc/rdoc.rb b/lib/rdoc/rdoc.rb index 8beeac52f5..94159d1f72 100644 --- a/lib/rdoc/rdoc.rb +++ b/lib/rdoc/rdoc.rb @@ -358,6 +358,11 @@ def parse_file(filename) cm.done_documenting = false end + # Mark page as hidden if it matches hide patterns + if @options.hide&.match?(top_level.relative_name) + top_level.hidden = true + end + top_level rescue Errno::EACCES => e @@ -426,6 +431,32 @@ def remove_unparseable(files) end end + ## + # Validates that hide patterns match files that will actually be generated. + # Emits warnings for patterns that match files that are excluded or not + # included for documentation. + + def validate_hide_patterns + processed_files = @store.all_files.map(&:relative_name) + + # Find all files that match hide patterns by checking what was actually marked hidden + hidden_files = @store.all_files.select(&:hidden).map(&:relative_name) + + # Check each file that matches hide pattern to see if it was actually processed + Dir.glob("**/*").each do |file| + next unless File.file?(file) + next unless @options.hide.match?(file) + next if hidden_files.include?(file) || processed_files.include?(file) + + # File matches hide pattern but wasn't processed + if @options.exclude&.match?(file) + warn "WARNING: '#{file}' is in `hide` but won't be generated because it's also excluded" + else + warn "WARNING: '#{file}' is in `hide` but won't be generated because it's not included for documentation" + end + end + end + ## # Generates documentation or a coverage report depending upon the settings # in +options+. @@ -466,6 +497,8 @@ def document(options) file_info = parse_files @options.files + validate_hide_patterns if @options.hide + @options.default_title = "RDoc Documentation" @store.complete @options.visibility diff --git a/test/rdoc/rdoc_options_test.rb b/test/rdoc/rdoc_options_test.rb index 31da288023..c07daec6bb 100644 --- a/test/rdoc/rdoc_options_test.rb +++ b/test/rdoc/rdoc_options_test.rb @@ -67,6 +67,7 @@ def test_to_yaml 'encoding' => encoding, 'embed_mixins' => false, 'exclude' => [], + 'hide' => [], 'hyperlink_all' => false, 'line_numbers' => false, 'locale_dir' => 'locale', @@ -968,6 +969,28 @@ def test_exclude_option_without_default assert_not_match exclude, "foo~" end + def test_hide_option + @options.parse %w[--hide=doc/internal/.*] + hide = @options.hide + assert_kind_of Regexp, hide + assert_match hide, "doc/internal/foo.md" + assert_not_match hide, "doc/public/bar.md" + end + + def test_hide_option_multiple + @options.parse %w[--hide=doc/internal/.* --hide=doc/fragments/.*] + hide = @options.hide + assert_kind_of Regexp, hide + assert_match hide, "doc/internal/foo.md" + assert_match hide, "doc/fragments/bar.md" + assert_not_match hide, "doc/public/baz.md" + end + + def test_hide_option_empty + @options.parse %w[] + assert_nil @options.hide + end + class DummyCoder < Hash alias add :[]= def tag=(tag) diff --git a/test/rdoc/rdoc_rdoc_test.rb b/test/rdoc/rdoc_rdoc_test.rb index 642e69074b..21bfe671b5 100644 --- a/test/rdoc/rdoc_rdoc_test.rb +++ b/test/rdoc/rdoc_rdoc_test.rb @@ -579,4 +579,72 @@ def test_normalized_file_list_removes_created_rid_dir assert_empty output end end + + def test_parse_file_hide + @rdoc.store = RDoc::Store.new(@options) + @rdoc.options.hide = /internal/ + + temp_dir do + @rdoc.options.root = Pathname(Dir.pwd) + + File.write 'internal.md', '# Hidden Page' + + top_level = @rdoc.parse_file 'internal.md' + + assert top_level.hidden, "Expected internal.md to be hidden" + end + end + + def test_parse_file_not_hide + @rdoc.store = RDoc::Store.new(@options) + @rdoc.options.hide = /internal/ + + temp_dir do + @rdoc.options.root = Pathname(Dir.pwd) + + File.write 'public.md', '# Public Page' + + top_level = @rdoc.parse_file 'public.md' + + refute top_level.hidden, "Expected public.md to not be hidden" + end + end + + def test_validate_hide_patterns_warns_for_excluded_file + @rdoc.store = RDoc::Store.new(@options) + @rdoc.options.hide = /secret\.md/ + @rdoc.options.exclude = /secret/ + + temp_dir do + @rdoc.options.root = Pathname(Dir.pwd) + + File.write 'secret.md', '# Secret' + + _, err = capture_output do + @rdoc.validate_hide_patterns + end + + assert_match(/secret\.md.*hide.*exclude/, err) + end + end + + def test_validate_hide_patterns_warns_for_unprocessed_file + @rdoc.store = RDoc::Store.new(@options) + @rdoc.options.hide = /notincluded\.md/ + + temp_dir do + @rdoc.options.root = Pathname(Dir.pwd) + + # Create a file that won't be processed (not in .document) + FileUtils.mkdir 'subdir' + File.write 'subdir/notincluded.md', '# Not included' + File.write 'subdir/.document', '' # empty .document excludes everything + + _, err = capture_output do + @rdoc.validate_hide_patterns + end + + assert_match(/notincluded\.md.*hide.*not included for documentation/, err) + end + end end diff --git a/test/rdoc/rdoc_top_level_test.rb b/test/rdoc/rdoc_top_level_test.rb index a785abf06e..a9a15ef3e1 100644 --- a/test/rdoc/rdoc_top_level_test.rb +++ b/test/rdoc/rdoc_top_level_test.rb @@ -281,4 +281,17 @@ def test_text_eh_no_parser refute rd.text? end + def test_hidden_default + t = RDoc::TopLevel.new 'path/file.rb' + + refute t.hidden + end + + def test_hidden_set + t = RDoc::TopLevel.new 'path/file.rb' + t.hidden = true + + assert t.hidden + end + end