Skip to content
Closed
Show file tree
Hide file tree
Changes from 20 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions bin/webri
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/usr/bin/env ruby

# A console application to display Ruby HTML documentation.

require 'optparse'
require_relative '../lib/rdoc/web_ri'

options = {}

parser = OptionParser.new

parser.version = RDoc::VERSION
parser.banner = <<-BANNER
webri is a console application for accessing Ruby online HTML documentation.

Usage: #{parser.program_name} [options] name

Argument name selects the documentation to be accessed.

o If name specifies a single item, that item is selected.
o If name specifies multiple items, those items are displayed.

The given name is converted to a Regexp, which is used to select documentation.

Note that your command window may require you to escape certain characters;
in particular, you may need to escape circumflex (^), dollar sign ($),
and pound sign (#).

BANNER

parser.separator('Options:')
parser.on('-r', '--release=STR',
'Sets the target documentation release to STR.',
'If not given, uses the release of the installed Ruby',
"(currently #{RUBY_VERSION}).",
'If given and not valid, the valid releases are displayed.',
) do |release|
options[:release] = release
end
parser.on('-h', '--help', 'Prints this help.') do
puts parser
exit
end
parser.on('-v', '--version', 'Prints the version of webri.') do
puts RDoc::VERSION
exit
end
parser.parse!

error_message = case ARGV.size
when 0
'No name given.'
when 1
nil
else
'Multiple names given.'
end
raise ArgumentError.new(error_message) if error_message

target_name = ARGV.shift

web_ri = RDoc::WebRI.new(target_name, options)
170 changes: 170 additions & 0 deletions lib/rdoc/web_ri.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
# frozen_string_literal: true
require 'open-uri'
require 'nokogiri'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we can assume users would have these gems installed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed (for nokogiri); I think open-uri is installed with Ruby. But, better to avoid both.

include Nokogiri

require_relative '../rdoc'

# A class to display Ruby HTML documentation.
class RDoc::WebRI

# Where the documentation lives.
ReleasesUrl = 'https://docs.ruby-lang.org/en/'


def initialize(target_name, options)
release_name = get_release_name(options[:release])
entries = get_entries(release_name)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we'd have sent 2 requests by this point just to get the release and entries list. Considering both of them are rarely changed, I think we may introduce something like rdoc-data used to do that packages all these information into a gem. And a tool like this can simply use it to generate the target url without making additional requests.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On my machine(!), making the two requests is much faster than reading the static RI files.

So a new gem would include the data that this app gathers from the two requests? And thus not need nokogiri? Interesting. But would not have master (though I'm not sure that's a drawback).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The more think about this idea (the gem, already containing the data for each release), the better I like it. Will explore.

# target_entries = entries[target_name]
target_entries = []
entries.select do |name, value|
if name.match(Regexp.new(target_name))
value.each do |x|
target_entries << x
end
end
end
if target_entries.empty?
puts "No documentation found for #{target_name}."
else
target_url = get_target_url(target_entries, release_name)
open_url(target_url)
end
end

class Entry

attr_accessor :type, :full_name, :href

def initialize(type, full_name, href)
self.type = type
self.full_name = full_name
self.href = href
end

end

def get_release_name(requested_release_name)
if requested_release_name.nil?
puts "Selecting documentation release based on installed Ruby (#{RUBY_VERSION})."
requested_release_name = RUBY_VERSION
end
available_release_names = []
html = URI.open(ReleasesUrl)
@doc = Nokogiri::HTML(html)
link_eles = @doc.xpath("//a")
link_eles.each do |link_ele|
text = link_ele.text
next if text.match('outdated')
release_name = text.sub('Ruby ', '')
available_release_names.push(release_name)
end
release_name = nil
if available_release_names.include?(requested_release_name)
release_name = requested_release_name
else
available_release_names.each do |name|
if requested_release_name.start_with?(name)
release_name = name
break
end
end
end
if release_name.nil?
puts "Could not find documentation for release '#{requested_release_name}': available releases:"
release_name = get_choice(available_release_names)
end
puts "Selected documentation release #{release_name}."
release_name
end

def get_entries(release_name)
toc_url = File.join(ReleasesUrl, release_name, 'table_of_contents.html')
html = URI.open(toc_url)
doc = Nokogiri::HTML(html)
entries = {}
%w[file class module method].each do |type|
add_entries(entries, doc, type)
end
entries
end

def add_entries(entries, doc, type)
xpath = "//li[@class='#{type}']"
li_eles = doc.xpath(xpath)
li_eles.each do |li_ele|
a_ele = li_ele.xpath('./a').first
short_name = a_ele.text
full_name = if type == 'method'
method_span_ele = li_ele.xpath('./span').first
class_name = method_span_ele.text
class_name + short_name
else
short_name
end
href = a_ele.attributes['href'].value
entry = Entry.new(type, full_name, href)
entries[short_name] ||= []
entries[short_name].push(entry)
next unless type == 'method'
# We want additional entries for full name, bare name, and dot name.
bare_name = short_name.sub(/^::/, '').sub(/^#/, '')
dot_name = '.' + bare_name
[full_name, bare_name, dot_name].each do |other_name|
entries[other_name] ||= []
entries[other_name].push(entry)
end
end
end

def get_target_url(target_entries, release_name)
target_entry = nil
if target_entries.size == 1
target_entry = target_entries.first
else
sorted_target_entries = target_entries.sort_by {|entry| entry.full_name}
full_names = sorted_target_entries.map { |entry| "#{entry.full_name} (#{entry.type})" }
index = get_choice_index(full_names)
target_entry = sorted_target_entries[index]
end
File.join(ReleasesUrl, release_name, target_entry.href).to_s
end

def open_url(target_url)
host_os = RbConfig::CONFIG['host_os']
executable_name = case host_os
when /linux|bsd/
'xdg-open'
when /darwin/
'open'
when /32$/
'start'
else
message = "Unrecognized host OS: '#{host_os}'."
raise RuntimeError.new(message)
end
command = "#{executable_name} #{target_url}"
system(command)
end

def get_choice(choices)
choices[get_choice_index(choices)]
end

def get_choice_index(choices)
index = nil
range = (0..choices.size - 1)
until range.include?(index)
choices.each_with_index do |choice, i|
s = "%6d" % i
puts " #{s}: #{choice}"
end
print "Choose (#{range}): "
$stdout.flush
response = gets
index = response.match(/\d+/) ? response.to_i : -1
end
index
end

end