diff --git a/packages/parse/src/combinators/lookahead.ts b/packages/parse/src/combinators/lookahead.ts new file mode 100644 index 0000000000..7cd04034da --- /dev/null +++ b/packages/parse/src/combinators/lookahead.ts @@ -0,0 +1,47 @@ +import type { Parser } from "../api"; + +/** + * Repeatedly runs look-`ahead` and `main` parsers until the former + * succeeds or end of input is reached. + * + * @remarks + * Result of `ahead` parser will NOT be cosumed and on successful match + * the final read position will be at beginning of `ahead` pattern. If + * the `ahead` parser never succeeds, the entire parser fails and any + * partial matches are discarded. + * + * @example + * ```ts + * const ctx = defContext("ababaaabbabba"); + * + * // consume while 'a' or `b` until 1st occurrence of "abba" + * join(lookahead(oneOf("ab"), stringD("abba")))(ctx) + * // true + * + * ctx.result + * // 'ababaa' + * + * ctx.state + * // { p: 6, l: 1, c: 7, done: false, last: 'a' } + * ``` + * + * @param parser + * @param ahead + * @param id + */ +export const lookahead = ( + parser: Parser, + ahead: Parser, + id = "lookahead" +): Parser => (ctx) => { + if (ctx.done) return false; + ctx.start(id); + while (true) { + const state = { ...ctx.state }; + if (ahead(ctx)) { + ctx.state = state; + return ctx.end(); + } + if (!parser(ctx)) return ctx.discard(); + } +}; diff --git a/packages/parse/src/index.ts b/packages/parse/src/index.ts index 8463384ee5..6789af3478 100644 --- a/packages/parse/src/index.ts +++ b/packages/parse/src/index.ts @@ -8,6 +8,7 @@ export * from "./combinators/boundary"; export * from "./combinators/check"; export * from "./combinators/dynamic"; export * from "./combinators/expect"; +export * from "./combinators/lookahead"; export * from "./combinators/maybe"; export * from "./combinators/not"; export * from "./combinators/repeat"; diff --git a/packages/parse/test/lookahead.ts b/packages/parse/test/lookahead.ts new file mode 100644 index 0000000000..312384e7d5 --- /dev/null +++ b/packages/parse/test/lookahead.ts @@ -0,0 +1,32 @@ +import * as assert from "assert"; +import { defContext, join, lookahead, oneOf, stringD, string } from "../src"; + +describe("lookahead", () => { + it("oneof", () => { + const ctx = defContext("ababaaabbabba"); + assert(join(lookahead(oneOf("ab"), stringD("abba")))(ctx)); + assert.equal(ctx.result, "ababaa"); + assert.deepEqual(ctx.state, { + p: 6, + l: 1, + c: 7, + done: false, + last: "a", + }); + assert(string("abba")(ctx)); + }); + + it("string", () => { + const ctx = defContext("abababbabba"); + assert(join(lookahead(string("ab"), stringD("abba")))(ctx)); + assert.equal(ctx.result, "abab"); + assert.deepEqual(ctx.state, { + p: 4, + l: 1, + c: 5, + done: false, + last: "b", + }); + assert(string("abba")(ctx)); + }); +});