diff --git a/lib/rdoc/ri/driver.rb b/lib/rdoc/ri/driver.rb index 13ad9366ec..fe95de0a4f 100644 --- a/lib/rdoc/ri/driver.rb +++ b/lib/rdoc/ri/driver.rb @@ -125,6 +125,8 @@ def self.process_args(argv) Class::method | Class#method | Class.method | method + $global_variable | PREDEFINED_CONSTANT + gem_name: | gem_name:README | gem_name:History ruby: | ruby:NEWS | ruby:globals @@ -152,9 +154,12 @@ def self.process_args(argv) #{opt.program_name} zip #{opt.program_name} rdoc:README #{opt.program_name} ruby:comments + #{opt.program_name} ARGF + #{opt.program_name} '$<' + #{opt.program_name} '$LOAD_PATH' Note that shell quoting or escaping may be required for method names -containing punctuation: +containing punctuation or for global variables: #{opt.program_name} 'Array.[]' #{opt.program_name} compact\\! @@ -843,6 +848,116 @@ def display_method(name) display out end + ## + # Pre-defined global constants that can be looked up from globals.rdoc + + PREDEFINED_GLOBAL_CONSTANTS = %w[ + STDIN STDOUT STDERR ARGV ARGF DATA TOPLEVEL_BINDING + ].freeze + + ## + # Prefixes for pre-defined global constants + + PREDEFINED_GLOBAL_CONSTANT_PREFIXES = %w[RUBY_].freeze + + ## + # Returns true if +name+ is a pre-defined global constant like STDIN, STDOUT, + # RUBY_VERSION, etc. + + def predefined_global_constant?(name) + PREDEFINED_GLOBAL_CONSTANTS.include?(name) || + PREDEFINED_GLOBAL_CONSTANT_PREFIXES.any? { |prefix| name.start_with?(prefix) } + end + + ## + # Outputs formatted RI data for the global variable or pre-defined constant + # +name+. Looks up the documentation in the globals.rdoc page from the system + # store. + + def display_global(name) + store = @stores.find { |s| s.type == :system } + + raise NotFoundError, name unless store + + begin + page = store.load_page('globals.rdoc') + rescue RDoc::Store::MissingFileError + raise NotFoundError, name + end + + document = page.comment.parse + section = extract_global_section(document, name) + + raise NotFoundError, name unless section + + display section + + true + end + + ## + # Extracts the section for global +name+ from +document+. + # Returns an RDoc::Markup::Document containing just that section, + # or nil if not found. + # + # The globals.rdoc document has a hierarchical structure with headings: + # = Pre-Defined Global Variables (level 1) + # == Streams (level 2) + # === $< (ARGF or $stdin) (level 3) + # paragraph content... + # === $> (Default Output) (level 3) + # paragraph content... + # + # This method finds the heading matching +name+ and collects all content + # until the next heading at the same or higher level. + + def extract_global_section(document, name) + result = RDoc::Markup::Document.new + in_section = false # true once we find the matching heading + section_level = nil # heading level of the matched section (e.g., 3 for ===) + + document.parts.each do |part| + if RDoc::Markup::Heading === part + if heading_matches_global?(part, name) + # Found our target heading - start capturing content + in_section = true + section_level = part.level + result << part + elsif in_section && part.level <= section_level + # Hit next section at same or higher level - stop capturing + break + elsif in_section + # Sub-heading within our section - include it + result << part + end + elsif in_section + # Non-heading content (paragraphs, code blocks, etc.) - include it + result << part + end + end + + result.empty? ? nil : result + end + + ## + # Returns true if +heading+ matches the global +name+. + # Handles formats like "$< (ARGF or $stdin)", "$< (ARGF...)", or just "STDOUT". + + def heading_matches_global?(heading, name) + text = heading.text + + # Direct match: "STDOUT" or "$<" + return true if text == name + return true if text.start_with?("#{name} ") || text.start_with?("#{name}\t") + + # Match with wrapper: "$< (description)" + tt_wrapped = "#{name}" + return true if text.start_with?(tt_wrapped) + return true if text.start_with?("#{tt_wrapped} ") + + false + end + ## # Outputs formatted RI data for the class or method +name+. # @@ -850,6 +965,9 @@ def display_method(name) # be guessed, raises an error if +name+ couldn't be guessed. def display_name(name) + # Handle global variables immediately (classes can't start with $) + return display_global(name) if name.start_with?('$') + if name =~ /\w:(\w|$)/ then display_page name return true @@ -859,10 +977,23 @@ def display_name(name) display_method name if name =~ /::|#|\./ + # If no class was found and it's a predefined constant, try globals lookup + # This handles ARGV, STDIN, etc. that look like class names but aren't + return display_global(name) if predefined_global_constant?(name) + true rescue NotFoundError + # Before giving up, check if it's a predefined global constant + if predefined_global_constant?(name) + begin + return display_global(name) + rescue NotFoundError + # Fall through to original error handling + end + end + matches = list_methods_matching name if name =~ /::|#|\./ - matches = classes.keys.grep(/^#{Regexp.escape name}/) if matches.empty? + matches = classes.keys.grep(/^#{Regexp.escape name}/) if matches.nil? || matches.empty? raise if matches.empty? @@ -983,6 +1114,12 @@ def expand_class(klass) # #expand_class. def expand_name(name) + # Global variables don't need expansion + return name if name.start_with?('$') + + # Predefined global constants don't need expansion + return name if predefined_global_constant?(name) + klass, selector, method = parse_name name return [selector, method].join if klass.empty? diff --git a/test/rdoc/ri/driver_test.rb b/test/rdoc/ri/driver_test.rb index 2d1a2ce741..72f54b70d6 100644 --- a/test/rdoc/ri/driver_test.rb +++ b/test/rdoc/ri/driver_test.rb @@ -951,6 +951,147 @@ def test_display_page_list assert_match %r%OTHER\.rdoc%, out end + def test_display_global_variable + util_store + + # Create a globals page in the store + globals = @store1.add_file 'globals.rdoc' + globals.parser = RDoc::Parser::Simple + globals.comment = RDoc::Comment.from_document(doc( + head(1, 'Pre-Defined Global Variables'), + head(2, 'Streams'), + head(3, '$< (ARGF or $stdin)'), + para('Points to stream ARGF if not empty, else to stream $stdin; read-only.'), + head(3, '$> (Default Standard Output)'), + para('An output stream, initially $stdout.') + )) + @store1.save_page globals + @store1.type = :system + + out, = capture_output do + @driver.display_global '$<' + end + + assert_match %r%\$< \(ARGF or \$stdin\)%, out + assert_match %r%Points to stream ARGF%, out + refute_match %r%\$>%, out + end + + def test_display_global_constant + util_store + + # Create a globals page in the store + globals = @store1.add_file 'globals.rdoc' + globals.parser = RDoc::Parser::Simple + globals.comment = RDoc::Comment.from_document(doc( + head(1, 'Pre-Defined Global Constants'), + head(2, 'Streams'), + head(3, 'STDIN'), + para('The standard input stream.'), + head(3, 'STDOUT'), + para('The standard output stream.') + )) + @store1.save_page globals + @store1.type = :system + + out, = capture_output do + @driver.display_global 'STDOUT' + end + + assert_match %r%STDOUT%, out + assert_match %r%standard output stream%, out + refute_match %r%STDIN%, out + end + + def test_display_global_not_found + util_store + + # Create a globals page in the store (without the requested global) + globals = @store1.add_file 'globals.rdoc' + globals.parser = RDoc::Parser::Simple + globals.comment = RDoc::Comment.from_document(doc( + head(1, 'Pre-Defined Global Variables'), + head(3, '$<'), + para('Some doc') + )) + @store1.save_page globals + @store1.type = :system + + assert_raise RDoc::RI::Driver::NotFoundError do + @driver.display_global '$NONEXISTENT' + end + end + + def test_display_global_no_system_store + util_store + # Store type is :home by default, not :system + + assert_raise RDoc::RI::Driver::NotFoundError do + @driver.display_global '$<' + end + end + + def test_display_name_global_variable + util_store + + # Create a globals page in the store + globals = @store1.add_file 'globals.rdoc' + globals.parser = RDoc::Parser::Simple + globals.comment = RDoc::Comment.from_document(doc( + head(1, 'Pre-Defined Global Variables'), + head(3, '$< (ARGF or $stdin)'), + para('Points to stream ARGF.') + )) + @store1.save_page globals + @store1.type = :system + + out, = capture_output do + @driver.display_name '$<' + end + + assert_match %r%\$<%, out + assert_match %r%ARGF%, out + end + + def test_display_name_predefined_constant + util_store + + # Create a globals page in the store + globals = @store1.add_file 'globals.rdoc' + globals.parser = RDoc::Parser::Simple + globals.comment = RDoc::Comment.from_document(doc( + head(1, 'Pre-Defined Global Constants'), + head(3, 'ARGV'), + para('An array of the given command-line arguments.') + )) + @store1.save_page globals + @store1.type = :system + + out, = capture_output do + @driver.display_name 'ARGV' + end + + assert_match %r%ARGV%, out + assert_match %r%command-line arguments%, out + end + + def test_predefined_global_constant? + assert @driver.predefined_global_constant?('STDIN') + assert @driver.predefined_global_constant?('STDOUT') + assert @driver.predefined_global_constant?('STDERR') + assert @driver.predefined_global_constant?('ARGV') + assert @driver.predefined_global_constant?('ARGF') + assert @driver.predefined_global_constant?('DATA') + assert @driver.predefined_global_constant?('TOPLEVEL_BINDING') + assert @driver.predefined_global_constant?('RUBY_VERSION') + assert @driver.predefined_global_constant?('RUBY_PLATFORM') + + refute @driver.predefined_global_constant?('ENV') # ENV is a class, not a simple constant + refute @driver.predefined_global_constant?('MyClass') + refute @driver.predefined_global_constant?('Foo') + refute @driver.predefined_global_constant?('$<') + end + def test_expand_class util_store