|
var express = require('express'); |
|
var bodyParser = require('body-parser'); |
|
var compress = require('compression'); |
|
var http = require('http'); |
|
var fs = require('fs'); |
|
var qfs = require('q-io/fs'); |
|
var sugar = require('sugar'); |
|
var _ = require('underscore'); |
|
|
|
var markdownit = require('markdown-it')({ |
|
html: true, |
|
xhtmlOut: true, |
|
typographer: true |
|
}).use(require('markdown-it-footnote')); |
|
var Rss = require('rss'); |
|
var Handlebars = require('handlebars'); |
|
|
|
var YAML = require('json2yaml'), json, data, yml; |
|
|
|
|
|
var config = require('./config'); |
|
var version = require('./package.json').version; |
|
var Twitter = require('twitter'); |
|
|
|
// "Statics" |
|
var postsRoot = './posts/'; |
|
var templateRoot = './templates/'; |
|
var metadataMarker = '@@'; |
|
var maxCacheSize = 50; |
|
var postsPerPage = 10; |
|
//var postRegex = /^(.\/)?posts\/\d{4}\/\d{1,2}\/\d{1,2}\/(\w|-)*(.md)?/; |
|
var postRegex = /^(.\/)?posts\/\d{4}\/\d{1,2}\/\d{1,2}\/(\w|-)*(.redirect|.md)?$/; |
|
var footnoteAnchorRegex = /[#"]fn\d+/g; |
|
var footnoteIdRegex = /fnref\d+/g; |
|
var utcOffset = 5; |
|
var cacheResetTimeInMillis = 1800000; |
|
|
|
|
|
// set your twitter information... |
|
var twitterClient = new Twitter({ |
|
consumer_key: config.Social.autoTweets.consumer_key, |
|
consumer_secret: config.Social.autoTweets.consumer_secret, |
|
access_token_key: config.Social.autoTweets.access_token_key, |
|
access_token_secret: config.Social.autoTweets.access_token_secret |
|
}); |
|
var twitterUsername = config.Social.autoTweets.twitterUsername; |
|
var twitterClientNeedle = config.Social.autoTweets.twitterClientNeedle; |
|
|
|
var renderedPosts = {}; |
|
var renderedRss = {}; |
|
var renderedAlternateRss = {}; |
|
var allPostsSortedGrouped = {}; |
|
var headerSource; |
|
var footerSource = null; |
|
|
|
var pageHeaderTemplate = null; |
|
var pageFooterTemplate = null; |
|
var postHeaderTemplate = null; |
|
var postFooterTemplate = null; |
|
var rssFooterTemplate = null; |
|
var singleHeaderTemplate = null; |
|
var singleFooterTemplate = null; |
|
var postBodyStartTemplate = null; |
|
var postBodyEndTemplate = null; |
|
|
|
var siteMetadata = {}; |
|
siteMetadata.SiteUrl = config.Site.Url; |
|
siteMetadata.SiteRoot = config.Site.Url; |
|
siteMetadata.SiteTitle = config.Site.Title; |
|
siteMetadata.Twitter = config.Social.Twitter; |
|
siteMetadata.siteAbout = config.Site.About; |
|
siteMetadata.siteAuthor = config.Site.Author; |
|
siteMetadata.DefaultImage = config.Site.DefaultImage; |
|
siteMetadata.CurrentYear = new Date().getFullYear(); |
|
|
|
function allPostsSortedAndGrouped(completion) { |
|
if (Object.size(allPostsSortedGrouped) !== 0) { |
|
completion(allPostsSortedGrouped); |
|
} else { |
|
qfs.listTree(postsRoot, function (name, stat) { |
|
return postRegex.test(name); |
|
}).then(function (files) { |
|
// Lump the posts together by day |
|
var groupedFiles = _.groupBy(files, function (file) { |
|
var parts = file.split('/'); |
|
return new Date(parts[1], parts[2] - 1, parts[3]); |
|
}); |
|
|
|
// Sort the days from newest to oldest |
|
var retVal = []; |
|
var sortedKeys = _.sortBy(_.keys(groupedFiles), function (date) { |
|
return new Date(date); |
|
}).reverse(); |
|
|
|
// For each day... |
|
_.each(sortedKeys, function (key) { |
|
// Get all the filenames... |
|
var articleFiles = groupedFiles[key]; |
|
var articles = []; |
|
// ...get all the data for that file ... |
|
_.each(articleFiles, function (file) { |
|
if (!file.endsWith('redirect')) { |
|
articles.push(generateHtmlAndMetadataForFile(file)); |
|
} |
|
}); |
|
|
|
// ...so we can sort the posts... |
|
articles = _.sortBy(articles, function (article) { |
|
// ...by their post date and TIME. |
|
return Date.create(article.metadata.Date); |
|
}).reverse(); |
|
// Array of objects; each object's key is the date, value |
|
// is an array of objects |
|
// In that array of objects, there is a body & metadata. |
|
// Note if this day only had a redirect, it may have no articles. |
|
if (articles.length > 0) { |
|
retVal.push({date: key, articles: articles}); |
|
} |
|
}); |
|
|
|
allPostsSortedGrouped = retVal; |
|
completion(retVal); |
|
}); |
|
} |
|
} |
|
|
|
function parseMetadata(lines) { |
|
var retVal = {}; |
|
|
|
lines.each(function (line) { |
|
line = line.replace(metadataMarker, ''); |
|
line = line.compact(); |
|
if (line.has('=')) { |
|
var firstIndex = line.indexOf('='); |
|
retVal[line.first(firstIndex)] = line.from(firstIndex + 1); |
|
} |
|
}); |
|
|
|
// NOTE: Some metadata is added in generateHtmlAndMetadataForFile(). |
|
|
|
// Merge with site default metadata |
|
/* |
|
Object.merge(retVal, siteMetadata, false, function(key, targetVal, sourceVal) { |
|
// Ensure that the file wins over the defaults. |
|
console.log('overwriting "' + sourceVal + '" with "' + targetVal); |
|
return targetVal; |
|
}); |
|
*/ |
|
return retVal; |
|
} |
|
|
|
function performMetadataReplacements(replacements, haystack) { |
|
_.keys(replacements).each(function (key) { |
|
// Ensure that it's a global replacement; non-regex treatment is first-only. |
|
haystack = haystack.replace(new RegExp(metadataMarker + key + metadataMarker, 'g'), replacements[key]); |
|
}); |
|
|
|
return haystack; |
|
} |
|
|
|
// Parses the HTML and renders it. |
|
function parseHtml(lines, replacements, postHeader, postFooter) { |
|
// Convert from markdown |
|
var body = performMetadataReplacements(replacements, markdownit.render(lines) ); |
|
// Perform replacements |
|
var header = performMetadataReplacements(replacements, headerSource); |
|
var body = body; |
|
// Concatenate HTML |
|
return header + '<article>' + postHeader + '<div class="entry">' + body + '</div>' + postFooter + '</article>' + footerSource; |
|
} |
|
|
|
// Gets all the lines in a post and separates the metadata from the body |
|
function agetLinesFromPost(file) { |
|
file = file.endsWith('.md') ? file : file + '.md'; |
|
var data = fs.readFileSync(file, {encoding: 'UTF8'}); |
|
|
|
// Extract the pieces |
|
var lines = data.lines(); |
|
var metadataLines = _.filter(lines, function (line) { return line.startsWith(metadataMarker); }); |
|
var body = _.difference(lines, metadataLines).join('\n'); |
|
return {metadata: metadataLines, body: body}; |
|
} |
|
|
|
function generateHtmlAndMetadataForLines(lines, file) { |
|
var metadata = parseMetadata(lines.metadata); |
|
if (typeof(file) !== 'undefined') { |
|
metadata.relativeLink = externalFilenameForFile(file); |
|
// If this is a post, assume a body class of 'post'. |
|
if (postRegex.test(file)) { |
|
metadata.BodyClass = 'post'; |
|
} |
|
} |
|
|
|
return { |
|
metadata: metadata, |
|
header: performMetadataReplacements(metadata, headerSource), |
|
postHeader: performMetadataReplacements(metadata, postHeaderTemplate(metadata)), |
|
rssFooter: performMetadataReplacements(metadata, rssFooterTemplate(metadata)), |
|
unwrappedBody: performMetadataReplacements(metadata, markdownit.render(lines.body)), |
|
html: function () { |
|
return this.header + |
|
this.postHeader + |
|
this.unwrappedBody + |
|
footerSource; |
|
} |
|
}; |
|
} |
|
|
|
function externalFilenameForFile(file, request) { |
|
var hostname = request != undefined ? request.headers.host : ''; |
|
|
|
var retVal = hostname.length ? ('http://' + hostname) : ''; |
|
retVal += file.at(0) == '/' && hostname.length > 0 ? '' : '/'; |
|
retVal += file.replace('.md', '').replace(postsRoot, '').replace(postsRoot.replace('./', ''), ''); |
|
return retVal; |
|
} |
|
|
|
|
|
// Gets the metadata & rendered HTML for this file |
|
function generateHtmlAndMetadataForFile(file) { |
|
var retVal = fetchFromCache(file); |
|
if (retVal == undefined) { |
|
var lines = getLinesFromPost(file); |
|
var metadata = parseMetadata(lines.metadata); |
|
// console.log( metadata ); |
|
metadata.relativeLink = externalFilenameForFile(file); |
|
metadata.permalink = metadata.relativeLink; |
|
|
|
// Description |
|
if ( typeof(metadata.Description) === 'undefined') { |
|
metadata.Description = metadata.Title; |
|
} |
|
|
|
if( metadata.permalink == '/index' ){ |
|
metadata.canonicalLink = metadata.SiteRoot; |
|
metadata.ogtype = 'website'; |
|
}else{ |
|
metadata.canonicalLink = metadata.SiteRoot + '' + metadata.permalink; |
|
metadata.ogtype = 'article'; |
|
} |
|
|
|
if ( typeof(metadata.Image) === 'undefined') { |
|
metadata.Image = metadata.DefaultImage; |
|
} |
|
|
|
// If this is a post, assume a body class of 'post'. |
|
if (postRegex.test(file)) { |
|
metadata.BodyClass = 'post'; |
|
} |
|
if( metadata.BodyClass == 'BodyClass' ){ |
|
metadata.BodyClass = 'post'; |
|
} |
|
|
|
var body = lines['body']; |
|
// var html = parseHtml(body, metadata, mheader, mfooter); |
|
addRenderedPostToCache(file, { |
|
metadata: metadata, |
|
body: body |
|
}); |
|
} |
|
return fetchFromCache(file); |
|
} |
|
|
|
String.prototype.capitalize = function() { |
|
return this.charAt(0).toUpperCase() + this.slice(1); |
|
} |
|
|
|
function leadingZero(value){ |
|
if(value < 10){ |
|
return "0" + value.toString(); |
|
} |
|
return value.toString(); |
|
} |
|
|
|
function normalizedFileName(file) { |
|
var retVal = file; |
|
if (file.startsWith('posts')) { |
|
retVal = './' + file; |
|
} |
|
|
|
retVal = retVal.replace('.md', ''); |
|
|
|
return retVal; |
|
} |
|
|
|
function fetchFromCache(file) { |
|
return renderedPosts[normalizedFileName(file)] || null; |
|
} |
|
|
|
function addRenderedPostToCache(file, postData) { |
|
console.log('Adding to cache: ' + normalizedFileName(file)); |
|
renderedPosts[normalizedFileName(file)] = _.extend({ file: normalizedFileName(file), date: new Date() }, postData); |
|
|
|
if (_.size(renderedPosts) > maxCacheSize) { |
|
var sorted = _.sortBy(renderedPosts, function (post) { return post.date; }); |
|
delete renderedPosts[sorted.first().file]; |
|
} |
|
|
|
//console.log('Cache has ' + JSON.stringify(_.keys(renderedPosts))); |
|
} |
|
|
|
// Separate the metadata from the body |
|
function getLinesFromData(data) { |
|
// Extract the metadata |
|
var metadataLines = _.filter(data.lines(), function (line) { return line.startsWith(metadataMarker); }); |
|
// The body starts after metadata. Thus, it starts at (index of last line of metadata + length of metadata). |
|
var body = data.substring(data.indexOf(metadataLines[metadataLines.length - 1]) + metadataLines[metadataLines.length - 1].length).trim(); |
|
|
|
return {metadata: metadataLines, body: body}; |
|
} |
|
|
|
// Gets all the lines in a post and separates the metadata from the body |
|
function getLinesFromPost(file) { |
|
file = file.endsWith('.md') ? file : file + '.md'; |
|
var data = fs.readFileSync(file, {encoding: 'UTF8'}); |
|
|
|
return getLinesFromData(data); |
|
} |
|
|
|
|
|
// render the code into _posts folder... |
|
allPostsSortedAndGrouped(function (postsByDay) { |
|
postsByDay.forEach(function (day) { |
|
day['articles'].forEach(function (article) { |
|
var date = Date.create( article.metadata.Date ); |
|
var d = (article.metadata.Date).split(" "); |
|
var lastmod = d[0]; |
|
var file = (article.file).split("/"); |
|
var file = file.pop(); |
|
var file = "_posts/" + lastmod + "-" + file + ".md"; |
|
console.log( file ); |
|
|
|
var pmeta = {}; |
|
pmeta.layout = "post"; |
|
for( var i in article.metadata ){ |
|
if( i.toLowerCase() === "tags" ){ |
|
var tags = article.metadata[i]; |
|
tags = tags.split(", "); |
|
// tags = tags.join("\n- "); |
|
pmeta[ "tags" ] = tags; |
|
}else if( i.toLowerCase() !== "linked" && |
|
i.toLowerCase() !== "relativelink" && |
|
i.toLowerCase() !== "permalink" && |
|
i.toLowerCase() !== "canonicallink" && |
|
i.toLowerCase() !== "description" |
|
){ |
|
pmeta[ i.toLowerCase() ] = article.metadata[i]; |
|
} |
|
} |
|
|
|
yml = YAML.stringify( pmeta ); |
|
// console.log( yml ); |
|
var yml = yml.replace(/\ \ /g, ""); |
|
var body = yml + "---\n\n"+article.body; |
|
fs.writeFile(file, body, function(err) { |
|
if(err) { |
|
return console.log('Unable to write file ' + err); |
|
} |
|
console.log( file + ': was saved'); |
|
}); |
|
}); |
|
}); |
|
}); |