diff --git a/api-extractor.json b/api-extractor.json index a403a04..20b08f5 100644 --- a/api-extractor.json +++ b/api-extractor.json @@ -1,7 +1,7 @@ { "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", - "mainEntryPointFilePath": "/lib/parser-combinators.d.ts", + "mainEntryPointFilePath": "/lib/index.d.ts", "apiReport": { "enabled": true, diff --git a/api/prsc.api.json b/api/prsc.api.json index a46f7ac..7f13702 100644 --- a/api/prsc.api.json +++ b/api/prsc.api.json @@ -169,6 +169,78 @@ "canonicalReference": "prsc!", "name": "", "members": [ + { + "kind": "Function", + "canonicalReference": "prsc!collect:function(1)", + "docComment": "/**\n * Helper to collect both the yielded values and the returned value from a generator.\n *\n * @param gen - Generator to collect from\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare function collect(gen: " + }, + { + "kind": "Reference", + "text": "Generator", + "canonicalReference": "!Generator:interface" + }, + { + "kind": "Content", + "text": "" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Content", + "text": "[T[], R]" + }, + { + "kind": "Content", + "text": ";" + } + ], + "returnTypeTokenRange": { + "startIndex": 4, + "endIndex": 5 + }, + "releaseTag": "Public", + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "gen", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 3 + } + } + ], + "typeParameters": [ + { + "typeParameterName": "T", + "constraintTokenRange": { + "startIndex": 0, + "endIndex": 0 + }, + "defaultTypeTokenRange": { + "startIndex": 0, + "endIndex": 0 + } + }, + { + "typeParameterName": "R", + "constraintTokenRange": { + "startIndex": 0, + "endIndex": 0 + }, + "defaultTypeTokenRange": { + "startIndex": 0, + "endIndex": 0 + } + } + ], + "name": "collect" + }, { "kind": "Function", "canonicalReference": "prsc!complete:function(1)", @@ -2174,6 +2246,489 @@ "endIndex": 3 } }, + { + "kind": "Function", + "canonicalReference": "prsc!streaming:function(1)", + "docComment": "/**\n * Creates a StreamingParser which applies the given Parser and yields the value produced if it matches.\n *\n * @param parser - The Parser to apply\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare function streaming(parser: " + }, + { + "kind": "Reference", + "text": "Parser", + "canonicalReference": "prsc!Parser:type" + }, + { + "kind": "Content", + "text": "" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Reference", + "text": "StreamingParser", + "canonicalReference": "prsc!StreamingParser:type" + }, + { + "kind": "Content", + "text": "" + }, + { + "kind": "Content", + "text": ";" + } + ], + "returnTypeTokenRange": { + "startIndex": 4, + "endIndex": 6 + }, + "releaseTag": "Public", + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "parser", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 3 + } + } + ], + "typeParameters": [ + { + "typeParameterName": "T", + "constraintTokenRange": { + "startIndex": 0, + "endIndex": 0 + }, + "defaultTypeTokenRange": { + "startIndex": 0, + "endIndex": 0 + } + } + ], + "name": "streaming" + }, + { + "kind": "Function", + "canonicalReference": "prsc!streamingComplete:function(1)", + "docComment": "/**\n * Creates a StreamingParser that applies the given parser and directly yields values produced by it, and then only succeeds if parsing concludes at the end of the input string.\n *\n * @param parser - StreamingParser to apply\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare function streamingComplete(parser: " + }, + { + "kind": "Reference", + "text": "StreamingParser", + "canonicalReference": "prsc!StreamingParser:type" + }, + { + "kind": "Content", + "text": "" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Reference", + "text": "StreamingParser", + "canonicalReference": "prsc!StreamingParser:type" + }, + { + "kind": "Content", + "text": "" + }, + { + "kind": "Content", + "text": ";" + } + ], + "returnTypeTokenRange": { + "startIndex": 4, + "endIndex": 6 + }, + "releaseTag": "Public", + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "parser", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 3 + } + } + ], + "typeParameters": [ + { + "typeParameterName": "T", + "constraintTokenRange": { + "startIndex": 0, + "endIndex": 0 + }, + "defaultTypeTokenRange": { + "startIndex": 0, + "endIndex": 0 + } + } + ], + "name": "streamingComplete" + }, + { + "kind": "Function", + "canonicalReference": "prsc!streamingFilterUndefined:function(1)", + "docComment": "/**\n * Creates a StreamingParser which discards undefined values yielded by the given StreamingParser.\n *\n * @param parser - The StreamingParser to filter\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare function streamingFilterUndefined(parser: " + }, + { + "kind": "Reference", + "text": "StreamingParser", + "canonicalReference": "prsc!StreamingParser:type" + }, + { + "kind": "Content", + "text": "" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Reference", + "text": "StreamingParser", + "canonicalReference": "prsc!StreamingParser:type" + }, + { + "kind": "Content", + "text": "" + }, + { + "kind": "Content", + "text": ";" + } + ], + "returnTypeTokenRange": { + "startIndex": 4, + "endIndex": 6 + }, + "releaseTag": "Public", + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "parser", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 3 + } + } + ], + "typeParameters": [ + { + "typeParameterName": "T", + "constraintTokenRange": { + "startIndex": 0, + "endIndex": 0 + }, + "defaultTypeTokenRange": { + "startIndex": 0, + "endIndex": 0 + } + } + ], + "name": "streamingFilterUndefined" + }, + { + "kind": "Function", + "canonicalReference": "prsc!streamingOptional:function(1)", + "docComment": "/**\n * Creates a StreamingParser that tries to apply the given parser optionally. It only yields the values produced by the inner parser if it matches successfully, and does not yield anything otherwise.\n *\n * @param parser - StreamingParser to attempt to apply\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare function streamingOptional(parser: " + }, + { + "kind": "Reference", + "text": "StreamingParser", + "canonicalReference": "prsc!StreamingParser:type" + }, + { + "kind": "Content", + "text": "" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Reference", + "text": "StreamingParser", + "canonicalReference": "prsc!StreamingParser:type" + }, + { + "kind": "Content", + "text": "" + }, + { + "kind": "Content", + "text": ";" + } + ], + "returnTypeTokenRange": { + "startIndex": 4, + "endIndex": 6 + }, + "releaseTag": "Public", + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "parser", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 3 + } + } + ], + "typeParameters": [ + { + "typeParameterName": "T", + "constraintTokenRange": { + "startIndex": 0, + "endIndex": 0 + }, + "defaultTypeTokenRange": { + "startIndex": 0, + "endIndex": 0 + } + } + ], + "name": "streamingOptional" + }, + { + "kind": "TypeAlias", + "canonicalReference": "prsc!StreamingParser:type", + "docComment": "/**\n * A StreamingParser is similar to a Parser, but instead of returning a value when parsing is complete it can parse incrementally and yield values as they are produced. The generator returns a ParseResult when iteration is done which indicates whether parsing was successful.\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare type StreamingParser = " + }, + { + "kind": "Content", + "text": "(input: string, offset: number) => " + }, + { + "kind": "Reference", + "text": "Generator", + "canonicalReference": "!Generator:interface" + }, + { + "kind": "Content", + "text": ">" + }, + { + "kind": "Content", + "text": ";" + } + ], + "releaseTag": "Public", + "name": "StreamingParser", + "typeParameters": [ + { + "typeParameterName": "T", + "constraintTokenRange": { + "startIndex": 0, + "endIndex": 0 + }, + "defaultTypeTokenRange": { + "startIndex": 0, + "endIndex": 0 + } + } + ], + "typeTokenRange": { + "startIndex": 1, + "endIndex": 6 + } + }, + { + "kind": "Function", + "canonicalReference": "prsc!streamingStar:function(1)", + "docComment": "/**\n * Creates a StreamingParser that tries to apply the given StreamingParser zero or more times in sequence. Values produced during each iteration are only yielded whenever the inner parser matches successfully.\n *\n * @param parser - StreamingParser to apply repeatedly\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare function streamingStar(parser: " + }, + { + "kind": "Reference", + "text": "StreamingParser", + "canonicalReference": "prsc!StreamingParser:type" + }, + { + "kind": "Content", + "text": "" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Reference", + "text": "StreamingParser", + "canonicalReference": "prsc!StreamingParser:type" + }, + { + "kind": "Content", + "text": "" + }, + { + "kind": "Content", + "text": ";" + } + ], + "returnTypeTokenRange": { + "startIndex": 4, + "endIndex": 6 + }, + "releaseTag": "Public", + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "parser", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 3 + } + } + ], + "typeParameters": [ + { + "typeParameterName": "T", + "constraintTokenRange": { + "startIndex": 0, + "endIndex": 0 + }, + "defaultTypeTokenRange": { + "startIndex": 0, + "endIndex": 0 + } + } + ], + "name": "streamingStar" + }, + { + "kind": "Function", + "canonicalReference": "prsc!streamingThen:function(1)", + "docComment": "/**\n * Creates a StreamingParser which applies the given two StreamingParsers in sequence.\n *\n * Unlike `then`, this does not combine values using a function, but instead simply yields the values produced by both parsers as they produce them.\n *\n * @param parser1 - First StreamingParser to apply\n *\n * @param parser2 - StreamingParser to apply if the first one is successful\n *\n * @public\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "export declare function streamingThen(parser1: " + }, + { + "kind": "Reference", + "text": "StreamingParser", + "canonicalReference": "prsc!StreamingParser:type" + }, + { + "kind": "Content", + "text": "" + }, + { + "kind": "Content", + "text": ", parser2: " + }, + { + "kind": "Reference", + "text": "StreamingParser", + "canonicalReference": "prsc!StreamingParser:type" + }, + { + "kind": "Content", + "text": "" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Reference", + "text": "StreamingParser", + "canonicalReference": "prsc!StreamingParser:type" + }, + { + "kind": "Content", + "text": "" + }, + { + "kind": "Content", + "text": ";" + } + ], + "returnTypeTokenRange": { + "startIndex": 7, + "endIndex": 9 + }, + "releaseTag": "Public", + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "parser1", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 3 + } + }, + { + "parameterName": "parser2", + "parameterTypeTokenRange": { + "startIndex": 4, + "endIndex": 6 + } + } + ], + "typeParameters": [ + { + "typeParameterName": "T", + "constraintTokenRange": { + "startIndex": 0, + "endIndex": 0 + }, + "defaultTypeTokenRange": { + "startIndex": 0, + "endIndex": 0 + } + }, + { + "typeParameterName": "U", + "constraintTokenRange": { + "startIndex": 0, + "endIndex": 0 + }, + "defaultTypeTokenRange": { + "startIndex": 0, + "endIndex": 0 + } + } + ], + "name": "streamingThen" + }, { "kind": "Function", "canonicalReference": "prsc!then:function(1)", diff --git a/api/prsc.api.md b/api/prsc.api.md index 570ef4a..f12768a 100644 --- a/api/prsc.api.md +++ b/api/prsc.api.md @@ -4,6 +4,9 @@ ```ts +// @public +export function collect(gen: Generator): [T[], R]; + // @public export function complete(parser: Parser): Parser; @@ -97,6 +100,27 @@ export function star(parser: Parser): Parser; // @public export const start: Parser; +// @public +export function streaming(parser: Parser): StreamingParser; + +// @public +export function streamingComplete(parser: StreamingParser): StreamingParser; + +// @public +export function streamingFilterUndefined(parser: StreamingParser): StreamingParser; + +// @public +export function streamingOptional(parser: StreamingParser): StreamingParser; + +// @public +export type StreamingParser = (input: string, offset: number) => Generator>; + +// @public +export function streamingStar(parser: StreamingParser): StreamingParser; + +// @public +export function streamingThen(parser1: StreamingParser, parser2: StreamingParser): StreamingParser; + // @public export function then(parser1: Parser, parser2: Parser, join: (value1: T1, value2: T2) => T): Parser; diff --git a/docs/prsc.collect.md b/docs/prsc.collect.md new file mode 100644 index 0000000..2cf3006 --- /dev/null +++ b/docs/prsc.collect.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [prsc](./prsc.md) > [collect](./prsc.collect.md) + +## collect() function + +Helper to collect both the yielded values and the returned value from a generator. + +Signature: + +```typescript +export declare function collect(gen: Generator): [T[], R]; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| gen | Generator<T, R> | Generator to collect from | + +Returns: + +\[T\[\], R\] + diff --git a/docs/prsc.md b/docs/prsc.md index f914c63..1131d96 100644 --- a/docs/prsc.md +++ b/docs/prsc.md @@ -8,6 +8,7 @@ | Function | Description | | --- | --- | +| [collect(gen)](./prsc.collect.md) | Helper to collect both the yielded values and the returned value from a generator. | | [complete(parser)](./prsc.complete.md) | Creates a Parser that applies the given parser and only succeeds (returning the inner parser's result) if parsing concludes at the end of the input string. | | [consume(parser)](./prsc.consume.md) | Creates a Parser that applies the given parser but discards the resulting value. | | [cut(parser)](./prsc.cut.md) | Creates a Parser that turns errors returned by the inner parser into fatal errors. Parsers such as or and star will not continue to attempt additional matches if a parser returns a fatal error, and will usually return the error instead. | @@ -39,6 +40,12 @@ const as: Parser = filterUndefined(abs); | [second(x, y)](./prsc.second.md) | Returns the second of the given two arguments. Useful as a join function for then. See also preceded. | | [skipChars(nCodepoints)](./prsc.skipchars.md) | Creates a Parser that skips the given number of characters.This counts in unicode characters (code points), not UTF-16 code units. | | [star(parser)](./prsc.star.md) | Creates a Parser that tries to apply the given parser zero or more times in sequence. Values for successful matches are collected in an array. Once the inner parser no longer matches, success is returned at the offset reached with the accumulated values.If the inner parser returns a fatal failure, the error is returned as-is. | +| [streaming(parser)](./prsc.streaming.md) | Creates a StreamingParser which applies the given Parser and yields the value produced if it matches. | +| [streamingComplete(parser)](./prsc.streamingcomplete.md) | Creates a StreamingParser that applies the given parser and directly yields values produced by it, and then only succeeds if parsing concludes at the end of the input string. | +| [streamingFilterUndefined(parser)](./prsc.streamingfilterundefined.md) | Creates a StreamingParser which discards undefined values yielded by the given StreamingParser. | +| [streamingOptional(parser)](./prsc.streamingoptional.md) | Creates a StreamingParser that tries to apply the given parser optionally. It only yields the values produced by the inner parser if it matches successfully, and does not yield anything otherwise. | +| [streamingStar(parser)](./prsc.streamingstar.md) | Creates a StreamingParser that tries to apply the given StreamingParser zero or more times in sequence. Values produced during each iteration are only yielded whenever the inner parser matches successfully. | +| [streamingThen(parser1, parser2)](./prsc.streamingthen.md) | Creates a StreamingParser which applies the given two StreamingParsers in sequence.Unlike then, this does not combine values using a function, but instead simply yields the values produced by both parsers as they produce them. | | [then(parser1, parser2, join)](./prsc.then.md) | Creates a Parser that applies the given two parsers in sequence, returning success only if both succeed. The given join function is used to combine the values from both parsers into the single value to return. If either parser fails, the failure is returned as-is. | | [token(token)](./prsc.token.md) | Creates a Parser that matches the given string. | @@ -55,4 +62,5 @@ const as: Parser = filterUndefined(abs); | --- | --- | | [Parser](./prsc.parser.md) | A parser is a function that tries to match whatever it expects at the given offset in the input string. Returns a ParseResult. | | [ParseResult](./prsc.parseresult.md) | The result of parsing - either success (with an offset at which to resume parsing the next thing) or failure. If a failure is fatal, parsing should not continue to try alternative options.A ParseResult may contain a value that represents the parsed input. | +| [StreamingParser](./prsc.streamingparser.md) | A StreamingParser is similar to a Parser, but instead of returning a value when parsing is complete it can parse incrementally and yield values as they are produced. The generator returns a ParseResult when iteration is done which indicates whether parsing was successful. | diff --git a/docs/prsc.streaming.md b/docs/prsc.streaming.md new file mode 100644 index 0000000..b841cc1 --- /dev/null +++ b/docs/prsc.streaming.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [prsc](./prsc.md) > [streaming](./prsc.streaming.md) + +## streaming() function + +Creates a StreamingParser which applies the given Parser and yields the value produced if it matches. + +Signature: + +```typescript +export declare function streaming(parser: Parser): StreamingParser; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| parser | [Parser](./prsc.parser.md)<T> | The Parser to apply | + +Returns: + +[StreamingParser](./prsc.streamingparser.md)<T> + diff --git a/docs/prsc.streamingcomplete.md b/docs/prsc.streamingcomplete.md new file mode 100644 index 0000000..f0e13bb --- /dev/null +++ b/docs/prsc.streamingcomplete.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [prsc](./prsc.md) > [streamingComplete](./prsc.streamingcomplete.md) + +## streamingComplete() function + +Creates a StreamingParser that applies the given parser and directly yields values produced by it, and then only succeeds if parsing concludes at the end of the input string. + +Signature: + +```typescript +export declare function streamingComplete(parser: StreamingParser): StreamingParser; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| parser | [StreamingParser](./prsc.streamingparser.md)<T> | StreamingParser to apply | + +Returns: + +[StreamingParser](./prsc.streamingparser.md)<T> + diff --git a/docs/prsc.streamingfilterundefined.md b/docs/prsc.streamingfilterundefined.md new file mode 100644 index 0000000..0bbc38f --- /dev/null +++ b/docs/prsc.streamingfilterundefined.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [prsc](./prsc.md) > [streamingFilterUndefined](./prsc.streamingfilterundefined.md) + +## streamingFilterUndefined() function + +Creates a StreamingParser which discards undefined values yielded by the given StreamingParser. + +Signature: + +```typescript +export declare function streamingFilterUndefined(parser: StreamingParser): StreamingParser; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| parser | [StreamingParser](./prsc.streamingparser.md)<T \| void> | The StreamingParser to filter | + +Returns: + +[StreamingParser](./prsc.streamingparser.md)<T> + diff --git a/docs/prsc.streamingoptional.md b/docs/prsc.streamingoptional.md new file mode 100644 index 0000000..d85083e --- /dev/null +++ b/docs/prsc.streamingoptional.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [prsc](./prsc.md) > [streamingOptional](./prsc.streamingoptional.md) + +## streamingOptional() function + +Creates a StreamingParser that tries to apply the given parser optionally. It only yields the values produced by the inner parser if it matches successfully, and does not yield anything otherwise. + +Signature: + +```typescript +export declare function streamingOptional(parser: StreamingParser): StreamingParser; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| parser | [StreamingParser](./prsc.streamingparser.md)<T> | StreamingParser to attempt to apply | + +Returns: + +[StreamingParser](./prsc.streamingparser.md)<T> + diff --git a/docs/prsc.streamingparser.md b/docs/prsc.streamingparser.md new file mode 100644 index 0000000..73720bd --- /dev/null +++ b/docs/prsc.streamingparser.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [prsc](./prsc.md) > [StreamingParser](./prsc.streamingparser.md) + +## StreamingParser type + +A StreamingParser is similar to a Parser, but instead of returning a value when parsing is complete it can parse incrementally and yield values as they are produced. The generator returns a ParseResult when iteration is done which indicates whether parsing was successful. + +Signature: + +```typescript +export declare type StreamingParser = (input: string, offset: number) => Generator>; +``` +References: [ParseResult](./prsc.parseresult.md) + diff --git a/docs/prsc.streamingstar.md b/docs/prsc.streamingstar.md new file mode 100644 index 0000000..d77dc5c --- /dev/null +++ b/docs/prsc.streamingstar.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [prsc](./prsc.md) > [streamingStar](./prsc.streamingstar.md) + +## streamingStar() function + +Creates a StreamingParser that tries to apply the given StreamingParser zero or more times in sequence. Values produced during each iteration are only yielded whenever the inner parser matches successfully. + +Signature: + +```typescript +export declare function streamingStar(parser: StreamingParser): StreamingParser; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| parser | [StreamingParser](./prsc.streamingparser.md)<T> | StreamingParser to apply repeatedly | + +Returns: + +[StreamingParser](./prsc.streamingparser.md)<T> + diff --git a/docs/prsc.streamingthen.md b/docs/prsc.streamingthen.md new file mode 100644 index 0000000..f5828f3 --- /dev/null +++ b/docs/prsc.streamingthen.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [prsc](./prsc.md) > [streamingThen](./prsc.streamingthen.md) + +## streamingThen() function + +Creates a StreamingParser which applies the given two StreamingParsers in sequence. + +Unlike `then`, this does not combine values using a function, but instead simply yields the values produced by both parsers as they produce them. + +Signature: + +```typescript +export declare function streamingThen(parser1: StreamingParser, parser2: StreamingParser): StreamingParser; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| parser1 | [StreamingParser](./prsc.streamingparser.md)<T> | First StreamingParser to apply | +| parser2 | [StreamingParser](./prsc.streamingparser.md)<U> | StreamingParser to apply if the first one is successful | + +Returns: + +[StreamingParser](./prsc.streamingparser.md)<T \| U> + diff --git a/package.json b/package.json index 70f8913..a176bb7 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,13 @@ ], "main": "dist/prsc.umd.js", "module": "dist/prsc.esm.js", + "exports": { + ".": { + "require": "./dist/prsc.umd.js", + "import": "./dist/prsc.esm.js", + "default": "./dist/prsc.esm.js" + } + }, "types": "dist/prsc.d.ts", "scripts": { "build:clean": "rimraf dist && rimraf lib && rimraf temp", diff --git a/rollup.config.js b/rollup.config.js index 26d4fd0..94f2a57 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -4,19 +4,16 @@ import { terser } from 'rollup-plugin-terser'; const { main: MAIN_DEST_FILE, module: MODULE_DEST_FILE } = require('./package.json'); export default { - input: 'lib/parser-combinators.js', + input: 'lib/index.js', output: [ { name: 'prsc', file: MAIN_DEST_FILE, format: 'umd', exports: 'named', - sourcemap: true + sourcemap: true, }, - { file: MODULE_DEST_FILE, format: 'es', sourcemap: true } + { file: MODULE_DEST_FILE, format: 'es', sourcemap: true }, ], - plugins: [ - sourcemaps(), - terser() - ] + plugins: [sourcemaps(), terser()], }; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..7ebef6b --- /dev/null +++ b/src/index.ts @@ -0,0 +1,2 @@ +export * from './parser-combinators'; +export * from './streaming'; diff --git a/src/streaming.ts b/src/streaming.ts new file mode 100644 index 0000000..7f08a64 --- /dev/null +++ b/src/streaming.ts @@ -0,0 +1,168 @@ +import { end, ok, Parser, ParseResult } from './parser-combinators'; + +/** + * Helper to collect both the yielded values and the returned value from a generator. + * + * @public + * + * @param gen - Generator to collect from + */ +export function collect(gen: Generator): [T[], R] { + const values: T[] = []; + let it = gen.next(); + while (!it.done) { + values.push(it.value); + it = gen.next(); + } + return [values, it.value]; +} + +/** + * A StreamingParser is similar to a Parser, but instead of returning a value when parsing is + * complete it can parse incrementally and yield values as they are produced. The generator returns + * a ParseResult when iteration is done which indicates whether parsing was successful. + * + * @public + */ +export type StreamingParser = ( + input: string, + offset: number +) => Generator>; + +/** + * Creates a StreamingParser which applies the given Parser and yields the value produced if it + * matches. + * + * @public + * + * @param parser - The Parser to apply + */ +export function streaming(parser: Parser): StreamingParser { + return function* (input: string, offset: number) { + const res = parser(input, offset); + if (res.success) { + yield res.value; + } + return res; + }; +} + +/** + * Creates a StreamingParser which applies the given two StreamingParsers in sequence. + * + * Unlike `then`, this does not combine values using a function, but instead simply yields the + * values produced by both parsers as they produce them. + * + * @public + * + * @param parser1 - First StreamingParser to apply + * @param parser2 - StreamingParser to apply if the first one is successful + */ +export function streamingThen( + parser1: StreamingParser, + parser2: StreamingParser +): StreamingParser { + return function* (input: string, offset: number) { + const res1 = yield* parser1(input, offset); + if (!res1.success) { + return res1; + } + return yield* parser2(input, res1.offset); + }; +} + +/** + * Creates a StreamingParser which discards undefined values yielded by the given StreamingParser. + * + * @public + * + * @param parser - The StreamingParser to filter + */ +export function streamingFilterUndefined(parser: StreamingParser): StreamingParser { + return function* (input: string, offset: number) { + const gen = parser(input, offset); + let it = gen.next(); + while (!it.done) { + const value = it.value; + if (value !== undefined) { + yield value; + } + it = gen.next(); + } + return it.value; + }; +} + +/** + * Creates a StreamingParser that tries to apply the given StreamingParser zero or more times in + * sequence. Values produced during each iteration are only yielded whenever the inner parser + * matches successfully. + * + * @public + * + * @param parser - StreamingParser to apply repeatedly + */ +export function streamingStar(parser: StreamingParser): StreamingParser { + return function* (input: string, offset: number) { + while (true) { + const [values, result] = collect(parser(input, offset)); + if (!result.success) { + if (result.fatal) { + return result; + } + return ok(offset); + } + + yield* values; + + if (offset === result.offset) { + // Did not advance + return ok(offset); + } + offset = result.offset; + } + }; +} + +/** + * Creates a StreamingParser that tries to apply the given parser optionally. It only yields the + * values produced by the inner parser if it matches successfully, and does not yield anything + * otherwise. + * + * @public + * + * @param parser - StreamingParser to attempt to apply + */ +export function streamingOptional(parser: StreamingParser): StreamingParser { + return function* (input: string, offset: number) { + const [values, result] = collect(parser(input, offset)); + if (!result.success) { + if (result.fatal) { + return result; + } + return ok(offset); + } + + yield* values; + + return result; + }; +} + +/** + * Creates a StreamingParser that applies the given parser and directly yields values produced by + * it, and then only succeeds if parsing concludes at the end of the input string. + * + * @public + * + * @param parser - StreamingParser to apply + */ +export function streamingComplete(parser: StreamingParser): StreamingParser { + return function* (input: string, offset: number) { + const res = yield* parser(input, offset); + if (!res.success) { + return res; + } + return end(input, res.offset); + }; +} diff --git a/test/streaming.tests.ts b/test/streaming.tests.ts new file mode 100644 index 0000000..157fa60 --- /dev/null +++ b/test/streaming.tests.ts @@ -0,0 +1,162 @@ +import { + collect, + consume, + cut, + not, + peek, + streaming, + streamingComplete, + streamingFilterUndefined, + streamingOptional, + streamingStar, + streamingThen, + token, +} from '../src'; + +describe('streaming combinators', () => { + describe('streaming', () => { + it('turns a parser into a generator', () => { + const parser = streaming(token('a')); + const [values, result] = collect(parser('a', 0)); + expect(values).toEqual(['a']); + expect(result.success).toBe(true); + expect(result.offset).toBe(1); + }); + }); + + describe('streamingThen', () => { + it('applies two parsers in sequence', () => { + const parser = streamingThen(streaming(token('a')), streaming(token('b'))); + const [values, result] = collect(parser('ab', 0)); + expect(values).toEqual(['a', 'b']); + expect(result.success).toBe(true); + expect(result.offset).toBe(2); + }); + + it('fails if the first parser fails', () => { + const parser = streamingThen(streaming(token('a')), streaming(token('b'))); + const [values, result] = collect(parser('xb', 0)); + expect(values).toEqual([]); + expect(result.success).toBe(false); + expect(result.offset).toBe(0); + }); + + it('fails if the second parser fails', () => { + const parser = streamingThen(streaming(token('a')), streaming(token('b'))); + const [values, result] = collect(parser('ax', 0)); + expect(values).toEqual(['a']); + expect(result.success).toBe(false); + expect(result.offset).toBe(1); + }); + }); + + describe('streamingFilterUndefined', () => { + it('discards undefined values', () => { + const parser = streamingFilterUndefined( + streamingThen(streaming(token('a')), streaming(consume(token('b')))) + ); + const [values, result] = collect(parser('ab', 0)); + expect(values).toEqual(['a']); + expect(result.success).toBe(true); + expect(result.offset).toBe(2); + }); + }); + + describe('streamingStar', () => { + it('repeatedly applies the parser', () => { + const parser = streamingStar(streaming(token('a'))); + const [values, result] = collect(parser('aaaaab', 0)); + expect(values).toEqual(['a', 'a', 'a', 'a', 'a']); + expect(result.success).toBe(true); + expect(result.offset).toBe(5); + }); + + it('does not yield values from partial iterations', () => { + const parser = streamingStar( + streamingThen(streaming(token('a')), streaming(token('b'))) + ); + const [values, result] = collect(parser('ababa', 0)); + expect(values).toEqual(['a', 'b', 'a', 'b']); + expect(result.success).toBe(true); + expect(result.offset).toBe(4); + }); + + it('propagates fatal errors', () => { + const parser = streamingStar(streaming(cut(token('a')))); + const [values, result] = collect(parser('aab', 0)); + expect(values).toEqual(['a', 'a']); + expect(result.success).toBe(false); + expect(result.offset).toBe(2); + }); + + it('handles an inner parser that does not consume input', () => { + const parser = streamingStar(streaming(peek(token('a')))); + const [values, result] = collect(parser('a', 0)); + expect(values).toEqual(['a']); + expect(result.success).toBe(true); + expect(result.offset).toBe(0); + }); + }); + + describe('streamingOptional', () => { + it('matches the inner parser zero times', () => { + const parser = streamingOptional(streaming(token('a'))); + const [values, result] = collect(parser('ba', 0)); + expect(values).toEqual([]); + expect(result.success).toBe(true); + expect(result.offset).toBe(0); + }); + + it('matches the inner parser once', () => { + const parser = streamingOptional(streaming(token('a'))); + const [values, result] = collect(parser('aa', 0)); + expect(values).toEqual(['a']); + expect(result.success).toBe(true); + expect(result.offset).toBe(1); + }); + + it('does not yield values from partial matches', () => { + const parser = streamingOptional( + streamingThen(streaming(token('a')), streaming(token('b'))) + ); + const [values, result] = collect(parser('a', 0)); + expect(values).toEqual([]); + expect(result.success).toBe(true); + expect(result.offset).toBe(0); + }); + + it('propagates fatal errors', () => { + const parser = streamingOptional(streaming(cut(token('a')))); + const [values, result] = collect(parser('ba', 0)); + expect(values).toEqual([]); + expect(result.success).toBe(false); + expect(result.offset).toBe(0); + }); + }); + + describe('streamingComplete', () => { + it('accepts if the inner parser consumes the remaining input', () => { + const parser = streamingComplete(streaming(token('a'))); + const [values, result] = collect(parser('a', 0)); + expect(values).toEqual(['a']); + expect(result.success).toBe(true); + expect(result.offset).toBe(1); + }); + + it('fails if the inner parser fails', () => { + const parser = streamingComplete(streaming(token('a'))); + const [values, result] = collect(parser('b', 0)); + expect(values).toEqual([]); + expect(result.success).toBe(false); + expect(result.offset).toBe(0); + }); + + it('fails if not all input is consumed', () => { + const parser = streamingComplete(streaming(token('a'))); + const [values, result] = collect(parser('aa', 0)); + expect(values).toEqual(['a']); + expect(result.success).toBe(false); + expect(result.offset).toBe(1); + }); + }); +});