├── .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 | [](https://travis-ci.org/hlfcoding/custom-rss)
4 | [](https://codeclimate.com/github/hlfcoding/custom-rss)
5 | [](https://www.npmjs.com/package/custom-rss)
6 | [](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 | 
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\n\t\n',
15 | '\t\n\t\t\n\t\n',
16 | '\t\n\t\t\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
--------------------------------------------------------------------------------