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