1325 lines
No EOL
50 KiB
JavaScript
1325 lines
No EOL
50 KiB
JavaScript
/*:
|
|
* @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 "<unknown>";
|
|
}
|
|
};
|
|
|
|
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 };
|
|
}; |