@thi.ng/parse
Version:
Purely functional parser combinators & AST generation for generic inputs
160 lines (159 loc) • 4.13 kB
JavaScript
import { isArrayLike } from "@thi.ng/checks/is-arraylike";
import { isString } from "@thi.ng/checks/is-string";
import { parseError } from "./error.js";
import { defArrayReader } from "./readers/array-reader.js";
import { defStringReader } from "./readers/string-reader.js";
import { __indent } from "./utils.js";
class ParseState {
constructor(p, l, c, done) {
this.p = p;
this.l = l;
this.c = c;
this.done = done;
}
copy() {
return new ParseState(this.p, this.l, this.c, this.done);
}
}
class ParseScope {
constructor(id, state, children, result) {
this.id = id;
this.state = state;
this.children = children;
this.result = result;
}
copy() {
return new ParseScope(
this.id,
this.state,
this.children,
this.result
);
}
}
class ParseContext {
constructor(reader, opts) {
this.reader = reader;
this.opts = { maxDepth: 64, debug: false, retain: false, ...opts };
this._maxDepth = this.opts.maxDepth;
this._debug = this.opts.debug;
this._retain = this.opts.retain;
this.reset();
}
opts;
_scopes;
_curr;
_maxDepth;
_peakDepth;
_debug;
_retain;
reset() {
this._curr = new ParseScope("root", new ParseState(0, 1, 1));
this._scopes = [this._curr];
this._peakDepth = 1;
this.reader.isDone(this._curr.state);
return this;
}
start(id) {
const { _scopes: scopes, _maxDepth } = this;
const num = scopes.length;
if (num >= _maxDepth) {
parseError(this, `recursion limit reached ${_maxDepth}`);
}
const scope = new ParseScope(id, scopes[num - 1].state.copy());
this._peakDepth = Math.max(this._peakDepth, scopes.push(scope));
this._debug && console.log(
`${__indent(scopes.length)}start: ${id} (${scope.state.p})`
);
return this._curr = scope;
}
discard() {
const scopes = this._scopes;
const child = scopes.pop();
this._curr = scopes[scopes.length - 1];
this._debug && console.log(`${__indent(scopes.length + 1)}discard: ${child.id}`);
return false;
}
end() {
const scopes = this._scopes;
const child = scopes.pop();
const parent = scopes[scopes.length - 1];
const cstate = child.state;
this._debug && console.log(
`${__indent(scopes.length + 1)}end: ${child.id} (${cstate.p})`
);
child.state = this._retain ? parent.state.copy() : null;
parent.state = cstate;
parent.children?.push(child) ?? (parent.children = [child]);
this._curr = parent;
return true;
}
addChild(id, result = null, newState = false) {
const curr = this._curr;
const child = new ParseScope(
id,
this._retain ? curr.state.copy() : null,
null,
result
);
this._debug && console.log(
`${__indent(this._scopes.length + 1)}addChild: ${id} (${curr.state.p})`
);
curr.children?.push(child) ?? (curr.children = [child]);
if (newState !== false) {
newState === true ? this.reader.next(curr.state) : this._curr.state = newState;
}
return true;
}
get scope() {
return this._curr;
}
get state() {
return this._curr.state;
}
set state(state) {
this._curr.state = state;
}
get done() {
return this._curr.state.done;
}
/**
* Returns root node.
*/
get root() {
return this._scopes[0];
}
/**
* Returns root node's `result` or `undefined`.
*/
get result() {
const children = this.root.children;
return children ? children[0].result : void 0;
}
/**
* Returns root node's children or `undefined`.
*/
get children() {
const children = this.root.children;
return children ? children[0].children : void 0;
}
/**
* Returns max. recursion depth which was actually reached. Will always be
* less or equal configured {@link ContextOpts.maxDepth}.
*/
get peakDepth() {
return this._peakDepth;
}
}
function defContext(input, opts) {
return new ParseContext(
isString(input) ? defStringReader(input) : isArrayLike(input) ? defArrayReader(input) : input,
opts
);
}
export {
ParseContext,
ParseScope,
ParseState,
defContext
};