diff --git a/README.md b/README.md index d100ccc..ed098e4 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,22 @@ feed: autodiscovery: true ``` -- **type** - Feed type. (atom/rss2) -- **path** - Feed path. (Default: atom.xml/rss2.xml) +- **type** - Feed type. `atom` or `rss2`. Specify `['atom', 'rss2']` to output both types. (Default: `atom`) + * Example: + ``` yaml + feed: + # Generate atom feed + type: atom + + # Generate both atom and rss2 feeds + type: + - atom + - rss2 + path: + - atom.xml + - rss2.xml + ``` +- **path** - Feed path. When both types are specified, path must follow the order of type value. (Default: atom.xml/rss2.xml) - **limit** - Maximum number of posts in the feed (Use `0` or `false` to show all posts) - **hub** - URL of the PubSubHubbub hubs (Leave it empty if you don't use it) - **content** - (optional) set to 'true' to include the contents of the entire post in the feed. diff --git a/index.js b/index.js index 675cd2e..ca52d28 100644 --- a/index.js +++ b/index.js @@ -14,26 +14,52 @@ const config = hexo.config.feed = Object.assign({ autodiscovery: true }, hexo.config.feed); -const type = config.type.toLowerCase(); +let type = config.type; +let path = config.path; +const feedFn = require('./lib/generator'); -// Check feed type -if (type !== 'atom' && type !== 'rss2') { - config.type = 'atom'; -} else { - config.type = type; +if (typeof type === 'string') type = [type]; + +if (!type || !Array.isArray(type)) { + type = ['atom']; +} + +if (Array.isArray(type) && type.length > 2) { + type = type.slice(0, 2); +} + +type = type.map((str, i) => { + str = str.toLowerCase(); + if (str !== 'atom' && str !== 'rss2') { + if (i === 0) str = 'atom'; + else str = 'rss2'; + } + return str; +}); + +if (!path || typeof path === 'string' || !Array.isArray(path)) { + path = type.map(str => str.concat('.xml')); } -// Set default feed path -if (!config.path) { - config.path = config.type + '.xml'; +if (Array.isArray(path) && path.length > 2) { + path = path.slice(0, 2); } -// Add extension name if don't have -if (!extname(config.path)) { - config.path += '.xml'; +path = path.map(str => { + if (!extname(str)) return str.concat('.xml'); + return str; +}); + +config.type = type; +config.path = path; + +for (const feedType of type) { + hexo.extend.generator.register(feedType, locals => { + return feedFn.call(hexo, locals, feedType, path[type.indexOf(feedType)]); + }); } -hexo.extend.generator.register('feed', require('./lib/generator')); +if (typeof config.autodiscovery === 'undefined') config.autodiscovery = true; if (config.autodiscovery === true) { hexo.extend.filter.register('after_render:html', require('./lib/autodiscovery')); diff --git a/lib/autodiscovery.js b/lib/autodiscovery.js index 4fcb262..30c273f 100644 --- a/lib/autodiscovery.js +++ b/lib/autodiscovery.js @@ -5,12 +5,16 @@ const { url_for } = require('hexo-util'); function autodiscoveryInject(data) { const { config } = this; const { feed } = config; - const type = feed.type.replace(/2$/, ''); + const type = feed.type; + const path = feed.path; + let autodiscoveryTag = ''; - if (!feed.autodiscovery - || data.match(/type=['|"]?application\/(atom|rss)\+xml['|"]?/i)) return; + if (data.match(/type=['|"]?application\/(atom|rss)\+xml['|"]?/i)) return; - const autodiscoveryTag = ``; + type.forEach((feedType, i) => { + autodiscoveryTag += `\n`; + }); return data.replace(/(?!<\/head>).+?<\/head>/s, (str) => str.replace('', `${autodiscoveryTag}`)); } diff --git a/lib/generator.js b/lib/generator.js index 2bd0484..079faa6 100644 --- a/lib/generator.js +++ b/lib/generator.js @@ -19,10 +19,10 @@ const atomTmpl = nunjucks.compile(readFileSync(atomTmplSrc, 'utf8'), env); const rss2TmplSrc = join(__dirname, '../rss2.xml'); const rss2Tmpl = nunjucks.compile(readFileSync(rss2TmplSrc, 'utf8'), env); -module.exports = function(locals) { +module.exports = function(locals, type, path) { const config = this.config; const feedConfig = config.feed; - const template = feedConfig.type === 'rss2' ? rss2Tmpl : atomTmpl; + const template = type === 'atom' ? atomTmpl : rss2Tmpl; let posts = locals.posts.sort(feedConfig.order_by || '-date'); posts = posts.filter(post => { @@ -43,11 +43,11 @@ module.exports = function(locals) { url, icon, posts, - feed_url: config.root + feedConfig.path + feed_url: config.root + path }); return { - path: feedConfig.path, + path, data: xml }; }; diff --git a/test/index.js b/test/index.js index c1ffa8b..04b0751 100644 --- a/test/index.js +++ b/test/index.js @@ -57,7 +57,8 @@ describe('Feed generator', () => { limit: 3 }; hexo.config = Object.assign(hexo.config, urlConfig); - const result = generator(locals); + const feedCfg = hexo.config.feed; + const result = generator(locals, feedCfg.type, feedCfg.path); result.path.should.eql('atom.xml'); result.data.should.eql(atomTmpl.render({ @@ -75,7 +76,8 @@ describe('Feed generator', () => { limit: 3 }; hexo.config = Object.assign(hexo.config, urlConfig); - const result = generator(locals); + const feedCfg = hexo.config.feed; + const result = generator(locals, feedCfg.type, feedCfg.path); result.path.should.eql('rss2.xml'); result.data.should.eql(rss2Tmpl.render({ @@ -93,8 +95,8 @@ describe('Feed generator', () => { limit: 0 }; hexo.config = Object.assign(hexo.config, urlConfig); - - const result = generator(locals); + const feedCfg = hexo.config.feed; + const result = generator(locals, feedCfg.type, feedCfg.path); result.path.should.eql('atom.xml'); result.data.should.eql(atomTmpl.render({ @@ -111,7 +113,8 @@ describe('Feed generator', () => { path: 'rss2.xml', content: true }; - let result = generator(locals); + let feedCfg = hexo.config.feed; + let result = generator(locals, feedCfg.type, feedCfg.path); let $ = cheerio.load(result.data, {xmlMode: true}); let description = $('content\\:encoded').html() @@ -125,7 +128,8 @@ describe('Feed generator', () => { path: 'atom.xml', content: true }; - result = generator(locals); + feedCfg = hexo.config.feed; + result = generator(locals, feedCfg.type, feedCfg.path); $ = cheerio.load(result.data, {xmlMode: true}); description = $('content[type="html"]').html() .replace(/^ { hexo.config.url = url; hexo.config.root = root; - const result = generator(locals); + const feedCfg = hexo.config.feed; + const result = generator(locals, feedCfg.type, feedCfg.path); const $ = cheerio.load(result.data); $('feed>id').text().should.eql(valid); @@ -174,7 +179,8 @@ describe('Feed generator', () => { hexo.config.url = url; hexo.config.root = root; - const result = generator(locals); + const feedCfg = hexo.config.feed; + const result = generator(locals, feedCfg.type, feedCfg.path); const $ = cheerio.load(result.data); if (url[url.length - 1] !== '/') url += '/'; @@ -200,7 +206,8 @@ describe('Feed generator', () => { hexo.config.url = domain; hexo.config.root = root; - const result = generator(locals); + const feedCfg = hexo.config.feed; + const result = generator(locals, feedCfg.type, feedCfg.path); const $ = cheerio.load(result.data); $('feed>link').attr('href').should.eql(valid); @@ -220,7 +227,8 @@ describe('Feed generator', () => { hexo.config.url = url; hexo.config.root = root; - const result = generator(locals); + const feedCfg = hexo.config.feed; + const result = generator(locals, feedCfg.type, feedCfg.path); const $ = cheerio.load(result.data); $(selector).length.should.eq(1); @@ -246,7 +254,8 @@ describe('Feed generator', () => { icon: 'icon.svg' }; - const result = generator(locals); + const feedCfg = hexo.config.feed; + const result = generator(locals, feedCfg.type, feedCfg.path); const $ = cheerio.load(result.data); $('feed>icon').text().should.eql(full_url_for.call(hexo, hexo.config.feed.icon)); @@ -259,7 +268,8 @@ describe('Feed generator', () => { icon: undefined }; - const result = generator(locals); + const feedCfg = hexo.config.feed; + const result = generator(locals, feedCfg.type, feedCfg.path); const $ = cheerio.load(result.data); $('feed>icon').length.should.eql(0); @@ -275,7 +285,8 @@ describe('Feed generator', () => { icon: 'icon.svg' }; - const result = generator(locals); + const feedCfg = hexo.config.feed; + const result = generator(locals, feedCfg.type, feedCfg.path); const $ = cheerio.load(result.data); $('rss>channel>image>url').text().should.eql(full_url_for.call(hexo, hexo.config.feed.icon)); @@ -288,33 +299,51 @@ describe('Feed generator', () => { icon: undefined }; - const result = generator(locals); + const feedCfg = hexo.config.feed; + const result = generator(locals, feedCfg.type, feedCfg.path); const $ = cheerio.load(result.data); $('rss>channel>image').length.should.eql(0); }); + + it('path must follow order of type', () => { + hexo.config.feed = { + type: ['rss2', 'atom'], + path: ['rss-awesome.xml', 'atom-awesome.xml'] + }; + hexo.config = Object.assign(hexo.config, urlConfig); + + const feedCfg = hexo.config.feed; + const rss = generator(locals, feedCfg.type[0], feedCfg.path[0]); + rss.path.should.eql(hexo.config.feed.path[0]); + + const atom = generator(locals, feedCfg.type[1], feedCfg.path[1]); + atom.path.should.eql(hexo.config.feed.path[1]); + }); }); describe('Autodiscovery', () => { const hexo = new Hexo(); const autoDiscovery = require('../lib/autodiscovery').bind(hexo); - hexo.config.title = 'foo'; + hexo.config = { + title: 'foo', + root: '/' + }; hexo.config.feed = { - type: 'atom', - path: 'atom.xml', - autodiscovery: true + type: ['atom'], + path: ['atom.xml'] }; + hexo.config = Object.assign(hexo.config, urlConfig); + it('default', () => { const content = ''; - const result = autoDiscovery(content); + const result = autoDiscovery(content).trim(); const $ = cheerio.load(result); $('link[type="application/atom+xml"]').length.should.eql(1); - $('link[type="application/atom+xml"]').attr('href').should.eql('/' + hexo.config.feed.path); + $('link[type="application/atom+xml"]').attr('href').should.eql(urlConfig.root + hexo.config.feed.path); $('link[type="application/atom+xml"]').attr('title').should.eql(hexo.config.title); - - result.should.eql(''); }); it('prepend root', () => { @@ -325,20 +354,9 @@ describe('Autodiscovery', () => { const $ = cheerio.load(result); $('link[type="application/atom+xml"]').attr('href').should.eql(hexo.config.root + hexo.config.feed.path); - result.should.eql(''); hexo.config.root = '/'; }); - it('disable autodiscovery', () => { - hexo.config.feed.autodiscovery = false; - const content = ''; - const result = autoDiscovery(content); - - const resultType = typeof result; - resultType.should.eql('undefined'); - hexo.config.feed.autodiscovery = true; - }); - it('no duplicate tag', () => { const content = '' + ''; @@ -356,11 +374,6 @@ describe('Autodiscovery', () => { const $ = cheerio.load(result); $('link[type="application/atom+xml"]').length.should.eql(1); - - const expected = '' - + '' - + ''; - result.should.eql(expected); }); it('apply to first non-empty head tag only', () => { @@ -371,18 +384,12 @@ describe('Autodiscovery', () => { const $ = cheerio.load(result); $('link[type="application/atom+xml"]').length.should.eql(1); - - const expected = '' - + '' - + ''; - result.should.eql(expected); }); it('rss2', () => { hexo.config.feed = { - type: 'rss2', - path: 'rss2.xml', - autodiscovery: true + type: ['rss2'], + path: ['rss2.xml'] }; const content = ''; const result = autoDiscovery(content); @@ -390,12 +397,9 @@ describe('Autodiscovery', () => { const $ = cheerio.load(result); $('link[rel="alternate"]').attr('type').should.eql('application/rss+xml'); - result.should.eql(''); - hexo.config.feed = { - type: 'atom', - path: 'atom.xml', - autodiscovery: true + type: ['atom'], + path: ['atom.xml'] }; }); @@ -403,6 +407,23 @@ describe('Autodiscovery', () => { const content = '\n\n'; const result = autoDiscovery(content); - result.should.eql('\n\n'); + const $ = cheerio.load(result); + $('link[rel="alternate"]').length.should.eql(1); + }); + + it('atom + rss2', () => { + hexo.config.feed = { + type: ['atom', 'rss2'], + path: ['atom.xml', 'rss2.xml'] + }; + hexo.config = Object.assign(hexo.config, urlConfig); + + const content = ''; + const result = autoDiscovery(content); + + const $ = cheerio.load(result); + $('link[rel="alternate"]').length.should.eql(2); + $('link[rel="alternate"]').eq(0).attr('type').should.eql('application/atom+xml'); + $('link[rel="alternate"]').eq(1).attr('type').should.eql('application/rss+xml'); }); });