//============================================================================= // RPG Maker MZ - plugin base //============================================================================= /*: * @target MZ * @plugindesc * 🚩 Basic function library for RPG Maker plugins. * Version: 0.3.0 * @author F_ * @url https://github.com/f-space/rmmz-plugins * * @help * * Basic function library for RPG Maker plugins. * * --- * Copyright (c) 2020 F_ * Released under the MIT license * https://github.com/f-space/rmmz-plugins/blob/master/LICENSE */ "use strict"; { const LinkedList = (() => { const make = () => node(null); const init = (node, value) => Object.assign(node, { value, prev: node, next: node }); const node = (value) => init({}, value); const value = (node) => node.value; const first = (list) => list.next; const last = (list) => list.prev; const $set = (node, value) => { node.value = value; }; const $insertBefore = (node, ref) => { const prev = ref.prev; node.prev = prev; node.next = ref; ref.prev = prev.next = node; }; const $remove = (node) => { const { prev, next } = node; prev.next = next; next.prev = prev; node.prev = node.next = node; }; return { make, node, value, first, last, $set, $insertBefore, $remove }; })(); const LruCache = (() => { const make = (capacity) => ({ capacity: Math.max(capacity, 1), map: new Map(), list: LinkedList.make(), }); const $get = (cache, key) => { const { map, list } = cache; const node = map.get(key); if (node !== undefined) { $touch(list, node); return LinkedList.value(node).value; } else { return undefined; } }; const $set = (cache, key, value) => { const { capacity, map, list } = cache; const node = map.get(key); if (node !== undefined) { LinkedList.$set(node, value); $touch(cache, node); } else { while (map.size >= capacity) { $remove(map, LinkedList.last(list)); } $add(map, list, LinkedList.node({ key, value })); } }; const $touch = (list, node) => { LinkedList.$remove(node); LinkedList.$insertBefore(node, LinkedList.first(list)); }; const $add = (map, list, node) => { const { key } = LinkedList.value(node); map.set(key, node); LinkedList.$insertBefore(node, LinkedList.first(list)); }; const $remove = (map, node) => { const { key } = LinkedList.value(node); map.delete(key); LinkedList.$remove(node); }; return { make, $get, $set }; })(); const Monad = (unit, bind) => { const map = (monad, fn) => bind(monad, (value) => unit(fn(value))); const zip = (monads) => zipRec(monads, []); const zipRec = (ms, xs) => ms.length !== 0 ? bind(ms[0], (x) => zipRec(ms.slice(1), [...xs, x])) : unit(xs); const zipL = (monads) => zipLRec(monads, []); const zipLRec = (ms, xs) => ms.length !== 0 ? bind(ms[0](), (x) => zipLRec(ms.slice(1), [...xs, x])) : unit(xs); return { map, zip, zipL }; }; const panic = (message) => { throw new Error(message); }; const try_ = (fn, handler) => { try { return fn(); } catch (e) { return handler(e); } }; const O = (() => { const some = (value) => ({ value }); const none = () => undefined; const unwrap = (option) => option.value; const isSome = (option) => option !== undefined; const isNone = (option) => option === undefined; const andThen = (option, fn) => isSome(option) ? fn(unwrap(option)) : option; const orElse = (option, fn) => (isSome(option) ? option : fn()); const match = (option, onSome, onNone) => isSome(option) ? onSome(unwrap(option)) : onNone(); const expect = (option, formatter) => isSome(option) ? unwrap(option) : panic(formatter()); const withDefault = (option, value) => isSome(option) ? unwrap(option) : value; const { map, zip, zipL } = Monad(some, andThen); return { some, none, unwrap, isSome, isNone, andThen, orElse, match, expect, withDefault, map, zip, zipL, }; })(); const R = (() => { const ok = (value) => ({ value }); const err = (error) => ({ error }); const unwrap = (result) => result.value; const unwrapErr = (result) => result.error; const isOk = (result) => result.hasOwnProperty("value"); const isErr = (result) => !isOk(result); const andThen = (result, fn) => isOk(result) ? fn(unwrap(result)) : result; const orElse = (result, fn) => isOk(result) ? result : fn(unwrapErr(result)); const match = (result, onOk, onErr) => isOk(result) ? onOk(unwrap(result)) : onErr(unwrapErr(result)); const expect = (result, formatter) => isOk(result) ? unwrap(result) : panic(formatter(unwrapErr(result))); const attempt = (fn) => try_(() => ok(fn()), err); const mapBoth = (result, mapOk, mapErr) => match( result, (value) => ok(mapOk(value)), (error) => err(mapErr(error)) ); const { map: map, zip: all, zipL: allL } = Monad(ok, andThen); const { map: mapErr, zip: any, zipL: anyL } = Monad(err, orElse); return { ok, err, unwrap, unwrapErr, isOk, isErr, andThen, orElse, match, expect, attempt, mapBoth, map, mapErr, all, any, allL, anyL, }; })(); const L = (() => { const nil = () => null; const cons = (x, xs) => [x, xs]; const singleton = (x) => cons(x, nil()); const empty = (list) => list === null; const head = (list) => list[0]; const tail = (list) => list[1]; const match = (list, onNil, onCons) => empty(list) ? onNil() : onCons(head(list), tail(list)); const find = (list, fn) => empty(list) ? undefined : fn(head(list)) ? head(list) : find(tail(list), fn); const some = (list, fn) => empty(list) ? false : fn(head(list)) ? true : some(tail(list), fn); const every = (list, fn) => empty(list) ? true : fn(head(list)) ? every(tail(list), fn) : false; const reverse = (list) => reverseRec(list, nil()); const reverseRec = (list, acc) => empty(list) ? acc : reverseRec(tail(list), cons(head(list), acc)); const reduce = (list, fn, value) => empty(list) ? value : reduce(tail(list), fn, fn(value, head(list))); const reduceRight = (list, fn, value) => reduce(reverse(list), fn, value); const map = (list, fn) => reduceRight(list, (xs, x) => cons(fn(x), xs), nil()); const toArray = (list) => reduce(list, (xs, x) => [...xs, x], []); return { nil, cons, singleton, empty, head, tail, match, find, some, every, reverse, reduce, reduceRight, map, toArray, }; })(); const S = (() => { const ELLIPSIS = "..."; const ellipsis = (s, length) => s.length > length ? s.slice(0, length - ELLIPSIS.length) + ELLIPSIS : s; const debug = (value, replacer) => { const rec = (value, context) => { const { replacer } = context; const v = replacer !== undefined ? replacer(value) : value; switch (typeof v) { case "undefined": return String(v); case "number": return String(v); case "string": return JSON.stringify(v); case "boolean": return String(v); case "symbol": return String(v); case "bigint": return `${v}n`; case "object": return object(v, context); case "function": return `[Function: ${name(v)}]`; default: return ""; } }; const name = ({ name }) => typeof name === "string" && name !== "" ? name : "(anonymous)"; const object = (value, context) => { const { stack } = context; if (value === null) return "null"; if (stack.includes(value)) return "..."; return objectCore(value, { ...context, stack: [...stack, value] }); }; const objectCore = (value, context) => { const array = (value, step) => value.length !== 0 ? `[ ${value.map(step).join(", ")} ]` : "[]"; const step = (value) => rec(value, context); if (Array.isArray(value)) return array(value, step); if (value instanceof RegExp) return String(value); if (value instanceof Date) return value.toISOString(); return normalObject(value, step); }; const normalObject = (value, step) => { const type = objectType(value); const entries = objectEntries(value, step); const label = type !== undefined ? `${type} ` : ""; const contents = entries !== "" ? `{ ${entries} }` : "{}"; return label + contents; }; const objectType = (value) => { const prototype = Object.getPrototypeOf(value); if (prototype === null) return "(null)"; if (prototype === Object.prototype) return undefined; return name(prototype.constructor); }; const objectEntries = (value, step) => { const map = (value, step) => Array.from(value) .map((entry) => entry.map(step).join(" => ")) .join(", "); const set = (value, step) => Array.from(value).map(step).join(", "); const other = (value, step) => Object.entries(value) .map(([k, v]) => `${key(k)}: ${step(v)}`) .join(", "); const key = (s) => /^[a-z_$][a-z0-9_$]*$/i.test(s) ? s : JSON.stringify(s); if (value instanceof Map) return map(value, step); if (value instanceof Set) return set(value, step); if (value instanceof WeakMap) return "<***>"; if (value instanceof WeakSet) return "<***>"; return other(value, step); }; return rec(value, { replacer, stack: [] }); }; return { ellipsis, debug }; })(); const U = (() => { const simpleEqual = (a, b) => { const arrayEqual = (a, b, eq) => a.length === b.length && a.every((v, i) => eq(v, b[i])); const pojoEqual = (a, b, eq) => { const has = (obj, key) => obj.hasOwnProperty(key); const akeys = Object.keys(a); const bkeys = Object.keys(b); return ( akeys.length === bkeys.length && akeys.every((k) => has(b, k) && eq(a[k], b[k])) ); }; const isPojo = (x) => Object.getPrototypeOf(x) === Object.prototype; if (Object.is(a, b)) return true; if (typeof a !== typeof b) return false; if (typeof a !== "object") return false; if (a === null || b === null) return false; if (Array.isArray(a)) return Array.isArray(b) && arrayEqual(a, b, simpleEqual); if (isPojo(a)) return isPojo(b) && pojoEqual(a, b, simpleEqual); return false; }; const defaultSerialize = (args) => JSON.stringify(args); const memo = (fn, size, serialize = defaultSerialize) => { const cache = LruCache.make(size); return (...args) => { const key = serialize(args); const result = LruCache.$get(cache, key); if (result !== undefined) { return result.value; } else { const value = fn(...args); LruCache.$set(cache, key, { value }); return value; } }; }; const defaultEq = (a, b) => a.every((v, i) => Object.is(v, b[i])); const memo1 = (fn, eq = defaultEq) => { let cache = null; return (...args) => { if (cache !== null && eq(cache.args, args)) { return cache.value; } else { const value = fn(...args); cache = { args, value }; return value; } }; }; const memoW = (fn) => { const cache = new WeakMap(); return (obj) => { if (cache.has(obj)) { return cache.get(obj); } else { const value = fn(obj); cache.set(obj, value); return value; } }; }; return { simpleEqual, memo, memo1, memoW }; })(); const G = (() => { const tokenError = ({ cache, ...context }, cause) => ({ type: "token", context, cause, }); const eoiError = ({ cache, ...context }) => ({ type: "eoi", context }); const andError = ({ cache, ...context }, error) => ({ type: "and", context, error, }); const notError = ({ cache, ...context }, value) => ({ type: "not", context, value, }); const validationError = ({ cache, ...context }, cause) => ({ type: "validation", context, cause, }); const token = (accept) => (context) => { const { source, position } = context; return R.match( accept(source, position), ([value, position]) => R.ok([value, { ...context, position }]), (cause) => R.err(tokenError(context, cause)) ); }; const eoi = () => (context) => { const { source, position } = context; return position === source.length ? R.ok([null, context]) : R.err(eoiError(context)); }; const succeed = (value) => (context) => R.ok([value, context]); const fail = (error) => () => R.err(error); const andThen = (parser, fn) => (context) => R.andThen(parser(context), ([value, context]) => fn(value)(context)); const orElse = (parser, fn) => (context) => R.orElse(parser(context), (error) => fn(error)(context)); const if_ = (cond, then, else_) => (context) => R.match( cond(context), ([value, context]) => then(value)(context), (error) => else_(error)(context) ); const map = (parser, fn) => (context) => R.map(parser(context), ([value, context]) => [fn(value), context]); const mapError = (parser, fn) => (context) => R.mapErr(parser(context), fn); const fromResult = (result) => R.match(result, succeed, fail); const sequence = (a, b) => andThen(a, (v1) => map(b, (v2) => L.cons(v2, v1))); const choice = (a, b) => orElse(a, (e1) => mapError(b, (e2) => L.cons(e2, e1))); const loop = (parser, acc) => if_( parser, (value) => loop(parser, L.cons(value, acc)), () => succeed(acc) ); const toArray = (list) => L.toArray(L.reverse(list)); const seqOf = (parsers) => map(parsers.reduce(sequence, succeed(L.nil())), toArray); const oneOf = (parsers) => mapError(parsers.reduce(choice, fail(L.nil())), toArray); const optional = (parser) => choice(map(parser, O.some), succeed(O.none())); const many = (parser) => map(loop(parser, L.nil()), toArray); const many1 = (parser) => map( andThen(parser, (value) => loop(parser, L.singleton(value))), toArray ); const and = (pred, parser) => (context) => R.match( pred(context), () => parser(context), (error) => R.err(andError(context, error)) ); const not = (pred, parser) => (context) => R.match( pred(context), ([value]) => R.err(notError(context, value)), () => parser(context) ); const ref = (getter) => (context) => getter()(context); const validate = (parser, validator) => (context) => andThen(parser, (value) => fromResult( R.mapErr(validator(value), (cause) => validationError(context, cause)) ) )(context); const memo = (parser) => (context) => { const { position, cache } = context; if (cache !== null && position < cache.length) { const list = cache[position]; const entry = L.find(list, (x) => x.parser === parser); if (entry !== undefined) { return entry.result; } else { const result = parser(context); cache[position] = L.cons({ parser, result }, list); return result; } } else { return parser(context); } }; const makeContext = (source, position, options = {}) => { const cache = options.noCache ? null : [...new Array(source.length).fill(L.nil())]; return { source, position, cache }; }; const make = (parser, options) => (source, position = 0) => R.map( parser(makeContext(source, position, options)), ([value, { position }]) => [value, position] ); const mk = (parser, options) => (source) => R.map(parser(makeContext(source, 0, options)), ([value]) => value); const parse = (source, parser, errorFormatter = defaultErrorFormatter) => R.expect(parser(source), errorFormatter); const makeDefaultErrorFormatter = ( tokenErrorFormatter, validationErrorFormatter ) => { const dots = (s) => S.ellipsis(s, 16); const rest = ({ source: s, position: i }) => typeof s == "string" ? `"${dots(s.slice(i))}"` : S.debug(s[i]); const pick = (error) => { const reduce = (errors) => errors.reduce((acc, e) => choice(acc, pick(e)), undefined); const choice = (a, b) => (priority(a) >= priority(b) ? a : b); const priority = (e) => e?.context.position ?? -Infinity; return Array.isArray(error) ? reduce(error) : error; }; const message = (error) => { switch (error?.type) { case "token": return tokenErrorFormatter(error.cause); case "eoi": return `end-of-input expected, but ${rest(error.context)} found`; case "and": return `and-predicate failed at ${rest(error.context)}`; case "not": return `not-predicate failed at ${rest(error.context)}`; case "validation": return validationErrorFormatter(error.cause); default: return `unknown error: ${S.debug(error)}`; } }; return (error) => message(pick(error)); }; const simpleErrorFormatter = (error) => typeof error === "string" ? error : S.debug(error); const defaultErrorFormatter = makeDefaultErrorFormatter( simpleErrorFormatter, simpleErrorFormatter ); return { token, eoi, succeed, fail, andThen, orElse, if: if_, map, mapError, seqOf, oneOf, optional, many, many1, and, not, ref, validate, memo, make, mk, parse, makeDefaultErrorFormatter, defaultErrorFormatter, }; })(); const E = (() => { const NUMBER = "number"; const BOOLEAN = "boolean"; const ANY = "any"; const Lexer = (() => { const RE_WHITESPACE = /^[ \t\r\n]*/; const RE_DECIMAL_DIGITS = `(?:[0-9]+(?:_[0-9]+)*)`; const RE_DECIMAL_INTEGER_LITERAL = `(?:0(?![bBoOxX])|[1-9](?:_?${RE_DECIMAL_DIGITS})?)`; const RE_EXPORNENT_PART = `(?:[eE][+-]?${RE_DECIMAL_DIGITS})`; const RE_DECIMAL_LITERAL = `(?:(?:${RE_DECIMAL_INTEGER_LITERAL}(?:\\.${RE_DECIMAL_DIGITS}?)?|\\.${RE_DECIMAL_DIGITS})${RE_EXPORNENT_PART}?)`; const RE_BINARY_INTEGER_LITERAL = `(?:0[bB][0-1]+(?:_[0-1]+)*)`; const RE_OCTAL_INTEGER_LITERAL = `(?:0[oO][0-7]+(?:_[0-7]+)*)`; const RE_HEX_INTEGER_LITERAL = `(?:0[xX][0-9a-fA-F]+(?:_[0-9a-fA-F]+)*)`; const RE_NON_DECIMAL_INTEGER_LITERAL = `(?:${RE_BINARY_INTEGER_LITERAL}|${RE_OCTAL_INTEGER_LITERAL}|${RE_HEX_INTEGER_LITERAL})`; const RE_NUMERIC_LITERAL = `(?:${RE_DECIMAL_LITERAL}|${RE_NON_DECIMAL_INTEGER_LITERAL})`; const RE_NUMBER = new RegExp(`^${RE_NUMERIC_LITERAL}`); const RE_BOOLEAN = /^(?:true|false)\b/; const RE_IDENTIFIER = /^[a-z$][a-z0-9$_]*/i; const RE_UNKNOWN = /^(?:[a-z0-9$_]+|[\p{L}\p{N}\p{Pc}\p{M}\p{Cf}]+|[\p{P}\p{S}]+|[^ \t\r\n]+)/iu; const next = (type, text, position) => [ { type, text, position }, position + text.length, ]; const symbol = (symbol) => G.token((source, position) => { return source.startsWith(symbol, position) ? R.ok(next(symbol, symbol, position)) : R.err(symbol); }); const regexp = (type, re) => G.memo( G.token((source, position) => { const match = source.slice(position).match(re); return match !== null ? R.ok(next(type, match[0], position)) : R.err(type); }) ); return { "!": symbol("!"), "!==": symbol("!=="), "%": symbol("%"), "&&": symbol("&&"), "(": symbol("("), ")": symbol(")"), "*": symbol("*"), "**": symbol("**"), "+": symbol("+"), ",": symbol(","), "-": symbol("-"), ".": symbol("."), "/": symbol("/"), ":": symbol(":"), "<": symbol("<"), "<=": symbol("<="), "===": symbol("==="), ">": symbol(">"), ">=": symbol(">="), "?": symbol("?"), "[": symbol("["), "]": symbol("]"), "||": symbol("||"), whitespace: regexp("whitespace", RE_WHITESPACE), number: regexp("number", RE_NUMBER), boolean: regexp("boolean", RE_BOOLEAN), identifier: regexp("identifier", RE_IDENTIFIER), unknown: regexp("unknown", RE_UNKNOWN), }; })(); const parser = (() => { const numberNode = (value) => ({ type: "number", value }); const booleanNode = (value) => ({ type: "boolean", value }); const identifierNode = (name) => ({ type: "identifier", name }); const memberAccessNode = (object, property) => ({ type: "member-access", object, property, }); const elementAccessNode = (array, index) => ({ type: "element-access", array, index, }); const functionCallNode = (callee, args) => ({ type: "function-call", callee, args, }); const unaryOpNode = (operator, expr) => ({ type: "unary-operator", operator, expr, }); const binaryOpNode = (operator, lhs, rhs) => ({ type: "binary-operator", operator, lhs, rhs, }); const condOpNode = (if_, then, else_) => ({ type: "conditional-operator", if: if_, then, else: else_, }); const token = (type) => ((parser) => G.andThen(Lexer.whitespace, () => parser))(Lexer[type]); const fail = (type) => G.token(() => R.err(type)); const number = G.map(token("number"), (value) => numberNode(value)); const boolean = G.map(token("boolean"), (value) => booleanNode(value)); const identifier = G.map(token("identifier"), (name) => identifierNode(name) ); const group = (term, expr) => { const succ = G.andThen(expr, (node) => G.map(token(")"), () => node)); return G.if( token("("), () => succ, () => term ); }; const postfixOp = (term, expr) => { const cont = G.ref(() => rec); const cases = [ memberAccess(cont), elementAccess(expr, cont), functionCall(expr, cont), ]; const rec = cases.reduceRight( (next, case_) => case_(next), G.succeed((node) => node) ); return G.andThen(term, (node) => G.map(rec, (ctor) => ctor(node))); }; const memberAccess = (cont) => (next) => { const succ = G.map( G.seqOf([identifier, cont]), ([property, ctor]) => (object) => ctor(memberAccessNode(object, property)) ); return G.if( token("."), () => succ, () => next ); }; const elementAccess = (expr, cont) => (next) => { const succ = G.map( G.seqOf([expr, token("]"), cont]), ([index, , ctor]) => (array) => ctor(elementAccessNode(array, index)) ); return G.if( token("["), () => succ, () => next ); }; const functionCall = (expr, cont) => (next) => { const succ = G.map( G.seqOf([functionArgs(expr), token(")"), cont]), ([args, , ctor]) => (callee) => ctor(functionCallNode(callee, args)) ); return G.if( token("("), () => succ, () => next ); }; const functionArgs = (expr) => { const rec = G.andThen(expr, (node) => G.if( token(","), () => G.orElse(G.and(token(")"), G.succeed(L.singleton(node))), () => G.map(rec, (rest) => L.cons(node, rest)) ), () => G.succeed(L.singleton(node)) ) ); return G.orElse(G.not(expr, G.succeed([])), () => G.map(rec, L.toArray) ); }; const unaryOp = (term, op) => { const rec = G.if( op, (operator) => G.map(rec, (expr) => unaryOpNode(operator, expr)), () => term ); return rec; }; const binaryOpL = (term, op) => G.map( binaryOpList(term, op), (list) => L.reduce(list, binaryOpReducerL, [])[0] ); const binaryOpReducerL = (xs, x) => xs.length == 2 ? [binaryOpNode(xs[1], xs[0], x)] : [...xs, x]; const binaryOpR = (term, op) => G.map( binaryOpList(term, op), (list) => L.reduceRight(list, binaryOpReducerR, [])[0] ); const binaryOpReducerR = (xs, x) => xs.length == 2 ? [binaryOpNode(xs[1], x, xs[0])] : [...xs, x]; const binaryOpList = (term, op) => { const rec = G.andThen(term, (lhs) => G.if( op, (operator) => G.map(rec, (rhs) => L.cons(lhs, L.cons(operator, rhs))), () => G.succeed(L.singleton(lhs)) ) ); return rec; }; const condOp = (term, expr) => { const succ = G.seqOf([expr, token(":"), G.ref(() => rec)]); const rec = G.andThen(term, (if_) => G.if( token("?"), () => G.map(succ, ([then, , else_]) => condOpNode(if_, then, else_)), () => G.succeed(if_) ) ); return rec; }; const expression = G.ref(() => exprL0); const exprL11 = G.memo( G.orElse(G.oneOf([number, boolean, identifier]), () => fail("expression") ) ); const exprL10 = G.memo(group(exprL11, expression)); const exprL9 = G.memo(postfixOp(exprL10, expression)); const exprL8 = G.memo( unaryOp(exprL9, G.oneOf(["+", "-", "!"].map(token))) ); const exprL7 = G.memo(binaryOpR(exprL8, token("**"))); const exprL6 = G.memo( binaryOpL(exprL7, G.oneOf(["*", "/", "%"].map(token))) ); const exprL5 = G.memo(binaryOpL(exprL6, G.oneOf(["+", "-"].map(token)))); const exprL4 = G.memo( binaryOpL(exprL5, G.oneOf(["<=", ">=", "<", ">"].map(token))) ); const exprL3 = G.memo( binaryOpL(exprL4, G.oneOf(["===", "!=="].map(token))) ); const exprL2 = G.memo(binaryOpL(exprL3, token("&&"))); const exprL1 = G.memo(binaryOpL(exprL2, token("||"))); const exprL0 = G.memo(condOp(exprL1, expression)); return expression; })(); const parse = (() => { const tail = G.andThen(Lexer.whitespace, () => G.eoi()); const whole = G.andThen(parser, (value) => G.map(tail, () => value)); return G.mk(whole); })(); const build = (() => { const BUILTIN_VARS = { Infinity, NaN, Math }; const MEMBER_BLOCK_LIST = ["prototype", "constructor"]; const VALUE_BLOCK_LIST = new Map([ [globalThis, "global object"], [Object, "Object"], [Object.prototype, "Object.prototype"], [Function, "Function"], [Function.prototype, "Function.prototype"], ]); const referenceError = (name) => ({ type: "reference", name }); const propertyError = (property) => ({ type: "property", property }); const rangeError = (index) => ({ type: "range", index }); const typeError = (expected, actual) => ({ type: "type", expected, actual, }); const securityError = (target) => ({ type: "security", target }); const builtinValue = (name) => BUILTIN_VARS.hasOwnProperty(name) ? O.some(BUILTIN_VARS[name]) : O.none(); const blockedMember = (name) => MEMBER_BLOCK_LIST.includes(name) ? O.some(`${name} property`) : O.none(); const blockedValue = (value) => VALUE_BLOCK_LIST.has(value) ? O.some(VALUE_BLOCK_LIST.get(value)) : O.none(); const bind = (fn) => (a) => R.andThen(a, fn); const bindL2 = (fn) => (a, b) => R.andThen(a(), (a) => R.andThen(b(), (b) => fn(a, b))); const liftL1 = (fn) => (a) => R.map(a(), (a) => fn(a)); const liftL2 = (fn) => (a, b) => R.andThen(a(), (a) => R.map(b(), (b) => fn(a, b))); const liftL3 = (fn) => (a, b, c) => R.andThen(a(), (a) => R.andThen(b(), (b) => R.map(c(), (c) => fn(a, b, c))) ); const type = (name, fn) => bind((value) => fn(value) ? R.ok(value) : R.err(typeError(name, value)) ); const isNumber = type("number", (value) => typeof value === "number"); const isInteger = type( "integer", (value) => typeof value === "number" && Number.isSafeInteger(value) ); const isBoolean = type("boolean", (value) => typeof value === "boolean"); const isFunction = type( "function", (value) => typeof value === "function" ); const isObject = type( "object", (value) => (typeof value === "object" && value !== null) || typeof value === "function" ); const isArray = type("array", (value) => Array.isArray(value)); const secure = bind((value) => O.match( blockedValue(value), (target) => R.err(securityError(target)), () => R.ok(value) ) ); const member = bindL2((object, property) => property in object ? R.ok(object[property]) : R.err(propertyError(property)) ); const element = bindL2((array, index) => index >= 0 && index < array.length ? R.ok(array[index]) : R.err(rangeError(index)) ); const callStatic = liftL2((fn, args) => fn(...args)); const callMember = liftL3((this_, fn, args) => fn.apply(this_, args)); const unary = liftL1; const binary = liftL2; const build = (type, node) => { const ensureType = typeContract(type); const evalExpr = expression(node); return (env) => ensureType(evalExpr(env)); }; const typeContract = (type) => { switch (type) { case NUMBER: return isNumber; case BOOLEAN: return isBoolean; case ANY: return (x) => x; default: return panic(`unsupported expression type: ${type}`); } }; const expression = (node) => { const { type } = node; switch (type) { case "number": return number(node); case "boolean": return boolean(node); case "identifier": return identifier(node); case "member-access": return memberAccess(node); case "element-access": return elementAccess(node); case "function-call": return functionCall(node); case "unary-operator": return unaryOp(node); case "binary-operator": return binaryOp(node); case "conditional-operator": return condOp(node); default: return panic(`invalid AST node type: ${type}`); } }; const number = (node) => { const value = Number(node.value.text.replace(/_/g, "")); return () => R.ok(value); }; const boolean = (node) => { const value = node.value.text === "true"; return () => R.ok(value); }; const identifier = (node) => { const name = node.name.text; return O.match( builtinValue(name), (value) => () => R.ok(value), () => (env) => env.hasOwnProperty(name) ? R.ok(env[name]) : R.err(referenceError(name)) ); }; const memberAccess = (node) => { const { evalThis, evalExpr } = memberAccessCore(node); return (env) => evalExpr(evalThis(env), env); }; const memberAccessCore = (node) => { const evalObject = expression(node.object); const property = node.property.name.text; return { evalThis: evalObject, evalExpr: O.match( blockedMember(property), (target) => () => R.err(securityError(target)), () => (this_) => secure( member( () => isObject(this_), () => R.ok(property) ) ) ), }; }; const elementAccess = (node) => { const { evalThis, evalExpr } = elementAccessCore(node); return (env) => evalExpr(evalThis(env), env); }; const elementAccessCore = (node) => { const evalArray = expression(node.array); const evalIndex = expression(node.index); return { evalThis: evalArray, evalExpr: (this_, env) => secure( element( () => isArray(this_), () => isInteger(evalIndex(env)) ) ), }; }; const functionCall = (node) => { const callee = functionCallee(node.callee); const evalArgs = functionArgs(node.args); return callee.isStatic ? functionStaticCall(callee.eval, evalArgs) : functionMemberCall(callee.evalThis, callee.evalExpr, evalArgs); }; const functionCallee = (node) => { switch (node.type) { case "member-access": return { isStatic: false, ...memberAccessCore(node) }; case "element-access": return { isStatic: false, ...elementAccessCore(node) }; default: return { isStatic: true, eval: expression(node) }; } }; const functionArgs = (nodes) => { const evalList = nodes.map((node) => expression(node)); return (env) => R.allL(evalList.map((evalArg) => () => evalArg(env))); }; const functionStaticCall = (evalCallee, evalArgs) => { return (env) => secure( callStatic( () => isFunction(evalCallee(env)), () => evalArgs(env) ) ); }; const functionMemberCall = (evalThis, evalExpr, evalArgs) => { return (env) => { const this_ = evalThis(env); return secure( callMember( () => this_, () => isFunction(evalExpr(this_, env)), () => evalArgs(env) ) ); }; }; const unaryOp = (node) => { const operator = node.operator.text; const evalExpr = expression(node.expr); switch (operator) { case "+": return (env) => unary((a) => +a)(() => isNumber(evalExpr(env))); case "-": return (env) => unary((a) => -a)(() => isNumber(evalExpr(env))); case "!": return (env) => unary((a) => !a)(() => isBoolean(evalExpr(env))); default: return panic(`unsupported unary operator: ${operator}`); } }; const binaryOp = (node) => { const operator = node.operator.text; const evalL = expression(node.lhs); const evalR = expression(node.rhs); switch (operator) { case "+": return (env) => binary((a, b) => a + b)( () => isNumber(evalL(env)), () => isNumber(evalR(env)) ); case "-": return (env) => binary((a, b) => a - b)( () => isNumber(evalL(env)), () => isNumber(evalR(env)) ); case "*": return (env) => binary((a, b) => a * b)( () => isNumber(evalL(env)), () => isNumber(evalR(env)) ); case "/": return (env) => binary((a, b) => a / b)( () => isNumber(evalL(env)), () => isNumber(evalR(env)) ); case "%": return (env) => binary((a, b) => a % b)( () => isNumber(evalL(env)), () => isNumber(evalR(env)) ); case "**": return (env) => binary((a, b) => a ** b)( () => isNumber(evalL(env)), () => isNumber(evalR(env)) ); case "===": return (env) => binary((a, b) => a === b)( () => isNumber(evalL(env)), () => isNumber(evalR(env)) ); case "!==": return (env) => binary((a, b) => a !== b)( () => isNumber(evalL(env)), () => isNumber(evalR(env)) ); case "<=": return (env) => binary((a, b) => a <= b)( () => isNumber(evalL(env)), () => isNumber(evalR(env)) ); case ">=": return (env) => binary((a, b) => a >= b)( () => isNumber(evalL(env)), () => isNumber(evalR(env)) ); case "<": return (env) => binary((a, b) => a < b)( () => isNumber(evalL(env)), () => isNumber(evalR(env)) ); case ">": return (env) => binary((a, b) => a > b)( () => isNumber(evalL(env)), () => isNumber(evalR(env)) ); case "&&": return (env) => R.andThen(isBoolean(evalL(env)), (value) => value ? isBoolean(evalR(env)) : R.ok(false) ); case "||": return (env) => R.andThen(isBoolean(evalL(env)), (value) => value ? R.ok(true) : isBoolean(evalR(env)) ); default: return panic(`unsupported binary operator: ${operator}`); } }; const condOp = (node) => { const { if: if_, then, else: else_ } = node; const evalIf = expression(if_); const evalThen = expression(then); const evalElse = expression(else_); return (env) => R.andThen(isBoolean(evalIf(env)), (value) => value ? evalThen(env) : evalElse(env) ); }; return build; })(); const compile = (type, source) => R.map(parse(source), (node) => build(type, node)); const expect = (result, errorFormatter = defaultCompileErrorFormatter) => R.expect(result, errorFormatter); const run = ( evaluator, env, errorFormatter = defaultRuntimeErrorFormatter ) => R.expect(evaluator(env), errorFormatter); const interpret = ( type, source, env, parseErrorFormatter, runtimeErrorFormatter ) => run( expect(compile(type, source), parseErrorFormatter), env, runtimeErrorFormatter ); const defaultCompileErrorFormatter = (error) => { const tokenize = G.make(G.andThen(Lexer.whitespace, () => Lexer.unknown)); const sample = (source, position) => R.match( tokenize(source, position), ([token]) => token, () => undefined ); const formatTokenError = (error) => { const { context: { source, position }, cause: expected, } = error; const token = sample(source, position); const found = token !== undefined ? `"${token.text}"` : "no more tokens"; return `'${expected}' expected, but ${found} found`; }; const formatEoiError = (error) => { const { context: { source, position }, } = error; const token = sample(source, position); const found = `"${token.text}"`; return `end-of-input expected, but ${found} found`; }; switch (error?.type) { case "token": return formatTokenError(error); case "eoi": return formatEoiError(error); default: return `unknown error: ${S.debug(error)}`; } }; const defaultRuntimeErrorFormatter = (error) => { switch (error?.type) { case "reference": return `"${error.name}" not found`; case "property": return `"${error.property}" property not exists`; case "range": return `${error.index} is out of range`; case "type": return `'${error.expected}' expected, but ${S.debug( error.actual )} found`; case "security": return `<${error.target}> is not allowed for security reasons`; default: return `unknown error: ${S.debug(error)}`; } }; return { NUMBER, BOOLEAN, ANY, Lexer, parser, parse, build, compile, expect, run, interpret, defaultCompileErrorFormatter, defaultRuntimeErrorFormatter, }; })(); const P = (() => { const RE_INTEGER = /^[+\-]?\d+$/; const RE_NUMBER = /^[+\-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+\-]?\d+)?$/i; const formatError = (source, expected) => ({ type: "format", source, expected, }); const jsonError = (source, inner) => ({ type: "json", source, inner }); const expressionError = (source, cause) => ({ type: "expression", source, cause, }); const validationError = (source, cause) => ({ type: "validation", source, cause, }); const succeed = (value) => () => R.ok(value); const fail = (error) => () => R.err(error); const andThen = (parser, fn) => (s) => R.andThen(parser(s), (value) => fn(value)(s)); const orElse = (parser, fn) => (s) => R.orElse(parser(s), (error) => fn(error)(s)); const map = (parser, fn) => (s) => R.map(parser(s), fn); const mapError = (parser, fn) => (s) => R.mapErr(parser(s), fn); const wrapError = (parser, fn) => (s) => mapError(parser, (error) => fn(s, error))(s); const fromResult = (result) => R.match(result, succeed, fail); const withDefault = (parser, value) => orElse( map(empty, () => value), () => parser ); const validate = (parser, validator) => andThen(parser, (value) => wrapError(fromResult(validator(value)), validationError) ); const empty = (s) => s === "" ? R.ok(undefined) : R.err(formatError(s, "empty")); const integer = (s) => RE_INTEGER.test(s) ? R.ok(Number.parseInt(s, 10)) : R.err(formatError(s, "integer")); const number = (s) => RE_NUMBER.test(s) ? R.ok(Number.parseFloat(s)) : R.err(formatError(s, "number")); const string = (s) => R.ok(s); const boolean = (s) => s === "true" ? R.ok(true) : s === "false" ? R.ok(false) : R.err(formatError(s, "boolean")); const custom = (fn) => (s) => R.mapErr(fn(s), (expected) => formatError(s, expected)); const json = (s) => R.mapErr( R.attempt(() => JSON.parse(s)), (e) => jsonError(s, e) ); const array = (parser) => andThen( json, (value) => (s) => Array.isArray(value) ? R.allL(value.map((x) => () => parser(x))) : R.err(formatError(s, "array")) ); const struct = (parsers) => andThen( json, (value) => (s) => typeof value === "object" && value !== null && !Array.isArray(value) ? R.map(entries(parsers)(value), Object.fromEntries) : R.err(formatError(s, "struct")) ); const entries = (parsers) => (object) => R.allL(parsers.map((parser) => () => parser(object))); const entry = (key, parser) => (object) => map(parser, (value) => [key, value])(object[key] ?? ""); const expression = (type, errorFormatter) => (s) => R.match( E.compile(type, s), (evaluator) => R.ok((env) => E.run(evaluator, env, errorFormatter)), (cause) => R.err(expressionError(s, cause)) ); const make = (archetype) => { if (typeof archetype === "function") { return archetype; } else if (typeof archetype === "object" && archetype !== null) { if (Array.isArray(archetype)) { if (archetype.length === 1) { return array(make(archetype[0])); } else { return panic( `archetype array must be a singleton: ${S.debug(archetype)}` ); } } else { return struct( Object.entries(archetype).map(([key, value]) => entry(key, make(value)) ) ); } } else { return panic(`invalid archetype item: ${S.debug(archetype)}`); } }; const parse = (s, parser, errorFormatter = defaultErrorFormatter) => R.expect(parser(s), errorFormatter); const parseAll = (args, parsers, errorFormatter) => { return Object.fromEntries( Object.entries(parsers).map(([key, parser]) => { return [key, parse(args[key], parser, errorFormatter)]; }) ); }; const makeDefaultErrorFormatter = (validationErrorFormatter) => (error) => { const dots = (s) => S.ellipsis(s, 32); switch (error?.type) { case "format": return `'${error.expected}' expected, but "${dots( error.source )}" found`; case "json": return `failed to parse JSON; ${error.inner.message}`; case "expression": return `failed to parse expression; ${E.defaultCompileErrorFormatter( error.cause )}`; case "validation": return validationErrorFormatter(error.cause); default: return `unknown error: ${S.debug(error)}`; } }; const simpleErrorFormatter = (error) => typeof error === "string" ? error : S.debug(error); const defaultErrorFormatter = makeDefaultErrorFormatter(simpleErrorFormatter); return { succeed, fail, andThen, orElse, map, mapError, withDefault, validate, empty, integer, number, string, boolean, custom, json, array, struct, entry, expression, make, parse, parseAll, makeDefaultErrorFormatter, defaultErrorFormatter, }; })(); const N = (() => { const RE_SPACING = /^[ \r\n]*/; const RE_SPACES = /^[ \r\n]+/; const RE_NATURAL = /^\d+/; const RE_INTEGER = /^[+\-]?\d+/; const RE_NUMBER = /^[+\-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+\-]?\d+)?/i; const RE_BOOLEAN = /^(?:true|false)\b/; const RE_TEXT = /^(?:'(?:[^'`]|`.)*'|"(?:[^"`]|`.)*")/u; const symbolError = (source, position, symbol) => ({ type: "symbol", source, position, symbol, }); const regexpError = (source, position, name, regexp) => ({ type: "regexp", source, position, name, regexp, }); const expressionError = (source, position, cause) => ({ type: "expression", source, position, cause, }); const { succeed, fail } = G; const map = (parser, fn) => G.memo(G.map(parser, fn)); const mapError = (parser, fn) => G.memo(G.mapError(parser, fn)); const seqOf = (parsers) => G.memo(G.seqOf(parsers)); const oneOf = (parsers) => G.memo(G.oneOf(parsers)); const validate = (parser, validator) => G.memo(G.validate(parser, validator)); const symbol = (symbol) => G.memo( G.token((source, position) => { return source.startsWith(symbol, position) ? R.ok([symbol, position + symbol.length]) : R.err(symbolError(source, position, symbol)); }) ); const regexp = (name, re, fn) => G.memo( G.token((source, position) => { const slice = source.slice(position); const match = slice.match(re); if (match !== null && match.index === 0) { const token = match[0]; const value = fn !== undefined ? fn(...match) : token; return R.ok([value, position + token.length]); } else { return R.err(regexpError(source, position, name, re)); } }) ); const expression = (type, errorFormatter) => { const pipe = (a, fn) => fn(a); const parse = G.make(E.parser); const build = (node) => E.build(type, node); const make = (evaluator) => (env) => E.run(evaluator, env, errorFormatter); return G.memo( G.token((source, position) => R.mapBoth( parse(source, position), ([node, next]) => [pipe(build(node), make), next], (cause) => expressionError(source, position, cause) ) ) ); }; const spacing = regexp("spacing", RE_SPACING); const spaces = regexp("spaces", RE_SPACES); const natural = regexp("natural", RE_NATURAL, (s) => Number.parseInt(s, 10) ); const integer = regexp("integer", RE_INTEGER, (s) => Number.parseInt(s, 10) ); const number = regexp("number", RE_NUMBER, (s) => Number.parseFloat(s)); const boolean = regexp("boolean", RE_BOOLEAN, (s) => s === "true"); const text = regexp("text", RE_TEXT, (value) => value.slice(1, -1).replace(/`(.)/gu, "$1") ); const between = (parser, start, end) => map(G.seqOf([start, parser, end]), (value) => value[1]); const margin = (parser) => between(parser, spacing, spacing); const group = (parser, begin, end) => between(margin(parser), begin, end); const parens = (parser) => group(parser, symbol("("), symbol(")")); const braces = (parser) => group(parser, symbol("{"), symbol("}")); const brackets = (parser) => group(parser, symbol("["), symbol("]")); const endWith = (parser) => G.andThen(parser, (value) => G.map(G.eoi(), () => value)); const withDefault = (parser, value) => map(G.optional(parser), (option) => O.withDefault(option, value)); const chain = (item, delimiter) => withDefault(chain1(item, delimiter), []); const chain1 = (item, delimiter) => flatten(G.seqOf([item, G.many(G.seqOf([delimiter, item]))])); const flatten = (parser) => map(parser, ([first, rest]) => [first, ...rest.map(([, item]) => item)]); const join = (items, delimiter) => map(delimit(items, delimiter), (array) => array.filter((_, i) => i % 2 === 0) ); const delimit = (items, delimiter) => G.seqOf( items.flatMap((item, i) => (i === 0 ? [item] : [delimiter, item])) ); const list = (parser) => chain(parser, spaces); const tuple = (parsers) => join(parsers, spaces); const make = (parser) => G.mk(parser); const parse = (source, parser, errorFormatter = defaultErrorFormatter) => G.parse(source, parser, errorFormatter); const defaultTokenErrorFormatter = (error) => { const dots = (s) => S.ellipsis(s, 16); const rest = ({ source: s, position: i }) => s.length === i ? "no more characters" : `"${dots(s.slice(i))}"`; switch (error?.type) { case "symbol": return `'${error.symbol}' expected, but ${rest(error)} found`; case "regexp": return `'${error.name}' expected, but ${rest(error)} found`; case "expression": return E.defaultCompileErrorFormatter(error.cause); default: return `unknown error: ${S.debug(error)}`; } }; const makeDefaultErrorFormatter = (validationErrorFormatter) => G.makeDefaultErrorFormatter( defaultTokenErrorFormatter, validationErrorFormatter ); const simpleErrorFormatter = (error) => typeof error === "string" ? error : S.debug(error); const defaultErrorFormatter = makeDefaultErrorFormatter(simpleErrorFormatter); return { succeed, fail, map, mapError, seqOf, oneOf, validate, symbol, regexp, expression, spacing, spaces, natural, integer, number, boolean, text, margin, group, parens, braces, brackets, endWith, withDefault, chain, chain1, join, list, tuple, make, parse, defaultTokenErrorFormatter, makeDefaultErrorFormatter, defaultErrorFormatter, }; })(); const M = (() => { const notationError = (expected, name, value) => ({ type: "notation", expected, name, value, }); const attributeError = (name, source, cause) => ({ type: "attribute", name, source, cause, }); const flag = (name) => (meta) => { const v = meta[name]; switch (v) { case undefined: return R.ok(O.some(false)); case true: return R.ok(O.some(true)); default: return R.err(notationError("flag", name, v)); } }; const attr = (name, parser) => (meta) => { const v = meta[name]; if (typeof v === "string") { return R.mapErr(R.map(parser(v), O.some), (cause) => attributeError(name, v, cause) ); } else if (v === undefined) { return R.ok(O.none()); } else { return R.err(notationError("attr", name, v)); } }; const attrN = (name, parser) => attr(name, N.make(N.endWith(N.margin(parser)))); const succeed = (value) => () => R.ok(O.some(value)); const miss = () => () => R.ok(O.none()); const fail = (error) => () => R.err(error); const andThen = (parser, fn) => (data) => R.andThen(parser(data), (option) => O.match(option, fn, miss)(data)); const orElse = (parser, fn) => (data) => R.andThen(parser(data), (option) => O.match(option, succeed, fn)(data)); const map = (parser, fn) => andThen(parser, (value) => succeed(fn(value))); const mapError = (parser, fn) => (data) => R.mapErr(parser(data), fn); const withDefault = (parser, value) => orElse(parser, () => succeed(value)); const oneOf = (parsers) => parsers.reduceRight((acc, x) => orElse(x, () => acc), miss()); const make = (archetype) => { const arrayOf = (parsers) => parsers.reduce( (xs, x) => andThen(xs, (xs) => map(x, (x) => [...xs, x])), succeed([]) ); const structOf = (parsers) => map(arrayOf(parsers.map(entry)), Object.fromEntries); const entry = ([key, parser]) => map(parser, (value) => [key, value]); if (typeof archetype === "function") { return archetype; } else if (typeof archetype === "object" && archetype !== null) { if (Array.isArray(archetype)) { return arrayOf(archetype.map(make)); } else { return structOf( Object.entries(archetype).map(([key, value]) => [key, make(value)]) ); } } else { return () => R.ok(R.some(archetype)); } }; const parse = (meta, parser, errorFormatter = defaultErrorFormatter) => R.expect( R.map(parser(meta), (value) => O.withDefault(value, undefined)), errorFormatter ); const meta = (parser, errorFormatter) => U.memoW((meta) => parse(meta, parser, errorFormatter)); const makeDefaultErrorFormatter = (attributeErrorFormatter) => (error) => { switch (error?.type) { case "notation": switch (error.expected) { case "flag": return `'${error.name}' metadata does not require value`; case "attr": return `'${error.name}' metadata requires value`; default: return `unknown metadata type: ${error.expected}`; } case "attribute": const message = attributeErrorFormatter(error.cause); return `failed to parse '${error.name}' metadata value; ${message}`; default: return `unknown error: ${S.debug(error)}`; } }; const defaultErrorFormatter = makeDefaultErrorFormatter( N.defaultErrorFormatter ); return { flag, attr, attrN, succeed, miss, fail, andThen, orElse, map, mapError, withDefault, oneOf, make, parse, meta, makeDefaultErrorFormatter, defaultErrorFormatter, }; })(); const Z = (() => { const pluginName = () => document.currentScript?.src.match(/\/([^\/]*)\.js$/)?.[1]; const redef = (target, define) => { const table = {}; const base = (this_) => new Proxy(table, { get(target, p, receiver) { const value = Reflect.get(target, p, receiver); return typeof value !== "function" ? value : value.bind(this_); }, }); const definitions = define(base); const proto = Object.getPrototypeOf(target); for (const [key, value] of Object.entries(definitions)) { if (typeof value === "function") { if (target.hasOwnProperty(key)) { table[key] = target[key]; } else if (key in proto) { table[key] = function () { return proto[key].apply(this, arguments); }; } } target[key] = value; } }; const extProp = (defaultValue, nonWeak = false) => nonWeak ? propWithMap(defaultValue) : propWithWeakMap(defaultValue); const propWithMap = (defaultValue) => { const store = new Map(); const get = (key) => (store.has(key) ? store.get(key) : defaultValue); const set = (key, value) => void store.set(key, value); const delete_ = (key) => void store.delete(key); const clear = () => store.clear(); return { get, set, delete: delete_, clear }; }; const propWithWeakMap = (defaultValue) => { const store = new WeakMap(); const get = (key) => (store.has(key) ? store.get(key) : defaultValue); const set = (key, value) => void store.set(key, value); const delete_ = (key) => void store.delete(key); return { get, set, delete: delete_ }; }; const extend = (target, name, prop) => { const { get, set } = prop; Object.defineProperty(target, name, { get() { return get(this); }, set(value) { set(this, value); }, configurable: true, }); }; const swapper = (key) => (owner, value, block) => { const current = owner[key]; return enclose( () => (owner[key] = value), () => (owner[key] = current), block ); }; const context = (defaultValue) => { const { get: getContext, set: setContext } = propWithWeakMap(O.none()); const enter = (owner, value, block) => { const current = getContext(owner); return enclose( () => setContext(owner, O.some(value)), () => setContext(owner, current), block ); }; const value = (owner) => O.withDefault(getContext(owner), defaultValue); const exists = (owner) => O.isSome(getContext(owner)); return { enter, value, exists }; }; const defer = (cleanup) => (block) => { try { return block(); } finally { cleanup(); } }; const enclose = (begin, end, block) => { begin(); return defer(end)(block); }; return { pluginName, redef, extProp, extend, swapper, context, defer, enclose, }; })(); globalThis.Fs = { O, R, L, S, U, G, E, P, N, M, Z }; }