: never;
/**
* Create a parser which tries to match the given parser
* as many times as possible
* @param parser the parser to test
* @returns a parser which tries to match the given parser
* as many times as possible
*/
export function many(parser: P): ManyOf
{
if (typeof parser !== 'function') {
throw new TypeError('many: parser must be a valid parser function');
}
return _parser((state) => {
let next: ParserOutput = state;
const results: any[] = [];
while (true) {
const test: ParserOutput = parser(next);
if (test.isError) {
break;
}
results.push((next = test).result);
}
return _state(next, results);
}) as any;
}
/**
* Create a parser which match at least one time,
* then as many times as possible, the given parser
* @param parser the parser to test
* @returns a parser which match at least one time,
* then as many times as possible, the given parser
*/
export function oneOrMore
(parser: P): ManyOf
{
if (typeof parser !== 'function') {
throw new TypeError('oneOrMore: parser must be a valid parser function');
}
return _parser((state) => {
let next: ParserOutput = state;
const results: any[] = [];
while (true) {
const test: ParserOutput = parser(next);
if (test.isError) {
break;
}
results.push((next = test).result);
}
if (!results.length) {
return _error(state, `oneOrMore: Unable to match any input parser @ index ${state.index}`);
}
return _state(next, results);
}) as any;
}
/**
* Create a parser which match the given parser
* the given number of times
* @param parser the parser to test
* @param times how many exact times the parser must match
* @returns a parser which match the given parser
* the given number of times
*/
export function times
(parser: P, times: number): ManyOf
{
if (typeof parser !== 'function') {
throw new TypeError('times: parser must be a valid parser function');
}
times = Number(times);
if (isNaN(times)) {
throw new TypeError('times: times argument must be a valid number');
}
if (times < 1) {
throw new RangeError(`times: times argument must be at least 1, ${times} given`);
}
return _parser((state) => {
let next: ParserOutput = state;
const results: any[] = [];
for (let i = 0; i < times; ++i) {
next = parser(next);
if (next.isError) {
return _error(state, `times: Unable to match the input parser ${times} times @ index ${state.index}`);
}
results.push(next.result);
}
return _state(next, results);
}) as any;
}
/**
* Create a parser which check the following syntax:
* `wrapper content wrapper` from the given parsers.
* The resultant parser will consider only the output of the
* content parser.\
* For example, to parse a string you can use `between(match('"'), contentParser)`
* @param wrapper the wrapper parser to match before and after the content
* @param content the parser responsible to match the content
* @return a parser which consider only the wrapped content
*/
export function between(
wrapper: Parser,
content: Parser,
): Parser;
/**
* Create a parser which check the following syntax:
* `left content rigth` from the given parsers.
* The resultant parser will consider only the output of the
* content parser.\
* For example, to parse an array you can use `between(match('['), valueParser, match(']'))`
* @param left the parser to match before the content
* @param content the parser responsible to match the content
* @param right the parser to match after the content
* @return a parser which consider only the wrapped content
*/
export function between(
left: Parser,
content: Parser,
right: Parser,
): Parser;
export function between(left: Parser, content: Parser, right?: Parser) {
return sequence(left, content, right || left).map((r) => r[1]);
}
/**
* Generate an high-order function which creates a
* between expression for a parser. This function
* returns another function which takes the content
* parser as argument and generate a parser which will
* match the following syntax: `wrapper content wrapper`.
* The resultant parser will consider only the output of the
* content parser.\
* For example, to parse a string you can use
* ```js
* const contentParser = letters; // or whatever
* const betweenQuotes = parsebetween(match('"'));
* const stringParser = betweenQuotes(contentParser);
* ```
* @param wrapper the wrapper parser to match before and after the content
* @return an high-order function which generate a between parser expression
*/
export function parseBetween(
wrapper: Parser,
): (content: Parser) => Parser;
/**
* Generate an high-order function which creates a
* between expression for a parser. This function
* returns another function which takes the content
* parser as argument and generate a parser which will
* match the following syntax: `left content right`.
* The resultant parser will consider only the output of the
* content parser.\
* For example, to parse a simple numeric array you can use:
* ```js
* const contentParser = sequence(digits, match(',')); // or whatever
* const betweenBrackets = parsebetween(match('['));
* const arrayParser = betweenBrackets(contentParser);
* ```
* @param left the parser to match before the content
* @param right the parser to match after the content
* @return an high-order function which generate a between parser expression
*/
export function parseBetween(
left: Parser,
right: Parser,
): (content: Parser) => Parser;
export function parseBetween(left: Parser, right?: Parser) {
return (content: Parser) => between(left, content, right!);
}
/**
* Generate a parser which tries to match one or more
* times the following syntax: `(content separator)* content`.
* The resultant parser will consider only the output of
* the content parser.\
* For example, to parse a comma-separated list you can use:
* `separated(match(','), contentPasrer)`.
* @param separator the parser that matches separator
* @param content the parser that match the content
* @returns a parser which generate a separated expression
*/
export function separated(
separator: Parser,
content: Parser,
): Parser {
return _parser((state) => {
const results: OR2[] = [];
let next = state;
while (true) {
const test = content(next);
if (test.isError) {
break;
}
results.push(test.result);
next = test as any;
const sep = separator(test);
if (sep.isError) {
break;
}
next = sep;
}
if (results.length === 0) {
return _error(state, `separated: Unable to capture any results @ index ${state.index}`) as any;
}
return _state(next, results);
});
}
/**
* Generate an high-order function which creates a
* separated expression for a parser. This function
* returns another function which takes the content
* parser as argument and generate a parser which will
* match the following syntax: `(content separator)* content`.
* The resultant parser will consider only the output of the
* content parser.\
* For example, to parse a comma-separated list you can use:
* ```js
* const contentParser = letters; // or whatever
* const commaSeparated = parseSeparated(match(','));
* const commaSeparatedParser = commaSeparated(contentParser);
* ```
* @param separator the parser that matches separator
* @return an high-order function which generate a between parser expression
*/
export function parseSeparated(
separator: Parser,
): (content: Parser) => Parser {
return (content: Parser) => separated(separator, content) as any;
}
/**
* Generate a parser which will use the given thunk lazily.
* This is helpful if you need to use parsers which depends one
* on the other:
* ```js
* const parserA = lazy(() => sequence([ parserB, digits, parserB ]));
* const parserB = lazy(() => oneOf([ parserA, digits, letters ]));
* ```
* @param thunk the lazy evaluated function which produce a parser
* @param notCached wether to cache or not the output of the thunk (default: `false --> cached`)
* @returns a parser which evaluates lazily the given thunk
*/
export function lazy(thunk: () => P, notCached?: boolean): P {
if (notCached) {
return _parser((state) => thunk()(state)) as P;
}
let res: P | null = null;
return _parser((state) => (res ||= thunk())(state)) as P;
}
/**
* Create a parser which always produces an error state
* @param message the message of the error state
* @returns a failure parser
*/
export function fail(message: E): Parser {
return _parser((state) => _error(state, message));
}
/**
* Create a parser which always produces a success parser state
* @param value the result value of the success state
* @returns a success parser
*/
export function success(value: T): Parser {
return _parser((state) => _state(state, value));
}
type ContextualOneOf = P extends Parser
? R
: P extends Parser | infer Rest
? Rest extends Parser
? ContextualOneOf extends never
? never
: R | ContextualOneOf
: never
: never;
type ContextualCB = () => Generator
| R, ContextualOneOf
| R>;
/**
* Create a parser which automatically chain the parsers produced
* by a generator function, yielding their parserd results states.\
* For example:
* ```js
* const declTypeParser = combo.oneOf(
* combo.match('VAR '),
* combo.match('GLOBAL_VAR ')
* );
* const typeParser = combo.oneOf(
* combo.match(' INT '),
* combo.match(' STRING '),
* combo.match(' BOOL ')
* ).map(v => v.trim().toLowerCase());
* const stringParser = combo.between(combo.match('"'), combo.letters);
* const numParser = combo.digits.map(Number);
* const boolParser = combo.oneOf(combo.match('true'), combo.match('false')).map(v => v === 'true');
* const parser = combo.contextual(function*() { // Note that we use a generator function here
* const declarationType = yield declTypeParser; // yielding parser => returning 'var' | 'global_var'
* const varName = yield combo.letters; // returning string
* const type = yield typeParser; // returning 'int' | 'string' | 'bool'
* let data;
* switch (type) {
* case 'int': data = yield numParser; break;
* case 'string': data = yield stringParser; break;
* case 'bool': data = yield boolParser; break;
* }
* return { varName: varName, data, type: type.trim().toLowerCase(), declarationType: declarationType.trim().toLowerCase() };
* });
* parser('VAR theAnswer INT 42'); // { varName: 'theAnswer', data: 42, type: 'int', declarationType: 'var' }
* parser('GLOBAL_VAR greeting STRING "Hello"'); // { varName: 'greetubg', data: 'Hello', type: 'string', declarationType: 'global_var' }
* parser('VAR skyIsBlue BOOL true'); // { varName: 'skyIsBlue', data: true, type: 'bool', declarationType: 'var' }
* ```
* @param generator the function that generates the parsers to chain
* @returns a automatically chained parser
*/
export function contextual
(generator: ContextualCB
): Parser {
return success(null).chain(() => {
const iterator = generator();
function runStep(next: ContextualOneOf): Parser {
const itResult = iterator.next(next);
if (itResult.done) {
return success(itResult.value);
}
const nextParser = itResult.value;
if (typeof nextParser !== 'function') {
throw new TypeError('contextual: yielded values must always be parsers!');
}
return nextParser.chain(runStep);
}
return runStep(undefined as any);
});
}
export namespace binary {
function _compare(
left: Binariable,
right: Binariable,
leftOffset: number = 0,
rightOffset: number = 0,
leftLength: number = -1,
rightLength: number = -1,
): boolean {
left = _toBinary(left);
right = _toBinary(right);
leftOffset = Math.max(Number(leftOffset) || 0, 0);
rightOffset = Math.max(Number(rightOffset) || 0, 0);
leftLength = leftLength < 0 ? left.byteLength : Math.min(leftLength, left.byteLength);
rightLength = rightLength < 0 ? right.byteLength : Math.min(rightLength, right.byteLength);
const maxOffset = rightLength - rightOffset;
if (leftLength - leftOffset < maxOffset) {
return false;
}
for (let i = 0; i < maxOffset; ++i) {
if (left.getUint8(leftOffset + i) !== right.getUint8(rightOffset + i)) {
return false;
}
}
return true;
}
function _len(value: Binariable): number {
if (typeof value === 'string' || Array.isArray(value)) {
return value.length;
}
return value.byteLength;
}
function _hex(value: number, minDigits: number = 2): string {
let res = value.toString(16);
while (res.length < minDigits) {
res = '0' + res;
}
return res;
}
function _str(value: Binariable, start?: number, end?: number): string {
if (typeof value === 'string') {
return value.slice(start, end);
}
if (
value instanceof ArrayBuffer ||
(typeof SharedArrayBuffer !== 'undefined' && value instanceof SharedArrayBuffer)
) {
value = Array.prototype.slice.call(new Uint8Array(value)) as number[];
} else if (ArrayBuffer.isView(value)) {
value = Array.prototype.slice.call(new Uint8Array(value.buffer, value.byteOffset, value.byteLength)) as number[];
}
return (
'<' +
(value as number[])
.slice(start, end)
.map((v) => _hex(v))
.join(', ') +
'>'
);
}
/**
* Create a binary parser which check if the current state
* starts with the given binary expression
* @param expression the string expression to match
* @param expressionOffset the offset where the considered expression starts
* @param expressionLength the length of considered expression
* @return a binary parser which check if the current state starts with the given bianry expression
*/
export function match(
expression: Binariable,
expressionOffset: number = 0,
expressionLength: number = -1,
): BinaryParser {
expression = _toBinary(expression);
return _binary((state) =>
_compare(state.source, expression, state.index, expressionOffset, -1, expressionLength)
? _state(state, expression, state.index + (expression as DataView).byteLength)
: _error(
state,
`match: Tried to match '${_str(expression)}', but got '${_str(
state.source,
state.index,
state.index + _len(expression),
)}' @ index ${state.index}`,
),
);
}
/** A binary parser which consumes a buffer bit by bit */
export const bit: BinaryParser<0 | 1, string> = _binary((state) => {
const byteOffset = Math.floor(state.index / 8);
if (byteOffset >= state.source.byteLength) {
return _error(state, `bit: Unexpected end of input @ index ${state.index}`);
}
const byte = state.source.getUint8(byteOffset);
const bitOffset = 7 - (state.index % 8);
const res = byte & (1 << bitOffset) ? 1 : 0;
return _state(state, res, state.index + 1);
});
/** A binary parser which consume a buffer bit by bit, expecting an unsetted bit (0) */
export const zero: BinaryParser<0, string> = _binary((state) => {
const byteOffset = Math.floor(state.index / 8);
if (byteOffset >= state.source.byteLength) {
return _error(state, `bit: Unexpected end of input @ index ${state.index}`);
}
const byte = state.source.getUint8(byteOffset);
const bitOffset = 7 - (state.index % 8);
const res = byte & (1 << bitOffset) ? 1 : 0;
return res
? _error(state, `zero: expected 0 but got 1 @ index ${state.index}`)
: _state(state, res, state.index + 1);
});
/** A binary parser which consume a buffer bit by bit, expecting a setted bit (1) */
export const one: BinaryParser<1, string> = _binary((state) => {
const byteOffset = Math.floor(state.index / 8);
if (byteOffset >= state.source.byteLength) {
return _error(state, `bit: Unexpected end of input @ index ${state.index}`);
}
const byte = state.source.getUint8(byteOffset);
const bitOffset = 7 - (state.index % 8);
const res = byte & (1 << bitOffset) ? 1 : 0;
return res
? _state(state, res, state.index + 1)
: _error(state, `one: expected 1 but got 0 @ index ${state.index}`);
});
/**
* Create a binary parser which read an unsigned integer from
* a fixed number of bits into a {@link Number}. The number of bits must not exceed
* 32. For integers longer than 32 bits use {@link biguint}
* @param bits the number of bits to read (1 <= bits <= 32)
* @param bigEndian wether to read the number in Big Endian or Little Endian mode
* @returns a binary parser which read an unsigned integer with the given specs
*/
export function uint(bits: number, bigEndian?: boolean): BinaryParser {
if (bits < 1) {
throw new RangeError(`uint: btis must be larger than 0, got ${bits}`);
}
if (bits > 32) {
throw new RangeError(`uint: btis must be less or equal to 32, got ${bits}`);
}
const seq: BinaryParser<0 | 1, string>[] = [];
for (let i = 0; i < bits; ++i) {
seq.push(bit);
}
return sequence(seq).map(
bigEndian
? (bs) => bs.reverse().reduce((a, b, i) => a + (b << (bits - 1 - i)), 0 as number)
: (bs) => bs.reduce((a, b, i) => a + (b << (bits - 1 - i)), 0 as number),
);
}
/** A binary parser which reads an unsigned 8-bit (1 byte) integer as {@link Number} */
export const uint8 = uint(8);
/** A binary parser which reads an unsigned 8-bit (1 byte) integer as {@link Number}, Big Endian mode */
export const uint8BE = uint(8, true);
/** A binary parser which reads an unsigned 16-bit (2 byte) integer as {@link Number} */
export const uint16 = uint(16);
/** A binary parser which reads an unsigned 16-bit (2 byte) integer as {@link Number}, Big Endian mode */
export const uint16BE = uint(16, true);
/** A binary parser which reads an unsigned 24-bit (3 byte) integer as {@link Number} */
export const uint24 = uint(24);
/** A binary parser which reads an unsigned 24-bit (3 byte) integer as {@link Number}, Big Endian mode */
export const uint24BE = uint(24, true);
/** A binary parser which reads an unsigned 32-bit (4 byte) integer as {@link Number} */
export const uint32 = uint(32);
/** A binary parser which reads an unsigned 32-bit (4 byte) integer as {@link Number}, BigEndian mode */
export const uint32BE = uint(32, true);
const _biZero = BigInt(0);
const _biOne = BigInt(1);
/**
* Create a binary parser which read an unsigned integer from
* a fixed number of bits into a {@link BigInt}. The number of
* readable bits is limited by JS environment implementation.
* @param bits the number of bits to read (bits >= 1)
* @param bigEndian wether to read the number in Big Endian or Little Endian mode
* @returns a binary parser which read an unsigned integer with the given specs
*/
export function biguint(bits: number, bigEndian?: boolean): BinaryParser {
if (bits < 1) {
throw new RangeError(`biguint: btis must be larger than 0, got ${bits}`);
}
const seq: BinaryParser<0 | 1, string>[] = [];
for (let i = 0; i < bits; ++i) {
seq.push(bit);
}
const _biBits = BigInt(bits - 1);
return sequence(seq).map(
bigEndian
? (bs) => bs.reverse().reduce((a, b, i) => a + ((b ? _biOne : _biZero) << (_biBits - BigInt(i))), _biZero)
: (bs) => bs.reduce((a, b, i) => a + ((b ? _biOne : _biZero) << (_biBits - BigInt(i))), _biZero),
);
}
/** A binary parser which reads an unsigned 64-bit (8 byte) integer as {@link BigInt} */
export const uint64 = biguint(64);
/** A binary parser which reads an unsigned 64-bit (8 byte) integer as {@link BigInt}, Big Endian mode */
export const uint64BE = biguint(64, true);
/** A binary parser which reads an unsigned 128-bit (16 byte) integer as {@link BigInt} */
export const uint128 = biguint(128);
/** A binary parser which reads an unsigned 128-bit (16 byte) integer as {@link BigInt}, Big Endian mode */
export const uint128BE = biguint(128, true);
/** A binary parser which reads an unsigned 256-bit (32 byte) integer as {@link BigInt} */
export const uint256 = biguint(256);
/** A binary parser which reads an unsigned 256-bit (32 byte) integer as {@link BigInt}, Big Endian mode */
export const uint256BE = biguint(256, true);
/**
* Create a binary parser which read a signed integer from
* a fixed number of bits into a {@link Number}. The number of bits must not exceed
* 32. For integers longer than 32 bits use {@link bigint}
* @param bits the number of bits to read (1 <= bits <= 32)
* @param bigEndian wether to read the number in Big Endian or Little Endian mode
* @returns a binary parser which read a signed integer with the given specs
*/
export function int(bits: number, bigEndian?: boolean): BinaryParser {
if (bits < 1) {
throw new RangeError(`int: btis must be larger than 0, got ${bits}`);
}
if (bits > 32) {
throw new RangeError(`int: btis must be less or equal to 32, got ${bits}`);
}
const seq: BinaryParser<0 | 1, string>[] = [];
for (let i = 0; i < bits; ++i) {
seq.push(bit);
}
return sequence(seq).map(
bigEndian
? (bs) =>
!(bs = bs.reverse())[0]
? bs.reduce((a, b, i) => a + (b << (bits - 1 - i)), 0 as number)
: -(1 + bs.reduce((a, b, i) => a + ((b ? 0 : 1) << (bits - 1 - i)), 0 as number))
: (bs) =>
!bs[0]
? bs.reduce((a, b, i) => a + (b << (bits - 1 - i)), 0 as number)
: -(1 + bs.reduce((a, b, i) => a + ((b ? 0 : 1) << (bits - 1 - i)), 0 as number)),
);
}
/** A binary parser which reads a signed 8-bit (1 byte) integer as {@link Number} */
export const int8 = int(8);
/** A binary parser which reads a signed 8-bit (1 byte) integer as {@link Number}, Big Endian mode */
export const int8BE = int(8, true);
/** A binary parser which reads a signed 16-bit (2 byte) integer as {@link Number} */
export const int16 = int(16);
/** A binary parser which reads a signed 16-bit (2 byte) integer as {@link Number}, Big Endian mode */
export const int16BE = int(16, true);
/** A binary parser which reads a signed 24-bit (3 byte) integer as {@link Number} */
export const int24 = int(24);
/** A binary parser which reads a signed 24-bit (3 byte) integer as {@link Number}, Big Endian mode */
export const int24BE = int(24, true);
/** A binary parser which reads a signed 32-bit (4 byte) integer as {@link Number} */
export const int32 = int(32);
/** A binary parser which reads a signed 32-bit (4 byte) integer as {@link Number}, Big Endian mode */
export const int32BE = int(32, true);
/**
* Create a binary parser which read a signed integer from
* a fixed number of bits into a {@link BigInt}. The number of
* readable bits is limited by JS environment implementation.
* @param bits the number of bits to read (bits >= 1)
* @param bigEndian wether to read the number in Big Endian or Little Endian mode
* @returns a binary parser which read a signed integer with the given specs
*/
export function bigint(bits: number, bigEndian?: boolean): BinaryParser {
if (bits < 1) {
throw new RangeError(`bigint: btis must be larger than 0, got ${bits}`);
}
const seq: BinaryParser<0 | 1, string>[] = [];
for (let i = 0; i < bits; ++i) {
seq.push(bit);
}
const _biBits = BigInt(bits - 1);
return sequence(seq).map(
bigEndian
? (bs) =>
!(bs = bs.reverse())[0]
? bs.reduce((a, b, i) => a + ((b ? _biOne : _biZero) << (_biBits - BigInt(i))), _biZero)
: -(_biOne + bs.reduce((a, b, i) => a + ((b ? _biZero : _biOne) << (_biBits - BigInt(i))), _biZero))
: (bs) =>
!bs[0]
? bs.reduce((a, b, i) => a + ((b ? _biOne : _biZero) << (_biBits - BigInt(i))), _biZero)
: -(_biOne + bs.reduce((a, b, i) => a + ((b ? _biZero : _biOne) << (_biBits - BigInt(i))), _biZero)),
);
}
/** A binary parser which reads an unsigned 64-bit (8 byte) integer as {@link BigInt} */
export const int64 = bigint(64);
/** A binary parser which reads an unsigned 64-bit (8 byte) integer as {@link BigInt}, Big Endian mode */
export const int64BE = bigint(64, true);
/** A binary parser which reads an unsigned 128-bit (16 byte) integer as {@link BigInt} */
export const int128 = bigint(128);
/** A binary parser which reads an unsigned 128-bit (16 byte) integer as {@link BigInt}, Big Endian mode */
export const int128BE = bigint(128, true);
/** A binary parser which reads an unsigned 256-bit (32 byte) integer as {@link BigInt} */
export const int256 = bigint(256);
/** A binary parser which reads an unsigned 256-bit (32 byte) integer as {@link BigInt}, Big Endian mode */
export const int256BE = bigint(256, true);
type SequenceOfParsersResult = Parsers extends []
? []
: Parsers extends [BinaryParser]
? [OR]
: Parsers extends [BinaryParser, ...infer Rest1]
? Rest1 extends [BinaryParser, ...infer Rest2]
? Rest2 extends BinaryParser[]
? [OR1, OR2, ...SequenceOfParsersResult]
: never
: never
: Parsers extends BinaryParser[]
? OR[]
: never;
type SequenceOfParsersError = Parsers extends []
? never
: Parsers extends [BinaryParser]
? OE
: Parsers extends [BinaryParser, ...infer Rest1]
? Rest1 extends [BinaryParser, ...infer Rest2]
? Rest2 extends BinaryParser[]
? OE1 | OE2 | SequenceOfParsersError
: never
: never
: Parsers extends BinaryParser[]
? OE
: never;
type SequenceOfParsers = SequenceOfParsersResult extends never
? never
: SequenceOfParsersError extends never
? never
: BinaryParser, SequenceOfParsersError, any, any>;
/**
* Create a binary parser which check the concatenation in
* sequence of the given binary parsers
* @param parsers an array of binary parsers to concatenate
* @return a binary parser that is the concatenation in a sequence
* of the given binary parsers
*/
export function sequence(parsers: Parsers): SequenceOfParsers;
/**
* Create a binary parser which check the concatenation in
* sequence of the given binary parsers
* @param parsers binary parsers to concatenate
* @return a binary parser that is the concatenation in a sequence
* of the given binary parsers
*/
export function sequence(...parsers: Parsers): SequenceOfParsers;
export function sequence(...parsers: BinaryParser[]): any {
parsers = _extract(parsers);
if (!parsers.length) {
throw new TypeError('sequence: you must provide at least one parser');
}
return _binary((state) => {
let next: BinaryParserOutput = state;
const results = parsers.map((p) => (next = p(next)).result);
return _state(next, results);
});
}
type ParserOneOf = SequenceOfParsersResult extends never
? never
: SequenceOfParsersError extends never
? never
: Parser>, SequenceOfParsersError, any, any>;
/**
* Create a binary parser which returns the first success state
* from any given binary parsers.
* @param parsers an array of binary parsers to test
* @return a binary parser which test the given binary parsers
*/
export function oneOf(parsers: Parsers): ParserOneOf;
/**
* Create a binary parser which returns the first success state
* from any given binary parsers.
* @param parsers binary parsers to test
* @return a binary parser which test the given binary parsers
*/
export function oneOf(...parsers: Parsers): ParserOneOf;
export function oneOf(...parsers: BinaryParser[]): ParserOneOf {
parsers = _extract(parsers);
if (!parsers.length) {
throw new TypeError('oneOf: you must provide at least one parser');
}
return _binary((state) => {
for (const p of parsers) {
const next = p(state);
if (!next.isError) {
return next;
}
}
return _error(state, `oneOf: Unable to match with any parser @ index ${state.index}`);
}) as any;
}
type ManyOf = P extends BinaryParser
? BinaryParser
: never;
/**
* Create a binary parser which tries to match the given binary parser
* as many times as possible
* @param parser the binary parser to test
* @returns a binary parser which tries to match the given binary parser
* as many times as possible
*/
export function many(parser: P): ManyOf
{
if (typeof parser !== 'function') {
throw new TypeError('many: parser must be a valid parser function');
}
return _binary((state) => {
let next: BinaryParserOutput = state;
const results: any[] = [];
while (true) {
const test: BinaryParserOutput = parser(next);
if (test.isError) {
break;
}
results.push((next = test).result);
}
return _state(next, results);
}) as any;
}
/**
* Create a binary parser which match at least one time,
* then as many times as possible, the given binary parser
* @param parser the binary parser to test
* @returns a binary parser which match at least one time,
* then as many times as possible, the given binary parser
*/
export function oneOrMore
(parser: P): ManyOf
{
if (typeof parser !== 'function') {
throw new TypeError('oneOrMore: parser must be a valid parser function');
}
return _binary((state) => {
let next: BinaryParserOutput = state;
const results: any[] = [];
while (true) {
const test: BinaryParserOutput = parser(next);
if (test.isError) {
break;
}
results.push((next = test).result);
}
if (!results.length) {
return _error(state, `oneOrMore: Unable to match any input parser @ index ${state.index}`);
}
return _state(next, results);
}) as any;
}
/**
* Create a binary parser which match the given binary parser
* the given number of times
* @param parser the binary parser to test
* @param times how many exact times the binary parser must match
* @returns a binary parser which match the given binary parser
* the given number of times
*/
export function times
(parser: P, times: number): ManyOf
{
if (typeof parser !== 'function') {
throw new TypeError('many: parser must be a valid parser function');
}
times = Number(times);
if (isNaN(times)) {
throw new TypeError('times: times argument must be a valid number');
}
if (times < 1) {
throw new RangeError(`times: times argument must be at least 1, ${times} given`);
}
return _binary((state) => {
let next: BinaryParserOutput = state;
const results: any[] = [];
for (let i = 0; i < times; ++i) {
next = parser(next);
if (next.isError) {
return _error(state, `times: Unable to match the input parser ${times} times @ index ${state.index}`);
}
results.push(next.result);
}
return _state(next, results);
}) as any;
}
/**
* Create a binary parser which check the following syntax:
* `wrapper content wrapper` from the given binary parsers.
* The resultant binary parser will consider only the output of the
* content binary parser.\
* For example, to parse a binary string enclosed by the sequence `0110` you can use
* `between(sequence(zero, one, one, zero), contentParser)`
* @param wrapper the wrapper binary parser to match before and after the content
* @param content the binary parser responsible to match the content
* @return a binary parser which consider only the wrapped content
*/
export function between(
wrapper: BinaryParser,
content: BinaryParser,
): BinaryParser;
/**
* Create a binary parser which check the following syntax:
* `left content rigth` from the given binary parsers.
* The resultant binary parser will consider only the output of the
* content binary parser.\
* For example, to parse a binary string enclosed by the sequence `0110` and `1001` you can use
* `between(sequence(zero, one, one, zero), contentParser, sequence(one, zero, zero, one))`
* @param left the binary parser to match before the content
* @param content the binary parser responsible to match the content
* @param right the binary parser to match after the content
* @return a binary parser which consider only the wrapped content
*/
export function between(
left: BinaryParser,
content: BinaryParser,
right: BinaryParser,
): BinaryParser;
export function between(left: BinaryParser, content: BinaryParser, right?: BinaryParser) {
return sequence(left, content, right || left).map((r) => r[1]);
}
/**
* Generate an high-order function which creates a
* between expression for a binary parser. This function
* returns another function which takes the content
* binary parser as argument and generate a binary parser which will
* match the following syntax: `wrapper content wrapper`.
* The resultant binary parser will consider only the output of the
* content binary parser.\
* For example, to parse content between the sequence `0110` you can use
* ```js
* const contentParser = uint8; // or whatever
* const betweenSequence = parsebetween(sequence(zero, one, one, zero));
* const myParser = betweenSequence(contentParser);
* ```
* @param wrapper the wrapper binary parser to match before and after the content
* @return an high-order function which generate a binary between parser expression
*/
export function parseBetween(
wrapper: BinaryParser,
): (content: BinaryParser) => BinaryParser;
/**
* Generate an high-order function which creates a
* between expression for a binary parser. This function
* returns another function which takes the content
* binary parser as argument and generate a binary parser which will
* match the following syntax: `left content right`.
* The resultant binary parser will consider only the output of the
* content binary parser.\
* For example, to parse content between the sequence `0110` and `1001` you can use
* ```js
* const contentParser = uint8; // or whatever
* const betweenSequence = parsebetween(sequence(zero, one, one, zero), sequence(one, zero, zero, one));
* const myParser = betweenSequence(contentParser);
* ```
* @param left the parser to match before the content
* @param right the parser to match after the content
* @return an high-order function which generate a between parser expression
*/
export function parseBetween(
left: BinaryParser,
right: BinaryParser,
): (content: BinaryParser) => BinaryParser;
export function parseBetween(left: BinaryParser, right?: BinaryParser) {
return (content: BinaryParser) => between(left, content, right!);
}
/**
* Generate a binary parser which tries to match one or more
* times the following syntax: `(content separator)* content`.
* The resultant binary parser will consider only the output of
* the content binary parser.\
* For example, to a list separated by the sequence `000` you can use:
* `separated(sequence(zero, zero, zero), contentPasrer)`.
* @param separator the binary parser that matches separator
* @param content the binary parser that match the content
* @returns a binary parser which generate a separated expression
*/
export function separated(
separator: BinaryParser,
content: BinaryParser,
): BinaryParser {
return _binary((state) => {
const results: OR2[] = [];
let next = state;
while (true) {
const test = content(next);
if (test.isError) {
break;
}
results.push(test.result);
next = test as any;
const sep = separator(test);
if (sep.isError) {
break;
}
next = sep;
}
if (results.length === 0) {
return _error(state, `separated: Unable to capture any results @ index ${state.index}`) as any;
}
return _state(next, results);
});
}
/**
* Generate an high-order function which creates a
* separated expression for a binary parser. This function
* returns another function which takes the content
* binary parser as argument and generate a binary parser which will
* match the following syntax: `(content separator)* content`.
* The resultant binary parser will consider only the output of the
* content binary parser.\
* For example, to parse a list which elements are separated by `0` you can use:
* ```js
* const contentParser = uint8; // or whatever
* const zeroSeparated = parseSeparated(zero);
* const zeroSeparatedParser = zeroSeparated(contentParser);
* ```
* @param separator the binary parser that matches separator
* @return an high-order function which generate a binary between parser expression
*/
export function parseSeparated(
separator: BinaryParser,
): (content: BinaryParser) => BinaryParser {
return (content: BinaryParser) => separated(separator, content) as any;
}
/**
* Generate a binary parser which will use the given thunk lazily.
* This is helpful if you need to use binary parsers which depends one
* on the other:
* ```js
* const parserA = lazy(() => sequence(parserB, zero, zero, parserB));
* const parserB = lazy(() => oneOf(parserA, sequence(zero, zero, one), sequence(one, one, zero)));
* ```
* @param thunk the lazy evaluated function which produce a binary parser
* @param notCached wether to cache or not the output of the thunk (default: `false --> cached`)
* @returns a binary parser which evaluates lazily the given thunk
*/
export function lazy(thunk: () => P, notCached?: boolean): P {
if (notCached) {
return _binary((state) => thunk()(state)) as P;
}
let res: P | null = null;
return _binary((state) => (res ||= thunk())(state)) as P;
}
/**
* Create a binary parser which always produces an error state
* @param message the message of the error state
* @returns a failure binary parser
*/
export function fail(message: E): BinaryParser {
return _binary((state) => _error(state, message));
}
/**
* Create a binary parser which always produces a success binary parser state
* @param value the result value of the success state
* @returns a success binary parser
*/
export function success(value: T): BinaryParser {
return _binary((state) => _state(state, value));
}
type ContextualOneOf = P extends BinaryParser
? R
: P extends BinaryParser | infer Rest
? Rest extends BinaryParser
? ContextualOneOf extends never
? never
: R | ContextualOneOf
: never
: never;
type ContextualCB = () => Generator
| R, ContextualOneOf
| R>;
/**
* Create a binary parser which automatically chain the binary parsers produced
* by a generator function, yielding their parserd binary results states.\
* See the non binary version of this function for a tip on the usage.
* @param generator the function that generates the binary parsers to chain
* @returns a automatically chained binary parser
*/
export function contextual
(
generator: ContextualCB
,
): BinaryParser {
return success(null).chain(() => {
const iterator = generator();
function runStep(next: ContextualOneOf): BinaryParser {
const itResult = iterator.next(next);
if (itResult.done) {
return success(itResult.value);
}
const nextParser = itResult.value;
if (typeof nextParser !== 'function') {
throw new TypeError('contextual: yielded values must always be parsers!');
}
return nextParser.chain(runStep);
}
return runStep(undefined as any);
});
}
const _asBinaryRep = /\s+/g;
/**
* Utility function to convert a string or an array
* into a buffer. Note that all non-zero characters
* are considered as a one bit, as well as any non
* truish numeric values (eg. `0, NaN`).
* @param value the value to being converted
* @returns the converted buffer as a {@link DataView}
*/
export function asBinary(value: string | (boolean | 1 | 0 | '1' | '0')[]): DataView {
if (typeof value === 'string') {
value = value
.replace(_asBinaryRep, '')
.split('')
.map((v) => v === '1');
}
const output: number[] = [];
const len = Math.ceil(value.length / 8);
for (let i = 0; i < len; ++i) {
output[i] = 0;
for (let j = 0; j < 8; ++j) {
output[i] |= (Number(value[i * 8 + j]) ? 1 : 0) << (7 - j);
}
}
const buf = new Uint8Array(output);
return new DataView(buf.buffer);
}
/**
* Converts a string to a byte array buffer
* @param value the string to convert
* @returns the converted buffer as a {@link DataView}
*/
export function toCharCode(value: string): DataView {
return new DataView(new Uint8Array(value.split('').map((v) => v.charCodeAt(0))).buffer);
}
}
export default {
binary,
between,
contextual,
digits,
end,
eof,
eoi,
fail,
lazy,
letters,
many,
match,
parseBetween,
parseSeparated,
parser,
success,
separated,
sequence,
spaces,
state,
times,
oneOf,
oneOrMore,
};