rfc-reader 1.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.
- 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: []
|