├── bin ├── usage.txt └── rfc ├── jsdoc.conf.json ├── .gitignore ├── test.js ├── rfc.1.md ├── package.json ├── rfc.1 ├── README.md └── index.js /bin/usage.txt: -------------------------------------------------------------------------------- 1 | usage: rfc [-rvVh] [args ...] 2 | -------------------------------------------------------------------------------- /jsdoc.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "plugins/markdown" 4 | ], 5 | "source": { 6 | "include": ["./index.js"] 7 | }, 8 | "opts": { 9 | "encoding": "utf8", 10 | "destination": "./docs/", 11 | "recurse": true, 12 | "readme": "./README.md", 13 | "package": "./package.json" 14 | }, 15 | "markdown": { 16 | "hardwrap": true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | 7 | # Runtime data 8 | pids 9 | *.pid 10 | *.seed 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # node-waf configuration 22 | .lock-wscript 23 | 24 | # Compiled binary addons (http://nodejs.org/api/addons.html) 25 | build/Release 26 | 27 | # Dependency directory 28 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 29 | node_modules 30 | 31 | # Optional npm cache directory 32 | .npm 33 | 34 | # Optional REPL history 35 | .node_repl_history 36 | 37 | # my ignores 38 | docs/ 39 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | const test = require('tape') 2 | const rfc = require('./') 3 | 4 | test('simple', (t) => { 5 | const search = rfc.search('punycode') 6 | 7 | search.on('result', (result) => { 8 | if (result.isSynced()) { 9 | t.ok() 10 | result.open() 11 | .on('open', (pager) => { 12 | t.ok() 13 | pager.close() 14 | }) 15 | .on('close', () => t.end()) 16 | .on('error', (err) => { 17 | throw err 18 | }) 19 | } else { 20 | 21 | result.sync() 22 | .on('error', (err) => { 23 | console.error('error: %s', err) 24 | }) 25 | .on('end', () => { 26 | t.ok() 27 | result.open() 28 | .on('open', (pager) => pager.close()) 29 | .on('close', () => t.end()) 30 | }) 31 | } 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /rfc.1.md: -------------------------------------------------------------------------------- 1 | rfc(1) -- IETF RFC Reader Tool 2 | ================================= 3 | 4 | ## SYNOPSIS 5 | 6 | `rfc` [-rvVh] [args ...] 7 | 8 | ## OPTIONS 9 | 10 | `-r, --regex` use regex query input 11 | `-v, --verbose` show verbose output 12 | `-V, --version` output version 13 | `-h, --help` display this message 14 | 15 | ## COMMANDS 16 | 17 | `sync` sync remote rfc index file 18 | `search ` search rfc index 19 | `open ` open an rfc document by id 20 | `list` list all local rfc documents 21 | `clear` clear local cache 22 | 23 | ## DEBUG 24 | 25 | `DEBUG` tags: `rfc:search`, `rfc:match`, `rfc:sync` 26 | See [visionmedia/debug](https://github.com/visionmedia/debug) for details on usage. 27 | 28 | ## AUTHOR 29 | 30 | - Joseph Werle 31 | 32 | ## REPORTING BUGS 33 | 34 | - 35 | 36 | ## SEE ALSO 37 | 38 | - http://www.ietf.org/ 39 | - http://www.ietf.org/rfc 40 | 41 | ## LICENSE 42 | 43 | MIT 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rfc", 3 | "version": "0.1.0", 4 | "description": "IETF RFC reader tool", 5 | "main": "index.js", 6 | "preferGlobal": true, 7 | "bin": { 8 | "rfc": "./bin/rfc" 9 | }, 10 | "man": "./rfc.1", 11 | "scripts": { 12 | "docs": "jsdoc -c ./jsdoc.conf.json", 13 | "man": "curl -# -F page=@rfc.1.md -o rfc.1 http://mantastic.herokuapp.com", 14 | "test": "node test.js" 15 | }, 16 | "keywords": [ 17 | "ietf", 18 | "rfc", 19 | "reader" 20 | ], 21 | "author": "Joseph Werle", 22 | "license": "MIT", 23 | "dependencies": { 24 | "debug": "^4.3.3", 25 | "mkdirp": "^1.0.4", 26 | "os-homedir": "^2.0.0", 27 | "progress": "^2.0.3", 28 | "rmrf": "^2.0.4", 29 | "superagent": "^7.0.2", 30 | "through": "^2.3.8" 31 | }, 32 | "devDependencies": { 33 | "jsdoc": "^3.4.0", 34 | "tape": "^5.4.1" 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "git://github.com/jwerle/rfc.git" 39 | }, 40 | "bugs": { 41 | "url": "https://github.com/jwerle/rfc/issues" 42 | }, 43 | "homepage": "https://github.com/jwerle/rfc" 44 | } 45 | -------------------------------------------------------------------------------- /rfc.1: -------------------------------------------------------------------------------- 1 | .\" Generated with Ronnjs 0.3.8 2 | .\" http://github.com/kapouer/ronnjs/ 3 | . 4 | .TH "RFC" "1" "February 2016" "" "" 5 | . 6 | .SH "NAME" 7 | \fBrfc\fR \-\- IETF RFC Reader Tool 8 | . 9 | .SH "SYNOPSIS" 10 | \fBrfc\fR [\-rvVh] [args \.\.\.] 11 | . 12 | .SH "OPTIONS" 13 | \fB\-r, \-\-regex\fR use regex query input 14 | \fB\-v, \-\-verbose\fR show verbose output 15 | \fB\-V, \-\-version\fR output version 16 | \fB\-h, \-\-help\fR display this message 17 | . 18 | .SH "COMMANDS" 19 | \fBsync\fR sync remote rfc index file 20 | \fBsearch \fR search rfc index 21 | \fBopen \fR open an rfc document by id 22 | \fBlist\fR list all local rfc documents 23 | \fBclear\fR clear local cache 24 | . 25 | .SH "DEBUG" 26 | \fBDEBUG\fR tags: \fBrfc:search\fR, \fBrfc:match\fR, \fBrfc:sync\fR 27 | See visionmedia/debug \fIhttps://github\.com/visionmedia/debug\fR for details on usage\. 28 | . 29 | .SH "AUTHOR" 30 | . 31 | .IP "\(bu" 4 32 | Joseph Werle \fIjoseph\.werle@gmail\.com\fR 33 | . 34 | .IP "" 0 35 | . 36 | .SH "REPORTING BUGS" 37 | . 38 | .IP "\(bu" 4 39 | \fIhttps://github\.com/jwerle/rfc/issues\fR 40 | . 41 | .IP "" 0 42 | . 43 | .SH "SEE ALSO" 44 | . 45 | .IP "\(bu" 4 46 | http://www\.ietf\.org/ 47 | . 48 | .IP "\(bu" 4 49 | http://www\.ietf\.org/rfc 50 | . 51 | .IP "" 0 52 | . 53 | .SH "LICENSE" 54 | MIT -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | rfc(1) 2 | ===== 3 | 4 | [IETF](http://www.ietf.org) [RFC](http://www.ietf.org/rfc) reader tool 5 | 6 | ## install 7 | 8 | with **npm** 9 | 10 | ```sh 11 | $ npm install rfc -g 12 | ``` 13 | 14 | with **git** 15 | 16 | ```sh 17 | $ npm install github:jwerle/rfc -g 18 | ``` 19 | 20 | ## usage 21 | 22 | **search** 23 | 24 | ``` 25 | $ rfc search punycode 26 | ... searching 27 | 28 | 3492 Punycode: A Bootstring encoding of Unicode for Internationalized 29 | Domain Names in Applications (IDNA). A. Costello. March 2003. 30 | (Format: TXT=67439 bytes) (Updated by RFC5891) (Status: PROPOSED 31 | STANDARD) 32 | 33 | 1 result 34 | ``` 35 | 36 | **view** 37 | 38 | ``` 39 | $ rfc open 3492 40 | 41 | Network Working Group A. Costello 42 | Request for Comments: 3492 Univ. of California, Berkeley 43 | Category: Standards Track March 2003 44 | 45 | 46 | Punycode: A Bootstring encoding of Unicode 47 | for Internationalized Domain Names in Applications (IDNA) 48 | 49 | Status of this Memo 50 | 51 | This document specifies an Internet standards track protocol for the 52 | Internet community, and requests discussion and suggestions for 53 | improvements. Please refer to the current edition of the "Internet 54 | Official Protocol Standards" (STD 1) for the standardization state 55 | and status of this protocol. Distribution of this memo is unlimited. 56 | 57 | Copyright Notice 58 | 59 | Copyright (C) The Internet Society (2003). All Rights Reserved. 60 | 61 | Abstract 62 | 63 | Punycode is a simple and efficient transfer encoding syntax designed 64 | for use with Internationalized Domain Names in Applications (IDNA). 65 | It uni 66 | 67 | ... 68 | ``` 69 | 70 | ### module 71 | 72 | ```js 73 | var rfc = require('rfc') 74 | 75 | var count = 0; 76 | rfc.search('idna') 77 | .on('error', function (err) { 78 | // handle error 79 | }) 80 | .on('result', function (result) { 81 | count++; 82 | console.log(" %d %s", result.rfc, result.desc); 83 | }) 84 | .on('end', function () { 85 | console.log(" got %d result(s)", count); 86 | }); 87 | ``` 88 | 89 | ## api 90 | 91 | ### RFC\_BASE\_URL 92 | 93 | IETF RFC Base URL 94 | 95 | ### RFC\_INDEX\_URL 96 | 97 | IETF RFC Index file URL 98 | 99 | ### RFC\_CACHE 100 | 101 | Default RFC cache 102 | 103 | ### RFC\_CACHE\_INDEX 104 | 105 | Default RFC Index cache file name 106 | 107 | ### sync() 108 | 109 | Sync RFC Index file to a local file (`RFC_CACHE_INDEX`) in a directory 110 | defined by the environment variable `RFC_CACHE`. 111 | 112 | `sync()` returns a stream that is readable. 113 | 114 | ```js 115 | rfc.sync() 116 | .on('data', function (chunk) { 117 | console.log(chunk) 118 | }); 119 | ``` 120 | 121 | ### search(query) 122 | 123 | Searches RFC Index based on a query 124 | 125 | ```js 126 | rfc.search('idna') 127 | .on('result', function (result) { 128 | console.log("%d (%s) %s", 129 | result.rfc, 130 | result.path, 131 | result.desc); 132 | }); 133 | ``` 134 | 135 | ## debug 136 | 137 | `DEBUG` tags: `rfc:search`, `rfc:match`, `rfc:sync` 138 | See [visionmedia/debug](https://github.com/visionmedia/debug) for details on usage. 139 | 140 | ## license 141 | 142 | MIT 143 | -------------------------------------------------------------------------------- /bin/rfc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* 4 | * Module dependencies 5 | */ 6 | 7 | var rfc = require('../') 8 | , fs = require('fs') 9 | , fmt = require('util').format 10 | , p = require('path') 11 | , pkg = require('../package') 12 | , Progress = require('progress') 13 | 14 | /* 15 | * Program version 16 | */ 17 | 18 | var VERSION = pkg.version; 19 | 20 | /* 21 | * Program flags 22 | */ 23 | 24 | var USE_REGEX = false; 25 | var VERBOSE = false; 26 | 27 | var i = 0; 28 | var o = console.log; 29 | var e = console.error; 30 | var fread = fs.readFileSync; 31 | var exit = process.exit; 32 | var argv = process.argv; argv.shift(); 33 | var argc = argv.length; 34 | var hasCmd = false; 35 | var cargs = null; 36 | var arg = null; 37 | var cmd = null; 38 | var opt = null; 39 | var usageTxt = null; 40 | 41 | function printf () { 42 | process.stdout.write(fmt.apply(null, arguments)); 43 | } 44 | 45 | function c (ch, n) { 46 | var s = '', i = 0; 47 | for (; i < n; ++i) s += ch; 48 | return s; 49 | } 50 | 51 | cmd = argv[1]; 52 | usageTxt = String(fread(p.resolve(__dirname, 'usage.txt'))); 53 | 54 | if (null == cmd) { 55 | help(); 56 | exit(0); 57 | } 58 | 59 | for (i = 0; (arg = argv[i]); ++i) { 60 | 61 | if (null == cmd && '-' != arg[0]) { 62 | cmd = arg; 63 | } 64 | 65 | if ('-' != arg[0]) { 66 | continue; 67 | } 68 | 69 | switch (arg) { 70 | case '-r': case '--regex': 71 | USE_REGEX = true; 72 | break; 73 | 74 | case '-v': case '--verbose': 75 | VERBOSE = true; 76 | break; 77 | 78 | case '-V': case '--version': 79 | version(); 80 | break; 81 | 82 | case '-h': case '--help': 83 | help(); 84 | break; 85 | 86 | default: 87 | e('unknown option: "'+ arg +'"'); 88 | usage(); 89 | exit(1); 90 | } 91 | } 92 | 93 | for (i = 0; false == hasCmd && (arg = argv[i]); ++i) { 94 | if ('-' == arg[0]) { 95 | continue; 96 | } 97 | 98 | cargs = argv.slice(i + 1); 99 | 100 | switch (arg) { 101 | case 'sync': 102 | hasCmd = true; 103 | sync(); 104 | break; 105 | 106 | case 'search': 107 | hasCmd = true; 108 | search(cargs); 109 | break; 110 | 111 | case 'open': 112 | hasCmd = true; 113 | open(cargs); 114 | break; 115 | 116 | case 'clear': 117 | hasCmd = true; 118 | clear(); 119 | break; 120 | 121 | case 'index': 122 | hasCmd = true; 123 | index(); 124 | break; 125 | 126 | case 'ls': 127 | case 'list': 128 | hasCmd = true; 129 | list(); 130 | break; 131 | } 132 | } 133 | 134 | 135 | function usage () { 136 | o(usageTxt) 137 | } 138 | 139 | function version () { 140 | o(VERSION); 141 | exit(0); 142 | } 143 | 144 | function help () { 145 | usage(); 146 | o('options:'); 147 | o(' -r, --regex use regex query input'); 148 | o(' -v, --verbose show verbose output'); 149 | o(' -V, --version output version'); 150 | o(' -h, --help display this message'); 151 | o(); 152 | o('commands:'); 153 | o(' sync sync remote rfc index file'); 154 | o(' search search rfc index'); 155 | o(' open open an rfc document by id'); 156 | o(' list list all local rfc documents'); 157 | o(' index show local rfc index'); 158 | o(' clear clear local cache'); 159 | exit(0); 160 | } 161 | 162 | function sync () { 163 | var bar = null; 164 | 165 | o(''); 166 | o(' ... syncing from "%s"', rfc.RFC_INDEX_URL); 167 | 168 | rfc.sync() 169 | .on('length', function (len) { 170 | bar = new Progress(' syncing [:bar] :percent (:etas)', { 171 | complete: '-', 172 | incomplete: ' ', 173 | width: 50, 174 | total: len 175 | }); 176 | }) 177 | .on('progress', function (n) { 178 | bar.tick(n); 179 | }) 180 | .on('error', function (err) { 181 | e('error: %s', err.message); 182 | exit(1); 183 | }) 184 | .on('end', function () { 185 | o(' ✓ synced! (%s)', 186 | p.resolve(rfc.RFC_CACHE, rfc.RFC_CACHE_INDEX)); 187 | exit(0); 188 | }); 189 | } 190 | 191 | function search (query) { 192 | if (true == USE_REGEX) { 193 | if ('/' == query[0]) { 194 | query = Function('return '+ query); 195 | } else { 196 | query = RegExp(query, 'g'); 197 | } 198 | } else { 199 | query = query.join(' '); 200 | } 201 | 202 | var count = 0; 203 | 204 | o(' ... searching'); 205 | o(); 206 | rfc.search(query, true) 207 | .on('error', function (err) { 208 | e('error: %s', err.message); 209 | exit(1); 210 | }) 211 | .on('result', function (result) { 212 | count++; 213 | printf(' %d ', result.rfc); 214 | printf('%s\n\n', result.desc.replace(/\n/gm, '\n ')); 215 | printf(' %s\n\n', c('-', 78)); 216 | }) 217 | .on('end', function () { 218 | o() 219 | o(' ... (%d) result%s', count, count > 1? 's' : ''); 220 | exit(0); 221 | }); 222 | } 223 | 224 | function open (arg) { 225 | var results = []; 226 | var item = null; 227 | var tmp = null; 228 | var m = 0; 229 | var query = arg.join(' '); 230 | var resultOpened = false; 231 | 232 | if ('index' == query) { 233 | rfc.open(rfc.RFC_CACHE +'/'+ rfc.RFC_CACHE_INDEX) 234 | .on('error', function (err) { 235 | e('error: %s', err.message); 236 | exit(1); 237 | }).on('end', function () { 238 | exit(0); 239 | }); 240 | 241 | return; 242 | } 243 | 244 | // open with RFC number 245 | var num = parseInt(query, 10); 246 | if (!isNaN(num)) { 247 | return openRFC(new rfc.RFC({rfc: num})); 248 | } 249 | 250 | // do search 251 | rfc.search(query) 252 | .on('error', function (err) { 253 | e('error: %s', err.message); 254 | exit(1); 255 | }) 256 | .on('result', function (result) { 257 | if (resultOpened) { 258 | return; 259 | } 260 | 261 | resultOpened = true; 262 | openRFC(result); 263 | }) 264 | .on('end', function () { 265 | if (!resultOpened) { 266 | e('"%s" not found', query); 267 | } 268 | }); 269 | 270 | // handles sync (download) and open 271 | function openRFC (rfc) { 272 | if (rfc.isSynced()) { 273 | return rfc.open(); 274 | } 275 | 276 | rfc.sync() 277 | .on('error', function (err) { 278 | e('error: %s', err.message); 279 | }) 280 | .on('end', function () { 281 | rfc.open(); 282 | }); 283 | } 284 | } 285 | 286 | function list () { 287 | o(); 288 | rfc.list() 289 | .on('error', function (err) { 290 | e('error: %s', err.message); 291 | exit(1); 292 | }) 293 | .on('item', function (item) { 294 | o(' * %s - (%s)', item.name, item.path); 295 | }) 296 | .on('empty', function () { 297 | o() 298 | o(' no local rfc documents found!'); 299 | o(' try `rfc sync` first'); 300 | o(); 301 | exit(1); 302 | }); 303 | 304 | o(); 305 | } 306 | 307 | function clear () { 308 | o(); 309 | o(' ... clearing "%s"', rfc.RFC_CACHE); 310 | o(); 311 | rfc.clear(); 312 | exit(0); 313 | } 314 | 315 | function index () { 316 | o(); 317 | rfc.search('*', {local: true}) 318 | .on('error', function (err) { 319 | e('error: %s', err.message); 320 | exit(1); 321 | }) 322 | .on('result', function (result) { 323 | printf(' %d ', result.rfc); 324 | printf('%s\n\n', result.desc.replace(/\n/gm, '\n ')); 325 | printf(' %s\n\n', c('-', 78)); 326 | }) 327 | .on('end', function () { 328 | o(); 329 | o(' ... Try `rfc open index` too view the index in your PAGER'); 330 | o(); 331 | }); 332 | } 333 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const through = require('through') 2 | const agent = require('superagent') 3 | const fs = require('fs') 4 | const cp = require('child_process') 5 | const p = require('path') 6 | const mkdirp = require('mkdirp') 7 | const rmrf = require('rmrf') 8 | const HOME = require('os-homedir')() 9 | 10 | const SEARCHD = require('debug')('rfc:search') 11 | const MATCHD = require('debug')('rfc:match') 12 | const SYNCD = require('debug')('rfc:sync') 13 | 14 | const fexists = fs.existsSync 15 | 16 | /** 17 | * IETF RFC Base URL 18 | * 19 | * @default 20 | */ 21 | 22 | exports.RFC_BASE_URL = 'http://www.ietf.org/rfc' 23 | 24 | /** 25 | * IETF RFC Index file URL 26 | * 27 | * @default http://www.ietf.org/rfc/rfc-index.txt 28 | */ 29 | 30 | exports.RFC_INDEX_URL = exports.RFC_BASE_URL + '/rfc-index.txt' 31 | 32 | /** 33 | * Default RFC cache folder 34 | * 35 | * @default $HOME/.rfc.d 36 | */ 37 | 38 | exports.RFC_CACHE = p.resolve(HOME, '.rfc.d') 39 | 40 | /** 41 | * Default RFC Index cache file (file name) 42 | * 43 | * @default 44 | */ 45 | 46 | exports.RFC_CACHE_INDEX = 'rfc-index' 47 | 48 | /** 49 | * Syncs RFC Index file to `RFC_CACHE/RFC_CACHE_INDEX` 50 | * 51 | * @return {Stream} 52 | * @emits length 53 | * @emits progress 54 | * @emits data 55 | * @emits error 56 | * @emits end 57 | */ 58 | function sync () { 59 | const stream = through() 60 | const path = p.resolve(exports.RFC_CACHE, exports.RFC_CACHE_INDEX) 61 | 62 | mkdirp(exports.RFC_CACHE) 63 | .catch((err) => { 64 | if (err && !/EEXIST/.test(err.message)) { 65 | return stream.emit('error', err) 66 | } 67 | }) 68 | .then(() => fetchIndex()) 69 | 70 | return stream 71 | 72 | function fetchIndex () { 73 | SYNCD('GET index', this.rfc, exports.RFC_INDEX_URL) 74 | agent.get(exports.RFC_INDEX_URL, function (err, res) { 75 | if (err) { 76 | return stream.emit('error', err) 77 | } 78 | 79 | SYNCD('RESPONSE >', path) 80 | 81 | const out = fs.createWriteStream(path) 82 | 83 | let lines = null 84 | let line = null 85 | 86 | if (!res.text.length) { 87 | return stream.end() 88 | } 89 | 90 | lines = res.text.split('\n') 91 | stream.emit('length', lines.length) 92 | stream.pipe(out) 93 | 94 | // into chunks 95 | while ((line = lines.shift())) { 96 | stream.emit('progress', 1) 97 | line += '\n' 98 | stream.write(line) 99 | } 100 | stream.end() 101 | }) 102 | } 103 | } 104 | exports.sync = sync 105 | 106 | /** 107 | * Searches RFC Index based on a query 108 | * 109 | * @param {String|Regex} [query='*'] 110 | * @param {Object} [opts] 111 | * @param {Boolean} [opts.useRemote = false] - always use remote index 112 | * @return {Stream} 113 | * @emits result 114 | * @emits error 115 | * @emits end 116 | * @todo support query function 117 | */ 118 | function search (query, opts) { 119 | const stream = through(write) 120 | const indexFile = p.resolve(exports.RFC_CACHE, exports.RFC_CACHE_INDEX) 121 | 122 | if (!query || query === '*') { 123 | query = '.*' 124 | } 125 | 126 | if (!opts) { 127 | opts = {} 128 | } 129 | 130 | if (opts.useRemote || !fexists(indexFile)) { 131 | SEARCHD('REMOTE RFC_INDEX') 132 | agent.get(exports.RFC_INDEX_URL, function (err, res) { 133 | if (err) { 134 | return stream.emit('error', err) 135 | } 136 | 137 | parse(res.text) 138 | }) 139 | } else { 140 | SEARCHD('CACHED RFC_INDEX') 141 | fs.readFile(indexFile, function (err, buf) { 142 | if (err) { 143 | return stream.emit('error', err) 144 | } 145 | 146 | parse(String(buf)) 147 | }) 148 | } 149 | 150 | return stream 151 | 152 | /** 153 | * parse RFC documents in RFC_CACHE_INDEX and write to `stream` 154 | * @param {String} data - content of RFC_CACHE_INDEX 155 | */ 156 | function parse (data) { 157 | const lines = data.split('\n') 158 | const tmp = [] 159 | 160 | let didReachBody = false 161 | 162 | do { 163 | const line = trim(lines.shift()) 164 | const header = trim(lines[0]) 165 | if (/^RFC Index$/i.test(line) && /^[-]{9}$/.test(header)) { 166 | didReachBody = true 167 | lines.shift() 168 | break 169 | } 170 | } while (!didReachBody && lines.length) 171 | 172 | if (!didReachBody) { 173 | return stream.emit( 174 | 'error', 175 | new Error('Failed to parse body of RFC index.') 176 | ) 177 | } 178 | 179 | do { 180 | const line = lines.shift() 181 | if (line.length) { 182 | tmp.push(line) 183 | } else if (!line.length && tmp.length) { 184 | stream.write(tmp.join('\n')) 185 | tmp.length = 0 186 | } 187 | } while (lines.length) 188 | 189 | // DO NOT end stream here 190 | // stream should emit all 'result's and then 'end' 191 | // use empty string to signal EOS 192 | SEARCHD('END parse()') 193 | stream.write('') 194 | } 195 | 196 | /** 197 | * data handler of the `through` stream 198 | * search `query` in `chunk` and emit `result` if matched 199 | * 200 | * @param {String} chunk - one RFC in RFC_CACHE_INDEX, written by `parse()` 201 | * @emit result 202 | */ 203 | function write (chunk) { 204 | const str = String(chunk) 205 | 206 | let regex = null 207 | let parts = null 208 | let desc = null 209 | let num = null 210 | 211 | if (query instanceof RegExp) { 212 | regex = query 213 | } else { 214 | regex = RegExp('(' + query + ')', 'ig') 215 | } 216 | 217 | if (str === '') { 218 | SEARCHD('END write') 219 | stream.end() 220 | } 221 | 222 | SEARCHD('CHUNK[%d]: ', str.length, str) 223 | 224 | if (regex.test(str)) { 225 | parts = str.split(/^([0-9]+)\s+/) 226 | 227 | if (!parts.length) { 228 | return stream.emit('error', new Error('result parse error')) 229 | } 230 | 231 | if (!trim(parts[0])) { 232 | parts.shift() 233 | } 234 | 235 | if (!parts.length) { 236 | return 237 | } 238 | 239 | num = parts.shift() 240 | 241 | if (isNaN(num = parseInt(num, 10))) { 242 | return stream.emit('error', new Error('rfc # result parse error')) 243 | } 244 | 245 | desc = parts.shift() 246 | 247 | if (!desc || !desc.length) { 248 | return stream.emit( 249 | 'error', 250 | new Error('rfc description result parse error') 251 | ) 252 | } 253 | 254 | const rfc = new RFC({ rfc: num, desc: desc }) 255 | MATCHD('RFC[%d] local[%s] synced[%s]', num, !!opts.local, rfc.isSynced()) 256 | 257 | return stream.emit('result', rfc) 258 | } 259 | } 260 | } 261 | exports.search = search 262 | 263 | /** 264 | * Opens a file with the user `PAGER` 265 | * 266 | * @param {String} file - path of file to open 267 | * @return {Stream} 268 | * @emits error 269 | * @emits end 270 | */ 271 | function open (file) { 272 | const stream = through() 273 | const PAGER = getPager() 274 | 275 | if (!fexists(file)) { 276 | return null 277 | } 278 | 279 | if (!PAGER) { 280 | console.error('error: PAGER environment constiable not set') 281 | process.exit(1) 282 | } 283 | 284 | const pager = cp.spawn(PAGER, [file], { 285 | stdio: 'inherit' 286 | }) 287 | 288 | pager.on('error', function (err) { 289 | stream.emit('error', err) 290 | }) 291 | 292 | pager.on('close', function () { 293 | stream.emit('end') 294 | }) 295 | 296 | pager.once('spawn', () => process.nextTick(() => stream.emit('open', stream))) 297 | 298 | stream.pager = pager 299 | stream.on('error', close) 300 | 301 | return Object.assign(stream, { close }) 302 | 303 | function close() { 304 | pager.kill() 305 | stream.end() 306 | } 307 | } 308 | exports.open = open 309 | 310 | /** 311 | * Removes everything from the `RFC_CACHE` 312 | * 313 | */ 314 | function clear () { 315 | return rmrf(exports.RFC_CACHE) 316 | } 317 | exports.clear = clear 318 | 319 | /** 320 | * Clears RFC Index cache 321 | * 322 | */ 323 | function clearIndex () { 324 | return rmrf(p.resolve(exports.RFC_CACHE, exports.RFC_CACHE_INDEX)) 325 | } 326 | exports.clearIndex = clearIndex 327 | 328 | /** 329 | * Lists all downloaded RFC files in `RFC_CACHE` 330 | * 331 | */ 332 | function list () { 333 | const stream = through() 334 | const ls = cp.spawn('ls', [exports.RFC_CACHE], { 335 | stdio: [null, 'pipe', 'pipe'] 336 | }) 337 | 338 | ls.on('error', function (err) { 339 | stream.emit('error', err) 340 | }) 341 | 342 | ls.stdout.on('data', function (chunk) { 343 | const lines = trim(String(chunk)).split('\n') 344 | 345 | let line = null 346 | let item = null 347 | 348 | while ((line = lines.shift())) { 349 | if (/\.txt/.test(line)) { 350 | item = {} 351 | item.name = p.basename(line) 352 | item.path = p.resolve(exports.RFC_CACHE, line) 353 | stream.emit('item', item) 354 | } 355 | } 356 | }) 357 | 358 | ls.stderr.on('data', function (chunk) { 359 | if (/no such file or directory/.test(String(chunk).toLowerCase())) { 360 | stream.emit('empty') 361 | } else { 362 | stream.emit('error', new Error(String(chunk))) 363 | } 364 | }) 365 | 366 | return stream 367 | } 368 | exports.list = list 369 | 370 | /** 371 | * Removes an RFC document from the cache 372 | * 373 | * @param {String|Number} rfc 374 | */ 375 | function clearRfc (rfc) { 376 | return rmrf(rfcCachePath(rfc)) 377 | } 378 | exports.clearRfc = clearRfc 379 | 380 | /** 381 | * `RFC` class representing an RFC document 382 | * 383 | * @class 384 | * @param {Object} opts 385 | * @param {Number} opts.rfc - RFC number 386 | * @param {String} [opts.desc = ''] - RFC description 387 | * @param {String} [opts.path = rfcCachePath(opts.rfc)] - Path for the downloaded RFC document 388 | */ 389 | function RFC (opts) { 390 | if (isNaN(opts.rfc)) { 391 | throw new Error('rfc should be a number') 392 | } 393 | 394 | this.rfc = Number(opts.rfc) 395 | this.desc = String(opts.desc || '') 396 | this.path = opts.path || rfcCachePath(opts.rfc) 397 | } 398 | exports.RFC = RFC 399 | 400 | /** 401 | * Opens the RFC document with the user's `$PAGER` 402 | * 403 | */ 404 | RFC.prototype.open = function () { 405 | return exports.open(this.path) 406 | } 407 | 408 | /** 409 | * Predicate to test if the RFC document is sync'd to file system 410 | * 411 | */ 412 | RFC.prototype.isSynced = function () { 413 | return fexists(this.path) 414 | } 415 | 416 | /** 417 | * Syncs the RFC document to file system 418 | * 419 | */ 420 | RFC.prototype.sync = function () { 421 | const stream = through() 422 | const name = '/rfc' + this.rfc + '.txt' 423 | const path = this.path 424 | 425 | SYNCD('GET rfc[%d]', this.rfc, exports.RFC_BASE_URL + name) 426 | agent 427 | .get(exports.RFC_BASE_URL + name) 428 | .end(function (err, res) { 429 | if (err) { 430 | SYNCD('RESPONSE Error', err) 431 | return stream.emit('error', err) 432 | } 433 | 434 | SYNCD('RESPONSE >', path) 435 | stream.pipe(fs.createWriteStream(path)) 436 | stream.write(res.text) 437 | stream.end() 438 | // will this end prematurely? 439 | }) 440 | 441 | return stream 442 | } 443 | 444 | /** 445 | * Trim utility 446 | * 447 | * @private 448 | */ 449 | function trim (str) { 450 | str = str.replace(/^\s+/, '').trim() 451 | for (let i = str.length - 1; i >= 0; i--) { 452 | if (/\S/.test(str.charAt(i))) { 453 | str = str.substring(0, i + 1) 454 | break 455 | } 456 | } 457 | 458 | return str 459 | } 460 | 461 | /** 462 | * Get path of RFC document in cache 463 | * 464 | * @private 465 | * @param {String|Number} rfc - RFC number 466 | */ 467 | function rfcCachePath (rfc) { 468 | return p.resolve(exports.RFC_CACHE, 'rfc' + rfc + '.txt') 469 | } 470 | 471 | /** 472 | * Get available terminal pager 473 | * 474 | * @private 475 | * @return {String} 476 | */ 477 | function getPager () { 478 | const less = function less () { return cp.execSync('which less').toString().trim() } 479 | const more = function more () { return cp.execSync('which more').toString().trim() } 480 | const pg = function pg () { return cp.execSync('which pg').toString().trim() } 481 | 482 | return process.env.PAGER || less() || more() || pg() 483 | } 484 | --------------------------------------------------------------------------------