diff --git a/CLAUDE.md b/CLAUDE.md index d3f4981..4b43a27 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,7 +17,7 @@ npm run lint:fix # auto-fix lint issues npm run format # oxfmt (format all files) npm run generate:index # regenerate all index.ts barrel files from source -npm run generate:readme # regenerate README.MD from JSDoc examples +npm run generate:readme # regenerate README.md and doc/api/*.md from JSDoc examples npm run test:extract # extract JSDoc @examples from .js files into test/examples.test.ts ``` @@ -55,13 +55,17 @@ type Failure = { ok: false; error?: string }; - Every combinator/terminal/primitive lives in its own file: `src/combinators/map.ts` - Co-located test: `src/combinators/map.test.ts` - `index.ts` in each module is generated — run `npm run generate:index` after adding or removing files; do not edit manually -- JSDoc `@example` blocks in source files are the source of truth for README and `test/examples.test.ts` +- JSDoc `@example` blocks in source files are the source of truth for the docs and `test/examples.test.ts` +- `README.md` is generated from `doc/README.template.md` — a landing page (pitch, quick start, intent-based cheat sheet, one-line API index) +- `doc/api/{core,terminals,primitives,combinators,utils}.md` are generated from `doc/api.template.md` — the full per-export reference; the whole directory is wiped and rewritten on every run +- Neither `README.md` nor `doc/api/` is edited by hand; hand-written prose belongs in the two templates in `doc/` ### Adding a new combinator 1. Create `src/combinators/myThing.ts` + `src/combinators/myThing.test.ts` 2. Run `npm run generate:index` to update `src/combinators/index.ts` -3. Add JSDoc `@example` block — run `npm run generate:readme` and `npm run test:extract` to keep README and examples test in sync +3. Add JSDoc `@example` block — run `npm run generate:readme` and `npm run test:extract` to keep the docs and examples test in sync +4. Add the new name to the "Which function do I need?" cheat sheet in `doc/README.template.md` — that table is hand-written, and `generate:readme` warns about any export missing from it ### Test structure diff --git a/README.MD b/README.MD deleted file mode 100644 index 91e913a..0000000 --- a/README.MD +++ /dev/null @@ -1,1441 +0,0 @@ -# unitas — composing parsers into a unified whole - -Unitas logo - -[![npm version](https://img.shields.io/npm/v/unitas)](https://www.npmjs.com/package/unitas) -[![Coverage](https://coveralls.io/repos/github/sovrin/unitas/badge.svg?branch=master)](https://coveralls.io/github/sovrin/unitas?branch=master) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) - -A lightweight, TypeScript-first parser combinator library for building expressive and composable parsers. - -## Features - -- **Parser Combinators**: Compose small parsers into complex ones using combinators like `many`, `choice`, `sequence`, and more -- **Terminals**: Factory functions for common patterns (`char`, `string`, `regex`, etc.) -- **Primitives**: Pre-built parser instances ready to use (`digit`, `letter`, `whitespace`, etc.) -- **TypeScript**: Full TypeScript support with generic types and inference -- **Tree-shakeable**: ESM-only with separate exports for `combinators`, `terminals`, `primitives`, and `utils` -- **No dependencies**: Zero external runtime dependencies - -> **Note:** This library is in active development. The API may change before v1.0.0. - -## Installation - -```bash -npm install unitas -``` - -## Quick Start - -**CSV parser** — parsing comma-separated values with quoted fields - -```typescript -import { grammar, run } from 'unitas'; -import { choice, inner, separatedBy } from 'unitas/combinators'; -import { char, regex } from 'unitas/terminals'; -import { letters } from 'unitas/primitives'; - -const csv = grammar({ - row: (p) => separatedBy(p.value, char(',')), - value: (p) => choice(p.quoted, p.unquoted), - quoted: () => inner(char('"'), regex(/^[^"]*/), char('"')), - unquoted: () => letters, -}); - -run(csv.row, 'a,b,c'); // ['a', 'b', 'c'] -run(csv.row, '"a,b",c'); // ['a,b', 'c'] -``` - -**JSON value parser** — parsing simple json values - -```typescript -import { grammar, run } from 'unitas'; -import { choice, map, quoted } from 'unitas/combinators'; -import { string } from 'unitas/terminals'; -import { bool, digits, letters } from 'unitas/primitives'; - -const json = grammar({ - value: (p) => choice(p.string, p.number, p.bool, p.null), - string: () => quoted(letters), - number: () => digits, - bool: () => bool, - null: () => map(string('null'), () => null), -}); - -run(json.value, '"hello"'); // 'hello' -run(json.value, '42'); // 42 -run(json.value, 'true'); // true -run(json.value, 'null'); // null -``` - -**Query string parser** — parsing URL query parameters - -```typescript -import { grammar, run } from 'unitas'; -import { map, outer, separatedBy } from 'unitas/combinators'; -import { char } from 'unitas/terminals'; -import { letters } from 'unitas/primitives'; - -type Query = { - params: Record; - param: [string, string]; - key: string; - value: string; -}; - -const query = grammar({ - params: (p) => - map(separatedBy(p.param, char('&')), (pairs) => - Object.fromEntries(pairs), - ), - param: (p) => outer(p.key, char('='), p.value), - key: () => letters, - value: () => letters, -}); - -run(query.params, 'foo=bar&baz=qux'); // { foo: 'bar', baz: 'qux' } -``` - -**INI file section** — parsing section headers and key-value pairs - -```typescript -import { grammar, run } from 'unitas'; -import { map, outer, sequence } from 'unitas/combinators'; -import { char, regex } from 'unitas/terminals'; -import { letters, nl } from 'unitas/primitives'; -import { pick } from 'unitas/utils'; - -const ini = grammar({ - section: (p) => - map( - sequence(char('['), p.name, char(']'), nl, p.entry), - pick(1, 4), - ([name, entry]) => ({ name, entry }), - ), - name: () => letters, - entry: (p) => outer(p.key, char('='), p.value), - key: () => letters, - value: () => regex(/^[^\n]+/), -}); - -run(ini.section, '[database]\nhost=localhost'); // { name: 'database', entry: ['host', 'localhost'] } -``` - -## Table of Contents - -- [Core](#core) - - [`failure`](#failure) - - [`grammar`](#grammar) - - [`label`](#label) - - [`lazy`](#lazy) - - [`match`](#match) - - [`memoize`](#memoize) - - [`parser`](#parser) - - [`run`](#run) - - [`success`](#success) -- [Terminals](#terminals) - - [`char`](#char) - - [`charOf`](#charOf) - - [`noneOf`](#noneOf) - - [`oneOf`](#oneOf) - - [`regex`](#regex) - - [`satisfy`](#satisfy) - - [`string`](#string) - - [`stringOf`](#stringOf) - - [`take`](#take) - - [`takeWhile`](#takeWhile) - - [`token`](#token) - - [`word`](#word) -- [Primitives](#primitives) - - [`alphaNum`](#alphaNum) - - [`alphaNums`](#alphaNums) - - [`anyChar`](#anyChar) - - [`bool`](#bool) - - [`crlf`](#crlf) - - [`digit`](#digit) - - [`digits`](#digits) - - [`eof`](#eof) - - [`eol`](#eol) - - [`float`](#float) - - [`hexDigit`](#hexDigit) - - [`hexDigits`](#hexDigits) - - [`identifier`](#identifier) - - [`integer`](#integer) - - [`letter`](#letter) - - [`letters`](#letters) - - [`line`](#line) - - [`literal`](#literal) - - [`lowercase`](#lowercase) - - [`lowercases`](#lowercases) - - [`nl`](#nl) - - [`number`](#number) - - [`octDigit`](#octDigit) - - [`octDigits`](#octDigits) - - [`position`](#position) - - [`rest`](#rest) - - [`space`](#space) - - [`spaces`](#spaces) - - [`tab`](#tab) - - [`uppercase`](#uppercase) - - [`uppercases`](#uppercases) - - [`whitespace`](#whitespace) - - [`whitespaces`](#whitespaces) -- [Combinators](#combinators) - - [`attempt`](#attempt) - - [`bind`](#bind) - - [`braced`](#braced) - - [`bracketed`](#bracketed) - - [`chainLeft`](#chainLeft) - - [`chainLeft1`](#chainLeft1) - - [`chainRight`](#chainRight) - - [`chainRight1`](#chainRight1) - - [`choice`](#choice) - - [`concat`](#concat) - - [`consume`](#consume) - - [`endBy`](#endBy) - - [`endBy1`](#endBy1) - - [`exactly`](#exactly) - - [`first`](#first) - - [`flag`](#flag) - - [`fold`](#fold) - - [`fold1`](#fold1) - - [`foldRight`](#foldRight) - - [`foldRight1`](#foldRight1) - - [`fuse`](#fuse) - - [`guard`](#guard) - - [`inner`](#inner) - - [`interleaved`](#interleaved) - - [`last`](#last) - - [`left`](#left) - - [`lexeme`](#lexeme) - - [`many`](#many) - - [`many1`](#many1) - - [`manyAtLeast`](#manyAtLeast) - - [`manyAtMost`](#manyAtMost) - - [`manyBetween`](#manyBetween) - - [`manyTill`](#manyTill) - - [`map`](#map) - - [`node`](#node) - - [`not`](#not) - - [`nth`](#nth) - - [`optional`](#optional) - - [`optionalConsume`](#optionalConsume) - - [`optionalSeparatedBy`](#optionalSeparatedBy) - - [`outer`](#outer) - - [`padded`](#padded) - - [`parenthesized`](#parenthesized) - - [`peek`](#peek) - - [`postfix`](#postfix) - - [`prefix`](#prefix) - - [`pure`](#pure) - - [`quoted`](#quoted) - - [`recover`](#recover) - - [`right`](#right) - - [`separatedBy`](#separatedBy) - - [`separatedBy1`](#separatedBy1) - - [`separatedEndBy`](#separatedEndBy) - - [`separatedEndBy1`](#separatedEndBy1) - - [`separatedUntil`](#separatedUntil) - - [`sequence`](#sequence) - - [`skip`](#skip) - - [`skipMany`](#skipMany) - - [`skipMany1`](#skipMany1) - - [`surrounded`](#surrounded) - - [`unless`](#unless) - - [`until`](#until) - - [`validate`](#validate) - - [`value`](#value) - - [`when`](#when) -- [Utils](#utils) - - [`filter`](#filter) - - [`flatten`](#flatten) - - [`join`](#join) - - [`pick`](#pick) - - [`pipe`](#pipe) - - [`pop`](#pop) - - [`shift`](#shift) - - [`spread`](#spread) - -## Core Concepts - -### The Parser Type - -A `Parser` is a function that takes an input string and returns a `Result`. The generic `T` represents the type of value the parser produces. - -```typescript -type Parser = (input: string) => Result; -``` - -### The Result Type - -Every parser returns a `Result` which is either: - -- **Success** — The parser matched and produced a value -- **Failure** — The parser did not match - -```typescript -type Success = { ok: true; value: T; remaining: string }; -type Failure = { ok: false; error?: string }; -type Result = Success | Failure; -``` - -The `remaining` string is crucial — it represents what input is left after the parser has done its work. This is how we "consume" input and chain parsers together. - -### Creating a Parser - -Use `create` to wrap a parsing function: - -```typescript -import { create, success, failure } from 'unitas'; - -const parser = create((input) => { - if (input.startsWith('hello')) { - return success('hello', input.slice(5)); - } - return failure('expected "hello"'); -}); -``` - -### Understanding the Monadic Nature - -Parsers are monadic, which means they follow certain laws that make them composable: - -1. **Left identity**: `create(success(a, input))` behaves like `a` -2. **Right identity**: `parser` composed with `success` returns equivalent result -3. **Associativity**: Composition order doesn't affect final result - -The practical implication is that you can chain and combine parsers predictably. - -### Success Results - -When a parser successfully matches, it returns: - -```typescript -{ ok: true, value: 'hello', remaining: ' world' } - │ │ │ - │ │ └── What's left to parse - │ └── The parsed value - └── Always true for success -``` - -### Failure Results - -When a parser fails, it returns: - -```typescript -{ ok: false } // Generic failure -{ ok: false, error: 'expected a' } // Failure with message -``` - -The `error` field is optional — you can always add meaningful error messages later using `label`. - -## Core (`unitas`) - -**Core** provides the fundamental types and functions for building parsers. - -
-failure — Creates a failed result with an optional error message. - -```typescript -failure('unexpected input'); // { ok: false, error: 'unexpected input' } -``` - -
-
-grammar — Creates a recursive grammar where rules can reference each other. - -```typescript -type Math = { - expr: number; - term: number; - value: number; -}; -const g = grammar({ - expr: (p) => - chainLeft1( - p.term, - map(char('+'), () => (l, r) => l + r), - ), - term: (p) => - choice( - p.value, - map(sequence(char('('), p.expr, char(')')), ([, v]) => v), - ), - value: () => digits, -}); -run(g.expr, '1+2'); // 3 -run(g.expr, '1+2+3'); // 6 -run(g.expr, '(1+2)'); // 3 -``` - -
-
-label — Labels a parser with a custom error message on failure. - -```typescript -label(char('x'), 'letter x')(''); // { ok: false, error: 'expected letter x' } -``` - -
-
-lazy — Defers parser creation, useful for recursive grammars. - -```typescript -lazy(() => char('a'))('abc'); // { ok: true, value: 'a', remaining: 'bc' } -``` - -
-
-match — Pattern matching on a Result to handle success and failure cases. - -```typescript -match(success('hello', ''), { success: (v) => v, failure: () => 'failed' }); // 'hello' -``` - -
-
-memoize — Memoizes a parser to cache results by input string. Useful for expensive parsers and recursive grammars to avoid exponential backtracking. - -```typescript -const memoDigits = memoize(digits); -memoDigits('123'); // { ok: true, value: 123, remaining: '' } -``` - -
-
-parser — Creates a parser from a parser function. - -```typescript -create((input) => success('parsed', input.slice(6)))('hello world'); // { ok: true, value: 'parsed', remaining: 'world' } -``` - -
-
-run — Runs a parser and returns the value, throws on failure or unconsumed input. - -```typescript -run(string('hello'), 'hello'); // 'hello' -``` - -
-
-success — Creates a successful result with a value and remaining input. - -```typescript -success('hello', ' world'); // { ok: true, value: 'hello', remaining: ' world' } -``` - -
- -## Terminals (`unitas/terminals`) - -**Terminals** are the basic building blocks that match specific parts of the input. They don't combine other parsers — they directly inspect the input string. - -
-char — Parse a specific character. - -```typescript -char('A')('ABC'); // { ok: true, value: 'A', remaining: 'BC' } -``` - -
-
-charOf — Parse any character from a set. - -```typescript -charOf(['a', 'b', 'c'])('abc'); // { ok: true, value: 'a', remaining: 'bc' } -``` - -
-
-noneOf — Parse any character not in the set. - -```typescript -noneOf(['a', 'b', 'c'])('xyz'); // { ok: true, value: 'x', remaining: 'yz' } -``` - -
-
-oneOf — Parse one string from a set of strings (longest match wins). - -```typescript -oneOf(['hello', 'hell', 'help'])('helpful'); // { ok: true, value: 'help', remaining: 'ful' } -``` - -
-
-regex — Parse with a regular expression. - -```typescript -regex(/^\w+/)('hello world'); // { ok: true, value: 'hello', remaining: ' world' } -``` - -
-
-satisfy — Parse a character satisfying a predicate. - -```typescript -satisfy((c) => c === 'a')('abc'); // { ok: true, value: 'a', remaining: 'bc' } -``` - -
-
-string — Parse a specific string. - -```typescript -string('hello')('hello world'); // { ok: true, value: 'hello', remaining: ' world' } -``` - -
-
-stringOf — Parse first character that exists in string (like charOf but for a string). - -```typescript -stringOf('abc')('abcdef'); // { ok: true, value: 'a', remaining: 'bcdef' } -``` - -
-
-take — Take n characters. - -```typescript -take(3)('abcdef'); // { ok: true, value: 'abc', remaining: 'def' } -``` - -
-
-takeWhile — Takes characters while the predicate returns true. - -```typescript -takeWhile((c) => c !== 'x')('abcx'); // { ok: true, value: 'abc', remaining: 'x' } -``` - -
-
-token — Parses a string as a token, skipping trailing whitespace. Unlike word, it does not enforce word boundaries - useful for parsing strings that might be followed by any character. - -```typescript -token('let')('let x'); // { ok: true, value: 'let', remaining: 'x' } -token('let')('let1'); // { ok: true, value: 'let', remaining: '1' } -token('let')('let x'); // { ok: true, value: 'let', remaining: 'x' } -``` - -
-
-word — Parses a specific word and ensures it is not followed by word characters. Use this when parsing keywords that should not be part of a longer identifier. - -```typescript -word('let')('let x'); // { ok: true, value: 'let', remaining: 'x' } -word('let')('let1'); // { ok: false } -word('if')('if (x)'); // { ok: true, value: 'if', remaining: '(x)' } -``` - -
- -## Primitives (`unitas/primitives`) - -**Primitives** are pre-built parser instances ready to use. Unlike terminals which are factory functions (like `char('x')`), primitives are constants you can pass directly to combinators. - -
-alphaNum — Parse a single alphanumeric character. - -```typescript -alphaNum('a1'); // { ok: true, value: 'a', remaining: '1' } -alphaNum('1a'); // { ok: true, value: '1', remaining: 'a' } -``` - -
-
-alphaNums — Parse one or more alphanumeric characters. - -```typescript -alphaNums('abc123'); // { ok: true, value: 'abc123', remaining: '' } -``` - -
-
-anyChar — Parse any single character. - -```typescript -anyChar('abc'); // { ok: true, value: 'a', remaining: 'bc' } -``` - -
-
-bool — Parse a boolean literal. - -```typescript -bool('true'); // { ok: true, value: true, remaining: '' } -bool('false'); // { ok: true, value: false, remaining: '' } -bool('trueABC'); // { ok: true, value: true, remaining: 'ABC' } -``` - -
-
-crlf — Parse CRLF line ending. - -```typescript -crlf('\r\nabc'); // { ok: true, value: '\r\n', remaining: 'abc' } -``` - -
-
-digit — Parse a single digit and return as number. - -```typescript -digit('5abc'); // { ok: true, value: 5, remaining: 'abc' } -``` - -
-
-digits — Parse one or more digits and return as number. - -```typescript -digits('123abc'); // { ok: true, value: 123, remaining: 'abc' } -``` - -
-
-eof — Parse end of file (succeeds only on empty input). - -```typescript -eof(''); // { ok: true, value: null, remaining: '' } -``` - -
-
-eol — Parse end of line (\\n, \\r\\n, or EOF). - -```typescript -eol('\nabc'); // { ok: true, value: '\n', remaining: 'abc' } -``` - -
-
-float — Parse a floating point number. - -```typescript -float('1.23'); // { ok: true, value: 1.23, remaining: '' } -float('-2.5'); // { ok: true, value: -2.5, remaining: '' } -float('1.23abc'); // { ok: true, value: 1.23, remaining: 'abc' } -``` - -
-
-hexDigit — Parse a single hexadecimal digit. - -```typescript -hexDigit('fF9'); // { ok: true, value: 'f', remaining: 'F9' } -``` - -
-
-hexDigits — Parse one or more hexadecimal digits. - -```typescript -hexDigits('deadbeef'); // { ok: true, value: 'deadbeef', remaining: '' } -``` - -
-
-identifier — Parse an identifier — starts with letter or underscore, no leading digit, no hyphen. - -```typescript -identifier('variable_name'); // { ok: true, value: 'variable_name', remaining: '' } -``` - -
-
-integer — Parse a signed integer. - -```typescript -integer('42'); // { ok: true, value: 42, remaining: '' } -integer('-7'); // { ok: true, value: -7, remaining: '' } -integer('123abc'); // { ok: true, value: 123, remaining: 'abc' } -``` - -
-
-letter — Parse a single letter. - -```typescript -letter('abc'); // { ok: true, value: 'a', remaining: 'bc' } -``` - -
-
-letters — Parse one or more letters. - -```typescript -letters('abc123'); // { ok: true, value: 'abc', remaining: '123' } -``` - -
-
-line — Parse until end of line. - -```typescript -line('hello\nworld'); // { ok: true, value: 'hello', remaining: '\nworld' } -``` - -
-
-literal — Parse a word-like value including hyphens. - -```typescript -literal('foo-bar'); // { ok: true, value: 'foo-bar', remaining: '' } -literal('123abc'); // { ok: true, value: '123abc', remaining: '' } -``` - -
-
-lowercase — Parse a single lowercase letter. - -```typescript -lowercase('abc'); // { ok: true, value: 'a', remaining: 'bc' } -``` - -
-
-lowercases — Parses one or more lowercase letters. - -```typescript -lowercases('abcDEF'); // { ok: true, value: 'abc', remaining: 'DEF' } -``` - -
-
-nl — Parse a newline character. - -```typescript -nl('\ntext'); // { ok: true, value: '\n', remaining: 'text' } -``` - -
-
-number — Parse an integer or float. - -```typescript -number('42'); // { ok: true, value: 42, remaining: '' } -number('3.14'); // { ok: true, value: 3.14, remaining: '' } -number('-7'); // { ok: true, value: -7, remaining: '' } -number('-2.5'); // { ok: true, value: -2.5, remaining: '' } -``` - -
-
-octDigit — Parse a single octal digit. - -```typescript -octDigit('7abc'); // { ok: true, value: '7', remaining: 'abc' } -``` - -
-
-octDigits — Parse one or more octal digits. - -```typescript -octDigits('0777abc'); // { ok: true, value: '0777', remaining: 'abc' } -``` - -
-
-position — Get current position (remaining input length). - -```typescript -position('abc'); // { ok: true, value: 3, remaining: 'abc' } -``` - -
-
-rest — Parse the rest of the input. - -```typescript -rest('hello'); // { ok: true, value: 'hello', remaining: '' } -``` - -
-
-space — Parse a single space character. - -```typescript -space(' abc'); // { ok: true, value: ' ', remaining: 'abc' } -``` - -
-
-spaces — Parse one or more space characters. - -```typescript -spaces(' abc'); // { ok: true, value: ' ', remaining: 'abc' } -``` - -
-
-tab — Parse tab character. - -```typescript -tab('\ttext'); // { ok: true, value: '\t', remaining: 'text' } -``` - -
-
-uppercase — Parses a single uppercase letter. - -```typescript -uppercase('ABC'); // { ok: true, value: 'A', remaining: 'BC' } -``` - -
-
-uppercases — Parses one or more uppercase letters. - -```typescript -uppercases('ABCdef'); // { ok: true, value: 'ABC', remaining: 'def' } -``` - -
-
-whitespace — Parses a single whitespace character. - -```typescript -whitespace(' abc'); // { ok: true, value: ' ', remaining: 'abc' } -``` - -
-
-whitespaces — Parses one or more whitespace characters. - -```typescript -whitespaces(' abc'); // { ok: true, value: ' ', remaining: 'abc' } -``` - -
- -## Combinators (`unitas/combinators`) - -**Combinators** are functions that take one or more parsers and return a new parser. They are the "glue" that lets you compose complex parsers from simple ones. - -
-attempt — Attempt wraps a parser to handle backtracking on failure. - -```typescript -attempt(string('hello'))('hello world'); // { ok: true, value: 'hello', remaining: ' world' } -``` - -
-
-bind — Chain parsers where the second parser depends on the first result. - -```typescript -bind(digits, (n) => take(n))('3abc'); // { ok: true, value: 'abc', remaining: '' } -``` - -
-
-braced — Parse content surrounded by braces. - -```typescript -braced(string('hi'))('{hi}'); // { ok: true, value: 'hi', remaining: '' } -``` - -
-
-bracketed — Parse content surrounded by brackets. - -```typescript -bracketed(string('hi'))('[hi]'); // { ok: true, value: 'hi', remaining: '' } -``` - -
-
-chainLeft — Chain left-associative operations (right-to-left for same precedence). - -```typescript -chainLeft(digits, operation)('1+2+3'); // { ok: true, value: 6, remaining: '' } -chainLeft(digits, operation)('10-3+2'); // { ok: true, value: 9, remaining: '' } -``` - -
-
-chainLeft1 — Chain left-associative operations (fails on empty input). - -```typescript -chainLeft1(digits, operation)('1+2+3'); // { ok: true, value: 6, remaining: '' } -chainLeft1(digits, operation)('8/2*3'); // { ok: true, value: 12, remaining: '' } -``` - -
-
-chainRight — Chain right-associative operations (right-to-left grouping). - -```typescript -chainRight(digits, operation)('2-1-1'); // { ok: true, value: 2, remaining: '' } -chainRight(digits, operation)('4/2/2'); // { ok: true, value: 4, remaining: '' } -``` - -
-
-chainRight1 — Chain right-associative operations (fails on empty input). - -```typescript -chainRight1(digits, operation)('2-1-1'); // { ok: true, value: 2, remaining: '' } -chainRight1(digits, operation)('4/2/2'); // { ok: true, value: 4, remaining: '' } -``` - -
-
-choice — Try each parser in order, return first success. - -```typescript -choice(string('hello'), string('world'))('hello'); // { ok: true, value: 'hello', remaining: '' } -``` - -
-
-concat — Join string array parser result into a single string. - -```typescript -concat(many(letter))('abc123'); // { ok: true, value: 'abc', remaining: '123' } -concat(many(letter), '-')('abc123'); // { ok: true, value: 'a-b-c', remaining: '123' } -``` - -
-
-consume — Consume input but discard the result (return null). - -```typescript -consume(string('hello'))('hello world'); // { ok: true, value: null, remaining: ' world' } -``` - -
-
-endBy — Zero or more items separated and ending with terminator. - -```typescript -endBy(string('item'), char(';'))('item;item;item;'); // { ok: true, value: ['item', 'item', 'item'], remaining: '' } -``` - -
-
-endBy1 — One or more items separated and ending with terminator. - -```typescript -endBy1(string('item'), char(';'))('item;item;item;'); // { ok: true, value: ['item', 'item', 'item'], remaining: '' } -``` - -
-
-exactly — Parse exactly n occurrences. - -```typescript -exactly(char('a'), 3)('aaa'); // { ok: true, value: ['a', 'a', 'a'], remaining: '' } -``` - -
-
-first — Extract the first element from a parser result array. - -```typescript -first(sequence(char('a'), digit))('a1bc'); // { ok: true, value: 'a', remaining: 'bc' } -``` - -
-
-flag — Return true if parser succeeds, false otherwise. Always succeeds without consuming input on failure. - -```typescript -flag(string('*'))('*abc'); // { ok: true, value: true, remaining: 'abc' } -flag(string('*'))('abc'); // { ok: true, value: false, remaining: 'abc' } -``` - -
-
-fold — Parse zero or more and fold into a single value. - -```typescript -fold(digit, [], (acc, d) => [...acc, d])('123'); // { ok: true, value: [1, 2, 3], remaining: '' } -``` - -
-
-fold1 — Parse one or more and fold into a single value. - -```typescript -fold1(digit, 0, (acc, d) => acc + d)('123'); // { ok: true, value: 6, remaining: '' } -``` - -
-
-foldRight — Parse zero or more and fold right-to-left. - -```typescript -foldRight(digit, [], (acc, d) => [...acc, d])('123'); // { ok: true, value: [3, 2, 1], remaining: '' } -``` - -
-
-foldRight1 — Parse one or more and fold right-to-left. - -```typescript -foldRight1(digit, [], (acc, d) => [...acc, d])('123'); // { ok: true, value: [3, 2, 1], remaining: '' } -``` - -
-
-fuse — Fuse multiple string parsers into a single one. The fused parser concatenates all string results. - -```typescript -fuse(char('a'), char('b'), char('c'))('abc'); // { ok: true, value: 'abc', remaining: '' } -fuse(string('hello'), char(' '), string('world'))('hello world'); // { ok: true, value: 'hello world', remaining: '' } -``` - -
-
-guard — Conditionally apply parser based on a condition. - -```typescript -guard(true, string('hello'))('hello'); // { ok: true, value: 'hello', remaining: '' } -guard(false, string('hello'))('hello'); // { ok: false } -``` - -
-
-inner — Extract inner value from surrounded content (like inner of braced). - -```typescript -inner(char('('), string('hi'), char(')'))('(hi)'); // { ok: true, value: 'hi', remaining: '' } -``` - -
-
-interleaved — Parse items with interleaved separators. - -```typescript -interleaved(char('a'), char(','))('a,a,a'); // { ok: true, value: ['a', ',', 'a', ',', 'a'], remaining: '' } -``` - -
-
-last — Extract the last element from a parser result array. - -```typescript -last(sequence(char('a'), char('b')))('ab'); // { ok: true, value: 'b', remaining: '' } -``` - -
-
-left — Keep only the left result from a sequence. - -```typescript -left(string('hello'), string('world'))('helloworld'); // { ok: true, value: 'hello', remaining: '' } -``` - -
-
-lexeme — Parser that consumes trailing whitespace. - -```typescript -lexeme(string('hello'))('hello world'); // { ok: true, value: 'hello', remaining: 'world' } -``` - -
-
-many — Zero or more occurrences (never fails). - -```typescript -many(char('a'))('aaa'); // { ok: true, value: ['a', 'a', 'a'], remaining: '' } -``` - -
-
-many1 — One or more occurrences (fails if no matches). - -```typescript -many1(char('a'))('aaa'); // { ok: true, value: ['a', 'a', 'a'], remaining: '' } -``` - -
-
-manyAtLeast — Parse at least n occurrences. - -```typescript -manyAtLeast(char('a'), 2)('aaa'); // { ok: true, value: ['a', 'a', 'a'], remaining: '' } -``` - -
-
-manyAtMost — Parse at most n occurrences. - -```typescript -manyAtMost(char('a'), 2)('aaa'); // { ok: true, value: ['a', 'a'], remaining: 'a' } -``` - -
-
-manyBetween — Parse between min and max occurrences. - -```typescript -manyBetween(char('a'), 2, 3)('aaa'); // { ok: true, value: ['a', 'a', 'a'], remaining: '' } -``` - -
-
-manyTill — Parse zero or more until terminator matches. - -```typescript -manyTill(char('a'), char('b'))('aaab'); // { ok: true, value: ['a', 'a', 'a'], remaining: '' } -``` - -
-
-map — Transform the parsed value. - -```typescript -map(string('hello'), (v) => v.toUpperCase())('hello'); // { ok: true, value: 'HELLO', remaining: '' } -``` - -
-
-node — Create a node from parser fields. - -```typescript -node('binop', { left: digits, op: char('+'), right: digits })('1+2'); // { ok: true, value: { type: 'binop', left: 1, op: '+', right: 2 }, remaining: '' } -node('number', { value: digits })('123'); // { ok: true, value: { type: 'number', value: 123 }, remaining: '' } -``` - -
-
-not — Succeed if parser fails (without consuming input). - -```typescript -not(string('hello'))('world'); // { ok: true, value: null, remaining: 'world' } -``` - -
-
-nth — Extract the nth element from a parser result array. - -```typescript -nth(sequence(char('a'), char('b'), char('c')), 1)('abc'); // { ok: true, value: 'b', remaining: '' } -``` - -
-
-optional — Make parser optional (return null on failure, without consuming input). - -```typescript -optional(string('hello'))('hello'); // { ok: true, value: 'hello', remaining: '' } -optional(string('hello'))('world'); // { ok: true, value: null, remaining: 'world' } -``` - -
-
-optionalConsume — Optionally consume input (always succeeds, returns void). - -```typescript -optionalConsume(string('hello'))('hello world'); // { ok: true, value: undefined, remaining: ' world' } -optionalConsume(string('hello'))('world'); // { ok: true, value: undefined, remaining: 'world' } -``` - -
-
-optionalSeparatedBy — Zero or more items separated by a separator, with optional null values. - -```typescript -optionalSeparatedBy(digits, char(','))('1,2'); // { ok: true, value: [1, 2], remaining: '' } -optionalSeparatedBy(digits, char(','))(',1'); // { ok: true, value: [null, 1], remaining: '' } -optionalSeparatedBy(digits, char(','))('1,'); // { ok: true, value: [1], remaining: '' } -``` - -
-
-outer — Extract outer values from a sequence of 3 parsers (skip middle). - -```typescript -outer(char('('), string('hi'), char(')'))('(hi)'); // { ok: true, value: ['(', ')'], remaining: '' } -``` - -
-
-padded — Parse content with optional whitespace on both sides. - -```typescript -padded(string('hi'))(' hi '); // { ok: true, value: 'hi', remaining: '' } -``` - -
-
-parenthesized — Parse content surrounded by parentheses. - -```typescript -parenthesized(string('hi'))('(hi)'); // { ok: true, value: 'hi', remaining: '' } -``` - -
-
-peek — Parse without consuming input. - -```typescript -peek(string('hello'))('hello world'); // { ok: true, value: 'hello', remaining: 'hello world' } -``` - -
-
-postfix — Parse postfix operators (chains atom with operators that return functions). - -```typescript -postfix( - char('a'), - map(char('!'), () => (x) => x), -)('a!'); // { ok: true, value: 'a', remaining: '' } -``` - -
-
-prefix — Parse prefix operators (like - in -5). - -```typescript -prefix( - map(char('-'), () => (x) => -x), - digit, -)('-5'); // { ok: true, value: -5, remaining: '' } -``` - -
-
-pure — Always return a value without consuming input. - -```typescript -pure(42)('abc'); // { ok: true, value: 42, remaining: 'abc' } -pure('ok')(''); // { ok: true, value: 'ok', remaining: '' } -``` - -
-
-quoted — Parse content surrounded by single or double quotes. - -```typescript -quoted(string('hello'))('"hello"'); // { ok: true, value: 'hello', remaining: '' } -``` - -
-
-recover — Use fallback value when parser fails. - -```typescript -recover(string('hello'), 'default')('world'); // { ok: true, value: 'default', remaining: 'world' } -``` - -
- -
-separatedBy — Zero or more items separated by a separator. - -```typescript -separatedBy(char('a'), char(','))('a,a,a'); // { ok: true, value: ['a', 'a', 'a'], remaining: '' } -``` - -
-
-separatedBy1 — One or more items separated by a separator. - -```typescript -separatedBy1(char('a'), char(','))('a,a,a'); // { ok: true, value: ['a', 'a', 'a'], remaining: '' } -``` - -
-
-separatedEndBy — Zero or more items separated by and ending with a terminator. - -```typescript -separatedEndBy(char('a'), char(';'))('a;a;a;'); // { ok: true, value: ['a', 'a', 'a'], remaining: '' } -``` - -
-
-separatedEndBy1 — One or more items separated by and ending with a terminator. - -```typescript -separatedEndBy1(char('a'), char(';'))('a;a;a;'); // { ok: true, value: ['a', 'a', 'a'], remaining: '' } -``` - -
-
-separatedUntil — Parse items separated by separator until terminator matches. - -```typescript -separatedUntil(char('a'), char(','), char(';'))('a,a,a;'); // { ok: true, value: ['a', 'a', 'a'], remaining: '' } -``` - -
-
-sequence — Parse a sequence of parsers and return all results as an array. - -```typescript -sequence(char('a'), char('b'), char('c'))('abc'); // { ok: true, value: ['a', 'b', 'c'], remaining: '' } -``` - -
-
-skip — Skip a parser n times. - -```typescript -skip(char('a'), 2)('aabc'); // { ok: true, value: null, remaining: 'bc' } -``` - -
-
-skipMany — Skip zero or more occurrences (never fails, returns null). - -```typescript -skipMany(char('a'))('aaabc'); // { ok: true, value: null, remaining: 'bc' } -``` - -
-
-skipMany1 — Skip one or more occurrences (fails if no matches). - -```typescript -skipMany1(char('a'))('aaabc'); // { ok: true, value: null, remaining: 'bc' } -``` - -
-
-surrounded — Parse content surrounded by delimiters. - -```typescript -surrounded(char('['), string('hi'), char(']'))('[hi]'); // { ok: true, value: 'hi', remaining: '' } -surrounded(char('a'), char('b'), char('c'))('abc'); // { ok: true, value: 'b', remaining: '' } -``` - -
-
-unless — Parse unless condition is true (inverse of guard). - -```typescript -unless(false, string('hello'))('hello'); // { ok: true, value: 'hello', remaining: '' } -unless(true, string('hello'))('hello'); // { ok: true, value: null, remaining: 'hello' } -``` - -
-
-until — Parse until terminator matches (fails if terminator never matches). - -```typescript -until(char('a'), char('b'))('baaa'); // { ok: true, value: [], remaining: 'baaa' } -until(char('a'), char('b'))('aaba'); // { ok: true, value: ['a', 'a'], remaining: 'ba' } -``` - -
-
-validate — Validate parsed value with a predicate. - -```typescript -validate(digit, (n) => n > 5)('7'); // { ok: true, value: 7, remaining: '' } -validate(digit, (n) => n > 5)('3'); // { ok: false } -``` - -
-
-value — Replace parsed value with a constant. - -```typescript -value(string('true'), true)('true'); // { ok: true, value: true, remaining: '' } -value(string('null'), null)('null'); // { ok: true, value: null, remaining: '' } -``` - -
-
-when — Branch on a boolean parser result. - -```typescript -when(flag(char('*')), pure('many'), pure('one'))('*rest'); // { ok: true, value: 'many', remaining: 'rest' } -when(flag(char('*')), pure('many'), pure('one'))('abc'); // { ok: true, value: 'one', remaining: 'abc' } -``` - -
- -## Utils (`unitas/utils`) - -**Utils** are utility functions for working with parser results, arrays, and function composition. - -
-filter — Exclude values from array. - -```typescript -filter([1, 2, 3])([1, 2, 3, 4, 5]); // [4, 5] -filter([1, 2], true)([1, false, 3]); // [3] -``` - -
-
-flatten — Flatten nested arrays. - -```typescript -flatten()([1, [2, [3]]]); // [1, 2, [3]] -flatten(2)([1, [2, [3]]]); // [1, 2, 3] -``` - -
-
-join — Join array elements into a string. - -```typescript -join()([1, 2, 3]); // '123' -join('-')([1, 2, 3]); // '1-2-3' -``` - -
-
-pick — Pick elements from an array by index. - -```typescript -pick(0, 2)(['a', 'b', 'c']); // ['a', 'c'] -pick(2, 4)(['a', 'b', 'c', 'd', 'e']); // ['c', 'e'] -``` - -
-
-pipe — Pipe parser functions together. - -```typescript -pipe(lexeme)(letters)('xyz abc'); // { ok: true, value: 'xyz', remaining: 'abc' } -``` - -
-
-pop — Get the last element of an array. - -```typescript -pop()([1, 2, 3]); // 3 -``` - -
-
-shift — Get the first element of an array. - -```typescript -shift()([1, 2, 3]); // 1 -``` - -
-
-spread — Collect spread arguments into an array. - -```typescript -spread()(1, 2, 3); // [1, 2, 3] -``` - -
- -## License - -MIT diff --git a/README.md b/README.md new file mode 100644 index 0000000..7d52ae5 --- /dev/null +++ b/README.md @@ -0,0 +1,612 @@ + + +# unitas + + + +**Composing parsers into a unified whole.** A lightweight, TypeScript-first parser combinator library — write small parsers, combine them into big ones, get typed results back. + +[![npm version](https://img.shields.io/npm/v/unitas)](https://www.npmjs.com/package/unitas) +[![Coverage](https://coveralls.io/repos/github/sovrin/unitas/badge.svg?branch=master)](https://coveralls.io/github/sovrin/unitas?branch=master) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +```bash +npm install unitas +``` + +```typescript +import { grammar, run } from 'unitas'; +import { choice, inner, separatedBy } from 'unitas/combinators'; +import { char, regex } from 'unitas/terminals'; +import { letters } from 'unitas/primitives'; + +const csv = grammar({ + row: (p) => separatedBy(p.value, char(',')), + value: (p) => choice(p.quoted, p.unquoted), + quoted: () => inner(char('"'), regex(/^[^"]*/), char('"')), + unquoted: () => letters, +}); + +run(csv.row, 'a,b,c'); // ['a', 'b', 'c'] +run(csv.row, '"a,b",c'); // ['a,b', 'c'] +``` + +- **Zero dependencies**, ESM-only, fully typed +- **Tree-shakeable** — five entry points, import only what you touch +- **126 composable exports** — grouped by what they do, indexed below +- **Mutual recursion out of the box** via `grammar`, no forward declarations + +> **Note:** This library is in active development. The API may change before v1.0.0. + +## Contents + +- [Entry points](#entry-points) +- [Which function do I need?](#which-function-do-i-need) +- [Core concepts](#core-concepts) +- [More examples](#more-examples) +- [API index](#api-index) + +## Entry points + +Nothing is re-exported across entry points, so your bundler only ever sees what you import. + +| Import from | Contains | +| -------------------- | ------------------------------------------------------------------ | +| `unitas` | Types, `success`/`failure`, `run`, `grammar`, `lazy`, `memoize` | +| `unitas/terminals` | Factories that match input directly — `char`, `string`, `regex`, … | +| `unitas/primitives` | Ready-made parsers — `digit`, `letters`, `whitespace`, … | +| `unitas/combinators` | Parsers that take parsers — `map`, `choice`, `sequence`, `many`, … | +| `unitas/utils` | Helpers for `map` callbacks — `pick`, `join`, `pipe`, … | + +## Which function do I need? + +The library is broad on purpose: each function does one small thing, so grammars read like the format they parse. This table is the fastest way in — start from the intent, then look the name up in the [API index](#api-index). + +### Matching a single thing + +| I want to match… | Reach for | +| ------------------------------------------- | ------------------------------------------------------------------------------------------ | +| an exact character or string | `char`, `string` | +| one of several strings (longest wins) | `oneOf` | +| any character in / not in a set | `charOf`, `stringOf`, `noneOf` | +| a character passing a predicate | `satisfy` | +| a regular expression | `regex` | +| a fixed number of characters, or a run | `take`, `takeWhile` | +| a keyword not glued to more word characters | `word` | +| a token, eating trailing whitespace | `token`, `lexeme` | +| a value — no need to build it | `number`, `integer`, `float`, `bool`, `identifier`, `literal`, `line`, `rest` | +| a character class | `letter`, `digit`, `alphaNum`, `hexDigit`, `octDigit`, `lowercase`, `uppercase`, `anyChar` | +| whitespace or a line break | `whitespace`, `space`, `tab`, `nl`, `crlf`, `eol`, `eof` | + +Most primitives come in a singular and a plural form: `letter` matches one, `letters` matches a run of them. The same holds for `digit`/`digits`, `alphaNum`/`alphaNums`, `hexDigit`/`hexDigits`, `octDigit`/`octDigits`, `lowercase`/`lowercases`, `uppercase`/`uppercases`, `space`/`spaces` and `whitespace`/`whitespaces`. Need to know how much input is left? `position`. + +### Repeating + +| I want to… | Reach for | +| ----------------------------------------- | ----------------------------------------------------- | +| repeat zero or more / one or more times | `many`, `many1` | +| repeat a bounded number of times | `exactly`, `manyAtLeast`, `manyAtMost`, `manyBetween` | +| repeat with a separator between items | `separatedBy`, `separatedBy1`, `optionalSeparatedBy` | +| …and allow a trailing separator | `separatedEndBy`, `separatedEndBy1` | +| …where every item ends with the separator | `endBy`, `endBy1`, `interleaved` | +| repeat until something else shows up | `manyTill`, `until`, `separatedUntil` | +| repeat but throw the values away | `skip`, `skipMany`, `skipMany1` | +| reduce the repetitions as I go | `fold`, `fold1`, `foldRight`, `foldRight1` | + +### Choosing and branching + +| I want to… | Reach for | +| ------------------------------------------- | ------------------------------------------------ | +| take the first alternative that matches | `choice` | +| succeed either way | `optional`, `optionalConsume`, `recover`, `flag` | +| succeed only when something _doesn't_ match | `not` | +| gate on a boolean I already have | `guard`, `unless`, `when` | +| look ahead without consuming input | `peek` | + +### Sequencing and pulling values out + +| I want to… | Reach for | +| ------------------------------------------ | ------------------------------------------------------------------------ | +| run parsers in order and keep every result | `sequence` | +| keep only one side of a pair | `left`, `right` | +| keep specific positions of a sequence | `first`, `last`, `nth`, `pick` | +| drop the delimiters, keep the middle | `inner`, `outer` | +| glue string results together | `concat`, `fuse`, `join` | +| match content inside delimiters | `surrounded`, `parenthesized`, `braced`, `bracketed`, `quoted`, `padded` | + +### Shaping the result + +| I want to… | Reach for | +| ------------------------------------------ | ----------------------------------------------------- | +| transform the parsed value | `map`, `pipe` | +| replace it with a constant | `value`, `pure`, `consume` | +| build a typed AST node | `node` | +| reject a value after parsing it | `validate` | +| choose the next parser from the last value | `bind` | +| tidy up arrays inside a `map` | `pick`, `filter`, `flatten`, `shift`, `pop`, `spread` | + +### Grammars, recursion and errors + +| I want to… | Reach for | +| ------------------------------------------ | ------------------------------------------------------ | +| let rules reference each other by name | `grammar` | +| refer to a parser defined further down | `lazy` | +| cache results and avoid exponential blowup | `memoize` | +| attach a readable error message | `label` | +| parse infix operators with precedence | `chainLeft`, `chainLeft1`, `chainRight`, `chainRight1` | +| parse prefix / postfix operators | `prefix`, `postfix` | +| handle both outcomes of a `Result` | `match` | + +## Core concepts + +### The `Parser` type + +A `Parser` is a function from an input string to a `Result`. That is the whole abstraction — everything else in this library either produces one or wraps one. + +```typescript +type Parser = (input: string) => Result; +``` + +### The `Result` type + +```typescript +type Success = { ok: true; value: T; remaining: string }; +type Failure = { ok: false; error?: string }; +type Result = Success | Failure; +``` + +```typescript +{ ok: true, value: 'hello', remaining: ' world' } + │ │ │ + │ │ └── what is left to parse + │ └── the parsed value + └── always true for success +``` + +`remaining` is the important part: it is how input gets consumed and how parsers chain. A failure carries an optional message, which you can always supply later with `label`. + +```typescript +{ ok: false } // generic failure +{ ok: false, error: 'expected a' } // failure with a message +``` + +### Backtracking is free + +Note what a `Failure` does _not_ carry: a position. There is nowhere to record how much input a failed parser got through, and combinators hand every alternative the same string they started with, so a branch that fails can never leave the cursor moved. `choice(a, b)` always offers `b` the full input, however far `a` got. + +If you are coming from Parsec, this is why there is no `try`/`attempt` here — backtracking is unconditional, so there is nothing to opt into. + +### Writing one by hand + +Most of the time you compose existing pieces, but nothing stops you from dropping down a level: + +```typescript +import { create, success, failure } from 'unitas'; + +const parser = create((input) => { + if (input.startsWith('hello')) { + return success('hello', input.slice(5)); + } + + return failure('expected "hello"'); +}); +``` + +### Grammars + +`grammar` hands every rule a proxy `p`, so rules can reference each other — including themselves — without forward declarations or `lazy` boilerplate. + +```typescript +import { grammar, run } from 'unitas'; +import { chainLeft1, choice, map, sequence } from 'unitas/combinators'; +import { char } from 'unitas/terminals'; +import { digits } from 'unitas/primitives'; + +type Math = { expr: number; term: number; value: number }; + +const g = grammar({ + expr: (p) => + chainLeft1( + p.term, + map(char('+'), () => (l: number, r: number) => l + r), + ), + term: (p) => + choice( + p.value, + map(sequence(char('('), p.expr, char(')')), ([, v]) => v), + ), + value: () => digits, +}); + +run(g.expr, '1+2+3'); // 6 +run(g.expr, '(1+2)'); // 3 +``` + +## More examples + +**JSON value** — a tagged union of scalars + +```typescript +import { grammar, run } from 'unitas'; +import { choice, quoted, value } from 'unitas/combinators'; +import { string } from 'unitas/terminals'; +import { bool, digits, letters } from 'unitas/primitives'; + +const json = grammar({ + value: (p) => choice(p.string, p.number, p.bool, p.null), + string: () => quoted(letters), + number: () => digits, + bool: () => bool, + null: () => value(string('null'), null), +}); + +run(json.value, '"hello"'); // 'hello' +run(json.value, '42'); // 42 +run(json.value, 'true'); // true +run(json.value, 'null'); // null +``` + +**Query string** — separated pairs folded into an object + +```typescript +import { grammar, run } from 'unitas'; +import { map, outer, separatedBy } from 'unitas/combinators'; +import { char } from 'unitas/terminals'; +import { letters } from 'unitas/primitives'; + +type Query = { + params: Record; + param: [string, string]; + key: string; + value: string; +}; + +const query = grammar({ + params: (p) => + map(separatedBy(p.param, char('&')), (pairs) => + Object.fromEntries(pairs), + ), + param: (p) => outer(p.key, char('='), p.value), + key: () => letters, + value: () => letters, +}); + +run(query.params, 'foo=bar&baz=qux'); // { foo: 'bar', baz: 'qux' } +``` + +**INI section** — a header plus an entry, with the punctuation dropped + +```typescript +import { grammar, run } from 'unitas'; +import { bracketed, map, outer, sequence } from 'unitas/combinators'; +import { char, regex } from 'unitas/terminals'; +import { letters, nl } from 'unitas/primitives'; +import { pick } from 'unitas/utils'; + +const ini = grammar({ + section: (p) => + map( + sequence(bracketed(p.name), nl, p.entry), + pick(0, 2), + ([name, entry]) => ({ name, entry }), + ), + name: () => letters, + entry: (p) => outer(p.key, char('='), p.value), + key: () => letters, + value: () => regex(/^[^\n]+/), +}); + +run(ini.section, '[database]\nhost=localhost'); // { name: 'database', entry: ['host', 'localhost'] } +``` + +## API index + +One line per export. Every name links to its full description and runnable example in the reference pages under [`doc/api`](https://github.com/sovrin/unitas/tree/master/doc/api). + +### Core — `unitas` + +Types, constructors and the grammar runner. **9** exports — [full reference →][api-core] + +| | | +| ------------------------- | --------------------------------------------------------------------------- | +| [`failure`][core-failure] | Creates a failed result with an optional error message. | +| [`grammar`][core-grammar] | Creates a recursive grammar where rules can reference each other. | +| [`label`][core-label] | Labels a parser with a custom error message on failure. | +| [`lazy`][core-lazy] | Defers parser creation, useful for recursive grammars. | +| [`match`][core-match] | Pattern matching on a Result to handle success and failure cases. | +| [`memoize`][core-memoize] | Memoizes a parser to cache results by input string. | +| [`parser`][core-parser] | Creates a parser from a parser function. | +| [`run`][core-run] | Runs a parser and returns the value, throws on failure or unconsumed input. | +| [`success`][core-success] | Creates a successful result with a value and remaining input. | + +### Terminals — `unitas/terminals` + +Factories that match the input string directly. **12** exports — [full reference →][api-terminals] + +| | | +| ---------------------------------- | --------------------------------------------------------------------------- | +| [`char`][terminals-char] | Parse a specific character. | +| [`charOf`][terminals-charof] | Parse any character from a set. | +| [`noneOf`][terminals-noneof] | Parse any character not in the set. | +| [`oneOf`][terminals-oneof] | Parse one string from a set of strings (longest match wins). | +| [`regex`][terminals-regex] | Parse with a regular expression. | +| [`satisfy`][terminals-satisfy] | Parse a character satisfying a predicate. | +| [`string`][terminals-string] | Parse a specific string. | +| [`stringOf`][terminals-stringof] | Parse first character that exists in string (like charOf but for a string). | +| [`take`][terminals-take] | Take n characters. | +| [`takeWhile`][terminals-takewhile] | Takes characters while the predicate returns true. | +| [`token`][terminals-token] | Parses a string as a token, skipping trailing whitespace. | +| [`word`][terminals-word] | Parses a specific word and ensures it is not followed by word characters. | + +### Primitives — `unitas/primitives` + +Ready-made parsers for the usual suspects. **33** exports — [full reference →][api-primitives] + +| | | +| --------------------------------------- | ------------------------------------------------------------------------------------ | +| [`alphaNum`][primitives-alphanum] | Parse a single alphanumeric character. | +| [`alphaNums`][primitives-alphanums] | Parse one or more alphanumeric characters. | +| [`anyChar`][primitives-anychar] | Parse any single character. | +| [`bool`][primitives-bool] | Parse a boolean literal. | +| [`crlf`][primitives-crlf] | Parse CRLF line ending. | +| [`digit`][primitives-digit] | Parse a single digit and return as number. | +| [`digits`][primitives-digits] | Parse one or more digits and return as number. | +| [`eof`][primitives-eof] | Parse end of file (succeeds only on empty input). | +| [`eol`][primitives-eol] | Parse end of line (\\n, \\r\\n, or EOF). | +| [`float`][primitives-float] | Parse a floating point number. | +| [`hexDigit`][primitives-hexdigit] | Parse a single hexadecimal digit. | +| [`hexDigits`][primitives-hexdigits] | Parse one or more hexadecimal digits. | +| [`identifier`][primitives-identifier] | Parse an identifier — starts with letter or underscore, no leading digit, no hyphen. | +| [`integer`][primitives-integer] | Parse a signed integer. | +| [`letter`][primitives-letter] | Parse a single letter. | +| [`letters`][primitives-letters] | Parse one or more letters. | +| [`line`][primitives-line] | Parse until end of line. | +| [`literal`][primitives-literal] | Parse a word-like value including hyphens. | +| [`lowercase`][primitives-lowercase] | Parse a single lowercase letter. | +| [`lowercases`][primitives-lowercases] | Parses one or more lowercase letters. | +| [`nl`][primitives-nl] | Parse a newline character. | +| [`number`][primitives-number] | Parse an integer or float. | +| [`octDigit`][primitives-octdigit] | Parse a single octal digit. | +| [`octDigits`][primitives-octdigits] | Parse one or more octal digits. | +| [`position`][primitives-position] | Get current position (remaining input length). | +| [`rest`][primitives-rest] | Parse the rest of the input. | +| [`space`][primitives-space] | Parse a single space character. | +| [`spaces`][primitives-spaces] | Parse one or more space characters. | +| [`tab`][primitives-tab] | Parse tab character. | +| [`uppercase`][primitives-uppercase] | Parses a single uppercase letter. | +| [`uppercases`][primitives-uppercases] | Parses one or more uppercase letters. | +| [`whitespace`][primitives-whitespace] | Parses a single whitespace character. | +| [`whitespaces`][primitives-whitespaces] | Parses one or more whitespace characters. | + +### Combinators — `unitas/combinators` + +Take parsers, return a new parser. **64** exports — [full reference →][api-combinators] + +| | | +| -------------------------------------------------------- | --------------------------------------------------------------------------- | +| [`bind`][combinators-bind] | Chain parsers where the second parser depends on the first result. | +| [`braced`][combinators-braced] | Parse content surrounded by braces. | +| [`bracketed`][combinators-bracketed] | Parse content surrounded by brackets. | +| [`chainLeft`][combinators-chainleft] | Chain left-associative operations (right-to-left for same precedence). | +| [`chainLeft1`][combinators-chainleft1] | Chain left-associative operations (fails on empty input). | +| [`chainRight`][combinators-chainright] | Chain right-associative operations (right-to-left grouping). | +| [`chainRight1`][combinators-chainright1] | Chain right-associative operations (fails on empty input). | +| [`choice`][combinators-choice] | Try each parser in order, return first success. | +| [`concat`][combinators-concat] | Join string array parser result into a single string. | +| [`consume`][combinators-consume] | Consume input but discard the result (return null). | +| [`endBy`][combinators-endby] | Zero or more items separated and ending with terminator. | +| [`endBy1`][combinators-endby1] | One or more items separated and ending with terminator. | +| [`exactly`][combinators-exactly] | Parse exactly n occurrences. | +| [`first`][combinators-first] | Extract the first element from a parser result array. | +| [`flag`][combinators-flag] | Return true if parser succeeds, false otherwise. | +| [`fold`][combinators-fold] | Parse zero or more and fold into a single value. | +| [`fold1`][combinators-fold1] | Parse one or more and fold into a single value. | +| [`foldRight`][combinators-foldright] | Parse zero or more and fold right-to-left. | +| [`foldRight1`][combinators-foldright1] | Parse one or more and fold right-to-left. | +| [`fuse`][combinators-fuse] | Fuse multiple string parsers into a single one. | +| [`guard`][combinators-guard] | Conditionally apply parser based on a condition. | +| [`inner`][combinators-inner] | Extract inner value from surrounded content (like inner of braced). | +| [`interleaved`][combinators-interleaved] | Parse items with interleaved separators. | +| [`last`][combinators-last] | Extract the last element from a parser result array. | +| [`left`][combinators-left] | Keep only the left result from a sequence. | +| [`lexeme`][combinators-lexeme] | Parser that consumes trailing whitespace. | +| [`many`][combinators-many] | Zero or more occurrences (never fails). | +| [`many1`][combinators-many1] | One or more occurrences (fails if no matches). | +| [`manyAtLeast`][combinators-manyatleast] | Parse at least n occurrences. | +| [`manyAtMost`][combinators-manyatmost] | Parse at most n occurrences. | +| [`manyBetween`][combinators-manybetween] | Parse between min and max occurrences. | +| [`manyTill`][combinators-manytill] | Parse zero or more until terminator matches. | +| [`map`][combinators-map] | Transform the parsed value. | +| [`node`][combinators-node] | Create a node from parser fields. | +| [`not`][combinators-not] | Succeed if parser fails (without consuming input). | +| [`nth`][combinators-nth] | Extract the nth element from a parser result array. | +| [`optional`][combinators-optional] | Make parser optional (return null on failure, without consuming input). | +| [`optionalConsume`][combinators-optionalconsume] | Optionally consume input (always succeeds, returns void). | +| [`optionalSeparatedBy`][combinators-optionalseparatedby] | Zero or more items separated by a separator, with optional null values. | +| [`outer`][combinators-outer] | Extract outer values from a sequence of 3 parsers (skip middle). | +| [`padded`][combinators-padded] | Parse content with optional whitespace on both sides. | +| [`parenthesized`][combinators-parenthesized] | Parse content surrounded by parentheses. | +| [`peek`][combinators-peek] | Parse without consuming input. | +| [`postfix`][combinators-postfix] | Parse postfix operators (chains atom with operators that return functions). | +| [`prefix`][combinators-prefix] | Parse prefix operators (like - in -5). | +| [`pure`][combinators-pure] | Always return a value without consuming input. | +| [`quoted`][combinators-quoted] | Parse content surrounded by single or double quotes. | +| [`recover`][combinators-recover] | Use fallback value when parser fails. | +| [`right`][combinators-right] | Keep only the right result from a sequence. | +| [`separatedBy`][combinators-separatedby] | Zero or more items separated by a separator. | +| [`separatedBy1`][combinators-separatedby1] | One or more items separated by a separator. | +| [`separatedEndBy`][combinators-separatedendby] | Zero or more items separated by and ending with a terminator. | +| [`separatedEndBy1`][combinators-separatedendby1] | One or more items separated by and ending with a terminator. | +| [`separatedUntil`][combinators-separateduntil] | Parse items separated by separator until terminator matches. | +| [`sequence`][combinators-sequence] | Parse a sequence of parsers and return all results as an array. | +| [`skip`][combinators-skip] | Skip a parser n times. | +| [`skipMany`][combinators-skipmany] | Skip zero or more occurrences (never fails, returns null). | +| [`skipMany1`][combinators-skipmany1] | Skip one or more occurrences (fails if no matches). | +| [`surrounded`][combinators-surrounded] | Parse content surrounded by delimiters. | +| [`unless`][combinators-unless] | Parse unless condition is true (inverse of guard). | +| [`until`][combinators-until] | Parse until terminator matches (fails if terminator never matches). | +| [`validate`][combinators-validate] | Validate parsed value with a predicate. | +| [`value`][combinators-value] | Replace parsed value with a constant. | +| [`when`][combinators-when] | Branch on a boolean parser result. | + +### Utils — `unitas/utils` + +Plain helpers for `map` callbacks. **8** exports — [full reference →][api-utils] + +| | | +| -------------------------- | --------------------------------------- | +| [`filter`][utils-filter] | Exclude values from array. | +| [`flatten`][utils-flatten] | Flatten nested arrays. | +| [`join`][utils-join] | Join array elements into a string. | +| [`pick`][utils-pick] | Pick elements from an array by index. | +| [`pipe`][utils-pipe] | Pipe parser functions together. | +| [`pop`][utils-pop] | Get the last element of an array. | +| [`shift`][utils-shift] | Get the first element of an array. | +| [`spread`][utils-spread] | Collect spread arguments into an array. | + +[api-core]: https://github.com/sovrin/unitas/blob/master/doc/api/core.md +[core-failure]: https://github.com/sovrin/unitas/blob/master/doc/api/core.md#failure +[core-grammar]: https://github.com/sovrin/unitas/blob/master/doc/api/core.md#grammar +[core-label]: https://github.com/sovrin/unitas/blob/master/doc/api/core.md#label +[core-lazy]: https://github.com/sovrin/unitas/blob/master/doc/api/core.md#lazy +[core-match]: https://github.com/sovrin/unitas/blob/master/doc/api/core.md#match +[core-memoize]: https://github.com/sovrin/unitas/blob/master/doc/api/core.md#memoize +[core-parser]: https://github.com/sovrin/unitas/blob/master/doc/api/core.md#parser +[core-run]: https://github.com/sovrin/unitas/blob/master/doc/api/core.md#run +[core-success]: https://github.com/sovrin/unitas/blob/master/doc/api/core.md#success +[api-terminals]: https://github.com/sovrin/unitas/blob/master/doc/api/terminals.md +[terminals-char]: https://github.com/sovrin/unitas/blob/master/doc/api/terminals.md#char +[terminals-charof]: https://github.com/sovrin/unitas/blob/master/doc/api/terminals.md#charof +[terminals-noneof]: https://github.com/sovrin/unitas/blob/master/doc/api/terminals.md#noneof +[terminals-oneof]: https://github.com/sovrin/unitas/blob/master/doc/api/terminals.md#oneof +[terminals-regex]: https://github.com/sovrin/unitas/blob/master/doc/api/terminals.md#regex +[terminals-satisfy]: https://github.com/sovrin/unitas/blob/master/doc/api/terminals.md#satisfy +[terminals-string]: https://github.com/sovrin/unitas/blob/master/doc/api/terminals.md#string +[terminals-stringof]: https://github.com/sovrin/unitas/blob/master/doc/api/terminals.md#stringof +[terminals-take]: https://github.com/sovrin/unitas/blob/master/doc/api/terminals.md#take +[terminals-takewhile]: https://github.com/sovrin/unitas/blob/master/doc/api/terminals.md#takewhile +[terminals-token]: https://github.com/sovrin/unitas/blob/master/doc/api/terminals.md#token +[terminals-word]: https://github.com/sovrin/unitas/blob/master/doc/api/terminals.md#word +[api-primitives]: https://github.com/sovrin/unitas/blob/master/doc/api/primitives.md +[primitives-alphanum]: https://github.com/sovrin/unitas/blob/master/doc/api/primitives.md#alphanum +[primitives-alphanums]: https://github.com/sovrin/unitas/blob/master/doc/api/primitives.md#alphanums +[primitives-anychar]: https://github.com/sovrin/unitas/blob/master/doc/api/primitives.md#anychar +[primitives-bool]: https://github.com/sovrin/unitas/blob/master/doc/api/primitives.md#bool +[primitives-crlf]: https://github.com/sovrin/unitas/blob/master/doc/api/primitives.md#crlf +[primitives-digit]: https://github.com/sovrin/unitas/blob/master/doc/api/primitives.md#digit +[primitives-digits]: https://github.com/sovrin/unitas/blob/master/doc/api/primitives.md#digits +[primitives-eof]: https://github.com/sovrin/unitas/blob/master/doc/api/primitives.md#eof +[primitives-eol]: https://github.com/sovrin/unitas/blob/master/doc/api/primitives.md#eol +[primitives-float]: https://github.com/sovrin/unitas/blob/master/doc/api/primitives.md#float +[primitives-hexdigit]: https://github.com/sovrin/unitas/blob/master/doc/api/primitives.md#hexdigit +[primitives-hexdigits]: https://github.com/sovrin/unitas/blob/master/doc/api/primitives.md#hexdigits +[primitives-identifier]: https://github.com/sovrin/unitas/blob/master/doc/api/primitives.md#identifier +[primitives-integer]: https://github.com/sovrin/unitas/blob/master/doc/api/primitives.md#integer +[primitives-letter]: https://github.com/sovrin/unitas/blob/master/doc/api/primitives.md#letter +[primitives-letters]: https://github.com/sovrin/unitas/blob/master/doc/api/primitives.md#letters +[primitives-line]: https://github.com/sovrin/unitas/blob/master/doc/api/primitives.md#line +[primitives-literal]: https://github.com/sovrin/unitas/blob/master/doc/api/primitives.md#literal +[primitives-lowercase]: https://github.com/sovrin/unitas/blob/master/doc/api/primitives.md#lowercase +[primitives-lowercases]: https://github.com/sovrin/unitas/blob/master/doc/api/primitives.md#lowercases +[primitives-nl]: https://github.com/sovrin/unitas/blob/master/doc/api/primitives.md#nl +[primitives-number]: https://github.com/sovrin/unitas/blob/master/doc/api/primitives.md#number +[primitives-octdigit]: https://github.com/sovrin/unitas/blob/master/doc/api/primitives.md#octdigit +[primitives-octdigits]: https://github.com/sovrin/unitas/blob/master/doc/api/primitives.md#octdigits +[primitives-position]: https://github.com/sovrin/unitas/blob/master/doc/api/primitives.md#position +[primitives-rest]: https://github.com/sovrin/unitas/blob/master/doc/api/primitives.md#rest +[primitives-space]: https://github.com/sovrin/unitas/blob/master/doc/api/primitives.md#space +[primitives-spaces]: https://github.com/sovrin/unitas/blob/master/doc/api/primitives.md#spaces +[primitives-tab]: https://github.com/sovrin/unitas/blob/master/doc/api/primitives.md#tab +[primitives-uppercase]: https://github.com/sovrin/unitas/blob/master/doc/api/primitives.md#uppercase +[primitives-uppercases]: https://github.com/sovrin/unitas/blob/master/doc/api/primitives.md#uppercases +[primitives-whitespace]: https://github.com/sovrin/unitas/blob/master/doc/api/primitives.md#whitespace +[primitives-whitespaces]: https://github.com/sovrin/unitas/blob/master/doc/api/primitives.md#whitespaces +[api-combinators]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md +[combinators-bind]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#bind +[combinators-braced]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#braced +[combinators-bracketed]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#bracketed +[combinators-chainleft]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#chainleft +[combinators-chainleft1]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#chainleft1 +[combinators-chainright]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#chainright +[combinators-chainright1]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#chainright1 +[combinators-choice]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#choice +[combinators-concat]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#concat +[combinators-consume]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#consume +[combinators-endby]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#endby +[combinators-endby1]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#endby1 +[combinators-exactly]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#exactly +[combinators-first]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#first +[combinators-flag]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#flag +[combinators-fold]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#fold +[combinators-fold1]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#fold1 +[combinators-foldright]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#foldright +[combinators-foldright1]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#foldright1 +[combinators-fuse]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#fuse +[combinators-guard]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#guard +[combinators-inner]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#inner +[combinators-interleaved]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#interleaved +[combinators-last]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#last +[combinators-left]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#left +[combinators-lexeme]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#lexeme +[combinators-many]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#many +[combinators-many1]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#many1 +[combinators-manyatleast]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#manyatleast +[combinators-manyatmost]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#manyatmost +[combinators-manybetween]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#manybetween +[combinators-manytill]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#manytill +[combinators-map]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#map +[combinators-node]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#node +[combinators-not]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#not +[combinators-nth]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#nth +[combinators-optional]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#optional +[combinators-optionalconsume]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#optionalconsume +[combinators-optionalseparatedby]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#optionalseparatedby +[combinators-outer]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#outer +[combinators-padded]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#padded +[combinators-parenthesized]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#parenthesized +[combinators-peek]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#peek +[combinators-postfix]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#postfix +[combinators-prefix]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#prefix +[combinators-pure]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#pure +[combinators-quoted]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#quoted +[combinators-recover]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#recover +[combinators-right]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#right +[combinators-separatedby]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#separatedby +[combinators-separatedby1]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#separatedby1 +[combinators-separatedendby]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#separatedendby +[combinators-separatedendby1]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#separatedendby1 +[combinators-separateduntil]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#separateduntil +[combinators-sequence]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#sequence +[combinators-skip]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#skip +[combinators-skipmany]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#skipmany +[combinators-skipmany1]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#skipmany1 +[combinators-surrounded]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#surrounded +[combinators-unless]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#unless +[combinators-until]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#until +[combinators-validate]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#validate +[combinators-value]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#value +[combinators-when]: https://github.com/sovrin/unitas/blob/master/doc/api/combinators.md#when +[api-utils]: https://github.com/sovrin/unitas/blob/master/doc/api/utils.md +[utils-filter]: https://github.com/sovrin/unitas/blob/master/doc/api/utils.md#filter +[utils-flatten]: https://github.com/sovrin/unitas/blob/master/doc/api/utils.md#flatten +[utils-join]: https://github.com/sovrin/unitas/blob/master/doc/api/utils.md#join +[utils-pick]: https://github.com/sovrin/unitas/blob/master/doc/api/utils.md#pick +[utils-pipe]: https://github.com/sovrin/unitas/blob/master/doc/api/utils.md#pipe +[utils-pop]: https://github.com/sovrin/unitas/blob/master/doc/api/utils.md#pop +[utils-shift]: https://github.com/sovrin/unitas/blob/master/doc/api/utils.md#shift +[utils-spread]: https://github.com/sovrin/unitas/blob/master/doc/api/utils.md#spread + +## Contributing + +```bash +npm test # run the suite +npm run build # compile to dist/ +npm run generate:index # regenerate barrel files after adding a source file +npm run generate:readme # regenerate README.md and doc/api/ from JSDoc +``` + +Every export lives in its own file with a co-located test and a JSDoc `@example`. Those examples are the single source of truth: they are extracted into `test/examples.test.ts` and into the documentation, so they cannot drift. + +## License + +MIT © [Oleg Kamlowski](https://github.com/sovrin) diff --git a/bin/generate-readme.mjs b/bin/generate-readme.mjs index 7d03a11..1e79e1e 100644 --- a/bin/generate-readme.mjs +++ b/bin/generate-readme.mjs @@ -5,11 +5,66 @@ import { fileURLToPath } from 'node:url'; const directoryName = path.dirname(fileURLToPath(import.meta.url)); const rootDir = path.resolve(directoryName, '..'); const srcDir = path.join(rootDir, 'src'); -const templatePath = path.join(rootDir, 'doc', 'README.template.md'); +const docDir = path.join(rootDir, 'doc'); +const apiDir = path.join(docDir, 'api'); +const readmeTemplatePath = path.join(docDir, 'README.template.md'); +const apiTemplatePath = path.join(docDir, 'api.template.md'); const readmePath = path.join(rootDir, 'README.md'); const COMMENT_REGEX = /\/\*\*([\s\S]*?)\*\//g; +const CHEAT_SHEET_HEADING = '## Which function do I need?'; + +/** Covered by the "Core concepts" prose instead of the cheat sheet. */ +const CHEAT_SHEET_EXEMPT = new Set(['failure', 'parser', 'run', 'success']); + +/** + * Module metadata. Order here drives the order of the API index in the README + * and the cross-page navigation on every generated reference page. + */ +const MODULES = [ + { + key: 'core', + title: 'Core', + entry: 'unitas', + tagline: 'Types, constructors and the grammar runner.', + intro: + 'The core entry point holds everything a parser is built from: the `Parser` and `Result` types, the `success`/`failure` constructors, and the tools for running and wiring parsers together (`run`, `grammar`, `lazy`, `memoize`).', + }, + { + key: 'terminals', + title: 'Terminals', + entry: 'unitas/terminals', + tagline: 'Factories that match the input string directly.', + intro: + 'Terminals are the leaves of a grammar. They do not take other parsers — they inspect the input string themselves. Each one is a factory: call it with what you want to match and it hands back a `Parser`.', + }, + { + key: 'primitives', + title: 'Primitives', + entry: 'unitas/primitives', + tagline: 'Ready-made parsers for the usual suspects.', + intro: + 'Primitives are parser *instances*, not factories. Where a terminal needs an argument (`char("a")`), a primitive is already a parser and can be passed straight to a combinator (`many(digit)`).', + }, + { + key: 'combinators', + title: 'Combinators', + entry: 'unitas/combinators', + tagline: 'Take parsers, return a new parser.', + intro: + 'Combinators are the glue. Every one of them takes one or more parsers and returns a new parser, which is what lets a grammar stay a set of small, independently testable pieces.', + }, + { + key: 'utils', + title: 'Utils', + entry: 'unitas/utils', + tagline: 'Plain helpers for `map` callbacks.', + intro: + 'Utils are not parsers. They are small curried helpers meant to be dropped into a `map` callback so reshaping a result stays a one-liner instead of an arrow function.', + }, +]; + function extractDocs(filePath) { const content = fs.readFileSync(filePath, 'utf-8'); const docs = []; @@ -113,68 +168,149 @@ function formatExample(example) { return `\`\`\`typescript\n${resultLines.join('\n')}\n\`\`\``; } -function generateDocsSection(namespace, items) { - if (items.length === 0) return ''; +/** + * The README index shows one line per export, so it gets the headline sentence + * only — the full description stays on the reference page. + */ +function summarize(description) { + const [first] = description.split(/(?<=\.)\s+(?=[A-Z])/); - const lines = items.map(({ name, description, example }) => { - const formatted = formatExample(example); - return `
-
${name} — ${description} + return (first || description).replace(/\|/g, '\\|'); +} -${formatted} -`; - }); +function generateApiPage(module, items) { + const template = fs.readFileSync(apiTemplatePath, 'utf-8'); + + const toc = items + .map(({ name }) => `[\`${name}\`](#${name.toLowerCase()})`) + .join(' · '); + + const body = items + .map(({ name, description, example }) => + [ + `### \`${name}\``, + '', + description, + '', + formatExample(example), + ].join('\n'), + ) + .join('\n\n'); + + const nav = MODULES.map(({ key, title }) => + key === module.key ? `**${title}**` : `[${title}](./${key}.md)`, + ).join(' · '); + + return template + .replaceAll('<$title>', module.title) + .replaceAll('<$entry>', module.entry) + .replaceAll('<$intro>', module.intro) + .replaceAll('<$count>', String(items.length)) + .replaceAll('<$nav>', nav) + .replaceAll('<$toc>', toc) + .replaceAll('<$body>', body); +} - return lines.join('\n'); +/** + * Absolute links, not relative ones: the `doc/` directory is not published to + * npm, so a relative link would dead-end on npmjs.com. + */ +function apiUrl(baseUrl, key, anchor) { + const url = `${baseUrl}/doc/api/${key}.md`; + + return anchor ? `${url}#${anchor.toLowerCase()}` : url; } -function generateTOC(docsByNamespace) { - const sections = [ - { key: 'core', title: 'Core' }, - { key: 'terminals', title: 'Terminals' }, - { key: 'primitives', title: 'Primitives' }, - { key: 'combinators', title: 'Combinators' }, - { key: 'utils', title: 'Utils' }, - ]; - - const lines = []; - for (const { key, title } of sections) { - const items = docsByNamespace[key] || []; - if (items.length === 0) continue; +/** + * Reference-style links, not inline ones: the absolute URLs are long enough + * that inlining them makes the formatter pad every table cell to ~80 columns. + */ +function generateIndex(baseUrl, docsByNamespace) { + const sections = []; + const definitions = []; - lines.push(`- [${title}](#${title.toLowerCase()})`); + for (const module of MODULES) { + const items = docsByNamespace[module.key] || []; + if (items.length === 0) continue; - for (const { name } of items) { - lines.push(` - [\`${name}\`](#${name})`); - } + const pageRef = `api-${module.key}`; + definitions.push(`[${pageRef}]: ${apiUrl(baseUrl, module.key)}`); + + const rows = items.map(({ name, description }) => { + const ref = `${module.key}-${name.toLowerCase()}`; + definitions.push(`[${ref}]: ${apiUrl(baseUrl, module.key, name)}`); + + return `| [\`${name}\`][${ref}] | ${summarize(description)} |`; + }); + + sections.push( + [ + `### ${module.title} — \`${module.entry}\``, + '', + `${module.tagline} **${items.length}** exports — [full reference →][${pageRef}]`, + '', + '| | |', + '| --- | --- |', + ...rows, + ].join('\n'), + ); } - return lines.join('\n'); + return [...sections, definitions.join('\n')].join('\n\n'); } -function generateReadme(docsByNamespace) { - let template = fs.readFileSync(templatePath, 'utf-8'); - - const placeholderMap = { - '<$core>': 'core', - '<$terminals>': 'terminals', - '<$primitives>': 'primitives', - '<$combinators>': 'combinators', - '<$utils>': 'utils', - }; - - for (const [placeholder, namespace] of Object.entries(placeholderMap)) { - const items = docsByNamespace[namespace] || []; - const sectionContent = generateDocsSection(namespace, items); - template = template.replace(placeholder, sectionContent); - } +function generateReadme(baseUrl, docsByNamespace) { + const template = fs.readFileSync(readmeTemplatePath, 'utf-8'); + const total = Object.values(docsByNamespace).reduce( + (sum, items) => sum + items.length, + 0, + ); - template = template.replace('<$toc>', generateTOC(docsByNamespace)); + const content = template + .replaceAll('<$index>', generateIndex(baseUrl, docsByNamespace)) + .replaceAll('<$total>', String(total)); - fs.writeFileSync(readmePath, template); + fs.writeFileSync(readmePath, content); console.log(`Updated ${readmePath}`); + + warnAboutCheatSheetGaps(template, docsByNamespace); } +/** + * The cheat sheet is hand-written, so it silently rots as exports are added. + * Every name should be reachable by intent, not just alphabetically. + */ +function warnAboutCheatSheetGaps(template, docsByNamespace) { + const start = template.indexOf(CHEAT_SHEET_HEADING); + const end = template.indexOf('\n## ', start + 1); + if (start === -1) return; + + const section = template.slice(start, end === -1 ? undefined : end); + const mentioned = new Set( + [...section.matchAll(/`([A-Za-z][A-Za-z0-9]*)`/g)].map(([, n]) => n), + ); + + const missing = MODULES.flatMap(({ key }) => + (docsByNamespace[key] || []) + .map(({ name }) => name) + .filter( + (name) => + !mentioned.has(name) && !CHEAT_SHEET_EXEMPT.has(name), + ), + ); + + if (missing.length > 0) { + console.warn( + `Warning: ${missing.length} export(s) missing from the "${CHEAT_SHEET_HEADING}" table in ${path.relative(rootDir, readmeTemplatePath)}: ${missing.join(', ')}`, + ); + } +} + +const pkg = JSON.parse( + fs.readFileSync(path.join(rootDir, 'package.json'), 'utf-8'), +); +const baseUrl = `${pkg.repository.url.replace(/\.git$/, '')}/blob/master`; + const files = findSourceFiles(srcDir); const docsByNamespace = {}; @@ -189,4 +325,20 @@ for (const file of files) { docsByNamespace[namespace].push(...docs); } -generateReadme(docsByNamespace); +for (const items of Object.values(docsByNamespace)) { + items.sort((a, b) => a.name.localeCompare(b.name)); +} + +fs.rmSync(apiDir, { force: true, recursive: true }); +fs.mkdirSync(apiDir, { recursive: true }); + +for (const module of MODULES) { + const items = docsByNamespace[module.key] || []; + if (items.length === 0) continue; + + const target = path.join(apiDir, `${module.key}.md`); + fs.writeFileSync(target, generateApiPage(module, items)); + console.log(`Updated ${target} (${items.length} exports)`); +} + +generateReadme(baseUrl, docsByNamespace); diff --git a/doc/README.template.md b/doc/README.template.md index 4a2761f..846254d 100644 --- a/doc/README.template.md +++ b/doc/README.template.md @@ -1,34 +1,19 @@ -# unitas — composing parsers into a unified whole + -Unitas logo +# unitas + + + +**Composing parsers into a unified whole.** A lightweight, TypeScript-first parser combinator library — write small parsers, combine them into big ones, get typed results back. [![npm version](https://img.shields.io/npm/v/unitas)](https://www.npmjs.com/package/unitas) [![Coverage](https://coveralls.io/repos/github/sovrin/unitas/badge.svg?branch=master)](https://coveralls.io/github/sovrin/unitas?branch=master) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) -A lightweight, TypeScript-first parser combinator library for building expressive and composable parsers. - -## Features - -- **Parser Combinators**: Compose small parsers into complex ones using combinators like `many`, `choice`, `sequence`, and more -- **Terminals**: Factory functions for common patterns (`char`, `string`, `regex`, etc.) -- **Primitives**: Pre-built parser instances ready to use (`digit`, `letter`, `whitespace`, etc.) -- **TypeScript**: Full TypeScript support with generic types and inference -- **Tree-shakeable**: ESM-only with separate exports for `combinators`, `terminals`, `primitives`, and `utils` -- **No dependencies**: Zero external runtime dependencies - -> **Note:** This library is in active development. The API may change before v1.0.0. - -## Installation - ```bash npm install unitas ``` -## Quick Start - -**CSV parser** — parsing comma-separated values with quoted fields - ```typescript import { grammar, run } from 'unitas'; import { choice, inner, separatedBy } from 'unitas/combinators'; @@ -46,11 +31,204 @@ run(csv.row, 'a,b,c'); // ['a', 'b', 'c'] run(csv.row, '"a,b",c'); // ['a,b', 'c'] ``` -**JSON value parser** — parsing simple json values +- **Zero dependencies**, ESM-only, fully typed +- **Tree-shakeable** — five entry points, import only what you touch +- **<$total> composable exports** — grouped by what they do, indexed below +- **Mutual recursion out of the box** via `grammar`, no forward declarations + +> **Note:** This library is in active development. The API may change before v1.0.0. + +## Contents + +- [Entry points](#entry-points) +- [Which function do I need?](#which-function-do-i-need) +- [Core concepts](#core-concepts) +- [More examples](#more-examples) +- [API index](#api-index) + +## Entry points + +Nothing is re-exported across entry points, so your bundler only ever sees what you import. + +| Import from | Contains | +| ---------------------- | --------------------------------------------------------------------- | +| `unitas` | Types, `success`/`failure`, `run`, `grammar`, `lazy`, `memoize` | +| `unitas/terminals` | Factories that match input directly — `char`, `string`, `regex`, … | +| `unitas/primitives` | Ready-made parsers — `digit`, `letters`, `whitespace`, … | +| `unitas/combinators` | Parsers that take parsers — `map`, `choice`, `sequence`, `many`, … | +| `unitas/utils` | Helpers for `map` callbacks — `pick`, `join`, `pipe`, … | + +## Which function do I need? + +The library is broad on purpose: each function does one small thing, so grammars read like the format they parse. This table is the fastest way in — start from the intent, then look the name up in the [API index](#api-index). + +### Matching a single thing + +| I want to match… | Reach for | +| ------------------------------------------- | ---------------------------------------------------- | +| an exact character or string | `char`, `string` | +| one of several strings (longest wins) | `oneOf` | +| any character in / not in a set | `charOf`, `stringOf`, `noneOf` | +| a character passing a predicate | `satisfy` | +| a regular expression | `regex` | +| a fixed number of characters, or a run | `take`, `takeWhile` | +| a keyword not glued to more word characters | `word` | +| a token, eating trailing whitespace | `token`, `lexeme` | +| a value — no need to build it | `number`, `integer`, `float`, `bool`, `identifier`, `literal`, `line`, `rest` | +| a character class | `letter`, `digit`, `alphaNum`, `hexDigit`, `octDigit`, `lowercase`, `uppercase`, `anyChar` | +| whitespace or a line break | `whitespace`, `space`, `tab`, `nl`, `crlf`, `eol`, `eof` | + +Most primitives come in a singular and a plural form: `letter` matches one, `letters` matches a run of them. The same holds for `digit`/`digits`, `alphaNum`/`alphaNums`, `hexDigit`/`hexDigits`, `octDigit`/`octDigits`, `lowercase`/`lowercases`, `uppercase`/`uppercases`, `space`/`spaces` and `whitespace`/`whitespaces`. Need to know how much input is left? `position`. + +### Repeating + +| I want to… | Reach for | +| -------------------------------------------- | ---------------------------------------------------- | +| repeat zero or more / one or more times | `many`, `many1` | +| repeat a bounded number of times | `exactly`, `manyAtLeast`, `manyAtMost`, `manyBetween` | +| repeat with a separator between items | `separatedBy`, `separatedBy1`, `optionalSeparatedBy` | +| …and allow a trailing separator | `separatedEndBy`, `separatedEndBy1` | +| …where every item ends with the separator | `endBy`, `endBy1`, `interleaved` | +| repeat until something else shows up | `manyTill`, `until`, `separatedUntil` | +| repeat but throw the values away | `skip`, `skipMany`, `skipMany1` | +| reduce the repetitions as I go | `fold`, `fold1`, `foldRight`, `foldRight1` | + +### Choosing and branching + +| I want to… | Reach for | +| -------------------------------------------- | ---------------------------------------------------- | +| take the first alternative that matches | `choice` | +| succeed either way | `optional`, `optionalConsume`, `recover`, `flag` | +| succeed only when something *doesn't* match | `not` | +| gate on a boolean I already have | `guard`, `unless`, `when` | +| look ahead without consuming input | `peek` | + +### Sequencing and pulling values out + +| I want to… | Reach for | +| -------------------------------------------- | ---------------------------------------------------- | +| run parsers in order and keep every result | `sequence` | +| keep only one side of a pair | `left`, `right` | +| keep specific positions of a sequence | `first`, `last`, `nth`, `pick` | +| drop the delimiters, keep the middle | `inner`, `outer` | +| glue string results together | `concat`, `fuse`, `join` | +| match content inside delimiters | `surrounded`, `parenthesized`, `braced`, `bracketed`, `quoted`, `padded` | + +### Shaping the result + +| I want to… | Reach for | +| -------------------------------------------- | ---------------------------------------------------- | +| transform the parsed value | `map`, `pipe` | +| replace it with a constant | `value`, `pure`, `consume` | +| build a typed AST node | `node` | +| reject a value after parsing it | `validate` | +| choose the next parser from the last value | `bind` | +| tidy up arrays inside a `map` | `pick`, `filter`, `flatten`, `shift`, `pop`, `spread` | + +### Grammars, recursion and errors + +| I want to… | Reach for | +| -------------------------------------------- | ---------------------------------------------------- | +| let rules reference each other by name | `grammar` | +| refer to a parser defined further down | `lazy` | +| cache results and avoid exponential blowup | `memoize` | +| attach a readable error message | `label` | +| parse infix operators with precedence | `chainLeft`, `chainLeft1`, `chainRight`, `chainRight1` | +| parse prefix / postfix operators | `prefix`, `postfix` | +| handle both outcomes of a `Result` | `match` | + +## Core concepts + +### The `Parser` type + +A `Parser` is a function from an input string to a `Result`. That is the whole abstraction — everything else in this library either produces one or wraps one. + +```typescript +type Parser = (input: string) => Result; +``` + +### The `Result` type + +```typescript +type Success = { ok: true; value: T; remaining: string }; +type Failure = { ok: false; error?: string }; +type Result = Success | Failure; +``` + +```typescript +{ ok: true, value: 'hello', remaining: ' world' } + │ │ │ + │ │ └── what is left to parse + │ └── the parsed value + └── always true for success +``` + +`remaining` is the important part: it is how input gets consumed and how parsers chain. A failure carries an optional message, which you can always supply later with `label`. + +```typescript +{ ok: false } // generic failure +{ ok: false, error: 'expected a' } // failure with a message +``` + +### Backtracking is free + +Note what a `Failure` does *not* carry: a position. There is nowhere to record how much input a failed parser got through, and combinators hand every alternative the same string they started with, so a branch that fails can never leave the cursor moved. `choice(a, b)` always offers `b` the full input, however far `a` got. + +If you are coming from Parsec, this is why there is no `try`/`attempt` here — backtracking is unconditional, so there is nothing to opt into. + +### Writing one by hand + +Most of the time you compose existing pieces, but nothing stops you from dropping down a level: + +```typescript +import { create, success, failure } from 'unitas'; + +const parser = create((input) => { + if (input.startsWith('hello')) { + return success('hello', input.slice(5)); + } + + return failure('expected "hello"'); +}); +``` + +### Grammars + +`grammar` hands every rule a proxy `p`, so rules can reference each other — including themselves — without forward declarations or `lazy` boilerplate. ```typescript import { grammar, run } from 'unitas'; -import { choice, map, quoted, value } from 'unitas/combinators'; +import { chainLeft1, choice, map, sequence } from 'unitas/combinators'; +import { char } from 'unitas/terminals'; +import { digits } from 'unitas/primitives'; + +type Math = { expr: number; term: number; value: number }; + +const g = grammar({ + expr: (p) => + chainLeft1( + p.term, + map(char('+'), () => (l: number, r: number) => l + r), + ), + term: (p) => + choice( + p.value, + map(sequence(char('('), p.expr, char(')')), ([, v]) => v), + ), + value: () => digits, +}); + +run(g.expr, '1+2+3'); // 6 +run(g.expr, '(1+2)'); // 3 +``` + +## More examples + +**JSON value** — a tagged union of scalars + +```typescript +import { grammar, run } from 'unitas'; +import { choice, quoted, value } from 'unitas/combinators'; import { string } from 'unitas/terminals'; import { bool, digits, letters } from 'unitas/primitives'; @@ -68,7 +246,7 @@ run(json.value, 'true'); // true run(json.value, 'null'); // null ``` -**Query string parser** — parsing URL query parameters +**Query string** — separated pairs folded into an object ```typescript import { grammar, run } from 'unitas'; @@ -96,11 +274,11 @@ const query = grammar({ run(query.params, 'foo=bar&baz=qux'); // { foo: 'bar', baz: 'qux' } ``` -**INI file section** — parsing section headers and key-value pairs +**INI section** — a header plus an entry, with the punctuation dropped ```typescript import { grammar, run } from 'unitas'; -import { map, outer, sequence, bracketed } from 'unitas/combinators'; +import { bracketed, map, outer, sequence } from 'unitas/combinators'; import { char, regex } from 'unitas/terminals'; import { letters, nl } from 'unitas/primitives'; import { pick } from 'unitas/utils'; @@ -121,113 +299,23 @@ const ini = grammar({ run(ini.section, '[database]\nhost=localhost'); // { name: 'database', entry: ['host', 'localhost'] } ``` -## Table of Contents - -<$toc> - -## Core Concepts - -### The Parser Type - -A `Parser` is a function that takes an input string and returns a `Result`. The generic `T` represents the type of value the parser produces. - -```typescript -type Parser = (input: string) => Result; -``` - -### The Result Type +## API index -Every parser returns a `Result` which is either: +One line per export. Every name links to its full description and runnable example in the reference pages under [`doc/api`](https://github.com/sovrin/unitas/tree/master/doc/api). -- **Success** — The parser matched and produced a value -- **Failure** — The parser did not match +<$index> -```typescript -type Success = { ok: true; value: T; remaining: string }; -type Failure = { ok: false; error?: string }; -type Result = Success | Failure; -``` - -The `remaining` string is crucial — it represents what input is left after the parser has done its work. This is how we "consume" input and chain parsers together. - -### Creating a Parser - -Use `create` to wrap a parsing function: - -```typescript -import { create, success, failure } from 'unitas'; - -const parser = create((input) => { - if (input.startsWith('hello')) { - return success('hello', input.slice(5)); - } - return failure('expected "hello"'); -}); -``` +## Contributing -### Understanding the Monadic Nature - -Parsers are monadic, which means they follow certain laws that make them composable: - -1. **Left identity**: `create(success(a, input))` behaves like `a` -2. **Right identity**: `parser` composed with `success` returns equivalent result -3. **Associativity**: Composition order doesn't affect final result - -The practical implication is that you can chain and combine parsers predictably. - -### Success Results - -When a parser successfully matches, it returns: - -```typescript -{ ok: true, value: 'hello', remaining: ' world' } - │ │ │ - │ │ └── What's left to parse - │ └── The parsed value - └── Always true for success -``` - -### Failure Results - -When a parser fails, it returns: - -```typescript -{ ok: false } // Generic failure -{ ok: false, error: 'expected a' } // Failure with message +```bash +npm test # run the suite +npm run build # compile to dist/ +npm run generate:index # regenerate barrel files after adding a source file +npm run generate:readme # regenerate README.md and doc/api/ from JSDoc ``` -The `error` field is optional — you can always add meaningful error messages later using `label`. - -## Core (`unitas`) - -**Core** provides the fundamental types and functions for building parsers. - -<$core> - -## Terminals (`unitas/terminals`) - -**Terminals** are the basic building blocks that match specific parts of the input. They don't combine other parsers — they directly inspect the input string. - -<$terminals> - -## Primitives (`unitas/primitives`) - -**Primitives** are pre-built parser instances ready to use. Unlike terminals which are factory functions (like `char('x')`), primitives are constants you can pass directly to combinators. - -<$primitives> - -## Combinators (`unitas/combinators`) - -**Combinators** are functions that take one or more parsers and return a new parser. They are the "glue" that lets you compose complex parsers from simple ones. - -<$combinators> - -## Utils (`unitas/utils`) - -**Utils** are utility functions for working with parser results, arrays, and function composition. - -<$utils> +Every export lives in its own file with a co-located test and a JSDoc `@example`. Those examples are the single source of truth: they are extracted into `test/examples.test.ts` and into the documentation, so they cannot drift. ## License -MIT +MIT © [Oleg Kamlowski](https://github.com/sovrin) diff --git a/doc/api.template.md b/doc/api.template.md new file mode 100644 index 0000000..19e1ba6 --- /dev/null +++ b/doc/api.template.md @@ -0,0 +1,19 @@ + + +# <$title> — `<$entry>` + +[← README](../../README.md) · <$nav> + +<$intro> + +```typescript +import { /* … */ } from '<$entry>'; +``` + +## Index + +<$count> exports: <$toc> + +## Reference + +<$body> diff --git a/doc/api/combinators.md b/doc/api/combinators.md new file mode 100644 index 0000000..0188ffc --- /dev/null +++ b/doc/api/combinators.md @@ -0,0 +1,555 @@ + + +# Combinators — `unitas/combinators` + +[← README](../../README.md) · [Core](./core.md) · [Terminals](./terminals.md) · [Primitives](./primitives.md) · **Combinators** · [Utils](./utils.md) + +Combinators are the glue. Every one of them takes one or more parsers and returns a new parser, which is what lets a grammar stay a set of small, independently testable pieces. + +```typescript +import {} from /* … */ 'unitas/combinators'; +``` + +## Index + +64 exports: [`bind`](#bind) · [`braced`](#braced) · [`bracketed`](#bracketed) · [`chainLeft`](#chainleft) · [`chainLeft1`](#chainleft1) · [`chainRight`](#chainright) · [`chainRight1`](#chainright1) · [`choice`](#choice) · [`concat`](#concat) · [`consume`](#consume) · [`endBy`](#endby) · [`endBy1`](#endby1) · [`exactly`](#exactly) · [`first`](#first) · [`flag`](#flag) · [`fold`](#fold) · [`fold1`](#fold1) · [`foldRight`](#foldright) · [`foldRight1`](#foldright1) · [`fuse`](#fuse) · [`guard`](#guard) · [`inner`](#inner) · [`interleaved`](#interleaved) · [`last`](#last) · [`left`](#left) · [`lexeme`](#lexeme) · [`many`](#many) · [`many1`](#many1) · [`manyAtLeast`](#manyatleast) · [`manyAtMost`](#manyatmost) · [`manyBetween`](#manybetween) · [`manyTill`](#manytill) · [`map`](#map) · [`node`](#node) · [`not`](#not) · [`nth`](#nth) · [`optional`](#optional) · [`optionalConsume`](#optionalconsume) · [`optionalSeparatedBy`](#optionalseparatedby) · [`outer`](#outer) · [`padded`](#padded) · [`parenthesized`](#parenthesized) · [`peek`](#peek) · [`postfix`](#postfix) · [`prefix`](#prefix) · [`pure`](#pure) · [`quoted`](#quoted) · [`recover`](#recover) · [`right`](#right) · [`separatedBy`](#separatedby) · [`separatedBy1`](#separatedby1) · [`separatedEndBy`](#separatedendby) · [`separatedEndBy1`](#separatedendby1) · [`separatedUntil`](#separateduntil) · [`sequence`](#sequence) · [`skip`](#skip) · [`skipMany`](#skipmany) · [`skipMany1`](#skipmany1) · [`surrounded`](#surrounded) · [`unless`](#unless) · [`until`](#until) · [`validate`](#validate) · [`value`](#value) · [`when`](#when) + +## Reference + +### `bind` + +Chain parsers where the second parser depends on the first result. + +```typescript +bind(digits, (n) => take(n))('3abc'); // { ok: true, value: 'abc', remaining: '' } +``` + +### `braced` + +Parse content surrounded by braces. + +```typescript +braced(string('hi'))('{hi}'); // { ok: true, value: 'hi', remaining: '' } +``` + +### `bracketed` + +Parse content surrounded by brackets. + +```typescript +bracketed(string('hi'))('[hi]'); // { ok: true, value: 'hi', remaining: '' } +``` + +### `chainLeft` + +Chain left-associative operations (right-to-left for same precedence). + +```typescript +chainLeft(digits, operation)('1+2+3'); // { ok: true, value: 6, remaining: '' } +chainLeft(digits, operation)('10-3+2'); // { ok: true, value: 9, remaining: '' } +``` + +### `chainLeft1` + +Chain left-associative operations (fails on empty input). + +```typescript +chainLeft1(digits, operation)('1+2+3'); // { ok: true, value: 6, remaining: '' } +chainLeft1(digits, operation)('8/2*3'); // { ok: true, value: 12, remaining: '' } +``` + +### `chainRight` + +Chain right-associative operations (right-to-left grouping). + +```typescript +chainRight(digits, operation)('2-1-1'); // { ok: true, value: 2, remaining: '' } +chainRight(digits, operation)('4/2/2'); // { ok: true, value: 4, remaining: '' } +``` + +### `chainRight1` + +Chain right-associative operations (fails on empty input). + +```typescript +chainRight1(digits, operation)('2-1-1'); // { ok: true, value: 2, remaining: '' } +chainRight1(digits, operation)('4/2/2'); // { ok: true, value: 4, remaining: '' } +``` + +### `choice` + +Try each parser in order, return first success. + +```typescript +choice(string('hello'), string('world'))('hello'); // { ok: true, value: 'hello', remaining: '' } +``` + +### `concat` + +Join string array parser result into a single string. + +```typescript +concat(many(letter))('abc123'); // { ok: true, value: 'abc', remaining: '123' } +concat(many(letter), '-')('abc123'); // { ok: true, value: 'a-b-c', remaining: '123' } +``` + +### `consume` + +Consume input but discard the result (return null). + +```typescript +consume(string('hello'))('hello world'); // { ok: true, value: null, remaining: ' world' } +``` + +### `endBy` + +Zero or more items separated and ending with terminator. + +```typescript +endBy(string('item'), char(';'))('item;item;item;'); // { ok: true, value: ['item', 'item', 'item'], remaining: '' } +``` + +### `endBy1` + +One or more items separated and ending with terminator. + +```typescript +endBy1(string('item'), char(';'))('item;item;item;'); // { ok: true, value: ['item', 'item', 'item'], remaining: '' } +``` + +### `exactly` + +Parse exactly n occurrences. + +```typescript +exactly(char('a'), 3)('aaa'); // { ok: true, value: ['a', 'a', 'a'], remaining: '' } +``` + +### `first` + +Extract the first element from a parser result array. + +```typescript +first(sequence(char('a'), digit))('a1bc'); // { ok: true, value: 'a', remaining: 'bc' } +``` + +### `flag` + +Return true if parser succeeds, false otherwise. Always succeeds without consuming input on failure. + +```typescript +flag(string('*'))('*abc'); // { ok: true, value: true, remaining: 'abc' } +flag(string('*'))('abc'); // { ok: true, value: false, remaining: 'abc' } +``` + +### `fold` + +Parse zero or more and fold into a single value. + +```typescript +fold(digit, [], (acc, d) => [...acc, d])('123'); // { ok: true, value: [1, 2, 3], remaining: '' } +``` + +### `fold1` + +Parse one or more and fold into a single value. + +```typescript +fold1(digit, 0, (acc, d) => acc + d)('123'); // { ok: true, value: 6, remaining: '' } +``` + +### `foldRight` + +Parse zero or more and fold right-to-left. + +```typescript +foldRight(digit, [], (acc, d) => [...acc, d])('123'); // { ok: true, value: [3, 2, 1], remaining: '' } +``` + +### `foldRight1` + +Parse one or more and fold right-to-left. + +```typescript +foldRight1(digit, [], (acc, d) => [...acc, d])('123'); // { ok: true, value: [3, 2, 1], remaining: '' } +``` + +### `fuse` + +Fuse multiple string parsers into a single one. The fused parser concatenates all string results. + +```typescript +fuse(char('a'), char('b'), char('c'))('abc'); // { ok: true, value: 'abc', remaining: '' } +fuse(string('hello'), char(' '), string('world'))('hello world'); // { ok: true, value: 'hello world', remaining: '' } +``` + +### `guard` + +Conditionally apply parser based on a condition. + +```typescript +guard(true, string('hello'))('hello'); // { ok: true, value: 'hello', remaining: '' } +guard(false, string('hello'))('hello'); // { ok: false } +``` + +### `inner` + +Extract inner value from surrounded content (like inner of braced). + +```typescript +inner(char('('), string('hi'), char(')'))('(hi)'); // { ok: true, value: 'hi', remaining: '' } +``` + +### `interleaved` + +Parse items with interleaved separators. + +```typescript +interleaved(char('a'), char(','))('a,a,a'); // { ok: true, value: ['a', ',', 'a', ',', 'a'], remaining: '' } +``` + +### `last` + +Extract the last element from a parser result array. + +```typescript +last(sequence(char('a'), char('b')))('ab'); // { ok: true, value: 'b', remaining: '' } +``` + +### `left` + +Keep only the left result from a sequence. + +```typescript +left(string('hello'), string('world'))('helloworld'); // { ok: true, value: 'hello', remaining: '' } +``` + +### `lexeme` + +Parser that consumes trailing whitespace. + +```typescript +lexeme(string('hello'))('hello world'); // { ok: true, value: 'hello', remaining: 'world' } +``` + +### `many` + +Zero or more occurrences (never fails). + +```typescript +many(char('a'))('aaa'); // { ok: true, value: ['a', 'a', 'a'], remaining: '' } +``` + +### `many1` + +One or more occurrences (fails if no matches). + +```typescript +many1(char('a'))('aaa'); // { ok: true, value: ['a', 'a', 'a'], remaining: '' } +``` + +### `manyAtLeast` + +Parse at least n occurrences. + +```typescript +manyAtLeast(char('a'), 2)('aaa'); // { ok: true, value: ['a', 'a', 'a'], remaining: '' } +``` + +### `manyAtMost` + +Parse at most n occurrences. + +```typescript +manyAtMost(char('a'), 2)('aaa'); // { ok: true, value: ['a', 'a'], remaining: 'a' } +``` + +### `manyBetween` + +Parse between min and max occurrences. + +```typescript +manyBetween(char('a'), 2, 3)('aaa'); // { ok: true, value: ['a', 'a', 'a'], remaining: '' } +``` + +### `manyTill` + +Parse zero or more until terminator matches. + +```typescript +manyTill(char('a'), char('b'))('aaab'); // { ok: true, value: ['a', 'a', 'a'], remaining: '' } +``` + +### `map` + +Transform the parsed value. + +```typescript +map(string('hello'), (v) => v.toUpperCase())('hello'); // { ok: true, value: 'HELLO', remaining: '' } +``` + +### `node` + +Create a node from parser fields. + +```typescript +node('binop', { left: digits, op: char('+'), right: digits })('1+2'); // { ok: true, value: { type: 'binop', left: 1, op: '+', right: 2 }, remaining: '' } +node('number', { value: digits })('123'); // { ok: true, value: { type: 'number', value: 123 }, remaining: '' } +``` + +### `not` + +Succeed if parser fails (without consuming input). + +```typescript +not(string('hello'))('world'); // { ok: true, value: null, remaining: 'world' } +``` + +### `nth` + +Extract the nth element from a parser result array. + +```typescript +nth(sequence(char('a'), char('b'), char('c')), 1)('abc'); // { ok: true, value: 'b', remaining: '' } +``` + +### `optional` + +Make parser optional (return null on failure, without consuming input). + +```typescript +optional(string('hello'))('hello'); // { ok: true, value: 'hello', remaining: '' } +optional(string('hello'))('world'); // { ok: true, value: null, remaining: 'world' } +``` + +### `optionalConsume` + +Optionally consume input (always succeeds, returns void). + +```typescript +optionalConsume(string('hello'))('hello world'); // { ok: true, value: undefined, remaining: ' world' } +optionalConsume(string('hello'))('world'); // { ok: true, value: undefined, remaining: 'world' } +``` + +### `optionalSeparatedBy` + +Zero or more items separated by a separator, with optional null values. + +```typescript +optionalSeparatedBy(digits, char(','))('1,2'); // { ok: true, value: [1, 2], remaining: '' } +optionalSeparatedBy(digits, char(','))(',1'); // { ok: true, value: [null, 1], remaining: '' } +optionalSeparatedBy(digits, char(','))('1,'); // { ok: true, value: [1], remaining: '' } +``` + +### `outer` + +Extract outer values from a sequence of 3 parsers (skip middle). + +```typescript +outer(char('('), string('hi'), char(')'))('(hi)'); // { ok: true, value: ['(', ')'], remaining: '' } +``` + +### `padded` + +Parse content with optional whitespace on both sides. + +```typescript +padded(string('hi'))(' hi '); // { ok: true, value: 'hi', remaining: '' } +``` + +### `parenthesized` + +Parse content surrounded by parentheses. + +```typescript +parenthesized(string('hi'))('(hi)'); // { ok: true, value: 'hi', remaining: '' } +``` + +### `peek` + +Parse without consuming input. + +```typescript +peek(string('hello'))('hello world'); // { ok: true, value: 'hello', remaining: 'hello world' } +``` + +### `postfix` + +Parse postfix operators (chains atom with operators that return functions). + +```typescript +postfix( + char('a'), + map(char('!'), () => (x) => x), +)('a!'); // { ok: true, value: 'a', remaining: '' } +``` + +### `prefix` + +Parse prefix operators (like - in -5). + +```typescript +prefix( + map(char('-'), () => (x) => -x), + digit, +)('-5'); // { ok: true, value: -5, remaining: '' } +``` + +### `pure` + +Always return a value without consuming input. + +```typescript +pure(42)('abc'); // { ok: true, value: 42, remaining: 'abc' } +pure('ok')(''); // { ok: true, value: 'ok', remaining: '' } +``` + +### `quoted` + +Parse content surrounded by single or double quotes. + +```typescript +quoted(string('hello'))('"hello"'); // { ok: true, value: 'hello', remaining: '' } +``` + +### `recover` + +Use fallback value when parser fails. + +```typescript +recover(string('hello'), 'default')('world'); // { ok: true, value: 'default', remaining: 'world' } +``` + +### `right` + +Keep only the right result from a sequence. + +```typescript +right(string('hello'), string('world'))('helloworld'); // { ok: true, value: 'world', remaining: '' } +``` + +### `separatedBy` + +Zero or more items separated by a separator. + +```typescript +separatedBy(char('a'), char(','))('a,a,a'); // { ok: true, value: ['a', 'a', 'a'], remaining: '' } +``` + +### `separatedBy1` + +One or more items separated by a separator. + +```typescript +separatedBy1(char('a'), char(','))('a,a,a'); // { ok: true, value: ['a', 'a', 'a'], remaining: '' } +``` + +### `separatedEndBy` + +Zero or more items separated by and ending with a terminator. + +```typescript +separatedEndBy(char('a'), char(';'))('a;a;a;'); // { ok: true, value: ['a', 'a', 'a'], remaining: '' } +``` + +### `separatedEndBy1` + +One or more items separated by and ending with a terminator. + +```typescript +separatedEndBy1(char('a'), char(';'))('a;a;a;'); // { ok: true, value: ['a', 'a', 'a'], remaining: '' } +``` + +### `separatedUntil` + +Parse items separated by separator until terminator matches. + +```typescript +separatedUntil(char('a'), char(','), char(';'))('a,a,a;'); // { ok: true, value: ['a', 'a', 'a'], remaining: '' } +``` + +### `sequence` + +Parse a sequence of parsers and return all results as an array. + +```typescript +sequence(char('a'), char('b'), char('c'))('abc'); // { ok: true, value: ['a', 'b', 'c'], remaining: '' } +``` + +### `skip` + +Skip a parser n times. + +```typescript +skip(char('a'), 2)('aabc'); // { ok: true, value: null, remaining: 'bc' } +``` + +### `skipMany` + +Skip zero or more occurrences (never fails, returns null). + +```typescript +skipMany(char('a'))('aaabc'); // { ok: true, value: null, remaining: 'bc' } +``` + +### `skipMany1` + +Skip one or more occurrences (fails if no matches). + +```typescript +skipMany1(char('a'))('aaabc'); // { ok: true, value: null, remaining: 'bc' } +``` + +### `surrounded` + +Parse content surrounded by delimiters. + +```typescript +surrounded(char('['), string('hi'), char(']'))('[hi]'); // { ok: true, value: 'hi', remaining: '' } +surrounded(char('a'), char('b'), char('c'))('abc'); // { ok: true, value: 'b', remaining: '' } +``` + +### `unless` + +Parse unless condition is true (inverse of guard). + +```typescript +unless(false, string('hello'))('hello'); // { ok: true, value: 'hello', remaining: '' } +unless(true, string('hello'))('hello'); // { ok: true, value: null, remaining: 'hello' } +``` + +### `until` + +Parse until terminator matches (fails if terminator never matches). + +```typescript +until(char('a'), char('b'))('baaa'); // { ok: true, value: [], remaining: 'baaa' } +until(char('a'), char('b'))('aaba'); // { ok: true, value: ['a', 'a'], remaining: 'ba' } +``` + +### `validate` + +Validate parsed value with a predicate. + +```typescript +validate(digit, (n) => n > 5)('7'); // { ok: true, value: 7, remaining: '' } +validate(digit, (n) => n > 5)('3'); // { ok: false } +``` + +### `value` + +Replace parsed value with a constant. + +```typescript +value(string('true'), true)('true'); // { ok: true, value: true, remaining: '' } +value(string('null'), null)('null'); // { ok: true, value: null, remaining: '' } +``` + +### `when` + +Branch on a boolean parser result. + +```typescript +when(flag(char('*')), pure('many'), pure('one'))('*rest'); // { ok: true, value: 'many', remaining: 'rest' } +when(flag(char('*')), pure('many'), pure('one'))('abc'); // { ok: true, value: 'one', remaining: 'abc' } +``` diff --git a/doc/api/core.md b/doc/api/core.md new file mode 100644 index 0000000..b23d036 --- /dev/null +++ b/doc/api/core.md @@ -0,0 +1,110 @@ + + +# Core — `unitas` + +[← README](../../README.md) · **Core** · [Terminals](./terminals.md) · [Primitives](./primitives.md) · [Combinators](./combinators.md) · [Utils](./utils.md) + +The core entry point holds everything a parser is built from: the `Parser` and `Result` types, the `success`/`failure` constructors, and the tools for running and wiring parsers together (`run`, `grammar`, `lazy`, `memoize`). + +```typescript +import {} from /* … */ 'unitas'; +``` + +## Index + +9 exports: [`failure`](#failure) · [`grammar`](#grammar) · [`label`](#label) · [`lazy`](#lazy) · [`match`](#match) · [`memoize`](#memoize) · [`parser`](#parser) · [`run`](#run) · [`success`](#success) + +## Reference + +### `failure` + +Creates a failed result with an optional error message. + +```typescript +failure('unexpected input'); // { ok: false, error: 'unexpected input' } +``` + +### `grammar` + +Creates a recursive grammar where rules can reference each other. + +```typescript +type Math = { + expr: number; + term: number; + value: number; +}; +const g = grammar({ + expr: (p) => + chainLeft1( + p.term, + map(char('+'), () => (l, r) => l + r), + ), + term: (p) => + choice( + p.value, + map(sequence(char('('), p.expr, char(')')), ([, v]) => v), + ), + value: () => digits, +}); +run(g.expr, '1+2'); // 3 +run(g.expr, '1+2+3'); // 6 +run(g.expr, '(1+2)'); // 3 +``` + +### `label` + +Labels a parser with a custom error message on failure. + +```typescript +label(char('x'), 'letter x')(''); // { ok: false, error: 'expected letter x' } +``` + +### `lazy` + +Defers parser creation, useful for recursive grammars. + +```typescript +lazy(() => char('a'))('abc'); // { ok: true, value: 'a', remaining: 'bc' } +``` + +### `match` + +Pattern matching on a Result to handle success and failure cases. + +```typescript +match(success('hello', ''), { success: (v) => v, failure: () => 'failed' }); // 'hello' +``` + +### `memoize` + +Memoizes a parser to cache results by input string. Useful for expensive parsers and recursive grammars to avoid exponential backtracking. + +```typescript +const memoDigits = memoize(digits); +memoDigits('123'); // { ok: true, value: 123, remaining: '' } +``` + +### `parser` + +Creates a parser from a parser function. + +```typescript +create((input) => success('parsed', input.slice(6)))('hello world'); // { ok: true, value: 'parsed', remaining: 'world' } +``` + +### `run` + +Runs a parser and returns the value, throws on failure or unconsumed input. + +```typescript +run(string('hello'), 'hello'); // 'hello' +``` + +### `success` + +Creates a successful result with a value and remaining input. + +```typescript +success('hello', ' world'); // { ok: true, value: 'hello', remaining: ' world' } +``` diff --git a/doc/api/primitives.md b/doc/api/primitives.md new file mode 100644 index 0000000..39d945f --- /dev/null +++ b/doc/api/primitives.md @@ -0,0 +1,292 @@ + + +# Primitives — `unitas/primitives` + +[← README](../../README.md) · [Core](./core.md) · [Terminals](./terminals.md) · **Primitives** · [Combinators](./combinators.md) · [Utils](./utils.md) + +Primitives are parser _instances_, not factories. Where a terminal needs an argument (`char("a")`), a primitive is already a parser and can be passed straight to a combinator (`many(digit)`). + +```typescript +import {} from /* … */ 'unitas/primitives'; +``` + +## Index + +33 exports: [`alphaNum`](#alphanum) · [`alphaNums`](#alphanums) · [`anyChar`](#anychar) · [`bool`](#bool) · [`crlf`](#crlf) · [`digit`](#digit) · [`digits`](#digits) · [`eof`](#eof) · [`eol`](#eol) · [`float`](#float) · [`hexDigit`](#hexdigit) · [`hexDigits`](#hexdigits) · [`identifier`](#identifier) · [`integer`](#integer) · [`letter`](#letter) · [`letters`](#letters) · [`line`](#line) · [`literal`](#literal) · [`lowercase`](#lowercase) · [`lowercases`](#lowercases) · [`nl`](#nl) · [`number`](#number) · [`octDigit`](#octdigit) · [`octDigits`](#octdigits) · [`position`](#position) · [`rest`](#rest) · [`space`](#space) · [`spaces`](#spaces) · [`tab`](#tab) · [`uppercase`](#uppercase) · [`uppercases`](#uppercases) · [`whitespace`](#whitespace) · [`whitespaces`](#whitespaces) + +## Reference + +### `alphaNum` + +Parse a single alphanumeric character. + +```typescript +alphaNum('a1'); // { ok: true, value: 'a', remaining: '1' } +alphaNum('1a'); // { ok: true, value: '1', remaining: 'a' } +``` + +### `alphaNums` + +Parse one or more alphanumeric characters. + +```typescript +alphaNums('abc123'); // { ok: true, value: 'abc123', remaining: '' } +``` + +### `anyChar` + +Parse any single character. + +```typescript +anyChar('abc'); // { ok: true, value: 'a', remaining: 'bc' } +``` + +### `bool` + +Parse a boolean literal. + +```typescript +bool('true'); // { ok: true, value: true, remaining: '' } +bool('false'); // { ok: true, value: false, remaining: '' } +bool('trueABC'); // { ok: true, value: true, remaining: 'ABC' } +``` + +### `crlf` + +Parse CRLF line ending. + +```typescript +crlf('\r\nabc'); // { ok: true, value: '\r\n', remaining: 'abc' } +``` + +### `digit` + +Parse a single digit and return as number. + +```typescript +digit('5abc'); // { ok: true, value: 5, remaining: 'abc' } +``` + +### `digits` + +Parse one or more digits and return as number. + +```typescript +digits('123abc'); // { ok: true, value: 123, remaining: 'abc' } +``` + +### `eof` + +Parse end of file (succeeds only on empty input). + +```typescript +eof(''); // { ok: true, value: null, remaining: '' } +``` + +### `eol` + +Parse end of line (\\n, \\r\\n, or EOF). + +```typescript +eol('\nabc'); // { ok: true, value: '\n', remaining: 'abc' } +``` + +### `float` + +Parse a floating point number. + +```typescript +float('1.23'); // { ok: true, value: 1.23, remaining: '' } +float('-2.5'); // { ok: true, value: -2.5, remaining: '' } +float('1.23abc'); // { ok: true, value: 1.23, remaining: 'abc' } +``` + +### `hexDigit` + +Parse a single hexadecimal digit. + +```typescript +hexDigit('fF9'); // { ok: true, value: 'f', remaining: 'F9' } +``` + +### `hexDigits` + +Parse one or more hexadecimal digits. + +```typescript +hexDigits('deadbeef'); // { ok: true, value: 'deadbeef', remaining: '' } +``` + +### `identifier` + +Parse an identifier — starts with letter or underscore, no leading digit, no hyphen. + +```typescript +identifier('variable_name'); // { ok: true, value: 'variable_name', remaining: '' } +``` + +### `integer` + +Parse a signed integer. + +```typescript +integer('42'); // { ok: true, value: 42, remaining: '' } +integer('-7'); // { ok: true, value: -7, remaining: '' } +integer('123abc'); // { ok: true, value: 123, remaining: 'abc' } +``` + +### `letter` + +Parse a single letter. + +```typescript +letter('abc'); // { ok: true, value: 'a', remaining: 'bc' } +``` + +### `letters` + +Parse one or more letters. + +```typescript +letters('abc123'); // { ok: true, value: 'abc', remaining: '123' } +``` + +### `line` + +Parse until end of line. + +```typescript +line('hello\nworld'); // { ok: true, value: 'hello', remaining: '\nworld' } +``` + +### `literal` + +Parse a word-like value including hyphens. + +```typescript +literal('foo-bar'); // { ok: true, value: 'foo-bar', remaining: '' } +literal('123abc'); // { ok: true, value: '123abc', remaining: '' } +``` + +### `lowercase` + +Parse a single lowercase letter. + +```typescript +lowercase('abc'); // { ok: true, value: 'a', remaining: 'bc' } +``` + +### `lowercases` + +Parses one or more lowercase letters. + +```typescript +lowercases('abcDEF'); // { ok: true, value: 'abc', remaining: 'DEF' } +``` + +### `nl` + +Parse a newline character. + +```typescript +nl('\ntext'); // { ok: true, value: '\n', remaining: 'text' } +``` + +### `number` + +Parse an integer or float. + +```typescript +number('42'); // { ok: true, value: 42, remaining: '' } +number('3.14'); // { ok: true, value: 3.14, remaining: '' } +number('-7'); // { ok: true, value: -7, remaining: '' } +number('-2.5'); // { ok: true, value: -2.5, remaining: '' } +``` + +### `octDigit` + +Parse a single octal digit. + +```typescript +octDigit('7abc'); // { ok: true, value: '7', remaining: 'abc' } +``` + +### `octDigits` + +Parse one or more octal digits. + +```typescript +octDigits('0777abc'); // { ok: true, value: '0777', remaining: 'abc' } +``` + +### `position` + +Get current position (remaining input length). + +```typescript +position('abc'); // { ok: true, value: 3, remaining: 'abc' } +``` + +### `rest` + +Parse the rest of the input. + +```typescript +rest('hello'); // { ok: true, value: 'hello', remaining: '' } +``` + +### `space` + +Parse a single space character. + +```typescript +space(' abc'); // { ok: true, value: ' ', remaining: 'abc' } +``` + +### `spaces` + +Parse one or more space characters. + +```typescript +spaces(' abc'); // { ok: true, value: ' ', remaining: 'abc' } +``` + +### `tab` + +Parse tab character. + +```typescript +tab('\ttext'); // { ok: true, value: '\t', remaining: 'text' } +``` + +### `uppercase` + +Parses a single uppercase letter. + +```typescript +uppercase('ABC'); // { ok: true, value: 'A', remaining: 'BC' } +``` + +### `uppercases` + +Parses one or more uppercase letters. + +```typescript +uppercases('ABCdef'); // { ok: true, value: 'ABC', remaining: 'def' } +``` + +### `whitespace` + +Parses a single whitespace character. + +```typescript +whitespace(' abc'); // { ok: true, value: ' ', remaining: 'abc' } +``` + +### `whitespaces` + +Parses one or more whitespace characters. + +```typescript +whitespaces(' abc'); // { ok: true, value: ' ', remaining: 'abc' } +``` diff --git a/doc/api/terminals.md b/doc/api/terminals.md new file mode 100644 index 0000000..424d9c8 --- /dev/null +++ b/doc/api/terminals.md @@ -0,0 +1,117 @@ + + +# Terminals — `unitas/terminals` + +[← README](../../README.md) · [Core](./core.md) · **Terminals** · [Primitives](./primitives.md) · [Combinators](./combinators.md) · [Utils](./utils.md) + +Terminals are the leaves of a grammar. They do not take other parsers — they inspect the input string themselves. Each one is a factory: call it with what you want to match and it hands back a `Parser`. + +```typescript +import {} from /* … */ 'unitas/terminals'; +``` + +## Index + +12 exports: [`char`](#char) · [`charOf`](#charof) · [`noneOf`](#noneof) · [`oneOf`](#oneof) · [`regex`](#regex) · [`satisfy`](#satisfy) · [`string`](#string) · [`stringOf`](#stringof) · [`take`](#take) · [`takeWhile`](#takewhile) · [`token`](#token) · [`word`](#word) + +## Reference + +### `char` + +Parse a specific character. + +```typescript +char('A')('ABC'); // { ok: true, value: 'A', remaining: 'BC' } +``` + +### `charOf` + +Parse any character from a set. + +```typescript +charOf(['a', 'b', 'c'])('abc'); // { ok: true, value: 'a', remaining: 'bc' } +``` + +### `noneOf` + +Parse any character not in the set. + +```typescript +noneOf(['a', 'b', 'c'])('xyz'); // { ok: true, value: 'x', remaining: 'yz' } +``` + +### `oneOf` + +Parse one string from a set of strings (longest match wins). + +```typescript +oneOf(['hello', 'hell', 'help'])('helpful'); // { ok: true, value: 'help', remaining: 'ful' } +``` + +### `regex` + +Parse with a regular expression. + +```typescript +regex(/^\w+/)('hello world'); // { ok: true, value: 'hello', remaining: ' world' } +``` + +### `satisfy` + +Parse a character satisfying a predicate. + +```typescript +satisfy((c) => c === 'a')('abc'); // { ok: true, value: 'a', remaining: 'bc' } +``` + +### `string` + +Parse a specific string. + +```typescript +string('hello')('hello world'); // { ok: true, value: 'hello', remaining: ' world' } +``` + +### `stringOf` + +Parse first character that exists in string (like charOf but for a string). + +```typescript +stringOf('abc')('abcdef'); // { ok: true, value: 'a', remaining: 'bcdef' } +``` + +### `take` + +Take n characters. + +```typescript +take(3)('abcdef'); // { ok: true, value: 'abc', remaining: 'def' } +``` + +### `takeWhile` + +Takes characters while the predicate returns true. + +```typescript +takeWhile((c) => c !== 'x')('abcx'); // { ok: true, value: 'abc', remaining: 'x' } +``` + +### `token` + +Parses a string as a token, skipping trailing whitespace. Unlike word, it does not enforce word boundaries - useful for parsing strings that might be followed by any character. + +```typescript +token('let')('let x'); // { ok: true, value: 'let', remaining: 'x' } +token('let')('let1'); // { ok: true, value: 'let', remaining: '1' } +token('let')('let x'); // { ok: true, value: 'let', remaining: 'x' } +``` + +### `word` + +Parses a specific word and ensures it is not followed by word characters. Use this when parsing keywords that should not be part of a longer identifier. + +```typescript +word('let')('let x'); // { ok: true, value: 'let', remaining: 'x' } +word('let')('let1'); // { ok: false } +word('if')('if (x)'); // { ok: true, value: 'if', remaining: '(x)' } +``` diff --git a/doc/api/utils.md b/doc/api/utils.md new file mode 100644 index 0000000..11e2e9d --- /dev/null +++ b/doc/api/utils.md @@ -0,0 +1,85 @@ + + +# Utils — `unitas/utils` + +[← README](../../README.md) · [Core](./core.md) · [Terminals](./terminals.md) · [Primitives](./primitives.md) · [Combinators](./combinators.md) · **Utils** + +Utils are not parsers. They are small curried helpers meant to be dropped into a `map` callback so reshaping a result stays a one-liner instead of an arrow function. + +```typescript +import {} from /* … */ 'unitas/utils'; +``` + +## Index + +8 exports: [`filter`](#filter) · [`flatten`](#flatten) · [`join`](#join) · [`pick`](#pick) · [`pipe`](#pipe) · [`pop`](#pop) · [`shift`](#shift) · [`spread`](#spread) + +## Reference + +### `filter` + +Exclude values from array. + +```typescript +filter([1, 2, 3])([1, 2, 3, 4, 5]); // [4, 5] +filter([1, 2], true)([1, false, 3]); // [3] +``` + +### `flatten` + +Flatten nested arrays. + +```typescript +flatten()([1, [2, [3]]]); // [1, 2, [3]] +flatten(2)([1, [2, [3]]]); // [1, 2, 3] +``` + +### `join` + +Join array elements into a string. + +```typescript +join()([1, 2, 3]); // '123' +join('-')([1, 2, 3]); // '1-2-3' +``` + +### `pick` + +Pick elements from an array by index. + +```typescript +pick(0, 2)(['a', 'b', 'c']); // ['a', 'c'] +pick(2, 4)(['a', 'b', 'c', 'd', 'e']); // ['c', 'e'] +``` + +### `pipe` + +Pipe parser functions together. + +```typescript +pipe(lexeme)(letters)('xyz abc'); // { ok: true, value: 'xyz', remaining: 'abc' } +``` + +### `pop` + +Get the last element of an array. + +```typescript +pop()([1, 2, 3]); // 3 +``` + +### `shift` + +Get the first element of an array. + +```typescript +shift()([1, 2, 3]); // 1 +``` + +### `spread` + +Collect spread arguments into an array. + +```typescript +spread()(1, 2, 3); // [1, 2, 3] +``` diff --git a/package.json b/package.json index 64f5875..38f4fc3 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,21 @@ { "name": "unitas", "version": "0.2.5", + "description": "TypeScript-first parser combinator library — compose small typed parsers into whole grammars. Zero dependencies, ESM-only, tree-shakeable.", "keywords": [ + "parser", + "parser-combinator", "combinator", + "parsing", + "grammar", + "peg", + "recursive-descent", + "lexer", + "tokenizer", "functional", - "parser" + "typescript", + "esm", + "zero-dependency" ], "license": "MIT", "author": { @@ -15,6 +26,10 @@ "type": "git", "url": "https://github.com/sovrin/unitas.git" }, + "homepage": "https://github.com/sovrin/unitas#readme", + "bugs": { + "url": "https://github.com/sovrin/unitas/issues" + }, "files": [ "dist", "README.md" @@ -68,7 +83,7 @@ "test:extract": "tsx bin/extract-examples.mjs && oxfmt ./test/examples.test.ts", "test:coverage": "vitest run --coverage", "generate:index": "tsx bin/generate-index.mjs", - "generate:readme": "tsx bin/generate-readme.mjs && oxfmt ./README.md", + "generate:readme": "tsx bin/generate-readme.mjs && oxfmt ./README.md ./doc/api", "lint": "oxlint && node bin/check-test-imports.mjs", "lint:fix": "oxlint --fix", "format": "oxfmt", diff --git a/src/combinators/attempt.test.ts b/src/combinators/attempt.test.ts deleted file mode 100644 index 27a34f5..0000000 --- a/src/combinators/attempt.test.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { describe, it } from 'vitest'; - -import { - assertFailure, - assertSuccess, - createTestParser, -} from '../../test/utils'; -import { attempt } from './attempt'; - -describe('attempt', () => { - it('should run parser and return result on success', () => { - const parser1 = createTestParser('A'); - const parser = attempt(parser1); - const result = parser('ABC'); - - assertSuccess(result, 'A', 'BC'); - }); - - it('should backtrack on failure', () => { - const parser1 = createTestParser('A'); - const parser = attempt(parser1); - const result = parser('BAC'); - - assertFailure(result); - }); -}); diff --git a/src/combinators/attempt.ts b/src/combinators/attempt.ts deleted file mode 100644 index 676697b..0000000 --- a/src/combinators/attempt.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { Parser } from '../core/parser'; - -import { forward } from '../core/forward'; -import { create } from '../core/parser'; - -/** - * Attempt wraps a parser to handle backtracking on failure. - * - * @example - * attempt(string('hello'))('hello world') // { ok: true, value: 'hello', remaining: ' world' } - */ -export const attempt = (parser: Parser) => { - return create((input) => { - const result = parser(input); - - return forward(result); - }); -}; diff --git a/src/combinators/chainLeft.ts b/src/combinators/chainLeft.ts index 47ffd2c..2e0f747 100644 --- a/src/combinators/chainLeft.ts +++ b/src/combinators/chainLeft.ts @@ -1,7 +1,6 @@ import type { Parser } from '../core/parser'; import { failure } from '../core/failure'; -import { forward } from '../core/forward'; import { create } from '../core/parser'; import { chainLeft1 } from './chainLeft1'; import { optional } from './optional'; @@ -23,6 +22,6 @@ export const chainLeft = ( return failure(); } - return forward(result); + return result; }); }; diff --git a/src/combinators/chainRight.ts b/src/combinators/chainRight.ts index 08efcea..46c4d64 100644 --- a/src/combinators/chainRight.ts +++ b/src/combinators/chainRight.ts @@ -1,7 +1,6 @@ import type { Parser } from '../core/parser'; import { failure } from '../core/failure'; -import { forward } from '../core/forward'; import { create } from '../core/parser'; import { chainRight1 } from './chainRight1'; import { optional } from './optional'; @@ -23,6 +22,6 @@ export const chainRight = ( return failure(); } - return forward(result); + return result; }); }; diff --git a/src/combinators/choice.ts b/src/combinators/choice.ts index 7941c4a..6a3fa0c 100644 --- a/src/combinators/choice.ts +++ b/src/combinators/choice.ts @@ -1,7 +1,6 @@ import type { Parser } from '../core/parser'; import { failure } from '../core/failure'; -import { forward } from '../core/forward'; import { create } from '../core/parser'; type ParserOutput

= P extends Parser ? T : never; @@ -19,7 +18,7 @@ export const choice = []>( for (const parser of parsers) { const result = parser(input); if (result.ok) { - return forward(result); + return result; } } diff --git a/src/combinators/guard.ts b/src/combinators/guard.ts index ea37aab..8c70fe0 100644 --- a/src/combinators/guard.ts +++ b/src/combinators/guard.ts @@ -1,7 +1,6 @@ import type { Parser } from '../core/parser'; import { failure } from '../core/failure'; -import { forward } from '../core/forward'; import { create } from '../core/parser'; /** @@ -19,6 +18,6 @@ export const guard = (condition: boolean, parser: Parser) => { const result = parser(input); - return forward(result); + return result; }); }; diff --git a/src/combinators/index.ts b/src/combinators/index.ts index 9f3dbe6..bcb2d66 100644 --- a/src/combinators/index.ts +++ b/src/combinators/index.ts @@ -1,4 +1,3 @@ -export * from './attempt'; export * from './bind'; export * from './braced'; export * from './bracketed'; diff --git a/src/combinators/optional.ts b/src/combinators/optional.ts index 88f6e29..cc8dab6 100644 --- a/src/combinators/optional.ts +++ b/src/combinators/optional.ts @@ -1,6 +1,5 @@ import type { Parser } from '../core/parser'; -import { forward } from '../core/forward'; import { create } from '../core/parser'; import { success } from '../core/success'; @@ -15,6 +14,6 @@ export const optional = (parser: Parser) => { return create((input) => { const result = parser(input); - return result.ok ? forward(result) : success(null, input); + return result.ok ? result : success(null, input); }); }; diff --git a/src/combinators/recover.ts b/src/combinators/recover.ts index 17a559d..ed3829c 100644 --- a/src/combinators/recover.ts +++ b/src/combinators/recover.ts @@ -1,6 +1,5 @@ import type { Parser } from '../core/parser'; -import { forward } from '../core/forward'; import { create } from '../core/parser'; import { success } from '../core/success'; @@ -14,6 +13,6 @@ export const recover = (parser: Parser, fallback: T) => { return create((input) => { const result = parser(input); - return result.ok ? forward(result) : success(fallback, input); + return result.ok ? result : success(fallback, input); }); }; diff --git a/src/core/forward.test.ts b/src/core/forward.test.ts deleted file mode 100644 index abd104c..0000000 --- a/src/core/forward.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { assertFailure, assertSuccess } from '../../test/utils'; -import { failure } from '../core/failure'; -import { success } from '../core/success'; -import { forward } from './forward'; - -describe('forward', () => { - it('should return success result unchanged', () => { - const result = success('test', 'rest'); - const forwarded = forward(result); - - assertSuccess(forwarded, 'test', 'rest'); - expect(forwarded).toBe(result); - }); - - it('should return failure result unchanged', () => { - const result = failure('test error'); - const forwarded = forward(result); - - assertFailure(result, 'test error'); - expect(forwarded).toBe(result); - }); - - it('should return failure without error unchanged', () => { - const result = failure(); - const forwarded = forward(result); - - assertFailure(result); - expect(forwarded).toBe(result); - }); -}); diff --git a/src/core/forward.ts b/src/core/forward.ts deleted file mode 100644 index c5e826a..0000000 --- a/src/core/forward.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { type Result } from './result'; - -export const forward = (result: Result): Result => result; diff --git a/src/core/index.ts b/src/core/index.ts index 8af3a84..e93843e 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -1,5 +1,4 @@ export * from './failure'; -export * from './forward'; export * from './grammar'; export * from './label'; export * from './lazy'; diff --git a/src/core/label.ts b/src/core/label.ts index f487711..fb5f7f4 100644 --- a/src/core/label.ts +++ b/src/core/label.ts @@ -1,7 +1,6 @@ import type { Parser } from '../core/parser'; import { failure } from './failure'; -import { forward } from './forward'; import { create } from './parser'; /** @@ -17,6 +16,6 @@ export const label = (parser: Parser, expected: string): Parser => { return failure(`expected ${expected}`); } - return forward(result); + return result; }); }; diff --git a/test/examples.test.ts b/test/examples.test.ts index 15df021..f50acfa 100644 --- a/test/examples.test.ts +++ b/test/examples.test.ts @@ -20,15 +20,6 @@ Object.assign( describe('examples from source', () => { describe('combinators', () => { - it('attempt: Attempt wraps a parser to handle backtracking on failure.', () => { - const result0 = attempt(string('hello'))('hello world'); - expect(result0).toEqual({ - ok: true, - value: 'hello', - remaining: ' world', - }); - }); - it('bind: Chain parsers where the second parser depends on the first result.', () => { const result0 = bind(digits, (n) => take(n))('3abc'); expect(result0).toEqual({ ok: true, value: 'abc', remaining: '' });