diff --git a/package-lock.json b/package-lock.json index 56e9b3a..3667629 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8296,6 +8296,30 @@ "glob": "^7.1.3" } }, + "rss": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/rss/-/rss-1.2.2.tgz", + "integrity": "sha1-UKFpiHYTgTOnT5oF0r3I240nqSE=", + "requires": { + "mime-types": "2.1.13", + "xml": "1.0.1" + }, + "dependencies": { + "mime-db": { + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.25.0.tgz", + "integrity": "sha1-wY29fHOl2/b0SgJNwNFloeexw5I=" + }, + "mime-types": { + "version": "2.1.13", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.13.tgz", + "integrity": "sha1-4HqqnGxrmnyjASxpADrSWjnpKog=", + "requires": { + "mime-db": "~1.25.0" + } + } + } + }, "rsvp": { "version": "4.8.5", "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", @@ -8670,7 +8694,8 @@ "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true }, "semver": { "version": "5.7.0", @@ -9669,13 +9694,10 @@ "async-limiter": "~1.0.0" } }, - "xml-js": { - "version": "1.6.11", - "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", - "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", - "requires": { - "sax": "^1.2.4" - } + "xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=" }, "xml-name-validator": { "version": "3.0.0", diff --git a/package.json b/package.json index 5136935..04dee11 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,8 @@ "ejs": "^2.6.2", "express": "^4.17.1", "ncp": "^2.0.0", - "showdown": "^1.9.0", - "xml-js": "^1.6.11" + "rss": "^1.2.2", + "showdown": "^1.9.0" }, "devDependencies": { "babel-cli": "^6.26.0", diff --git a/src/app.js b/src/app.js index b4c129b..4319149 100644 --- a/src/app.js +++ b/src/app.js @@ -2,6 +2,7 @@ const express = require('express'); const app = express(); const fs = require('fs'); const path = require('path'); +const Rss = require('rss'); /** * Terminal colors and symbols to display status messages @@ -23,6 +24,7 @@ module.exports = (config) => { app.set('views', path.join(__dirname, '..')); const articles = {}; + let lastRSS = ''; /** * Fetch articles from the data folder and send success as a response @@ -41,6 +43,9 @@ module.exports = (config) => { console.log(cons.ok, `loaded ${nb} article${nb > 1 ? 's' : ''}`); else console.log(cons.warn, `no articles loaded, check your configuration`); + + lastRSS = ''; + callback(true); }); }; @@ -87,10 +92,37 @@ module.exports = (config) => { if (err) showError(req.path, 404, res); else - render(res, homePath, {articles: Object.values(articles)}); + render(res, homePath, {articles: Object.values(articles).sort((a, b) => ('' + b.path).localeCompare(a.path))}); }); }); + //RSS endpoint + app.get(config['rss']['endpoint'], (req, res) => { + if (config['modules']['rss']) { + if (!lastRSS) { + const feed = new Rss({ + 'title': config['rss']['title'], + 'description': config['rss']['description'], + 'feed_url': 'http://' + req.headers.host + req.url, + 'site_url': 'http://' + req.headers.host + }); + Object.values(articles) + .slice(0, config['rss']['length']) + .forEach((article) => { + feed.item({ + title: article.title, + url: 'http://' + req.headers.host + article.url, + date: article.date + }); + }); + lastRSS = feed.xml(); + } + res.type('rss').send(lastRSS); + } else { + showError(req.path, 404, res); + } + }); + // catch all article urls and render them app.get('*', (req, res, next) => { if (/^\/\d{4}\/\d{2}\/\d{2}\/(\w*\/)?$/.test(req.path)) { diff --git a/src/config.default.json b/src/config.default.json index 88c5156..d5ac59a 100644 --- a/src/config.default.json +++ b/src/config.default.json @@ -2,7 +2,7 @@ "node_port": 3000, "data_dir": "data", "view_engine": "ejs", - "loopback_url": "http://localhost:3000", + "language": "en-us", "modules": { "plantuml": false, "rss": true, diff --git a/src/file_walker.js b/src/file_walker.js index d545359..b9bc8a6 100644 --- a/src/file_walker.js +++ b/src/file_walker.js @@ -87,6 +87,7 @@ module.exports = (config) => { day: parseInt(matches[3]) }; article.date = new Date(article.year, article.month, article.day); + article.date.setUTCHours(0); remaining++; readIndexFile(path.join(article.realPath, config['article']['index']), config['article']['thumbnail_tag'], (err, info) => { if (err) diff --git a/src/rss.js b/src/rss.js deleted file mode 100644 index efd36aa..0000000 --- a/src/rss.js +++ /dev/null @@ -1,40 +0,0 @@ -const convert = require('xml-js'); - -const mapArticle = (url, article) => { - return { - 'title': {'_text': article.title}, - 'link': {'_text': (url + article.url).replace(/([^:])\/\//g, '$1/')}, - 'pubDate': {'_text': article.date.toString()}, - }; -}; - -module.exports = (config) => { - return { - get: (dict) => { - const items = Object.values(dict) - .sort((a, b) => ('' + b.path).localeCompare(a.path)) - .slice(0, config['rss']['length']); - const data = { - '_declaration': { - '_attributes': { - 'version': '1.0', - 'encoding': 'UTF-8' - } - }, - 'rss': { - '_attributes': { - 'version': '2.0' - }, - 'title': {'_text': config['rss']['title']}, - 'description': {'_text': config['rss']['description']}, - 'link': {'_text': config['loopback_url']}, - 'lastBuildDate': {'_text': new Date().toString()}, - 'lastPubDate': {'_text': new Date().toString()}, - 'ttl': {'_text': '60'}, - 'item': items.map((a) => mapArticle(config['loopback_url'], a)) - } - }; - return convert.js2xml(data, {compact: true}); - } - }; -}; \ No newline at end of file diff --git a/test/app.test.js b/test/app.test.js index bbf98df..2342920 100644 --- a/test/app.test.js +++ b/test/app.test.js @@ -9,30 +9,26 @@ const testIndex = 'testindex.ejs'; const testError = 'testerror.ejs'; const testTemplate = 'testtemplate.ejs'; -const config = { - 'test': true, - 'data_dir': dataDir, - 'view_engine': 'ejs', - 'home': { - 'index': testIndex, - 'error': testError, - 'hidden': ['.ejs', '.test'] - }, - 'article': { - 'index': 'index.md', - 'template': testTemplate, - 'thumbnail_tag': 'thumbnail', - 'default_title': 'Untitled', - 'default_thumbnail': null - }, - 'showdown': {} -}; +const config = require('../src/config')(); + +config['test'] = true; +config['data_dir'] = dataDir; +config['home']['index'] = testIndex; +config['home']['error'] = testError; +config['article']['template'] = testTemplate; +config['home']['hidden'].push('.test'); +config['rss']['endpoint'] = '/rsstest'; +config['rss']['length'] = 2; const app = require('../src/app')(config); -beforeEach(() => { +beforeEach((done) => { utils.deleteFolderSync(dataDir); fs.mkdirSync(dataDir); + app.reload((res) => { + expect(res).toBe(true); + done(); + }); }); afterAll(() => { @@ -86,6 +82,65 @@ describe('Test root path', () => { }); }); +describe('Test RSS feed', () => { + test('404 rss deactivated', (done) => { + config['modules']['rss'] = false; + request(app).get('/rsstest').then((response) => { + expect(response.statusCode).toBe(404); + config['modules']['rss'] = true; + done(); + }); + }); + test('200 empty rss', (done) => { + request(app).get('/rsstest').then((response) => { + expect(response.statusCode).toBe(200); + expect(response.text.length).toBeGreaterThan(0); + expect(response.text.split('').length).toBe(1); + done(); + }); + }); + test('200 2 rss items', (done) => { + utils.createEmptyDirs([ + path.join(dataDir, '2019', '05', '05'), + path.join(dataDir, '2018', '05', '05') + ]); + utils.createEmptyFiles([ + path.join(dataDir, '2019', '05', '05', 'index.md'), + path.join(dataDir, '2018', '05', '05', 'index.md') + ]); + app.reload((res) => { + expect(res).toBe(true); + request(app).get('/rsstest').then((response) => { + expect(response.statusCode).toBe(200); + expect(response.text.length).toBeGreaterThan(0); + expect(response.text.split('').length).toBe(3); + done(); + }); + }); + }); + test('200 max rss items', (done) => { + utils.createEmptyDirs([ + path.join(dataDir, '2019', '05', '05'), + path.join(dataDir, '2018', '05', '05'), + path.join(dataDir, '2017', '05', '05') + ]); + utils.createEmptyFiles([ + path.join(dataDir, '2019', '05', '05', 'index.md'), + path.join(dataDir, '2018', '05', '05', 'index.md'), + path.join(dataDir, '2017', '05', '05', 'index.md') + ]); + app.reload((res) => { + expect(res).toBe(true); + request(app).get('/rsstest').then((response) => { + expect(response.statusCode).toBe(200); + expect(response.text.length).toBeGreaterThan(0); + expect(response.text.split('').length).toBe(3); + done(); + }); + }); + }); +}); + describe('Test articles rendering', () => { test('404 article not found', (done) => { request(app).get('/2019/05/06/untitled/').then((response) => { diff --git a/test/file_walker.test.js b/test/file_walker.test.js index 1285863..20ec67b 100644 --- a/test/file_walker.test.js +++ b/test/file_walker.test.js @@ -220,6 +220,8 @@ describe('Test article fetching', () => { const file = path.join(dir, testIndex); utils.createEmptyDirs([dir]); utils.createEmptyFiles([file]); + const date = new Date(2019, 5, 5); + date.setUTCHours(0); fw.fetchArticles((err, dict) => { expect(err).toBeNull(); expect(dict).toBeDefined(); @@ -230,7 +232,7 @@ describe('Test article fetching', () => { year: 2019, month: 5, day: 5, - date: new Date(2019, 5, 5), + date: date, title: 'Untitled', thumbnail: 'default.png', escapedTitle: 'untitled', @@ -248,6 +250,8 @@ describe('Test article fetching', () => { ![thumbnail](./thumbnail.jpg) this is some text `); + const date = new Date(2019, 5, 5); + date.setUTCHours(0); fw.fetchArticles((err, dict) => { expect(err).toBeNull(); expect(dict).toBeDefined(); @@ -258,7 +262,7 @@ describe('Test article fetching', () => { year: 2019, month: 5, day: 5, - date: new Date(2019, 5, 5), + date: date, title: 'Title with : info !', thumbnail: path.join('2019', '05', '05', './thumbnail.jpg'), escapedTitle: 'title_with___info', diff --git a/test/rss.test.js b/test/rss.test.js deleted file mode 100644 index 087a2eb..0000000 --- a/test/rss.test.js +++ /dev/null @@ -1,100 +0,0 @@ -/* jshint -W117 */ -const config = { - 'loopback_url': 'http://test.test/', - 'rss': { - 'title': 'test rss', - 'description': 'description', - 'endpoint': '/rss', - 'length': 2 - }, -}; - -const rss = require('../src/rss')(config); - -test('empty rss', () => { - const xml = rss.get({}); - expect(xml).toBe('' + - '' + - 'test rss' + - 'description' + - 'http://test.test/' + - '' + new Date().toString() + '' + - '' + new Date().toString() + '' + - '60' + - ''); -}); - -test('1 item', () => { - const data = { - 'a': { - path: 'a', - realPath: 'b', - year: 2019, - month: 5, - day: 5, - date: new Date(2019, 5, 5), - title: 'Title with : info !', - thumbnail: '/2019/05/05/thumbnail.jpg/', - escapedTitle: 'title_with___info', - url: '/2019/05/05/title_with___info/', - } - }; - const xml = rss.get(data); - expect(xml).toEqual('' + - '' + - 'test rss' + - 'description' + - 'http://test.test/' + - '' + new Date().toString() + '' + - '' + new Date().toString() + '' + - '60' + - '' + - 'Title with : info !' + - 'http://test.test/2019/05/05/title_with___info/' + - '' + new Date(2019, 5, 5).toString() + '' + - '' + - ''); -}); - -test('3 items only 2 shown sorted', () => { - const data = { - 'a': { - path: '2019/05/05', - date: new Date(2019, 5, 5), - title: 'a', - url: 'a', - }, - 'b': { - path: '2018/05/05', - date: new Date(2018, 5, 5), - title: 'b', - url: 'b', - }, - 'c': { - path: '2020/05/05', - date: new Date(2020, 5, 5), - title: 'c', - url: 'c', - } - }; - const xml = rss.get(data); - expect(xml).toEqual('' + - '' + - 'test rss' + - 'description' + - 'http://test.test/' + - '' + new Date().toString() + '' + - '' + new Date().toString() + '' + - '60' + - '' + - 'c' + - 'http://test.test/c' + - '' + new Date(2020, 5, 5).toString() + '' + - '' + - '' + - 'a' + - 'http://test.test/a' + - '' + new Date(2019, 5, 5).toString() + '' + - '' + - ''); -}); \ No newline at end of file