@thi.ng/sexpr
Version:
Extensible S-Expression parser & runtime infrastructure
61 lines (60 loc) • 1.75 kB
JavaScript
import { isString } from "@thi.ng/checks/is-string";
import { unescape } from "@thi.ng/strings/escape";
import {
DEFAULT_SYNTAX
} from "./api.js";
import { tokenize } from "./tokenize.js";
class ParseError extends Error {
line;
col;
constructor(msg, line, col) {
super(msg);
this.line = line;
this.col = col;
}
}
const parse = (src, opts) => {
const { scopes } = { ...DEFAULT_SYNTAX, ...opts };
const scopeOpen = scopes.map((x) => x[0]);
const scopeClose = scopes.map((x) => x[1]);
const tree = [{ type: "root", children: [] }];
let currScope = -1;
for (let token of isString(src) ? tokenize(src, opts) : src) {
const t = token.value;
let tmp;
if ((tmp = scopeOpen.indexOf(t)) !== -1) {
tree.push({ type: "expr", value: t, children: [] });
currScope = tmp;
} else if ((tmp = scopeClose.indexOf(t)) !== -1) {
if (tree.length < 2 || currScope !== tmp) {
throw new ParseError(`unmatched '${t}'`, token.line, token.col);
}
tree[tree.length - 2].children.push(tree.pop());
currScope = scopeOpen.indexOf(
tree[tree.length - 1].value
);
} else {
let node;
let value;
if (t.startsWith('"')) {
node = {
type: "str",
value: unescape(t.substring(1, t.length - 1))
};
} else if (t.startsWith("0x") && !isNaN(value = parseInt(t.substring(2), 16)) || !isNaN(value = parseFloat(t))) {
node = { type: "num", value };
} else {
node = { type: "sym", value: t };
}
tree[tree.length - 1].children.push(node);
}
}
if (tree.length > 1) {
throw new ParseError("unclosed s-expression", -1, -1);
}
return tree[0];
};
export {
ParseError,
parse
};