rfc-reader 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/exe/rfc-reader +7 -0
- data/lib/rfc_reader/command.rb +80 -0
- data/lib/rfc_reader/library.rb +110 -0
- data/lib/rfc_reader/recent.rb +54 -0
- data/lib/rfc_reader/search.rb +84 -0
- data/lib/rfc_reader/terminal.rb +26 -0
- data/lib/rfc_reader/version.rb +5 -0
- data/lib/rfc_reader.rb +11 -0
- metadata +112 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 7b42f53e0145a5153d21e1f555c006e2081c43c126b60c581f5687567530f00b
|
4
|
+
data.tar.gz: cb3d545f31ebba48757242e003e9f3ef2d9960c1a6277256cfc6dfcc389af38c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3337413fc3634c92bf8ec4ff3f347749de4b4cff385e6b29184fa63a641e8456f9cfd21142e07d125b09e320aa38bbab57e90c7f64cc2f75d516c7fa3307d81a
|
7
|
+
data.tar.gz: 557038fda98a3ea623ea128cd99aa4423ff2befb1493f4f0e7313807a60273bf2c14007df1d818b90ce5e33bdd6de08a8d5814e69227104ede1eaa21cb4349e8
|
data/exe/rfc-reader
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "thor"
|
4
|
+
|
5
|
+
module RfcReader
|
6
|
+
class Command < Thor
|
7
|
+
# @return [Boolean]
|
8
|
+
def self.exit_on_failure? = true
|
9
|
+
|
10
|
+
# @param command [String, nil]
|
11
|
+
def help(command = nil)
|
12
|
+
unless command
|
13
|
+
puts <<~DESCRIPTION
|
14
|
+
>>> rfc-reader
|
15
|
+
|
16
|
+
This command downloads the plaintext version of RFCs from
|
17
|
+
rfc-editor.org so that they can be read at the command line.
|
18
|
+
|
19
|
+
The last 100 downloaded RFCs are saved locally so that they can be
|
20
|
+
read later on without the need for an internet connection.
|
21
|
+
|
22
|
+
DESCRIPTION
|
23
|
+
end
|
24
|
+
|
25
|
+
super
|
26
|
+
end
|
27
|
+
|
28
|
+
desc "search [TERM]", "Search for RFCs by TERM for reading"
|
29
|
+
long_desc <<-LONGDESC
|
30
|
+
Search for RFCs on rfc-editor.org by the given term and list them.
|
31
|
+
|
32
|
+
Choose any RFC from the list that seems interesting and
|
33
|
+
it will get downloaded so that you can read it in your terminal.
|
34
|
+
LONGDESC
|
35
|
+
# @param term [String]
|
36
|
+
def search(term)
|
37
|
+
search_results = Search.search_by(term: term)
|
38
|
+
title = Terminal.choose("Choose an RFC to read:", search_results.keys)
|
39
|
+
url = search_results.fetch(title)
|
40
|
+
content = Library.download_document(title: title, url: url)
|
41
|
+
Terminal.page(content)
|
42
|
+
end
|
43
|
+
|
44
|
+
desc "recent", "List recent RFC releases for reading"
|
45
|
+
long_desc <<-LONGDESC
|
46
|
+
Fetch the most recent 15 RFCs on rfc-editor.org and list them.
|
47
|
+
|
48
|
+
Choose any RFC from the list that seems interesting and
|
49
|
+
it will get downloaded so that you can read it in your terminal.
|
50
|
+
LONGDESC
|
51
|
+
def recent
|
52
|
+
recent_results = Recent.list
|
53
|
+
title = Terminal.choose("Choose an RFC to read:", recent_results.keys)
|
54
|
+
url = recent_results.fetch(title)
|
55
|
+
content = Library.download_document(title: title, url: url)
|
56
|
+
Terminal.page(content)
|
57
|
+
end
|
58
|
+
|
59
|
+
desc "library", "List already downloaded RFCs for reading"
|
60
|
+
long_desc <<-LONGDESC
|
61
|
+
List the last 100 RFCs that have already been downloaded.
|
62
|
+
|
63
|
+
Choose any RFC from the list that seems interesting and
|
64
|
+
you can read it offline in your terminal.
|
65
|
+
LONGDESC
|
66
|
+
def library
|
67
|
+
rfc_catalog = Library.catalog
|
68
|
+
all_titles = rfc_catalog.map { _1[:title] }
|
69
|
+
title = Terminal.choose("Choose an RFC to read:", all_titles)
|
70
|
+
rfc = rfc_catalog.find { _1[:title] == title }
|
71
|
+
content = Library.load_document(**rfc)
|
72
|
+
Terminal.page(content)
|
73
|
+
end
|
74
|
+
|
75
|
+
desc "version", "Print the program version"
|
76
|
+
def version
|
77
|
+
puts VERSION
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fileutils"
|
4
|
+
require "json"
|
5
|
+
require "net/http"
|
6
|
+
|
7
|
+
module RfcReader
|
8
|
+
module Library
|
9
|
+
MAX_DOCUMENT_COUNT = 100
|
10
|
+
private_constant :MAX_DOCUMENT_COUNT
|
11
|
+
|
12
|
+
# @param title [String] the RFC title
|
13
|
+
# @param url [String] the text file URL for the RFC
|
14
|
+
# @return [String] the RFC content
|
15
|
+
def self.download_document(title:, url:)
|
16
|
+
file_name = File.basename(url)
|
17
|
+
file_path = File.join(library_cache_dir, file_name)
|
18
|
+
|
19
|
+
content = Net::HTTP.get(URI(url))
|
20
|
+
FileUtils.mkdir_p(library_cache_dir)
|
21
|
+
File.write(file_path, content)
|
22
|
+
add_to_catalog(title: title, url: url, path: file_path)
|
23
|
+
|
24
|
+
content
|
25
|
+
end
|
26
|
+
|
27
|
+
# @param title [String] the RFC title
|
28
|
+
# @param url [String] the text file URL for the RFC
|
29
|
+
# @param path [String] the path to the local copy of the RFC
|
30
|
+
# @return [String] the RFC content
|
31
|
+
def self.load_document(title:, url:, path:)
|
32
|
+
content = File.read(path)
|
33
|
+
|
34
|
+
add_to_catalog(title: title, url: url, path: path)
|
35
|
+
|
36
|
+
content
|
37
|
+
end
|
38
|
+
|
39
|
+
# These are referenced later on by the `rfc-reader library` command.
|
40
|
+
#
|
41
|
+
# @example
|
42
|
+
# [
|
43
|
+
# { title: "My RFC", url: "www.my-rfc.com/my-rfc.txt", path: ".cache/rfc-reader/library/my-rfc.txt" },
|
44
|
+
# ...
|
45
|
+
# ]
|
46
|
+
# @return [Array<Hash<String, String>>] a list of RFC info hashes
|
47
|
+
def self.catalog
|
48
|
+
if File.exist?(library_cache_list_path)
|
49
|
+
content = File.read(library_cache_list_path)
|
50
|
+
JSON.parse(content, symbolize_names: true)
|
51
|
+
else
|
52
|
+
[]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Adds the RFC to the beginning of the catalog and removes any existing entries.
|
57
|
+
# These are referenced later on by the `rfc-reader library` command.
|
58
|
+
#
|
59
|
+
# @param title [String] the RFC title
|
60
|
+
# @param url [String] the text file URL for the RFC
|
61
|
+
# @param path [String] the path to the local copy of the RFC
|
62
|
+
def self.add_to_catalog(title:, url:, path:)
|
63
|
+
list = catalog.reject do |rfc|
|
64
|
+
title == rfc[:title] ||
|
65
|
+
url == rfc[:url] ||
|
66
|
+
path == rfc[:path]
|
67
|
+
end
|
68
|
+
|
69
|
+
rfc = {
|
70
|
+
title: title,
|
71
|
+
url: url,
|
72
|
+
path: path,
|
73
|
+
}
|
74
|
+
|
75
|
+
list = [rfc, *list]
|
76
|
+
while list.size > MAX_DOCUMENT_COUNT
|
77
|
+
path = list.pop[:path]
|
78
|
+
FileUtils.rm_f(path) if path.start_with?(library_cache_dir)
|
79
|
+
end
|
80
|
+
|
81
|
+
json = JSON.pretty_generate(list)
|
82
|
+
FileUtils.mkdir_p(program_cache_dir)
|
83
|
+
File.write(library_cache_list_path, json)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Cache directories
|
87
|
+
|
88
|
+
# @return [String]
|
89
|
+
def self.xdg_cache_home
|
90
|
+
ENV
|
91
|
+
.fetch("XDG_CACHE_HOME") { File.join(Dir.home, ".cache") }
|
92
|
+
.then { |path| File.expand_path(path) }
|
93
|
+
end
|
94
|
+
|
95
|
+
# @return [String]
|
96
|
+
def self.program_cache_dir
|
97
|
+
File.join(xdg_cache_home, "rfc_reader")
|
98
|
+
end
|
99
|
+
|
100
|
+
# @return [String]
|
101
|
+
def self.library_cache_list_path
|
102
|
+
File.join(program_cache_dir, "library_list.json")
|
103
|
+
end
|
104
|
+
|
105
|
+
# @return [String]
|
106
|
+
def self.library_cache_dir
|
107
|
+
File.join(program_cache_dir, "library")
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "net/http"
|
4
|
+
require "nokogiri"
|
5
|
+
|
6
|
+
module RfcReader
|
7
|
+
module Recent
|
8
|
+
RECENT_RFCS_RSS_URI = URI("https://www.rfc-editor.org/rfcrss.xml").freeze
|
9
|
+
private_constant :RECENT_RFCS_RSS_URI
|
10
|
+
|
11
|
+
# @return [Hash<String, String>] from RFC title to text file url
|
12
|
+
def self.list
|
13
|
+
xml = fetch
|
14
|
+
parse(xml)
|
15
|
+
end
|
16
|
+
|
17
|
+
# @return [String] the raw XML from the recent RFCs RSS feed
|
18
|
+
def self.fetch
|
19
|
+
Net::HTTP.get(RECENT_RFCS_RSS_URI)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Example: XML fragment we're trying to parse title and link data from.
|
23
|
+
#
|
24
|
+
# ```xml
|
25
|
+
# <item>
|
26
|
+
# <title>
|
27
|
+
# RFC 9624: EVPN Broadcast, Unknown Unicast, or Multicast (BUM) Using Bit Index Explicit Replication (BIER)
|
28
|
+
# </title>
|
29
|
+
# <link>https://www.rfc-editor.org/info/rfc9624</link>
|
30
|
+
# <description>
|
31
|
+
# This document specifies protocols and procedures for forwarding Broadcast, Unknown Unicast, or Multicast (BUM) traffic of Ethernet VPNs (EVPNs) using Bit Index Explicit Replication (BIER).
|
32
|
+
# </description>
|
33
|
+
# </item>
|
34
|
+
# ```
|
35
|
+
#
|
36
|
+
# @param xml [String] the XML of the recent RFCs RSS endpoint
|
37
|
+
# @return [Hash<String, String>] from RFC title to text file url
|
38
|
+
def self.parse(xml)
|
39
|
+
Nokogiri::XML(xml).xpath("//item").to_h do |item|
|
40
|
+
item_hash = item.elements.to_h do |elem|
|
41
|
+
[elem.name, elem.text.strip]
|
42
|
+
end
|
43
|
+
|
44
|
+
# The link is to the webpage and not the plaintext document so we must convert it.
|
45
|
+
file_name = File.basename(item_hash.fetch("link"))
|
46
|
+
|
47
|
+
[
|
48
|
+
item_hash.fetch("title"),
|
49
|
+
"https://www.rfc-editor.org/rfc/#{file_name}.txt",
|
50
|
+
]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "net/http"
|
4
|
+
require "nokogiri"
|
5
|
+
|
6
|
+
module RfcReader
|
7
|
+
module Search
|
8
|
+
RFC_SEARCH_URI = URI("https://www.rfc-editor.org/search/rfc_search_detail.php").freeze
|
9
|
+
private_constant :RFC_SEARCH_URI
|
10
|
+
|
11
|
+
# @param term [String]
|
12
|
+
# @return [Hash<String, String>] from RFC title to text file url
|
13
|
+
def self.search_by(term:)
|
14
|
+
html = fetch_by(term: term)
|
15
|
+
parse(html)
|
16
|
+
end
|
17
|
+
|
18
|
+
# @param term [String]
|
19
|
+
# @return [String] the raw HTML of the search results for the given term
|
20
|
+
def self.fetch_by(term:)
|
21
|
+
Net::HTTP.post_form(RFC_SEARCH_URI, { combo_box: term }).body
|
22
|
+
end
|
23
|
+
|
24
|
+
# Example: HTML fragment we're trying to parse title and link info from.
|
25
|
+
#
|
26
|
+
# ```html
|
27
|
+
# <div class="scrolltable">
|
28
|
+
# <table class='gridtable'>
|
29
|
+
# <tr>
|
30
|
+
# <th>
|
31
|
+
# <a href='rfc_search_detail.php?sortkey=Number&sorting=DESC&page=25&title=ftp&pubstatus[]=Any&pub_date_type=any'>Number</a>
|
32
|
+
# </th>
|
33
|
+
# <th>Files</th>
|
34
|
+
# <th>Title</th>
|
35
|
+
# <th>Authors</th>
|
36
|
+
# <th>
|
37
|
+
# <a href='rfc_search_detail.php?sortkey=Date&sorting=DESC&page=25&title=ftp&pubstatus[]=Any&pub_date_type=any'>Date</a>
|
38
|
+
# </th>
|
39
|
+
# <th>More Info</th>
|
40
|
+
# <th>Status</th>
|
41
|
+
# </tr>
|
42
|
+
# <tr>
|
43
|
+
# <td>
|
44
|
+
# <a href="https://www.rfc-editor.org/info/rfc114" target="_blank">RFC 114</a>
|
45
|
+
# </td>
|
46
|
+
# <td>
|
47
|
+
# <a href="https://www.rfc-editor.org/rfc/rfc114.txt" target="_blank">ASCII</a>
|
48
|
+
# ,
|
49
|
+
# <a href="https://www.rfc-editor.org/pdfrfc/rfc114.txt.pdf" target="_blank">PDF</a>
|
50
|
+
# ,
|
51
|
+
# <a href="https://www.rfc-editor.org/rfc/rfc114.html" target="_blank">HTML</a>
|
52
|
+
# </td>
|
53
|
+
# <td class="title"> File Transfer Protocol </td>
|
54
|
+
# <td> A.K. Bhushan</td>
|
55
|
+
# <td>April 1971</td>
|
56
|
+
# <td>
|
57
|
+
# Updated by
|
58
|
+
# <a href="https://www.rfc-editor.org/info/rfc133" target="_blank">RFC 133</a>
|
59
|
+
# ,
|
60
|
+
# <a href="https://www.rfc-editor.org/info/rfc141" target="_blank">RFC 141</a>
|
61
|
+
# ,
|
62
|
+
# <a href="https://www.rfc-editor.org/info/rfc171" target="_blank">RFC 171</a>
|
63
|
+
# ,
|
64
|
+
# <a href="https://www.rfc-editor.org/info/rfc172" target="_blank">RFC 172</a>
|
65
|
+
# </td>
|
66
|
+
# <td>Unknown</td>
|
67
|
+
# </tr>
|
68
|
+
# ...
|
69
|
+
# ```
|
70
|
+
#
|
71
|
+
# @param html [String] the HTML of the search results
|
72
|
+
# @return [Hash<String, String>] from RFC title to text file url
|
73
|
+
def self.parse(html)
|
74
|
+
# NOTE: The first element in the table is just some general search information. See example HTML above.
|
75
|
+
Nokogiri::HTML(html).xpath("//div[@class='scrolltable']//table[@class='gridtable']//tr").drop(1).to_h do |tr_node|
|
76
|
+
td_nodes = tr_node.elements
|
77
|
+
title = td_nodes[2].text.strip
|
78
|
+
url = td_nodes[1].elements.map { _1.attribute("href").text.strip }.find { _1.end_with?(".txt") }
|
79
|
+
|
80
|
+
[title, url]
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RfcReader
|
4
|
+
module Terminal
|
5
|
+
# @param content [String]
|
6
|
+
def self.page(content)
|
7
|
+
require "tty-pager"
|
8
|
+
TTY::Pager.page(content)
|
9
|
+
end
|
10
|
+
|
11
|
+
# @param prompt [String]
|
12
|
+
# @param choices [Array<String>] where all choices are unique
|
13
|
+
def self.choose(prompt, choices)
|
14
|
+
require "tty-prompt"
|
15
|
+
TTY::Prompt
|
16
|
+
.new(
|
17
|
+
quiet: true,
|
18
|
+
track_history: false,
|
19
|
+
interrupt: :exit,
|
20
|
+
symbols: { marker: ">" },
|
21
|
+
enable_color: !ENV["NO_COLOR"]
|
22
|
+
)
|
23
|
+
.select(prompt, choices)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/rfc_reader.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "rfc_reader/version"
|
4
|
+
|
5
|
+
module RfcReader
|
6
|
+
autoload :Command, "rfc_reader/command"
|
7
|
+
autoload :Recent, "rfc_reader/recent"
|
8
|
+
autoload :Search, "rfc_reader/search"
|
9
|
+
autoload :Terminal, "rfc_reader/terminal"
|
10
|
+
autoload :Library, "rfc_reader/library"
|
11
|
+
end
|
metadata
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rfc-reader
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Kevin Robell
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-09-24 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: nokogiri
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.16'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.16'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: thor
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1.3'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '1.3'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: tty-pager
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.14'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.14'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: tty-prompt
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0.23'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0.23'
|
69
|
+
description:
|
70
|
+
email:
|
71
|
+
- apainintheneck@gmail.com
|
72
|
+
executables:
|
73
|
+
- rfc-reader
|
74
|
+
extensions: []
|
75
|
+
extra_rdoc_files: []
|
76
|
+
files:
|
77
|
+
- exe/rfc-reader
|
78
|
+
- lib/rfc_reader.rb
|
79
|
+
- lib/rfc_reader/command.rb
|
80
|
+
- lib/rfc_reader/library.rb
|
81
|
+
- lib/rfc_reader/recent.rb
|
82
|
+
- lib/rfc_reader/search.rb
|
83
|
+
- lib/rfc_reader/terminal.rb
|
84
|
+
- lib/rfc_reader/version.rb
|
85
|
+
homepage: https://github.com/apainintheneck/rfc-reader/
|
86
|
+
licenses:
|
87
|
+
- MIT
|
88
|
+
metadata:
|
89
|
+
homepage_uri: https://github.com/apainintheneck/rfc-reader/
|
90
|
+
source_code_uri: https://github.com/apainintheneck/rfc-reader/
|
91
|
+
changelog_uri: https://github.com/apainintheneck/rfc-reader/blob/main/CHANELOG.md
|
92
|
+
rubygems_mfa_required: 'true'
|
93
|
+
post_install_message:
|
94
|
+
rdoc_options: []
|
95
|
+
require_paths:
|
96
|
+
- lib
|
97
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - ">="
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: 3.0.0
|
102
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
103
|
+
requirements:
|
104
|
+
- - ">="
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: '0'
|
107
|
+
requirements: []
|
108
|
+
rubygems_version: 3.5.6
|
109
|
+
signing_key:
|
110
|
+
specification_version: 4
|
111
|
+
summary: Search for and read RFCs at the command line.
|
112
|
+
test_files: []
|