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