diff --git a/examples/simple.js b/examples/simple.js new file mode 100644 index 00000000..224f8a26 --- /dev/null +++ b/examples/simple.js @@ -0,0 +1,75 @@ +var RSS = require('../lib/index'); + +/* let's create an rss feed */ +var feed = new RSS({ + title: 'title', + description: 'description', + feed_url: 'http://example.com/rss.xml', + site_url: 'http://example.com', + image_url: 'http://example.com/icon.png', + docs: 'http://example.com/rss/docs.html', + managingEditor: 'Dylan Greene', + webMaster: 'Dylan Greene', + copyright: '2013 Dylan Greene', + language: 'en', + categories: ['Category 1','Category 2','Category 3'], + pubDate: 'May 20, 2012 04:00:00 GMT', + ttl: '60', + no_cdata_fields: ['title', 'category'], + customNamespaces: { + 'itunes': 'http://www.itunes.com/dtds/podcast-1.0.dtd' + }, + custom_elements: [ + {'itunes:subtitle': 'A show about everything'}, + {'itunes:author': 'John Doe'}, + {'itunes:summary': 'All About Everything is a show about everything. Each week we dive into any subject known to man and talk about it as much as we can. Look for our podcast in the Podcasts app or in the iTunes Store'}, + {'itunes:owner': [ + {'itunes:name': 'John Doe'}, + {'itunes:email': 'john.doe@example.com'} + ]}, + {'itunes:image': { + _attr: { + href: 'http://example.com/podcasts/everything/AllAboutEverything.jpg' + } + }}, + {'itunes:category': [ + {_attr: { + text: 'Technology' + }}, + {'itunes:category': { + _attr: { + text: 'Gadgets' + } + }} + ]} + ] +}); + +// Add an item/article too the feed +feed.item({ + title: 'Item Title & Fun', + description: 'Use this for the content. It can include html.', + url: 'http://example.com/article4?this&that', // link to the item + guid: '1123', // optional - defaults to url + categories: ['Category 1','Category 2'], // optional - array of item categories + author: 'Guest Author', // optional - defaults to feed author property + date: 'May 27, 2012', // any format that js Date can parse. + lat: 33.417974, //optional latitude field for GeoRSS + long: -111.933231, //optional longitude field for GeoRSS + enclosure: { url: 'https://www.google.com/images/srpr/logo11w.png' }, + // enclosure: {file:'path-to-file'}, // enclosure from file + custom_elements: [ + {'itunes:author': 'John Doe'}, + {'itunes:subtitle': 'A short primer on table spices'}, + {'itunes:image': { + _attr: { + href: 'http://example.com/podcasts/everything/AllAboutEverything/Episode1.jpg' + } + }}, + {'itunes:duration': '7:04'} + ] +}); + +// generate xml with default indent (4 sp) +var xml = feed.xml({indent: true}); +console.log(xml); \ No newline at end of file diff --git a/lib/index.js b/lib/index.js index e9fa9ce5..0c8fb0d3 100755 --- a/lib/index.js +++ b/lib/index.js @@ -1,9 +1,8 @@ 'use strict'; -var mime = require('mime-types'); -var xml = require('xml'); -var fs = require('fs'); - +var xml = require('xml'), + mime = require('mime'), + fs = require('fs'); function ifTruePush(bool, array, data) { if (bool) { @@ -29,11 +28,27 @@ function getSize(filename) { } function generateXML (data){ + // Field names that should not be CDATA wrapped + var no_cdata_fields = data.no_cdata_fields || []; + + // Handle formatting of CDATA-able output + function output(field_name, value) { + if (!value) { + return; + } + var ret_value = {}; + if (no_cdata_fields.indexOf(field_name) !== -1) { + ret_value[field_name] = value; + } else { + ret_value[field_name] = { _cdata: value }; // CDATA + } + return ret_value; + } var channel = []; - channel.push({ title: { _cdata: data.title } }); - channel.push({ description: { _cdata: data.description || data.title } }); - channel.push({ link: data.site_url || 'http://github.com/dylang/node-rss' }); + channel.push( output('title', data.title) ); + channel.push( output('description', (data.description || data.title)) ); + channel.push({ link: data.site_url || 'http://github.com/dylang/node-rss' }); // image_url set? if (data.image_url) { channel.push({ image: [ {url: data.image_url}, {title: data.title}, {link: data.site_url} ] }); @@ -42,37 +57,35 @@ function generateXML (data){ channel.push({ lastBuildDate: new Date().toUTCString() }); ifTruePush(data.feed_url, channel, { 'atom:link': { _attr: { href: data.feed_url, rel: 'self', type: 'application/rss+xml' } } }); - ifTruePush(data.author, channel, { 'author': { _cdata: data.author } }); + ifTruePush(data.author, channel, output('author', data.author)); ifTruePush(data.pubDate, channel, { 'pubDate': new Date(data.pubDate).toGMTString() }); - ifTruePush(data.copyright, channel, { 'copyright': { _cdata: data.copyright } }); - ifTruePush(data.language, channel, { 'language': { _cdata: data.language } }); - ifTruePush(data.managingEditor, channel, { 'managingEditor': { _cdata: data.managingEditor } }); - ifTruePush(data.webMaster, channel, { 'webMaster': { _cdata: data.webMaster } }); + ifTruePush(data.copyright, channel, output('copyright', data.copyright) ); + ifTruePush(data.language, channel, output('language', data.language) ); + ifTruePush(data.managingEditor, channel, output('managingEditor', data.managingEditor) ); + ifTruePush(data.webMaster, channel, output('webMaster', data.webMaster) ); ifTruePush(data.docs, channel, { 'docs': data.docs }); ifTruePush(data.ttl, channel, { 'ttl': data.ttl }); ifTruePush(data.hub, channel, { 'atom:link': { _attr: { href: data.hub, rel: 'hub' } } }); if (data.categories) { data.categories.forEach(function(category) { - ifTruePush(category, channel, { category: { _cdata: category } }); + ifTruePush(category, channel, output('category', category)); }); } ifTruePushArray(data.custom_elements, channel, data.custom_elements); data.items.forEach(function(item) { - var item_values = [ - { title: { _cdata: item.title } } - ]; - ifTruePush(item.description, item_values, { description: { _cdata: item.description } }); + var item_values = [ output('title', item.title) ]; + ifTruePush(item.description, item_values, output('description', item.description)); ifTruePush(item.url, item_values, { link: item.url }); ifTruePush(item.link || item.guid || item.title, item_values, { guid: [ { _attr: { isPermaLink: !item.guid && !!item.url } }, item.guid || item.url || item.title ] }); item.categories.forEach(function(category) { - ifTruePush(category, item_values, { category: { _cdata: category } }); + ifTruePush(category, item_values, output('category', category)); }); - ifTruePush(item.author || data.author, item_values, { 'dc:creator': { _cdata: item.author || data.author } }); + ifTruePush(item.author || data.author, item_values, output('dc:creator', (item.author || data.author)) ); ifTruePush(item.date, item_values, { pubDate: new Date(item.date).toGMTString() }); //Set GeoRSS to true if lat and long are set @@ -158,7 +171,8 @@ function RSS (options, items) { this.geoRSS = options.geoRSS || false; this.custom_namespaces = options.custom_namespaces || {}; this.custom_elements = options.custom_elements || []; - this.items = items || []; + this.no_cdata_fields = options.no_cdata_fields || []; + this.items = []; // passed in "items" handled below this.item = function (options) { options = options || {}; @@ -180,10 +194,38 @@ function RSS (options, items) { return this; }; + // replace items + this.replace_items = function(items) { + items = items || []; + if (items && items.length > 0) { + this.items = []; + return this.concat_items(items); + } else { + return this; + } + }; + + // Concat new items to this.items + this.concat_items = function (items) { + var self = this; + items = items || []; + if (items && items.length > 0) { + items.forEach(function(item){ + self.item(item); + }); + } + return this; + }; + this.xml = function(indent) { return '\n' + xml(generateXML(this), indent); }; + + // handle passed in "items" on obj creation using item() + if (items) { + return this.replace_items(items); + } } module.exports = RSS; diff --git a/package.json b/package.json index eaa2646e..f04c8fdc 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,8 @@ "Patrick Garman ", "Fred Morstatter", "Eric Vantillard ", - "Jason Karns " + "Jason Karns ", + "Kip Gebhardt " ], "repository": { "type": "git", @@ -57,10 +58,10 @@ "dependencies": { "folderify": "^0.6.0", "mime-types": "^2.0.3", + "mime": "^1.2.11", "xml": "^1.0.0" }, "devDependencies": { - "folderify": "^0.6.0", "grunt": "^0.4.5", "grunt-contrib-jshint": "^0.10.0", "grunt-release": "^0.9.0", diff --git a/readme.md b/readme.md index bafa3003..aa66f891 100644 --- a/readme.md +++ b/readme.md @@ -38,6 +38,7 @@ var feed = new RSS(feedOptions); * `hub` _optional_ **PubSubHubbub hub url** Where is the PubSubHub hub located. * `custom_namespaces` _optional_ **object** Put additional namespaces in element (without 'xmlns:' prefix) * `custom_elements` _optional_ **array** Put additional elements in the feed (node-xml syntax) + * `no_cdata_fields` _optional_ **array** Field names that shouldn't be wrapped with CDATA tag. The data will be escaped for XML. Default is to wrap with CDATA. You should only use this to work around problematic XML clients. #### Add items to a feed @@ -67,8 +68,20 @@ feed.item(itemOptions); * `long` _optional_ **number** The longitude coordinate of the item. * `custom_elements` _optional_ **array** Put additional elements in the item (node-xml syntax) -##### Feed XML +###### Add single item +```js +feed.item(itemOptions); +``` +###### Concatenate an array of items +```js +feed.concat_items(arrayOfItemOptions); +``` +###### Replace items with a new array of items +```js +feed.replace_items(arrayOfItemOptions); +``` +##### Feed XML ```js var xml = feed.xml({indent: true}); ``` @@ -82,7 +95,7 @@ For example you can use `'\t'` for tab character, or `' '` for two-space tabs. ### Example Usage - +(examples/simple.js) ```js var RSS = require('rss'); diff --git a/templates/readme/examples.md b/templates/readme/examples.md index 90181e3b..07fdabb9 100644 --- a/templates/readme/examples.md +++ b/templates/readme/examples.md @@ -1,5 +1,5 @@ ## Example Usage - +(examples/simple.js) ```js var RSS = require('rss'); diff --git a/templates/readme/usage.md b/templates/readme/usage.md index fe0528eb..465f126d 100644 --- a/templates/readme/usage.md +++ b/templates/readme/usage.md @@ -27,6 +27,7 @@ var feed = new RSS(feedOptions); * `hub` _optional_ **PubSubHubbub hub url** Where is the PubSubHub hub located. * `custom_namespaces` _optional_ **object** Put additional namespaces in element (without 'xmlns:' prefix) * `custom_elements` _optional_ **array** Put additional elements in the feed (node-xml syntax) + * `no_cdata_fields` _optional_ **array** Field names that shouldn't be wrapped with CDATA tag. The data will be escaped for XML. Default is to wrap with CDATA. You should only use this to work around problematic XML clients. ### Add items to a feed @@ -56,8 +57,20 @@ feed.item(itemOptions); * `long` _optional_ **number** The longitude coordinate of the item. * `custom_elements` _optional_ **array** Put additional elements in the item (node-xml syntax) -#### Feed XML +##### Add single item +```js +feed.item(itemOptions); +``` +##### Concatenate an array of items +```js +feed.concat_items(arrayOfItemOptions); +``` +##### Replace items with a new array of items +```js +feed.replace_items(arrayOfItemOptions); +``` +#### Feed XML ```js var xml = feed.xml({indent: true}); ``` diff --git a/test/expectedOutput/concatenateItems.xml b/test/expectedOutput/concatenateItems.xml new file mode 100644 index 00000000..aa5df699 --- /dev/null +++ b/test/expectedOutput/concatenateItems.xml @@ -0,0 +1,71 @@ + + + + <![CDATA[title]]> + + http://example.com + + http://example.com/icon.png + title + http://example.com + + Example Generator + Wed, 10 Dec 2014 19:04:57 GMT + + + Sun, 20 May 2012 04:00:00 GMT + + + + + http://example.com/rss/docs.html + 60 + + + + + <![CDATA[item 1]]> + + http://example.com/article1 + http://example.com/article1 + + Thu, 24 May 2012 04:00:00 GMT + + + <![CDATA[item 2]]> + + http://example.com/article2 + http://example.com/article2 + + Fri, 25 May 2012 04:00:00 GMT + + + <![CDATA[item 3]]> + + http://example.com/article3 + item3 + + Sat, 26 May 2012 04:00:00 GMT + + + <![CDATA[item 4 & html test with <strong>]]> + html]]> + http://example.com/article4?this&that + http://example.com/article4?this&that + + Sun, 27 May 2012 04:00:00 GMT + + + <![CDATA[item 5 & test for categories]]> + + http://example.com/article5 + http://example.com/article5 + + + + + + Mon, 28 May 2012 04:00:00 GMT + + + \ No newline at end of file diff --git a/test/expectedOutput/doNotWrapSomeElementsWithCdata.xml b/test/expectedOutput/doNotWrapSomeElementsWithCdata.xml new file mode 100644 index 00000000..f34c97b8 --- /dev/null +++ b/test/expectedOutput/doNotWrapSomeElementsWithCdata.xml @@ -0,0 +1,12 @@ + + + + title + + http://example.com + RSS for Node + Wed, 10 Dec 2014 19:04:57 GMT + + Dylan Greene + + \ No newline at end of file diff --git a/test/expectedOutput/escapeCdataFields.xml b/test/expectedOutput/escapeCdataFields.xml new file mode 100644 index 00000000..ffa1a782 --- /dev/null +++ b/test/expectedOutput/escapeCdataFields.xml @@ -0,0 +1,35 @@ + + + + <b>title</b> + + http://example.com + + http://example.com/icon.png + <b>title</b> + http://example.com + + Example Generator + Wed, 10 Dec 2014 19:04:57 GMT + + + Sun, 20 May 2012 04:00:00 GMT + + + + + http://example.com/rss/docs.html + 60 + + + + + This & That + + http://example.com/article1 + http://example.com/article1 + + Thu, 24 May 2012 04:00:00 GMT + + + \ No newline at end of file diff --git a/test/expectedOutput/processItemArrayOnObjCreation.xml b/test/expectedOutput/processItemArrayOnObjCreation.xml new file mode 100644 index 00000000..5cd806e6 --- /dev/null +++ b/test/expectedOutput/processItemArrayOnObjCreation.xml @@ -0,0 +1,63 @@ + + + + <b>title</b> + + http://example.com + + http://example.com/icon.png + <b>title</b> + http://example.com + + Example Generator + Wed, 10 Dec 2014 19:04:57 GMT + + + Sun, 20 May 2012 04:00:00 GMT + + + + + http://example.com/rss/docs.html + 60 + + + + + item 2 + + http://example.com/article2 + http://example.com/article2 + + Fri, 25 May 2012 04:00:00 GMT + + + item 3 + + http://example.com/article3 + item3 + + Sat, 26 May 2012 04:00:00 GMT + + + item 4 & html test with <strong> + html]]> + http://example.com/article4?this&that + http://example.com/article4?this&that + + Sun, 27 May 2012 04:00:00 GMT + + + item 5 & test for categories + + http://example.com/article5 + http://example.com/article5 + + + + + + Mon, 28 May 2012 04:00:00 GMT + + + \ No newline at end of file diff --git a/test/expectedOutput/replacedItems.xml b/test/expectedOutput/replacedItems.xml new file mode 100644 index 00000000..aa5df699 --- /dev/null +++ b/test/expectedOutput/replacedItems.xml @@ -0,0 +1,71 @@ + + + + <![CDATA[title]]> + + http://example.com + + http://example.com/icon.png + title + http://example.com + + Example Generator + Wed, 10 Dec 2014 19:04:57 GMT + + + Sun, 20 May 2012 04:00:00 GMT + + + + + http://example.com/rss/docs.html + 60 + + + + + <![CDATA[item 1]]> + + http://example.com/article1 + http://example.com/article1 + + Thu, 24 May 2012 04:00:00 GMT + + + <![CDATA[item 2]]> + + http://example.com/article2 + http://example.com/article2 + + Fri, 25 May 2012 04:00:00 GMT + + + <![CDATA[item 3]]> + + http://example.com/article3 + item3 + + Sat, 26 May 2012 04:00:00 GMT + + + <![CDATA[item 4 & html test with <strong>]]> + html]]> + http://example.com/article4?this&that + http://example.com/article4?this&that + + Sun, 27 May 2012 04:00:00 GMT + + + <![CDATA[item 5 & test for categories]]> + + http://example.com/article5 + http://example.com/article5 + + + + + + Mon, 28 May 2012 04:00:00 GMT + + + \ No newline at end of file diff --git a/test/expectedOutput/wrapElementsWithCdata.xml b/test/expectedOutput/wrapElementsWithCdata.xml new file mode 100644 index 00000000..687166cf --- /dev/null +++ b/test/expectedOutput/wrapElementsWithCdata.xml @@ -0,0 +1,11 @@ + + + + <![CDATA[title]]> + + http://example.com + RSS for Node + Wed, 10 Dec 2014 19:04:57 GMT + + + \ No newline at end of file diff --git a/test/index.js b/test/index.js index 36dec7a3..2de08713 100644 --- a/test/index.js +++ b/test/index.js @@ -300,3 +300,254 @@ test('custom namespaces', function(t) { t.equal(feed.xml({indent: true}), expectedOutput.customNamespaces); }); + +test('wrap elements with CDATA', function(t) { + t.plan(1); + + var feed = new RSS({ + title: 'title', + description: 'description', + feed_url: 'http://example.com/rss.xml', + site_url: 'http://example.com' + }); + + t.equal(feed.xml({indent: true}), expectedOutput.wrapElementsWithCdata); +}); + +test('do not wrap some elements with CDATA', function(t) { + t.plan(1); + + var feed = new RSS({ + title: 'title', + description: 'description', + author: 'Dylan Greene', + feed_url: 'http://example.com/rss.xml', + site_url: 'http://example.com', + no_cdata_fields: ['title', 'author'] + }); + + t.equal(feed.xml({indent: true}), expectedOutput.doNotWrapSomeElementsWithCdata); +}); + +test('concatenate array of items to existing items', function(t) { + t.plan(1); + var feed = new RSS({ + title: 'title', + description: 'description', + generator: 'Example Generator', + feed_url: 'http://example.com/rss.xml', + site_url: 'http://example.com', + image_url: 'http://example.com/icon.png', + author: 'Dylan Greene', + categories: ['Category 1','Category 2','Category 3'], + pubDate: 'May 20, 2012 04:00:00 GMT', + docs: 'http://example.com/rss/docs.html', + copyright: '2013 Dylan Green', + language: 'en', + managingEditor: 'Dylan Green', + webMaster: 'Dylan Green', + ttl: '60' + }); + + feed.item({ + title: 'item 1', + description: 'description 1', + url: 'http://example.com/article1', + date: 'May 24, 2012 04:00:00 GMT' + }); + + var additional_items = [ + { + title: 'item 2', + description: 'description 2', + url: 'http://example.com/article2', + date: 'May 25, 2012 04:00:00 GMT' + }, + { + title: 'item 3', + description: 'description 3', + url: 'http://example.com/article3', + guid: 'item3', + date: 'May 26, 2012 04:00:00 GMT' + }, + { + title: 'item 4 & html test with ', + description: 'description 4 uses some html', + url: 'http://example.com/article4?this&that', + author: 'Guest Author', + date: 'May 27, 2012 04:00:00 GMT' + }, + { + title: 'item 5 & test for categories', + description: 'description 5', + url: 'http://example.com/article5', + categories: ['Category 1','Category 2','Category 3','Category 4'], + author: 'Guest Author', + date: 'May 28, 2012 04:00:00 GMT' + } + ]; + + feed.concat_items(additional_items); + + t.equal(feed.xml({indent: true}), expectedOutput.concatenateItems); +}); + +test('replace items with array of new items', function(t) { + t.plan(1); + var feed = new RSS({ + title: 'title', + description: 'description', + generator: 'Example Generator', + feed_url: 'http://example.com/rss.xml', + site_url: 'http://example.com', + image_url: 'http://example.com/icon.png', + author: 'Dylan Greene', + categories: ['Category 1','Category 2','Category 3'], + pubDate: 'May 20, 2012 04:00:00 GMT', + docs: 'http://example.com/rss/docs.html', + copyright: '2013 Dylan Green', + language: 'en', + managingEditor: 'Dylan Green', + webMaster: 'Dylan Green', + ttl: '60' + }); + + feed.item({ + title: 'BOGUS ITEM - REPLACE ME', + description: 'BOGUS ITEM - REPLACE ME', + url: 'http://example.com/article1', + date: 'May 24, 2012 04:00:00 GMT' + }); + + var new_items = [ + { + title: 'item 1', + description: 'description 1', + url: 'http://example.com/article1', + date: 'May 24, 2012 04:00:00 GMT' + }, + { + title: 'item 2', + description: 'description 2', + url: 'http://example.com/article2', + date: 'May 25, 2012 04:00:00 GMT' + }, + { + title: 'item 3', + description: 'description 3', + url: 'http://example.com/article3', + guid: 'item3', + date: 'May 26, 2012 04:00:00 GMT' + }, + { + title: 'item 4 & html test with ', + description: 'description 4 uses some html', + url: 'http://example.com/article4?this&that', + author: 'Guest Author', + date: 'May 27, 2012 04:00:00 GMT' + }, + { + title: 'item 5 & test for categories', + description: 'description 5', + url: 'http://example.com/article5', + categories: ['Category 1','Category 2','Category 3','Category 4'], + author: 'Guest Author', + date: 'May 28, 2012 04:00:00 GMT' + } + ]; + + feed.replace_items(new_items); + t.equal(feed.xml({indent: true}), expectedOutput.replacedItems); +}); + +test('xml escape fields specified in no_cdata_fields', function(t) { + t.plan(1); + var feed = new RSS({ + title: 'title', // This should be escaped + description: 'description', + generator: 'Example Generator', + feed_url: 'http://example.com/rss.xml', + site_url: 'http://example.com', + image_url: 'http://example.com/icon.png', + author: 'Dylan Greene', + categories: ['Category 1','Category 2','Category 3'], + pubDate: 'May 20, 2012 04:00:00 GMT', + docs: 'http://example.com/rss/docs.html', + copyright: '2013 Dylan Green', + language: 'en', + managingEditor: 'Dylan Green', + webMaster: 'Dylan Green', + ttl: '60', + no_cdata_fields: ['title'] + }); + + feed.item({ + title: 'This & That', // This should be escaped + description: 'TEST & SUCCEED', + url: 'http://example.com/article1', + date: 'May 24, 2012 04:00:00 GMT' + }); + + t.equal(feed.xml({indent: true}), expectedOutput.escapeCdataFields); +}); + +test('process item array passed to RSS object creation', function(t) { + t.plan(1); + + var item_array = [ + { + title: 'item 2', + description: 'description 2', + url: 'http://example.com/article2', + date: 'May 25, 2012 04:00:00 GMT' + }, + { + title: 'item 3', + description: 'description 3', + url: 'http://example.com/article3', + guid: 'item3', + date: 'May 26, 2012 04:00:00 GMT' + }, + { + title: 'item 4 & html test with ', + description: 'description 4 uses some html', + url: 'http://example.com/article4?this&that', + author: 'Guest Author', + date: 'May 27, 2012 04:00:00 GMT' + }, + { + title: 'item 5 & test for categories', + description: 'description 5', + url: 'http://example.com/article5', + categories: ['Category 1','Category 2','Category 3','Category 4'], + author: 'Guest Author', + date: 'May 28, 2012 04:00:00 GMT' + } + ]; + + var feed = new RSS({ + title: 'title', // This should be escaped + description: 'description', + generator: 'Example Generator', + feed_url: 'http://example.com/rss.xml', + site_url: 'http://example.com', + image_url: 'http://example.com/icon.png', + author: 'Dylan Greene', + categories: ['Category 1','Category 2','Category 3'], + pubDate: 'May 20, 2012 04:00:00 GMT', + docs: 'http://example.com/rss/docs.html', + copyright: '2013 Dylan Green', + language: 'en', + managingEditor: 'Dylan Green', + webMaster: 'Dylan Green', + ttl: '60', + no_cdata_fields: ['title'] + }, + item_array); + + // console.log(feed.xml({indent: true})); + + t.equal(feed.xml({indent: true}), expectedOutput.processItemArrayOnObjCreation); +}); + +