diff --git a/docs/modules/Parser.ts.md b/docs/modules/Parser.ts.md index c854a26..ad0a01e 100644 --- a/docs/modules/Parser.ts.md +++ b/docs/modules/Parser.ts.md @@ -199,7 +199,10 @@ Matches the provided parser `p` that occurs between the provided `left` and `rig **Signature** ```ts -export declare const between: (left: Parser, right: Parser) => (p: Parser) => Parser +export declare const between: ( + left: Parser, + right: Parser +) => (p: Parser) => Parser ``` Added in v0.6.4 @@ -487,7 +490,7 @@ run(parser, 'a') // { _tag: 'Right', right: { _tag: 'Some', value: 'a' } } run(parser, 'b') -// { _tag: 'Left', left: { _tag: 'None' } } +// { _tag: 'Right', right: { _tag: 'None' } } ``` Added in v0.6.10 diff --git a/docs/modules/char.ts.md b/docs/modules/char.ts.md index 9c99a2c..48e4c98 100644 --- a/docs/modules/char.ts.md +++ b/docs/modules/char.ts.md @@ -28,6 +28,7 @@ Added in v0.6.0 - [upper](#upper) - [constructors](#constructors) - [char](#char) + - [charC](#charc) - [notChar](#notchar) - [notOneOf](#notoneof) - [oneOf](#oneof) @@ -213,6 +214,19 @@ export declare const char: (c: Char) => P.Parser Added in v0.6.0 +## charC + +The `charC` parser constructor returns a parser which matches only the +specified single character, case-insensitive + +**Signature** + +```ts +export declare const charC: (c: Char) => P.Parser +``` + +Added in v0.6.15 + ## notChar The `notChar` parser constructor makes a parser which will match any diff --git a/docs/modules/string.ts.md b/docs/modules/string.ts.md index 0f4281e..32872ed 100644 --- a/docs/modules/string.ts.md +++ b/docs/modules/string.ts.md @@ -26,7 +26,9 @@ Added in v0.6.0 - [spaces1](#spaces1) - [constructors](#constructors) - [oneOf](#oneof) + - [oneOfC](#oneofc) - [string](#string) + - [stringC](#stringc) - [destructors](#destructors) - [fold](#fold) @@ -171,14 +173,53 @@ Matches one of a list of strings. **Signature** ```ts -export declare function oneOf( - F: Functor1 & Foldable1 -): (ss: Kind) => P.Parser -export declare function oneOf(F: Functor & Foldable): (ss: HKT) => P.Parser +export declare const oneOf: { + < + U extends + | 'Option' + | 'ReadonlyRecord' + | 'Eq' + | 'Ord' + | 'NonEmptyArray' + | 'Array' + | 'ReadonlyNonEmptyArray' + | 'ReadonlyArray' + >( + F: Functor1 & Foldable1 + ): (ss: Kind) => P.Parser + (F: Functor & Foldable): (ss: HKT) => P.Parser +} ``` Added in v0.6.0 +## oneOfC + +Matches one of a list of strings, case-insensitive. + +**Signature** + +```ts +export declare const oneOfC: { + < + U extends + | 'Option' + | 'ReadonlyRecord' + | 'Eq' + | 'Ord' + | 'NonEmptyArray' + | 'Array' + | 'ReadonlyNonEmptyArray' + | 'ReadonlyArray' + >( + F: Functor1 & Foldable1 + ): (ss: Kind) => P.Parser + (F: Functor & Foldable): (ss: HKT) => P.Parser +} +``` + +Added in v0.6.15 + ## string Matches the exact string provided. @@ -191,6 +232,18 @@ export declare const string: (s: string) => P.Parser Added in v0.6.0 +## stringC + +Matches the exact string provided, case-insensitive + +**Signature** + +```ts +export declare const stringC: (s: string) => P.Parser +``` + +Added in v0.6.15 + # destructors ## fold diff --git a/examples/command.ts b/examples/command.ts index 45a8dab..75bbc24 100644 --- a/examples/command.ts +++ b/examples/command.ts @@ -99,8 +99,8 @@ export const Named = (name: string, value: string): Named => ({ _tag: 'Named', n export const Positional = (value: string): Positional => ({ _tag: 'Positional', value }) -export const FlagArg = (value: string): Args => ({ - flags: [value], +export const FlagArg = (values: string): Args => ({ + flags: values.split(''), named: R.empty, positional: A.empty }) @@ -164,7 +164,7 @@ const flag: P.Parser = pipe( const named: P.Parser = pipe( doubleDash, P.chain(() => P.sepBy1(equals, identifier)), - P.map(([name, value]) => Named(name, value)) + P.map(([name, ...values]) => Named(name, values.join('='))) ) const positional: P.Parser = pipe(C.many1(C.notSpace), P.map(Positional)) @@ -189,34 +189,44 @@ const ast = (command: string, source: string): P.Parser => { ) } -const parseCommand = (cmd: string, onLeft: (cmd: string) => E) => (source: string): Either => - pipe( - run(ast(cmd, source), source), - mapLeft(() => onLeft(cmd)) - ) +const parseCommand = (cmd: string, onLeft: (error: string) => E) => (source: string): Either => + pipe(run(ast(cmd, source), source), mapLeft(onLeft)) -const cmd = 'foo' -const source = 'foo ./bar -b --baz=qux' +// tslint:disable-next-line: no-console +const command = parseCommand('foo', e => console.error(e)) // tslint:disable-next-line: no-console -console.log(JSON.stringify(parseCommand(cmd, c => console.error(`command not found: ${c}`))(source), null, 2)) +console.log(JSON.stringify(command('foo ./bar -bd --baz=qux=45 --tar=get --t'), null, 2)) /* { - _tag: 'Right', - right: { - command: 'foo', - source: 'foo ./bar -b --baz=qux', - arguments: { - flags: [ - "b" + "_tag": "Right", + "right": { + "command": "foo", + "source": "foo ./bar -b --baz=qux=45 --tar=get --t", + "args": { + "flags": [ + "b", + "d" ], - named: { - baz: "qux", + "named": { + "baz": "qux=45", + "tar": "get", + "t": "" }, - positional: [ - "./bar", - ], + "positional": [ + "./bar" + ] } } } */ + +// tslint:disable-next-line: no-console +console.log(JSON.stringify(command('bar ./bar -bd --baz=qux=45 --tar=get --t'), null, 2)) +/* +> 1 | bar ./bar -bd --baz=qux=45 --tar=get --t + | ^ Expected: "foo" +{ + "_tag": "Left" +} +*/ diff --git a/package-lock.json b/package-lock.json index 32001f3..0439057 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "parser-ts", - "version": "0.6.12", + "version": "0.6.14", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/src/ParseResult.ts b/src/ParseResult.ts index 3074e51..97650fc 100644 --- a/src/ParseResult.ts +++ b/src/ParseResult.ts @@ -3,7 +3,7 @@ */ import { empty, getMonoid } from 'fp-ts/lib/Array' import { Either, left, right } from 'fp-ts/lib/Either' -import { getFirstSemigroup, getLastSemigroup, getStructSemigroup, Semigroup } from 'fp-ts/lib/Semigroup' +import { getFirstSemigroup, semigroupAny, getStructSemigroup, Semigroup } from 'fp-ts/lib/Semigroup' import { Stream } from './Stream' // ------------------------------------------------------------------------------------- @@ -103,12 +103,11 @@ export const extend = (err1: ParseError, err2: ParseError): ParseError< const getSemigroup = (): Semigroup> => ({ concat: (x, y) => { - if (x.input.cursor < y.input.cursor) return getLastSemigroup>().concat(x, y) - if (x.input.cursor > y.input.cursor) return getFirstSemigroup>().concat(x, y) - + if (x.input.cursor < y.input.cursor) return y + if (x.input.cursor > y.input.cursor) return x return getStructSemigroup>({ input: getFirstSemigroup>(), - fatal: getFirstSemigroup(), + fatal: semigroupAny, expected: getMonoid() }).concat(x, y) } diff --git a/src/Parser.ts b/src/Parser.ts index 6211f2c..a03bc18 100644 --- a/src/Parser.ts +++ b/src/Parser.ts @@ -180,7 +180,8 @@ export const either: (p: Parser, f: () => Parser) => Parser extend(e.left, err)) ) } @@ -265,13 +266,11 @@ export const many1: (p: Parser) => Parser> = * @category combinators * @since 0.6.0 */ -export const sepBy = (sep: Parser, p: Parser): Parser> => { - const nil: Parser> = of(A.empty) - return pipe( +export const sepBy = (sep: Parser, p: Parser): Parser> => + pipe( sepBy1(sep, p), - alt(() => nil) + alt(() => of>(A.empty)) ) -} /** * Matches the provided parser `p` one or more times, but requires the @@ -353,7 +352,7 @@ export const filter: { * @category combinators * @since 0.6.4 */ -export const between: (left: Parser, right: Parser) => (p: Parser) => Parser = ( +export const between: (left: Parser, right: Parser) => (p: Parser) => Parser = ( left, right ) => p => @@ -431,7 +430,7 @@ export const takeUntil: (predicate: Predicate) => Parser> = pr * // { _tag: 'Right', right: { _tag: 'Some', value: 'a' } } * * run(parser, 'b') - * // { _tag: 'Left', left: { _tag: 'None' } } + * // { _tag: 'Right', right: { _tag: 'None' } } * * @category combinators * @since 0.6.10 @@ -532,9 +531,7 @@ const map_: Monad2['map'] = (ma, f) => i => const ap_: Monad2['ap'] = (mab, ma) => chain_(mab, f => map_(ma, f)) const chain_: Chain2['chain'] = (ma, f) => seq(ma, f) const chainRec_: ChainRec2['chainRec'] = (a: A, f: (a: A) => Parser>): Parser => { - const split = (start: Stream) => ( - result: ParseSuccess> - ): E.Either, ParseResult> => + const split = (start: Stream, result: ParseSuccess>): E.Either, ParseResult> => E.isLeft(result.value) ? E.left({ value: result.value.left, stream: result.next }) : E.right(success(result.value.right, result.next, start)) @@ -544,7 +541,7 @@ const chainRec_: ChainRec2['chainRec'] = (a: A, f: (a: A) => Parse if (E.isLeft(result)) { return E.right(error(state.stream, result.left.expected, result.left.fatal)) } - return split(start)(result.right) + return split(start, result.right) }) } const alt_: Alt2['alt'] = (fa, that) => either(fa, that) diff --git a/src/char.ts b/src/char.ts index c9101c5..7484fc1 100644 --- a/src/char.ts +++ b/src/char.ts @@ -35,6 +35,16 @@ export const char: (c: Char) => P.Parser = c => `"${c}"` ) +/** + * The `charC` parser constructor returns a parser which matches only the + * specified single character, case-insensitive + * + * @category constructors + * @since 0.6.15 + */ +export const charC: (c: Char) => P.Parser = c => + P.either(char(c.toLowerCase()), () => char(c.toUpperCase())) + /** * The `notChar` parser constructor makes a parser which will match any * single character other than the one provided. diff --git a/src/string.ts b/src/string.ts index 42f16d4..4db745d 100644 --- a/src/string.ts +++ b/src/string.ts @@ -12,52 +12,86 @@ import * as C from './char' import * as P from './Parser' import * as S from './Stream' import * as PR from './ParseResult' +import { flow } from 'fp-ts/lib/function' +import { snoc } from 'fp-ts/lib/Array' // ------------------------------------------------------------------------------------- // constructors // ------------------------------------------------------------------------------------- /** - * Matches the exact string provided. - * - * @category constructors - * @since 0.6.0 + * @internal */ -export const string: (s: string) => P.Parser = s => +const string_ = (cp: (c: C.Char) => P.Parser, expected: (s: string) => string) => ( + s: string +): P.Parser => P.expected( - P.ChainRec.chainRec(s, acc => + P.ChainRec.chainRec], string>([s, []], ([sTail, acc]) => pipe( - charAt(0, acc), + charAt(0, sTail), O.fold( - () => P.of(E.right(s)), + () => P.of(E.right(acc.join(''))), c => pipe( - C.char(c), - P.chain(() => P.of(E.left(acc.slice(1)))) + cp(c), + P.chain(m => P.of(E.left([sTail.slice(1), snoc(acc, m)]))) ) ) ) ), - JSON.stringify(s) + expected(s) ) /** - * Matches one of a list of strings. + * Matches the exact string provided. * * @category constructors * @since 0.6.0 */ -export function oneOf(F: Functor1 & Foldable1): (ss: Kind) => P.Parser -export function oneOf(F: Functor & Foldable): (ss: HKT) => P.Parser -export function oneOf(F: Functor & Foldable): (ss: HKT) => P.Parser { - return ss => - F.reduce(ss, P.fail(), (p, s) => - pipe( - p, - P.alt(() => string(s)) - ) +export const string: (s: string) => P.Parser = string_(C.char, JSON.stringify) + +/** + * Matches the exact string provided, case-insensitive + * + * @category constructors + * @since 0.6.15 + */ +export const stringC: (s: string) => P.Parser = string_(C.charC, s => `${JSON.stringify(s)}/i`) + +/** + * @internal + */ +const oneOf_ = (sc: (s: string) => P.Parser) => (F: Functor & Foldable) => ( + ss: HKT +): P.Parser => + F.reduce(ss, P.fail(), (p, s) => + pipe( + p, + P.alt(() => sc(s)) ) -} + ) + +/** + * Matches one of a list of strings. + * + * @category constructors + * @since 0.6.0 + */ +export const oneOf: { + (F: Functor1 & Foldable1): (ss: Kind) => P.Parser + (F: Functor & Foldable): (ss: HKT) => P.Parser +} = oneOf_(string) + +/** + * Matches one of a list of strings, case-insensitive. + * + * @category constructors + * @since 0.6.15 + */ +export const oneOfC: { + (F: Functor1 & Foldable1): (ss: Kind) => P.Parser + (F: Functor & Foldable): (ss: HKT) => P.Parser +} = oneOf_(stringC) // ------------------------------------------------------------------------------------- // destructors @@ -160,9 +194,9 @@ export const int: P.Parser = P.expected( export const float: P.Parser = P.expected( pipe( fold([maybe(C.char('-')), C.many(C.digit), maybe(fold([C.char('.'), C.many1(C.digit)]))]), - P.chain(s => - pipe( - fromString(s), + P.chain( + flow( + fromString, O.fold(() => P.fail(), P.succeed) ) ) diff --git a/test/Parser.ts b/test/Parser.ts index 614148f..00aa497 100644 --- a/test/Parser.ts +++ b/test/Parser.ts @@ -1,10 +1,11 @@ import * as assert from 'assert' import { none, some } from 'fp-ts/lib/Option' import { pipe } from 'fp-ts/lib/pipeable' - import { char as C, parser as P, string as S } from '../src' import { error, success } from '../src/ParseResult' import { stream } from '../src/Stream' +import { constant } from 'fp-ts/function' +import { string } from '../src/string' describe('Parser', () => { it('eof', () => { @@ -33,6 +34,10 @@ describe('Parser', () => { const parser4 = P.either(C.char('a'), () => S.string('bb')) assert.deepStrictEqual(S.run('bc')(parser4), error(stream(['b', 'c'], 1), ['"bb"'])) + + const fatalParser: P.Parser = i => error(i, ['expected'], true) + const parser5 = P.either(C.char('a'), () => fatalParser) + assert.deepStrictEqual(S.run('c')(parser5), error(stream(['c']), ['"a"', 'expected'], true)) }) it('map', () => { @@ -62,6 +67,7 @@ describe('Parser', () => { it('cutWith', () => { const parser = P.cutWith(C.char('a'), C.char('b')) + assert.deepStrictEqual(S.run('ab')(parser), success('b', stream(['a', 'b'], 2), stream(['a', 'b']))) assert.deepStrictEqual(S.run('ac')(parser), error(stream(['a', 'c'], 1), ['"b"'], true)) }) @@ -82,6 +88,7 @@ describe('Parser', () => { const parser = P.sepBy1(C.char(','), C.char('a')) assert.deepStrictEqual(S.run('')(parser), error(stream([]), ['"a"'])) assert.deepStrictEqual(S.run('a,b')(parser), success(['a'], stream(['a', ',', 'b'], 1), stream(['a', ',', 'b']))) + assert.deepStrictEqual(S.run('ab')(parser), success(['a'], stream(['a', 'b'], 1), stream(['a', 'b']))) }) it('sepByCut', () => { @@ -113,6 +120,20 @@ describe('Parser', () => { assert.deepStrictEqual(S.run('(1')(parser), error(stream(['(', '1'], 2), ['")"'])) assert.deepStrictEqual(S.run('(1)')(parser), success(1, stream(['(', '1', ')'], 3), stream(['(', '1', ')']))) }) + + it('triple polymorphic', () => { + const boolTrue: P.Parser = P.expected( + pipe(string('true'), P.map(constant(true))), + 'a true boolean' + ) + const betweenParens = P.between(C.char('('), boolTrue) + const parser = betweenParens(S.int) + assert.deepStrictEqual(S.run('(1')(parser), error(stream(['(', '1'], 2), ['a true boolean'])) + assert.deepStrictEqual( + S.run('(1true')(parser), + success(1, stream(['(', '1', 't', 'r', 'u', 'e'], 6), stream(['(', '1', 't', 'r', 'u', 'e'])) + ) + }) }) describe('surroundedBy', () => { diff --git a/test/char.ts b/test/char.ts index 01d264b..87b4f7b 100644 --- a/test/char.ts +++ b/test/char.ts @@ -7,9 +7,22 @@ describe('char', () => { it('char', () => { const parser = C.char('a') assert.deepStrictEqual(S.run('ab')(parser), success('a', stream(['a', 'b'], 1), stream(['a', 'b']))) + assert.deepStrictEqual(S.run('Ab')(parser), error(stream(['A', 'b']), ['"a"'])) assert.deepStrictEqual(S.run('bb')(parser), error(stream(['b', 'b']), ['"a"'])) }) + it('charC', () => { + const parser = C.charC('a') + assert.deepStrictEqual(S.run('Ab')(parser), success('A', stream(['A', 'b'], 1), stream(['A', 'b']))) + assert.deepStrictEqual(S.run('ab')(parser), success('a', stream(['a', 'b'], 1), stream(['a', 'b']))) + assert.deepStrictEqual(S.run('bb')(parser), error(stream(['b', 'b']), ['"a"', '"A"'])) + + const parser2 = C.charC('A') + assert.deepStrictEqual(S.run('Ab')(parser2), success('A', stream(['A', 'b'], 1), stream(['A', 'b']))) + assert.deepStrictEqual(S.run('ab')(parser2), success('a', stream(['a', 'b'], 1), stream(['a', 'b']))) + assert.deepStrictEqual(S.run('bb')(parser2), error(stream(['b', 'b']), ['"a"', '"A"'])) + }) + it('run', () => { const parser = C.char('a') assert.deepStrictEqual(S.run('a')(parser), success('a', stream(['a'], 1), stream(['a']))) diff --git a/test/string.ts b/test/string.ts index 31dd77f..7ebb2f3 100644 --- a/test/string.ts +++ b/test/string.ts @@ -27,6 +27,16 @@ describe('string', () => { assert.deepStrictEqual(S.run('barfoo')(parser), error(stream(['b', 'a', 'r', 'f', 'o', 'o']), ['"foo"'])) }) + it('should parse a non-empty string case insensitive', () => { + const parser = S.stringC('fOo') + assert.deepStrictEqual(S.run('FoO')(parser), success('FoO', stream(['F', 'o', 'O'], 3), stream(['F', 'o', 'O']))) + assert.deepStrictEqual( + S.run('fOobAr')(parser), + success('fOo', stream(['f', 'O', 'o', 'b', 'A', 'r'], 3), stream(['f', 'O', 'o', 'b', 'A', 'r'])) + ) + assert.deepStrictEqual(S.run('bArfOo')(parser), error(stream(['b', 'A', 'r', 'f', 'O', 'o']), ['"fOo"/i'])) + }) + it('should handle long strings without exceeding the recursion limit (#41)', () => { const lorem = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.' const target = intercalate(monoidString, RA.Foldable)(' ', RA.replicate(1000, lorem)) @@ -60,6 +70,19 @@ describe('string', () => { assert.deepStrictEqual(S.run('ca')(parser), error(stream(['c', 'a']), ['"a"', '"b"'])) }) + it('oneOfC', () => { + const parser = S.oneOfC(array)(['A', 'b']) + assert.deepStrictEqual(S.run('a')(parser), success('a', stream(['a'], 1), stream(['a']))) + assert.deepStrictEqual(S.run('A')(parser), success('A', stream(['A'], 1), stream(['A']))) + assert.deepStrictEqual(S.run('b')(parser), success('b', stream(['b'], 1), stream(['b']))) + assert.deepStrictEqual(S.run('B')(parser), success('B', stream(['B'], 1), stream(['B']))) + assert.deepStrictEqual(S.run('ab')(parser), success('a', stream(['a', 'b'], 1), stream(['a', 'b']))) + assert.deepStrictEqual(S.run('Ab')(parser), success('A', stream(['A', 'b'], 1), stream(['A', 'b']))) + assert.deepStrictEqual(S.run('ba')(parser), success('b', stream(['b', 'a'], 1), stream(['b', 'a']))) + assert.deepStrictEqual(S.run('Ba')(parser), success('B', stream(['B', 'a'], 1), stream(['B', 'a']))) + assert.deepStrictEqual(S.run('ca')(parser), error(stream(['c', 'a']), ['"A"/i', '"b"/i'])) + }) + it('int', () => { const parser = S.int assert.deepStrictEqual(S.run('1')(parser), success(1, stream(['1'], 1), stream(['1'])))