Documentation
¶
Overview ¶
Package mdreflow reflows Markdown prose: it changes where lines break inside paragraph text and leaves everything else in a document untouched. Its home mode is sentence-per-line (one sentence per source line, aka semantic line breaks); paragraph-per-line and hard-wrap modes share the same pipeline (see docs/design.md).
mdreflow is a reflow tool, not a formatter: it never rewrites block structure (heading style, list markers, tables, escaping). The Markdown parser is used read-only to locate paragraph prose; output is produced by splicing reflowed prose into the verbatim source bytes.
The library API in docs/design.md is complete: all three modes, MaxWidth, hard-break spelling, the abbreviation list, and a pluggable Segmenter are all implemented.
Index ¶
- Constants
- Variables
- func DefaultAbbreviations() []string
- func Format(src []byte, opts Options) ([]byte, error)
- func FormatReader(dst io.Writer, src io.Reader, opts Options) error
- func NeedsFormat(src []byte, opts Options) (bool, error)
- type Dialect
- type FrozenParagraph
- type Mode
- type Options
- type Segmenter
- type SkipReason
- type Span
Constants ¶
const MinMaxWidth = 20
MinMaxWidth is the smallest non-zero Options.MaxWidth Format accepts — see Options.MaxWidth's doc comment for why tiny widths are refused.
Variables ¶
var ErrInvalidUTF8 = errors.New("mdreflow: input is not valid UTF-8")
ErrInvalidUTF8 is returned (possibly wrapped) by Format, Check, and FormatReader when the input is not valid UTF-8. Markdown is text; bytes with no character interpretation have nothing meaningful to reflow. Callers branch with errors.Is.
Functions ¶
func DefaultAbbreviations ¶
func DefaultAbbreviations() []string
DefaultAbbreviations returns a copy of the built-in sentence segmenter's abbreviation exception list (see docs/design.md's "Sentence segmentation" section). Options.Abbreviations adds to this list; it is never replaced wholesale short of providing a custom Segmenter.
func Format ¶
Format reflows src according to opts and returns the result. Everything outside reflowed paragraph prose — code blocks, front matter, headings, lists, blockquotes, tables, and so on — is returned byte-for-byte (see docs/design.md's "Guarantees" section).
Format returns an error, without partial output, if src is not valid UTF-8 (ErrInvalidUTF8) or opts is invalid: see the doc comments on Mode and Options.MaxWidth.
Format's output is a fixpoint: formatting it again returns it unchanged. In the vanishingly rare case where reflow will not converge (docs/design.md, Convergence), Format returns src as-is rather than unstable output.
func FormatReader ¶
FormatReader reads all of src, formats it per opts, and writes the result to dst. Markdown is not streamable in general — e.g. a reference link definition on the last line can affect how the first paragraph renders — so FormatReader buffers the full input before writing anything.
func NeedsFormat ¶ added in v0.1.5
NeedsFormat reports whether Format would change src, without writing anything: true means src is not in formatted form. (Renamed from Check pre-v1: "ok, err := Check(...)" — the shape a reader expects from that name — read exactly backwards.)
Types ¶
type Dialect ¶ added in v0.1.5
Dialect names the renderer profile a document tree targets. It is a single-select enum like Mode, and a bundle rather than a feature flag: a dialect selects which flavor-specific block recognitions are on (docs/design.md, "Dialects: renderer profiles, and the skip-list").
const ( // DialectGFM is the default: the permissive GitHub-flavored superset // (GFM extensions plus footnotes). There is deliberately no // "commonmark" value — that name is kept for a possible future // strict profile with the GFM extensions off, and aliasing it to the // default would burn the name on the one thing it doesn't accurately // describe. DialectGFM Dialect = opts.DialectGFM // DialectMkDocs additionally reflows MkDocs / Python-Markdown // admonition bodies ("!!! note" plus a 4-space-indented body). A // CommonMark parser can only see such a body as an indented code // block, so reflowing it changes what a CommonMark renderer emits — // which is why it is opt-in and can never be the default. DialectMkDocs Dialect = opts.DialectMkDocs )
type FrozenParagraph ¶ added in v0.1.7
type FrozenParagraph struct {
StartLine, EndLine int
Reason SkipReason
// Detail is a short human-readable statement of what fired, phrased
// to follow "skipped: ".
Detail string
// Remediation says how to make the paragraph reflowable, when there
// is something to do; frozen paragraphs are always preserved
// byte-for-byte, so hand-formatting once is always an option.
Remediation string
}
FrozenParagraph reports one paragraph Format passes through byte-for-byte instead of reflowing: where it is, which guard froze it, and what an author can do about it. Line numbers are 1-based and inclusive, counted in the source given to Explain.
func Explain ¶ added in v0.1.7
func Explain(src []byte, opts Options) ([]FrozenParagraph, error)
Explain reports every paragraph Format would pass through byte-for-byte instead of reflowing, in source order, with the guard that froze it and a remediation hint. It is purely diagnostic: it never writes anything, and Format's output is unaffected by whether Explain runs.
Explain judges src as given (the same verdicts Format's first pass uses). Options participate the same way they do in Format — the dialect selects block recognition — and invalid options or non-UTF-8 input fail with the same errors Format returns.
type Mode ¶
Mode selects the top-level reflow strategy.
(Mode and its constants are aliases of one shared definition in an internal leaf package the pipeline uses directly, so the public and internal values can never drift; this alias is the documented, supported name.)
const ( // ModeSentence joins each paragraph's lines and splits at sentence // boundaries, one sentence per line. The default mode. ModeSentence Mode = opts.ModeSentence // ModePara joins each paragraph (each hard-break cluster) to a single // line, with no further splitting. // Options.MaxWidth must be 0 in this mode: para mode's whole point is // one unconditional line, so a non-zero MaxWidth has nothing to apply // to and Format returns an error rather than silently ignoring it — // see Options.MaxWidth's doc comment. ModePara Mode = opts.ModePara // ModeWrap hard-wraps at Options.MaxWidth, breaking at word // boundaries only: a no-break span or a single word wider than the // limit overflows rather than being split. Options.MaxWidth of 0 // defaults to 80 in this mode only — see Options.MaxWidth's doc // comment. ModeWrap Mode = opts.ModeWrap )
type Options ¶
type Options struct {
// Mode selects the reflow strategy. Zero value is ModeSentence.
Mode Mode
// Dialect opts into flavor-specific block recognition. Zero value
// is DialectGFM (today's behavior); see the Dialect constants.
Dialect Dialect
// MaxWidth bounds line length, measured in runes — not bytes, and not
// Unicode grapheme clusters or East-Asian display width. That is a
// deliberate, pragmatic simplification (the same starting point every
// shipping line-wrap tool uses); full grapheme-cluster/width-aware
// measurement is a documented future refinement, not implemented
// here. In every mode, a no-break span (inline code, a link, ...) or
// a single word wider than the limit overflows rather than being
// split.
//
// A non-zero MaxWidth below MinMaxWidth (20) is an options error.
// Nearly every pathological width behavior the fuzz campaign found
// needed single-digit widths, where the geometry forces breaks
// inside constructs; no real document wants width 12. Refusing the
// range outright deletes that adversarial surface from the product
// (docs/design.md, "The width floor").
//
// MaxWidth's meaning is mode-dependent:
//
// - ModeSentence: 0 (default) leaves long sentences alone. A
// non-zero value adds a secondary break to any sentence longer
// than MaxWidth, at the last clause boundary (a comma or
// semicolon followed by a space) before the limit, falling back
// to the last word boundary before the limit, falling back to an
// overflowing single word or no-break span if neither exists.
// - ModeWrap: classic hard wrap at word boundaries. 0 means 80 in
// this mode specifically, matching the CLI's --max-width default
// (docs/design.md's CLI table) — the one place in this package a
// zero MaxWidth does not mean "unbounded".
// - ModePara: MaxWidth must be 0. A paragraph always joins to a
// single line unconditionally in this mode, so Format returns an
// error for a non-zero MaxWidth rather than silently ignoring it.
MaxWidth int
// Abbreviations adds to (never replaces) the built-in segmenter's
// abbreviation exception list. Ignored if Segmenter is set.
Abbreviations []string
// Segmenter overrides the built-in sentence segmenter. Nil (default)
// uses the built-in regex/abbreviation-list segmenter seeded with
// DefaultAbbreviations plus Abbreviations.
Segmenter Segmenter
}
Options configures Format, NeedsFormat, and FormatReader. The zero value is the default and is always valid: sentence mode, unbounded width, the GFM dialect, the built-in segmenter with its default abbreviation list.
type Segmenter ¶
type Segmenter interface {
// Breaks returns the whitespace gaps separating sentences in text: for
// each boundary, the [start,end) byte range of the inter-sentence
// whitespace. The caller (not the Segmenter) decides what replaces
// each gap.
Breaks(text string) []Span
}
Segmenter finds sentence boundaries in prose text. It is independently testable and swappable: provide Options.Segmenter to plug in something smarter than the built-in regex/abbreviation-list segmenter.
type SkipReason ¶ added in v0.1.7
type SkipReason string
SkipReason is a stable, machine-legible code naming the guard that froze a paragraph. Codes are part of the public surface: agents and scripts may branch on them. New codes may be added; existing ones do not change meaning.
const ( // SkipLinkRefDefShape: the paragraph contains a line shaped like a // link reference definition ("[label]: ..."). SkipLinkRefDefShape SkipReason = "link-ref-def-shape" // SkipLinkRefDefNeighbor: the paragraph sits directly against link- // reference-definition machinery above it, within a definition's // possible reach. SkipLinkRefDefNeighbor SkipReason = "link-ref-def-neighbor" // SkipPossibleLinkRefDef: reflow could complete an unbalanced "[" or // an unclosed destination into a real definition. SkipPossibleLinkRefDef SkipReason = "possible-link-ref-def" // SkipRawHTMLDeclOpener: a raw "<?" or "<!" opener outside a code // span. SkipRawHTMLDeclOpener SkipReason = "raw-html-decl-opener" // SkipUnterminatedTag: the paragraph opens with an HTML/JSX tag whose // closing ">" is on a later line. SkipUnterminatedTag SkipReason = "unterminated-tag" // SkipTableAdjacency: the paragraph sits directly under a table with // no blank line between them. SkipTableAdjacency SkipReason = "table-adjacency" // SkipDeepNesting: the paragraph is nested more than two container // (list/blockquote) levels deep. SkipDeepNesting SkipReason = "deep-nesting" // SkipControlBytes: the paragraph contains a C0 control byte other // than tab, CR, or LF. SkipControlBytes SkipReason = "control-bytes" // SkipDegenerateBlank: the paragraph (or one of its lines) trims to // nothing — a control-character parser artifact. SkipDegenerateBlank SkipReason = "degenerate-blank" // SkipHiddenLineGap: the parser carved another construct's bytes out // of this paragraph's line range. SkipHiddenLineGap SkipReason = "hidden-line-gap" // SkipDoubleOwnedLine: a line belongs to both this paragraph and a // duplicate link-reference definition. SkipDoubleOwnedLine SkipReason = "duplicate-def-line" // SkipDialectBlock: the block is a recognized non-prose dialect // construct (front-matter fence, math block, MDX, shortcode). SkipDialectBlock SkipReason = "dialect-block" )
type Span ¶
type Span struct {
Start, End int
}
Span is a half-open byte range [Start, End) into the text passed to a Segmenter's Breaks method. It is a concrete struct (not an alias to an internal type) so its fields are visible in rendered documentation; Format adapts it at the Segmenter boundary.
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
mdreflow
command
Command mdreflow reflows Markdown prose: sentence-per-line by default, paragraph-per-line or hard-wrap on request.
|
Command mdreflow reflows Markdown prose: sentence-per-line by default, paragraph-per-line or hard-wrap on request. |
|
internal
|
|
|
blockmap
Package blockmap derives the set of reflow-eligible paragraphs from a goldmark AST, anywhere in the document: top-level, and nested inside any depth of List/ListItem/Blockquote nesting.
|
Package blockmap derives the set of reflow-eligible paragraphs from a goldmark AST, anywhere in the document: top-level, and nested inside any depth of List/ListItem/Blockquote nesting. |
|
config
Package config loads and discovers .mdreflow.yaml configuration files (docs/design.md's "Configuration" section) and merges them with CLI flag values under the documented precedence: flags > config file > built-in defaults.
|
Package config loads and discovers .mdreflow.yaml configuration files (docs/design.md's "Configuration" section) and merges them with CLI flag values under the documented precedence: flags > config file > built-in defaults. |
|
exclude
Package exclude decides whether a path should be skipped by mdreflow's CLI: repo .gitignore patterns, config-file exclude patterns, and a small set of always-excluded directories (docs/design.md's "Excludes" section).
|
Package exclude decides whether a path should be skipped by mdreflow's CLI: repo .gitignore patterns, config-file exclude patterns, and a small set of always-excluded directories (docs/design.md's "Excludes" section). |
|
gm
Package gm provides the single goldmark configuration used throughout mdreflow: by the reflow pipeline to parse source Markdown, and by tests to render HTML for the render-preservation property check.
|
Package gm provides the single goldmark configuration used throughout mdreflow: by the reflow pipeline to parse source Markdown, and by tests to render HTML for the render-preservation property check. |
|
opts
Package opts holds the option types shared between the public mdreflow package and the internal pipeline packages.
|
Package opts holds the option types shared between the public mdreflow package and the internal pipeline packages. |
|
reflow
Package reflow implements mdreflow's reflow pipeline: given a parsed goldmark document and its source bytes, join each reflow-eligible paragraph's prose lines, find sentence boundaries, and splice the result back into the verbatim source.
|
Package reflow implements mdreflow's reflow pipeline: given a parsed goldmark document and its source bytes, join each reflow-eligible paragraph's prose lines, find sentence boundaries, and splice the result back into the verbatim source. |
|
render
Package render renders Markdown through the same goldmark configuration the reflow pipeline parses with (package gm) and normalizes the HTML for render-preservation comparison.
|
Package render renders Markdown through the same goldmark configuration the reflow pipeline parses with (package gm) and normalizes the HTML for render-preservation comparison. |
|
segment
Package segment implements mdreflow's built-in sentence segmenter and the no-break inline span detector the reflow pipeline uses to protect inline code and links from being split.
|
Package segment implements mdreflow's built-in sentence segmenter and the no-break inline span detector the reflow pipeline uses to protect inline code and links from being split. |