1
0
textfmt/lib.go
2025-11-03 01:29:31 +01:00

311 lines
6.6 KiB
Go

// Package textfmt can be used to format structured data into plain text for terminal, roff, markdown or HTML.
package textfmt
import (
"io"
"time"
)
const (
invalid = iota
title
paragraph
list
numberedList
definitions
numberedDefinitions
table
code
syntax
)
type Txt struct {
text string
link string
bold, italic bool
cat []Txt
}
type ListItem struct {
text Txt
}
type DefinitionItem struct {
name, value Txt
}
type TableCell struct {
text Txt
}
type TableRow struct {
cells []TableCell
header bool
}
type SyntaxItem struct {
symbol string
multiple bool
required bool
optional bool
sequence []SyntaxItem
choice []SyntaxItem
topLevel bool
delimited bool
}
type Entry struct {
typ int
text Txt
titleLevel int
items []ListItem
definitions []DefinitionItem
rows []TableRow
syntax SyntaxItem
wrapWidth int
wrapWidthFirst int
indent int
indentFirst int
man struct {
section int
date time.Time
version string
category string
}
}
type TitleInfo struct {
section int
date time.Time
version string
category string
}
type Document struct {
entries []Entry
}
func Text(text string) Txt {
return Txt{text: text}
}
func Link(label, uri string) Txt {
return Txt{text: label, link: uri}
}
func Bold(t Txt) Txt {
t.bold = true
return t
}
func Italic(t Txt) Txt {
t.italic = true
return t
}
func Cat(t ...Txt) Txt {
return Txt{cat: t}
}
// Title can be used for document titles or subtitles. Titles with different levels behave differently with the
// different output formats. TTY output does prints every level the same way. HTML and markdown titles utilize
// the level info from 0 to 5, where 0 will mean H1. In case of Roff, if the level is 0 and the ManSection is
// set, the output will be in manpage title format, expecting the man macro to be used when processing the roff
// output.
func Title(level int, text string, manInfo ...TitleInfo) Entry {
if level != 0 {
return Entry{
typ: title,
titleLevel: level,
text: Text(text),
}
}
e := Entry{
typ: title,
titleLevel: level,
text: Text(text),
}
for _, m := range manInfo {
if m.section != 0 {
e.man.section = m.section
}
if !m.date.IsZero() {
e.man.date = m.date
}
if m.version != "" {
e.man.version = m.version
}
if m.category != "" {
e.man.category = m.category
}
}
return e
}
func ManSection(s int) TitleInfo {
return TitleInfo{section: s}
}
func ReleaseDate(d time.Time) TitleInfo {
return TitleInfo{date: d}
}
func ReleaseVersion(v string) TitleInfo {
return TitleInfo{version: v}
}
func ManCategory(c string) TitleInfo {
return TitleInfo{category: c}
}
func Paragraph(t Txt) Entry {
return Entry{typ: paragraph, text: t}
}
func Item(text Txt) ListItem {
return ListItem{text: text}
}
func List(items ...ListItem) Entry {
return Entry{typ: list, items: items}
}
func NumberedList(items ...ListItem) Entry {
return Entry{typ: numberedList, items: items}
}
func Definition(name, value Txt) DefinitionItem {
return DefinitionItem{name: name, value: value}
}
func DefinitionList(items ...DefinitionItem) Entry {
return Entry{typ: definitions, definitions: items}
}
func NumberedDefinitionList(items ...DefinitionItem) Entry {
return Entry{typ: numberedDefinitions, definitions: items}
}
func Cell(text Txt) TableCell {
return TableCell{text: text}
}
func Header(cells ...TableCell) TableRow {
return TableRow{cells: cells, header: true}
}
func Row(cells ...TableCell) TableRow {
return TableRow{cells: cells}
}
func Table(rows ...TableRow) Entry {
return Entry{typ: table, rows: rows}
}
func CodeBlock(codeBlock string) Entry {
return Entry{typ: code, text: Text(codeBlock)}
}
func Symbol(text string) SyntaxItem {
return SyntaxItem{symbol: text}
}
func OneOrMore(item SyntaxItem) SyntaxItem {
item.required = true
item.optional = false
item.multiple = true
return item
}
func ZeroOrMore(item SyntaxItem) SyntaxItem {
item.required = false
item.optional = true
item.multiple = true
return item
}
func Required(item SyntaxItem) SyntaxItem {
item.required = true
item.optional = false
return item
}
func Optional(item SyntaxItem) SyntaxItem {
item.required = false
item.optional = true
return item
}
func Sequence(items ...SyntaxItem) SyntaxItem {
return SyntaxItem{sequence: items}
}
func Choice(items ...SyntaxItem) SyntaxItem {
return SyntaxItem{choice: items}
}
func Syntax(items ...SyntaxItem) Entry {
if len(items) == 1 {
return Entry{typ: syntax, syntax: items[0]}
}
return Entry{typ: syntax, syntax: Sequence(items...)}
}
// Wrap can be used to wrap text into multiple similar length lines of maximum width. It may behave different
// for different entries and different output types.
func Wrap(e Entry, width int) Entry {
e.wrapWidth = width
return e
}
// Indent can be used to control indentation. Indentation may behave different depending on the output format.
// The indent parameter sets the indentation in spaces for a section. The indentFirst parameter sets the
// indentation for the first line in the section and it is relative to the indent parameter.
func Indent(e Entry, indent, indentFirst int) Entry {
e.indent, e.indentFirst = indent, indentFirst
return e
}
func Doc(e ...Entry) Document {
return Document{entries: e}
}
// Teletype can be used to render the input document in plain text usable to print in TTY terminals, typically
// directly to the standard output.
func Teletype(out io.Writer, d Document) error {
return renderTeletype(out, d)
}
// Runoff is an attempt to render roff format. It is primarily targeting man pages. While it may be possible to
// use for other purposes, the man macro will likely be required to render the output.
//
// Text is always wrapped, or as controlled by the roff processor, except for tables. The Wrap instrunction has
// no effect, except for tables.
func Runoff(out io.Writer, d Document) error {
return renderRoff(out, d)
}
// Markdown can be used to render the input document in basic markdown format. The only not entirely standard
// output feature is the support for tables.
func Markdown(out io.Writer, d Document) error {
return renderMarkdown(out, d)
}
// HTMLFragment renders the input document as HTML fragments, without html and body tags.
func HTMLFragment(out io.Writer, doc Document) error {
return renderHTMLFragment(out, doc)
}
// HTML renders the input document as an HTML document. If the lang attribute is empty, it will be omitted from
// the html element.
func HTML(out io.Writer, doc Document, lang string) error {
return renderHTML(out, doc, lang)
}