Skip to content

feat: add an outline command over document-outline.js - #242

Merged
Mearman merged 9 commits into
mainfrom
feat/outline-command
Aug 19, 2026
Merged

feat: add an outline command over document-outline.js#242
Mearman merged 9 commits into
mainfrom
feat/outline-command

Conversation

@Mearman

@Mearman Mearman commented Aug 19, 2026

Copy link
Copy Markdown
Member

Adds an outline command that prints a document's table-of-contents projection — headings, list items, and the synthetic slide/sheet/page groups — as indented text, or as JSON with --json.

This makes document-cli the first real consumer of document-outline.js, which just cut its 1.0.0 (see ExaDev/document-outline.js#2 for the phase-1 work that produced it).

$ document-cli outline notes.md
Introduction
  Intro paragraph.
  Details
    First item
    Second item

Indentation comes from tree depth, deliberately not from an OutlineNode's own level. That field carries the source's level signal — a heading's headingLevel, a list item's list.level, 1 for the synthetic groups — and those scales legitimately coexist in one tree, so a level-0 list item can sit inside a level-1 slide group. Depth is unambiguous and recoverable from the nesting itself.

To get a DocumentPackage to project, the command runs the cheapest conversion that still populates one and throws the output bytes away: a PDF-bypassing bridge to a sibling format, so no layout engine runs. pdf and odf take the one conversion each of them actually has. The internal conversion's diagnostics still reach stderr — a PDF reconstruction's parse warnings show up exactly as they would on pdf-to-docx. An absent package fails loudly rather than printing nothing: the port declares the field optional, but the local converter populates one for every conversion, so its absence is a broken contract, not an empty document.

--json emits document-outline.js's tree verbatim rather than a CLI-private shape, so a consumer can walk it with that package's own isOutlineNode/isOutlineChild guards.

document-outline.js goes into minimumReleaseAgeExclude as a bare package name, matching the seven siblings already listed — same org, same semantic-release + npm OIDC trusted publishing path, so a version-agnostic exemption avoids re-pinning the file on every release. minimumReleaseAge stays at 60; that is the only one of the two settings pnpm install --frozen-lockfile honours, so CI is unaffected either way.

Tested against a markdown source (heading and list nesting), an ods source (per-sheet groups), a pdf source (the reconstruction path, the one format that cannot be bridged), and a --json run pinning the tree shape.

Typecheck, lint, unit tests, workers tests, and build are all green locally.

Generated by Claude Code

…ge gate

The outline command below reads document-outline.js's buildOutline over a
ConversionResult.package, so the package becomes a real runtime dependency
rather than a vendored copy.

The minimumReleaseAgeExclude entry is a bare package name, matching every
other sibling already listed: these are the org's own packages, published
only through their own semantic-release CI with npm OIDC trusted publishing,
so a version-agnostic exemption avoids re-pinning the file on every release.
minimumReleaseAge itself is unchanged at 60, since that is the only setting
`pnpm install --frozen-lockfile` honours on the CI path.
…projection

Prints headings, list items, and the synthetic slide/sheet/page groups as
indented text, two spaces per nesting depth, with leaves rendering their own
text (a paragraph's runs, a table's cell text, an image's alt text) or their
kind in brackets when they carry none.

Indentation comes from tree depth, never from an OutlineNode's own `level`:
that field is the source's level signal, and the heading and list scales
legitimately coexist within one tree, so a level-0 list item can sit inside a
level-1 slide group.

The command reaches a DocumentPackage by running the cheapest conversion that
still populates one -- a PDF-bypassing bridge to a sibling format, so no
layout engine runs -- and discards the resulting bytes. pdf and odf take the
one conversion each actually has. An absent package fails loudly rather than
printing nothing, since the local converter populates one for every
conversion and its absence would mean a broken port contract, not an empty
document.

--json emits document-outline.js's own tree verbatim rather than a
CLI-private shape, so a consumer can walk it with that package's own
isOutlineNode/isOutlineChild guards.
… the family

Adds the command's own section, its entry in the flag-registration paragraph
(the shared flags minus --out, since it prints to stdout and writes no file),
its mention in the two command lists that enumerate what `formats` does not
cover, and document-outline.js as a node in the dependency graph.
OUTLINE_CONVERSION_TARGET.odg was 'svg', and buildSvgText refuses to write
a document with more than one page (SvgMultiPageNotSpecifiedError) since
this command has no --page flag to answer it with. Every multi-page .odg
therefore failed outright with an error naming a write-side target format
the caller never asked to convert to.

odp is a registered odg conversion pair with no per-document page-selection
constraint, so it bridges cleanly regardless of page count. The trade-off is
that each drawing page becomes a presentation slide group (labelled
"Slide N", document-outline.js's own presentation convention) rather than a
"Page N" draw-page group, since odp is a cross-variant bridge and svg -- the
only same-variant target odg has -- is exactly the format whose write side
cannot hold more than one page.
…tline

A spreadsheet source outlines to sheet-group labels only -- cells are
addressable data, not outline content -- so a csv source's --delimiter had
nothing downstream to change: outline semi.csv and outline semi.csv
--delimiter ';' printed the identical output regardless of the flag. Flags
are registered only on commands whose pair can actually reach the edge in
question; advertising one that cannot do anything invites a caller to
believe it has an effect it doesn't.
… can't resolve

inferFormatFromExtension('-') always returns undefined by design, so
outline - failed before ever reaching the markdown-image-resolver line
that special-cases stdin -- that branch was genuinely unreachable dead
code, and the failure message told a stdin caller to "rename the file
with a recognised extension", advice that makes no sense for a pipe.

resolveSourceFormat gives an explicit --from priority over extension
inference, mirroring resolveTargetFormat's own --to resolution order
(commands/shared.ts) on the source side. This makes stdin genuinely
usable (outline - --from docx) rather than only rewording the error,
and it makes the stdin arm of the image-resolver ternary reachable
again, since resolveSourceFormat now requires --from for '-' instead
of failing before the conversion ever runs.
…tline content

lines.join('\n') on an empty array is '', and the write appended '\n'
unconditionally, so a document with nothing to outline (no headings, no
list items, no leaves) printed one stray blank line instead of no output
at all. --json already handled this correctly, printing a bare [].
…rces

"headings nested by heading level" doesn't hold across the board for docx
or odt: readDocxContent/readOdtContent correctly recover headingLevel from
w:outlineLvl/text:outline-level when the file on disk actually carries it --
true of anything Word or LibreOffice authors, and always true of a markdown
source parsed straight from #/## -- but buildDocxPackage and buildOdtPackage
never write that signal back out (no w:outlineLvl, no text:h element), so a
docx or odt this ecosystem's own conversions produce loses heading nesting
even though its paragraphs still carry a Heading1/Heading2-style reference.
List nesting and slide/sheet/page grouping are unaffected either way.
…a real round trip

The smoke suite is this repo's convention for proving every command family
works through the actual built dist/cli.js, not just the in-process command
tree -- outline had neither a --help mention nor a subprocess-level case, so
a green smoke run proved nothing about it.
@Mearman
Mearman merged commit ce949f0 into main Aug 19, 2026
9 checks passed
@Mearman
Mearman deleted the feat/outline-command branch August 19, 2026 02:33
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 3.1.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant