webri 1.0.5 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/bin/scrape +12 -0
- data/data/3.2.json +30958 -0
- data/data/3.4.json +33900 -0
- data/data/4.0.json +28533 -0
- data/exe/webri +80 -0
- data/lib/scraper.rb +285 -0
- data/lib/webri/version.rb +1 -1
- data/lib/webri.rb +287 -372
- metadata +8 -3
- data/bin/webri +0 -65
data/lib/webri.rb
CHANGED
|
@@ -1,70 +1,118 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'rbconfig'
|
|
4
|
-
require 'open-uri'
|
|
5
|
-
require 'rexml'
|
|
6
|
-
require 'cgi'
|
|
7
4
|
require 'reline'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'json/add/core'
|
|
7
|
+
require 'uri'
|
|
8
|
+
require 'open3'
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
# TODO: Use reline.
|
|
11
|
-
#
|
|
12
|
-
# TODO: Subroutinize.
|
|
13
|
-
# TODO: Make initialization faster.
|
|
10
|
+
require_relative 'scraper'
|
|
14
11
|
|
|
15
12
|
# TODO: Choose dynamically the test names (rather than fixed)?
|
|
16
13
|
# TODO: Test on Linux.
|
|
17
|
-
# TODO: Test all releases(?).
|
|
18
|
-
# TODO: Test all pages(?).
|
|
19
|
-
|
|
20
|
-
# TODO: Make it work for
|
|
14
|
+
# TODO: Test all releases(?).
|
|
15
|
+
# TODO: Test all web pages(?).
|
|
16
|
+
|
|
17
|
+
# TODO: Make it work for:
|
|
18
|
+
# - Array.new
|
|
19
|
+
# - Array::new
|
|
20
|
+
# - Array.sort
|
|
21
|
+
# - Array#sort
|
|
22
|
+
# - .new
|
|
23
|
+
# - ::new
|
|
24
|
+
# - .sort
|
|
25
|
+
# - #sort
|
|
21
26
|
|
|
22
27
|
# TODO: Support pager.
|
|
23
28
|
|
|
24
29
|
# TODO: Support .webrirc.
|
|
25
30
|
# TODO: Make it save options to .webrirc.
|
|
26
31
|
# TODO: Make it show .webrirc.
|
|
32
|
+
# TODO: Support ENV.
|
|
27
33
|
|
|
34
|
+
# TODO: Support favorites.
|
|
35
|
+
# TODO: Support recents.
|
|
36
|
+
# TODO: Support direct in REPL; e.g., Array.sort.
|
|
37
|
+
# TODO: Support direct from command-line.
|
|
38
|
+
# TODO: Support partial on command-line, into REPL.
|
|
39
|
+
#
|
|
40
|
+
# TODO: Support alternate character for '\#' on command-line. ('3'?)
|
|
41
|
+
#
|
|
42
|
+
# TODO: Token begins with character, period, colon, or hashmark;
|
|
43
|
+
# anything else could signal a special op.
|
|
44
|
+
#
|
|
28
45
|
# A class to display Ruby online HTML documentation.
|
|
29
46
|
class WebRI
|
|
30
47
|
|
|
31
48
|
# Site of the official documentation.
|
|
32
|
-
|
|
49
|
+
DOC_SITE = 'https://docs.ruby-lang.org/en/'
|
|
33
50
|
|
|
34
|
-
|
|
35
|
-
# and build our @index_for_type.
|
|
36
|
-
def initialize(options = {})
|
|
37
|
-
msg = <<MSG
|
|
38
|
-
The release of Ruby 4.0 broke webri in a couple of ways:
|
|
51
|
+
PROMPT = "('?' for help) webri> "
|
|
39
52
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
53
|
+
CLASS = 'class/module'
|
|
54
|
+
SINGLETON = 'singleton method'
|
|
55
|
+
INSTANCE = 'instance method'
|
|
56
|
+
FILE = 'file'
|
|
43
57
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
58
|
+
attr_accessor :release_name,
|
|
59
|
+
:href_for_class_name,
|
|
60
|
+
:href_for_file_name,
|
|
61
|
+
:href_for_singleton_method_name,
|
|
62
|
+
:href_for_instance_method_name
|
|
63
|
+
|
|
64
|
+
def initialize(name = nil, options = {})
|
|
48
65
|
capture_options(options)
|
|
49
|
-
set_doc_release
|
|
50
|
-
|
|
51
|
-
|
|
66
|
+
self.release_name = set_doc_release(@release_name)
|
|
67
|
+
# data_file_path = File.join('data', self.release_name + '.json')
|
|
68
|
+
data_file_path = File.expand_path("../data/#{self.release_name}.json", __dir__)
|
|
69
|
+
json = open(data_file_path).read
|
|
70
|
+
@data = JSON.parse(json, create_additions: true)
|
|
71
|
+
make_groups
|
|
52
72
|
print_info if @info
|
|
53
|
-
|
|
54
|
-
|
|
73
|
+
if name
|
|
74
|
+
do_name(name)
|
|
75
|
+
elsif os_type == :linux && !@noreline
|
|
55
76
|
repl_reline
|
|
56
77
|
else
|
|
57
78
|
repl_plain
|
|
58
79
|
end
|
|
59
80
|
end
|
|
60
81
|
|
|
82
|
+
def do_name(name)
|
|
83
|
+
show(name)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def make_groups
|
|
87
|
+
self.href_for_class_name = {}
|
|
88
|
+
self.href_for_file_name = {}
|
|
89
|
+
self.href_for_singleton_method_name = {}
|
|
90
|
+
self.href_for_instance_method_name = {}
|
|
91
|
+
@data['hrefs_for_name'].group_by do |name, hrefs|
|
|
92
|
+
case
|
|
93
|
+
when name.start_with?('ruby:')
|
|
94
|
+
self.href_for_file_name[name] = hrefs.first
|
|
95
|
+
when name.start_with?('#')
|
|
96
|
+
self.href_for_instance_method_name[name] = hrefs
|
|
97
|
+
when name.start_with?('::')
|
|
98
|
+
self.href_for_singleton_method_name[name] = hrefs
|
|
99
|
+
else
|
|
100
|
+
self.href_for_class_name[name] = hrefs.first
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
61
105
|
def repl_plain # Read-evaluate-print loop, without Reline.
|
|
62
106
|
while true
|
|
63
|
-
$stdout.write(
|
|
107
|
+
$stdout.write(PROMPT)
|
|
64
108
|
$stdout.flush
|
|
65
109
|
response = $stdin.gets.chomp
|
|
66
110
|
exit if response == 'exit'
|
|
67
111
|
next if response.empty?
|
|
112
|
+
if response.start_with?('?')
|
|
113
|
+
help(response)
|
|
114
|
+
next
|
|
115
|
+
end
|
|
68
116
|
if response.split(' ').size > 1
|
|
69
117
|
puts "One name at a time, please."
|
|
70
118
|
next
|
|
@@ -80,18 +128,11 @@ MSG
|
|
|
80
128
|
end
|
|
81
129
|
|
|
82
130
|
begin
|
|
83
|
-
completion_words= []
|
|
84
|
-
@index_for_type.each_pair do |type, index|
|
|
85
|
-
if type == :page
|
|
86
|
-
completion_words += index.keys.map {|name| 'ruby:' + name }
|
|
87
|
-
else
|
|
88
|
-
completion_words += index.keys
|
|
89
|
-
end
|
|
90
|
-
end
|
|
131
|
+
completion_words= @data['hrefs_for_name'].keys.sort
|
|
91
132
|
Reline.completion_proc = proc { |word|
|
|
92
133
|
completion_words
|
|
93
134
|
}
|
|
94
|
-
while line = Reline.readline(
|
|
135
|
+
while line = Reline.readline(PROMPT, true)
|
|
95
136
|
case line.chomp
|
|
96
137
|
when 'exit'
|
|
97
138
|
exit 0
|
|
@@ -113,147 +154,50 @@ MSG
|
|
|
113
154
|
puts
|
|
114
155
|
end
|
|
115
156
|
|
|
116
|
-
def set_doc_release
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
supported_releases.push(release)
|
|
133
|
-
end
|
|
134
|
-
end
|
|
135
|
-
all_releases = [master_release] + supported_releases + unsupported_releases
|
|
136
|
-
if @doc_release
|
|
137
|
-
unless all_releases.include?(@doc_release)
|
|
138
|
-
puts "Unknown documentation release: #{@doc_release}"
|
|
139
|
-
puts "Available releases: #{all_releases.join(' ')}"
|
|
140
|
-
exit
|
|
141
|
-
end
|
|
142
|
-
else
|
|
143
|
-
a = RUBY_VERSION.split('.')
|
|
144
|
-
@doc_release ||= a[0..1].join('.')
|
|
145
|
-
end
|
|
157
|
+
def set_doc_release(release_name)
|
|
158
|
+
# If doc release not specified, get it from the local Ruby version.
|
|
159
|
+
unless release_name
|
|
160
|
+
s = RUBY_VERSION.split('.')
|
|
161
|
+
release_name ||= s[0..1].join('.')
|
|
162
|
+
puts "Documentation release defaulting to #{release_name} (the Ruby version you're running)."
|
|
163
|
+
release_name
|
|
164
|
+
end
|
|
165
|
+
# If the doc release is not available, let them choose.
|
|
166
|
+
release_names = Scraper.release_names
|
|
167
|
+
unless release_names.include?(release_name)
|
|
168
|
+
puts "Found no documentation release #{release_name}."
|
|
169
|
+
puts "Releases:"
|
|
170
|
+
release_name = get_choice_(release_names, required: true)
|
|
171
|
+
end
|
|
172
|
+
release_name
|
|
146
173
|
end
|
|
147
174
|
|
|
148
175
|
def print_info
|
|
149
|
-
puts "Ruby documentation
|
|
150
|
-
puts "
|
|
151
|
-
puts "
|
|
176
|
+
puts "Ruby documentation:"
|
|
177
|
+
puts " Release: #{release_name}"
|
|
178
|
+
puts " Site: #{DOC_SITE}"
|
|
179
|
+
puts " Snapshot: #{@data['timestamp']}"
|
|
152
180
|
puts "Names:"
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
181
|
+
puts format(" %5d %s", href_for_file_name.size, 'Files')
|
|
182
|
+
puts format(" %5d %s", href_for_class_name.size, 'Classes and modules')
|
|
183
|
+
count = 0
|
|
184
|
+
href_for_singleton_method_name.each_pair do |name, href_for_name|
|
|
185
|
+
count += href_for_name.size
|
|
186
|
+
end
|
|
187
|
+
puts format(" %5d %s", count, 'Singleton methods')
|
|
188
|
+
count = 0
|
|
189
|
+
href_for_instance_method_name.each_pair do |name, href_for_name|
|
|
190
|
+
count += href_for_name.size
|
|
191
|
+
end
|
|
192
|
+
puts format(" %5d %s", count, 'Instance methods')
|
|
156
193
|
exit
|
|
157
194
|
end
|
|
158
195
|
|
|
159
|
-
def build_indexes
|
|
160
|
-
# Index for each type of entry.
|
|
161
|
-
# Each index has a hash; key is name, value is array of URIs.
|
|
162
|
-
@index_for_type = {
|
|
163
|
-
class: {}, # Has both classes and modules.
|
|
164
|
-
singleton_method: {},
|
|
165
|
-
instance_method: {},
|
|
166
|
-
page: {},
|
|
167
|
-
}
|
|
168
|
-
# Iterate over the lines of the TOC page.
|
|
169
|
-
lines = @toc_html.split("\n")
|
|
170
|
-
i = 0
|
|
171
|
-
while i < lines.count
|
|
172
|
-
item_line = lines[i]
|
|
173
|
-
i += 1
|
|
174
|
-
next unless item_line.match('<li class="(\w+)"')
|
|
175
|
-
class_attr_value = $1 # Save for later.
|
|
176
|
-
# We have a pair of lines such as:
|
|
177
|
-
# <li class="file">
|
|
178
|
-
# <a href="COPYING.html">COPYING</a>
|
|
179
|
-
anchor_line = lines[i] # Second line of pair.
|
|
180
|
-
# Consume anchor_line.
|
|
181
|
-
i += 1
|
|
182
|
-
# We capture variables thus:
|
|
183
|
-
# - +type+ is the value of attribute 'class'.
|
|
184
|
-
# - +path+ is the value of attribute 'href'.
|
|
185
|
-
# - +full_name+ is the HTML text.
|
|
186
|
-
type = class_attr_value
|
|
187
|
-
_, path, rest = anchor_line.split('"')
|
|
188
|
-
full_name = rest.split(/<|>/)[1]
|
|
189
|
-
full_name = CGI.unescapeHTML(full_name)
|
|
190
|
-
case type
|
|
191
|
-
when 'class', 'module'
|
|
192
|
-
index = @index_for_type[:class]
|
|
193
|
-
if index.include?(full_name)
|
|
194
|
-
entry = index[full_name]
|
|
195
|
-
else
|
|
196
|
-
entry = ClassEntry.new(full_name)
|
|
197
|
-
index[full_name] = entry
|
|
198
|
-
end
|
|
199
|
-
entry.paths.push(path) unless entry.paths.include?(path)
|
|
200
|
-
when 'file'
|
|
201
|
-
index = @index_for_type[:page]
|
|
202
|
-
if index.include?(full_name)
|
|
203
|
-
entry = index[full_name]
|
|
204
|
-
else
|
|
205
|
-
entry = FileEntry.new(full_name)
|
|
206
|
-
index[full_name] = entry
|
|
207
|
-
end
|
|
208
|
-
entry.paths.push(path) unless entry.paths.include?(path)
|
|
209
|
-
when 'method'
|
|
210
|
-
case anchor_line
|
|
211
|
-
when /method-c-/
|
|
212
|
-
index = @index_for_type[:singleton_method]
|
|
213
|
-
if index.include?(full_name)
|
|
214
|
-
entry = index[full_name]
|
|
215
|
-
else
|
|
216
|
-
entry = SingletonMethodEntry.new(full_name)
|
|
217
|
-
index[full_name] = entry
|
|
218
|
-
end
|
|
219
|
-
entry.paths.push(path) unless entry.paths.include?(path)
|
|
220
|
-
when /method-i-/
|
|
221
|
-
index = @index_for_type[:instance_method]
|
|
222
|
-
if index.include?(full_name)
|
|
223
|
-
entry = index[full_name]
|
|
224
|
-
else
|
|
225
|
-
entry = InstanceMethodEntry.new(full_name)
|
|
226
|
-
index[full_name] = entry
|
|
227
|
-
end
|
|
228
|
-
entry.paths.push(path) unless entry.paths.include?(path)
|
|
229
|
-
else
|
|
230
|
-
fail anchor_line
|
|
231
|
-
end
|
|
232
|
-
else
|
|
233
|
-
fail class_attr_val
|
|
234
|
-
end
|
|
235
|
-
end
|
|
236
|
-
end
|
|
237
|
-
|
|
238
196
|
def capture_options(options)
|
|
239
197
|
@noop = options[:noop]
|
|
240
198
|
@info = options[:info]
|
|
241
199
|
@noreline = options[:noreline]
|
|
242
|
-
@
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
def get_toc_html
|
|
246
|
-
# Construct the doc release; e.g., '3.4'.
|
|
247
|
-
# Get the doc table of contents as a temp file.
|
|
248
|
-
@toc_url = DocSite + @doc_release + '/table_of_contents.html'
|
|
249
|
-
begin
|
|
250
|
-
toc_file = URI.open(@toc_url)
|
|
251
|
-
@toc_html = toc_file.read
|
|
252
|
-
rescue Socket::ResolutionError => x
|
|
253
|
-
message = "#{x.class}: #{x.message}\nPossibly not connected to internet."
|
|
254
|
-
$stderr.puts(message)
|
|
255
|
-
exit
|
|
256
|
-
end
|
|
200
|
+
@release_name = options[:release_name]
|
|
257
201
|
end
|
|
258
202
|
|
|
259
203
|
class Entry
|
|
@@ -333,15 +277,15 @@ MSG
|
|
|
333
277
|
# Figure out what's asked for.
|
|
334
278
|
case
|
|
335
279
|
when name.match(/^[A-Z]/)
|
|
336
|
-
show_class(name
|
|
280
|
+
show_class(name)
|
|
337
281
|
when %w[fatal fata fat fa f].include?(name)
|
|
338
|
-
show_class(name
|
|
282
|
+
show_class(name)
|
|
339
283
|
when name.start_with?('ruby:')
|
|
340
|
-
show_file(name
|
|
284
|
+
show_file(name)
|
|
341
285
|
when name.start_with?('::')
|
|
342
|
-
show_singleton_method(name
|
|
286
|
+
show_singleton_method(name)
|
|
343
287
|
when name.start_with?('#')
|
|
344
|
-
show_instance_method(name
|
|
288
|
+
show_instance_method(name)
|
|
345
289
|
when name == '@help'
|
|
346
290
|
show_help
|
|
347
291
|
when name == '@readme'
|
|
@@ -351,207 +295,123 @@ MSG
|
|
|
351
295
|
# when name.match(/^[a-z]/)
|
|
352
296
|
# show_method(name, @index_for_type[:singleton_method], @index_for_type[:instance_method])
|
|
353
297
|
else
|
|
298
|
+
|
|
354
299
|
puts "No documentation available for name '#{name}'."
|
|
355
300
|
end
|
|
356
301
|
end
|
|
357
302
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
key.start_with?(name)
|
|
365
|
-
end
|
|
366
|
-
# Find paths for selected_choices
|
|
367
|
-
selected_paths = []
|
|
368
|
-
selected_entries.each_pair do |name, entry|
|
|
369
|
-
entry.paths.each do |path|
|
|
370
|
-
selected_paths.push(path)
|
|
371
|
-
end
|
|
372
|
-
end
|
|
373
|
-
case selected_paths.size
|
|
374
|
-
when 1
|
|
375
|
-
selected_choices = ClassEntry.choices(selected_entries)
|
|
376
|
-
choice = selected_choices.keys.first
|
|
377
|
-
path = selected_choices.values.first
|
|
378
|
-
puts "Found one class/module name starting with '#{name}'\n #{choice}"
|
|
379
|
-
full_name = ClassEntry.full_name_for_choice(choice)
|
|
380
|
-
if name != full_name
|
|
381
|
-
message = "Open page #{path}?"
|
|
382
|
-
return unless get_boolean_answer(message)
|
|
383
|
-
end
|
|
384
|
-
path
|
|
385
|
-
when 0
|
|
386
|
-
puts "Found no class/module name starting with '#{name}'."
|
|
387
|
-
message = "Show #{all_choices.size} class/module names?"
|
|
388
|
-
return unless get_boolean_answer(message)
|
|
389
|
-
choice_index = get_choice(all_choices.keys)
|
|
390
|
-
return if choice_index.nil?
|
|
391
|
-
path = all_choices[choice_index]
|
|
392
|
-
else
|
|
393
|
-
selected_choices = ClassEntry.choices(selected_entries)
|
|
394
|
-
puts "Found #{selected_choices.size} class/module names starting with '#{name}'."
|
|
395
|
-
message = "Show #{selected_choices.size} class/module names?'"
|
|
396
|
-
return unless get_boolean_answer(message)
|
|
397
|
-
key = get_choice(selected_choices.keys)
|
|
398
|
-
return if key.nil?
|
|
399
|
-
path = selected_choices[key]
|
|
400
|
-
end
|
|
401
|
-
uri = Entry.uri(path)
|
|
402
|
-
open_page(name, uri)
|
|
403
|
-
end
|
|
404
|
-
|
|
405
|
-
# Show page.
|
|
406
|
-
def show_file(name, file_index)
|
|
407
|
-
# Target page is a free-standing page such as 'COPYING'.
|
|
408
|
-
name = name.sub(/^ruby:/, '') # Discard leading 'ruby:'
|
|
409
|
-
all_entries = @index_for_type[:page]
|
|
410
|
-
all_choices = FileEntry.choices(all_entries)
|
|
411
|
-
# Find entries whose names that start with name.
|
|
412
|
-
selected_entries = all_entries.select do |key, value|
|
|
413
|
-
key.start_with?(name)
|
|
414
|
-
end
|
|
415
|
-
# Find paths for selected_choices
|
|
416
|
-
selected_paths = []
|
|
417
|
-
selected_entries.each_pair do |name, entry|
|
|
418
|
-
entry.paths.each do |path|
|
|
419
|
-
selected_paths.push(path)
|
|
420
|
-
end
|
|
421
|
-
end
|
|
422
|
-
case selected_paths.size
|
|
423
|
-
when 1
|
|
424
|
-
selected_choices = FileEntry.choices(selected_entries)
|
|
425
|
-
choice = selected_choices.keys.first
|
|
426
|
-
path = selected_choices.values.first
|
|
427
|
-
puts "Found one page name starting with '#{name}'\n #{choice}"
|
|
428
|
-
full_name = FileEntry.full_name_for_choice(choice)
|
|
429
|
-
if name != full_name
|
|
430
|
-
message = "Open page #{path}?"
|
|
431
|
-
return unless get_boolean_answer(message)
|
|
432
|
-
end
|
|
433
|
-
path
|
|
434
|
-
when 0
|
|
435
|
-
puts "Found no page name starting with '#{name}'."
|
|
436
|
-
message = "Show names of all #{all_choices.size} pages?"
|
|
437
|
-
return unless get_boolean_answer(message)
|
|
438
|
-
key = get_choice(all_choices.keys)
|
|
439
|
-
return if key.nil?
|
|
440
|
-
path = all_choices[key]
|
|
441
|
-
else
|
|
442
|
-
selected_choices = FileEntry.choices(selected_entries)
|
|
443
|
-
puts "Found #{selected_choices.size} page names starting with '#{name}'."
|
|
444
|
-
message = "Show #{selected_choices.size} names?'"
|
|
445
|
-
return unless get_boolean_answer(message)
|
|
446
|
-
key = get_choice(selected_choices.keys)
|
|
447
|
-
return if key.nil?
|
|
448
|
-
path = selected_choices[key]
|
|
449
|
-
end
|
|
450
|
-
uri = Entry.uri(path)
|
|
451
|
-
open_page(name, uri)
|
|
452
|
-
end
|
|
453
|
-
|
|
454
|
-
# Show singleton method.
|
|
455
|
-
def show_singleton_method(name, singleton_method_index)
|
|
456
|
-
# Target page is a singleton method such as ::new.
|
|
457
|
-
all_entries = @index_for_type[:singleton_method]
|
|
458
|
-
all_choices = SingletonMethodEntry.choices(all_entries)
|
|
459
|
-
# Find entries whose names that start with name.
|
|
460
|
-
selected_entries = all_entries.select do |key, value|
|
|
461
|
-
key.start_with?(name)
|
|
462
|
-
end
|
|
463
|
-
# Find paths for selected_choices
|
|
464
|
-
selected_paths = []
|
|
465
|
-
selected_entries.each_pair do |name, entry|
|
|
466
|
-
entry.paths.each do |path|
|
|
467
|
-
selected_paths.push(path)
|
|
468
|
-
end
|
|
469
|
-
end
|
|
470
|
-
selected_choices = SingletonMethodEntry.choices(selected_entries)
|
|
471
|
-
case selected_paths.size
|
|
472
|
-
when 1
|
|
473
|
-
full_name = selected_entries.keys.first
|
|
474
|
-
path = selected_choices.values.first
|
|
475
|
-
puts "Found one singleton method name starting with '#{name}'\n #{full_name}"
|
|
476
|
-
if name != full_name
|
|
477
|
-
uri = URI.parse(path)
|
|
478
|
-
message = "Open page #{uri.path} at method #{full_name}?"
|
|
479
|
-
return unless get_boolean_answer(message)
|
|
480
|
-
end
|
|
481
|
-
path
|
|
482
|
-
when 0
|
|
483
|
-
puts "Found no singleton method name starting with '#{name}'."
|
|
484
|
-
message = "Show names of all #{all_choices.size} singleton methods?"
|
|
485
|
-
return unless get_boolean_answer(message)
|
|
486
|
-
choice = get_choice(all_choices.keys)
|
|
487
|
-
return if choice.nil?
|
|
488
|
-
full_name = SingletonMethodEntry.full_name_for_choice(choice)
|
|
489
|
-
path = all_choices[choice]
|
|
490
|
-
else
|
|
491
|
-
puts "Found #{selected_paths.size} singleton method names starting with '#{name}'."
|
|
492
|
-
message = "Show #{selected_paths.size} names?'"
|
|
493
|
-
return unless get_boolean_answer(message)
|
|
494
|
-
choice = get_choice(selected_choices.keys)
|
|
495
|
-
return if choice.nil?
|
|
496
|
-
full_name = SingletonMethodEntry.full_name_for_choice(choice)
|
|
497
|
-
path = all_choices[choice]
|
|
498
|
-
end
|
|
499
|
-
uri = Entry.uri(path)
|
|
500
|
-
open_page(full_name, uri)
|
|
501
|
-
end
|
|
502
|
-
|
|
503
|
-
# Show instance method.
|
|
504
|
-
def show_instance_method(name, instance_method_index)
|
|
505
|
-
# Target page is an instance method such as #to_s.
|
|
506
|
-
all_entries = @index_for_type[:instance_method]
|
|
507
|
-
all_choices = InstanceMethodEntry.choices(all_entries)
|
|
508
|
-
# Find entries whose names that start with name.
|
|
509
|
-
selected_entries = all_entries.select do |key, value|
|
|
510
|
-
key.start_with?(name)
|
|
303
|
+
def get_choice(situation, choices, type)
|
|
304
|
+
puts situation
|
|
305
|
+
count = choices.size
|
|
306
|
+
if count > 20
|
|
307
|
+
message = "Show #{count} #{type} names?"
|
|
308
|
+
return nil unless get_boolean_answer(message)
|
|
511
309
|
end
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
310
|
+
get_choice_(choices)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Show web page for selected file or class name.
|
|
314
|
+
def show_web_page_for_file_or_class(partial_name, href_for_name, type)
|
|
315
|
+
# Find names that start with partial name (which may in fact be the full name).
|
|
316
|
+
selected_names = href_for_name.keys.select do |name|
|
|
317
|
+
name.start_with?(partial_name)
|
|
318
|
+
end
|
|
319
|
+
count = selected_names.size
|
|
320
|
+
selected_name =
|
|
321
|
+
case count
|
|
322
|
+
when 0
|
|
323
|
+
situation = "Found no #{type} name starting with '#{partial_name}'."
|
|
324
|
+
selected_name = get_choice(situation, href_for_name.keys, type)
|
|
325
|
+
return if selected_name.nil?
|
|
326
|
+
selected_name
|
|
327
|
+
when 1
|
|
328
|
+
full_name = selected_names.first
|
|
329
|
+
puts "Found one #{type} name starting with '#{partial_name}': #{full_name}"
|
|
330
|
+
if partial_name != full_name
|
|
331
|
+
message = "Open web page #{full_name}?"
|
|
332
|
+
return unless get_boolean_answer(message)
|
|
333
|
+
end
|
|
334
|
+
full_name
|
|
335
|
+
else
|
|
336
|
+
situation = "Found #{count} #{type} names starting with '#{partial_name}'."
|
|
337
|
+
selected_name = get_choice(situation, selected_names, type)
|
|
338
|
+
return if selected_name.nil?
|
|
339
|
+
selected_name
|
|
517
340
|
end
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
341
|
+
href = href_for_name[selected_name]
|
|
342
|
+
show_web_page(selected_name, href)
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# Show web page for selected class name.
|
|
346
|
+
def show_class(partial_name)
|
|
347
|
+
show_web_page_for_file_or_class(partial_name, href_for_class_name, CLASS)
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# Show web page for selected file name.
|
|
351
|
+
def show_file(partial_name)
|
|
352
|
+
show_web_page_for_file_or_class(partial_name, href_for_file_name, FILE)
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Show web page for selected method name.
|
|
356
|
+
def show_web_page_for_method(partial_name, href_for_name, type)
|
|
357
|
+
# Find names that start with partial name (which may in fact be the full name).
|
|
358
|
+
selected_names = href_for_name.keys.select do |name|
|
|
359
|
+
name.start_with?(partial_name)
|
|
360
|
+
end
|
|
361
|
+
count = selected_names.size
|
|
362
|
+
selected_name =
|
|
363
|
+
case count
|
|
364
|
+
when 0
|
|
365
|
+
situation = "Found no #{type} name starting with '#{partial_name}'."
|
|
366
|
+
selected_name = get_choice(situation, href_for_name, type)
|
|
367
|
+
return if selected_name.nil?
|
|
368
|
+
selected_name
|
|
369
|
+
when 1
|
|
370
|
+
full_name = selected_names.first
|
|
371
|
+
puts "Found one #{type} name starting with '#{partial_name}': #{full_name}"
|
|
372
|
+
if partial_name != full_name
|
|
373
|
+
message = "Open web page #{full_name}?"
|
|
374
|
+
return unless get_boolean_answer(message)
|
|
375
|
+
end
|
|
376
|
+
full_name
|
|
377
|
+
else
|
|
378
|
+
situation = "Found #{count} #{type} names starting with '#{partial_name}'."
|
|
379
|
+
selected_name = get_choice(situation, selected_names, type)
|
|
380
|
+
return if selected_name.nil?
|
|
381
|
+
selected_name
|
|
529
382
|
end
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
full_name = InstanceMethodEntry.full_name_for_choice(choice)
|
|
383
|
+
qualified_names = []
|
|
384
|
+
@data['classes_for_method'][selected_name].each do |class_name|
|
|
385
|
+
qualified_names << "#{class_name}#{selected_name}"
|
|
386
|
+
end
|
|
387
|
+
count = qualified_names.size
|
|
388
|
+
if count == 1
|
|
389
|
+
puts "Found one #{CLASS} that has method '#{selected_name}'."
|
|
390
|
+
qualified_name = qualified_names.first
|
|
539
391
|
else
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
return
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
392
|
+
situation = "Found #{count} #{type} names that have method '#{selected_name}'."
|
|
393
|
+
qualified_name = get_choice(situation, qualified_names, type)
|
|
394
|
+
return if qualified_name.nil?
|
|
395
|
+
end
|
|
396
|
+
method_href = href_for_name[selected_name]
|
|
397
|
+
class_name = qualified_name.sub(selected_name, '')
|
|
398
|
+
href = "#{class_name}.html#{method_href}"
|
|
399
|
+
show_web_page(selected_name, href)
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
# Show web page for singleton method name.
|
|
403
|
+
def show_singleton_method(partial_name)
|
|
404
|
+
show_web_page_for_method(partial_name, href_for_singleton_method_name, SINGLETON)
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# Show web page for instance method name.
|
|
408
|
+
def show_instance_method(partial_name)
|
|
409
|
+
show_web_page_for_method(partial_name, href_for_instance_method_name, INSTANCE)
|
|
550
410
|
end
|
|
551
411
|
|
|
552
412
|
def show_help
|
|
553
413
|
puts 'Showing help.'
|
|
554
|
-
puts `ruby
|
|
414
|
+
puts `ruby exe/webri --help`
|
|
555
415
|
end
|
|
556
416
|
|
|
557
417
|
def show_readme
|
|
@@ -559,7 +419,7 @@ MSG
|
|
|
559
419
|
end
|
|
560
420
|
|
|
561
421
|
# Present choices; return choice.
|
|
562
|
-
def
|
|
422
|
+
def get_choice_(choices, required: false)
|
|
563
423
|
index = nil
|
|
564
424
|
range = (0..choices.size - 1)
|
|
565
425
|
until range.include?(index)
|
|
@@ -568,14 +428,20 @@ MSG
|
|
|
568
428
|
puts " #{s}: #{choice}"
|
|
569
429
|
end
|
|
570
430
|
while true
|
|
571
|
-
|
|
431
|
+
message = if required
|
|
432
|
+
'Type a number to choose: '
|
|
433
|
+
else
|
|
434
|
+
'Type a number to choose, or Return to skip: '
|
|
435
|
+
end
|
|
436
|
+
print message
|
|
572
437
|
$stdout.flush
|
|
573
438
|
response = $stdin.gets
|
|
574
439
|
case response
|
|
575
440
|
when /(\d+)/
|
|
576
|
-
|
|
441
|
+
index = $1.to_i
|
|
442
|
+
return choices[index] if index < choices.size
|
|
577
443
|
when "\n"
|
|
578
|
-
return nil
|
|
444
|
+
return nil unless required
|
|
579
445
|
else
|
|
580
446
|
|
|
581
447
|
end
|
|
@@ -597,8 +463,9 @@ MSG
|
|
|
597
463
|
end
|
|
598
464
|
|
|
599
465
|
# Open URL in browser.
|
|
600
|
-
def
|
|
601
|
-
|
|
466
|
+
def show_web_page(name, href)
|
|
467
|
+
href.gsub!('::', '/')
|
|
468
|
+
uri = URI.parse(File.join(DOC_SITE, release_name, href))
|
|
602
469
|
open_uri(name, uri)
|
|
603
470
|
end
|
|
604
471
|
|
|
@@ -640,10 +507,58 @@ MSG
|
|
|
640
507
|
puts message
|
|
641
508
|
command = "#{opener_name} #{full_url}"
|
|
642
509
|
if @noop
|
|
643
|
-
puts "Command: '#{command}'"
|
|
510
|
+
# puts "Command: '#{command}'"
|
|
511
|
+
else
|
|
512
|
+
# system(command)
|
|
513
|
+
Open3.popen3(command) do |stdin, stdout, stderr, wait_thr|
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def self.get_webri_root_dir
|
|
519
|
+
webri_root_dir = `git rev-parse --show-toplevel`.chomp
|
|
520
|
+
if $?.success? && File.basename(webri_root_dir) == 'webri'
|
|
521
|
+
return webri_root_dir
|
|
522
|
+
end
|
|
523
|
+
message = "Current working directory must be in a webri project, not #{Dir.pwd}."
|
|
524
|
+
puts message
|
|
525
|
+
exit
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
def self.check_release_name(release_name)
|
|
529
|
+
release_names = Scraper.release_names
|
|
530
|
+
unless release_names.include?(release_name)
|
|
531
|
+
message = "Release must be one of #{release_names.inspect}, not #{release_name.inspect}."
|
|
532
|
+
puts message
|
|
533
|
+
exit 1
|
|
534
|
+
end
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
def help(response)
|
|
538
|
+
if response == '?'
|
|
539
|
+
puts <<HELP
|
|
540
|
+
Type:
|
|
541
|
+
- 'exit' to exit webri.
|
|
542
|
+
- #{CLASS.capitalize} name (full or partial) to see #{CLASS} names:
|
|
543
|
+
- 'Array' (full name, not the start of other names).
|
|
544
|
+
- 'Ar' (partial name).
|
|
545
|
+
- #{SINGLETON.capitalize} name (full or partial) to see #{SINGLETON} names:
|
|
546
|
+
- '::tanh' (full name, not the start of other names).
|
|
547
|
+
- '::ta' (partial name).
|
|
548
|
+
- #{INSTANCE.capitalize} name (full or partial) to see #{INSTANCE} names:
|
|
549
|
+
- '#query=' (full name, not the start of other names).
|
|
550
|
+
- '#qu' (partial name).
|
|
551
|
+
- #{FILE.capitalize}name (full or partial) to see #{FILE} names:
|
|
552
|
+
- 'ruby:syntax_rdoc' (full name, not the start of other names).
|
|
553
|
+
- 'ruby:syntax' (partial name).
|
|
554
|
+
HELP
|
|
644
555
|
else
|
|
645
|
-
|
|
556
|
+
p response
|
|
646
557
|
end
|
|
647
558
|
end
|
|
648
559
|
|
|
560
|
+
def help_main
|
|
561
|
+
|
|
562
|
+
end
|
|
563
|
+
|
|
649
564
|
end
|