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.
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
- # TODO: Make it work on Aliki.
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(?).0
18
- # TODO: Test all pages(?).
19
-
20
- # TODO: Make it work for naked method ('parse') or dotted method ('.parse').
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
- DocSite = 'https://docs.ruby-lang.org/en/'
49
+ DOC_SITE = 'https://docs.ruby-lang.org/en/'
33
50
 
34
- # Get the info from the Ruby doc site's table of contents
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
- - The documentation format changed from Darkfish to Alike,
41
- thus breaking all the parsing.
42
- - Class CGI was pushed out of the Ruby core.
53
+ CLASS = 'class/module'
54
+ SINGLETON = 'singleton method'
55
+ INSTANCE = 'instance method'
56
+ FILE = 'file'
43
57
 
44
- Look for fixes in August 2026.
45
- MSG
46
- puts msg
47
- exit
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
- get_toc_html
51
- build_indexes
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
- print @noreline
54
- if os_type == :linux && !@noreline
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('webri> ')
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("webri> ", true)
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
- supported_releases = []
118
- unsupported_releases = []
119
- master_release = nil
120
- io = URI.open('https://docs.ruby-lang.org/en/')
121
- lines = io.readlines
122
- lines.each do |line|
123
- next unless line.match(/<a/)
124
- doc = REXML::Document.new(line)
125
- _, release, end_of_support = doc.root.text.split(' ')
126
- break if release.start_with?('2')
127
- if end_of_support
128
- unsupported_releases.push(release)
129
- elsif release == 'master'
130
- master_release = release
131
- else
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 release: #{@doc_release}"
150
- puts "Ruby documentation URL: #{@toc_url}"
151
- puts "Executable to open page: #{opener_name}"
176
+ puts "Ruby documentation:"
177
+ puts " Release: #{release_name}"
178
+ puts " Site: #{DOC_SITE}"
179
+ puts " Snapshot: #{@data['timestamp']}"
152
180
  puts "Names:"
153
- @index_for_type.each_pair do |type, items|
154
- puts format(" %5d %s names", items.count, type)
155
- end
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
- @doc_release = options[:release]
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, @index_for_type[:class])
280
+ show_class(name)
337
281
  when %w[fatal fata fat fa f].include?(name)
338
- show_class(name, @index_for_type[:class])
282
+ show_class(name)
339
283
  when name.start_with?('ruby:')
340
- show_file(name, @index_for_type[:page])
284
+ show_file(name)
341
285
  when name.start_with?('::')
342
- show_singleton_method(name, @index_for_type[:singleton_method])
286
+ show_singleton_method(name)
343
287
  when name.start_with?('#')
344
- show_instance_method(name, @index_for_type[:instance_method])
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
- # Show class.
359
- def show_class(name, class_index)
360
- all_entries = @index_for_type[:class]
361
- all_choices = ClassEntry.choices(all_entries)
362
- # Find entries whose names that start with name.
363
- selected_entries = all_entries.select do |key, value|
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
- # Find paths for selected_choices
513
- selected_paths = []
514
- selected_entries.each_pair do |name, entry|
515
- entry.paths.each do |path|
516
- selected_paths.push(path)
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
- end
519
- selected_choices = InstanceMethodEntry.choices(selected_entries)
520
- case selected_paths.size
521
- when 1
522
- full_name = selected_entries.keys.first
523
- path = selected_choices.values.first
524
- puts "Found one instance method name starting with '#{name}'\n #{full_name}"
525
- if name != full_name
526
- uri = URI.parse(path)
527
- message = "Open page #{uri.path} at method #{full_name}?"
528
- return unless get_boolean_answer(message)
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
- path
531
- when 0
532
- puts "Found no instance method name starting with '#{name}'."
533
- message = "Show names of all #{all_choices.size} instance methods?"
534
- return unless get_boolean_answer(message)
535
- choice = get_choice(all_choices.keys)
536
- return if choice.nil?
537
- path = all_choices[choice]
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
- puts "Found #{selected_paths.size} instance method names starting with '#{name}'."
541
- message = "Show #{selected_paths.size} names?'"
542
- return unless get_boolean_answer(message)
543
- choice = get_choice(selected_choices.keys)
544
- return if choice.nil?
545
- path = all_choices[choice]
546
- full_name = InstanceMethodEntry.full_name_for_choice(choice)
547
- end
548
- uri = Entry.uri(path)
549
- open_page(full_name, uri)
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 bin/webri --help`
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 get_choice(choices)
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
- print "Type a number to choose, or Return to skip: "
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
- return choices[$1.to_i]
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 open_page(name, target_uri)
601
- uri = URI.parse(File.join(DocSite, @doc_release, target_uri.to_s))
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
- system(command)
556
+ p response
646
557
  end
647
558
  end
648
559
 
560
+ def help_main
561
+
562
+ end
563
+
649
564
  end