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 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,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
5
+ require "rfc_reader"
6
+
7
+ RfcReader::Command.start
@@ -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&nbsp;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&nbsp;133</a>
59
+ # ,
60
+ # <a href="https://www.rfc-editor.org/info/rfc141" target="_blank">RFC&nbsp;141</a>
61
+ # ,
62
+ # <a href="https://www.rfc-editor.org/info/rfc171" target="_blank">RFC&nbsp;171</a>
63
+ # ,
64
+ # <a href="https://www.rfc-editor.org/info/rfc172" target="_blank">RFC&nbsp;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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RfcReader
4
+ VERSION = "1.0.0"
5
+ 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: []