catlogjs
Version:
Static site generator, translate human readable text format(such as markdown) into html, with a lot of other functions
305 lines (275 loc) • 10 kB
JavaScript
;
var path = require('path');
//
// Returns an array object of all the directives for the given html.
// Each item of the array has the following form:
//
//
// {
// type: 'css',
// dest: 'css/site.css',
// src: [
// 'css/normalize.css',
// 'css/main.css'
// ],
// raw: [
// ' <!-- build:css css/site.css -->',
// ' <link rel="stylesheet" href="css/normalize.css">',
// ' <link rel="stylesheet" href="css/main.css">',
// ' <!-- endbuild -->'
// ]
// }
// Note that when treating an HTML file making usage of requireJS
// an additional information for the block is added, regarding RequireJS
// configuration. For example:
//
// requirejs: {
// name: 'scripts/main',
// dest: 'scripts/foo.js'
// }
//
// Note also that dest is expressed relatively from the root. I.e., if the block starts with:
// <!-- build:css /foo/css/site.css -->
// then dest will equal foo/css/site.css (note missing trailing /)
//
var getBlocks = function (dest, dir, content) {
// start build pattern: will match
// * <!-- build:[target] output -->
// * <!-- build:[target](alternate search path) output -->
// The following matching param are set when there's match
// * 0 : the whole matched expression
// * 1 : the target (ie. type)
// * 2 : the alternate search path
// * 3 : the output
//
var regbuild = /<!--\s*build:(\w+)(?:\(([^\)]+)\))?\s*([^\s]+)\s*-->/;
// end build pattern -- <!-- endbuild -->
var regend = /<!--\s*endbuild\s*-->/;
var lines = content.replace(/\r\n/g, '\n').split(/\n/),
block = false,
sections = [],
last;
var originDir = dir;
lines.forEach(function (l) {
var indent = (l.match(/^\s*/) || [])[0];
var build = l.match(regbuild);
var endbuild = regend.test(l);
var startFromRoot = false;
// discard empty lines
if (build) {
block = true;
// Handle absolute path (i.e. with respect to the server root)
if (build[3][0] === '/') {
startFromRoot = true;
build[3] = build[3].substr(1);
}
if (build[2]) {
// Alternate search path
originDir = build[2];
}
last = {
type: build[1],
dest: path.join(dest, build[3]),
startFromRoot: startFromRoot,
indent: indent,
src: [],
raw: []
};
}
// switch back block flag when endbuild
if (block && endbuild) {
last.raw.push(l);
sections.push(last);
block = false;
originDir = dir;
}
if (block && last) {
var asset = l.match(/(href|src)=["']([^'"]+)["']/);
if (asset && asset[2]) {
last.src.push(path.join(originDir, asset[2]));
// RequireJS uses a data-main attribute on the script tag to tell it
// to load up the main entry point of the amp app
//
// If we find one, we must record the name of the main entry point,
// as well the name of the destination file, and treat
// the furnished requirejs as an asset (src)
var main = l.match(/data-main=['"]([^'"]+)['"]/);
if (main) {
last.requirejs = last.requirejs || {};
last.requirejs.dest = last.dest;
last.requirejs.baseUrl = path.join(originDir, path.dirname(main[1]));
last.requirejs.name = path.basename(main[1]);
last.requirejs.src = last.src.pop();
var req = l.match(/src=['"]([^'"]+)['"]/);
if (req) {
last.requirejs.origScript = req[1];
} else {
// For whatever reason ...
last.requirejs.origScript = 'scripts/vendor/require.js';
}
last.requirejs.srcDest = path.join(dest, last.requirejs.origScript);
last.src.push(last.dest);
}
// preserve media attribute
var media = l.match(/media=['"]([^'"]+)['"]/);
if (media) {
last.media = media[1];
}
// preserve defer attribute
var defer = /defer/.test(l);
if (defer && last.defer === false || last.defer && !defer) {
throw 'Error: You are not suppose to mix deferred and non-deferred scripts in one block.';
} else if (defer) {
last.defer = true;
} else {
last.defer = false;
}
}
last.raw.push(l);
}
});
return sections;
};
//
// HTMLProcessor takes care, and processes HTML files.
// It is given:
// - A base directory, which is the directory under which to look at references files
// - A destination directory, which is the directory under which will be generated the files
// - A file content to be processed
// - a file replacement locator
// - an optional log callback that will be called as soon as there's something to log
//
var HTMLProcessor = module.exports = function (src, dest, content, revvedfinder, logcb) {
// FIXME: Check consistency of the file object
this.src = src;
this.dest = dest || src;
this.content = content;
this.relativeSrc = path.relative(process.cwd(), src);
this.revvedfinder = revvedfinder;
this.linefeed = /\r\n/g.test(this.content) ? '\r\n' : '\n';
this.blocks = getBlocks(this.dest, this.relativeSrc, this.content);
this.logcb = logcb || function () {};
};
//
// Calls the log callback function
//
HTMLProcessor.prototype.log = function log(msg) {
this.logcb(msg);
};
//
// Return the string that will replace the furnished block
//
HTMLProcessor.prototype.replaceWith = function replaceWith(block) {
var result;
var backslash = /\\/g;
// Determine the relative path from the destination to the source
// file
var dest = path.relative(this.relativeSrc, block.dest);
if (block.startFromRoot) {
dest = '/' + dest;
}
// fix windows style paths. Dirty but works.
dest = dest.replace(backslash, '/');
if (block.type === 'css') {
var media = block.media ? ' media="' + block.media + '"' : '';
result = block.indent + '<link rel="stylesheet" href="' + dest + '"' + media + '>';
} else if (block.requirejs !== undefined) {
var dataMain = path.relative(this.relativeSrc, block.requirejs.dest);
dataMain = dataMain.replace(backslash, '/');
var requireSrc = path.relative(this.relativeSrc, block.requirejs.srcDest);
requireSrc = requireSrc.replace(backslash, '/');
if (block.startFromRoot) {
dataMain = '/' + dataMain;
requireSrc = '/' + requireSrc;
}
result = block.indent + '<script data-main="' + dataMain + '" src="' + requireSrc + '"><\/script>';
} else if (block.defer) {
result = block.indent + '<script defer src="' + dest + '"><\/script>';
} else if (block.type === 'js') {
result = block.indent + '<script src="' + dest + '"><\/script>';
} else {
result = '';
}
var conditionalEnd = /<!\[endif\]-->/;
// Test conditional comment regex here: http://refiddle.com/gqz
var conditionalBegin = /<!--\[if\ (gt|lt|gte|lte)?\ ?\!?IE\ ?[0-9\.]*\]>/;
var isConditionalBlock = block.raw.length >= 5 && conditionalEnd.test(block.raw[block.raw.length - 2]) && conditionalBegin.test(block.raw[1]);
if (!!result && isConditionalBlock) {
result = block.indent + block.raw[1].trim() + '\n' + result + '\n' + block.indent + block.raw[block.raw.length - 2].trim();
}
return result;
};
//
// Replace blocks by their target
//
HTMLProcessor.prototype.replaceBlocks = function replaceBlocks() {
var result = this.content;
this.blocks.forEach(function (block) {
var blockLine = block.raw.join(this.linefeed);
result = result.replace(blockLine, this.replaceWith(block));
}, this);
return result;
};
//
// Replace reference to scripts, css, images, .. in +lines+ with their revved version
// If +lines+ is not furnished used instead the cached version (i.e. stored at constructor time)
//
HTMLProcessor.prototype.replaceWithRevved = function replaceWithRevved(lines) {
// Replace script sources
var self = this;
var content = lines || this.content;
var regexps = [
/*jshint regexp:false */
[/<script.+src=['"]([^"']+)["']/gm,
'Update the HTML to reference our concat/min/revved script files'
],
[/<link[^\>]+href=['"]([^"']+)["']/gm,
'Update the HTML with the new css filenames'
],
[/<img[^\>]+src=['"]([^"']+)["']/gm,
'Update the HTML with the new img filenames'
],
[/data-main\s*=['"]([^"']+)['"]/gm,
'Update the HTML with data-main tags',
function (m) { return m.match(/\.js$/) ? m : m + '.js'; },
function (m) { return m.replace('.js', ''); }
],
[/data-[A-Za-z0-9]*=['"]([^"']+)["']/gm,
'Update the HTML with the data tags'
],
[/url\(\s*['"]([^"']+)["']\s*\)/gm,
'Update the HTML with background imgs, case there is some inline style'
],
[/<a[^\>]+href=['"]([^"']+)["']/gm,
'Update the HTML with anchors images'
],
[/<input[^\>]+src=['"]([^"']+)["']/gm,
'Update the HTML with reference in input'
]
];
var identity = function (m) { return m; };
// Replace reference to script with the actual name of the revved script
regexps.forEach(function (rxl) {
var filterIn = rxl[2] || identity;
var filterOut = rxl[3] || identity;
self.log(rxl[1]);
content = content.replace(rxl[0], function (match, src) {
// Consider reference from site root
var srcfile = filterIn(src);
var file = self.revvedfinder.find(srcfile, self.src);
var res = match.replace(src, filterOut(file));
if (srcfile !== file) {
self.log(match + ' changed to ' + res);
}
return res;
});
});
return content;
};
// Process the HTML file, which is:
// - replace any blocks by its "target"
// - replace files (images, scripts, css) references by their revved version
//
HTMLProcessor.prototype.process = function process() {
return this.replaceWithRevved(this.replaceBlocks());
};