Format cleanup
A lint for how a document is built, not what it says. Word documents rot: formatting set by hand instead of through styles, empty paragraphs for spacing, duplicate styles from every paste. The engine finds these, explains each one, and fixes them in one undo step.
Packages
| Package | Contents |
|---|---|
@stesura/format-lint | formatLintPlugin, the engine (collectFormatSuggestions, applyFormatSuggestionsTr), commands, types |
@stesura/format-lint-react | formatLintUIExtension: Export-tab button, status-bar count, right panel |
Setup
import { formatLintPlugin } from "@stesura/format-lint";
import { formatLintUIExtension } from "@stesura/format-lint-react";
// in the mount hook's extraPlugins
formatLintPlugin({ debounceMs: 600 });
// in uiExtensions (module level)
const uiExtensions = [formatLintUIExtension];The plugin recollects a debounce after each edit, in the root editor only. Headers, footers, notes and text boxes count for usage (a style used only in a footer is not unused), but paragraph fixes apply to the main story.
Guarantees
Every suggestion says what fixing it does to the page:
| Guarantee | Meaning |
|---|---|
lossless | Every block renders exactly as before. |
tolerance | Lengths move by at most tolerancePx (default 3 px). |
restructure | Changes the document on purpose. Hints only, never applied by “Fix all”. |
A fix that moves a value into a style keeps everything else rendering as before: paragraphs and child styles that inherited the old value get it as their own.
Rules
| Rule | Severity | Finds |
|---|---|---|
| FMT-1 | warning | Custom styles nothing uses or references |
| FMT-2 | warning | Styles that resolve identically (Body Text 2, Normal (Web)) |
| FMT-3 | error | basedOn/next/link naming a missing style, basedOn loops, blocks on a missing style |
| FMT-4 | hint | Unused styles that only exist to be inherited from |
| FMT-5 | warning | Identical list definitions (counters stay separate after the merge) |
| FMT-10 | warning | Paragraph formatting equal to what the style already gives |
| FMT-11 | warning | Direct formatting: a value nearly every paragraph on a style sets moves into the style; repeated combinations become a style; near-misses snap back |
| FMT-12 | warning | Empty paragraphs used as spacing |
| FMT-13 | hint | Line breaks used as paragraph breaks, and the reverse |
| FMT-15 | hint | Typed numbers (1., (a)) next to a real list |
| FMT-16 | hint | Short bold or large lines that read as headings; heading levels that disagree with their style |
| FMT-17 | warning | Numbered paragraphs indented a hair off their list |
| FMT-18 | hint | Manual page breaks before headings |
| FMT-19 | hint | A typed table of contents |
| FMT-20 | warning | Character formatting the style already has |
| FMT-21 | warning | A font, size or colour on 90% of a style’s text, set by hand |
| FMT-22 | hint | Fonts used in the text but in no style |
| FMT-23 | hint | Whole paragraphs highlighted (reported, never fixed) |
Options: tolerancePx (3), coverageThreshold (0.9), minClusterSize (3), rules (run only these)
and ignore (suggestion ids). The defaults come from a survey of real legal documents.
Applying
“Fix all” applies every fixable error and warning in one transaction, in a fixed order (repairs,
content, style fields, clusters, inline coverage, redundancy, styles, lists, chains), each pass
repeated until it settles: a second collect finds nothing left. Hints apply one click each, or under
“Fix all” when named in rules.
Suggestion ids are a hash of the rule and what it is about (a style id, a list, a block), so they
survive edits elsewhere. “Ignore” writes the id to doc.attrs.formatLintIgnore, which travels with
the document to collaborators.
While track changes is on, applying is refused (trackChangesBlockedReason): every cleanup would be
recorded as a tracked formatting change. The panel says so and offers to apply anyway (force: true).
import { applyFormatSuggestions, collectFormatSuggestions } from "@stesura/format-lint";
const suggestions = collectFormatSuggestions(state);
applyFormatSuggestions("all")(state, dispatch);