From fa822ea3ac92dddb8dc4b0e4d406cfc18b2c1e65 Mon Sep 17 00:00:00 2001 From: rowan Date: Fri, 18 Apr 2025 03:59:50 -0500 Subject: [PATCH] unit tests; improved iterator --- jsconfig.json | 4 +- package-lock.json | 34 +++------------- src/byte.js | 83 +++++++++++++++++++++++++++++++++++++++ src/char.js | 41 ------------------- src/clone.js | 11 ++++++ src/combinator.js | 42 +++++++++++--------- src/cond.js | 16 +++++--- src/fn.js | 62 +++++++---------------------- src/index.js | 4 +- src/iter.js | 93 ++++++++++++++++++++++---------------------- src/multi.js | 48 +++++++++++++++++++++++ src/seq.js | 71 --------------------------------- src/state.js | 57 +++++++++++++++++++-------- src/tuple.js | 61 +++++++++++++++++++++++++++++ tests/units/bytes.js | 81 ++++++++++++++++++++++++++++++++++++++ tests/units/index.js | 11 +----- tests/units/iter.js | 21 ++++++++++ 17 files changed, 450 insertions(+), 290 deletions(-) create mode 100644 src/byte.js delete mode 100644 src/char.js create mode 100644 src/clone.js create mode 100644 src/multi.js delete mode 100644 src/seq.js create mode 100644 src/tuple.js create mode 100644 tests/units/bytes.js create mode 100644 tests/units/iter.js diff --git a/jsconfig.json b/jsconfig.json index e74eccc..ab41553 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,13 +1,11 @@ { "compilerOptions": { "strict": true, + "moduleResolution": "node", "module": "es2020", "target": "es6", "lib": ["es2022", "dom"], "checkJs": false, - "paths": { - "/*": ["./*"] - } }, "exclude": [ "node_modules" diff --git a/package-lock.json b/package-lock.json index e48dff3..59a508c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -41,7 +40,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -58,7 +56,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -75,7 +72,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -92,7 +88,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -109,7 +104,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -126,7 +120,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -143,7 +136,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -160,7 +152,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -177,7 +168,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -194,7 +184,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -211,7 +200,6 @@ "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -228,7 +216,6 @@ "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -245,7 +232,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -262,7 +248,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -279,7 +264,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -296,7 +280,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -313,7 +296,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -330,7 +312,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -347,7 +328,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -364,7 +344,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -381,7 +360,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -398,7 +376,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -415,7 +392,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -432,7 +408,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -446,7 +421,6 @@ "version": "0.25.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", - "dev": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -485,20 +459,22 @@ }, "node_modules/folktest": { "version": "1.0.0", - "resolved": "git+https://git.kitsu.cafe/rowan/folktest.git#708d44f1215be33fcceba426029f44b4f963dbe5", + "resolved": "git+https://git.kitsu.cafe/rowan/folktest.git#cbf48ff3b1334eb883f202a77a5bc89d24534520", "dev": true, "license": "GPL-3.0-or-later" }, "node_modules/izuna": { "version": "1.0.0", - "resolved": "git+https://git.kitsu.cafe/rowan/izuna.git#e11a0870c27eeb5ea1a4ae3fedccca008eda15c2", + "resolved": "git+https://git.kitsu.cafe/rowan/izuna.git#4ab7c265d83856f2dc527780a3ac87b3d54676f1", "license": "GPL-3.0-or-later" }, "node_modules/kojima": { "version": "1.0.0", - "resolved": "git+https://git.kitsu.cafe/rowan/kojima.git#d6615248572d2e5c16661d8aab0650ae28aeb6c2", + "resolved": "git+https://git.kitsu.cafe/rowan/kojima.git#b997bca5e31323bb81b38fafe5f823cae6e574de", "license": "GPL-3.0-or-later", "dependencies": { + "esbuild": "^0.25.2", + "izuna": "git+https://git.kitsu.cafe/rowan/izuna.git", "typescript": "^5.8.2" } }, diff --git a/src/byte.js b/src/byte.js new file mode 100644 index 0000000..679544f --- /dev/null +++ b/src/byte.js @@ -0,0 +1,83 @@ +import { chain, curry } from 'izuna' +import { any, map, seq } from './combinator.js' +import { not } from './cond.js' +import { Digits, LowerAlpha, UpperAlpha } from './const.js' +import { clone, fail, join, mapStr, next, succeed } from './fn.js' +import { ok } from 'kojima' + +/** @import { ParserState } from './state.js' */ + +export const char = curry( + /** + * @param {string} ch + * @param {ParserState} state + */ + (ch, state) => { + return next(state) === ch ? succeed(ch, state) : fail(`could not parse ${ch} `, state) + }) + +export const tag = curry( + /** + * @param {string} str + * @param {ParserState} state + */ + (str, state) => ( + map( + join(''), + seq(...mapStr(char, str)) + )(state) + )) + +export const charNoCase = curry((ch, state) => { + return any( + char(ch.toLowerCase()), + char(ch.toUpperCase()) + )(state) +}) + +export const tagNoCase = curry( + (str, state) => ( + map( + join(''), + seq(...mapStr(charNoCase, str)) + )(state) + )) + +export const anyChar = state => { + const ch = next(state) + return !!ch ? succeed(ch, state) : fail('end of input', state) +} + +export const take = curry( + (limit, state) => { + let res = ok(state) + + while (res.isOk() && limit > 0) { + res = chain(anyChar, res) + limit -= 1 + } + return res + } +) + +export const oneOf = curry( + /** + * @param {string} str + * @param {ParserState} state + */ + (str, state) => ( + any(...mapStr(char, str))(state) + )) + +export const noneOf = curry((str, state) => seq(not(any(oneOf(str), eof)), anyChar)(state)) + +export const digit = oneOf(Digits) +export const lowerAlpha = oneOf(LowerAlpha) +export const upperAlpha = oneOf(UpperAlpha) +export const alpha = any(lowerAlpha, upperAlpha) +export const alphanumeric = any(alpha, digit) + +export const eof = state => { + return clone(state).next().done ? succeed([], state) : fail('not end of stream', state) +} + diff --git a/src/char.js b/src/char.js deleted file mode 100644 index 79c66f7..0000000 --- a/src/char.js +++ /dev/null @@ -1,41 +0,0 @@ -import { State } from './state.js' -import { any, anyOf, map } from './combinator.js' -import { Alpha, Alphanumeric, Digits, LowerAlpha, UpperAlpha } from './const.js' -import { fail, join, mapStr, next, succeed } from './fn.js' -import { seq } from './seq.js' -import { curry } from 'izuna' - -/** @import { ParserState } from './state.js' */ - -export const char = curry( - /** - * @param {string} ch - * @param {ParserState} state - */ - (ch, state) => ( - next(state) === ch ? succeed(ch, state) : fail(`could not parse ${ch} `, state) - )) - -export const str = curry( - /** - * @param {string} str - * @param {ParserState} state - */ - (str, state) => ( - map( - join(''), - seq(...mapStr(char, str)) - )(state) - )) - -export const anyChar = state => { - const ch = next(state) - return !!ch ? succeed(ch, state) : fail('end of input', state) -} - -export const digit = anyOf(Digits) -export const lowerAlpha = anyOf(LowerAlpha) -export const upperAlpha = anyOf(UpperAlpha) -export const alpha = any(lowerAlpha, upperAlpha) -export const alphanumeric = any(alpha, digit) - diff --git a/src/clone.js b/src/clone.js new file mode 100644 index 0000000..453e17c --- /dev/null +++ b/src/clone.js @@ -0,0 +1,11 @@ +export const Clone = Symbol('clone') + +export const clone = target => { + if (target[Clone]) { + return target[Clone]() + } else { + return structuredClone(target) + } +} + + diff --git a/src/combinator.js b/src/combinator.js index 87c3c84..8862a95 100644 --- a/src/combinator.js +++ b/src/combinator.js @@ -1,9 +1,28 @@ -import { char } from './char.js' -import { clone, diff, fail, mapStr, succeed, Tuple } from './fn.js' -import { curry } from 'izuna' +import { chain, curry } from 'izuna' +import { ok } from 'kojima' +import { clone, diff, fail, succeed } from './fn.js' +import { state as State } from './state.js' /** @import { ParserState } from './state.js' */ +/** + * @param {...Parser} parsers + */ +export const seq = (...parsers) => + /** @param {ParserState} state */ + state => { + let acc = ok(state) + for (const parser of parsers) { + if (acc.isOk()) { + acc = chain(parser, acc) + } else { + break + } + } + + return acc + } + /** * @param {...any} parsers */ @@ -13,6 +32,7 @@ export const any = (...parsers) => */ state => { for (const parser of parsers) { + //console.log('combinator.js::35/any:', state) const result = parser(clone(state)) if (result.isOk()) { return result @@ -22,15 +42,6 @@ export const any = (...parsers) => return fail('no matching parsers', state) } -export const anyOf = curry( - /** - * @param {string} str - * @param {ParserState} state - */ - (str, state) => ( - any(...mapStr(char, str))(state) - )) - export const map = curry( /** * @param {(...args: any[]) => any} fn @@ -42,8 +53,7 @@ export const map = curry( try { /** @type {Result} */ const parsed = fn(diff(state[0], result[0])) - const backtrack = Tuple(state[0], result[1]) - + const backtrack = State(result[1], state[0]) return succeed(parsed, backtrack) } catch (e) { @@ -52,7 +62,3 @@ export const map = curry( }) }) -export const eof = state => { - return clone(state).next().done ? succeed([], state) : fail('not end of stream', state) -} - diff --git a/src/cond.js b/src/cond.js index aca738d..9ca4a09 100644 --- a/src/cond.js +++ b/src/cond.js @@ -1,8 +1,8 @@ import { ParseError } from './state.js' -import { clone, fail, fork, succeed } from './fn.js' -import { anyChar } from './char.js' +import { clone, fail, succeed } from './fn.js' +import { anyChar } from './byte.js' import { ok } from 'kojima' -import { curry, pipe } from 'izuna' +import { chain, curry, pipe } from 'izuna' /** @import { Result } from '../vendor/kojima/src/index.js' */ /** @import { ParserState } from './state.js' */ @@ -21,7 +21,7 @@ export const not = curry((parser, state) => { const result = parser(clone(state)) if (result.isOk()) { - return fail(`'not' parser failed for ${parser.name}`, state) + return fail(`'not' parser failed`, state) } else { return succeed([], state) } @@ -31,7 +31,6 @@ export const until = curry((parser, state) => { let result = ok(state) while (result.isOk()) { - console.log(parser.name, state) result = result.chain(pipe(clone, parser)) if (result.isOk()) { break @@ -43,6 +42,13 @@ export const until = curry((parser, state) => { return result }) +export const verify = curry((parser, predicate, state) => { + const result = chain(parser, clone(state)) + return chain(x => ( + predicate(x) ? result : fail('verification failed', state) + ), result) +}) + export const skip = curry((parser, state) => { }) diff --git a/src/fn.js b/src/fn.js index b8c2390..620211f 100644 --- a/src/fn.js +++ b/src/fn.js @@ -1,64 +1,26 @@ -import { ParseError } from './state.js' -import { Iter } from './iter.js' import { err, ok } from 'kojima' -import { curry } from 'izuna' +import { concat, curry } from 'izuna' +import { ParseError, state } from './state.js' +import { IndexableIterator } from './iter.js' -/** @import { ParserState } from './state.js'* / +/** @import { ParserState } from './state.js' */ -/** - * @param {...any} values - */ -export const Tuple = (...values) => Object.freeze(values) -/** - * @template T - * @param {Iterator | Iterable} iterable - * @param {number} [n=2] - */ -function tee(iterable, n = 2) { - const iterator = Iter.from(iterable) - - /** - * @param {{ next: any, value: T }} current - */ - function* gen(current) { - while (true) { - if (!current.next) { - const { done, value } = iterator.next() - if (done) return - current.next = { value } - } - - current = current.next - yield current.value - } - } - - return Array(n).fill({}).map(gen) -} - -/** - * @param {import('./state.js').ParserState} state - */ -export const fork = ([tokens, state]) => { - const [a, b] = tee(state) - return Tuple( - Tuple(tokens.slice(), a), - Tuple(tokens.slice(), b), - ) -} +export const iter = value => new IndexableIterator(value) /** * @param {ParserState} state */ -export const clone = ([h, iter]) => [h, iter.clone()] +export const clone = (state) => state.clone() /** * @template T * @param {T | T[]} v * @param {ParserState} state */ -export const succeed = (v, [x, y]) => ok(Tuple(x.concat(v), y)) +export const succeed = (v, [x, y]) => { + return ok(state(y, concat(v, x))) +} /** * @param {string} msg @@ -77,7 +39,7 @@ export const nth = (n, iter) => iter[n] /** * @param {ParserState} state */ -export const next = state => state[1].next().value +export const next = state => state.next().value /** * @template T @@ -99,7 +61,9 @@ export const mapStr = curry( * @param {(...args: any[]) => any} fn * @param {string} str */ - (fn, str) => Array.from(str).map(v => fn(v)) + (fn, str) => Array.from(str).flatMap(x => { + return fn(x) + }) ) diff --git a/src/index.js b/src/index.js index a797e01..604d8f2 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,6 @@ -export * from './char.js' +export * from './byte.js' export * from './combinator.js' export * from './cond.js' -export * from './seq.js' +export * from './multi.js' export * from './state.js' diff --git a/src/iter.js b/src/iter.js index 0ad05ce..fd88dda 100644 --- a/src/iter.js +++ b/src/iter.js @@ -1,21 +1,50 @@ +import { Clone } from './clone.js' + +export class IndexableIterator extends Iterator { + _value + _index + + constructor(value, index = 0) { + super() + + this._value = value + this._index = index + } + + clone() { + return this[Clone]() + } + + done() { + return this._index >= this._value.length + } + + next() { + if (this.done()) { + return { value: undefined, done: true } + } else { + return { value: this._value[this._index++], done: false } + } + } + + [Clone]() { + return new IndexableIterator( + this._value, + this._index + ) + } + + [Symbol.iterator]() { + return this.clone() + } +} + /** * @template T * @implements Iterator * @implements Iterable */ -export class Iter { - _iterator - _source - - /** - * @param {Iterator} iterator - * @param {Iterabl} [source] - */ - constructor(iterator, source) { - this._iterator = iterator - this._source = source - } - +class Iter { /** * @template T * @param {any} value @@ -34,6 +63,10 @@ export class Iter { return value } + if (Array.isArray(value) || typeof value === 'string') { + return new IndexableIterator(value) + } + if (Iter._isIterable(value)) { const iterator = value[Symbol.iterator]() @@ -41,44 +74,12 @@ export class Iter { return iterator } - return new Iter(iterator, value) + return new this(iterator, value) } throw new TypeError('object is not an iterator') } - clone() { - if (this._source) { - return Iter.from(this._source) - } - - throw new Error('Cannot clone Iterator: not created from an iterable') - } - - /** - * @param {any} [value] - */ - next(value) { - const n = this._iterator.next(value) - return n - } - - /** - * @param {any} [value] - */ - return(value) { - // @ts-ignore - return this._iterator.return(value) - } - - /** - * @param {any} err - */ - throw(err) { - // @ts-ignore - return this._iterator.throw(err) - } - /** * @param {number} limit */ diff --git a/src/multi.js b/src/multi.js new file mode 100644 index 0000000..dff32c6 --- /dev/null +++ b/src/multi.js @@ -0,0 +1,48 @@ +import { ok } from 'kojima' +import { curry, } from 'izuna' +import { clone } from './fn.js' +import { verify } from './cond.js' +import { anyChar } from './byte.js' + +/** @import { ParseError, ParserState } from './state.js' */ +/** @import { Result } from '../vendor/kojima/src/index.js' */ + +export const takeWhile = curry((predicate, state) => { + let result = ok(state) + + while (result.isOk()) { + result = verify(anyChar, predicate, state) + } + + return result +}) + +export const takeUntil = curry((parser, state) => not(takeWhile(parser, state))) + +export const skip = curry((parser, state) => { + const tokens = state[0] + const result = parser(state) + if (result.isOk()) { + return result.map(other => [tokens, other[1]]) + } else { + return result + } +}) + +export const many = curry((parser, state) => { + let result = ok(state) + + while (true) { + const res = parser(clone(state)) + if (res.isOk()) { + result = res + } else { + break + } + } + + return result +}) + +export const many1 = parser => seq(parser, many(parser)) + diff --git a/src/seq.js b/src/seq.js deleted file mode 100644 index 335d028..0000000 --- a/src/seq.js +++ /dev/null @@ -1,71 +0,0 @@ -import { clone, diff, fail, succeed, Tuple } from './fn.js' -import { anyChar } from './char.js' -import { ok } from 'kojima' -import { curry } from 'izuna' - -/** @import { ParseError, ParserState } from './state.js' */ -/** @import { Result } from '../vendor/kojima/src/index.js' */ - -/** - * @typedef {(value: any) => Result} Parser - */ - -export const take = n => state => { - let result = anyChar(state) - for (let i = n; i > 0; i--) { - if (result.isErr()) { - return result.chain(e => fail(`"take(${n})" failed`, state, e)) - } - - result = result.chain(anyChar) - } - - return result -} - -export const skip = parser => state => { - const tokens = state[0] - const result = parser(state) - if (result.isOk()) { - return result.map(other => [tokens, other[1]]) - } else { - return result - } -} - -/** - * @param {...Parser} parsers - */ -export const seq = (...parsers) => - /** @param {ParserState} state */ - state => { - let acc = ok(state) - for (const parser of parsers) { - if (acc.isOk()) { - acc = acc.chain(parser) - } else { - break - } - } - - return acc - } - - -export const many = curry((parser, state) => { - let result = ok(state) - - while (true) { - const res = parser(clone(state)) - if (res.isOk()) { - result = res - } else { - break - } - } - - return result -}) - -export const many1 = parser => seq(parser, many(parser)) - diff --git a/src/state.js b/src/state.js index 94c661b..28e48ae 100644 --- a/src/state.js +++ b/src/state.js @@ -1,8 +1,9 @@ -import { Iter } from './iter.js' -import { seq } from './seq.js' -import { until } from './cond.js' -import { eof } from './combinator.js' -import { curry, pipe } from 'izuna' +import { ok } from 'kojima' +import { chain, compose, curry, pipe } from 'izuna' +import { eof } from './byte.js' +import { Tuple } from './tuple.js' +import { iter } from './fn.js' +import { Clone } from './clone.js' /** * @typedef {Readonly<[any[], Iterator]>} ParserState @@ -20,18 +21,40 @@ export class ParseError extends Error { } } -/** - * @param {any} value - * @returns {ParserState} - */ -export const State = value => Object.freeze([[], Iter.from(value)]) +export class State extends Tuple { + constructor(remaining, read = []) { + super([['read', read], ['remaining', remaining]]) + } -export const parse = curry((parser, input) => parser(State(input))) -export const parseAll = curry((parser, input) => pipe( - State, - seq( - parser, - until(eof) - ) + static from(values) { + return new State(iter(values)) + } + + next() { + return this.remaining.next() + } + + toString() { + return `State(${this.read}, ${[...this.clone().remaining]})` + } + + [Clone]() { + return Object.freeze(new State(this.remaining.clone(), this.read)) + } + + [Symbol.iterator]() { + return super[Symbol.iterator]() + } +} + +export const state = (remaining, read) => new State(remaining, read) + +export const parse = curry((parser, input) => pipe( + iter, + state, + ok, + chain(parser), )(input)) +export const parseAll = curry((parser, input) => compose(eof, parse(parser), input)) + diff --git a/src/tuple.js b/src/tuple.js new file mode 100644 index 0000000..cbfb911 --- /dev/null +++ b/src/tuple.js @@ -0,0 +1,61 @@ +import { entries } from 'izuna' +import { Clone } from './clone.js' +import { IndexableIterator } from './iter.js' + +/** + * @param {...any} values + */ +export class Tuple { + _values + _length + + get length() { + return this._length + } + + constructor(values) { + if (values.length > 0) { + this._length = values.length + values.forEach(this._setEntry.bind(this)) + } + + this._values = values + } + + _setEntry([key, value], index) { + if (typeof key !== 'number') { + Object.defineProperty(this, key, { + get() { + return this[index] + } + }) + } + + this[index] = value + } + + static from(values) { + return Object.freeze(new this(entries(values))) + } + + + toString() { + return `(${this._values.map(([_, v]) => v.toString())})` + } + + clone() { + return this[Clone]() + } + + [Clone]() { + return Object.freeze(new this.constructor(this._values)) + } + + [Symbol.iterator]() { + return new IndexableIterator(this._values.map(([_, v]) => v)) + } +} + +export const tuple = Tuple.from + + diff --git a/tests/units/bytes.js b/tests/units/bytes.js new file mode 100644 index 0000000..4580328 --- /dev/null +++ b/tests/units/bytes.js @@ -0,0 +1,81 @@ +import { it, assert, assertEq } from 'folktest' +import { char, charNoCase, noneOf, oneOf, parse, tag, tagNoCase, take, takeWhile } from '../../src/index.js' +import { Alphanumeric } from '../../src/const.js' +import { chain, curry, map } from 'izuna' + +const assertState = expected => state => { + assertEq(expected, state[0]) +} + +const parseEq = curry((parser, input, expected) => + chain(assertState(expected), parse(parser, input)) +) + +const parseErr = curry((parser, input) => + assert(parse(parser, input).isErr(), `expected an error but "${input}" parsed successfully`) +) + +export const Byte = [ + it('char', () => { + const parser = char('a') + + parseEq(parser, 'abc', ['a']) + assert(parse(parser, ' abc').isErr()) + assert(parse(parser, 'bc').isErr()) + assert(parse(parser, '').isErr()) + }), + + it('oneOf', () => { + parse(oneOf('abc'), 'b').chain(assertState('b')) + assert(parse(oneOf('a'), 'bc').isErr()) + assert(parse(oneOf('a'), '').isErr()) + }), + + it('noneOf', () => { + parse(noneOf('abc'), 'z').chain(assertState(['z'])) + assert(parse(noneOf('ab'), 'a').isErr()) + assert(parse(noneOf('ab'), '').isErr()) + }), + + it('tag', () => { + const parser = tag('Hello') + parse(parser, 'Hello, World!').chain(assertState(['Hello'])) + assert(parse(parser, 'Something').isErr()) + assert(parse(parser, '').isErr()) + }), + + it('charNoCase', () => { + const parser = charNoCase('a') + parse(parser, 'abc').chain(assertState(['a'])) + parse(parser, 'Abc').chain(assertState(['A'])) + assert(parse(parser, ' abc').isErr()) + assert(parse(parser, 'bc').isErr()) + assert(parse(parser, '').isErr()) + }), + + it('tagNoCase', () => { + const parser = tagNoCase('hello') + parse(parser, 'Hello, World!').chain(assertState(['Hello'])) + parse(parser, 'hello, World!').chain(assertState(['hello'])) + parse(parser, 'HeLlO, World!').chain(assertState(['HeLlO'])) + assert(parse(parser, 'Something').isErr()) + assert(parse(parser, '').isErr()) + }), + + it('take', () => { + const parser = take(6) + parse(parser, '1234567').chain(assertState('123456'.split(''))) + parse(parser, 'things').chain(assertState('things'.split(''))) + assert(parse(parser, 'short').isErr()) + assert(parse(parser, '').isErr()) + }), + + it('takeWhile', () => { + const parser = takeWhile(x => Alphanumeric.includes(x)) + parse(parser, 'latin123').chain(assertState(['latin'])) + parse(parser, '123').chain(assertState([])) + parse(parser, 'latin').chain(assertState(['latin'])) + parse(parser, '').chain(assertState([])) + }) +] + diff --git a/tests/units/index.js b/tests/units/index.js index fbff9d6..e7297eb 100644 --- a/tests/units/index.js +++ b/tests/units/index.js @@ -1,10 +1,3 @@ -import { it, assert } from 'folktest' -import { map, not, until, char, parse, State, seq, str } from '../../src/index.js' -import { mapStr, join } from '../../src/fn.js' - -export const Tests = [ - it('whatever', () => { - - }) -] +export * from './bytes.js' +export * from './iter.js' diff --git a/tests/units/iter.js b/tests/units/iter.js new file mode 100644 index 0000000..61d9ca0 --- /dev/null +++ b/tests/units/iter.js @@ -0,0 +1,21 @@ +import { it, assert, assertEq } from 'folktest' +import { IndexableIterator } from '../../src/iter.js' +import { state } from '../../src/state.js' + +export const Iterator = [ + it('should be cloneable', () => { + const hi = new IndexableIterator('hi :3') + const hihi = hi.clone() + + assertEq(hi.next().value, 'h') + + assertEq(hi.next().value, 'i') + assertEq(hihi.next().value, 'h') + }), + + it('should have iterator helpers', () => { + const aaaaa = new IndexableIterator('awawawawa').filter(x => x !== 'w') + assertEq([...aaaaa].join(''), 'aaaaa') + }) +] +