diff --git a/packages/moon/dist/moon.js b/packages/moon/dist/moon.js index 5071bb9f..c958fd03 100644 --- a/packages/moon/dist/moon.js +++ b/packages/moon/dist/moon.js @@ -22,16 +22,6 @@ component: 2, fragment: 3 }; - /** - * Checks if a given character is a quote. - * - * @param {string} char - * @returns {boolean} True if the character is a quote - */ - - function isQuote(_char) { - return _char === "\"" || _char === "'"; - } /** * Logs an error message to the console. * @param {string} message @@ -74,6 +64,11 @@ return full; } + /** + * Capture whitespace-only text. + */ + + var whitespaceRE = /^\s+$/; /** * Capture the tag name, attribute text, and closing slash from an opening tag. */ @@ -98,12 +93,23 @@ */ var globals = ["NaN", "event", "false", "in", "null", "this", "true", "typeof", "undefined", "window"]; + /** + * Checks if a given character is a quote. + * + * @param {string} char + * @returns {boolean} True if the character is a quote + */ + + function isQuote(_char) { + return _char === "\"" || _char === "'"; + } /** * Scope an expression to use variables within the `data` object. * * @param {string} expression */ + function scopeExpression(expression) { return expression.replace(expressionRE, function (match, name) { return name === undefined || globals.indexOf(name) !== -1 ? match : "data." + name; @@ -191,9 +197,9 @@ var tokens = []; for (var i = 0; i < input.length;) { - var _char = input[i]; + var _char2 = input[i]; - if (_char === "<") { + if (_char2 === "<") { var charNext = input[i + 1]; if ("development" === "development" && charNext === undefined) { @@ -286,18 +292,18 @@ closed: closeSlash === "/" }); i += typeMatch.length; - } else if (_char === "{") { + } else if (_char2 === "{") { // If a sequence of characters begins with "{", process it as an // expression token. var expression = ""; // Consume the input until the end of the expression. for (i += 1; i < input.length; i++) { - var _char2 = input[i]; + var _char3 = input[i]; - if (_char2 === "}") { + if (_char3 === "}") { break; } else { - expression += _char2; + expression += _char3; } } // Append the expression as a `` element with the appropriate // text content attribute. @@ -317,25 +323,27 @@ var text = ""; // Consume the input until the start of a new tag or expression. for (; i < input.length; i++) { - var _char3 = input[i]; + var _char4 = input[i]; - if (_char3 === "<" || _char3 === "{") { + if (_char4 === "<" || _char4 === "{") { break; } else { - text += _char3; + text += _char4; } } // Append the text as a `` element with the appropriate text - // content attribute. + // content attribute if it isn't only whitespace. - tokens.push({ - type: "tagOpen", - value: "text", - attributes: { - "": "\"" + text + "\"" - }, - closed: true - }); + if (!whitespaceRE.test(text)) { + tokens.push({ + type: "tagOpen", + value: "text", + attributes: { + "": "\"" + text + "\"" + }, + closed: true + }); + } } } @@ -531,23 +539,92 @@ } /** - * Generator + * Global variable number + */ + var generateVariable; + /** + * Set variable number to a new number. * - * The generator is responsible for generating a function that creates a view. - * A view could be represented as a normal set of recursive function calls, but - * it uses lightweight objects to represent them instead. This allows the - * executor to execute the function over multiple frames with its own - * representation of the stack. + * @param {number} newGenerateVariable + */ + + function setGenerateVariable(newGenerateVariable) { + generateVariable = newGenerateVariable; + } + + /** + * Generates view function code and prelude code for an `if` element. * * @param {Object} element - * @returns {string} View function code + * @param {Object} parent + * @param {number} index + * @returns {Object} View function code and prelude code */ - function generate(element) { + function generateNodeIf(element, parent, index) { + var variable = "m" + generateVariable; + var prelude = ""; + var emptyElseClause = true; + setGenerateVariable(generateVariable + 1); // Generate the initial `if` clause. + + var generateIf = generateNode(element.children[0], element, 0); + prelude += "var " + variable + ";if(" + element.attributes[""] + "){" + generateIf.prelude + variable + "=" + generateIf.node + ";}"; // Search for `else-if` and `else` clauses if there are siblings. + + if (parent !== null) { + var siblings = parent.children; + + for (var i = index + 1; i < siblings.length; i++) { + var sibling = siblings[i]; + + if (sibling.type === "else-if") { + // Generate the `else-if` clause. + var generateElseIf = generateNode(sibling.children[0], sibling, 0); + prelude += "else if(" + sibling.attributes[""] + "){" + generateElseIf.prelude + variable + "=" + generateElseIf.node + ";}"; // Remove the `else-if` clause so that it isn't generated + // individually by the parent. + + siblings.splice(i, 1); + } else if (sibling.type === "else") { + // Generate the `else` clause. + var generateElse = generateNode(sibling.children[0], sibling, 0); + prelude += "else{" + generateElse.prelude + variable + "=" + generateElse.node + ";}"; // Skip generating the empty `else` clause. + + emptyElseClause = false; // Remove the `else` clause so that it isn't generated + // individually by the parent. + + siblings.splice(i, 1); + } else { + break; + } + } + } // Generate an empty `else` clause represented by an empty text node. + + + if (emptyElseClause) { + prelude += "else{" + variable + "={type:" + types.text + ",name:\"text\",data:{children:[]}};}"; + } + + return { + prelude: prelude, + node: variable + }; + } + + /** + * Generates view function code for a Moon node from an element. + * + * @param {Object} element + * @param {Object} parent + * @param {number} index + * @returns {Object} View function code and prelude code + */ + + function generateNode(element, parent, index) { var name = element.type; - var type; + var type; // Generate the correct type number for the given name. - if (name === "text") { + if (name === "if") { + return generateNodeIf(element, parent, index); + } else if (name === "text") { type = types.text; } else if (name === "fragment") { type = types.fragment; @@ -558,6 +635,7 @@ } var attributes = element.attributes; + var prelude = ""; var data = "{"; var separator = ""; @@ -573,14 +651,43 @@ separator = ""; for (var i = 0; i < children.length; i++) { - data += separator + generate(children[i]); + var childNode = generateNode(children[i], element, i); + prelude += childNode.prelude; + data += separator + childNode.node; separator = ","; } data += "]"; } - return "{type:" + type + ",name:\"" + name + "\",data:" + data + "}}"; + return { + prelude: prelude, + node: "{type:" + type + ",name:\"" + name + "\",data:" + data + "}}" + }; + } + /** + * Generator + * + * The generator is responsible for generating a function that creates a view. + * A view could be represented as a normal set of recursive function calls, but + * it uses lightweight objects to represent them instead. This allows the + * executor to execute the function over multiple frames with its own + * representation of the stack. + * + * @param {Object} element + * @returns {string} View function code + */ + + function generate(element) { + // Reset generator variable. + setGenerateVariable(0); // Generate the root node and get the prelude and node code. + + var _generateNode = generateNode(element, null, 0), + prelude = _generateNode.prelude, + node = _generateNode.node; // Convert the code into a usable function body. + + + return prelude + "return " + node + ";"; } function compile(input) { @@ -603,6 +710,7 @@ var components = {}; /** * Set old view to a new object. + * * @param {Object} viewOld */ @@ -611,6 +719,7 @@ } /** * Set new view to a new object. + * * @param {Object} viewOld */ @@ -619,6 +728,7 @@ } /** * Set current view to a new function. + * * @param {Function} viewCurrentNew */ @@ -1082,7 +1192,7 @@ } if (typeof view === "string") { - view = new Function("data", "return " + compile(view)); + view = new Function("data", compile(view)); } // If a `root` option is given, start the root renderer, or else just return // the component. diff --git a/packages/moon/dist/moon.min.js b/packages/moon/dist/moon.min.js index e1526eb9..16918a55 100644 --- a/packages/moon/dist/moon.min.js +++ b/packages/moon/dist/moon.min.js @@ -4,4 +4,4 @@ * Released under the MIT License * https://kbrsh.github.io/moon */ -!function(e,n){"undefined"==typeof module?e.Moon=n():module.exports=n()}(this,function(){"use strict";var v={element:0,text:1,component:2,fragment:3};var N=/<([\w\d-_]+)([^>]*?)(\/?)>/g,A=/\s*([\w\d-_:@]*)(?:=(?:("[^"]*"|'[^']*')|{([^{}]*)}))?/g,n=/"[^"]*"|'[^']*'|\d+[a-zA-Z$_]\w*|\.[a-zA-Z$_]\w*|[a-zA-Z$_]\w*:|([a-zA-Z$_]\w*)/g,t=["NaN","event","false","in","null","this","true","typeof","undefined","window"];function E(e){return e.replace(n,function(e,n){return void 0===n||-1!==t.indexOf(n)?e:"data."+n})}function r(e){e=e.trim();for(var n=[],t=0;t",t+2),i=e.slice(t+2,o);0,n.push({type:"tagClose",value:i}),t=o+1;continue}if("!"===a&&"-"===e[t+2]&&"-"===e[t+3]){var d=e.indexOf("--\x3e",t+4);0,t=d+3;continue}N.lastIndex=t;var u=N.exec(e);0;for(var l=u[0],p=u[1],f=u[2],s=u[3],c={},v=void 0;null!==(v=A.exec(f));){var h=v[0],m=v[1],g=v[2],w=v[3];0===h.length?A.lastIndex+=1:(c[m]=void 0===w?g:E(w),"@"===m[0]&&(c[m]="function(event){"+c[m]+"}"))}n.push({type:"tagOpen",value:p,attributes:c,closed:"/"===s}),t+=l.length}else if("{"===r){var y="";for(t+=1;t]*?)(\/?)>/g,k=/\s*([\w\d-_:@]*)(?:=(?:("[^"]*"|'[^']*')|{([^{}]*)}))?/g,n=/"[^"]*"|'[^']*'|\d+[a-zA-Z$_]\w*|\.[a-zA-Z$_]\w*|[a-zA-Z$_]\w*:|([a-zA-Z$_]\w*)/g,t=["NaN","event","false","in","null","this","true","typeof","undefined","window"];function E(e){return e.replace(n,function(e,n){return void 0===n||-1!==t.indexOf(n)?e:"data."+n})}function r(e){e=e.trim();for(var n=[],t=0;t",t+2),i=e.slice(t+2,o);0,n.push({type:"tagClose",value:i}),t=o+1;continue}if("!"===a&&"-"===e[t+2]&&"-"===e[t+3]){var d=e.indexOf("--\x3e",t+4);0,t=d+3;continue}A.lastIndex=t;var l=A.exec(e);0;for(var u=l[0],p=l[1],f=l[2],s=l[3],c={},v=void 0;null!==(v=k.exec(f));){var h=v[0],m=v[1],g=v[2],w=v[3];0===h.length?k.lastIndex+=1:(c[m]=void 0===w?g:E(w),"@"===m[0]&&(c[m]="function(event){"+c[m]+"}"))}n.push({type:"tagOpen",value:p,attributes:c,closed:"/"===s}),t+=u.length}else if("{"===r){var y="";for(t+=1;t` element with the appropriate text - // content attribute. - tokens.push({ - type: "tagOpen", - value: "text", - attributes: { - "": `"${text}"` - }, - closed: true - }); + // content attribute if it isn't only whitespace. + if (!whitespaceRE.test(text)) { + tokens.push({ + type: "tagOpen", + value: "text", + attributes: { + "": `"${text}"` + }, + closed: true + }); + } } } diff --git a/packages/moon/src/index.js b/packages/moon/src/index.js index f04d44cf..bbdad1b8 100644 --- a/packages/moon/src/index.js +++ b/packages/moon/src/index.js @@ -46,7 +46,7 @@ export default function Moon(options) { } if (typeof view === "string") { - view = new Function("data", "return " + compile(view)); + view = new Function("data", compile(view)); } // If a `root` option is given, start the root renderer, or else just return diff --git a/packages/moon/src/util/globals.js b/packages/moon/src/util/globals.js index dd282201..a0357d5a 100644 --- a/packages/moon/src/util/globals.js +++ b/packages/moon/src/util/globals.js @@ -15,6 +15,7 @@ export const components = {}; /** * Set old view to a new object. + * * @param {Object} viewOld */ export function setViewOld(viewOldNew) { @@ -23,6 +24,7 @@ export function setViewOld(viewOldNew) { /** * Set new view to a new object. + * * @param {Object} viewOld */ export function setViewNew(viewNewNew) { @@ -31,6 +33,7 @@ export function setViewNew(viewNewNew) { /** * Set current view to a new function. + * * @param {Function} viewCurrentNew */ export function setViewCurrent(viewCurrentNew) { diff --git a/packages/moon/src/util/util.js b/packages/moon/src/util/util.js index e5dab1e7..83423218 100644 --- a/packages/moon/src/util/util.js +++ b/packages/moon/src/util/util.js @@ -8,16 +8,6 @@ export const types = { fragment: 3 }; -/** - * Checks if a given character is a quote. - * - * @param {string} char - * @returns {boolean} True if the character is a quote - */ -export function isQuote(char) { - return char === "\"" || char === "'"; -} - /** * Logs an error message to the console. * @param {string} message