From 63f611e1d222c860d8792ea90bf5bc7955528ce5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frank=20Boe=CC=88s?= Date: Wed, 14 Sep 2016 17:03:29 +0200 Subject: [PATCH] RSS feeds and paged indexes for all tags and authors --- README.md | 2 +- generate.js | 4 +- install.js | 18 +- package.json | 2 +- src/blogophon-console.js | 3 +- src/generator.js | 761 ++++++++++---------- src/helpers/url.js | 11 +- src/index.js | 404 ++++++----- src/models/rss-js.js | 4 +- src/templates/atom.xml | 2 +- src/templates/rss.xml | 2 +- test/index.js | 11 + test/url.js | 28 +- themes/default/templates/index.html | 9 +- themes/default/templates/partials/meta.html | 3 - themes/default/templates/post.html | 3 + 16 files changed, 667 insertions(+), 600 deletions(-) create mode 100644 test/index.js diff --git a/README.md b/README.md index 0172f8bb..27c1e53c 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ If you do not want to use [`node ./index.js`](index.js), [read the manual](docs/ Version ------- -Version: 0.3.1 (2016-09-14) +Version: 0.4.0 (2016-09-15) Legal stuff ----------- diff --git a/generate.js b/generate.js index caa1acf0..10a2d9d1 100755 --- a/generate.js +++ b/generate.js @@ -1,13 +1,15 @@ #!/usr/bin/env node 'use strict'; -var generator = require('./src/generator'); +var Generator = require('./src/generator'); var args = require('./src/helpers/arguments')(); +var config = require('./src/config'); if (args.log) { console.log('---- ' + new Date() + ' -----'); } +var generator = new Generator(config); generator .getArticles() .then(function () { diff --git a/install.js b/install.js index f4dbbfe0..4dfd6980 100755 --- a/install.js +++ b/install.js @@ -10,6 +10,7 @@ var configFilename = defaultValues.directories.user + '/config.json'; var themesAvailable= fs.readdirSync(defaultValues.directories.theme); var args = require('./src/helpers/arguments')(); var Mustache = require('mustache'); +var manifest = require('./src/models/manifest'); Mustache.escape = function(string) { var entityMap = { @@ -253,9 +254,9 @@ inquirer.prompt(questions).then( } ), function(err) { if (err) { - console.error('htdocs/robots.txt' + ' could not be written' ); process.exit(1); + console.error(defaultValues.directories.htdocs+'/robots.txt' + ' could not be written' ); process.exit(1); } else { - console.log( 'htdocs/robots.txt' + ' created'); + console.log( defaultValues.directories.htdocs+'/robots.txt' + ' created'); } }); fs.writeFile(defaultValues.directories.htdocs+'/browserconfig.xml', Mustache.render( @@ -264,11 +265,20 @@ inquirer.prompt(questions).then( } ), function(err) { if (err) { - console.error('htdocs/browserconfig.xml' + ' could not be written' ); process.exit(1); + console.error(defaultValues.directories.htdocs+'/browserconfig.xml' + ' could not be written' ); process.exit(1); } else { - console.log( 'htdocs/browserconfig.xml' + ' created'); + console.log( defaultValues.directories.htdocs+'/browserconfig.xml' + ' created'); } }); + + fs.writeFile( defaultValues.directories.htdocs+'/manifest.json', JSON.stringify(manifest(defaultValues), undefined, 2), function(err) { + if (err) { + console.error(defaultValues.directories.htdocs+'/manifest.json' + ' could not be written' ); process.exit(1); + } else { + console.log( defaultValues.directories.htdocs+'/manifest.json' + ' created'); + } + }); + }, function(err) { console.error(err); process.exit(1); } ); diff --git a/package.json b/package.json index 00a923e0..7d9e793a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "blogophon", - "version": "0.3.1", + "version": "0.4.0", "description": "A small and simple Static Site Generator for blogs.", "main": "./index.js", "repository": { diff --git a/src/blogophon-console.js b/src/blogophon-console.js index 7f93cbb2..ea18fc3b 100644 --- a/src/blogophon-console.js +++ b/src/blogophon-console.js @@ -8,7 +8,7 @@ var fs = require('fs-extra-promise'); var shell = require('shelljs'); var Mustache = require('./helpers/blogophon-mustache').getTemplates(config.directories.currentTheme + '/templates'); var chalk = require('chalk'); -var generator = require('./generator'); +var Generator = require('./generator'); /** * Represents the Inquirer dialogue with which to edit articles. @@ -320,6 +320,7 @@ var BlogophonConsole = function() { } } ]; + var generator = new Generator(config); inquirer .prompt(questions) .then( diff --git a/src/generator.js b/src/generator.js index b91e0785..3c1e6850 100644 --- a/src/generator.js +++ b/src/generator.js @@ -1,434 +1,453 @@ 'use strict'; -var config = require('./config'); var Promise = require('promise/lib/es6-extensions'); var fs = require('fs-extra-promise'); var glob = require("glob"); var gm = require('gm').subClass({imageMagick: true}); var dateFormat = require('dateformat'); -var Mustache = require('./helpers/blogophon-mustache').getTemplates(config.directories.currentTheme + '/templates'); +var Mustache = require('./helpers/blogophon-mustache'); var PostReader = require('./post-reader'); var rssJs = require('./models/rss-js'); -var manifest = require('./models/manifest'); var Translations = require('./helpers/translations'); var toolshed = require('./helpers/js-toolshed'); var IndexUrl = require('./helpers/index-url'); -var index = require('./index'); +var Index = require('./index'); var hashes = require('./models/hashes'); /** * Generator used for creating the blog. * @constructor */ -var Generator = { - strings: new Translations(config.language).getAll(), - currentIndex: null, - hashes: hashes(), - - /** - * Get all articles from file system and populate `index` into {Post}. Uses {PostReader}. - * @return {Promise} with first parameter of `resolve` being the number of files converted. - */ - getArticles: function() { - Generator.currentIndex = index(); - return new Promise ( - function(resolve, reject) { - glob(config.directories.data + "/**/*.md", function(err, files) { - if (err) { - reject(err); - } - // Making promises - var promises = files.map(function(i) { - return PostReader(i); - }); - // Checking promises - Promise - .all(promises) - .then(function(posts) { - Generator.currentIndex.pushArray(posts); - console.log('Removed ' + Generator.currentIndex.removeFutureItems() + ' item(s) with future timestamp from index'); - Generator.currentIndex.makeNextPrev(); - resolve( files.length ); - }) - .catch(reject) - ; - }); - } - ); - }, - - /** - * Get all {Post} from `index` and generate HTML pages. - * @return {Promise} with first parameter of `resolve` being the list of files generated. - */ - buildAllArticles: function(force) { - var allPosts = Generator.currentIndex.getPosts(); - var skipped = 0; - var generatedArticles = []; - - if (force) { - fs.removeSync(config.directories.htdocs + '/posts/*'); - } +var Generator = function (config) { + this.config = config; + this.strings = new Translations(config.language).getAll(); + this.currentIndex = null; + this.hashes = hashes(); + Mustache = Mustache.getTemplates(config.directories.currentTheme + '/templates'); + return this; +}; - return new Promise ( - function(resolve, reject) { - // Making promises - var promises = allPosts.map(function(post) { - if (!force && Generator.hashes.isHashed(post.meta.Url, post.hash)) { - skipped++; - } else { - generatedArticles.push(post.meta.Url); - return Generator.buildSingleArticle(post); - } +/** + * Get all articles from file system and populate `index` into {Post}. Uses {PostReader}. + * @return {Promise} with first parameter of `resolve` being the number of files converted. + */ +Generator.prototype.getArticles = function() { + var that = this; + this.currentIndex = new Index(); + return new Promise ( + function(resolve, reject) { + glob(that.config.directories.data + "/**/*.md", function(err, files) { + if (err) { + reject(err); + } + // Making promises + var promises = files.map(function(i) { + return PostReader(i); }); // Checking promises Promise .all(promises) - .then(function() { - Generator.hashes.save(); - console.log("Created " + generatedArticles.length + " articles, skipped " + skipped + " articles"); - resolve(generatedArticles); + .then(function(posts) { + that.currentIndex.pushArray(posts); + console.log('Removed ' + that.currentIndex.removeFutureItems() + ' item(s) with future timestamp from index'); + that.currentIndex.makeNextPrev(); + resolve( files.length ); }) .catch(reject) ; - } - ); - }, - - /** - * Build a single article - * @param {Post} post [description] - * @return {Promise} with first parameter being the filename - */ - buildSingleArticle: function(post) { - if (!post) { - throw new Error('Empty post'); + }); } - return new Promise ( - function(resolve, reject) { - fs.ensureDir(config.directories.htdocs + post.meta.Url, function() { - fs.writeFile(post.meta.Filename, Mustache.render(Mustache.templates.post, { - post: post, - config: config - },Mustache.partials), function(err) { - if (err) { - reject(err); - } - if (Generator.hashes) { - Generator.hashes.update(post.meta.Url, post.hash); - } - resolve(post.meta.Filename); - }); - }); - } - ); - }, - - /** - * Build special pages from `index` like index pages, tag pages, etc. - * @return {Promise} with first parameter of `resolve` being an array with the numbers of files converted. - */ - buildSpecialPages: function() { - return new Promise ( - function(resolve, reject) { - Promise - .all([ - Generator.buildIndexFiles(), - Generator.buildTagPages(), - Generator.buildAuthorPages(), - Generator.buildMetaFiles() - ]) - .then(resolve) - .catch(reject) - ; - } - ); - }, - - /** - * [buildIndexFiles description] - * @return {Promise} with first parameter of `resolve` being the number of files converted. - */ - buildIndexFiles: function() { - return new Promise ( - function(resolve, reject) { - fs.remove(config.directories.htdocs + '/index*', function(err) { - var promises = []; - var page; - var pagedPosts = Generator.currentIndex.getPagedPosts(config.itemsPerPage); - - for (page = 0; page < pagedPosts.length; page ++) { - var curPageObj = Generator.currentIndex.getPageData(page, pagedPosts.length); - curPageObj.index = pagedPosts[page]; - curPageObj.config = config; - curPageObj.meta = { - title : (curPageObj.currentPage === 1) ? Generator.strings.index : Generator.strings.page.sprintf(curPageObj.currentPage, curPageObj.maxPages), - absoluteUrl: new IndexUrl(curPageObj.currentUrl).absoluteUrl() - }; - curPageObj.prevUrl = new IndexUrl(curPageObj.prevUrl).relativeUrl(); - curPageObj.nextUrl = new IndexUrl(curPageObj.nextUrl).relativeUrl(); - promises.push(fs.writeFile(new IndexUrl(curPageObj.currentUrl).filename(), Mustache.render(Mustache.templates.index, curPageObj, Mustache.partials))); - } - Promise - .all(promises) - .then(function() { - console.log("Wrote "+promises.length+" index files"); - return resolve(promises.length); - }) - .catch(reject) - ; - }); - } - ); - }, - - /** - * [buildTagPages description] - * @return {Promise} with first parameter of `resolve` being the number of files converted. - */ - buildTagPages: function() { - return new Promise ( - function(resolve, reject) { - var tags = Generator.currentIndex.getTags(); - var tagPages = Object.keys(tags).sort().map(function(key) { - return { - title: tags[key].title, - url : tags[key].urlObj.relativeUrl() - }; - }); + ); +}; - fs.remove(config.directories.htdocs + '/tagged', function(err) { - fs.ensureDirSync(config.directories.htdocs + '/tagged'); - - var promises = Object.keys(tags).map(function(key) { - fs.ensureDirSync(tags[key].urlObj.dirname()); - tags[key].config = config; - tags[key].meta = { - title : Generator.strings.tag.sprintf(tags[key].title), - absoluteUrl: tags[key].urlObj.absoluteUrl() - }; - return fs.writeFile(tags[key].urlObj.filename(), Mustache.render(Mustache.templates.index, tags[key], Mustache.partials)); - }); +/** + * Get all {Post} from `index` and generate HTML pages. + * @return {Promise} with first parameter of `resolve` being the list of files generated. + */ +Generator.prototype.buildAllArticles = function(force) { + var that = this; + var allPosts = this.currentIndex.getPosts(); + var skipped = 0; + var generatedArticles = []; + + if (force) { + fs.removeSync(this.config.directories.htdocs + '/posts/*'); + } - promises.push(fs.writeFile( new IndexUrl('tagged/index.html').filename(), Mustache.render(Mustache.templates.tags, { - index: tagPages, - config: config - }, Mustache.partials))); + return new Promise ( + function(resolve, reject) { + // Making promises + var promises = allPosts.map(function(post) { + if (!force && that.hashes.isHashed(post.meta.Url, post.hash)) { + skipped++; + } else { + generatedArticles.push(post.meta.Url); + return that.buildSingleArticle(post); + } + }); + // Checking promises + Promise + .all(promises) + .then(function() { + that.hashes.save(); + console.log("Created " + generatedArticles.length + " articles, skipped " + skipped + " articles"); + resolve(generatedArticles); + }) + .catch(reject) + ; + } + ); +}; - Promise - .all(promises) - .then(function() { - console.log("Wrote "+promises.length+" tag pages"); - return resolve(promises.length); - }) - .catch(reject) - ; - }); - } - ); - }, - - /** - * [buildAuthorPages description] - * @return {Promise} with first parameter of `resolve` being the number of files converted. - */ - buildAuthorPages: function() { - return new Promise ( - function(resolve, reject) { - var authors = Generator.currentIndex.getAuthors(); - var authorPages = Object.keys(authors).sort().map(function(name) { - return { - title: name, - url : authors[name].urlObj.relativeUrl() - }; +/** + * Build a single article + * @param {Post} post [description] + * @return {Promise} with first parameter being the filename + */ +Generator.prototype.buildSingleArticle = function(post) { + var that = this; + if (!post) { + throw new Error('Empty post'); + } + return new Promise ( + function(resolve, reject) { + fs.ensureDir(that.config.directories.htdocs + post.meta.Url, function() { + fs.writeFile(post.meta.Filename, Mustache.render(Mustache.templates.post, { + post: post, + config: that.config + },Mustache.partials), function(err) { + if (err) { + reject(err); + } + if (that.hashes) { + that.hashes.update(post.meta.Url, post.hash); + } + resolve(post.meta.Filename); }); + }); + } + ); +}; - fs.remove(config.directories.htdocs + '/authored-by', function(err) { - fs.ensureDirSync(config.directories.htdocs + '/authored-by'); - - var promises = Object.keys(authors).map(function(name) { - fs.ensureDirSync(authors[name].urlObj.dirname()); - authors[name].config = config; - authors[name].meta = { - title : Generator.strings.tag.sprintf(authors[name].title), - absoluteUrl: authors[name].urlObj.absoluteUrl() - }; - return fs.writeFile(authors[name].urlObj.filename(), Mustache.render(Mustache.templates.index, authors[name], Mustache.partials)); - }); - - promises.push(fs.writeFile( new IndexUrl('authored-by/index.html').filename(), Mustache.render(Mustache.templates.authors, { - index: authorPages, - config: config - }, Mustache.partials))); +/** + * Build special pages from `index` like index pages, tag pages, etc. + * @return {Promise} with first parameter of `resolve` being an array with the numbers of files converted. + */ +Generator.prototype.buildSpecialPages = function() { + var that = this; + return new Promise ( + function(resolve, reject) { + Promise + .all([ + that.buildIndexFiles(), + that.buildTagPages(), + that.buildAuthorPages(), + that.buildMetaFiles() + ]) + .then(resolve) + .catch(reject) + ; + } + ); +}; - Promise - .all(promises) - .then(function() { - console.log("Wrote "+promises.length+" author pages"); - return resolve(promises.length); - }) - .catch(reject) - ; - }); - } - ); - }, - - /** - * Build 404 pages, sitemaps, newsfeeds an stuff like that - * @return {Promise} with first parameter of `resolve` being the number of files converted. - */ - buildMetaFiles: function() { - return new Promise ( - function(resolve, reject) { - var tags = Generator.currentIndex.getTags(); - var tagPages = Object.keys(tags).sort().map(function(key) { - return { - title: tags[key].title, - url : tags[key].urlObj.relativeUrl() - }; - }); +/** + * [buildIndexFiles description] + * @return {Promise} with first parameter of `resolve` being the number of files converted. + */ +Generator.prototype.buildIndexFiles = function(index, path, title) { + var that = this; + index = index || this.currentIndex; + path = path || '/'; + title = title || this.strings.index; + + return new Promise ( + function(resolve, reject) { + fs.ensureDirSync(that.config.directories.htdocs + path); + fs.remove(that.config.directories.htdocs + path + 'index*', function(err) { + var page; + var pagedPosts = index.getPagedPosts(that.config.itemsPerPage); var promises = [ - fs.writeFile( new IndexUrl('404.html').filename(), Mustache.render(Mustache.templates.four, { - index: Generator.currentIndex.getPosts(5), - config: config - }, Mustache.partials)), - - fs.writeFile( new IndexUrl('posts.rss').filename(), Mustache.render(Mustache.templates.rss, { - index: Generator.currentIndex.getPosts(10), - pubDate: dateFormat(Generator.currentIndex.pubDate, 'ddd, dd mmm yyyy hh:MM:ss o'), - config: config + fs.writeFile( new IndexUrl(path + 'posts.rss').filename(), Mustache.render(Mustache.templates.rss, { + index: index.getPosts(10), + pubDate: dateFormat(index.pubDate, 'ddd, dd mmm yyyy hh:MM:ss o'), + config: that.config, + title: title })), - fs.writeFile( new IndexUrl('rss.json').filename(), JSON.stringify(rssJs(Generator.currentIndex.getPosts(20), dateFormat(Generator.currentIndex.pubDate, 'ddd, dd mmm yyyy hh:MM:ss o'), config), undefined, 2)), - - fs.writeFile( new IndexUrl('manifest.json').filename(), JSON.stringify(manifest(config), undefined, 2)), - - fs.writeFile( new IndexUrl('posts.atom').filename(), Mustache.render(Mustache.templates.atom, { - index: Generator.currentIndex.getPosts(10), - pubDate: dateFormat(Generator.currentIndex.pubDate, 'isoDateTime').replace(/(\d\d)(\d\d)$/, '$1:$2'), - config: config - })), + fs.writeFile( new IndexUrl(path + 'rss.json').filename(), JSON.stringify(rssJs(index.getPosts(20), dateFormat(index.pubDate, 'ddd, dd mmm yyyy hh:MM:ss o'), that.config, title), undefined, 2)), - fs.writeFile( new IndexUrl('sitemap.xml').filename(), Mustache.render(Mustache.templates.sitemap, { - index: Generator.currentIndex.getPosts(), - tagPages: tagPages, - pubDate: dateFormat(Generator.currentIndex.pubDate, 'isoDateTime').replace(/(\d\d)(\d\d)$/, '$1:$2'), - config: config + fs.writeFile( new IndexUrl(path + 'posts.atom').filename(), Mustache.render(Mustache.templates.atom, { + index: index.getPosts(10), + pubDate: dateFormat(index.pubDate, 'isoDateTime').replace(/(\d\d)(\d\d)$/, '$1:$2'), + config: that.config, + title: title })) ]; - fs.ensureDirSync(config.directories.htdocs + '/notifications'); - Generator.currentIndex.getPosts(5).forEach(function(post,index) { - promises.push( - fs.writeFile( new IndexUrl('notifications/livetile-'+(index+1)+'.xml').filename(), Mustache.render(Mustache.templates.livetile, { - post: post - })) + for (page = 0; page < pagedPosts.length; page ++) { + var curPageObj = index.getPageData(page, pagedPosts.length, false, path); + var curUrlObj = new IndexUrl(curPageObj.currentUrl); + curPageObj.index = pagedPosts[page]; + curPageObj.config = that.config; + curPageObj.meta = { + title : title, + subtitle : (curPageObj.currentPage === 1) ? '' : that.strings.page.sprintf(curPageObj.currentPage, curPageObj.maxPages), + absoluteUrl: curUrlObj.absoluteUrl(), + absoluteUrlDirname: curUrlObj.absoluteUrlDirname() + }; + curPageObj.prevUrl = new IndexUrl(curPageObj.prevUrl).relativeUrl(); + curPageObj.nextUrl = new IndexUrl(curPageObj.nextUrl).relativeUrl(); + promises.push(fs.writeFile(new IndexUrl(curPageObj.currentUrl).filename(), Mustache.render(Mustache.templates.index, curPageObj, Mustache.partials))); + } + Promise + .all(promises) + .then(function() { + console.log("Wrote "+promises.length+" files for '"+title+"'"); + return resolve(promises.length); + }) + .catch(reject) + ; + }); + } + ); +}; + +/** + * [buildTagPages description] + * @return {Promise} with first parameter of `resolve` being the number of files converted. + */ +Generator.prototype.buildTagPages = function() { + var that = this; + return new Promise ( + function(resolve, reject) { + var tags = that.currentIndex.getTags(); + var tagPages = Object.keys(tags).sort().map(function(key) { + return { + title: tags[key].title, + url : tags[key].urlObj.relativeUrl() + }; + }); + + fs.remove(that.config.directories.htdocs + '/tagged', function(err) { + fs.ensureDirSync(that.config.directories.htdocs + '/tagged'); + + var promises = Object.keys(tags).map(function(key) { + return that.buildIndexFiles( + tags[key].index, + tags[key].urlObj.relativeUrl(), + that.strings.tag.sprintf(tags[key].title) ); }); + promises.push(fs.writeFile( new IndexUrl('tagged/index.html').filename(), Mustache.render(Mustache.templates.tags, { + index: tagPages, + config: that.config + }, Mustache.partials))); + Promise .all(promises) .then(function() { - console.log("Wrote "+promises.length+" meta files"); return resolve(promises.length); }) .catch(reject) ; - } - ); - }, - - /** - * Copy images from Markdown area to live `htdocs`, scaling and optimizing them. - * @return {Promise} with first parameter of `resolve` being the number of files converted. - */ - copyImages: function(article) { - article = article.replace(/\/$/, '').replace(/^.+\//,'') || '**'; - var i, j, processed = 0, maxProcessed = -1; - return new Promise ( - function(resolve, reject) { - glob(config.directories.data + "/" + article + "/*.{png,jpg,gif}", function(er, files) { - maxProcessed = files.length * (config.imageSizes.length + 1); - if (files.length === 0) { + }); + } + ); +}; + +/** + * [buildAuthorPages description] + * @return {Promise} with first parameter of `resolve` being the number of files converted. + */ +Generator.prototype.buildAuthorPages = function() { + var that = this; + return new Promise ( + function(resolve, reject) { + var authors = that.currentIndex.getAuthors(); + var authorPages = Object.keys(authors).sort().map(function(name) { + return { + title: name, + url : authors[name].urlObj.relativeUrl() + }; + }); + + fs.remove(that.config.directories.htdocs + '/authored-by', function(err) { + fs.ensureDirSync(that.config.directories.htdocs + '/authored-by'); + + var promises = Object.keys(authors).map(function(name) { + return that.buildIndexFiles( + authors[name].index, + authors[name].urlObj.relativeUrl(), + that.strings.author.sprintf(name) + ); + }); + + promises.push(fs.writeFile( new IndexUrl('authored-by/index.html').filename(), Mustache.render(Mustache.templates.authors, { + index: authorPages, + config: that.config + }, Mustache.partials))); + + Promise + .all(promises) + .then(function() { + return resolve(promises.length); + }) + .catch(reject) + ; + }); + } + ); +}; + +/** + * Build 404 pages, sitemaps, newsfeeds an stuff like that + * @return {Promise} with first parameter of `resolve` being the number of files converted. + */ +Generator.prototype.buildMetaFiles = function() { + var that = this; + return new Promise ( + function(resolve, reject) { + var tags = that.currentIndex.getTags(); + var tagPages = Object.keys(tags).sort().map(function(key) { + return { + title: tags[key].title, + url : tags[key].urlObj.relativeUrl() + }; + }); + + var promises = [ + fs.writeFile( new IndexUrl('404.html').filename(), Mustache.render(Mustache.templates.four, { + index: that.currentIndex.getPosts(5), + config: that.config + }, Mustache.partials)), + + fs.writeFile( new IndexUrl('sitemap.xml').filename(), Mustache.render(Mustache.templates.sitemap, { + index: that.currentIndex.getPosts(), + tagPages: tagPages, + pubDate: dateFormat(that.currentIndex.pubDate, 'isoDateTime').replace(/(\d\d)(\d\d)$/, '$1:$2'), + config: that.config + })) + ]; + + fs.ensureDirSync(that.config.directories.htdocs + '/notifications'); + that.currentIndex.getPosts(5).forEach(function(post,index) { + promises.push( + fs.writeFile( new IndexUrl('notifications/livetile-'+(index+1)+'.xml').filename(), Mustache.render(Mustache.templates.livetile, { + post: post + })) + ); + }); + + Promise + .all(promises) + .then(function() { + console.log("Wrote "+promises.length+" meta files"); + return resolve(promises.length); + }) + .catch(reject) + ; + } + ); +}; + +/** + * Copy images from Markdown area to live `htdocs`, scaling and optimizing them. + * @return {Promise} with first parameter of `resolve` being the number of files converted. + */ +Generator.prototype.copyImages = function(article) { + article = article.replace(/\/$/, '').replace(/^.+\//,'') || '**'; + + var that = this; + var i; + var j; + var processed = 0; + var maxProcessed = -1; + + return new Promise ( + function(resolve, reject) { + glob(that.config.directories.data + "/" + article + "/*.{png,jpg,gif}", function(er, files) { + maxProcessed = files.length * (that.config.imageSizes.length + 1); + if (files.length === 0) { + resolve( processed ); + } + var checkProcessed = function(err) { + if (err) { + reject(err); + } + if (++processed === maxProcessed) { + console.log("Converted " + processed + " images"); resolve( processed ); } - var checkProcessed = function(err) { - if (err) { - reject(err); - } - if (++processed === maxProcessed) { - console.log("Converted " + processed + " images"); - resolve( processed ); - } - }; - for (i = 0; i < files.length; i++) { - var targetFile = files[i].replace(/^user\//, config.directories.htdocs + '/'); - fs.ensureDirSync(targetFile.replace(/(\/).+?$/, '$1')); + }; + for (i = 0; i < files.length; i++) { + var targetFile = files[i].replace(/^user\//, that.config.directories.htdocs + '/'); + fs.ensureDirSync(targetFile.replace(/(\/).+?$/, '$1')); + gm(files[i]) + .noProfile() + .interlace('Line') + .write(targetFile,checkProcessed) + ; + for (j = 0; j < that.config.imageSizes.length; j++) { + var imageSize = that.config.imageSizes[j]; gm(files[i]) .noProfile() + .geometry(imageSize[0], imageSize[1], "^") + .gravity('Center') + .crop(imageSize[0], imageSize[1]) .interlace('Line') - .write(targetFile,checkProcessed) + .write(targetFile.replace(/(\.[a-z]+$)/,'-'+imageSize[0]+'x'+imageSize[1]+'$1'),checkProcessed) ; - for (j = 0; j < config.imageSizes.length; j++) { - var imageSize = config.imageSizes[j]; - gm(files[i]) - .noProfile() - .geometry(imageSize[0], imageSize[1], "^") - .gravity('Center') - .crop(imageSize[0], imageSize[1]) - .interlace('Line') - .write(targetFile.replace(/(\.[a-z]+$)/,'-'+imageSize[0]+'x'+imageSize[1]+'$1'),checkProcessed) - ; - } } - }); - } - ); - }, - - /** - * Build all articles, special pages and images. - * @return {Promise} [description] - */ - buildAll: function(force) { - return new Promise ( - function(resolve, reject) { - Generator - .buildAllArticles(force) - .then(function(generatedArticles) { - var promises = generatedArticles.map(function(article) { - return Generator.copyImages( article ); - }); - if (generatedArticles.length) { - promises.push(Generator.buildSpecialPages()); - } - Promise - .all(promises) - .then(resolve) - .catch(reject) - ; - }) - .catch(reject) - ; - } - ); - }, - - /** - * Executes deployment command as given in `config.json`. - * @return {Boolean} [description] - */ - deploy: function() { - var shell = require('shelljs'); - if (config.deployCmd) { - console.log('Deploying...'); - shell.exec(config.deployCmd); - console.log('Finished deploying'); + } + }); + } + ); +}; + +/** + * Build all articles, special pages and images. + * @return {Promise} [description] + */ +Generator.prototype.buildAll = function(force) { + var that = this; + return new Promise ( + function(resolve, reject) { + that + .buildAllArticles(force) + .then(function(generatedArticles) { + var promises = generatedArticles.map(function(article) { + return that.copyImages( article ); + }); + if (generatedArticles.length) { + promises.push(that.buildSpecialPages()); + } + Promise + .all(promises) + .then(resolve) + .catch(reject) + ; + }) + .catch(reject) + ; } - return true; + ); +}; + +/** + * Executes deployment command as given in `this.config.json`. + * @return {Boolean} [description] + */ +Generator.prototype.deploy = function() { + var shell = require('shelljs'); + if (this.config.deployCmd) { + console.log('Deploying...'); + shell.exec(this.config.deployCmd); + console.log('Finished deploying'); } + return true; }; module.exports = Generator; diff --git a/src/helpers/url.js b/src/helpers/url.js index 69f7dc48..c576a624 100644 --- a/src/helpers/url.js +++ b/src/helpers/url.js @@ -10,7 +10,7 @@ var path = require('path'); * @return {Url} [description] */ var Url = function (identifier) { - this.identifier = identifier; + this.identifier = identifier? identifier.replace(/^\/+/, '') : null; return this; }; @@ -40,6 +40,15 @@ Url.prototype.absoluteUrl = function () { return !url ? null : config.baseUrl + url; }; +/** + * [absoluteUrlDirname description] + * @return {String} [description] + */ +Url.prototype.absoluteUrlDirname = function () { + var url = this.absoluteUrl(); + return !url ? null : path.dirname(url); +}; + /** * [filename description] * @return {String} [description] diff --git a/src/index.js b/src/index.js index c8a1ea9a..a4080e30 100644 --- a/src/index.js +++ b/src/index.js @@ -5,226 +5,224 @@ * @constructor */ var Index = function() { - var index = []; - var tags = {}; - var authors = {}; - var isSorted = true; - var pubDate = new Date(); + this.index = []; + this.isSorted = true; + this.pubDate = new Date(); + return this; +}; - var internal = { - /** - * [sortIndex description] - * @return {[type]} [description] - */ - sortIndex: function() { - index.sort(function(a,b){ - if (a.meta.Created.timestamp < b.meta.Created.timestamp) { - return 1; - } else if (a.meta.Created.timestamp > b.meta.Created.timestamp) { - return -1; - } - return 0; - }); - isSorted = true; - }, - /** - * [getPageName description] - * @param {[type]} curPage [description] - * @param {[type]} maxPage [description] - * @param {[type]} reverse [description] - * @return {[type]} [description] - */ - getPageName: function(curPage, maxPage, reverse) { - curPage ++; - if (curPage <= 0 || curPage > maxPage) { - return null; - } else if ((!reverse && curPage === 1) || (reverse && curPage === maxPage)) { - return 'index.html'; - } else { - return 'index-' + curPage + '.html'; - } + +/** + * [sortIndex description] + * @return {[type]} [description] + */ +Index.prototype.sortIndex = function() { + this.index.sort(function(a,b){ + if (a.meta.Created.timestamp < b.meta.Created.timestamp) { + return 1; + } else if (a.meta.Created.timestamp > b.meta.Created.timestamp) { + return -1; } - }; + return 0; + }); + this.isSorted = true; +}; - var exports = { - /** - * [clear description] - * @return {[type]} [description] - */ - clear: function() { - isSorted = false; - index = []; - }, +/** + * [getPageName description] + * @param {Number} curPage [description] + * @param {Number} maxPage [description] + * @param {Boolean} reverse [description] + * @param {String} path [description] + * @return {String} [description] + */ +Index.prototype.getPageName = function(curPage, maxPage, reverse, path) { + curPage ++; + if (curPage <= 0 || curPage > maxPage) { + return null; + } else if ((!reverse && curPage === 1) || (reverse && curPage === maxPage)) { + return path + 'index.html'; + } else { + return path + 'index-' + curPage + '.html'; + } +}; - /** - * [push description] - * @param {[type]} post [description] - * @return {[type]} [description] - */ - push: function(post) { - isSorted = false; - index.push(post); - }, +/** + * [clear description] + * @return {[type]} [description] + */ +Index.prototype.clear = function() { + this.isSorted = false; + this.index = []; +}; - /** - * [pushArray description] - * @param {[type]} posts [description] - * @return {[type]} [description] - */ - pushArray: function(posts) { - isSorted = false; - index = posts; - }, +/** + * [push description] + * @param {[type]} post [description] + * @return {[type]} [description] + */ +Index.prototype.push = function(post) { + this.isSorted = false; + this.index.push(post); +}; - /** - * Remove all items form index which have a future timestamp. - * @return {Number} of items removed - */ - removeFutureItems: function() { - if (!isSorted) { - internal.sortIndex(); - } - var now = Math.round(new Date().getTime() / 1000); - var count = 0, i; - for(i = 0; i < index.length; i++) { - if (index[i].meta.Created.timestamp > now) { - count++; - } else { - break; - } - } - if (count) { - index.splice(0,count); - } - return count; - }, +/** + * [pushArray description] + * @param {[type]} posts [description] + * @return {[type]} [description] + */ +Index.prototype.pushArray = function(posts) { + this.isSorted = false; + this.index = posts; +}; - /** - * [makeNextPrev description] - * @return {[type]} [description] - */ - makeNextPrev: function() { - if (!isSorted) { - internal.sortIndex(); - } - var i; - for(i = 0; i < index.length; i++) { - if (i > 0 && index[i-1]) { - index[i].prev = index[i-1].meta; - } - if (i < index.length -1 && index[i+1]) { - index[i].next = index[i+1].meta; - } - } - return this; - }, +/** + * Remove all items form index which have a future timestamp. + * @return {Number} of items removed + */ +Index.prototype.removeFutureItems = function() { + if (!this.isSorted) { + this.sortIndex(); + } + var now = Math.round(new Date().getTime() / 1000); + var count = 0, i; + for(i = 0; i < this.index.length; i++) { + if (this.index[i].meta.Created.timestamp > now) { + count++; + } else { + break; + } + } + if (count) { + this.index.splice(0,count); + } + return count; +}; - /** - * Get all posts, sorted by date. - * @param {integer} i Only return i results. If left empty, all results will be returned. - * @return {Array} [description] - */ - getPosts: function(i) { - if (!isSorted) { - internal.sortIndex(); - } - return i ? index.slice(0,i) : index; - }, +/** + * [makeNextPrev description] + * @return {[type]} [description] + */ +Index.prototype.makeNextPrev = function() { + if (!this.isSorted) { + this.sortIndex(); + } + var i; + for(i = 0; i < this.index.length; i++) { + if (i > 0 && this.index[i-1]) { + this.index[i].prev = this.index[i-1].meta; + } + if (i < this.index.length -1 && this.index[i+1]) { + this.index[i].next = this.index[i+1].meta; + } + } + return this; +}; - /** - * [getTags description] - * @return {[type]} [description] - */ - getTags: function() { - if (!isSorted) { - internal.sortIndex(); - } - tags = {}; - index.forEach(function(post){ - if (post.meta.Tags) { - post.meta.Tags.forEach(function(tag){ - if (tags[tag.id] === undefined) { - tags[tag.id] = tag; - tags[tag.id].index = []; - } - tags[tag.id].index.push(post); - }); - } - }); - return tags; - }, +/** + * Get all posts, sorted by date. + * @param {integer} i Only return i results. If left empty, all results will be returned. + * @return {Array} [description] + */ +Index.prototype.getPosts = function(i) { + if (!this.isSorted) { + this.sortIndex(); + } + return i ? this.index.slice(0,i) : this.index; +}; - /** - * [getAuthors description] - * @return {[type]} [description] - */ - getAuthors: function() { - if (!isSorted) { - internal.sortIndex(); - } - authors = {}; - index.forEach(function(post){ - if (post.meta.AuthorName) { - if (authors[post.meta.AuthorName] === undefined) { - authors[post.meta.AuthorName] = { - name: post.meta.AuthorName, - urlObj: post.meta.AuthorUrlObj, - index: [] - }; - } - authors[post.meta.AuthorName].index.push(post); +/** + * [getTags description] + * @return {[type]} [description] + */ +Index.prototype.getTags = function() { + if (!this.isSorted) { + this.sortIndex(); + } + var tags = {}; + this.index.forEach(function(post){ + if (post.meta.Tags) { + post.meta.Tags.forEach(function(tag){ + if (tags[tag.id] === undefined) { + tags[tag.id] = tag; + tags[tag.id].index = new Index(); } + tags[tag.id].index.push(post); }); - return authors; - }, + } + }); + return tags; +}; - /** - * Get whole index, split up into separate pages. - * @param {[type]} itemsPerPage [description] - * @param {[type]} reverse [description] - * @return {[type]} [description] - */ - getPagedPosts: function(itemsPerPage, reverse) { - if (!isSorted) { - internal.sortIndex(); - } - if (!itemsPerPage) {itemsPerPage = 10;} - var pages = [], page = 0, newIndex = index, currentSlice = []; - if (reverse) { - newIndex = newIndex.reverse(); +/** + * [getAuthors description] + * @return {[type]} [description] + */ +Index.prototype.getAuthors = function() { + if (!this.isSorted) { + this.sortIndex(); + } + var authors = {}; + this.index.forEach(function(post){ + if (post.meta.AuthorName) { + if (authors[post.meta.AuthorName] === undefined) { + authors[post.meta.AuthorName] = { + name: post.meta.AuthorName, + urlObj: post.meta.AuthorUrlObj, + index: new Index() + }; } - do { - page ++; - currentSlice = newIndex.slice(itemsPerPage * (page-1),itemsPerPage * page); - if (reverse) { - currentSlice = currentSlice.reverse(); - } - pages.push(currentSlice); - } while (page * itemsPerPage < index.length); - return pages; - }, + authors[post.meta.AuthorName].index.push(post); + } + }); + return authors; +}; - /** - * Get meta data for a single page. - * @param {[type]} curPage [description] - * @param {[type]} maxPage [description] - * @param {[type]} reverse [description] - * @return {[type]} [description] - */ - getPageData: function(curPage, maxPage, reverse) { - return { - currentUrl: internal.getPageName(curPage, maxPage, reverse), - nextUrl: internal.getPageName(curPage+1, maxPage, reverse), - prevUrl: internal.getPageName(curPage-1, maxPage, reverse), - currentPage: (curPage+1), - nextPage: ((curPage+2 < maxPage) ? curPage+2 : null), - prevPage: ((curPage > 0) ? curPage : null), - maxPages: maxPage - }; +/** + * Get whole index, split up into separate pages. + * @param {[type]} itemsPerPage [description] + * @param {[type]} reverse [description] + * @return {[type]} [description] + */ +Index.prototype.getPagedPosts = function(itemsPerPage, reverse) { + if (!this.isSorted) { + this.sortIndex(); + } + if (!itemsPerPage) {itemsPerPage = 10;} + var pages = [], page = 0, newIndex = this.index, currentSlice = []; + if (reverse) { + newIndex = newIndex.reverse(); + } + do { + page ++; + currentSlice = newIndex.slice(itemsPerPage * (page-1),itemsPerPage * page); + if (reverse) { + currentSlice = currentSlice.reverse(); } - }; + pages.push(currentSlice); + } while (page * itemsPerPage < this.index.length); + return pages; +}; - return exports; +/** + * Get meta data for a single page. + * @param {Number} curPage [description] + * @param {Number} maxPage [description] + * @param {Boolean} reverse [description] + * @param {String} path [description] + * @return {Object} [description] + */ +Index.prototype.getPageData = function(curPage, maxPage, reverse, path) { + path = path || ''; + return { + currentUrl: this.getPageName(curPage, maxPage, reverse, path), + nextUrl: this.getPageName(curPage+1, maxPage, reverse, path), + prevUrl: this.getPageName(curPage-1, maxPage, reverse, path), + currentPage: (curPage+1), + nextPage: ((curPage+2 < maxPage) ? curPage+2 : null), + prevPage: ((curPage > 0) ? curPage : null), + maxPages: maxPage + }; }; module.exports = Index; diff --git a/src/models/rss-js.js b/src/models/rss-js.js index a34c14b9..38afd75d 100644 --- a/src/models/rss-js.js +++ b/src/models/rss-js.js @@ -4,11 +4,11 @@ * Returns RSS as a javascript object. * @constructor */ -var RssJs = function(index, pubDate, config) { +var RssJs = function(index, pubDate, config, title) { return { version: 2.0, channel: { - title: config.name, + title: config.name + (title ? ' | ' + title : ''), link: config.baseUrl + config.basePath, description: config.description, language: config.language, diff --git a/src/templates/atom.xml b/src/templates/atom.xml index 934762c1..93a10de8 100644 --- a/src/templates/atom.xml +++ b/src/templates/atom.xml @@ -1,6 +1,6 @@ - {{config.name}} + {{config.name}} | {{title}} {{config.absoluteBasePath}} {{pubDate}} diff --git a/src/templates/rss.xml b/src/templates/rss.xml index 2e1c8c28..183cd1df 100644 --- a/src/templates/rss.xml +++ b/src/templates/rss.xml @@ -1,7 +1,7 @@ - {{config.name}} + {{config.name}} | {{title}} {{config.absoluteBasePath}} {{config.description}} {{config.language}} diff --git a/test/index.js b/test/index.js new file mode 100644 index 00000000..646b2ead --- /dev/null +++ b/test/index.js @@ -0,0 +1,11 @@ +exports.testGeneralFunctionality = function(test) { + 'use strict'; + //test.expect(2); + + var Index = require('../src/index'); + + //test.throws(function() {translations('xx');}, Error); + //test.throws(function() {translations('de').getString('xx');}, Error); + + test.done(); +}; diff --git a/test/url.js b/test/url.js index fb1598ae..7d1a7546 100644 --- a/test/url.js +++ b/test/url.js @@ -54,15 +54,29 @@ exports.testBasicTransformation = function(test) { exports.testSpecialTransformation = function(test) { 'use strict'; - test.expect(6); + test.expect(12); - test.strictEqual(new PostUrl('Ich-ünd-Dü.md').relativeUrl(), '/posts/ich-uend-due/'); - test.ok(new PostUrl('Ich-ünd-Dü.md').absoluteUrl().match(/^\S+\/posts\/ich-uend-due\/$/)); - test.ok(new PostUrl('Ich-ünd-Dü.md').filename().match(/^\S+\/posts\/ich-uend-due\/index\.html$/)); + var url; + + url = new PostUrl('Ich-ünd-Dü.md'); + test.strictEqual(url.relativeUrl(), '/posts/ich-uend-due/'); + test.ok(url.absoluteUrl().match(/^\S+\/posts\/ich-uend-due\/$/)); + test.ok(url.filename().match(/^\S+\/posts\/ich-uend-due\/index\.html$/)); + + url = new TagUrl('Ich bin ein merkwürdiges Tag'); + test.strictEqual(url.relativeUrl(), '/tagged/ich-bin-ein-merkwuerdiges-tag/'); + test.ok(url.absoluteUrl().match(/^\S+\/tagged\/ich-bin-ein-merkwuerdiges-tag\/$/)); + test.ok(url.filename().match(/^\S+\/tagged\/ich-bin-ein-merkwuerdiges-tag\/index\.html$/)); + + url = new IndexUrl('/Tag'); + test.strictEqual(url.relativeUrl(), '/tag'); + test.ok(url.absoluteUrl().match(/^\S+\/tag$/)); + test.ok(url.filename().match(/^\S+\/tag$/)); - test.strictEqual(new TagUrl('Ich bin ein merkwürdiges Tag').relativeUrl(), '/tagged/ich-bin-ein-merkwuerdiges-tag/'); - test.ok(new TagUrl('Ich bin ein merkwürdiges Tag').absoluteUrl().match(/^\S+\/tagged\/ich-bin-ein-merkwuerdiges-tag\/$/)); - test.ok(new TagUrl('Ich bin ein merkwürdiges Tag').filename().match(/^\S+\/tagged\/ich-bin-ein-merkwuerdiges-tag\/index\.html$/)); + url = new IndexUrl('////Tag'); + test.strictEqual(url.relativeUrl(), '/tag'); + test.ok(url.absoluteUrl().match(/^\S+\/tag$/)); + test.ok(url.filename().match(/^\S+\/tag$/)); test.done(); }; diff --git a/themes/default/templates/index.html b/themes/default/templates/index.html index efcfbd40..e0d16744 100644 --- a/themes/default/templates/index.html +++ b/themes/default/templates/index.html @@ -2,12 +2,15 @@ - {{#meta.title}}{{meta.title}} | {{/meta.title}}{{config.name}} + {{#meta.title}}{{meta.title}}{{#meta.subtitle}}, {{meta.subtitle}}{{/meta.subtitle}} | {{/meta.title}}{{config.name}} - + + + + {{>meta}} @@ -16,7 +19,7 @@
{{#meta.title}} -

{{meta.title}}

+

{{meta.title}}{{#meta.subtitle}}, {{meta.subtitle}}{{/meta.subtitle}}

{{/meta.title}} {{#index.length}} diff --git a/themes/default/templates/partials/meta.html b/themes/default/templates/partials/meta.html index 18594732..eec10986 100644 --- a/themes/default/templates/partials/meta.html +++ b/themes/default/templates/partials/meta.html @@ -21,9 +21,6 @@ - - - {{#config.icons}} {{/config.icons}} diff --git a/themes/default/templates/post.html b/themes/default/templates/post.html index 905edace..8a3ba197 100644 --- a/themes/default/templates/post.html +++ b/themes/default/templates/post.html @@ -20,6 +20,9 @@ {{/post.meta.Latitude}} + + + {{>meta}}