UNPKG

hyperscript.org

Version:

a small scripting language for the web

1,473 lines (1,352 loc) 307 kB
/** * @typedef {Object} Hyperscript */ (function (self, factory) { const _hyperscript = factory(self) if (typeof exports === 'object' && typeof exports['nodeName'] !== 'string') { module.exports = _hyperscript } else { self['_hyperscript'] = _hyperscript if ('document' in self) self['_hyperscript'].browserInit() } })(typeof self !== 'undefined' ? self : this, (globalScope) => { 'use strict'; /** * @type {Object} * @property {DynamicConverter[]} dynamicResolvers * * @callback DynamicConverter * @param {String} str * @param {*} value * @returns {*} */ const conversions = { dynamicResolvers: [ function(str, value){ if (str === "Fixed") { return Number(value).toFixed(); } else if (str.indexOf("Fixed:") === 0) { let num = str.split(":")[1]; return Number(value).toFixed(parseInt(num)); } } ], String: function (val) { if (val.toString) { return val.toString(); } else { return "" + val; } }, Int: function (val) { return parseInt(val); }, Float: function (val) { return parseFloat(val); }, Number: function (val) { return Number(val); }, Date: function (val) { return new Date(val); }, Array: function (val) { return Array.from(val); }, JSON: function (val) { return JSON.stringify(val); }, Object: function (val) { if (val instanceof String) { val = val.toString(); } if (typeof val === "string") { return JSON.parse(val); } else { return Object.assign({}, val); } }, } const config = { attributes: "_, script, data-script", defaultTransition: "all 500ms ease-in", disableSelector: "[disable-scripting], [data-disable-scripting]", hideShowStrategies: {}, conversions, } class Lexer { static OP_TABLE = { "+": "PLUS", "-": "MINUS", "*": "MULTIPLY", "/": "DIVIDE", ".": "PERIOD", "..": "ELLIPSIS", "\\": "BACKSLASH", ":": "COLON", "%": "PERCENT", "|": "PIPE", "!": "EXCLAMATION", "?": "QUESTION", "#": "POUND", "&": "AMPERSAND", $: "DOLLAR", ";": "SEMI", ",": "COMMA", "(": "L_PAREN", ")": "R_PAREN", "<": "L_ANG", ">": "R_ANG", "<=": "LTE_ANG", ">=": "GTE_ANG", "==": "EQ", "===": "EQQ", "!=": "NEQ", "!==": "NEQQ", "{": "L_BRACE", "}": "R_BRACE", "[": "L_BRACKET", "]": "R_BRACKET", "=": "EQUALS", }; /** * isValidCSSClassChar returns `true` if the provided character is valid in a CSS class. * @param {string} c * @returns boolean */ static isValidCSSClassChar(c) { return Lexer.isAlpha(c) || Lexer.isNumeric(c) || c === "-" || c === "_" || c === ":"; } /** * isValidCSSIDChar returns `true` if the provided character is valid in a CSS ID * @param {string} c * @returns boolean */ static isValidCSSIDChar(c) { return Lexer.isAlpha(c) || Lexer.isNumeric(c) || c === "-" || c === "_" || c === ":"; } /** * isWhitespace returns `true` if the provided character is whitespace. * @param {string} c * @returns boolean */ static isWhitespace(c) { return c === " " || c === "\t" || Lexer.isNewline(c); } /** * positionString returns a string representation of a Token's line and column details. * @param {Token} token * @returns string */ static positionString(token) { return "[Line: " + token.line + ", Column: " + token.column + "]"; } /** * isNewline returns `true` if the provided character is a carriage return or newline * @param {string} c * @returns boolean */ static isNewline(c) { return c === "\r" || c === "\n"; } /** * isNumeric returns `true` if the provided character is a number (0-9) * @param {string} c * @returns boolean */ static isNumeric(c) { return c >= "0" && c <= "9"; } /** * isAlpha returns `true` if the provided character is a letter in the alphabet * @param {string} c * @returns boolean */ static isAlpha(c) { return (c >= "a" && c <= "z") || (c >= "A" && c <= "Z"); } /** * @param {string} c * @param {boolean} [dollarIsOp] * @returns boolean */ static isIdentifierChar(c, dollarIsOp) { return c === "_" || c === "$"; } /** * @param {string} c * @returns boolean */ static isReservedChar(c) { return c === "`" || c === "^"; } /** * @param {Token[]} tokens * @returns {boolean} */ static isValidSingleQuoteStringStart(tokens) { if (tokens.length > 0) { var previousToken = tokens[tokens.length - 1]; if ( previousToken.type === "IDENTIFIER" || previousToken.type === "CLASS_REF" || previousToken.type === "ID_REF" ) { return false; } if (previousToken.op && (previousToken.value === ">" || previousToken.value === ")")) { return false; } } return true; } /** * @param {string} string * @param {boolean} [template] * @returns {Tokens} */ static tokenize(string, template) { var tokens = /** @type {Token[]}*/ []; var source = string; var position = 0; var column = 0; var line = 1; var lastToken = "<START>"; var templateBraceCount = 0; function inTemplate() { return template && templateBraceCount === 0; } while (position < source.length) { if ((currentChar() === "-" && nextChar() === "-" && (Lexer.isWhitespace(nextCharAt(2)) || nextCharAt(2) === "" || nextCharAt(2) === "-")) || (currentChar() === "/" && nextChar() === "/" && (Lexer.isWhitespace(nextCharAt(2)) || nextCharAt(2) === "" || nextCharAt(2) === "/"))) { consumeComment(); } else if (currentChar() === "/" && nextChar() === "*" && (Lexer.isWhitespace(nextCharAt(2)) || nextCharAt(2) === "" || nextCharAt(2) === "*")) { consumeCommentMultiline(); } else { if (Lexer.isWhitespace(currentChar())) { tokens.push(consumeWhitespace()); } else if ( !possiblePrecedingSymbol() && currentChar() === "." && (Lexer.isAlpha(nextChar()) || nextChar() === "{" || nextChar() === "-") ) { tokens.push(consumeClassReference()); } else if ( !possiblePrecedingSymbol() && currentChar() === "#" && (Lexer.isAlpha(nextChar()) || nextChar() === "{") ) { tokens.push(consumeIdReference()); } else if (currentChar() === "[" && nextChar() === "@") { tokens.push(consumeAttributeReference()); } else if (currentChar() === "@") { tokens.push(consumeShortAttributeReference()); } else if (currentChar() === "*" && Lexer.isAlpha(nextChar())) { tokens.push(consumeStyleReference()); } else if (inTemplate() && (Lexer.isAlpha(currentChar()) || currentChar() === "\\")) { tokens.push(consumeTemplateIdentifier()); } else if (!inTemplate() && (Lexer.isAlpha(currentChar()) || Lexer.isIdentifierChar(currentChar()))) { tokens.push(consumeIdentifier()); } else if (Lexer.isNumeric(currentChar())) { tokens.push(consumeNumber()); } else if (!inTemplate() && (currentChar() === '"' || currentChar() === "`")) { tokens.push(consumeString()); } else if (!inTemplate() && currentChar() === "'") { if (Lexer.isValidSingleQuoteStringStart(tokens)) { tokens.push(consumeString()); } else { tokens.push(consumeOp()); } } else if (Lexer.OP_TABLE[currentChar()]) { if (lastToken === "$" && currentChar() === "{") { templateBraceCount++; } if (currentChar() === "}") { templateBraceCount--; } tokens.push(consumeOp()); } else if (inTemplate() || Lexer.isReservedChar(currentChar())) { tokens.push(makeToken("RESERVED", consumeChar())); } else { if (position < source.length) { throw Error("Unknown token: " + currentChar() + " "); } } } } return new Tokens(tokens, [], source); /** * @param {string} [type] * @param {string} [value] * @returns {Token} */ function makeOpToken(type, value) { var token = makeToken(type, value); token.op = true; return token; } /** * @param {string} [type] * @param {string} [value] * @returns {Token} */ function makeToken(type, value) { return { type: type, value: value || "", start: position, end: position + 1, column: column, line: line, }; } function consumeComment() { while (currentChar() && !Lexer.isNewline(currentChar())) { consumeChar(); } consumeChar(); // Consume newline } function consumeCommentMultiline() { while (currentChar() && !(currentChar() === '*' && nextChar() === '/')) { consumeChar(); } consumeChar(); // Consume "*/" consumeChar(); } /** * @returns Token */ function consumeClassReference() { var classRef = makeToken("CLASS_REF"); var value = consumeChar(); if (currentChar() === "{") { classRef.template = true; value += consumeChar(); while (currentChar() && currentChar() !== "}") { value += consumeChar(); } if (currentChar() !== "}") { throw Error("Unterminated class reference"); } else { value += consumeChar(); // consume final curly } } else { while (Lexer.isValidCSSClassChar(currentChar())) { value += consumeChar(); } } classRef.value = value; classRef.end = position; return classRef; } /** * @returns Token */ function consumeAttributeReference() { var attributeRef = makeToken("ATTRIBUTE_REF"); var value = consumeChar(); while (position < source.length && currentChar() !== "]") { value += consumeChar(); } if (currentChar() === "]") { value += consumeChar(); } attributeRef.value = value; attributeRef.end = position; return attributeRef; } function consumeShortAttributeReference() { var attributeRef = makeToken("ATTRIBUTE_REF"); var value = consumeChar(); while (Lexer.isValidCSSIDChar(currentChar())) { value += consumeChar(); } if (currentChar() === '=') { value += consumeChar(); if (currentChar() === '"' || currentChar() === "'") { let stringValue = consumeString(); value += stringValue.value; } else if(Lexer.isAlpha(currentChar()) || Lexer.isNumeric(currentChar()) || Lexer.isIdentifierChar(currentChar())) { let id = consumeIdentifier(); value += id.value; } } attributeRef.value = value; attributeRef.end = position; return attributeRef; } function consumeStyleReference() { var styleRef = makeToken("STYLE_REF"); var value = consumeChar(); while (Lexer.isAlpha(currentChar()) || currentChar() === "-") { value += consumeChar(); } styleRef.value = value; styleRef.end = position; return styleRef; } /** * @returns Token */ function consumeIdReference() { var idRef = makeToken("ID_REF"); var value = consumeChar(); if (currentChar() === "{") { idRef.template = true; value += consumeChar(); while (currentChar() && currentChar() !== "}") { value += consumeChar(); } if (currentChar() !== "}") { throw Error("Unterminated id reference"); } else { consumeChar(); // consume final quote } } else { while (Lexer.isValidCSSIDChar(currentChar())) { value += consumeChar(); } } idRef.value = value; idRef.end = position; return idRef; } /** * @returns Token */ function consumeTemplateIdentifier() { var identifier = makeToken("IDENTIFIER"); var value = consumeChar(); var escd = value === "\\"; if (escd) { value = ""; } while (Lexer.isAlpha(currentChar()) || Lexer.isNumeric(currentChar()) || Lexer.isIdentifierChar(currentChar()) || currentChar() === "\\" || currentChar() === "{" || currentChar() === "}" ) { if (currentChar() === "$" && escd === false) { break; } else if (currentChar() === "\\") { escd = true; consumeChar(); } else { escd = false; value += consumeChar(); } } if (currentChar() === "!" && value === "beep") { value += consumeChar(); } identifier.value = value; identifier.end = position; return identifier; } /** * @returns Token */ function consumeIdentifier() { var identifier = makeToken("IDENTIFIER"); var value = consumeChar(); while (Lexer.isAlpha(currentChar()) || Lexer.isNumeric(currentChar()) || Lexer.isIdentifierChar(currentChar())) { value += consumeChar(); } if (currentChar() === "!" && value === "beep") { value += consumeChar(); } identifier.value = value; identifier.end = position; return identifier; } /** * @returns Token */ function consumeNumber() { var number = makeToken("NUMBER"); var value = consumeChar(); // given possible XXX.YYY(e|E)[-]ZZZ consume XXX while (Lexer.isNumeric(currentChar())) { value += consumeChar(); } // consume .YYY if (currentChar() === "." && Lexer.isNumeric(nextChar())) { value += consumeChar(); } while (Lexer.isNumeric(currentChar())) { value += consumeChar(); } // consume (e|E)[-] if (currentChar() === "e" || currentChar() === "E") { // possible scientific notation, e.g. 1e6 or 1e-6 if (Lexer.isNumeric(nextChar())) { // e.g. 1e6 value += consumeChar(); } else if (nextChar() === "-") { // e.g. 1e-6 value += consumeChar(); // consume the - as well since otherwise we would stop on the next loop value += consumeChar(); } } // consume ZZZ while (Lexer.isNumeric(currentChar())) { value += consumeChar(); } number.value = value; number.end = position; return number; } /** * @returns Token */ function consumeOp() { var op = makeOpToken(); var value = consumeChar(); // consume leading char while (currentChar() && Lexer.OP_TABLE[value + currentChar()]) { value += consumeChar(); } op.type = Lexer.OP_TABLE[value]; op.value = value; op.end = position; return op; } /** * @returns Token */ function consumeString() { var string = makeToken("STRING"); var startChar = consumeChar(); // consume leading quote string.template = startChar === "`"; var value = ""; while (currentChar() && currentChar() !== startChar) { if (currentChar() === "\\") { consumeChar(); // consume escape char and get the next one let nextChar = consumeChar(); if (nextChar === "b") { value += "\b"; } else if (nextChar === "f") { value += "\f"; } else if (nextChar === "n") { value += "\n"; } else if (nextChar === "r") { value += "\r"; } else if (nextChar === "t") { value += "\t"; } else if (nextChar === "v") { value += "\v"; } else if (string.template && nextChar === "$") { value += "\\$"; } else if (nextChar === "x") { const hex = consumeHexEscape(); if (Number.isNaN(hex)) { throw Error("Invalid hexadecimal escape at " + Lexer.positionString(string)); } value += String.fromCharCode(hex); } else { value += nextChar; } } else { value += consumeChar(); } } if (currentChar() !== startChar) { throw Error("Unterminated string at " + Lexer.positionString(string)); } else { consumeChar(); // consume final quote } string.value = value; string.end = position; return string; } /** * @returns number */ function consumeHexEscape() { const BASE = 16; if (!currentChar()) { return NaN; } let result = BASE * Number.parseInt(consumeChar(), BASE); if (!currentChar()) { return NaN; } result += Number.parseInt(consumeChar(), BASE); return result; } /** * @returns string */ function currentChar() { return source.charAt(position); } /** * @returns string */ function nextChar() { return source.charAt(position + 1); } function nextCharAt(number = 1) { return source.charAt(position + number); } /** * @returns string */ function consumeChar() { lastToken = currentChar(); position++; column++; return lastToken; } /** * @returns boolean */ function possiblePrecedingSymbol() { return ( Lexer.isAlpha(lastToken) || Lexer.isNumeric(lastToken) || lastToken === ")" || lastToken === "\"" || lastToken === "'" || lastToken === "`" || lastToken === "}" || lastToken === "]" ); } /** * @returns Token */ function consumeWhitespace() { var whitespace = makeToken("WHITESPACE"); var value = ""; while (currentChar() && Lexer.isWhitespace(currentChar())) { if (Lexer.isNewline(currentChar())) { column = 0; line++; } value += consumeChar(); } whitespace.value = value; whitespace.end = position; return whitespace; } } /** * @param {string} string * @param {boolean} [template] * @returns {Tokens} */ tokenize(string, template) { return Lexer.tokenize(string, template) } } /** * @typedef {Object} Token * @property {string} [type] * @property {string} value * @property {number} [start] * @property {number} [end] * @property {number} [column] * @property {number} [line] * @property {boolean} [op] `true` if this token represents an operator * @property {boolean} [template] `true` if this token is a template, for class refs, id refs, strings */ class Tokens { constructor(tokens, consumed, source) { this.tokens = tokens this.consumed = consumed this.source = source this.consumeWhitespace(); // consume initial whitespace } get list() { return this.tokens } /** @type Token | null */ _lastConsumed = null; consumeWhitespace() { while (this.token(0, true).type === "WHITESPACE") { this.consumed.push(this.tokens.shift()); } } /** * @param {Tokens} tokens * @param {*} error * @returns {never} */ raiseError(tokens, error) { Parser.raiseParseError(tokens, error); } /** * @param {string} value * @returns {Token} */ requireOpToken(value) { var token = this.matchOpToken(value); if (token) { return token; } else { this.raiseError(this, "Expected '" + value + "' but found '" + this.currentToken().value + "'"); } } /** * @param {string} op1 * @param {string} [op2] * @param {string} [op3] * @returns {Token | void} */ matchAnyOpToken(op1, op2, op3) { for (var i = 0; i < arguments.length; i++) { var opToken = arguments[i]; var match = this.matchOpToken(opToken); if (match) { return match; } } } /** * @param {string} op1 * @param {string} [op2] * @param {string} [op3] * @returns {Token | void} */ matchAnyToken(op1, op2, op3) { for (var i = 0; i < arguments.length; i++) { var opToken = arguments[i]; var match = this.matchToken(opToken); if (match) { return match; } } } /** * @param {string} value * @returns {Token | void} */ matchOpToken(value) { if (this.currentToken() && this.currentToken().op && this.currentToken().value === value) { return this.consumeToken(); } } /** * @param {string} type1 * @param {string} [type2] * @param {string} [type3] * @param {string} [type4] * @returns {Token} */ requireTokenType(type1, type2, type3, type4) { var token = this.matchTokenType(type1, type2, type3, type4); if (token) { return token; } else { this.raiseError(this, "Expected one of " + JSON.stringify([type1, type2, type3])); } } /** * @param {string} type1 * @param {string} [type2] * @param {string} [type3] * @param {string} [type4] * @returns {Token | void} */ matchTokenType(type1, type2, type3, type4) { if ( this.currentToken() && this.currentToken().type && [type1, type2, type3, type4].indexOf(this.currentToken().type) >= 0 ) { return this.consumeToken(); } } /** * @param {string} value * @param {string} [type] * @returns {Token} */ requireToken(value, type) { var token = this.matchToken(value, type); if (token) { return token; } else { this.raiseError(this, "Expected '" + value + "' but found '" + this.currentToken().value + "'"); } } peekToken(value, peek, type) { peek = peek || 0; type = type || "IDENTIFIER"; if(this.tokens[peek] && this.tokens[peek].value === value && this.tokens[peek].type === type){ return this.tokens[peek]; } } /** * @param {string} value * @param {string} [type] * @returns {Token | void} */ matchToken(value, type) { if (this.follows.indexOf(value) !== -1) { return; // disallowed token here } type = type || "IDENTIFIER"; if (this.currentToken() && this.currentToken().value === value && this.currentToken().type === type) { return this.consumeToken(); } } /** * @returns {Token} */ consumeToken() { var match = this.tokens.shift(); this.consumed.push(match); this._lastConsumed = match; this.consumeWhitespace(); // consume any whitespace return match; } /** * @param {string | null} value * @param {string | null} [type] * @returns {Token[]} */ consumeUntil(value, type) { /** @type Token[] */ var tokenList = []; var currentToken = this.token(0, true); while ( (type == null || currentToken.type !== type) && (value == null || currentToken.value !== value) && currentToken.type !== "EOF" ) { var match = this.tokens.shift(); this.consumed.push(match); tokenList.push(currentToken); currentToken = this.token(0, true); } this.consumeWhitespace(); // consume any whitespace return tokenList; } /** * @returns {string} */ lastWhitespace() { if (this.consumed[this.consumed.length - 1] && this.consumed[this.consumed.length - 1].type === "WHITESPACE") { return this.consumed[this.consumed.length - 1].value; } else { return ""; } } consumeUntilWhitespace() { return this.consumeUntil(null, "WHITESPACE"); } /** * @returns {boolean} */ hasMore() { return this.tokens.length > 0; } /** * @param {number} n * @param {boolean} [dontIgnoreWhitespace] * @returns {Token} */ token(n, dontIgnoreWhitespace) { var /**@type {Token}*/ token; var i = 0; do { if (!dontIgnoreWhitespace) { while (this.tokens[i] && this.tokens[i].type === "WHITESPACE") { i++; } } token = this.tokens[i]; n--; i++; } while (n > -1); if (token) { return token; } else { return { type: "EOF", value: "<<<EOF>>>", }; } } /** * @returns {Token} */ currentToken() { return this.token(0); } /** * @returns {Token | null} */ lastMatch() { return this._lastConsumed; } /** * @returns {string} */ static sourceFor = function () { return this.programSource.substring(this.startToken.start, this.endToken.end); } /** * @returns {string} */ static lineFor = function () { return this.programSource.split("\n")[this.startToken.line - 1]; } follows = []; pushFollow(str) { this.follows.push(str); } popFollow() { this.follows.pop(); } clearFollows() { var tmp = this.follows; this.follows = []; return tmp; } restoreFollows(f) { this.follows = f; } } /** * @callback ParseRule * @param {Parser} parser * @param {Runtime} runtime * @param {Tokens} tokens * @param {*} [root] * @returns {ASTNode | undefined} * * @typedef {Object} ASTNode * @member {boolean} isFeature * @member {string} type * @member {any[]} args * @member {(this: ASTNode, ctx:Context, root:any, ...args:any) => any} op * @member {(this: ASTNode, context?:Context) => any} evaluate * @member {ASTNode} parent * @member {Set<ASTNode>} children * @member {ASTNode} root * @member {String} keyword * @member {Token} endToken * @member {ASTNode} next * @member {(context:Context) => ASTNode} resolveNext * @member {EventSource} eventSource * @member {(this: ASTNode) => void} install * @member {(this: ASTNode, context:Context) => void} execute * @member {(this: ASTNode, target: object, source: object, args?: Object) => void} apply * * */ class Parser { /** * * @param {Runtime} runtime */ constructor(runtime) { this.runtime = runtime this.possessivesDisabled = false /* ============================================================================================ */ /* Core hyperscript Grammar Elements */ /* ============================================================================================ */ this.addGrammarElement("feature", function (parser, runtime, tokens) { if (tokens.matchOpToken("(")) { var featureElement = parser.requireElement("feature", tokens); tokens.requireOpToken(")"); return featureElement; } var featureDefinition = parser.FEATURES[tokens.currentToken().value || ""]; if (featureDefinition) { return featureDefinition(parser, runtime, tokens); } }); this.addGrammarElement("command", function (parser, runtime, tokens) { if (tokens.matchOpToken("(")) { const commandElement = parser.requireElement("command", tokens); tokens.requireOpToken(")"); return commandElement; } var commandDefinition = parser.COMMANDS[tokens.currentToken().value || ""]; let commandElement; if (commandDefinition) { commandElement = commandDefinition(parser, runtime, tokens); } else if (tokens.currentToken().type === "IDENTIFIER") { commandElement = parser.parseElement("pseudoCommand", tokens); } if (commandElement) { return parser.parseElement("indirectStatement", tokens, commandElement); } return commandElement; }); this.addGrammarElement("commandList", function (parser, runtime, tokens) { if (tokens.hasMore()) { var cmd = parser.parseElement("command", tokens); if (cmd) { tokens.matchToken("then"); const next = parser.parseElement("commandList", tokens); if (next) cmd.next = next; return cmd; } } return { type: "emptyCommandListCommand", op: function(context){ return runtime.findNext(this, context); }, execute: function (context) { return runtime.unifiedExec(this, context); } } }); this.addGrammarElement("leaf", function (parser, runtime, tokens) { var result = parser.parseAnyOf(parser.LEAF_EXPRESSIONS, tokens); // symbol is last so it doesn't consume any constants if (result == null) { return parser.parseElement("symbol", tokens); } return result; }); this.addGrammarElement("indirectExpression", function (parser, runtime, tokens, root) { for (var i = 0; i < parser.INDIRECT_EXPRESSIONS.length; i++) { var indirect = parser.INDIRECT_EXPRESSIONS[i]; root.endToken = tokens.lastMatch(); var result = parser.parseElement(indirect, tokens, root); if (result) { return result; } } return root; }); this.addGrammarElement("indirectStatement", function (parser, runtime, tokens, root) { if (tokens.matchToken("unless")) { root.endToken = tokens.lastMatch(); var conditional = parser.requireElement("expression", tokens); var unless = { type: "unlessStatementModifier", args: [conditional], op: function (context, conditional) { if (conditional) { return this.next; } else { return root; } }, execute: function (context) { return runtime.unifiedExec(this, context); }, }; root.parent = unless; return unless; } return root; }); this.addGrammarElement("primaryExpression", function (parser, runtime, tokens) { var leaf = parser.parseElement("leaf", tokens); if (leaf) { return parser.parseElement("indirectExpression", tokens, leaf); } parser.raiseParseError(tokens, "Unexpected value: " + tokens.currentToken().value); }); } use(plugin) { plugin(this) return this } /** @type {Object<string,ParseRule>} */ GRAMMAR = {}; /** @type {Object<string,ParseRule>} */ COMMANDS = {}; /** @type {Object<string,ParseRule>} */ FEATURES = {}; /** @type {string[]} */ LEAF_EXPRESSIONS = []; /** @type {string[]} */ INDIRECT_EXPRESSIONS = []; /** * @param {*} parseElement * @param {*} start * @param {Tokens} tokens */ initElt(parseElement, start, tokens) { parseElement.startToken = start; parseElement.sourceFor = Tokens.sourceFor; parseElement.lineFor = Tokens.lineFor; parseElement.programSource = tokens.source; } /** * @param {string} type * @param {Tokens} tokens * @param {ASTNode?} root * @returns {ASTNode} */ parseElement(type, tokens, root = undefined) { var elementDefinition = this.GRAMMAR[type]; if (elementDefinition) { var start = tokens.currentToken(); var parseElement = elementDefinition(this, this.runtime, tokens, root); if (parseElement) { this.initElt(parseElement, start, tokens); parseElement.endToken = parseElement.endToken || tokens.lastMatch(); var root = parseElement.root; while (root != null) { this.initElt(root, start, tokens); root = root.root; } } return parseElement; } } /** * @param {string} type * @param {Tokens} tokens * @param {string} [message] * @param {*} [root] * @returns {ASTNode} */ requireElement(type, tokens, message, root) { var result = this.parseElement(type, tokens, root); if (!result) Parser.raiseParseError(tokens, message || "Expected " + type); // @ts-ignore return result; } /** * @param {string[]} types * @param {Tokens} tokens * @returns {ASTNode} */ parseAnyOf(types, tokens) { for (var i = 0; i < types.length; i++) { var type = types[i]; var expression = this.parseElement(type, tokens); if (expression) { return expression; } } } /** * @param {string} name * @param {ParseRule} definition */ addGrammarElement(name, definition) { this.GRAMMAR[name] = definition; } /** * @param {string} keyword * @param {ParseRule} definition */ addCommand(keyword, definition) { var commandGrammarType = keyword + "Command"; var commandDefinitionWrapper = function (parser, runtime, tokens) { const commandElement = definition(parser, runtime, tokens); if (commandElement) { commandElement.type = commandGrammarType; commandElement.execute = function (context) { context.meta.command = commandElement; return runtime.unifiedExec(this, context); }; return commandElement; } }; this.GRAMMAR[commandGrammarType] = commandDefinitionWrapper; this.COMMANDS[keyword] = commandDefinitionWrapper; } /** * @param {string} keyword * @param {ParseRule} definition */ addFeature(keyword, definition) { var featureGrammarType = keyword + "Feature"; /** @type {ParseRule} */ var featureDefinitionWrapper = function (parser, runtime, tokens) { var featureElement = definition(parser, runtime, tokens); if (featureElement) { featureElement.isFeature = true; featureElement.keyword = keyword; featureElement.type = featureGrammarType; return featureElement; } }; this.GRAMMAR[featureGrammarType] = featureDefinitionWrapper; this.FEATURES[keyword] = featureDefinitionWrapper; } /** * @param {string} name * @param {ParseRule} definition */ addLeafExpression(name, definition) { this.LEAF_EXPRESSIONS.push(name); this.addGrammarElement(name, definition); } /** * @param {string} name * @param {ParseRule} definition */ addIndirectExpression(name, definition) { this.INDIRECT_EXPRESSIONS.push(name); this.addGrammarElement(name, definition); } /** * * @param {Tokens} tokens * @returns string */ static createParserContext(tokens) { var currentToken = tokens.currentToken(); var source = tokens.source; var lines = source.split("\n"); var line = currentToken && currentToken.line ? currentToken.line - 1 : lines.length - 1; var contextLine = lines[line]; var offset = /** @type {number} */ ( currentToken && currentToken.line ? currentToken.column : contextLine.length - 1); return contextLine + "\n" + " ".repeat(offset) + "^^\n\n"; } /** * @param {Tokens} tokens * @param {string} [message] * @returns {never} */ static raiseParseError(tokens, message) { message = (message || "Unexpected Token : " + tokens.currentToken().value) + "\n\n" + Parser.createParserContext(tokens); var error = new Error(message); error["tokens"] = tokens; throw error; } /** * @param {Tokens} tokens * @param {string} [message] */ raiseParseError(tokens, message) { Parser.raiseParseError(tokens, message) } /** * @param {Tokens} tokens * @returns {ASTNode} */ parseHyperScript(tokens) { var result = this.parseElement("hyperscript", tokens); if (tokens.hasMore()) this.raiseParseError(tokens); if (result) return result; } /** * @param {ASTNode | undefined} elt * @param {ASTNode} parent */ setParent(elt, parent) { if (typeof elt === 'object') { elt.parent = parent; if (typeof parent === 'object') { parent.children = (parent.children || new Set()); parent.children.add(elt) } this.setParent(elt.next, parent); } } /** * @param {Token} token * @returns {ParseRule} */ commandStart(token) { return this.COMMANDS[token.value || ""]; } /** * @param {Token} token * @returns {ParseRule} */ featureStart(token) { return this.FEATURES[token.value || ""]; } /** * @param {Token} token * @returns {boolean} */ commandBoundary(token) { if ( token.value == "end" || token.value == "then" || token.value == "else" || token.value == "otherwise" || token.value == ")" || this.commandStart(token) || this.featureStart(token) || token.type == "EOF" ) { return true; } return false; } /** * @param {Tokens} tokens * @returns {(string | ASTNode)[]} */ parseStringTemplate(tokens) { /** @type {(string | ASTNode)[]} */ var returnArr = [""]; do { returnArr.push(tokens.lastWhitespace()); if (tokens.currentToken().value === "$") { tokens.consumeToken(); var startingBrace = tokens.matchOpToken("{"); returnArr.push(this.requireElement("expression", tokens)); if (startingBrace) { tokens.requireOpToken("}"); } returnArr.push(""); } else if (tokens.currentToken().value === "\\") { tokens.consumeToken(); // skip next tokens.consumeToken(); } else { var token = tokens.consumeToken(); returnArr[returnArr.length - 1] += token ? token.value : ""; } } while (tokens.hasMore()); returnArr.push(tokens.lastWhitespace()); return returnArr; } /** * @param {ASTNode} commandList */ ensureTerminated(commandList) { const runtime = this.runtime var implicitReturn = { type: "implicitReturn", op: function (context) { context.meta.returned = true; if (context.meta.resolve) { context.meta.resolve(); } return runtime.HALT; }, execute: function (ctx) { // do nothing }, }; var end = commandList; while (end.next) { end = end.next; } end.next = implicitReturn; } } class Runtime { /** * * @param {Lexer} [lexer] * @param {Parser} [parser] */ constructor(lexer, parser) { this.lexer = lexer ?? new Lexer; this.parser = parser ?? new Parser(this) .use(hyperscriptCoreGrammar) .use(hyperscriptWebGrammar); this.parser.runtime = this } /** * @param {HTMLElement} elt * @param {string} selector * @returns boolea