70 lines
2.8 KiB
Markdown
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.
|