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');
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.log('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'
+ ' , "paginate": 10\n'
+ ' , "exclude": ["\\\\.swp"]\n'
+ ' , "autoGenerate": [{"feed": "feed.xml"}] }\n'
},
|
Default index file, used by site generator.
|
defaultIndex: function() {
return ''
+ '---\n'
+ 'layout: default\n'
+ 'title: My Site\n'
+ '---\n\n'
+ 'h1 Welcome to my site\n'
+ 'p Coming soon.\n';
},
|
Default layout, used by site generator.
|
defaultLayout: function() {
return ''
+ '!!! 5\n'
+ 'html(lang="en")\n'
+ ' head\n'
+ ' title Example Site\n'
+ ' body\n'
+ ' #content\n'
+ ' !{content}\n';
},
|
Site generator. Will not create a site if pathName exists.
|
makeSite: function(pathName, fn) {
if (path.existsSync(pathName)) {
console.log('Error: Path already exists');
process.exit(1);
} else {
var paths = ['_posts', '_lib', '_layouts', '_includes'];
fs.mkdirSync(pathName, 0777);
paths.forEach(function(p) {
fs.mkdirSync(path.join(pathName, p), 0777);
});
fs.writeFileSync(path.join(pathName, '_config.json'), this.defaultConfig());
fs.writeFileSync(path.join(pathName, 'index.jade'), this.defaultIndex());
fs.writeFileSync(path.join(pathName, '_layouts', 'default.jade'), this.defaultLayout());
console.log('Site created:', pathName);
fn();
}
}
};
|
| lib/config.js |
Config file reader.
|
function readConfigFile(file) {
var defaults = {
perPage: 20
};
function applyDefaults(config) {
for (var key in defaults) {
config[key] = config[key] || defaults[key];
}
return config;
}
try {
var data = fs.readFileSync(file).toString();
return applyDefaults(JSON.parse(data));
} catch(exception) {
console.log('Error reading config:', exception.message);
throw(exception);
}
}
|
Module dependencies and additional config variables.
|
var root = process.cwd()
, 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');
});
}
}
});
});
});
};
|
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;
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/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 } });
},
|
Atom Jade template.
|
atom: function(url, title, feed, perPage) {
perPage = perPage || 20;
var template = ''
, site = this;
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';
template += ' updated #{dx(site.posts[0].date)}\n';
template += ' id #{url}\n';
template += ' author\n';
template += ' name #{title}\n';
template += ' - for (var i = 0, post = site.posts[i]; i < ' + perPage + '; i++, post = site.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';
template += ' content(type="html") !{h(post.content)}\n';
return jade.render(template, { locals: site.applyHelpers({ paginator: site.paginator, title: title, url: url, feed: feed }) });
},
|
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) {
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';
template += ' p.byline.author.vcard by <span class="fn">#{post.author}</span>\n';
if (post.tags) template += ' div.tags !{tags(post.tags)}\n';
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');
},
|
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;
}
};
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')
, 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)
, version = '0.0.1';
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 += 'server Create a server on port 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) {
generateSite(loadConfig(), 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() {
while (args.length) {
arg = args.shift();
switch (arg) {
case 'server':
loadConfigAndGenerateSite(true);
break;
case 'post':
cliTools.makePost(loadConfig(), args.shift(), function() { process.exit(0); });
break;
case 'new':
cliTools.makeSite(args.shift(), function() { process.exit(0); });
break;
case '-v':
case '--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(4000);
console.log('Listening on port 4000');
}
|
Watches for file changes and regenerates files as required.
## TODO Work in progress
|
function watchChanges() {
watch
.add(siteBuilder.root)
.add(path.join(siteBuilder.root, '_posts'))
.onChange(function(file) {
console.log('File changed:', file);
siteBuilder.buildChange(file);
});
}
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 = {};
|
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);
}
this.outputRoot = config.output || path.join(this.root, '_site');
this.fileMap = fileMap;
this.posts = [];
this.helpers = helpers;
this.events = new EventEmitter();
this.includes = {};
}
|
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');
}
}
posts.forEach(function(file) {
self.renderPost(file, function() {
filesLeft--;
postsLeft--;
checkFinished();
if (postsLeft === 0) {
autoGen.forEach(function(file) {
self.autoGenerateFile(file, function() {
filesLeft--;
checkFinished();
});
});
}
});
});
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
are currently available.
|
SiteBuilder.prototype.autoGenerateFile = function(file, fn) {
var self = this;
if (file.feed) {
if (!this.config.url || !this.config.title) {
console.log('ERROR: Built-in feed generation requires config values for: url and title.');
} else {
var layoutData = "!{atom('" + this.config.url + "', '" + this.config.title + "', '" + this.config.url + file.feed + "')}";
var html = jade.render(layoutData, { locals: self.applyHelpers({ }) });
this.write(this.outFileName(file.feed), html);
fn();
}
}
};
|
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 (file.type === 'file jade') {
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() {
return this.fileMap.files.filter(function(file) {
return file.type === 'file jade' || file.type === 'file styl';
});
};
|
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();
fs.mkdir_p(outDir, 0777, function(err) {
if (err) {
console.log('Error creating directory:', outDir);
throw(err);
}
fs.readFile(file.name, function(err, data) {
if (err) {
console.log('Error reading file:', file.name);
throw(err);
}
fs.writeFile(fileName, data, function(err) {
if (err) {
console.log('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[2] || '').trim()];
} else {
return ['', data];
}
} catch (e) {
console.log("Couldn't parse YAML in:", file, ':', e);
}
};
|
Writes a file, making directories recursively when required.
|
SiteBuilder.prototype.write = function(fileName, content) {
if (!content) {
console.log('No content for:', fileName);
return;
}
fs.mkdir_p(path.dirname(fileName), 0777, function(err) {
if (err) {
console.log('Error creating directory:', path.dirname(fileName));
throw(err);
}
fs.writeFile(fileName, content, function(err) {
if (err) {
console.log('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.log('Error in: ' + templateFile);
console.log(err);
console.log(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.log('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.log('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);
});
});
}
function render(fileData, paginator, 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: 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) {
var paginator = new Paginator(self.config.perPage, self.posts);
render(fileData, paginator, meta);
} else {
fs.readFile(path.join(self.root, '_layouts', meta.layout + '.jade'), function(err, layoutData) {
if (err) {
console.log('Unable to read layout:', meta.layout + '.jade');
throw(err);
}
var paginator = new Paginator(self.config.perPage, self.posts)
render(fileData, paginator, meta, layoutData);
if (meta.paginate) {
while (paginator.items.length) {
paginator.advancePage();
render(fileData, paginator, meta, layoutData, '/page' + 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];
return { date: new Date(year, month, 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;
};
|
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));
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;
|