commit 2baf53640d7834329196b14c1503a35da699f387 Author: rowan Date: Sat Mar 29 06:18:26 2025 -0500 initial commit diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..bcab54b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "vendor/kojima"] + path = vendor/kojima + url = https://git.kitsu.cafe/rowan/kojima.git diff --git a/package.json b/package.json new file mode 100644 index 0000000..66457cd --- /dev/null +++ b/package.json @@ -0,0 +1,13 @@ +{ + "name": "kuebiko", + "version": "1.0.0", + "type": "module", + "main": "index.js", + "author": "Rowan (https://kitsu.cafe)", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "license": "ISC", + "description": "" +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..e69de29 diff --git a/src/parser.js b/src/parser.js new file mode 100644 index 0000000..0981956 --- /dev/null +++ b/src/parser.js @@ -0,0 +1,125 @@ +import { Ok, Err, curry } from '../vendor/kojima/src/index.js' + +class ParseError extends Error { + constructor(message, state, source) { + super(message) + this.state = state + this.source = source + } +} + +const Tuple = (...values) => Object.freeze(values) +const State = value => Tuple([], Iterator.from(value)) + +const LowerAlpha = 'abcdefghijklmnopqrstuvwxyz' +const UpperAlpha = LowerAlpha.toUpperCase() +const Alpha = LowerAlpha + UpperAlpha +const Digits = '1234567890' +const Alphanumeric = Alpha + Digits + + +const tee = (iterator, n = 2) => { + iterator = Iterator.from(iterator) + + 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) +} + +const fork = ([tokens, state]) => { + const [a, b] = tee(state) + return Tuple( + Tuple(tokens.slice(), a), + Tuple(tokens.slice(), b), + ) +} + +const succeed = (v, [x, y]) => Ok(Tuple(x.concat(v), y)) +const fail = (msg, state, err = undefined) => Err(new ParseError(msg, state, err)) +const nth = (n, iter) => iter[n] +const next = state => nth(1, state).next().value +const diff = (a, b) => b.slice(-Math.max(0, b.length - a.length)) +const join = curry((delim, val) => val.join(delim)) +const mapStr = curry((fn, str) => Array.from(str).map(v => fn(v))) + +const any = (...parsers) => state => { + for (const parser of parsers) { + const [original, clone] = fork(state) + const result = parser(clone) + if (result.isOk) { + return result + } + } + + return fail('no matching parsers', state) +} + +const anyOf = curry((str, state) => ( + any(...mapStr(char, str))(state) +)) + +const seq = (...parsers) => state => { + let acc = Ok(state) + + for (const parser of parsers) { + if (acc.isOk) { + acc = acc.bind(parser) + } else { + break + } + } + + return acc +} + +const map = curry((fn, parser, state) => { + return parser(state).bind(result => { + try { + const parsed = diff(state[0], result[0]) + const backtrack = Tuple(state[0], result[1]) + return succeed(fn(parsed), backtrack) + } catch (e) { + return fail('failed to map', state, e) + } + }) +}) + +const char = curry((ch, state) => ( + next(state) === ch ? succeed(ch, state) : fail(`could not parse ${ch} `, state) +)) + +const anyChar = state => { + const ch = next(state) + return !!ch ? succeed(ch, state) : fail(`could not parse ${ch}`, state) +} + +const string = curry((str, state) => ( + map( + join(''), + seq(...mapStr(char, str)) + )(state) +)) + +const digit = anyOf(Digits) +const lowerAlpha = anyOf(LowerAlpha) +const upperAlpha = anyOf(UpperAlpha) +const alpha = anyOf(Alpha) +const alphanumeric = anyOf(Alphanumeric) + +const maybe = curry((parser, state) => { + const [original, clone] = fork(state) + const result = parser(clone) + return result.isOk ? result : succeed([], original) +}) + + diff --git a/vendor/kojima b/vendor/kojima new file mode 160000 index 0000000..1402219 --- /dev/null +++ b/vendor/kojima @@ -0,0 +1 @@ +Subproject commit 1402219aad22f55c39ac448bff372474a17f5c00