PopPop is a static site and blog generator for Node.
| |
| lib/cli_tools.js |
Module dependencies.
|
var yamlish = require('yamlish')
, path = require('path')
, fs = require('fs')
, generators = require(__dirname + '/generators');
module.exports = {
|
Adds zero padding to single digit numbers.
|
datePad: function(num) {
return num.toString().length === 1 ? '0' + num : num;
},
|
Makes a post file name (not URL) based on the config's permanlink format.
param: String Permalink format param: Date Date for the file name param: String The title for the post param: String The file format (md or textile) return: String
|
getPostFileName: function(pf, date, title, format) {
title = title.toLowerCase().replace(/\s+/g, '-');
return '_posts/' + pf.replace(':year', date.getFullYear())
.replace(':month', this.datePad(date.getMonth() + 1))
.replace(':day', this.datePad(date.getDate()))
.replace(/^\//, '')
.replace(/\//g, '-')
.replace(':title', title + '.' + format);
},
|
Generates a stubbed post and writes it based on a file name.
|
makePost: function(config, title, fn) {
var meta = {
layout: 'post'
, title: title
, author: ''
, tags: ['tag_1', 'tag_2']
}
, url = ''
, format = 'md'
, date = new Date()
, frontMatter = ''
, fileName = this.getPostFileName(config.permalink, date, title, format);
frontMatter = '---\n' + yamlish.encode(meta).replace(/^\s+/mg, '') + '\n---\n';
fs.writeFile(path.join(config.root, fileName), frontMatter, function(err) {
if (err) {
console.error('Error writing file:', fileName);
throw(err);
}
console.log('Post created:', fileName);
fn();
});
},
|
Default config settings, used by site generator.
|
defaultConfig: function() {
return ''
+ '{ "url": "http://example.com"\n'
+ ' , "title": "Example"\n'
+ ' , "permalink": "/:year/:month/:day/:title"\n'
+ ' , "perPage": 10\n'
+ ' , "exclude": ["\\\\.swp"]\n'
+ ' , "autoGenerate": [{"feed": "feed.xml", "rss": "feed.rss"}] }\n'
},
|
Default index file, used by site generator.
|
defaultIndex: function() {
return ''
+ '---\n'
+ 'layout: default\n'
+ 'title: My Site\n'
+ 'paginate: true\n'
+ '---\n\n'
+ '!{paginatedPosts()}\n'
+ '!{paginate}\n';
},
|
Default layout, used by site generator.
|
defaultLayout: function() {
return fs.readFileSync(__dirname + '/assets/default.jade');
},
|
Default post layout, used by site generator.
|
defaultPostLayout: function() {
return fs.readFileSync(__dirname + '/assets/post.jade');
},
|
Sample post, to get people started.
|
samplePost: function() {
return ''
+ '---\n'
+ 'title: Example Post About Something\n'
+ 'author: Pop\n'
+ 'layout: post\n'
+ 'tags:\n'
+ '- tag1\n'
+ '- tag2\n'
+ '---\n'
+ 'Pop is a static site generator. It can be used to make blogs. I hope you enjoy it!\n';
},
|
Built-in stylesheet.
|
defaultStylus: function() {
return fs.readFileSync(__dirname + '/assets/screen.styl');
},
|
Site generator. Will not create a site if pathName exists.
|
makeSite: function(args, fn) {
var pathName
, generator;
if (args.length === 1) {
pathName = args[0];
generator = 'default';
} else {
pathName = args[1];
generator = args[0];
}
if (generators.hasOwnProperty(generator)) {
generators[generator].run(this, pathName, fn);
} else {
try {
var pluginGenerator = require(generator);
} catch (e) {
console.error('Error: Unable to find the', generator, 'generator');
return;
}
pluginGenerator.generator.run(this, pathName, fn);
}
},
|
Renders files that match pattern .
|
renderFile: function(pop, config, pattern) {
var fileMap = new pop.FileMap(config)
, siteBuilder = new pop.SiteBuilder(config);
fileMap.on('ready', function() {
if (fileMap.files.length === 0) {
fn();
} else {
siteBuilder.fileMap = fileMap;
siteBuilder.build();
siteBuilder.on('ready', function() {
console.log('%d files rendered.', fileMap.files.length);
});
}
});
fileMap.search(pattern);
}
};
|
| lib/config.js |
Config file reader.
|
function readConfigFile(file) {
var defaults = {
perPage: 20
, port: 4000
, output: '_site/'
};
function applyDefaults(config) {
for (var key in defaults) {
config[key] = config[key] || defaults[key];
}
if (config.url) config.url = config.url.replace(/\/$/, '');
return config;
}
try {
var data = fs.readFileSync(file).toString();
return applyDefaults(JSON.parse(data));
} catch(exception) {
if (exception.code === 'EBADF') {
console.error('No _config.json file in this directory. Is this a Pop site?');
process.exit(1);
} else {
console.log('Error reading config:', exception.message);
throw(exception);
}
}
}
|
Module dependencies and additional config variables.
|
var fs = require(__dirname + '/graceful');
module.exports = readConfigFile;
|
| lib/file_map.js |
Module dependencies.
|
var fs = require(__dirname + '/graceful')
, path = require('path')
, EventEmitter = require('events').EventEmitter;
|
Initialize FileMap with a config object.
param: Object options api: public
|
function FileMap(config) {
this.config = config || {};
this.config.exclude = config && config.exclude ? config.exclude : [];
this.config.exclude.push('/_site');
this.ignoreDotFiles = true;
this.root = config.root;
this.files = [];
this.events = new EventEmitter();
this.filesLeft = 0;
this.dirsLeft = 1;
}
|
Determines file type based on file extension.
|
FileMap.prototype.fileType = function(fileName) {
var extension = path.extname(fileName).substring(1);
if (fileName.match(/\/_posts\
return 'post ' + extension;
} else if (fileName.match(/\/_layouts\
return 'layout ' + extension;
} else if (fileName.match(/\/_includes\
return 'include ' + extension;
} else if (['jade', 'ejs', 'styl'].indexOf(extension) !== -1) {
return 'file ' + extension;
} else {
return 'file';
}
};
|
Recursively iterates from an initial path.
|
FileMap.prototype.walk = function(dir) {
if (!dir) dir = this.root;
var self = this;
fs.readdir(dir, function(err, files) {
self.dirsLeft--;
if (!files) return;
files.forEach(function(file) {
file = path.join(dir, file);
self.filesLeft++;
fs.stat(file, function(err, stats) {
if (err) console.log('Error:', err);
if (!stats) return;
if (stats.isDirectory(file)) {
self.filesLeft--;
self.dirsLeft++;
self.walk(file);
self.addFile(file, 'dir');
} else {
self.filesLeft--;
self.addFile(file, self.fileType(file));
if (self.filesLeft === 0 && self.dirsLeft === 0) {
process.nextTick(function() {
self.events.emit('ready');
});
}
}
});
});
});
};
|
Searches for files that match pattern .
|
FileMap.prototype.search = function(pattern) {
this.searchPattern = pattern;
this.walk();
};
|
Checks to see if a file name matches the excluded patterns.
|
FileMap.prototype.isExcludedFile = function(file) {
if (this.ignoreDotFiles)
if (file.match(/\/\./)) return true;
return this.config.exclude.some(function(pattern) {
return file.match(pattern);
});
};
|
Determines file type based on file extension.
param: String File name param: String File type
|
FileMap.prototype.addFile = function(file, type) {
if (this.isExcludedFile(file)) return;
if (this.searchPattern && !file.match(this.searchPattern)) return;
this.files.push({
name: file,
type: type,
});
};
|
Bind an event to the internal EventEmitter .
param: String Event name param: Function Handler
|
FileMap.prototype.on = function(eventName, fn) {
this.events.on(eventName, fn);
};
module.exports = FileMap;
|
| lib/filters.js |
module.exports = {
|
*
* Replaces liquid tag highlight directives with prettyprint HTML tags.
*
* @param {String} The text for a post
* @return {String}
|
highlight: function(data) {
data = data.replace(/{% highlight ([^ ]*) %}/g, '<pre class="prettyprint lang-$1">');
data = data.replace(/{% endhighlight %}/g, '</pre>');
return data;
}
};
|
|
| lib/generators.js |
module.exports = {
'default': require(__dirname + '/generators/default')
};
|
|
| lib/graceful.js |
Module dependencies.
|
var fs = require('fs')
, path = require('path')
, defaultTimeout = 0
, timeout = defaultTimeout;
|
Offers functionality similar to mkdir -p , but is async.
|
function mkdir_p(dir, mode, callback, position) {
mode = mode || process.umask();
position = position || 0;
parts = path.normalize(dir).split('/');
if (position >= parts.length) {
if (callback) {
return callback();
} else {
return true;
}
}
var directory = parts.slice(0, position + 1).join('/') || '/';
fs.stat(directory, function(err) {
if (err === null) {
mkdir_p(dir, mode, callback, position + 1);
} else {
fs.mkdir(directory, mode, function(err) {
if (err && err.errno != 17) {
if (callback) {
return callback(err);
} else {
throw err;
}
} else {
mkdir_p(dir, mode, callback, position + 1);
}
});
}
});
}
|
Polymorphic approach to fs.mkdir()
|
fs.mkdir_p = function(dir, mode, callback) {
mkdir_p(dir, mode, callback || process.noop);
}
Object.keys(fs)
.forEach(function(i) {
exports[i] = (typeof fs[i] !== 'function') ? fs[i]
: (i.match(/^[A-Z]|^create|Sync$/)) ? function() {
return fs[i].apply(fs, arguments);
}
: graceful(fs[i]);
});
function graceful(fn) { return function GRACEFUL() {
var args = Array.prototype.slice.call(arguments)
, cb_ = args.pop();
args.push(cb);
function cb(er) {
if (er && er.message.match(/^EMFILE, Too many open files/)) {
setTimeout(function() {
GRACEFUL.apply(fs, args)
}, timeout++);
return;
}
timeout = defaultTimeout;
cb_.apply(null, arguments);
}
fn.apply(fs, args)
}};
|
| lib/helpers.js |
Module dependencies and local variables.
|
var jade = require('jade')
, fs = require('fs')
, path = require('path')
, cache = {}
, _date = require('underscore.date')
, helpers;
helpers = {
|
Pagination links.
|
paginate: function(paginator) {
var template = '';
template += '.pages\n';
template += ' - if (paginator.previousPage)\n';
template += ' span.prev_next\n';
template += ' - if (paginator.previousPage === 1)\n';
template += ' span ←\n';
template += ' a.previous(href="/") Previous\n';
template += ' - else\n';
template += ' span ←\n';
template += ' a.previous(href="/page" + paginator.previousPage + "/") Previous\n';
template += ' - if (paginator.pages > 1)\n';
template += ' span.prev_next\n';
template += ' - for (var i = 1; i <= paginator.pages; i++)\n';
template += ' - if (i === paginator.page)\n';
template += ' strong.page #{i}\n';
template += ' - else if (i !== 1)\n';
template += ' a.page(href="/page" + i + "/") #{i}\n';
template += ' - else\n';
template += ' a.page(href="/") 1\n';
template += ' - if (paginator.nextPage <= paginator.pages)\n';
template += ' a.next(href="/page" + paginator.nextPage + "/") Next\n';
template += ' span →\n';
return jade.render(template, { locals: { paginator: paginator } });
},
|
Generates paginated blog posts, suitable for use on an index page.
|
paginatedPosts: function() {
var template
, site = this;
template = ''
+ '- for (var i = 0; i < paginator.items.length; i++)\n'
+ ' !{hNews(paginator.items[i], true)}\n';
return jade.render(template, { locals: site.applyHelpers({ paginator: site.paginator }) });
},
|
Atom Jade template.
|
atom: function(feed, summarise) {
var template = ''
, url = this.config.url
, title = this.config.title
, perPage = this.config.perPage
, posts = this.posts.slice(-perPage).reverse()
, site = this;
summarise = typeof summarise === 'boolean' && summarise ? 3 : summarise;
perPage = site.posts.length < perPage ? site.posts.length : perPage;
template += '!!!xml\n';
template += 'feed(xmlns="http://www.w3.org/2005/Atom")\n';
template += ' title #{title}\n';
template += ' link(href=feed, rel="self")\n';
template += ' link(href=url)\n';
if (posts.length > 0)
template += ' updated #{dx(posts[0].date)}\n';
template += ' id #{url}\n';
template += ' author\n';
template += ' name #{title}\n';
template += ' - for (var i = 0, post = posts[i]; i < ' + perPage + '; i++, post = posts[i])\n';
template += ' entry\n';
template += ' title #{post.title}\n';
template += ' link(href=url + post.url)\n';
template += ' updated #{dx(post.date)}\n';
template += ' id #{url.replace(/\\/$/, "")}#{post.url}\n';
if (summarise)
template += ' content(type="html") !{h(truncateParagraphs(post.content, summarise, ""))}\n';
else
template += ' content(type="html") !{h(post.content)}\n';
return jade.render(template, { locals: site.applyHelpers({
paginator: site.paginator
, posts: posts
, title: title
, url: url
, feed: feed
, summarise: summarise
})});
},
|
RSS Jade template.
|
rss: function(feed, summarise, description) {
var template = ''
, url = this.config.url
, title = this.config.title
, perPage = this.config.perPage
, posts = this.posts.slice(-perPage).reverse()
, site = this;
description = description || title;
summarise = typeof summarise === 'boolean' && summarise ? 3 : summarise;
perPage = site.posts.length < perPage ? site.posts.length : perPage;
template += '!!!xml\n';
template += 'rss(version="2.0")\n';
template += ' channel\n';
template += ' title #{title}\n';
template += ' link #{url}\n';
template += ' description #{description}\n';
if (posts.length > 0) {
template += ' pubDate #{d822(posts[0].date)}\n';
template += ' lastBuildDate #{d822(posts[0].date)}\n';
}
template += ' generator Pop\n';
template += ' - for (var i = 0, post = posts[i]; i < ' + perPage + '; i++, post = posts[i])\n';
template += ' item\n';
template += ' title #{post.title}\n';
template += ' link #{url + post.url}\n';
template += ' pubDate #{d822(post.date)}\n';
template += ' guid #{url.replace(/\\/$/, "")}#{post.url}\n';
if (summarise)
template += ' description !{h(truncateParagraphs(post.content, summarise, ""))}\n';
else
template += ' description !{h(post.content)}\n';
return jade.render(template, { locals: site.applyHelpers({
paginator: site.paginator
, posts: posts
, title: title
, url: url
, feed: feed
, description: description
, summarise: summarise
})});
},
|
Returns unique sorted tags for every post.
|
allTags: function() {
var allTags = [];
for (var key in this.posts) {
if (this.posts[key].tags) {
for (var i = 0; i < this.posts[key].tags.length; i++) {
var tag = this.posts[key].tags[i];
if (allTags.indexOf(tag) === -1) allTags.push(tag);
}
}
}
allTags.sort(function(a, b) {
a = a.toLowerCase();
b = b.toLowerCase();
if (a < b) return -1;
if (a > b) return 1;
return 0;
});
return allTags;
},
|
Display a list of tags.
TODO Link options
param: Array Tag names return: String
|
tags: function(tags) {
return tags.map(function(tag) {
return '<a href="/tags.html#' + escape(tag) + '">' + tag + '</a>';
}).join(', ');
},
|
Renders a post using the hNews microformat, based on:
http://www.readability.com/publishers/guidelines/#view-exampleGuidelines
|
hNews: function(post, summary) {
var template = '';
template += 'article.hentry\n';
template += ' header\n';
template += ' h1.entry-title\n';
template += ' a(href=post.url) !{post.title}\n';
template += ' time.updated(datetime=dx(post.date), pubdate) #{ds(post.date)}\n';
if (post.author)
template += ' p.byline.author.vcard by <span class="fn">#{post.author}</span>\n';
if (post.tags) template += ' div.tags !{tags(post.tags)}\n';
if (summary) {
if (post.summary) {
template += ' !{post.summary + "<p><a class=\\"read-more\\" href=\\"' + post.url + '\\">Read More →</a></p>"}\n';
} else {
template += ' !{truncateParagraphs(post.content, 2, "<p><a class=\\"read-more\\" href=\\"' + post.url + '\\">Read More →</a></p>")}\n';
}
} else {
template += ' !{post.content}\n';
}
return jade.render(template, { locals: this.applyHelpers({ post: post }) });
},
|
Formats a date with date formatting rules according to underscore.date's rules.
|
df: function(date, format) {
return _date(date).format(format);
},
|
Short date (01 January 2001).
|
ds: function(date) {
return helpers.df(date, 'DD MMMM YYYY');
},
|
Atom date formatting.
|
dx: function(date) {
return helpers.df(date, 'YYYY-MM-DDTHH:MM:ssZ');
},
|
RFC-822 dates.
|
d822: function(date) {
return helpers.df(date, 'ddd, DD MMM YYYY HH:MM:ss') + ' GMT';
},
|
Escapes brackets and ampersands.
|
h: function(text) {
return text && text.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
},
|
Truncates HTML based on paragraph counts.
param: String Text to truncate param: Integer Number of paragraphs param: String Text to append when truncated return: String
|
truncateParagraphs: function(text, length, moreText) {
var t = text.split('</p>');
return t.length < length ? text : t.slice(0, length).join('</p>') + '</p>' + moreText;
},
|
Truncates based on characters (not HTML safe, use with pre-formatted text).
|
truncate: function(text, length, moreText) {
return text.length > length ? text.slice(0, length).trim() + moreText : text;
},
|
Truncates based on words (not HTML safe, use with pre-formatted text).
param: String Text to truncate param: Integer Number of words param: String Text to append when truncated return: String
|
truncateWords: function(text, length, moreText) {
var t = text.split(/\s/);
return t.length > length ? t.slice(0, length).join(' ') + moreText : text;
}
};
module.exports = helpers;
|
| lib/paginator.js |
Initialize Paginator with the number of items per-page and a list of items.
|
function Paginator(perPage, items) {
this.allItems = this.sort(items);
this.perPage = perPage;
this.items = this.allItems.slice(0, perPage);
this.previousPage = 0;
this.nextPage = 2;
this.page = 1;
this.pages = Math.round(this.allItems.length / this.perPage) + 1;
}
|
Moves to the next page.
|
Paginator.prototype.advancePage = function() {
this.page++;
this.previousPage = this.page - 1;
this.nextPage = this.page + 1;
var start = (this.page - 1) * this.perPage
, end = this.page * this.perPage;
this.items = this.allItems.slice(start, end);
};
|
Sort items according to date.
param: Array Array of items return: Integer -1 , 1 , 0 according to the date comparison
|
Paginator.prototype.sort = function(items) {
return items.sort(function(a, b) {
a = a.date.valueOf();
b = b.date.valueOf();
if (a > b)
return -1;
else if (a < b)
return 1;
return 0;
});
};
module.exports = Paginator;
|
| lib/pop.js |
Module dependencies and local variables.
|
var path = require('path')
, fs = require(__dirname + '/../lib/graceful')
, FileMap = require(__dirname + '/file_map')
, SiteBuilder = require(__dirname + '/site_builder')
, cliTools = require(__dirname + '/cli_tools')
, readConfig = require(__dirname + '/config')
, usage
, args = process.argv.slice(2);
usage = 'pop is a static site builder.\n\n';
usage += 'Usage: pop [command] [options]\n';
usage += 'new path Generates a new site at path/\n';
usage += 'post "Post Title" Writes a new post file\n';
usage += 'render pattern Renders files that match "pattern"\n';
usage += 'server [port] Create a server on port (default: 4000) for _site/\n\n';
usage += '-v, --version Display version and exit\n';
usage += '-h, --help Shows this message\n';
|
Loads the config script and sets the local variable.
|
function loadConfig() {
var root = process.cwd()
, config = readConfig(path.join(root, '_config.json'));
config.root = root;
return config;
}
|
Loads configuration then runs generateSite .
|
function loadConfigAndGenerateSite(useServer, port) {
var config = loadConfig();
if (port) config.port = port;
generateSite(config, useServer);
}
|
Runs FileMap and SiteBuilder based on the config.
params: Object Configuration options params: Boolean Use a HTTP sever? return: SiteBuilder A SiteBuilder instance
|
function generateSite(config, useServer) {
var fileMap = new FileMap(config)
, siteBuilder = new SiteBuilder(config)
, server = require(__dirname + '/server')(siteBuilder);
fileMap.walk();
fileMap.on('ready', function() {
siteBuilder.fileMap = fileMap;
siteBuilder.build();
});
siteBuilder.on('ready', function() {
if (useServer) {
server.run();
server.watch();
}
});
return siteBuilder;
}
module.exports = function() {
if (args.length === 0) return loadConfigAndGenerateSite();
while (args.length) {
arg = args.shift();
switch (arg) {
case 'server':
loadConfigAndGenerateSite(true, args.shift());
break;
case 'post':
return cliTools.makePost(loadConfig(), args.shift(), function() { process.exit(0); });
break;
case 'new':
return cliTools.makeSite(args, function() { process.exit(0); });
break;
case 'render':
return cliTools.renderFile(module.exports, loadConfig(), args.shift());
break;
case '-v':
case '--version':
var version = JSON.parse(fs.readFileSync(__dirname + '/../package.json')).version;
console.log('pop version:', version);
process.exit(0);
break;
case '-h':
case '--help':
console.log(usage);
process.exit(1);
default:
loadConfigAndGenerateSite();
}
}
};
module.exports.SiteBuilder = SiteBuilder;
module.exports.FileMap = FileMap;
module.exports.generateSite = generateSite;
module.exports.cliTools = cliTools;
|
| lib/server.js |
Module dependencies and local variables.
|
var path = require('path')
, watch = require('nodewatch')
, siteBuilder;
|
Instantiates and runs the Express server.
|
function server() {
var express = require('express'),
app = express.createServer();
app.configure(function() {
app.use(express.static(siteBuilder.outputRoot));
app.use(express.errorHandler({ dumpExceptions: true, showStack: true }));
});
app.get('*', function(req, res) {
var postPath = siteBuilder.outputRoot + req.url + '/';
if (req.url.match(/[^/]$/) && path.existsSync(postPath)) {
res.redirect(req.url + '/');
} else {
res.send('404');
}
});
app.listen(siteBuilder.config.port);
console.log('Listening on port', siteBuilder.config.port);
}
|
Watches for file changes and regenerates files as required.
## TODO Work in progress
|
function watchChanges() {
function buildChange(file) {
console.log('File changed:', file);
try {
siteBuilder.buildChange(file);
} catch (e) {
console.log('Error building site:', e);
}
}
siteBuilder.fileMap.files.forEach(function(file) {
if (file.type === 'dir') {
watch.add(file.name).onChange(buildChange);
}
});
}
module.exports = function(s) {
siteBuilder = s;
return {
run: server
, watch: watchChanges
};
};
|
| lib/site_builder.js |
Module dependencies.
|
var textile = require('stextile')
, fs = require('./graceful')
, path = require('path')
, jade = require('jade')
, stylus = require('stylus')
, yamlish = require('yamlish')
, markdown = require('markdown-js')
, Paginator = require('./paginator')
, FileMap = require('./file_map.js').FileMap
, EventEmitter = require('events').EventEmitter
, filters = require('./filters')
, helpers = require('./helpers')
, userHelpers = {}
, userFilters = {}
, userPostFilters = {};
|
Initialize SiteBuilder with a config object and FileMap .
|
function SiteBuilder(config, fileMap) {
this.config = config;
this.root = config.root;
var helperFile = path.join(this.root, '_lib', 'helpers.js');
if (path.existsSync(helperFile)) {
userHelpers = require(helperFile);
}
var filterFile = path.join(this.root, '_lib', 'filters.js');
if (path.existsSync(filterFile)) {
userFilters = require(filterFile);
}
var postFilterFile = path.join(this.root, '_lib', 'post-filters.js');
if (path.existsSync(postFilterFile)) {
userPostFilters = require(postFilterFile);
}
this.outputRoot = config.output;
this.fileMap = fileMap;
this.posts = [];
this.helpers = helpers;
this.events = new EventEmitter();
this.includes = {};
this.loadPlugins();
}
|
Loads Pop plugins based on config.require .
|
SiteBuilder.prototype.loadPlugins = function() {
if (!this.config.require) return;
var self = this;
this.config.require.forEach(function(name) {
try {
var plugin = require(name);
self.loadPlugin(name, plugin);
} catch (e) {
console.error('Unable to load plugin:', name, '-', e.message);
throw(e);
}
});
};
|
Applies helpers and "user helpers" to an object so it can be easily passed to Jade.
param: String The name of the plugin param: Object The plugin's module. Properties loaded: helpers , filters , postFilters
|
SiteBuilder.prototype.loadPlugin = function(name, plugin) {
if (!plugin) return;
for (key in plugin.helpers) {
if (plugin.helpers.hasOwnProperty(key))
userHelpers[key] = this.bind(plugin.helpers[key]);
}
for (key in plugin.filters) {
if (plugin.filters.hasOwnProperty(key))
userFilters[key] = this.bind(plugin.filters[key]);
}
for (key in plugin.postFilters) {
if (plugin.postFilters.hasOwnProperty(key))
userPostFilters[key] = this.bind(plugin.postFilters[key]);
}
};
|
Applies helpers and "user helpers" to an object so it can be easily passed to Jade.
|
SiteBuilder.prototype.applyHelpers = function(obj) {
var self = this
, key;
for (key in this.helpers) {
obj[key] = this.bind(this.helpers[key]);
}
for (key in userHelpers) {
obj[key] = this.bind(userHelpers[key]);
}
obj.include = function(template) {
return self.includes[template];
};
if (obj.paginate && obj.paginator)
obj.paginate = obj.paginate(obj.paginator);
obj.site = self;
return obj;
};
|
Binds methods to this SiteBuilder .
|
SiteBuilder.prototype.bind = function(fn) {
var self = this;
return function() {
return fn.apply(self, arguments);
};
};
|
Builds the site. This is asynchronous, so various counters
and events are used to track progress.
|
SiteBuilder.prototype.build = function() {
var self = this;
function build() {
var posts = self.findPosts()
, otherFiles = self.otherRenderedFiles()
, staticFiles = self.staticFiles()
, autoGen = self.autoGenerate()
, postsLeft = posts.length
, filesLeft;
if (posts.length === 0) autoGen = [];
filesLeft = posts.length + otherFiles.length + staticFiles.length + autoGen.length;
function checkFinished() {
if (filesLeft === 0) {
self.events.emit('ready');
}
}
function renderAutoGen() {
autoGen.forEach(function(file) {
self.autoGenerateFile(file, function() {
filesLeft--;
checkFinished();
});
});
}
if (posts.length === 0) {
renderAutoGen();
}
posts.forEach(function(file) {
self.renderPost(file, function() {
filesLeft--;
postsLeft--;
checkFinished();
if (postsLeft === 0) {
renderAutoGen();
}
});
});
otherFiles.forEach(function(file) {
self.renderFile(file, function() {
filesLeft--;
checkFinished();
});
});
staticFiles.forEach(function(file) {
self.copyStatic(file, function() {
filesLeft--;
checkFinished();
});
});
}
this.events.once('cached includes', build);
this.cacheIncludes();
};
|
Returns any configured built-in pages,
or an empty array.
|
SiteBuilder.prototype.autoGenerate = function() {
if (!this.config.autoGenerate) return [];
return this.config.autoGenerate;
};
|
Generates a built-in page. Only atom feeds and RSS
are currently available.
|
SiteBuilder.prototype.autoGenerateFile = function(file, fn) {
var self = this;
if (file.feed) {
if (!this.config.url || !this.config.title) {
console.error('Error: Built-in feed generation requires config values for: url and title.');
} else {
var layoutData = "!{atom('" + this.config.url + '/' + file.feed + "')}"
, html = jade.render(layoutData, { locals: self.applyHelpers({ }) });
this.write(this.outFileName(file.feed), html);
fn();
}
}
if (file.rss) {
if (!this.config.url || !this.config.title) {
console.error('Error: Built-in feed generation requires config values for: url and title.');
} else {
var layoutData = "!{rss('" + this.config.url + '/' + file.rss + "')}"
, html = jade.render(layoutData, { locals: self.applyHelpers({ }) });
this.write(this.outFileName(file.rss), html);
fn();
}
}
};
|
Determines if a file needs Jade or Stylus rendering.
|
SiteBuilder.prototype.isRenderedFile = function(file) {
return file.type === 'file jade' || file.type === 'file styl';
};
|
Builds a single file.
## TODO Work in progress.
|
SiteBuilder.prototype.buildChange = function(file) {
if (this.fileMap.isExcludedFile(file)) return;
file = {
type: this.fileMap.fileType(file)
, name: file
};
if (file.type.indexOf('post') !== -1) {
this.renderPost(file);
} else if (this.isRenderedFile(file)) {
this.renderFile(file);
} else if (file.type === 'file') {
this.copyStatic(file);
}
};
|
Iterates over the files in the FileMap object to find posts.
|
SiteBuilder.prototype.findPosts = function() {
return this.fileMap.files.filter(function(file) {
return file.type.indexOf('post') !== -1;
});
};
|
Iterates over the files in the FileMap object to find "other" rendered files.
|
SiteBuilder.prototype.otherRenderedFiles = function() {
var self = this;
return this.fileMap.files.filter(function(file) {
return self.isRenderedFile(file);
});
};
|
Iterates over the files in the FileMap object to find static files that require copying.
|
SiteBuilder.prototype.staticFiles = function() {
return this.fileMap.files.filter(function(file) {
return file.type === 'file';
});
};
|
Copies a static file.
|
SiteBuilder.prototype.copyStatic = function(file, fn) {
var outDir = this.outFileName(path.dirname(file.name.replace(this.root, '')))
, fileName = path.join(outDir, path.basename(file.name))
, self = this;
if (path.basename(file.name).match(/^_/)) return fn.apply(self);
fs.mkdir_p(outDir, 0777, function(err) {
if (err) {
console.error('Error creating directory:', outDir);
throw(err);
}
fs.readFile(file.name, function(err, data) {
if (err) {
console.error('Error reading file:', file.name);
throw(err);
}
fs.writeFile(fileName, data, function(err) {
if (err) {
console.error('Error writing file:', fileName);
throw(err);
}
if (fn) fn.apply(self);
});
});
});
};
|
Parse YAML meta data for both files and posts.
param: String File name (used by logging on errors) param: String Data to parse return: Array Parsed YAML
|
SiteBuilder.prototype.parseMeta = function(file, data) {
function clean(yaml) {
for (var key in yaml) {
if (typeof yaml[key] === 'string') {
var m = yaml[key].match(/^"([^"]*)"$/);
if (m) yaml[key] = m[1];
}
}
return yaml;
}
function fix(text) {
if (!text) return;
return text.split('\n').map(function(line) {
if (line.match(/^- /))
line = ' ' + line;
return line;
}).join('\n');
}
var dataChunks = data.split('---')
, parsedYAML = [];
try {
if (dataChunks[1]) {
parsedYAML = clean(yamlish.decode(fix(dataChunks[1] || data)));
return [parsedYAML, ((dataChunks || []).slice(2).join('---')).trim()];
} else {
return ['', data];
}
} catch (e) {
console.error("Couldn't parse YAML in:", file, ':', e);
}
};
|
Writes a file, making directories recursively when required.
|
SiteBuilder.prototype.write = function(fileName, content) {
if (!content) return console.error('No content for:', fileName);
content = this.applyPostFilters(content);
fs.mkdir_p(path.dirname(fileName), 0777, function(err) {
if (err) {
console.error('Error creating directory:', path.dirname(fileName));
throw(err);
}
fs.writeFile(fileName, content, function(err) {
if (err) {
console.error('Error writing file:', fileName);
throw(err);
}
});
});
};
|
Returns a full path name.
|
SiteBuilder.prototype.outFileName = function(subDir, name) {
return path.join(this.outputRoot, subDir, name);
};
|
Caches templates inside _includes/
|
SiteBuilder.prototype.cacheIncludes = function() {
var self = this;
function done() {
self.events.emit('cached includes');
}
path.exists(path.join(this.root, '_includes'), function(exists) {
if (!exists) {
done();
} else {
fs.readdir(path.join(self.root, '_includes'), function(err, files) {
if (!files) return;
var file;
if (files.length === 0) done();
for (var i = 0; i < files.length; i++) {
file = files[i];
if (path.extname(file) !== '.jade') {
if (i === files.length) return self.events.emit('cached includes');
} else {
fs.readFile(path.join(self.root, '_includes', file), 'utf8', function(err, data) {
var html = jade.render(data, { locals: self.applyHelpers({}) })
, name = path.basename(file).replace(path.extname(file), '');
self.includes[name] = html;
if (i === files.length) done();
});
}
}
});
}
});
};
|
Renders a post using a template. Called by renderPost .
param: String Template file name param: Object A post object param: String The post's content
|
SiteBuilder.prototype.renderTemplate = function(templateFile, post, content) {
var self = this;
templateFile = path.join(this.root, '_layouts', templateFile + '.jade');
fs.readFile(templateFile, 'utf8', function(err, data) {
if (err) {
console.error('Error in: ' + templateFile);
console.error(err);
console.error(err.message);
throw(err);
}
try {
var html = jade.render(data, { locals: self.applyHelpers({ post: post, content: content }) })
, fileName = self.outFileName(post.fileName, 'index.html')
, dirName = path.dirname(fileName);
} catch (e) {
console.error('Error rendering:', templateFile);
throw(e);
}
path.exists(dirName, function(exists) {
if (exists) {
self.write(fileName, html);
} else {
fs.mkdir_p(dirName, 0777, function(err) {
if (err) {
console.error('Error making directory:', dirName);
throw(err);
}
self.write(fileName, html);
});
}
});
});
};
|
Renders a generic Jade file and will supply pagination if required.
|
SiteBuilder.prototype.renderFile = function(file, fn) {
var self = this;
if (file.type === 'file styl') {
var outFileName = self.outFileName(
path.dirname(file.name.replace(self.root, '')),
path.basename(file.name).replace(path.extname(file.name), '.css')
);
return fs.readFile(file.name, 'utf8', function(err, fileData) {
stylus.render(fileData, { filename: path.basename(outFileName) }, function(err, css) {
if (err) throw err;
self.write(outFileName, css);
if (fn) fn.apply(self);
});
});
}
function render(fileData, meta, layoutData, dirName) {
var ext = file.name.match('\\.[^.]*\\' + path.extname(file.name) + '$') ? '' : '.html';
dirName = dirName || '';
var outFileName = self.outFileName(
path.dirname(file.name.replace(self.root, '')) + dirName,
path.basename(file.name).replace(path.extname(file.name), ext)
);
var fileContent = jade.render(fileData, {
locals: self.applyHelpers({ paginator: self.paginator, page: meta }),
});
if (!layoutData) {
self.write(outFileName, fileContent);
} else {
var html = jade.render(layoutData, { locals: self.applyHelpers({ content: fileContent }) });
self.write(outFileName, html);
}
if (fn) fn.apply(self);
}
fs.readFile(file.name, 'utf8', function(err, fileData) {
var meta = self.parseMeta(file.name, fileData)
, fileContent = '';
fileData = meta[1];
meta = meta[0];
if (!meta.layout) {
self.paginator = new Paginator(self.config.perPage, self.posts);
render(fileData, meta);
} else {
fs.readFile(path.join(self.root, '_layouts', meta.layout + '.jade'), function(err, layoutData) {
if (err) {
console.error('Unable to read layout:', meta.layout + '.jade');
throw(err);
}
self.paginator = new Paginator(self.config.perPage, self.posts)
render(fileData, meta, layoutData);
if (meta.paginate) {
while (self.paginator.items.length) {
self.paginator.advancePage();
render(fileData, meta, layoutData, '/page' + self.paginator.page + '/');
}
}
});
}
});
};
|
Parses a file name according to the permalink format.
|
SiteBuilder.prototype.parseFileName = function(fileName) {
var format = this.config.permalink
, parts = fileName.match(/(\d+)-(\d+)-(\d+)-(.*)/)
, year = parts[1]
, month = parts[2]
, day = parts[3]
, title = parts[4].replace(/\.(textile|md)/, '');
return { date: new Date(Date.UTC(year, month - 1, day)),
fileName: format.replace(':year', year).
replace(':month', month).
replace(':day', day).
replace(':title', title) };
};
|
Applies internal and user-supplied content pre-filters.
|
SiteBuilder.prototype.applyFilters = function(text) {
for (var key in filters) {
text = filters[key](text);
}
for (key in userFilters) {
text = userFilters[key].apply(this, [text]);
}
return text;
};
|
Applies user-supplied content post-filters. These are run after HTML is generated.
|
SiteBuilder.prototype.applyPostFilters = function(text) {
for (key in userPostFilters) {
text = userPostFilters[key].apply(this, [text]);
}
return text;
};
|
Renders a post.
|
SiteBuilder.prototype.renderPost = function(file, fn) {
var self = this
, formatter;
if (file.type.indexOf('textile') !== -1) {
formatter = textile;
} else if (file.type.indexOf('md') !== -1) {
formatter = markdown.makeHtml;
}
fs.readFile(file.name, 'utf8', function(err, data) {
var meta = self.parseMeta(file.name, data);
data = meta[1];
meta = meta[0];
if (!meta.tags) meta.tags = meta.categories ? meta.categories : [];
if (data && meta) {
var fileDetails = self.parseFileName(file.name);
meta.fileName = fileDetails.fileName;
meta.url = fileDetails.fileName;
meta.date = fileDetails.date;
meta.content = formatter(self.applyFilters(data));
if (meta.summary)
meta.summary = formatter(self.applyFilters(meta.summary));
self.renderTemplate(meta.layout, meta, meta.content);
self.posts.push(meta);
}
if (fn) fn.apply(self);
});
};
|
Adds a listener to the internal EventEmitter object.
|
SiteBuilder.prototype.on = function(eventName, fn) {
this.events.on(eventName, fn);
};
module.exports = SiteBuilder;
|