Skip to content

refactor(docs/output): unify options-rendering paths via shared (rows × columns) intermediate #399

@toiroakr

Description

@toiroakr

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions