├── .gitignore ├── .jshintrc ├── .travis.yml ├── 404.shtml ├── LICENSE ├── README.md ├── app.js ├── config.json ├── custom-rss.sublime-project ├── node_modules ├── connect │ ├── index.js │ └── package.json ├── debug │ ├── browser.js │ ├── debug.js │ ├── node.js │ └── package.json ├── ee-first │ ├── index.js │ └── package.json ├── escape-html │ ├── index.js │ └── package.json ├── finalhandler │ ├── index.js │ └── package.json ├── ms │ ├── index.js │ └── package.json ├── on-finished │ ├── index.js │ └── package.json ├── parseurl │ ├── index.js │ └── package.json ├── unpipe │ ├── index.js │ └── package.json └── utils-merge │ ├── index.js │ └── package.json ├── package.json ├── src ├── entry-logger.js ├── feeds │ ├── gama-sutra.js │ ├── hacker-news.js │ ├── index.js │ ├── nyt-business.js │ ├── quartz.js │ └── ray-wenderlich.js ├── fetch-feed.js ├── filter-feed.js ├── repost-guard.js ├── util.js └── xml-transformer.js ├── tests ├── entry-logger-tests.js ├── filter-feed-tests.js ├── index.js ├── lib │ └── runner.js ├── repost-guard-tests.js ├── tmp │ └── .gitkeep ├── util-tests.js └── xml-transformer-tests.js └── tmp └── .gitkeep /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | # node_modules # For deployment 28 | node_modules/.bin 29 | node_modules/supervisor 30 | node_modules/**/.npmignore 31 | node_modules/**/.*rc 32 | node_modules/**/*.md 33 | node_modules/**/*.yml 34 | node_modules/**/bower.json 35 | node_modules/**/component.json 36 | node_modules/**/LICENSE* 37 | node_modules/**/*file 38 | 39 | # Optional npm cache directory 40 | .npm 41 | 42 | # Optional REPL history 43 | .node_repl_history 44 | 45 | # Additions 46 | *.shtml 47 | *.sublime-workspace 48 | .htaccess 49 | robots.txt 50 | tmp/* 51 | 52 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esversion": 5 3 | } 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | -------------------------------------------------------------------------------- /404.shtml: -------------------------------------------------------------------------------- 1 |
9 |
14 |  __         __       ___
15 | /\ \       /\ \     / __\
16 | \ \ \___   \ \ \   /\ \_/_
17 |  \ \  __`\  \ \ \  \ \  __\
18 |   \ \ \ \ \  \ \ \  \ \ \_/
19 |    \ \_\ \_\  \ \_\  \ \_\
20 |     \/_/ /_/   \/_/   \/_/
21 | 
22 |
ERROR: 404: NOT FOUND
23 |
-------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT NON-AI License 2 | 3 | Copyright (c) 2016 Peng Wang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions. 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | In addition, the following restrictions apply: 16 | 17 | 1. The Software and any modifications made to it may not be used for the 18 | purpose of training or improving machine learning algorithms, including but 19 | not limited to artificial intelligence, natural language processing, or 20 | data mining. This condition applies to any derivatives, modifications, or 21 | updates based on the Software code. Any usage of the Software in an AI- 22 | training dataset is considered a breach of this License. 23 | 24 | 2. The Software may not be included in any dataset used for training or 25 | improving machine learning algorithms, including but not limited to 26 | artificial intelligence, natural language processing, or data mining. 27 | 28 | 3. Any person or organization found to be in violation of these 29 | restrictions will be subject to legal action and may be held liable for 30 | any damages resulting from such use. 31 | 32 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 33 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 34 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 35 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 36 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 37 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 38 | THE SOFTWARE. 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Custom RSS 2 | 3 | [![Build Status](https://travis-ci.org/hlfcoding/custom-rss.svg?branch=master)](https://travis-ci.org/hlfcoding/custom-rss) 4 | [![Code Climate](https://codeclimate.com/github/hlfcoding/custom-rss/badges/gpa.svg)](https://codeclimate.com/github/hlfcoding/custom-rss) 5 | [![Package](https://img.shields.io/npm/v/custom-rss.svg?style=flat)](https://www.npmjs.com/package/custom-rss) 6 | [![License](https://img.shields.io/npm/l/custom-rss.svg?style=flat)](https://github.com/hlfcoding/custom-rss/blob/master/LICENSE) 7 | 8 | :satellite: Filtering RSS because Zapier is too expensive (and Pipes is gone). 9 | 10 | ### Features 11 | 12 | - [x] Blacklist filtering 13 | - [x] Repost guarding 14 | - [x] Skipped entry logging 15 | - [x] No database required 16 | - [x] Uses JSON for configuration 17 | - [x] Feeds as middleware functions 18 | - [x] As few dependencies as possible 19 | - [x] ES5, so compatible with older Node versions 20 | 21 | ### Usage 22 | 23 | The sample app is what's in the root directory. It's a barebones Connect app, with personal configuration in `config.json`. This is foremost for personal use, but what's in `/src` should be reusable with any Connect-like web framework. Or to use as-is: 24 | 25 | ```shell 26 | $ npm run develop 27 | $ subl config.json # continued below 28 | $ subl src/feeds/feedd.js # continued below 29 | $ git push origin master # git tag && git push origin && npm publish 30 | 31 | $ ssh 32 | $ cd # or `mkdir ; cd ` 33 | $ git pull origin master # or `git clone ` 34 | $ npm start # or touch tmp/restart.txt 35 | $ exit 36 | 37 | $ curl /feedd 38 | ``` 39 | 40 | ```js 41 | // config.json 42 | { 43 | "feeds": [ 44 | // ... 45 | { 46 | "name": "feedd", 47 | "filters": [ 48 | { "name": "tired-topics", "type": "blacklist", "tokens": [ "Foo", "Bar", "Baz" ] } 49 | ] 50 | } 51 | ] 52 | } 53 | 54 | // src/feeds/feedd.js 55 | var fetchFeed = require('../fetch-feed'); 56 | var filterFeed = require('../filter-feed'); 57 | var url = require('url'); 58 | 59 | module.exports = function(config, request, response) { 60 | config.originalURL = 'http://feedd.com/rss.xml'; 61 | config.url = url.format({ 62 | protocol: 'http', host: request.headers.host, pathname: config.name 63 | }); 64 | fetchFeed({ 65 | url: config.originalURL, 66 | onResponse: function(resFetch, data) { 67 | response.setHeader('Content-Type', resFetch.headers['content-type']); 68 | filterFeed({ 69 | config: config, 70 | data: data, 71 | onDone: function(data) { response.end(data); } 72 | }); 73 | }, 74 | onError: function(e) { response.end(e.message); } 75 | }); 76 | }; 77 | ``` 78 | 79 | ### Feeds 80 | 81 | - [x] Hacker News (via [hnapp](http://hnapp.com)) 82 | - [x] Quartz 83 | - [x] NYTimes Business 84 | - [x] Ray Wenderlich 85 | - [x] Gama Sutra 86 | 87 | --- 88 | 89 | ### Implementation 90 | 91 | Some shared hosting providers, including mine, refuse to have NPM installed on their system. So dependencies need to be few to none, unless they're small enough to version. No XML parser or writer is used; a much lighter hand-rolled transformer does basic regex parsing. No MySQL client is used; data is stored with limits in plain files and manipulated in buffers (memory). No logger or mailer is used for feedback; custom loggers are handrolled as needed, with utilities on top of `fs`. The test-runner is handrolled (only because not a lot is required). Connect is the only dependency (but not really, see usage). This core constraint also yields the opportunity to learn Node fundamentals. 92 | 93 | ![custom-rss](https://cloud.githubusercontent.com/assets/100884/13690283/08d7c958-e6e4-11e5-9a83-dacfb7dc7d2f.png) 94 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var app = require('connect')(); 4 | var createRepostGuard = require('./src/repost-guard'); 5 | var feeds = require('./config').feeds; 6 | var fs = require('fs'); 7 | var log = require('./src/util').log; 8 | var path = require('path'); 9 | 10 | // Keeping this forever in memory for now. 11 | createRepostGuard.shared = createRepostGuard({ 12 | directory: path.join(__dirname, 'tmp'), 13 | lineLimit: 5000, // ~350 links * 14 days 14 | // Number of most recent links discounted for being on current page. 15 | feedPageSize: 30, 16 | sync: false, 17 | onReady: function() { log('Links loaded.'); } 18 | }); 19 | createRepostGuard.shared.setUp(); 20 | 21 | feeds.forEach(function(feed) { 22 | var middleware = require(path.join(__dirname, 'src/feeds', feed.name)); 23 | app.use('/'+ feed.name, middleware.bind(null, feed)); 24 | }); 25 | 26 | app.use(function(request, response) { 27 | response.statusCode = 404; 28 | fs.readFile(path.join(__dirname, '404.shtml'), function(error, data) { 29 | if (error) { 30 | response.end('Feed not found!\n'); 31 | } else { 32 | response.setHeader('Content-Type', 'text/html'); 33 | response.end(data); 34 | } 35 | }); 36 | }); 37 | 38 | require('http').createServer(app).listen(3000); 39 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "feeds": [ 3 | { 4 | "name": "gama-sutra", 5 | 6 | "filters": [ 7 | { 8 | "name": "uninteresting-topics", 9 | "type": "blacklist", 10 | "tokens": [ 11 | "Get a job", "Sponsored", "Video: ", 12 | "Angry Birds", "Pokémon", "PS4", "Xbox" 13 | ] 14 | }, 15 | { 16 | "name": "tired-topics", 17 | "type": "blacklist", 18 | "tokens": [ 19 | "Oculus", "Vive" 20 | ] 21 | } 22 | ] 23 | }, 24 | { 25 | "name": "hacker-news", 26 | 27 | "hnappQuery": "score%3E3%20%7C%20comments%3E1%20-type%3Acomment%20-type%3Aask%20-type%3Ashow%20-type%3Ajob%20-host%3Aqz.com%20-host%3Ayahoo.com", 28 | 29 | "filters": [ 30 | { 31 | "name": "uninteresting-topics", 32 | "type": "blacklist", 33 | "tokens": [ 34 | "\\.NET", "BEM", "Clojure", "F#", "Lisp", "MatLab", "OCaml", 35 | "Perl", "R", "Scheme", "Emacs", "LaTeX", "Vim", "Vitamin D" 36 | ] 37 | }, 38 | { 39 | "name": "tired-topics", 40 | "type": "blacklist", 41 | "tokens": [ 42 | "Trump" 43 | ] 44 | }, 45 | { 46 | "name": "no-comments", 47 | "input": "text-content", 48 | "type": "black-pattern", 49 | "pattern": ", 0 comments$" 50 | }, 51 | { 52 | "name": "noisy", 53 | "type": "graylist", 54 | "tokens": [ 55 | "Amazon", "Apple", "Facebook", "Google", "Netflix", "SpaceX", 56 | "Tesla", "Bitcoin", "China", "Ethereum", "North Korea", "Russia" 57 | ] 58 | } 59 | ] 60 | }, 61 | { 62 | "name": "nyt-business", 63 | 64 | "filters": [ 65 | { 66 | "name": "uninteresting-topics", 67 | "type": "blacklist", 68 | "tokens": [ 69 | "Tech Tip", "The Week Ahead", "Farhad and Mike" 70 | ] 71 | }, 72 | { 73 | "name": "tired-topics", 74 | "type": "blacklist", 75 | "tokens": [ 76 | "Trump", "Bernie", "Hillary", "Cosby" 77 | ] 78 | } 79 | ] 80 | }, 81 | { 82 | "name": "quartz", 83 | 84 | "filters": [ 85 | { 86 | "name": "uninteresting-topics", 87 | "type": "blacklist", 88 | "tokens": [ 89 | "Jenner", "Indian?s?", "Modi", "Mallya", "Jaitley", "Rajinikanth", 90 | "Quartz Daily Brief", "Quartz Weekend Brief", "stories you might have missed", 91 | "Bollywood", "Mumbai", "Delhi", "African?s?", "Nigerian?s?", "Goop", 92 | "Scientology" 93 | ] 94 | }, 95 | { 96 | "name": "tired-topics", 97 | "type": "blacklist", 98 | "tokens": [ 99 | "Trump", "Bernie", "Hillary", "Clinton", "San Bernardino", 100 | "Brexit", "hoverboard", "marijuana", "Jeff Sessions", "Silicon Valley" 101 | ] 102 | }, 103 | { 104 | "name": "noisy", 105 | "type": "graylist", 106 | "tokens": [ 107 | "Amazon", "Apple", "Facebook", "Google", "Netflix", "SpaceX", 108 | "Tesla" 109 | ] 110 | } 111 | ] 112 | }, 113 | { 114 | "name": "ray-wenderlich", 115 | 116 | "filters": [ 117 | { 118 | "name": "uninteresting-topics", 119 | "type": "blacklist", 120 | "tokens": [ 121 | "Announcing", "Available", "Open Call For Applications", "Opportunity: ", 122 | "Podcast", "Sale", "SUBSCRIBER", "Video Tutorial" 123 | ] 124 | } 125 | ] 126 | } 127 | ] 128 | } 129 | -------------------------------------------------------------------------------- /custom-rss.sublime-project: -------------------------------------------------------------------------------- 1 | { 2 | "folders": 3 | [ 4 | { 5 | "path": ".", 6 | "file_exclude_patterns": [ "*.sublime-workspace" ], 7 | "folder_exclude_patterns": [ "node_modules" ] 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /node_modules/connect/index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * connect 3 | * Copyright(c) 2010 Sencha Inc. 4 | * Copyright(c) 2011 TJ Holowaychuk 5 | * Copyright(c) 2015 Douglas Christopher Wilson 6 | * MIT Licensed 7 | */ 8 | 9 | 'use strict'; 10 | 11 | /** 12 | * Module dependencies. 13 | * @private 14 | */ 15 | 16 | var debug = require('debug')('connect:dispatcher'); 17 | var EventEmitter = require('events').EventEmitter; 18 | var finalhandler = require('finalhandler'); 19 | var http = require('http'); 20 | var merge = require('utils-merge'); 21 | var parseUrl = require('parseurl'); 22 | 23 | /** 24 | * Module exports. 25 | * @public 26 | */ 27 | 28 | module.exports = createServer; 29 | 30 | /** 31 | * Module variables. 32 | * @private 33 | */ 34 | 35 | var env = process.env.NODE_ENV || 'development'; 36 | var proto = {}; 37 | 38 | /* istanbul ignore next */ 39 | var defer = typeof setImmediate === 'function' 40 | ? setImmediate 41 | : function(fn){ process.nextTick(fn.bind.apply(fn, arguments)) } 42 | 43 | /** 44 | * Create a new connect server. 45 | * 46 | * @return {function} 47 | * @public 48 | */ 49 | 50 | function createServer() { 51 | function app(req, res, next){ app.handle(req, res, next); } 52 | merge(app, proto); 53 | merge(app, EventEmitter.prototype); 54 | app.route = '/'; 55 | app.stack = []; 56 | return app; 57 | } 58 | 59 | /** 60 | * Utilize the given middleware `handle` to the given `route`, 61 | * defaulting to _/_. This "route" is the mount-point for the 62 | * middleware, when given a value other than _/_ the middleware 63 | * is only effective when that segment is present in the request's 64 | * pathname. 65 | * 66 | * For example if we were to mount a function at _/admin_, it would 67 | * be invoked on _/admin_, and _/admin/settings_, however it would 68 | * not be invoked for _/_, or _/posts_. 69 | * 70 | * @param {String|Function|Server} route, callback or server 71 | * @param {Function|Server} callback or server 72 | * @return {Server} for chaining 73 | * @public 74 | */ 75 | 76 | proto.use = function use(route, fn) { 77 | var handle = fn; 78 | var path = route; 79 | 80 | // default route to '/' 81 | if (typeof route !== 'string') { 82 | handle = route; 83 | path = '/'; 84 | } 85 | 86 | // wrap sub-apps 87 | if (typeof handle.handle === 'function') { 88 | var server = handle; 89 | server.route = path; 90 | handle = function (req, res, next) { 91 | server.handle(req, res, next); 92 | }; 93 | } 94 | 95 | // wrap vanilla http.Servers 96 | if (handle instanceof http.Server) { 97 | handle = handle.listeners('request')[0]; 98 | } 99 | 100 | // strip trailing slash 101 | if (path[path.length - 1] === '/') { 102 | path = path.slice(0, -1); 103 | } 104 | 105 | // add the middleware 106 | debug('use %s %s', path || '/', handle.name || 'anonymous'); 107 | this.stack.push({ route: path, handle: handle }); 108 | 109 | return this; 110 | }; 111 | 112 | /** 113 | * Handle server requests, punting them down 114 | * the middleware stack. 115 | * 116 | * @private 117 | */ 118 | 119 | proto.handle = function handle(req, res, out) { 120 | var index = 0; 121 | var protohost = getProtohost(req.url) || ''; 122 | var removed = ''; 123 | var slashAdded = false; 124 | var stack = this.stack; 125 | 126 | // final function handler 127 | var done = out || finalhandler(req, res, { 128 | env: env, 129 | onerror: logerror 130 | }); 131 | 132 | // store the original URL 133 | req.originalUrl = req.originalUrl || req.url; 134 | 135 | function next(err) { 136 | if (slashAdded) { 137 | req.url = req.url.substr(1); 138 | slashAdded = false; 139 | } 140 | 141 | if (removed.length !== 0) { 142 | req.url = protohost + removed + req.url.substr(protohost.length); 143 | removed = ''; 144 | } 145 | 146 | // next callback 147 | var layer = stack[index++]; 148 | 149 | // all done 150 | if (!layer) { 151 | defer(done, err); 152 | return; 153 | } 154 | 155 | // route data 156 | var path = parseUrl(req).pathname || '/'; 157 | var route = layer.route; 158 | 159 | // skip this layer if the route doesn't match 160 | if (path.toLowerCase().substr(0, route.length) !== route.toLowerCase()) { 161 | return next(err); 162 | } 163 | 164 | // skip if route match does not border "/", ".", or end 165 | var c = path[route.length]; 166 | if (c !== undefined && '/' !== c && '.' !== c) { 167 | return next(err); 168 | } 169 | 170 | // trim off the part of the url that matches the route 171 | if (route.length !== 0 && route !== '/') { 172 | removed = route; 173 | req.url = protohost + req.url.substr(protohost.length + removed.length); 174 | 175 | // ensure leading slash 176 | if (!protohost && req.url[0] !== '/') { 177 | req.url = '/' + req.url; 178 | slashAdded = true; 179 | } 180 | } 181 | 182 | // call the layer handle 183 | call(layer.handle, route, err, req, res, next); 184 | } 185 | 186 | next(); 187 | }; 188 | 189 | /** 190 | * Listen for connections. 191 | * 192 | * This method takes the same arguments 193 | * as node's `http.Server#listen()`. 194 | * 195 | * HTTP and HTTPS: 196 | * 197 | * If you run your application both as HTTP 198 | * and HTTPS you may wrap them individually, 199 | * since your Connect "server" is really just 200 | * a JavaScript `Function`. 201 | * 202 | * var connect = require('connect') 203 | * , http = require('http') 204 | * , https = require('https'); 205 | * 206 | * var app = connect(); 207 | * 208 | * http.createServer(app).listen(80); 209 | * https.createServer(options, app).listen(443); 210 | * 211 | * @return {http.Server} 212 | * @api public 213 | */ 214 | 215 | proto.listen = function listen() { 216 | var server = http.createServer(this); 217 | return server.listen.apply(server, arguments); 218 | }; 219 | 220 | /** 221 | * Invoke a route handle. 222 | * @private 223 | */ 224 | 225 | function call(handle, route, err, req, res, next) { 226 | var arity = handle.length; 227 | var error = err; 228 | var hasError = Boolean(err); 229 | 230 | debug('%s %s : %s', handle.name || '', route, req.originalUrl); 231 | 232 | try { 233 | if (hasError && arity === 4) { 234 | // error-handling middleware 235 | handle(err, req, res, next); 236 | return; 237 | } else if (!hasError && arity < 4) { 238 | // request-handling middleware 239 | handle(req, res, next); 240 | return; 241 | } 242 | } catch (e) { 243 | // replace the error 244 | error = e; 245 | } 246 | 247 | // continue 248 | next(error); 249 | } 250 | 251 | /** 252 | * Log error using console.error. 253 | * 254 | * @param {Error} err 255 | * @private 256 | */ 257 | 258 | function logerror(err) { 259 | if (env !== 'test') console.error(err.stack || err.toString()); 260 | } 261 | 262 | /** 263 | * Get get protocol + host for a URL. 264 | * 265 | * @param {string} url 266 | * @private 267 | */ 268 | 269 | function getProtohost(url) { 270 | if (url.length === 0 || url[0] === '/') { 271 | return undefined; 272 | } 273 | 274 | var searchIndex = url.indexOf('?'); 275 | var pathLength = searchIndex !== -1 276 | ? searchIndex 277 | : url.length; 278 | var fqdnIndex = url.substr(0, pathLength).indexOf('://'); 279 | 280 | return fqdnIndex !== -1 281 | ? url.substr(0, url.indexOf('/', 3 + fqdnIndex)) 282 | : undefined; 283 | } 284 | -------------------------------------------------------------------------------- /node_modules/connect/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_args": [ 3 | [ 4 | "connect@^3.4.1", 5 | "/Users/destrado/Projects/custom-rss" 6 | ] 7 | ], 8 | "_from": "connect@>=3.4.1 <4.0.0", 9 | "_id": "connect@3.4.1", 10 | "_inCache": true, 11 | "_installable": true, 12 | "_location": "/connect", 13 | "_nodeVersion": "4.2.3", 14 | "_npmUser": { 15 | "email": "doug@somethingdoug.com", 16 | "name": "dougwilson" 17 | }, 18 | "_npmVersion": "2.14.7", 19 | "_phantomChildren": {}, 20 | "_requested": { 21 | "name": "connect", 22 | "raw": "connect@^3.4.1", 23 | "rawSpec": "^3.4.1", 24 | "scope": null, 25 | "spec": ">=3.4.1 <4.0.0", 26 | "type": "range" 27 | }, 28 | "_requiredBy": [ 29 | "/" 30 | ], 31 | "_resolved": "https://registry.npmjs.org/connect/-/connect-3.4.1.tgz", 32 | "_shasum": "a21361d3f4099ef761cda6dc4a973bb1ebb0a34d", 33 | "_shrinkwrap": null, 34 | "_spec": "connect@^3.4.1", 35 | "_where": "/Users/destrado/Projects/custom-rss", 36 | "author": { 37 | "email": "tj@vision-media.ca", 38 | "name": "TJ Holowaychuk", 39 | "url": "http://tjholowaychuk.com" 40 | }, 41 | "bugs": { 42 | "url": "https://github.com/senchalabs/connect/issues" 43 | }, 44 | "contributors": [ 45 | { 46 | "email": "doug@somethingdoug.com", 47 | "name": "Douglas Christopher Wilson" 48 | }, 49 | { 50 | "email": "me@jongleberry.com", 51 | "name": "Jonathan Ong" 52 | }, 53 | { 54 | "email": "tim@creationix.com", 55 | "name": "Tim Caswell" 56 | } 57 | ], 58 | "dependencies": { 59 | "debug": "~2.2.0", 60 | "finalhandler": "0.4.1", 61 | "parseurl": "~1.3.1", 62 | "utils-merge": "1.0.0" 63 | }, 64 | "description": "High performance middleware framework", 65 | "devDependencies": { 66 | "istanbul": "0.4.2", 67 | "mocha": "2.3.4", 68 | "supertest": "1.1.0" 69 | }, 70 | "directories": {}, 71 | "dist": { 72 | "shasum": "a21361d3f4099ef761cda6dc4a973bb1ebb0a34d", 73 | "tarball": "http://registry.npmjs.org/connect/-/connect-3.4.1.tgz" 74 | }, 75 | "engines": { 76 | "node": ">= 0.10.0" 77 | }, 78 | "files": [ 79 | "LICENSE", 80 | "History.md", 81 | "Readme.md", 82 | "index.js" 83 | ], 84 | "gitHead": "5cc4b6aab3fd7458719ba61edee6ac149831dbbf", 85 | "homepage": "https://github.com/senchalabs/connect#readme", 86 | "keywords": [ 87 | "framework", 88 | "web", 89 | "middleware", 90 | "connect", 91 | "rack" 92 | ], 93 | "license": "MIT", 94 | "maintainers": [ 95 | { 96 | "email": "doug@somethingdoug.com", 97 | "name": "dougwilson" 98 | }, 99 | { 100 | "email": "jonathanrichardong@gmail.com", 101 | "name": "jongleberry" 102 | }, 103 | { 104 | "email": "tj@vision-media.ca", 105 | "name": "tjholowaychuk" 106 | } 107 | ], 108 | "name": "connect", 109 | "optionalDependencies": {}, 110 | "readme": "ERROR: No README data found!", 111 | "repository": { 112 | "type": "git", 113 | "url": "git+https://github.com/senchalabs/connect.git" 114 | }, 115 | "scripts": { 116 | "test": "mocha --require test/support/env --reporter spec --bail --check-leaks test/", 117 | "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --require test/support/env --reporter dot --check-leaks test/", 118 | "test-travis": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --require test/support/env --reporter spec --check-leaks test/" 119 | }, 120 | "version": "3.4.1" 121 | } 122 | -------------------------------------------------------------------------------- /node_modules/debug/browser.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * This is the web browser implementation of `debug()`. 4 | * 5 | * Expose `debug()` as the module. 6 | */ 7 | 8 | exports = module.exports = require('./debug'); 9 | exports.log = log; 10 | exports.formatArgs = formatArgs; 11 | exports.save = save; 12 | exports.load = load; 13 | exports.useColors = useColors; 14 | exports.storage = 'undefined' != typeof chrome 15 | && 'undefined' != typeof chrome.storage 16 | ? chrome.storage.local 17 | : localstorage(); 18 | 19 | /** 20 | * Colors. 21 | */ 22 | 23 | exports.colors = [ 24 | 'lightseagreen', 25 | 'forestgreen', 26 | 'goldenrod', 27 | 'dodgerblue', 28 | 'darkorchid', 29 | 'crimson' 30 | ]; 31 | 32 | /** 33 | * Currently only WebKit-based Web Inspectors, Firefox >= v31, 34 | * and the Firebug extension (any Firefox version) are known 35 | * to support "%c" CSS customizations. 36 | * 37 | * TODO: add a `localStorage` variable to explicitly enable/disable colors 38 | */ 39 | 40 | function useColors() { 41 | // is webkit? http://stackoverflow.com/a/16459606/376773 42 | return ('WebkitAppearance' in document.documentElement.style) || 43 | // is firebug? http://stackoverflow.com/a/398120/376773 44 | (window.console && (console.firebug || (console.exception && console.table))) || 45 | // is firefox >= v31? 46 | // https://developer.mozilla.org/en-US/docs/Tools/Web_Console#Styling_messages 47 | (navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/) && parseInt(RegExp.$1, 10) >= 31); 48 | } 49 | 50 | /** 51 | * Map %j to `JSON.stringify()`, since no Web Inspectors do that by default. 52 | */ 53 | 54 | exports.formatters.j = function(v) { 55 | return JSON.stringify(v); 56 | }; 57 | 58 | 59 | /** 60 | * Colorize log arguments if enabled. 61 | * 62 | * @api public 63 | */ 64 | 65 | function formatArgs() { 66 | var args = arguments; 67 | var useColors = this.useColors; 68 | 69 | args[0] = (useColors ? '%c' : '') 70 | + this.namespace 71 | + (useColors ? ' %c' : ' ') 72 | + args[0] 73 | + (useColors ? '%c ' : ' ') 74 | + '+' + exports.humanize(this.diff); 75 | 76 | if (!useColors) return args; 77 | 78 | var c = 'color: ' + this.color; 79 | args = [args[0], c, 'color: inherit'].concat(Array.prototype.slice.call(args, 1)); 80 | 81 | // the final "%c" is somewhat tricky, because there could be other 82 | // arguments passed either before or after the %c, so we need to 83 | // figure out the correct index to insert the CSS into 84 | var index = 0; 85 | var lastC = 0; 86 | args[0].replace(/%[a-z%]/g, function(match) { 87 | if ('%%' === match) return; 88 | index++; 89 | if ('%c' === match) { 90 | // we only are interested in the *last* %c 91 | // (the user may have provided their own) 92 | lastC = index; 93 | } 94 | }); 95 | 96 | args.splice(lastC, 0, c); 97 | return args; 98 | } 99 | 100 | /** 101 | * Invokes `console.log()` when available. 102 | * No-op when `console.log` is not a "function". 103 | * 104 | * @api public 105 | */ 106 | 107 | function log() { 108 | // this hackery is required for IE8/9, where 109 | // the `console.log` function doesn't have 'apply' 110 | return 'object' === typeof console 111 | && console.log 112 | && Function.prototype.apply.call(console.log, console, arguments); 113 | } 114 | 115 | /** 116 | * Save `namespaces`. 117 | * 118 | * @param {String} namespaces 119 | * @api private 120 | */ 121 | 122 | function save(namespaces) { 123 | try { 124 | if (null == namespaces) { 125 | exports.storage.removeItem('debug'); 126 | } else { 127 | exports.storage.debug = namespaces; 128 | } 129 | } catch(e) {} 130 | } 131 | 132 | /** 133 | * Load `namespaces`. 134 | * 135 | * @return {String} returns the previously persisted debug modes 136 | * @api private 137 | */ 138 | 139 | function load() { 140 | var r; 141 | try { 142 | r = exports.storage.debug; 143 | } catch(e) {} 144 | return r; 145 | } 146 | 147 | /** 148 | * Enable namespaces listed in `localStorage.debug` initially. 149 | */ 150 | 151 | exports.enable(load()); 152 | 153 | /** 154 | * Localstorage attempts to return the localstorage. 155 | * 156 | * This is necessary because safari throws 157 | * when a user disables cookies/localstorage 158 | * and you attempt to access it. 159 | * 160 | * @return {LocalStorage} 161 | * @api private 162 | */ 163 | 164 | function localstorage(){ 165 | try { 166 | return window.localStorage; 167 | } catch (e) {} 168 | } 169 | -------------------------------------------------------------------------------- /node_modules/debug/debug.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * This is the common logic for both the Node.js and web browser 4 | * implementations of `debug()`. 5 | * 6 | * Expose `debug()` as the module. 7 | */ 8 | 9 | exports = module.exports = debug; 10 | exports.coerce = coerce; 11 | exports.disable = disable; 12 | exports.enable = enable; 13 | exports.enabled = enabled; 14 | exports.humanize = require('ms'); 15 | 16 | /** 17 | * The currently active debug mode names, and names to skip. 18 | */ 19 | 20 | exports.names = []; 21 | exports.skips = []; 22 | 23 | /** 24 | * Map of special "%n" handling functions, for the debug "format" argument. 25 | * 26 | * Valid key names are a single, lowercased letter, i.e. "n". 27 | */ 28 | 29 | exports.formatters = {}; 30 | 31 | /** 32 | * Previously assigned color. 33 | */ 34 | 35 | var prevColor = 0; 36 | 37 | /** 38 | * Previous log timestamp. 39 | */ 40 | 41 | var prevTime; 42 | 43 | /** 44 | * Select a color. 45 | * 46 | * @return {Number} 47 | * @api private 48 | */ 49 | 50 | function selectColor() { 51 | return exports.colors[prevColor++ % exports.colors.length]; 52 | } 53 | 54 | /** 55 | * Create a debugger with the given `namespace`. 56 | * 57 | * @param {String} namespace 58 | * @return {Function} 59 | * @api public 60 | */ 61 | 62 | function debug(namespace) { 63 | 64 | // define the `disabled` version 65 | function disabled() { 66 | } 67 | disabled.enabled = false; 68 | 69 | // define the `enabled` version 70 | function enabled() { 71 | 72 | var self = enabled; 73 | 74 | // set `diff` timestamp 75 | var curr = +new Date(); 76 | var ms = curr - (prevTime || curr); 77 | self.diff = ms; 78 | self.prev = prevTime; 79 | self.curr = curr; 80 | prevTime = curr; 81 | 82 | // add the `color` if not set 83 | if (null == self.useColors) self.useColors = exports.useColors(); 84 | if (null == self.color && self.useColors) self.color = selectColor(); 85 | 86 | var args = Array.prototype.slice.call(arguments); 87 | 88 | args[0] = exports.coerce(args[0]); 89 | 90 | if ('string' !== typeof args[0]) { 91 | // anything else let's inspect with %o 92 | args = ['%o'].concat(args); 93 | } 94 | 95 | // apply any `formatters` transformations 96 | var index = 0; 97 | args[0] = args[0].replace(/%([a-z%])/g, function(match, format) { 98 | // if we encounter an escaped % then don't increase the array index 99 | if (match === '%%') return match; 100 | index++; 101 | var formatter = exports.formatters[format]; 102 | if ('function' === typeof formatter) { 103 | var val = args[index]; 104 | match = formatter.call(self, val); 105 | 106 | // now we need to remove `args[index]` since it's inlined in the `format` 107 | args.splice(index, 1); 108 | index--; 109 | } 110 | return match; 111 | }); 112 | 113 | if ('function' === typeof exports.formatArgs) { 114 | args = exports.formatArgs.apply(self, args); 115 | } 116 | var logFn = enabled.log || exports.log || console.log.bind(console); 117 | logFn.apply(self, args); 118 | } 119 | enabled.enabled = true; 120 | 121 | var fn = exports.enabled(namespace) ? enabled : disabled; 122 | 123 | fn.namespace = namespace; 124 | 125 | return fn; 126 | } 127 | 128 | /** 129 | * Enables a debug mode by namespaces. This can include modes 130 | * separated by a colon and wildcards. 131 | * 132 | * @param {String} namespaces 133 | * @api public 134 | */ 135 | 136 | function enable(namespaces) { 137 | exports.save(namespaces); 138 | 139 | var split = (namespaces || '').split(/[\s,]+/); 140 | var len = split.length; 141 | 142 | for (var i = 0; i < len; i++) { 143 | if (!split[i]) continue; // ignore empty strings 144 | namespaces = split[i].replace(/\*/g, '.*?'); 145 | if (namespaces[0] === '-') { 146 | exports.skips.push(new RegExp('^' + namespaces.substr(1) + '$')); 147 | } else { 148 | exports.names.push(new RegExp('^' + namespaces + '$')); 149 | } 150 | } 151 | } 152 | 153 | /** 154 | * Disable debug output. 155 | * 156 | * @api public 157 | */ 158 | 159 | function disable() { 160 | exports.enable(''); 161 | } 162 | 163 | /** 164 | * Returns true if the given mode name is enabled, false otherwise. 165 | * 166 | * @param {String} name 167 | * @return {Boolean} 168 | * @api public 169 | */ 170 | 171 | function enabled(name) { 172 | var i, len; 173 | for (i = 0, len = exports.skips.length; i < len; i++) { 174 | if (exports.skips[i].test(name)) { 175 | return false; 176 | } 177 | } 178 | for (i = 0, len = exports.names.length; i < len; i++) { 179 | if (exports.names[i].test(name)) { 180 | return true; 181 | } 182 | } 183 | return false; 184 | } 185 | 186 | /** 187 | * Coerce `val`. 188 | * 189 | * @param {Mixed} val 190 | * @return {Mixed} 191 | * @api private 192 | */ 193 | 194 | function coerce(val) { 195 | if (val instanceof Error) return val.stack || val.message; 196 | return val; 197 | } 198 | -------------------------------------------------------------------------------- /node_modules/debug/node.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var tty = require('tty'); 7 | var util = require('util'); 8 | 9 | /** 10 | * This is the Node.js implementation of `debug()`. 11 | * 12 | * Expose `debug()` as the module. 13 | */ 14 | 15 | exports = module.exports = require('./debug'); 16 | exports.log = log; 17 | exports.formatArgs = formatArgs; 18 | exports.save = save; 19 | exports.load = load; 20 | exports.useColors = useColors; 21 | 22 | /** 23 | * Colors. 24 | */ 25 | 26 | exports.colors = [6, 2, 3, 4, 5, 1]; 27 | 28 | /** 29 | * The file descriptor to write the `debug()` calls to. 30 | * Set the `DEBUG_FD` env variable to override with another value. i.e.: 31 | * 32 | * $ DEBUG_FD=3 node script.js 3>debug.log 33 | */ 34 | 35 | var fd = parseInt(process.env.DEBUG_FD, 10) || 2; 36 | var stream = 1 === fd ? process.stdout : 37 | 2 === fd ? process.stderr : 38 | createWritableStdioStream(fd); 39 | 40 | /** 41 | * Is stdout a TTY? Colored output is enabled when `true`. 42 | */ 43 | 44 | function useColors() { 45 | var debugColors = (process.env.DEBUG_COLORS || '').trim().toLowerCase(); 46 | if (0 === debugColors.length) { 47 | return tty.isatty(fd); 48 | } else { 49 | return '0' !== debugColors 50 | && 'no' !== debugColors 51 | && 'false' !== debugColors 52 | && 'disabled' !== debugColors; 53 | } 54 | } 55 | 56 | /** 57 | * Map %o to `util.inspect()`, since Node doesn't do that out of the box. 58 | */ 59 | 60 | var inspect = (4 === util.inspect.length ? 61 | // node <= 0.8.x 62 | function (v, colors) { 63 | return util.inspect(v, void 0, void 0, colors); 64 | } : 65 | // node > 0.8.x 66 | function (v, colors) { 67 | return util.inspect(v, { colors: colors }); 68 | } 69 | ); 70 | 71 | exports.formatters.o = function(v) { 72 | return inspect(v, this.useColors) 73 | .replace(/\s*\n\s*/g, ' '); 74 | }; 75 | 76 | /** 77 | * Adds ANSI color escape codes if enabled. 78 | * 79 | * @api public 80 | */ 81 | 82 | function formatArgs() { 83 | var args = arguments; 84 | var useColors = this.useColors; 85 | var name = this.namespace; 86 | 87 | if (useColors) { 88 | var c = this.color; 89 | 90 | args[0] = ' \u001b[3' + c + ';1m' + name + ' ' 91 | + '\u001b[0m' 92 | + args[0] + '\u001b[3' + c + 'm' 93 | + ' +' + exports.humanize(this.diff) + '\u001b[0m'; 94 | } else { 95 | args[0] = new Date().toUTCString() 96 | + ' ' + name + ' ' + args[0]; 97 | } 98 | return args; 99 | } 100 | 101 | /** 102 | * Invokes `console.error()` with the specified arguments. 103 | */ 104 | 105 | function log() { 106 | return stream.write(util.format.apply(this, arguments) + '\n'); 107 | } 108 | 109 | /** 110 | * Save `namespaces`. 111 | * 112 | * @param {String} namespaces 113 | * @api private 114 | */ 115 | 116 | function save(namespaces) { 117 | if (null == namespaces) { 118 | // If you set a process.env field to null or undefined, it gets cast to the 119 | // string 'null' or 'undefined'. Just delete instead. 120 | delete process.env.DEBUG; 121 | } else { 122 | process.env.DEBUG = namespaces; 123 | } 124 | } 125 | 126 | /** 127 | * Load `namespaces`. 128 | * 129 | * @return {String} returns the previously persisted debug modes 130 | * @api private 131 | */ 132 | 133 | function load() { 134 | return process.env.DEBUG; 135 | } 136 | 137 | /** 138 | * Copied from `node/src/node.js`. 139 | * 140 | * XXX: It's lame that node doesn't expose this API out-of-the-box. It also 141 | * relies on the undocumented `tty_wrap.guessHandleType()` which is also lame. 142 | */ 143 | 144 | function createWritableStdioStream (fd) { 145 | var stream; 146 | var tty_wrap = process.binding('tty_wrap'); 147 | 148 | // Note stream._type is used for test-module-load-list.js 149 | 150 | switch (tty_wrap.guessHandleType(fd)) { 151 | case 'TTY': 152 | stream = new tty.WriteStream(fd); 153 | stream._type = 'tty'; 154 | 155 | // Hack to have stream not keep the event loop alive. 156 | // See https://github.com/joyent/node/issues/1726 157 | if (stream._handle && stream._handle.unref) { 158 | stream._handle.unref(); 159 | } 160 | break; 161 | 162 | case 'FILE': 163 | var fs = require('fs'); 164 | stream = new fs.SyncWriteStream(fd, { autoClose: false }); 165 | stream._type = 'fs'; 166 | break; 167 | 168 | case 'PIPE': 169 | case 'TCP': 170 | var net = require('net'); 171 | stream = new net.Socket({ 172 | fd: fd, 173 | readable: false, 174 | writable: true 175 | }); 176 | 177 | // FIXME Should probably have an option in net.Socket to create a 178 | // stream from an existing fd which is writable only. But for now 179 | // we'll just add this hack and set the `readable` member to false. 180 | // Test: ./node test/fixtures/echo.js < /etc/passwd 181 | stream.readable = false; 182 | stream.read = null; 183 | stream._type = 'pipe'; 184 | 185 | // FIXME Hack to have stream not keep the event loop alive. 186 | // See https://github.com/joyent/node/issues/1726 187 | if (stream._handle && stream._handle.unref) { 188 | stream._handle.unref(); 189 | } 190 | break; 191 | 192 | default: 193 | // Probably an error on in uv_guess_handle() 194 | throw new Error('Implement me. Unknown stream file type!'); 195 | } 196 | 197 | // For supporting legacy API we put the FD here. 198 | stream.fd = fd; 199 | 200 | stream._isStdio = true; 201 | 202 | return stream; 203 | } 204 | 205 | /** 206 | * Enable namespaces listed in `process.env.DEBUG` initially. 207 | */ 208 | 209 | exports.enable(load()); 210 | -------------------------------------------------------------------------------- /node_modules/debug/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_args": [ 3 | [ 4 | "debug@~2.2.0", 5 | "/Users/destrado/Projects/custom-rss/node_modules/connect" 6 | ] 7 | ], 8 | "_from": "debug@>=2.2.0 <2.3.0", 9 | "_id": "debug@2.2.0", 10 | "_inCache": true, 11 | "_installable": true, 12 | "_location": "/debug", 13 | "_nodeVersion": "0.12.2", 14 | "_npmUser": { 15 | "email": "nathan@tootallnate.net", 16 | "name": "tootallnate" 17 | }, 18 | "_npmVersion": "2.7.4", 19 | "_phantomChildren": {}, 20 | "_requested": { 21 | "name": "debug", 22 | "raw": "debug@~2.2.0", 23 | "rawSpec": "~2.2.0", 24 | "scope": null, 25 | "spec": ">=2.2.0 <2.3.0", 26 | "type": "range" 27 | }, 28 | "_requiredBy": [ 29 | "/connect", 30 | "/finalhandler" 31 | ], 32 | "_resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", 33 | "_shasum": "f87057e995b1a1f6ae6a4960664137bc56f039da", 34 | "_shrinkwrap": null, 35 | "_spec": "debug@~2.2.0", 36 | "_where": "/Users/destrado/Projects/custom-rss/node_modules/connect", 37 | "author": { 38 | "email": "tj@vision-media.ca", 39 | "name": "TJ Holowaychuk" 40 | }, 41 | "browser": "./browser.js", 42 | "bugs": { 43 | "url": "https://github.com/visionmedia/debug/issues" 44 | }, 45 | "component": { 46 | "scripts": { 47 | "debug/debug.js": "debug.js", 48 | "debug/index.js": "browser.js" 49 | } 50 | }, 51 | "contributors": [ 52 | { 53 | "email": "nathan@tootallnate.net", 54 | "name": "Nathan Rajlich", 55 | "url": "http://n8.io" 56 | } 57 | ], 58 | "dependencies": { 59 | "ms": "0.7.1" 60 | }, 61 | "description": "small debugging utility", 62 | "devDependencies": { 63 | "browserify": "9.0.3", 64 | "mocha": "*" 65 | }, 66 | "directories": {}, 67 | "dist": { 68 | "shasum": "f87057e995b1a1f6ae6a4960664137bc56f039da", 69 | "tarball": "http://registry.npmjs.org/debug/-/debug-2.2.0.tgz" 70 | }, 71 | "gitHead": "b38458422b5aa8aa6d286b10dfe427e8a67e2b35", 72 | "homepage": "https://github.com/visionmedia/debug", 73 | "keywords": [ 74 | "debug", 75 | "log", 76 | "debugger" 77 | ], 78 | "license": "MIT", 79 | "main": "./node.js", 80 | "maintainers": [ 81 | { 82 | "email": "tj@vision-media.ca", 83 | "name": "tjholowaychuk" 84 | }, 85 | { 86 | "email": "nathan@tootallnate.net", 87 | "name": "tootallnate" 88 | } 89 | ], 90 | "name": "debug", 91 | "optionalDependencies": {}, 92 | "readme": "ERROR: No README data found!", 93 | "repository": { 94 | "type": "git", 95 | "url": "git://github.com/visionmedia/debug.git" 96 | }, 97 | "scripts": {}, 98 | "version": "2.2.0" 99 | } 100 | -------------------------------------------------------------------------------- /node_modules/ee-first/index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * ee-first 3 | * Copyright(c) 2014 Jonathan Ong 4 | * MIT Licensed 5 | */ 6 | 7 | 'use strict' 8 | 9 | /** 10 | * Module exports. 11 | * @public 12 | */ 13 | 14 | module.exports = first 15 | 16 | /** 17 | * Get the first event in a set of event emitters and event pairs. 18 | * 19 | * @param {array} stuff 20 | * @param {function} done 21 | * @public 22 | */ 23 | 24 | function first(stuff, done) { 25 | if (!Array.isArray(stuff)) 26 | throw new TypeError('arg must be an array of [ee, events...] arrays') 27 | 28 | var cleanups = [] 29 | 30 | for (var i = 0; i < stuff.length; i++) { 31 | var arr = stuff[i] 32 | 33 | if (!Array.isArray(arr) || arr.length < 2) 34 | throw new TypeError('each array member must be [ee, events...]') 35 | 36 | var ee = arr[0] 37 | 38 | for (var j = 1; j < arr.length; j++) { 39 | var event = arr[j] 40 | var fn = listener(event, callback) 41 | 42 | // listen to the event 43 | ee.on(event, fn) 44 | // push this listener to the list of cleanups 45 | cleanups.push({ 46 | ee: ee, 47 | event: event, 48 | fn: fn, 49 | }) 50 | } 51 | } 52 | 53 | function callback() { 54 | cleanup() 55 | done.apply(null, arguments) 56 | } 57 | 58 | function cleanup() { 59 | var x 60 | for (var i = 0; i < cleanups.length; i++) { 61 | x = cleanups[i] 62 | x.ee.removeListener(x.event, x.fn) 63 | } 64 | } 65 | 66 | function thunk(fn) { 67 | done = fn 68 | } 69 | 70 | thunk.cancel = cleanup 71 | 72 | return thunk 73 | } 74 | 75 | /** 76 | * Create the event listener. 77 | * @private 78 | */ 79 | 80 | function listener(event, done) { 81 | return function onevent(arg1) { 82 | var args = new Array(arguments.length) 83 | var ee = this 84 | var err = event === 'error' 85 | ? arg1 86 | : null 87 | 88 | // copy args to prevent arguments escaping scope 89 | for (var i = 0; i < args.length; i++) { 90 | args[i] = arguments[i] 91 | } 92 | 93 | done(err, ee, event, args) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /node_modules/ee-first/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_args": [ 3 | [ 4 | "ee-first@1.1.1", 5 | "/Users/destrado/Projects/custom-rss/node_modules/on-finished" 6 | ] 7 | ], 8 | "_from": "ee-first@1.1.1", 9 | "_id": "ee-first@1.1.1", 10 | "_inCache": true, 11 | "_installable": true, 12 | "_location": "/ee-first", 13 | "_npmUser": { 14 | "email": "doug@somethingdoug.com", 15 | "name": "dougwilson" 16 | }, 17 | "_npmVersion": "1.4.28", 18 | "_phantomChildren": {}, 19 | "_requested": { 20 | "name": "ee-first", 21 | "raw": "ee-first@1.1.1", 22 | "rawSpec": "1.1.1", 23 | "scope": null, 24 | "spec": "1.1.1", 25 | "type": "version" 26 | }, 27 | "_requiredBy": [ 28 | "/on-finished" 29 | ], 30 | "_resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 31 | "_shasum": "590c61156b0ae2f4f0255732a158b266bc56b21d", 32 | "_shrinkwrap": null, 33 | "_spec": "ee-first@1.1.1", 34 | "_where": "/Users/destrado/Projects/custom-rss/node_modules/on-finished", 35 | "author": { 36 | "email": "me@jongleberry.com", 37 | "name": "Jonathan Ong", 38 | "url": "http://jongleberry.com" 39 | }, 40 | "bugs": { 41 | "url": "https://github.com/jonathanong/ee-first/issues" 42 | }, 43 | "contributors": [ 44 | { 45 | "email": "doug@somethingdoug.com", 46 | "name": "Douglas Christopher Wilson" 47 | } 48 | ], 49 | "dependencies": {}, 50 | "description": "return the first event in a set of ee/event pairs", 51 | "devDependencies": { 52 | "istanbul": "0.3.9", 53 | "mocha": "2.2.5" 54 | }, 55 | "directories": {}, 56 | "dist": { 57 | "shasum": "590c61156b0ae2f4f0255732a158b266bc56b21d", 58 | "tarball": "http://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" 59 | }, 60 | "files": [ 61 | "index.js", 62 | "LICENSE" 63 | ], 64 | "gitHead": "512e0ce4cc3643f603708f965a97b61b1a9c0441", 65 | "homepage": "https://github.com/jonathanong/ee-first", 66 | "license": "MIT", 67 | "maintainers": [ 68 | { 69 | "email": "jonathanrichardong@gmail.com", 70 | "name": "jongleberry" 71 | }, 72 | { 73 | "email": "doug@somethingdoug.com", 74 | "name": "dougwilson" 75 | } 76 | ], 77 | "name": "ee-first", 78 | "optionalDependencies": {}, 79 | "readme": "ERROR: No README data found!", 80 | "repository": { 81 | "type": "git", 82 | "url": "git+https://github.com/jonathanong/ee-first.git" 83 | }, 84 | "scripts": { 85 | "test": "mocha --reporter spec --bail --check-leaks test/", 86 | "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --reporter dot --check-leaks test/", 87 | "test-travis": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --reporter spec --check-leaks test/" 88 | }, 89 | "version": "1.1.1" 90 | } 91 | -------------------------------------------------------------------------------- /node_modules/escape-html/index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * escape-html 3 | * Copyright(c) 2012-2013 TJ Holowaychuk 4 | * Copyright(c) 2015 Andreas Lubbe 5 | * Copyright(c) 2015 Tiancheng "Timothy" Gu 6 | * MIT Licensed 7 | */ 8 | 9 | 'use strict'; 10 | 11 | /** 12 | * Module variables. 13 | * @private 14 | */ 15 | 16 | var matchHtmlRegExp = /["'&<>]/; 17 | 18 | /** 19 | * Module exports. 20 | * @public 21 | */ 22 | 23 | module.exports = escapeHtml; 24 | 25 | /** 26 | * Escape special characters in the given string of html. 27 | * 28 | * @param {string} string The string to escape for inserting into HTML 29 | * @return {string} 30 | * @public 31 | */ 32 | 33 | function escapeHtml(string) { 34 | var str = '' + string; 35 | var match = matchHtmlRegExp.exec(str); 36 | 37 | if (!match) { 38 | return str; 39 | } 40 | 41 | var escape; 42 | var html = ''; 43 | var index = 0; 44 | var lastIndex = 0; 45 | 46 | for (index = match.index; index < str.length; index++) { 47 | switch (str.charCodeAt(index)) { 48 | case 34: // " 49 | escape = '"'; 50 | break; 51 | case 38: // & 52 | escape = '&'; 53 | break; 54 | case 39: // ' 55 | escape = '''; 56 | break; 57 | case 60: // < 58 | escape = '<'; 59 | break; 60 | case 62: // > 61 | escape = '>'; 62 | break; 63 | default: 64 | continue; 65 | } 66 | 67 | if (lastIndex !== index) { 68 | html += str.substring(lastIndex, index); 69 | } 70 | 71 | lastIndex = index + 1; 72 | html += escape; 73 | } 74 | 75 | return lastIndex !== index 76 | ? html + str.substring(lastIndex, index) 77 | : html; 78 | } 79 | -------------------------------------------------------------------------------- /node_modules/escape-html/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_args": [ 3 | [ 4 | "escape-html@~1.0.3", 5 | "/Users/destrado/Projects/custom-rss/node_modules/finalhandler" 6 | ] 7 | ], 8 | "_from": "escape-html@>=1.0.3 <1.1.0", 9 | "_id": "escape-html@1.0.3", 10 | "_inCache": true, 11 | "_installable": true, 12 | "_location": "/escape-html", 13 | "_npmUser": { 14 | "email": "doug@somethingdoug.com", 15 | "name": "dougwilson" 16 | }, 17 | "_npmVersion": "1.4.28", 18 | "_phantomChildren": {}, 19 | "_requested": { 20 | "name": "escape-html", 21 | "raw": "escape-html@~1.0.3", 22 | "rawSpec": "~1.0.3", 23 | "scope": null, 24 | "spec": ">=1.0.3 <1.1.0", 25 | "type": "range" 26 | }, 27 | "_requiredBy": [ 28 | "/finalhandler" 29 | ], 30 | "_resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 31 | "_shasum": "0258eae4d3d0c0974de1c169188ef0051d1d1988", 32 | "_shrinkwrap": null, 33 | "_spec": "escape-html@~1.0.3", 34 | "_where": "/Users/destrado/Projects/custom-rss/node_modules/finalhandler", 35 | "bugs": { 36 | "url": "https://github.com/component/escape-html/issues" 37 | }, 38 | "dependencies": {}, 39 | "description": "Escape string for use in HTML", 40 | "devDependencies": { 41 | "beautify-benchmark": "0.2.4", 42 | "benchmark": "1.0.0" 43 | }, 44 | "directories": {}, 45 | "dist": { 46 | "shasum": "0258eae4d3d0c0974de1c169188ef0051d1d1988", 47 | "tarball": "http://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" 48 | }, 49 | "files": [ 50 | "LICENSE", 51 | "Readme.md", 52 | "index.js" 53 | ], 54 | "gitHead": "7ac2ea3977fcac3d4c5be8d2a037812820c65f28", 55 | "homepage": "https://github.com/component/escape-html", 56 | "keywords": [ 57 | "escape", 58 | "html", 59 | "utility" 60 | ], 61 | "license": "MIT", 62 | "maintainers": [ 63 | { 64 | "email": "tj@vision-media.ca", 65 | "name": "tjholowaychuk" 66 | }, 67 | { 68 | "email": "doug@somethingdoug.com", 69 | "name": "dougwilson" 70 | } 71 | ], 72 | "name": "escape-html", 73 | "optionalDependencies": {}, 74 | "readme": "ERROR: No README data found!", 75 | "repository": { 76 | "type": "git", 77 | "url": "git+https://github.com/component/escape-html.git" 78 | }, 79 | "scripts": { 80 | "bench": "node benchmark/index.js" 81 | }, 82 | "version": "1.0.3" 83 | } 84 | -------------------------------------------------------------------------------- /node_modules/finalhandler/index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * finalhandler 3 | * Copyright(c) 2014-2015 Douglas Christopher Wilson 4 | * MIT Licensed 5 | */ 6 | 7 | 'use strict' 8 | 9 | /** 10 | * Module dependencies. 11 | * @private 12 | */ 13 | 14 | var debug = require('debug')('finalhandler') 15 | var escapeHtml = require('escape-html') 16 | var http = require('http') 17 | var onFinished = require('on-finished') 18 | var unpipe = require('unpipe') 19 | 20 | /** 21 | * Module variables. 22 | * @private 23 | */ 24 | 25 | /* istanbul ignore next */ 26 | var defer = typeof setImmediate === 'function' 27 | ? setImmediate 28 | : function(fn){ process.nextTick(fn.bind.apply(fn, arguments)) } 29 | var isFinished = onFinished.isFinished 30 | 31 | /** 32 | * Module exports. 33 | * @public 34 | */ 35 | 36 | module.exports = finalhandler 37 | 38 | /** 39 | * Create a function to handle the final response. 40 | * 41 | * @param {Request} req 42 | * @param {Response} res 43 | * @param {Object} [options] 44 | * @return {Function} 45 | * @public 46 | */ 47 | 48 | function finalhandler(req, res, options) { 49 | var opts = options || {} 50 | 51 | // get environment 52 | var env = opts.env || process.env.NODE_ENV || 'development' 53 | 54 | // get error callback 55 | var onerror = opts.onerror 56 | 57 | return function (err) { 58 | var status = res.statusCode 59 | 60 | // ignore 404 on in-flight response 61 | if (!err && res._header) { 62 | debug('cannot 404 after headers sent') 63 | return 64 | } 65 | 66 | // unhandled error 67 | if (err) { 68 | // respect err.statusCode 69 | if (err.statusCode) { 70 | status = err.statusCode 71 | } 72 | 73 | // respect err.status 74 | if (err.status) { 75 | status = err.status 76 | } 77 | 78 | // default status code to 500 79 | if (!status || status < 400) { 80 | status = 500 81 | } 82 | 83 | // production gets a basic error message 84 | var msg = env === 'production' 85 | ? http.STATUS_CODES[status] 86 | : err.stack || err.toString() 87 | msg = escapeHtml(msg) 88 | .replace(/\n/g, '
') 89 | .replace(/ /g, '  ') + '\n' 90 | } else { 91 | status = 404 92 | msg = 'Cannot ' + escapeHtml(req.method) + ' ' + escapeHtml(req.originalUrl || req.url) + '\n' 93 | } 94 | 95 | debug('default %s', status) 96 | 97 | // schedule onerror callback 98 | if (err && onerror) { 99 | defer(onerror, err, req, res) 100 | } 101 | 102 | // cannot actually respond 103 | if (res._header) { 104 | return req.socket.destroy() 105 | } 106 | 107 | send(req, res, status, msg) 108 | } 109 | } 110 | 111 | /** 112 | * Send response. 113 | * 114 | * @param {IncomingMessage} req 115 | * @param {OutgoingMessage} res 116 | * @param {number} status 117 | * @param {string} body 118 | * @private 119 | */ 120 | 121 | function send(req, res, status, body) { 122 | function write() { 123 | res.statusCode = status 124 | 125 | // security header for content sniffing 126 | res.setHeader('X-Content-Type-Options', 'nosniff') 127 | 128 | // standard headers 129 | res.setHeader('Content-Type', 'text/html; charset=utf-8') 130 | res.setHeader('Content-Length', Buffer.byteLength(body, 'utf8')) 131 | 132 | if (req.method === 'HEAD') { 133 | res.end() 134 | return 135 | } 136 | 137 | res.end(body, 'utf8') 138 | } 139 | 140 | if (isFinished(req)) { 141 | write() 142 | return 143 | } 144 | 145 | // unpipe everything from the request 146 | unpipe(req) 147 | 148 | // flush the request 149 | onFinished(req, write) 150 | req.resume() 151 | } 152 | -------------------------------------------------------------------------------- /node_modules/finalhandler/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_args": [ 3 | [ 4 | "finalhandler@0.4.1", 5 | "/Users/destrado/Projects/custom-rss/node_modules/connect" 6 | ] 7 | ], 8 | "_from": "finalhandler@0.4.1", 9 | "_id": "finalhandler@0.4.1", 10 | "_inCache": true, 11 | "_installable": true, 12 | "_location": "/finalhandler", 13 | "_npmUser": { 14 | "email": "doug@somethingdoug.com", 15 | "name": "dougwilson" 16 | }, 17 | "_npmVersion": "1.4.28", 18 | "_phantomChildren": {}, 19 | "_requested": { 20 | "name": "finalhandler", 21 | "raw": "finalhandler@0.4.1", 22 | "rawSpec": "0.4.1", 23 | "scope": null, 24 | "spec": "0.4.1", 25 | "type": "version" 26 | }, 27 | "_requiredBy": [ 28 | "/connect" 29 | ], 30 | "_resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-0.4.1.tgz", 31 | "_shasum": "85a17c6c59a94717d262d61230d4b0ebe3d4a14d", 32 | "_shrinkwrap": null, 33 | "_spec": "finalhandler@0.4.1", 34 | "_where": "/Users/destrado/Projects/custom-rss/node_modules/connect", 35 | "author": { 36 | "email": "doug@somethingdoug.com", 37 | "name": "Douglas Christopher Wilson" 38 | }, 39 | "bugs": { 40 | "url": "https://github.com/pillarjs/finalhandler/issues" 41 | }, 42 | "dependencies": { 43 | "debug": "~2.2.0", 44 | "escape-html": "~1.0.3", 45 | "on-finished": "~2.3.0", 46 | "unpipe": "~1.0.0" 47 | }, 48 | "description": "Node.js final http responder", 49 | "devDependencies": { 50 | "istanbul": "0.4.1", 51 | "mocha": "2.3.4", 52 | "readable-stream": "2.0.4", 53 | "supertest": "1.1.0" 54 | }, 55 | "directories": {}, 56 | "dist": { 57 | "shasum": "85a17c6c59a94717d262d61230d4b0ebe3d4a14d", 58 | "tarball": "http://registry.npmjs.org/finalhandler/-/finalhandler-0.4.1.tgz" 59 | }, 60 | "engines": { 61 | "node": ">= 0.8" 62 | }, 63 | "files": [ 64 | "LICENSE", 65 | "HISTORY.md", 66 | "index.js" 67 | ], 68 | "gitHead": "ac2036774059eb93dbac8475580e52433204d4d4", 69 | "homepage": "https://github.com/pillarjs/finalhandler", 70 | "license": "MIT", 71 | "maintainers": [ 72 | { 73 | "email": "doug@somethingdoug.com", 74 | "name": "dougwilson" 75 | }, 76 | { 77 | "email": "jonathanrichardong@gmail.com", 78 | "name": "jongleberry" 79 | }, 80 | { 81 | "email": "tj@vision-media.ca", 82 | "name": "tjholowaychuk" 83 | }, 84 | { 85 | "email": "fishrock123@rocketmail.com", 86 | "name": "fishrock123" 87 | }, 88 | { 89 | "email": "shtylman@gmail.com", 90 | "name": "defunctzombie" 91 | } 92 | ], 93 | "name": "finalhandler", 94 | "optionalDependencies": {}, 95 | "readme": "ERROR: No README data found!", 96 | "repository": { 97 | "type": "git", 98 | "url": "git+https://github.com/pillarjs/finalhandler.git" 99 | }, 100 | "scripts": { 101 | "test": "mocha --reporter spec --bail --check-leaks test/", 102 | "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --reporter dot --check-leaks test/", 103 | "test-travis": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --reporter spec --check-leaks test/" 104 | }, 105 | "version": "0.4.1" 106 | } 107 | -------------------------------------------------------------------------------- /node_modules/ms/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Helpers. 3 | */ 4 | 5 | var s = 1000; 6 | var m = s * 60; 7 | var h = m * 60; 8 | var d = h * 24; 9 | var y = d * 365.25; 10 | 11 | /** 12 | * Parse or format the given `val`. 13 | * 14 | * Options: 15 | * 16 | * - `long` verbose formatting [false] 17 | * 18 | * @param {String|Number} val 19 | * @param {Object} options 20 | * @return {String|Number} 21 | * @api public 22 | */ 23 | 24 | module.exports = function(val, options){ 25 | options = options || {}; 26 | if ('string' == typeof val) return parse(val); 27 | return options.long 28 | ? long(val) 29 | : short(val); 30 | }; 31 | 32 | /** 33 | * Parse the given `str` and return milliseconds. 34 | * 35 | * @param {String} str 36 | * @return {Number} 37 | * @api private 38 | */ 39 | 40 | function parse(str) { 41 | str = '' + str; 42 | if (str.length > 10000) return; 43 | var match = /^((?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|years?|yrs?|y)?$/i.exec(str); 44 | if (!match) return; 45 | var n = parseFloat(match[1]); 46 | var type = (match[2] || 'ms').toLowerCase(); 47 | switch (type) { 48 | case 'years': 49 | case 'year': 50 | case 'yrs': 51 | case 'yr': 52 | case 'y': 53 | return n * y; 54 | case 'days': 55 | case 'day': 56 | case 'd': 57 | return n * d; 58 | case 'hours': 59 | case 'hour': 60 | case 'hrs': 61 | case 'hr': 62 | case 'h': 63 | return n * h; 64 | case 'minutes': 65 | case 'minute': 66 | case 'mins': 67 | case 'min': 68 | case 'm': 69 | return n * m; 70 | case 'seconds': 71 | case 'second': 72 | case 'secs': 73 | case 'sec': 74 | case 's': 75 | return n * s; 76 | case 'milliseconds': 77 | case 'millisecond': 78 | case 'msecs': 79 | case 'msec': 80 | case 'ms': 81 | return n; 82 | } 83 | } 84 | 85 | /** 86 | * Short format for `ms`. 87 | * 88 | * @param {Number} ms 89 | * @return {String} 90 | * @api private 91 | */ 92 | 93 | function short(ms) { 94 | if (ms >= d) return Math.round(ms / d) + 'd'; 95 | if (ms >= h) return Math.round(ms / h) + 'h'; 96 | if (ms >= m) return Math.round(ms / m) + 'm'; 97 | if (ms >= s) return Math.round(ms / s) + 's'; 98 | return ms + 'ms'; 99 | } 100 | 101 | /** 102 | * Long format for `ms`. 103 | * 104 | * @param {Number} ms 105 | * @return {String} 106 | * @api private 107 | */ 108 | 109 | function long(ms) { 110 | return plural(ms, d, 'day') 111 | || plural(ms, h, 'hour') 112 | || plural(ms, m, 'minute') 113 | || plural(ms, s, 'second') 114 | || ms + ' ms'; 115 | } 116 | 117 | /** 118 | * Pluralization helper. 119 | */ 120 | 121 | function plural(ms, n, name) { 122 | if (ms < n) return; 123 | if (ms < n * 1.5) return Math.floor(ms / n) + ' ' + name; 124 | return Math.ceil(ms / n) + ' ' + name + 's'; 125 | } 126 | -------------------------------------------------------------------------------- /node_modules/ms/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_args": [ 3 | [ 4 | "ms@0.7.1", 5 | "/Users/destrado/Projects/custom-rss/node_modules/debug" 6 | ] 7 | ], 8 | "_from": "ms@0.7.1", 9 | "_id": "ms@0.7.1", 10 | "_inCache": true, 11 | "_installable": true, 12 | "_location": "/ms", 13 | "_nodeVersion": "0.12.2", 14 | "_npmUser": { 15 | "email": "rauchg@gmail.com", 16 | "name": "rauchg" 17 | }, 18 | "_npmVersion": "2.7.5", 19 | "_phantomChildren": {}, 20 | "_requested": { 21 | "name": "ms", 22 | "raw": "ms@0.7.1", 23 | "rawSpec": "0.7.1", 24 | "scope": null, 25 | "spec": "0.7.1", 26 | "type": "version" 27 | }, 28 | "_requiredBy": [ 29 | "/debug" 30 | ], 31 | "_resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", 32 | "_shasum": "9cd13c03adbff25b65effde7ce864ee952017098", 33 | "_shrinkwrap": null, 34 | "_spec": "ms@0.7.1", 35 | "_where": "/Users/destrado/Projects/custom-rss/node_modules/debug", 36 | "bugs": { 37 | "url": "https://github.com/guille/ms.js/issues" 38 | }, 39 | "component": { 40 | "scripts": { 41 | "ms/index.js": "index.js" 42 | } 43 | }, 44 | "dependencies": {}, 45 | "description": "Tiny ms conversion utility", 46 | "devDependencies": { 47 | "expect.js": "*", 48 | "mocha": "*", 49 | "serve": "*" 50 | }, 51 | "directories": {}, 52 | "dist": { 53 | "shasum": "9cd13c03adbff25b65effde7ce864ee952017098", 54 | "tarball": "http://registry.npmjs.org/ms/-/ms-0.7.1.tgz" 55 | }, 56 | "gitHead": "713dcf26d9e6fd9dbc95affe7eff9783b7f1b909", 57 | "homepage": "https://github.com/guille/ms.js", 58 | "main": "./index", 59 | "maintainers": [ 60 | { 61 | "email": "rauchg@gmail.com", 62 | "name": "rauchg" 63 | } 64 | ], 65 | "name": "ms", 66 | "optionalDependencies": {}, 67 | "readme": "ERROR: No README data found!", 68 | "repository": { 69 | "type": "git", 70 | "url": "git://github.com/guille/ms.js.git" 71 | }, 72 | "scripts": {}, 73 | "version": "0.7.1" 74 | } 75 | -------------------------------------------------------------------------------- /node_modules/on-finished/index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * on-finished 3 | * Copyright(c) 2013 Jonathan Ong 4 | * Copyright(c) 2014 Douglas Christopher Wilson 5 | * MIT Licensed 6 | */ 7 | 8 | 'use strict' 9 | 10 | /** 11 | * Module exports. 12 | * @public 13 | */ 14 | 15 | module.exports = onFinished 16 | module.exports.isFinished = isFinished 17 | 18 | /** 19 | * Module dependencies. 20 | * @private 21 | */ 22 | 23 | var first = require('ee-first') 24 | 25 | /** 26 | * Variables. 27 | * @private 28 | */ 29 | 30 | /* istanbul ignore next */ 31 | var defer = typeof setImmediate === 'function' 32 | ? setImmediate 33 | : function(fn){ process.nextTick(fn.bind.apply(fn, arguments)) } 34 | 35 | /** 36 | * Invoke callback when the response has finished, useful for 37 | * cleaning up resources afterwards. 38 | * 39 | * @param {object} msg 40 | * @param {function} listener 41 | * @return {object} 42 | * @public 43 | */ 44 | 45 | function onFinished(msg, listener) { 46 | if (isFinished(msg) !== false) { 47 | defer(listener, null, msg) 48 | return msg 49 | } 50 | 51 | // attach the listener to the message 52 | attachListener(msg, listener) 53 | 54 | return msg 55 | } 56 | 57 | /** 58 | * Determine if message is already finished. 59 | * 60 | * @param {object} msg 61 | * @return {boolean} 62 | * @public 63 | */ 64 | 65 | function isFinished(msg) { 66 | var socket = msg.socket 67 | 68 | if (typeof msg.finished === 'boolean') { 69 | // OutgoingMessage 70 | return Boolean(msg.finished || (socket && !socket.writable)) 71 | } 72 | 73 | if (typeof msg.complete === 'boolean') { 74 | // IncomingMessage 75 | return Boolean(msg.upgrade || !socket || !socket.readable || (msg.complete && !msg.readable)) 76 | } 77 | 78 | // don't know 79 | return undefined 80 | } 81 | 82 | /** 83 | * Attach a finished listener to the message. 84 | * 85 | * @param {object} msg 86 | * @param {function} callback 87 | * @private 88 | */ 89 | 90 | function attachFinishedListener(msg, callback) { 91 | var eeMsg 92 | var eeSocket 93 | var finished = false 94 | 95 | function onFinish(error) { 96 | eeMsg.cancel() 97 | eeSocket.cancel() 98 | 99 | finished = true 100 | callback(error) 101 | } 102 | 103 | // finished on first message event 104 | eeMsg = eeSocket = first([[msg, 'end', 'finish']], onFinish) 105 | 106 | function onSocket(socket) { 107 | // remove listener 108 | msg.removeListener('socket', onSocket) 109 | 110 | if (finished) return 111 | if (eeMsg !== eeSocket) return 112 | 113 | // finished on first socket event 114 | eeSocket = first([[socket, 'error', 'close']], onFinish) 115 | } 116 | 117 | if (msg.socket) { 118 | // socket already assigned 119 | onSocket(msg.socket) 120 | return 121 | } 122 | 123 | // wait for socket to be assigned 124 | msg.on('socket', onSocket) 125 | 126 | if (msg.socket === undefined) { 127 | // node.js 0.8 patch 128 | patchAssignSocket(msg, onSocket) 129 | } 130 | } 131 | 132 | /** 133 | * Attach the listener to the message. 134 | * 135 | * @param {object} msg 136 | * @return {function} 137 | * @private 138 | */ 139 | 140 | function attachListener(msg, listener) { 141 | var attached = msg.__onFinished 142 | 143 | // create a private single listener with queue 144 | if (!attached || !attached.queue) { 145 | attached = msg.__onFinished = createListener(msg) 146 | attachFinishedListener(msg, attached) 147 | } 148 | 149 | attached.queue.push(listener) 150 | } 151 | 152 | /** 153 | * Create listener on message. 154 | * 155 | * @param {object} msg 156 | * @return {function} 157 | * @private 158 | */ 159 | 160 | function createListener(msg) { 161 | function listener(err) { 162 | if (msg.__onFinished === listener) msg.__onFinished = null 163 | if (!listener.queue) return 164 | 165 | var queue = listener.queue 166 | listener.queue = null 167 | 168 | for (var i = 0; i < queue.length; i++) { 169 | queue[i](err, msg) 170 | } 171 | } 172 | 173 | listener.queue = [] 174 | 175 | return listener 176 | } 177 | 178 | /** 179 | * Patch ServerResponse.prototype.assignSocket for node.js 0.8. 180 | * 181 | * @param {ServerResponse} res 182 | * @param {function} callback 183 | * @private 184 | */ 185 | 186 | function patchAssignSocket(res, callback) { 187 | var assignSocket = res.assignSocket 188 | 189 | if (typeof assignSocket !== 'function') return 190 | 191 | // res.on('socket', callback) is broken in 0.8 192 | res.assignSocket = function _assignSocket(socket) { 193 | assignSocket.call(this, socket) 194 | callback(socket) 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /node_modules/on-finished/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_args": [ 3 | [ 4 | "on-finished@~2.3.0", 5 | "/Users/destrado/Projects/custom-rss/node_modules/finalhandler" 6 | ] 7 | ], 8 | "_from": "on-finished@>=2.3.0 <2.4.0", 9 | "_id": "on-finished@2.3.0", 10 | "_inCache": true, 11 | "_installable": true, 12 | "_location": "/on-finished", 13 | "_npmUser": { 14 | "email": "doug@somethingdoug.com", 15 | "name": "dougwilson" 16 | }, 17 | "_npmVersion": "1.4.28", 18 | "_phantomChildren": {}, 19 | "_requested": { 20 | "name": "on-finished", 21 | "raw": "on-finished@~2.3.0", 22 | "rawSpec": "~2.3.0", 23 | "scope": null, 24 | "spec": ">=2.3.0 <2.4.0", 25 | "type": "range" 26 | }, 27 | "_requiredBy": [ 28 | "/finalhandler" 29 | ], 30 | "_resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 31 | "_shasum": "20f1336481b083cd75337992a16971aa2d906947", 32 | "_shrinkwrap": null, 33 | "_spec": "on-finished@~2.3.0", 34 | "_where": "/Users/destrado/Projects/custom-rss/node_modules/finalhandler", 35 | "bugs": { 36 | "url": "https://github.com/jshttp/on-finished/issues" 37 | }, 38 | "contributors": [ 39 | { 40 | "email": "doug@somethingdoug.com", 41 | "name": "Douglas Christopher Wilson" 42 | }, 43 | { 44 | "email": "me@jongleberry.com", 45 | "name": "Jonathan Ong", 46 | "url": "http://jongleberry.com" 47 | } 48 | ], 49 | "dependencies": { 50 | "ee-first": "1.1.1" 51 | }, 52 | "description": "Execute a callback when a request closes, finishes, or errors", 53 | "devDependencies": { 54 | "istanbul": "0.3.9", 55 | "mocha": "2.2.5" 56 | }, 57 | "directories": {}, 58 | "dist": { 59 | "shasum": "20f1336481b083cd75337992a16971aa2d906947", 60 | "tarball": "http://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz" 61 | }, 62 | "engines": { 63 | "node": ">= 0.8" 64 | }, 65 | "files": [ 66 | "HISTORY.md", 67 | "LICENSE", 68 | "index.js" 69 | ], 70 | "gitHead": "34babcb58126a416fcf5205768204f2e12699dda", 71 | "homepage": "https://github.com/jshttp/on-finished", 72 | "license": "MIT", 73 | "maintainers": [ 74 | { 75 | "email": "doug@somethingdoug.com", 76 | "name": "dougwilson" 77 | }, 78 | { 79 | "email": "jonathanrichardong@gmail.com", 80 | "name": "jongleberry" 81 | } 82 | ], 83 | "name": "on-finished", 84 | "optionalDependencies": {}, 85 | "readme": "ERROR: No README data found!", 86 | "repository": { 87 | "type": "git", 88 | "url": "git+https://github.com/jshttp/on-finished.git" 89 | }, 90 | "scripts": { 91 | "test": "mocha --reporter spec --bail --check-leaks test/", 92 | "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --reporter dot --check-leaks test/", 93 | "test-travis": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --reporter spec --check-leaks test/" 94 | }, 95 | "version": "2.3.0" 96 | } 97 | -------------------------------------------------------------------------------- /node_modules/parseurl/index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * parseurl 3 | * Copyright(c) 2014 Jonathan Ong 4 | * Copyright(c) 2014 Douglas Christopher Wilson 5 | * MIT Licensed 6 | */ 7 | 8 | 'use strict' 9 | 10 | /** 11 | * Module dependencies. 12 | */ 13 | 14 | var url = require('url') 15 | var parse = url.parse 16 | var Url = url.Url 17 | 18 | /** 19 | * Pattern for a simple path case. 20 | * See: https://github.com/joyent/node/pull/7878 21 | */ 22 | 23 | var simplePathRegExp = /^(\/\/?(?!\/)[^\?#\s]*)(\?[^#\s]*)?$/ 24 | 25 | /** 26 | * Exports. 27 | */ 28 | 29 | module.exports = parseurl 30 | module.exports.original = originalurl 31 | 32 | /** 33 | * Parse the `req` url with memoization. 34 | * 35 | * @param {ServerRequest} req 36 | * @return {Object} 37 | * @api public 38 | */ 39 | 40 | function parseurl(req) { 41 | var url = req.url 42 | 43 | if (url === undefined) { 44 | // URL is undefined 45 | return undefined 46 | } 47 | 48 | var parsed = req._parsedUrl 49 | 50 | if (fresh(url, parsed)) { 51 | // Return cached URL parse 52 | return parsed 53 | } 54 | 55 | // Parse the URL 56 | parsed = fastparse(url) 57 | parsed._raw = url 58 | 59 | return req._parsedUrl = parsed 60 | }; 61 | 62 | /** 63 | * Parse the `req` original url with fallback and memoization. 64 | * 65 | * @param {ServerRequest} req 66 | * @return {Object} 67 | * @api public 68 | */ 69 | 70 | function originalurl(req) { 71 | var url = req.originalUrl 72 | 73 | if (typeof url !== 'string') { 74 | // Fallback 75 | return parseurl(req) 76 | } 77 | 78 | var parsed = req._parsedOriginalUrl 79 | 80 | if (fresh(url, parsed)) { 81 | // Return cached URL parse 82 | return parsed 83 | } 84 | 85 | // Parse the URL 86 | parsed = fastparse(url) 87 | parsed._raw = url 88 | 89 | return req._parsedOriginalUrl = parsed 90 | }; 91 | 92 | /** 93 | * Parse the `str` url with fast-path short-cut. 94 | * 95 | * @param {string} str 96 | * @return {Object} 97 | * @api private 98 | */ 99 | 100 | function fastparse(str) { 101 | // Try fast path regexp 102 | // See: https://github.com/joyent/node/pull/7878 103 | var simplePath = typeof str === 'string' && simplePathRegExp.exec(str) 104 | 105 | // Construct simple URL 106 | if (simplePath) { 107 | var pathname = simplePath[1] 108 | var search = simplePath[2] || null 109 | var url = Url !== undefined 110 | ? new Url() 111 | : {} 112 | url.path = str 113 | url.href = str 114 | url.pathname = pathname 115 | url.search = search 116 | url.query = search && search.substr(1) 117 | 118 | return url 119 | } 120 | 121 | return parse(str) 122 | } 123 | 124 | /** 125 | * Determine if parsed is still fresh for url. 126 | * 127 | * @param {string} url 128 | * @param {object} parsedUrl 129 | * @return {boolean} 130 | * @api private 131 | */ 132 | 133 | function fresh(url, parsedUrl) { 134 | return typeof parsedUrl === 'object' 135 | && parsedUrl !== null 136 | && (Url === undefined || parsedUrl instanceof Url) 137 | && parsedUrl._raw === url 138 | } 139 | -------------------------------------------------------------------------------- /node_modules/parseurl/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_args": [ 3 | [ 4 | "parseurl@~1.3.1", 5 | "/Users/destrado/Projects/custom-rss/node_modules/connect" 6 | ] 7 | ], 8 | "_from": "parseurl@>=1.3.1 <1.4.0", 9 | "_id": "parseurl@1.3.1", 10 | "_inCache": true, 11 | "_installable": true, 12 | "_location": "/parseurl", 13 | "_npmUser": { 14 | "email": "doug@somethingdoug.com", 15 | "name": "dougwilson" 16 | }, 17 | "_npmVersion": "1.4.28", 18 | "_phantomChildren": {}, 19 | "_requested": { 20 | "name": "parseurl", 21 | "raw": "parseurl@~1.3.1", 22 | "rawSpec": "~1.3.1", 23 | "scope": null, 24 | "spec": ">=1.3.1 <1.4.0", 25 | "type": "range" 26 | }, 27 | "_requiredBy": [ 28 | "/connect" 29 | ], 30 | "_resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz", 31 | "_shasum": "c8ab8c9223ba34888aa64a297b28853bec18da56", 32 | "_shrinkwrap": null, 33 | "_spec": "parseurl@~1.3.1", 34 | "_where": "/Users/destrado/Projects/custom-rss/node_modules/connect", 35 | "author": { 36 | "email": "me@jongleberry.com", 37 | "name": "Jonathan Ong", 38 | "url": "http://jongleberry.com" 39 | }, 40 | "bugs": { 41 | "url": "https://github.com/pillarjs/parseurl/issues" 42 | }, 43 | "contributors": [ 44 | { 45 | "email": "doug@somethingdoug.com", 46 | "name": "Douglas Christopher Wilson" 47 | } 48 | ], 49 | "dependencies": {}, 50 | "description": "parse a url with memoization", 51 | "devDependencies": { 52 | "beautify-benchmark": "0.2.4", 53 | "benchmark": "2.0.0", 54 | "fast-url-parser": "1.1.3", 55 | "istanbul": "0.4.2", 56 | "mocha": "~1.21.5" 57 | }, 58 | "directories": {}, 59 | "dist": { 60 | "shasum": "c8ab8c9223ba34888aa64a297b28853bec18da56", 61 | "tarball": "http://registry.npmjs.org/parseurl/-/parseurl-1.3.1.tgz" 62 | }, 63 | "engines": { 64 | "node": ">= 0.8" 65 | }, 66 | "files": [ 67 | "LICENSE", 68 | "HISTORY.md", 69 | "README.md", 70 | "index.js" 71 | ], 72 | "gitHead": "6d22d376d75b927ab2b5347ce3a1d6735133dd43", 73 | "homepage": "https://github.com/pillarjs/parseurl", 74 | "license": "MIT", 75 | "maintainers": [ 76 | { 77 | "email": "jonathanrichardong@gmail.com", 78 | "name": "jongleberry" 79 | }, 80 | { 81 | "email": "doug@somethingdoug.com", 82 | "name": "dougwilson" 83 | }, 84 | { 85 | "email": "tj@vision-media.ca", 86 | "name": "tjholowaychuk" 87 | }, 88 | { 89 | "email": "mscdex@mscdex.net", 90 | "name": "mscdex" 91 | }, 92 | { 93 | "email": "fishrock123@rocketmail.com", 94 | "name": "fishrock123" 95 | }, 96 | { 97 | "email": "shtylman@gmail.com", 98 | "name": "defunctzombie" 99 | } 100 | ], 101 | "name": "parseurl", 102 | "optionalDependencies": {}, 103 | "readme": "ERROR: No README data found!", 104 | "repository": { 105 | "type": "git", 106 | "url": "git+https://github.com/pillarjs/parseurl.git" 107 | }, 108 | "scripts": { 109 | "bench": "node benchmark/index.js", 110 | "test": "mocha --check-leaks --bail --reporter spec test/", 111 | "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --check-leaks --reporter dot test/", 112 | "test-travis": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --check-leaks --reporter spec test/" 113 | }, 114 | "version": "1.3.1" 115 | } 116 | -------------------------------------------------------------------------------- /node_modules/unpipe/index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * unpipe 3 | * Copyright(c) 2015 Douglas Christopher Wilson 4 | * MIT Licensed 5 | */ 6 | 7 | 'use strict' 8 | 9 | /** 10 | * Module exports. 11 | * @public 12 | */ 13 | 14 | module.exports = unpipe 15 | 16 | /** 17 | * Determine if there are Node.js pipe-like data listeners. 18 | * @private 19 | */ 20 | 21 | function hasPipeDataListeners(stream) { 22 | var listeners = stream.listeners('data') 23 | 24 | for (var i = 0; i < listeners.length; i++) { 25 | if (listeners[i].name === 'ondata') { 26 | return true 27 | } 28 | } 29 | 30 | return false 31 | } 32 | 33 | /** 34 | * Unpipe a stream from all destinations. 35 | * 36 | * @param {object} stream 37 | * @public 38 | */ 39 | 40 | function unpipe(stream) { 41 | if (!stream) { 42 | throw new TypeError('argument stream is required') 43 | } 44 | 45 | if (typeof stream.unpipe === 'function') { 46 | // new-style 47 | stream.unpipe() 48 | return 49 | } 50 | 51 | // Node.js 0.8 hack 52 | if (!hasPipeDataListeners(stream)) { 53 | return 54 | } 55 | 56 | var listener 57 | var listeners = stream.listeners('close') 58 | 59 | for (var i = 0; i < listeners.length; i++) { 60 | listener = listeners[i] 61 | 62 | if (listener.name !== 'cleanup' && listener.name !== 'onclose') { 63 | continue 64 | } 65 | 66 | // invoke the listener 67 | listener.call(stream) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /node_modules/unpipe/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_args": [ 3 | [ 4 | "unpipe@~1.0.0", 5 | "/Users/destrado/Projects/custom-rss/node_modules/finalhandler" 6 | ] 7 | ], 8 | "_from": "unpipe@>=1.0.0 <1.1.0", 9 | "_id": "unpipe@1.0.0", 10 | "_inCache": true, 11 | "_installable": true, 12 | "_location": "/unpipe", 13 | "_npmUser": { 14 | "email": "doug@somethingdoug.com", 15 | "name": "dougwilson" 16 | }, 17 | "_npmVersion": "1.4.28", 18 | "_phantomChildren": {}, 19 | "_requested": { 20 | "name": "unpipe", 21 | "raw": "unpipe@~1.0.0", 22 | "rawSpec": "~1.0.0", 23 | "scope": null, 24 | "spec": ">=1.0.0 <1.1.0", 25 | "type": "range" 26 | }, 27 | "_requiredBy": [ 28 | "/finalhandler" 29 | ], 30 | "_resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 31 | "_shasum": "b2bf4ee8514aae6165b4817829d21b2ef49904ec", 32 | "_shrinkwrap": null, 33 | "_spec": "unpipe@~1.0.0", 34 | "_where": "/Users/destrado/Projects/custom-rss/node_modules/finalhandler", 35 | "author": { 36 | "email": "doug@somethingdoug.com", 37 | "name": "Douglas Christopher Wilson" 38 | }, 39 | "bugs": { 40 | "url": "https://github.com/stream-utils/unpipe/issues" 41 | }, 42 | "dependencies": {}, 43 | "description": "Unpipe a stream from all destinations", 44 | "devDependencies": { 45 | "istanbul": "0.3.15", 46 | "mocha": "2.2.5", 47 | "readable-stream": "1.1.13" 48 | }, 49 | "directories": {}, 50 | "dist": { 51 | "shasum": "b2bf4ee8514aae6165b4817829d21b2ef49904ec", 52 | "tarball": "http://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" 53 | }, 54 | "engines": { 55 | "node": ">= 0.8" 56 | }, 57 | "files": [ 58 | "HISTORY.md", 59 | "LICENSE", 60 | "README.md", 61 | "index.js" 62 | ], 63 | "gitHead": "d2df901c06487430e78dca62b6edb8bb2fc5e99d", 64 | "homepage": "https://github.com/stream-utils/unpipe", 65 | "license": "MIT", 66 | "maintainers": [ 67 | { 68 | "email": "doug@somethingdoug.com", 69 | "name": "dougwilson" 70 | } 71 | ], 72 | "name": "unpipe", 73 | "optionalDependencies": {}, 74 | "readme": "ERROR: No README data found!", 75 | "repository": { 76 | "type": "git", 77 | "url": "git+https://github.com/stream-utils/unpipe.git" 78 | }, 79 | "scripts": { 80 | "test": "mocha --reporter spec --bail --check-leaks test/", 81 | "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --reporter dot --check-leaks test/", 82 | "test-travis": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --reporter spec --check-leaks test/" 83 | }, 84 | "version": "1.0.0" 85 | } 86 | -------------------------------------------------------------------------------- /node_modules/utils-merge/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Merge object b with object a. 3 | * 4 | * var a = { foo: 'bar' } 5 | * , b = { bar: 'baz' }; 6 | * 7 | * merge(a, b); 8 | * // => { foo: 'bar', bar: 'baz' } 9 | * 10 | * @param {Object} a 11 | * @param {Object} b 12 | * @return {Object} 13 | * @api public 14 | */ 15 | 16 | exports = module.exports = function(a, b){ 17 | if (a && b) { 18 | for (var key in b) { 19 | a[key] = b[key]; 20 | } 21 | } 22 | return a; 23 | }; 24 | -------------------------------------------------------------------------------- /node_modules/utils-merge/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_args": [ 3 | [ 4 | "utils-merge@1.0.0", 5 | "/Users/destrado/Projects/custom-rss/node_modules/connect" 6 | ] 7 | ], 8 | "_from": "utils-merge@1.0.0", 9 | "_id": "utils-merge@1.0.0", 10 | "_inCache": true, 11 | "_installable": true, 12 | "_location": "/utils-merge", 13 | "_npmUser": { 14 | "email": "jaredhanson@gmail.com", 15 | "name": "jaredhanson" 16 | }, 17 | "_npmVersion": "1.2.25", 18 | "_phantomChildren": {}, 19 | "_requested": { 20 | "name": "utils-merge", 21 | "raw": "utils-merge@1.0.0", 22 | "rawSpec": "1.0.0", 23 | "scope": null, 24 | "spec": "1.0.0", 25 | "type": "version" 26 | }, 27 | "_requiredBy": [ 28 | "/connect" 29 | ], 30 | "_resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz", 31 | "_shasum": "0294fb922bb9375153541c4f7096231f287c8af8", 32 | "_shrinkwrap": null, 33 | "_spec": "utils-merge@1.0.0", 34 | "_where": "/Users/destrado/Projects/custom-rss/node_modules/connect", 35 | "author": { 36 | "email": "jaredhanson@gmail.com", 37 | "name": "Jared Hanson", 38 | "url": "http://www.jaredhanson.net/" 39 | }, 40 | "bugs": { 41 | "url": "http://github.com/jaredhanson/utils-merge/issues" 42 | }, 43 | "dependencies": {}, 44 | "description": "merge() utility function", 45 | "devDependencies": { 46 | "chai": "1.x.x", 47 | "mocha": "1.x.x" 48 | }, 49 | "directories": {}, 50 | "dist": { 51 | "shasum": "0294fb922bb9375153541c4f7096231f287c8af8", 52 | "tarball": "http://registry.npmjs.org/utils-merge/-/utils-merge-1.0.0.tgz" 53 | }, 54 | "engines": { 55 | "node": ">= 0.4.0" 56 | }, 57 | "homepage": "https://github.com/jaredhanson/utils-merge#readme", 58 | "keywords": [ 59 | "util" 60 | ], 61 | "licenses": [ 62 | { 63 | "type": "MIT", 64 | "url": "http://www.opensource.org/licenses/MIT" 65 | } 66 | ], 67 | "main": "./index", 68 | "maintainers": [ 69 | { 70 | "email": "jaredhanson@gmail.com", 71 | "name": "jaredhanson" 72 | } 73 | ], 74 | "name": "utils-merge", 75 | "optionalDependencies": {}, 76 | "readme": "ERROR: No README data found!", 77 | "repository": { 78 | "type": "git", 79 | "url": "git://github.com/jaredhanson/utils-merge.git" 80 | }, 81 | "scripts": { 82 | "test": "node_modules/.bin/mocha --reporter spec --require test/bootstrap/node test/*.test.js" 83 | }, 84 | "version": "1.0.0" 85 | } 86 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "custom-rss", 3 | "version": "0.4.1", 4 | "description": "Filtering RSS because Zapier is too expensive.", 5 | "main": "src/feeds/index.js", 6 | "keywords": ["feed filter", "rss", "atom", "xml", "hn"], 7 | "author": "Peng Wang ", 8 | "license": "MIT", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/hlfcoding/custom-rss" 12 | }, 13 | "dependencies": { 14 | "connect": "^3.4.1" 15 | }, 16 | "scripts": { 17 | "start": "NODE_ENV=production node app.js", 18 | "develop": "supervisor --watch . --debug -- app.js", 19 | "test": "node tests/index.js" 20 | }, 21 | "devDependencies": { 22 | "supervisor": "^0.9.1" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/entry-logger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var util = require('./util'); 5 | 6 | module.exports = function createEntryLogger(delegate) { 7 | // delegate.directory: a full path 8 | // delegate.feedName: a dasherized string 9 | // delegate.lineLimit: a natural integer 10 | // delegate.sync: a bool 11 | // delegate.onReady: a function, if 'sync' is off 12 | return { 13 | logEntry: function(entry) { 14 | if (this.data === null) { throw 'no entry data loaded'; } 15 | if (this.data.indexOf(entry.id) !== -1) { return false; } 16 | 17 | var line = entry.id +' : '+ entry.title +'\n'; 18 | if (!this.dataChanged) { 19 | this.dataChanged = true; 20 | } 21 | var data = this.data + line; 22 | var lines = this.lines + 1; 23 | 24 | var archiveUntil; 25 | if (lines > delegate.lineLimit) { 26 | // End index of lines past the limit, include '\n'. 27 | archiveUntil = util.nthIndexOf(data, '\n', lines - delegate.lineLimit) + 1; 28 | this.archiveBuffer += data.substring(0, archiveUntil); 29 | data = data.substring(archiveUntil); 30 | } 31 | 32 | this.setData(data); 33 | }, 34 | 35 | setUp: function() { 36 | util.readFile({ 37 | file: this.contextFile(), 38 | sync: delegate.sync, 39 | onData: function(data) { 40 | this.setData(data); 41 | if (!delegate.sync) { 42 | delegate.onReady(); 43 | } 44 | }.bind(this) 45 | }); 46 | }, 47 | 48 | tearDown: function() { 49 | if (!this.dataChanged) { 50 | this.resetData(); 51 | return false; 52 | } 53 | util.writeFile({ 54 | data: this.data, 55 | file: this.contextFile(), 56 | sync: delegate.sync, 57 | onDone: this.resetData.bind(this) 58 | }); 59 | util.appendFile({ 60 | data: this.archiveBuffer, 61 | file: this.archiveFile(), 62 | sync: delegate.sync, 63 | onDone: function() { 64 | this.archiveBuffer = ''; 65 | }.bind(this) 66 | }); 67 | }, 68 | 69 | // Internal: 70 | 71 | archiveBuffer: '', 72 | data: null, 73 | dataChanged: false, 74 | lines: 0, 75 | 76 | archiveFile: function() { 77 | return path.join(delegate.directory, 78 | delegate.feedName +'-entries.txt'); 79 | }, 80 | 81 | contextFile: function() { 82 | return path.join(delegate.directory, 83 | delegate.feedName +'-entries-context.txt'); 84 | }, 85 | 86 | resetData: function() { 87 | this.data = null; 88 | this.dataChanged = false; 89 | this.lines = 0; 90 | }, 91 | 92 | setData: function(data) { 93 | this.data = data; 94 | var matchResults = data.match(util.patterns.line); 95 | this.lines = matchResults ? matchResults.length : 0; 96 | } 97 | }; 98 | }; 99 | -------------------------------------------------------------------------------- /src/feeds/gama-sutra.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fetchFeed = require('../fetch-feed'); 4 | var filterFeed = require('../filter-feed'); 5 | var url = require('url'); 6 | 7 | function transformMeta(root) { 8 | root.transformContent('title', { to: 'Gama Sutra (filtered)' }); 9 | } 10 | 11 | module.exports = function(config, request, response) { 12 | config.originalURL = 'http://feeds.feedburner.com/GamasutraFeatureArticles'; 13 | config.url = url.format({ 14 | protocol: 'http', host: request.headers.host, pathname: config.name 15 | }); 16 | 17 | fetchFeed({ 18 | url: config.originalURL, 19 | onResponse: function(resFetch, data) { 20 | response.setHeader('Content-Type', resFetch.headers['content-type']); 21 | 22 | filterFeed({ 23 | config: config, 24 | data: data, 25 | findEntry: function(root) { return root.find('item'); }, 26 | findId: function(entry) { return entry.find('guid'); }, 27 | guardReposts: false, 28 | transformMeta: transformMeta, 29 | verbose: true, 30 | onDone: function(data) { 31 | response.end(data); 32 | } 33 | }); 34 | }, 35 | onError: function(e) { 36 | response.end(e.message); 37 | } 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /src/feeds/hacker-news.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fetchFeed = require('../fetch-feed'); 4 | var filterFeed = require('../filter-feed'); 5 | var patterns = require('../util').patterns; 6 | var url = require('url'); 7 | 8 | var rCommentsURL = /news.ycombinator.com\/item\?id=/; 9 | var rCounts = /(\d+)\s(?:point|comment)s?/g; 10 | var rScorePrefix = /^(\[\w+\]\s)?\d+\s+\S+\s+/; 11 | 12 | function createSuffix(entry) { 13 | var suffix = ' ['; 14 | var counts = entry.find('content').match(rCounts) || []; 15 | suffix += counts.map(function(s) { return parseInt(s); }).join('|'); 16 | var link = entry.find('link', 'href') || ''; 17 | suffix += (!link.length ? '' : '|'+link.match(patterns.domain)[1]); 18 | return suffix+']'; 19 | } 20 | 21 | function transformContent(entry) { 22 | function replace(match) { 23 | var replaced = match.replace(rCommentsURL, 'hn.premii.com/#/comments/'); 24 | return replaced; 25 | } 26 | entry.transformContent('content', { to: replace }); 27 | } 28 | 29 | function transformMeta(root) { 30 | root.transformContent('title', { to: 'Hacker News (filtered)' }); 31 | } 32 | 33 | function transformTitle(entry) { 34 | function replace(match) { 35 | // No score. 36 | var replaced = match.replace(rScorePrefix, '$1'); 37 | // Add domain if any. 38 | replaced += createSuffix(entry); 39 | return replaced; 40 | } 41 | entry.transformContent('title', { to: replace }); 42 | } 43 | 44 | module.exports = function(config, request, response) { 45 | config.originalURL = 'http://hnapp.com/rss?q='+ config.hnappQuery; 46 | config.url = url.format({ 47 | protocol: 'http', host: request.headers.host, pathname: config.name 48 | }); 49 | 50 | fetchFeed({ 51 | url: config.originalURL, 52 | onResponse: function(resFetch, data) { 53 | response.setHeader('Content-Type', resFetch.headers['content-type']); 54 | 55 | filterFeed({ 56 | config: config, 57 | data: data, 58 | findId: function(entry) { return entry.find('id'); }, 59 | findLink: function(entry) { return entry.find('link', 'href'); }, 60 | transformEntry: function(entry) { 61 | transformTitle(entry); 62 | transformContent(entry); 63 | }, 64 | transformMeta: transformMeta, 65 | verbose: true, 66 | onDone: function(data) { 67 | response.end(data); 68 | } 69 | }); 70 | }, 71 | onError: function(e) { 72 | response.end(e.message); 73 | } 74 | }); 75 | }; 76 | -------------------------------------------------------------------------------- /src/feeds/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | gamaSutra: require('./gama-sutra'), 3 | hackerNews: require('./hacker-news'), 4 | nytBusiness: require('./nyt-business'), 5 | quartz: require('./quartz'), 6 | rayWenderlich: require('./ray-wenderlich'), 7 | }; 8 | -------------------------------------------------------------------------------- /src/feeds/nyt-business.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // NOTE: Unused, unmaintained. 4 | 5 | var fetchFeed = require('../fetch-feed'); 6 | var filterFeed = require('../filter-feed'); 7 | var url = require('url'); 8 | 9 | function transformLink(entry) { 10 | // Replace with actual article link, instead of redirect. 11 | entry.transformContent('link', { to: entry.find('guid') }); 12 | } 13 | 14 | function transformMeta(root) { 15 | root.transformContent('title', { to: 'NYT Business (filtered)' }); 16 | } 17 | 18 | module.exports = function(config, request, response) { 19 | config.originalURL = 'http://www.nytimes.com/services/xml/rss/nyt/Business.xml'; 20 | config.url = url.format({ 21 | protocol: 'http', host: request.headers.host, pathname: config.name 22 | }); 23 | 24 | fetchFeed({ 25 | url: 'http://rss.nytimes.com/services/xml/rss/nyt/Business.xml', 26 | onResponse: function(resFetch, data) { 27 | response.setHeader('Content-Type', resFetch.headers['content-type']); 28 | 29 | filterFeed({ 30 | config: config, 31 | data: data, 32 | findEntry: function(root) { return root.find('item'); }, 33 | findId: function(entry) { return entry.find('guid'); }, 34 | guardReposts: false, 35 | transformEntry: transformLink, 36 | transformMeta: transformMeta, 37 | verbose: true, 38 | onDone: function(data) { 39 | response.end(data); 40 | } 41 | }); 42 | }, 43 | onError: function(e) { 44 | response.end(e.message); 45 | } 46 | }); 47 | }; 48 | -------------------------------------------------------------------------------- /src/feeds/quartz.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fetchFeed = require('../fetch-feed'); 4 | var filterFeed = require('../filter-feed'); 5 | var url = require('url'); 6 | 7 | function transformMeta(root) { 8 | root.transformContent('title', { to: 'Quartz (filtered)' }); 9 | } 10 | 11 | module.exports = function(config, request, response) { 12 | config.originalURL = 'https://cms.qz.com/feed/'; 13 | config.url = url.format({ 14 | protocol: 'http', host: request.headers.host, pathname: config.name 15 | }); 16 | 17 | fetchFeed({ 18 | url: config.originalURL, 19 | onResponse: function(resFetch, data) { 20 | response.setHeader('Content-Type', resFetch.headers['content-type']); 21 | 22 | filterFeed({ 23 | config: config, 24 | data: data, 25 | findEntry: function(root) { return root.find('item'); }, 26 | findId: function(entry) { return entry.find('guid'); }, 27 | guardReposts: false, 28 | transformMeta: transformMeta, 29 | verbose: true, 30 | onDone: function(data) { 31 | response.end(data); 32 | } 33 | }); 34 | }, 35 | onError: function(e) { 36 | response.end(e.message); 37 | } 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /src/feeds/ray-wenderlich.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fetchFeed = require('../fetch-feed'); 4 | var filterFeed = require('../filter-feed'); 5 | var url = require('url'); 6 | 7 | function transformMeta(root) { 8 | root.transformContent('title', { to: 'Ray Wenderlich (filtered)' }); 9 | } 10 | 11 | function transformTitle(entry) { 12 | function replace(match) { 13 | return match.replace(' [FREE]', ''); 14 | } 15 | entry.transformContent('title', { to: replace }); 16 | } 17 | 18 | module.exports = function(config, request, response) { 19 | config.originalURL = 'https://www.raywenderlich.com/feed?max-results=1'; 20 | config.url = url.format({ 21 | protocol: 'http', host: request.headers.host, pathname: config.name 22 | }); 23 | 24 | fetchFeed({ 25 | url: config.originalURL, 26 | onResponse: function(resFetch, data) { 27 | response.setHeader('Content-Type', resFetch.headers['content-type']); 28 | 29 | filterFeed({ 30 | config: config, 31 | data: data, 32 | findId: function(entry) { return entry.find('id'); }, 33 | guardReposts: false, 34 | transformEntry: transformTitle, 35 | transformMeta: transformMeta, 36 | verbose: true, 37 | onDone: function(data) { 38 | response.end(data); 39 | } 40 | }); 41 | }, 42 | onError: function(e) { 43 | response.end(e.message); 44 | } 45 | }); 46 | }; 47 | -------------------------------------------------------------------------------- /src/fetch-feed.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var util = require('./util'); 4 | 5 | module.exports = function fetchFeed(delegate) { 6 | // delegate.url: a url string 7 | // delegate.verbose: a bool 8 | // delegate.onError: an error handler 9 | // delegate.onResponse: a response and data handler 10 | 11 | var request = util.request(delegate.url); 12 | 13 | request.on('error', function(e) { 14 | util.log(e.message); 15 | delegate.onError(e); 16 | }); 17 | 18 | request.on('response', function(response) { 19 | util.log('status', response.statusCode); 20 | util.log('headers', JSON.stringify(response.headers)); 21 | 22 | if (response.statusCode !== 200) { 23 | delegate.onError({ 24 | code: response.statusCode, 25 | message: response.statusMessage 26 | }); 27 | return; 28 | } 29 | 30 | var data = ''; 31 | response.setEncoding('utf8'); 32 | response.on('data', function(chunk) { 33 | data += chunk; 34 | }).on('end', function() { 35 | if (delegate.verbose) { util.log('data', data); } 36 | delegate.onResponse(response, data); 37 | }); 38 | }); 39 | 40 | request.end(); 41 | }; 42 | -------------------------------------------------------------------------------- /src/filter-feed.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var createEntryLogger = require('./entry-logger'); 4 | var createRepostGuard = require('./repost-guard'); 5 | var createXMLTransformer = require('./xml-transformer'); 6 | var util = require('./util'); 7 | var patterns = util.patterns; 8 | 9 | var directory = require('path').join(__dirname, '../tmp'); 10 | 11 | function createFilters(configs) { 12 | return configs.map(function(config) { 13 | var filter = { name: config.name, type: config.type, input: config.input || 'title' }; 14 | switch (config.type) { 15 | case 'blacklist': 16 | case 'graylist': 17 | filter.pattern = patterns.createFromTokens(config.tokens); break; 18 | case 'black-pattern': 19 | filter.pattern = new RegExp(config.pattern); break; 20 | default: throw 'unsupported filter type'; 21 | } 22 | return filter; 23 | }); 24 | } 25 | 26 | var defaultFinders = { 27 | entry: function(root) { return root.find('entry'); }, 28 | 29 | content: function(entry) { return entry.find('content'); }, 30 | link: function(entry) { return entry.find('link'); }, 31 | title: function(entry) { return entry.find('title'); } 32 | }; 33 | 34 | function defaultShouldSkipEntry(entry, finders, filters, repostGuard) { 35 | var text, title; 36 | var skip = filters.reduce(function(skip, filter) { 37 | var input, isMatch; 38 | if (skip) { 39 | return skip; 40 | } 41 | switch (filter.input) { 42 | case 'text-content': 43 | if (!text) { text = patterns.stripTags(finders.content(entry)); } 44 | input = text; 45 | break; 46 | case 'title': 47 | if (!title) { title = finders.title(entry); } 48 | input = title; 49 | break; 50 | default: throw 'unsupported input type'; 51 | } 52 | isMatch = filter.pattern.test(input); 53 | if (isMatch && filter.type === 'graylist') { 54 | switch (filter.input) { 55 | case 'title': 56 | entry.transformContent('title', { to: function(match) { 57 | return '['+ filter.name +'] '+ title; 58 | }}); 59 | break; 60 | default: throw 'unsupported input type'; 61 | } 62 | return false; 63 | } 64 | return isMatch; 65 | }, false); 66 | if (skip) { skip = 'blocked'; } 67 | 68 | var link = finders.link(entry); 69 | if (repostGuard && skip === false) { 70 | skip = !repostGuard.checkLink(link); 71 | if (skip) { skip = 'repost'; } 72 | } 73 | 74 | return skip; 75 | } 76 | 77 | function mergeFinders(delegate) { 78 | return { 79 | entry: delegate.findEntry || defaultFinders.entry, 80 | 81 | content: delegate.findContent || defaultFinders.content, 82 | id: delegate.findId, 83 | link: delegate.findLink || defaultFinders.link, 84 | title: delegate.findTitle || defaultFinders.title 85 | }; 86 | } 87 | 88 | 89 | function main(delegate) { 90 | // delegate.config.filters: an array of filter objects for createFilters 91 | // delegate.data: an xml string 92 | // delegate.findId: a function returning id string for xml-transformer 'entry' 93 | // delegate.find(Entry|Link|Title): optional functions return data for xml-transformer 'entry' 94 | // delegate.guardReposts: a bool 95 | // delegate.logger: a logger whose 'logEntry' takes a dictionary 96 | // delegate.shouldSkipEntry: 97 | // delegate.transform(Entry|Meta): optional transform functions that mutate given xml-transformer's 'string' 98 | 99 | var filters = createFilters(delegate.config.filters); 100 | 101 | var root = createXMLTransformer({ 102 | string: delegate.data, verbose: delegate.verbose 103 | }); 104 | if (delegate.transformMeta) { 105 | delegate.transformMeta(root); 106 | } 107 | 108 | var finders = mergeFinders(delegate); 109 | var guard; 110 | if (delegate.guardReposts !== false) { 111 | guard = createRepostGuard.shared; 112 | } 113 | var shouldSkipEntry = delegate.shouldSkipEntry || defaultShouldSkipEntry; 114 | 115 | var entry, skip; 116 | while ((entry = finders.entry(root))) { 117 | 118 | if ((skip = shouldSkipEntry(entry, finders, filters, guard)) && 119 | skip !== false) 120 | { 121 | root.skip(); 122 | delegate.logger.logEntry({ 123 | id: finders.id(entry), 124 | title: finders.title(entry) +' ('+ skip +')' 125 | }); 126 | 127 | } else { 128 | if (delegate.transformEntry) { 129 | delegate.transformEntry(entry); 130 | } 131 | root.next(); 132 | } 133 | 134 | } 135 | delegate.onDone(root.string); 136 | } 137 | 138 | function filterFeed(delegate) { 139 | // Remove any XML stylesheets; we won't be serving them. 140 | delegate.data = delegate.data.replace(/<\?xml-stylesheet[^]+?\?>\s*/g, ''); 141 | 142 | // Update URL values in feed. 143 | delegate.data = delegate.data.split(delegate.config.originalURL) 144 | .join(delegate.config.url); 145 | 146 | // Wait for logger. 147 | delegate.logger = createEntryLogger({ 148 | directory: directory, 149 | feedName: delegate.config.name, 150 | lineLimit: 500, 151 | sync: false, 152 | onReady: main.bind(null, delegate) 153 | }); 154 | 155 | // Wait for feed. 156 | var onDone = delegate.onDone; 157 | delegate.onDone = function() { 158 | onDone.apply(delegate, arguments); 159 | createRepostGuard.shared.persistLinks(function() { 160 | util.log('Links persisted.'); 161 | }); 162 | delegate.logger.tearDown(); 163 | }; 164 | 165 | // Start. 166 | delegate.logger.setUp(); 167 | } 168 | 169 | filterFeed.createFilters = createFilters; 170 | filterFeed.defaultShouldSkipEntry = defaultShouldSkipEntry; 171 | 172 | module.exports = filterFeed; 173 | -------------------------------------------------------------------------------- /src/repost-guard.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var util = require('./util'); 5 | 6 | module.exports = function createRepostGuard(delegate) { 7 | // delegate.directory: a full path 8 | // delegate.feedPageSize: a natural integer 9 | // delegate.lineLimit: a natural integer 10 | // delegate.sync: a bool 11 | // delegate.onReady: a function, if 'sync' is off 12 | return { 13 | checkLink: function(link) { 14 | if (this.data === null) { throw 'no links data loaded'; } 15 | 16 | link = util.normalizeLink(link); 17 | var isRepost = this.dataToCheck.indexOf(link) !== -1; 18 | if (!isRepost && this.data.indexOf(link) === -1) { 19 | this.appendLink(link); 20 | } 21 | return !isRepost; 22 | }, 23 | 24 | persistLinks: function(callback) { 25 | if (!this.dataChanged || this.isPersisting) { 26 | if (callback) { callback(); } 27 | return false; 28 | } 29 | this.isPersisting = true; 30 | util.writeFile({ 31 | data: this.data, 32 | file: this.storeFile(), 33 | sync: delegate.sync, 34 | onDone: function() { 35 | this.isPersisting = false; 36 | if (callback) { callback(); } 37 | }.bind(this) 38 | }); 39 | }, 40 | 41 | setUp: function() { 42 | if (this.data) { 43 | throw 'existing data will be overwritten by read file'; 44 | } 45 | util.readFile({ 46 | file: this.storeFile(), 47 | sync: delegate.sync, 48 | onData: function(data) { 49 | this.setData(data); 50 | if (!delegate.sync) { 51 | delegate.onReady(); 52 | } 53 | }.bind(this) 54 | }); 55 | }, 56 | 57 | tearDown: function() { 58 | return this.persistLinks(this.resetData.bind(this)); 59 | }, 60 | 61 | // Internal: 62 | 63 | data: null, 64 | dataToCheck: null, 65 | dataChanged: false, 66 | isPersisting: false, 67 | lines: 0, 68 | 69 | appendLink: function(link) { 70 | if (!this.dataChanged) { 71 | this.dataChanged = true; 72 | } 73 | var data = this.data + (link +'\n'); 74 | var firstLineEnd = data.indexOf('\n'); 75 | if (this.lines === delegate.lineLimit) { 76 | data = data.substring(firstLineEnd); 77 | } 78 | this.setData(data); 79 | }, 80 | 81 | resetData: function() { 82 | this.data = this.dataToCheck = null; 83 | this.dataChanged = false; 84 | this.lines = 0; 85 | }, 86 | 87 | setData: function(data) { 88 | this.data = data; 89 | var matchResults = data.match(util.patterns.line); 90 | this.lines = matchResults ? matchResults.length : 0; 91 | 92 | var currentPageIndex = util.nthLastIndexOf(this.data, '\n', delegate.feedPageSize); 93 | this.dataToCheck = data.substring(0, (currentPageIndex === -1) ? 0 : currentPageIndex); 94 | }, 95 | 96 | storeFile: function() { 97 | return path.join(delegate.directory, 'links.txt'); 98 | } 99 | }; 100 | }; 101 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var log = require('util').log; 5 | var url = require('url'); 6 | 7 | var protocolModules = { 8 | http: require('http'), 9 | https: require('https') 10 | }; 11 | 12 | // section: debugging 13 | 14 | var mode = process.env.NODE_ENV || 'development'; 15 | module.exports.mode = mode; 16 | 17 | function debugLog(label) { 18 | function toString(value) { 19 | if (typeof value === 'function') { 20 | return '[function]'; 21 | } 22 | return value.toString ? value.toString() : value; 23 | } 24 | 25 | var string; 26 | if (arguments.length > 1) { 27 | if (arguments.length > 2) { 28 | string = Array.prototype.slice.call(arguments, 1).map(toString).join(' '); 29 | } else { 30 | string = toString(arguments[1]); 31 | } 32 | string = label.toUpperCase() +': '+ string; 33 | } else { 34 | string = 'LOG: '+ arguments[0]; 35 | } 36 | 37 | if (string.indexOf('\n') !== -1) { 38 | string = '\n\n'+ string; 39 | } 40 | 41 | log(string +'\n'); 42 | } 43 | module.exports.log = (mode !== 'development') ? function() {} : debugLog; 44 | 45 | // section: regex 46 | 47 | module.exports.patterns = { 48 | brackets: { open: /</g, close: />/g }, 49 | domain: /:\/\/(?:www\.)?([^\/]+)\.([^\/]+)/, 50 | line: /\n/g, 51 | tag: /(<([^>]+)>)/g, 52 | 53 | createFromTokens: function(escapedTokens) { 54 | return new RegExp('\\b(' + 55 | escapedTokens.join('|').replace(/\s/g, '\\s') + 56 | ')\\b'); 57 | }, 58 | 59 | decodeTags: function(string) { 60 | return string.replace(this.brackets.open, '<') 61 | .replace(this.brackets.close, '>'); 62 | }, 63 | 64 | stripTags: function(string) { 65 | return this.decodeTags(string).replace(this.tag, ''); 66 | } 67 | }; 68 | 69 | // section: http 70 | 71 | module.exports.normalizeLink = function(link) { 72 | var parsed = url.parse(link); 73 | parsed.host = parsed.host.replace(/^www\./, ''); 74 | return parsed.host + parsed.pathname; 75 | }; 76 | 77 | module.exports.request = function() { 78 | var module, protocol; 79 | if (typeof arguments[0] === 'string') { 80 | protocol = url.parse(arguments[0]).protocol; 81 | } else { 82 | protocol = arguments[0].protocol; 83 | } 84 | // http/s (`Error: Protocol "https:" not supported. Expected "http:".`) 85 | module = protocolModules[protocol.replace(':', '')]; 86 | return module.request.apply(null, arguments); 87 | }; 88 | 89 | // section: fs 90 | 91 | function handleFileError(delegate, retry, error) { 92 | if (error.code === 'ENOENT') { 93 | fs.openSync(delegate.file, 'a'); 94 | retry(delegate); 95 | } else { 96 | throw error; 97 | } 98 | } 99 | 100 | function appendFile(delegate) { 101 | delegate.onError = delegate.onError || handleFileError.bind(null, delegate, appendFile); 102 | var o = delegate.options || null; 103 | 104 | if (delegate.sync) { 105 | try { delegate.onDone(fs.appendFileSync(delegate.file, delegate.data, o)); } 106 | catch (error) { delegate.onError(error); } 107 | 108 | } else { 109 | fs.appendFile(delegate.file, delegate.data, o, function(error) { 110 | if (error) { return delegate.onError(error); } 111 | delegate.onDone(); 112 | }); 113 | } 114 | } 115 | function readFile(delegate) { 116 | delegate.onError = delegate.onError || handleFileError.bind(null, delegate, readFile); 117 | var o = delegate.options || 'utf8'; 118 | 119 | if (delegate.sync) { 120 | try { delegate.onData(fs.readFileSync(delegate.file, o)); } 121 | catch (error) { delegate.onError(error); } 122 | 123 | } else { 124 | fs.readFile(delegate.file, o, function(error, data) { 125 | if (error) { return delegate.onError(error); } 126 | delegate.onData(data); 127 | }); 128 | } 129 | } 130 | function writeFile(delegate) { 131 | delegate.onError = delegate.onError || handleFileError.bind(null, delegate, writeFile); 132 | var o = delegate.options || null; 133 | 134 | if (delegate.sync) { 135 | try { delegate.onDone(fs.writeFileSync(delegate.file, delegate.data, o)); } 136 | catch (error) { delegate.onError(error); } 137 | 138 | } else { 139 | fs.writeFile(delegate.file, delegate.data, o, function(error) { 140 | if (error) { return delegate.onError(error); } 141 | delegate.onDone(); 142 | }); 143 | } 144 | } 145 | module.exports.appendFile = appendFile; 146 | module.exports.readFile = readFile; 147 | module.exports.writeFile = writeFile; 148 | 149 | // section: async 150 | 151 | module.exports.callOn = function(calls, fn) { 152 | var remaining = calls - 1; 153 | return function() { 154 | if (remaining > 0) { 155 | remaining -= 1; 156 | return; 157 | } 158 | fn(); 159 | }; 160 | }; 161 | 162 | // section: string 163 | 164 | module.exports.nthIndexOf = function(string, search, n) { 165 | var i; 166 | for (i = 0; n > 0 && i !== -1; n -= 1) { 167 | i = string.indexOf(search, /* fromIndex */ i ? (i + 1) : i); 168 | } 169 | return i; 170 | }; 171 | 172 | module.exports.nthLastIndexOf = function(string, search, n) { 173 | var i; 174 | for (i = string.length; n > 0 && i !== -1; n -= 1) { 175 | i = string.lastIndexOf(search, /* fromIndex */ i ? (i - 1) : i); 176 | } 177 | return i; 178 | }; 179 | -------------------------------------------------------------------------------- /src/xml-transformer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var log = require('./util').log; 4 | var rAttrs = {}; 5 | var rHasTag = /<\/.+?>/; // Check closing tag. 6 | var rTags = {}; 7 | 8 | function lazyCreateAttrRegExp(tagName, attrName) { 9 | if (!rTags[attrName]) { 10 | var pattern = ( 11 | // Start of tag, allows whitespace or breaks. 12 | '\s*<'+ tagName +'[^]*?'+ attrName +'="' + 13 | // Content, lazy, allows whitespace and breaks. 14 | '([^]+?)' + 15 | // End of tag, allows whitespace or breaks. 16 | '"[^]*?>\s*' 17 | ); 18 | rAttrs[attrName] = new RegExp(pattern, 'i', 'm'); 19 | } 20 | return rAttrs[attrName]; 21 | } 22 | 23 | function lazyCreateTagRegExp(tagName) { 24 | if (!rTags[tagName]) { 25 | var pattern = ( 26 | // Opening tag, allows whitespace and breaks. 27 | '<'+ tagName +'[^]*?>\\s*' + 28 | // Content, lazy, allows whitespace and breaks. 29 | '([^]+?)' + 30 | // Closing tag, allows whitespace and breaks. 31 | '\\s*<\\/'+ tagName +'>' 32 | ); 33 | rTags[tagName] = new RegExp(pattern, 'i'); 34 | } 35 | return rTags[tagName]; 36 | } 37 | 38 | module.exports = function createXMLTransformer(delegate) { 39 | // delegate.string: an xml string 40 | // delegate.verbose: a bool 41 | return { 42 | creator: createXMLTransformer, 43 | 44 | content: function(raw) { 45 | var string = this.matchResults[1]; 46 | if (rHasTag.test(string) && !raw) { 47 | this.lazyCreateChild(string); 48 | return this.child; 49 | } 50 | return string; 51 | }, 52 | 53 | find: function(tagName, attrName) { 54 | var regExp = lazyCreateTagRegExp(tagName); 55 | var string = this.scope(); 56 | var matchResults = string.match(regExp); 57 | 58 | if (attrName) { 59 | regExp = lazyCreateAttrRegExp(tagName, attrName); 60 | matchResults = string.match(regExp); 61 | return !matchResults ? null : matchResults[1]; 62 | 63 | } else if (matchResults) { 64 | this.matchResults = matchResults; 65 | return this.content(); 66 | } 67 | }, 68 | 69 | next: function() { 70 | var mr, nextCursor; 71 | if ((mr = this.matchResults)) { 72 | nextCursor = this.cursor + mr.index + mr[0].length; 73 | if (nextCursor < this.string.length) { 74 | this.cursor = nextCursor; 75 | if (delegate.verbose) { 76 | log('next', this.cursor, this.scope().substr(0, 300)); 77 | } 78 | return true; 79 | } 80 | } 81 | return false; 82 | }, 83 | 84 | skip: function() { 85 | this.replaceFromCursor(this.matchingTag(), ''); 86 | 87 | if (this.parent) { 88 | this.parent.replaceChildString(); 89 | } 90 | }, 91 | 92 | transformContent: function(tagName, args) { 93 | var string = this.find(tagName); 94 | if (typeof string !== 'string') { 95 | throw 'only replaces string'; 96 | } 97 | args.from = args.from || string; 98 | if (delegate.verbose) { 99 | log('transform', tagName, args.from, args.to); 100 | } 101 | 102 | this.replaceFromCursor(args.from, args.to); 103 | 104 | if (this.parent) { 105 | this.parent.replaceChildString(); 106 | } 107 | 108 | return this; 109 | }, 110 | 111 | // Internal: 112 | 113 | child: null, 114 | cursor: 0, 115 | matchResults: null, 116 | originalString: delegate.string, 117 | parent: null, 118 | string: delegate.string, 119 | 120 | lazyCreateChild: function(string) { 121 | if (!this.child || this.child.string !== string) { 122 | if (this.child) { 123 | this.child.parent = null; 124 | } 125 | this.child = createXMLTransformer({ string: string, verbose: delegate.verbose }); 126 | this.child.parent = this; 127 | if (delegate.verbose) { 128 | log('child', string); 129 | } 130 | } 131 | }, 132 | 133 | matchingTag: function(string) { 134 | if (string) { this.matchResults[0] = string; } 135 | return this.matchResults[0]; 136 | }, 137 | 138 | matchingContent: function(string) { 139 | if (string) { this.matchResults[1] = string; } 140 | return this.matchResults[1]; 141 | }, 142 | 143 | replaceFromCursor: function(pattern, replacement) { 144 | if (this.cursor === 0) { 145 | this.cursor = this.matchResults.index; 146 | } 147 | 148 | var rest = this.string.substring(0, this.cursor); 149 | var replaced = this.scope().replace(pattern, replacement); 150 | 151 | var oldString = this.string; 152 | this.string = rest + replaced; 153 | if (delegate.verbose) { 154 | log('replace', oldString.length, this.string.length); 155 | } 156 | }, 157 | 158 | replaceChildString: function(replacement) { 159 | var c = this.child; 160 | replacement = replacement || c.string; 161 | this.replaceFromCursor(c.originalString, replacement); 162 | this.matchingTag( 163 | this.matchingTag().replace(c.originalString, replacement) 164 | ); 165 | 166 | c.originalString = c.string; 167 | }, 168 | 169 | scope: function() { 170 | return this.string.substring(this.cursor); 171 | } 172 | }; 173 | }; 174 | -------------------------------------------------------------------------------- /tests/entry-logger-tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var fs = require('fs'); 5 | var path = require('path'); 6 | var runner = require('./lib/runner'); 7 | var test = runner.test.bind(runner); 8 | 9 | var createEntryLogger = require('../src/entry-logger'); 10 | runner.subject('createEntryLogger'); 11 | 12 | var fixtureLines = [ 13 | 'http://foo.com/1 : Some Title 1', 14 | 'http://foo.com/2 : Some Title 2', 15 | 'http://foo.com/3 : Some Title 3' 16 | ]; 17 | var fixtureData = fixtureLines.join('\n') + '\n'; 18 | 19 | var entry4 = { id: 'http://foo.com/4', title: 'Some Title 4' }; 20 | var entry5 = { id: 'http://foo.com/5', title: 'Some Title 5' }; 21 | 22 | var logger; 23 | runner.beforeEach(function() { 24 | logger = createEntryLogger({ 25 | directory: path.join(__dirname, 'tmp'), 26 | feedName: 'some-feed', 27 | lineLimit: 4, 28 | sync: true 29 | }); 30 | 31 | logger.setUpWithData = function() { 32 | this.setUp(); 33 | this.setData(fixtureData); 34 | this.dataChanged = true; 35 | return this; 36 | }; 37 | }); 38 | 39 | runner.afterEach(function() { 40 | try { fs.unlinkSync(logger.archiveFile()); } catch (e) {} 41 | fs.unlinkSync(logger.contextFile()); 42 | }); 43 | 44 | 45 | runner.subject('#logEntry'); 46 | 47 | test('logs entry to data if id is unique', function() { 48 | logger.setUpWithData(); 49 | assert(logger.logEntry(entry4) !== false, 'is successful'); 50 | assert(logger.logEntry(entry4) === false, 'fails uniqueness check'); 51 | assert.equal( 52 | logger.data.indexOf(entry4.id), 53 | logger.data.lastIndexOf(entry4.id), 54 | 'appends link only once' 55 | ); 56 | }); 57 | 58 | test('moves lines from top to archive when line-limit reached ', function() { 59 | logger.setUpWithData(); 60 | assert(logger.logEntry(entry4) !== false, 'is successful'); 61 | assert(logger.logEntry(entry5) !== false, 'is successful'); 62 | assert.equal(logger.data.indexOf(fixtureLines[0]), -1, 'moves first line'); 63 | assert.equal(logger.archiveBuffer.indexOf(fixtureLines[0]), 0, 'to archive'); 64 | }); 65 | 66 | test('works with no initial data', function() { 67 | logger.setUp(); 68 | assert(logger.logEntry(entry4) !== false, 'is successful'); 69 | }); 70 | 71 | 72 | runner.subject('#setUp'); 73 | 74 | test('creates context file if none exist', function() { 75 | assert.doesNotThrow(function() { 76 | logger.setUp(); 77 | }); 78 | assert.equal(logger.data, '', 'sets data to blank'); 79 | }); 80 | 81 | test('loads data from context file', function() { 82 | logger.setUpWithData().tearDown(); 83 | logger.setUp(); 84 | assert.equal(logger.data, fixtureData, 'loads fixture data'); 85 | }); 86 | 87 | 88 | runner.subject('#tearDown'); 89 | 90 | test('flushes data into context file', function() { 91 | var addition = 'http://foo.com/4 : Some Title 4\n'; 92 | logger.setUpWithData(); 93 | logger.setData(logger.data + addition); 94 | logger.tearDown(); 95 | assert.equal(logger.data, null, 'sets data to null'); 96 | 97 | logger.setUp(); 98 | assert.equal(logger.data, fixtureData + addition, 'file has data'); 99 | }); 100 | 101 | test('flushes archive buffer into end of archive file', function() { 102 | logger.setUpWithData(); 103 | logger.logEntry(entry4); 104 | logger.logEntry(entry5); 105 | logger.tearDown(); 106 | 107 | var archiveData = fs.readFileSync(logger.archiveFile(), 'utf8'); 108 | assert.equal(archiveData, fixtureLines[0] +'\n', 'file has data'); 109 | }); 110 | 111 | runner.report(); 112 | -------------------------------------------------------------------------------- /tests/filter-feed-tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var runner = require('./lib/runner'); 5 | var test = runner.test.bind(runner); 6 | 7 | var filterFeed = require('../src/filter-feed'); 8 | runner.subject('filterFeed'); 9 | 10 | var fixtureFiltersConfig = [ 11 | { name: 'foo', type: 'blacklist', tokens: ['Foo'] }, 12 | { name: 'bar', type: 'blacklist', tokens: ['Bar'] }, 13 | { name: 'baz', type: 'graylist', tokens: ['Baz'] } 14 | ]; 15 | 16 | 17 | var createFilters = filterFeed.createFilters; 18 | runner.subject('.createFilters'); 19 | 20 | test('creates filters from configs', function() { 21 | var filters = createFilters(fixtureFiltersConfig); 22 | 23 | assert.equal(filters[0].name, 'foo'); 24 | assert(filters[0].pattern.test('Some title with Foo')); 25 | assert(!filters[0].pattern.test('Some title with Bar')); 26 | 27 | assert.equal(filters[1].name, 'bar'); 28 | assert(filters[1].pattern.test('Some title with Bar')); 29 | assert(!filters[1].pattern.test('Some title with Foo')); 30 | }); 31 | 32 | 33 | var filters = createFilters(fixtureFiltersConfig); 34 | var mockFinders = { 35 | link: function(mockEntry) { return mockEntry.link; }, 36 | title: function(mockEntry) { return mockEntry.title; } 37 | }; 38 | var mockRepostGuard = { 39 | checkLink: function(link) { return link !== 'repost.com'; } 40 | }; 41 | var defaultShouldSkipEntry = filterFeed.defaultShouldSkipEntry; 42 | runner.subject('.defaultShouldSkipEntry'); 43 | 44 | test("returns 'blocked' reason for filtered posts", function() { 45 | var entryToBlock = { title: 'Some title with Foo', link: 'foo.com' }; 46 | var skip = defaultShouldSkipEntry(entryToBlock, mockFinders, filters, mockRepostGuard); 47 | assert.equal(skip, 'blocked'); 48 | }); 49 | 50 | test("returns 'repost' reason for duplicate posts", function() { 51 | var entryToDedupe = { title: 'Some title', link: 'repost.com' }; 52 | var skip = defaultShouldSkipEntry(entryToDedupe, mockFinders, filters, mockRepostGuard); 53 | assert.equal(skip, 'repost'); 54 | }); 55 | 56 | test('otherwise returns false', function() { 57 | var entryToKeep = { title: 'Some title', link: 'foo.com' }; 58 | var skip = defaultShouldSkipEntry(entryToKeep, mockFinders, filters, mockRepostGuard); 59 | assert(!skip); 60 | }); 61 | 62 | test('also returns false for questionable posts, tags title ', function() { 63 | var entryToKeep = { title: 'Some title with Baz', link: 'baz.com', 64 | transformContent: function(tagName, args) { 65 | assert.equal(tagName, 'title'); 66 | this.title = args.to(); 67 | } 68 | }; 69 | var skip = defaultShouldSkipEntry(entryToKeep, mockFinders, filters, mockRepostGuard); 70 | assert(!skip); 71 | assert.equal(entryToKeep.title.indexOf('[baz] '), 0); 72 | }); 73 | 74 | 75 | runner.report(); 76 | -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | require('./entry-logger-tests'); 2 | require('./filter-feed-tests'); 3 | require('./repost-guard-tests'); 4 | require('./util-tests'); 5 | require('./xml-transformer-tests'); 6 | -------------------------------------------------------------------------------- /tests/lib/runner.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var afterEach, beforeEach, failed, total; 4 | failed = total = 0; 5 | 6 | var colors = { 7 | none: '\x1b[0m', 8 | fail: '\x1b[31m', 9 | pass: '\x1b[32m' 10 | }; 11 | 12 | module.exports = { 13 | exitEarly: false, 14 | 15 | afterEach: function(block) { 16 | afterEach = block; 17 | }, 18 | 19 | beforeEach: function(block) { 20 | beforeEach = block; 21 | }, 22 | 23 | test: function(description, block) { 24 | try { 25 | if (beforeEach) { beforeEach(); } 26 | block(); 27 | console.log(colors.pass, 'PASS', description); 28 | 29 | } catch (error) { 30 | console.error(colors.fail, 'FAIL', description); 31 | if ('actual' in error && 'expected' in error) { 32 | console.log(' ACTUALLY', error.actual, 33 | 'EXPECTED', error.expected, colors.none); 34 | } else { 35 | console.log('\n', error.stack, '\n'); 36 | } 37 | if (this.exitEarly && failed > 0) { 38 | console.log(colors.fail, 'STOPPING early from failures!\n'); 39 | process.exit(1); 40 | } else { 41 | failed += 1; 42 | } 43 | 44 | } finally { 45 | total += 1; 46 | if (afterEach) { afterEach(); } 47 | } 48 | }, 49 | 50 | report: function() { 51 | console.log('\n'); 52 | console.log( 53 | colors.pass, 'PASSED', total - failed, 54 | colors.fail, 'FAILED', failed, 55 | colors.none, 'TOTAL', total, '\n' 56 | ); 57 | afterEach = beforeEach = null; 58 | }, 59 | 60 | subject: function(description) { 61 | console.log(colors.none, '\n', description, '\n'); 62 | } 63 | }; 64 | -------------------------------------------------------------------------------- /tests/repost-guard-tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var fs = require('fs'); 5 | var path = require('path'); 6 | var runner = require('./lib/runner'); 7 | var test = runner.test.bind(runner); 8 | 9 | var createRepostGuard = require('../src/repost-guard'); 10 | runner.subject('createRepostGuard'); 11 | 12 | var fixtureLines = [ 13 | 'medium.com/some-user/some-post', 14 | 'google.com/some-page', 15 | 'yahoo.com/some-page' 16 | ]; 17 | var fixtureData = fixtureLines.join('\n') + '\n'; 18 | var guard; 19 | runner.beforeEach(function() { 20 | guard = createRepostGuard({ 21 | directory: path.join(__dirname, 'tmp'), 22 | feedPageSize: 2, 23 | lineLimit: 4, 24 | sync: true 25 | }); 26 | 27 | guard.setUpWithData = function() { 28 | this.setUp(); 29 | this.setData(fixtureData); 30 | this.dataChanged = true; 31 | return this; 32 | }; 33 | }); 34 | 35 | runner.afterEach(function() { 36 | fs.unlinkSync(guard.storeFile()); 37 | }); 38 | 39 | 40 | runner.subject('#checkLink'); 41 | 42 | test('adds normalized link to data if unique', function() { 43 | guard.setUpWithData(); 44 | assert(guard.checkLink('http://twitter.com/me?q=1#id'), 'unique'); 45 | assert(guard.checkLink('http://twitter.com/me?q=1#id'), 'still in page'); 46 | assert.equal( 47 | guard.data.indexOf('twitter.com/me\n'), 48 | guard.data.lastIndexOf('twitter.com/me\n'), 49 | 'appends link only once' 50 | ); 51 | }); 52 | 53 | test("any existing link outside of 'current page' is a repost", function() { 54 | guard.setUpWithData(); 55 | assert(guard.checkLink('http://twitter.com/me?q=1#id'), 'unique'); 56 | assert(guard.checkLink('http://twitter.com/me?q=1#id'), 'still in page'); 57 | assert(guard.checkLink('http://vine.co/playlists/foo'), 'unique'); 58 | assert(!guard.checkLink('http://twitter.com/me?q=1#id'), 'repost'); 59 | }); 60 | 61 | test('removes lines from top when line-limit reached ', function() { 62 | guard.setUpWithData(); 63 | assert(guard.checkLink('http://twitter.com/hashtag/foo'), 'returns success'); 64 | assert(guard.checkLink('http://vine.co/playlists/foo'), 'returns success'); 65 | assert.equal(guard.data.indexOf(fixtureLines[0]), -1, 'removes first link'); 66 | }); 67 | 68 | test('works with no initial data', function() { 69 | guard.setUp(); 70 | assert(guard.checkLink('http://twitter.com/me?q=1#id'), 'returns success'); 71 | }); 72 | 73 | 74 | runner.subject('#setUp'); 75 | 76 | test('creates store file if none exist', function() { 77 | assert.doesNotThrow(function() { guard.setUp(); }); 78 | assert.equal(guard.data, '', 'sets data to blank'); 79 | }); 80 | 81 | test('loads data from store file', function() { 82 | guard.setUpWithData().tearDown(); 83 | guard.setUp(); 84 | assert.equal(guard.data, fixtureData, 'loads fixture data'); 85 | }); 86 | 87 | 88 | runner.subject('#tearDown'); 89 | 90 | test('flushes data into store file', function() { 91 | var addition = 'twitter.com/hashtag/foo\n'; 92 | guard.setUpWithData(); 93 | guard.setData(guard.data + addition); 94 | guard.tearDown(); 95 | assert.equal(guard.data, null, 'sets data to null'); 96 | 97 | guard.setUp(); 98 | assert.equal(guard.data, fixtureData + addition, 'file has data'); 99 | }); 100 | 101 | 102 | runner.report(); 103 | -------------------------------------------------------------------------------- /tests/tmp/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hlfcoding/custom-rss/e50f13a6b0266a9a067b4edbc025fafdcd104d87/tests/tmp/.gitkeep -------------------------------------------------------------------------------- /tests/util-tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var runner = require('./lib/runner'); 5 | var test = runner.test.bind(runner); 6 | 7 | var util = require('../src/util'); 8 | runner.subject('util'); 9 | 10 | 11 | runner.subject('.stripTags'); 12 | 13 | test('decodes then strips tags', function() { 14 | var html = '<p>1 point, <a href="https://news.ycombinator.com/item?id=12024279">0 comments</a></p>'; 15 | assert.equal(util.patterns.stripTags(html), '1 point, 0 comments'); 16 | }); 17 | 18 | 19 | runner.subject('.normalizeLink'); 20 | 21 | test('returns only host and pathname', function() { 22 | var link = 'http://some-domain.com/some-path?some-query#some-fragment'; 23 | assert.equal(util.normalizeLink(link), 'some-domain.com/some-path'); 24 | }); 25 | 26 | test("does not include 'www'", function() { 27 | assert.equal(util.normalizeLink('http://www.some-domain.com/'), 'some-domain.com/'); 28 | }); 29 | 30 | 31 | runner.subject('.callOn'); 32 | 33 | test('returns function that calls original on nth call', function() { 34 | var calls = 0; 35 | function fn() { calls += 1; } 36 | var deferredFn = util.callOn(2, fn); 37 | deferredFn(); 38 | assert.equal(calls, 0, 'not yet called'); 39 | deferredFn(); 40 | assert.equal(calls, 1, 'called'); 41 | deferredFn(); 42 | assert.equal(calls, 2, 'subsequent calls pass through'); 43 | }); 44 | 45 | 46 | (function() { 47 | var string = 'first line\nsecond line\nthird line\n'; 48 | 49 | runner.subject('.nthIndexOf'); 50 | 51 | test('returns nth index of search value', function() { 52 | assert.equal(util.nthIndexOf(string, '\n', 2), 22); 53 | assert.equal(util.nthIndexOf(string, 'f', 1), 0, 'handles edge cases'); 54 | }); 55 | 56 | test("returns '-1' if not found, like indexOf", function() { 57 | assert.equal(util.nthIndexOf(string, '\n', 4), -1, 'not found if out of bounds'); 58 | assert.equal(util.nthIndexOf(string, '1', 1), -1, 'not found if true'); 59 | }); 60 | 61 | runner.subject('.nthLastIndexOf'); 62 | 63 | test('returns nth last index of search value', function() { 64 | assert.equal(util.nthLastIndexOf(string, '\n', 2), 22); 65 | }); 66 | 67 | test("returns '-1' if not found, like lastIndexOf", function() { 68 | assert.equal(util.nthLastIndexOf(string, '\n', 4), -1, 'not found if out of bounds'); 69 | assert.equal(util.nthLastIndexOf(string, '1', 1), -1, 'not found if true'); 70 | }); 71 | }()); 72 | 73 | 74 | runner.report(); 75 | -------------------------------------------------------------------------------- /tests/xml-transformer-tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var assert = require('assert'); 4 | var runner = require('./lib/runner'); 5 | var test = runner.test.bind(runner); 6 | 7 | var createXMLTransformer = require('../src/xml-transformer'); 8 | runner.subject('createXMLTransformer'); 9 | 10 | var root; 11 | runner.beforeEach(function() { 12 | root = createXMLTransformer({ string: [ 13 | '\n', 14 | '\t\n\t\t<![CDATA[foo]]>\n\t\n', 15 | '\t\n\t\t<![CDATA[bar]]>\n\t\n', 16 | '\t\n\t\t<![CDATA[baz]]>\n\t\n', 17 | '\n', 18 | ].join('') }); 19 | 20 | root.findNext = function() { 21 | this.next(); 22 | return this.find.apply(this, arguments); 23 | }; 24 | }); 25 | 26 | 27 | runner.subject('#find'); 28 | 29 | test('returns first tag content by tag name', function() { 30 | var title = root.find('title'); 31 | assert.equal(title , '', 'returns correct tag content'); 32 | assert.equal(root.content() , title, '#content returns same content'); 33 | }); 34 | 35 | test('returns first attribute content by tag, attribute names', function() { 36 | var entry = root.find('entry'); 37 | var relSrc = root.find('entry', 'rel-src'); 38 | assert.equal(relSrc , '/foo.html', 'returns correct attribute content'); 39 | assert.equal(root.content() , entry, '#content returns tag transformer'); 40 | }); 41 | 42 | test('returns child transformer if content is another tag', function() { 43 | var entry = root.find('entry'); 44 | assert.equal(entry.creator, createXMLTransformer, 'returns transformer'); 45 | assert.equal(root.content(), entry, '#content returns same instance'); 46 | assert.equal(root.find('entry'), entry, 're-find returns same instance'); 47 | }); 48 | 49 | 50 | runner.subject('#next'); 51 | 52 | test('updates internal cursor to end of current match', function() { 53 | var oldCursor = root.cursor; 54 | root.find('title'); 55 | assert(root.next(), 'returns bool for success'); 56 | assert(root.cursor > oldCursor, 'shifts cursor'); 57 | }); 58 | 59 | test('fails when end of string is reached', function() { 60 | assert.equal(root.find('title'), ''); 61 | assert.equal(root.findNext('title'), ''); 62 | assert.equal(root.findNext('title'), ''); 63 | assert(root.next(), 'moves to end of string'); 64 | var oldCursor = root.cursor; 65 | assert(!root.next(), 'returns bool for failure'); 66 | assert.equal(root.cursor, oldCursor, 'cursor stays still'); 67 | }); 68 | 69 | 70 | runner.subject('#skip'); 71 | 72 | test('removes current match from string', function() { 73 | root.find('entry'); 74 | root.skip(); 75 | assert.equal(root.find('title'), '', 'now starts with second entry'); 76 | }); 77 | 78 | test('skips successive posts reliably', function() { 79 | root.find('entry'); 80 | root.skip(); 81 | root.find('entry'); 82 | root.skip(); 83 | assert.equal(root.find('title'), '', 'now starts with third entry'); 84 | }); 85 | 86 | 87 | runner.subject('#transformContent'); 88 | 89 | test("transforms tag content based on 'from' and 'to' patterns", function() { 90 | root.transformContent('title', { from: /f(oo)/, to: 'b$1' }); 91 | assert.equal(root.find('title'), '', 'partially replaces content'); 92 | 93 | root.transformContent('title', { to: 'boo' }); 94 | root.transformContent('title', { to: '$& boo' }); 95 | assert.equal(root.find('title'), 'boo boo', "defaults 'from' to full content"); 96 | }); 97 | 98 | test('transforms content of successive tags reliably', function() { 99 | root.transformContent('title', { from: /foo/, to: '$& (foo)' }).next(); 100 | root.transformContent('title', { from: /bar/, to: '$& (bar)' }).next(); 101 | root.transformContent('title', { from: /baz/, to: '$& (baz)' }); 102 | root.cursor = 0; 103 | assert.equal(root.find('title'), '', 'content remains as intended'); 104 | }); 105 | 106 | test('causes parent tag(s) to sync and update their strings', function() { 107 | var entry = root.find('entry'); 108 | entry.transformContent('title', { to: 'new' }); 109 | assert.equal(root.find('title'), 'new', 'parent transformer updates'); 110 | }); 111 | 112 | 113 | runner.report(); 114 | -------------------------------------------------------------------------------- /tmp/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hlfcoding/custom-rss/e50f13a6b0266a9a067b4edbc025fafdcd104d87/tmp/.gitkeep --------------------------------------------------------------------------------