rfc-reader 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: []