|
|
|
#!/usr/bin/env node
|
|
|
|
var fs = require('fs');
|
|
|
|
var marked = require('marked');
|
|
|
|
var mkdirp = require('mkdirp');
|
|
|
|
var glob = require('glob');
|
|
|
|
var ejs = require('ejs');
|
|
|
|
var path = require('path');
|
|
|
|
var semver = require('semver');
|
|
|
|
|
|
|
|
var input = path.resolve(process.argv[2]);
|
|
|
|
var output = path.resolve(process.argv[3]);
|
|
|
|
var template = path.resolve(process.argv[4]);
|
|
|
|
var rssTemplate = path.resolve(process.argv[5]);
|
|
|
|
|
|
|
|
var config = {
|
|
|
|
postsPerPage: 4
|
|
|
|
};
|
|
|
|
|
|
|
|
console.error("argv=%j", process.argv)
|
|
|
|
|
|
|
|
fs.readFile(template, 'utf8', function(er, contents) {
|
|
|
|
if (er) throw er;
|
|
|
|
template = ejs.compile(contents, template);
|
|
|
|
template.filename = 'index.html';
|
|
|
|
fs.readFile(rssTemplate, 'utf8', function(er, contents) {
|
|
|
|
if (er) throw er;
|
|
|
|
rssTemplate = ejs.compile(contents, rssTemplate);
|
|
|
|
rssTemplate.filename = 'index.xml';
|
|
|
|
readInput();
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
function readInput() {
|
|
|
|
glob(input + '/**/*.md', function(er, files) {
|
|
|
|
if (er) throw er;
|
|
|
|
readFiles(files);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function readFiles(files) {
|
|
|
|
var n = files.length;
|
|
|
|
var data = { files: {}, feeds: {}, posts: {}};
|
|
|
|
|
|
|
|
files.forEach(function(file) {
|
|
|
|
fs.readFile(file, 'utf8', next(file));
|
|
|
|
});
|
|
|
|
|
|
|
|
function next(file) { return function(er, contents) {
|
|
|
|
if (er) throw er;
|
|
|
|
if (contents) {
|
|
|
|
contents = parseFile(file, contents);
|
|
|
|
if (contents) {
|
|
|
|
data.files[file] = contents
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (--n === 0) {
|
|
|
|
buildOutput(data);
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
}
|
|
|
|
|
|
|
|
function parseFile(file, contents) {
|
|
|
|
var c = contents.split('\n\n');
|
|
|
|
var head = c.shift();
|
|
|
|
c = c.join('\n\n');
|
|
|
|
var post = head.split('\n').reduce(function(set, kv) {
|
|
|
|
kv = kv.split(':');
|
|
|
|
var key = kv.shift().trim();
|
|
|
|
var val = kv.join(':').trim();
|
|
|
|
set[key] = val;
|
|
|
|
return set;
|
|
|
|
}, {});
|
|
|
|
if (post.status && post.status !== 'publish') return null;
|
|
|
|
post.body = c;
|
|
|
|
post.src = file;
|
|
|
|
return post;
|
|
|
|
}
|
|
|
|
|
|
|
|
function buildPermalinks(data) {
|
|
|
|
Object.keys(data.files).forEach(function(k) {
|
|
|
|
data.posts[k] = buildPermalink(k, data.files[k]);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function buildPermalink(key, post) {
|
|
|
|
var data = {};
|
|
|
|
data.pageid = post.slug;
|
|
|
|
data.title = post.title;
|
|
|
|
data.content = post.content = marked.parse(post.body);
|
|
|
|
|
|
|
|
// Fix for chjj/marked#56
|
|
|
|
data.content = post.content = data.content
|
|
|
|
.replace(/<a href="([^"]+)<\/a>">\1<\/a>/g, '$1');
|
|
|
|
|
|
|
|
data.post = post;
|
|
|
|
|
|
|
|
if (!post.date) throw new Error('post date is required ' + post.src);
|
|
|
|
else post.date = new Date(post.date);
|
|
|
|
var d = post.date;
|
|
|
|
|
|
|
|
var y = d.getYear() + 1900;
|
|
|
|
var m = d.getMonth() + 1;
|
|
|
|
if (m < 10) m = '0' + m;
|
|
|
|
var d = d.getDate();
|
|
|
|
if (d < 10) d = '0' + d;
|
|
|
|
var uri = '/' + y + '/' + m + '/' + d + '/' + post.slug + '/';
|
|
|
|
post.data = data;
|
|
|
|
post.uri = uri;
|
|
|
|
|
|
|
|
post.permalink = data.permalink = uri;
|
|
|
|
return data;
|
|
|
|
}
|
|
|
|
|
|
|
|
function writeFile(uri, data, templ) {
|
|
|
|
if (!templ) templ = template;
|
|
|
|
data.uri = path.join(data.uri);
|
|
|
|
uri = path.join(uri);
|
|
|
|
var contents = templ(data);
|
|
|
|
var outdir = path.join(output, uri);
|
|
|
|
mkdirp(outdir, function(er) {
|
|
|
|
if (er) throw er;
|
|
|
|
var file = path.resolve(outdir, templ.filename);
|
|
|
|
fs.writeFile(file, contents, 'utf8', function(er) {
|
|
|
|
if (er) throw er;
|
|
|
|
console.log('wrote: ', data.pageid, path.relative(process.cwd(), file));
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// sort in reverse chronological order
|
|
|
|
// prune out any releases that are not the most recent on their branch.
|
|
|
|
function buildFeeds(data) {
|
|
|
|
// first, sort by date.
|
|
|
|
var posts = Object.keys(data.posts).map(function(k) {
|
|
|
|
return data.posts[k].post;
|
|
|
|
}).sort(function(a, b) {
|
|
|
|
a = a.date.getTime();
|
|
|
|
b = b.date.getTime();
|
|
|
|
return (a === b) ? 0 : a > b ? -1 : 1;
|
|
|
|
})
|
|
|
|
|
|
|
|
// separate release posts by release families.
|
|
|
|
var releases = posts.reduce(function(releases, post) {
|
|
|
|
if (post.category !== 'release') return releases;
|
|
|
|
var ver = semver.parse(post.version);
|
|
|
|
if (!ver) return;
|
|
|
|
var major = +ver[1];
|
|
|
|
var minor = +ver[2];
|
|
|
|
var patch = +ver[3];
|
|
|
|
var family = [major, minor];
|
|
|
|
ver = [major, minor, patch, post];
|
|
|
|
if (family[1] % 2) family[1]++;
|
|
|
|
family = family.join('.');
|
|
|
|
post.family = family;
|
|
|
|
releases[family] = releases[family] || [];
|
|
|
|
releases[family].push(post);
|
|
|
|
return releases;
|
|
|
|
}, {});
|
|
|
|
|
|
|
|
// separate by categories.
|
|
|
|
var categories = posts.reduce(function(categories, post) {
|
|
|
|
if (!post.category) return categories;
|
|
|
|
if (!categories[post.category]) {
|
|
|
|
categories[post.category] = [];
|
|
|
|
}
|
|
|
|
categories[post.category].push(post);
|
|
|
|
return categories;
|
|
|
|
}, {});
|
|
|
|
|
|
|
|
// paginate categories.
|
|
|
|
for (var cat in categories) {
|
|
|
|
categories[cat] = paginate(categories[cat], cat);
|
|
|
|
}
|
|
|
|
|
|
|
|
// filter non-latest release notices out of main feeds.
|
|
|
|
// still show the first stable release of the family, since
|
|
|
|
// it usually is an important milestone with benchmarks and stuff.
|
|
|
|
var main = posts.filter(function(post) {
|
|
|
|
if (post.version && post.family) {
|
|
|
|
var ver = semver.parse(post.version)
|
|
|
|
if (+ver[2] % 2 === 0 && +ver[3] === 0) {
|
|
|
|
// 0.x.0, where x is event
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
if (post.version && post.family && post !== releases[post.family][0]) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
});
|
|
|
|
|
|
|
|
// add previous/next based on main feed.
|
|
|
|
main.forEach(function (post, i, posts) {
|
|
|
|
post.next = posts[i - 1];
|
|
|
|
post.prev = posts[i + 1];
|
|
|
|
})
|
|
|
|
|
|
|
|
// paginate each feed.
|
|
|
|
main = paginate(main, '');
|
|
|
|
|
|
|
|
// put previous/next links on orphaned old releases so you can get back
|
|
|
|
for (var family in releases) {
|
|
|
|
releases[family].forEach(function(post, i, family) {
|
|
|
|
if (!post.next) post.next = family[i - 1];
|
|
|
|
if (!post.next) post.next = family[0].next;
|
|
|
|
// if (!post.next) post.next = family[0];
|
|
|
|
|
|
|
|
if (!post.prev) post.prev = family[i + 1];
|
|
|
|
if (!post.prev) post.prev = family[0].prev;
|
|
|
|
});
|
|
|
|
// paginate
|
|
|
|
releases[family] = paginate(releases[family], 'release-' + family);
|
|
|
|
}
|
|
|
|
|
|
|
|
// paginate
|
|
|
|
data.feeds = {
|
|
|
|
main: main,
|
|
|
|
categories: categories,
|
|
|
|
releases: releases
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
function paginate(set, title) {
|
|
|
|
var pp = config.postsPerPage || 5
|
|
|
|
var pages = [];
|
|
|
|
for (var i = 0; i < set.length; i += pp) {
|
|
|
|
pages.push(set.slice(i, i + pp));
|
|
|
|
}
|
|
|
|
var id = title.replace(/[^a-zA-Z0-9.]+/g, '-');
|
|
|
|
return { id: id || 'index', pageid: id, posts: set, pages: pages, title: title };
|
|
|
|
}
|
|
|
|
|
|
|
|
function writePermalinks(data) {
|
|
|
|
Object.keys(data.posts).forEach(function(k) {
|
|
|
|
var post = data.posts[k];
|
|
|
|
writeFile(post.permalink, post);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function writeFeeds(data) {
|
|
|
|
writeFeed(data.feeds.main);
|
|
|
|
|
|
|
|
for (var feed in data.feeds.categories) {
|
|
|
|
writeFeed(data.feeds.categories[feed]);
|
|
|
|
}
|
|
|
|
for (var feed in data.feeds.releases) {
|
|
|
|
writeFeed(data.feeds.releases[feed]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function writeFeed(feed) {
|
|
|
|
var title = feed.title;
|
|
|
|
feed.pages.forEach(function(page, p, pages) {
|
|
|
|
writePaginated(feed.title, page, p, pages.length, feed.id);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function writePaginated(title, posts, p, total, id) {
|
|
|
|
var uri = '/' + encodeURIComponent(title) + '/';
|
|
|
|
var d = {
|
|
|
|
title: title,
|
|
|
|
page: p,
|
|
|
|
posts: posts,
|
|
|
|
total: total,
|
|
|
|
paginated: true,
|
|
|
|
pageid: id + '-' + p,
|
|
|
|
uri: uri
|
|
|
|
};
|
|
|
|
if (p === 0) {
|
|
|
|
writeFile(uri, d);
|
|
|
|
writeFile('/feed' + uri, d, rssTemplate);
|
|
|
|
}
|
|
|
|
writeFile(uri + p, d);
|
|
|
|
writeFile('/feed' + uri + p, d, rssTemplate);
|
|
|
|
}
|
|
|
|
|
|
|
|
function buildOutput(data) {
|
|
|
|
buildPermalinks(data);
|
|
|
|
buildFeeds(data);
|
|
|
|
writePermalinks(data);
|
|
|
|
writeFeeds(data);
|
|
|
|
}
|
|
|
|
|