diff --git a/packages/moon/dist/moon.js b/packages/moon/dist/moon.js index 91c8e364..fcd9d5ff 100644 --- a/packages/moon/dist/moon.js +++ b/packages/moon/dist/moon.js @@ -114,23 +114,12 @@ "\n": "\\n", "\r": "\\r" }; - /** - * 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 === "'"; - } /** * Escape text to make it usable in a JavaScript string literal. * * @param {string} text */ - function escapeText(text) { return text.replace(textRE, function (match) { return escapeTextMap[match]; @@ -144,9 +133,22 @@ function scopeExpression(expression) { - return expression.replace(expressionRE, function (match, name) { - return name === undefined || name[0] === "$" || globals.indexOf(name) !== -1 ? match : "data." + name; + var isStatic = true; + var value = expression.replace(expressionRE, function (match, name) { + if (name === undefined || globals.indexOf(name) !== -1) { + // Return a static match if there are no dynamic names or if it is a + // global variable. + return match; + } else { + // Return a dynamic match if there is a dynamic name or a local. + isStatic = false; + return name[0] === "$" ? match : "data." + name; + } }); + return { + value: value, + isStatic: isStatic + }; } /** * Convert a token into a string, accounting for `` components. @@ -163,10 +165,10 @@ // and doesn't need the quotes. If not, it was an expression and // needs to be formatted with curly braces. - if (isQuote(content[0])) { - return content.slice(1, -1); + if (content.isStatic) { + return content.value.slice(1, -1); } else { - return "{" + content + "}"; + return "{" + content.value + "}"; } } else { var tag = "<" + token.value; @@ -174,7 +176,7 @@ for (var attributeKey in attributes) { var attributeValue = attributes[attributeKey]; - tag += " " + attributeKey + "=" + (isQuote(attributeValue[0]) ? attributeValue : "{" + attributeValue + "}"); + tag += " " + attributeKey + "=" + (attributeValue.isStatic ? attributeValue.value : "{" + attributeValue.value + "}"); } if (token.closed) { @@ -230,9 +232,9 @@ var tokens = []; for (var i = 0; i < input.length;) { - var _char2 = input[i]; + var _char = input[i]; - if (_char2 === "<") { + if (_char === "<") { var charNext = input[i + 1]; if ("development" === "development" && charNext === undefined) { @@ -308,10 +310,18 @@ } else { // Store the key/value pair using the matched value or // expression. - attributes[attributeKey] = attributeExpression === undefined ? attributeValue === undefined ? "\"\"" : attributeValue : scopeExpression(attributeExpression); // Add a wrapper function for events. + if (attributeExpression === undefined) { + attributes[attributeKey] = { + value: attributeValue === undefined ? "\"\"" : attributeValue, + isStatic: true + }; + } else { + attributes[attributeKey] = scopeExpression(attributeExpression); + } // Add a wrapper function for events. + if (attributeKey[0] === "@") { - attributes[attributeKey] = "function($event){" + attributes[attributeKey] + "}"; + attributes[attributeKey].value = "function($event){" + attributes[attributeKey].value + "}"; } } } // Append an opening tag token with the name, attributes, and optional @@ -325,18 +335,18 @@ closed: closeSlash === "/" }); i += nameMatch.length; - } else if (_char2 === "{") { + } else if (_char === "{") { // 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 _char3 = input[i]; + var _char2 = input[i]; - if (_char3 === "}") { + if (_char2 === "}") { break; } else { - expression += _char3; + expression += _char2; } } // Append the expression as a `` element with the appropriate // text content attribute. @@ -356,12 +366,12 @@ var text = ""; // Consume the input until the start of a new tag or expression. for (; i < input.length; i++) { - var _char4 = input[i]; + var _char3 = input[i]; - if (_char4 === "<" || _char4 === "{") { + if (_char3 === "<" || _char3 === "{") { break; } else { - text += _char4; + text += _char3; } } // Append the text as a `` element with the appropriate text // content attribute if it isn't only whitespace. @@ -372,7 +382,10 @@ type: "tagOpen", value: "text", attributes: { - "": "\"" + escapeText(text) + "\"" + "": { + value: "\"" + escapeText(text) + "\"", + isStatic: true + } }, closed: true }); @@ -622,11 +635,12 @@ function generateNodeIf(element, parent, index, staticNodes) { var variable = "m" + generateVariable; + var attributes = element.attributes; var prelude = ""; var emptyElseClause = true; setGenerateVariable(generateVariable + 1); // Generate the initial `if` clause. - prelude += "var " + variable + ";if(" + element.attributes[""] + "){" + generateClause(variable, element, staticNodes) + "}"; // Search for `else-if` and `else` clauses if there are siblings. + prelude += "var " + variable + ";if(" + attributes[""].value + "){" + generateClause(variable, element, staticNodes) + "}"; // Search for `else-if` and `else` clauses if there are siblings. if (parent !== null) { var siblings = parent.children; @@ -636,7 +650,7 @@ if (sibling.name === "else-if") { // Generate the `else-if` clause. - prelude += "else if(" + sibling.attributes[""] + "){" + generateClause(variable, sibling, staticNodes) + "}"; // Remove the `else-if` clause so that it isn't generated + prelude += "else if(" + attributes[""].value + "){" + generateClause(variable, sibling, staticNodes) + "}"; // Remove the `else-if` clause so that it isn't generated // individually by the parent. siblings.splice(i, 1); @@ -681,9 +695,10 @@ function generateNodeFor(element, staticNodes) { var variable = "m" + generateVariable; - var dataLocals = element.attributes[""].split(","); - var dataArray = element.attributes.of; - var dataObject = element.attributes["in"]; + var attributes = element.attributes; + var dataLocals = attributes[""].value.split(","); + var dataArray = attributes.of; + var dataObject = attributes["in"]; var dataKey; var dataValue; var prelude; @@ -704,6 +719,7 @@ // Generate a `for` loop over an object. The first local is the key and // the second is the value. var dataObjectValue; + dataObject = dataObject.value; dataKey = dataLocals[0]; if (dataLocals.length === 2) { @@ -717,6 +733,7 @@ } else { // Generate a `for` loop over an array. The first local is the value and // the second is the key (index). + dataArray = dataArray.value; dataKey = dataLocals.length === 2 ? dataLocals[1] : "mi"; dataValue = dataLocals[0]; prelude = "for(var " + dataKey + "=0;" + dataKey + "<" + dataArray + ".length;" + dataKey + "++){var " + dataValue + "=" + dataArray + "[" + dataKey + "];" + body + "}"; @@ -765,11 +782,11 @@ var attributeValue = attributes[attribute]; // Mark the current node as dynamic if there are any events or dynamic // attributes. - if (attribute[0] === "@" || attributeValue[0] !== "\"" && attributeValue[0] !== "'") { + if (attribute[0] === "@" || !attributeValue.isStatic) { isStatic = false; } - data += separator + "\"" + attribute + "\":" + attributeValue; + data += separator + "\"" + attribute + "\":" + attributeValue.value; separator = ","; } diff --git a/packages/moon/dist/moon.min.js b/packages/moon/dist/moon.min.js index 78cf4a35..5a3ddbc5 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 b={element:0,text:1,component:2};var y,A=/^\s+$/,k=/<([\w\d-_]+)([^>]*?)(\/?)>/g,S=/\s*([\w\d-_:@]*)(?:=(?:("[^"]*"|'[^']*')|{([^{}]*)}))?/g,n=/"[^"]*"|'[^']*'|\d+[a-zA-Z$_]\w*|\.[a-zA-Z$_]\w*|[a-zA-Z$_]\w*:|([a-zA-Z$_]\w*)/g,C=/&|>|<| |"|\\|"|\n|\r/g,t=["NaN","false","in","null","this","true","typeof","undefined","window"],P={"&":"&",">":">","<":"<"," ":" ",""":'\\"',"\\":"\\\\",'"':'\\"',"\n":"\\n","\r":"\\r"};function q(e){return e.replace(n,function(e,n){return void 0===n||"$"===n[0]||-1!==t.indexOf(n)?e:"data."+n})}function r(e){e=e.trim();for(var n,t=[],r=0;r",r+2),u=e.slice(r+2,i);0,t.push({type:"tagClose",value:u}),r=i+1;continue}if("!"===o&&"-"===e[r+2]&&"-"===e[r+3]){var l=e.indexOf("--\x3e",r+4);0,r=l+3;continue}k.lastIndex=r;var d=k.exec(e);0;for(var f=d[0],s=d[1],p=d[2],c=d[3],v={},h=void 0;null!==(h=S.exec(p));){var m=h[0],g=h[1],w=h[2],b=h[3];0===m.length?S.lastIndex+=1:(v[g]=void 0===b?void 0===w?'""':w:q(b),"@"===g[0]&&(v[g]="function($event){"+v[g]+"}"))}t.push({type:"tagOpen",value:s,attributes:v,closed:"/"===c}),r+=f.length}else if("{"===a){var y="";for(r+=1;r]*?)(\/?)>/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,C=/&|>|<| |"|\\|"|\n|\r/g,r=["NaN","false","in","null","this","true","typeof","undefined","window"],P={"&":"&",">":">","<":"<"," ":" ",""":'\\"',"\\":"\\\\",'"':'\\"',"\n":"\\n","\r":"\\r"};function q(e){var t=!0;return{value:e.replace(n,function(e,n){return void 0===n||-1!==r.indexOf(n)?e:(t=!1,"$"===n[0]?e:"data."+n)}),isStatic:t}}function t(e){e=e.trim();for(var n,t=[],r=0;r",r+2),l=e.slice(r+2,i);0,t.push({type:"tagClose",value:l}),r=i+1;continue}if("!"===o&&"-"===e[r+2]&&"-"===e[r+3]){var u=e.indexOf("--\x3e",r+4);0,r=u+3;continue}A.lastIndex=r;var d=A.exec(e);0;for(var f=d[0],v=d[1],s=d[2],p=d[3],c={},h=void 0;null!==(h=k.exec(s));){var m=h[0],g=h[1],w=h[2],b=h[3];0===m.length?k.lastIndex+=1:(c[g]=void 0===b?{value:void 0===w?'""':w,isStatic:!0}:q(b),"@"===g[0]&&(c[g].value="function($event){"+c[g].value+"}"))}t.push({type:"tagOpen",value:v,attributes:c,closed:"/"===p}),r+=f.length}else if("{"===a){var y="";for(r+=1;r - ( - name === undefined || - name[0] === "$" || - globals.indexOf(name) !== -1 - ) ? - match : - "data." + name - ); + let isStatic = true; + + const value = expression.replace(expressionRE, (match, name) => { + if (name === undefined || globals.indexOf(name) !== -1) { + // Return a static match if there are no dynamic names or if it is a + // global variable. + return match; + } else { + // Return a dynamic match if there is a dynamic name or a local. + isStatic = false; + return name[0] === "$" ? match : "data." + name; + } + }); + + return { + value, + isStatic + }; } /** @@ -99,10 +98,10 @@ export function tokenString(token) { // If the text content is surrounded with quotes, it was normal text // and doesn't need the quotes. If not, it was an expression and // needs to be formatted with curly braces. - if (isQuote(content[0])) { - return content.slice(1, -1); + if (content.isStatic) { + return content.value.slice(1, -1); } else { - return `{${content}}`; + return `{${content.value}}`; } } else { let tag = "<" + token.value; @@ -110,7 +109,7 @@ export function tokenString(token) { for (let attributeKey in attributes) { const attributeValue = attributes[attributeKey]; - tag += ` ${attributeKey}=${isQuote(attributeValue[0]) ? attributeValue : `{${attributeValue}}`}`; + tag += ` ${attributeKey}=${attributeValue.isStatic ? attributeValue.value : `{${attributeValue.value}}`}`; } if (token.closed) { @@ -184,7 +183,6 @@ export function lex(input) { if (charNext === "/") { // Append a closing tag token if a sequence of characters begins // with "", i + 2); const name = input.slice(i + 2, closeIndex); @@ -259,16 +257,18 @@ export function lex(input) { } else { // Store the key/value pair using the matched value or // expression. - attributes[attributeKey] = - attributeExpression === undefined ? - attributeValue === undefined ? - "\"\"" : - attributeValue : - scopeExpression(attributeExpression); + if (attributeExpression === undefined) { + attributes[attributeKey] = { + value: attributeValue === undefined ? "\"\"" : attributeValue, + isStatic: true + }; + } else { + attributes[attributeKey] = scopeExpression(attributeExpression); + } // Add a wrapper function for events. if (attributeKey[0] === "@") { - attributes[attributeKey] = `function($event){${attributes[attributeKey]}}`; + attributes[attributeKey].value = `function($event){${attributes[attributeKey].value}}`; } } } @@ -333,7 +333,10 @@ export function lex(input) { type: "tagOpen", value: "text", attributes: { - "": `"${escapeText(text)}"` + "": { + value: `"${escapeText(text)}"`, + isStatic: true + } }, closed: true }); diff --git a/packages/moon/test/compiler/lexer.test.js b/packages/moon/test/compiler/lexer.test.js index 9ce1e2fc..0b0c6628 100644 --- a/packages/moon/test/compiler/lexer.test.js +++ b/packages/moon/test/compiler/lexer.test.js @@ -13,7 +13,7 @@ test("lex self closing tag", () => { }); test("lex text", () => { - expect(lex(`text test`)).toEqual([{"attributes": {"": "\"text test\""}, "closed": true, "type": "tagOpen", "value": "text"}]); + expect(lex(`text test`)).toEqual([{"attributes": {"": {"value": "\"text test\"", "isStatic": true}}, "closed": true, "type": "tagOpen", "value": "text"}]); }); test("lex text inside tag", () => { @@ -28,7 +28,10 @@ test("lex text inside tag", () => { "type": "tagOpen", "value": "text", "attributes": { - "": "\"text\"" + "": { + "value": "\"text\"", + "isStatic": true + } }, "closed": true }, @@ -40,15 +43,15 @@ test("lex text inside tag", () => { }) test("lex expression", () => { - expect(lex(`{data + 1}`)).toEqual([{"attributes": {"": "data.data + 1"}, "closed": true, "type": "tagOpen", "value": "text"}]); + expect(lex(`{data + 1}`)).toEqual([{"attributes": {"": {"value": "data.data + 1", isStatic: false}}, "closed": true, "type": "tagOpen", "value": "text"}]); }); test("lex attributes", () => { - expect(lex(`
`)).toEqual([{"attributes": {"id": "\"test-id\"", "class": "'test-class'", dynamic: "true", self: "\"\""}, "closed": false, "type": "tagOpen", "value": "div"}]); + expect(lex(`
`)).toEqual([{"attributes": {"id": {"value": "\"test-id\"", "isStatic": true}, "class": {"value": "'test-class'", "isStatic": true}, "dynamic": {"value": "true", "isStatic": true}, "local": {"value": "$local", "isStatic": false}, "self": {"value": "\"\"", "isStatic": true}}, "closed": false, "type": "tagOpen", "value": "div"}]); }); test("lex events", () => { - expect(lex(`
`)).toEqual([{"attributes": {"id": "\"test-id\"", "class": "'test-class'", dynamic: "true", "@event": "function($event){data.doSomething()}", self: "\"\""}, "closed": false, "type": "tagOpen", "value": "div"}]); + expect(lex(`
`)).toEqual([{"attributes": {"id": {"value": "\"test-id\"", "isStatic": true}, "class": {"value": "'test-class'", "isStatic": true}, dynamic: {"value": "true", "isStatic": true}, "@event": {"value": "function($event){data.doSomething()}", "isStatic": false}, self: {"value": "\"\"", "isStatic": true}}, "closed": false, "type": "tagOpen", "value": "div"}]); }); test("lex comments", () => { diff --git a/packages/moon/test/compiler/parser.test.js b/packages/moon/test/compiler/parser.test.js index ef204949..3ff79c36 100644 --- a/packages/moon/test/compiler/parser.test.js +++ b/packages/moon/test/compiler/parser.test.js @@ -17,7 +17,10 @@ test("parse text element", () => { expect(parseTest(`test text`)).toEqual({ "name": "text", "attributes": { - "": `"test text"` + "": { + value: `"test text"`, + isStatic: true + } }, "children": [] }); @@ -32,7 +35,7 @@ test("parse nested elements", () => { `)).toEqual({ "name": "div", "attributes": { - "dynamic": "true" + "dynamic": {"value": "true", "isStatic": true} }, "children": [ { @@ -42,7 +45,7 @@ test("parse nested elements", () => { { "name": "text", "attributes": { - "": "\"Title\"" + "": {"value": "\"Title\"", "isStatic": true} }, "children": [] } @@ -51,13 +54,13 @@ test("parse nested elements", () => { { "name": "p", "attributes": { - "color": "\"blue\"" + "color": {"value": "\"blue\"", "isStatic": true} }, "children": [ { "name": "text", "attributes": { - "": "\"Text\"" + "": {"value": "\"Text\"", "isStatic": true} }, "children": [] }