diff --git a/docs/content/.vitepress/config.ts b/docs/content/.vitepress/config.ts index cf5b90e..069ad7d 100644 --- a/docs/content/.vitepress/config.ts +++ b/docs/content/.vitepress/config.ts @@ -143,6 +143,7 @@ function getSidebar() { Sidebar.item('chainl', '/chainl'), Sidebar.item('choice', '/choice'), Sidebar.item('error', '/error'), + Sidebar.item('lookahead', '/lookahead'), Sidebar.item('many', '/many'), Sidebar.item('many1', '/many1'), Sidebar.item('map', '/map'), diff --git a/docs/content/combinators/lookahead.md b/docs/content/combinators/lookahead.md new file mode 100644 index 0000000..5d884bb --- /dev/null +++ b/docs/content/combinators/lookahead.md @@ -0,0 +1,65 @@ +--- +title: 'lookahead' +kind: 'primitive' +description: "lookahead combinator applies parser without consuming any input. If parser fails and consumes some input, so does lookahead." +--- + +# {{ $frontmatter.title }} + +## Signature + +```ts +function lookahead(parser: Parser): Parser +``` + +## Description + +`lookahead` combinator applies `parser` without consuming any input. If `parser` fails and consumes some input, so does `lookahead`. + +## Usage + +The example is rather contrived, but it clearly illustrates how the combinator works, allowing one, for example, collect ambiguous results for further processing. + +```ts +const Parser = sequence( + takeLeft(string('hello'), whitespace()), + lookahead(string('let')), + string('lettuce') +) +``` + +::: tip Success +```ts +run(Parser).with('hello lettuce') + +{ + isOk: true, + pos: 13, + value: [ 'hello', 'let', 'lettuce' ] +} +``` +::: + +::: danger Failure +```ts +run(Parser).with('hello let') + +{ + isOk: false, + pos: 9, + expected: 'lettuce' +} +``` +::: + +::: danger Failure +```ts +run(Parser).with('hello something') + +{ + isOk: false, + pos: 9, + expected: 'let' +} +``` +::: diff --git a/src/__tests__/@helpers/index.ts b/src/__tests__/@helpers/index.ts index a41f551..419b94c 100644 --- a/src/__tests__/@helpers/index.ts +++ b/src/__tests__/@helpers/index.ts @@ -39,6 +39,10 @@ export const should = { expect(a, message).toBe(b) }, + beStrictEqual(a: T, b: T, message?: string) { + expect(a, message).toStrictEqual(b) + }, + throw(f: () => void) { expect(f).toThrow() }, @@ -66,6 +70,7 @@ export const expectedCombinators = [ 'chainl', 'choice', 'error', + 'lookahead', 'many', 'many1', 'map', diff --git a/src/__tests__/combinators/lookahead.spec.ts b/src/__tests__/combinators/lookahead.spec.ts new file mode 100644 index 0000000..b676e31 --- /dev/null +++ b/src/__tests__/combinators/lookahead.spec.ts @@ -0,0 +1,41 @@ +import { lookahead, sequence, takeLeft } from '@combinators' +import { string, whitespace } from '@parsers' +import { run, should, describe, it } from '@testing' + +describe('lookahead', () => { + const parser = sequence( + takeLeft(string('hello'), whitespace()), + lookahead(string('let')), + string('lettuce') + ) + + it('should successfully lookahead and return pos untouched', () => { + const actual = run(parser, 'hello lettuce') + + should.beStrictEqual(actual, { + isOk: true, + pos: 13, + value: ['hello', 'let', 'lettuce'] + }) + }) + + it('should correctly fail if placed before a failing parser (OOB check)', () => { + const actual = run(parser, 'hello let') + + should.beStrictEqual(actual, { + isOk: false, + pos: 9, + expected: 'lettuce' + }) + }) + + it('should correctly fail if given a failing parser (consuming check)', () => { + const actual = run(parser, 'hello const') + + should.beStrictEqual(actual, { + isOk: false, + pos: 9, + expected: 'let' + }) + }) +}) diff --git a/src/combinators.ts b/src/combinators.ts index a5ec752..bb48453 100644 --- a/src/combinators.ts +++ b/src/combinators.ts @@ -1,6 +1,7 @@ export * from '@combinators/chain' export * from '@combinators/choice' export * from '@combinators/error' +export * from '@combinators/lookahead' export * from '@combinators/many' export * from '@combinators/map' export * from '@combinators/optional' diff --git a/src/combinators/lookahead.ts b/src/combinators/lookahead.ts new file mode 100644 index 0000000..1653462 --- /dev/null +++ b/src/combinators/lookahead.ts @@ -0,0 +1,33 @@ +import type { Parser } from '@types' + +/** + * Applies `parser` without consuming any input. If `parser` fails and consumes some input, so does + * `lookahead`. + * + * @param parser - Parser to apply + * + * @returns Result of `parser` + */ +export function lookahead(parser: Parser): Parser { + return { + parse(input, pos) { + const result = parser.parse(input, pos) + + switch (result.isOk) { + // If parser succeeded, keep the position untouched. + case true: { + return { + isOk: true, + pos, + value: result.value + } + } + + // If the parser failed, then still advance the pos cursor. + case false: { + return result + } + } + } + } +}