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/exe/webri ADDED
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # A console application to display Ruby HTML documentation.
4
+
5
+ require 'optparse'
6
+ require_relative '../lib/webri/version'
7
+ require_relative '../lib/webri'
8
+
9
+ options = {}
10
+
11
+ parser = OptionParser.new
12
+
13
+ parser.version = WebRI::VERSION
14
+ parser.banner = <<-BANNER
15
+ webri is a console application for displaying Ruby online HTML documentation.
16
+ Documentation pages are opened in the default web browser.
17
+
18
+ Usage: #{parser.program_name} [options]
19
+
20
+ For more information, see https://github.com/BurdetteLamar/webri/blob/main/README.md.
21
+
22
+ BANNER
23
+
24
+ parser.separator('Options:')
25
+ parser.on('-i', '--info', 'Print information about webri.') do
26
+ options[:info] = true
27
+ end
28
+ parser.on('--noreline', 'Do not use Reline (useful for testing).') do |value|
29
+ options[:noreline] = true
30
+ end
31
+ parser.on('-n', '--noop', 'Do not actually open web pages.') do |value|
32
+ options[:noop] = true
33
+ end
34
+ parser.on('-h', '--help', 'Print this help.') do
35
+ puts parser
36
+ exit
37
+ end
38
+ parser.on('-v', '--version', 'Print the version of webri.') do
39
+ puts WebRI::VERSION
40
+ exit
41
+ end
42
+
43
+ parser.parse!
44
+
45
+ release_name = ARGV[0]
46
+ WebRI.new(release_name, options)
data/lib/scraper.rb ADDED
@@ -0,0 +1,285 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+ require 'rexml'
4
+ require 'json'
5
+ require 'json/add/core'
6
+
7
+ # Class to scrape Ruby info from the documentation site and store it as JSON.
8
+
9
+ class Scraper
10
+
11
+ # The official documentation site for English.
12
+ BASE_URL = 'https://docs.ruby-lang.org/en'
13
+
14
+ # Translations for hex characters.
15
+ HEX_CHARS = {
16
+ '-21' => '!',
17
+ '-25' => '%',
18
+ '-26' => '&',
19
+ '-2A' => '*',
20
+ '-2B' => '+',
21
+ '-2D' => '-',
22
+ '-2F' => '/',
23
+ '-3C' => '<',
24
+ '-40' => '@',
25
+ '-3D' => '=',
26
+ '-3E' => '>',
27
+ '-3F' => '?',
28
+ '-5B' => '[',
29
+ '-5D' => ']',
30
+ '-5E' => '^',
31
+ '-60' => '`',
32
+ '-7C' => '|',
33
+ }
34
+
35
+ # Hash: relative URL paths for a name.
36
+ attr_accessor :hrefs_for_name
37
+
38
+ # Hash: the names of the classes that have a method.
39
+ attr_accessor :classes_for_method
40
+
41
+ def initialize
42
+ self.hrefs_for_name = {}
43
+ self.classes_for_method = {}
44
+ end
45
+
46
+ def self.scrapers
47
+ {
48
+ '3.2' => Scraper34,
49
+ '3.4' => Scraper32,
50
+ '4.0' => Scraper40,
51
+ }
52
+ end
53
+
54
+ def self.release_names
55
+ self.scrapers.keys
56
+ end
57
+
58
+ def get_home_page(release_name, suffix)
59
+ url = File.join(BASE_URL, release_name, suffix)
60
+ uri = URI(url)
61
+ response = Net::HTTP.get_response(uri)
62
+ unless response.code == '200'
63
+ message = "Page #{url} for release #{url} not found; code #{response.code}."
64
+ raise RuntimeError.new(message)
65
+ end
66
+ response.body
67
+ end
68
+
69
+ # Argument is an REXML::Element representing a link to a file.
70
+ # Returns the parsed name and href.
71
+ def add_file(element)
72
+ unless element.name == 'a'
73
+ message = "Expecting 'a', not '#{element.name}'"
74
+ raise ArgumentError.new(message)
75
+ end
76
+ file_href = element.attributes['href']
77
+ file_href.sub!(%r[^./], '')
78
+ file_name = 'ruby:' + file_href.sub(/\.html$/, '')
79
+ puts "Adding file #{file_name}"
80
+ hrefs_for_name[file_name] = [file_href]
81
+ [file_name, file_href]
82
+ end
83
+
84
+ # Argument is an REXML::Element representing a link to a class.
85
+ # Returns the parsed name and href.
86
+ def add_class(element)
87
+ unless element.name == 'a'
88
+ message = "Expecting 'a', not '#{element.name}'"
89
+ raise ArgumentError.new(message)
90
+ end
91
+ class_href = element.attributes['href'].sub(%r[^./], '')
92
+ class_name = class_href.gsub('/', '::').sub(%r[\.html$], '')
93
+ puts "Adding class #{class_name}"
94
+ hrefs_for_name[class_name] = [class_href]
95
+ [class_name, class_href]
96
+ end
97
+
98
+ # Arguments are an REXML::Element representing a link to a class, and the class name.
99
+ # Returns the parsed name and href.
100
+ def add_method(element, class_name)
101
+ method_href = element.attributes['href']
102
+ # Translate hex characters.
103
+ method_name = method_href.dup
104
+ hex_chars = method_name.scan(/-[0-9A-F]{2}/)
105
+ hex_chars.each do |hex_char|
106
+ char = HEX_CHARS[hex_char]
107
+ raise hex_char unless char
108
+ method_name.sub!(hex_char, char)
109
+ end
110
+ method_name = case
111
+ # when method_href == '#method-i-2D'
112
+ # # Special case; not worth the trouble of handling below.
113
+ # '#-'
114
+ when method_href.match(/[#]?method-i-2D/)
115
+ # Special case; not worth the trouble of handling below.
116
+ '#-'
117
+ # when method_href == '#method-i-2D-40'
118
+ # # Special case; not worth the trouble of handling below.
119
+ # '#-@'
120
+ when method_href.match(/[#]?method-i-2D-40/)
121
+ # Special case; not worth the trouble of handling below.
122
+ '#-@'
123
+ when method_href.match('-i-')
124
+ # Instance method.
125
+ '#' + method_name.sub(/[#]?method-i[-]?/, '')
126
+ when method_href.match('-c-')
127
+ # Singleton method.
128
+ '::' + method_name.sub(/[#]?method-c[-]?/, '')
129
+ end
130
+ puts " Adding method #{method_name}"
131
+ hrefs_for_name[method_name] = "#{method_href}"
132
+ classes_for_method[method_name] ||= []
133
+ classes_for_method[method_name] << "#{class_name}"
134
+ [method_name, method_href]
135
+ end
136
+
137
+ def write_json(release_name)
138
+ # This JSON is bulky, but readable by humans.
139
+ data = {
140
+ :timestamp => Time.now,
141
+ :hrefs_for_name => hrefs_for_name.sort.to_h,
142
+ :classes_for_method => classes_for_method.sort.to_h,
143
+ }
144
+ json = JSON.generate(
145
+ data.sort.to_h,
146
+ indent: " ", # 4 spaces per level
147
+ space: " ", # space after :
148
+ array_nl: "\n", # newline after each array element
149
+ object_nl: "\n" # newline after each object member
150
+ )
151
+ filepath = "data/#{release_name}.json"
152
+ File.write(filepath, json)
153
+ end
154
+
155
+ end
156
+
157
+ class Scraper40 < Scraper
158
+
159
+ RELEASE_NAME = '4.0'
160
+
161
+ def scrape
162
+ home_page = get_home_page(RELEASE_NAME, '')
163
+ home_page.lines.each do |line|
164
+ line.chomp!
165
+ next unless line.match(%r[<a href="])
166
+ line += '</a>' unless line.match(%r[</a>]) # Add end-tag if needed.
167
+ # Parse the line as XML.
168
+ doc = REXML::Document.new(line)
169
+ root = doc.root
170
+ case root.name
171
+ when 'a' # Each file URL is the href attribute in an anchor element.
172
+ add_file(root)
173
+ when 'ul' # All class/module URLs are in a single ul element.
174
+ add_classes(root)
175
+ break # We don't need anything farther down.
176
+ else
177
+ # Ignore.
178
+ end
179
+ end
180
+ write_json(RELEASE_NAME)
181
+ end
182
+
183
+ def add_classes(element)
184
+ REXML::XPath.each(element, "//*[@href]") do |element|
185
+ class_name, class_href = add_class(element)
186
+ url = File.join(BASE_URL, RELEASE_NAME, class_href)
187
+ uri = URI(url)
188
+ response = Net::HTTP.get_response(uri)
189
+ raise response.code unless response.code == '200'
190
+ response.body.lines.each do |line|
191
+ next unless line.match(%r[<li ><a href="#method-])
192
+ line.chomp!
193
+ doc = REXML::Document.new(line)
194
+ REXML::XPath.each(doc.root, "//*[@href]") do |element|
195
+ add_method(element, class_name)
196
+ end
197
+ end
198
+ end
199
+ end
200
+ end
201
+
202
+ class Scraper34 < Scraper
203
+
204
+ RELEASE_NAME = '3.4'
205
+
206
+ def add_method(element)
207
+ href = element.attributes['href']
208
+ m = href.match(%r[(#)])
209
+ class_name = m.pre_match.gsub('/', '::').sub(%r[\.html$], '')
210
+ new_href = m.post_match
211
+ element.attributes['href'] = new_href
212
+ super(element, class_name)
213
+ end
214
+
215
+ def scrape
216
+ home_page = get_home_page(RELEASE_NAME, 'table_of_contents.html')
217
+ enum = home_page.lines.to_enum
218
+ while true do
219
+ begin
220
+ line = enum.next
221
+ next unless line.match(%r[<li class="(\w+)">])
222
+ type = $1
223
+ next_line = enum.peek
224
+ element = REXML::Document.new(next_line).root
225
+ case type
226
+ when 'file'
227
+ add_file(element)
228
+ when 'class', 'module'
229
+ add_class(element)
230
+ when 'method'
231
+ add_method(element)
232
+ else
233
+ raise type
234
+ end
235
+ rescue StopIteration
236
+ break
237
+ end
238
+ end
239
+ write_json(RELEASE_NAME)
240
+ end
241
+
242
+ end
243
+
244
+ class Scraper32 < Scraper
245
+
246
+ RELEASE_NAME = '3.2'
247
+
248
+ def add_method(element)
249
+ href = element.attributes['href']
250
+ m = href.match(%r[(#)])
251
+ class_name = m.pre_match.gsub('/', '::').sub(%r[\.html$], '')
252
+ new_href = m.post_match
253
+ element.attributes['href'] = new_href
254
+ super(element, class_name)
255
+ end
256
+
257
+ def scrape
258
+ home_page = get_home_page(RELEASE_NAME, 'table_of_contents.html')
259
+ enum = home_page.lines.to_enum
260
+ while true do
261
+ begin
262
+ line = enum.next
263
+ next unless line.match(%r[<li class="(\w+)">])
264
+ type = $1
265
+ next_line = enum.peek
266
+ element = REXML::Document.new(next_line).root
267
+ case type
268
+ when 'file'
269
+ add_file(element)
270
+ when 'class', 'module'
271
+ add_class(element)
272
+ when 'method'
273
+ add_method(element)
274
+ else
275
+ raise type
276
+ end
277
+ rescue StopIteration
278
+ break
279
+ end
280
+ end
281
+ write_json(RELEASE_NAME)
282
+ end
283
+
284
+ end
285
+
data/lib/webri/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class WebRI
4
- VERSION = "1.0.5"
4
+ VERSION = "2.0.0"
5
5
  end