@masala/parser
Version:
899 lines (733 loc) • 23.6 kB
TypeScript
/**
* Written by Nicolas Zozol
*/
export interface Unit {
}
/**
* Represents an Option/Optional/Maybe/Either type
*/
export interface Option<T> {
/**
* The inner value wrapped by the Option
*/
value: T;
/**
* Returns true if value is present
*/
isPresent(): boolean;
/**
* Transform the result
* @param mapper : the map function
*/
map<Y>(mapper: (t: T) => Y): Option<Y>;
/**
* The flatmap mapper must returns an option
* @param bindCall
*/
flatMap<Y>(bindCall: (y: Y) => Option<Y>): Option<Y>;
filter(f: (value: T) => boolean): T | Option<T>;
/**
* Returns the inner value when present.
*/
get(): T;
/**
* Returns the inner value or another one
* @param value
*/
orElse<Y>(value: Y): T | Y;
/**
* Accepts a provider function called with no params. Will
* return the inner value when present, or call the provider.
* @param provider
*/
orLazyElse<Y>(provider: () => Y): T | Y;
}
/**
* The Try interface allow the parser to recover from a parse failure
* and try backtacking with another parser using for example `or()`.
* It accepts a value when success, or an error value when failed
*
*/
export interface Try<V, E> {
isSuccess(): boolean;
isFailure(): boolean;
/**
* Acts like a mapper called only in case of success
* @param f
*/
onSuccess<T>(f: (v: V) => void): Try<V, E>;
/**
* Acts like a mapper called only in case of failure
* @param f
*/
onFailure(f: (e: E) => void): Try<V, E>;
/**
* Transform the success value. If `map()` throws an error,
* then we have a failure.
* @param bindCall
*/
map<T>(bindCall: (v: V) => T): Try<T, E>;
flatMap<T>(bindCall: (v: V) => Try<T, E>): Try<T, E>;
/**
* Returns the success value
*/
success(): V;
/**
* Returns the failure value
*/
failure(): E;
/**
* Will provide another value in case of failure
* @param otherValue
*/
recoverWith<T>(otherValue: T): V | T;
/**
* Will call a provider accepting the error as argument when the
* try as failed, or the success value.
* @param recoverFunction
*/
lazyRecoverWith<T>(recoverFunction: (error?: E) => T): V | T;
/**
* Returns a new Try that will succeed only if first is a success
* AND is the predicate is accepted on the value
* @param f
*/
filter(predicate: (value: V) => boolean): Try<V, E>;
}
export declare type NEUTRAL = symbol;
/**
* Represents the sequence of tokens found by the parser.
* A Tuple accepts a `NEUTRAL` element
*/
export interface Tuple<T> {
/**
* Wrapped array
*/
value: T[];
isEmpty: boolean;
/**
* Number of elements in the wrapped array
*/
size(): number;
/**
* Returns the first token. It's up to the coder to know
* if there is only one token
* * ```js
* const parser = C.char('a')
* .then(C.char('b'))
* .drop()
* .then(C.char('c')
* .single(); // Parsing will return 'c'
* ```
* See [[array]]
*/
single(): T;
/**
* Returns all tokens in an array
* ```js
* const parser = C.char('a')
* .then(C.char('b'))
* .then(C.char('c')
* .array(); // Parsing will return ['a','b','c']
* ```
* See [[single]]
*/
array(): T[];
/**
* Returns the last element of the Tuple
*/
last(): T;
/**
* Returns the first element of the Tuple
*/
first(): T;
/**
* Returns value at index
* @param index
*/
at(index: number): T;
/**
* Join elements as one string joined by optional separator (ie empty '' separator by default)
* @param separator join separator
*/
join(separator?: string): string;
/**
* The tuple will not change with the NEUTRAL element.
* It will concatenate the two tuples as one, or add
* a single element if it's not a Tuple.
* Therefore a Tuple can wrap multiple arrays.
* @param neutral : neutral element
*
* See [[TupleParser]]
*/
append(neutral: NEUTRAL): this;
append<Y>(other: Tuple<Y>): Tuple<T | Y>;
append<Y>(element: Y): Tuple<T | Y>;
}
/**
* Creates an empty or not Tuple, which is the preferred way
*
* `const t = tuple().append(x)`
*
* `const t = tuple([2, 4, 5])`;
*/
export function tuple<T>(array?: T[]): Tuple<T>;
/**
* Represents a **source** of data that will be parsed.
* These data are characters with Stream<String> of tokens defined
* by low-level tokens (if, '{', number, class, function}) with Stream<Parser>.
* There are two types of Streams: low level where source is a text or an array,
* and high level where source is composed with another source.
*
* A source can be parsed multiple times by various parsers. A
* `Stream` is supposed to be parsed only once.
*
*/
export interface Stream<Data> {
/**
* For high-level stream, returns the index of the low-level source.
* For low-level, returns the same value as index.
* @param index
*/
location(index: number): number;
/**
* Return the token at the index.
* @param index
*/
get(index: number): Try<Data, void>;
subStreamAt(index: number, stream: Stream<Data>): any;
}
/**
* Data parsed by the parser
*/
interface Streams {
ofString(string: string): Stream<string>;
ofArray<X>(): Stream<X[]>;
ofParser<T, D>(tokenParser: IParser<T>, lowerStream: Stream<D>): Stream<IParser<T>>;
/**
* Creates a bufferedStream that checks into a cache
* @param source
*/
buffered<D>(source: D): Stream<D>;
}
/**
* When a Parser parses a Stream, it produces a Response.
* This response can be an Accept, or a Reject
*/
export interface Response<T> {
/**
* The value built by the parsing
*/
value: T;
/**
* Index in the stream. See [[location]].
*/
offset: number;
/**
* True if the parser succeed all steps, false if parser stopped
*/
isAccepted(): boolean;
/**
* Returns true if parser has reached end of stream to build the Response
*/
isEos(): boolean;
/**
* Execute accept if response in an Accept, or reject and create a new
* Response. A Reject could thus lead to a new Accept
* @param accept
* @param reject
*/
fold(accept: () => Response<T>, reject?: () => Response<T>): Response<T>;
/**
* Transform the response **in case of** success. Won't touch a Reject.
* @param f
*/
map<Y>(f: (v: T) => Y): Response<Y>;
/**
* ```js
* Response.accept('a')
* .flatMap(a=>Response.accept(a))
* .isAccepted(),
* ```
*
* @param f mapping function
*/
flatMap<Y>(f: (v: T) => Response<Y>): Response<Y>;
filter(predicate: (value: any) => boolean): Response<T>;
/**
* Index in the final text. Is equal to
* [[offset]] for low level parsers.
*/
location(): number;
}
/**
* Response rejected. Can be transformed in
* `Accept` using [[fold]]
*/
export interface Reject<T> extends Response<T> {
}
export interface Accept<T> extends Response<T> {
}
/* Array with different values*/
export interface TupleParser<T> extends IParser<Tuple<T>> {
/**
* Combine two parsers for reading the stream
* ```js
* const parser = C.char('a')
* .then(C.char('b')) // first TupleParser
* .then(C.char('c'));
* const value = parser.parse(Streams.ofString('abc')).value;
* test.deepEqual(value, new Tuple(['a','b','c']));
* ```
* @param p next parser
*/
then(p: VoidParser): TupleParser<T>;
then(p: IParser<T>): TupleParser<T>;
then<Y>(p: IParser<Y>): TupleParser<T | Y>;
or(other: TupleParser<T>): TupleParser<T>;
or<Y>(other: TupleParser<Y>): TupleParser<T> | TupleParser<Y>;
or<T, P extends IParser<T>>(other: P): VoidParser | P;
/**
* Map the response value as an array
* ```js
* const parser = C.char('a')
* .then(C.char('b'))
* .array();
* const value = parser.parse(Streams.ofString('abcd')).value;
* test.deepEqual(value, ['a','b']); // parsing stopped at index 2
* ```
*
*/
array(): SingleParser<T[]>;
/**
* Map the response value as the **first** Tuple element.
* ```js
* const parser = C.char('a')
* // 'b' is dropped, but we still have a Tuple
* .then(C.char('b').drop())
* .single();
* const value = parser.parse(Streams.ofString('ab')).value;
* test.equal(value, 'a');
* ```
*
* WARNING: This may change and throw an exception if it's not a single value.
* `first()` may appear.
*/
single(): SingleParser<T>;
/**
* Map the response value as the last value
* ```js
* const parser = C.char('a')
* .then(C.char('b'))
* .then(C.char('c'))
* .last();
* const value = parser.parse(Streams.ofString('abc')).value;
* test.deepEqual(value, ['a','b']); // parsing stopped at index 2
* ```
*
*/
last(): SingleParser<T>;
first(): SingleParser<T>;
/**
* Accepted with one or more occurrences.Will produce an Tuple of at least one T
*/
rep(): TupleParser<T>;
/**
* Accepted with zero or more occurrences. Will produce a Tuple of zero or more T
*/
optrep(): TupleParser<T>;
}
declare type MASALA_VOID_TYPE = symbol;
/**
* Special case of a [[SingleParser]], but easier to write.
* Note that `VoidParser.then(VoidParser)` will produce a [[TupleParser]] where
* inner value is `[]`. Same result with `optrep()` and `rep()`
*/
export interface VoidParser extends SingleParser<MASALA_VOID_TYPE> {
/**
* Combine two parsers for reading the stream
* ```js
* const parser = C.char('a').drop()
* .then(C.char('b').drop());
* const value = parser.parse(Streams.ofString('ab')).value;
* test.ok(value.size() === 0 );
* ```
* @param p next parser
*/
then(p: VoidParser): TupleParser<MASALA_VOID_TYPE>;
then<Y>(p: TupleParser<Y>): TupleParser<Y>;
then<Y>(p: SingleParser<Y>): TupleParser<Y>;
then<Y>(p: IParser<Y>): TupleParser<Y>;
or(other: VoidParser): VoidParser;
or<T, P extends IParser<T>>(other: P): VoidParser | P;
opt(): SingleParser<Option<MASALA_VOID_TYPE>>;
/**
* Accepted with one or more occurrences.Will produce an Tuple of at least one T
*/
rep(): TupleParser<MASALA_VOID_TYPE>;
/**
* Accepted with zero or more occurrences. Will produce a Tuple of zero or more T
*/
optrep(): TupleParser<MASALA_VOID_TYPE>;
}
export interface SingleParser<T> extends IParser<T> {
/**
* Combine two parsers for reading the stream
* ```js
* const parser = C.char('a')
* .then(C.char('b'));
* const value = parser.parse(Streams.ofString('ab')).value;
* test.equal(value.size(), 2 );
* ```
* @param p next parser
*/
then(p: VoidParser): TupleParser<T>;
then(p: IParser<T>): TupleParser<T>;
then<Y>(p: IParser<Y>): TupleParser<T | Y>;
or(other: SingleParser<T>): SingleParser<T>;
or<Y>(other: SingleParser<Y>): SingleParser<T | Y>;
or(other: TupleParser<T>): TupleParser<T>;
or<Y>(other: TupleParser<Y>): TupleParser<Y> | TupleParser<T>;
or<Y, P extends IParser<Y>>(other: P): SingleParser<T> | P;
opt(): SingleParser<Option<T>>;
/**
* Accepted with one or more occurrences.Will produce an Tuple of at least one T
*/
rep(): TupleParser<T>;
/**
* Accepted with zero or more occurrences. Will produce a Tuple of zero or more T
*/
optrep(): TupleParser<T>;
}
/**
* Parsers are most of the time made by combination of Parsers given by
* [[CharBundle]] (**C**), [[NumberBundle]] (**N**), and/or [[FlowBundle]] (**F**).
* You can also create a custom Parser from scratch.
* T is the type of the Response
*/
export interface IParser<T> {
/**
* Parses a stream of items (usually characters or tokens) and returns a Response
* @param stream
* @param index optional, index start in the stream
*/
parse<D>(stream: Stream<D>, index?: number): Response<T>;
/**
* Mainly designed for quick debugging or unit tests
* Parses the text and returns the value, or **undefined** if parsing has failed.
* @param text text to parse
*/
val(text: string): T;
/**
* Creates a sequence of tokens accepted by the parser
* ```js
* const abParser = C.char('a').then(C.char('b'))
* ```
* @param p
*/
then(p: VoidParser): TupleParser<T>;
then(p: IParser<T>): TupleParser<T>;
then<Y>(p: IParser<Y>): TupleParser<T | Y>;
/**
* Transforms the Response value
*
* ```js
* const parser = N.number().map (n => n*2);
* const value = parser.parse(Streams.ofString('1')).value;
* test.equal(value, 2 );
* ```
*
* @param f
*/
map<Y>(f: (value: T) => Y): SingleParser<Y>;
/**
* Create a new parser value *knowing* the current parsing value
*
* ```js
* // Next char must be the double of the previous
* function doubleNumber(param:number){
* return C.string(''+ (param*2) );
* }
*
* const combinator = N.digit()
* .flatMap(doubleNumber);
* let response = combinator.parse(Streams.ofString('12'));
* assertTrue(response.isAccepted());
* ```
*
* @param builder: the function building the parser
*/
flatMap<Y, P extends IParser<Y>>(builder: parserBuilder<Y, P>): P;
/**
* The parser will return the Neutral element for the Tuple
*/
drop(): VoidParser;
/**
* If accepted, the parser will return the given value
* @param value
*/
returns<Y>(value: Y): SingleParser<Y>;
/**
* Will display debug info when the previous parser is accepted.
*
* ```js
* // 'found digit' will be displayed only if N.digit() has been accepted
* N.digit().debug('found digit', false);
* ```
*
* @param hint: an indication that the previous parser attempted to parse
* @param showValue: if true, will display the response value given by the previous parser. true by default.
*/
debug(hint: string, showValue?: boolean): this;
/**
* Given an accepted parser and a response value, this parser.filter(predicate) could be rejected depending on
* the predicate applied on the response value.
* Filtering a rejected parser will always results in a rejected response.
* @param predicate : predicate applied on the response value
*/
filter(predicate: (value: T) => boolean): this;
/**
* If this parser is not accepted, the other will be used. It's often use with `F.try()` to
* make backtracking.
* @param other
*/
or<Y, P extends IParser<Y>>(other: P): IParser<T> | IParser<Y>;
/**
* If all parsers are accepted, the response value will be a Tuple of these values.
* It's a rare case where a Tuple could contain a Tuple.
* Warning: still experimental
* @param p
*/
and<P extends IParser<T>>(p: P): TupleParser<T>;
and<Y, P extends IParser<Y>>(p: P): TupleParser<T | Y>;
/**
* When `parser()` is rejected, then `parser().opt()` is accepted with an empty Option as response value.
* If `parser()` is accepted with a value `X`, then `parser().opt()` is accepted with an `Option.of(X)`
*/
opt(): IParser<Option<T>>;
/**
* Accepted with one or more occurrences.Will produce an Tuple of at least one T
*/
rep(): TupleParser<any>;
/**
* Accepted with zero or more occurrences. Will produce a Tuple of zero or more T
*/
optrep(): TupleParser<any>;
/**
* Search for next n occurrences. `TupleParser.occurence()` will continue to build one larger Tuple
* ```js
* let parser = C.char('a').then (C.char('b')).occurrence(3);
* let resp = parser.parse(Streams.ofString('ababab'));
* // -> Tuple { value: [ 'a', 'b', 'a', 'b', 'a', 'b' ] } }
* ```
* @param n
*/
occurrence(n: number): TupleParser<T>;
/**
* Specialized version of [[filter]] using `val` equality as predicate
* @param val the parser is rejected if it's response value is not `val`
*/
match(val: T): this;
/**
* Build a new High Leven parser that parses a ParserStream of tokens.
* Tokens are defined by `this` parser.
* It's internally used by [[GenLex]] which is more easy to work with
*
* @highParser high level parser
*/
chain<Y, P extends IParser<Y>>(highParser: P): P;
/**
* Accept the end of stream. If accepted, it will not change the response
*/
eos(): this;
}
/**
* A `parseFunction` is a function that returns a parser. To build this parser at runtime, any params are available.
*/
export type parseFunction<X, T> = (stream: Stream<X>, index?: number) => Response<T>;
interface Parser<T> extends IParser<T> {
}
/**
* Note: the documentation shows two parametric arguments `T` for `Parser<T,T>`, but it's really
* one argument. Looks like the only tooling bug, but sadly on the first line.
*/
export class Parser<T> {
new<D>(f: parseFunction<D, T>): Parser<T>;
}
interface CharBundle {
UTF8_LETTER: symbol;
OCCIDENTAL_LETTER: symbol;
ASCII_LETTER: symbol;
/**
* Accepts any letter, including one character emoji. Issue on two characters emoji
*/
utf8Letter(): SingleParser<string>;
/**
* Accepts any occidental letter, including most accents
*/
letter(): SingleParser<string>;
/**
* Choose s in [[UTF8_LETTER]], [[OCCIDENTAL_LETTER]], [[ASCII_LETTER]]
* Ascii letters are A-Za-z letters, excluding any accent
* @param s
*/
letterAs(s: symbol): SingleParser<string>;
/**
* Accepts a sequence of letters, joined as a string text
*/
letters(): SingleParser<string>;
/**
* Sequence of letters, joined as a string text. See [[letterAs]]
*/
lettersAs(s: symbol): SingleParser<string>;
/**
* Accepts an emoji
*
* WARNING: experimental, help needed; Working on emoji is a full-time specialist job.
*/
emoji(): SingleParser<string>;
/**
* Accepts any character that is not the `exclude` character
* @param exclude one character exclude
*/
notChar(exclude: string): SingleParser<string>;
/**
* Accepts any character that is not listed in the `exclude` chain
* @param exclude excluded characters
*
* WARNING: might be issues on emoji
*/
charNotIn(exclude: string): SingleParser<string>;
/**
* Accepts a char
* @param c
*/
char(c: string): SingleParser<string>;
/**
* Accepts any char listed in
* @param strings
*/
charIn(strings: string): SingleParser<string>;
/**
* Accepts a string
* @param string
*/
string(string: string): SingleParser<string>;
/**
* Accept one of these strings
* @param strings
*/
stringIn(strings: string[]): SingleParser<string>;
/**
* Accept anything that is not this string. Useful to build *stop*. See also [[FlowBundle.moveUntil]],
* [[FlowBundle.dropTo]]
* @param string
*/
notString(string: string): SingleParser<string>;
/**
* Single character inside single quote, like C/Java characters: `'a'`, `'x'`
*/
charLiteral(): SingleParser<string>;
/**
* String characters inside double quotes, like Java strings : `"Hello"`, `"World"`
*/
stringLiteral(): SingleParser<string>;
/**
* a-z single letter. WARNING: doesn't work yet on accents or utf-8 characters
*/
lowerCase(): SingleParser<string>;
/**
* A-Z single letter. WARNING: doesn't work yet on accents or utf-8 characters
*/
upperCase(): SingleParser<string>;
}
type parserBuilder<Y, P extends IParser<Y>> = (...rest: any[]) => P;
type extension<Y, T extends IParser<Y>> = T;
type Predicate<V> = (value: V) => boolean;
interface FlowBundle {
parse<Y, P extends IParser<Y>>(parser: P): P;
nop(): VoidParser;
layer<Y, P extends IParser<Y>>(parser: P): P;
try<Y, P extends IParser<Y>>(parser: P): P;
any(): SingleParser<any>;
subStream(length: number): VoidParser;
not<Y, P extends IParser<Y>>(parser: P): SingleParser<any>;
lazy<Y, P extends IParser<Y>>(builder: parserBuilder<Y, P>, args?: any[]): P;
returns<T>(value: T): SingleParser<T>;
error(): VoidParser;
eos(): SingleParser<Unit>;
satisfy<V>(predicate: Predicate<V>): SingleParser<any>;
startWith<V>(value: V): SingleParser<V>;
/**
* moveUntil moves the offset until stop is found and returns the text found between.
* The *stop* is **not** read
* @param stop
*/
moveUntil(stop: string): SingleParser<string>;
moveUntil(stops: string[]): SingleParser<string>;
moveUntil<Y>(p: IParser<Y>): SingleParser<string>;
/**
* Move until the stop, stop **included**, and drops it.
* @param s
*/
dropTo(s: string): VoidParser;
dropTo<Y>(p: IParser<Y>): VoidParser;
}
interface NumberBundle {
number(): SingleParser<number>;
integer(): SingleParser<number>;
digit(): SingleParser<number>;
digits(): SingleParser<number>;
}
type ParserOrString<T> = IParser<T> | string;
interface Token<T> extends SingleParser<T> {
}
type TokenCollection = {
[key: string]: Token<any>
};
export interface TokenResult<T> {
name: string;
value: T;
}
interface GenLex {
// TODO: make overload here
/**
*
* @param parser parser of the token
* @param name token name
* @param precedence the token with lowest precedence is taken before others.
*
* Choice with grammar is made after token selection !
*/
tokenize<T, P extends IParser<T>>(parser: P, name: string, precedence?: number): P;
use<T, P extends IParser<T>>(grammar: P): P;
tokens(): TokenCollection;
setSeparators(spacesCharacters: string): GenLex;
/**
* Select the parser used by genlex. It will be used as `separator.optrep().then(token).then(separator.optrep())`.
* So the separator must not be optional or it will make an infinite loop.
* The separation in your text can't be a strict one-time separation with Genlex.
* @param parser
*/
setSeparatorsParser<T>(parser: IParser<T>): GenLex;
/**
* Should separators be repeated ?
*
* `separators.optrep().then(myToken()).then(separators.optrep())`
* @param repeat default is true
*/
setSeparatorRepetition(repeat: boolean): GenLex;
/**
* tonkenize all items, given them the name of the token
* Exemple : keywords(['AND', 'OR']) will create the tokens named 'AND' and 'OR' with C.string('AND'), C.string('OR)
* @param tokens
*/
keywords(tokens: string[]): Array<Token<string>>;
get(tokenName: string): Token<any>;
}
export class GenLex implements GenLex {
}
export declare const F: FlowBundle;
export declare const C: CharBundle;
export declare const N: NumberBundle;
export declare const Streams: Streams;