Background
The option-rendering logic is currently spread across four+ similar functions that each iterate over ResolvedFieldMeta and emit a slightly different output format:
| Location |
Output |
Input shape |
Notes |
src/docs/default-renderers.ts:renderOptionsTable |
Markdown table |
CommandInfo |
env column conditional |
src/docs/default-renderers.ts:renderOptionsTableFromArray |
Markdown table |
ResolvedFieldMeta[] |
same body, different input wrapper |
src/docs/default-renderers.ts:renderOptionsList |
Markdown list |
CommandInfo |
|
src/docs/default-renderers.ts:renderOptionsListFromArray |
Markdown list |
ResolvedFieldMeta[] |
same body, different input wrapper |
src/docs/render-args.ts:renderFilteredTable |
Markdown table |
ResolvedFieldMeta[] |
adds per-column filtering, branched separately because the table renderers above don't support it |
src/output/help-generator.ts:formatFlags / renderOptions |
Terminal (ANSI) |
union/discriminated variants |
shares the boolean inline-negation pattern |
Adding a feature that touches the per-option visual representation today means editing roughly 4-5 places. The recent custom-negation work (#393) made this concrete: the same negation inline / separate-row / (↔ --positive) marker logic was duplicated across renderOptionsTable, renderOptionsList, renderOptionsTableFromArray, renderOptionsListFromArray, and renderFilteredTable, plus the analogous treatment in help-generator.
Proposal
Introduce a normalized intermediate representation that both the docs renderers and (optionally) the help generator consume:
```ts
type OptionRow = {
key: string; // "--cliName", "--cliName / --negation", etc.
cells: {
option: string;
alias: string;
description: string;
required: string;
default: string;
env: string;
};
extraRows?: OptionRow[]; // e.g. the separate negation row when negationDescription is set
};
function toOptionRows(options: ResolvedFieldMeta[], opts?: { columns?: ColumnId[] }): OptionRow[];
```
Then the actual emitters become trivial:
- `emitMarkdownTable(rows, columns)` — pure formatting
- `emitMarkdownList(rows)` — pure formatting
- `emitHelpLines(rows, ansiStyles)` — pure formatting
This way:
- The "decide what to display for this field" logic (negation handling, alias join order, placeholder resolution, boolean vs. value form, escape rules) lives once.
- Column filtering folds into the same path naturally — `renderFilteredTable` collapses into `emitMarkdownTable(toOptionRows(options, { columns }), columns)`.
- `From` / non-`From` variants reduce to thin adapters that just extract the array from `CommandInfo`.
Scope / non-goals
- Pure refactor — no change to rendered output. Golden tests in `playground/*/README.md` and snapshot tests in `src/docs/golden-test.test.ts` are the safety net.
- Help-generator integration is optional in a first pass; the docs renderers alone already remove most of the duplication.
- Out of scope: redesigning the field metadata itself.
Acceptance
- One canonical "render an option row" code path used by both Markdown table and list renderers.
- `render-args.ts:renderFilteredTable` removed / collapsed.
- All existing tests (including playground goldens) pass without regenerating output.
Context
Surfaced during review of #393 — the inline `/` negation join, the `(↔ --positive)` marker, and the separate-row `negationDescription` form each had to be added in 4-5 places. Future per-option features (e.g. value validators, deprecation markers) would hit the same fan-out.
Background
The option-rendering logic is currently spread across four+ similar functions that each iterate over
ResolvedFieldMetaand emit a slightly different output format:src/docs/default-renderers.ts:renderOptionsTableCommandInfosrc/docs/default-renderers.ts:renderOptionsTableFromArrayResolvedFieldMeta[]src/docs/default-renderers.ts:renderOptionsListCommandInfosrc/docs/default-renderers.ts:renderOptionsListFromArrayResolvedFieldMeta[]src/docs/render-args.ts:renderFilteredTableResolvedFieldMeta[]src/output/help-generator.ts:formatFlags/renderOptionsAdding a feature that touches the per-option visual representation today means editing roughly 4-5 places. The recent custom-negation work (#393) made this concrete: the same
negationinline / separate-row /(↔ --positive)marker logic was duplicated acrossrenderOptionsTable,renderOptionsList,renderOptionsTableFromArray,renderOptionsListFromArray, andrenderFilteredTable, plus the analogous treatment inhelp-generator.Proposal
Introduce a normalized intermediate representation that both the docs renderers and (optionally) the help generator consume:
```ts
type OptionRow = {
key: string; // "--cliName", "--cliName / --negation", etc.
cells: {
option: string;
alias: string;
description: string;
required: string;
default: string;
env: string;
};
extraRows?: OptionRow[]; // e.g. the separate negation row when negationDescription is set
};
function toOptionRows(options: ResolvedFieldMeta[], opts?: { columns?: ColumnId[] }): OptionRow[];
```
Then the actual emitters become trivial:
This way:
Scope / non-goals
Acceptance
Context
Surfaced during review of #393 — the inline `/` negation join, the `(↔ --positive)` marker, and the separate-row `negationDescription` form each had to be added in 4-5 places. Future per-option features (e.g. value validators, deprecation markers) would hit the same fan-out.