rtf

package module
v0.2.0 Latest Latest
Warning

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

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

README

rtf

An RTF ⇄ richdoc converter, written in pure Go (CGO-free, including GOOS=js).

rtf parses a practical subset of the Rich Text Format into the neutral richdoc document model, and emits a minimal, well-formed RTF document from a richdoc.Document. The two directions are designed as a faithful round-trip.

d, err := rtf.Parse(src)   // RTF subset -> *richdoc.Document
out, err := rtf.Write(d)   // *richdoc.Document -> RTF document

API

func Parse(src []byte) (*richdoc.Document, error)
func Write(d *richdoc.Document) ([]byte, error)

Parse tokenises RTF properly — groups {}, control words \word with an optional numeric parameter, control symbols (\\ \{ \} \~ \- \_), \'hh hex bytes (decoded through the \ansicpgN / default code page, best-effort cp1252) and \uN Unicode escapes (skipping \ucN fallback characters). RTF groups scope formatting, so character state is tracked on a group stack. Anything the model has no node for is preserved verbatim through RawInline with Format: "rtf", so nothing in the source is silently lost. Write escapes the RTF specials \ { }, emits non-ASCII as \uN?, and declares a two-font table (proportional \f0, monospace \f1) plus a small stylesheet so headings survive the round-trip.

Construct mapping

The supported subset maps to richdoc as follows.

RTF → richdoc (Parse)
RTF richdoc
\b / \b0 Strong
\i / \i0 Emph
\strike / \strike0 Strikethrough
a run in a monospace font (\fN\fmodern / Courier / Consolas / mono) Code (inline)
plain text, \'hh, \uN, \tab, \~, \_ Text
\line / \softline LineBreak
\par ends a Paragraph
\pard resets paragraph properties
\sN (resolved against {\stylesheet}) or \outlinelvlN Heading (level 1–6)
{\pntext…} / {\*\pn…} / \listtext / \ilvl / \ls List / ListItem (\pndec or a digit marker → ordered, \pnlvlblt → unordered)
\liN left indent (not a list/heading) BlockQuote
\brdrb on an empty paragraph ThematicBreak
{\footnote …} (a footnote group, usually after a \chftn mark) Footnote (its paragraphs parsed as the note body)
{\*\bkmkstart name}{\*\bkmkend name} Anchor (ID = name; the marked run becomes its inlines, an empty/unclosed pair a point anchor)
{\field{\*\fldinst HYPERLINK "url"}{\fldrslt text}} Link
{\field{\*\fldinst REF name}{\fldrslt text}} / PAGEREF CrossRef{Target:name, Kind:RefLabel}
{\fonttbl…} read to classify monospace fonts, then dropped
{\stylesheet…} read to resolve heading styles, then dropped
{\colortbl…}, {\info…}, {\*\…} and other unrecognised destinations consumed and dropped
richdoc → RTF (Write)
richdoc RTF
Heading \pard\sN\outlinelvl(N-1) … \par (+ a stylesheet entry)
Paragraph \pard … \par
Strong {\b …}
Emph {\i …}
Strikethrough {\strike …}
Code, Math {\f1 …} (monospace)
List one \pard{\pntext…}{\*\pn…}…\par per item
BlockQuote \pard\li720 … \par per block
CodeBlock \pard monospace lines separated by \line
LineBreak \line
Link {\field{\*\fldinst HYPERLINK "url"}{\fldrslt text}}
Footnote {\super\chftn}{\footnote … } (the mark at the reference site, the body as its own group)
Anchor {\*\bkmkstart ID}…{\*\bkmkend ID} around its inlines
CrossRef (RefLabel) {\field{\*\fldinst REF Target}{\fldrslt text}}
CrossRef (RefCite) {\field{\*\fldinst CITATION Target}{\fldrslt text}} (best-effort; not read back)
ThematicBreak \pard\brdrb\brdrs\brdrw10 \par
MathBlock \pard{\f1 tex}\par
Table tab-separated \par rows (flattened)
Image its alternative text
RawBlock / RawInline (Format: "rtf") emitted verbatim; other formats dropped

Model gaps (routed through Raw)

RTF has no node for some richdoc constructs and vice-versa. Constructs the model cannot represent are preserved verbatim by Parse as RawInline (Format: "rtf"):

  • Embedded pictures {\pict…} — the model's Image only references a URL, so pixel data has no home; the whole group (including its hex data) is kept raw.
  • Other fields {\field…}HYPERLINK maps to Link and REF/PAGEREF to CrossRef; every other field instruction (dates, page numbers, TOC, …) is kept raw.

Conversely, a few richdoc nodes have no faithful RTF concept and are written one-way (visually faithful, but not reconstructed by Parse): CodeBlock and MathBlock become monospace paragraphs, inline Math becomes monospace text, Table is flattened to tab-separated rows, Image is reduced to its alt text, and a RefCite cross-reference is emitted as a CITATION field (which Parse does not recognise back into a CrossRef).

Reference library

RTF has no dominant Go library. The maintained options are one-directional or lossy: j45k4/rtf and lu4p/cat strip RTF to plain text, while therox/rtf-doc and max-legrand/rtf-doc only create documents; docconv shells out to external tools. None parse RTF into a structured, writable model or target richdoc. RTF is a well-specified control-word format the Go standard library handles cleanly, so this package ships a focused, in-org parser and writer with no non-Go dependency.

License

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

Documentation

Overview

Package rtf converts between a practical subset of the Rich Text Format (RTF) and the neutral github.com/go-richdoc/richdoc document model.

Parse tokenises RTF (groups, control words, control symbols, \'hh hex bytes and \uN Unicode escapes) and folds the group-scoped character state (bold, italic, strikethrough, monospace) into a richdoc.Document. Write emits a minimal, well-formed RTF document from a richdoc.Document. The two directions are designed as a faithful round-trip for the supported subset: Parse(Write(Parse(src))) is semantically equal to Parse(src).

Footnote groups map to richdoc.Footnote, bookmarks ({\*\bkmkstart}/{\*\bkmkend}) to richdoc.Anchor, and REF/PAGEREF fields to a RefLabel richdoc.CrossRef; HYPERLINK fields stay a richdoc.Link.

RTF has no native concept for several richdoc nodes (code blocks, block and inline math, tables, images with embedded data). Those are emitted in a best-effort visual form by Write and are documented as one-way mappings. Conversely, constructs the model has no node for (embedded pictures and fields other than hyperlinks and references) are preserved verbatim through richdoc.RawInline with Format "rtf", so nothing in the source is silently lost.

The package is pure Go and builds with CGO disabled, including for GOOS=js/GOARCH=wasm.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrEmpty is returned for empty input.
	ErrEmpty = errors.New("rtf: empty input")
	// ErrNotRTF is returned when the stream does not begin with {\rtf.
	ErrNotRTF = errors.New("rtf: not an RTF document (missing {\\rtf header)")
	// ErrUnbalanced is returned for a group brace mismatch.
	ErrUnbalanced = errors.New("rtf: unbalanced group braces")
	// ErrTruncated is returned for a control sequence cut off by end of input.
	ErrTruncated = errors.New("rtf: truncated control sequence")
	// ErrBadHex is returned for a malformed \'hh escape.
	ErrBadHex = errors.New("rtf: invalid hex escape")
)

Sentinel errors returned by Parse. They are wrapped-free so callers can compare with errors.Is.

Functions

func Parse

func Parse(src []byte) (*richdoc.Document, error)

Parse converts a practical subset of RTF into a richdoc.Document.

func Write

func Write(d *richdoc.Document) ([]byte, error)

Write emits a well-formed RTF document from a richdoc.Document. It always succeeds; the error result is part of the symmetric API with Parse. A nil document produces an empty but valid RTF file.

The header declares two fonts (proportional \f0, monospace \f1) and a small stylesheet mapping \s1..\s6 to heading levels, which Parse reads back to reconstruct headings. Font 1 also carries inline code, code blocks and math.

Types

This section is empty.

Jump to

Keyboard shortcuts

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