From 858b9a3fa931bbfbbe8aa3d600b90b5c8315011b Mon Sep 17 00:00:00 2001 From: Marc Laval Date: Fri, 7 Mar 2014 17:51:59 +0100 Subject: [PATCH] documents and reorganizes the compiler code - part 2 Closes #97 --- hsp/compiler/compiler.js | 12 +- hsp/compiler/jsgenerator/README.md | 51 +- hsp/compiler/jsgenerator/index.js | 89 +- .../jsgenerator/jsvalidator/validator.js | 40 +- hsp/compiler/jsgenerator/processors.js | 477 +++++----- hsp/compiler/jsgenerator/templateWalker.js | 72 +- hsp/compiler/jsgenerator/treeWalker.js | 7 +- hsp/compiler/parser/README.md | 264 +++++- hsp/compiler/parser/index.js | 7 +- hsp/compiler/renderer.js | 16 +- hsp/compiler/treebuilder/README.md | 67 +- hsp/compiler/treebuilder/hExpression.js | 139 +-- hsp/compiler/treebuilder/htmlEntities.js | 538 +++++------ hsp/compiler/treebuilder/index.js | 23 +- hsp/compiler/treebuilder/syntaxTree.js | 871 ++++++++++-------- 15 files changed, 1663 insertions(+), 1010 deletions(-) diff --git a/hsp/compiler/compiler.js b/hsp/compiler/compiler.js index 244f581..fc13955 100644 --- a/hsp/compiler/compiler.js +++ b/hsp/compiler/compiler.js @@ -3,12 +3,12 @@ var treebuilder = require("./treebuilder/index"); var jsgenerator = require("./jsgenerator/index"); /** - * Compile a template and return a JS compiled string and a list of errors - * @param template {String} the template file content as a string - * @param fileName {String} the name of the file being compiled (optional - used for error messages) - * @param includeSyntaxTree {Boolean} if true, the result object will contain the syntax tree generated by the compiler - * @param bypassJSvalidation {Boolean} if true, the validation of the generated JS file (including non-template code) is - * bypassed - default:false + * Compiles a template and return a JS compiled string and a list of errors. + * @param {String} template the template file content as a string. + * @param {String} dirPath the directory path. + * @param {String} fileName the name of the file being compiled (optional - used for error messages). + * @param {Boolean} includeSyntaxTree if true, the result object will contain the syntax tree generated by the compiler. + * @param {Boolean} bypassJSvalidation if true, the validation of the generated JS file (including non-template code) is bypassed - default:false. * @return {JSON} a JSON structure with the following properties: * errors: {Array} the error list - each error having the following structure: * description: {String} - a message describing the error diff --git a/hsp/compiler/jsgenerator/README.md b/hsp/compiler/jsgenerator/README.md index 45c8620..b848665 100644 --- a/hsp/compiler/jsgenerator/README.md +++ b/hsp/compiler/jsgenerator/README.md @@ -1,3 +1,52 @@ # JS Generator # -TODO \ No newline at end of file +The **jsgenerator** is the third and last step of the compilation process. +It generates the final compiled template as a javascript string, using the syntax tree built previously. + +## Input ## +The syntax tree, e.g.: +``` +{ + "type": "template", + "name": "test", + "args": ["person"], + "content": [ + {type:"element", name:"div", closed:false, attributes:[ + {name:"title", type:"text", value:"Some text"}, + {name:"id", type:"expression", "category": "objectref", "bound": true, "path": [ "person", "id" ]}, + {name:"class", type:"textblock", content:[ + {type:"expression", "category": "objectref", "bound": true, "path": [ "person", "gender" ]}, + {type:"text", value:" "}, + {type:"expression", "category": "objectref", "bound": true, "path": [ "person", "category" ]} + ]} + ],"content": [ + { "type": "textblock", "content": [ + {"type": "text","value": "Hello "}, + {"type": "expression", "category": "objectref", "bound": true, "path": [ "person", "name" ]}, + {"type": "text","value": "! "} + ]} + ]} + ] +} +``` + +## Output ## +The compiled template, e.g.: +``` +n.elt( "div", + {e1:[1,2,"person","id"],e2:[1,2,"person","gender"],e3:[1,2,"person","category"]} , + {"title":"Some text","id":["",1],"class":["",2," ",3]}, + 0,[ + n.$text({e1:[1,2,"person","name"]},["Hello ",1,"! "]) + ] +) +``` + +## Algorithm ## + +The first part of this step is the actual generation. +It is done thanks to the TemplateWalker class (which extend TreeWalker). +It walks the SyntaxTree built previously and manages each node with a processor, depending on its type. +Each processor generates a javascript string for the node and its children. + +Once the generation is done, the resulting javascript string is validated with the *acorn* parser. \ No newline at end of file diff --git a/hsp/compiler/jsgenerator/index.js b/hsp/compiler/jsgenerator/index.js index 48c4e74..2890e70 100644 --- a/hsp/compiler/jsgenerator/index.js +++ b/hsp/compiler/jsgenerator/index.js @@ -16,7 +16,23 @@ var HEADER = module.exports.HEADER = HEADER_ARR.join('\r\n'); var HEADER_SZ = HEADER_ARR.length; /** - * TODO + * Generates the JS compiled string and a list of errors. + * @param {Object} res the result of the previous steps of the compilation. + * @param {String} template the template file content as a string. + * @param {String} dirPath the directory path. + * @param {String} fileName the name of the file being compiled (optional - used for error messages). + * @param {Boolean} includeSyntaxTree if true, the result object will contain the syntax tree generated by the compiler. + * @param {Boolean} bypassJSvalidation if true, the validation of the generated JS file (including non-template code) is bypassed - default:false. + * @return {JSON} a JSON structure with the following properties: + * errors: {Array} the error list - each error having the following structure: + * description: {String} - a message describing the error + * line: {Number} - the error line number + * column: {Number} - the error column number + * code: {String} - a code extract showing where the error occurs (optional) + * code: {String} the generated JavaScript code + * syntaxTree: {JSON} the syntax tree generated by the parser (optional - cf. parameters) + * lineMap: {Array} array of the new line indexes: lineMap[3] returns the new line index for line 4 in + * the orginal file (lineMap[0] is always 0 as all line count starts at 1 for both input and output values) */ exports.generate = function(res, template, fileName, dirPath, includeSyntaxTree, bypassJSvalidation) { res.code = ''; @@ -68,41 +84,46 @@ exports.generate = function(res, template, fileName, dirPath, includeSyntaxTree, }; /** - * TODO + * Validates a javascript string using the jsvalidator module, and generates an error report if not valid. + * @param {String} code the javascript string. + * @param {Object} lineMap the line mapping between the source template and the compiled one + * @return {Object} a result map */ function _validate (code, lineMap) { - var r = jsv.validate(code); - var result = {isValid: r.isValid}; - if (!r.isValid) { + var validationResult = jsv.validate(code); + var result = {isValid: validationResult.isValid}; + if (!validationResult.isValid) { // translate error line numbers - var err, ln, lm = lineMap; - for (var i = 0, sz = r.errors.length; sz > i; i++) { - err = r.errors[i]; - ln = err.line; - - err.line = -1; // to avoid sending a wrong line in case of pb - for (var j = 0, sz2 = lm.length; sz2 > j; j++) { - if (lm[j] === ln) { - err.line = j; // original line nbr + var error, lineNumber; + for (var i = 0; i < validationResult.errors.length; i++) { + error = validationResult.errors[i]; + lineNumber = error.line; + + error.line = -1; // to avoid sending a wrong line in case of pb + for (var j = 0; j < lineMap.length; j++) { + if (lineMap[j] === lineNumber) { + error.line = j; // original line nbr break; } } } - result.errors = r.errors; + result.errors = validationResult.errors; } return result; } /** - * Generate an error script to include in the template compiled script in order to show errors in the browser when the - * script is loaded + * Generate an error script to include in the template compiled script in order to show errors in the browser when the script is loaded + * @param {Array} errors the errror list + * @param {String} fileName the name of the file being compiled + * @return {String} the javascript snippet to be included */ function _getErrorScript (errors, fileName) { - var r = ''; + var result = ''; if (errors && errors.length) { - r = ['\r\nrequire("hsp/rt").logErrors("', fileName, '",', JSON.stringify(errors, null), ');\r\n'].join(""); + result = ['\r\nrequire("hsp/rt").logErrors("', fileName, '",', JSON.stringify(errors, null), ');\r\n'].join(""); } - return r; + return result; } /** @@ -114,29 +135,29 @@ function _generateLineMap (res, file) { if (res.errors && res.errors.length) { return; } - var st = res.syntaxTree, templates = []; + var syntaxTree = res.syntaxTree, templates = []; // identify the templates in the syntax tree - for (var i = 0; st.length > i; i++) { - if (st[i].type === 'template') { - templates.push(st[i]); + for (var i = 0; i < syntaxTree.length; i++) { + if (syntaxTree[i].type === 'template') { + templates.push(syntaxTree[i]); } } var nbrOfLinesInCompiledTemplate = 5; - var lm = [], sz = file.split(/\n/g).length + 1, pos = HEADER_SZ, tpl; + var lineMap = [], pos = HEADER_SZ, template; var pos1 = -1; // position of the next template start var pos2 = -1; // position of the next template end var tplIdx = -1; // position of the current template - for (var i = 0; sz > i; i++) { + for (var i = 0; i < (file.split(/\n/g).length + 1); i++) { if (i === 0 || i === pos2) { // end of current template: let's determine next pos1 and pos2 tplIdx = (i === 0) ? 0 : tplIdx + 1; if (tplIdx < templates.length) { // there is another template - tpl = templates[tplIdx]; - pos1 = tpl.startLine; - pos2 = tpl.endLine; + template = templates[tplIdx]; + pos1 = template.startLine; + pos2 = template.endLine; if (pos2 < pos1) { // this case should never arrive.. pos2 = pos1; @@ -146,23 +167,23 @@ function _generateLineMap (res, file) { tplIdx = pos1 = pos2 = -1; } if (i === 0) { - lm[0] = 0; + lineMap[0] = 0; } i++; } if (i === pos1) { - for (var j = pos1, max = pos2 + 1; max > j; j++) { + for (var j = pos1; j < (pos2 + 1); j++) { // all lines are set to the template start - lm[i] = pos; + lineMap[i] = pos; i++; } pos += nbrOfLinesInCompiledTemplate; i -= 2; // to enter the i===pos2 clause at next step } else { - lm[i] = pos; + lineMap[i] = pos; pos++; } } - return lm; + return lineMap; } \ No newline at end of file diff --git a/hsp/compiler/jsgenerator/jsvalidator/validator.js b/hsp/compiler/jsgenerator/jsvalidator/validator.js index 2de10cf..f6465aa 100644 --- a/hsp/compiler/jsgenerator/jsvalidator/validator.js +++ b/hsp/compiler/jsgenerator/jsvalidator/validator.js @@ -1,11 +1,12 @@ var acorn = require("acorn/acorn"); /** - * Validate a JavaScript string Return a JSON structure with 'valid' and 'errors' properties e.g. {valid:false, - * errors:[{msg:'...',lineInfoTxt:'...',lineInfoHTML:'...',loc:{line:2,column:30}'}]} + * Validates a JavaScript string + * @param {String} input the Javascript string + * @return {Object} JSON structure with 'valid' and 'errors' properties e.g. {valid:false, errors:[{msg:'...',lineInfoTxt:'...',lineInfoHTML:'...',loc:{line:2,column:30}'}]} */ module.exports.validate = function (input) { - var res = { + var result = { isValid : true }; @@ -17,45 +18,48 @@ module.exports.validate = function (input) { forbidReserved : true }); } catch (ex) { - res.isValid = false; - res.errors = [formatError(ex, input)]; + result.isValid = false; + result.errors = [formatError(ex, input)]; } - return res; + return result; }; /** - * Format the error as an error structure with line extract information + * Formats the error as an error structure with line extract information. + * @param {Object} error the exception. + * @param {String} input the Javascript string. + * @return {Object} the structured error. */ -function formatError (err, input) { - var msg = err.toString().replace(/\s*\(\d*\:\d*\)\s*$/i, ''); // remove line number / col number +function formatError (error, input) { + var message = error.toString().replace(/\s*\(\d*\:\d*\)\s*$/i, ''); // remove line number / col number - var bm = ('' + input.slice(0, err.pos)).match(/.*$/i); - var am = ('' + input.slice(err.pos)).match(/.*/i); - var before = bm ? bm[0] : ''; - var after = am ? am[0] : ''; + var beforeMatch = ('' + input.slice(0, error.pos)).match(/.*$/i); + var afterMatch = ('' + input.slice(error.pos)).match(/.*/i); + var before = beforeMatch ? beforeMatch[0] : ''; + var after = afterMatch ? afterMatch[0] : ''; // Prepare line info for txt display var cursorPos = before.length; var errChar = (after.length) ? after.slice(0, 1) : 'X'; var lineStr = before + after; var lncursor = []; - for (var i = 0, sz = lineStr.length; sz > i; i++) { + for (var i = 0; i < lineStr.length; i++) { lncursor[i] = (i === cursorPos) ? '^' : '-'; } var lineInfoTxt = lineStr + '\r\n' + lncursor.join(''); // Prepare line info for HTML display - var lineInfoHTML = ['', before, '', errChar, '', + var lineInfoHTML = ['', before, '', errChar, '', after.slice(1), ''].join(''); return { - description : msg, + description : message, lineInfoTxt : lineInfoTxt, lineInfoHTML : lineInfoHTML, code : lineStr, - line : err.loc ? err.loc.line : -1, - column : err.loc ? err.loc.column : -1 + line : error.loc ? error.loc.line : -1, + column : error.loc ? error.loc.column : -1 }; } diff --git a/hsp/compiler/jsgenerator/processors.js b/hsp/compiler/jsgenerator/processors.js index 496d1a6..41f4486 100644 --- a/hsp/compiler/jsgenerator/processors.js +++ b/hsp/compiler/jsgenerator/processors.js @@ -1,9 +1,17 @@ +/** + * Escapes new lines characters in a string. + * @param {String} text the input string. + * @return {String} the excaped strin. + */ function escapeNewLines (text) { return text.replace(/\r\n?|[\n\u2028\u2029]/g, "\n").replace(/\n/g, "\\n"); } /** - * Text outside a template, just return what we've got + * Text outside a template, just return what we've got. + * @param {Node} node the current Node object as built by the treebuilder. + * @param {TreeWalker} walker the template walker instance. + * @return {String} a snippet of Javascript code built from the node. */ exports["plaintext"] = function (node, walker) { return node.value; @@ -13,57 +21,64 @@ exports["plaintext"] = function (node, walker) { * Template definition, this is the root of the tree, return a self calling function that recursively applies * - walker.walk on its content array * - walker.each on its arguments definition, used for simple serialization + * @param {Node} node the current Node object as built by the treebuilder. + * @param {TreeWalker} walker the template walker instance. + * @return {String} a snippet of Javascript code built from the node. */ exports["template"] = function (node, walker) { - var tplName = node.name; + var templateName = node.name; var CRLF = '\r\n'; - // add template arguments to the scope + //Adds template arguments to the scope if (node.args) { - for (var i = 0, sz = node.args.length; sz > i; i++) { + for (var i = 0; i < node.args.length; i++) { walker.addScopeVariable(node.args[i]); } } else if (node.controller) { walker.addScopeVariable(node.controller.ref); // the controller reference - e.g. "c" } - var tplCode = ["[", walker.walk(node.content, module.exports).join(","), "]"].join(""); + //Generates the code of the template's content + var templateCode = ["[", walker.walk(node.content, module.exports).join(","), "]"].join(""); var globals = walker._globals; - // generate globals validation statement - e.g. var _c;try {_c=c} catch(e) {}; - var gs = [], gsz = globals.length; - if (gsz) { - gs = [" var _"+globals.join(',_')+";"]; - for (var i=0;gsz>i;i++) { - gs.push( "try {_" + globals[i] + "=" + globals[i] + "} catch(e) {};"); + //Generates globals validation statement - e.g. var _c;try {_c=c} catch(e) {}; + var globalsStatement = [], globalsLength = globals.length; + if (globalsLength) { + globalsStatement = [" var _" + globals.join(',_') + ";"]; + for (var i=0; i < globalsLength; i++) { + globalsStatement.push( "try {_" + globals[i] + "=" + globals[i] + "} catch(e) {};"); } - gs.push(CRLF); + globalsStatement.push(CRLF); } - var gss=gs.join(""); + var globalsStatementString = globalsStatement.join(""); - // reset template scope and global list + //Resets template scope and global list walker.resetScope(); walker.resetGlobalRefs(); - walker.templates[tplName] = tplCode; + walker.templates[templateName] = templateCode; - var exp = ''; + var exportString = ''; if (node.export === true) { - exp = ' =exports.' + tplName; + exportString = ' =exports.' + templateName; } if (node.controller) { - var p = node.controller.path; - return ['var ', tplName, exp, ' = require("hsp/rt").template({ctl:[', p[0], ',', walker.each(p, argAsString), - '],ref:"', node.controller.ref, '"}, function(n){', CRLF, gss, ' return ', tplCode, ';', CRLF, '});', CRLF].join(""); + var path = node.controller.path; + return ['var ', templateName, exportString, ' = require("hsp/rt").template({ctl:[', path[0], ',', walker.each(path, argAsString), + '],ref:"', node.controller.ref, '"}, function(n){', CRLF, globalsStatementString, ' return ', templateCode, ';', CRLF, '});', CRLF].join(""); } else { - return ['var ', tplName, exp, ' = require("hsp/rt").template([', walker.each(node.args, argAsString), - '], function(n){', CRLF, gss, ' return ', tplCode, ';', CRLF, '});', CRLF].join(""); + return ['var ', templateName, exportString, ' = require("hsp/rt").template([', walker.each(node.args, argAsString), + '], function(n){', CRLF, globalsStatementString, ' return ', templateCode, ';', CRLF, '});', CRLF].join(""); } }; /** - * Generate a text node + * Generates a text node. + * @param {Node} node the current Node object as built by the treebuilder. + * @param {TreeWalker} walker the template walker instance. + * @return {String} a snippet of Javascript code built from the node. */ exports["text"] = function (node, walker) { if (node.value === undefined) { @@ -75,8 +90,10 @@ exports["text"] = function (node, walker) { }; /** - * For a given value double it's definition returning "value",value. This method should only be called on object - * literals (strings) + * For a given value double it's definition returning "value",value. + * This method should only be called on object literals (strings). + * @param {String} value the initial value. + * @return {String} the doubled value. */ function argAsString (value) { // No need to toString because it's already a string @@ -84,366 +101,416 @@ function argAsString (value) { } /** - * Generate a text node + * Generate a textblock node. + * @param {Node} node the current Node object as built by the treebuilder. + * @param {TreeWalker} walker the template walker instance. + * @return {String} a snippet of Javascript code built from the node. */ exports["textblock"] = function (node, walker) { // we should generate sth like // n.$text({e1:[1,"person","firstName"],e2:[1,"person","lastName"]},["Hello ",1," ",2,"!"]) - var tb = formatTextBlock(node, 1, walker); - return ["n.$text(", tb.expArg, ",", tb.blockArgs, ")"].join(''); + var textBlock = formatTextBlock(node, 1, walker); + return ["n.$text(", textBlock.exprArg, ",", textBlock.blockArgs, ")"].join(''); }; /** - * Generate a log expression + * Generates a log expression. + * @param {Node} node the current Node object as built by the treebuilder. + * @param {TreeWalker} walker the template walker instance. + * @return {String} a snippet of Javascript code built from the node. */ exports["log"] = function (node, walker) { - var e, idx=1, code=[], indexes=[]; - for (var i=0,sz=node.exprs.length;sz>i;i++) { - e = formatExpression(node.exprs[i], idx, walker); - idx = e.nextIndex; - indexes.push(e.exprIdx); - code.push(e.code); + var expr, index = 1, code = [], indexes = []; + for (var i = 0; i < node.exprs.length; i++) { + expr = formatExpression(node.exprs[i], index, walker); + index = expr.nextIndex; + indexes.push(expr.exprIdx); + code.push(expr.code); } - return ["n.log({", code.join(",") , "},[", indexes.join(',') , "],'",walker.fileName,"','",walker.dirPath,"',",node.line,",",node.column,")"].join(''); + return ["n.log({", code.join(","), "},[", indexes.join(','), "],'", walker.fileName, "','", walker.dirPath, "',",node.line, ",", node.column, ")"].join(''); }; /** - * Generate a let expression + * Generates a let expression. + * @param {Node} node the current Node object as built by the treebuilder. + * @param {TreeWalker} walker the template walker instance. + * @return {String} a snippet of Javascript code built from the node. */ exports["let"] = function (node, walker) { - var e, idx=1, code=[], asn=[], varName; - for (var i=0,sz=node.assignments.length;sz>i;i++) { - e = formatExpression(node.assignments[i].value, idx, walker); - idx = e.nextIndex; - varName=node.assignments[i].identifier; + var expr, index = 1, code = [], assignment = [], varName; + for (var i = 0; i < node.assignments.length; i++) { + expr = formatExpression(node.assignments[i].value, index, walker); + index = expr.nextIndex; + varName = node.assignments[i].identifier; walker.addScopeVariable(varName); - asn.push("'"+varName+"'"); - asn.push(e.exprIdx); - code.push(e.code); + assignment.push("'" + varName + "'"); + assignment.push(expr.exprIdx); + code.push(expr.code); } - return ["n.let({", code.join(",") , "},[", asn.join(',') , "])"].join(''); + return ["n.let({", code.join(",") , "},[", assignment.join(',') , "])"].join(''); }; /** - * Generate an if node + * Generates an if node. + * @param {Node} node the current Node object as built by the treebuilder. + * @param {TreeWalker} walker the template walker instance. + * @return {String} a snippet of Javascript code built from the node. */ exports["if"] = function (node, walker) { // we should generate sth like // n.$if({e1:[1,"person","firstName"]}, 1, [n.$text({e1:[1,"person","firstName"]},["Hello ",1])], [..]) - var e = formatExpression(node.condition, 1, walker); + var expr = formatExpression(node.condition, 1, walker); - var c1 = ',[]', c2 = ''; + var content1 = ',[]', content2 = ''; if (node.content1) { - c1 = ',[' + walker.walk(node.content1, module.exports).join(",") + ']'; + content1 = ',[' + walker.walk(node.content1, module.exports).join(",") + ']'; } if (node.content2) { - c2 = ',[' + walker.walk(node.content2, module.exports).join(",") + ']'; + content2 = ',[' + walker.walk(node.content2, module.exports).join(",") + ']'; } - if (e.code !== '') { - e.code = "{" + e.code + "}"; + if (expr.code !== '') { + expr.code = "{" + expr.code + "}"; } - return ['n.$if(', e.code, ',', e.exprIdx, c1, c2, ')'].join(''); + return ['n.$if(', expr.code, ',', expr.exprIdx, content1, content2, ')'].join(''); }; /** - * Generate a foreach node + * Generates a foreach node. + * @param {Node} node the current Node object as built by the treebuilder. + * @param {TreeWalker} walker the template walker instance. + * @return {String} a snippet of Javascript code built from the node. */ exports["foreach"] = function (node, walker) { // we should generate sth like // n.$foreach( {e1: [1, 0, "things"]}, "thing", "thing_key", 1, [...]) - var e = formatExpression(node.collection, 1, walker); + var expr = formatExpression(node.collection, 1, walker); - var c = '[]'; + var content = '[]'; if (node.content) { // add all contextual variables walker.pushSubScope([node.item, node.key, node.item + "_isfirst", node.item + "_islast"]); - - c = '[' + walker.walk(node.content, module.exports).join(",") + ']'; - + content = '[' + walker.walk(node.content, module.exports).join(",") + ']'; walker.popSubScope(); } - if (e.code !== '') { - e.code = "{" + e.code + "}"; + if (expr.code !== '') { + expr.code = "{" + expr.code + "}"; } var forType = 0; // to support types than 'in' - return ['n.$foreach(', e.code, ',"', node.key, '","', node.item, '",', forType, ',', e.exprIdx, ',', c, ')'].join(''); + return ['n.$foreach(', expr.code, ',"', node.key, '","', node.item, '",', forType, ',', expr.exprIdx, ',', content, ')'].join(''); }; /* - * Manage insertion expressions + * Manages insertion expressions. + * @param {Node} node the current Node object as built by the treebuilder. + * @param {TreeWalker} walker the template walker instance. + * @return {String} a snippet of Javascript code built from the node. */ exports["insert"] = function (node, walker) { node.category = "functionref"; - var e = formatExpression(node, 1, walker); - var exprcode = e.code.length === 0 ? "0" : "{" + e.code + "}"; + var expr = formatExpression(node, 1, walker); + var exprcode = expr.code.length === 0 ? "0" : "{" + expr.code + "}"; - return ['n.$insert(', exprcode, ',', e.exprIdx, ')'].join(''); + return ['n.$insert(', exprcode, ',', expr.exprIdx, ')'].join(''); }; /* - * Manage element and component nodes + * Manages element and component nodes. + * @param {Node} node the current Node object as built by the treebuilder. + * @param {TreeWalker} walker the template walker instance. + * @return {String} a snippet of Javascript code built from the node. */ function elementOrComponent (node, walker) { // we should generate sth like // n.elt("div", {e1:[0,0,"arg"]}, {"title":["",1]}, 0, [...]) - var attcontent = "0", evtcontent = "0", exprcode = "0", atts = node.attributes, sz = atts.length; - if (sz > 0) { - var list, aname, attlist = [], evtlist = [], exprlist = [], att, type, exprIdx = 1; - - for (var i = 0; sz > i; i++) { - att = atts[i]; - list = attlist; - aname = att.name; - if (att.name.match(/^on/i)) { + var attributeContent = "0", eventContent = "0", exprCode = "0", attributes = node.attributes, length = attributes.length; + if (length > 0) { + var list, attributeName, attributesList = [], eventList = [], exprList = [], attribute, type, exprIndex = 1; + + for (var i = 0; length > i; i++) { + attribute = attributes[i]; + list = attributesList; + attributeName = attribute.name; + if (attribute.name.match(/^on/i)) { // this is an event handler - list = evtlist; - aname = att.name.slice(2); + list = eventList; + attributeName = attribute.name.slice(2); } - type = att.type; + type = attribute.type; if (type === "text") { - list.push('"' + aname + '":"' + att.value + '"'); + list.push('"' + attributeName + '":"' + attribute.value + '"'); } else if (type === "expression") { - var e = formatExpression(att, exprIdx, walker); - exprIdx = e.nextIndex; - exprlist.push(e.code); - if (list === evtlist) { - list.push('"' + aname + '":' + e.exprIdx); + var expr = formatExpression(attribute, exprIndex, walker); + exprIndex = expr.nextIndex; + exprList.push(expr.code); + if (list === eventList) { + list.push('"' + attributeName + '":' + expr.exprIdx); } else { - list.push('"' + aname + '":["",' + e.exprIdx + ']'); + list.push('"' + attributeName + '":["",' + expr.exprIdx + ']'); } } else if (type === "textblock") { - var tb = formatTextBlock(att, exprIdx, walker); - exprIdx = tb.nextIndex; - if (tb.expArg !== '0') { - exprlist.push(tb.expArg.slice(1, -1)); + var textBlock = formatTextBlock(attribute, exprIndex, walker); + exprIndex = textBlock.nextIndex; + if (textBlock.exprArg !== '0') { + exprList.push(textBlock.exprArg.slice(1, -1)); } - list.push('"' + aname + '":' + tb.blockArgs); + list.push('"' + attributeName + '":' + textBlock.blockArgs); } else if (type === "name") { - list.push('"' + aname + '":null'); + list.push('"' + attributeName + '":null'); } else { walker.logError("Invalid attribute type: " + type); } } - if (attlist.length) { - attcontent = "{" + attlist.join(',') + "}"; + if (attributesList.length) { + attributeContent = "{" + attributesList.join(',') + "}"; } - if (evtlist.length) { - evtcontent = "{" + evtlist.join(',') + "}"; + if (eventList.length) { + eventContent = "{" + eventList.join(',') + "}"; } - exprcode = exprlist.length === 0 ? "0" : "{" + exprlist.join(',') + "}"; + exprCode = exprList.length === 0 ? "0" : "{" + exprList.join(',') + "}"; } - var c = ''; + var content = ''; if (node.content && node.content.length) { - c = ',[' + walker.walk(node.content, module.exports).join(",") + ']'; + content = ',[' + walker.walk(node.content, module.exports).join(",") + ']'; } - return [exprcode, ',', attcontent, ',', evtcontent, c].join(''); + return [exprCode, ',', attributeContent, ',', eventContent, content].join(''); } +/** + * Generates an element node. + * @param {Node} node the current Node object as built by the treebuilder. + * @param {TreeWalker} walker the template walker instance. + * @return {String} a snippet of Javascript code built from the node. + */ exports["element"] = function (node, walker) { - var s = elementOrComponent(node, walker); - var subScope=(node.needSubScope===true)? ',1' : ''; - return ['n.elt("', node.name, '",', s, subScope, ')'].join(''); + var generatedNode = elementOrComponent(node, walker); + var subScope = (node.needSubScope === true)? ',1' : ''; + return ['n.elt("', node.name, '",', generatedNode, subScope, ')'].join(''); }; +/** + * Generates a component node. + * @param {Node} node the current Node object as built by the treebuilder. + * @param {TreeWalker} walker the template walker instance. + * @return {String} a snippet of Javascript code built from the node. + */ exports["component"] = function (node, walker) { - var s = elementOrComponent(node, walker); - var p = node.ref.path; + var generatedNode = elementOrComponent(node, walker); + var path = node.ref.path; - walker.addGlobalRef(p[0]); + walker.addGlobalRef(path[0]); - return ['n.cpt([_', p[0], ',"', p.join('","'), '"],', s, ')'].join(''); + return ['n.cpt([_', path[0], ',"', path.join('","'), '"],', generatedNode, ')'].join(''); }; +/** + * Generates a cptattribute node. + * @param {Node} node the current Node object as built by the treebuilder. + * @param {TreeWalker} walker the template walker instance. + * @return {String} a snippet of Javascript code built from the node. + */ exports["cptattribute"] = function (node, walker) { - var s = elementOrComponent(node, walker); - return ['n.catt("', node.name, '",', s, ')'].join(''); + var generatedNode = elementOrComponent(node, walker); + return ['n.catt("', node.name, '",', generatedNode, ')'].join(''); }; /** - * Format an expression according to its category - * Return the expression string and the next expression index that can be used + * Formats an expression according to its category. + * @param {Object} expression the expression to format. + * @param {Integer} firstIndex index of the expression. + * @param {TemplateWalker} walker the template walker instance. + * @return {Object} the expression string and the next expression index that can be used */ -function formatExpression (expression, firstIdx, walker) { - var cat = expression.category, code = '', nextIndex = firstIdx, bound = (expression.bound === false) ? 0 : 1; - var exprIdx = firstIdx; - if (cat === 'objectref' || cat === 'functionref') { - var path = expression.path, argExprs = null, argExprIdx = null, args = expression.args; +function formatExpression (expression, firstIndex, walker) { + var category = expression.category, code = '', nextIndex = firstIndex, bound = (expression.bound === false) ? 0 : 1; + var exprIndex = firstIndex; + if (category === 'objectref' || category === 'functionref') { + var path = expression.path, argExprs = null, argExprIndex = null, args = expression.args; if (path.length === 0) { walker.logError("Expression path cannot be empty"); } else { var root = path[0], isRootInScope = walker.isInScope(root); - var arg1 = isRootInScope ? bound : 2; + /* Possible expression types are: + * 0: unbound data ref - e.g. {e1:[0,1,"item_key"]} + * 1: bound data ref - e.g. {e1:[1,2,"person","name"]} + * 2: literal data ref - e.g. {e1:[2,2,person,"name"]} + * 3: function call - e.g. {e1:[3,2,"ctl","deleteItem",1,2,1,0]} + * 4: function call literal- e.g. {e1:[4,1,myfunc,1,2,1,0]} + * 5: literal value - e.g. {e1:[5,"some value"]} + * 6: function expression - e.g. {e1:[6,function(a0,a1){return a0+a1;},2,3]}*/ + var exprType = isRootInScope ? bound : 2; if (root === "event") { - arg1 = 0; + exprType = 0; } - if (arg1===2) { + if (exprType === 2) { // root is a global reference walker.addGlobalRef(root); - root="_"+root; - path[0]="_"+path[0]; + root = "_" + root; + path[0] = "_" + path[0]; } - if (cat === 'functionref') { - arg1 = isRootInScope ? 3 : 4; + if (category === 'functionref') { + exprType = isRootInScope ? 3 : 4; argExprs = []; - argExprIdx = []; - var arg, acat, e, idx = exprIdx + 1; - for (var i = 0, sz = args.length; sz > i; i++) { + argExprIndex = []; + var arg, argCategory, expr, index = exprIndex + 1; + for (var i = 0; i < args.length; i++) { arg = args[i]; - acat = arg.category; - if (acat === "string" || acat === "boolean" || acat === "number") { + argCategory = arg.category; + if (argCategory === "string" || argCategory === "boolean" || argCategory === "number") { continue; } - e = formatExpression(arg, idx, walker); - argExprs.push(e.code); - argExprIdx[i] = e.exprIdx; - idx = e.nextIndex; + expr = formatExpression(arg, index, walker); + argExprs.push(expr.code); + argExprIndex[i] = expr.exprIdx; + index = expr.nextIndex; } - nextIndex = idx; + nextIndex = index; } else { nextIndex++; } - var res, rootRef = path[0]; + var result, rootRef = path[0]; if (isRootInScope || root === "event") { rootRef = '"' + rootRef + '"'; } - var psz = path.length; - if (psz === 1) { - res = ['e', exprIdx, ':[', arg1, ',1,', rootRef]; - } else { - var p = [], pitm; - p.push(rootRef); - for (var i = 1; psz > i; i++) { - pitm = path[i]; - if ((typeof pitm) === "string") { - p.push('"' + pitm + '"'); - } else { - p.push(pitm); - } + var pathLength = path.length; + + var generatedPath = [], pathItem; + generatedPath.push(rootRef); + for (var i = 1; i < pathLength; i++) { + pathItem = path[i]; + if ((typeof pathItem) === "string") { + generatedPath.push('"' + pathItem + '"'); + } else { + generatedPath.push(pathItem); } - res = ['e', exprIdx, ':[', arg1, ',', psz, ',', p.join(',')]; } + result = ['e', exprIndex, ':[', exprType, ',', pathLength, ',', generatedPath.join(',')]; + if (args && args.length > 0) { - var acat, arg; - for (var i = 0, sz = args.length; sz > i; i++) { + var argCategory, arg; + for (var i = 0; i < args.length; i++) { arg = args[i]; - acat = arg.category; - if (acat === "string") { - res.push(',0,"' + escapeNewLines(arg.value.replace(/"/g, "\\\"")) + '"'); - } else if (acat === "boolean" || acat === "number") { - res.push(',0,' + arg.value); + argCategory = arg.category; + if (argCategory === "string") { + result.push(',0,"' + escapeNewLines(arg.value.replace(/"/g, "\\\"")) + '"'); + } else if (argCategory === "boolean" || argCategory === "number") { + result.push(',0,' + arg.value); } else { // this is not a literal - res.push(',1,' + argExprIdx[i]); + result.push(',1,' + argExprIndex[i]); } } if (argExprs && argExprs.length > 0) { - res.push("],"); - res.push(argExprs.join(",")); + result.push("],"); + result.push(argExprs.join(",")); } else { - res.push("]"); + result.push("]"); } } else { - res.push("]"); + result.push("]"); } - code = res.join(""); + code = result.join(""); } - } else if (cat === 'boolean' || cat === 'number') { - code = ['e', exprIdx, ':[5,', expression.value, ']'].join(''); + } else if (category === 'boolean' || category === 'number') { + code = ['e', exprIndex, ':[5,', expression.value, ']'].join(''); nextIndex++; - } else if (cat === 'string') { - code = ['e', exprIdx, ':[5,"', ('' + expression.value).replace(/"/g, "\\\""), '"]'].join(''); + } else if (category === 'string') { + code = ['e', exprIndex, ':[5,"', ('' + expression.value).replace(/"/g, "\\\""), '"]'].join(''); nextIndex++; - } else if (cat === 'jsexpression') { - var refs = expression.objectrefs, ref, e, idx = exprIdx + 1, exprs = [], exprIdxs = []; + } else if (category === 'jsexpression') { + var refs = expression.objectrefs, ref, expr, index = exprIndex + 1, exprs = [], exprIdxs = []; if (refs === undefined) { console.warn("[formatExpression] The following expression has not been pre-processed - parser should be updated: "); console.dir(expression); } - var args = [], sz = refs.length, argSeparator = (sz > 0) ? ',' : ''; - for (var i = 0; sz > i; i++) { + var args = [], length = refs.length, argSeparator = (length > 0) ? ',' : ''; + for (var i = 0; length > i; i++) { ref = refs[i]; args[i] = "a" + i; - e = formatExpression(ref, idx, walker); - exprs.push(e.code); - exprIdxs[i] = e.exprIdx; - idx = e.nextIndex; + expr = formatExpression(ref, index, walker); + exprs.push(expr.code); + exprIdxs[i] = expr.exprIdx; + index = expr.nextIndex; } var func = ['function(', args.join(','), ') {return ', expression.code, ';}'].join(''); - var code0 = ['e', exprIdx, ':[6,', func, argSeparator, exprIdxs.join(','), ']'].join(''); + var code0 = ['e', exprIndex, ':[6,', func, argSeparator, exprIdxs.join(','), ']'].join(''); exprs.splice(0, 0, code0); code = exprs.join(','); nextIndex = exprIdxs[exprIdxs.length - 1] + 1; } else { - walker.logError("Unsupported expression: " + cat, expression); + walker.logError("Unsupported expression: " + category, expression); } return { code : code, - exprIdx : exprIdx, + exprIdx : exprIndex, nextIndex : nextIndex }; } /** - * Format the textblock content for textblock and attribute nodes + * Format the textblock content for textblock and attribute nodes. + * @param {Node} node the current Node object as built by the treebuilder. + * @param {Integer} nextExprIndex the index of the next expression. + * @param {TreeWalker} walker the template walker instance. + * @return {String} a snippet of Javascript code built from the node. */ -function formatTextBlock (node, nextExprIdx, walker) { - var c = node.content, sz = c.length, itm, expArr = [], args = [], idx = 0; // idx is the index in the $text array +function formatTextBlock (node, nextExprIndex, walker) { + var content = node.content, item, exprArray = [], args = [], index = 0; // idx is the index in the $text array // (=args) - for (var i = 0; sz > i; i++) { - itm = c[i]; - if (itm.type === "text") { - if (idx % 2 === 0) { + for (var i = 0; i < content.length; i++) { + item = content[i]; + if (item.type === "text") { + if (index % 2 === 0) { // even index: arg must be a string - args[idx] = '"' + escapeNewLines(itm.value.replace(/"/g, "\\\"")) + '"'; - idx++; + args[index] = '"' + escapeNewLines(item.value.replace(/"/g, "\\\"")) + '"'; + index++; } else { // odd index: arg must be an expression - so add the text to the previous item - if (idx > 0) { - args[idx - 1] = args[idx - 1].slice(0, -1) + escapeNewLines(itm.value.replace(/"/g, "\\\"")) + '"'; + if (index > 0) { + args[index - 1] = args[index - 1].slice(0, -1) + escapeNewLines(item.value.replace(/"/g, "\\\"")) + '"'; } else { - // we should never get there as idx is odd ! + // we should never get there as index is odd ! walker.logError("Invalid textblock structure", node); } } - } else if (itm.type === "expression") { - if (idx % 2 === 0) { + } else if (item.type === "expression") { + if (index % 2 === 0) { // even index: arg must be a string - args[idx] = '""'; - idx++; + args[index] = '""'; + index++; } - var e = formatExpression(itm, nextExprIdx, walker); - nextExprIdx = e.nextIndex; - if (e.code) { - expArr.push(e.code); - args[idx] = e.exprIdx; // expression idx + var expr = formatExpression(item, nextExprIndex, walker); + nextExprIndex = expr.nextIndex; + if (expr.code) { + exprArray.push(expr.code); + args[index] = expr.exprIdx; // expression index } else { - args[idx] = 0; // invalid expression + args[index] = 0; // invalid expression } - idx++; + index++; } } - var expArg = "0"; - if (expArr.length) { - expArg = '{' + expArr.join(",") + '}'; + var exprArg = "0"; + if (exprArray.length) { + exprArg = '{' + exprArray.join(",") + '}'; } var blockArgs = "[]"; if (args.length) { @@ -451,8 +518,8 @@ function formatTextBlock (node, nextExprIdx, walker) { } return { - expArg : expArg, - nextIndex : nextExprIdx, + exprArg : exprArg, + nextIndex : nextExprIndex, blockArgs : blockArgs }; } diff --git a/hsp/compiler/jsgenerator/templateWalker.js b/hsp/compiler/jsgenerator/templateWalker.js index d2211c0..cbb941e 100644 --- a/hsp/compiler/jsgenerator/templateWalker.js +++ b/hsp/compiler/jsgenerator/templateWalker.js @@ -6,9 +6,15 @@ var TreeWalker = require("./treeWalker").TreeWalker; */ var TemplateWalker = klass({ $extends : TreeWalker, - $constructor : function (fileName,dirPath) { - this.fileName=fileName; - this.dirPath=dirPath; + + /** + * Constructor. + * @param {String} fileName the name of the file being compiled. + * @param {String} dirPath the directory path. + */ + $constructor : function (fileName, dirPath) { + this.fileName = fileName; + this.dirPath = dirPath; this.templates = {}; // used by processors to store intermediate values in order to ease testing this.globals={}; // global validation code for each template - used for unit testing this.errors = []; @@ -16,6 +22,11 @@ var TemplateWalker = klass({ this.resetScope(); }, + /** + * Adds an error to the current error list. + * @param {String} description the error description + * @param {Object} errdesc additional object (block, node, ...) which can contain additional info about the error (line/column number, code). + */ logError : function (description, errdesc) { var desc = { description : description @@ -32,13 +43,18 @@ var TemplateWalker = klass({ this.errors.push(desc); }, - // reset the list of global variables that have been found since the last reset + /** + * Resets the list of global variables that have been found since the last reset + */ resetGlobalRefs : function () { this._globals=[]; this._globalKeys={}; }, - // add a global reference (e.g. "foo") to the current _globals list + /** + * Adds a global reference (e.g. "foo") to the current _globals list. + * @param {String} ref the reference key. + */ addGlobalRef : function (ref) { if (!this._globalKeys[ref]) { this._globals.push(ref); @@ -46,37 +62,59 @@ var TemplateWalker = klass({ } }, - // reset the scope variables that are used to determine if a variable name is in the current scope + /** + * Resets the scope variables that are used to determine if a variable name is in the current scope. + */ resetScope : function () { this._scopes = [{}]; this._scope = this._scopes[0]; }, - addScopeVariable : function (varname) { - this._scope[varname] = true; + /** + * Adds a scope variable. + * @param {String} varName the variable name. + */ + addScopeVariable : function (varName) { + this._scope[varName] = true; }, - rmScopeVariable : function (varname) { - this._scope[varname] = null; + /** + * Removes a scope variable. + * @param {String} varName the variable name. + */ + rmScopeVariable : function (varName) { + this._scope[varName] = null; }, - isInScope : function (varname) { - if (varname === "scope") { + /** + * Checks if a scope variable exists. + * @param {String} varName the variable name. + * @return {Boolean} true if it exists. + */ + isInScope : function (varName) { + if (varName === "scope") { return true; // scope is a reserved key word and is automatically created on the scope object } - return this._scope[varname] ? true : false; + return this._scope[varName] ? true : false; }, - pushSubScope : function (vararray) { + /** + * Pushes a sub scope. + * @param {Array} varArray an array of variable. + */ + pushSubScope : function (varArray) { var newScope = Object.create(this._scope); - for (var i = 0, sz = vararray.length; sz > i; i++) { - newScope[vararray[i]] = true; + for (var i = 0; i < varArray.length; i++) { + newScope[varArray[i]] = true; } this._scopes.push(newScope); this._scope = this._scopes[this._scopes.length - 1]; }, - popSubScope : function (varnames) { + /** + * Pops a sub scope. + */ + popSubScope : function () { this._scopes.pop(); this._scope = this._scopes[this._scopes.length - 1]; } diff --git a/hsp/compiler/jsgenerator/treeWalker.js b/hsp/compiler/jsgenerator/treeWalker.js index 9543ff1..d848a13 100644 --- a/hsp/compiler/jsgenerator/treeWalker.js +++ b/hsp/compiler/jsgenerator/treeWalker.js @@ -3,7 +3,9 @@ var klass = require("../../klass"); var TreeWalker = klass({ /** * Start traversing a parse tree. This method takes the intermediate representation created by the parser and - * executes, for each of the nodes, a function defined on the processor object + * executes, for each of the nodes, a function defined on the processor object. + * @param {SyntaxTree} tree the syntax tree. + * @param {Object} processor a set of function to process the tree elements. * @return {[type]} [description] */ walk : function (tree, processor) { @@ -22,6 +24,9 @@ var TreeWalker = klass({ /** * Execute a callback on each element of an array. The callback receives the value of the array. This method returns * an array with the return value of the callbacks if not null. + * @param {Array} array the input array. + * @param {Function} callback the callback. + * @return {Array} an array made with the result of each callback. */ each : function (array, callback) { var result = []; diff --git a/hsp/compiler/parser/README.md b/hsp/compiler/parser/README.md index f99adfb..d2a6b0a 100644 --- a/hsp/compiler/parser/README.md +++ b/hsp/compiler/parser/README.md @@ -1,3 +1,265 @@ # Parser # -TODO \ No newline at end of file +The **parser** is the first step of the compilation process. +It parses the hashspace syntax and builds a flat list of the blocks which compose the template. + +The parsing is done thanks to the PEG.js library, for more information see http://pegjs.majda.cz/ +The grammar is stored in the `hspblocks.pegjs` file, which is itself compiled into javascript during the pre-publish grunt task. + +## Input ## +The hashspace template source code, e.g.: +``` +# template test(person) +
+ Hello {person.name}! +
+# /template +``` + +## Output ## +The flat list of blocks, e.g.: +``` +{ + "type": "template", + "name": "test", + "args": ["person"], + "content": [ + {type:"element", name:"div", closed:false, attributes:[ + {type:"attribute", name:"title", value:[{type:"text", value:"Some text"}]}, + {type:"attribute", name:"id", value:[{type:"expression", "category": "jsexpression", expType:"PropertyAccess", "bound": true, base:{type:"Variable"name:"person"}, name:"id"}]}, + {type:"attribute", name:"class", value:[ + {type:"expression", "category": "jsexpression", expType:"PropertyAccess", "bound": true, base:{type:"Variable", name:"person"}, name:"gender"}, + {type:"text", value:" "}, + {type:"expression", "category": "jsexpression", expType:"PropertyAccess", "bound": true, base:{type:"Variable", name:"person"}, name:"category"} + ]} + ]}, + {"type": "text","value": "Hello "}, + {"type": "expression", "category": "jsexpression", expType:"PropertyAccess", "bound": true, base:{type:"Variable", name:"person"}, name:"name"}, + {"type": "text","value": "! "}, + {"type": "endelement",name:"div"} + ] +} +``` +The list is available in the `"content"` attribute. + +## Blocks ## + +The blocks returned are defined in the PEG grammar. +They all have a **type** property which is the primary key. +Some of them have a **category** property which is the secondary key. + +Here is an extract of the grammar listing all the possible blocks: + +### Hashspace specific blocks ### + +``` +TextBlock +{type:"plaintext", value:lines.join('')} + +TemplateBlock +- Template with a controller +{type:"template", name:name, mod:mod, controller:args.ctl, controllerRef: args.ctlref, line:line, column:column} +- Template without a controller +{type:"template", name:name, mod:mod, args:(args==='')? []:args, line:line, column:column} + +TemplateEnd +{type:"/template",line:line,column:column}} + +InvalidTemplate +{type:"invalidtemplate", line:line, column:column, code: "# "+mod+"template "+name+" "+args.invalidTplArg} + +TplTextBlock "text" +{type:"text", value:chars.join(''), line:line, column:column} + +InvalidBlock +{type:"invalidblock", code:chars.join(''), line:line, column:column} + +IfBlock +{type:"if", condition:expr, line:line, column:column} + +ElseIfBlock +{type:"elseif", condition:expr, line:line, column:column} + +ElseBlock +{type:"else", line:line, column:column} + +EndIfBlock +{type:"endif", line:line, column:column} + +CommentBlock +{type:"comment", value:chars.join('')} + +HTMLCommentBlock +{type:"comment", value:chars.join('')} + +ForeachBlock +{type:"foreach", item:args.item, key:args.key, colref:args.colref, line:line, column:column} + +EndForeachBlock +{type:"endforeach", line:line, column:column} + +HTMLElement +{type:"element", name:name, closed:(end!==""), attributes:atts, line:line, column:column} + +EndHTMLElement +{type:"endelement", name:name, line:line, column:column} + +HspComponent +{type:"component", ref:ref, closed:(end!==""), attributes:atts, line:line, column:column} + +EndHspComponent +{type:"endcomponent", ref:ref, line:line, column:column} + +HspCptAttribute +{type:"cptattribute", name:ref, closed:(end!==""), attributes:atts, line:line, column:column} + +EndHspCptAttribute +{type:"endcptattribute", name:ref, line:line, column:column} + +InvalidHTMLElement +{type:"invalidelement", code:'<'+code.join(''), line:line, column:column} + +HTMLAttribute +{type:"attribute", name:name, value:v, line:line, column:column} + +HTMLAttributeText +{type:"text", value:chars.join('')} + +LogBlock +{type:"log",exprs:exprs, line:line, column:column} + +LetBlock +{type:"let",assignments:asn, line:line, column:column} + +ExpressionBlock +{type:"expression", category: "jsexpression", expType: //see JS blocks below} + +InvalidExpression +{type:"expression", category: "invalidexpression", r.code=code.join('')} + +HExpressionCssClassElt +{type:"CssClassElement", left:head, right:tail} + +InvalidExpressionValue +{type:"invalidexpression", code:chars.join(''), line:line, column:column} +``` + +### Javascript general blocks ### + +``` +JSLiteral +- NullLiteral { return {type:"expression", category: "null", code:"null"};} +- BooleanLiteral { return {type:"expression", category: "boolean", value:v.value, code:""+v.value};} +- NumericLiteral { return {type:"expression", category: "number", value: v, code:""+v};} +- StringLiteral { return {type:"expression", category: "string", value: v, code:""+v};} + +NullLiteral +{type: "nullliteral", value: null } + +BooleanLiteral +- True { return { type: "booleanliteral", value: true }; } +- False { return { type: "booleanliteral", value: false }; } + +PrimaryExpression +{ type: "Variable", name: name, code:name } + +ArrayLiteral +{ + type: "ArrayLiteral", + elements: elements !== "" ? elements : [] +} + +ObjectLiteral +{ + type: "ObjectLiteral", + properties: properties !== "" ? properties[0] : [] +} + +PropertyAssignment // changed +{ + type: "PropertyAssignment", + name: name, + value: value +} + +MemberExpression +{ + type: "PropertyAccess", + base: result, + name: accessors[i] +} + +CallExpression +{ + type: "FunctionCall", + name: name, + arguments: arguments +} + + { + type: "FunctionCallArguments", + arguments: arguments + } + { + type: "PropertyAccessProperty", + name: name + } + +PostfixExpression +{ + type: "PostfixExpression", + operator: operator, + expression: expression +} + +UnaryExpression +{ + type: "UnaryExpression", + operator: operator, + expression: expression +} + +MultiplicativeExpression +AdditiveExpression +ShiftExpression +RelationalExpression +RelationalExpressionNoIn +EqualityExpression +EqualityExpressionNoIn +BitwiseANDExpression +BitwiseANDExpressionNoIn +BitwiseXORExpression +BitwiseXORExpressionNoIn +BitwiseORExpression +BitwiseORExpressionNoIn +LogicalANDExpression +LogicalANDExpressionNoIn +LogicalORExpression +LogicalORExpressionNoIn +Expression +ExpressionNoIn +{ + type: "BinaryExpression", + operator: tail[i][1], + left: result, + right: tail[i][3] +} + +ConditionalExpression +ConditionalExpressionNoIn +{ + type: "ConditionalExpression", + condition: condition, + trueExpression: trueExpression, + falseExpression: falseExpression +} + +AssignmentExpression +AssignmentExpressionNoIn +{ + type: "AssignmentExpression", + operator: operator, + left: left, + right: right +} +``` \ No newline at end of file diff --git a/hsp/compiler/parser/index.js b/hsp/compiler/parser/index.js index 4d0a103..086e745 100644 --- a/hsp/compiler/parser/index.js +++ b/hsp/compiler/parser/index.js @@ -1,10 +1,11 @@ var blockParser = require("./hspblocks.peg.js"); /** - * Return the list of instruction blocks that compose a template file at this stage the template AST is not complete - - * cf. parse() function to get the complete syntax tree Note: this function is exposed for unit test purposes and should - * not be used directly + * Return the list of instruction blocks that compose a template file. + * At this stage the template AST is not complete, it is built in the next step of the compilation process, i.e. the treebuilder. + * Note: this function is exposed for unit test purposes and should not be used directly * @param {String} template the template to parse + * @return {Object} the parse result */ exports.parse = function (template) { // add a last line feed a the end of the template as the parser parses the plaintext diff --git a/hsp/compiler/renderer.js b/hsp/compiler/renderer.js index 208d0ac..6d9c086 100644 --- a/hsp/compiler/renderer.js +++ b/hsp/compiler/renderer.js @@ -16,8 +16,8 @@ exports.renderFile = function (path, options, fn) { var compiledTemplate; if (!err) { try { - var r = compiler.compile(content,path); - compiledTemplate = r.code; + var result = compiler.compile(content, path); + compiledTemplate = result.code; // err=r.errors; } catch (ex) { err = ex; @@ -34,16 +34,16 @@ exports.renderFile = function (path, options, fn) { * @return the compiled JS */ exports.renderString = function (src, path) { - var r = { + var result = { code : '', errors : null }; try { - r = compiler.compile(src, path); + result = compiler.compile(src, path); } catch (ex) { - r.serverErrors = [{ - description : ex.toString() - }]; + result.serverErrors = [{ + description : ex.toString() + }]; } - return r; + return result; }; diff --git a/hsp/compiler/treebuilder/README.md b/hsp/compiler/treebuilder/README.md index d0e0eaa..882d60d 100644 --- a/hsp/compiler/treebuilder/README.md +++ b/hsp/compiler/treebuilder/README.md @@ -1,3 +1,68 @@ # Tree Builder # -TODO \ No newline at end of file +The **treebuilder** is the second step of the compilation process. +From the list of blocks created by the **parser**, it builds a syntax tree. + +This step is also responsible for transforming HTML entities to UTF8, and to ensure that reserved keywords (event, scope) are not used. + +## Input ## +The flat list of blocks, e.g.: +``` +{ + "type": "template", + "name": "test", + "args": ["person"], + "content": [ + {type:"element", name:"div", closed:false, attributes:[ + {type:"attribute", name:"title", value:[{type:"text", value:"Some text"}]}, + {type:"attribute", name:"id", value:[{type:"expression", "category": "jsexpression", expType:"PropertyAccess", "bound": true, base:{type:"Variable"name:"person"}, name:"id"}]}, + {type:"attribute", name:"class", value:[ + {type:"expression", "category": "jsexpression", expType:"PropertyAccess", "bound": true, base:{type:"Variable", name:"person"}, name:"gender"}, + {type:"text", value:" "}, + {type:"expression", "category": "jsexpression", expType:"PropertyAccess", "bound": true, base:{type:"Variable", name:"person"}, name:"category"} + ]} + ]}, + {"type": "text","value": "Hello "}, + {"type": "expression", "category": "jsexpression", expType:"PropertyAccess", "bound": true, base:{type:"Variable", name:"person"}, name:"name"}, + {"type": "text","value": "! "}, + {"type": "endelement",name:"div"} + ] +} +``` +The list is available in the `"content"` attribute. + +## Output ## +The syntax tree, e.g.: +``` +{ + "type": "template", + "name": "test", + "args": ["person"], + "content": [ + {type:"element", name:"div", closed:false, attributes:[ + {name:"title", type:"text", value:"Some text"}, + {name:"id", type:"expression", "category": "objectref", "bound": true, "path": [ "person", "id" ]}, + {name:"class", type:"textblock", content:[ + {type:"expression", "category": "objectref", "bound": true, "path": [ "person", "gender" ]}, + {type:"text", value:" "}, + {type:"expression", "category": "objectref", "bound": true, "path": [ "person", "category" ]} + ]} + ],"content": [ + { "type": "textblock", "content": [ + {"type": "text","value": "Hello "}, + {"type": "expression", "category": "objectref", "bound": true, "path": [ "person", "name" ]}, + {"type": "text","value": "! "} + ]} + ]} + ] +} +``` + +## Algorithm ## +The main process of the treebuilder is the `generateTree()` function in SyntaxTree class. +It loops over the list of blocks created by the parser. +For each type of block, there is an associated method manage it. By convention, type *xyz* is managed by the `__xyz` function. + +Depending on the type of the block, the process will decide if it has go deeper or not in the syntax tree. +This is done by recursive calls to the `_advance` function. +For example, a *foreach* block means that the next ones will be children, until a *endfroeach* block is found. diff --git a/hsp/compiler/treebuilder/hExpression.js b/hsp/compiler/treebuilder/hExpression.js index ad3d9d7..3b59816 100644 --- a/hsp/compiler/treebuilder/hExpression.js +++ b/hsp/compiler/treebuilder/hExpression.js @@ -4,6 +4,7 @@ var HExpression = klass({ * Process the HExpression node parsed by the PEG grammar to generate a more digestable format for the syntax tree * (note:processing is mostly for the JSExpression parts) * @param {JSON} node the node generated by the PEG Grammar (cf. HExpression) + * @param {SyntaxTree} globalSyntaxTree the parent syntax tree */ $constructor : function (node, globalSyntaxTree) { this.rootExpression = node; @@ -15,13 +16,13 @@ var HExpression = klass({ if (node.category !== "jsexpression") { // we only reprocess jsexpression - this._st = node; + this.syntaxTree = node; } else { this._objectRefs = []; // list of objectref expressions found in the jsexpression var code = this._process(node); this.rootExpression.code = code; - this._st = { + this.syntaxTree = { type : "expression", category : "jsexpression", objectrefs : this._objectRefs, @@ -30,10 +31,10 @@ var HExpression = klass({ // if we have only one variable, we can simplify the syntaxtree if (code === "a0") { - this._st = this._objectRefs[0]; + this.syntaxTree = this._objectRefs[0]; } else if (code.match(/^ *$/)) { // there is no code to display - this._st = { + this.syntaxTree = { "type" : "text", "value" : code }; @@ -41,13 +42,13 @@ var HExpression = klass({ // add line / column nbr if present if (node.line) { - this._st.line = node.line; - this._st.column = node.column; + this.syntaxTree.line = node.line; + this.syntaxTree.column = node.column; } // check errors if (this.errors) { - for (var i = 0, sz = this.errors.length; sz > i; i++) { + for (var i = 0; i < this.errors.length; i++) { globalSyntaxTree._logError(this.errors[i], this.rootExpression); } } @@ -57,7 +58,7 @@ var HExpression = klass({ * Return the syntax tree node to put in the global syntax tree */ getSyntaxTree : function () { - return this._st; + return this.syntaxTree; }, /** @@ -76,26 +77,26 @@ var HExpression = klass({ * @return {String} the JS code associated to this node */ _process : function (node) { - var r = ""; + var result = ""; switch (node.type) { case "expression" : - r = this._getValue(node); + result = this._getValue(node); break; case "BinaryExpression" : - r = '(' + this._process(node.left) + ' ' + node.operator + ' ' + this._process(node.right) + ')'; + result = '(' + this._process(node.left) + ' ' + node.operator + ' ' + this._process(node.right) + ')'; break; - case "UnaryExpression" : // e.g. !x +x -x typeof x ++x and --x will not be supported - r = '' + node.operator + '(' + this._process(node.expression) + ')'; + case "UnaryExpression" : // e.g. !x +x -x typeof x. Note that ++x and --x will not be supported + result = '' + node.operator + '(' + this._process(node.expression) + ')'; if (node.operator === '++' || node.operator === '--') { this._logError('Unary operator ' + node.operator + ' is not allowed'); } break; case "PostfixExpression" : // e.g. x++ or x-- (not allowed) - r = '' + '(' + this._process(node.expression) + ')' + node.operator; + result = '' + '(' + this._process(node.expression) + ')' + node.operator; this._logError('Postfix operator ' + node.operator + ' is not allowed'); break; case "Variable" : - r = this._getValue({ + result = this._getValue({ type : "expression", "category" : "objectref", bound : node.bound, @@ -104,86 +105,86 @@ var HExpression = klass({ break; case "PropertyAccess" : // this is an object ref - var n = node, p = [], nm; + var n = node, path = [], name; while (n) { - nm = n.name; - if (nm.type && nm.type === "expression") { - p.push(nm.value); + name = n.name; + if (name.type && name.type === "expression") { + path.push(name.value); } else { - p.push(nm); + path.push(name); } n = n.base; } - p.reverse(); - r = this._getValue({ + path.reverse(); + result = this._getValue({ type : "expression", "category" : "objectref", bound : node.bound, - "path" : p + "path" : path }); break; case "ConditionalExpression" : - r = '(' + this._process(node.condition) + '? ' + this._process(node.trueExpression) + ' : ' + result = '(' + this._process(node.condition) + '? ' + this._process(node.trueExpression) + ' : ' + this._process(node.falseExpression) + ')'; break; case "FunctionCall" : // this is an object ref - var n = node.name, p = []; + var n = node.name, path = []; while (n) { - p.push(n.name); + path.push(n.name); n = n.base; } - p.reverse(); + path.reverse(); var n = { type : "expression", "category" : "functionref", bound : node.bound, - "path" : p + "path" : path }; - r = this._getValue(n); + result = this._getValue(n); // add arguments - var args2 = [], args = node.arguments, sz = args ? args.length : 0, e; - for (var i = 0; sz > i; i++) { + var outArgs = [], args = node.arguments; + for (var i = 0; i < (args ? args.length : 0); i++) { if (!args[i].category) { // add category otherwise HExpression will not be parsed args[i].category = "jsexpression"; } - e = new HExpression(args[i], this.globalSyntaxTree); - args2[i] = e.getSyntaxTree(); + var expr = new HExpression(args[i], this.globalSyntaxTree); + outArgs[i] = expr.getSyntaxTree(); } - n.args = args2; + n.args = outArgs; break; case "CssClassElement" : - r = "((" + this._process(node.right) + ")? ''+" + this._process(node.left) + ":'')"; + result = "((" + this._process(node.right) + ")? ''+" + this._process(node.left) + ":'')"; break; case "CssClassExpression" : - var ls = node.list, sz = ls.length, code = []; - for (var i = 0; sz > i; i++) { - code[i] = this._process(ls[i]); + var list = node.list, length = list.length, code = []; + for (var i = 0; i < length; i++) { + code[i] = this._process(list[i]); } - if (sz < 1) { - r = ''; + if (length < 1) { + result = ''; } else { - r = "[" + code.join(",") + "].join(' ')"; + result = "[" + code.join(",") + "].join(' ')"; } break; case "ObjectLiteral": - var properties = node.properties, size = properties.length, code = []; - for (var i = 0; size > i; i++) { + var properties = node.properties, length = properties.length, code = []; + for (var i = 0; i < length; i++) { code[i] = this._process(properties[i]); } - if (size < 1) { - r = ''; + if (length < 1) { + result = ''; } else { - r = "{" + code.join(",") + "}"; + result = "{" + code.join(",") + "}"; } break; case "PropertyAssignment": var name = node.name, child = node.value; var code = this._process(child); - r = name + ":" + code; + result = name + ":" + code; break; default : this._logError(node.type + '(s) are not supported yet'); @@ -191,65 +192,67 @@ var HExpression = klass({ console.dir(node); break; } - return r; + return result; }, /** - * Return the code value for a node of type "expression" (i.e. literals, objectrefs...) + * Returns the code value for a node of type "expression" (i.e. literals, objectrefs...). + * @param {Object} node the node. + * @return {String} the value of the node. */ _getValue : function (node) { - var r = ''; + var result = ''; switch (node.category) { case "objectref" : - var sz = this._objectRefs.length, e, pl, ok; + var length = this._objectRefs.length, expr, pathLength, ok; // check if an indentical expression already exist - for (var i = 0; sz > i; i++) { - e = this._objectRefs[i], pl = e.path.length, ok = true; + for (var i = 0; i < length; i++) { + expr = this._objectRefs[i], pathLength = expr.path.length, ok = true; // only the path may vary - if (e.category !== "objectref") { + if (expr.category !== "objectref") { continue; } - if (pl === node.path.length) { - for (var j = 0; pl > j; j++) { - if (e.path[j] !== node.path[j]) { + if (pathLength === node.path.length) { + for (var j = 0; j < pathLength; j++) { + if (expr.path[j] !== node.path[j]) { ok = false; break; } } if (ok) { - r = "a" + i; + result = "a" + i; break; } } } - if (!r) { + if (!result) { // objectref doesn't exist in previous expressions - r = "a" + sz; // argument variable - this._objectRefs[sz] = node; + result = "a" + length; // argument variable + this._objectRefs[length] = node; } break; case "functionref" : // add the function call to the object ref list // we don't optimize the storeage as it is less likely to have the same combination repeated // than with simple object refs - var sz = this._objectRefs.length; - r = "a" + sz; // argument variable - this._objectRefs[sz] = node; + var length = this._objectRefs.length; + result = "a" + length; // argument variable + this._objectRefs[length] = node; break; case "string" : - r = '"' + node.value.replace(/\"/g, '\\"') + '"'; + result = '"' + node.value.replace(/\"/g, '\\"') + '"'; break; case "boolean" : case "number" : - r = node.value; + result = node.value; break; case "null" : - r = "null"; + result = "null"; break; } - return r; + return result; } }); exports.HExpression = HExpression; \ No newline at end of file diff --git a/hsp/compiler/treebuilder/htmlEntities.js b/hsp/compiler/treebuilder/htmlEntities.js index 625c485..4addb88 100644 --- a/hsp/compiler/treebuilder/htmlEntities.js +++ b/hsp/compiler/treebuilder/htmlEntities.js @@ -1,281 +1,281 @@ var NAMED_HTML_ENTITIES = { - "quot": 0x0022, - "amp": 0x0026, - "apos": 0x0027, - "lt": 0x003C, - "gt": 0x003E, - "nbsp": 0x00A0, - "iexcl": 0x00A1, - "cent": 0x00A2, - "pound": 0x00A3, - "curren": 0x00A4, - "yen": 0x00A5, - "brvbar": 0x00A6, - "sect": 0x00A7, - "uml": 0x00A8, - "copy": 0x00A9, - "ordf": 0x00AA, - "laquo": 0x00AB, - "not": 0x00AC, - "shy": 0x00AD, - "reg": 0x00AE, - "macr": 0x00AF, - "deg": 0x00B0, - "plusmn": 0x00B1, - "sup2": 0x00B2, - "sup3": 0x00B3, - "acute": 0x00B4, - "micro": 0x00B5, - "para": 0x00B6, - "middot": 0x00B7, - "cedil": 0x00B8, - "sup1": 0x00B9, - "ordm": 0x00BA, - "raquo": 0x00BB, - "frac14": 0x00BC, - "frac12": 0x00BD, - "frac34": 0x00BE, - "iquest": 0x00BF, - "Agrave": 0x00C0, - "Aacute": 0x00C1, - "Acirc": 0x00C2, - "Atilde": 0x00C3, - "Auml": 0x00C4, - "Aring": 0x00C5, - "AElig": 0x00C6, - "Ccedil": 0x00C7, - "Egrave": 0x00C8, - "Eacute": 0x00C9, - "Ecirc": 0x00CA, - "Euml": 0x00CB, - "Igrave": 0x00CC, - "Iacute": 0x00CD, - "Icirc": 0x00CE, - "Iuml": 0x00CF, - "ETH": 0x00D0, - "Ntilde": 0x00D1, - "Ograve": 0x00D2, - "Oacute": 0x00D3, - "Ocirc": 0x00D4, - "Otilde": 0x00D5, - "Ouml": 0x00D6, - "times": 0x00D7, - "Oslash": 0x00D8, - "Ugrave": 0x00D9, - "Uacute": 0x00DA, - "Ucirc": 0x00DB, - "Uuml": 0x00DC, - "Yacute": 0x00DD, - "THORN": 0x00DE, - "szlig": 0x00DF, - "agrave": 0x00E0, - "aacute": 0x00E1, - "acirc": 0x00E2, - "atilde": 0x00E3, - "auml": 0x00E4, - "aring": 0x00E5, - "aelig": 0x00E6, - "ccedil": 0x00E7, - "egrave": 0x00E8, - "eacute": 0x00E9, - "ecirc": 0x00EA, - "euml": 0x00EB, - "igrave": 0x00EC, - "iacute": 0x00ED, - "icirc": 0x00EE, - "iuml": 0x00EF, - "eth": 0x00F0, - "ntilde": 0x00F1, - "ograve": 0x00F2, - "oacute": 0x00F3, - "ocirc": 0x00F4, - "otilde": 0x00F5, - "ouml": 0x00F6, - "divide": 0x00F7, - "oslash": 0x00F8, - "ugrave": 0x00F9, - "uacute": 0x00FA, - "ucirc": 0x00FB, - "uuml": 0x00FC, - "yacute": 0x00FD, - "thorn": 0x00FE, - "yuml": 0x00FF, - "OElig": 0x0152, - "oelig": 0x0153, - "Scaron": 0x0160, - "scaron": 0x0161, - "Yuml": 0x0178, - "fnof": 0x0192, - "circ": 0x02C6, - "tilde": 0x02DC, - "Alpha": 0x0391, - "Beta": 0x0392, - "Gamma": 0x0393, - "Delta": 0x0394, - "Epsilon": 0x0395, - "Zeta": 0x0396, - "Eta": 0x0397, - "Theta": 0x0398, - "Iota": 0x0399, - "Kappa": 0x039A, - "Lambda": 0x039B, - "Mu": 0x039C, - "Nu": 0x039D, - "Xi": 0x039E, - "Omicron": 0x039F, - "Pi": 0x03A0, - "Rho": 0x03A1, - "Sigma": 0x03A3, - "Tau": 0x03A4, - "psilon": 0x03A5, - "Phi": 0x03A6, - "Chi": 0x03A7, - "Psi": 0x03A8, - "Omega": 0x03A9, - "alpha": 0x03B1, - "beta": 0x03B2, - "gamma": 0x03B3, - "delta": 0x03B4, - "epsilon": 0x03B5, - "zeta": 0x03B6, - "eta": 0x03B7, - "theta": 0x03B8, - "iota": 0x03B9, - "kappa": 0x03BA, - "lambda": 0x03BB, - "mu": 0x03BC, - "nu": 0x03BD, - "xi": 0x03BE, - "omicron": 0x03BF, - "pi": 0x03C0, - "rho": 0x03C1, - "sigmaf": 0x03C2, - "sigma": 0x03C3, - "tau": 0x03C4, - "upsilon": 0x03C5, - "phi": 0x03C6, - "chi": 0x03C7, - "psi": 0x03C8, - "omega": 0x03C9, - "thetasym": 0x03D1, - "upsih": 0x03D2, - "piv": 0x03D6, - "ensp": 0x2002, - "emsp": 0x2003, - "thinsp": 0x2009, - "zwnj": 0x200C, - "zwj": 0x200D, - "lrm": 0x200E, - "rlm": 0x200F, - "ndash": 0x2013, - "mdash": 0x2014, - "lsquo": 0x2018, - "rsquo": 0x2019, - "sbquo": 0x201A, - "ldquo": 0x201C, - "rdquo": 0x201D, - "bdquo": 0x201E, - "dagger": 0x2020, - "Dagger": 0x2021, - "bull": 0x2022, - "hellip": 0x2026, - "permil": 0x2030, - "prime": 0x2032, - "Prime": 0x2033, - "lsaquo": 0x2039, - "rsaquo": 0x203A, - "oline": 0x203E, - "frasl": 0x2044, - "euro": 0x20AC, - "image": 0x2111, - "weierp": 0x2118, - "real": 0x211C, - "trade": 0x2122, - "alefsym": 0x2135, - "larr": 0x2190, - "uarr": 0x2191, - "rarr": 0x2192, - "darr": 0x2193, - "harr": 0x2194, - "crarr": 0x21B5, - "lArr": 0x21D0, - "uArr": 0x21D1, - "rArr": 0x21D2, - "dArr": 0x21D3, - "hArr": 0x21D4, - "forall": 0x2200, - "part": 0x2202, - "exist": 0x2203, - "empty": 0x2205, - "nabla": 0x2207, - "isin": 0x2208, - "notin": 0x2209, - "ni": 0x220B, - "prod": 0x220F, - "sum": 0x2211, - "minus": 0x2212, - "lowast": 0x2217, - "radic": 0x221A, - "prop": 0x221D, - "infin": 0x221E, - "ang": 0x2220, - "and": 0x2227, - "or": 0x2228, - "cap": 0x2229, - "cup": 0x222A, - "int": 0x222B, - "there4": 0x2234, - "sim": 0x223C, - "cong": 0x2245, - "asymp": 0x2248, - "ne": 0x2260, - "equiv": 0x2261, - "le": 0x2264, - "ge": 0x2265, - "sub": 0x2282, - "sup": 0x2283, - "nsub": 0x2284, - "sube": 0x2286, - "supe": 0x2287, - "oplus": 0x2295, - "otimes": 0x2297, - "perp": 0x22A5, - "sdot": 0x22C5, - "vellip": 0x22EE, - "lceil": 0x2308, - "rceil": 0x2309, - "lfloor": 0x230A, - "rfloor": 0x230B, - "lang": 0x2329, - "rang": 0x232A, - "loz": 0x25CA, - "spades": 0x2660, - "clubs": 0x2663, - "hearts": 0x2665, - "diams": 0x2666 + "quot": 0x0022, + "amp": 0x0026, + "apos": 0x0027, + "lt": 0x003C, + "gt": 0x003E, + "nbsp": 0x00A0, + "iexcl": 0x00A1, + "cent": 0x00A2, + "pound": 0x00A3, + "curren": 0x00A4, + "yen": 0x00A5, + "brvbar": 0x00A6, + "sect": 0x00A7, + "uml": 0x00A8, + "copy": 0x00A9, + "ordf": 0x00AA, + "laquo": 0x00AB, + "not": 0x00AC, + "shy": 0x00AD, + "reg": 0x00AE, + "macr": 0x00AF, + "deg": 0x00B0, + "plusmn": 0x00B1, + "sup2": 0x00B2, + "sup3": 0x00B3, + "acute": 0x00B4, + "micro": 0x00B5, + "para": 0x00B6, + "middot": 0x00B7, + "cedil": 0x00B8, + "sup1": 0x00B9, + "ordm": 0x00BA, + "raquo": 0x00BB, + "frac14": 0x00BC, + "frac12": 0x00BD, + "frac34": 0x00BE, + "iquest": 0x00BF, + "Agrave": 0x00C0, + "Aacute": 0x00C1, + "Acirc": 0x00C2, + "Atilde": 0x00C3, + "Auml": 0x00C4, + "Aring": 0x00C5, + "AElig": 0x00C6, + "Ccedil": 0x00C7, + "Egrave": 0x00C8, + "Eacute": 0x00C9, + "Ecirc": 0x00CA, + "Euml": 0x00CB, + "Igrave": 0x00CC, + "Iacute": 0x00CD, + "Icirc": 0x00CE, + "Iuml": 0x00CF, + "ETH": 0x00D0, + "Ntilde": 0x00D1, + "Ograve": 0x00D2, + "Oacute": 0x00D3, + "Ocirc": 0x00D4, + "Otilde": 0x00D5, + "Ouml": 0x00D6, + "times": 0x00D7, + "Oslash": 0x00D8, + "Ugrave": 0x00D9, + "Uacute": 0x00DA, + "Ucirc": 0x00DB, + "Uuml": 0x00DC, + "Yacute": 0x00DD, + "THORN": 0x00DE, + "szlig": 0x00DF, + "agrave": 0x00E0, + "aacute": 0x00E1, + "acirc": 0x00E2, + "atilde": 0x00E3, + "auml": 0x00E4, + "aring": 0x00E5, + "aelig": 0x00E6, + "ccedil": 0x00E7, + "egrave": 0x00E8, + "eacute": 0x00E9, + "ecirc": 0x00EA, + "euml": 0x00EB, + "igrave": 0x00EC, + "iacute": 0x00ED, + "icirc": 0x00EE, + "iuml": 0x00EF, + "eth": 0x00F0, + "ntilde": 0x00F1, + "ograve": 0x00F2, + "oacute": 0x00F3, + "ocirc": 0x00F4, + "otilde": 0x00F5, + "ouml": 0x00F6, + "divide": 0x00F7, + "oslash": 0x00F8, + "ugrave": 0x00F9, + "uacute": 0x00FA, + "ucirc": 0x00FB, + "uuml": 0x00FC, + "yacute": 0x00FD, + "thorn": 0x00FE, + "yuml": 0x00FF, + "OElig": 0x0152, + "oelig": 0x0153, + "Scaron": 0x0160, + "scaron": 0x0161, + "Yuml": 0x0178, + "fnof": 0x0192, + "circ": 0x02C6, + "tilde": 0x02DC, + "Alpha": 0x0391, + "Beta": 0x0392, + "Gamma": 0x0393, + "Delta": 0x0394, + "Epsilon": 0x0395, + "Zeta": 0x0396, + "Eta": 0x0397, + "Theta": 0x0398, + "Iota": 0x0399, + "Kappa": 0x039A, + "Lambda": 0x039B, + "Mu": 0x039C, + "Nu": 0x039D, + "Xi": 0x039E, + "Omicron": 0x039F, + "Pi": 0x03A0, + "Rho": 0x03A1, + "Sigma": 0x03A3, + "Tau": 0x03A4, + "psilon": 0x03A5, + "Phi": 0x03A6, + "Chi": 0x03A7, + "Psi": 0x03A8, + "Omega": 0x03A9, + "alpha": 0x03B1, + "beta": 0x03B2, + "gamma": 0x03B3, + "delta": 0x03B4, + "epsilon": 0x03B5, + "zeta": 0x03B6, + "eta": 0x03B7, + "theta": 0x03B8, + "iota": 0x03B9, + "kappa": 0x03BA, + "lambda": 0x03BB, + "mu": 0x03BC, + "nu": 0x03BD, + "xi": 0x03BE, + "omicron": 0x03BF, + "pi": 0x03C0, + "rho": 0x03C1, + "sigmaf": 0x03C2, + "sigma": 0x03C3, + "tau": 0x03C4, + "upsilon": 0x03C5, + "phi": 0x03C6, + "chi": 0x03C7, + "psi": 0x03C8, + "omega": 0x03C9, + "thetasym": 0x03D1, + "upsih": 0x03D2, + "piv": 0x03D6, + "ensp": 0x2002, + "emsp": 0x2003, + "thinsp": 0x2009, + "zwnj": 0x200C, + "zwj": 0x200D, + "lrm": 0x200E, + "rlm": 0x200F, + "ndash": 0x2013, + "mdash": 0x2014, + "lsquo": 0x2018, + "rsquo": 0x2019, + "sbquo": 0x201A, + "ldquo": 0x201C, + "rdquo": 0x201D, + "bdquo": 0x201E, + "dagger": 0x2020, + "Dagger": 0x2021, + "bull": 0x2022, + "hellip": 0x2026, + "permil": 0x2030, + "prime": 0x2032, + "Prime": 0x2033, + "lsaquo": 0x2039, + "rsaquo": 0x203A, + "oline": 0x203E, + "frasl": 0x2044, + "euro": 0x20AC, + "image": 0x2111, + "weierp": 0x2118, + "real": 0x211C, + "trade": 0x2122, + "alefsym": 0x2135, + "larr": 0x2190, + "uarr": 0x2191, + "rarr": 0x2192, + "darr": 0x2193, + "harr": 0x2194, + "crarr": 0x21B5, + "lArr": 0x21D0, + "uArr": 0x21D1, + "rArr": 0x21D2, + "dArr": 0x21D3, + "hArr": 0x21D4, + "forall": 0x2200, + "part": 0x2202, + "exist": 0x2203, + "empty": 0x2205, + "nabla": 0x2207, + "isin": 0x2208, + "notin": 0x2209, + "ni": 0x220B, + "prod": 0x220F, + "sum": 0x2211, + "minus": 0x2212, + "lowast": 0x2217, + "radic": 0x221A, + "prop": 0x221D, + "infin": 0x221E, + "ang": 0x2220, + "and": 0x2227, + "or": 0x2228, + "cap": 0x2229, + "cup": 0x222A, + "int": 0x222B, + "there4": 0x2234, + "sim": 0x223C, + "cong": 0x2245, + "asymp": 0x2248, + "ne": 0x2260, + "equiv": 0x2261, + "le": 0x2264, + "ge": 0x2265, + "sub": 0x2282, + "sup": 0x2283, + "nsub": 0x2284, + "sube": 0x2286, + "supe": 0x2287, + "oplus": 0x2295, + "otimes": 0x2297, + "perp": 0x22A5, + "sdot": 0x22C5, + "vellip": 0x22EE, + "lceil": 0x2308, + "rceil": 0x2309, + "lfloor": 0x230A, + "rfloor": 0x230B, + "lang": 0x2329, + "rang": 0x232A, + "loz": 0x25CA, + "spades": 0x2660, + "clubs": 0x2663, + "hearts": 0x2665, + "diams": 0x2666 }; /** * A utility function that accepts a string and converts all the HTML character references * (both named and numeric) into their unicode representation. - * @param {String} inputText text in which HTML entities should be replaced + * @param {String} inputText text in which HTML entities should be replaced. + * @return {String} The converted string. */ exports.htmlEntitiesToUtf8 = function(inputText) { - //http://www.w3.org/TR/html5/syntax.html#character-references - var entitiesPattern = /&(#([xX])?)?([A-Za-z0-9]+);/g; - - return inputText ? inputText.replace(entitiesPattern, function(match, isNumeric, isHex, entityName){ - var replacementCode; - if (isNumeric) { - replacementCode = parseInt(entityName, isHex ? 16 : 10); - } else { - //named entities - if (NAMED_HTML_ENTITIES.hasOwnProperty(entityName)) { - replacementCode = NAMED_HTML_ENTITIES[entityName]; + //http://www.w3.org/TR/html5/syntax.html#character-references + var entitiesPattern = /&(#([xX])?)?([A-Za-z0-9]+);/g; + return inputText ? inputText.replace(entitiesPattern, function(match, isNumeric, isHex, entityName){ + var replacementCode; + if (isNumeric) { + replacementCode = parseInt(entityName, isHex ? 16 : 10); } else { - throw new Error('"' + match + '" is not a valid HTML entity.'); + //named entities + if (NAMED_HTML_ENTITIES.hasOwnProperty(entityName)) { + replacementCode = NAMED_HTML_ENTITIES[entityName]; + } else { + throw new Error('"' + match + '" is not a valid HTML entity.'); + } } - } - return String.fromCharCode(replacementCode); + return String.fromCharCode(replacementCode); }) : inputText; }; \ No newline at end of file diff --git a/hsp/compiler/treebuilder/index.js b/hsp/compiler/treebuilder/index.js index 6f073c1..853ab2e 100644 --- a/hsp/compiler/treebuilder/index.js +++ b/hsp/compiler/treebuilder/index.js @@ -1,20 +1,23 @@ var SyntaxTree = require("./syntaxTree").SyntaxTree; /** - * TODO + * Builds the syntax tree from the block list generated by the parser. + * @param {Object} blockList the blockList + * @return {Object} a JSON structure with the following properties: + * syntaxTree: {Object} the syntax tree + * errors: {Array} the error list */ exports.build = function (blockList) { - var res = {}; + var result = {}; try { - var st = new SyntaxTree(); - st.generateTree(blockList); - // st.displayErrors(); - res = { - syntaxTree : st.tree.content, - errors : st.errors + var syntaxTree = new SyntaxTree(); + syntaxTree.generateTree(blockList); + result = { + syntaxTree : syntaxTree.tree.content, + errors : syntaxTree.errors }; } catch (ex) { - res = { + result = { syntaxTree : null, errors : [{ description : ex.toString(), @@ -23,5 +26,5 @@ exports.build = function (blockList) { }] }; } - return res; + return result; }; diff --git a/hsp/compiler/treebuilder/syntaxTree.js b/hsp/compiler/treebuilder/syntaxTree.js index 9f72ed8..1de0788 100644 --- a/hsp/compiler/treebuilder/syntaxTree.js +++ b/hsp/compiler/treebuilder/syntaxTree.js @@ -4,26 +4,35 @@ var htmlEntitiesToUtf8 = require("./htmlEntities").htmlEntitiesToUtf8; //http://www.w3.org/TR/html-markup/syntax.html#syntax-elements var VOID_HTML_ELEMENTS = { - "area": true, - "base": true, - "br": true, - "col": true, - "command": true, - "embed": true, - "hr": true, - "img": true, - "input": true, - "keygen": true, - "link": true, - "meta": true, - "param": true, - "source": true, - "track": true, - "wbr": true + "area": true, + "base": true, + "br": true, + "col": true, + "command": true, + "embed": true, + "hr": true, + "img": true, + "input": true, + "keygen": true, + "link": true, + "meta": true, + "param": true, + "source": true, + "track": true, + "wbr": true }; +/** + * Checks if an element is a void one. + * @param {String} the element name. + * @return {Boolean} true if the element is a void one. + */ function isVoidElement(elName) { - return VOID_HTML_ELEMENTS.hasOwnProperty(elName.toLowerCase()); + var result = false; + if (elName && elName.toLowerCase) { + result = VOID_HTML_ELEMENTS.hasOwnProperty(elName.toLowerCase()); + } + return result; } /** @@ -36,26 +45,40 @@ var Node = klass({ } }); -var reservedKeywords={ +/** + * A map of the reserved keywords. + */ +var reservedKeywords = { "event": true, "scope": true }; +/** + * The SyntaxTree class made to build the syntax tree from the block list from the parser. + * Entry point: generateTree() + */ var SyntaxTree = klass({ /** - * Generate the syntax tree from the root block list + * Generate the syntax tree from the root block list. + * @param {Object} blockList the block list. */ generateTree : function (blockList) { this.errors = []; this.tree = new Node("file", null); this.tree.content = []; - this._advance(0, blockList, this.tree.content); + this._advance(blockList, 0, this.tree.content); this._postProcessTree(); }, + /** + * Adds an error to the current error list. + * @param {String} description the error description + * @param {Object} errdesc additional object (block, node, ...) which can contain additional info about the error (line/column number, code). + */ _logError : function (description, errdesc) { + //TODO: errdesc is a bit vague var desc = { description : description }; @@ -71,34 +94,30 @@ var SyntaxTree = klass({ this.errors.push(desc); }, - displayErrors : function () { - if (this.errors.length) { - for (var i = 0, sz = this.errors.length; sz > i; i++) { - console.log("Error " + (i + 1) + "/" + sz + ": " + this.errors[i].description); - } - } - }, - /** - * Process a list of blocks and advance the cursor index that scans the collection - * @param {function} optEndFn an optional end function that takes a node type as argument - * @return {int} the index of the block where the function stopped or -1 if all blocks have been handled + * Process a list of blocks and advance the cursor index that scans the collection. + * @param {Array} blocks the full list of blocks. + * @param {Integer} startIndex the index from which the process has to start. + * @param {Array} out the output as an array of Node. + * @param {Function} optEndFn an optional end function that takes a node type as argument. + * @return {Integer} the index of the block where the function stopped or -1 if all blocks have been handled. */ - _advance : function (startIdx, blocks, out, optEndFn) { - var b, type; + _advance : function (blocks, startIndex, out, optEndFn) { + var block, type; if (blocks) { - for (var i = startIdx, sz = blocks.length; sz > i; i++) { - b = blocks[i]; - type = b.type; + for (var i = startIndex; i < blocks.length; i++) { + block = blocks[i]; + type = block.type; - if (optEndFn && optEndFn(type, b.name)) { + if (optEndFn && optEndFn(type, block.name)) { // we stop here return i; } - if (this["_" + type]) { - i = this["_" + type](i, blocks, out); + //by convention, a block of type xyz is managed by a __xyz function in the class + if (this["__" + type]) { + i = this["__" + type](i, blocks, out); } else { - this._logError("Invalid statement: " + type, b); + this._logError("Invalid statement: " + type, block); } } return blocks.length; @@ -106,532 +125,627 @@ var SyntaxTree = klass({ }, /** - * Post validation once the tree is properly parsed + * Post validation once the tree is properly parsed. */ - _postProcessTree:function(nodelist) { - var nodes=this.tree.content; - for (var i=0,sz=nodes.length;sz>i;i++) { - if (nodes[i].type==="template") { + _postProcessTree : function() { + var nodes = this.tree.content; + for (var i = 0; i < nodes.length; i++) { + if (nodes[i].type === "template") { this._processNodeContent(nodes[i].content,nodes[i]); } } }, /** - * Validate the content of a container node - * @param {Array} nodelist the content of a container node + * Validates the content of a container node. + * @param {Array} nodeList the content of a container node * @param {Node} parent the parent node */ - _processNodeContent:function(nodelist,parent) { + _processNodeContent:function(nodeList, parent) { // Ensure that {let} nodes are always at the beginning of a containter element - var nd, contentFound=false; // true when a node different from let is found - for (var i=0,sz=nodelist.length;sz>i;i++) { - nd=nodelist[i]; - //console.log(i+":"+nd.type) - if (nd.type==="comment") { + var node, contentFound = false; // true when a node different from let is found + for (var i = 0; i < nodeList.length; i++) { + node = nodeList[i]; + //console.log(i+":"+node.type) + if (node.type === "comment") { continue; } - if (nd.type==="text") { + if (node.type==="text") { // tolerate static white space text - if (nd.value.match(/^\s*$/)) { + if (node.value.match(/^\s*$/)) { continue; } } - if (nd.type==="let") { + if (node.type === "let") { if (contentFound) { // error: let must be defined before any piece of content - this._logError("Let statements must be defined at the beginning of a block",nd); + this._logError("Let statements must be defined at the beginning of a block", node); } else { - parent.needSubScope=true; + parent.needSubScope = true; } } else { - contentFound=true; - if (nd.content) { - this._processNodeContent(nd.content,nd); + contentFound = true; + if (node.content) { + this._processNodeContent(node.content, node); } - if (nd.content1) { - this._processNodeContent(nd.content1,nd); + if (node.content1) { + this._processNodeContent(node.content1, node); } - if (nd.content2) { - this._processNodeContent(nd.content2,nd); + if (node.content2) { + this._processNodeContent(node.content2, node); } } } }, /** - * Template block management + * Manages a template block. + * @param {Array} blocks the full list of blocks. + * @param {Integer} index the index of the block to manage. + * @param {Array} out the output as an array of Node. + * @return {Integer} the index of the block where the function stopped or -1 if all blocks have been handled. */ - _template : function (idx, blocks, out) { - var n = new Node("template"), b = blocks[idx]; - n.name = b.name; - if (b.controller) { - n.controller = b.controller; - n.controller.ref = b.controllerRef; + __template : function (index, blocks, out) { + var node = new Node("template"), block = blocks[index]; + node.name = block.name; + if (block.controller) { + node.controller = block.controller; + node.controller.ref = block.controllerRef; } else { - n.args = b.args; + node.args = block.args; // check args - for (var i=0,sz=n.args.length;sz>i;i++) { - if (reservedKeywords[n.args[i]]) { - this._logError("Reserved keywords cannot be used as template argument: "+n.args[i], b); + for (var i=0; i < node.args.length; i++) { + if (reservedKeywords[node.args[i]]) { + this._logError("Reserved keywords cannot be used as template argument: "+node.args[i], block); } } } - n.export = b.mod === "export"; - n.startLine = b.line; - n.endLine = b.endLine; - n.content = []; - out.push(n); - - if (b.mod !== '' && b.mod !== "export") { - this._logError("Invalid template template modifier: " + b.mod, blocks[idx]); + node.export = block.mod === "export"; + node.startLine = block.line; + node.endLine = block.endLine; + node.content = []; + out.push(node); + + if (block.mod !== '' && block.mod !== "export") { + this._logError("Invalid template template modifier: " + block.mod, blocks[index]); } - if (!b.closed) { - this._logError("Missing end template statement", b); + if (!block.closed) { + this._logError("Missing end template statement", block); } // parse sub-list of blocks - this._advance(0, b.content, n.content); - return idx; + this._advance(block.content, 0, node.content); + return index; }, /** - * Catch invalid template definitions + * Catches invalid template definitions. + * @param {Array} blocks the full list of blocks. + * @param {Integer} index the index of the block to manage. + * @param {Array} out the output as an array of Node. + * @return {Integer} the index of the block where the function stopped or -1 if all blocks have been handled. */ - _invalidtemplate : function (idx, blocks, out) { - this._logError("Invalid template declaration", blocks[idx]); - return idx; + __invalidtemplate : function (index, blocks, out) { + this._logError("Invalid template declaration", blocks[index]); + return index; }, /** - * Text block management + * Manages a text block. + * @param {Array} blocks the full list of blocks. + * @param {Integer} index the index of the block to manage. + * @param {Array} out the output as an array of Node. + * @return {Integer} the index of the block where the function stopped or -1 if all blocks have been handled. */ - _plaintext : function (idx, blocks, out) { - var n = new Node("plaintext"), b = blocks[idx]; - n.value = b.value; - out.push(n); - return idx; + __plaintext : function (index, blocks, out) { + var node = new Node("plaintext"), block = blocks[index]; + node.value = block.value; + out.push(node); + return index; }, /** - * Log statement + * Manages a log statement. + * @param {Array} blocks the full list of blocks. + * @param {Integer} index the index of the block to manage. + * @param {Array} out the output as an array of Node. + * @return {Integer} the index of the block where the function stopped or -1 if all blocks have been handled. */ - _log : function (idx, blocks, out) { - var n = new Node("log"), b = blocks[idx], exprs=[],e; - n.line = b.line; - n.column = b.column; - for (var i=0,sz=b.exprs.length;sz>i;i++) { - e=new HExpression(b.exprs[i], this); - exprs[i]=e.getSyntaxTree(); + __log : function (index, blocks, out) { + var node = new Node("log"), block = blocks[index], exprs = []; + node.line = block.line; + node.column = block.column; + for (var i = 0; i < block.exprs.length; i++) { + var expr = new HExpression(block.exprs[i], this); + exprs[i] = expr.getSyntaxTree(); } - n.exprs=exprs; - out.push(n); - return idx; + node.exprs = exprs; + out.push(node); + return index; }, /** - * Let statement + * Manages a let statement. + * @param {Array} blocks the full list of blocks. + * @param {Integer} index the index of the block to manage. + * @param {Array} out the output as an array of Node. + * @return {Integer} the index of the block where the function stopped or -1 if all blocks have been handled. */ - _let : function (idx, blocks, out) { - var n = new Node("let"), b = blocks[idx], asn=[],e; - n.line = b.line; - n.column = b.column; - for (var i=0,sz=b.assignments.length;sz>i;i++) { - e=new HExpression(b.assignments[i].value, this); - asn.push({identifier:b.assignments[i].identifier, value: e.getSyntaxTree()}); + __let : function (index, blocks, out) { + var node = new Node("let"), block = blocks[index], assignments = []; + node.line = block.line; + node.column = block.column; + for (var i = 0; i < block.assignments.length; i++) { + var expr = new HExpression(block.assignments[i].value, this); + assignments.push({identifier:block.assignments[i].identifier, value: expr.getSyntaxTree()}); } - n.assignments=asn; - out.push(n); - return idx; + node.assignments = assignments ; + out.push(node); + return index; }, /** - * Text block management: regroups adjacent text and expression blocks + * Manages a text block: regroups adjacent text and expression blocks + * @param {Array} blocks the full list of blocks. + * @param {Integer} index the index of the block to manage. + * @param {Array} out the output as an array of Node. + * @return {Integer} the index of the block where the function stopped or -1 if all blocks have been handled. */ - _text : function (idx, blocks, out) { - var sz = blocks.length, idx2 = idx, goahead = (sz > idx2), b, buf = [], n = null, insertIdx = -1; - - while (goahead) { - b = blocks[idx2]; - if (b.type === "text") { - if (b.value !== "") { + __text : function (index, blocks, out) { + var length = blocks.length, buffer = [], insertIndex = -1; + + //Regroups adjacent text and expression blocks by looking at the next ones + var nextIndex = index, goAhead = (length > nextIndex), block; + while (goAhead) { + block = blocks[nextIndex]; + if (block.type === "text") { + if (block.value !== "") { try { - b.value = htmlEntitiesToUtf8(b.value); - buf.push(b); + block.value = htmlEntitiesToUtf8(block.value); + buffer.push(block); } catch (e) { - this._logError(e.message, b); + this._logError(e.message, block); } } - } else if (b.type === "expression") { - if (b.category === "jsexpression") { + } else if (block.type === "expression") { + if (block.category === "jsexpression") { // pre-process expression - var e = new HExpression(b, this); + var expr = new HExpression(block, this); // inject the processed expression in the block list - b = blocks[idx2] = e.getSyntaxTree(); + block = blocks[nextIndex] = expr.getSyntaxTree(); } - if (b.category === "invalidexpression") { - this._logError("Invalid expression", b); - } else if (b.category !== "functionref") { - buf.push(b); + if (block.category === "invalidexpression") { + this._logError("Invalid expression", block); + } else if (block.category !== "functionref") { + buffer.push(block); } else { // this is an insert statement - insertIdx = idx2; - idx2++; // will be handled below - goahead = false; + insertIndex = nextIndex; + nextIndex++; // will be handled below + goAhead = false; } - } else if (b.type === "comment") { + } else if (block.type === "comment") { // ignore comments } else { - goahead = false; + goAhead = false; } - if (goahead) { - idx2++; - goahead = (sz > idx2); + if (goAhead) { + nextIndex++; + goAhead = (length > nextIndex); } } - if (buf.length === 1 && buf[0].type === "text") { + //Manages the adjacent text and expression blocks found + var node = null; + if (buffer.length === 1 && buffer[0].type === "text") { // only one text block - n = new Node("text"); - n.value = buf[0].value; - } else if (buf.length > 0) { - // if buf is composed of only text expressions we concatenate them + node = new Node("text"); + node.value = buffer[0].value; + } else if (buffer.length > 0) { + // if buffer is composed of only text expressions we concatenate them var onlyText=true; - for (var i=0,sz=buf.length;sz>i;i++) { - if (buf[i].type!=="text") { - onlyText=false; + for (var i = 0; i < buffer.length; i++) { + if (buffer[i].type !== "text") { + onlyText = false; break; } } if (onlyText) { var texts=[]; - for (var i=0,sz=buf.length;sz>i;i++) { - texts.push(buf[i].value); + for (var i = 0; i < buffer.length; i++) { + texts.push(buffer[i].value); } - n = new Node("text"); - n.value = texts.join(''); + node = new Node("text"); + node.value = texts.join(''); } else { // an expression or several blocks have to be aggregated - n = new Node("textblock"); - n.content = buf; + node = new Node("textblock"); + node.content = buffer; } } - if (n) { - out.push(n); + if (node) { + out.push(node); } - if (insertIdx > -1) { + if (insertIndex > -1) { // an insert block has to be added after the text block - this._insert(insertIdx, blocks, out); + this.__insert(insertIndex, blocks, out); } // return the last index that was handled - return idx2 > idx ? idx2 - 1 : idx; + return nextIndex > index ? nextIndex - 1 : index; }, /** - * Text block management: regroups adjacent text and expression blocks + * Manages an expression block. + * @param {Array} blocks the full list of blocks. + * @param {Integer} index the index of the block to manage. + * @param {Array} out the output as an array of Node. + * @return {Integer} the index of the block where the function stopped or -1 if all blocks have been handled. */ - _expression : function (idx, blocks, out) { - var b = blocks[idx], cat = b.category; - if (cat === "invalidexpression") { - this._logError("Invalid expression", b); - return idx; + __expression : function (index, blocks, out) { + var block = blocks[index]; + if (block.category === "invalidexpression") { + this._logError("Invalid expression", block); + return index; } - return this._text(idx, blocks, out); + return this.__text(index, blocks, out); }, /** - * Catch invalid expressions + * Catches invalid expressions. + * @param {Array} blocks the full list of blocks. + * @param {Integer} index the index of the block to manage. + * @param {Array} out the output as an array of Node. + * @return {Integer} the index of the block where the function stopped or -1 if all blocks have been handled. */ - _invalidexpression : function (idx, blocks, out) { - this._logError("Invalid expression", blocks[idx]); - return idx; + __invalidexpression : function (index, blocks, out) { + this._logError("Invalid expression", blocks[index]); + return index; }, /** - * Insert + * Manages an insert block. + * @param {Array} blocks the full list of blocks. + * @param {Integer} index the index of the block to manage. + * @param {Array} out the output as an array of Node. + * @return {Integer} the index of the block where the function stopped or -1 if all blocks have been handled. */ - _insert : function (idx, blocks, out) { - var n = new Node("insert"), b = blocks[idx]; - n.path = b.path; - n.args = b.args; + __insert : function (index, blocks, out) { + var node = new Node("insert"), block = blocks[index]; + node.path = block.path; + node.args = block.args; - if (n.path.length > 1) { - this._logError("Long paths for insert statements are not supported yet: " + n.path.join("."), b); + if (node.path.length > 1) { + this._logError("Long paths for insert statements are not supported yet: " + node.path.join("."), block); } else { - out.push(n); + out.push(node); } - return idx; + return index; }, /** - * If block management + * Manages an if block. + * @param {Array} blocks the full list of blocks. + * @param {Integer} index the index of the block to manage. + * @param {Array} out the output as an array of Node. + * @return {Integer} the index of the block where the function stopped or -1 if all blocks have been handled. */ - _if : function (idx, blocks, out) { - var n = new Node("if"), b = blocks[idx], lastValidIdx = idx; - var condition = new HExpression(b.condition, this); - n.condition = condition.getSyntaxTree(); - n.condition.bound = true; - n.content1 = []; - out.push(n); - - var endFound = false, out2 = n.content1, idx2 = idx; - - if (n.condition.type === "invalidexpression") { - this._logError("Invalid if condition", n.condition); + __if : function (index, blocks, out) { + //creates the if node + var node = new Node("if"), block = blocks[index], lastValidIndex = index; + var condition = new HExpression(block.condition, this); + node.condition = condition.getSyntaxTree(); + node.condition.bound = true; + node.content1 = []; + out.push(node); + + var endFound = false, out2 = node.content1; + + if (node.condition.type === "invalidexpression") { + this._logError("Invalid if condition", node.condition); } + //process the content of the if block, until one of the if end is found (i.e. endif, else or elseif), if any while (!endFound) { - idx2 = this._advance(idx2 + 1, blocks, out2, this._ifEndTypes); - if (idx2 < 0 || !blocks[idx2]) { - this._logError("Missing end if statement", blocks[lastValidIdx]); + //fills node.content1 with the next blocks + index = this._advance(blocks, index + 1, out2, this._ifEndTypes); + if (index < 0 || !blocks[index]) { + this._logError("Missing end if statement", blocks[lastValidIndex]); endFound = true; } else { - var type = blocks[idx2].type; + var type = blocks[index].type; if (type === "endif") { endFound = true; } else if (type === "else") { - n.content2 = []; - out2 = n.content2; - lastValidIdx = idx2; + //loop will restrat, filling node.content2 with the next blocks + node.content2 = []; + out2 = node.content2; + lastValidIndex = index; } else if (type === "elseif") { // consider as a standard else statement - n.content2 = []; - out2 = n.content2; - lastValidIdx = idx2; + node.content2 = []; + out2 = node.content2; + lastValidIndex = index; endFound = true; // process as if it were an if node - idx2 = this._if(idx2, blocks, out2); + index = this.__if(index, blocks, out2); } } } - return idx2; + return index; }, /** - * Detects if blocks end types + * Detects if blocks end types. + * @param {String} type the block type. + * @retrun {Boolean} true if the block terminates an if. */ _ifEndTypes : function (type) { return (type === "endif" || type === "else" || type === "elseif"); }, - _endif : function (idx, blocks, out) { - // only called in case of error - var b = blocks[idx]; - this._logError("{/if} statement does not match any {if} block", b); - return idx; + /** + * Manages an endif block. + * @param {Array} blocks the full list of blocks. + * @param {Integer} index the index of the block to manage. + * @param {Array} out the output as an array of Node. + * @return {Integer} the index of the block where the function stopped or -1 if all blocks have been handled. + */ + __endif : function (index, blocks, out) { + // only called in case of error, i.e not digested by __if + var block = blocks[index]; + this._logError("{/if} statement does not match any {if} block", block); + return index; }, - _else : function (idx, blocks, out) { - // only called in case of error - var b = blocks[idx]; - this._logError("{else} statement found outside any {if} block", b); - return idx; + /** + * Manages an else block. + * @param {Array} blocks the full list of blocks. + * @param {Integer} index the index of the block to manage. + * @param {Array} out the output as an array of Node. + * @return {Integer} the index of the block where the function stopped or -1 if all blocks have been handled. + */ + __else : function (index, blocks, out) { + // only called in case of error, i.e not digested by __if + var block = blocks[index]; + this._logError("{else} statement found outside any {if} block", block); + return index; }, - _elseif : function (idx, blocks, out) { - // only called in case of error - var b = blocks[idx]; - this._logError("{else if} statement found outside any {if} block", b); - return idx; + /** + * Manages an elseif block. + * @param {Array} blocks the full list of blocks. + * @param {Integer} index the index of the block to manage. + * @param {Array} out the output as an array of Node. + * @return {Integer} the index of the block where the function stopped or -1 if all blocks have been handled. + */ + __elseif : function (index, blocks, out) { + // only called in case of error, i.e not digested by __if + var block = blocks[index]; + this._logError("{else if} statement found outside any {if} block", block); + return index; }, /** - * Foreach block management + * Manages a foreach block. + * @param {Array} blocks the full list of blocks. + * @param {Integer} index the index of the block to manage. + * @param {Array} out the output as an array of Node. + * @return {Integer} the index of the block where the function stopped or -1 if all blocks have been handled. */ - _foreach : function (idx, blocks, out) { - var n = new Node("foreach"), b = blocks[idx]; - n.item = b.item; - n.key = b.key; - n.collection = b.colref; - n.collection.bound = true; - n.content = []; - out.push(n); - - var idx2 = this._advance(idx + 1, blocks, n.content, this._foreachEndTypes); - if (idx2 < 0 || !blocks[idx2]) { - this._logError("Missing end foreach statement", blocks[idx]); + __foreach : function (index, blocks, out) { + //creates the foreach node + var node = new Node("foreach"), block = blocks[index]; + node.item = block.item; + node.key = block.key; + node.collection = block.colref; + node.collection.bound = true; + node.content = []; + out.push(node); + + //fills node.content with the next blocks, until an endforeach is found, if any + var nextIndex = this._advance(blocks, index + 1, node.content, this._foreachEndTypes); + if (nextIndex < 0 || !blocks[nextIndex]) { + this._logError("Missing end foreach statement", blocks[index]); } - return idx2; + return nextIndex; }, /** - * Detects foreach end + * Detects foreach end. + * @param {String} type the block type. + * @retrun {Boolean} true if the block terminates an foreach. */ _foreachEndTypes : function (type) { return (type === "endforeach"); }, - _endforeach : function (idx, blocks, out) { - // only called in case of error - var b = blocks[idx]; - this._logError("{/foreach} statement does not match any {foreach} block", b); - return idx; + /** + * Manages an endforeach block. + * @param {Array} blocks the full list of blocks. + * @param {Integer} index the index of the block to manage. + * @param {Array} out the output as an array of Node. + * @return {Integer} the index of the block where the function stopped or -1 if all blocks have been handled. + */ + __endforeach : function (index, blocks, out) { + // only called in case of error, i.e not digested by __foreach + var block = blocks[index]; + this._logError("{/foreach} statement does not match any {foreach} block", block); + return index; }, /** - * Element block management + * Manages an element block. + * @param {Array} blocks the full list of blocks. + * @param {Integer} index the index of the block to manage. + * @param {Array} out the output as an array of Node. + * @return {Integer} the index of the block where the function stopped or -1 if all blocks have been handled. */ - _element : function (idx, blocks, out) { - var b = blocks[idx]; - if (isVoidElement(b.name)) { - b.closed=true; + __element : function (index, blocks, out) { + var block = blocks[index]; + if (isVoidElement(block.name)) { + block.closed=true; } - return this._elementOrComponent("element", idx, blocks, out); + return this._elementOrComponent("element", index, blocks, out); }, /** - * Component block management + * Manages a component block. + * @param {Array} blocks the full list of blocks. + * @param {Integer} index the index of the block to manage. + * @param {Array} out the output as an array of Node. + * @return {Integer} the index of the block where the function stopped or -1 if all blocks have been handled. */ - _component : function (idx, blocks, out) { - return this._elementOrComponent("component", idx, blocks, out); + __component : function (index, blocks, out) { + return this._elementOrComponent("component", index, blocks, out); }, /** - * Component attribute block management + * Manages a component attribute block. + * @param {Array} blocks the full list of blocks. + * @param {Integer} index the index of the block to manage. + * @param {Array} out the output as an array of Node. + * @return {Integer} the index of the block where the function stopped or -1 if all blocks have been handled. */ - _cptattribute : function (idx, blocks, out) { - return this._elementOrComponent("cptattribute", idx, blocks, out); + __cptattribute : function (index, blocks, out) { + return this._elementOrComponent("cptattribute", index, blocks, out); }, /** - * Processing function for elements and components - * @arg blockType {String} "element" or "component" + * Processing function for elements, components and component attributes + * @arg blockType {String} "element", "component" or "cptattribute". + * @param {Array} blocks the full list of blocks. + * @param {Integer} index the index of the block to manage. + * @param {Array} out the output as an array of Node. + * @return {Integer} the index of the block where the function stopped or -1 if all blocks have been handled. */ - _elementOrComponent : function (blockType, idx, blocks, out) { - var n = new Node(blockType), b = blocks[idx]; - n.name = b.name; - n.closed = b.closed; - if (b.ref) { + _elementOrComponent : function (blockType, index, blocks, out) { + var node = new Node(blockType), block = blocks[index]; + node.name = block.name; + node.closed = block.closed; + if (block.ref) { // only for components - n.ref = b.ref; + node.ref = block.ref; } // Handle attributes - var atts = b.attributes, att, att2, type, lc; - n.attributes = []; - - for (var i = 0, sz = atts.length; sz > i; i++) { - att = atts[i]; - var sz2 = att.value.length; - - // check if attribute contains uppercase - lc = att.name.toLowerCase(); - if (lc !== att.name) { - // this._logError("Invalid attribute name - must be lower case: "+att.name,b); - // continue; - } + var attributes = block.attributes, attribute, outAttribute; + node.attributes = []; - if (sz2 === 0) { + for (var i = 0; i < attributes.length; i++) { + attribute = attributes[i]; + var length = attribute.value.length; + + if (length === 0) { // this case arises when the attibute is empty - so let's create an empty text node - if (att.value === '') { + if (attribute.value === '') { // attribute has no value - e.g. autocomplete in an input element - att2 = { - name : att.name, + outAttribute = { + name : attribute.name, type : "name", - line : att.line, - column : att.column + line : attribute.line, + column : attribute.column }; - n.attributes.push(att2); + node.attributes.push(outAttribute); continue; } else { - att.value.push({ + attribute.value.push({ type : "text", value : "" }); } - sz2 = 1; + length = 1; } - if (sz2 === 1) { + if (length === 1) { // literal or expression - type = att.value[0].type; + var type = attribute.value[0].type; if (type === "text" || type === "expression") { if (type === "expression") { - var v = att.value[0], cat = v.category; - if (cat === "jsexpression") { + var value = attribute.value[0], category = value.category; + if (category === "jsexpression") { // pre-process expression - var e = new HExpression(v, this); + var expr = new HExpression(value, this); // inject the processed expression in the block list - att.value[0] = e.getSyntaxTree(); - } else if (cat === "invalidexpression") { - this._logError("Invalid expression", v); - } else if (att.name.match(/^on/i) && cat !== "functionref") { - this._logError("Event handler attribute only support function expressions", v); + attribute.value[0] = expr.getSyntaxTree(); + } else if (category === "invalidexpression") { + this._logError("Invalid expression", value); + } else if (attribute.name.match(/^on/i) && category !== "functionref") { + this._logError("Event handler attribute only support function expressions", value); } } - att2 = att.value[0]; - att2.name = att.name; + outAttribute = attribute.value[0]; + outAttribute.name = attribute.name; } else { - this._logError("Invalid attribute type: " + type, att); + this._logError("Invalid attribute type: " + type, attribute); continue; } } else { // length > 1 so attribute is a text block // if attribute is an event handler, raise an error - if (att.name.match(/^on/i)) { - this._logError("Event handler attributes don't support text and expression mix", att); + if (attribute.name.match(/^on/i)) { + this._logError("Event handler attributes don't support text and expression mix", attribute); } // raise errors if we have invalid attributes - for (var j = 0; sz2 > j; j++) { - var v = att.value[j]; - if (v.type === "expression") { - if (v.category === "jsexpression") { + for (var j = 0; j < length; j++) { + var value = attribute.value[j]; + if (value.type === "expression") { + if (value.category === "jsexpression") { // pre-process expression - var e = new HExpression(v, this); + var expr = new HExpression(value, this); // inject the processed expression in the block list - att.value[j] = e.getSyntaxTree(); - } else if (v.category === "invalidexpression") { - this._logError("Invalid expression", v); + attribute.value[j] = expr.getSyntaxTree(); + } else if (value.category === "invalidexpression") { + this._logError("Invalid expression", value); } } } - att2 = { - name : att.name, + outAttribute = { + name : attribute.name, type : "textblock", - content : att.value + content : attribute.value }; } - n.attributes.push(att2); + node.attributes.push(outAttribute); } - n.content = []; - out.push(n); + //fills node.content with the next blocks, until an matching end element is found, if any + node.content = []; + out.push(node); - var idx2 = idx; - if (!b.closed) { - var endFound = false, out2 = n.content, ename = b.name; + if (!block.closed) { + var endFound = false, blockName = block.name; while (!endFound) { - idx2 = this._advance(idx2 + 1, blocks, out2, function (type, name) { - return (type === "end" + blockType); // && name===ename + index = this._advance(blocks, index + 1, node.content, function (type, name) { + return (type === "end" + blockType); // && name===blockName }); - if (idx2 < 0 || !blocks[idx2]) { + if (index < 0 || !blocks[index]) { // we didn't find any endelement or endcomponent - this._logError("Missing end " + blockType + " ", b); + this._logError("Missing end " + blockType + " ", block); endFound = true; } else { // check if the end name is correct - var b2=blocks[idx2]; - if (b2.type==="endelement" || b2.type==="endcptattribute") { - if (blocks[idx2].name !== ename) { - this._logError("Missing end " + blockType + " ", b); - idx2 -= 1; // the current end element/component may be caught by a container element + var endBlock = blocks[index]; + if (endBlock.type === "endelement" || endBlock.type === "endcptattribute") { + if (endBlock.name !== blockName) { + this._logError("Missing end " + blockType + " ", block); + index -= 1; // the current end element/component may be caught by a container element } } else { // endcomponent - var p1=this._getComponentPathAsString(b.ref), p2=this._getComponentPathAsString(b2.ref); - if (p1!==p2) { - this._logError("Missing end component ", b); - idx2 -= 1; // the current end element/component may be caught by a container element + var beginPath = this._getComponentPathAsString(block.ref), endPath = this._getComponentPathAsString(endBlock.ref); + if (beginPath !== endPath) { + this._logError("Missing end component ", block); + index -= 1; // the current end element/component may be caught by a container element } } endFound = true; @@ -639,74 +753,95 @@ var SyntaxTree = klass({ } } - return idx2; + return index; }, /** * Transform a component path into a string - useful for error checking * If path is invalid null is returned * @param {Object} ref the ref structure returned by the PEG parser for components and endcomponents + * @retrun {String} the path as a string */ - _getComponentPathAsString:function(ref) { - if (ref.category!=="objectref" || !ref.path || !ref.path.length || !ref.path.join) { + _getComponentPathAsString : function(ref) { + if (ref.category !== "objectref" || !ref.path || !ref.path.length || !ref.path.join) { return null; } return ref.path.join("."); }, /** - * Catch invalid element errors + * Catches invalid element errors. + * @param {Array} blocks the full list of blocks. + * @param {Integer} index the index of the block to manage. + * @param {Array} out the output as an array of Node. + * @return {Integer} the index of the block where the function stopped or -1 if all blocks have been handled. */ - _invalidelement : function (idx, blocks, out) { + __invalidelement : function (index, blocks, out) { // only called in case of error - var b = blocks[idx]; - var msg="Invalid HTML element syntax"; - if (b.code && b.code.match(/^<\/?\@/gi)) { - msg="Invalid component attribute syntax"; + var block = blocks[index]; + var msg = "Invalid HTML element syntax"; + if (block.code && block.code.match(/^<\/?\@/gi)) { + msg = "Invalid component attribute syntax"; } - this._logError(msg, b); - return idx; + this._logError(msg, block); + return index; }, /** - * Ignore comment blocks + * Ignores comment blocks. + * @param {Array} blocks the full list of blocks. + * @param {Integer} index the index of the block to manage. + * @param {Array} out the output as an array of Node. + * @return {Integer} the index of the block where the function stopped or -1 if all blocks have been handled. */ - _comment : function (idx, blocks, out) { - return idx; + __comment : function (index, blocks, out) { + return index; }, /** - * Capture isolated end elements to raise an error + * Captures isolated end elements to raise an error. + * @param {Array} blocks the full list of blocks. + * @param {Integer} index the index of the block to manage. + * @param {Array} out the output as an array of Node. + * @return {Integer} the index of the block where the function stopped or -1 if all blocks have been handled. */ - _endelement : function (idx, blocks, out) { - // only called in case of error - var b = blocks[idx], nm = b.name; - if (isVoidElement(nm)) { - this._logError("The end element was rejected as <" + nm + "> is a void HTML element and can't have a closing element", b); + __endelement : function (index, blocks, out) { + // only called in case of error, i.e not digested by _elementOrComponent + var block = blocks[index], name = block.name; + if (isVoidElement(name)) { + this._logError("The end element was rejected as <" + name + "> is a void HTML element and can't have a closing element", block); } else { - this._logError("End element does not match any <" + nm + "> element", b); + this._logError("End element does not match any <" + name + "> element", block); } - return idx; + return index; }, /** - * Capture isolated end components to raise an error + * Captures isolated end components to raise an error. + * @param {Array} blocks the full list of blocks. + * @param {Integer} index the index of the block to manage. + * @param {Array} out the output as an array of Node. + * @return {Integer} the index of the block where the function stopped or -1 if all blocks have been handled. */ - _endcomponent: function (idx, blocks, out) { - // only called in case of error - var b = blocks[idx], p = this._getComponentPathAsString(b.ref) ; - this._logError("End component does not match any <#" + p + "> component", b); - return idx; + __endcomponent: function (index, blocks, out) { + // only called in case of error, i.e not digested by _elementOrComponent + var block = blocks[index], path = this._getComponentPathAsString(block.ref) ; + this._logError("End component does not match any <#" + path + "> component", block); + return index; }, /** - * Capture isolated end component attributes to raise an error + * Captures isolated end component attributes to raise an error. + * @param {Array} blocks the full list of blocks. + * @param {Integer} index the index of the block to manage. + * @param {Array} out the output as an array of Node. + * @return {Integer} the index of the block where the function stopped or -1 if all blocks have been handled. */ - _endcptattribute: function (idx, blocks, out) { - // only called in case of error - var b = blocks[idx], p = b.name ; - this._logError("End component attribute does not match any <@" + p + "> component attribute", b); - return idx; + __endcptattribute: function (index, blocks, out) { + // only called in case of error, i.e not digested by _elementOrComponent + var block = blocks[index], name = block.name ; + this._logError("End component attribute does not match any <@" + name + "> component attribute", block); + return index; } });