richdoc

package module
v0.6.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 4, 2026 License: BSD-3-Clause Imports: 1 Imported by: 0

README

richdoc

A neutral, format-agnostic and widget-agnostic document model for rich text, written in pure Go (CGO-free).

richdoc is the foundation of a multi-format rich-document system: a WYSIWYG toolkit widget and converters for Markdown, LaTeX, ODT and RTF are all built on top of this one model. It intentionally does no rendering, parsing or I/O — it only defines the tree and a few small utilities to traverse, build, extract and copy it.

Model

A Document is an ordered slice of block nodes plus a format-agnostic map[string]string of metadata. Blocks and inlines are closed interface sets: each concrete type carries an unexported marker method, so consumers (converters especially) can type-switch over them exhaustively.

  • Blocks: Heading, Paragraph, List (ListItem), CodeBlock, BlockQuote, Table (Cell, Alignment), ThematicBreak, MathBlock, RawBlock.
  • Inlines: Text, Emph, Strong, Strikethrough, Code, Link, Image, Math, LineBreak, RawInline.

RawBlock/RawInline carry format-specific passthrough text for round-trip fidelity.

A cell can hold blocks (v0.5.0)

reST's grid tables allow full block content in a cell, HTML's <td> allows any flow content, LaTeX's tabular allows a parbox. Cell therefore has both:

type Cell struct {
	Inlines []Inline // the flattened view, for a consumer that predates Blocks
	Blocks  []Block  // the faithful content, when there is more than a paragraph
	ColSpan int
	RowSpan int
}

The duplication is deliberate and its contract is short: a producer that sets Blocks must also set Inlines, a consumer reads whichever it can represent, and Cell.Content() is that preference order in one call — Blocks when present, otherwise the Inlines wrapped in a single Paragraph. A cell holding one paragraph, which is nearly all of them, still sets Inlines alone.

It is measured, not speculative: over go-richdoc/rst's 1564-document corpus, 64 list items, 47 lines in 14 line blocks, 35 literal blocks and 23 bullet lists in 24 files were being flattened to a run of text. Table.Caption []Inline arrived with it, for 24 captions in 13 files.

Utilities

  • Walk(d *Document, v Visitor) — depth-first traversal (Enter/Leave).
  • New() *Builder — fluent construction with inline/structural constructor helpers.
  • PlainText(d *Document) string — textual content for search and previews.
  • Clone(d *Document) *Document — deep copy for undo snapshots and rewrites.

Example

doc := richdoc.New().
	Meta("title", "Hello").
	H(1, richdoc.Txt("Hello")).
	P(
		richdoc.Bold(richdoc.Txt("bold")),
		richdoc.Txt(" and "),
		richdoc.Italic(richdoc.Txt("italic")),
	).
	UList(true,
		richdoc.Item(richdoc.Paragraph{Inlines: []richdoc.Inline{richdoc.Txt("first")}}),
		richdoc.Item(richdoc.Paragraph{Inlines: []richdoc.Inline{richdoc.Txt("second")}}),
	).
	Doc()

Toolchain

Requires Go 1.27.1, which is the version the CI workflow pins. The two were out of step — the module asked for 1.26.4 while CI already ran 1.27.1 — and that difference decides two things a reader of this repository should not have to guess: 1.27 counts statements more finely, so a coverage figure from an older toolchain is an upper bound rather than a measurement, and its gofmt reindents a composite literal inside a multi-value return, which an older local gofmt reports as clean. Both were checked with 1.27.1 here: coverage is unchanged at 100%, and the tree needs no reformatting.

License

BSD-3-Clause. Copyright (c) the go-richdoc authors.

Documentation

Overview

Package richdoc defines a neutral, format-agnostic and widget-agnostic model for rich text documents.

The model is a typed tree, not a generic attribute bag. A Document is an ordered slice of Block nodes; blocks and inlines are closed interface sets (each concrete type carries an unexported marker method), so consumers such as converters and editor widgets can exhaustively type-switch over them.

The package is deliberately small and orthogonal. On top of the model it provides four utilities:

  • Walk with a Visitor performs a depth-first traversal.
  • Builder (via New) offers fluent, ergonomic construction.
  • PlainText extracts the textual content of a document.
  • Clone returns a deep copy.

The package has no dependencies beyond the standard library and is safe to build with CGO disabled.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func PlainText

func PlainText(d *Document) string

PlainText returns the textual content of d with block-level nodes separated by newlines. It is intended for search, previews and tests, not for faithful rendering.

It concatenates the values of Text and Code inlines and of CodeBlock blocks, descending through every container (headings, lists, quotes, table cells, and emphasis-like inlines). A Footnote contributes its body text inline at the position it occurs, because footnotes are document text a search should find; an Anchor and a CrossRef contribute their visible inlines but not their identifiers. Nodes that carry no literal text in that sense contribute nothing: ThematicBreak, LineBreak, Image, Math, MathBlock, RawInline and RawBlock. It returns "" for a nil document.

func Walk

func Walk(d *Document, v Visitor)

Walk performs a depth-first traversal of d, driving v. It is a no-op when d is nil.

Types

type Alignment

type Alignment int

Alignment is the horizontal alignment of a table column.

const (
	AlignDefault Alignment = iota
	AlignLeft
	AlignCenter
	AlignRight
)

Column alignments. AlignDefault leaves the choice to the renderer.

type Anchor added in v0.2.0

type Anchor struct {
	ID      string
	Inlines []Inline
}

Anchor is a labeled target attached to inline content: the destination a CrossRef points at (a LaTeX \label, an ODF bookmark, a Markdown heading anchor target). ID is the label; Inlines is the content the label marks and may be empty for a point target that carries no visible text of its own.

func Mark added in v0.2.0

func Mark(id string, inlines ...Inline) Anchor

Mark builds an Anchor labeling the given inlines with id. The inlines may be omitted for a point target.

type Block

type Block interface {
	// contains filtered or unexported methods
}

Block is a top-level or nested block-level node. The set of concrete block types is closed: only types defined in this package satisfy Block, which lets consumers type-switch exhaustively.

type BlockQuote

type BlockQuote struct {
	Blocks  []Block
	Classes []string
}

BlockQuote is a quotation containing nested blocks.

type Builder

type Builder struct {
	// contains filtered or unexported fields
}

Builder incrementally assembles a Document with a fluent, chainable API. Every method appends a top-level block and returns the receiver, and New starts an empty builder:

doc := richdoc.New().
	H(1, richdoc.Txt("Title")).
	P(richdoc.Bold(richdoc.Txt("bold")), richdoc.Txt(" and "), richdoc.Italic(richdoc.Txt("italic"))).
	Doc()

Inline and structural values are produced by the constructor helpers in this file (Txt, Bold, Italic, Item, Td, ...); the plain struct literals remain available for anything the helpers do not cover.

A Builder is not safe for concurrent use.

func New

func New() *Builder

New returns an empty Builder.

func (*Builder) Add

func (b *Builder) Add(blocks ...Block) *Builder

Add appends arbitrary pre-built blocks, an escape hatch for constructs the typed methods below do not cover.

func (*Builder) CodeBlock

func (b *Builder) CodeBlock(language, text string) *Builder

CodeBlock appends a CodeBlock with an optional language tag.

func (*Builder) Doc

func (b *Builder) Doc() *Document

Doc finalizes construction and returns the assembled document.

func (*Builder) H

func (b *Builder) H(level int, inlines ...Inline) *Builder

H appends a Heading of the given level (1..6).

func (*Builder) HR

func (b *Builder) HR() *Builder

HR appends a ThematicBreak.

func (*Builder) MathBlock

func (b *Builder) MathBlock(tex string) *Builder

MathBlock appends a display-math MathBlock carrying TeX source.

func (*Builder) Meta

func (b *Builder) Meta(key, value string) *Builder

Meta sets a metadata key/value pair (for example "title" or "author").

func (*Builder) OList

func (b *Builder) OList(start int, tight bool, items ...ListItem) *Builder

OList appends an ordered List starting at start (clamped to a minimum of 1).

func (*Builder) P

func (b *Builder) P(inlines ...Inline) *Builder

P appends a Paragraph.

func (*Builder) Quote

func (b *Builder) Quote(blocks ...Block) *Builder

Quote appends a BlockQuote wrapping the given blocks.

func (*Builder) RawBlock

func (b *Builder) RawBlock(format, text string) *Builder

RawBlock appends a RawBlock passthrough for the named format.

func (*Builder) Table

func (b *Builder) Table(align []Alignment, header []Cell, rows [][]Cell) *Builder

Table appends a Table with the given column alignments, header cells and rows. Any argument may be nil for an unaligned, headerless or empty table.

func (*Builder) UList

func (b *Builder) UList(tight bool, items ...ListItem) *Builder

UList appends an unordered List.

type Cell

type Cell struct {
	Inlines []Inline
	Blocks  []Block
	ColSpan int
	RowSpan int
}

Cell is a single table cell.

Inlines and Blocks

A cell can hold real block content: a list, a literal block, several paragraphs, even another table. Blocks carries it. Inlines carries the same content FLATTENED to a line of inlines, and remains what a consumer that does not know about Blocks reads.

Both are filled for a cell whose content is more than one paragraph, and that duplication is the point: a converter written before Blocks existed keeps rendering the cell's words instead of silently rendering an empty cell. The contract is therefore

  • a producer that sets Blocks MUST also set Inlines, to whatever degraded rendering it would have produced before;
  • a consumer reads Blocks when it can represent them and Inlines when it cannot, which is what Cell.Content expresses in one call;
  • the two must say the same thing. Nothing enforces it, so a producer that edits one edits the other.

A cell holding exactly one paragraph — the overwhelming common case — sets Inlines alone and leaves Blocks nil, so nothing about the simple case changes.

Spans

ColSpan and RowSpan are the number of columns/rows this cell occupies. Zero, the default — what an existing Cell{Inlines: ...} literal or a Td call already produces, with no field for either — means the same as 1: an ordinary cell spanning nothing extra. A converter that has no notion of spanning cells at all (a plain CommonMark table, say) never needs to touch these fields to keep working correctly.

func Td

func Td(inlines ...Inline) Cell

Td builds a table Cell holding the given inlines.

func (Cell) Content added in v0.5.0

func (c Cell) Content() []Block

Content returns the cell's content as blocks: Blocks when the producer filled it, and otherwise the Inlines wrapped in a single Paragraph. It returns nil for an empty cell.

It exists so that a consumer able to render block content in a cell needs one call rather than a branch at every use, and so that the preference order is written down once.

type Code

type Code struct {
	Value   string
	Classes []string
}

Code is an inline code span. Value is the verbatim code.

Classes are the author's own presentational names for it; see [Classes].

func Mono

func Mono(value string) Code

Mono builds an inline Code span.

type CodeBlock

type CodeBlock struct {
	Language string
	Text     string
	Classes  []string
}

CodeBlock is a block of preformatted code. Language is an optional informational language tag (for example "go"); Text is the verbatim source including its internal newlines.

type CrossRef added in v0.2.0

type CrossRef struct {
	Target  string
	Kind    RefKind
	Inlines []Inline
}

CrossRef is a reference to an Anchor/label or a bibliographic citation. Target is the label or citation key it resolves to and Kind selects between the two. Inlines is the visible text; when it is empty the renderer or writer supplies the resolved number or label.

func Cite added in v0.2.0

func Cite(target string, inlines ...Inline) CrossRef

Cite builds a CrossRef of kind RefCite citing target. The inlines are the visible text and may be omitted for the writer to resolve.

func Ref added in v0.2.0

func Ref(target string, inlines ...Inline) CrossRef

Ref builds a CrossRef of kind RefLabel to target. The inlines are the visible text and may be omitted for the writer to resolve.

type Document

type Document struct {
	Blocks []Block
	Meta   map[string]string
}

Document is a rich text document: an ordered sequence of top-level blocks together with format-agnostic metadata (title, author, and similar).

Meta is an optional, unstructured string map; converters decide how to map its keys onto their target format. A nil Meta is valid and means "no metadata".

func Clone

func Clone(d *Document) *Document

Clone returns a deep copy of d: the returned document shares no mutable state (slices or maps) with the original, so either may be mutated independently. It returns nil for a nil document.

Editors use it for undo snapshots and converters for non-destructive rewrites.

type Emph

type Emph struct {
	Inlines []Inline
}

Emph is emphasized (conventionally italic) inline content.

func Italic

func Italic(inlines ...Inline) Emph

Italic builds an Emph (italic) inline.

type Footnote added in v0.2.0

type Footnote struct {
	Blocks []Block
}

Footnote is a footnote placed inline, whose content is block-level (LaTeX \footnote, an ODF footnote, a Markdown [^id] reference with its definition). Blocks holds the note body; it appears at the position the note is referenced, and a writer is free to relocate the body to the page or document end.

func Note added in v0.2.0

func Note(blocks ...Block) Footnote

Note builds a Footnote whose body is the given blocks.

type Heading

type Heading struct {
	Level   int
	ID      string
	Inlines []Inline
}

Heading is a section heading. Level is 1..6, following the common HTML/ Markdown convention (1 is the most prominent).

ID is an optional anchor identifier for the heading (a Markdown heading anchor, a LaTeX \section immediately followed by \label). An empty ID means the heading carries no explicit anchor.

type Image

type Image struct {
	URL     string
	Alt     string
	Title   string
	Width   string
	Height  string
	Scale   int
	Align   Alignment
	Classes []string
}

Image is an inline image reference. Alt is the textual alternative and Title an optional advisory title.

Width and Height are the requested display size as the author wrote it, including its unit: "200" (no unit, meaning pixels or big points depending on the target), "50%", "4em". Scale is a percentage applied to the intrinsic size, 0 meaning "not given" — a zero-percent image being meaningless, there is no need for a pointer. Align is the horizontal placement; AlignDefault leaves it to the renderer.

These four are not decoration. Every format that can size an image at all needs them — reST's ":width:"/":align:", LaTeX's \includegraphics[width=,height=,scale=], HTML's width/height — and a model without them forces each converter to drop what the author asked for. 43 of the 79 standalone images in go-richdoc/rst's 1564-document corpus carry at least one.

A converter whose format cannot express them ignores them, exactly as it already ignores Title: CommonMark has no syntax for a size, so go-richdoc/markdown emits the same "![alt](url)" it always did.

func Img

func Img(url, alt, title string) Image

Img builds an Image inline.

type Inline

type Inline interface {
	// contains filtered or unexported methods
}

Inline is an inline-level node. Like Block, the set of concrete inline types is closed, so consumers can type-switch exhaustively.

type LineBreak

type LineBreak struct{}

LineBreak is a hard line break within a block.

func Br

func Br() LineBreak

Br builds a hard LineBreak inline.

type Link struct {
	URL     string
	Title   string
	Inlines []Inline
}

Link is a hyperlink wrapping inline content. Title is an optional advisory title (for example a tooltip).

func Href

func Href(url, title string, inlines ...Inline) Link

Href builds a Link inline.

type List

type List struct {
	Ordered bool
	Start   int
	Tight   bool
	Items   []ListItem
	Classes []string
}

List is an ordered or unordered list.

When Ordered is true, Start is the number of the first item (1 when unset). Tight indicates a list whose items should render without inter-item spacing, mirroring the CommonMark tight/loose distinction.

type ListItem

type ListItem struct {
	Blocks []Block
}

ListItem is a single entry of a List. Items hold blocks, which makes arbitrary nesting (paragraphs, sub-lists, quotes, ...) possible.

func Item

func Item(blocks ...Block) ListItem

Item builds a ListItem holding the given blocks.

type Math

type Math struct {
	TeX string
}

Math is inline mathematics, carrying its TeX source.

func InlineMath

func InlineMath(tex string) Math

InlineMath builds a Math inline carrying TeX source.

type MathBlock

type MathBlock struct {
	TeX string
}

MathBlock is display (block-level) mathematics, carrying its TeX source.

type Paragraph

type Paragraph struct {
	Inlines []Inline
	Classes []string
}

Paragraph is a run of inline content forming a single logical paragraph.

Classes are the author's own presentational names for it; see [Classes].

type RawBlock

type RawBlock struct {
	Format string
	Text   string
}

RawBlock is a verbatim, format-specific block passthrough used to preserve round-trip fidelity for constructs the model does not represent natively. Format names the target format the Text belongs to (for example "latex" or "html"); a converter for a different format is free to drop it.

type RawInline

type RawInline struct {
	Format string
	Text   string
}

RawInline is a verbatim, format-specific inline passthrough used to preserve round-trip fidelity, analogous to RawBlock. Format names the target format the Text belongs to.

func RawI

func RawI(format, text string) RawInline

RawI builds a RawInline passthrough for the named format.

type RefKind added in v0.2.0

type RefKind int

RefKind distinguishes the two kinds of reference a CrossRef can be.

const (
	RefLabel RefKind = iota
	RefCite
)

Reference kinds. RefLabel is a cross-reference to a labeled target (LaTeX \ref/\eqref, a Markdown link to an internal id); RefCite is a citation of a bibliographic key (LaTeX \cite).

type Strikethrough

type Strikethrough struct {
	Inlines []Inline
}

Strikethrough is struck-out inline content.

func Strike

func Strike(inlines ...Inline) Strikethrough

Strike builds a Strikethrough inline.

type Strong

type Strong struct {
	Inlines []Inline
}

Strong is strongly emphasized (conventionally bold) inline content.

func Bold

func Bold(inlines ...Inline) Strong

Bold builds a Strong (bold) inline.

type Table

type Table struct {
	Caption []Inline
	Align   []Alignment
	Header  []Cell
	Rows    [][]Cell
	Classes []string
}

Table is a simple grid with an optional header row.

Align gives the per-column alignment; a shorter Align slice leaves the remaining columns at AlignDefault. Header may be empty for a headerless table. Rows is a list of rows, each a slice of cells.

Caption is the table's own title — reST's ".. table:: Caption", HTML's <caption>, LaTeX's \caption — and is empty for a table that has none. It is inline content because every format that has a caption renders it as a line of text, not as a sequence of blocks.

type Text

type Text struct {
	Value string
}

Text is a run of literal text.

func Txt

func Txt(value string) Text

Txt builds a Text inline.

type ThematicBreak

type ThematicBreak struct{}

ThematicBreak is a horizontal rule separating content.

type Visitor

type Visitor interface {
	Enter(node any) (descend bool)
	Leave(node any)
}

Visitor observes a depth-first traversal driven by Walk.

For every node Walk calls Enter before descending into that node's children and Leave once its subtree has been fully visited. Returning false from Enter skips the node's children (Leave is still called), which lets a visitor prune whole subtrees.

Nodes are passed as any. The concrete dynamic types are the Document (passed as *Document), every Block and Inline, and the structural ListItem and Cell containers, so a visitor can recover the full tree structure by type-switching.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL