UNPKG

hyperscript.org

Version:

a small scripting language for the web

2,088 lines (1,924 loc) 153 kB
///========================================================================= /// This module provides the core runtime and grammar for hyperscript ///========================================================================= import {getOrInitObject, mergeObjects, parseJSON, varargConstructor} from "./utils.js"; /** * @type {HyperscriptObject} */ let _hyperscript var globalScope = globalThis; //==================================================================== // Standard library //==================================================================== class ElementCollection { constructor(css, relativeToElement, escape) { this._css = css; this.relativeToElement = relativeToElement; this.escape = escape; } get css() { if (this.escape) { return _runtime.escapeSelector(this._css); } else { return this._css; } } get className() { return this._css.substr(1); } get id() { return this.className(); } contains(elt) { for (let element of this) { if (element.contains(elt)) { return true; } } return false; } get length() { return this.selectMatches().length; } [Symbol.iterator]() { let query = this.selectMatches(); return query [Symbol.iterator](); } selectMatches() { let query = _runtime.getRootNode(this.relativeToElement).querySelectorAll(this.css); return query; } } //==================================================================== // Lexer //==================================================================== /** @type LexerObject */ var _lexer = (function () { var 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 */ function isValidCSSClassChar(c) { return isAlpha(c) || isNumeric(c) || c === "-" || c === "_" || c === ":"; } /** * isValidCSSIDChar returns `true` if the provided character is valid in a CSS ID * @param {string} c * @returns boolean */ function isValidCSSIDChar(c) { return isAlpha(c) || isNumeric(c) || c === "-" || c === "_" || c === ":"; } /** * isWhitespace returns `true` if the provided character is whitespace. * @param {string} c * @returns boolean */ function isWhitespace(c) { return c === " " || c === "\t" || isNewline(c); } /** * positionString returns a string representation of a Token's line and column details. * @param {Token} token * @returns string */ function positionString(token) { return "[Line: " + token.line + ", Column: " + token.column + "]"; } /** * isNewline returns `true` if the provided character is a carrage return or newline * @param {string} c * @returns boolean */ function isNewline(c) { return c === "\r" || c === "\n"; } /** * isNumeric returns `true` if the provided character is a number (0-9) * @param {string} c * @returns boolean */ function 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 */ function isAlpha(c) { return (c >= "a" && c <= "z") || (c >= "A" && c <= "Z"); } /** * @param {string} c * @param {boolean} [dollarIsOp] * @returns boolean */ function isIdentifierChar(c, dollarIsOp) { return c === "_" || c === "$"; } /** * @param {string} c * @returns boolean */ function isReservedChar(c) { return c === "`" || c === "^"; } /** * @param {Token[]} tokens * @param {Token[]} consumed * @param {string} source * @returns {TokensObject} */ function makeTokensObject(tokens, consumed, source) { consumeWhitespace(); // consume initial whitespace /** @type Token | null */ var _lastConsumed = null; function consumeWhitespace() { while (token(0, true).type === "WHITESPACE") { consumed.push(tokens.shift()); } } /** * @param {TokensObject} tokens * @param {*} error */ function raiseError(tokens, error) { _parser.raiseParseError(tokens, error); } /** * @param {string} value * @returns {Token} */ function requireOpToken(value) { var token = matchOpToken(value); if (token) { return token; } else { raiseError(this, "Expected '" + value + "' but found '" + currentToken().value + "'"); } } /** * @param {string} op1 * @param {string} [op2] * @param {string} [op3] * @returns {Token | void} */ function matchAnyOpToken(op1, op2, op3) { for (var i = 0; i < arguments.length; i++) { var opToken = arguments[i]; var match = matchOpToken(opToken); if (match) { return match; } } } /** * @param {string} op1 * @param {string} [op2] * @param {string} [op3] * @returns {Token | void} */ function matchAnyToken(op1, op2, op3) { for (var i = 0; i < arguments.length; i++) { var opToken = arguments[i]; var match = matchToken(opToken); if (match) { return match; } } } /** * @param {string} value * @returns {Token | void} */ function matchOpToken(value) { if (currentToken() && currentToken().op && currentToken().value === value) { return consumeToken(); } } /** * @param {string} type1 * @param {string} [type2] * @param {string} [type3] * @param {string} [type4] * @returns {Token} */ function requireTokenType(type1, type2, type3, type4) { var token = matchTokenType(type1, type2, type3, type4); if (token) { return token; } else { 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} */ function matchTokenType(type1, type2, type3, type4) { if ( currentToken() && currentToken().type && [type1, type2, type3, type4].indexOf(currentToken().type) >= 0 ) { return consumeToken(); } } /** * @param {string} value * @param {string} [type] * @returns {Token} */ function requireToken(value, type) { var token = matchToken(value, type); if (token) { return token; } else { raiseError(this, "Expected '" + value + "' but found '" + currentToken().value + "'"); } } function peekToken(value, peek, type) { return tokens[peek] && tokens[peek].value === value && tokens[peek].type === type } /** * @param {string} value * @param {string} [type] * @returns {Token | void} */ function matchToken(value, type) { if (follows.indexOf(value) !== -1) { return; // disallowed token here } var type = type || "IDENTIFIER"; if (currentToken() && currentToken().value === value && currentToken().type === type) { return consumeToken(); } } /** * @returns {Token} */ function consumeToken() { var match = tokens.shift(); consumed.push(match); _lastConsumed = match; consumeWhitespace(); // consume any whitespace return match; } /** * @param {string} value * @param {string} [type] * @returns {Token[]} */ function consumeUntil(value, type) { /** @type Token[] */ var tokenList = []; var currentToken = token(0, true); while ( (type == null || currentToken.type !== type) && (value == null || currentToken.value !== value) && currentToken.type !== "EOF" ) { var match = tokens.shift(); consumed.push(match); tokenList.push(currentToken); currentToken = token(0, true); } consumeWhitespace(); // consume any whitespace return tokenList; } /** * @returns {string} */ function lastWhitespace() { if (consumed[consumed.length - 1] && consumed[consumed.length - 1].type === "WHITESPACE") { return consumed[consumed.length - 1].value; } else { return ""; } } function consumeUntilWhitespace() { return consumeUntil(null, "WHITESPACE"); } /** * @returns {boolean} */ function hasMore() { return tokens.length > 0; } /** * @param {number} n * @param {boolean} [dontIgnoreWhitespace] * @returns {Token} */ function token(n, dontIgnoreWhitespace) { var /**@type {Token}*/ token; var i = 0; do { if (!dontIgnoreWhitespace) { while (tokens[i] && tokens[i].type === "WHITESPACE") { i++; } } token = tokens[i]; n--; i++; } while (n > -1); if (token) { return token; } else { return { type: "EOF", value: "<<<EOF>>>", }; } } /** * @returns {Token} */ function currentToken() { return token(0); } /** * @returns {Token | null} */ function lastMatch() { return _lastConsumed; } /** * @returns {string} */ function sourceFor() { return source.substring(this.startToken.start, this.endToken.end); } /** * @returns {string} */ function lineFor() { return source.split("\n")[this.startToken.line - 1]; } var follows = []; function pushFollow(str) { follows.push(str); } function popFollow() { follows.pop(); } function clearFollows() { var tmp = follows; follows = []; return tmp; } function restoreFollows(f) { follows = f; } /** @type {TokensObject} */ return { pushFollow: pushFollow, popFollow: popFollow, clearFollow: clearFollows, restoreFollow: restoreFollows, matchAnyToken: matchAnyToken, matchAnyOpToken: matchAnyOpToken, matchOpToken: matchOpToken, requireOpToken: requireOpToken, matchTokenType: matchTokenType, requireTokenType: requireTokenType, consumeToken: consumeToken, peekToken: peekToken, matchToken: matchToken, requireToken: requireToken, list: tokens, consumed: consumed, source: source, hasMore: hasMore, currentToken: currentToken, lastMatch: lastMatch, token: token, consumeUntil: consumeUntil, consumeUntilWhitespace: consumeUntilWhitespace, lastWhitespace: lastWhitespace, sourceFor: sourceFor, lineFor: lineFor, }; } /** * @param {Token[]} tokens * @returns {boolean} */ function 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 {TokensObject} */ function 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() === "-" && (isWhitespace(charAfterThat()) || charAfterThat() === "")) { consumeComment(); } else { if (isWhitespace(currentChar())) { tokens.push(consumeWhitespace()); } else if ( !possiblePrecedingSymbol() && currentChar() === "." && (isAlpha(nextChar()) || nextChar() === "{") ) { tokens.push(consumeClassReference()); } else if ( !possiblePrecedingSymbol() && currentChar() === "#" && (isAlpha(nextChar()) || nextChar() === "{") ) { tokens.push(consumeIdReference()); } else if (currentChar() === "[" && nextChar() === "@") { tokens.push(consumeAttributeReference()); } else if (currentChar() === "@") { tokens.push(consumeShortAttributeReference()); } else if (currentChar() === "*" && isAlpha(nextChar())) { tokens.push(consumeStyleReference()); } else if (isAlpha(currentChar()) || (!inTemplate() && isIdentifierChar(currentChar()))) { tokens.push(consumeIdentifier()); } else if (isNumeric(currentChar())) { tokens.push(consumeNumber()); } else if (!inTemplate() && (currentChar() === '"' || currentChar() === "`")) { tokens.push(consumeString()); } else if (!inTemplate() && currentChar() === "'") { if (isValidSingleQuoteStringStart(tokens)) { tokens.push(consumeString()); } else { tokens.push(consumeOp()); } } else if (OP_TABLE[currentChar()]) { if (lastToken === "$" && currentChar() === "{") { templateBraceCount++; } if (currentChar() === "}") { templateBraceCount--; } tokens.push(consumeOp()); } else if (inTemplate() || isReservedChar(currentChar())) { tokens.push(makeToken("RESERVED", consumeChar())); } else { if (position < source.length) { throw Error("Unknown token: " + currentChar() + " "); } } } } return makeTokensObject(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() && !isNewline(currentChar())) { consumeChar(); } 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 (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 (isValidCSSIDChar(currentChar())) { value += consumeChar(); } attributeRef.value = value; attributeRef.end = position; return attributeRef; } function consumeStyleReference() { var styleRef = makeToken("STYLE_REF"); var value = consumeChar(); while (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 (isValidCSSIDChar(currentChar())) { value += consumeChar(); } } idRef.value = value; idRef.end = position; return idRef; } /** * @returns Token */ function consumeIdentifier() { var identifier = makeToken("IDENTIFIER"); var value = consumeChar(); while (isAlpha(currentChar()) || isNumeric(currentChar()) || 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(); while (isNumeric(currentChar())) { value += consumeChar(); } if (currentChar() === "." && isNumeric(nextChar())) { value += consumeChar(); } while (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() && OP_TABLE[value + currentChar()]) { value += consumeChar(); } op.type = 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 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 { value += nextChar; } } else { value += consumeChar(); } } if (currentChar() !== startChar) { throw Error("Unterminated string at " + positionString(string)); } else { consumeChar(); // consume final quote } string.value = value; string.end = position; string.template = startChar === "`"; return string; } /** * @returns string */ function currentChar() { return source.charAt(position); } /** * @returns string */ function nextChar() { return source.charAt(position + 1); } function charAfterThat() { return source.charAt(position + 2); } /** * @returns string */ function consumeChar() { lastToken = currentChar(); position++; column++; return lastToken; } /** * @returns boolean */ function possiblePrecedingSymbol() { return ( isAlpha(lastToken) || isNumeric(lastToken) || lastToken === ")" || lastToken === "\"" || lastToken === "'" || lastToken === "`" || lastToken === "}" || lastToken === "]" ); } /** * @returns Token */ function consumeWhitespace() { var whitespace = makeToken("WHITESPACE"); var value = ""; while (currentChar() && isWhitespace(currentChar())) { if (isNewline(currentChar())) { column = 0; line++; } value += consumeChar(); } whitespace.value = value; whitespace.end = position; return whitespace; } } return { tokenize: tokenize, makeTokensObject: makeTokensObject, }; })(); //==================================================================== // Parser //==================================================================== /** @type ParserObject */ var _parser = (function () { /** @type {Object<string,GrammarDefinition>} */ var GRAMMAR = {}; /** @type {Object<string,GrammarDefinition>} */ var COMMANDS = {}; /** @type {Object<string,GrammarDefinition>} */ var FEATURES = {}; var LEAF_EXPRESSIONS = []; var INDIRECT_EXPRESSIONS = []; /** * @param {*} parseElement * @param {*} start * @param {TokensObject} tokens */ function initElt(parseElement, start, tokens) { parseElement.startToken = start; parseElement.sourceFor = tokens.sourceFor; parseElement.lineFor = tokens.lineFor; parseElement.programSource = tokens.source; } /** * @param {string} type * @param {TokensObject} tokens * @param {GrammarElement?} root * @returns GrammarElement */ function parseElement(type, tokens, root = undefined) { var elementDefinition = GRAMMAR[type]; if (elementDefinition) { var start = tokens.currentToken(); var parseElement = elementDefinition(_parser, _runtime, tokens, root); if (parseElement) { initElt(parseElement, start, tokens); parseElement.endToken = parseElement.endToken || tokens.lastMatch(); var root = parseElement.root; while (root != null) { initElt(root, start, tokens); root = root.root; } } return parseElement; } } /** * @param {string} type * @param {TokensObject} tokens * @param {string} [message] * @param {*} [root] * @returns {GrammarElement} */ function requireElement(type, tokens, message, root) { var result = parseElement(type, tokens, root); if (!result) raiseParseError(tokens, message || "Expected " + type); // @ts-ignore return result; } /** * @param {string[]} types * @param {TokensObject} tokens * @returns {GrammarElement} */ function parseAnyOf(types, tokens) { for (var i = 0; i < types.length; i++) { var type = types[i]; var expression = parseElement(type, tokens); if (expression) { return expression; } } } /** * @param {string} name * @param {GrammarDefinition} definition */ function addGrammarElement(name, definition) { GRAMMAR[name] = definition; } /** * @param {string} keyword * @param {GrammarDefinition} definition */ function 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; } }; GRAMMAR[commandGrammarType] = commandDefinitionWrapper; COMMANDS[keyword] = commandDefinitionWrapper; } /** * @param {string} keyword * @param {GrammarDefinition} definition */ function addFeature(keyword, definition) { var featureGrammarType = keyword + "Feature"; /** @type {GrammarDefinition} */ 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; } }; GRAMMAR[featureGrammarType] = featureDefinitionWrapper; FEATURES[keyword] = featureDefinitionWrapper; } /** * @param {string} name * @param {GrammarDefinition} definition */ function addLeafExpression(name, definition) { LEAF_EXPRESSIONS.push(name); addGrammarElement(name, definition); } /** * @param {string} name * @param {GrammarDefinition} definition */ function addIndirectExpression(name, definition) { INDIRECT_EXPRESSIONS.push(name); addGrammarElement(name, definition); } /* ============================================================================================ */ /* Core hyperscript Grammar Elements */ /* ============================================================================================ */ addGrammarElement("feature", function (parser, runtime, tokens) { if (tokens.matchOpToken("(")) { var featureElement = parser.requireElement("feature", tokens); tokens.requireOpToken(")"); return featureElement; } var featureDefinition = FEATURES[tokens.currentToken().value]; if (featureDefinition) { return featureDefinition(parser, runtime, tokens); } }); addGrammarElement("command", function (parser, runtime, tokens) { if (tokens.matchOpToken("(")) { const commandElement = parser.requireElement("command", tokens); tokens.requireOpToken(")"); return commandElement; } var commandDefinition = 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; }); addGrammarElement("commandList", function (parser, runtime, tokens) { var cmd = parser.parseElement("command", tokens); if (cmd) { tokens.matchToken("then"); const next = parser.parseElement("commandList", tokens); if (next) cmd.next = next; return cmd; } }); addGrammarElement("leaf", function (parser, runtime, tokens) { var result = parseAnyOf(LEAF_EXPRESSIONS, tokens); // symbol is last so it doesn't consume any constants if (result == null) { return parseElement("symbol", tokens); } return result; }); addGrammarElement("indirectExpression", function (parser, runtime, tokens, root) { for (var i = 0; i < INDIRECT_EXPRESSIONS.length; i++) { var indirect = INDIRECT_EXPRESSIONS[i]; root.endToken = tokens.lastMatch(); var result = parser.parseElement(indirect, tokens, root); if (result) { return result; } } return root; }); 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; }); 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); }); /* ============================================================================================ */ /* END Core hyperscript Grammar Elements */ /* ============================================================================================ */ /** * * @param {TokensObject} tokens * @returns string */ function 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 = currentToken && currentToken.line ? currentToken.column : contextLine.length - 1; return contextLine + "\n" + " ".repeat(offset) + "^^\n\n"; } /** * @param {TokensObject} tokens * @param {string} [message] */ function raiseParseError(tokens, message) { message = (message || "Unexpected Token : " + tokens.currentToken().value) + "\n\n" + createParserContext(tokens); var error = new Error(message); error["tokens"] = tokens; throw error; } /** * @param {TokensObject} tokens * @returns {GrammarElement} */ function parseHyperScript(tokens) { var result = parseElement("hyperscript", tokens); if (tokens.hasMore()) raiseParseError(tokens); if (result) return result; } /** * @param {GrammarElement} elt * @param {GrammarElement} parent */ function setParent(elt, parent) { if (typeof elt === 'object') { elt.parent = parent; if (typeof parent === 'object') { parent.children = (parent.children || new Set()); parent.children.add(elt) } setParent(elt.next, parent); } } /** * @param {Token} token * @returns {GrammarDefinition} */ function commandStart(token) { return COMMANDS[token.value]; } /** * @param {Token} token * @returns {GrammarDefinition} */ function featureStart(token) { return FEATURES[token.value]; } /** * @param {Token} token * @returns {boolean} */ function commandBoundary(token) { if ( token.value == "end" || token.value == "then" || token.value == "else" || token.value == "otherwise" || token.value == ")" || commandStart(token) || featureStart(token) || token.type == "EOF" ) { return true; } return false; } /** * @param {TokensObject} tokens * @returns {(string | GrammarElement)[]} */ function parseStringTemplate(tokens) { /** @type {(string | GrammarElement)[]} */ var returnArr = [""]; do { returnArr.push(tokens.lastWhitespace()); if (tokens.currentToken().value === "$") { tokens.consumeToken(); var startingBrace = tokens.matchOpToken("{"); returnArr.push(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 {GrammarElement} commandList */ function ensureTerminated(commandList) { 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; } // parser API return { setParent, requireElement, parseElement, featureStart, commandStart, commandBoundary, parseAnyOf, parseHyperScript, raiseParseError, addGrammarElement, addCommand, addFeature, addLeafExpression, addIndirectExpression, parseStringTemplate, ensureTerminated, }; })(); //==================================================================== // Runtime //==================================================================== var CONVERSIONS = { dynamicResolvers: /** @type DynamicConversionFunction[] */ [ 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 mergeObjects({}, val); } }, }; /******************************************** * RUNTIME OBJECT ********************************************/ /** @type {RuntimeObject} */ var _runtime = (function () { /** * @param {HTMLElement} elt * @param {string} selector * @returns boolean */ function matchesSelector(elt, selector) { // noinspection JSUnresolvedVariable var matchesFunction = // @ts-ignore elt.matches || elt.matchesSelector || elt.msMatchesSelector || elt.mozMatchesSelector || elt.webkitMatchesSelector || elt.oMatchesSelector; return matchesFunction && matchesFunction.call(elt, selector); } /** * @param {string} eventName * @param {Object} [detail] * @returns {Event} */ function makeEvent(eventName, detail) { var evt; if (globalScope.Event && typeof globalScope.Event === "function") { evt = new Event(eventName, { bubbles: true, cancelable: true, }); evt['detail'] = detail; } else { evt = document.createEvent("CustomEvent"); evt.initCustomEvent(eventName, true, true, detail); } return evt; } /** * @param {Element} elt * @param {string} eventName * @param {Object} [detail] * @param {Element} sender * @returns {boolean} */ function triggerEvent(elt, eventName, detail, sender) { detail = detail || {}; detail["sender"] = sender; var event = makeEvent(eventName, detail); var eventResult = elt.dispatchEvent(event); return eventResult; } /** * isArrayLike returns `true` if the provided value is an array or * a NodeList (which is close enough to being an array for our purposes). * * @param {any} value * @returns {value is Array | NodeList} */ function isArrayLike(value) { return Array.isArray(value) || (typeof NodeList !== 'undefined' && (value instanceof NodeList || value instanceof HTMLCollection)); } /** * isIterable returns `true` if the provided value supports the * iterator protocol. * * @param {any} value * @returns {value is Iterable} */ function isIterable(value) { return typeof value === 'object' && Symbol.iterator in value && typeof value[Symbol.iterator] === 'function'; } /** * shouldAutoIterate returns `true` if the provided value * should be implicitly iterated over when accessing properties, * and as the target of some commands. * * Currently, this is when the value is an {ElementCollection} * or {isArrayLike} returns true. * * @param {any} value * @returns {value is any[] | NodeList | ElementCollection} */ function shouldAutoIterate(value) { return value instanceof ElementCollection || isArrayLike(value); } /** * forEach executes the provided `func` on every item in the `value` array. * if `value` is a single item (and not an array) then `func` is simply called * once. If `value` is null, then no further actions are taken. * * @template T * @param {T | Iterable<T>} value * @param {(item: T) => void} func */ function forEach(value, func) { if (value == null) { // do nothing } else if (isIterable(value)) { for (const nth of value) { func(nth); } } else if (isArrayLike(value)) { for (var i = 0; i < value.length; i++) { func(value[i]); } } else { func(value); } } /** * implicitLoop executes the provided `func` on: * - every item of {value}, if {value} should be auto-iterated * (see {shouldAutoIterate}) * - {value} otherwise * * @template T * @param {NodeList | T | T[]} value * @param {(item:Node | T) => void} func */ function implicitLoop(value, func) { if (shouldAutoIterate(value)) { for (const x of value) func(x); } else { func(value); } } function wrapArrays(args) { var arr = []; for (var i = 0; i < args.length; i++) { var arg = args[i]; if (Array.isArray(arg)) { arr.push(Promise.all(arg)); } else { arr.push(arg); } } return arr; } function unwrapAsyncs(values) { for (var i = 0; i < values.length; i++) { var value = values[i]; if (value.asyncWrapper) { values[i] = value.value; } if (Array.isArray(value)) { for (var j = 0; j < value.length; j++) { var valueElement = value[j]; if (valueElement.asyncWrapper) { value[j] = valueElement.value; } } } } } var HALT = {}; /** * @param {GrammarElement} command * @param {Context} ctx */ function unifiedExec(command, ctx) { while (true) { try { var next = unifiedEval(command, ctx); } catch (e) { if (ctx.meta.handlingFinally) { console.error(" Exception in finally block: ", e); next = HALT; } else { _runtime.registerHyperTrace(ctx, e); if (ctx.meta.errorHandler && !ctx.meta.handlingError) { ctx.meta.handlingError = true; ctx[ctx.meta.errorSymbol] = e; command = ctx.meta.errorHandler; continue; } else { ctx.meta.currentException = e; next = HALT; } } } if (next == null) { console.error(command, " did not return a next element to execute! context: ", ctx); return; } else if (next.then) { next.then(function (resolvedNext) { unifiedExec(resolvedNext, ctx); }).catch(function (reason) { unifiedExec({ // Anonymous command to simply throw the exception op: function(){ throw reason; } }, ctx); }); return; } else if (next === HALT) { if (ctx.meta.finallyHandler && !ctx.meta.handlingFinally) { ctx.meta.handlingFinally = true; command = ctx.meta.finallyHandler; } else { if (ctx.meta.onHalt) { ctx.meta.onHalt(); } if (ctx.meta.currentException) { if (ctx.meta.reject) { ctx.meta.reject(ctx.meta.currentException); return; } else { throw ctx.meta.currentException; } } else { return; } } } else { command = next; // move to the next command } } } /** * @param {*} parseElement * @param {Context} ctx * @returns {*} */ function unifiedEval(parseElement, ctx) { /** @type any[] */ var args = [ctx]; var async = false; var wrappedAsyncs = false; if (parseElement.args) { for (var i = 0; i < parseElement.args.length; i++) { var argument = parseElement.args[i]; if (argument == null) { args.push(null); } else if (Array.isArray(argument)) { var arr = []; for (var j = 0; j < argument.length; j++) { var element = argument[j]; var value = element ? element.evaluate(ctx) : null; // OK if (value) { if (value.then) { async = true; } else if (value.asyncWrapper) { wrappedAsyncs = true; } } arr.push(value); } args.push(arr); } else if (argument.evaluate) { var value = argument.evaluate(ctx); // OK if (value) { if (value.then) { async = true; } else if (value.asyncWrapper) { wrappedAsyncs = true; } } args.push(value); } else { args.push(argument); } } } if (async) { return new Promise(function (resolve, reject) { args = wrapArrays(args); Promise.all(args) .then(function (values) { if (wrappedAsyncs) { unwrapAsyncs(values); } try { var apply = parseElement.op.apply(parseElement, values); resolve(apply); } catch (e) { reject(e); } }) .catch(function (reason) { reject(reason); }); }); } else { if (wrappedAsyncs) { unwrapAsyncs(args); } return parseElement.op.apply(parseElement, args); } } let _scriptAttrs = null; /** * getAttributes returns the attribute name(s) to use when * locating hyperscript scripts in a DOM element. If no value * has been configured, it defaults to _hyperscript.config.attributes * @returns string[] */ function getScriptAttributes() { if (_scriptAttrs == null) { _scriptAttrs = _hyperscript.config.attributes.replace(/ /g, "").split(","); } return _scriptAttrs; } /** * @param {Element} elt * @returns {string | null} */ function getScript(elt) { for (var i = 0; i < getScriptAttributes().length; i++) { var scriptAttribute = getScriptAttributes()[i]; if (elt.hasAttribute && elt.hasAttribute(scriptAttribute)) { return elt.getAttribute(scriptAttribute); } } if (elt instanceof HTMLScriptElement && elt.type === "text/hyperscript") { return elt.innerText; } return null; } var hyperscriptFeaturesMap = new WeakMap /** * @param {*} elt * @returns {Object} */ function getHyperscriptFeatures(elt) { var hyperscriptFeatures = hyperscriptFeaturesMap.get(elt); if (typeof hyperscriptFeatures === 'undefined') { hyperscriptFeaturesMap.set(elt, hyperscriptFeatures = {}); } return hyperscriptFeatures; } /** * @param {Object} owner * @param {Context} ctx */ function addFeatures(owner, ctx) { if (owner) { mergeObjects(ctx, getHyperscriptFeatures(owner)); addFeatures(owner.parentElement, ctx); } } /** * @param {*} owner * @param {*} feature * @param {*} hyperscriptTarget * @param {*} event * @returns {Context} */ function makeContext(owner, feature, hyperscriptTarget, event) { /** @type {Context} */ var ctx = { meta: { parser: _parser, lexer: _lexer, runtime: _runtime, owner: owner, feature: feature, iterators: {}, }, me: hyperscriptTarget, event: event, target: event ? event.target : null, detail: event ? event.detail : null, sender: event ? event.detail ? event.detail.sender : null : null, body: "document" in globalScope ? document.body : null, }; ctx.meta.ctx = ctx; addFeatures(owner, ctx); return ctx; } /** * @returns string */ function getScriptSelector() { return getScriptAttributes() .map(function (attribute) { return "[" + attribute + "]"; }) .join(", "); } /** * @param {any} value * @param {string} type * @returns {any} */ function convertValue(value, type) { var dynamicResolvers = CONVERSIONS.dynamicResolvers; for (var i = 0; i < dynamicResolvers.length; i++) { var dynamicResolver = dynamicResolvers[i]; var converted = dynamicResolver(type, value); if (converted !== undefined) { return converted; } } if (value == null) { return null; } var converter = CONVERSIONS[type]; if (converter) { return converter(value); } throw "Unknown conversion : " + type; } // TODO: There do not seem to be any references to this function. // Is it still in use, or can it be removed? function isType(o, type) { return Object.prototype.toString.call(o) === "[object " + type + "]"; } /** * @param {string} src * @returns {GrammarElement} */ function parse(src) { var tokens = _lexer.tokenize(src); if (_parser.commandStart(tokens.currentToken())) { var commandList = _parser.requireElement("commandList", tokens); if (tokens.hasMore()) _parser.raiseParseError(tokens); _parser.ensureTerminated(commandList); return commandList; } else if (_parser.featureStart(tokens.currentToken())) { var hyperscript = _parser.requireElement("hyperscript", tokens); if (tokens.hasMore()) _parser.raiseParseError(tokens); return hyperscript; } else { var expression = _parser.requireElement("expression", tokens); if (tokens.hasMore()) _parser.raiseParseError(tokens); return expression; } } /** * * @param {GrammarElement} elt * @param {Context} ctx * @returns {any} */ function evaluateNoPromise(elt, ctx) { let result = elt.evaluate(ctx); if (result.next) { throw new Error(elt.sourceFor() + " returned a Promise in a context that they are not allowed."); } return result; } /** * @param {string} src * @param {Context} [ctx] * @param {Object} [args] * @returns {any} */ function evaluate(src, ctx, args) { class HyperscriptModule extends EventTarget { constructor(mod) { super(); this.module = mod; } toString() { return this.module.id; } } var body = 'document' in globalScope ? globalScope.document.body : new HyperscriptModule(args && args.module); ctx = mergeObjects(makeContext(body, null, body, null), ctx || {}); var element = parse(src); if (element.execute) { element.execute(ctx); return ctx.result; } else if (element.apply) { element.apply(body, body, args); return getHyperscriptFeatures(body); } else { return element.evaluate(ctx); } function makeModule() { return {} } } /** * @param {HTMLElement} elt */ function processNode(elt) { var selector = _runtime.getScriptSelector(); if (matchesSelector(elt, selector)) { initElement(elt, elt); } if (elt instanceof HTMLScriptElement && elt.type === "text/hyperscript") { initElement(elt, document.body); } if (elt.querySelectorAll) { forEach(elt.querySelectorAll(selector + ", [type='text/hyperscript']"), function (elt) { initElement(elt, elt instanceof HTMLScriptElement && elt.type === "text/hyperscript" ? document.body : elt); }); } } /** * @param {Element} elt * @param {Element} [target] */ function initElement(elt, target) { if (elt.closest && elt.closest(_hyperscript.config.disableSelector)) { return; } var internalData = getInternalData(elt); if (!internalData.initialized) { var src = getScript(elt); if (src) { try { internalData.initialized = true; internalData.script = src; var tokens = _lexer.tokenize(src); var hyperScript = _parser.parseHyperScript(tokens); if (!hyperScript) return; hyperScript.apply(target || elt, elt); setTimeout(function () { triggerEvent(target || elt, "load", { hyperscript: true, }); }, 1); } catch (e) { _runtime.triggerEvent(elt, "exception", { error: e, }); console.error( "hyperscript errors were found on the following element:", elt, "\n\n", e.message, e.stack ); } } } } var internalDataMap = new WeakMap /** * @param {Element} elt * @returns {Object} */ function getInternalData(elt) { var internalData = internalDataMap.get(elt); if (typeof internalData === 'undefined') { internalDataMap.set(elt, internalData = {}); } return internalData; } /** * @param {any} value * @param {string} typeString * @param {boolean} [nullOk] * @returns {boolean} */ function typeCheck(value, typeString, nullOk) { if (value == null && nullOk) { return true; } var typeName = Object.prototype.toString.call(value).slice(8, -1); return typeName === typeString; } function getElementScope(context) { var elt = context.meta && context.meta.owner; if (elt) { var internalData = getInternalData(elt); var scopeName = "elementScope"; if (context.meta.feature && context.meta.feature.behavior) { scopeName = context.meta.feature.behavior + "Scope"; } var elementScope = getOrInitObject(internalData, scopeName); return elementScope; } else { return {}; // no element, return empty scope } } /** * @param {string} str * @param {Context} context * @returns {any} */ function resolveSymbol(str, context, type) { if (str === "me" || str === "my" || str === "I") { return context["me"]; } if (str === "it" || str === "its") { return context["result"]; } if (str === "you" || str === "your" || str === "yourself") { return context["beingTold"]; } else { if (type === "global") { return globalScope[str]; } else if (type === "element") { var elementScope = getElementScope(context); return elementScope[str]; } else if (type === "local") { return context[str]; } else { // meta scope (used for event conditionals) if (context.meta && context.meta.context) { var fromMetaContext = context.meta.context[str]; if (typeof fromMetaContext !== "undefined") { return fromMetaContext; } } // local scope var fromContext = context[str]; if (typeof fromContext !== "undefined") { return fromContext; } else { // element scope var elementScope = getElementScope(context); fromContext = elementScope[str]; if (typeof fromContext !== "undefined") { return fromContext; } else { // global scope return globalScope[str]; } } } } } function setSymbol(str, context, type, value) { if (type === "global") { globalScope[str] = value; } else if (type === "element") { var elementScope = getElementScope(context); elementScope[str] = value; } else if (type === "local") { context[str] = value; } else { // local scope var fromContext = context[str]; if (typeof fromContext !== "undefined") { context[str] = value; } else { // element scope var elementScope = getElementScope(context); fromContext = elementScope[str]; if (typeof fromContext !== "undefined") { elementScope[str] = value; } else { context[str] = value; } } } } /** * @param {GrammarElement} command * @param {Context} context * @returns {undefined | GrammarElement} */ function findNext(command, context) { if (command) { if (command.resolveNext) { return command.resolveNext(context); } else if (command.next) { return command.next; } else { return findNext(command.parent, context); } } } /** * @param {Object<string,any>} root * @param {string} property * @param {boolean} attribute * @returns {any} */ function flatGet(root, property, getter) { if (root != null) { var val = getter(root, property); if (typeof val !== "undefined") { return val; } if (shouldAutoIterate(root)) { // flat map var result = []; for (var component of root) { var componentValue = getter(component, property); if (componentValue) { result.push(componentValue); } } return result; } } } function resolveProperty(root, property) { return flatGet(root, property, (ro