-
Notifications
You must be signed in to change notification settings - Fork 445
Webri - a new console app for accessing Ruby online HTML docs [expermental] #1213
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 20 commits
8cce02a
7a5739d
59fa771
d7917d0
bb5946f
df22951
77f869c
1b1abb8
9d2ed90
850266f
13acd2a
a4e3205
54d3019
5af183d
f3087cf
d5e133d
47c75ce
af80aa9
2c2eab7
2cf14b9
3370f50
55d4d3e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
# frozen_string_literal: true | ||
require 'open-uri' | ||
require 'nokogiri' | ||
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) | ||
|
||
# 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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed (for
nokogiri
); I thinkopen-uri
is installed with Ruby. But, better to avoid both.