From 981c575d68c03b21f6128e9c085573436b9bdc4c Mon Sep 17 00:00:00 2001 From: Kabir Shah Date: Mon, 25 Nov 2019 22:23:25 -0800 Subject: [PATCH] new MVL, compiler, and router First, this change involves a refactor of some of the module structure of the code to be consistent. Next, there is a new Moon View Language (MVL). It has three types of nodes: * Node: A node that is equivalent to the contents of the expression within it, analogous to a variable reference. - `<{node}#>` * Data Node: A node that is equivalent to a function call that can call a function with an expression directly or attributes to represent an object. - `<{node} {data}/>` - `<{node} foo="bar" bar="baz"/>` * Data & Children Node: A node that is equivalent to a function call with attributes representing an object along with support for children, which can include text, dynamic text blocks, and other nodes. - `<{node} foo="bar" bar="baz">Text {dynamic} ` - `

Hello Moon!

` This means that all nodes must now be functions, so `Moon.view.m` is now an object with keys being HTML elements and a special `text` element which takes a `value` data property. All elements must then usually be imported or declared at the top of files. The new parser uses parser combinators and comes with better error reporting that displays the line of the error, the surrounding lines, and the exact position where something was expected. It also supports full JavaScript syntax along with MVL, *without* needing parentheses around nodes. The new generator is much simpler and only generates function calls or variable references as appropriate for the node type in question. It doesn't do any static function call optimization as the nodes can be dynamic at any time. This can easily be done by the developer and included with a reference using a normal variable reference node. Lastly, work on the router has started. There is a simple file structure set up, a basic driver that changes the route using the `history` API, and a basic `Link` component that creates `a` elements with the data provided. Nothing fancy for now. --- .eslintrc | 1 + packages/moon-browser/dist/moon-browser.js | 1416 ++++------------- .../moon-browser/dist/moon-browser.min.js | 2 +- packages/moon-browser/src/index.js | 2 +- packages/moon-browser/test/browser.test.js | 8 +- packages/moon-cli/src/index.js | 12 +- packages/moon-compiler/dist/moon-compiler.js | 1416 ++++------------- .../moon-compiler/dist/moon-compiler.min.js | 2 +- packages/moon-compiler/src/compile.js | 28 + packages/moon-compiler/src/generate.js | 103 ++ .../src/generator/components/element.js | 20 - .../src/generator/components/for.js | 82 - .../src/generator/components/if.js | 113 -- .../moon-compiler/src/generator/generator.js | 157 -- .../moon-compiler/src/generator/util/util.js | 32 - packages/moon-compiler/src/index.js | 200 +-- packages/moon-compiler/src/lexer/lexer.js | 488 ------ packages/moon-compiler/src/parse.js | 298 ++++ packages/moon-compiler/src/parser/parser.js | 226 --- packages/moon-compiler/src/util.js | 66 + packages/moon-compiler/src/util/util.js | 16 - packages/moon-compiler/test/compiler.test.js | 8 + packages/moon-compiler/test/generator.test.js | 197 ++- packages/moon-compiler/test/lexer.test.js | 127 -- packages/moon-compiler/test/parser.test.js | 145 +- packages/moon-compiler/test/util.test.js | 19 + packages/moon/dist/moon.js | 154 +- packages/moon/dist/moon.min.js | 2 +- packages/moon/src/route/Link.js | 10 + packages/moon/src/route/Router.js | 3 + packages/moon/src/route/driver.js | 24 + packages/moon/src/route/index.js | 9 + packages/moon/src/run.js | 2 +- packages/moon/src/use.js | 2 +- packages/moon/src/view/NodeNew.js | 4 +- packages/moon/src/view/driver.js | 119 +- packages/moon/src/view/m.js | 20 +- packages/moon/test/executor/view.test.js | 87 +- packages/moon/test/index.test.js | 1 + packages/util/{util.js => index.js} | 0 40 files changed, 1604 insertions(+), 4017 deletions(-) create mode 100644 packages/moon-compiler/src/compile.js create mode 100644 packages/moon-compiler/src/generate.js delete mode 100644 packages/moon-compiler/src/generator/components/element.js delete mode 100644 packages/moon-compiler/src/generator/components/for.js delete mode 100644 packages/moon-compiler/src/generator/components/if.js delete mode 100644 packages/moon-compiler/src/generator/generator.js delete mode 100644 packages/moon-compiler/src/generator/util/util.js delete mode 100644 packages/moon-compiler/src/lexer/lexer.js create mode 100644 packages/moon-compiler/src/parse.js delete mode 100644 packages/moon-compiler/src/parser/parser.js create mode 100644 packages/moon-compiler/src/util.js delete mode 100644 packages/moon-compiler/src/util/util.js create mode 100644 packages/moon-compiler/test/compiler.test.js delete mode 100644 packages/moon-compiler/test/lexer.test.js create mode 100644 packages/moon-compiler/test/util.test.js create mode 100644 packages/moon/src/route/Link.js create mode 100644 packages/moon/src/route/Router.js create mode 100644 packages/moon/src/route/driver.js create mode 100644 packages/moon/src/route/index.js rename packages/util/{util.js => index.js} (100%) diff --git a/.eslintrc b/.eslintrc index a80f9f60..9edadbc0 100644 --- a/.eslintrc +++ b/.eslintrc @@ -8,6 +8,7 @@ "node": true }, "rules": { + "arrow-parens": ["error", "as-needed"], "constructor-super": 2, "dot-notation": 2, "eqeqeq": 2, diff --git a/packages/moon-browser/dist/moon-browser.js b/packages/moon-browser/dist/moon-browser.js index dcb39a09..df426897 100644 --- a/packages/moon-browser/dist/moon-browser.js +++ b/packages/moon-browser/dist/moon-browser.js @@ -8,1051 +8,422 @@ "use strict"; /** - * Capture whitespace-only text. + * Matches an identifier character. */ - var whitespaceRE = /^\s+$/; + var identifierRE = /[@$\w.?]/; /** - * See if a character is an unescaped quote. - * - * @param {string} char - * @param {string} charPrevious - * @returns {Boolean} quote status - */ - - function isQuote(_char, charPrevious) { - return charPrevious !== "\\" && (_char === "\"" || _char === "'" || _char === "`"); - } - - /** - * Logs an error message to the console. - * @param {string} message - */ - function error(message) { - console.error("[Moon] ERROR: " + message); - } - - /** - * Capture the variables in expressions to scope them within the data - * parameter. This ignores property names and deep object accesses. - */ - - var expressionRE = /"[^"]*"|'[^']*'|`[^`]*`|\d+[a-zA-Z$_]\w*|\.[a-zA-Z$_]\w*|[a-zA-Z$_]\w*:|([a-zA-Z$_]\w*)/g; - /** - * Capture special characters in text that need to be escaped. - */ - - var textRE = /&|>|<| |"|\\|"|\n|\r/g; - /** - * List of global variables to ignore in expression scoping - */ - - var globals = ["Infinity", "NaN", "break", "case", "catch", "class", "const", "continue", "default", "delete", "do", "else", "extends", "false", "finally", "for", "function", "if", "in", "instanceof", "let", "new", "null", "return", "super", "switch", "this", "throw", "true", "try", "typeof", "undefined", "var", "void", "while", "window"]; - /** - * Map from special characters to a safe format for JavaScript string literals. - */ - - var escapeTextMap = { - "&": "&", - ">": ">", - "<": "<", - " ": " ", - """: "\\\"", - "\\": "\\\\", - "\"": "\\\"", - "\n": "\\n", - "\r": "\\r" - }; - /** - * Check if an expression is static. - * - * @param {string} expression - * @returns {Boolean} static status + * Stores an error message, a slice of tokens associated with the error, and a + * related error for later reporting. */ - function expressionIsStatic(expression) { - var result; - - while ((result = expressionRE.exec(expression)) !== null) { - var name = result[1]; - - if (name !== undefined && globals.indexOf(name) === -1) { - // Reset the last matched index to prevent some sneaky bugs that can - // cause the function to become nondeterministic. - expressionRE.lastIndex = 0; - return false; - } - } - - expressionRE.lastIndex = 0; - return true; + function ParseError(expected, index) { + this.expected = expected; + this.index = index; } /** - * Convert a token into a string, accounting for `` components. - * - * @param {Object} token - * @returns {string} token converted into a string + * Parser combinators */ - function tokenString(token) { - if (token.type === "tagOpen") { - if (token.value === "text") { - var content = token.attributes[""].value; // 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. + var parser = { + type: function type(_type, parse) { + return function (input, index) { + var output = parse(input, index); + return output instanceof ParseError ? output : [{ + type: _type, + value: output[0] + }, output[1]]; + }; + }, + EOF: function EOF(input, index) { + return index === input.length ? ["EOF", index] : new ParseError("EOF", index); + }, + empty: function empty(input, index) { + return ["", index]; + }, + any: function any(input, index) { + return index < input.length ? [input[index], index + 1] : new ParseError("any", index); + }, + character: function character(_character) { + return function (input, index) { + var head = input[index]; + return head === _character ? [head, index + 1] : new ParseError("\"" + _character + "\"", index); + }; + }, + regex: function regex(_regex) { + return function (input, index) { + var head = input[index]; + return head !== undefined && _regex.test(head) ? [head, index + 1] : new ParseError(_regex.toString(), index); + }; + }, + string: function string(_string) { + return function (input, index) { + var indexNew = index + _string.length; + return input.slice(index, indexNew) === _string ? [_string, indexNew] : new ParseError("\"" + _string + "\"", index); + }; + }, + not: function not(strings) { + return function (input, index) { + if (index < input.length) { + for (var i = 0; i < strings.length; i++) { + var string = strings[i]; + + if (input.slice(index, index + string.length) === string) { + return new ParseError("not \"" + string + "\"", index); + } + } - if (content[0] === "\"" && content[content.length - 1] === "\"") { - return content.slice(1, -1); + return [input[index], index + 1]; } else { - return "{" + content + "}"; - } - } else { - var tag = "<" + token.value; - var attributes = token.attributes; - - for (var attributeKey in attributes) { - var attributeValue = attributes[attributeKey]; - tag += " " + attributeKey + "=" + (attributeValue.isStatic ? attributeValue.value : "{" + attributeValue.value + "}"); + return new ParseError("not " + strings.map(JSON.stringify).join(", "), index); } - - if (token.closed) { - tag += "/"; + }; + }, + or: function or(parse1, parse2) { + return function (input, index) { + var output1 = parse1(input, index); + + if (output1 instanceof ParseError) { + var output2 = parse2(input, index); + + if (output2 instanceof ParseError) { + // For now, the first branch is unreachable because all uses of + // "or" in the grammar do not have a valid case where both + // alternates fail where the first one is fails after the the + // second. + + /* istanbul ignore next */ + return output1.index > output2.index ? output1 : output2; + } else { + return output2; + } + } else { + return output1; } + }; + }, + and: function and(parse1, parse2) { + return function (input, index) { + var output1 = parse1(input, index); - return tag + ">"; - } - } else { - return ""; - } - } - /** - * Logs a lexer error message to the console along with the surrounding - * characters. - * - * @param {string} message - * @param {string} input - * @param {number} index - */ - - function lexError(message, input, index) { - var lexMessage = message + "\n\n"; // Show input characters surrounding the source of the error. - - for (var i = Math.max(0, index - 16); i < Math.min(index + 16, input.length); i++) { - lexMessage += input[i]; - } - - error(lexMessage); - } - /** - * Lexer - * - * The lexer is responsible for taking an input view template and converting it - * into a list of tokens. To make the parser's job easier, it does some extra - * processing and handles tag names, attribute key/value pairs, and converting - * text into `` components. - * - * It works by running through the input text and checking for specific initial - * characters such as "<", "{", or any text. After identifying the type of - * token, it processes each part individually until the end of the token. The - * lexer appends the new token to a cumulative list and eventually returns it. - * - * @param {string} input - * @returns {Object[]} list of tokens - */ - - - function lex(input) { - // Remove leading and trailing whitespace because the lexer should only - // accept one element as an input, and whitespace counts as text. - input = input.trim(); - var tokens = []; - - for (var i = 0; i < input.length;) { - var _char = input[i]; - - if (_char === "<") { - var charNext = input[i + 1]; - - if ("development" === "development" && charNext === undefined) { - lexError("Lexer expected a character after \"<\".", input, i); - break; + if (output1 instanceof ParseError) { + return output1; + } else { + var output2 = parse2(input, output1[1]); + return output2 instanceof ParseError ? output2 : [[output1[0], output2[0]], output2[1]]; } + }; + }, + sequence: function sequence(parses) { + return function (input, index) { + var values = []; - if (charNext === "/") { - // Append a closing tag token if a sequence of characters begins - // with "", i + 2); - - var _name = input.slice(i + 2, closeIndex); - - if ("development" === "development" && closeIndex === -1) { - lexError("Lexer expected a closing \">\" after \"", i + 4); - - if ("development" === "development" && _closeIndex === -1) { - lexError("Lexer expected a closing \"-->\" after \"", i + 4); - - if ("development" === "development" && _closeIndex === -1) { - lexError("Lexer expected a closing \"-->\" after \"", i + 4); - - if (process.env.MOON_ENV === "development" && closeIndex === -1) { - lexError(`Lexer expected a closing "-->" after "`)).toEqual([]); -}); - -test("opening tag token to string", () => { - const input = "
"; - expect(tokenString(lex(input)[0])).toBe(input); -}); - -test("opening tag token with attributes to string", () => { - const input = `
`; - expect(tokenString(lex(input)[0])).toBe(input.replace("dynamic", "dynamic")); -}); - -test("self-closing tag token to string", () => { - const input = ``; - expect(tokenString(lex(input)[0])).toBe(input); -}); - -test("self-closing tag token with attributes to string", () => { - const input = ``; - expect(tokenString(lex(input)[0])).toBe(input.replace("dynamic", "dynamic")); -}); - -test("closing tag token to string", () => { - const input = `
`; - expect(tokenString(lex(input)[0])).toBe(input); -}); - -test("text token to string", () => { - const input = `Test Text`; - expect(tokenString(lex(input)[0])).toBe(input); -}); - -test("expression token to string", () => { - const input = `{dynamic + 1}`; - expect(tokenString(lex(input)[0])).toBe(input.replace("dynamic", "dynamic")); -}); - -test("lex error from unclosed opening bracket", () => { - console.error = jest.fn(); - - expect(Array.isArray(lex("
<"))).toBe(true); - expect(console.error).toBeCalled(); -}); - -test("lex error from unclosed closing tag", () => { - console.error = jest.fn(); - - expect(Array.isArray(lex(" { - console.error = jest.fn(); - - expect(Array.isArray(lex("