Files
skills/docs/media/audio/audio-cli/src/pipeline/parser.md
2026-05-29 08:20:56 +01:00

70 lines
2.8 KiB
Markdown

# `pipeline/parser` — `parsePipeline` and `validatePipeline`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
This module turns a CLI user's free-form pipeline expression into a typed
`[opName, params]` chain that the rest of the CLI feeds into
`AudioDocument.run`. It supports both the **pipe-form** string —
`"trim 30s | normalize -16lufs | fade 0.5s 1s"` — and the **positional
bare-op** token array — `["trim", "30s", "normalize", "-16lufs"]` — and
collapses both surface forms onto the same internal representation so there is
exactly one downstream code path.
It also exposes `validatePipeline(chain, knownOps)` which the CLI calls before
opening any file, so an unknown op name throws `AUDIO_CLI.UNKNOWN_OP` fast and
no I/O is wasted.
## Features
- `tokenise` respects double-quoted strings (`"a b"`) and backslash escapes
(`a\ b`) so a single op can carry a string parameter with spaces.
- Unit suffix coercion: `30s → start=30`, `500ms → start=0.5`,
`-16lufs → lufs=-16`, `6db → db=6`, `44100hz → rate=44100`,
`48khz → rate=48000`.
- `key=value` arguments (`foo=bar`) become named params; numeric literals
without a unit become positional `start`/`duration`.
- `validatePipeline` lists the registry up-front so the diagnostic names the
offending op alongside the sorted set of known ops.
## Usage: Basic
```ts
import { parsePipeline } from "@suaveplan/audio-cli/pipeline/parser";
const chain = parsePipeline("trim 30s | normalize -16lufs");
// → [["trim", { start: 30 }], ["normalize", { lufs: -16 }]]
```
## Usage: Advanced
```ts
import { parsePipeline, validatePipeline } from "@suaveplan/audio-cli/pipeline/parser";
import { OpRegistry } from "@suaveplan/audio-core";
const registry = OpRegistry.createWithDefaults();
const known = new Set(registry.list().map((d) => d.name));
const chain = parsePipeline(["trim", "30s", "normalize", "-16lufs", "fade", "0.5s", "1s"]);
validatePipeline(chain, known); // throws AUDIO_CLI.UNKNOWN_OP if a name is unknown
```
## API Reference
- `ParsedPipeline` — `ReadonlyArray<readonly [string, Record<string, unknown>]>`.
- `parsePipeline(spec: string | readonly string[])` — produce the chain.
- `validatePipeline(chain, knownOps: ReadonlySet<string>)` — throws
`AudioCliError(AUDIO_CLI.UNKNOWN_OP)` on the first unknown op name.
## Implementation Notes
The hand-written tokenizer is deliberately small: a single forward pass with
two flags (`inQuote`, `escaped`) and a per-stage accumulator. The pipe-form
splits on `|`; the bare-op form treats every identifier-shaped token (matching
`/^[a-zA-Z][a-zA-Z0-9_-]*$/`) as the start of a new stage so positional
arguments belonging to the previous op are correctly grouped. Unit coercion
runs as a small regex per argument so `trim 30s` always lands as
`{ start: 30 }` regardless of which surface form it arrived through.