UNPKG

toloframework

Version:

Javascript/HTML/CSS compiler for Firefox OS or nodewebkit apps using modules in the nodejs style.

545 lines (521 loc) 17.2 kB
/** * @module compile-html */ var FS = require("fs"); var Path = require("path"); var Source = require("./source"); var Tree = require("./htmltree"); var Util = require("./util"); var CompilerJS = require("./compiler-js"); var CompilerCSS = require("./compiler-css"); var Template = require("./template"); /** * Compile an HTML file if it is not uptodate. * @param {Project} prj Project object. * @param {string} filename name of the HTML file without the full path. * @see project~Project */ module.exports.compile = function(prj, filename) { var source = new Source(prj, filename); if (!isHtmlFileUptodate(source)) { console.log("Compiling " + filename.yellow); var root = Tree.parse(source.read()); source.tag("resources", []); source.tag("dependencies", []); lookForStaticJavascriptAndStyle(root, source); expandWidgets(root, source); initControllers(root, source); zipInnerScriptsAndStyles(root, source); cleanupTreeAndStoreItInTag(root, source); var resources = new Util.Resources(source.tag("resources")); source.tag("resources", resources.data()); } compileDependantScripts(root, source); compileDependantStyles(root, source); source.tag("dependencies", Util.removeDoubles(source.tag("dependencies"))); source.tag("innerMapCSS", null); source.save(); return source; }; /** * To be uptodate, an HTML page must be more recent that all its dependencies. */ function isHtmlFileUptodate(source) { var dependencies = source.tag("dependencies") || []; var i, dep, file, prj = source.prj(), stat, mtime = source.modificationTime(); for (i = 0 ; i < dependencies.length ; i++) { dep = dependencies[i]; file = prj.srcOrLibPath(dep); if (file) { stat = FS.statSync(file); if (stat.mtime > mtime) return false; } } return source.isUptodate(); } /** * Remove all `extra` properties in the tree and store it in tag __`tree`__. */ function cleanupTreeAndStoreItInTag(root, source) { Tree.walk( root, function(node) { delete node.extra; if (node.children && node.children.length == 0) { delete node.children; } if (node.attribs) { var count = 0, key; for (key in node.attribs) { count++; } if (count == 0) { delete node.attribs; } } } ); source.tag("tree", root); } /** * Work on following tags: * ``` * <script src="foo.js"></script> * <script>Hello world!</script> * <link href="mystyle.css" rel="stylesheet" type="text/css" /> * <style>body {background: orange;}</style> * ``` */ function lookForStaticJavascriptAndStyle(root, source) { var innerJS = ""; var innerCSS = ""; var outerJS = []; var outerCSS = []; source.tag("outerJS", outerJS); source.tag("outerCSS", outerCSS); Tree.walk( root, null, function(node, parent) { if (node.type !== Tree.TAG) return; var src; switch (node.name) { case "script": src = Tree.att(node, "src"); if (typeof src === 'string' && src.length > 0) { // outer script. if (src.substr(0, 5) != "http:" && src.substr(0, 6) != "https:") { outerJS.push(src); Tree.removeChild(parent, node); } else { console.log("Remote Javascript dependence: " + src.magenta); } } else { // Inner script. innerJS += Tree.text(node).trim() + "\n"; Tree.removeChild(parent, node); } break; case "style": innerCSS += Tree.text(root); Tree.removeChild(parent, node); break; case "link": if (Tree.att("rel") && Tree.att("rel").toLowerCase() == "stylesheet") { src = Tree.att("href").trim(); if (typeof src === 'string' && src.length > 0) { outerCSS.push(src); } } break; } } ); source.tag("innerJS", innerJS); source.tag("innerCSS", innerCSS); } /** * All elements with the namespace `w:` are widgets. If the HTML file * contains, for instance, the widgets `<w:foo>` and `<w:bar>`, we put * `["wdg/foo/compile-foo.js", "wdg/bar/compile-bar.js"]` into the tag * __`dependencies`__. * * Then we eventually add the content of `root.extra.css` to the tag __`innerCSS`__. */ function expandWidgets(root, source) { var prj = source.prj(); var availableWidgets = prj.getAvailableWidgetCompilers(); Tree.walk( root, null, // Top-down. function (node, parent) { if (node.type === Tree.TAG) { if (node.name.substr(0, 2) == 'w:') { var widgetName = node.name.substr(2).toLowerCase(); var widget = availableWidgets[widgetName]; if (!widget) { prj.fatal( "Unknown widget: \"" + widgetName + "\"!", prj.ERR_WIDGET_NOT_FOUND ); } precompileWidget(node, source, widget); } } } ); Tree.normalizeChildren(root, true); Tree.walk( root, // Bottom-up. function (node, parent) { if (node.type === Tree.TAG) { if (node.name.substr(0, 2) == 'w:') { var widgetName = node.name.substr(2).toLowerCase(); var widget = availableWidgets[widgetName]; if (!widget) { prj.fatal( "Unknown widget: \"" + widgetName + "\"!", prj.ERR_WIDGET_NOT_FOUND ); } compileWidget(node, source, widget); } } } ); } /** * Compile the widget. */ function precompileWidget(root, source, widget) { try { genericCompileWidget(root, source, widget, "precompile"); } catch (ex) { console.log( ( "Fatal error during widget's precompilation:\n" + Path.join( widget.path, "/compile-" + widget.name + ".js" ) ).err() ); throw ex; } } /** * Compile the widget. */ function compileWidget(root, source, widget) { try { genericCompileWidget(root, source, widget, "compile"); } catch (ex) { console.log( ( "Fatal error during widget's compilation:\n" + Path.join( widget.path, "/compile-" + widget.name + ".js" ) ).err() ); throw ex; } } function genericCompileWidget(root, source, widget, functionName) { if (typeof root.name !== 'string') return; if (root.name.substr(0, 2) != "w:") return; var prj = source.prj(); var dependencies = source.tag("dependencies"); var resources = source.tag("resources"); var outerJS = source.tag("outerJS"); var innerCSS = source.tag("innerCSS"); var innerMapCSS = source.tag("innerMapCSS") || {}; if (functionName === 'compile') { // We change the tag name only on Bottom-Up traversal. root.name = "div"; } Tree.addClass(root, "custom wtag-" + widget.name); root.extra = {dependencies: [], resources: []}; var id = Tree.att(root, "id") || Tree.nextId(); Tree.att(root, "id", id); root.extra.init = {id: id}; var controller = "wtag." + widget.name.substr(0, 1).toUpperCase() + widget.name.substr(1).toLowerCase(); if (prj.srcOrLibPath("mod/" + controller + ".js")) { root.extra.controller = controller; outerJS.push("mod/" + controller + ".js"); outerJS.push("require.js"); } // Looking for extra CSS. FS.readdirSync(widget.path).forEach( function(filename) { if (Path.extname(filename) !== '.css') return; if (filename.substr(0, widget.name.length + 1) !== widget.name + "-") return; var file = Path.resolve(Path.join(widget.path, filename)); if (file in innerMapCSS) return; innerMapCSS[file] = 1; var content = FS.readFileSync(file).toString(); console.log("inner CSS: " + filename.yellow); innerCSS += Util.lessCSS(file, content, false); dependencies.push("wdg/" + widget.name + "/" + filename); } ); // Compilation. var compiler = widget.compiler; if (compiler && typeof compiler[functionName] === 'function') { // There is a transformer: we will call it. try { compiler[functionName].call(prj, root); dependencies.push("wdg/" + widget.name + "/compile-" + widget.name + ".js"); root.extra.resources.forEach( function(itm) { resources.push(itm); } ); root.extra.dependencies.forEach( function(itm) { dependencies.push(itm); } ); if (typeof root.extra.innerCSS === 'string') { if (innerCSS.indexOf(root.extra.innerCSS) < 0) { innerCSS += root.extra.innerCSS; } } if (typeof root.extra.css === 'string') { if (innerCSS.indexOf(root.extra.css) < 0) { innerCSS += root.extra.css; } } } catch (ex) { if (ex.fatal) { throw ex; } else { prj.fatal( "" + ex, prj.ERR_WIDGET_TRANSFORMER, widget.path ); } } } source.tag("innerCSS", innerCSS); source.tag("innerMapCSS", innerMapCSS); if (functionName == 'compile') { // For Bottom-Up traversal, we have to check if another pass is needed or not. var deepness = 64; var nextNodeToCompile; var currentWidgetName; var currentWidget; var availableWidgets = prj.getAvailableWidgetCompilers(); while (null != (nextNodeToCompile = needsWidgetCompilation(root))) { expandWidgets(nextNodeToCompile, source); deepness--; if (deepness < 1) { prj.fatal( "Too much recursions for widget \"" + widget.name + "\"!", prj.ERR_WIDGET_TOO_DEEP, widget.path ); } } } } /** * @return the first node with the namespace `w:`, or null. */ function needsWidgetCompilation(root) { if (root.type == Tree.TAG && root.name && root.name.substr(0, 2) == 'w:') { return root; } if (!root.children) return null; var i, node, result; for (i = 0 ; i < root.children.length ; i++) { node = root.children[i]; result = needsWidgetCompilation(node); if (result) return result; } return null; } /** * Initialize controllers by adding script in the __`innerJS`__ tag. */ function initControllers(root, source) { var innerJS = source.tag("innerJS") + "function addListener(e,l){" + "if(window.addEventListener){\n" + "window.addEventListener(e,l,false)}else{\n" + "window.attachEvent('on' + e, l)}}" + "\naddListener('DOMContentLoaded',\n" + " function() {\n" + " document.body.parentNode.$data = {};\n" + " // Attach controllers.\n"; var outerJS = source.tag("outerJS") || []; var extraCSS = ""; var app = null; // Add core JS for application config and internationalisation. outerJS.push("mod/$.js"); Tree.walk( root, function(node) { if (node.name == "body" && node.attribs && node.attribs.app) { app = node.attribs.app; outerJS.push("mod/" + app + ".js"); outerJS.push("require.js"); delete node.attribs.app; } var item = node.extra; if (!item) return; if (item.controller) { var ctrlFilename = "mod/" + item.controller + ".js"; innerJS += " require('" + item.controller.toLowerCase() + "')("; if (typeof item.init === 'object') { innerJS += "{"; var sep = '', key, val; for (key in item.init) { val = item.init[key]; if (typeof val === 'string' && val.substr(0, 9) == 'function(') { // This is a function: we don't want to stringify it. } else { val = JSON.stringify(val); } innerJS += sep; sep = ', '; innerJS += key + ": " + val; } innerJS += "}"; } innerJS += ");\n"; } if (item.css) { extraCSS += item.css; } } ); if (app) { innerJS += " window.APP = require('" + app + "')\n" + " if (typeof APP.start === 'function') " + "setTimeout(APP.start);"; } innerJS += " }\n);\n"; source.tag("innerJS", innerJS); source.tag("outerJS", outerJS); } /** * Look for all TEXT nodes and eventually replace double curlies. */ function expandDoubleCurlies(root, source) { Tree.walk( root, null, // Top-Down walk for databindings. function(node) { if (node.type !== Tree.TEXT) return; if (typeof node.text !== 'string') return; var text = node.text; if (text.trim().length == 0) return; expandDoubleCurliesInTextNode(node); } ); } /** * Write tags __`zipJS`__ and __`zipCSS`__. */ function zipInnerScriptsAndStyles(root, source) { source.tag("zipJS", Util.zipJS(source.tag("innerJS"))); source.tag("outerJS", Util.removeDoubles(source.tag("outerJS"))); source.tag("outerCSS", Util.removeDoubles(source.tag("outerCSS"))); } /** * Compile dependant JS scripts in cascade. */ function compileDependantScripts(root, source) { var needed = {}; var jobs = []; var prj = source.prj(); var file; source.tag("outerJS").forEach( function(file) { jobs.push(file); } ); while (jobs.length > 0) { file = jobs.pop(); if (file in needed) continue; var srcJS = source.create(file); try { CompilerJS.compile(srcJS); needed[file] = srcJS; srcJS.tag("needs").forEach( function(item) { jobs.push(item); } ); } catch (ex) { if (ex.fatal) { throw ex; } else { throw ex; prj.fatal(ex, -1, file); } } } // Save __`linkJS`__ tag. var linkJS = [], key; for (key in needed) { linkJS.push(key); } source.tag("linkJS", Util.removeDoubles(linkJS)); } /** * Compile dependant CSS styles in cascade. */ function compileDependantStyles(root, source) { var needed = {}; var jobs = []; var prj = source.prj(); var file; // Add CSS from JS classes. source.tag("linkJS").forEach( function(item) { var srcJS = source.create(item); var css = srcJS.tag("css"); if (css) { jobs.push(css); } } ); // Add source.tag("outerCSS").forEach( function(file) { jobs.push(file); } ); while (jobs.length > 0) { file = jobs.pop(); if (file in needed) continue; var srcCSS = source.create(file); try { CompilerCSS.compile(srcCSS); needed[file] = srcCSS; } catch (ex) { prj.fatal(ex, -1, file); } } // Save __`linkCSS`__ tag. var linkCSS = [], key; for (key in needed) { linkCSS.push(key); } source.tag("linkCSS", Util.removeDoubles(linkCSS)); }