From 2f630ddde0e801246fa10748991a848a33ef3f6d Mon Sep 17 00:00:00 2001 From: Simple_Not <44047940+moonbaseDelta@users.noreply.github.com> Date: Thu, 20 Jul 2023 01:32:57 +1000 Subject: [PATCH] we can send now? --- app.py | 16 +- static/src/_hyperscript.js | 7620 ++++++++++++++++++++++++++++++ templates/interactive-posts.html | 15 +- 3 files changed, 7631 insertions(+), 20 deletions(-) create mode 100644 static/src/_hyperscript.js diff --git a/app.py b/app.py index 3e1577b..a47abe2 100644 --- a/app.py +++ b/app.py @@ -194,21 +194,9 @@ def upload_file(): bucket_name, uploaded_file.filename, uploaded_file, size ) - return """ -

stored

-
- - -
- """ + return "OK" - return """ -

Upload IPS File

-
- - -
- """ + return " NE OK " diff --git a/static/src/_hyperscript.js b/static/src/_hyperscript.js new file mode 100644 index 0000000..a41b6be --- /dev/null +++ b/static/src/_hyperscript.js @@ -0,0 +1,7620 @@ +/** + * @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 carrage 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 = ""; + 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 (Lexer.isAlpha(currentChar()) || (!inTemplate() && 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 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 + 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 " + Lexer.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 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"; + return this.tokens[peek] && this.tokens[peek].value === value && this.tokens[peek].type === type + } + + /** + * @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: "<<>>", + }; + } + } + + /** + * @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} 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} */ + GRAMMAR = {}; + + /** @type {Object} */ + COMMANDS = {}; + + /** @type {Object} */ + 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 boolean + */ + 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} + */ + 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} + */ + triggerEvent(elt, eventName, detail, sender) { + detail = detail || {}; + detail["sender"] = sender; + var event = this.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} + */ + 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} + */ + 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[] | ElementCollection)} + */ + shouldAutoIterate(value) { + return value != null && value[shouldAutoIterateSymbol] || + this.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} value + * @param {(item: T) => void} func + */ + forEach(value, func) { + if (value == null) { + // do nothing + } else if (this.isIterable(value)) { + for (const nth of value) { + func(nth); + } + } else if (this.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 {ElementCollection | T | T[]} value + * @param {(item: T) => void} func + */ + implicitLoop(value, func) { + if (this.shouldAutoIterate(value)) { + for (const x of value) func(x); + } else { + func(value); + } + } + + 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; + } + + 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; + } + } + } + } + } + + static HALT = {}; + HALT = Runtime.HALT; + + /** + * @param {ASTNode} command + * @param {Context} ctx + */ + unifiedExec(command, ctx) { + while (true) { + try { + var next = this.unifiedEval(command, ctx); + } catch (e) { + if (ctx.meta.handlingFinally) { + console.error(" Exception in finally block: ", e); + next = Runtime.HALT; + } else { + this.registerHyperTrace(ctx, e); + if (ctx.meta.errorHandler && !ctx.meta.handlingError) { + ctx.meta.handlingError = true; + ctx.locals[ctx.meta.errorSymbol] = e; + command = ctx.meta.errorHandler; + continue; + } else { + ctx.meta.currentException = e; + next = Runtime.HALT; + } + } + } + if (next == null) { + console.error(command, " did not return a next element to execute! context: ", ctx); + return; + } else if (next.then) { + next.then(resolvedNext => { + this.unifiedExec(resolvedNext, ctx); + }).catch(reason => { + this.unifiedExec({ // Anonymous command to simply throw the exception + op: function(){ + throw reason; + } + }, ctx); + }); + return; + } else if (next === Runtime.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 {*} + */ + 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((resolve, reject) => { + args = this.wrapArrays(args); + Promise.all(args) + .then(function (values) { + if (wrappedAsyncs) { + this.unwrapAsyncs(values); + } + try { + var apply = parseElement.op.apply(parseElement, values); + resolve(apply); + } catch (e) { + reject(e); + } + }) + .catch(function (reason) { + reject(reason); + }); + }); + } else { + if (wrappedAsyncs) { + this.unwrapAsyncs(args); + } + return parseElement.op.apply(parseElement, args); + } + } + + /** + * @type {string[] | null} + */ + _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 config.attributes + * @returns string[] + */ + getScriptAttributes() { + if (this._scriptAttrs == null) { + this._scriptAttrs = config.attributes.replace(/ /g, "").split(","); + } + return this._scriptAttrs; + } + + /** + * @param {Element} elt + * @returns {string | null} + */ + getScript(elt) { + for (var i = 0; i < this.getScriptAttributes().length; i++) { + var scriptAttribute = this.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; + } + + hyperscriptFeaturesMap = new WeakMap + + /** + * @param {*} elt + * @returns {Object} + */ + getHyperscriptFeatures(elt) { + var hyperscriptFeatures = this.hyperscriptFeaturesMap.get(elt); + if (typeof hyperscriptFeatures === 'undefined') { + if (elt) { + // in some rare cases, elt is null and this line crashes + this.hyperscriptFeaturesMap.set(elt, hyperscriptFeatures = {}); + } + } + return hyperscriptFeatures; + } + + /** + * @param {Object} owner + * @param {Context} ctx + */ + addFeatures(owner, ctx) { + if (owner) { + Object.assign(ctx.locals, this.getHyperscriptFeatures(owner)); + this.addFeatures(owner.parentElement, ctx); + } + } + + /** + * @param {*} owner + * @param {*} feature + * @param {*} hyperscriptTarget + * @param {*} event + * @returns {Context} + */ + makeContext(owner, feature, hyperscriptTarget, event) { + return new Context(owner, feature, hyperscriptTarget, event, this) + } + + /** + * @returns string + */ + getScriptSelector() { + return this.getScriptAttributes() + .map(function (attribute) { + return "[" + attribute + "]"; + }) + .join(", "); + } + + /** + * @param {any} value + * @param {string} type + * @returns {any} + */ + 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; + } + + /** + * @param {string} src + * @returns {ASTNode} + */ + parse(src) { + const lexer = this.lexer, parser = this.parser + var tokens = lexer.tokenize(src); + if (this.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 {ASTNode} elt + * @param {Context} ctx + * @returns {any} + */ + evaluateNoPromise(elt, ctx) { + let result = elt.evaluate(ctx); + if (result.next) { + throw new Error(Tokens.sourceFor.call(elt) + " returned a Promise in a context that they are not allowed."); + } + return result; + } + + /** + * @param {string} src + * @param {Partial} [ctx] + * @param {Object} [args] + * @returns {any} + */ + 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 = Object.assign(this.makeContext(body, null, body, null), ctx || {}); + var element = this.parse(src); + if (element.execute) { + element.execute(ctx); + return ctx.result; + } else if (element.apply) { + element.apply(body, body, args); + return this.getHyperscriptFeatures(body); + } else { + return element.evaluate(ctx); + } + + function makeModule() { + return {} + } + } + + /** + * @param {HTMLElement} elt + */ + processNode(elt) { + var selector = this.getScriptSelector(); + if (this.matchesSelector(elt, selector)) { + this.initElement(elt, elt); + } + if (elt instanceof HTMLScriptElement && elt.type === "text/hyperscript") { + this.initElement(elt, document.body); + } + if (elt.querySelectorAll) { + this.forEach(elt.querySelectorAll(selector + ", [type='text/hyperscript']"), elt => { + this.initElement(elt, elt instanceof HTMLScriptElement && elt.type === "text/hyperscript" ? document.body : elt); + }); + } + } + + /** + * @param {Element} elt + * @param {Element} [target] + */ + initElement(elt, target) { + if (elt.closest && elt.closest(config.disableSelector)) { + return; + } + var internalData = this.getInternalData(elt); + if (!internalData.initialized) { + var src = this.getScript(elt); + if (src) { + try { + internalData.initialized = true; + internalData.script = src; + const lexer = this.lexer, parser = this.parser + var tokens = lexer.tokenize(src); + var hyperScript = parser.parseHyperScript(tokens); + if (!hyperScript) return; + hyperScript.apply(target || elt, elt); + setTimeout(() => { + this.triggerEvent(target || elt, "load", { + hyperscript: true, + }); + }, 1); + } catch (e) { + this.triggerEvent(elt, "exception", { + error: e, + }); + console.error( + "hyperscript errors were found on the following element:", + elt, + "\n\n", + e.message, + e.stack + ); + } + } + } + } + + internalDataMap = new WeakMap + + /** + * @param {Element} elt + * @returns {Object} + */ + getInternalData(elt) { + var internalData = this.internalDataMap.get(elt); + if (typeof internalData === 'undefined') { + this.internalDataMap.set(elt, internalData = {}); + } + return internalData; + } + + /** + * @param {any} value + * @param {string} typeString + * @param {boolean} [nullOk] + * @returns {boolean} + */ + typeCheck(value, typeString, nullOk) { + if (value == null && nullOk) { + return true; + } + var typeName = Object.prototype.toString.call(value).slice(8, -1); + return typeName === typeString; + } + + getElementScope(context) { + var elt = context.meta && context.meta.owner; + if (elt) { + var internalData = this.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 + * @returns {boolean} + */ + isReservedWord(str) { + return ["meta", "it", "result", "locals", "event", "target", "detail", "sender", "body"].includes(str) + } + + /** + * @param {any} context + * @returns {boolean} + */ + isHyperscriptContext(context) { + return context instanceof Context; + } + + /** + * @param {string} str + * @param {Context} context + * @returns {any} + */ + resolveSymbol(str, context, type) { + if (str === "me" || str === "my" || str === "I") { + return context.me; + } + if (str === "it" || str === "its" || str === "result") { + return context.result; + } + if (str === "you" || str === "your" || str === "yourself") { + return context.you; + } else { + if (type === "global") { + return globalScope[str]; + } else if (type === "element") { + var elementScope = this.getElementScope(context); + return elementScope[str]; + } else if (type === "local") { + return context.locals[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; + } + // resolve against the `detail` object in the meta context as well + if (context.meta.context.detail) { + fromMetaContext = context.meta.context.detail[str]; + if (typeof fromMetaContext !== "undefined") { + return fromMetaContext; + } + } + } + if (this.isHyperscriptContext(context) && !this.isReservedWord(str)) { + // local scope + var fromContext = context.locals[str]; + } else { + // direct get from normal JS object or top-level of context + var fromContext = context[str]; + } + if (typeof fromContext !== "undefined") { + return fromContext; + } else { + // element scope + var elementScope = this.getElementScope(context); + fromContext = elementScope[str]; + if (typeof fromContext !== "undefined") { + return fromContext; + } else { + // global scope + return globalScope[str]; + } + } + } + } + } + + setSymbol(str, context, type, value) { + if (type === "global") { + globalScope[str] = value; + } else if (type === "element") { + var elementScope = this.getElementScope(context); + elementScope[str] = value; + } else if (type === "local") { + context.locals[str] = value; + } else { + if (this.isHyperscriptContext(context) && !this.isReservedWord(str) && typeof context.locals[str] !== "undefined") { + // local scope + context.locals[str] = value; + } else { + // element scope + var elementScope = this.getElementScope(context); + var fromContext = elementScope[str]; + if (typeof fromContext !== "undefined") { + elementScope[str] = value; + } else { + if (this.isHyperscriptContext(context) && !this.isReservedWord(str)) { + // local scope + context.locals[str] = value; + } else { + // direct set on normal JS object or top-level of context + context[str] = value; + } + } + } + } + } + + /** + * @param {ASTNode} command + * @param {Context} context + * @returns {undefined | ASTNode} + */ + findNext(command, context) { + if (command) { + if (command.resolveNext) { + return command.resolveNext(context); + } else if (command.next) { + return command.next; + } else { + return this.findNext(command.parent, context); + } + } + } + + /** + * @param {Object} root + * @param {string} property + * @param {Getter} getter + * @returns {any} + * + * @callback Getter + * @param {Object} root + * @param {string} property + */ + flatGet(root, property, getter) { + if (root != null) { + var val = getter(root, property); + if (typeof val !== "undefined") { + return val; + } + + if (this.shouldAutoIterate(root)) { + // flat map + var result = []; + for (var component of root) { + var componentValue = getter(component, property); + result.push(componentValue); + } + return result; + } + } + } + + resolveProperty(root, property) { + return this.flatGet(root, property, (root, property) => root[property] ) + } + + resolveAttribute(root, property) { + return this.flatGet(root, property, (root, property) => root.getAttribute && root.getAttribute(property) ) + } + + /** + * + * @param {Object} root + * @param {string} property + * @returns {string} + */ + resolveStyle(root, property) { + return this.flatGet(root, property, (root, property) => root.style && root.style[property] ) + } + + /** + * + * @param {Object} root + * @param {string} property + * @returns {string} + */ + resolveComputedStyle(root, property) { + return this.flatGet(root, property, (root, property) => getComputedStyle( + /** @type {Element} */ (root)).getPropertyValue(property) ) + } + + /** + * @param {Element} elt + * @param {string[]} nameSpace + * @param {string} name + * @param {any} value + */ + assignToNamespace(elt, nameSpace, name, value) { + let root + if (typeof document !== "undefined" && elt === document.body) { + root = globalScope; + } else { + root = this.getHyperscriptFeatures(elt); + } + var propertyName; + while ((propertyName = nameSpace.shift()) !== undefined) { + var newRoot = root[propertyName]; + if (newRoot == null) { + newRoot = {}; + root[propertyName] = newRoot; + } + root = newRoot; + } + + root[name] = value; + } + + getHyperTrace(ctx, thrown) { + var trace = []; + var root = ctx; + while (root.meta.caller) { + root = root.meta.caller; + } + if (root.meta.traceMap) { + return root.meta.traceMap.get(thrown, trace); + } + } + + registerHyperTrace(ctx, thrown) { + var trace = []; + var root = null; + while (ctx != null) { + trace.push(ctx); + root = ctx; + ctx = ctx.meta.caller; + } + if (root.meta.traceMap == null) { + root.meta.traceMap = new Map(); // TODO - WeakMap? + } + if (!root.meta.traceMap.get(thrown)) { + var traceEntry = { + trace: trace, + print: function (logger) { + logger = logger || console.error; + logger("hypertrace /// "); + var maxLen = 0; + for (var i = 0; i < trace.length; i++) { + maxLen = Math.max(maxLen, trace[i].meta.feature.displayName.length); + } + for (var i = 0; i < trace.length; i++) { + var traceElt = trace[i]; + logger( + " ->", + traceElt.meta.feature.displayName.padEnd(maxLen + 2), + "-", + traceElt.meta.owner + ); + } + }, + }; + root.meta.traceMap.set(thrown, traceEntry); + } + } + + /** + * @param {string} str + * @returns {string} + */ + escapeSelector(str) { + return str.replace(/:/g, function (str) { + return "\\" + str; + }); + } + + /** + * @param {any} value + * @param {*} elt + */ + nullCheck(value, elt) { + if (value == null) { + throw new Error("'" + elt.sourceFor() + "' is null"); + } + } + + /** + * @param {any} value + * @returns {boolean} + */ + isEmpty(value) { + return value == undefined || value.length === 0; + } + + /** + * @param {any} value + * @returns {boolean} + */ + doesExist(value) { + if(value == null){ + return false; + } + if (this.shouldAutoIterate(value)) { + for (const elt of value) { + return true; + } + return false; + } + return true; + } + + /** + * @param {Node} node + * @returns {Document|ShadowRoot} + */ + getRootNode(node) { + if (node && node instanceof Node) { + var rv = node.getRootNode(); + if (rv instanceof Document || rv instanceof ShadowRoot) return rv; + } + return document; + } + + /** + * + * @param {Element} elt + * @param {ASTNode} onFeature + * @returns {EventQueue} + * + * @typedef {{queue:Array, executing:boolean}} EventQueue + */ + getEventQueueFor(elt, onFeature) { + let internalData = this.getInternalData(elt); + var eventQueuesForElt = internalData.eventQueues; + if (eventQueuesForElt == null) { + eventQueuesForElt = new Map(); + internalData.eventQueues = eventQueuesForElt; + } + var eventQueueForFeature = eventQueuesForElt.get(onFeature); + if (eventQueueForFeature == null) { + eventQueueForFeature = {queue:[], executing:false}; + eventQueuesForElt.set(onFeature, eventQueueForFeature); + } + return eventQueueForFeature; + } + + beepValueToConsole(element, expression, value) { + if (this.triggerEvent(element, "hyperscript:beep", {element, expression, value})) { + var typeName; + if (value) { + if (value instanceof ElementCollection) { + typeName = "ElementCollection"; + } else if (value.constructor) { + typeName = value.constructor.name; + } else { + typeName = "unknown"; + } + } else { + typeName = "object (null)" + } + var logValue = value; + if (typeName === "String") { + logValue = '"' + logValue + '"'; + } else if (value instanceof ElementCollection) { + logValue = Array.from(value); + } + console.log("///_ BEEP! The expression (" + Tokens.sourceFor.call(expression).replace("beep! ", "") + ") evaluates to:", logValue, "of type " + typeName); + } + } + + + /** @type string | null */ + // @ts-ignore + hyperscriptUrl = "document" in globalScope && document.currentScript ? document.currentScript.src : null; + } + + + function getCookiesAsArray() { + let cookiesAsArray = document.cookie + .split("; ") + .map(cookieEntry => { + let strings = cookieEntry.split("="); + return {name: strings[0], value: decodeURIComponent(strings[1])} + }); + return cookiesAsArray; + } + + function clearCookie(name) { + document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT"; + } + + function clearAllCookies() { + for (const cookie of getCookiesAsArray()) { + clearCookie(cookie.name); + } + } + + const CookieJar = new Proxy({}, { + get(target, prop) { + if (prop === 'then' || prop === 'asyncWrapper') { // ignore special symbols + return null; + } else if (prop === 'length') { + return getCookiesAsArray().length + } else if (prop === 'clear') { + return clearCookie; + } else if (prop === 'clearAll') { + return clearAllCookies; + } else if (typeof prop === "string") { + if (!isNaN(prop)) { + return getCookiesAsArray()[parseInt(prop)]; + + } else { + let value = document.cookie + .split("; ") + .find((row) => row.startsWith(prop + "=")) + ?.split("=")[1]; + if(value) { + return decodeURIComponent(value); + } + } + } else if (prop === Symbol.iterator) { + return getCookiesAsArray()[prop]; + } + }, + set(target, prop, value) { + var finalValue = null; + if ('string' === typeof value) { + finalValue = encodeURIComponent(value) + finalValue += ";samesite=lax" + } else { + finalValue = encodeURIComponent(value.value); + if (value.expires) { + finalValue+=";expires=" + value.maxAge; + } + if (value.maxAge) { + finalValue+=";max-age=" + value.maxAge; + } + if (value.partitioned) { + finalValue+=";partitioned=" + value.partitioned; + } + if (value.path) { + finalValue+=";path=" + value.path; + } + if (value.samesite) { + finalValue+=";samesite=" + value.path; + } + if (value.secure) { + finalValue+=";secure=" + value.path; + } + } + document.cookie= prop + "=" + value; + return true; + } + }) + + class Context { + /** + * @param {*} owner + * @param {*} feature + * @param {*} hyperscriptTarget + * @param {*} event + */ + constructor(owner, feature, hyperscriptTarget, event, runtime) { + this.meta = { + parser: runtime.parser, + lexer: runtime.lexer, + runtime, + owner: owner, + feature: feature, + iterators: {}, + ctx: this + } + this.locals = { + cookies:CookieJar + }; + this.me = hyperscriptTarget, + this.you = undefined + this.result = undefined + this.event = event; + this.target = event ? event.target : null; + this.detail = event ? event.detail : null; + this.sender = event ? event.detail ? event.detail.sender : null : null; + this.body = "document" in globalScope ? document.body : null; + runtime.addFeatures(owner, this); + } + } + + class ElementCollection { + constructor(css, relativeToElement, escape) { + this._css = css; + this.relativeToElement = relativeToElement; + this.escape = escape; + this[shouldAutoIterateSymbol] = true; + } + + get css() { + if (this.escape) { + return Runtime.prototype.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.prototype.getRootNode(this.relativeToElement).querySelectorAll(this.css); + return query; + } + } + + const shouldAutoIterateSymbol = Symbol() + + function getOrInitObject(root, prop) { + var value = root[prop]; + if (value) { + return value; + } else { + var newObj = {}; + root[prop] = newObj; + return newObj; + } + } + + /** + * parseJSON parses a JSON string into a corresponding value. If the + * value passed in is not valid JSON, then it logs an error and returns `null`. + * + * @param {string} jString + * @returns any + */ + function parseJSON(jString) { + try { + return JSON.parse(jString); + } catch (error) { + logError(error); + return null; + } + } + + /** + * logError writes an error message to the Javascript console. It can take any + * value, but msg should commonly be a simple string. + * @param {*} msg + */ + function logError(msg) { + if (console.error) { + console.error(msg); + } else if (console.log) { + console.log("ERROR: ", msg); + } + } + + // TODO: JSDoc description of what's happening here + function varargConstructor(Cls, args) { + return new (Cls.bind.apply(Cls, [Cls].concat(args)))(); + } + + // Grammar + + /** + * @param {Parser} parser + */ + function hyperscriptCoreGrammar(parser) { + parser.addLeafExpression("parenthesized", function (parser, _runtime, tokens) { + if (tokens.matchOpToken("(")) { + var follows = tokens.clearFollows(); + try { + var expr = parser.requireElement("expression", tokens); + } finally { + tokens.restoreFollows(follows); + } + tokens.requireOpToken(")"); + return expr; + } + }); + + parser.addLeafExpression("string", function (parser, runtime, tokens) { + var stringToken = tokens.matchTokenType("STRING"); + if (!stringToken) return; + var rawValue = /** @type {string} */ (stringToken.value); + /** @type {any[]} */ + var args; + if (stringToken.template) { + var innerTokens = Lexer.tokenize(rawValue, true); + args = parser.parseStringTemplate(innerTokens); + } else { + args = []; + } + return { + type: "string", + token: stringToken, + args: args, + op: function (context) { + var returnStr = ""; + for (var i = 1; i < arguments.length; i++) { + var val = arguments[i]; + if (val !== undefined) { + returnStr += val; + } + } + return returnStr; + }, + evaluate: function (context) { + if (args.length === 0) { + return rawValue; + } else { + return runtime.unifiedEval(this, context); + } + }, + }; + }); + + parser.addGrammarElement("nakedString", function (parser, runtime, tokens) { + if (tokens.hasMore()) { + var tokenArr = tokens.consumeUntilWhitespace(); + tokens.matchTokenType("WHITESPACE"); + return { + type: "nakedString", + tokens: tokenArr, + evaluate: function (context) { + return tokenArr + .map(function (t) { + return t.value; + }) + .join(""); + }, + }; + } + }); + + parser.addLeafExpression("number", function (parser, runtime, tokens) { + var number = tokens.matchTokenType("NUMBER"); + if (!number) return; + var numberToken = number; + var value = parseFloat(/** @type {string} */ (number.value)); + return { + type: "number", + value: value, + numberToken: numberToken, + evaluate: function () { + return value; + }, + }; + }); + + parser.addLeafExpression("idRef", function (parser, runtime, tokens) { + var elementId = tokens.matchTokenType("ID_REF"); + if (!elementId) return; + if (!elementId.value) return; + // TODO - unify these two expression types + if (elementId.template) { + var templateValue = elementId.value.substring(2); + var innerTokens = Lexer.tokenize(templateValue); + var innerExpression = parser.requireElement("expression", innerTokens); + return { + type: "idRefTemplate", + args: [innerExpression], + op: function (context, arg) { + return runtime.getRootNode(context.me).getElementById(arg); + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + } else { + const value = elementId.value.substring(1); + return { + type: "idRef", + css: elementId.value, + value: value, + evaluate: function (context) { + return ( + runtime.getRootNode(context.me).getElementById(value) + ); + }, + }; + } + }); + + parser.addLeafExpression("classRef", function (parser, runtime, tokens) { + var classRef = tokens.matchTokenType("CLASS_REF"); + + if (!classRef) return; + if (!classRef.value) return; + + // TODO - unify these two expression types + if (classRef.template) { + var templateValue = classRef.value.substring(2); + var innerTokens = Lexer.tokenize(templateValue); + var innerExpression = parser.requireElement("expression", innerTokens); + return { + type: "classRefTemplate", + args: [innerExpression], + op: function (context, arg) { + return new ElementCollection("." + arg, context.me, true) + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + } else { + const css = classRef.value; + return { + type: "classRef", + css: css, + evaluate: function (context) { + return new ElementCollection(css, context.me, true) + }, + }; + } + }); + + class TemplatedQueryElementCollection extends ElementCollection { + constructor(css, relativeToElement, templateParts) { + super(css, relativeToElement); + this.templateParts = templateParts; + this.elements = templateParts.filter(elt => elt instanceof Element); + } + + get css() { + let rv = "", i = 0 + for (const val of this.templateParts) { + if (val instanceof Element) { + rv += "[data-hs-query-id='" + i++ + "']"; + } else rv += val; + } + return rv; + } + + [Symbol.iterator]() { + this.elements.forEach((el, i) => el.dataset.hsQueryId = i); + const rv = super[Symbol.iterator](); + this.elements.forEach(el => el.removeAttribute('data-hs-query-id')); + return rv; + } + } + + parser.addLeafExpression("queryRef", function (parser, runtime, tokens) { + var queryStart = tokens.matchOpToken("<"); + if (!queryStart) return; + var queryTokens = tokens.consumeUntil("/"); + tokens.requireOpToken("/"); + tokens.requireOpToken(">"); + var queryValue = queryTokens + .map(function (t) { + if (t.type === "STRING") { + return '"' + t.value + '"'; + } else { + return t.value; + } + }) + .join(""); + + var template, innerTokens, args; + if (queryValue.indexOf("$") >= 0) { + template = true; + innerTokens = Lexer.tokenize(queryValue, true); + args = parser.parseStringTemplate(innerTokens); + } + + return { + type: "queryRef", + css: queryValue, + args: args, + op: function (context, ...args) { + if (template) { + return new TemplatedQueryElementCollection(queryValue, context.me, args) + } else { + return new ElementCollection(queryValue, context.me) + } + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + }); + + parser.addLeafExpression("attributeRef", function (parser, runtime, tokens) { + var attributeRef = tokens.matchTokenType("ATTRIBUTE_REF"); + if (!attributeRef) return; + if (!attributeRef.value) return; + var outerVal = attributeRef.value; + if (outerVal.indexOf("[") === 0) { + var innerValue = outerVal.substring(2, outerVal.length - 1); + } else { + var innerValue = outerVal.substring(1); + } + var css = "[" + innerValue + "]"; + var split = innerValue.split("="); + var name = split[0]; + var value = split[1]; + if (value) { + // strip quotes + if (value.indexOf('"') === 0) { + value = value.substring(1, value.length - 1); + } + } + return { + type: "attributeRef", + name: name, + css: css, + value: value, + op: function (context) { + var target = context.you || context.me; + if (target) { + return target.getAttribute(name); + } + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + }); + + parser.addLeafExpression("styleRef", function (parser, runtime, tokens) { + var styleRef = tokens.matchTokenType("STYLE_REF"); + if (!styleRef) return; + if (!styleRef.value) return; + var styleProp = styleRef.value.substr(1); + if (styleProp.startsWith("computed-")) { + styleProp = styleProp.substr("computed-".length); + return { + type: "computedStyleRef", + name: styleProp, + op: function (context) { + var target = context.you || context.me; + if (target) { + return runtime.resolveComputedStyle(target, styleProp); + } + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + } else { + return { + type: "styleRef", + name: styleProp, + op: function (context) { + var target = context.you || context.me; + if (target) { + return runtime.resolveStyle(target, styleProp); + } + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + } + }); + + parser.addGrammarElement("objectKey", function (parser, runtime, tokens) { + var token; + if ((token = tokens.matchTokenType("STRING"))) { + return { + type: "objectKey", + key: token.value, + evaluate: function () { + return token.value; + }, + }; + } else if (tokens.matchOpToken("[")) { + var expr = parser.parseElement("expression", tokens); + tokens.requireOpToken("]"); + return { + type: "objectKey", + expr: expr, + args: [expr], + op: function (ctx, expr) { + return expr; + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + } else { + var key = ""; + do { + token = tokens.matchTokenType("IDENTIFIER") || tokens.matchOpToken("-"); + if (token) key += token.value; + } while (token); + return { + type: "objectKey", + key: key, + evaluate: function () { + return key; + }, + }; + } + }); + + parser.addLeafExpression("objectLiteral", function (parser, runtime, tokens) { + if (!tokens.matchOpToken("{")) return; + var keyExpressions = []; + var valueExpressions = []; + if (!tokens.matchOpToken("}")) { + do { + var name = parser.requireElement("objectKey", tokens); + tokens.requireOpToken(":"); + var value = parser.requireElement("expression", tokens); + valueExpressions.push(value); + keyExpressions.push(name); + } while (tokens.matchOpToken(",")); + tokens.requireOpToken("}"); + } + return { + type: "objectLiteral", + args: [keyExpressions, valueExpressions], + op: function (context, keys, values) { + var returnVal = {}; + for (var i = 0; i < keys.length; i++) { + returnVal[keys[i]] = values[i]; + } + return returnVal; + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + }); + + parser.addGrammarElement("nakedNamedArgumentList", function (parser, runtime, tokens) { + var fields = []; + var valueExpressions = []; + if (tokens.currentToken().type === "IDENTIFIER") { + do { + var name = tokens.requireTokenType("IDENTIFIER"); + tokens.requireOpToken(":"); + var value = parser.requireElement("expression", tokens); + valueExpressions.push(value); + fields.push({ name: name, value: value }); + } while (tokens.matchOpToken(",")); + } + return { + type: "namedArgumentList", + fields: fields, + args: [valueExpressions], + op: function (context, values) { + var returnVal = { _namedArgList_: true }; + for (var i = 0; i < values.length; i++) { + var field = fields[i]; + returnVal[field.name.value] = values[i]; + } + return returnVal; + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + }); + + parser.addGrammarElement("namedArgumentList", function (parser, runtime, tokens) { + if (!tokens.matchOpToken("(")) return; + var elt = parser.requireElement("nakedNamedArgumentList", tokens); + tokens.requireOpToken(")"); + return elt; + }); + + parser.addGrammarElement("symbol", function (parser, runtime, tokens) { + /** @scope {SymbolScope} */ + var scope = "default"; + if (tokens.matchToken("global")) { + scope = "global"; + } else if (tokens.matchToken("element") || tokens.matchToken("module")) { + scope = "element"; + // optional possessive + if (tokens.matchOpToken("'")) { + tokens.requireToken("s"); + } + } else if (tokens.matchToken("local")) { + scope = "local"; + } + + // TODO better look ahead here + let eltPrefix = tokens.matchOpToken(":"); + let identifier = tokens.matchTokenType("IDENTIFIER"); + if (identifier && identifier.value) { + var name = identifier.value; + if (eltPrefix) { + name = ":" + name; + } + if (scope === "default") { + if (name.indexOf("$") === 0) { + scope = "global"; + } + if (name.indexOf(":") === 0) { + scope = "element"; + } + } + return { + type: "symbol", + token: identifier, + scope: scope, + name: name, + evaluate: function (context) { + return runtime.resolveSymbol(name, context, scope); + }, + }; + } + }); + + parser.addGrammarElement("implicitMeTarget", function (parser, runtime, tokens) { + return { + type: "implicitMeTarget", + evaluate: function (context) { + return context.you || context.me; + }, + }; + }); + + parser.addLeafExpression("boolean", function (parser, runtime, tokens) { + var booleanLiteral = tokens.matchToken("true") || tokens.matchToken("false"); + if (!booleanLiteral) return; + const value = booleanLiteral.value === "true"; + return { + type: "boolean", + evaluate: function (context) { + return value; + }, + }; + }); + + parser.addLeafExpression("null", function (parser, runtime, tokens) { + if (tokens.matchToken("null")) { + return { + type: "null", + evaluate: function (context) { + return null; + }, + }; + } + }); + + parser.addLeafExpression("arrayLiteral", function (parser, runtime, tokens) { + if (!tokens.matchOpToken("[")) return; + var values = []; + if (!tokens.matchOpToken("]")) { + do { + var expr = parser.requireElement("expression", tokens); + values.push(expr); + } while (tokens.matchOpToken(",")); + tokens.requireOpToken("]"); + } + return { + type: "arrayLiteral", + values: values, + args: [values], + op: function (context, values) { + return values; + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + }); + + parser.addLeafExpression("blockLiteral", function (parser, runtime, tokens) { + if (!tokens.matchOpToken("\\")) return; + var args = []; + var arg1 = tokens.matchTokenType("IDENTIFIER"); + if (arg1) { + args.push(arg1); + while (tokens.matchOpToken(",")) { + args.push(tokens.requireTokenType("IDENTIFIER")); + } + } + // TODO compound op token + tokens.requireOpToken("-"); + tokens.requireOpToken(">"); + var expr = parser.requireElement("expression", tokens); + return { + type: "blockLiteral", + args: args, + expr: expr, + evaluate: function (ctx) { + var returnFunc = function () { + //TODO - push scope + for (var i = 0; i < args.length; i++) { + ctx.locals[args[i].value] = arguments[i]; + } + return expr.evaluate(ctx); //OK + }; + return returnFunc; + }, + }; + }); + + parser.addIndirectExpression("propertyAccess", function (parser, runtime, tokens, root) { + if (!tokens.matchOpToken(".")) return; + var prop = tokens.requireTokenType("IDENTIFIER"); + var propertyAccess = { + type: "propertyAccess", + root: root, + prop: prop, + args: [root], + op: function (_context, rootVal) { + var value = runtime.resolveProperty(rootVal, prop.value); + return value; + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + return parser.parseElement("indirectExpression", tokens, propertyAccess); + }); + + parser.addIndirectExpression("of", function (parser, runtime, tokens, root) { + if (!tokens.matchToken("of")) return; + var newRoot = parser.requireElement("unaryExpression", tokens); + // find the urroot + var childOfUrRoot = null; + var urRoot = root; + while (urRoot.root) { + childOfUrRoot = urRoot; + urRoot = urRoot.root; + } + if (urRoot.type !== "symbol" && urRoot.type !== "attributeRef" && urRoot.type !== "styleRef" && urRoot.type !== "computedStyleRef") { + parser.raiseParseError(tokens, "Cannot take a property of a non-symbol: " + urRoot.type); + } + var attribute = urRoot.type === "attributeRef"; + var style = urRoot.type === "styleRef" || urRoot.type === "computedStyleRef"; + if (attribute || style) { + var attributeElt = urRoot + } + var prop = urRoot.name; + + var propertyAccess = { + type: "ofExpression", + prop: urRoot.token, + root: newRoot, + attribute: attributeElt, + expression: root, + args: [newRoot], + op: function (context, rootVal) { + if (attribute) { + return runtime.resolveAttribute(rootVal, prop); + } else if (style) { + if (urRoot.type === "computedStyleRef") { + return runtime.resolveComputedStyle(rootVal, prop); + } else { + return runtime.resolveStyle(rootVal, prop); + } + } else { + return runtime.resolveProperty(rootVal, prop); + } + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + + if (urRoot.type === "attributeRef") { + propertyAccess.attribute = urRoot; + } + if (childOfUrRoot) { + childOfUrRoot.root = propertyAccess; + childOfUrRoot.args = [propertyAccess]; + } else { + root = propertyAccess; + } + + return parser.parseElement("indirectExpression", tokens, root); + }); + + parser.addIndirectExpression("possessive", function (parser, runtime, tokens, root) { + if (parser.possessivesDisabled) { + return; + } + var apostrophe = tokens.matchOpToken("'"); + if ( + apostrophe || + (root.type === "symbol" && + (root.name === "my" || root.name === "its" || root.name === "your") && + (tokens.currentToken().type === "IDENTIFIER" || tokens.currentToken().type === "ATTRIBUTE_REF" || tokens.currentToken().type === "STYLE_REF")) + ) { + if (apostrophe) { + tokens.requireToken("s"); + } + + var attribute, style, prop; + attribute = parser.parseElement("attributeRef", tokens); + if (attribute == null) { + style = parser.parseElement("styleRef", tokens); + if (style == null) { + prop = tokens.requireTokenType("IDENTIFIER"); + } + } + var propertyAccess = { + type: "possessive", + root: root, + attribute: attribute || style, + prop: prop, + args: [root], + op: function (context, rootVal) { + if (attribute) { + // @ts-ignore + var value = runtime.resolveAttribute(rootVal, attribute.name); + } else if (style) { + var value + if (style.type === 'computedStyleRef') { + value = runtime.resolveComputedStyle(rootVal, style['name']); + } else { + value = runtime.resolveStyle(rootVal, style['name']); + } + } else { + var value = runtime.resolveProperty(rootVal, prop.value); + } + return value; + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + return parser.parseElement("indirectExpression", tokens, propertyAccess); + } + }); + + parser.addIndirectExpression("inExpression", function (parser, runtime, tokens, root) { + if (!tokens.matchToken("in")) return; + var target = parser.requireElement("unaryExpression", tokens); + var propertyAccess = { + type: "inExpression", + root: root, + args: [root, target], + op: function (context, rootVal, target) { + var returnArr = []; + if (rootVal.css) { + runtime.implicitLoop(target, function (targetElt) { + var results = targetElt.querySelectorAll(rootVal.css); + for (var i = 0; i < results.length; i++) { + returnArr.push(results[i]); + } + }); + } else if (rootVal instanceof Element) { + var within = false; + runtime.implicitLoop(target, function (targetElt) { + if (targetElt.contains(rootVal)) { + within = true; + } + }); + if(within) { + return rootVal; + } + } else { + runtime.implicitLoop(rootVal, function (rootElt) { + runtime.implicitLoop(target, function (targetElt) { + if (rootElt === targetElt) { + returnArr.push(rootElt); + } + }); + }); + } + return returnArr; + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + return parser.parseElement("indirectExpression", tokens, propertyAccess); + }); + + parser.addIndirectExpression("asExpression", function (parser, runtime, tokens, root) { + if (!tokens.matchToken("as")) return; + tokens.matchToken("a") || tokens.matchToken("an"); + var conversion = parser.requireElement("dotOrColonPath", tokens).evaluate(); // OK No promise + var propertyAccess = { + type: "asExpression", + root: root, + args: [root], + op: function (context, rootVal) { + return runtime.convertValue(rootVal, conversion); + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + return parser.parseElement("indirectExpression", tokens, propertyAccess); + }); + + parser.addIndirectExpression("functionCall", function (parser, runtime, tokens, root) { + if (!tokens.matchOpToken("(")) return; + var args = []; + if (!tokens.matchOpToken(")")) { + do { + args.push(parser.requireElement("expression", tokens)); + } while (tokens.matchOpToken(",")); + tokens.requireOpToken(")"); + } + + if (root.root) { + var functionCall = { + type: "functionCall", + root: root, + argExressions: args, + args: [root.root, args], + op: function (context, rootRoot, args) { + runtime.nullCheck(rootRoot, root.root); + var func = rootRoot[root.prop.value]; + runtime.nullCheck(func, root); + if (func.hyperfunc) { + args.push(context); + } + return func.apply(rootRoot, args); + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + } else { + var functionCall = { + type: "functionCall", + root: root, + argExressions: args, + args: [root, args], + op: function (context, func, argVals) { + runtime.nullCheck(func, root); + if (func.hyperfunc) { + argVals.push(context); + } + var apply = func.apply(null, argVals); + return apply; + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + } + return parser.parseElement("indirectExpression", tokens, functionCall); + }); + + parser.addIndirectExpression("attributeRefAccess", function (parser, runtime, tokens, root) { + var attribute = parser.parseElement("attributeRef", tokens); + if (!attribute) return; + var attributeAccess = { + type: "attributeRefAccess", + root: root, + attribute: attribute, + args: [root], + op: function (_ctx, rootVal) { + // @ts-ignore + var value = runtime.resolveAttribute(rootVal, attribute.name); + return value; + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + return attributeAccess; + }); + + parser.addIndirectExpression("arrayIndex", function (parser, runtime, tokens, root) { + if (!tokens.matchOpToken("[")) return; + var andBefore = false; + var andAfter = false; + var firstIndex = null; + var secondIndex = null; + + if (tokens.matchOpToken("..")) { + andBefore = true; + firstIndex = parser.requireElement("expression", tokens); + } else { + firstIndex = parser.requireElement("expression", tokens); + + if (tokens.matchOpToken("..")) { + andAfter = true; + var current = tokens.currentToken(); + if (current.type !== "R_BRACKET") { + secondIndex = parser.parseElement("expression", tokens); + } + } + } + tokens.requireOpToken("]"); + + var arrayIndex = { + type: "arrayIndex", + root: root, + prop: firstIndex, + firstIndex: firstIndex, + secondIndex: secondIndex, + args: [root, firstIndex, secondIndex], + op: function (_ctx, root, firstIndex, secondIndex) { + if (root == null) { + return null; + } + if (andBefore) { + if (firstIndex < 0) { + firstIndex = root.length + firstIndex; + } + return root.slice(0, firstIndex + 1); // returns all items from beginning to firstIndex (inclusive) + } else if (andAfter) { + if (secondIndex != null) { + if (secondIndex < 0) { + secondIndex = root.length + secondIndex; + } + return root.slice(firstIndex, secondIndex + 1); // returns all items from firstIndex to secondIndex (inclusive) + } else { + return root.slice(firstIndex); // returns from firstIndex to end of array + } + } else { + return root[firstIndex]; + } + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + + return parser.parseElement("indirectExpression", tokens, arrayIndex); + }); + + // taken from https://drafts.csswg.org/css-values-4/#relative-length + // and https://drafts.csswg.org/css-values-4/#absolute-length + // (NB: we do not support `in` dues to conflicts w/ the hyperscript grammar) + var STRING_POSTFIXES = [ + 'em', 'ex', 'cap', 'ch', 'ic', 'rem', 'lh', 'rlh', 'vw', 'vh', 'vi', 'vb', 'vmin', 'vmax', + 'cm', 'mm', 'Q', 'pc', 'pt', 'px' + ]; + parser.addGrammarElement("postfixExpression", function (parser, runtime, tokens) { + var root = parser.parseElement("primaryExpression", tokens); + + let stringPosfix = tokens.matchAnyToken.apply(tokens, STRING_POSTFIXES) || tokens.matchOpToken("%"); + if (stringPosfix) { + return { + type: "stringPostfix", + postfix: stringPosfix.value, + args: [root], + op: function (context, val) { + return "" + val + stringPosfix.value; + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + } + + var timeFactor = null; + if (tokens.matchToken("s") || tokens.matchToken("seconds")) { + timeFactor = 1000; + } else if (tokens.matchToken("ms") || tokens.matchToken("milliseconds")) { + timeFactor = 1; + } + if (timeFactor) { + return { + type: "timeExpression", + time: root, + factor: timeFactor, + args: [root], + op: function (_context, val) { + return val * timeFactor; + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + } + + if (tokens.matchOpToken(":")) { + var typeName = tokens.requireTokenType("IDENTIFIER"); + if (!typeName.value) return; + var nullOk = !tokens.matchOpToken("!"); + return { + type: "typeCheck", + typeName: typeName, + nullOk: nullOk, + args: [root], + op: function (context, val) { + var passed = runtime.typeCheck(val, this.typeName.value, nullOk); + if (passed) { + return val; + } else { + throw new Error("Typecheck failed! Expected: " + typeName.value); + } + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + } else { + return root; + } + }); + + parser.addGrammarElement("logicalNot", function (parser, runtime, tokens) { + if (!tokens.matchToken("not")) return; + var root = parser.requireElement("unaryExpression", tokens); + return { + type: "logicalNot", + root: root, + args: [root], + op: function (context, val) { + return !val; + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + }); + + parser.addGrammarElement("noExpression", function (parser, runtime, tokens) { + if (!tokens.matchToken("no")) return; + var root = parser.requireElement("unaryExpression", tokens); + return { + type: "noExpression", + root: root, + args: [root], + op: function (_context, val) { + return runtime.isEmpty(val); + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + }); + + parser.addLeafExpression("some", function (parser, runtime, tokens) { + if (!tokens.matchToken("some")) return; + var root = parser.requireElement("expression", tokens); + return { + type: "noExpression", + root: root, + args: [root], + op: function (_context, val) { + return !runtime.isEmpty(val); + }, + evaluate(context) { + return runtime.unifiedEval(this, context); + }, + }; + }); + + parser.addGrammarElement("negativeNumber", function (parser, runtime, tokens) { + if (!tokens.matchOpToken("-")) return; + var root = parser.requireElement("unaryExpression", tokens); + return { + type: "negativeNumber", + root: root, + args: [root], + op: function (context, value) { + return -1 * value; + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + }); + + parser.addGrammarElement("unaryExpression", function (parser, runtime, tokens) { + tokens.matchToken("the"); // optional "the" + return parser.parseAnyOf( + ["beepExpression", "logicalNot", "relativePositionalExpression", "positionalExpression", "noExpression", "negativeNumber", "postfixExpression"], + tokens + ); + }); + + parser.addGrammarElement("beepExpression", function (parser, runtime, tokens) { + if (!tokens.matchToken("beep!")) return; + var expression = parser.parseElement("unaryExpression", tokens); + if (expression) { + expression['booped'] = true; + var originalEvaluate = expression.evaluate; + expression.evaluate = function(ctx){ + let value = originalEvaluate.apply(expression, arguments); + let element = ctx.me; + runtime.beepValueToConsole(element, expression, value); + return value; + } + return expression; + } + }); + + var scanForwardQuery = function(start, root, match, wrap) { + var results = root.querySelectorAll(match); + for (var i = 0; i < results.length; i++) { + var elt = results[i]; + if (elt.compareDocumentPosition(start) === Node.DOCUMENT_POSITION_PRECEDING) { + return elt; + } + } + if (wrap) { + return results[0]; + } + } + + var scanBackwardsQuery = function(start, root, match, wrap) { + var results = root.querySelectorAll(match); + for (var i = results.length - 1; i >= 0; i--) { + var elt = results[i]; + if (elt.compareDocumentPosition(start) === Node.DOCUMENT_POSITION_FOLLOWING) { + return elt; + } + } + if (wrap) { + return results[results.length - 1]; + } + } + + var scanForwardArray = function(start, array, match, wrap) { + var matches = []; + Runtime.prototype.forEach(array, function(elt){ + if (elt.matches(match) || elt === start) { + matches.push(elt); + } + }) + for (var i = 0; i < matches.length - 1; i++) { + var elt = matches[i]; + if (elt === start) { + return matches[i + 1]; + } + } + if (wrap) { + var first = matches[0]; + if (first && first.matches(match)) { + return first; + } + } + } + + var scanBackwardsArray = function(start, array, match, wrap) { + return scanForwardArray(start, Array.from(array).reverse(), match, wrap); + } + + parser.addGrammarElement("relativePositionalExpression", function (parser, runtime, tokens) { + var op = tokens.matchAnyToken("next", "previous"); + if (!op) return; + var forwardSearch = op.value === "next"; + + var thingElt = parser.parseElement("expression", tokens); + + if (tokens.matchToken("from")) { + tokens.pushFollow("in"); + try { + var from = parser.requireElement("unaryExpression", tokens); + } finally { + tokens.popFollow(); + } + } else { + var from = parser.requireElement("implicitMeTarget", tokens); + } + + var inSearch = false; + var withinElt; + if (tokens.matchToken("in")) { + inSearch = true; + var inElt = parser.requireElement("unaryExpression", tokens); + } else if (tokens.matchToken("within")) { + withinElt = parser.requireElement("unaryExpression", tokens); + } else { + withinElt = document.body; + } + + var wrapping = false; + if (tokens.matchToken("with")) { + tokens.requireToken("wrapping") + wrapping = true; + } + + return { + type: "relativePositionalExpression", + from: from, + forwardSearch: forwardSearch, + inSearch: inSearch, + wrapping: wrapping, + inElt: inElt, + withinElt: withinElt, + operator: op.value, + args: [thingElt, from, inElt, withinElt], + op: function (context, thing, from, inElt, withinElt) { + + var css = thing.css; + if (css == null) { + throw "Expected a CSS value to be returned by " + Tokens.sourceFor.apply(thingElt); + } + + if(inSearch) { + if (inElt) { + if (forwardSearch) { + return scanForwardArray(from, inElt, css, wrapping); + } else { + return scanBackwardsArray(from, inElt, css, wrapping); + } + } + } else { + if (withinElt) { + if (forwardSearch) { + return scanForwardQuery(from, withinElt, css, wrapping); + } else { + return scanBackwardsQuery(from, withinElt, css, wrapping); + } + } + } + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + } + + }); + + parser.addGrammarElement("positionalExpression", function (parser, runtime, tokens) { + var op = tokens.matchAnyToken("first", "last", "random"); + if (!op) return; + tokens.matchAnyToken("in", "from", "of"); + var rhs = parser.requireElement("unaryExpression", tokens); + const operator = op.value; + return { + type: "positionalExpression", + rhs: rhs, + operator: op.value, + args: [rhs], + op: function (context, rhsVal) { + if (rhsVal && !Array.isArray(rhsVal)) { + if (rhsVal.children) { + rhsVal = rhsVal.children; + } else { + rhsVal = Array.from(rhsVal); + } + } + if (rhsVal) { + if (operator === "first") { + return rhsVal[0]; + } else if (operator === "last") { + return rhsVal[rhsVal.length - 1]; + } else if (operator === "random") { + return rhsVal[Math.floor(Math.random() * rhsVal.length)]; + } + } + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + }); + + parser.addGrammarElement("mathOperator", function (parser, runtime, tokens) { + var expr = parser.parseElement("unaryExpression", tokens); + var mathOp, + initialMathOp = null; + mathOp = tokens.matchAnyOpToken("+", "-", "*", "/") || tokens.matchToken('mod'); + while (mathOp) { + initialMathOp = initialMathOp || mathOp; + var operator = mathOp.value; + if (initialMathOp.value !== operator) { + parser.raiseParseError(tokens, "You must parenthesize math operations with different operators"); + } + var rhs = parser.parseElement("unaryExpression", tokens); + expr = { + type: "mathOperator", + lhs: expr, + rhs: rhs, + operator: operator, + args: [expr, rhs], + op: function (context, lhsVal, rhsVal) { + if (operator === "+") { + return lhsVal + rhsVal; + } else if (operator === "-") { + return lhsVal - rhsVal; + } else if (operator === "*") { + return lhsVal * rhsVal; + } else if (operator === "/") { + return lhsVal / rhsVal; + } else if (operator === "mod") { + return lhsVal % rhsVal; + } + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + mathOp = tokens.matchAnyOpToken("+", "-", "*", "/") || tokens.matchToken('mod'); + } + return expr; + }); + + parser.addGrammarElement("mathExpression", function (parser, runtime, tokens) { + return parser.parseAnyOf(["mathOperator", "unaryExpression"], tokens); + }); + + function sloppyContains(src, container, value){ + if (container['contains']) { + return container.contains(value); + } else if (container['includes']) { + return container.includes(value); + } else { + throw Error("The value of " + src.sourceFor() + " does not have a contains or includes method on it"); + } + } + function sloppyMatches(src, target, toMatch){ + if (target['match']) { + return !!target.match(toMatch); + } else if (target['matches']) { + return target.matches(toMatch); + } else { + throw Error("The value of " + src.sourceFor() + " does not have a match or matches method on it"); + } + } + + parser.addGrammarElement("comparisonOperator", function (parser, runtime, tokens) { + var expr = parser.parseElement("mathExpression", tokens); + var comparisonToken = tokens.matchAnyOpToken("<", ">", "<=", ">=", "==", "===", "!=", "!=="); + var operator = comparisonToken ? comparisonToken.value : null; + var hasRightValue = true; // By default, most comparisons require two values, but there are some exceptions. + var typeCheck = false; + + if (operator == null) { + if (tokens.matchToken("is") || tokens.matchToken("am")) { + if (tokens.matchToken("not")) { + if (tokens.matchToken("in")) { + operator = "not in"; + } else if (tokens.matchToken("a")) { + operator = "not a"; + typeCheck = true; + } else if (tokens.matchToken("empty")) { + operator = "not empty"; + hasRightValue = false; + } else { + if (tokens.matchToken("really")) { + operator = "!=="; + } else { + operator = "!="; + } + // consume additional optional syntax + if (tokens.matchToken("equal")) { + tokens.matchToken("to"); + } + } + } else if (tokens.matchToken("in")) { + operator = "in"; + } else if (tokens.matchToken("a")) { + operator = "a"; + typeCheck = true; + } else if (tokens.matchToken("empty")) { + operator = "empty"; + hasRightValue = false; + } else if (tokens.matchToken("less")) { + tokens.requireToken("than"); + if (tokens.matchToken("or")) { + tokens.requireToken("equal"); + tokens.requireToken("to"); + operator = "<="; + } else { + operator = "<"; + } + } else if (tokens.matchToken("greater")) { + tokens.requireToken("than"); + if (tokens.matchToken("or")) { + tokens.requireToken("equal"); + tokens.requireToken("to"); + operator = ">="; + } else { + operator = ">"; + } + } else { + if (tokens.matchToken("really")) { + operator = "==="; + } else { + operator = "=="; + } + if (tokens.matchToken("equal")) { + tokens.matchToken("to"); + } + } + } else if (tokens.matchToken("equals")) { + operator = "=="; + } else if (tokens.matchToken("really")) { + tokens.requireToken("equals") + operator = "==="; + } else if (tokens.matchToken("exist") || tokens.matchToken("exists")) { + operator = "exist"; + hasRightValue = false; + } else if (tokens.matchToken("matches") || tokens.matchToken("match")) { + operator = "match"; + } else if (tokens.matchToken("contains") || tokens.matchToken("contain")) { + operator = "contain"; + } else if (tokens.matchToken("includes") || tokens.matchToken("include")) { + operator = "include"; + } else if (tokens.matchToken("do") || tokens.matchToken("does")) { + tokens.requireToken("not"); + if (tokens.matchToken("matches") || tokens.matchToken("match")) { + operator = "not match"; + } else if (tokens.matchToken("contains") || tokens.matchToken("contain")) { + operator = "not contain"; + } else if (tokens.matchToken("exist") || tokens.matchToken("exist")) { + operator = "not exist"; + hasRightValue = false; + } else if (tokens.matchToken("include")) { + operator = "not include"; + } else { + parser.raiseParseError(tokens, "Expected matches or contains"); + } + } + } + + if (operator) { + // Do not allow chained comparisons, which is dumb + var typeName, nullOk, rhs + if (typeCheck) { + typeName = tokens.requireTokenType("IDENTIFIER"); + nullOk = !tokens.matchOpToken("!"); + } else if (hasRightValue) { + rhs = parser.requireElement("mathExpression", tokens); + if (operator === "match" || operator === "not match") { + rhs = rhs.css ? rhs.css : rhs; + } + } + var lhs = expr; + expr = { + type: "comparisonOperator", + operator: operator, + typeName: typeName, + nullOk: nullOk, + lhs: expr, + rhs: rhs, + args: [expr, rhs], + op: function (context, lhsVal, rhsVal) { + if (operator === "==") { + return lhsVal == rhsVal; + } else if (operator === "!=") { + return lhsVal != rhsVal; + } + if (operator === "===") { + return lhsVal === rhsVal; + } else if (operator === "!==") { + return lhsVal !== rhsVal; + } + if (operator === "match") { + return lhsVal != null && sloppyMatches(lhs, lhsVal, rhsVal); + } + if (operator === "not match") { + return lhsVal == null || !sloppyMatches(lhs, lhsVal, rhsVal); + } + if (operator === "in") { + return rhsVal != null && sloppyContains(rhs, rhsVal, lhsVal); + } + if (operator === "not in") { + return rhsVal == null || !sloppyContains(rhs, rhsVal, lhsVal); + } + if (operator === "contain") { + return lhsVal != null && sloppyContains(lhs, lhsVal, rhsVal); + } + if (operator === "not contain") { + return lhsVal == null || !sloppyContains(lhs, lhsVal, rhsVal); + } + if (operator === "include") { + return lhsVal != null && sloppyContains(lhs, lhsVal, rhsVal); + } + if (operator === "not include") { + return lhsVal == null || !sloppyContains(lhs, lhsVal, rhsVal); + } + if (operator === "===") { + return lhsVal === rhsVal; + } else if (operator === "!==") { + return lhsVal !== rhsVal; + } else if (operator === "<") { + return lhsVal < rhsVal; + } else if (operator === ">") { + return lhsVal > rhsVal; + } else if (operator === "<=") { + return lhsVal <= rhsVal; + } else if (operator === ">=") { + return lhsVal >= rhsVal; + } else if (operator === "empty") { + return runtime.isEmpty(lhsVal); + } else if (operator === "not empty") { + return !runtime.isEmpty(lhsVal); + } else if (operator === "exist") { + return runtime.doesExist(lhsVal); + } else if (operator === "not exist") { + return !runtime.doesExist(lhsVal); + } else if (operator === "a") { + return runtime.typeCheck(lhsVal, typeName.value, nullOk); + } else if (operator === "not a") { + return !runtime.typeCheck(lhsVal, typeName.value, nullOk); + } else { + throw "Unknown comparison : " + operator; + } + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + } + return expr; + }); + + parser.addGrammarElement("comparisonExpression", function (parser, runtime, tokens) { + return parser.parseAnyOf(["comparisonOperator", "mathExpression"], tokens); + }); + + parser.addGrammarElement("logicalOperator", function (parser, runtime, tokens) { + var expr = parser.parseElement("comparisonExpression", tokens); + var logicalOp, + initialLogicalOp = null; + logicalOp = tokens.matchToken("and") || tokens.matchToken("or"); + while (logicalOp) { + initialLogicalOp = initialLogicalOp || logicalOp; + if (initialLogicalOp.value !== logicalOp.value) { + parser.raiseParseError(tokens, "You must parenthesize logical operations with different operators"); + } + var rhs = parser.requireElement("comparisonExpression", tokens); + const operator = logicalOp.value; + expr = { + type: "logicalOperator", + operator: operator, + lhs: expr, + rhs: rhs, + args: [expr, rhs], + op: function (context, lhsVal, rhsVal) { + if (operator === "and") { + return lhsVal && rhsVal; + } else { + return lhsVal || rhsVal; + } + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + logicalOp = tokens.matchToken("and") || tokens.matchToken("or"); + } + return expr; + }); + + parser.addGrammarElement("logicalExpression", function (parser, runtime, tokens) { + return parser.parseAnyOf(["logicalOperator", "mathExpression"], tokens); + }); + + parser.addGrammarElement("asyncExpression", function (parser, runtime, tokens) { + if (tokens.matchToken("async")) { + var value = parser.requireElement("logicalExpression", tokens); + var expr = { + type: "asyncExpression", + value: value, + evaluate: function (context) { + return { + asyncWrapper: true, + value: this.value.evaluate(context), //OK + }; + }, + }; + return expr; + } else { + return parser.parseElement("logicalExpression", tokens); + } + }); + + parser.addGrammarElement("expression", function (parser, runtime, tokens) { + tokens.matchToken("the"); // optional the + return parser.parseElement("asyncExpression", tokens); + }); + + parser.addGrammarElement("assignableExpression", function (parser, runtime, tokens) { + tokens.matchToken("the"); // optional the + + // TODO obviously we need to generalize this as a left hand side / targetable concept + var expr = parser.parseElement("primaryExpression", tokens); + if (expr && ( + expr.type === "symbol" || + expr.type === "ofExpression" || + expr.type === "propertyAccess" || + expr.type === "attributeRefAccess" || + expr.type === "attributeRef" || + expr.type === "styleRef" || + expr.type === "arrayIndex" || + expr.type === "possessive") + ) { + return expr; + } else { + parser.raiseParseError( + tokens, + "A target expression must be writable. The expression type '" + (expr && expr.type) + "' is not." + ); + } + return expr; + }); + + parser.addGrammarElement("hyperscript", function (parser, runtime, tokens) { + var features = []; + + if (tokens.hasMore()) { + while (parser.featureStart(tokens.currentToken()) || tokens.currentToken().value === "(") { + var feature = parser.requireElement("feature", tokens); + features.push(feature); + tokens.matchToken("end"); // optional end + } + } + return { + type: "hyperscript", + features: features, + apply: function (target, source, args) { + // no op + for (const feature of features) { + feature.install(target, source, args); + } + }, + }; + }); + + var parseEventArgs = function (tokens) { + var args = []; + // handle argument list (look ahead 3) + if ( + tokens.token(0).value === "(" && + (tokens.token(1).value === ")" || tokens.token(2).value === "," || tokens.token(2).value === ")") + ) { + tokens.matchOpToken("("); + do { + args.push(tokens.requireTokenType("IDENTIFIER")); + } while (tokens.matchOpToken(",")); + tokens.requireOpToken(")"); + } + return args; + }; + + parser.addFeature("on", function (parser, runtime, tokens) { + if (!tokens.matchToken("on")) return; + var every = false; + if (tokens.matchToken("every")) { + every = true; + } + var events = []; + var displayName = null; + do { + var on = parser.requireElement("eventName", tokens, "Expected event name"); + + var eventName = on.evaluate(); // OK No Promise + + if (displayName) { + displayName = displayName + " or " + eventName; + } else { + displayName = "on " + eventName; + } + var args = parseEventArgs(tokens); + + var filter = null; + if (tokens.matchOpToken("[")) { + filter = parser.requireElement("expression", tokens); + tokens.requireOpToken("]"); + } + + var startCount, endCount ,unbounded; + if (tokens.currentToken().type === "NUMBER") { + var startCountToken = tokens.consumeToken(); + if (!startCountToken.value) return; + startCount = parseInt(startCountToken.value); + if (tokens.matchToken("to")) { + var endCountToken = tokens.consumeToken(); + if (!endCountToken.value) return; + endCount = parseInt(endCountToken.value); + } else if (tokens.matchToken("and")) { + unbounded = true; + tokens.requireToken("on"); + } + } + + var intersectionSpec, mutationSpec; + if (eventName === "intersection") { + intersectionSpec = {}; + if (tokens.matchToken("with")) { + intersectionSpec["with"] = parser.requireElement("expression", tokens).evaluate(); + } + if (tokens.matchToken("having")) { + do { + if (tokens.matchToken("margin")) { + intersectionSpec["rootMargin"] = parser.requireElement("stringLike", tokens).evaluate(); + } else if (tokens.matchToken("threshold")) { + intersectionSpec["threshold"] = parser.requireElement("expression", tokens).evaluate(); + } else { + parser.raiseParseError(tokens, "Unknown intersection config specification"); + } + } while (tokens.matchToken("and")); + } + } else if (eventName === "mutation") { + mutationSpec = {}; + if (tokens.matchToken("of")) { + do { + if (tokens.matchToken("anything")) { + mutationSpec["attributes"] = true; + mutationSpec["subtree"] = true; + mutationSpec["characterData"] = true; + mutationSpec["childList"] = true; + } else if (tokens.matchToken("childList")) { + mutationSpec["childList"] = true; + } else if (tokens.matchToken("attributes")) { + mutationSpec["attributes"] = true; + mutationSpec["attributeOldValue"] = true; + } else if (tokens.matchToken("subtree")) { + mutationSpec["subtree"] = true; + } else if (tokens.matchToken("characterData")) { + mutationSpec["characterData"] = true; + mutationSpec["characterDataOldValue"] = true; + } else if (tokens.currentToken().type === "ATTRIBUTE_REF") { + var attribute = tokens.consumeToken(); + if (mutationSpec["attributeFilter"] == null) { + mutationSpec["attributeFilter"] = []; + } + if (attribute.value.indexOf("@") == 0) { + mutationSpec["attributeFilter"].push(attribute.value.substring(1)); + } else { + parser.raiseParseError( + tokens, + "Only shorthand attribute references are allowed here" + ); + } + } else { + parser.raiseParseError(tokens, "Unknown mutation config specification"); + } + } while (tokens.matchToken("or")); + } else { + mutationSpec["attributes"] = true; + mutationSpec["characterData"] = true; + mutationSpec["childList"] = true; + } + } + + var from = null; + var elsewhere = false; + if (tokens.matchToken("from")) { + if (tokens.matchToken("elsewhere")) { + elsewhere = true; + } else { + tokens.pushFollow("or"); + try { + from = parser.requireElement("expression", tokens) + } finally { + tokens.popFollow(); + } + if (!from) { + parser.raiseParseError(tokens, 'Expected either target value or "elsewhere".'); + } + } + } + // support both "elsewhere" and "from elsewhere" + if (from === null && elsewhere === false && tokens.matchToken("elsewhere")) { + elsewhere = true; + } + + if (tokens.matchToken("in")) { + var inExpr = parser.parseElement('unaryExpression', tokens); + } + + if (tokens.matchToken("debounced")) { + tokens.requireToken("at"); + var timeExpr = parser.requireElement("unaryExpression", tokens); + // @ts-ignore + var debounceTime = timeExpr.evaluate({}); // OK No promise TODO make a literal time expr + } else if (tokens.matchToken("throttled")) { + tokens.requireToken("at"); + var timeExpr = parser.requireElement("unaryExpression", tokens); + // @ts-ignore + var throttleTime = timeExpr.evaluate({}); // OK No promise TODO make a literal time expr + } + + events.push({ + execCount: 0, + every: every, + on: eventName, + args: args, + filter: filter, + from: from, + inExpr: inExpr, + elsewhere: elsewhere, + startCount: startCount, + endCount: endCount, + unbounded: unbounded, + debounceTime: debounceTime, + throttleTime: throttleTime, + mutationSpec: mutationSpec, + intersectionSpec: intersectionSpec, + debounced: undefined, + lastExec: undefined, + }); + } while (tokens.matchToken("or")); + + var queueLast = true; + if (!every) { + if (tokens.matchToken("queue")) { + if (tokens.matchToken("all")) { + var queueAll = true; + var queueLast = false; + } else if (tokens.matchToken("first")) { + var queueFirst = true; + } else if (tokens.matchToken("none")) { + var queueNone = true; + } else { + tokens.requireToken("last"); + } + } + } + + var start = parser.requireElement("commandList", tokens); + parser.ensureTerminated(start); + + var errorSymbol, errorHandler; + if (tokens.matchToken("catch")) { + errorSymbol = tokens.requireTokenType("IDENTIFIER").value; + errorHandler = parser.requireElement("commandList", tokens); + parser.ensureTerminated(errorHandler); + } + + if (tokens.matchToken("finally")) { + var finallyHandler = parser.requireElement("commandList", tokens); + parser.ensureTerminated(finallyHandler); + } + + var onFeature = { + displayName: displayName, + events: events, + start: start, + every: every, + execCount: 0, + errorHandler: errorHandler, + errorSymbol: errorSymbol, + execute: function (/** @type {Context} */ ctx) { + let eventQueueInfo = runtime.getEventQueueFor(ctx.me, onFeature); + if (eventQueueInfo.executing && every === false) { + if (queueNone || (queueFirst && eventQueueInfo.queue.length > 0)) { + return; + } + if (queueLast) { + eventQueueInfo.queue.length = 0; + } + eventQueueInfo.queue.push(ctx); + return; + } + onFeature.execCount++; + eventQueueInfo.executing = true; + ctx.meta.onHalt = function () { + eventQueueInfo.executing = false; + var queued = eventQueueInfo.queue.shift(); + if (queued) { + setTimeout(function () { + onFeature.execute(queued); + }, 1); + } + }; + ctx.meta.reject = function (err) { + console.error(err.message ? err.message : err); + var hypertrace = runtime.getHyperTrace(ctx, err); + if (hypertrace) { + hypertrace.print(); + } + runtime.triggerEvent(ctx.me, "exception", { + error: err, + }); + }; + start.execute(ctx); + }, + install: function (elt, source) { + for (const eventSpec of onFeature.events) { + var targets; + if (eventSpec.elsewhere) { + targets = [document]; + } else if (eventSpec.from) { + targets = eventSpec.from.evaluate(runtime.makeContext(elt, onFeature, elt, null)); + } else { + targets = [elt]; + } + runtime.implicitLoop(targets, function (target) { + // OK NO PROMISE + + var eventName = eventSpec.on; + if (target == null) { + console.warn("'%s' feature ignored because target does not exists:", displayName, elt); + return; + } + + if (eventSpec.mutationSpec) { + eventName = "hyperscript:mutation"; + const observer = new MutationObserver(function (mutationList, observer) { + if (!onFeature.executing) { + runtime.triggerEvent(target, eventName, { + mutationList: mutationList, + observer: observer, + }); + } + }); + observer.observe(target, eventSpec.mutationSpec); + } + + if (eventSpec.intersectionSpec) { + eventName = "hyperscript:intersection"; + const observer = new IntersectionObserver(function (entries) { + for (const entry of entries) { + var detail = { + observer: observer, + }; + detail = Object.assign(detail, entry); + detail["intersecting"] = entry.isIntersecting; + runtime.triggerEvent(target, eventName, detail); + } + }, eventSpec.intersectionSpec); + observer.observe(target); + } + + var addEventListener = target.addEventListener || target.on; + addEventListener.call(target, eventName, function listener(evt) { + // OK NO PROMISE + if (typeof Node !== 'undefined' && elt instanceof Node && target !== elt && !elt.isConnected) { + target.removeEventListener(eventName, listener); + return; + } + + var ctx = runtime.makeContext(elt, onFeature, elt, evt); + if (eventSpec.elsewhere && elt.contains(evt.target)) { + return; + } + if (eventSpec.from) { + ctx.result = target; + } + + // establish context + for (const arg of eventSpec.args) { + let eventValue = ctx.event[arg.value]; + if (eventValue !== undefined) { + ctx.locals[arg.value] = eventValue; + } else if ('detail' in ctx.event) { + ctx.locals[arg.value] = ctx.event['detail'][arg.value]; + } + } + + // install error handler if any + ctx.meta.errorHandler = errorHandler; + ctx.meta.errorSymbol = errorSymbol; + ctx.meta.finallyHandler = finallyHandler; + + // apply filter + if (eventSpec.filter) { + var initialCtx = ctx.meta.context; + ctx.meta.context = ctx.event; + try { + var value = eventSpec.filter.evaluate(ctx); //OK NO PROMISE + if (value) { + // match the javascript semantics for if statements + } else { + return; + } + } finally { + ctx.meta.context = initialCtx; + } + } + + if (eventSpec.inExpr) { + var inElement = evt.target; + while (true) { + if (inElement.matches && inElement.matches(eventSpec.inExpr.css)) { + ctx.result = inElement; + break; + } else { + inElement = inElement.parentElement; + if (inElement == null) { + return; // no match found + } + } + } + } + + // verify counts + eventSpec.execCount++; + if (eventSpec.startCount) { + if (eventSpec.endCount) { + if ( + eventSpec.execCount < eventSpec.startCount || + eventSpec.execCount > eventSpec.endCount + ) { + return; + } + } else if (eventSpec.unbounded) { + if (eventSpec.execCount < eventSpec.startCount) { + return; + } + } else if (eventSpec.execCount !== eventSpec.startCount) { + return; + } + } + + //debounce + if (eventSpec.debounceTime) { + if (eventSpec.debounced) { + clearTimeout(eventSpec.debounced); + } + eventSpec.debounced = setTimeout(function () { + onFeature.execute(ctx); + }, eventSpec.debounceTime); + return; + } + + // throttle + if (eventSpec.throttleTime) { + if ( + eventSpec.lastExec && + Date.now() < (eventSpec.lastExec + eventSpec.throttleTime) + ) { + return; + } else { + eventSpec.lastExec = Date.now(); + } + } + + // apply execute + onFeature.execute(ctx); + }); + }); + } + }, + }; + parser.setParent(start, onFeature); + return onFeature; + }); + + parser.addFeature("def", function (parser, runtime, tokens) { + if (!tokens.matchToken("def")) return; + var functionName = parser.requireElement("dotOrColonPath", tokens); + var nameVal = functionName.evaluate(); // OK + var nameSpace = nameVal.split("."); + var funcName = nameSpace.pop(); + + var args = []; + if (tokens.matchOpToken("(")) { + if (tokens.matchOpToken(")")) { + // emtpy args list + } else { + do { + args.push(tokens.requireTokenType("IDENTIFIER")); + } while (tokens.matchOpToken(",")); + tokens.requireOpToken(")"); + } + } + + var start = parser.requireElement("commandList", tokens); + + var errorSymbol, errorHandler; + if (tokens.matchToken("catch")) { + errorSymbol = tokens.requireTokenType("IDENTIFIER").value; + errorHandler = parser.parseElement("commandList", tokens); + } + + if (tokens.matchToken("finally")) { + var finallyHandler = parser.requireElement("commandList", tokens); + parser.ensureTerminated(finallyHandler); + } + + var functionFeature = { + displayName: + funcName + + "(" + + args + .map(function (arg) { + return arg.value; + }) + .join(", ") + + ")", + name: funcName, + args: args, + start: start, + errorHandler: errorHandler, + errorSymbol: errorSymbol, + finallyHandler: finallyHandler, + install: function (target, source) { + var func = function () { + // null, worker + var ctx = runtime.makeContext(source, functionFeature, target, null); + + // install error handler if any + ctx.meta.errorHandler = errorHandler; + ctx.meta.errorSymbol = errorSymbol; + ctx.meta.finallyHandler = finallyHandler; + + for (var i = 0; i < args.length; i++) { + var name = args[i]; + var argumentVal = arguments[i]; + if (name) { + ctx.locals[name.value] = argumentVal; + } + } + ctx.meta.caller = arguments[args.length]; + if (ctx.meta.caller) { + ctx.meta.callingCommand = ctx.meta.caller.meta.command; + } + var resolve, + reject = null; + var promise = new Promise(function (theResolve, theReject) { + resolve = theResolve; + reject = theReject; + }); + start.execute(ctx); + if (ctx.meta.returned) { + return ctx.meta.returnValue; + } else { + ctx.meta.resolve = resolve; + ctx.meta.reject = reject; + return promise; + } + }; + func.hyperfunc = true; + func.hypername = nameVal; + runtime.assignToNamespace(target, nameSpace, funcName, func); + }, + }; + + parser.ensureTerminated(start); + + // terminate error handler if any + if (errorHandler) { + parser.ensureTerminated(errorHandler); + } + + parser.setParent(start, functionFeature); + return functionFeature; + }); + + parser.addFeature("set", function (parser, runtime, tokens) { + let setCmd = parser.parseElement("setCommand", tokens); + if (setCmd) { + if (setCmd.target.scope !== "element") { + parser.raiseParseError(tokens, "variables declared at the feature level must be element scoped."); + } + let setFeature = { + start: setCmd, + install: function (target, source) { + setCmd && setCmd.execute(runtime.makeContext(target, setFeature, target, null)); + }, + }; + parser.ensureTerminated(setCmd); + return setFeature; + } + }); + + parser.addFeature("init", function (parser, runtime, tokens) { + if (!tokens.matchToken("init")) return; + + var immediately = tokens.matchToken("immediately"); + + var start = parser.requireElement("commandList", tokens); + var initFeature = { + start: start, + install: function (target, source) { + let handler = function () { + start && start.execute(runtime.makeContext(target, initFeature, target, null)); + }; + if (immediately) { + handler(); + } else { + setTimeout(handler, 0); + } + }, + }; + + // terminate body + parser.ensureTerminated(start); + parser.setParent(start, initFeature); + return initFeature; + }); + + parser.addFeature("worker", function (parser, runtime, tokens) { + if (tokens.matchToken("worker")) { + parser.raiseParseError( + tokens, + "In order to use the 'worker' feature, include " + + "the _hyperscript worker plugin. See " + + "https://hyperscript.org/features/worker/ for " + + "more info." + ); + return undefined + } + }); + + parser.addFeature("behavior", function (parser, runtime, tokens) { + if (!tokens.matchToken("behavior")) return; + var path = parser.requireElement("dotOrColonPath", tokens).evaluate(); + var nameSpace = path.split("."); + var name = nameSpace.pop(); + + var formalParams = []; + if (tokens.matchOpToken("(") && !tokens.matchOpToken(")")) { + do { + formalParams.push(tokens.requireTokenType("IDENTIFIER").value); + } while (tokens.matchOpToken(",")); + tokens.requireOpToken(")"); + } + var hs = parser.requireElement("hyperscript", tokens); + for (var i = 0; i < hs.features.length; i++) { + var feature = hs.features[i]; + feature.behavior = path; + } + + return { + install: function (target, source) { + runtime.assignToNamespace( + globalScope.document && globalScope.document.body, + nameSpace, + name, + function (target, source, innerArgs) { + var internalData = runtime.getInternalData(target); + var elementScope = getOrInitObject(internalData, path + "Scope"); + for (var i = 0; i < formalParams.length; i++) { + elementScope[formalParams[i]] = innerArgs[formalParams[i]]; + } + hs.apply(target, source); + } + ); + }, + }; + }); + + parser.addFeature("install", function (parser, runtime, tokens) { + if (!tokens.matchToken("install")) return; + var behaviorPath = parser.requireElement("dotOrColonPath", tokens).evaluate(); + var behaviorNamespace = behaviorPath.split("."); + var args = parser.parseElement("namedArgumentList", tokens); + + var installFeature; + return (installFeature = { + install: function (target, source) { + runtime.unifiedEval( + { + args: [args], + op: function (ctx, args) { + var behavior = globalScope; + for (var i = 0; i < behaviorNamespace.length; i++) { + behavior = behavior[behaviorNamespace[i]]; + if (typeof behavior !== "object" && typeof behavior !== "function") + throw new Error("No such behavior defined as " + behaviorPath); + } + + if (!(behavior instanceof Function)) + throw new Error(behaviorPath + " is not a behavior"); + + behavior(target, source, args); + }, + }, + runtime.makeContext(target, installFeature, target, null) + ); + }, + }); + }); + + parser.addGrammarElement("jsBody", function (parser, runtime, tokens) { + var jsSourceStart = tokens.currentToken().start; + var jsLastToken = tokens.currentToken(); + + var funcNames = []; + var funcName = ""; + var expectFunctionDeclaration = false; + while (tokens.hasMore()) { + jsLastToken = tokens.consumeToken(); + var peek = tokens.token(0, true); + if (peek.type === "IDENTIFIER" && peek.value === "end") { + break; + } + if (expectFunctionDeclaration) { + if (jsLastToken.type === "IDENTIFIER" || jsLastToken.type === "NUMBER") { + funcName += jsLastToken.value; + } else { + if (funcName !== "") funcNames.push(funcName); + funcName = ""; + expectFunctionDeclaration = false; + } + } else if (jsLastToken.type === "IDENTIFIER" && jsLastToken.value === "function") { + expectFunctionDeclaration = true; + } + } + var jsSourceEnd = jsLastToken.end + 1; + + return { + type: "jsBody", + exposedFunctionNames: funcNames, + jsSource: tokens.source.substring(jsSourceStart, jsSourceEnd), + }; + }); + + parser.addFeature("js", function (parser, runtime, tokens) { + if (!tokens.matchToken("js")) return; + var jsBody = parser.requireElement("jsBody", tokens); + + var jsSource = + jsBody.jsSource + + "\nreturn { " + + jsBody.exposedFunctionNames + .map(function (name) { + return name + ":" + name; + }) + .join(",") + + " } "; + var func = new Function(jsSource); + + return { + jsSource: jsSource, + function: func, + exposedFunctionNames: jsBody.exposedFunctionNames, + install: function () { + Object.assign(globalScope, func()); + }, + }; + }); + + parser.addCommand("js", function (parser, runtime, tokens) { + if (!tokens.matchToken("js")) return; + // Parse inputs + var inputs = []; + if (tokens.matchOpToken("(")) { + if (tokens.matchOpToken(")")) { + // empty input list + } else { + do { + var inp = tokens.requireTokenType("IDENTIFIER"); + inputs.push(inp.value); + } while (tokens.matchOpToken(",")); + tokens.requireOpToken(")"); + } + } + + var jsBody = parser.requireElement("jsBody", tokens); + tokens.matchToken("end"); + + var func = varargConstructor(Function, inputs.concat([jsBody.jsSource])); + + var command = { + jsSource: jsBody.jsSource, + function: func, + inputs: inputs, + op: function (context) { + var args = []; + inputs.forEach(function (input) { + args.push(runtime.resolveSymbol(input, context, 'default')); + }); + var result = func.apply(globalScope, args); + if (result && typeof result.then === "function") { + return new Promise(function (resolve) { + result.then(function (actualResult) { + context.result = actualResult; + resolve(runtime.findNext(this, context)); + }); + }); + } else { + context.result = result; + return runtime.findNext(this, context); + } + }, + }; + return command; + }); + + parser.addCommand("async", function (parser, runtime, tokens) { + if (!tokens.matchToken("async")) return; + if (tokens.matchToken("do")) { + var body = parser.requireElement("commandList", tokens); + + // Append halt + var end = body; + while (end.next) end = end.next; + end.next = runtime.HALT; + + tokens.requireToken("end"); + } else { + var body = parser.requireElement("command", tokens); + } + var command = { + body: body, + op: function (context) { + setTimeout(function () { + body.execute(context); + }); + return runtime.findNext(this, context); + }, + }; + parser.setParent(body, command); + return command; + }); + + parser.addCommand("tell", function (parser, runtime, tokens) { + var startToken = tokens.currentToken(); + if (!tokens.matchToken("tell")) return; + var value = parser.requireElement("expression", tokens); + var body = parser.requireElement("commandList", tokens); + if (tokens.hasMore() && !parser.featureStart(tokens.currentToken())) { + tokens.requireToken("end"); + } + var slot = "tell_" + startToken.start; + var tellCmd = { + value: value, + body: body, + args: [value], + resolveNext: function (context) { + var iterator = context.meta.iterators[slot]; + if (iterator.index < iterator.value.length) { + context.you = iterator.value[iterator.index++]; + return body; + } else { + // restore original me + context.you = iterator.originalYou; + if (this.next) { + return this.next; + } else { + return runtime.findNext(this.parent, context); + } + } + }, + op: function (context, value) { + if (value == null) { + value = []; + } else if (!(Array.isArray(value) || value instanceof NodeList)) { + value = [value]; + } + context.meta.iterators[slot] = { + originalYou: context.you, + index: 0, + value: value, + }; + return this.resolveNext(context); + }, + }; + parser.setParent(body, tellCmd); + return tellCmd; + }); + + parser.addCommand("wait", function (parser, runtime, tokens) { + if (!tokens.matchToken("wait")) return; + var command; + + // wait on event + if (tokens.matchToken("for")) { + tokens.matchToken("a"); // optional "a" + var events = []; + do { + var lookahead = tokens.token(0); + if (lookahead.type === 'NUMBER' || lookahead.type === 'L_PAREN') { + events.push({ + time: parser.requireElement('expression', tokens).evaluate() // TODO: do we want to allow async here? + }) + } else { + events.push({ + name: parser.requireElement("dotOrColonPath", tokens, "Expected event name").evaluate(), + args: parseEventArgs(tokens), + }); + } + } while (tokens.matchToken("or")); + + if (tokens.matchToken("from")) { + var on = parser.requireElement("expression", tokens); + } + + // wait on event + command = { + event: events, + on: on, + args: [on], + op: function (context, on) { + var target = on ? on : context.me; + if (!(target instanceof EventTarget)) + throw new Error("Not a valid event target: " + this.on.sourceFor()); + return new Promise((resolve) => { + var resolved = false; + for (const eventInfo of events) { + var listener = (event) => { + context.result = event; + if (eventInfo.args) { + for (const arg of eventInfo.args) { + context.locals[arg.value] = + event[arg.value] || (event.detail ? event.detail[arg.value] : null); + } + } + if (!resolved) { + resolved = true; + resolve(runtime.findNext(this, context)); + } + }; + if (eventInfo.name){ + target.addEventListener(eventInfo.name, listener, {once: true}); + } else if (eventInfo.time != null) { + setTimeout(listener, eventInfo.time, eventInfo.time) + } + } + }); + }, + }; + return command; + } else { + var time; + if (tokens.matchToken("a")) { + tokens.requireToken("tick"); + time = 0; + } else { + time = parser.requireElement("expression", tokens); + } + + command = { + type: "waitCmd", + time: time, + args: [time], + op: function (context, timeValue) { + return new Promise((resolve) => { + setTimeout(() => { + resolve(runtime.findNext(this, context)); + }, timeValue); + }); + }, + execute: function (context) { + return runtime.unifiedExec(this, context); + }, + }; + return command; + } + }); + + // TODO - colon path needs to eventually become part of ruby-style symbols + parser.addGrammarElement("dotOrColonPath", function (parser, runtime, tokens) { + var root = tokens.matchTokenType("IDENTIFIER"); + if (root) { + var path = [root.value]; + + var separator = tokens.matchOpToken(".") || tokens.matchOpToken(":"); + if (separator) { + do { + path.push(tokens.requireTokenType("IDENTIFIER", "NUMBER").value); + } while (tokens.matchOpToken(separator.value)); + } + + return { + type: "dotOrColonPath", + path: path, + evaluate: function () { + return path.join(separator ? separator.value : ""); + }, + }; + } + }); + + + parser.addGrammarElement("eventName", function (parser, runtime, tokens) { + var token; + if ((token = tokens.matchTokenType("STRING"))) { + return { + evaluate: function() { + return token.value; + }, + }; + } + + return parser.parseElement("dotOrColonPath", tokens); + }); + + function parseSendCmd(cmdType, parser, runtime, tokens) { + var eventName = parser.requireElement("eventName", tokens); + + var details = parser.parseElement("namedArgumentList", tokens); + if ((cmdType === "send" && tokens.matchToken("to")) || + (cmdType === "trigger" && tokens.matchToken("on"))) { + var toExpr = parser.requireElement("expression", tokens); + } else { + var toExpr = parser.requireElement("implicitMeTarget", tokens); + } + + var sendCmd = { + eventName: eventName, + details: details, + to: toExpr, + args: [toExpr, eventName, details], + op: function (context, to, eventName, details) { + runtime.nullCheck(to, toExpr); + runtime.implicitLoop(to, function (target) { + runtime.triggerEvent(target, eventName, details, context.me); + }); + return runtime.findNext(sendCmd, context); + }, + }; + return sendCmd; + } + + parser.addCommand("trigger", function (parser, runtime, tokens) { + if (tokens.matchToken("trigger")) { + return parseSendCmd("trigger", parser, runtime, tokens); + } + }); + + parser.addCommand("send", function (parser, runtime, tokens) { + if (tokens.matchToken("send")) { + return parseSendCmd("send", parser, runtime, tokens); + } + }); + + var parseReturnFunction = function (parser, runtime, tokens, returnAValue) { + if (returnAValue) { + if (parser.commandBoundary(tokens.currentToken())) { + parser.raiseParseError(tokens, "'return' commands must return a value. If you do not wish to return a value, use 'exit' instead."); + } else { + var value = parser.requireElement("expression", tokens); + } + } + + var returnCmd = { + value: value, + args: [value], + op: function (context, value) { + var resolve = context.meta.resolve; + context.meta.returned = true; + context.meta.returnValue = value; + if (resolve) { + if (value) { + resolve(value); + } else { + resolve(); + } + } + return runtime.HALT; + }, + }; + return returnCmd; + }; + + parser.addCommand("return", function (parser, runtime, tokens) { + if (tokens.matchToken("return")) { + return parseReturnFunction(parser, runtime, tokens, true); + } + }); + + parser.addCommand("exit", function (parser, runtime, tokens) { + if (tokens.matchToken("exit")) { + return parseReturnFunction(parser, runtime, tokens, false); + } + }); + + parser.addCommand("halt", function (parser, runtime, tokens) { + if (tokens.matchToken("halt")) { + if (tokens.matchToken("the")) { + tokens.requireToken("event"); + // optional possessive + if (tokens.matchOpToken("'")) { + tokens.requireToken("s"); + } + var keepExecuting = true; + } + if (tokens.matchToken("bubbling")) { + var bubbling = true; + } else if (tokens.matchToken("default")) { + var haltDefault = true; + } + var exit = parseReturnFunction(parser, runtime, tokens, false); + + var haltCmd = { + keepExecuting: true, + bubbling: bubbling, + haltDefault: haltDefault, + exit: exit, + op: function (ctx) { + if (ctx.event) { + if (bubbling) { + ctx.event.stopPropagation(); + } else if (haltDefault) { + ctx.event.preventDefault(); + } else { + ctx.event.stopPropagation(); + ctx.event.preventDefault(); + } + if (keepExecuting) { + return runtime.findNext(this, ctx); + } else { + return exit; + } + } + }, + }; + return haltCmd; + } + }); + + parser.addCommand("log", function (parser, runtime, tokens) { + if (!tokens.matchToken("log")) return; + var exprs = [parser.parseElement("expression", tokens)]; + while (tokens.matchOpToken(",")) { + exprs.push(parser.requireElement("expression", tokens)); + } + if (tokens.matchToken("with")) { + var withExpr = parser.requireElement("expression", tokens); + } + var logCmd = { + exprs: exprs, + withExpr: withExpr, + args: [withExpr, exprs], + op: function (ctx, withExpr, values) { + if (withExpr) { + withExpr.apply(null, values); + } else { + console.log.apply(null, values); + } + return runtime.findNext(this, ctx); + }, + }; + return logCmd; + }); + + parser.addCommand("beep!", function (parser, runtime, tokens) { + if (!tokens.matchToken("beep!")) return; + var exprs = [parser.parseElement("expression", tokens)]; + while (tokens.matchOpToken(",")) { + exprs.push(parser.requireElement("expression", tokens)); + } + var beepCmd = { + exprs: exprs, + args: [exprs], + op: function (ctx, values) { + for (let i = 0; i < exprs.length; i++) { + const expr = exprs[i]; + const val = values[i]; + runtime.beepValueToConsole(ctx.me, expr, val); + } + return runtime.findNext(this, ctx); + }, + }; + return beepCmd; + }); + + parser.addCommand("throw", function (parser, runtime, tokens) { + if (!tokens.matchToken("throw")) return; + var expr = parser.requireElement("expression", tokens); + var throwCmd = { + expr: expr, + args: [expr], + op: function (ctx, expr) { + runtime.registerHyperTrace(ctx, expr); + throw expr; + }, + }; + return throwCmd; + }); + + var parseCallOrGet = function (parser, runtime, tokens) { + var expr = parser.requireElement("expression", tokens); + var callCmd = { + expr: expr, + args: [expr], + op: function (context, result) { + context.result = result; + return runtime.findNext(callCmd, context); + }, + }; + return callCmd; + }; + parser.addCommand("call", function (parser, runtime, tokens) { + if (!tokens.matchToken("call")) return; + var call = parseCallOrGet(parser, runtime, tokens); + if (call.expr && call.expr.type !== "functionCall") { + parser.raiseParseError(tokens, "Must be a function invocation"); + } + return call; + }); + parser.addCommand("get", function (parser, runtime, tokens) { + if (tokens.matchToken("get")) { + return parseCallOrGet(parser, runtime, tokens); + } + }); + + parser.addCommand("make", function (parser, runtime, tokens) { + if (!tokens.matchToken("make")) return; + tokens.matchToken("a") || tokens.matchToken("an"); + + var expr = parser.requireElement("expression", tokens); + + var args = []; + if (expr.type !== "queryRef" && tokens.matchToken("from")) { + do { + args.push(parser.requireElement("expression", tokens)); + } while (tokens.matchOpToken(",")); + } + + if (tokens.matchToken("called")) { + var target = parser.requireElement("symbol", tokens); + } + + var command; + if (expr.type === "queryRef") { + command = { + op: function (ctx) { + var match, + tagname = "div", + id, + classes = []; + var re = /(?:(^|#|\.)([^#\. ]+))/g; + while ((match = re.exec(expr.css))) { + if (match[1] === "") tagname = match[2].trim(); + else if (match[1] === "#") id = match[2].trim(); + else classes.push(match[2].trim()); + } + + var result = document.createElement(tagname); + if (id !== undefined) result.id = id; + for (var i = 0; i < classes.length; i++) { + var cls = classes[i]; + result.classList.add(cls) + } + + ctx.result = result; + if (target){ + runtime.setSymbol(target.name, ctx, target.scope, result); + } + + return runtime.findNext(this, ctx); + }, + }; + return command; + } else { + command = { + args: [expr, args], + op: function (ctx, expr, args) { + ctx.result = varargConstructor(expr, args); + if (target){ + runtime.setSymbol(target.name, ctx, target.scope, ctx.result); + } + + return runtime.findNext(this, ctx); + }, + }; + return command; + } + }); + + parser.addGrammarElement("pseudoCommand", function (parser, runtime, tokens) { + + let lookAhead = tokens.token(1); + if (!(lookAhead && lookAhead.op && (lookAhead.value === '.' || lookAhead.value === "("))) { + return null; + } + + var expr = parser.requireElement("primaryExpression", tokens); + + var rootRoot = expr.root; + var root = expr; + while (rootRoot.root != null) { + root = root.root; + rootRoot = rootRoot.root; + } + + if (expr.type !== "functionCall") { + parser.raiseParseError(tokens, "Pseudo-commands must be function calls"); + } + + if (root.type === "functionCall" && root.root.root == null) { + if (tokens.matchAnyToken("the", "to", "on", "with", "into", "from", "at")) { + var realRoot = parser.requireElement("expression", tokens); + } else if (tokens.matchToken("me")) { + var realRoot = parser.requireElement("implicitMeTarget", tokens); + } + } + + /** @type {ASTNode} */ + + var pseudoCommand + if(realRoot){ + pseudoCommand = { + type: "pseudoCommand", + root: realRoot, + argExressions: root.argExressions, + args: [realRoot, root.argExressions], + op: function (context, rootRoot, args) { + runtime.nullCheck(rootRoot, realRoot); + var func = rootRoot[root.root.name]; + runtime.nullCheck(func, root); + if (func.hyperfunc) { + args.push(context); + } + context.result = func.apply(rootRoot, args); + return runtime.findNext(pseudoCommand, context); + }, + execute: function (context) { + return runtime.unifiedExec(this, context); + }, + } + } else { + pseudoCommand = { + type: "pseudoCommand", + expr: expr, + args: [expr], + op: function (context, result) { + context.result = result; + return runtime.findNext(pseudoCommand, context); + }, + execute: function (context) { + return runtime.unifiedExec(this, context); + }, + }; + } + + return pseudoCommand; + }); + + /** + * @param {Parser} parser + * @param {Runtime} runtime + * @param {Tokens} tokens + * @param {*} target + * @param {*} value + * @returns + */ + var makeSetter = function (parser, runtime, tokens, target, value) { + + var symbolWrite = target.type === "symbol"; + var attributeWrite = target.type === "attributeRef"; + var styleWrite = target.type === "styleRef"; + var arrayWrite = target.type === "arrayIndex"; + + if (!(attributeWrite || styleWrite || symbolWrite) && target.root == null) { + parser.raiseParseError(tokens, "Can only put directly into symbols, not references"); + } + + var rootElt = null; + var prop = null; + if (symbolWrite) { + // rootElt is null + } else if (attributeWrite || styleWrite) { + rootElt = parser.requireElement("implicitMeTarget", tokens); + var attribute = target; + } else if(arrayWrite) { + prop = target.firstIndex; + rootElt = target.root; + } else { + prop = target.prop ? target.prop.value : null; + var attribute = target.attribute; + rootElt = target.root; + } + + /** @type {ASTNode} */ + var setCmd = { + target: target, + symbolWrite: symbolWrite, + value: value, + args: [rootElt, prop, value], + op: function (context, root, prop, valueToSet) { + if (symbolWrite) { + runtime.setSymbol(target.name, context, target.scope, valueToSet); + } else { + runtime.nullCheck(root, rootElt); + if (arrayWrite) { + root[prop] = valueToSet; + } else { + runtime.implicitLoop(root, function (elt) { + if (attribute) { + if (attribute.type === "attributeRef") { + if (valueToSet == null) { + elt.removeAttribute(attribute.name); + } else { + elt.setAttribute(attribute.name, valueToSet); + } + } else { + elt.style[attribute.name] = valueToSet; + } + } else { + elt[prop] = valueToSet; + } + }); + } + } + return runtime.findNext(this, context); + }, + }; + return setCmd; + }; + + parser.addCommand("default", function (parser, runtime, tokens) { + if (!tokens.matchToken("default")) return; + var target = parser.requireElement("assignableExpression", tokens); + tokens.requireToken("to"); + + var value = parser.requireElement("expression", tokens); + + /** @type {ASTNode} */ + var setter = makeSetter(parser, runtime, tokens, target, value); + var defaultCmd = { + target: target, + value: value, + setter: setter, + args: [target], + op: function (context, target) { + if (target) { + return runtime.findNext(this, context); + } else { + return setter; + } + }, + }; + setter.parent = defaultCmd; + return defaultCmd; + }); + + parser.addCommand("set", function (parser, runtime, tokens) { + if (!tokens.matchToken("set")) return; + if (tokens.currentToken().type === "L_BRACE") { + var obj = parser.requireElement("objectLiteral", tokens); + tokens.requireToken("on"); + var target = parser.requireElement("expression", tokens); + + var command = { + objectLiteral: obj, + target: target, + args: [obj, target], + op: function (ctx, obj, target) { + Object.assign(target, obj); + return runtime.findNext(this, ctx); + }, + }; + return command; + } + + try { + tokens.pushFollow("to"); + var target = parser.requireElement("assignableExpression", tokens); + } finally { + tokens.popFollow(); + } + tokens.requireToken("to"); + var value = parser.requireElement("expression", tokens); + return makeSetter(parser, runtime, tokens, target, value); + }); + + parser.addCommand("if", function (parser, runtime, tokens) { + if (!tokens.matchToken("if")) return; + var expr = parser.requireElement("expression", tokens); + tokens.matchToken("then"); // optional 'then' + var trueBranch = parser.parseElement("commandList", tokens); + var nestedIfStmt = false; + if (tokens.matchToken("else") || tokens.matchToken("otherwise")) { + nestedIfStmt = tokens.peekToken("if"); + var falseBranch = parser.parseElement("commandList", tokens); + } + if (tokens.hasMore() && !nestedIfStmt) { + tokens.requireToken("end"); + } + + /** @type {ASTNode} */ + var ifCmd = { + expr: expr, + trueBranch: trueBranch, + falseBranch: falseBranch, + args: [expr], + op: function (context, exprValue) { + if (exprValue) { + return trueBranch; + } else if (falseBranch) { + return falseBranch; + } else { + return runtime.findNext(this, context); + } + }, + }; + parser.setParent(trueBranch, ifCmd); + parser.setParent(falseBranch, ifCmd); + return ifCmd; + }); + + var parseRepeatExpression = function (parser, tokens, runtime, startedWithForToken) { + var innerStartToken = tokens.currentToken(); + var identifier; + if (tokens.matchToken("for") || startedWithForToken) { + var identifierToken = tokens.requireTokenType("IDENTIFIER"); + identifier = identifierToken.value; + tokens.requireToken("in"); + var expression = parser.requireElement("expression", tokens); + } else if (tokens.matchToken("in")) { + identifier = "it"; + var expression = parser.requireElement("expression", tokens); + } else if (tokens.matchToken("while")) { + var whileExpr = parser.requireElement("expression", tokens); + } else if (tokens.matchToken("until")) { + var isUntil = true; + if (tokens.matchToken("event")) { + var evt = parser.requireElement("dotOrColonPath", tokens, "Expected event name"); + if (tokens.matchToken("from")) { + var on = parser.requireElement("expression", tokens); + } + } else { + var whileExpr = parser.requireElement("expression", tokens); + } + } else { + if (!parser.commandBoundary(tokens.currentToken()) && + tokens.currentToken().value !== 'forever') { + var times = parser.requireElement("expression", tokens); + tokens.requireToken("times"); + } else { + tokens.matchToken("forever"); // consume optional forever + var forever = true; + } + } + + if (tokens.matchToken("index")) { + var identifierToken = tokens.requireTokenType("IDENTIFIER"); + var indexIdentifier = identifierToken.value; + } + + var loop = parser.parseElement("commandList", tokens); + if (loop && evt) { + // if this is an event based loop, wait a tick at the end of the loop so that + // events have a chance to trigger in the loop condition o_O))) + var last = loop; + while (last.next) { + last = last.next; + } + var waitATick = { + type: "waitATick", + op: function () { + return new Promise(function (resolve) { + setTimeout(function () { + resolve(runtime.findNext(waitATick)); + }, 0); + }); + }, + }; + last.next = waitATick; + } + if (tokens.hasMore()) { + tokens.requireToken("end"); + } + + if (identifier == null) { + identifier = "_implicit_repeat_" + innerStartToken.start; + var slot = identifier; + } else { + var slot = identifier + "_" + innerStartToken.start; + } + + var repeatCmd = { + identifier: identifier, + indexIdentifier: indexIdentifier, + slot: slot, + expression: expression, + forever: forever, + times: times, + until: isUntil, + event: evt, + on: on, + whileExpr: whileExpr, + resolveNext: function () { + return this; + }, + loop: loop, + args: [whileExpr, times], + op: function (context, whileValue, times) { + var iteratorInfo = context.meta.iterators[slot]; + var keepLooping = false; + var loopVal = null; + if (this.forever) { + keepLooping = true; + } else if (this.until) { + if (evt) { + keepLooping = context.meta.iterators[slot].eventFired === false; + } else { + keepLooping = whileValue !== true; + } + } else if (whileExpr) { + keepLooping = whileValue; + } else if (times) { + keepLooping = iteratorInfo.index < times; + } else { + var nextValFromIterator = iteratorInfo.iterator.next(); + keepLooping = !nextValFromIterator.done; + loopVal = nextValFromIterator.value; + } + + if (keepLooping) { + if (iteratorInfo.value) { + context.result = context.locals[identifier] = loopVal; + } else { + context.result = iteratorInfo.index; + } + if (indexIdentifier) { + context.locals[indexIdentifier] = iteratorInfo.index; + } + iteratorInfo.index++; + return loop; + } else { + context.meta.iterators[slot] = null; + return runtime.findNext(this.parent, context); + } + }, + }; + parser.setParent(loop, repeatCmd); + var repeatInit = { + name: "repeatInit", + args: [expression, evt, on], + op: function (context, value, event, on) { + var iteratorInfo = { + index: 0, + value: value, + eventFired: false, + }; + context.meta.iterators[slot] = iteratorInfo; + if (value && value[Symbol.iterator]) { + iteratorInfo.iterator = value[Symbol.iterator](); + } + if (evt) { + var target = on || context.me; + target.addEventListener( + event, + function (e) { + context.meta.iterators[slot].eventFired = true; + }, + { once: true } + ); + } + return repeatCmd; // continue to loop + }, + execute: function (context) { + return runtime.unifiedExec(this, context); + }, + }; + parser.setParent(repeatCmd, repeatInit); + return repeatInit; + }; + + parser.addCommand("repeat", function (parser, runtime, tokens) { + if (tokens.matchToken("repeat")) { + return parseRepeatExpression(parser, tokens, runtime, false); + } + }); + + parser.addCommand("for", function (parser, runtime, tokens) { + if (tokens.matchToken("for")) { + return parseRepeatExpression(parser, tokens, runtime, true); + } + }); + + parser.addCommand("continue", function (parser, runtime, tokens) { + + if (!tokens.matchToken("continue")) return; + + var command = { + op: function (context) { + + // scan for the closest repeat statement + for (var parent = this.parent ; true ; parent = parent.parent) { + + if (parent == undefined) { + parser.raiseParseError(tokens, "Command `continue` cannot be used outside of a `repeat` loop.") + } + if (parent.loop != undefined) { + return parent.resolveNext(context) + } + } + } + }; + return command; + }); + + parser.addCommand("break", function (parser, runtime, tokens) { + + if (!tokens.matchToken("break")) return; + + var command = { + op: function (context) { + + // scan for the closest repeat statement + for (var parent = this.parent ; true ; parent = parent.parent) { + + if (parent == undefined) { + parser.raiseParseError(tokens, "Command `continue` cannot be used outside of a `repeat` loop.") + } + if (parent.loop != undefined) { + return runtime.findNext(parent.parent, context); + } + } + } + }; + return command; + }); + + parser.addGrammarElement("stringLike", function (parser, runtime, tokens) { + return parser.parseAnyOf(["string", "nakedString"], tokens); + }); + + parser.addCommand("append", function (parser, runtime, tokens) { + if (!tokens.matchToken("append")) return; + var targetExpr = null; + + var value = parser.requireElement("expression", tokens); + + /** @type {ASTNode} */ + var implicitResultSymbol = { + type: "symbol", + evaluate: function (context) { + return runtime.resolveSymbol("result", context); + }, + }; + + if (tokens.matchToken("to")) { + targetExpr = parser.requireElement("expression", tokens); + } else { + targetExpr = implicitResultSymbol; + } + + var setter = null; + if (targetExpr.type === "symbol" || targetExpr.type === "attributeRef" || targetExpr.root != null) { + setter = makeSetter(parser, runtime, tokens, targetExpr, implicitResultSymbol); + } + + var command = { + value: value, + target: targetExpr, + args: [targetExpr, value], + op: function (context, target, value) { + if (Array.isArray(target)) { + target.push(value); + return runtime.findNext(this, context); + } else if (target instanceof Element) { + target.innerHTML += value; + return runtime.findNext(this, context); + } else if(setter) { + context.result = (target || "") + value; + return setter; + } else { + throw Error("Unable to append a value!") + } + }, + execute: function (context) { + return runtime.unifiedExec(this, context/*, value, target*/); + }, + }; + + if (setter != null) { + setter.parent = command; + } + + return command; + }); + + function parsePickRange(parser, runtime, tokens) { + tokens.matchToken("at") || tokens.matchToken("from"); + const rv = { includeStart: true, includeEnd: false } + + rv.from = tokens.matchToken("start") ? 0 : parser.requireElement("expression", tokens) + + if (tokens.matchToken("to") || tokens.matchOpToken("..")) { + if (tokens.matchToken("end")) { + rv.toEnd = true; + } else { + rv.to = parser.requireElement("expression", tokens); + } + } + + if (tokens.matchToken("inclusive")) rv.includeEnd = true; + else if (tokens.matchToken("exclusive")) rv.includeStart = false; + + return rv; + } + + class RegExpIterator { + constructor(re, str) { + this.re = re; + this.str = str; + } + + next() { + const match = this.re.exec(this.str); + if (match === null) return { done: true }; + else return { value: match }; + } + } + + class RegExpIterable { + constructor(re, flags, str) { + this.re = re; + this.flags = flags; + this.str = str; + } + + [Symbol.iterator]() { + return new RegExpIterator(new RegExp(this.re, this.flags), this.str); + } + } + + parser.addCommand("pick", (parser, runtime, tokens) => { + if (!tokens.matchToken("pick")) return; + + tokens.matchToken("the"); + + if (tokens.matchToken("item") || tokens.matchToken("items") + || tokens.matchToken("character") || tokens.matchToken("characters")) { + const range = parsePickRange(parser, runtime, tokens); + + tokens.requireToken("from"); + const root = parser.requireElement("expression", tokens); + + return { + args: [root, range.from, range.to], + op(ctx, root, from, to) { + if (range.toEnd) to = root.length; + if (!range.includeStart) from++; + if (range.includeEnd) to++; + if (to == null || to == undefined) to = from + 1; + ctx.result = root.slice(from, to); + return runtime.findNext(this, ctx); + } + } + } + + if (tokens.matchToken("match")) { + tokens.matchToken("of"); + const re = parser.parseElement("expression", tokens); + let flags = "" + if (tokens.matchOpToken("|")) { + flags = tokens.requireToken("identifier").value; + } + + tokens.requireToken("from"); + const root = parser.parseElement("expression", tokens); + + return { + args: [root, re], + op(ctx, root, re) { + ctx.result = new RegExp(re, flags).exec(root); + return runtime.findNext(this, ctx); + } + } + } + + if (tokens.matchToken("matches")) { + tokens.matchToken("of"); + const re = parser.parseElement("expression", tokens); + let flags = "gu" + if (tokens.matchOpToken("|")) { + flags = 'g' + tokens.requireToken("identifier").value.replace('g', ''); + } + console.log('flags', flags) + + tokens.requireToken("from"); + const root = parser.parseElement("expression", tokens); + + return { + args: [root, re], + op(ctx, root, re) { + ctx.result = new RegExpIterable(re, flags, root); + return runtime.findNext(this, ctx); + } + } + } + }); + + parser.addCommand("increment", function (parser, runtime, tokens) { + if (!tokens.matchToken("increment")) return; + var amountExpr; + + // This is optional. Defaults to "result" + var target = parser.parseElement("assignableExpression", tokens); + + // This is optional. Defaults to 1. + if (tokens.matchToken("by")) { + amountExpr = parser.requireElement("expression", tokens); + } + + var implicitIncrementOp = { + type: "implicitIncrementOp", + target: target, + args: [target, amountExpr], + op: function (context, targetValue, amount) { + targetValue = targetValue ? parseFloat(targetValue) : 0; + amount = amountExpr ? parseFloat(amount) : 1; + var newValue = targetValue + amount; + context.result = newValue; + return newValue; + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + } + }; + + return makeSetter(parser, runtime, tokens, target, implicitIncrementOp); + }); + + parser.addCommand("decrement", function (parser, runtime, tokens) { + if (!tokens.matchToken("decrement")) return; + var amountExpr; + + // This is optional. Defaults to "result" + var target = parser.parseElement("assignableExpression", tokens); + + // This is optional. Defaults to 1. + if (tokens.matchToken("by")) { + amountExpr = parser.requireElement("expression", tokens); + } + + var implicitDecrementOp = { + type: "implicitDecrementOp", + target: target, + args: [target, amountExpr], + op: function (context, targetValue, amount) { + targetValue = targetValue ? parseFloat(targetValue) : 0; + amount = amountExpr ? parseFloat(amount) : 1; + var newValue = targetValue - amount; + context.result = newValue; + return newValue; + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + } + }; + + return makeSetter(parser, runtime, tokens, target, implicitDecrementOp); + }); + + function parseConversionInfo(tokens, parser) { + var type = "text"; + var conversion; + tokens.matchToken("a") || tokens.matchToken("an"); + if (tokens.matchToken("json") || tokens.matchToken("Object")) { + type = "json"; + } else if (tokens.matchToken("response")) { + type = "response"; + } else if (tokens.matchToken("html")) { + type = "html"; + } else if (tokens.matchToken("text")) { + // default, ignore + } else { + conversion = parser.requireElement("dotOrColonPath", tokens).evaluate(); + } + return {type, conversion}; + } + + parser.addCommand("fetch", function (parser, runtime, tokens) { + if (!tokens.matchToken("fetch")) return; + var url = parser.requireElement("stringLike", tokens); + + if (tokens.matchToken("as")) { + var conversionInfo = parseConversionInfo(tokens, parser); + } + + if (tokens.matchToken("with") && tokens.currentToken().value !== "{") { + var args = parser.parseElement("nakedNamedArgumentList", tokens); + } else { + var args = parser.parseElement("objectLiteral", tokens); + } + + if (conversionInfo == null && tokens.matchToken("as")) { + conversionInfo = parseConversionInfo(tokens, parser); + } + + var type = conversionInfo ? conversionInfo.type : "text"; + var conversion = conversionInfo ? conversionInfo.conversion : null + + /** @type {ASTNode} */ + var fetchCmd = { + url: url, + argExpressions: args, + args: [url, args], + op: function (context, url, args) { + var detail = args || {}; + detail["sender"] = context.me; + detail["headers"] = detail["headers"] || {} + var abortController = new AbortController(); + let abortListener = context.me.addEventListener('fetch:abort', function(){ + abortController.abort(); + }, {once: true}); + detail['signal'] = abortController.signal; + runtime.triggerEvent(context.me, "hyperscript:beforeFetch", detail); + runtime.triggerEvent(context.me, "fetch:beforeRequest", detail); + args = detail; + var finished = false; + if (args.timeout) { + setTimeout(function () { + if (!finished) { + abortController.abort(); + } + }, args.timeout); + } + return fetch(url, args) + .then(function (resp) { + let resultDetails = {response:resp}; + runtime.triggerEvent(context.me, "fetch:afterResponse", resultDetails); + resp = resultDetails.response; + + if (type === "response") { + context.result = resp; + runtime.triggerEvent(context.me, "fetch:afterRequest", {result:resp}); + finished = true; + return runtime.findNext(fetchCmd, context); + } + if (type === "json") { + return resp.json().then(function (result) { + context.result = result; + runtime.triggerEvent(context.me, "fetch:afterRequest", {result}); + finished = true; + return runtime.findNext(fetchCmd, context); + }); + } + return resp.text().then(function (result) { + if (conversion) result = runtime.convertValue(result, conversion); + + if (type === "html") result = runtime.convertValue(result, "Fragment"); + + context.result = result; + runtime.triggerEvent(context.me, "fetch:afterRequest", {result}); + finished = true; + return runtime.findNext(fetchCmd, context); + }); + }) + .catch(function (reason) { + runtime.triggerEvent(context.me, "fetch:error", { + reason: reason, + }); + throw reason; + }).finally(function(){ + context.me.removeEventListener('fetch:abort', abortListener); + }); + }, + }; + return fetchCmd; + }); + } + + function hyperscriptWebGrammar(parser) { + parser.addCommand("settle", function (parser, runtime, tokens) { + if (tokens.matchToken("settle")) { + if (!parser.commandBoundary(tokens.currentToken())) { + var onExpr = parser.requireElement("expression", tokens); + } else { + var onExpr = parser.requireElement("implicitMeTarget", tokens); + } + + var settleCommand = { + type: "settleCmd", + args: [onExpr], + op: function (context, on) { + runtime.nullCheck(on, onExpr); + var resolve = null; + var resolved = false; + var transitionStarted = false; + + var promise = new Promise(function (r) { + resolve = r; + }); + + // listen for a transition begin + on.addEventListener( + "transitionstart", + function () { + transitionStarted = true; + }, + { once: true } + ); + + // if no transition begins in 500ms, cancel + setTimeout(function () { + if (!transitionStarted && !resolved) { + resolve(runtime.findNext(settleCommand, context)); + } + }, 500); + + // continue on a transition emd + on.addEventListener( + "transitionend", + function () { + if (!resolved) { + resolve(runtime.findNext(settleCommand, context)); + } + }, + { once: true } + ); + return promise; + }, + execute: function (context) { + return runtime.unifiedExec(this, context); + }, + }; + return settleCommand; + } + }); + + parser.addCommand("add", function (parser, runtime, tokens) { + if (tokens.matchToken("add")) { + var classRef = parser.parseElement("classRef", tokens); + var attributeRef = null; + var cssDeclaration = null; + if (classRef == null) { + attributeRef = parser.parseElement("attributeRef", tokens); + if (attributeRef == null) { + cssDeclaration = parser.parseElement("styleLiteral", tokens); + if (cssDeclaration == null) { + parser.raiseParseError(tokens, "Expected either a class reference or attribute expression"); + } + } + } else { + var classRefs = [classRef]; + while ((classRef = parser.parseElement("classRef", tokens))) { + classRefs.push(classRef); + } + } + + if (tokens.matchToken("to")) { + var toExpr = parser.requireElement("expression", tokens); + } else { + var toExpr = parser.requireElement("implicitMeTarget", tokens); + } + + if (tokens.matchToken("when")) { + if (cssDeclaration) { + parser.raiseParseError(tokens, "Only class and properties are supported with a when clause") + } + var when = parser.requireElement("expression", tokens); + } + + if (classRefs) { + return { + classRefs: classRefs, + to: toExpr, + args: [toExpr, classRefs], + op: function (context, to, classRefs) { + runtime.nullCheck(to, toExpr); + runtime.forEach(classRefs, function (classRef) { + runtime.implicitLoop(to, function (target) { + if (when) { + context.result = target; + let whenResult = runtime.evaluateNoPromise(when, context); + if (whenResult) { + if (target instanceof Element) target.classList.add(classRef.className); + } else { + if (target instanceof Element) target.classList.remove(classRef.className); + } + context.result = null; + } else { + if (target instanceof Element) target.classList.add(classRef.className); + } + }); + }); + return runtime.findNext(this, context); + }, + }; + } else if (attributeRef) { + return { + type: "addCmd", + attributeRef: attributeRef, + to: toExpr, + args: [toExpr], + op: function (context, to, attrRef) { + runtime.nullCheck(to, toExpr); + runtime.implicitLoop(to, function (target) { + if (when) { + context.result = target; + let whenResult = runtime.evaluateNoPromise(when, context); + if (whenResult) { + target.setAttribute(attributeRef.name, attributeRef.value); + } else { + target.removeAttribute(attributeRef.name); + } + context.result = null; + } else { + target.setAttribute(attributeRef.name, attributeRef.value); + } + }); + return runtime.findNext(this, context); + }, + execute: function (ctx) { + return runtime.unifiedExec(this, ctx); + }, + }; + } else { + return { + type: "addCmd", + cssDeclaration: cssDeclaration, + to: toExpr, + args: [toExpr, cssDeclaration], + op: function (context, to, css) { + runtime.nullCheck(to, toExpr); + runtime.implicitLoop(to, function (target) { + target.style.cssText += css; + }); + return runtime.findNext(this, context); + }, + execute: function (ctx) { + return runtime.unifiedExec(this, ctx); + }, + }; + } + } + }); + + parser.addGrammarElement("styleLiteral", function (parser, runtime, tokens) { + if (!tokens.matchOpToken("{")) return; + + var stringParts = [""] + var exprs = [] + + while (tokens.hasMore()) { + if (tokens.matchOpToken("\\")) { + tokens.consumeToken(); + } else if (tokens.matchOpToken("}")) { + break; + } else if (tokens.matchToken("$")) { + var opencurly = tokens.matchOpToken("{"); + var expr = parser.parseElement("expression", tokens); + if (opencurly) tokens.requireOpToken("}"); + + exprs.push(expr) + stringParts.push("") + } else { + var tok = tokens.consumeToken(); + stringParts[stringParts.length-1] += tokens.source.substring(tok.start, tok.end); + } + + stringParts[stringParts.length-1] += tokens.lastWhitespace(); + } + + return { + type: "styleLiteral", + args: [exprs], + op: function (ctx, exprs) { + var rv = ""; + + stringParts.forEach(function (part, idx) { + rv += part; + if (idx in exprs) rv += exprs[idx]; + }); + + return rv; + }, + evaluate: function(ctx) { + return runtime.unifiedEval(this, ctx); + } + } + }) + + parser.addCommand("remove", function (parser, runtime, tokens) { + if (tokens.matchToken("remove")) { + var classRef = parser.parseElement("classRef", tokens); + var attributeRef = null; + var elementExpr = null; + if (classRef == null) { + attributeRef = parser.parseElement("attributeRef", tokens); + if (attributeRef == null) { + elementExpr = parser.parseElement("expression", tokens); + if (elementExpr == null) { + parser.raiseParseError( + tokens, + "Expected either a class reference, attribute expression or value expression" + ); + } + } + } else { + var classRefs = [classRef]; + while ((classRef = parser.parseElement("classRef", tokens))) { + classRefs.push(classRef); + } + } + + if (tokens.matchToken("from")) { + var fromExpr = parser.requireElement("expression", tokens); + } else { + if (elementExpr == null) { + var fromExpr = parser.requireElement("implicitMeTarget", tokens); + } + } + + if (elementExpr) { + return { + elementExpr: elementExpr, + from: fromExpr, + args: [elementExpr, fromExpr], + op: function (context, element, from) { + runtime.nullCheck(element, elementExpr); + runtime.implicitLoop(element, function (target) { + if (target.parentElement && (from == null || from.contains(target))) { + target.parentElement.removeChild(target); + } + }); + return runtime.findNext(this, context); + }, + }; + } else { + return { + classRefs: classRefs, + attributeRef: attributeRef, + elementExpr: elementExpr, + from: fromExpr, + args: [classRefs, fromExpr], + op: function (context, classRefs, from) { + runtime.nullCheck(from, fromExpr); + if (classRefs) { + runtime.forEach(classRefs, function (classRef) { + runtime.implicitLoop(from, function (target) { + target.classList.remove(classRef.className); + }); + }); + } else { + runtime.implicitLoop(from, function (target) { + target.removeAttribute(attributeRef.name); + }); + } + return runtime.findNext(this, context); + }, + }; + } + } + }); + + parser.addCommand("toggle", function (parser, runtime, tokens) { + if (tokens.matchToken("toggle")) { + tokens.matchAnyToken("the", "my"); + if (tokens.currentToken().type === "STYLE_REF") { + let styleRef = tokens.consumeToken(); + var name = styleRef.value.substr(1); + var visibility = true; + var hideShowStrategy = resolveHideShowStrategy(parser, tokens, name); + if (tokens.matchToken("of")) { + tokens.pushFollow("with"); + try { + var onExpr = parser.requireElement("expression", tokens); + } finally { + tokens.popFollow(); + } + } else { + var onExpr = parser.requireElement("implicitMeTarget", tokens); + } + } else if (tokens.matchToken("between")) { + var between = true; + var classRef = parser.parseElement("classRef", tokens); + tokens.requireToken("and"); + var classRef2 = parser.requireElement("classRef", tokens); + } else { + var classRef = parser.parseElement("classRef", tokens); + var attributeRef = null; + if (classRef == null) { + attributeRef = parser.parseElement("attributeRef", tokens); + if (attributeRef == null) { + parser.raiseParseError(tokens, "Expected either a class reference or attribute expression"); + } + } else { + var classRefs = [classRef]; + while ((classRef = parser.parseElement("classRef", tokens))) { + classRefs.push(classRef); + } + } + } + + if (visibility !== true) { + if (tokens.matchToken("on")) { + var onExpr = parser.requireElement("expression", tokens); + } else { + var onExpr = parser.requireElement("implicitMeTarget", tokens); + } + } + + if (tokens.matchToken("for")) { + var time = parser.requireElement("expression", tokens); + } else if (tokens.matchToken("until")) { + var evt = parser.requireElement("dotOrColonPath", tokens, "Expected event name"); + if (tokens.matchToken("from")) { + var from = parser.requireElement("expression", tokens); + } + } + + var toggleCmd = { + classRef: classRef, + classRef2: classRef2, + classRefs: classRefs, + attributeRef: attributeRef, + on: onExpr, + time: time, + evt: evt, + from: from, + toggle: function (on, classRef, classRef2, classRefs) { + runtime.nullCheck(on, onExpr); + if (visibility) { + runtime.implicitLoop(on, function (target) { + hideShowStrategy("toggle", target); + }); + } else if (between) { + runtime.implicitLoop(on, function (target) { + if (target.classList.contains(classRef.className)) { + target.classList.remove(classRef.className); + target.classList.add(classRef2.className); + } else { + target.classList.add(classRef.className); + target.classList.remove(classRef2.className); + } + }); + } else if (classRefs) { + runtime.forEach(classRefs, function (classRef) { + runtime.implicitLoop(on, function (target) { + target.classList.toggle(classRef.className); + }); + }); + } else { + runtime.forEach(on, function (target) { + if (target.hasAttribute(attributeRef.name)) { + target.removeAttribute(attributeRef.name); + } else { + target.setAttribute(attributeRef.name, attributeRef.value); + } + }); + } + }, + args: [onExpr, time, evt, from, classRef, classRef2, classRefs], + op: function (context, on, time, evt, from, classRef, classRef2, classRefs) { + if (time) { + return new Promise(function (resolve) { + toggleCmd.toggle(on, classRef, classRef2, classRefs); + setTimeout(function () { + toggleCmd.toggle(on, classRef, classRef2, classRefs); + resolve(runtime.findNext(toggleCmd, context)); + }, time); + }); + } else if (evt) { + return new Promise(function (resolve) { + var target = from || context.me; + target.addEventListener( + evt, + function () { + toggleCmd.toggle(on, classRef, classRef2, classRefs); + resolve(runtime.findNext(toggleCmd, context)); + }, + { once: true } + ); + toggleCmd.toggle(on, classRef, classRef2, classRefs); + }); + } else { + this.toggle(on, classRef, classRef2, classRefs); + return runtime.findNext(toggleCmd, context); + } + }, + }; + return toggleCmd; + } + }); + + var HIDE_SHOW_STRATEGIES = { + display: function (op, element, arg) { + if (arg) { + element.style.display = arg; + } else if (op === "toggle") { + if (getComputedStyle(element).display === "none") { + HIDE_SHOW_STRATEGIES.display("show", element, arg); + } else { + HIDE_SHOW_STRATEGIES.display("hide", element, arg); + } + } else if (op === "hide") { + const internalData = parser.runtime.getInternalData(element); + if (internalData.originalDisplay == null) { + internalData.originalDisplay = element.style.display; + } + element.style.display = "none"; + } else { + const internalData = parser.runtime.getInternalData(element); + if (internalData.originalDisplay && internalData.originalDisplay !== 'none') { + element.style.display = internalData.originalDisplay; + } else { + element.style.removeProperty('display'); + } + } + }, + visibility: function (op, element, arg) { + if (arg) { + element.style.visibility = arg; + } else if (op === "toggle") { + if (getComputedStyle(element).visibility === "hidden") { + HIDE_SHOW_STRATEGIES.visibility("show", element, arg); + } else { + HIDE_SHOW_STRATEGIES.visibility("hide", element, arg); + } + } else if (op === "hide") { + element.style.visibility = "hidden"; + } else { + element.style.visibility = "visible"; + } + }, + opacity: function (op, element, arg) { + if (arg) { + element.style.opacity = arg; + } else if (op === "toggle") { + if (getComputedStyle(element).opacity === "0") { + HIDE_SHOW_STRATEGIES.opacity("show", element, arg); + } else { + HIDE_SHOW_STRATEGIES.opacity("hide", element, arg); + } + } else if (op === "hide") { + element.style.opacity = "0"; + } else { + element.style.opacity = "1"; + } + }, + }; + + var parseShowHideTarget = function (parser, runtime, tokens) { + var target; + var currentTokenValue = tokens.currentToken(); + if (currentTokenValue.value === "when" || currentTokenValue.value === "with" || parser.commandBoundary(currentTokenValue)) { + target = parser.parseElement("implicitMeTarget", tokens); + } else { + target = parser.parseElement("expression", tokens); + } + return target; + }; + + var resolveHideShowStrategy = function (parser, tokens, name) { + var configDefault = config.defaultHideShowStrategy; + var strategies = HIDE_SHOW_STRATEGIES; + if (config.hideShowStrategies) { + strategies = Object.assign(strategies, config.hideShowStrategies); // merge in user provided strategies + } + name = name || configDefault || "display"; + var value = strategies[name]; + if (value == null) { + parser.raiseParseError(tokens, "Unknown show/hide strategy : " + name); + } + return value; + }; + + parser.addCommand("hide", function (parser, runtime, tokens) { + if (tokens.matchToken("hide")) { + var targetExpr = parseShowHideTarget(parser, runtime, tokens); + + var name = null; + if (tokens.matchToken("with")) { + name = tokens.requireTokenType("IDENTIFIER", "STYLE_REF").value; + if (name.indexOf("*") === 0) { + name = name.substr(1); + } + } + var hideShowStrategy = resolveHideShowStrategy(parser, tokens, name); + + return { + target: targetExpr, + args: [targetExpr], + op: function (ctx, target) { + runtime.nullCheck(target, targetExpr); + runtime.implicitLoop(target, function (elt) { + hideShowStrategy("hide", elt); + }); + return runtime.findNext(this, ctx); + }, + }; + } + }); + + parser.addCommand("show", function (parser, runtime, tokens) { + if (tokens.matchToken("show")) { + var targetExpr = parseShowHideTarget(parser, runtime, tokens); + + var name = null; + if (tokens.matchToken("with")) { + name = tokens.requireTokenType("IDENTIFIER", "STYLE_REF").value; + if (name.indexOf("*") === 0) { + name = name.substr(1); + } + } + var arg = null; + if (tokens.matchOpToken(":")) { + var tokenArr = tokens.consumeUntilWhitespace(); + tokens.matchTokenType("WHITESPACE"); + arg = tokenArr + .map(function (t) { + return t.value; + }) + .join(""); + } + + if (tokens.matchToken("when")) { + var when = parser.requireElement("expression", tokens); + } + + var hideShowStrategy = resolveHideShowStrategy(parser, tokens, name); + + return { + target: targetExpr, + when: when, + args: [targetExpr], + op: function (ctx, target) { + runtime.nullCheck(target, targetExpr); + runtime.implicitLoop(target, function (elt) { + if (when) { + ctx.result = elt; + let whenResult = runtime.evaluateNoPromise(when, ctx); + if (whenResult) { + hideShowStrategy("show", elt, arg); + } else { + hideShowStrategy("hide", elt); + } + ctx.result = null; + } else { + hideShowStrategy("show", elt, arg); + } + }); + return runtime.findNext(this, ctx); + }, + }; + } + }); + + parser.addCommand("take", function (parser, runtime, tokens) { + if (tokens.matchToken("take")) { + var classRef = parser.parseElement("classRef", tokens); + + var attributeRef = null; + var replacementValue = null; + + if (classRef == null) { + attributeRef = parser.parseElement("attributeRef", tokens); + if (attributeRef == null) { + parser.raiseParseError(tokens, "Expected either a class reference or attribute expression"); + } + + if (tokens.matchToken("with")) { + replacementValue = parser.requireElement("expression", tokens); + } + } + + if (tokens.matchToken("from")) { + var fromExpr = parser.requireElement("expression", tokens); + } else { + var fromExpr = classRef; + } + + if (tokens.matchToken("for")) { + var forExpr = parser.requireElement("expression", tokens); + } else { + var forExpr = parser.requireElement("implicitMeTarget", tokens); + } + + if (classRef) { + var takeCmd = { + classRef: classRef, + from: fromExpr, + forElt: forExpr, + args: [classRef, fromExpr, forExpr], + op: function (context, eltColl, from, forElt) { + runtime.nullCheck(from, fromExpr); + runtime.nullCheck(forElt, forExpr); + var clazz = eltColl.className; + runtime.implicitLoop(from, function (target) { + target.classList.remove(clazz); + }); + runtime.implicitLoop(forElt, function (target) { + target.classList.add(clazz); + }); + return runtime.findNext(this, context); + }, + }; + return takeCmd; + } else { + var takeCmd = { + attributeRef: attributeRef, + from: fromExpr, + forElt: forExpr, + args: [fromExpr, forExpr, replacementValue], + op: function (context, from, forElt, replacementValue) { + runtime.nullCheck(from, fromExpr); + runtime.nullCheck(forElt, forExpr); + runtime.implicitLoop(from, function (target) { + if (!replacementValue) { + target.removeAttribute(attributeRef.name); + } else { + target.setAttribute(attributeRef.name, replacementValue) + } + }); + runtime.implicitLoop(forElt, function (target) { + target.setAttribute(attributeRef.name, attributeRef.value || "") + }); + return runtime.findNext(this, context); + }, + }; + return takeCmd; + } + } + }); + + function putInto(runtime, context, prop, valueToPut) { + if (prop != null) { + var value = runtime.resolveSymbol(prop, context); + } else { + var value = context; + } + if (value instanceof Element || value instanceof HTMLDocument) { + while (value.firstChild) value.removeChild(value.firstChild); + value.append(parser.runtime.convertValue(valueToPut, "Fragment")); + runtime.processNode(value); + } else { + if (prop != null) { + runtime.setSymbol(prop, context, null, valueToPut); + } else { + throw "Don't know how to put a value into " + typeof context; + } + } + } + + parser.addCommand("put", function (parser, runtime, tokens) { + if (tokens.matchToken("put")) { + var value = parser.requireElement("expression", tokens); + + var operationToken = tokens.matchAnyToken("into", "before", "after"); + + if (operationToken == null && tokens.matchToken("at")) { + tokens.matchToken("the"); // optional "the" + operationToken = tokens.matchAnyToken("start", "end"); + tokens.requireToken("of"); + } + + if (operationToken == null) { + parser.raiseParseError(tokens, "Expected one of 'into', 'before', 'at start of', 'at end of', 'after'"); + } + var target = parser.requireElement("expression", tokens); + + var operation = operationToken.value; + + var arrayIndex = false; + var symbolWrite = false; + var rootExpr = null; + var prop = null; + + if (target.type === "arrayIndex" && operation === "into") { + arrayIndex = true; + prop = target.prop; + rootExpr = target.root; + } else if (target.prop && target.root && operation === "into") { + prop = target.prop.value; + rootExpr = target.root; + } else if (target.type === "symbol" && operation === "into") { + symbolWrite = true; + prop = target.name; + } else if (target.type === "attributeRef" && operation === "into") { + var attributeWrite = true; + prop = target.name; + rootExpr = parser.requireElement("implicitMeTarget", tokens); + } else if (target.type === "styleRef" && operation === "into") { + var styleWrite = true; + prop = target.name; + rootExpr = parser.requireElement("implicitMeTarget", tokens); + } else if (target.attribute && operation === "into") { + var attributeWrite = target.attribute.type === "attributeRef"; + var styleWrite = target.attribute.type === "styleRef"; + prop = target.attribute.name; + rootExpr = target.root; + } else { + rootExpr = target; + } + + var putCmd = { + target: target, + operation: operation, + symbolWrite: symbolWrite, + value: value, + args: [rootExpr, prop, value], + op: function (context, root, prop, valueToPut) { + if (symbolWrite) { + putInto(runtime, context, prop, valueToPut); + } else { + runtime.nullCheck(root, rootExpr); + if (operation === "into") { + if (attributeWrite) { + runtime.implicitLoop(root, function (elt) { + elt.setAttribute(prop, valueToPut); + }); + } else if (styleWrite) { + runtime.implicitLoop(root, function (elt) { + elt.style[prop] = valueToPut; + }); + } else if (arrayIndex) { + root[prop] = valueToPut; + } else { + runtime.implicitLoop(root, function (elt) { + putInto(runtime, elt, prop, valueToPut); + }); + } + } else { + var op = + operation === "before" + ? Element.prototype.before + : operation === "after" + ? Element.prototype.after + : operation === "start" + ? Element.prototype.prepend + : operation === "end" + ? Element.prototype.append + : Element.prototype.append; // unreachable + + runtime.implicitLoop(root, function (elt) { + op.call( + elt, + valueToPut instanceof Node + ? valueToPut + : runtime.convertValue(valueToPut, "Fragment") + ); + // process any new content + if (elt.parentElement) { + runtime.processNode(elt.parentElement); + } else { + runtime.processNode(elt); + } + }); + } + } + return runtime.findNext(this, context); + }, + }; + return putCmd; + } + }); + + function parsePseudopossessiveTarget(parser, runtime, tokens) { + var targets; + if ( + tokens.matchToken("the") || + tokens.matchToken("element") || + tokens.matchToken("elements") || + tokens.currentToken().type === "CLASS_REF" || + tokens.currentToken().type === "ID_REF" || + (tokens.currentToken().op && tokens.currentToken().value === "<") + ) { + parser.possessivesDisabled = true; + try { + targets = parser.parseElement("expression", tokens); + } finally { + delete parser.possessivesDisabled; + } + // optional possessive + if (tokens.matchOpToken("'")) { + tokens.requireToken("s"); + } + } else if (tokens.currentToken().type === "IDENTIFIER" && tokens.currentToken().value === "its") { + var identifier = tokens.matchToken("its"); + targets = { + type: "pseudopossessiveIts", + token: identifier, + name: identifier.value, + evaluate: function (context) { + return runtime.resolveSymbol("it", context); + }, + }; + } else { + tokens.matchToken("my") || tokens.matchToken("me"); // consume optional 'my' + targets = parser.parseElement("implicitMeTarget", tokens); + } + return targets; + } + + parser.addCommand("transition", function (parser, runtime, tokens) { + if (tokens.matchToken("transition")) { + var targetsExpr = parsePseudopossessiveTarget(parser, runtime, tokens); + + var properties = []; + var from = []; + var to = []; + var currentToken = tokens.currentToken(); + while ( + !parser.commandBoundary(currentToken) && + currentToken.value !== "over" && + currentToken.value !== "using" + ) { + if (tokens.currentToken().type === "STYLE_REF") { + let styleRef = tokens.consumeToken(); + let styleProp = styleRef.value.substr(1); + properties.push({ + type: "styleRefValue", + evaluate: function () { + return styleProp; + }, + }); + } else { + properties.push(parser.requireElement("stringLike", tokens)); + } + + if (tokens.matchToken("from")) { + from.push(parser.requireElement("expression", tokens)); + } else { + from.push(null); + } + tokens.requireToken("to"); + if (tokens.matchToken("initial")) { + to.push({ + type: "initial_literal", + evaluate : function(){ + return "initial"; + } + }); + } else { + to.push(parser.requireElement("expression", tokens)); + } + currentToken = tokens.currentToken(); + } + if (tokens.matchToken("over")) { + var over = parser.requireElement("expression", tokens); + } else if (tokens.matchToken("using")) { + var using = parser.requireElement("expression", tokens); + } + + var transition = { + to: to, + args: [targetsExpr, properties, from, to, using, over], + op: function (context, targets, properties, from, to, using, over) { + runtime.nullCheck(targets, targetsExpr); + var promises = []; + runtime.implicitLoop(targets, function (target) { + var promise = new Promise(function (resolve, reject) { + var initialTransition = target.style.transition; + if (over) { + target.style.transition = "all " + over + "ms ease-in"; + } else if (using) { + target.style.transition = using; + } else { + target.style.transition = config.defaultTransition; + } + var internalData = runtime.getInternalData(target); + var computedStyles = getComputedStyle(target); + + var initialStyles = {}; + for (var i = 0; i < computedStyles.length; i++) { + var name = computedStyles[i]; + var initialValue = computedStyles[name]; + initialStyles[name] = initialValue; + } + + // store intitial values + if (!internalData.initalStyles) { + internalData.initalStyles = initialStyles; + } + + for (var i = 0; i < properties.length; i++) { + var property = properties[i]; + var fromVal = from[i]; + if (fromVal === "computed" || fromVal == null) { + target.style[property] = initialStyles[property]; + } else { + target.style[property] = fromVal; + } + } + //console.log("transition started", transition); + + var transitionStarted = false; + var resolved = false; + + target.addEventListener( + "transitionend", + function () { + if (!resolved) { + //console.log("transition ended", transition); + target.style.transition = initialTransition; + resolved = true; + resolve(); + } + }, + { once: true } + ); + + target.addEventListener( + "transitionstart", + function () { + transitionStarted = true; + }, + { once: true } + ); + + // it no transition has started in 100ms, continue + setTimeout(function () { + if (!resolved && !transitionStarted) { + //console.log("transition ended", transition); + target.style.transition = initialTransition; + resolved = true; + resolve(); + } + }, 100); + + setTimeout(function () { + var autoProps = []; + for (var i = 0; i < properties.length; i++) { + var property = properties[i]; + var toVal = to[i]; + if (toVal === "initial") { + var propertyValue = internalData.initalStyles[property]; + target.style[property] = propertyValue; + } else { + target.style[property] = toVal; + } + //console.log("set", property, "to", target.style[property], "on", target, "value passed in : ", toVal); + } + }, 0); + }); + promises.push(promise); + }); + return Promise.all(promises).then(function () { + return runtime.findNext(transition, context); + }); + }, + }; + return transition; + } + }); + + parser.addCommand("measure", function (parser, runtime, tokens) { + if (!tokens.matchToken("measure")) return; + + var targetExpr = parsePseudopossessiveTarget(parser, runtime, tokens); + + var propsToMeasure = []; + if (!parser.commandBoundary(tokens.currentToken())) + do { + propsToMeasure.push(tokens.matchTokenType("IDENTIFIER").value); + } while (tokens.matchOpToken(",")); + + return { + properties: propsToMeasure, + args: [targetExpr], + op: function (ctx, target) { + runtime.nullCheck(target, targetExpr); + if (0 in target) target = target[0]; // not measuring multiple elts + var rect = target.getBoundingClientRect(); + var scroll = { + top: target.scrollTop, + left: target.scrollLeft, + topMax: target.scrollTopMax, + leftMax: target.scrollLeftMax, + height: target.scrollHeight, + width: target.scrollWidth, + }; + + ctx.result = { + x: rect.x, + y: rect.y, + left: rect.left, + top: rect.top, + right: rect.right, + bottom: rect.bottom, + width: rect.width, + height: rect.height, + bounds: rect, + + scrollLeft: scroll.left, + scrollTop: scroll.top, + scrollLeftMax: scroll.leftMax, + scrollTopMax: scroll.topMax, + scrollWidth: scroll.width, + scrollHeight: scroll.height, + scroll: scroll, + }; + + runtime.forEach(propsToMeasure, function (prop) { + if (prop in ctx.result) ctx.locals[prop] = ctx.result[prop]; + else throw "No such measurement as " + prop; + }); + + return runtime.findNext(this, ctx); + }, + }; + }); + + parser.addLeafExpression("closestExpr", function (parser, runtime, tokens) { + if (tokens.matchToken("closest")) { + if (tokens.matchToken("parent")) { + var parentSearch = true; + } + + var css = null; + if (tokens.currentToken().type === "ATTRIBUTE_REF") { + var attributeRef = parser.requireElement("attributeRefAccess", tokens, null); + css = "[" + attributeRef.attribute.name + "]"; + } + + if (css == null) { + var expr = parser.requireElement("expression", tokens); + if (expr.css == null) { + parser.raiseParseError(tokens, "Expected a CSS expression"); + } else { + css = expr.css; + } + } + + if (tokens.matchToken("to")) { + var to = parser.parseElement("expression", tokens); + } else { + var to = parser.parseElement("implicitMeTarget", tokens); + } + + var closestExpr = { + type: "closestExpr", + parentSearch: parentSearch, + expr: expr, + css: css, + to: to, + args: [to], + op: function (ctx, to) { + if (to == null) { + return null; + } else { + let result = []; + runtime.implicitLoop(to, function(to){ + if (parentSearch) { + result.push(to.parentElement ? to.parentElement.closest(css) : null); + } else { + result.push(to.closest(css)); + } + }) + if (runtime.shouldAutoIterate(to)) { + return result; + } else { + return result[0]; + } + } + }, + evaluate: function (context) { + return runtime.unifiedEval(this, context); + }, + }; + + if (attributeRef) { + attributeRef.root = closestExpr; + attributeRef.args = [closestExpr]; + return attributeRef; + } else { + return closestExpr; + } + } + }); + + parser.addCommand("go", function (parser, runtime, tokens) { + if (tokens.matchToken("go")) { + if (tokens.matchToken("back")) { + var back = true; + } else { + tokens.matchToken("to"); + if (tokens.matchToken("url")) { + var target = parser.requireElement("stringLike", tokens); + var url = true; + if (tokens.matchToken("in")) { + tokens.requireToken("new"); + tokens.requireToken("window"); + var newWindow = true; + } + } else { + tokens.matchToken("the"); // optional the + var verticalPosition = tokens.matchAnyToken("top", "middle", "bottom"); + var horizontalPosition = tokens.matchAnyToken("left", "center", "right"); + if (verticalPosition || horizontalPosition) { + tokens.requireToken("of"); + } + var target = parser.requireElement("unaryExpression", tokens); + + var plusOrMinus = tokens.matchAnyOpToken("+", "-"); + if (plusOrMinus) { + tokens.pushFollow("px"); + try { + var offset = parser.requireElement("expression", tokens); + } finally { + tokens.popFollow(); + } + } + tokens.matchToken("px"); // optional px + + var smoothness = tokens.matchAnyToken("smoothly", "instantly"); + + var scrollOptions = {}; + if (verticalPosition) { + if (verticalPosition.value === "top") { + scrollOptions.block = "start"; + } else if (verticalPosition.value === "bottom") { + scrollOptions.block = "end"; + } else if (verticalPosition.value === "middle") { + scrollOptions.block = "center"; + } + } + + if (horizontalPosition) { + if (horizontalPosition.value === "left") { + scrollOptions.inline = "start"; + } else if (horizontalPosition.value === "center") { + scrollOptions.inline = "center"; + } else if (horizontalPosition.value === "right") { + scrollOptions.inline = "end"; + } + } + + if (smoothness) { + if (smoothness.value === "smoothly") { + scrollOptions.behavior = "smooth"; + } else if (smoothness.value === "instantly") { + scrollOptions.behavior = "instant"; + } + } + } + } + + var goCmd = { + target: target, + args: [target, offset], + op: function (ctx, to, offset) { + if (back) { + window.history.back(); + } else if (url) { + if (to) { + if (newWindow) { + window.open(to); + } else { + window.location.href = to; + } + } + } else { + runtime.implicitLoop(to, function (target) { + + if (target === window) { + target = document.body; + } + + if(plusOrMinus) { + // a top scroll w/ an offset of some sort + var boundingRect = target.getBoundingClientRect(); + let scrollShim = document.createElement('div'); + + if (plusOrMinus.value === "-") { + var finalOffset = -offset; + } else { + var finalOffset = - -offset; + } + + scrollShim.style.position = 'absolute'; + scrollShim.style.top = (boundingRect.x + finalOffset) + "px"; + scrollShim.style.left = (boundingRect.y + finalOffset) + "px"; + scrollShim.style.height = (boundingRect.height + (2 * finalOffset)) + "px"; + scrollShim.style.width = (boundingRect.width + (2 * finalOffset)) + "px"; + scrollShim.style.zIndex = "" + Number.MIN_SAFE_INTEGER; + scrollShim.style.opacity = "0"; + + document.body.appendChild(scrollShim); + setTimeout(function () { + document.body.removeChild(scrollShim); + }, 100); + + target = scrollShim; + } + + target.scrollIntoView(scrollOptions); + }); + } + return runtime.findNext(goCmd, ctx); + }, + }; + return goCmd; + } + }); + + config.conversions.dynamicResolvers.push(function (str, node) { + if (!(str === "Values" || str.indexOf("Values:") === 0)) { + return; + } + var conversion = str.split(":")[1]; + /** @type Object */ + var result = {}; + + var implicitLoop = parser.runtime.implicitLoop.bind(parser.runtime); + + implicitLoop(node, function (/** @type HTMLInputElement */ node) { + // Try to get a value directly from this node + var input = getInputInfo(node); + + if (input !== undefined) { + result[input.name] = input.value; + return; + } + + // Otherwise, try to query all child elements of this node that *should* contain values. + if (node.querySelectorAll != undefined) { + /** @type {NodeListOf} */ + var children = node.querySelectorAll("input,select,textarea"); + children.forEach(appendValue); + } + }); + + if (conversion) { + if (conversion === "JSON") { + return JSON.stringify(result); + } else if (conversion === "Form") { + /** @ts-ignore */ + // TODO: does this work with multiple inputs of the same name? + return new URLSearchParams(result).toString(); + } else { + throw "Unknown conversion: " + conversion; + } + } else { + return result; + } + + /** + * @param {HTMLInputElement} node + */ + function appendValue(node) { + var info = getInputInfo(node); + + if (info == undefined) { + return; + } + + // If there is no value already stored in this space. + if (result[info.name] == undefined) { + result[info.name] = info.value; + return; + } + + if (Array.isArray(result[info.name]) && Array.isArray(info.value)) { + result[info.name] = [].concat(result[info.name], info.value); + return; + } + } + + /** + * @param {HTMLInputElement} node + * @returns {{name:string, value:string | string[]} | undefined} + */ + function getInputInfo(node) { + try { + /** @type {{name: string, value: string | string[]}}*/ + var result = { + name: node.name, + value: node.value, + }; + + if (result.name == undefined || result.value == undefined) { + return undefined; + } + + if (node.type == "radio" && node.checked == false) { + return undefined; + } + + if (node.type == "checkbox") { + if (node.checked == false) { + result.value = undefined; + } else if (typeof result.value === "string") { + result.value = [result.value]; + } + } + + if (node.type == "select-multiple") { + /** @type {NodeListOf} */ + var selected = node.querySelectorAll("option[selected]"); + + result.value = []; + for (var index = 0; index < selected.length; index++) { + result.value.push(selected[index].value); + } + } + return result; + } catch (e) { + return undefined; + } + } + }); + + config.conversions["HTML"] = function (value) { + var toHTML = /** @returns {string}*/ function (/** @type any*/ value) { + if (value instanceof Array) { + return value + .map(function (item) { + return toHTML(item); + }) + .join(""); + } + + if (value instanceof HTMLElement) { + return value.outerHTML; + } + + if (value instanceof NodeList) { + var result = ""; + for (var i = 0; i < value.length; i++) { + var node = value[i]; + if (node instanceof HTMLElement) { + result += node.outerHTML; + } + } + return result; + } + + if (value.toString) { + return value.toString(); + } + + return ""; + }; + + return toHTML(value); + }; + + config.conversions["Fragment"] = function (val) { + var frag = document.createDocumentFragment(); + parser.runtime.implicitLoop(val, function (val) { + if (val instanceof Node) frag.append(val); + else { + var temp = document.createElement("template"); + temp.innerHTML = val; + frag.append(temp.content); + } + }); + return frag; + }; + } + + + // Public API + + const runtime_ = new Runtime(), lexer_ = runtime_.lexer, parser_ = runtime_.parser + + /** + * + * @param {string} src + * @param {Partial} [ctx] + */ + function run(src, ctx) { + return runtime_.evaluate(src, ctx) + } + + function browserInit() { + /** @type {HTMLScriptElement[]} */ + var scripts = Array.from(globalScope.document.querySelectorAll("script[type='text/hyperscript'][src]")) + Promise.all( + scripts.map(function (script) { + return fetch(script.src) + .then(function (res) { + return res.text(); + }); + }) + ) + .then(script_values => script_values.forEach(sc => _hyperscript(sc))) + .then(() => ready(function () { + mergeMetaConfig(); + runtime_.processNode(document.documentElement); + globalScope.document.addEventListener("htmx:load", function (/** @type {CustomEvent} */ evt) { + runtime_.processNode(evt.detail.elt); + }); + })); + + function ready(fn) { + if (document.readyState !== "loading") { + setTimeout(fn); + } else { + document.addEventListener("DOMContentLoaded", fn); + } + } + + function getMetaConfig() { + /** @type {HTMLMetaElement} */ + var element = document.querySelector('meta[name="htmx-config"]'); + if (element) { + return parseJSON(element.content); + } else { + return null; + } + } + + function mergeMetaConfig() { + var metaConfig = getMetaConfig(); + if (metaConfig) { + Object.assign(config, metaConfig); + } + } + } + + /** + * @typedef {Object} HyperscriptAPI + * + * @property {Object} config + * @property {string} config.attributes + * @property {string} config.defaultTransition + * @property {string} config.disableSelector + * @property {typeof conversions} config.conversions + * + * @property {Object} internals + * @property {Lexer} internals.lexer + * @property {typeof Lexer} internals.Lexer + * @property {Parser} internals.parser + * @property {typeof Parser} internals.Parser + * @property {Runtime} internals.runtime + * @property {typeof Runtime} internals.Runtime + * + * @property {typeof ElementCollection} ElementCollection + * + * @property {(keyword: string, definition: ParseRule) => void} addFeature + * @property {(keyword: string, definition: ParseRule) => void} addCommand + * @property {(keyword: string, definition: ParseRule) => void} addLeafExpression + * @property {(keyword: string, definition: ParseRule) => void} addIndirectExpression + * + * @property {(src: string, ctx?: Partial) => any} evaluate + * @property {(src: string) => ASTNode} parse + * @property {(node: Element) => void} processNode + * + * @property {() => void} browserInit + * + * + * @typedef {HyperscriptAPI & ((src: string, ctx?: Partial) => any)} Hyperscript + */ + + /** + * @type {Hyperscript} + */ + const _hyperscript = Object.assign( + run, + { + config, + + use(plugin) { plugin(_hyperscript) }, + + internals: { + lexer: lexer_, parser: parser_, runtime: runtime_, + Lexer, Tokens, Parser, Runtime, + }, + ElementCollection, + + addFeature: parser_.addFeature.bind(parser_), + addCommand: parser_.addCommand.bind(parser_), + addLeafExpression: parser_.addLeafExpression.bind(parser_), + addIndirectExpression: parser_.addIndirectExpression.bind(parser_), + + evaluate: runtime_.evaluate.bind(runtime_), + parse: runtime_.parse.bind(runtime_), + processNode: runtime_.processNode.bind(runtime_), + + browserInit, + } + ) + + return _hyperscript +}) diff --git a/templates/interactive-posts.html b/templates/interactive-posts.html index eccf724..dc07a5f 100644 --- a/templates/interactive-posts.html +++ b/templates/interactive-posts.html @@ -16,12 +16,15 @@
{% include 'answer-post.html' %}
-
-

Upload IPS File

-
- - -
+
+
+ + + +