webri 1.0.5 → 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,56 +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/'
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(release_name = nil, options = {})
65
+ self.release_name = set_doc_release(release_name)
48
66
  capture_options(options)
49
- set_doc_release
50
- get_toc_html
51
- 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
52
72
  print_info if @info
53
- print @noreline
54
73
  if os_type == :linux && !@noreline
55
74
  repl_reline
56
75
  else
@@ -58,13 +77,36 @@ MSG
58
77
  end
59
78
  end
60
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
+
61
99
  def repl_plain # Read-evaluate-print loop, without Reline.
62
100
  while true
63
- $stdout.write('webri> ')
101
+ $stdout.write(PROMPT)
64
102
  $stdout.flush
65
103
  response = $stdin.gets.chomp
66
104
  exit if response == 'exit'
67
105
  next if response.empty?
106
+ if response.start_with?('?')
107
+ help(response)
108
+ next
109
+ end
68
110
  if response.split(' ').size > 1
69
111
  puts "One name at a time, please."
70
112
  next
@@ -80,18 +122,11 @@ MSG
80
122
  end
81
123
 
82
124
  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
125
+ completion_words= @data['hrefs_for_name'].keys.sort
91
126
  Reline.completion_proc = proc { |word|
92
127
  completion_words
93
128
  }
94
- while line = Reline.readline("webri> ", true)
129
+ while line = Reline.readline(PROMPT, true)
95
130
  case line.chomp
96
131
  when 'exit'
97
132
  exit 0
@@ -113,147 +148,49 @@ MSG
113
148
  puts
114
149
  end
115
150
 
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
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
146
167
  end
147
168
 
148
169
  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}"
170
+ puts "Ruby documentation:"
171
+ puts " Release: #{release_name}"
172
+ puts " Site: #{DOC_SITE}"
173
+ puts " Snapshot: #{@data['timestamp']}"
152
174
  puts "Names:"
153
- @index_for_type.each_pair do |type, items|
154
- puts format(" %5d %s names", items.count, type)
155
- 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')
156
187
  exit
157
188
  end
158
189
 
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
190
  def capture_options(options)
239
191
  @noop = options[:noop]
240
192
  @info = options[:info]
241
193
  @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
257
194
  end
258
195
 
259
196
  class Entry
@@ -333,15 +270,15 @@ MSG
333
270
  # Figure out what's asked for.
334
271
  case
335
272
  when name.match(/^[A-Z]/)
336
- show_class(name, @index_for_type[:class])
273
+ show_class(name)
337
274
  when %w[fatal fata fat fa f].include?(name)
338
- show_class(name, @index_for_type[:class])
275
+ show_class(name)
339
276
  when name.start_with?('ruby:')
340
- show_file(name, @index_for_type[:page])
277
+ show_file(name)
341
278
  when name.start_with?('::')
342
- show_singleton_method(name, @index_for_type[:singleton_method])
279
+ show_singleton_method(name)
343
280
  when name.start_with?('#')
344
- show_instance_method(name, @index_for_type[:instance_method])
281
+ show_instance_method(name)
345
282
  when name == '@help'
346
283
  show_help
347
284
  when name == '@readme'
@@ -351,207 +288,123 @@ MSG
351
288
  # when name.match(/^[a-z]/)
352
289
  # show_method(name, @index_for_type[:singleton_method], @index_for_type[:instance_method])
353
290
  else
291
+
354
292
  puts "No documentation available for name '#{name}'."
355
293
  end
356
294
  end
357
295
 
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]
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)
400
302
  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)
511
- 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)
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
517
333
  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)
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
529
375
  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)
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
539
384
  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)
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)
550
403
  end
551
404
 
552
405
  def show_help
553
406
  puts 'Showing help.'
554
- puts `ruby bin/webri --help`
407
+ puts `ruby exe/webri --help`
555
408
  end
556
409
 
557
410
  def show_readme
@@ -559,7 +412,7 @@ MSG
559
412
  end
560
413
 
561
414
  # Present choices; return choice.
562
- def get_choice(choices)
415
+ def get_choice_(choices, required: false)
563
416
  index = nil
564
417
  range = (0..choices.size - 1)
565
418
  until range.include?(index)
@@ -568,14 +421,20 @@ MSG
568
421
  puts " #{s}: #{choice}"
569
422
  end
570
423
  while true
571
- 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
572
430
  $stdout.flush
573
431
  response = $stdin.gets
574
432
  case response
575
433
  when /(\d+)/
576
- return choices[$1.to_i]
434
+ index = $1.to_i
435
+ return choices[index] if index < choices.size
577
436
  when "\n"
578
- return nil
437
+ return nil unless required
579
438
  else
580
439
 
581
440
  end
@@ -597,8 +456,9 @@ MSG
597
456
  end
598
457
 
599
458
  # Open URL in browser.
600
- def open_page(name, target_uri)
601
- 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))
602
462
  open_uri(name, uri)
603
463
  end
604
464
 
@@ -640,10 +500,58 @@ MSG
640
500
  puts message
641
501
  command = "#{opener_name} #{full_url}"
642
502
  if @noop
643
- puts "Command: '#{command}'"
503
+ # puts "Command: '#{command}'"
644
504
  else
645
- 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
646
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
+
647
555
  end
648
556
 
649
557
  end