├── .editorconfig ├── .eslintrc.json ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .nvmrc ├── .travis.yml ├── History.md ├── LICENSE ├── README.md ├── bin ├── .eslintrc.json └── feedparser.js ├── examples ├── .eslintrc.json ├── complete.js └── simple.js ├── index.js ├── lib ├── feedparser │ └── index.js ├── namespaces.js └── utils.js ├── package-lock.json ├── package.json └── test ├── .eslintrc.json ├── api.js ├── bad.js ├── category.js ├── common.js ├── duplicate-enclosures.js ├── feeds ├── category-feed.xml ├── complexNamespaceFeed.xml ├── compressed.xml ├── guid-dupes.xml ├── iconv.xml ├── illegally-nested.xml ├── intertwingly.atom ├── invalid-characters-gzipped.xml ├── mediacontent-dupes.xml ├── non-text-alternate-links.xml ├── nondefaultnamespace-baseline.atom ├── nondefaultnamespace-xhtml.atom ├── nondefaultnamespace.atom ├── notafeed.html ├── relative-channel-image-url.xml ├── rss-with-relative-urls-with-absolute-xmlurl.xml ├── rss-with-relative-urls.xml ├── rss2sample.xml ├── tpm.atom ├── unknown-namespace.atom └── wapowellness.xml ├── illegally-nested.js ├── link.js ├── namespaces.js └── xmlbase.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | # Unix-style newlines with a newline ending every file 4 | [*] 5 | end_of_line = lf 6 | insert_final_newline = true 7 | 8 | [{*.js,package.json,.travis.yml}] 9 | charset = utf-8 10 | indent_style = space 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "extends": "eslint:recommended", 6 | "rules": { 7 | "indent": [ 8 | "error", 9 | 2, 10 | { 11 | "MemberExpression": "off" 12 | } 13 | ], 14 | "linebreak-style": [ 15 | "error", 16 | "unix" 17 | ], 18 | "quotes": [ 19 | "error", 20 | "single" 21 | ], 22 | "semi": [ 23 | "error", 24 | "always" 25 | ], 26 | "no-cond-assign": [ 27 | 0 28 | ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Before submitting your issue, please make sure these boxes are checked. Thank you! 2 | 3 | - [ ] Review the [compressed example](https://github.com/danmactough/node-feedparser/blob/master/examples/compressed.js). 4 | - [ ] Include which version of FeedParser you're using. 5 | - [ ] Include which version of Node you're using. 6 | - [ ] Include a link to any feed you're having a problem with. 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | 17 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/dubnium 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: linux 2 | dist: bionic 3 | language: node_js 4 | node_js: 5 | - "10" 6 | - "12" 7 | - "14" 8 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | 2.2.10 / 2020-05-01 3 | ================== 4 | 5 | * Changes a direct use of hasOwnProperty builtin to call it from Object.prototype instead 6 | * Update mri 7 | * Update readable-stream 8 | * Update npm audit fixes 9 | * Remove unused Makefile 10 | * Update examples to use node-fetch in place of request 11 | * Update mocha v7 12 | * Update eslint v6 13 | * Replace iconv with iconv-lite 14 | * Update travis config; drop support for unmaintained node versions 15 | * Merge pull request #271 from jakutis/readme-use-https 16 | * README: make links use https: instead of http: protocol 17 | * Update copyright 18 | * Update README 19 | * Merge pull request #255 from danmactough/greenkeeper/mocha-5.0.0 20 | * chore(package): update mocha to version 5.0.0 21 | 22 | 2.2.9 / 2018-01-27 23 | ================== 24 | 25 | * Skip illegally-nested items 26 | * Add failing test for illegally nested items 27 | 28 | 2.2.8 / 2018-01-07 29 | ================== 30 | 31 | * Fix meta['#ns'] array to avoid duplicates 32 | 33 | 2.2.7 / 2017-12-11 34 | ================== 35 | 36 | * Enhance cli to take feedparser options as cli parameters 37 | * Improve relative url resolution in RSS feeds 38 | * Add issue template 39 | * Add link to Dave Winer's demo to README 40 | 41 | 2.2.6 / 2017-12-10 42 | ================== 43 | 44 | * Prioritize alternate links for item.link 45 | 46 | 2.2.5 / 2017-12-09 47 | ================== 48 | 49 | * Fix reresolve helper to correctly resolve relative URLs in RSS channel image 50 | 51 | 2.2.4 / 2017-11-08 52 | ================== 53 | 54 | * Fix reresolve logic 55 | * Add failing test - no reresolving first link in feed 56 | * Add a test assertion for xml base resolution 57 | 58 | 2.2.3 / 2017-10-25 59 | ================== 60 | 61 | * Update npm package to minimize dist size 62 | 63 | 2.2.2 / 2017-10-12 64 | ================== 65 | 66 | * Update devDependencies 67 | * Update sax v1.2.4 68 | * Update travis - node 7->8 69 | * Make sure that all links are parsed, not only text/html 70 | * docs(readme): add Greenkeeper badge 71 | * chore(package): update dependencies 72 | 73 | 2.2.1 / 2017-06-22 74 | ================== 75 | 76 | * fix: pin sax to 1.2.3 77 | * Update mocha to version 3.4.1 78 | 79 | 2.2.0 / 2017-04-11 80 | ================== 81 | 82 | * support for g:image_link attribute 83 | 84 | 2.1.0 / 2017-01-18 85 | ================== 86 | 87 | * Keep optional media:content attributes in the enclosures default property 88 | 89 | 2.0.0 / 2016-12-26 90 | ================== 91 | 92 | * Make bin script useful as command line tool and rename to "feedparser" 93 | * Add lint script and run lint before tests 94 | * Update README to clarify the importance of the compressed example 95 | * Drop support for Node 0.10 and 0.12 96 | * Fix xml declaration parsing to handle extra whitespace 97 | * Fix assignment by reference in options parsing 98 | * Remove unnecessary method 99 | * Replace bespoke helpers with lodash equivalents where possible 100 | * Move feedparser to lib 101 | * Move helpers lib 102 | * Remove weird comment 103 | * Update copyright 104 | * Update addressparser v1.0.1 105 | * Update dependecy readable-stream v2.2.2 and update tests to conform to api change 106 | * Update dev-dependency (iconv v2.2.1) 107 | * Update dev-dependency (mocha v3.2.0) 108 | * Add eslint/editorconfig and linting 109 | 110 | 1.1.5 / 2016-09-24 111 | ================== 112 | 113 | * Handles line breaks in xml declaration. 114 | * Update README to remove suggestion to use IRC 115 | * Add Gitter badge 116 | * Update examples to work with current versions of request module 117 | 118 | 1.1.4 / 2015-10-24 119 | ================== 120 | 121 | * Display nested objects. 122 | 123 | 1.1.3 / 2015-06-12 124 | ================== 125 | 126 | * Prefer atom link elements with type=text/html 127 | 128 | 1.1.2 / 2015-06-02 129 | ================== 130 | 131 | * Be more careful about assigning item.link from atom:link elements 132 | 133 | 1.1.1 / 2015-05-28 134 | ================== 135 | 136 | * Add license attribute 137 | 138 | 1.1.0 / 2015-05-21 139 | ================== 140 | 141 | * Fix channel link selection when there is a mixture of rss and atom. Closes #142 142 | 143 | 1.0.1 / 2015-04-07 144 | ================== 145 | 146 | * Fix category parsing to avoid null in results. Resolves #136 147 | 148 | 1.0.0 / 2015-02-26 149 | ================== 150 | 151 | * Bump mocha devDependency to v2.1.x 152 | * Cleanup package.json 153 | * Update copyright year in README 154 | * Remove node v0.8 support 155 | * Merge pull request #134 from designfrontier/master 156 | * added a testing environment for node v0.12 157 | * removed resanitize as a dependency since the only thing in use was a 4 line function. Moved the function to utils 158 | 159 | v0.19.2 / 2014-09-02 160 | ================== 161 | 162 | * Change ispermalink value check to be case-insensitive. Closes #123. 163 | * Whoops. Remove debugging from example 164 | 165 | v0.19.1 / 2014-07-31 166 | ================== 167 | 168 | * Add compressed example 169 | * Refactor iconv example 170 | 171 | v0.19.0 / 2014-07-30 172 | ================== 173 | 174 | * Remove unnecessary code to trigger saxparser error. Apparently, calling the callback with an error will trigger an error anyway. Totally undocumented. So, this was actually calling double error emitting. 175 | * Manually trigger end when an exception is caught. We can't continue parsing after an exception is thrown. Also update test. 176 | * Use native try/catch. Other method is not a performance enhancement. 177 | * Wrap sax write and end methods in try/catch. Resolves #112 sax >= v0.6.0 can throw if a gzipped data stream containing certain characters gets written to the parser. This is a user error (to pipe gzipped data), but sometimes servers send gzipped data even when you've told them not to. So, we try to let the user handle this more gracefully. 178 | * Add failing test case for sax throwing 179 | 180 | v0.18.1 / 2014-06-20 181 | ================== 182 | 183 | * Don't assume el is not an array when defining attrs hash. Resolves #113 184 | * Add failing test for #113 185 | 186 | v0.18.0 / 2014-06-18 187 | ================== 188 | 189 | * Enforce de-duping on atom enclosures 190 | * Fix modification by reference defeating indexOf checking 191 | * Fix inverted index checking 192 | * Update test and fixture with tougher test case suggested by #111 193 | * Revert "test for different enclosure type" 194 | * test for different enclosure type 195 | 196 | v0.17.0 / 2014-05-27 197 | ================== 198 | 199 | * Improve tests 200 | * Use readable-stream instead of core stream; update dependencies. 201 | * Update README 202 | * Add permalink property for RSS feeds 203 | * Add nodeico badge 204 | * Remove unnecessary test server 205 | * Only colorize dump output if outputing to a terminal. 206 | * Fix small typo. 207 | 208 | v0.16.6 / 2014-02-12 209 | ================== 210 | 211 | * Update README to improve example code. 212 | * Fix error check in handleEnd method. 213 | * Remove unused dependency. 214 | * Add to namespaces and prettify. 215 | * Update iconv example to remove event-stream dependency. 216 | * Cleanup iconv example 217 | * Add gitignore 218 | * Merge branch 'kof-iconv' 219 | * Refactor iconv example to be more explicit. 220 | * Create a localhost server for example. 221 | * Refactor getParams method. 222 | * Move tips for url fetching to example script 223 | * Remove gitignore 224 | * complicated example using iconv and request 225 | 226 | v0.16.5 / 2013-12-29 227 | ================== 228 | 229 | * Workaround addressparser failing to parse strings ending with a colon. Closes #94. 230 | 231 | v0.16.4 / 2013-12-26 232 | ================== 233 | 234 | * Fix bad logic setting meta.image properties. 235 | * Fix TypeError in utils.reresolve failing to check for existence of parameter. Resolves #92. 236 | 237 | v0.16.3 / 2013-10-27 238 | ================== 239 | 240 | * Merge remote-tracking branch 'PaulMougel/master' 241 | * Updated readable side highWaterMark to be forward-compatible with node. 242 | * Improved stream watermark and buffering. 243 | * Reduced memory consumption. 244 | 245 | v0.16.2 / 2013-10-08 246 | ================== 247 | 248 | * Bump dependencies 249 | * Merge pull request #75 from jcrugzz/request-depend 250 | * [fix] remove unneeded dependency `request` 251 | * Update README.md 252 | * Update example code 253 | 254 | v0.16.1 / 2013-06-13 255 | ================== 256 | 257 | * Update travis config 258 | * Only emit meta once. title is a required channel element, so a feed without it is broken, but emitting more than once is still a no-no. Closes #69 259 | * Bump version: v0.16.0 260 | * Update README 261 | * Remove legacy libxml-like helpers 262 | * Update dump script 263 | * Update examples 264 | * Update tests 265 | * Emit SAXErrors and allow consumer to handle or bail on SAXErrors 266 | * Update copyright notices 267 | * Merge branch 'AndreasMadsen-transform-stream' 268 | * Change stream test to not require additional dependency 269 | * make feedparser a transform stream 270 | 271 | v0.16.0 / 2013-06-11 272 | ================== 273 | 274 | * Update README 275 | * Remove legacy libxml-like helpers 276 | * Update dump script 277 | * Update examples 278 | * Update tests 279 | * Emit SAXErrors and allow consumer to handle or bail on SAXErrors 280 | * Update copyright notices 281 | * Merge branch 'AndreasMadsen-transform-stream' 282 | * Change stream test to not require additional dependency 283 | * make feedparser a transform stream 284 | 285 | v0.15.8 / 2013-10-08 286 | ================== 287 | 288 | * Fix package.json 289 | 290 | v0.15.7 / 2013-09-26 291 | ================== 292 | 293 | * Bump dependencies 294 | 295 | v0.15.6 / 2013-09-24 296 | ================== 297 | 298 | * Bump dependencies 299 | * Update travis config 300 | 301 | v0.15.5 / 2013-06-13 302 | ================== 303 | 304 | * Only emit meta once. title is a required channel element, so a feed without it is broken, but emitting more than once is still a no-no. Closes #69 305 | * Update copyright notices 306 | 307 | v0.15.4 / 2013-06-04 308 | ================== 309 | 310 | * Fix processing instruction handler to avoid interpretting extraneouso whitespace as attribute names. 311 | * Use item source for xmlurl, if absent. Closes #63 312 | * Add more xml:base fallbacks. Resolves #64 313 | * Merge branch 'unexpected-arrays' 314 | * Fix date parsing. Don't trust that the dates are not arrays. 315 | * Make tests run on v0.10. Closes #61. 316 | 317 | v0.15.3 / 2013-05-05 318 | ================== 319 | 320 | * Update README to point to contributors graph 321 | * Merge pull request #59 from AndreasMadsen/rss-category 322 | * do not seperate rss catgories by comma 323 | 324 | v0.15.2 / 2013-04-16 325 | ================== 326 | 327 | * Be more forgiving of poorly-formatted feeds. Closes #58 328 | 329 | v0.15.1 / 2013-04-15 330 | ================== 331 | 332 | * Fix for no Content-Type header 333 | 334 | v0.15.0 / 2013-04-11 335 | ================== 336 | 337 | * Tweak #content-type; add #xml to meta 338 | * Tweak stream api test 339 | * Fix missing scope 340 | * Linting 341 | * Fix typo in README code example 342 | * Update README to add link to Issues page and IRC 343 | 344 | v0.14.0 / 2013-03-25 345 | ================== 346 | 347 | * Update examples 348 | * Update README 349 | * Remove nextEmit. Only use nextTick on parseString (other methods don't need it). 350 | * Remove _setCallback and set the callback directly. Don't use nextTick. 351 | * Add basic test for writable stream input api 352 | * Add basic tests for callback and event apis 353 | * Implement naive v0.8-style Stream API 354 | * Fix README (incorrect stream pipe examples) 355 | * Merge pull request #52 from supahgreg/master 356 | * Correcting a typo in README.md 357 | 358 | v0.13.4 / 2013-03-15 359 | ================== 360 | 361 | * Fix unsafe usage of 'in' when variable may be not an object. Closes #51. 362 | 363 | v0.13.3 / 2013-03-14 364 | ================== 365 | 366 | * Fix reresolve function to not assume that node property is a string. Closes #50. 367 | 368 | v0.13.2 / 2013-02-21 369 | ================== 370 | 371 | * Fix issue where namespaced elements with the same local part as a root element were being treated as having the save name, e.g., atom:link in an rss feed being part of the 'link' element. 372 | * Remove stray console.log from test 373 | 374 | v0.13.1 / 2013-02-21 375 | ================== 376 | 377 | * Deal with the astonishing fact that someone thinks a feed with 4 diffenet cloud/pubsubhubub elements is helpful. Resolves #49. 378 | 379 | v0.13.0 / 2013-02-18 380 | ================== 381 | 382 | * Remove old API. Update docs, examples and tests. 383 | * Fix .parseUrl url parameter processing. Throw early if no valid url is given. Also pass all options to request. Add tests. Closes #44 and #46. 384 | * Add url to error when possible. Change "Not a feed" error message because it's not always a remote server. Update tests. Closes #43." 385 | * Raise default sax.MAX_BUFFER_LENGTH to 16M and allow it to be set in options. Closes #38. 386 | * Strip HTML from `meta.title`, `meta.description` and `item.title` 387 | 388 | v0.12.0 / 2013-02-12 389 | ================== 390 | 391 | * Expose rssCloud/pubsubhubbub on `meta.cloud` property. Resolves #47. 392 | * Expose "has" util 393 | 394 | v0.11.0 / 2013-02-03 395 | ================== 396 | 397 | * Dedupe enclosures. Resolves #45. 398 | * Change test to be more lenient about which error code is returned as it seems to differ for no known reason 399 | * Drop support for node pre-v0.8.x 400 | * Refactor tests to not fetch remote URLs 401 | * Tell TravisCI to only run tests on master 402 | * Enable silencing the deprecation warnings 403 | 404 | v0.10.13 / 2013-01-08 405 | ================== 406 | 407 | * Bump sax version 408 | 409 | v0.10.12 / 2012-12-31 410 | ================== 411 | 412 | * Expose HTTP response on FeedParser instance 413 | 414 | v0.10.11 / 2012-12-28 415 | ================== 416 | 417 | * Update tests 418 | * Change HTTP Content-Type head checking to allow parsing valid feeds with incorrect Content-Type header. Add value of Content-Type header to meta. 419 | 420 | v0.10.10 / 2012-12-28 421 | ================== 422 | 423 | * Add example and test for passing request headers to .parseUrl() 424 | * Enable FeedParser.parseUrl to accept a Request object with headers 425 | * Update utils.merge() to be safer about relying on Object properties 426 | * Skip failing test that's not failing. Maybe the remote server changed something. 427 | * Cleanup 5f642af. Don't overwrite media:thumbnail array. 428 | * Increase test timeout. Fix incorrect test usage of deepEqual instead of strictEqual. 429 | * Merge pull request #41 from rborn/master 430 | * fix for multiple media:thumbnail 431 | * Add test for fetching uncompressed feed. 432 | 433 | v.0.10.9 / 2012-12-03 434 | ================== 435 | 436 | * Add "Accept-Encoding: identity" header on HTTP requests to only fetch uncompressed data. Resolves issue #36. 437 | * Merge pull request #37 from jchris/patch-1 438 | * make example work with new api 439 | 440 | v0.10.8 / 2012-11-06 441 | ================== 442 | 443 | * Ensure we only emit `end` once. Bump version. 444 | * Change FeedParser.parseStream so it doesn't try to attach to a `stream` that is not defined. A user could pass in a stream thinking it's valid, but the stream has been destroyed. Try not to throw. 445 | * Change FeedParser#handleError to not remove 'error' listeners on `this.stream` 446 | 447 | v0.10.7 / 2012-11-01 448 | ================== 449 | 450 | * Fix issue #34 .parseString() emitting too soon. All `emit()` and `callback()` are wrapped in `process.nextTick()`. Bump version. 451 | 452 | v0.10.6 / 2012-10-27 453 | ================== 454 | 455 | * Fix issue #33 uncaught exception trying to get the text string for an HTTP status code. 456 | 457 | v0.10.5 / 2012-10-26 458 | ================== 459 | 460 | * Bump version. Update README with additional dependency. Add History.md. 461 | * Fix issue #32 - parse RSS item:author. Enhance RSS authorish elements with parsed properties via addressparser. 462 | 463 | v0.10.4 / 2012-10-25 464 | ================== 465 | 466 | * Bump version 467 | * Fix major bug in parseString, parseFile, and parseStream -- failed to return the event emitter. 468 | * Refactor dump script to use new API 469 | * Fix dump script for API change 470 | 471 | v0.10.3 / 2012-10-24 472 | ================== 473 | 474 | * Bump version 475 | * Update documentation 476 | * Rename 'notModified' event to '304' 477 | * Add deprecation warnings to prototype methods. Reorganize .parseUrl and handleResponse. 478 | * Update tests for new static methods 479 | * Fix initialization of saxstream. Rename parser to feedparser. Add doc to parseString static. 480 | * Refactor options and init parsing. Refine error handling. Fix bug in handleSaxError. Add static methods for parseString, parseFile and parseStream. 481 | * Initial refactor of error handling 482 | * Reorganize some code 483 | * Rename FeedParser#_reset to FeedParser#init 484 | * Change module.exports to use an instance of FeedParser. Add non-prototype-based parseUrl. 485 | * :gem: Travis CI image/link in readme :gem: 486 | * :gem: Added travis.yml file :gem: 487 | 488 | v0.10.2 / 2012-10-17 489 | ================== 490 | 491 | * Add static callback methods 492 | * Move reresolve to utils 493 | * Update inline documentation of public api 494 | * Bump version 495 | * Refactor (part 2) to eliminate scope-passing (just moves things around in the class) 496 | * Refactor (part 1) to eliminate scope-passing 497 | 498 | v0.10.1 / 2012-10-05 499 | ================== 500 | 501 | * Bump version. Fix issue #25; add test. Add ability to pass "strict" boolean option to Sax. 502 | * Fix failing test 503 | 504 | v0.10.0-beta / 2012-09-13 505 | ================== 506 | 507 | * Mark package as beta version 508 | * Add more namespaces and sort sort-of alphabetically 509 | * Add brief description and usage info 510 | * Bump version 511 | * Add more namespaces 512 | * Handle namespaced elements that use nondefault namespace prefixes 513 | * Add more namespace-awareness tests 514 | * Add test for issue #23 (non-default namespaces) 515 | * Refactor to handle use of nondefault namespaces 516 | * Add Makefile to run tests 517 | * Add nsprefix function for getting the "default" prefix for a given namespace uri. 518 | * Add 'xml' to default namespaces lookup table. 519 | * Add nslookup function for checking whether a uri matches the default for a namespace. 520 | * Add default namespaces lookup table. 521 | * Add script to dump parsed feeds to console. Useful for debugging. 522 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ---------------------------------------------------------------------- 2 | node-feedparser is released under the MIT License 3 | Copyright (c) 2011-2018 Dan MacTough and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Feedparser - Robust RSS, Atom, and RDF feed parsing in Node.js 2 | 3 | [![Greenkeeper badge](https://badges.greenkeeper.io/danmactough/node-feedparser.svg)](https://greenkeeper.io/) 4 | 5 | [![Join the chat at https://gitter.im/danmactough/node-feedparser](https://badges.gitter.im/danmactough/node-feedparser.svg)](https://gitter.im/danmactough/node-feedparser?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 6 | 7 | [![Build Status](https://secure.travis-ci.org/danmactough/node-feedparser.png?branch=master)](https://travis-ci.org/danmactough/node-feedparser) 8 | 9 | [![NPM](https://nodei.co/npm/feedparser.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/feedparser/) 10 | 11 | Feedparser is for parsing RSS, Atom, and RDF feeds in node.js. 12 | 13 | It has a couple features you don't usually see in other feed parsers: 14 | 15 | 1. It resolves relative URLs (such as those seen in Tim Bray's "ongoing" [feed](https://www.tbray.org/ongoing/ongoing.atom)). 16 | 2. It properly handles XML namespaces (including those in unusual feeds 17 | that define a non-default namespace for the main feed elements). 18 | 19 | ## Installation 20 | 21 | ```bash 22 | npm install feedparser 23 | ``` 24 | 25 | ## Usage 26 | 27 | This example is just to briefly demonstrate basic concepts. 28 | 29 | **Please** also review the [complete example](examples/complete.js) for a 30 | thorough working example that is a suitable starting point for your app. 31 | 32 | ```js 33 | 34 | var FeedParser = require('feedparser'); 35 | var fetch = require('node-fetch'); // for fetching the feed 36 | 37 | var req = fetch('http://somefeedurl.xml') 38 | var feedparser = new FeedParser([options]); 39 | 40 | req.then(function (res) { 41 | if (res.status !== 200) { 42 | throw new Error('Bad status code'); 43 | } 44 | else { 45 | // The response `body` -- res.body -- is a stream 46 | res.body.pipe(feedparser); 47 | } 48 | }, function (err) { 49 | // handle any request errors 50 | }); 51 | 52 | feedparser.on('error', function (error) { 53 | // always handle errors 54 | }); 55 | 56 | feedparser.on('readable', function () { 57 | // This is where the action is! 58 | var stream = this; // `this` is `feedparser`, which is a stream 59 | var meta = this.meta; // **NOTE** the "meta" is always available in the context of the feedparser instance 60 | var item; 61 | 62 | while (item = stream.read()) { 63 | console.log(item); 64 | } 65 | }); 66 | 67 | ``` 68 | 69 | You can also check out this nice [working implementation](https://github.com/scripting/feedRead) that demonstrates one way to handle all the hard and annoying stuff. :smiley: 70 | 71 | ### options 72 | 73 | - `normalize` - Set to `false` to override Feedparser's default behavior, 74 | which is to parse feeds into an object that contains the generic properties 75 | patterned after (although not identical to) the RSS 2.0 format, regardless 76 | of the feed's format. 77 | 78 | - `addmeta` - Set to `false` to override Feedparser's default behavior, which 79 | is to add the feed's `meta` information to each article. 80 | 81 | - `feedurl` - The url (string) of the feed. FeedParser is very good at 82 | resolving relative urls in feeds. But some feeds use relative urls without 83 | declaring the `xml:base` attribute any place in the feed. This is perfectly 84 | valid, but we don't know know the feed's url before we start parsing the feed 85 | and trying to resolve those relative urls. If we discover the feed's url, we 86 | will go back and resolve the relative urls we've already seen, but this takes 87 | a little time (not much). If you want to be sure we never have to re-resolve 88 | relative urls (or if FeedParser is failing to properly resolve relative urls), 89 | you should set the `feedurl` option. Otherwise, feel free to ignore this option. 90 | 91 | - `resume_saxerror` - Set to `false` to override Feedparser's default behavior, which 92 | is to emit any `SAXError` on `error` and then automatically resume parsing. In 93 | my experience, `SAXErrors` are not usually fatal, so this is usually helpful 94 | behavior. If you want total control over handling these errors and optionally 95 | aborting parsing the feed, use this option. 96 | 97 | ## Examples 98 | 99 | See the [`examples`](examples/) directory. 100 | 101 | ## API 102 | 103 | ### Transform Stream 104 | 105 | Feedparser is a [transform stream](https://nodejs.org/api/stream.html#stream_class_stream_transform) operating in "object mode": XML in -> Javascript objects out. 106 | Each readable chunk is an object representing an article in the feed. 107 | 108 | ### Events Emitted 109 | 110 | * `meta` - called with feed `meta` when it has been parsed 111 | * `error` - called with `error` whenever there is a Feedparser error of any kind (SAXError, Feedparser error, etc.) 112 | 113 | ## What is the parsed output produced by feedparser? 114 | 115 | Feedparser parses each feed into a `meta` (emitted on the `meta` event) portion 116 | and one or more `articles` (emited on the `data` event or readable after the `readable` 117 | is emitted). 118 | 119 | Regardless of the format of the feed, the `meta` and each `article` contain a 120 | uniform set of generic properties patterned after (although not identical to) 121 | the RSS 2.0 format, as well as all of the properties originally contained in the 122 | feed. So, for example, an Atom feed may have a `meta.description` property, but 123 | it will also have a `meta['atom:subtitle']` property. 124 | 125 | The purpose of the generic properties is to provide the user a uniform interface 126 | for accessing a feed's information without needing to know the feed's format 127 | (i.e., RSS versus Atom) or having to worry about handling the differences 128 | between the formats. However, the original information is also there, in case 129 | you need it. In addition, Feedparser supports some popular namespace extensions 130 | (or portions of them), such as portions of the `itunes`, `media`, `feedburner` 131 | and `pheedo` extensions. So, for example, if a feed article contains either an 132 | `itunes:image` or `media:thumbnail`, the url for that image will be contained in 133 | the article's `image.url` property. 134 | 135 | All generic properties are "pre-initialized" to `null` (or empty arrays or 136 | objects for certain properties). This should save you from having to do a lot of 137 | checking for `undefined`, such as, for example, when you are using jade 138 | templates. 139 | 140 | In addition, all properties (and namespace prefixes) use only lowercase letters, 141 | regardless of how they were capitalized in the original feed. ("xmlUrl" and 142 | "pubDate" also are still used to provide backwards compatibility.) This decision 143 | places ease-of-use over purity -- hopefully, you will never need to think about 144 | whether you should camelCase "pubDate" ever again. 145 | 146 | The `title` and `description` properties of `meta` and the `title` property of 147 | each `article` have any HTML stripped if you let feedparser normalize the output. 148 | If you really need the HTML in those elements, there are always the originals: 149 | e.g., `meta['atom:subtitle']['#']`. 150 | 151 | ### List of meta properties 152 | 153 | * title 154 | * description 155 | * link (website link) 156 | * xmlurl (the canonical link to the feed, as specified by the feed) 157 | * date (most recent update) 158 | * pubdate (original published date) 159 | * author 160 | * language 161 | * image (an Object containing `url` and `title` properties) 162 | * favicon (a link to the favicon -- only provided by Atom feeds) 163 | * copyright 164 | * generator 165 | * categories (an Array of Strings) 166 | 167 | ### List of article properties 168 | 169 | * title 170 | * description (frequently, the full article content) 171 | * summary (frequently, an excerpt of the article content) 172 | * link 173 | * origlink (when FeedBurner or Pheedo puts a special tracking url in the `link` property, `origlink` contains the original link) 174 | * permalink (when an RSS feed has a `guid` field and the `isPermalink` attribute is not set to `false`, `permalink` contains the value of `guid`) 175 | * date (most recent update) 176 | * pubdate (original published date) 177 | * author 178 | * guid (a unique identifier for the article) 179 | * comments (a link to the article's comments section) 180 | * image (an Object containing `url` and `title` properties) 181 | * categories (an Array of Strings) 182 | * source (an Object containing `url` and `title` properties pointing to the original source for an article; see the [RSS Spec](https://cyber.law.harvard.edu/rss/rss.html#ltsourcegtSubelementOfLtitemgt) for an explanation of this element) 183 | * enclosures (an Array of Objects, each representing a podcast or other enclosure and having a `url` property and possibly `type` and `length` properties) 184 | * meta (an Object containing all the feed meta properties; especially handy when using the EventEmitter interface to listen to `article` emissions) 185 | 186 | ## Help 187 | 188 | - Don't be afraid to report an [issue](https://github.com/danmactough/node-feedparser/issues). 189 | - You can drop by [Gitter](https://gitter.im/danmactough/node-feedparser?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge), too. 190 | 191 | ## Contributors 192 | 193 | View all the [contributors](https://github.com/danmactough/node-feedparser/graphs/contributors). 194 | 195 | Although `node-feedparser` no longer shares any code with `node-easyrss`, it was 196 | the original inspiration and a starting point. 197 | 198 | ## License 199 | 200 | (The MIT License) 201 | 202 | Copyright (c) 2011-2020 Dan MacTough and contributors 203 | 204 | Permission is hereby granted, free of charge, to any person obtaining a copy of 205 | this software and associated documentation files (the 'Software'), to deal in 206 | the Software without restriction, including without limitation the rights to 207 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 208 | the Software, and to permit persons to whom the Software is furnished to do so, 209 | subject to the following conditions: 210 | 211 | The above copyright notice and this permission notice shall be included in all 212 | copies or substantial portions of the Software. 213 | 214 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 215 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 216 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 217 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 218 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 219 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 220 | -------------------------------------------------------------------------------- /bin/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": [ 4 | 0 5 | ] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /bin/feedparser.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* 3 | * Parse a feed and dump the result to the console 4 | * 5 | * Usage: curl | bin/feedparser.js 6 | * cat | bin/feedparser.js 7 | * 8 | */ 9 | var isatty = require('tty').isatty 10 | , util = require('util') 11 | , FeedParser = require('../'); 12 | 13 | var usingConsole = isatty(1) && isatty(2); 14 | 15 | var argv = require('mri')(process.argv.slice(2), { 16 | alias: { 17 | u: 'feedurl', 18 | g: 'group', 19 | j: 'json' 20 | }, 21 | boolean: [ 22 | 'normalize', 23 | 'addmeta', 24 | 'resume_sax_error', 25 | 'json' 26 | ], 27 | default: { 28 | normalize: true, 29 | addmeta: true, 30 | resume_saxerror: true, 31 | json: !usingConsole 32 | } 33 | }); 34 | 35 | var items = []; 36 | 37 | process.stdin.pipe(new FeedParser(argv)) 38 | .on('error', console.error) 39 | .on('readable', function() { 40 | var stream = this, item; 41 | while (item = stream.read()) { 42 | if (argv.group) { 43 | items.push(item); 44 | } 45 | else { 46 | if (argv.json) { 47 | console.log(JSON.stringify(item)); 48 | } 49 | else { 50 | console.log(util.inspect(item, null, 10, true)); 51 | } 52 | } 53 | } 54 | }) 55 | .on('end', function () { 56 | if (argv.group) { 57 | if (argv.json) { 58 | console.log(JSON.stringify(items)); 59 | } 60 | else { 61 | console.log(util.inspect(items, null, 10, true)); 62 | } 63 | } 64 | }); 65 | -------------------------------------------------------------------------------- /examples/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-console": [ 4 | 0 5 | ] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/complete.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Tip 3 | * ==== 4 | * - Set `user-agent` and `accept` headers when sending requests. Some services will not respond as expected without them. 5 | */ 6 | 7 | var fetch = require('node-fetch') 8 | , FeedParser = require(__dirname+'/..') 9 | , iconv = require('iconv-lite'); 10 | 11 | function get(feed) { 12 | // Get a response stream 13 | fetch(feed, { 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36', 'accept': 'text/html,application/xhtml+xml' }).then(function (res) { 14 | 15 | // Setup feedparser stream 16 | var feedparser = new FeedParser(); 17 | feedparser.on('error', done); 18 | feedparser.on('end', done); 19 | feedparser.on('readable', function() { 20 | var post; 21 | while (post = this.read()) { 22 | console.log(JSON.stringify(post, ' ', 4)); 23 | } 24 | }); 25 | 26 | // Handle our response and pipe it to feedparser 27 | if (res.status != 200) throw new Error('Bad status code'); 28 | var charset = getParams(res.headers.get('content-type') || '').charset; 29 | var responseStream = res.body; 30 | responseStream = maybeTranslate(responseStream, charset); 31 | // And boom goes the dynamite 32 | responseStream.pipe(feedparser); 33 | 34 | }).catch(done); 35 | } 36 | 37 | function maybeTranslate (res, charset) { 38 | var iconvStream; 39 | // Decode using iconv-lite if its not utf8 already. 40 | if (!iconvStream && charset && !/utf-*8/i.test(charset)) { 41 | try { 42 | iconvStream = iconv.decodeStream(charset); 43 | console.log('Converting from charset %s to utf-8', charset); 44 | iconvStream.on('error', done); 45 | // If we're using iconvStream, stream will be the output of iconvStream 46 | // otherwise it will remain the output of request 47 | res = res.pipe(iconvStream); 48 | } catch(err) { 49 | res.emit('error', err); 50 | } 51 | } 52 | return res; 53 | } 54 | 55 | function getParams(str) { 56 | var params = str.split(';').reduce(function (params, param) { 57 | var parts = param.split('=').map(function (part) { return part.trim(); }); 58 | if (parts.length === 2) { 59 | params[parts[0]] = parts[1]; 60 | } 61 | return params; 62 | }, {}); 63 | return params; 64 | } 65 | 66 | function done(err) { 67 | if (err) { 68 | console.log(err, err.stack); 69 | return process.exit(1); 70 | } 71 | server.close(); 72 | process.exit(); 73 | } 74 | 75 | // Don't worry about this. It's just a localhost file server so you can be 76 | // certain the "remote" feed is available when you run this example. 77 | var server = require('http').createServer(function (req, res) { 78 | var stream = require('fs').createReadStream(require('path').resolve(__dirname, '../test/feeds' + req.url)); 79 | res.setHeader('Content-Type', 'text/xml; charset=Windows-1251'); 80 | res.setHeader('Content-Encoding', 'gzip'); 81 | stream.pipe(res); 82 | }); 83 | server.listen(0, function () { 84 | get('http://localhost:' + this.address().port + '/compressed.xml'); 85 | }); 86 | -------------------------------------------------------------------------------- /examples/simple.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * node-feedparser 3 | * Copyright(c) 2013 Dan MacTough 4 | * MIT Licensed 5 | */ 6 | 7 | var FeedParser = require(__dirname+'/..') 8 | , fs = require('fs') 9 | , feed = __dirname+'/../test/feeds/rss2sample.xml'; 10 | 11 | fs.createReadStream(feed) 12 | .on('error', function (error) { 13 | console.error(error); 14 | }) 15 | .pipe(new FeedParser()) 16 | .on('error', function (error) { 17 | console.error(error); 18 | }) 19 | .on('meta', function (meta) { 20 | console.log('===== %s =====', meta.title); 21 | }) 22 | .on('readable', function() { 23 | var stream = this, item; 24 | while (item = stream.read()) { 25 | console.log('Got article: %s', item.title || item.description); 26 | } 27 | }); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/feedparser'); 2 | -------------------------------------------------------------------------------- /lib/feedparser/index.js: -------------------------------------------------------------------------------- 1 | /********************************************************************** 2 | node-feedparser - A robust RSS, Atom, RDF parser for node. 3 | http://github.com/danmactough/node-feedparser 4 | Copyright (c) 2011-2018 Dan MacTough and contributors 5 | http://mact.me 6 | 7 | **********************************************************************/ 8 | 9 | /** 10 | * Module dependencies. 11 | */ 12 | var sax = require('sax') 13 | , addressparser = require('addressparser') 14 | , indexOfObject = require('array-indexofobject') 15 | , util = require('util') 16 | , TransformStream = require('readable-stream').Transform 17 | , _ = require('../utils'); 18 | 19 | /** 20 | * FeedParser constructor. 21 | * 22 | * Exposes a duplex (transform) stream to parse a feed. 23 | * 24 | * Each article/post in the feed will have the following keys: 25 | * - title {String} 26 | * - description {String} 27 | * - summary {String} 28 | * - date {Date} (or null) 29 | * - pubdate {Date} (or null) 30 | * - link {String} 31 | * - origlink {String} 32 | * - author {String} 33 | * - guid {String} 34 | * - comments {String} 35 | * - image {Object} 36 | * - categories {Array} 37 | * - source {Object} 38 | * - enclosures {Array} 39 | * - meta {Object} 40 | * - Object.keys(meta): 41 | * - #ns {Array} key,value pairs of each namespace declared for the feed 42 | * - #type {String} one of 'atom', 'rss', 'rdf' 43 | * - #version {String} 44 | * - title {String} 45 | * - description {String} 46 | * - date {Date} (or null) 47 | * - pubdate {Date} (or null) 48 | * - link {String} i.e., to the website, not the feed 49 | * - xmlurl {String} the canonical URL of the feed, as declared by the feed 50 | * - author {String} 51 | * - language {String} 52 | * - image {Object} 53 | * - favicon {String} 54 | * - copyright {String} 55 | * - generator {String} 56 | * - categories {Array} 57 | * 58 | * @param {Object} options 59 | * @api public 60 | */ 61 | function FeedParser (options) { 62 | if (!(this instanceof FeedParser)) return new FeedParser(options); 63 | TransformStream.call(this); 64 | this._readableState.objectMode = true; 65 | this._readableState.highWaterMark = 16; // max. # of output nodes buffered 66 | 67 | this.init(); 68 | 69 | // Parse options 70 | this.options = _.assign({}, options); 71 | if (!('strict' in this.options)) this.options.strict = false; 72 | if (!('normalize' in this.options)) this.options.normalize = true; 73 | if (!('addmeta' in this.options)) this.options.addmeta = true; 74 | if (!('resume_saxerror' in this.options)) this.options.resume_saxerror = true; 75 | if ('MAX_BUFFER_LENGTH' in this.options) { 76 | sax.MAX_BUFFER_LENGTH = this.options.MAX_BUFFER_LENGTH; // set to Infinity to have unlimited buffers 77 | } else { 78 | sax.MAX_BUFFER_LENGTH = 16 * 1024 * 1024; // 16M versus the 64K default 79 | } 80 | if (this.options.feedurl) this.xmlbase.unshift({ '#name': 'xml', '#': this.options.feedurl}); 81 | 82 | // See https://github.com/isaacs/sax-js for more info 83 | this.stream = sax.createStream(this.options.strict /* strict mode - no by default */, {lowercase: true, xmlns: true }); 84 | this.stream.on('error', this.handleSaxError.bind(this)); 85 | this.stream.on('processinginstruction', this.handleProcessingInstruction.bind(this)); 86 | this.stream.on('opentag', this.handleOpenTag.bind(this)); 87 | this.stream.on('closetag',this.handleCloseTag.bind(this)); 88 | this.stream.on('text', this.handleText.bind(this)); 89 | this.stream.on('cdata', this.handleText.bind(this)); 90 | this.stream.on('end', this.handleEnd.bind(this)); 91 | } 92 | util.inherits(FeedParser, TransformStream); 93 | 94 | /* 95 | * Initializes the SAX stream 96 | * 97 | * Initializes the class-variables 98 | */ 99 | FeedParser.prototype.init = function (){ 100 | this.meta = { 101 | '#ns': [], 102 | '@': [], 103 | '#xml': {} 104 | }; 105 | this._namespaces = {}; 106 | this._emitted_meta = false; 107 | this.stack = []; 108 | this.xmlbase = []; 109 | this.in_xhtml = false; 110 | this.xhtml = {}; /* Where to store xhtml elements as associative 111 | array with keys: '#' (containing the text) 112 | and '#name' (containing the XML element name) */ 113 | this.errors = []; 114 | }; 115 | 116 | FeedParser.prototype.handleEnd = function (){ 117 | // We made it to the end without throwing, but let's make sure we were actually 118 | // parsing a feed 119 | if (!(this.meta && this.meta['#type'])) { 120 | var e = new Error('Not a feed'); 121 | return this.handleError(e); 122 | } 123 | this.push(null); 124 | }; 125 | 126 | FeedParser.prototype.handleSaxError = function (e) { 127 | this.emit('error', e); 128 | if (this.options.resume_saxerror) { 129 | this.resumeSaxError(); 130 | } 131 | }; 132 | 133 | FeedParser.prototype.resumeSaxError = function () { 134 | if (this.stream._parser) { 135 | this.stream._parser.error = null; 136 | this.stream._parser.resume(); 137 | } 138 | }; 139 | 140 | FeedParser.prototype.handleError = function (e){ 141 | this.emit('error', e); 142 | }; 143 | 144 | // parses the xml declaration, which looks like: 145 | // 146 | FeedParser.prototype.handleProcessingInstruction = function (node) { 147 | if (node.name === 'xml') { 148 | this.meta['#xml'] = node.body.trim().split(/\s+/).reduce(function (map, attr) { 149 | if (attr.indexOf('=') >= 0) { 150 | var parts = attr.split('='); 151 | map[parts[0]] = parts[1] && parts[1].length > 2 && parts[1].match(/^.(.*?).$/)[1]; 152 | } 153 | return map; 154 | }, this.meta['#xml']); 155 | } 156 | }; 157 | 158 | FeedParser.prototype.handleOpenTag = function (node){ 159 | var n = {}; 160 | n['#name'] = node.name; // Avoid namespace collissions later... 161 | n['#prefix'] = node.prefix; // The current ns prefix 162 | n['#local'] = node.local; // The current element name, sans prefix 163 | n['#uri'] = node.uri; // The current ns uri 164 | n['@'] = {}; 165 | n['#'] = ''; 166 | 167 | if (Object.keys(node.attributes).length) { 168 | n['@'] = this.handleAttributes(node.attributes, n['#name']); 169 | } 170 | 171 | if (this.in_xhtml && this.xhtml['#name'] != n['#name']) { // We are in an xhtml node 172 | // This builds the opening tag, e.g.,
173 | this.xhtml['#'] += '<'+n['#name']; 174 | Object.keys(n['@']).forEach(function(name){ 175 | this.xhtml['#'] += ' '+ name +'="'+ n['@'][name] + '"'; 176 | }, this); 177 | this.xhtml['#'] += '>'; 178 | } else if ( this.stack.length === 0 && 179 | (n['#name'] === 'rss' || 180 | (n['#local'] === 'rdf' && _.nslookup([n['#uri']], 'rdf')) || 181 | (n['#local'] === 'feed'&& _.nslookup([n['#uri']], 'atom')) ) ) { 182 | Object.keys(n['@']).forEach(function(name) { 183 | var o = {}; 184 | if (name != 'version') { 185 | o[name] = n['@'][name]; 186 | this.meta['@'].push(o); 187 | } 188 | }, this); 189 | switch(n['#local']) { 190 | case 'rss': 191 | this.meta['#type'] = 'rss'; 192 | this.meta['#version'] = n['@']['version']; 193 | break; 194 | case 'rdf': 195 | this.meta['#type'] = 'rdf'; 196 | this.meta['#version'] = n['@']['version'] || '1.0'; 197 | break; 198 | case 'feed': 199 | this.meta['#type'] = 'atom'; 200 | this.meta['#version'] = n['@']['version'] || '1.0'; 201 | break; 202 | } 203 | } 204 | this.stack.unshift(n); 205 | }; 206 | 207 | FeedParser.prototype.handleCloseTag = function (el){ 208 | var node = { 209 | '#name': el, 210 | '#prefix': '', 211 | '#local' : '' 212 | } 213 | , stdEl 214 | , item 215 | , baseurl 216 | , isIllegallyNested = false 217 | ; 218 | var n = this.stack.shift(); 219 | el = el.split(':'); 220 | 221 | if (el.length > 1 && el[0] === n['#prefix']) { 222 | if (_.nslookup(n['#uri'], 'atom')) { 223 | node['#prefix'] = el[0]; 224 | node['#local'] = el.slice(1).join(':'); 225 | node['#type'] = 'atom'; 226 | } else if (_.nslookup(n['#uri'], 'rdf')) { 227 | node['#prefix'] = el[0]; 228 | node['#local'] = el.slice(1).join(':'); 229 | node['#type'] = 'rdf'; 230 | } else { 231 | node['#prefix'] = _.nsprefix(n['#uri']) || n['#prefix']; 232 | node['#local'] = el.slice(1).join(':'); 233 | } 234 | } else { 235 | node['#local'] = node['#name']; 236 | node['#type'] = _.nsprefix(n['#uri']) || n['#prefix']; 237 | } 238 | delete n['#name']; 239 | delete n['#local']; 240 | delete n['#prefix']; 241 | delete n['#uri']; 242 | 243 | if (this.xmlbase && this.xmlbase.length) { 244 | baseurl = this.xmlbase[0]['#']; 245 | } 246 | 247 | var mayHaveResolvableUrl = ( 248 | ( 249 | (node['#local'] === 'logo' || node['#local'] === 'icon') && node['#type'] === 'atom' 250 | ) || 251 | ( 252 | node['#local'] === 'link' // include rss:link, even though it should _never_ be a relative URL 253 | ) 254 | ); 255 | if (baseurl && mayHaveResolvableUrl) { 256 | // Apply xml:base to these elements as they appear 257 | // rather than leaving it to the ultimate parser 258 | n['#'] = _.resolve(baseurl, n['#']); 259 | } 260 | 261 | if (this.xmlbase.length && (el == this.xmlbase[0]['#name'])) { 262 | void this.xmlbase.shift(); 263 | } 264 | 265 | if (this.in_xhtml) { 266 | if (node['#name'] == this.xhtml['#name']) { // The end of the XHTML 267 | 268 | // Add xhtml data to the container element 269 | n['#'] += this.xhtml['#'].trim(); 270 | // Clear xhtml nodes from the tree 271 | for (var key in n) { 272 | if (key != '@' && key != '#') { 273 | delete n[key]; 274 | } 275 | } 276 | this.xhtml = {}; 277 | this.in_xhtml = false; 278 | } else { // Somewhere in the middle of the XHTML 279 | this.xhtml['#'] += ''; 280 | } 281 | } 282 | 283 | if ('#' in n) { 284 | if (n['#'].match(/^\s*$/)) { 285 | // Delete text nodes with nothing by whitespace 286 | delete n['#']; 287 | } else { 288 | n['#'] = n['#'].trim(); 289 | if (Object.keys(n).length === 1) { 290 | // If there is only one text node, hoist it 291 | n = n['#']; 292 | } 293 | } 294 | } 295 | 296 | if (node['#name'] === 'item' || 297 | node['#name'] === 'entry' || 298 | (node['#local'] === 'item' && (node['#prefix'] === '' || node['#type'] === 'rdf')) || 299 | (node['#local'] == 'entry' && (node['#prefix'] === '' || node['#type'] === 'atom'))) { // We have an article! 300 | 301 | isIllegallyNested = ( 302 | ( node['#name'] === 'item' && this.stack[0]['#name'] === 'item' ) || 303 | ( node['#name'] === 'entry' && this.stack[0]['#name'] === 'entry' ) || 304 | ( (node['#local'] === 'item' && (node['#prefix'] === '' || node['#type'] === 'rdf')) && this.stack[0]['#name'] === 'item' ) || 305 | ( (node['#local'] == 'entry' && (node['#prefix'] === '' || node['#type'] === 'atom')) && this.stack[0]['#name'] === 'entry' ) 306 | ); 307 | 308 | if (isIllegallyNested) { 309 | return; 310 | } 311 | 312 | if (!this.meta.title) { // We haven't yet parsed all the metadata 313 | _.assign(this.meta, this.handleMeta(this.stack[0], this.meta['#type'], this.options)); 314 | if (!this._emitted_meta) { 315 | this.emit('meta', this.meta); 316 | this._emitted_meta = true; 317 | } 318 | } 319 | if (!baseurl && this.xmlbase && this.xmlbase.length) { // handleMeta was able to infer a baseurl without xml:base or options.feedurl 320 | n = _.reresolve(n, this.xmlbase[0]['#']); 321 | } 322 | item = this.handleItem(n, this.meta['#type'], this.options); 323 | if (this.options.addmeta) { 324 | item.meta = this.meta; 325 | } 326 | if (this.meta.author && !item.author) item.author = this.meta.author; 327 | this.push(item); 328 | } else if (!this.meta.title && // We haven't yet parsed all the metadata 329 | (node['#name'] === 'channel' || 330 | node['#name'] === 'feed' || 331 | (node['#local'] === 'channel' && (node['#prefix'] === '' || node['#type'] === 'rdf')) || 332 | (node['#local'] === 'feed' && (node['#prefix'] === '' || node['#type'] === 'atom')) ) ) { 333 | _.assign(this.meta, this.handleMeta(n, this.meta['#type'], this.options)); 334 | if (!this._emitted_meta) { 335 | this.emit('meta', this.meta); 336 | this._emitted_meta = true; 337 | } 338 | } 339 | 340 | if (this.stack.length > 0) { 341 | if (node['#prefix'] && node['#local'] && !node['#type']) { 342 | stdEl = node['#prefix'] + ':' + node['#local']; 343 | } else if (node['#name'] && node['#type'] && node['#type'] !== this.meta['#type']) { 344 | stdEl = node['#name']; 345 | } else { 346 | stdEl = node['#local'] || node['#name']; 347 | } 348 | if (!Object.prototype.hasOwnProperty.call(this.stack[0], stdEl)) { 349 | this.stack[0][stdEl] = n; 350 | } else if (this.stack[0][stdEl] instanceof Array) { 351 | this.stack[0][stdEl].push(n); 352 | } else { 353 | this.stack[0][stdEl] = [this.stack[0][stdEl], n]; 354 | } 355 | } 356 | }; 357 | 358 | FeedParser.prototype.handleText = function (text){ 359 | if (this.in_xhtml) { 360 | this.xhtml['#'] += text; 361 | } else { 362 | if (this.stack.length) { 363 | if (this.stack[0] && '#' in this.stack[0]) { 364 | this.stack[0]['#'] += text; 365 | } else { 366 | this.stack[0]['#'] = text; 367 | } 368 | } 369 | } 370 | }; 371 | 372 | FeedParser.prototype.handleAttributes = function handleAttributes (attrs, el) { 373 | /* 374 | * Using the sax.js option { xmlns: true } 375 | * attrs is an array of objects (not strings) having the following properties 376 | * name - e.g., xmlns:dc or href 377 | * value 378 | * prefix - the first part of the name of the attribute (before the colon) 379 | * local - the second part of the name of the attribute (after the colon) 380 | * uri - the uri of the namespace 381 | * 382 | */ 383 | 384 | var basepath = '' 385 | , simplifiedAttributes = {} 386 | ; 387 | 388 | if (this.xmlbase && this.xmlbase.length) { 389 | basepath = this.xmlbase[0]['#']; 390 | } 391 | 392 | Object.keys(attrs).forEach(function(key){ 393 | var attr = attrs[key] 394 | , ns = {} 395 | , prefix = '' 396 | ; 397 | if (attr.prefix === 'xmlns') { 398 | if (!(attr.name in this._namespaces)) { 399 | ns[attr.name] = attr.value; 400 | this.meta['#ns'].push(ns); 401 | _.assign(this._namespaces, ns); 402 | } 403 | } 404 | // If the feed is using a non-default prefix, we'll use it, too 405 | // But we force the use of the 'xml' prefix 406 | if (attr.uri && attr.prefix && !_.nslookup(attr.uri, attr.prefix) || _.nslookup(attr.uri, 'xml')) { 407 | prefix = ( _.nsprefix(attr.uri) || attr.prefix ) + ( attr.local ? ':' : '' ); 408 | } 409 | if (basepath && (attr.local == 'href' || attr.local == 'src' || attr.local == 'uri')) { 410 | // Apply xml:base to these elements as they appear 411 | // rather than leaving it to the ultimate parser 412 | attr.value = _.resolve(basepath, attr.value); 413 | } else if (attr.local === 'base' && _.nslookup(attr.uri, 'xml')) { 414 | // Keep track of the xml:base for the current node 415 | if (basepath) { 416 | attr.value = _.resolve(basepath, attr.value); 417 | } 418 | this.xmlbase.unshift({ '#name': el, '#': attr.value}); 419 | } else if (attr.name === 'type' && attr.value === 'xhtml') { 420 | this.in_xhtml = true; 421 | this.xhtml = {'#name': el, '#': ''}; 422 | } 423 | simplifiedAttributes[prefix + attr.local] = attr.value ? attr.value.trim() : ''; 424 | }, this); 425 | return simplifiedAttributes; 426 | }; 427 | 428 | FeedParser.prototype.handleMeta = function handleMeta (node, type, options) { 429 | if (!type || !node) return {}; 430 | 431 | var meta = {} 432 | , normalize = !options || (options && options.normalize) 433 | ; 434 | 435 | if (normalize) { 436 | ['title','description','date', 'pubdate', 'pubDate','link', 'xmlurl', 'xmlUrl','author','language','favicon','copyright','generator'].forEach(function (property){ 437 | meta[property] = null; 438 | }); 439 | meta.cloud = {}; 440 | meta.image = {}; 441 | meta.categories = []; 442 | } 443 | 444 | Object.keys(node).forEach(function(name){ 445 | var el = node[name]; 446 | 447 | if (normalize) { 448 | switch(name){ 449 | case('title'): 450 | meta.title = _.get(el); 451 | break; 452 | case('description'): 453 | case('subtitle'): 454 | meta.description = _.get(el); 455 | break; 456 | case('pubdate'): 457 | case('lastbuilddate'): 458 | case('published'): 459 | case('modified'): 460 | case('updated'): 461 | case('dc:date'): 462 | var date = _.get(el) ? new Date(_.get(el)) : null; 463 | if (!date) break; 464 | if (meta.pubdate === null || name == 'pubdate' || name == 'published') 465 | meta.pubdate = meta.pubDate = date; 466 | if (meta.date === null || name == 'lastbuilddate' || name == 'modified' || name == 'updated') 467 | meta.date = date; 468 | break; 469 | case('link'): 470 | case('atom:link'): 471 | case('atom10:link'): 472 | if (Array.isArray(el)) { 473 | el.forEach(function (link){ 474 | if (link['@']['href']) { // Atom 475 | if (_.get(link['@'], 'rel')) { 476 | if (link['@']['rel'] == 'alternate') { 477 | if (!meta.link) meta.link = link['@']['href']; 478 | } 479 | else if (link['@']['rel'] == 'self') { 480 | meta.xmlurl = meta.xmlUrl = link['@']['href']; 481 | if (_.isAbsoluteUrl(meta.xmlurl) && this.xmlbase && this.xmlbase.length === 0) { 482 | this.xmlbase.unshift({ '#name': 'xml', '#': meta.xmlurl }); 483 | this.stack[0] = _.reresolve(this.stack[0], meta.xmlurl); 484 | } 485 | else if (this.xmlbase && this.xmlbase.length > 0) { 486 | meta.xmlurl = meta.xmlUrl = _.resolve(_.get(this.xmlbase[0], '#'), meta.xmlurl); 487 | } 488 | } 489 | else if (link['@']['rel'] == 'hub' && !(meta.cloud.href || meta.cloud.domain)) { 490 | meta.cloud.type = 'hub'; 491 | meta.cloud.href = link['@']['href']; 492 | } 493 | } else { 494 | if (!meta.link) meta.link = link['@']['href']; 495 | } 496 | } else if (Object.keys(link['@']).length === 0) { // RSS 497 | meta.link = _.get(link); 498 | } 499 | if (_.isAbsoluteUrl(meta.link) && this.xmlbase && this.xmlbase.length === 0) { 500 | this.xmlbase.unshift({ '#name': 'xml', '#': meta.link}); 501 | this.stack[0] = _.reresolve(this.stack[0], meta.link); 502 | } 503 | else if (this.xmlbase && this.xmlbase.length > 0) { 504 | meta.link = _.resolve(_.get(this.xmlbase[0], '#'), meta.link); 505 | } 506 | }, this); 507 | } else { 508 | if (el['@']['href']) { // Atom 509 | if (_.get(el['@'], 'rel')) { 510 | if (el['@']['rel'] == 'alternate') { 511 | if (!meta.link) meta.link = el['@']['href']; 512 | } 513 | else if (el['@']['rel'] == 'self') { 514 | meta.xmlurl = meta.xmlUrl = el['@']['href']; 515 | if (_.isAbsoluteUrl(meta.xmlurl) && this.xmlbase && this.xmlbase.length === 0) { 516 | this.xmlbase.unshift({ '#name': 'xml', '#': meta.xmlurl}); 517 | this.stack[0] = _.reresolve(this.stack[0], meta.xmlurl); 518 | } 519 | else if (this.xmlbase && this.xmlbase.length > 0) { 520 | meta.xmlurl = meta.xmlUrl = _.resolve(_.get(this.xmlbase[0], '#'), meta.xmlurl); 521 | } 522 | } 523 | else if (el['@']['rel'] == 'hub' && !(meta.cloud.href || meta.cloud.domain)) { 524 | meta.cloud.type = 'hub'; 525 | meta.cloud.href = el['@']['href']; 526 | } 527 | } else { 528 | meta.link = el['@']['href']; 529 | } 530 | } else if (Object.keys(el['@']).length === 0) { // RSS 531 | if (!meta.link) meta.link = _.get(el); 532 | } 533 | if (_.isAbsoluteUrl(meta.link) && this.xmlbase && this.xmlbase.length === 0) { 534 | this.xmlbase.unshift({ '#name': 'xml', '#': meta.link}); 535 | this.stack[0] = _.reresolve(this.stack[0], meta.link); 536 | } 537 | else if (this.xmlbase && this.xmlbase.length > 0) { 538 | meta.link = _.resolve(_.get(this.xmlbase[0], '#'), meta.link); 539 | } 540 | } 541 | break; 542 | case('managingeditor'): 543 | case('webmaster'): 544 | case('author'): 545 | var author = {}; 546 | if (name == 'author') { 547 | meta.author = _.get(el.name) || _.get(el.email) || _.get(el.uri); 548 | } 549 | else if (_.get(el)) { 550 | author = addressparser(_.get(el))[0]; 551 | if (author) { 552 | el['name'] = author.name; 553 | el['email'] = author.address; 554 | } 555 | if (meta.author === null || name == 'managingeditor') { 556 | meta.author = author.name || author.address || _.get(el); 557 | } 558 | } 559 | break; 560 | case('cloud'): 561 | // I can't believe someone actually would put two cloud elements in their channel 562 | // but it happened 563 | // Nevertheless, there can be only one 564 | // This will ensure that rssCloud "wins" here. 565 | // If pubsubhubbub is also declared, it's still available in the link elements 566 | meta.cloud = {}; 567 | if (Array.isArray(el)) { 568 | Object.keys(el[0]['@']).forEach(function (attr) { 569 | if (_.has(el[0]['@'], attr)) { 570 | meta.cloud[attr] = el[0]['@'][attr]; 571 | } 572 | }); 573 | } 574 | else { 575 | Object.keys(el['@']).forEach(function (attr) { 576 | if (_.has(el['@'], attr)) { 577 | meta.cloud[attr] = el['@'][attr]; 578 | } 579 | }); 580 | } 581 | meta.cloud.type = 'rsscloud'; 582 | break; 583 | case('language'): 584 | meta.language = _.get(el); 585 | break; 586 | case('image'): 587 | case('logo'): 588 | if (el.url) 589 | meta.image.url = _.get(el.url); 590 | if (el.title) 591 | meta.image.title = _.get(el.title); 592 | if (!meta.image.url && _.get(el)) 593 | meta.image.url = _.get(el); 594 | break; 595 | case('icon'): 596 | meta.favicon = _.get(el); 597 | break; 598 | case('copyright'): 599 | case('rights'): 600 | case('dc:rights'): 601 | meta.copyright = _.get(el); 602 | break; 603 | case('generator'): 604 | meta.generator = _.get(el); 605 | if (_.get(el['@'], 'version')) 606 | meta.generator += (meta.generator ? ' ' : '') + 'v' + el['@'].version; 607 | if (_.get(el['@'], 'uri')) 608 | meta.generator += meta.generator ? ' (' + el['@'].uri + ')' : el['@'].uri; 609 | break; 610 | case('category'): 611 | case('dc:subject'): 612 | case('itunes:category'): 613 | case('media:category'): 614 | /* We handle all the kinds of categories within the switch loop because meta.categories 615 | * is an array, unlike the other properties, and therefore can handle multiple values 616 | */ 617 | var _category = '' 618 | , _categories = [] 619 | ; 620 | if (Array.isArray(el)) { 621 | el.forEach(function (category){ 622 | var _categoryValue; 623 | if ('category' == name && 'atom' == type) { 624 | if (category['@'] && (_categoryValue = _.safeTrim(_.get(category['@'], 'term')))) { 625 | meta.categories.push(_categoryValue); 626 | } 627 | } 628 | else if ('category' == name && 'rss' == type){ 629 | if ((_categoryValue = _.safeTrim(_.get(category)))) { 630 | meta.categories.push(_categoryValue); 631 | } 632 | } 633 | else if ('dc:subject' == name && (_categoryValue = _.safeTrim(_.get(category)))) { 634 | _categories = _categoryValue.split(' ').map(function (cat){ return cat.trim(); }); 635 | if (_categories.length) { 636 | meta.categories = meta.categories.concat(_categories); 637 | } 638 | } 639 | else if ('itunes:category' == name) { 640 | if (category['@'] && _.safeTrim(_.get(category['@'], 'text'))) _category = _.safeTrim(_.get(category['@'], 'text')); 641 | if (category[name]) { 642 | if (Array.isArray(category[name])) { 643 | category[name].forEach(function (subcategory){ 644 | var _subcategoryValue; 645 | if (subcategory['@'] && (_subcategoryValue = _.safeTrim(_.get(subcategory['@'], 'text')))) { 646 | meta.categories.push(_category + '/' + _subcategoryValue); 647 | } 648 | }); 649 | } 650 | else if (category[name]['@'] && (_categoryValue = _.safeTrim(_.get(category[name]['@'], 'text')))) { 651 | meta.categories.push(_category + '/' + _categoryValue); 652 | } 653 | } 654 | else if (_category) { 655 | meta.categories.push(_category); 656 | } 657 | } 658 | else if ('media:category' == name && (_categoryValue = _.safeTrim(_.get(category)))) { 659 | meta.categories.push(_categoryValue); 660 | } 661 | }); 662 | } else { 663 | if ('category' == name && 'atom' == type) { 664 | if ((_category = _.safeTrim(_.get(el['@'], 'term')))) { 665 | meta.categories.push(_category); 666 | } 667 | } 668 | else if ('category' == name && 'rss' == type) { 669 | if ((_category = _.safeTrim(_.get(el)))) { 670 | meta.categories.push(_category); 671 | } 672 | } 673 | else if ('dc:subject' == name && (_category = _.safeTrim(_.get(el)))) { 674 | _categories = _category.split(' ').map(function (cat){ return cat.trim(); }); 675 | if (_categories.length) { 676 | meta.categories = meta.categories.concat(_categories); 677 | } 678 | } 679 | else if ('itunes:category' == name) { 680 | if (el['@'] && _.safeTrim(_.get(el['@'], 'text'))) _category = _.safeTrim(_.get(el['@'], 'text')); 681 | if (el[name]) { 682 | if (Array.isArray(el[name])) { 683 | el[name].forEach(function (subcategory){ 684 | var _subcategoryValue; 685 | if (subcategory['@'] && (_subcategoryValue = _.safeTrim(_.get(subcategory['@'], 'text')))) { 686 | meta.categories.push(_category + '/' + _subcategoryValue); 687 | } 688 | }); 689 | } 690 | else if (el[name]['@'] && (_category = _.safeTrim(_.get(el[name]['@'], 'text')))) { 691 | meta.categories.push(_category + '/' + _category); 692 | } 693 | } 694 | else if (_category) { 695 | meta.categories.push(_category); 696 | } 697 | } 698 | else if ('media:category' == name && (_category = _.safeTrim(_.get(el)))) { 699 | meta.categories.push(_.get(el)); 700 | } 701 | } 702 | break; 703 | } // switch end 704 | } 705 | // Fill with all native other namespaced properties 706 | if (name.indexOf('#') !== 0) { 707 | if (~name.indexOf(':')) meta[name] = el; 708 | else meta[type + ':' + name] = el; 709 | } 710 | }, this); // forEach end 711 | 712 | if (normalize) { 713 | if (!meta.description) { 714 | if (node['itunes:summary']) meta.description = _.get(node['itunes:summary']); 715 | else if (node['tagline']) meta.description = _.get(node['tagline']); 716 | } 717 | if (!meta.author) { 718 | if (node['itunes:author']) meta.author = _.get(node['itunes:author']); 719 | else if (node['itunes:owner'] && node['itunes:owner']['itunes:name']) meta.author = _.get(node['itunes:owner']['itunes:name']); 720 | else if (node['dc:creator']) meta.author = _.get(node['dc:creator']); 721 | else if (node['dc:publisher']) meta.author = _.get(node['dc:publisher']); 722 | } 723 | if (!meta.language) { 724 | if (node['@'] && node['@']['xml:lang']) meta.language = _.get(node['@'], 'xml:lang'); 725 | else if (node['dc:language']) meta.language = _.get(node['dc:language']); 726 | } 727 | if (!meta.image.url) { 728 | if (node['itunes:image']) meta.image.url = _.get(node['itunes:image']['@'], 'href'); 729 | else if (node['media:thumbnail']) { 730 | if (Array.isArray(node['media:thumbnail'])) { 731 | node['media:thumbnail'] = node['media:thumbnail'][0]; 732 | } 733 | meta.image.url = _.get(node['media:thumbnail']['@'], 'url'); 734 | } 735 | } 736 | if (!meta.copyright) { 737 | if (node['media:copyright']) meta.copyright = _.get(node['media:copyright']); 738 | else if (node['dc:rights']) meta.copyright = _.get(node['dc:rights']); 739 | else if (node['creativecommons:license']) meta.copyright = _.get(node['creativecommons:license']); 740 | else if (node['cc:license']) { 741 | if (Array.isArray(node['cc:license']) && node['cc:license'][0]['@'] && node['cc:license'][0]['@']['rdf:resource']) { 742 | meta.copyright = _.get(node['cc:license'][0]['@'], 'rdf:resource'); 743 | } else if (node['cc:license']['@'] && node['cc:license']['@']['rdf:resource']) { 744 | meta.copyright = _.get(node['cc:license']['@'], 'rdf:resource'); 745 | } 746 | } 747 | } 748 | if (!meta.generator) { 749 | if (node['admin:generatoragent']) { 750 | if (Array.isArray(node['admin:generatoragent']) && node['admin:generatoragent'][0]['@'] && node['admin:generatoragent'][0]['@']['rdf:resource']) { 751 | meta.generator = _.get(node['admin:generatoragent'][0]['@'], 'rdf:resource'); 752 | } else if (node['admin:generatoragent']['@'] && node['admin:generatoragent']['@']['rdf:resource']) { 753 | meta.generator = _.get(node['admin:generatoragent']['@'], 'rdf:resource'); 754 | } 755 | } 756 | } 757 | if (meta.categories.length) { 758 | meta.categories = _.uniq(meta.categories); 759 | } 760 | if (!meta.link) { 761 | if (meta['atom:id'] && _.get(meta['atom:id']) && /^https?:/.test(_.get(meta['atom:id']))) { 762 | meta.link = _.get(meta['atom:id']); 763 | } 764 | } 765 | if (!meta.xmlurl && this.options.feedurl) { 766 | meta.xmlurl = meta.xmlUrl = this.options.feedurl; 767 | } 768 | meta.title = meta.title && _.stripHtml(meta.title); 769 | meta.description = meta.description && _.stripHtml(meta.description); 770 | } 771 | 772 | return meta; 773 | }; 774 | 775 | FeedParser.prototype.handleItem = function handleItem (node, type, options){ 776 | if (!type || !node) return {}; 777 | 778 | var item = {} 779 | , normalize = !options || (options && options.normalize) 780 | ; 781 | 782 | if (normalize) { 783 | ['title','description','summary','date','pubdate','pubDate','link','guid','author','comments', 'origlink'].forEach(function (property){ 784 | item[property] = null; 785 | }); 786 | item.image = {}; 787 | item.source = {}; 788 | item.categories = []; 789 | item.enclosures = []; 790 | } 791 | 792 | Object.keys(node).forEach(function(name){ 793 | var el = node[name] 794 | , attrs = _.get(el, '@') 795 | , enclosure; 796 | if (normalize) { 797 | switch(name){ 798 | case('title'): 799 | item.title = _.get(el); 800 | break; 801 | case('description'): 802 | case('summary'): 803 | item.summary = _.get(el); 804 | if (!item.description) item.description = _.get(el); 805 | break; 806 | case('content'): 807 | case('content:encoded'): 808 | item.description = _.get(el); 809 | break; 810 | case('pubdate'): 811 | case('published'): 812 | case('issued'): 813 | case('modified'): 814 | case('updated'): 815 | case('dc:date'): 816 | var date = _.get(el) ? new Date(_.get(el)) : null; 817 | if (!date) break; 818 | if (item.pubdate === null || name == 'pubdate' || name == 'published' || name == 'issued') 819 | item.pubdate = item.pubDate = date; 820 | if (item.date === null || name == 'modified' || name == 'updated') 821 | item.date = date; 822 | break; 823 | case('link'): 824 | if (Array.isArray(el)) { 825 | el.forEach(function (link){ 826 | if (link['@']['href']) { // Atom 827 | if (_.get(link['@'], 'rel')) { 828 | if (link['@']['rel'] == 'canonical') item.origlink = link['@']['href']; 829 | if (link['@']['rel'] == 'alternate') item.link = link['@']['href']; 830 | if (link['@']['rel'] == 'self' && !item.link) item.link = link['@']['href']; 831 | if (link['@']['rel'] == 'replies') item.comments = link['@']['href']; 832 | if (link['@']['rel'] == 'enclosure') { 833 | enclosure = {}; 834 | enclosure.url = link['@']['href']; 835 | enclosure.type = _.get(link['@'], 'type'); 836 | enclosure.length = _.get(link['@'], 'length'); 837 | if (indexOfObject(item.enclosures, enclosure, ['url', 'type']) === -1) { 838 | item.enclosures.push(enclosure); 839 | } 840 | } 841 | } else { 842 | item.link = link['@']['href']; 843 | } 844 | } else if (Object.keys(link['@']).length === 0) { // RSS 845 | if (!item.link) item.link = _.get(link); 846 | } 847 | }); 848 | } else { 849 | if (el['@']['href']) { // Atom 850 | if (_.get(el['@'], 'rel')) { 851 | if (el['@']['rel'] == 'canonical') item.origlink = el['@']['href']; 852 | if (el['@']['rel'] == 'alternate') item.link = el['@']['href']; 853 | if (el['@']['rel'] == 'self' && !item.link) item.link = el['@']['href']; 854 | if (el['@']['rel'] == 'replies') item.comments = el['@']['href']; 855 | if (el['@']['rel'] == 'enclosure') { 856 | enclosure = {}; 857 | enclosure.url = el['@']['href']; 858 | enclosure.type = _.get(el['@'], 'type'); 859 | enclosure.length = _.get(el['@'], 'length'); 860 | if (indexOfObject(item.enclosures, enclosure, ['url', 'type']) === -1) { 861 | item.enclosures.push(enclosure); 862 | } 863 | } 864 | } else { 865 | item.link = el['@']['href']; 866 | } 867 | } else if (Object.keys(el['@']).length === 0) { // RSS 868 | if (!item.link) item.link = _.get(el); 869 | } 870 | } 871 | if (!item.guid) item.guid = item.link; 872 | break; 873 | case('guid'): 874 | case('id'): 875 | item.guid = _.get(el); 876 | // http://cyber.law.harvard.edu/rss/rss.html#ltguidgtSubelementOfLtitemgt 877 | // If the guid element has an attribute named "isPermaLink" with a value 878 | // of true, the reader may assume that it is a permalink to the item, 879 | // that is, a url that can be opened in a Web browser, that points to 880 | // the full item described by the element. 881 | // isPermaLink is optional, its default value is true. If its value is 882 | // false, the guid may not be assumed to be a url, or a url to anything 883 | // in particular. 884 | if (item.guid && type == 'rss' && name == 'guid' && !(attrs.ispermalink && attrs.ispermalink.match(/false/i))) { 885 | item.permalink = item.guid; 886 | } 887 | break; 888 | case('author'): 889 | var author = {}; 890 | if (_.get(el)) { // RSS 891 | author = addressparser(_.get(el))[0]; 892 | if (author) { 893 | el['name'] = author.name; 894 | el['email'] = author.address; 895 | item.author = author.name || author.address; 896 | } 897 | // addressparser failed 898 | else { 899 | item.author = _.get(el); 900 | } 901 | } else { 902 | item.author = _.get(el.name) || _.get(el.email) || _.get(el.uri); 903 | } 904 | break; 905 | case('dc:creator'): 906 | item.author = _.get(el); 907 | break; 908 | case('comments'): 909 | item.comments = _.get(el); 910 | break; 911 | case('source'): 912 | if ('rss' == type) { 913 | item.source['title'] = _.get(el); 914 | item.source['url'] = _.get(el['@'], 'url'); 915 | } else if ('atom' == type) { 916 | if (el.title && _.get(el.title)) 917 | item.source['title'] = _.get(el.title); 918 | if (el.link && _.get(el.link['@'], 'href')) 919 | item.source['url'] = _.get(el.link['@'], 'href'); 920 | } 921 | if (item.source['url'] && !this.meta.xmlurl) { 922 | this.meta.xmlurl = this.meta.xmlUrl = item.source['url']; 923 | if (_.isAbsoluteUrl(item.source['url']) && this.xmlbase && this.xmlbase.length === 0) { 924 | this.xmlbase.unshift({ '#name': 'xml', '#': item.source['url']}); 925 | this.stack[0] = _.reresolve(this.stack[0], item.source['url']); 926 | } 927 | else if (this.xmlbase && this.xmlbase.length > 0) { 928 | this.meta.xmlurl = this.meta.xmlUrl = item.source['url'] = _.resolve(_.get(this.xmlbase[0], '#'), item.source['url']); 929 | } 930 | } 931 | break; 932 | case('enclosure'): 933 | if (Array.isArray(el)) { 934 | el.forEach(function (enc){ 935 | enclosure = {}; 936 | enclosure.url = _.get(enc['@'], 'url'); 937 | enclosure.type = _.get(enc['@'], 'type'); 938 | enclosure.length = _.get(enc['@'], 'length'); 939 | if (~indexOfObject(item.enclosures, enclosure, ['url', 'type'])) { 940 | item.enclosures.splice(indexOfObject(item.enclosures, enclosure, ['url', 'type']), 1, enclosure); 941 | } else { 942 | item.enclosures.push(enclosure); 943 | } 944 | }); 945 | } else { 946 | enclosure = {}; 947 | enclosure.url = _.get(el['@'], 'url'); 948 | enclosure.type = _.get(el['@'], 'type'); 949 | enclosure.length = _.get(el['@'], 'length'); 950 | if (~indexOfObject(item.enclosures, enclosure, ['url', 'type'])) { 951 | item.enclosures.splice(indexOfObject(item.enclosures, enclosure, ['url', 'type']), 1, enclosure); 952 | } else { 953 | item.enclosures.push(enclosure); 954 | } 955 | } 956 | break; 957 | case('media:content'): 958 | var optionalAttributes = ['bitrate', 'framerate', 'samplingrate', 'duration', 'height', 'width']; 959 | if (Array.isArray(el)) { 960 | el.forEach(function (enc){ 961 | enclosure = {}; 962 | enclosure.url = _.get(enc['@'], 'url'); 963 | enclosure.type = _.get(enc['@'], 'type') || _.get(enc['@'], 'medium'); 964 | enclosure.length = _.get(enc['@'], 'filesize'); 965 | var index = indexOfObject(item.enclosures, enclosure, ['url', 'type']); 966 | if (index !== -1) { 967 | enclosure = item.enclosures[index]; 968 | } 969 | optionalAttributes.forEach(function (attribute) { 970 | if (!enclosure[attribute] && _.get(enc['@'], attribute)) { 971 | enclosure[attribute] = _.get(enc['@'], attribute); 972 | } 973 | }); 974 | if (index === -1) { 975 | item.enclosures.push(enclosure); 976 | } 977 | }); 978 | } else { 979 | enclosure = {}; 980 | enclosure.url = _.get(el['@'], 'url'); 981 | enclosure.type = _.get(el['@'], 'type') || _.get(el['@'], 'medium'); 982 | enclosure.length = _.get(el['@'], 'filesize'); 983 | var index = indexOfObject(item.enclosures, enclosure, ['url', 'type']); 984 | if (index !== -1) { 985 | enclosure = item.enclosures[index]; 986 | } 987 | optionalAttributes.forEach(function (attribute) { 988 | if (!enclosure[attribute] && _.get(el['@'], attribute)) { 989 | enclosure[attribute] = _.get(el['@'], attribute); 990 | } 991 | }); 992 | if (index === -1) { 993 | item.enclosures.push(enclosure); 994 | } 995 | } 996 | break; 997 | case('enc:enclosure'): // Can't find this in use for an example to debug. Only example found does not comply with the spec -- can't code THAT! 998 | break; 999 | case('category'): 1000 | case('dc:subject'): 1001 | case('itunes:category'): 1002 | case('media:category'): 1003 | /* We handle all the kinds of categories within the switch loop because item.categories 1004 | * is an array, unlike the other properties, and therefore can handle multiple values 1005 | */ 1006 | var _category = '' 1007 | , _categories = [] 1008 | ; 1009 | if (Array.isArray(el)) { 1010 | el.forEach(function (category){ 1011 | if ('category' == name && 'atom' == type) { 1012 | if (category['@'] && _.get(category['@'], 'term')) item.categories.push(_.get(category['@'], 'term')); 1013 | } else if ('category' == name && _.get(category) && 'rss' == type) { 1014 | item.categories.push(_.get(category).trim()); 1015 | } else if ('dc:subject' == name && _.get(category)) { 1016 | _categories = _.get(category).split(' ').map(function (cat){ return cat.trim(); }); 1017 | if (_categories.length) item.categories = item.categories.concat(_categories); 1018 | } else if ('itunes:category' == name) { 1019 | if (category['@'] && _.get(category['@'], 'text')) _category = _.get(category['@'], 'text'); 1020 | if (category[name]) { 1021 | if (Array.isArray(category[name])) { 1022 | category[name].forEach(function (subcategory){ 1023 | if (subcategory['@'] && _.get(subcategory['@'], 'text')) item.categories.push(_category + '/' + _.get(subcategory['@'], 'text')); 1024 | }); 1025 | } else { 1026 | if (category[name]['@'] && _.get(category[name]['@'], 'text')) 1027 | item.categories.push(_category + '/' + _.get(category[name]['@'], 'text')); 1028 | } 1029 | } else { 1030 | item.categories.push(_category); 1031 | } 1032 | } else if ('media:category' == name) { 1033 | item.categories.push(_.get(category)); 1034 | } 1035 | }); 1036 | } else { 1037 | if ('category' == name && 'atom' == type) { 1038 | if (_.get(el['@'], 'term')) item.categories.push(_.get(el['@'], 'term')); 1039 | } else if ('category' == name && _.get(el) && 'rss' == type) { 1040 | item.categories.push(_.get(el).trim()); 1041 | } else if ('dc:subject' == name && _.get(el)) { 1042 | _categories = _.get(el).split(' ').map(function (cat){ return cat.trim(); }); 1043 | if (_categories.length) item.categories = item.categories.concat(_categories); 1044 | } else if ('itunes:category' == name) { 1045 | if (el['@'] && _.get(el['@'], 'text')) _category = _.get(el['@'], 'text'); 1046 | if (el[name]) { 1047 | if (Array.isArray(el[name])) { 1048 | el[name].forEach(function (subcategory){ 1049 | if (subcategory['@'] && _.get(subcategory['@'], 'text')) item.categories.push(_category + '/' + _.get(subcategory['@'], 'text')); 1050 | }); 1051 | } else { 1052 | if (el[name]['@'] && _.get(el[name]['@'], 'text')) 1053 | item.categories.push(_category + '/' + _.get(el[name]['@'], 'text')); 1054 | } 1055 | } else { 1056 | item.categories.push(_category); 1057 | } 1058 | } else if ('media:category' == name) { 1059 | item.categories.push(_.get(el)); 1060 | } 1061 | } 1062 | break; 1063 | case('feedburner:origlink'): 1064 | case('pheedo:origlink'): 1065 | if (!item.origlink) { 1066 | item.origlink = _.get(el); 1067 | } 1068 | break; 1069 | } // switch end 1070 | } 1071 | // Fill with all native other namespaced properties 1072 | if (name.indexOf('#') !== 0) { 1073 | if (~name.indexOf(':')) item[name] = el; 1074 | else item[type + ':' + name] = el; 1075 | } 1076 | }, this); // forEach end 1077 | 1078 | if (normalize) { 1079 | if (!item.description) { 1080 | if (node['itunes:summary']) item.description = _.get(node['itunes:summary']); 1081 | } 1082 | if (!item.author) { 1083 | if (node['itunes:author']) item.author = _.get(node['itunes:author']); 1084 | else if (node['itunes:owner'] && node['itunes:owner']['itunes:name']) item.author = _.get(node['itunes:owner']['itunes:name']); 1085 | else if (node['dc:publisher']) item.author = _.get(node['dc:publisher']); 1086 | } 1087 | if (!item.image.url) { 1088 | if (node['itunes:image']) item.image.url = _.get(node['itunes:image']['@'], 'href'); 1089 | else if (node['media:thumbnail']) { 1090 | if (Array.isArray(node['media:thumbnail'])) { 1091 | item.image.url = _.get(node['media:thumbnail'][0]['@'], 'url'); 1092 | } else { 1093 | item.image.url = _.get(node['media:thumbnail']['@'], 'url'); 1094 | } 1095 | } 1096 | else if (node['media:content'] && node['media:content']['media:thumbnail']) item.image.url = _.get(node['media:content']['media:thumbnail']['@'], 'url'); 1097 | else if (node['media:group'] && node['media:group']['media:thumbnail']) item.image.url = _.get(node['media:group']['media:thumbnail']['@'], 'url'); 1098 | else if (node['media:group'] && node['media:group']['media:content'] && node['media:group']['media:content']['media:thumbnail']) item.image.url = _.get(node['media:group']['media:content']['media:thumbnail']['@'], 'url'); 1099 | else if (node['g:image_link']) item.image.url = _.get(node['g:image_link']); 1100 | } 1101 | if (item.categories.length) { 1102 | item.categories = _.uniq(item.categories); 1103 | } 1104 | if (!item.link) { 1105 | if (item.guid && /^https?:/.test(item.guid)) { 1106 | item.link = item.guid; 1107 | } 1108 | } 1109 | item.title = item.title && _.stripHtml(item.title); 1110 | } 1111 | return item; 1112 | }; 1113 | 1114 | // Naive Stream API 1115 | FeedParser.prototype._transform = function (data, encoding, done) { 1116 | try { 1117 | this.stream.write(data); 1118 | done(); 1119 | } 1120 | catch (e) { 1121 | done(e); 1122 | this.push(null); // Manually trigger and end, since we can't reliably do any more parsing 1123 | } 1124 | }; 1125 | 1126 | FeedParser.prototype._flush = function (done) { 1127 | try { 1128 | this.stream.end(); 1129 | done(); 1130 | } 1131 | catch (e) { 1132 | done(e); 1133 | } 1134 | }; 1135 | 1136 | exports = module.exports = FeedParser; 1137 | -------------------------------------------------------------------------------- /lib/namespaces.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Default namespaces 3 | * 4 | * Lookup by URI 5 | */ 6 | module.exports = { 7 | 'http://www.w3.org/2005/Atom' :'atom', // v1.0 8 | 'http://purl.org/atom/ns#' :'atom', // v0.3 9 | 'http://www.w3.org/1999/02/22-rdf-syntax-ns#' :'rdf', 10 | 'http://purl.org/rss/1.0/' :'rdf', // rss v1.0 11 | 'http://my.netscape.com/rdf/simple/0.9/' :'rdf', // rss v0.90 12 | 'http://webns.net/mvcb/' :'admin', 13 | 'http://creativecommons.org/ns#' :'cc', 14 | 'http://web.resource.org/cc/' :'cc', 15 | 'http://purl.org/rss/1.0/modules/content/' :'content', 16 | 'http://backend.userland.com/creativeCommonsRSSModule' :'creativecommons', 17 | 'http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html' :'creativecommons', 18 | 'http://purl.org/dc/elements/1.1/' :'dc', 19 | 'http://purl.org/dc/elements/1.0/' :'dc', 20 | 'http://purl.oclc.org/net/rss_2.0/enc#' :'enc', 21 | 'http://rssnamespace.org/feedburner/ext/1.0' :'feedburner', 22 | 'http://www.bradsoft.com/feeddemon/xmlns/1.0/' :'fd', // FeedDemon 23 | 'http://www.itunes.com/dtds/podcast-1.0.dtd' :'itunes', 24 | 'http://www.w3.org/2003/01/geo/wgs84_pos#' :'geo', 25 | 'http://www.georss.org/georss' :'georss', 26 | 'http://search.yahoo.com/mrss/' :'media', 27 | 'http://search.yahoo.com/mrss' :'media', // commonly-used but wrong 28 | 'http://newsgator.com/schema/extensions' :'ng', // NewsGator 29 | 'http://opml.org/spec2' :'opml', // OPML 2.0 30 | 'http://www.pheedo.com/namespace/pheedo' :'pheedo', 31 | 'http://purl.org/rss/1.0/modules/syndication/' :'syn', 32 | 'http://feedsync.org/2007/feedsync' :'sx', // feedsync (Simple Sharing Extensions) http://feedsyncsamples.codeplex.com/ 33 | 'http://purl.org/rss/1.0/modules/taxonomy/' :'taxo', 34 | 'http://purl.org/syndication/thread/1.0' :'thr', 35 | 'http://www.w3.org/1999/xhtml' :'xhtml', 36 | 'http://www.w3.org/XML/1998/namespace' :'xml' 37 | }; 38 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | var URL = require('url') 2 | , namespaces = require('./namespaces') 3 | ; 4 | 5 | exports.has = require('lodash.has'); 6 | exports.assign = require('lodash.assign'); 7 | exports.uniq = require('lodash.uniq'); 8 | 9 | var _get = require('lodash.get'); 10 | /** 11 | * lodash.get, but wrapped to provide a default subkey (a/k/a path) of "#" 12 | * and defaultValue of "null" 13 | * 14 | * var obj = { '#': 'foo', 'bar': 'baz' }; 15 | * 16 | * get(obj); 17 | * // => 'foo' 18 | * 19 | * get(obj, 'bar'); 20 | * // => 'baz' 21 | * 22 | * @param {Object} obj 23 | * @param {String} [subkey="#"] By default, use the '#' key, but you may pass any key you like 24 | * @return Returns the value of the selected key or 'null' if undefined. 25 | * @private 26 | */ 27 | function get(obj, subkey, defaultValue) { 28 | if (!subkey) { 29 | subkey = '#'; 30 | } 31 | 32 | if (!defaultValue) { 33 | defaultValue = null; 34 | } 35 | 36 | if (Array.isArray(obj)) { 37 | return _get(obj[0], subkey, defaultValue); 38 | } 39 | else { 40 | return _get(obj, subkey, defaultValue); 41 | } 42 | } 43 | exports.get = get; 44 | 45 | /** 46 | * Safely trim a value if it's a String 47 | * @private 48 | */ 49 | function safeTrim (val) { 50 | if (typeof val === 'string') { 51 | return val.trim(); 52 | } 53 | return val; 54 | } 55 | exports.safeTrim = safeTrim; 56 | 57 | /* 58 | * Expose require('url').resolve, safely returning if either parameter 59 | * isn't provided 60 | * @private 61 | */ 62 | function resolve (baseUrl, pathUrl) { 63 | if (!baseUrl || !pathUrl) return pathUrl; 64 | return URL.resolve(baseUrl, pathUrl); 65 | } 66 | exports.resolve = resolve; 67 | 68 | /* 69 | * Check whether a given uri is an absolute URL 70 | * @param {String} uri 71 | * @private 72 | */ 73 | function isAbsoluteUrl (uri) { 74 | if (!uri || typeof uri !== 'string') return false; 75 | var parts = URL.parse(uri); 76 | return Boolean(parts.host); 77 | } 78 | exports.isAbsoluteUrl = isAbsoluteUrl; 79 | 80 | /* 81 | * Check whether a given namespace URI matches the given default 82 | * 83 | * @param {String} URI 84 | * @param {String} default, e.g., 'atom' 85 | * @return {Boolean} 86 | * @private 87 | */ 88 | function nslookup (uri, def) { 89 | return namespaces[uri] === def; 90 | } 91 | exports.nslookup = nslookup; 92 | 93 | /* 94 | * Return the "default" namespace prefix for a given namespace URI 95 | * 96 | * @param {String} URI 97 | * @return {String} 98 | * @private 99 | */ 100 | function nsprefix (uri) { 101 | return namespaces[uri]; 102 | } 103 | exports.nsprefix = nsprefix; 104 | 105 | /* 106 | * Walk a node and re-resolve the urls using the given baseurl 107 | * 108 | * @param {Object} node 109 | * @param {String} baseurl 110 | * @return {Object} modified node 111 | * @private 112 | */ 113 | function reresolve (node, baseurl) { 114 | if (!node || !baseurl) { 115 | return false; // Nothing to do. 116 | } 117 | 118 | function resolveLevel (level) { 119 | var els = Object.keys(level); 120 | els.forEach(function(el){ 121 | if (Array.isArray(level[el])) { 122 | // The shape of the array of element items is different than if the element is not an array. 123 | // We need it to be the same shape to enable using the same function for recursion. 124 | var levelFromArray = {}; 125 | level[el].forEach(function (attrs) { 126 | levelFromArray[el] = attrs; 127 | resolveLevel(levelFromArray); 128 | }); 129 | } else { 130 | if (level[el].constructor.name === 'Object') { 131 | if (el == 'logo' || el == 'icon' || el == 'link') { 132 | if ('#' in level[el]) { 133 | level[el]['#'] = URL.resolve(baseurl, level[el]['#']); 134 | } 135 | } else if (el == 'image') { 136 | if ('url' in level[el] && level[el]['url'].constructor.name === 'Object' && '#' in level[el]['url']) { 137 | level[el]['url']['#'] = URL.resolve(baseurl, level[el]['url']['#']); 138 | } 139 | if ('link' in level[el] && level[el]['link'].constructor.name === 'Object' && '#' in level[el]['link']) { 140 | level[el]['link']['#'] = URL.resolve(baseurl, level[el]['link']['#']); 141 | } 142 | } 143 | if ('@' in level[el]) { 144 | var attrs = Object.keys(level[el]['@']); 145 | attrs.forEach(function (name) { 146 | if (name == 'href' || name == 'src' || name == 'uri') { 147 | if ('string' === typeof level[el]['@'][name]) { 148 | level[el]['@'][name] = URL.resolve(baseurl, level[el]['@'][name]); 149 | } 150 | } 151 | }); 152 | } 153 | } 154 | } 155 | }); 156 | return level; 157 | } 158 | 159 | return resolveLevel(node); 160 | } 161 | exports.reresolve = reresolve; 162 | 163 | /* 164 | * Aggressivly strip HTML tags 165 | * Pulled out of node-resanitize because it was all that was being used 166 | * and it's way lighter... 167 | * 168 | * @param {String} str 169 | * @private 170 | */ 171 | function stripHtml (str) { 172 | return str.replace(/<.*?>/g, ''); 173 | } 174 | 175 | exports.stripHtml = stripHtml; 176 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "feedparser", 3 | "author": "Dan MacTough ", 4 | "description": "Robust RSS Atom and RDF feed parsing using sax js", 5 | "bin": { 6 | "feedparser": "./bin/feedparser.js" 7 | }, 8 | "version": "2.2.10", 9 | "keywords": [ 10 | "rss", 11 | "feed", 12 | "atom", 13 | "rdf", 14 | "xml", 15 | "syndication", 16 | "rsscloud", 17 | "pubsubhubbub" 18 | ], 19 | "license": "MIT", 20 | "homepage": "http://github.com/danmactough/node-feedparser", 21 | "repository": { 22 | "type": "git", 23 | "url": "git://github.com/danmactough/node-feedparser.git" 24 | }, 25 | "bugs": { 26 | "url": "http://github.com/danmactough/node-feedparser/issues" 27 | }, 28 | "main": "index.js", 29 | "files": [ 30 | "index.js", 31 | "bin/*.js", 32 | "lib" 33 | ], 34 | "engines": { 35 | "node": ">= 10.18.1" 36 | }, 37 | "dependencies": { 38 | "addressparser": "^1.0.1", 39 | "array-indexofobject": "~0.0.1", 40 | "lodash.assign": "^4.2.0", 41 | "lodash.get": "^4.4.2", 42 | "lodash.has": "^4.5.2", 43 | "lodash.uniq": "^4.5.0", 44 | "mri": "^1.1.5", 45 | "readable-stream": "^2.3.7", 46 | "sax": "^1.2.4" 47 | }, 48 | "devDependencies": { 49 | "eslint": "^6.8.0", 50 | "iconv-lite": "^0.5.1", 51 | "mocha": "^7.1.2", 52 | "node-fetch": "^2.6.0" 53 | }, 54 | "scripts": { 55 | "lint": "eslint .", 56 | "pretest": "npm run lint", 57 | "test": "mocha", 58 | "version": "git changelog ; git add History.md" 59 | }, 60 | "mocha": { 61 | "require": "./test/common.js", 62 | "globals": [ 63 | "assert", 64 | "fs", 65 | "FeedParser" 66 | ], 67 | "reporter": "spec", 68 | "timeout": 5000 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "assert": true, 7 | "fs": true, 8 | "FeedParser": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/api.js: -------------------------------------------------------------------------------- 1 | describe('api', function () { 2 | 3 | var feed = __dirname + '/feeds/rss2sample.xml'; 4 | 5 | it('should read a stream via .pipe()', function (done) { 6 | var meta 7 | , items = []; 8 | 9 | fs.createReadStream(feed).pipe(FeedParser()) 10 | .on('error', function (err) { 11 | assert.ifError(err); 12 | done(err); 13 | }) 14 | .on('meta', function (_meta) { 15 | meta = _meta; 16 | }) 17 | .on('readable', function () { 18 | var item; 19 | while (item = this.read()) { 20 | items.push(item); 21 | } 22 | assert.ok(items.length); 23 | }) 24 | .on('end', function () { 25 | assert(meta); 26 | assert.strictEqual(items.length, 4); 27 | done(); 28 | }); 29 | }); 30 | 31 | it('should parse and set options', function (done) { 32 | var meta 33 | , item 34 | , options = { normalize: false, addmeta: false }; 35 | 36 | fs.createReadStream(feed).pipe(FeedParser(options)) 37 | .on('error', function (err) { 38 | assert.ifError(err); 39 | done(err); 40 | }) 41 | .on('meta', function (_meta) { 42 | meta = _meta; 43 | }) 44 | .on('readable', function () { 45 | var _item = this.read(); 46 | item || (item = _item); 47 | }) 48 | .on('end', function () { 49 | assert(meta); 50 | assert.equal(meta.title, null); 51 | assert.equal(meta['rss:title']['#'], 'Liftoff News'); 52 | assert.equal(item.meta, null); 53 | done(); 54 | }); 55 | }); 56 | 57 | }); -------------------------------------------------------------------------------- /test/bad.js: -------------------------------------------------------------------------------- 1 | describe('bad feeds', function(){ 2 | 3 | describe('not a feed', function () { 4 | 5 | var feed = __dirname + '/feeds/notafeed.html'; 6 | 7 | it('should emit an error and no data', function (done) { 8 | var error; 9 | var feedparser = new FeedParser(); 10 | fs.createReadStream(feed).pipe(feedparser); 11 | feedparser.once('readable', function () { 12 | assert.strictEqual(this.read(), null); 13 | }) 14 | .on('error', function (err) { 15 | error = err; 16 | }) 17 | .on('end', function () { 18 | assert.ok(error instanceof Error); 19 | assert.equal(error.message, 'Not a feed'); 20 | done(); 21 | }); 22 | }); 23 | 24 | }); 25 | 26 | describe('duplicate guids', function () { 27 | 28 | var feed = __dirname + '/feeds/guid-dupes.xml'; 29 | 30 | it('should just use the first', function (done) { 31 | var feedparser = new FeedParser(); 32 | fs.createReadStream(feed).pipe(feedparser); 33 | feedparser.once('readable', function () { 34 | var stream = this; 35 | var item = stream.read(); 36 | assert.equal(item.guid, 'http://www.braingle.com/50366.html'); 37 | assert.equal(item.permalink, 'http://www.braingle.com/50366.html'); 38 | done(); 39 | }) 40 | .on('error', function (err) { 41 | assert.ifError(err); 42 | done(err); 43 | }); 44 | }); 45 | 46 | }); 47 | 48 | describe('gzipped feed', function () { 49 | 50 | var feed = __dirname + '/feeds/invalid-characters-gzipped.xml'; 51 | 52 | it('should gracefully emit an error and not throw', function (done) { 53 | var error; 54 | var feedparser = new FeedParser(); 55 | fs.createReadStream(feed).pipe(feedparser); 56 | feedparser.once('readable', function () { 57 | assert.strictEqual(this.read(), null); 58 | }) 59 | .on('error', function (err) { 60 | error = err; 61 | }) 62 | .on('end', function () { 63 | assert.ok(error instanceof Error); 64 | assert.equal(error.message, 'Not a feed'); 65 | done(); 66 | }); 67 | }); 68 | 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /test/category.js: -------------------------------------------------------------------------------- 1 | describe('categories', function(){ 2 | 3 | var feed = __dirname + '/feeds/category-feed.xml'; 4 | 5 | it('should not seperate by comma', function (done) { 6 | fs.createReadStream(feed).pipe(new FeedParser()) 7 | .once('readable', function () { 8 | var stream = this; 9 | assert.deepEqual(stream.read().categories, [ 10 | 'Water Pollution', 11 | 'Gowanus Canal (Brooklyn, NY)' 12 | ]); 13 | done(); 14 | }) 15 | .on('error', function (err) { 16 | assert.ifError(err); 17 | done(err); 18 | }); 19 | }); 20 | 21 | }); 22 | -------------------------------------------------------------------------------- /test/common.js: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-vars: 0 */ 2 | assert = require('assert'); 3 | fs = require('fs'); 4 | FeedParser = require('../'); 5 | -------------------------------------------------------------------------------- /test/duplicate-enclosures.js: -------------------------------------------------------------------------------- 1 | describe('duplicate enclosures', function(){ 2 | 3 | var feed = __dirname + '/feeds/mediacontent-dupes.xml'; 4 | 5 | it('should not have duplicate enclosures from different elements', function (done){ 6 | fs.createReadStream(feed).pipe(new FeedParser()) 7 | .once('readable', function () { 8 | var stream = this; 9 | var enclosures = stream.read().enclosures; 10 | assert.strictEqual(enclosures.length, 3); 11 | assert.deepEqual(enclosures, [{ 12 | url: 'http://i.mol.im/i/pix/2013/02/03/article-2272640-174FCEE2000005DC-697_154x115.jpg', 13 | type: 'image/jpeg', 14 | length: '4114', 15 | height: '115', 16 | width: '154' 17 | }, { 18 | url: 'http://i.mol.im/i/pix/2013/02/03/article-2272640-174FCEE2000005DC-697_154x115.mp4', 19 | type: 'video/mp4', 20 | length: '4114' 21 | }, { 22 | url: 'http://i.mol.im/i/pix/2013/02/03/article-2272640-174FCEE2000005DC-697.mp4', 23 | type: 'video/mp4', 24 | length: null, 25 | bitrate: '3000', 26 | height: '115', 27 | width: '154' 28 | }]); 29 | done(); 30 | }) 31 | .on('error', function (err) { 32 | assert.ifError(err); 33 | done(err); 34 | }); 35 | }); 36 | 37 | }); 38 | 39 | -------------------------------------------------------------------------------- /test/feeds/category-feed.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | NYT > Home Page 7 | http://www.nytimes.com/pages/index.html?partner=rss&emc=rss 8 | HomePage 9 | en-us 10 | Copyright 2013 The New York Times Company 11 | Sun, 05 May 2013 17:21:47 GMT 12 | Sun, 05 May 2013 17:21:47 GMT 13 | 14 | 15 | E.P.A. Plan to Clean Up Gowanus Canal Meets Local Resistance 16 | http://www.nytimes.com/2013/05/06/nyregion/epa-plan-to-clean-up-gowanus-canal-meets-local-resistance.html?partner=rss&emc=rss 17 | Removed for overview ... 18 | Water Pollution 19 | Gowanus Canal (Brooklyn, NY) 20 | Sun, 05 May 2013 17:15:11 GMT 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /test/feeds/complexNamespaceFeed.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | urn:uuid:9f547ffc-0b31-4794-bcb2-c00f79771738 7 | 8 | namespace/feed 9 | 10 | 11 | 12 | 13 | 2012-08-04T01:34:19.533Z 14 | 15 | 16 | This is the title 17 | 18 | John Doe 19 | 20 | 21 | Hello World 22 | 23 | 24 | 25 | 26 | 27 | 28 | urn:uuid:d5ffaea2-0a9a-4f38-98fc-5c364177b6b4 29 | 2012-08-04T01:29:34.677Z 30 | 31 | -------------------------------------------------------------------------------- /test/feeds/compressed.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmactough/node-feedparser/0a734f88195e4c7048a53867a8ec7c52f150fff2/test/feeds/compressed.xml -------------------------------------------------------------------------------- /test/feeds/guid-dupes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Braingle's Newest Brain Teasers and Riddles 5 | http://www.braingle.com/brainteasers/index.php 6 | Stay up to date with Braingle's newest brain teasers, riddles, and mind puzzles. 7 | Fri, 20 Jun 2014 00:00:01 GMT 8 | en-us 9 | 300 10 | 11 | Huh? 12 | http://www.braingle.com/50366.html 13 | http://www.braingle.com/50366.html 14 | Thu, 19 Jun 2014 06:00:00 GMT 15 | What's the Rebus? 16 | <br /> 17 | <br />How's that; how's that<br /><br /><br /> 18 | Check <a href="http://www.braingle.com/50366.html">Braingle.com</a> for the answer.<div class="feedflare"> 19 | <a href="http://feeds.braingle.com/~ff/braingle/newteasers?a=orRnfLWBRNc:kGTeZkA8aAU:yIl2AUoC8zA"><img src="http://feeds.feedburner.com/~ff/braingle/newteasers?d=yIl2AUoC8zA" border="0"></img></a> <a href="http://feeds.braingle.com/~ff/braingle/newteasers?a=orRnfLWBRNc:kGTeZkA8aAU:F7zBnMyn0Lo"><img src="http://feeds.feedburner.com/~ff/braingle/newteasers?i=orRnfLWBRNc:kGTeZkA8aAU:F7zBnMyn0Lo" border="0"></img></a> <a href="http://feeds.braingle.com/~ff/braingle/newteasers?a=orRnfLWBRNc:kGTeZkA8aAU:V_sGLiPBpWU"><img src="http://feeds.feedburner.com/~ff/braingle/newteasers?i=orRnfLWBRNc:kGTeZkA8aAU:V_sGLiPBpWU" border="0"></img></a> <a href="http://feeds.braingle.com/~ff/braingle/newteasers?a=orRnfLWBRNc:kGTeZkA8aAU:qj6IDK7rITs"><img src="http://feeds.feedburner.com/~ff/braingle/newteasers?d=qj6IDK7rITs" border="0"></img></a> <a href="http://feeds.braingle.com/~ff/braingle/newteasers?a=orRnfLWBRNc:kGTeZkA8aAU:gIN9vFwOqvQ"><img src="http://feeds.feedburner.com/~ff/braingle/newteasers?i=orRnfLWBRNc:kGTeZkA8aAU:gIN9vFwOqvQ" border="0"></img></a> 20 | </div><img src="http://feeds.feedburner.com/~r/braingle/newteasers/~4/orRnfLWBRNc" height="1" width="1"/> 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /test/feeds/iconv.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmactough/node-feedparser/0a734f88195e4c7048a53867a8ec7c52f150fff2/test/feeds/iconv.xml -------------------------------------------------------------------------------- /test/feeds/illegally-nested.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | PCMag.com: the Official John C. Dvorak RSS Feed 5 | http://www.pcmag.com 6 | Tech opinion and analysis from John C. Dvorak at PCMag.com 7 | 8 | Copyright 2009 Ziff Davis Media Inc. All Rights Reserved. 9 | pcmonline@ziffdavis.com?subject=RSS_feed 10 | 60 11 | en-us 12 | My Professional Armchair Analysis of CEShttp://feedproxy.google.com/~r/ziffdavis/pcmag/dvorak/~3/0O-eUfOYo94/0,2817,2525649,00.aspWed, 17 Jan 2018 15:50:00 ESTNo one needs to go to this annual gadget tradeshow when it's already over-covered by every media out<div class="feedflare"> 13 | <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=0O-eUfOYo94:Htv1BfHvrGA:yIl2AUoC8zA"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=yIl2AUoC8zA" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=0O-eUfOYo94:Htv1BfHvrGA:Gu391qSwH_A"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=Gu391qSwH_A" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=0O-eUfOYo94:Htv1BfHvrGA:V_sGLiPBpWU"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?i=0O-eUfOYo94:Htv1BfHvrGA:V_sGLiPBpWU" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=0O-eUfOYo94:Htv1BfHvrGA:F7zBnMyn0Lo"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?i=0O-eUfOYo94:Htv1BfHvrGA:F7zBnMyn0Lo" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=0O-eUfOYo94:Htv1BfHvrGA:dnMXMwOfBR0"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=dnMXMwOfBR0" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=0O-eUfOYo94:Htv1BfHvrGA:TzevzKxY174"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=TzevzKxY174" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=0O-eUfOYo94:Htv1BfHvrGA:qj6IDK7rITs"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=qj6IDK7rITs" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=0O-eUfOYo94:Htv1BfHvrGA:I9og5sOYxJI"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=I9og5sOYxJI" border="0"></img></a> 14 | </div><img src="http://feeds.feedburner.com/~r/ziffdavis/pcmag/dvorak/~4/0O-eUfOYo94" height="1" width="1" alt=""/>http://www.pcmag.com/article2/0,2817,2525649,00.asp?kc=PCRSS03079TX1K0000584Did We Learn Nothing From the Dot-Com Bust?http://feedproxy.google.com/~r/ziffdavis/pcmag/dvorak/~3/HbEBB3XuiYE/0,2817,2525452,00.aspSat, 13 Jan 2018 08:00:00 ESTA lot of "tech" these days feels like it would fit perfectly into the pre-bust Dotcom era-and that s<div class="feedflare"> 15 | <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=HbEBB3XuiYE:r_DXPKUoMGA:yIl2AUoC8zA"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=yIl2AUoC8zA" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=HbEBB3XuiYE:r_DXPKUoMGA:Gu391qSwH_A"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=Gu391qSwH_A" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=HbEBB3XuiYE:r_DXPKUoMGA:V_sGLiPBpWU"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?i=HbEBB3XuiYE:r_DXPKUoMGA:V_sGLiPBpWU" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=HbEBB3XuiYE:r_DXPKUoMGA:F7zBnMyn0Lo"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?i=HbEBB3XuiYE:r_DXPKUoMGA:F7zBnMyn0Lo" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=HbEBB3XuiYE:r_DXPKUoMGA:dnMXMwOfBR0"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=dnMXMwOfBR0" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=HbEBB3XuiYE:r_DXPKUoMGA:TzevzKxY174"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=TzevzKxY174" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=HbEBB3XuiYE:r_DXPKUoMGA:qj6IDK7rITs"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=qj6IDK7rITs" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=HbEBB3XuiYE:r_DXPKUoMGA:I9og5sOYxJI"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=I9og5sOYxJI" border="0"></img></a> 16 | </div><img src="http://feeds.feedburner.com/~r/ziffdavis/pcmag/dvorak/~4/HbEBB3XuiYE" height="1" width="1" alt=""/>http://www.pcmag.com/article2/0,2817,2525452,00.asp?kc=PCRSS03079TX1K0000584Knock It Off With the Tech Prediction Storieshttp://feedproxy.google.com/~r/ziffdavis/pcmag/dvorak/~3/t0DenMl_uOI/0,2817,2525164,00.aspThu, 11 Jan 2018 09:30:00 ESTEvery year about this time columnists like myself tend to write any number of standard or stock colu<div class="feedflare"> 17 | <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=t0DenMl_uOI:Cm9A1Z3bBEQ:yIl2AUoC8zA"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=yIl2AUoC8zA" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=t0DenMl_uOI:Cm9A1Z3bBEQ:Gu391qSwH_A"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=Gu391qSwH_A" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=t0DenMl_uOI:Cm9A1Z3bBEQ:V_sGLiPBpWU"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?i=t0DenMl_uOI:Cm9A1Z3bBEQ:V_sGLiPBpWU" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=t0DenMl_uOI:Cm9A1Z3bBEQ:F7zBnMyn0Lo"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?i=t0DenMl_uOI:Cm9A1Z3bBEQ:F7zBnMyn0Lo" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=t0DenMl_uOI:Cm9A1Z3bBEQ:dnMXMwOfBR0"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=dnMXMwOfBR0" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=t0DenMl_uOI:Cm9A1Z3bBEQ:TzevzKxY174"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=TzevzKxY174" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=t0DenMl_uOI:Cm9A1Z3bBEQ:qj6IDK7rITs"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=qj6IDK7rITs" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=t0DenMl_uOI:Cm9A1Z3bBEQ:I9og5sOYxJI"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=I9og5sOYxJI" border="0"></img></a> 18 | </div><img src="http://feeds.feedburner.com/~r/ziffdavis/pcmag/dvorak/~4/t0DenMl_uOI" height="1" width="1" alt=""/>http://www.pcmag.com/article2/0,2817,2525164,00.asp?kc=PCRSS03079TX1K0000584Microsoft Phone: Bend, Fold, and Wearhttp://feedproxy.google.com/~r/ziffdavis/pcmag/dvorak/~3/C2nrCwsZoOQ/0,2817,2525037,00.aspThu, 28 Dec 2017 11:18:00 ESTThe "cuff phone" could Be Microsoft's re-entry into the smartphone and wearables market all at once.<div class="feedflare"> 19 | <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=C2nrCwsZoOQ:ZfkpDvjdz60:yIl2AUoC8zA"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=yIl2AUoC8zA" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=C2nrCwsZoOQ:ZfkpDvjdz60:Gu391qSwH_A"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=Gu391qSwH_A" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=C2nrCwsZoOQ:ZfkpDvjdz60:V_sGLiPBpWU"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?i=C2nrCwsZoOQ:ZfkpDvjdz60:V_sGLiPBpWU" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=C2nrCwsZoOQ:ZfkpDvjdz60:F7zBnMyn0Lo"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?i=C2nrCwsZoOQ:ZfkpDvjdz60:F7zBnMyn0Lo" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=C2nrCwsZoOQ:ZfkpDvjdz60:dnMXMwOfBR0"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=dnMXMwOfBR0" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=C2nrCwsZoOQ:ZfkpDvjdz60:TzevzKxY174"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=TzevzKxY174" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=C2nrCwsZoOQ:ZfkpDvjdz60:qj6IDK7rITs"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=qj6IDK7rITs" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=C2nrCwsZoOQ:ZfkpDvjdz60:I9og5sOYxJI"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=I9og5sOYxJI" border="0"></img></a> 20 | </div><img src="http://feeds.feedburner.com/~r/ziffdavis/pcmag/dvorak/~4/C2nrCwsZoOQ" height="1" width="1" alt=""/>http://www.pcmag.com/article2/0,2817,2525037,00.asp?kc=PCRSS03079TX1K0000584Why Football, Baseball Are Behind the Tech Curvehttp://feedproxy.google.com/~r/ziffdavis/pcmag/dvorak/~3/4F07Uq3Rld0/0,2817,2524854,00.aspWed, 20 Dec 2017 13:00:00 ESTSports leagues will never truly embrace tech that improves the games for players or fans; unless it<div class="feedflare"> 21 | <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=4F07Uq3Rld0:iLeU0YI5rco:yIl2AUoC8zA"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=yIl2AUoC8zA" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=4F07Uq3Rld0:iLeU0YI5rco:Gu391qSwH_A"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=Gu391qSwH_A" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=4F07Uq3Rld0:iLeU0YI5rco:V_sGLiPBpWU"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?i=4F07Uq3Rld0:iLeU0YI5rco:V_sGLiPBpWU" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=4F07Uq3Rld0:iLeU0YI5rco:F7zBnMyn0Lo"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?i=4F07Uq3Rld0:iLeU0YI5rco:F7zBnMyn0Lo" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=4F07Uq3Rld0:iLeU0YI5rco:dnMXMwOfBR0"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=dnMXMwOfBR0" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=4F07Uq3Rld0:iLeU0YI5rco:TzevzKxY174"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=TzevzKxY174" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=4F07Uq3Rld0:iLeU0YI5rco:qj6IDK7rITs"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=qj6IDK7rITs" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=4F07Uq3Rld0:iLeU0YI5rco:I9og5sOYxJI"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=I9og5sOYxJI" border="0"></img></a> 22 | </div><img src="http://feeds.feedburner.com/~r/ziffdavis/pcmag/dvorak/~4/4F07Uq3Rld0" height="1" width="1" alt=""/>http://www.pcmag.com/article2/0,2817,2524854,00.asp?kc=PCRSS03079TX1K0000584Apple Is Ready to Ditch the Machttp://feedproxy.google.com/~r/ziffdavis/pcmag/dvorak/~3/xJ9Ssy_uaXA/0,2817,2524504,00.aspThu, 07 Dec 2017 08:00:00 ESTLet history-and advertising-be your guide into seeing just how iOS is being poised to utterly replac<div class="feedflare"> 23 | <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=xJ9Ssy_uaXA:2B97kW6H0nk:yIl2AUoC8zA"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=yIl2AUoC8zA" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=xJ9Ssy_uaXA:2B97kW6H0nk:Gu391qSwH_A"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=Gu391qSwH_A" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=xJ9Ssy_uaXA:2B97kW6H0nk:V_sGLiPBpWU"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?i=xJ9Ssy_uaXA:2B97kW6H0nk:V_sGLiPBpWU" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=xJ9Ssy_uaXA:2B97kW6H0nk:F7zBnMyn0Lo"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?i=xJ9Ssy_uaXA:2B97kW6H0nk:F7zBnMyn0Lo" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=xJ9Ssy_uaXA:2B97kW6H0nk:dnMXMwOfBR0"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=dnMXMwOfBR0" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=xJ9Ssy_uaXA:2B97kW6H0nk:TzevzKxY174"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=TzevzKxY174" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=xJ9Ssy_uaXA:2B97kW6H0nk:qj6IDK7rITs"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=qj6IDK7rITs" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=xJ9Ssy_uaXA:2B97kW6H0nk:I9og5sOYxJI"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=I9og5sOYxJI" border="0"></img></a> 24 | </div><img src="http://feeds.feedburner.com/~r/ziffdavis/pcmag/dvorak/~4/xJ9Ssy_uaXA" height="1" width="1" alt=""/>http://www.pcmag.com/article2/0,2817,2524504,00.asp?kc=PCRSS03079TX1K0000584Apple iPhone X Kicks Off Face-Recognition Maniahttp://feedproxy.google.com/~r/ziffdavis/pcmag/dvorak/~3/vYGr9a8zNWk/0,2817,2524285,00.aspWed, 29 Nov 2017 08:00:00 ESTEntire countries are going to try and replace IDs with face scans, and it's going to be bad.<div class="feedflare"> 25 | <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=vYGr9a8zNWk:vI2C1I4AbWE:yIl2AUoC8zA"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=yIl2AUoC8zA" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=vYGr9a8zNWk:vI2C1I4AbWE:Gu391qSwH_A"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=Gu391qSwH_A" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=vYGr9a8zNWk:vI2C1I4AbWE:V_sGLiPBpWU"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?i=vYGr9a8zNWk:vI2C1I4AbWE:V_sGLiPBpWU" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=vYGr9a8zNWk:vI2C1I4AbWE:F7zBnMyn0Lo"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?i=vYGr9a8zNWk:vI2C1I4AbWE:F7zBnMyn0Lo" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=vYGr9a8zNWk:vI2C1I4AbWE:dnMXMwOfBR0"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=dnMXMwOfBR0" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=vYGr9a8zNWk:vI2C1I4AbWE:TzevzKxY174"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=TzevzKxY174" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=vYGr9a8zNWk:vI2C1I4AbWE:qj6IDK7rITs"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=qj6IDK7rITs" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=vYGr9a8zNWk:vI2C1I4AbWE:I9og5sOYxJI"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=I9og5sOYxJI" border="0"></img></a> 26 | </div><img src="http://feeds.feedburner.com/~r/ziffdavis/pcmag/dvorak/~4/vYGr9a8zNWk" height="1" width="1" alt=""/>http://www.pcmag.com/article2/0,2817,2524285,00.asp?kc=PCRSS03079TX1K0000584Tech 'Improvements' That Actually Makes Things Worsehttp://feedproxy.google.com/~r/ziffdavis/pcmag/dvorak/~3/2qPu_IUxBuw/0,2817,2524088,00.aspTue, 21 Nov 2017 08:00:00 ESTIntel killing BIOS in favore of UEFI is another turn that will open computers up the malicious progr<div class="feedflare"> 27 | <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=2qPu_IUxBuw:vw1EjZcUeA4:yIl2AUoC8zA"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=yIl2AUoC8zA" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=2qPu_IUxBuw:vw1EjZcUeA4:Gu391qSwH_A"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=Gu391qSwH_A" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=2qPu_IUxBuw:vw1EjZcUeA4:V_sGLiPBpWU"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?i=2qPu_IUxBuw:vw1EjZcUeA4:V_sGLiPBpWU" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=2qPu_IUxBuw:vw1EjZcUeA4:F7zBnMyn0Lo"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?i=2qPu_IUxBuw:vw1EjZcUeA4:F7zBnMyn0Lo" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=2qPu_IUxBuw:vw1EjZcUeA4:dnMXMwOfBR0"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=dnMXMwOfBR0" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=2qPu_IUxBuw:vw1EjZcUeA4:TzevzKxY174"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=TzevzKxY174" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=2qPu_IUxBuw:vw1EjZcUeA4:qj6IDK7rITs"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=qj6IDK7rITs" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=2qPu_IUxBuw:vw1EjZcUeA4:I9og5sOYxJI"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=I9og5sOYxJI" border="0"></img></a> 28 | </div><img src="http://feeds.feedburner.com/~r/ziffdavis/pcmag/dvorak/~4/2qPu_IUxBuw" height="1" width="1" alt=""/>http://www.pcmag.com/article2/0,2817,2524088,00.asp?kc=PCRSS03079TX1K0000584Robots Won't Kill You, But They Will Take Your Jobhttp://feedproxy.google.com/~r/ziffdavis/pcmag/dvorak/~3/W_lflq6t2Rc/0,2817,2523668,00.aspWed, 08 Nov 2017 08:00:00 ESTThat's not true. Worrying about scifi Terminators and RoboCops means you're ignoring the real danger<div class="feedflare"> 29 | <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=W_lflq6t2Rc:RxUrrKNyYlU:yIl2AUoC8zA"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=yIl2AUoC8zA" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=W_lflq6t2Rc:RxUrrKNyYlU:Gu391qSwH_A"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=Gu391qSwH_A" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=W_lflq6t2Rc:RxUrrKNyYlU:V_sGLiPBpWU"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?i=W_lflq6t2Rc:RxUrrKNyYlU:V_sGLiPBpWU" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=W_lflq6t2Rc:RxUrrKNyYlU:F7zBnMyn0Lo"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?i=W_lflq6t2Rc:RxUrrKNyYlU:F7zBnMyn0Lo" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=W_lflq6t2Rc:RxUrrKNyYlU:dnMXMwOfBR0"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=dnMXMwOfBR0" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=W_lflq6t2Rc:RxUrrKNyYlU:TzevzKxY174"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=TzevzKxY174" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=W_lflq6t2Rc:RxUrrKNyYlU:qj6IDK7rITs"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=qj6IDK7rITs" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=W_lflq6t2Rc:RxUrrKNyYlU:I9og5sOYxJI"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=I9og5sOYxJI" border="0"></img></a> 30 | </div><img src="http://feeds.feedburner.com/~r/ziffdavis/pcmag/dvorak/~4/W_lflq6t2Rc" height="1" width="1" alt=""/>http://www.pcmag.com/article2/0,2817,2523668,00.asp?kc=PCRSS03079TX1K0000584Is iPhone X Apple's Vista?http://feedproxy.google.com/~r/ziffdavis/pcmag/dvorak/~3/srg3z1A6poQ/0,2817,2523452,00.aspWed, 01 Nov 2017 08:00:00 ESTApple is taking a different approach with review units for iPhone X. What does it all mean?<div class="feedflare"> 31 | <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=srg3z1A6poQ:LM0r5S6Gqsg:yIl2AUoC8zA"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=yIl2AUoC8zA" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=srg3z1A6poQ:LM0r5S6Gqsg:Gu391qSwH_A"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=Gu391qSwH_A" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=srg3z1A6poQ:LM0r5S6Gqsg:V_sGLiPBpWU"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?i=srg3z1A6poQ:LM0r5S6Gqsg:V_sGLiPBpWU" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=srg3z1A6poQ:LM0r5S6Gqsg:F7zBnMyn0Lo"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?i=srg3z1A6poQ:LM0r5S6Gqsg:F7zBnMyn0Lo" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=srg3z1A6poQ:LM0r5S6Gqsg:dnMXMwOfBR0"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=dnMXMwOfBR0" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=srg3z1A6poQ:LM0r5S6Gqsg:TzevzKxY174"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=TzevzKxY174" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=srg3z1A6poQ:LM0r5S6Gqsg:qj6IDK7rITs"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=qj6IDK7rITs" border="0"></img></a> <a href="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?a=srg3z1A6poQ:LM0r5S6Gqsg:I9og5sOYxJI"><img src="http://feeds.feedburner.com/~ff/ziffdavis/pcmag/dvorak?d=I9og5sOYxJI" border="0"></img></a> 32 | </div><img src="http://feeds.feedburner.com/~r/ziffdavis/pcmag/dvorak/~4/srg3z1A6poQ" height="1" width="1" alt=""/>http://www.pcmag.com/article2/0,2817,2523452,00.asp?kc=PCRSS03079TX1K0000584 33 | 34 | -------------------------------------------------------------------------------- /test/feeds/intertwingly.atom: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | http://intertwingly.net/blog/index.atom 7 | ../favicon.ico 8 | 9 | Sam Ruby 10 | It’s just data 11 | 12 | Sam Ruby 13 | rubys@intertwingly.net 14 | /blog/ 15 | 16 | 2012-07-25T12:39:39-07:00 17 | 18 | 19 | 20 | 21 | tag:intertwingly.net,2004:3299 22 | 23 | 24 | Inhibiting Suspend 25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |

The interface is a bit low level, but workable:

36 |
require 'dbus' # gem install ruby-dbus
 37 | bus = DBus::SessionBus.instance
 38 | sm = bus.service('org.gnome.SessionManager').object('/org/gnome/SessionManager')
 39 | sm.introspect
 40 | sm.default_iface = 'org.gnome.SessionManager'
 41 | cookie = sm.Inhibit($0, 0, 'inhibiting', 4).first
 42 | at_exit { cookie = sm.Uninhibit(cookie) if sm.IsInhibited(4).first }
43 |

Note: the call to Uninhibit is optional — it will occur on process exit anyway.

44 |

Hat tip to JanuZ.

45 | 2012-07-16T08:48:01-07:00 46 |
47 | 48 | 49 | tag:intertwingly.net,2004:3298 50 | 51 | 52 | utf8mb4 53 |
54 | 55 | 56 |

Jacques Distler: Remarkably, even after a decade of such pain, Unicode is, in 2012, still “cutting edge.”

57 |

Ouch.

58 | 2012-07-10T10:55:32-07:00 59 |
60 | 61 | 62 | tag:intertwingly.net,2004:3297 63 | 64 | 65 | Ubuntu 12.04 and Ruby 1.9.3 66 |

I previously had installed Ubuntu 12.04 on a NetBook, and my overall impression was simply that it was more stable than its predecessor — particularly for Unity.

67 |

For the first time I tried it on a desktop, and to my surprise the following worked:

68 |
sudo apt-get install ruby1.9.3
69 |

And by worked, I mean not only did it install Ruby 1.9.3, but it made it (and gem, and irc) the default ruby.

70 |

For those that still use rvm, (many of the ‘cool kids’ have moved on to rbenv, I noticed a few niggles

71 |
72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 |

I previously had installed Ubuntu 12.04 on a NetBook, and my overall impression was simply that it was more stable than its predecessor — particularly for Unity.

98 |

For the first time I tried it on a desktop, and to my surprise the following worked:

99 |
sudo apt-get install ruby1.9.3
100 |

And by worked, I mean not only did it install Ruby 1.9.3, but it made it (and gem, and irc) the default ruby.

101 |

For those that still use rvm, (many of the ‘cool kids’ have moved on to rbenv, I noticed a few niggles:

102 |
    103 |
  • Don’t follow the instructions and specify --ruby or --rails. You will get a version of Ruby that can’t install gems. Simply omit that parameter.
  • 104 |
  • Next set the ‘Run command as login shell’ checkbox.
  • 105 |
  • Then run rvm requirements and install what it tells you to install.
  • 106 |
  • Finally, run rvm install 1.9.3 to build the latest.
  • 107 |
108 | 109 |

Personally, I follow that up with rvm --default system.  That means that while I have other Rubies available at my finger-tips, the one I generally use is the one provided with Ubuntu.

110 | 2012-06-23T15:45:50-07:00 111 |
112 | 113 | 114 | tag:intertwingly.net,2004:3296 115 | 116 | 117 | Prefixed no more 118 |
119 | 120 | 121 | 122 | 123 | 124 |

Firefox 13 for developers: Support for -moz-border-radius*  and -moz-box-shadow has been removed. Authors should use unprefixed border-radius or box-shadow instead. See bug 693510

125 |

+1

126 | 2012-06-07T05:33:07-07:00 127 |
128 | 129 | 130 | tag:intertwingly.net,2004:3295 131 | 132 | 133 | Twitter -= #! 134 |
135 | 136 | 137 | 138 | Dan Webb: The first thing that you might notice is that permalink URLs are now simpler: they no longer use the hashbang (#!). While hashbang-style URLs have a handful of limitations, our primary reason for this change is to improve initial page-load performance.
139 | 2012-05-29T14:50:26-07:00 140 |
141 | 142 | 143 | tag:intertwingly.net,2004:3294 144 | 145 | 146 | WebSocket Demos 147 |
148 | 149 | W 150 | 151 |

chat implements a shared textarea field across multiple clients.  Demonstrates bi-directional communication.

152 |

diskusage is more typical of my usage.  The du command produces tabular output that the user may want to sort different ways and yet is may take considerable time to complete.

153 | 2012-04-29T18:33:49-07:00 154 |
155 | 156 | 157 | tag:intertwingly.net,2004:3293 158 | 159 | 160 | Wunderbar on Rails 161 |

Usage: add wunderbar and nokogiri to your Gemfile and run bundle install.  Template extensions supported are _html and _json.  Examples: view, layout, json.

162 |

Note that as Rails layouts and views are predicated on the assumption that output is produced by concatenating text, one must use _ yield instead of simply yield.  On the plus side, Wunderbar will note when the first argument to a call which creates an element is html_safe? and will treat it as markup.

163 |
164 | 165 | W 166 | 167 |

Usage: add wunderbar and nokogiri to your Gemfile and run bundle install.  Template extensions supported are _html and _json.  Examples: view, layout, json.

168 |

Note that as Rails layouts and views are predicated on the assumption that output is produced by concatenating text, one must use _ yield instead of simply yield.  I have noticed that this may lose blank lines in the process, which apparently is a known issue with Nokogiri.  Not a problem if the layout is erb, but then you lose the unified indentation that you get if you have a layout using _html.

169 |

On the plus side, Wunderbar will note when the first argument to a call which creates an element is html_safe? and will treat it as markup.  An example of where this is useful would be in the _td link_to calls below.

170 |
_h1_ 'Listing products'
171 | 
172 | _table do
173 |   _tr do
174 |     _th 'Title'
175 |     _th
176 |     _th
177 |     _th
178 |   end
179 | 
180 |   @products.each do |product|
181 |     _tr_ do
182 |       _td product.title
183 |       _td link_to 'Show', product
184 |       _td link_to 'Edit', edit_product_path(product)
185 |       _td link_to 'Destroy', product, confirm: 'Are you sure?', method: :delete 
186 |     end
187 |   end
188 | end
189 | 
190 | _br_
191 | 
192 | _ link_to 'New Product', new_product_path
193 | 2012-04-24T14:12:59-07:00 194 |
195 | 196 | 197 | tag:intertwingly.net,2004:3292 198 | 199 | 200 | Wunderbar now does Sinatra 201 |

Demo

202 |

The result is a lot like Markaby, except you get to be/have to be explicit when you are creating a tag.  In this demo, there is no logic, so the benefits of doing so are less clear, but include you being able to use tags that aren’t known to Markaby, like the ones that were added in HTML5.  Both inline and views are supported, but support for layouts has yet to be added.

203 |

Future plans include Rails.

204 |
205 | 206 | W 207 | 208 |

Demo

209 |

The result is a lot like Markaby, except you get to be/have to be explicit when you are creating a tag.  In this demo, there is no logic, so the benefits of doing so are less clear, but include you being able to use tags that aren’t known to Markaby, like the ones that were added in HTML5.  Both inline and views are supported, but support for layouts has yet to be added.

210 |

While the demos require Ruby 1.9.2+ (the Hash syntax is nicer), the library works equally well with Ruby 1.8.7.

211 |

The progression is that you start from scripts that you can run from the command line:

212 |
ruby helloworld.rb
213 |

...can pass arguments to:

214 |
ruby helloword.rb name=Sam
215 |

...can run as a standalone server:

216 |
ruby helloworld.rb --port=3004
217 |

...can install as a CGI:

218 |
ruby helloworld.rb --install="/Library/WebServer/Documents/helloworld.cgi"
219 |

.,. and can now run under Sinatra.  Future plans include Rails.

220 |

There even is a tool that will reverse engineer an existing web page into a script.

221 | 2012-04-12T17:12:31-07:00 222 |
223 | 224 | 225 | tag:intertwingly.net,2004:3291 226 | 227 | 228 | Hacked 229 |

This site was hacked.  A reader of the site noted that Google’s index of this site had been co-opted by dubious pharmaceutical offerings.  I’ll gladly thank that individual publicly if they give me permission to do so; but my email reply got bounced as spam.

230 |

The immediate culprit was the addition of the following lines to a number of .htaccess files

231 |
232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 |

This site was hacked.  A reader of the site noted that Google’s index of this site had been co-opted by dubious pharmaceutical offerings.  I’ll gladly thank that individual publicly if they give me permission to do so; but my email reply got bounced as spam.

243 |

The immediate culprit was the addition of the following lines to a number of .htaccess files:

244 |
<IfModule mod_rewrite.c>
245 | RewriteEngine On
246 | RewriteCond %{HTTP_USER_AGENT} (google|yahoo) [OR]
247 | RewriteCond %{HTTP_REFERER} (google|aol|yahoo)
248 | RewriteCond %{REQUEST_URI} /$ [OR]
249 | RewriteCond %{REQUEST_FILENAME} (html|htm|php)$ [NC]
250 | RewriteCond %{REQUEST_FILENAME} !common.php
251 | RewriteRule ^.*$    /common.php [L]
252 | </IfModule>
253 |

I removed those lines, as well as the common.php file, and scanned any and all php files on my site.  I saw the addition of lines such as the following:

254 |
$FYAqxDo='p'.'r'. 'eg_repl'. 'ace';...
255 | $IHxWfs=str_rot13('cert_ercynpr');...
256 | $DcNZVHCi="eW6DLAlbeAki"^"...
257 | $LYDmvYopCKSSSGcfCVNpsskU='ba'.'se64_'.'deco'.'de'...
258 |

I had old (vintage 2006) installations of PHP-openid-1.2.1 and PHP-yadis-1.0.2 that I am tentatively assuming were the ports of initial entry.

259 |

I also wiped my .ssh directory.  It has a private key there that was generated for this site that presumably was legitimate, but unused by me and now presumed compromised.  I never initiate sessions from this host, nor do I have any passwords saved there, so any damage caused was isolated.

260 |

I do daily backups of my site, which I keep for a week; as well as monthly backups that I basically keep forever.  In addition, as I recently migrated hosts, I have a hot backup.

261 |

The PHP hacks were done after I migrated but before March 1st.  The htaccess hacks were done over a week ago, but after March 1st.

262 |

Over the next few days, I’ll be looking at diffs of different snapshots of my site contents to see if there is anything else I missed.

263 | 2012-04-02T04:16:43-07:00 264 |
265 | 266 | 267 | tag:intertwingly.net,2004:3290 268 | 269 | 270 | Improved Wunderbar JSON support 271 |
272 | 273 | W 274 | 275 |

I’ve integrated jbuilder like functionality into Wunderbar.  Key differences?  A DSL that doesn’t suck, and output that isn’t ugly.

276 |

To harsh?  You be the judge.  Compare jbuilder ("json dot bar json bar json dot child bang") vs Wunderbar ("underbar underbar underbar underbar").

277 |

As to the output?  Don’t be fooled by the jbuilder readme.  In actuality is no unnecessary whitespace in the output.  That’s good if you are bandwidth limited.  Not so good when viewing the XHR traffic via firebug...

278 | 2012-04-01T05:57:41-07:00 279 |
280 | 281 | 282 | tag:intertwingly.net,2004:3289 283 | 284 | 285 | Keeping it on the Rails 286 |

It is increasingly becoming the case that Agile Web Development with Rails is being actively co-developed with Rails itself.

287 |

While my tests have been an official part of the release process for a long time now, yesterday’s release of 3.2.3RC1 provides a number of examples that illustrate this.

288 |

The intent is to prove an updated to the eBook free of charge which incorporates the necessary changes, either concurrent with the final release of 3.2.3 or shortly thereafter.

289 |
290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 |

It is increasingly becoming the case that Agile Web Development with Rails is being actively co-developed with Rails itself.

384 |

While my tests have been an official part of the release process for a long time now, yesterday’s release of 3.2.3RC1 provides a number of examples that illustrate this.

385 |

Within hours after the release, I got an excited IM from Santiago Pastorino that my tests were failing.  In particular, the failure was thus:

386 |
rake db:migrate
387 | rake aborted!
388 | An error has occurred, this and all later migrations canceled:
389 | uninitialized constant Arel::Relation
390 | Tasks: TOP => db:migrate
391 | (See full trace by running task with --trace)
392 |

The root cause was quickly determined to be a recent change to arel, and a number of corrective actions were promptly taken: first, the change was backed out, then Rails 4.0 was updated and Rails 3.2 was changed to point to a branch of arel, and finally, the original change was reapplied.

393 |

The previous error that was caught was connection pool of new applications have size 1.  This demonstrates the unique value that my tests bring to the table.  Outside of my tests, the bulk of the test of Rails is an impressive array of unit tests (which verify that the connection pool setting does what it is supposed to do), and real world testing (using applications with highly tuned configurations), and my tests.  Only the latter is effectively testing that the defaults provided actually work together to provide a viable configuration to use as a starter set for new applications.

394 |

One last example, this one shows the level cooperation involved.  The underlying security changes that were the raison d'être for the 3.2.3 release caused the following scenario to fail:

395 |
rails generate scaffold Product title:string
396 | rake db:migrate
397 | rake test
398 |

The root cause was that the code generated as scaffolding used the very feature which is now being discouraged as it creates a security issue. The fix required both changes to Rails itself (to change the scaffolding generated) and to the scenario provided in the book (both in identifying the code that needs to be changed, and in the changes that need to be made).

399 |

The intent is to prove an updated to the eBook free of charge which incorporates the necessary changes, either concurrent with the final release of 3.2.3 or shortly thereafter.

400 | 2012-03-28T13:44:13-07:00 401 |
402 | 403 | 404 | tag:intertwingly.net,2004:3288 405 | 406 | 407 | Hearing Aid 408 |
409 | 410 | 411 |

I now have an open fit hearing aid device for my left ear.  That ear has experienced tinnitus for approximately 10 years.  An ABR test given at that time found nothing.  My hearing loss is primarily in the 2000 Hz and above.

412 |

Last night, I went to dinner with my wife in a noisy restaurant and I could hear every word she said.

413 | 2012-02-25T04:34:06-08:00 414 |
415 | 416 | 417 | tag:intertwingly.net,2004:3287 418 | 419 | 420 | Your Next Desktop Could be a Phone 421 |
422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 |

Henri Sivonen: This getting interesting: Using an Android phone as an Ubuntu desktop when docked

448 |

Definitely Want.

449 |

Especially love the idea of sending and receiving texts from my desktop.  Would prefer a dock the size of a mac mini with a hard drive, USB and ethernet ports.

450 | 2012-02-22T03:27:07-08:00 451 |
452 | 453 | 454 | tag:intertwingly.net,2004:3286 455 | 456 | 457 | Mulligan 458 |
459 | 460 | 461 | 462 |

Dan Webb: I’m in charge of undoing twitters hashbang URLs

463 |

+1

464 | 2012-02-20T15:13:02-08:00 465 |
466 | 467 | 468 | tag:intertwingly.net,2004:3285 469 | 470 | 471 | OpenID upgrades 472 |
473 | 474 | 475 | 476 |

As a part of my server move, I’ve upgraded my consumer logic to Python openid-2.2.5 and provider from phpMyID to SimpleId 0.8.1.  In theory, I should now support OpenID 2.0.

477 |

The one small API change I noted in this process is in the consumer.  server.complete now needs an additional returnto parameter.

478 | 2012-02-19T11:09:11-08:00 479 |
480 | 481 | 482 | tag:intertwingly.net,2004:3284 483 | 484 | 485 | WunderWiki 486 |
487 | 488 | W 489 | 490 |

I added a simple wiki as a demo for WunderBar.  It demonstrates shelling out to handing multiple URIs, handing get/post, dealing with both unescaped text and markup, shelling out to commands, AJAX, CSS, jQuery.

491 |

The demo relies on git and rDiscount to do the version control and markup processing.  It also doesn’t have all the features that you would expect from a wiki, such as conflict detection and resolution.

492 | 2012-02-18T08:20:43-08:00 493 |
494 | 495 | 496 | tag:intertwingly.net,2004:3283 497 | 498 | 499 | Twelve Steps 500 |
501 | 502 | 504 | 505 | 506 | 507 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | Jeni Tennison: it became clear that there were several places where having some kind of standard method for building a tree from non-well-formed XML would be beneficial...So the XML Error Recovery Community Group has been set up for this purpose.
516 | 2012-02-15T07:43:10-08:00 517 |
518 | 519 | 520 | tag:intertwingly.net,2004:3282 521 | 522 | 523 | On The Move... 524 |
525 | 526 | 527 | 528 | 529 |

Intertwingly.net is moving to DreamHost.  I’m sure that every one of my scripts has hard coded paths or depend on the server being in EST/EDT or will otherwise break for unanticipated (but in retrospect entirely obvious) reasons.  I don’t believe that I will lose any email in the process, but you never know.

530 |

My @apache.org email address will not be affected by this move.

531 | 2012-02-13T10:06:16-08:00 532 |
533 | 534 | 535 | tag:intertwingly.net,2004:3281 536 | 537 | 538 | Dominoes 539 |
540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 |

Alex Russell: @glazou being entirely reasonable in the face of vendor-driven CRAZY (implementing other people’s prefixes): glazman.org/weblog/dotclea… Via @phae.

549 |

Alex, I think you need to move up the food chain a little.

550 |

The root-cause is vendor-driven advocacy directed at content producers which encourages them to produce compelling content using experimental features.  Everything else is consequences.  If you believe that those consequences are CRAZY, then you must conclude that the root-cause is CRAZY.

551 | 2012-02-09T04:57:20-08:00 552 |
553 | 554 | 555 | tag:intertwingly.net,2004:3280 556 | 557 | 558 | Default to Incognito 559 |
560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 |

Patch for /usr/share/applications/google-chrome.desktop:

574 |
108c108
575 | < Exec=/opt/google/chrome/google-chrome %U
576 | ---
577 | > Exec=/opt/google/chrome/google-chrome %U --incognito
578 | 114c114
579 | < X-Ayatana-Desktop-Shortcuts=NewWindow;NewIncognito
580 | ---
581 | > X-Ayatana-Desktop-Shortcuts=NewIncognito;NewWindow
582 | 2012-02-04T13:54:53-08:00 583 |
584 | 585 |
586 | 587 | -------------------------------------------------------------------------------- /test/feeds/invalid-characters-gzipped.xml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danmactough/node-feedparser/0a734f88195e4c7048a53867a8ec7c52f150fff2/test/feeds/invalid-characters-gzipped.xml -------------------------------------------------------------------------------- /test/feeds/mediacontent-dupes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Home | Mail Online 6 | http://www.dailymail.co.uk/ushome/index.html?ITO=1490&ns_mchannel=rss&ns_campaign=1490 7 | MailOnline - all the latest news, sport, showbiz, science and health stories from around the world from the Daily Mail and Mail on Sunday newspapers 8 | en-gb 9 | Copyright 2013 Associated Newspapers Ltd 10 | Sun, 03 Feb 2013 04:14:36 GMT 11 | editorial@dailymailonline.co.uk (Editor) 12 | Home 13 | Regional/Europe/United_Kingdom/News_and_Media/Newspapers 14 | 15 | Home | Mail Online 16 | http://i.mol.im/i/furniture/mastHead_1_rss.gif 17 | http://www.dailymail.co.uk/ushome/index.html?ITO=1490 18 | 19 | 20 | Could the infamous Black Dahlia case be about to be solved? Cadaver dog discovers death scent at Hollywood home of suspect 66 years after the horrific murder 21 | http://www.dailymail.co.uk/news/article-2272640/Could-infamous-Black-Dahlia-case-solved-Cadaver-dog-discovers-death-scent-Hollywood-home-suspect-66-years-horrific-murder.html?ITO=1490&ns_mchannel=rss&ns_campaign=1490 22 | The shocking murder of Elizabeth Short, known as the Black Dahlia, is one of the oldest unsolved murder cases in Los Angeles history. 23 | 24 | 25 | Sun, 03 Feb 2013 04:13:03 GMT 26 | http://www.dailymail.co.uk/news/article-2272640/Could-infamous-Black-Dahlia-case-solved-Cadaver-dog-discovers-death-scent-Hollywood-home-suspect-66-years-horrific-murder.html?ITO=1490&ns_mchannel=rss&ns_campaign=1490 27 | © Bettmann/CORBIS 28 | Black Dahlia: A head shot of Elizabeth Short who had been an aspiring actress until her untimely murder 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /test/feeds/non-text-alternate-links.xml: -------------------------------------------------------------------------------- 1 | 2 | nLab 3 | 4 | 5 | 2017-09-21T16:03:01Z 6 | tag:ncatlab.org,2008-11-28:nLab 7 | An Instiki Wiki 8 | Instiki 9 | 10 | (0,1)-category 11 | 12 | 13 | 2017-09-21T16:03:01Z 14 | 2009-09-08T23:20:59Z 15 | tag:ncatlab.org,2009-09-08:nLab,%280%2C1%29-category 16 | 17 | Mike Shulman 18 | 19 | Updated by Mike Shulman on 2017-09-21 at 16:03:01Z. 20 | 21 | 22 | -------------------------------------------------------------------------------- /test/feeds/nondefaultnamespace-baseline.atom: -------------------------------------------------------------------------------- 1 | 2 | 3 | Non-default namespace test baseline 4 | 2006-01-18T12:26:54+01:00 5 | 6 | 7 | Aristotle Pagaltzis 8 | pagaltzis@gmx.de 9 | 10 | urn:uuid:f8195e66-863f-11da-9fcb-dd680b0526e0 11 | 12 | urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a 13 | If you can read the content of this entry, checking the test case will be meaningful. 14 | 15 | 2006-01-18T12:26:54+01:00 16 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /test/feeds/nondefaultnamespace-xhtml.atom: -------------------------------------------------------------------------------- 1 | 2 | 3 | Non-default XHTML namespace test 4 | 2006-01-18T12:26:54+01:00 5 | 6 | 7 | Aristotle Pagaltzis 8 | pagaltzis@gmx.de 9 | 10 | urn:uuid:f8195e66-863f-11da-9fcb-dd680b0526e0 11 | 12 | urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a 13 | If you can read the content of this entry, and it has a list and links, your aggregator works fine. 14 | 15 | 2006-01-18T12:26:54+01:00 16 | 17 | For information, see: 18 | 19 | Who knows an XML document from a hole in the ground? 20 | More on Atom aggregator XML namespace conformance tests 21 | XML Namespace Conformance Tests 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /test/feeds/nondefaultnamespace.atom: -------------------------------------------------------------------------------- 1 | 2 | 3 | Non-default namespace test 4 | 2006-01-18T12:26:54+01:00 5 | 6 | 7 | Aristotle Pagaltzis 8 | pagaltzis@gmx.de 9 | 10 | urn:uuid:f8195e66-863f-11da-9fcb-dd680b0526e0 11 | 12 | urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a 13 | If you can read the content of this entry, your aggregator works fine. 14 | 15 | 2006-01-18T12:26:54+01:00 16 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /test/feeds/notafeed.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | The Is Not The Feed You're Looking For 6 | 7 | 8 |

Move Along. Move Along.

9 | 10 | -------------------------------------------------------------------------------- /test/feeds/relative-channel-image-url.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Oracle VM VirtualBox: Ticket #10860: Full resolution not exposed to Linux guest 5 | https://www.virtualbox.org/ticket/10860 6 | <p> 7 | When using <a class="wiki" href="https://www.virtualbox.org/wiki/VirtualBox">VirtualBox</a> on a MacBook Pro Retina, only half the resolution is exposed to, at least, Linux guests. This causes the fonts to be blurry in the guest OS. 8 | </p> 9 | <p> 10 | To verify this, install Linux, and go to fullscreen, type xrandr and notice the resolution is set to 1440x900. 11 | </p> 12 | <p> 13 | To compare with VMWare, full Retina mode is supported: <a class="ext-link" href="http://www.vmware.com/products/fusion/features.html#approved"><span class="icon"> </span>http://www.vmware.com/products/fusion/features.html#approved</a> 14 | </p> 15 | <p> 16 | This exposes the current full resolution to the guest, which in the default host retina mode would mean the guest sees 2880x1800 resolution and has to deal with it on its own accordingly. 17 | </p> 18 | 19 | en-us 20 | 21 | Oracle VM VirtualBox 22 | /graphics/vbox_logo2_gradient.png 23 | https://www.virtualbox.org/ticket/10860 24 | 25 | Trac 0.12 26 | 27 | 28 | dsvensson 29 | 30 | Fri, 24 Aug 2012 08:54:14 GMT 31 | 32 | https://www.virtualbox.org/ticket/10860#comment:1 33 | https://www.virtualbox.org/ticket/10860#comment:1 34 | 35 | <p> 36 | The <a class="wiki" href="https://www.virtualbox.org/wiki/VirtualBox">VirtualBox</a> version is actually the 4.2 beta but it couldn't be selected. 37 | </p> 38 | 39 | Ticket 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /test/feeds/rss-with-relative-urls-with-absolute-xmlurl.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <![CDATA[Neverwinter]]> 6 | /en/games/neverwinter/news 7 | 8 | en_US 9 | Sun, 10 Dec 17 13:01:36 -0800 10 | 12 | 13 | http://images-cdn.perfectworld.com/arc/0e/22/0e221ffcf7ab4eae547c72deecfc7be61501001169.png 14 | <![CDATA[Neverwinter]]> 15 | /en/games/neverwinter/news 16 | 17 | 60 18 | 19 | 20 | <![CDATA[Neverwinter & Jingle Jam Humble Bundle!]]> 21 | 32 | /en/games/neverwinter/news/detail/10743874-neverwinter-%26-jingle-jam-humble-bundle%21 33 | http://www.arcgames.com/en/games/PAB_nw/news/detail/10743874 34 | Thu, 07 Dec 17 10:00:29 -0800 35 | 36 | 37 | <![CDATA[2x Underdark Currency & 15% off Bags!]]> 38 | 105 | /en/games/neverwinter/news/detail/10738994-2x-underdark-currency-%26-15%25-off-bags%21 106 | http://www.arcgames.com/en/games/PAB_nw/news/detail/10738994 107 | Thu, 07 Dec 17 07:00:00 -0800 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /test/feeds/rss-with-relative-urls.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <![CDATA[Neverwinter]]> 6 | /en/games/neverwinter/news 7 | 8 | en_US 9 | Sun, 10 Dec 17 13:01:36 -0800 10 | 12 | 13 | http://images-cdn.perfectworld.com/arc/0e/22/0e221ffcf7ab4eae547c72deecfc7be61501001169.png 14 | <![CDATA[Neverwinter]]> 15 | /en/games/neverwinter/news 16 | 17 | 60 18 | 19 | 20 | <![CDATA[Neverwinter & Jingle Jam Humble Bundle!]]> 21 | 32 | /en/games/neverwinter/news/detail/10743874-neverwinter-%26-jingle-jam-humble-bundle%21 33 | http://www.arcgames.com/en/games/PAB_nw/news/detail/10743874 34 | Thu, 07 Dec 17 10:00:29 -0800 35 | 36 | 37 | <![CDATA[2x Underdark Currency & 15% off Bags!]]> 38 | 105 | /en/games/neverwinter/news/detail/10738994-2x-underdark-currency-%26-15%25-off-bags%21 106 | http://www.arcgames.com/en/games/PAB_nw/news/detail/10738994 107 | Thu, 07 Dec 17 07:00:00 -0800 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /test/feeds/rss2sample.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Liftoff News 5 | http://liftoff.msfc.nasa.gov/ 6 | Liftoff to Space Exploration. 7 | en-us 8 | Tue, 10 Jun 2003 04:00:00 GMT 9 | Tue, 10 Jun 2003 09:41:01 GMT 10 | http://blogs.law.harvard.edu/tech/rss 11 | Weblog Editor 2.0 12 | editor@example.com 13 | webmaster@example.com 14 | 15 | Star City 16 | writer@example.com (Writer) 17 | http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp 18 | How do Americans get ready to work with Russians aboard the International Space Station? They take a crash course in culture, language and protocol at Russia's <a href="http://howe.iki.rssi.ru/GCTC/gctc_e.htm">Star City</a>. 19 | Tue, 03 Jun 2003 09:39:21 GMT 20 | http://liftoff.msfc.nasa.gov/2003/06/03.html#item573 21 | 22 | 23 | Sky watchers in Europe, Asia, and parts of Alaska and Canada will experience a <a href="http://science.nasa.gov/headlines/y2003/30may_solareclipse.htm">partial eclipse of the Sun</a> on Saturday, May 31st. 24 | Fri, 30 May 2003 11:06:42 GMT 25 | http://liftoff.msfc.nasa.gov/2003/05/30.html#item572 26 | 27 | 28 | The Engine That Does More 29 | http://liftoff.msfc.nasa.gov/news/2003/news-VASIMR.asp 30 | Before man travels to Mars, NASA hopes to design new engines that will let us fly through the Solar System more quickly. The proposed VASIMR engine would do that. 31 | Tue, 27 May 2003 08:37:32 GMT 32 | http://liftoff.msfc.nasa.gov/2003/05/27.html#item571 33 | 34 | 35 | Astronauts' Dirty Laundry 36 | http://liftoff.msfc.nasa.gov/news/2003/news-laundry.asp 37 | Compared to earlier spacecraft, the International Space Station has many luxuries, but laundry facilities are not one of them. Instead, astronauts have other options. 38 | Tue, 20 May 2003 08:56:02 GMT 39 | http://liftoff.msfc.nasa.gov/2003/05/20.html#item570 40 | 41 | 42 | -------------------------------------------------------------------------------- /test/feeds/unknown-namespace.atom: -------------------------------------------------------------------------------- 1 | 2 | 3 | Prefixed XHTML with unprefixed bogo-namespace 4 | 2006-01-22T19:47:34Z 5 | 6 | 7 | Henri Sivonen 8 | hsivonen@iki.fi 9 | 10 | http://hsivonen.iki.fi/test/unknown-namespace.atom 11 | 12 | http://hsivonen.iki.fi/test/unknown-namespace.atom/entry 13 | This entry contains XHTML-looking markup that is not XHTML 14 | 15 | 2006-01-22T19:47:34Z 16 | 17 | 18 | This is an XHTML list item. If it is not rendered as a list item, the namespace support of the client app is broken. 19 | 20 |
    21 |
  • This is not an XHTML list item. If it is rendered as a list item, the namespace support of the client app is broken.
  • 22 |
23 |
24 |
25 |
26 | -------------------------------------------------------------------------------- /test/illegally-nested.js: -------------------------------------------------------------------------------- 1 | describe('illegally nested', function(){ 2 | 3 | var feed = __dirname + '/feeds/illegally-nested.xml'; 4 | 5 | it('should ignore illegally-nested items', function (done){ 6 | var itemCount = 0; 7 | fs.createReadStream(feed).pipe(new FeedParser()) 8 | .on('readable', function () { 9 | var stream = this; 10 | while (stream.read()) { 11 | itemCount++; 12 | } 13 | }) 14 | .on('finish', function () { 15 | assert.strictEqual(itemCount, 10); 16 | done(); 17 | }) 18 | .on('error', function (err) { 19 | assert.ifError(err); 20 | done(err); 21 | }); 22 | }); 23 | 24 | }); 25 | 26 | -------------------------------------------------------------------------------- /test/link.js: -------------------------------------------------------------------------------- 1 | describe('links', function(){ 2 | 3 | var feed = __dirname + '/feeds/non-text-alternate-links.xml'; 4 | 5 | it('should extract alternate links from feed', function (done) { 6 | fs.createReadStream(feed).pipe(new FeedParser()) 7 | .once('readable', function () { 8 | var stream = this; 9 | assert.equal(stream.read().link, 'https://ncatlab.org/nlab/show/%280%2C1%29-category'); 10 | done(); 11 | }) 12 | .on('error', function (err) { 13 | assert.ifError(err); 14 | done(err); 15 | }); 16 | }); 17 | 18 | }); 19 | -------------------------------------------------------------------------------- /test/namespaces.js: -------------------------------------------------------------------------------- 1 | describe('namespaced elements', function(){ 2 | 3 | describe('standard namespaces', function(){ 4 | 5 | var feed = __dirname + '/feeds/wapowellness.xml'; 6 | 7 | it('should parse common standard namespaces', function (done) { 8 | fs.createReadStream(feed).pipe(new FeedParser()) 9 | .once('readable', function () { 10 | var stream = this; 11 | var item = stream.read(); 12 | assert.equal(item.author, 'Lenny Bernstein'); 13 | assert.equal(item.origlink, 'http://www.washingtonpost.com/lifestyle/wellness/schools-minister-to-kids-fitness-and-nutrition-needs/2012/08/21/0ca90d46-e6eb-11e1-936a-b801f1abab19_story.html?wprss=rss_wellness'); 14 | done(); 15 | }) 16 | .on('error', function (err) { 17 | assert.ifError(err); 18 | done(err); 19 | }); 20 | }); 21 | 22 | }); 23 | 24 | describe('non-standard namespaces', function(){ 25 | 26 | var feed = __dirname + '/feeds/complexNamespaceFeed.xml'; 27 | 28 | it('should parse non-standard namespaces', function (done) { 29 | fs.createReadStream(feed).pipe(new FeedParser()) 30 | .once('readable', function () { 31 | var stream = this; 32 | var item = stream.read(); 33 | assert.equal(item.guid, 'urn:uuid:d5ffaea2-0a9a-4f38-98fc-5c364177b6b4'); 34 | done(); 35 | }) 36 | .on('error', function (err) { 37 | assert.ifError(err); 38 | done(err); 39 | }); 40 | }); 41 | 42 | }); 43 | 44 | describe('nondefaultnamespace-baseline', function(){ 45 | 46 | var feed = __dirname + '/feeds/nondefaultnamespace-baseline.atom'; 47 | 48 | it('should parse nondefaultnamespace test baseline', function (done) { 49 | fs.createReadStream(feed).pipe(new FeedParser()) 50 | .once('readable', function () { 51 | var stream = this; 52 | var item = stream.read(); 53 | assert.ok(item.title.match(/^If you can read/)); 54 | assert.ok(item.description.match(/^
/)); 55 | done(); 56 | }) 57 | .on('error', function (err) { 58 | assert.ifError(err); 59 | done(err); 60 | }); 61 | }); 62 | 63 | }); 64 | 65 | describe('nondefaultnamespace Test case 1', function(){ 66 | 67 | var feed = __dirname + '/feeds/nondefaultnamespace.atom'; 68 | 69 | it('should parse default namespace XHTML; Atom namespace mapped to a prefix', function (done) { 70 | fs.createReadStream(feed).pipe(new FeedParser()) 71 | .once('readable', function () { 72 | var stream = this; 73 | var item = stream.read(); 74 | assert.ok(item.title.match(/^If you can read/)); 75 | assert.ok(item.description.match(/^
/)); 76 | done(); 77 | }) 78 | .on('error', function (err) { 79 | assert.ifError(err); 80 | done(err); 81 | }); 82 | }); 83 | 84 | }); 85 | 86 | describe('nondefaultnamespace Test case 2', function(){ 87 | 88 | var feed = __dirname + '/feeds/nondefaultnamespace-xhtml.atom'; 89 | 90 | it('should parse default namespace Atom; XHTML namespace mapped to a prefix', function (done) { 91 | fs.createReadStream(feed).pipe(new FeedParser()) 92 | .once('readable', function () { 93 | var stream = this; 94 | var item = stream.read(); 95 | assert.ok(item.title.match(/^If you can read/)); 96 | assert.ok(item.description.match(/^/)); 97 | done(); 98 | }) 99 | .on('error', function (err) { 100 | assert.ifError(err); 101 | done(err); 102 | }); 103 | }); 104 | 105 | }); 106 | 107 | describe('nondefaultnamespace Test case 3', function(){ 108 | 109 | var feed = __dirname + '/feeds/unknown-namespace.atom'; 110 | 111 | it('should parse default namespace Atom; XHTML namespace mapped to a prefix; FooML namespace default in the namespace DIV', function (done) { 112 | fs.createReadStream(feed).pipe(new FeedParser()) 113 | .once('readable', function () { 114 | var stream = this; 115 | var item = stream.read(); 116 | assert.equal(item.title, 'This entry contains XHTML-looking markup that is not XHTML'); 117 | assert.ok(item.description.match(/^/)); 118 | assert.ok(item.description.match(/This is an XHTML list item./)); 119 | assert.ok(item.description.match(/
  • This is not an XHTML list item./)); 120 | done(); 121 | }) 122 | .on('error', function (err) { 123 | assert.ifError(err); 124 | done(err); 125 | }); 126 | }); 127 | 128 | }); 129 | 130 | }); 131 | -------------------------------------------------------------------------------- /test/xmlbase.js: -------------------------------------------------------------------------------- 1 | describe('xmlbase', function(){ 2 | 3 | it('should resolve relative URIs in meta elements with no root xml:base', function (done) { 4 | var feed = __dirname + '/feeds/intertwingly.atom'; 5 | 6 | fs.createReadStream(feed).pipe(new FeedParser()) 7 | .on('meta', function (meta) { 8 | assert.equal('http://intertwingly.net/blog/', meta.link); 9 | assert.equal('http://intertwingly.net/favicon.ico', meta.favicon); 10 | done(); 11 | }) 12 | .on('error', function (err) { 13 | assert.ifError(err); 14 | done(err); 15 | }); 16 | }); 17 | 18 | it('should resolve relative image url in channel', function (done) { 19 | var feed = __dirname + '/feeds/relative-channel-image-url.xml'; 20 | fs.createReadStream(feed).pipe(new FeedParser()) 21 | .on('meta', function (meta) { 22 | assert.equal('https://www.virtualbox.org/graphics/vbox_logo2_gradient.png', meta.image.url); 23 | done(); 24 | }) 25 | .on('error', function (err) { 26 | assert.ifError(err); 27 | done(err); 28 | }); 29 | }); 30 | 31 | it('should parse feedurl option and handle relative URIs with no root xml:base', function (done) { 32 | var feed = __dirname + '/feeds/intertwingly.atom'; 33 | var options = { feedurl: 'http://intertwingly.net/blog/index.atom' }; 34 | 35 | fs.createReadStream(feed).pipe(new FeedParser(options)) 36 | .on('meta', function (meta) { 37 | assert.equal('http://intertwingly.net/blog/', meta.link); 38 | assert.equal('http://intertwingly.net/favicon.ico', meta.favicon); 39 | done(); 40 | }) 41 | .on('error', function (err) { 42 | assert.ifError(err); 43 | done(err); 44 | }); 45 | }); 46 | 47 | it('should resolve relative URI item links with no root xml:base', function (done) { 48 | var feed = __dirname + '/feeds/tpm.atom'; 49 | var links = []; 50 | 51 | fs.createReadStream(feed).pipe(new FeedParser()) 52 | .on('readable', function () { 53 | var item; 54 | while ((item = this.read())) { 55 | links.push(item.link); 56 | } 57 | }) 58 | .on('error', function (err) { 59 | assert.ifError(err); 60 | done(err); 61 | }) 62 | .on('end', function () { 63 | assert.equal(links[0], 'http://talkingpointsmemo.com/livewire/hannity-announces-fox-hired-sebastian-gorka-national-security-strategist'); 64 | assert.equal(links[1], 'http://talkingpointsmemo.com/edblog/were-hiring-senior-editor'); 65 | assert.equal(links.length, 20); 66 | done(); 67 | }); 68 | }); 69 | 70 | it('should resolve relative URI item links in RSS with feedurl option', function (done) { 71 | var feed = __dirname + '/feeds/rss-with-relative-urls.xml'; 72 | var links = []; 73 | 74 | fs.createReadStream(feed).pipe(new FeedParser({ feedurl: 'http://www.arcgames.com' })) 75 | .on('readable', function () { 76 | var item; 77 | while ((item = this.read())) { 78 | links.push(item.link); 79 | } 80 | }) 81 | .on('error', function (err) { 82 | assert.ifError(err); 83 | done(err); 84 | }) 85 | .on('meta', function (meta) { 86 | assert.equal('http://www.arcgames.com/en/games/neverwinter/news', meta.link); 87 | assert.equal('http://www.arcgames.com/en/games/neverwinter/news/rss', meta.xmlurl); 88 | }) 89 | .on('end', function () { 90 | assert.equal(links[0], 'http://www.arcgames.com/en/games/neverwinter/news/detail/10743874-neverwinter-%26-jingle-jam-humble-bundle%21'); 91 | assert.equal(links[1], 'http://www.arcgames.com/en/games/neverwinter/news/detail/10738994-2x-underdark-currency-%26-15%25-off-bags%21'); 92 | assert.equal(links.length, 2); 93 | done(); 94 | }); 95 | }); 96 | 97 | it('should resolve relative URI item links in RSS if absolute xmlurl is present', function (done) { 98 | var feed = __dirname + '/feeds/rss-with-relative-urls-with-absolute-xmlurl.xml'; 99 | var links = []; 100 | 101 | fs.createReadStream(feed).pipe(new FeedParser()) 102 | .on('readable', function () { 103 | var item; 104 | while ((item = this.read())) { 105 | links.push(item.link); 106 | } 107 | }) 108 | .on('error', function (err) { 109 | assert.ifError(err); 110 | done(err); 111 | }) 112 | .on('meta', function (meta) { 113 | assert.equal('http://www.arcgames.com/en/games/neverwinter/news', meta.link); 114 | assert.equal('http://www.arcgames.com/en/games/neverwinter/news/rss', meta.xmlurl); 115 | }) 116 | .on('end', function () { 117 | assert.equal(links[0], 'http://www.arcgames.com/en/games/neverwinter/news/detail/10743874-neverwinter-%26-jingle-jam-humble-bundle%21'); 118 | assert.equal(links[1], 'http://www.arcgames.com/en/games/neverwinter/news/detail/10738994-2x-underdark-currency-%26-15%25-off-bags%21'); 119 | assert.equal(links.length, 2); 120 | done(); 121 | }); 122 | }); 123 | 124 | }); 125 | --------------------------------------------------------------------------------