1
0
textfmt/runoff.go

549 lines
10 KiB
Go
Raw Permalink Normal View History

2025-10-14 20:46:32 +02:00
package textfmt
import (
2025-10-23 02:55:49 +02:00
"bytes"
2025-10-14 20:46:32 +02:00
"errors"
"fmt"
2025-10-23 02:55:49 +02:00
"io"
2025-10-23 02:55:38 +02:00
"strings"
2025-10-23 02:55:49 +02:00
"time"
2025-10-14 20:46:32 +02:00
)
func manPageDate(d time.Time) string {
return fmt.Sprintf("%v %d", d.Month(), d.Year())
}
func escapeRoffString(s string, additionalEscape ...string) string {
2025-11-02 07:14:35 +01:00
var b bytes.Buffer
w, f := writeWith(&b, escapeRoff(additionalEscape...))
write(w, s)
f()
2025-11-02 07:14:35 +01:00
return b.String()
2025-10-14 20:46:32 +02:00
}
func roffTextLength(t Txt) int {
var l int
if len(t.cat) > 0 {
for i, tc := range t.cat {
if i > 0 {
l++
}
l += roffTextLength(tc)
}
return l
}
if t.link == "" {
return len([]rune(t.text))
}
if t.text == "" {
return len([]rune(t.link))
2025-10-14 20:46:32 +02:00
}
return len([]rune(t.text)) + len([]rune(t.link)) + 3
}
func roffTextToString(t Txt) string {
var b bytes.Buffer
renderRoffText(&b, t)
return b.String()
2025-10-14 20:46:32 +02:00
}
2025-11-02 07:14:35 +01:00
func roffCellTexts(r []TableRow) [][]string {
2025-10-23 02:55:38 +02:00
var cellTexts [][]string
for _, row := range r {
var c []string
for _, cell := range row.cells {
2025-11-02 07:14:35 +01:00
c = append(c, roffTextToString(cell.text))
2025-10-23 02:55:38 +02:00
}
cellTexts = append(cellTexts, c)
}
2025-11-02 07:14:35 +01:00
return cellTexts
2025-10-23 02:55:38 +02:00
}
2025-11-02 07:14:35 +01:00
func renderRoffText(w io.Writer, text Txt, additionalEscape ...string) {
2025-10-14 20:46:32 +02:00
if len(text.cat) > 0 {
for i, tc := range text.cat {
if i > 0 {
2025-11-02 07:14:35 +01:00
write(w, " ")
2025-10-14 20:46:32 +02:00
}
renderRoffText(w, tc)
}
return
}
if text.bold {
2025-11-02 07:14:35 +01:00
write(w, "\\fB")
2025-10-14 20:46:32 +02:00
}
if text.italic {
2025-11-02 07:14:35 +01:00
write(w, "\\fI")
2025-10-14 20:46:32 +02:00
}
if text.bold || text.italic {
2025-11-02 07:14:35 +01:00
defer write(w, "\\fR")
2025-10-14 20:46:32 +02:00
}
2025-11-02 07:14:35 +01:00
w, f := writeWith(w, singleLine(), escapeRoff(additionalEscape...))
if text.link == "" {
write(w, text.text)
f()
return
}
2025-10-14 20:46:32 +02:00
2025-11-02 07:14:35 +01:00
if text.text == "" {
write(w, text.link)
f()
2025-10-14 20:46:32 +02:00
return
}
2025-11-02 07:14:35 +01:00
write(w, text.text)
write(w, " (")
write(w, text.link)
write(w, ")")
f()
2025-10-14 20:46:32 +02:00
}
2025-11-02 07:14:35 +01:00
func renderRoffTitle(w io.Writer, e Entry) {
2025-10-14 20:46:32 +02:00
if e.titleLevel != 0 || e.man.section == 0 {
2025-10-23 02:55:38 +02:00
e.text.bold = true
2025-10-14 20:46:32 +02:00
p := Entry{
2025-10-23 02:55:49 +02:00
typ: paragraph,
text: e.text,
indent: e.indent,
2025-10-14 20:46:32 +02:00
indentFirst: e.indentFirst,
}
renderRoffParagraph(w, p)
return
}
2025-11-02 07:14:35 +01:00
write(w, ".TH \"")
2025-10-14 20:46:32 +02:00
renderRoffText(w, e.text, "\"", "\\(dq")
2025-11-02 07:14:35 +01:00
write(w, "\" ")
w, f := writeWith(w, singleLine(), escapeRoff("\"", "\\(dq"))
write(w, fmt.Sprint(e.man.section))
w, _ = f()
write(w, " \"")
2025-10-14 20:46:32 +02:00
if !e.man.date.IsZero() {
2025-11-02 07:14:35 +01:00
write(w, manPageDate(e.man.date))
}
write(w, "\" \"")
w, f = writeWith(w, singleLine(), escapeRoff("\"", "\\(dq"))
write(w, e.man.version)
w, _ = f()
write(w, "\" \"")
w, f = writeWith(w, singleLine(), escapeRoff("\"", "\\(dq"))
write(w, e.man.category)
w, _ = f()
write(w, "\"")
2025-10-14 20:46:32 +02:00
}
2025-11-02 07:14:35 +01:00
func renderRoffParagraph(w io.Writer, e Entry) {
write(w, ".in ", e.indent, "\n.ti ", e.indent+e.indentFirst, "\n")
2025-10-14 20:46:32 +02:00
renderRoffText(w, e.text)
}
2025-11-02 07:14:35 +01:00
func renderRoffList(w io.Writer, e Entry) {
bullets := make([]string, len(e.items))
bulletLengths := make([]int, len(e.items))
for i := range e.items {
itemStyle := mergeItemStyles(e.items[i].style)
if itemStyle.noBullet {
continue
2025-10-14 20:46:32 +02:00
}
if itemStyle.bullet == "" {
bullets[i] = "\\(bu"
bulletLengths[i] = 1
continue
}
bullets[i] = escapeRoffString(itemStyle.bullet, " ", "\\~")
bulletLengths[i] = len([]rune(itemStyle.bullet))
}
var maxBulletLength int
for _, l := range bulletLengths {
if l > maxBulletLength {
maxBulletLength = l
}
}
for i := range bullets {
if bulletLengths[i] == maxBulletLength {
continue
}
bullets[i] = fmt.Sprintf(
"%s%s",
bullets[i],
timesn("\\~", maxBulletLength-bulletLengths[i]),
)
2025-10-14 20:46:32 +02:00
}
for i, item := range e.items {
if i > 0 {
2025-11-02 07:14:35 +01:00
write(w, "\n.br\n")
2025-10-14 20:46:32 +02:00
}
indent := e.indent
if maxBulletLength > 0 {
indent += maxBulletLength + 1
}
write(w, ".in ", indent, "\n.ti ", e.indent+e.indentFirst, "\n")
write(w, bullets[i])
if maxBulletLength > 0 {
write(w, "\\~")
}
2025-10-14 20:46:32 +02:00
renderRoffText(w, item.text)
}
}
func renderRoffNumberedList(w io.Writer, e Entry) {
items := make([]ListItem, len(e.items))
for i := range e.items {
items[i] = Item(
e.items[i].text,
append(e.items[i].style, Bullet(fmt.Sprintf("%d.", i+1)))...,
)
}
e.typ = list
e.items = items
renderRoffList(w, e)
}
2025-11-02 07:14:35 +01:00
func renderRoffDefinitions(w io.Writer, e Entry) {
itemStyles := make([]ItemStyle, len(e.definitions))
for i := range e.definitions {
itemStyles[i] = mergeItemStyles(e.definitions[i].style)
}
bullets := make([]string, len(itemStyles))
bulletLengths := make([]int, len(itemStyles))
for i := range itemStyles {
if itemStyles[i].noBullet {
continue
2025-10-14 20:46:32 +02:00
}
if itemStyles[i].bullet == "" {
bullets[i] = "\\(bu"
bulletLengths[i] = 1
continue
}
bullets[i] = escapeRoffString(itemStyles[i].bullet, " ", "\\~")
bulletLengths[i] = len([]rune(itemStyles[i].bullet))
}
var maxBulletLength int
for i := range bulletLengths {
if bulletLengths[i] > maxBulletLength {
maxBulletLength = bulletLengths[i]
}
}
nameLengths := make([]int, len(e.definitions))
for i := range e.definitions {
nameLengths[i] = roffTextLength(e.definitions[i].name)
}
var maxNameLength int
for i := range nameLengths {
if nameLengths[i] > maxNameLength {
maxNameLength = nameLengths[i]
}
2025-10-14 20:46:32 +02:00
}
for i, definition := range e.definitions {
if i > 0 {
2025-11-02 07:14:35 +01:00
write(w, "\n.br\n")
2025-10-14 20:46:32 +02:00
}
indent := e.indent + maxNameLength + 2
if maxBulletLength > 0 {
indent += maxBulletLength + 1
}
write(w, ".in ", indent, "\n.ti ", e.indent+e.indentFirst, "\n")
write(w, bullets[i])
if maxBulletLength > 0 {
write(w, timesn("\\~", maxBulletLength-bulletLengths[i]+1))
}
2025-10-14 20:46:32 +02:00
renderRoffText(w, definition.name)
write(w, ":", timesn("\\~", maxNameLength-nameLengths[i]+1))
2025-10-14 20:46:32 +02:00
renderRoffText(w, definition.value)
}
}
func renderRoffNumberedDefinitions(w io.Writer, e Entry) {
defs := make([]DefinitionItem, len(e.definitions))
for i := range e.definitions {
defs[i] = Definition(
e.definitions[i].name,
e.definitions[i].value,
append(e.definitions[i].style, Bullet(fmt.Sprintf("%d.", i+1)))...,
)
}
e.typ = definitions
e.definitions = defs
renderRoffDefinitions(w, e)
}
2025-11-02 07:14:35 +01:00
func renderRoffTable(w io.Writer, e Entry) {
2025-10-23 02:55:38 +02:00
if len(e.rows) == 0 {
return
}
e.rows = normalizeTable(e.rows)
if len(e.rows[0].cells) == 0 {
return
}
2025-11-02 07:14:35 +01:00
write(w, ".nf\n")
defer write(w, "\n.fi")
2025-10-23 02:55:38 +02:00
2025-11-02 07:14:35 +01:00
cellTexts := roffCellTexts(e.rows)
2025-10-23 02:55:38 +02:00
totalSeparatorWidth := (len(cellTexts[0]) - 1) * 3
if e.wrapWidth > 0 {
allocatedWidth := e.wrapWidth - e.indent - totalSeparatorWidth
columnWeights := columnWeights(cellTexts)
targetColumnWidths := targetColumnWidths(allocatedWidth, columnWeights)
for i := range cellTexts {
for j := range cellTexts[i] {
2025-11-02 06:27:17 +01:00
cellTexts[i][j] = editString(cellTexts[i][j], wrap(targetColumnWidths[j], targetColumnWidths[j]))
2025-10-23 02:55:38 +02:00
}
}
}
columnWidths := columnWidths(cellTexts)
totalWidth := totalSeparatorWidth
for i := range columnWidths {
totalWidth += columnWidths[i]
}
2025-11-02 07:14:35 +01:00
w, f := writeWith(w, indent(e.indent, e.indent))
2025-10-23 02:55:38 +02:00
hasHeader := e.rows[0].header
for i := range cellTexts {
if i > 0 {
sep := "-"
if hasHeader && i == 1 {
sep = "="
}
2025-11-02 07:14:35 +01:00
write(w, "\n")
write(w, timesn(sep, totalWidth))
write(w, "\n")
2025-10-23 02:55:38 +02:00
}
lines := make([][]string, len(cellTexts[i]))
for j := range cellTexts[i] {
lines[j] = strings.Split(cellTexts[i][j], "\n")
}
var maxLines int
for j := range lines {
if len(lines[j]) > maxLines {
maxLines = len(lines[j])
}
}
for k := 0; k < maxLines; k++ {
if k > 0 {
2025-11-02 07:14:35 +01:00
write(w, "\n")
2025-10-23 02:55:38 +02:00
}
for j := range lines {
2025-11-02 07:14:35 +01:00
if j > 0 {
write(w, " | ")
2025-10-23 02:55:38 +02:00
}
var l string
if k < len(lines[j]) {
l = lines[j][k]
}
2025-11-02 07:14:35 +01:00
write(w, padRight(l, columnWidths[j]))
2025-10-23 02:55:38 +02:00
}
}
}
if hasHeader && len(cellTexts) == 1 {
2025-11-02 07:14:35 +01:00
write(w, "\n", timesn("=", totalWidth))
2025-10-23 02:55:38 +02:00
}
2025-11-02 07:14:35 +01:00
f()
2025-10-14 20:46:32 +02:00
}
2025-11-02 07:14:35 +01:00
func renderRoffCode(w io.Writer, e Entry) {
write(w, ".nf\n")
defer write(w, "\n.fi")
w, f := writeWith(w, escapeRoff(), indent(e.indent, e.indent))
write(w, e.text.text)
f()
2025-10-23 02:55:38 +02:00
}
2025-11-02 07:14:35 +01:00
func renderRoffMultiple(w io.Writer, s SyntaxItem) {
2025-10-23 02:55:38 +02:00
s.topLevel = false
s.multiple = false
renderRoffSyntaxItem(w, s)
2025-11-02 07:14:35 +01:00
write(w, "...")
2025-10-23 02:55:38 +02:00
}
2025-11-02 07:14:35 +01:00
func renderRoffRequired(w io.Writer, s SyntaxItem) {
2025-10-23 02:55:38 +02:00
s.delimited = true
s.topLevel = false
s.required = false
2025-11-02 07:14:35 +01:00
write(w, "<")
2025-10-23 02:55:38 +02:00
renderRoffSyntaxItem(w, s)
2025-11-02 07:14:35 +01:00
write(w, ">")
2025-10-23 02:55:38 +02:00
}
2025-11-02 07:14:35 +01:00
func renderRoffOptional(w io.Writer, s SyntaxItem) {
2025-10-23 02:55:38 +02:00
s.delimited = true
s.topLevel = false
s.optional = false
2025-11-02 07:14:35 +01:00
write(w, "[")
2025-10-23 02:55:38 +02:00
renderRoffSyntaxItem(w, s)
2025-11-02 07:14:35 +01:00
write(w, "]")
2025-10-23 02:55:38 +02:00
}
2025-11-02 07:14:35 +01:00
func renderRoffSequence(w io.Writer, s SyntaxItem) {
2025-10-23 02:55:38 +02:00
if !s.delimited && !s.topLevel {
2025-11-02 07:14:35 +01:00
write(w, "(")
2025-10-23 02:55:38 +02:00
}
for i, item := range s.sequence {
if i > 0 {
2025-11-02 07:14:35 +01:00
write(w, " ")
2025-10-23 02:55:38 +02:00
}
item.delimited = false
renderRoffSyntaxItem(w, item)
}
if !s.delimited && !s.topLevel {
2025-11-02 07:14:35 +01:00
write(w, ")")
2025-10-23 02:55:38 +02:00
}
}
2025-11-02 07:14:35 +01:00
func renderRoffChoice(w io.Writer, s SyntaxItem) {
2025-10-23 02:55:38 +02:00
if !s.delimited && !s.topLevel {
2025-11-02 07:14:35 +01:00
write(w, "(")
2025-10-23 02:55:38 +02:00
}
for i, item := range s.choice {
if i > 0 {
separator := "|"
if s.topLevel {
separator = "\n"
}
2025-11-02 07:14:35 +01:00
write(w, separator)
2025-10-23 02:55:38 +02:00
}
item.delimited = false
item.topLevel = s.topLevel
2025-10-23 02:55:38 +02:00
renderRoffSyntaxItem(w, item)
}
if !s.delimited && !s.topLevel {
2025-11-02 07:14:35 +01:00
write(w, ")")
2025-10-23 02:55:38 +02:00
}
}
2025-11-02 07:14:35 +01:00
func renderRoffSymbol(w io.Writer, s SyntaxItem) {
write(w, s.symbol)
2025-10-23 02:55:38 +02:00
}
2025-11-02 07:14:35 +01:00
func renderRoffSyntaxItem(w io.Writer, s SyntaxItem) {
2025-10-23 02:55:38 +02:00
switch {
// foo...
case s.multiple:
renderRoffMultiple(w, s)
// <foo>
case s.required:
renderRoffRequired(w, s)
// [foo]
case s.optional:
renderRoffOptional(w, s)
// foo bar baz or (foo bar baz)
case len(s.sequence) > 0:
renderRoffSequence(w, s)
// foo|bar|baz or (foo|bar|baz)
case len(s.choice) > 0:
renderRoffChoice(w, s)
// foo
default:
renderRoffSymbol(w, s)
}
2025-10-14 20:46:32 +02:00
}
2025-11-02 07:14:35 +01:00
func renderRoffSyntax(w io.Writer, e Entry) {
2025-10-23 02:55:38 +02:00
s := e.syntax
s.topLevel = true
2025-11-02 07:14:35 +01:00
write(w, ".nf\n")
defer write(w, "\n.fi")
w, f := writeWith(w, escapeRoff(), indent(e.indent, e.indent))
2025-10-23 02:55:38 +02:00
renderRoffSyntaxItem(w, s)
2025-11-02 07:14:35 +01:00
f()
2025-10-14 20:46:32 +02:00
}
func renderRoff(out io.Writer, d Document) error {
2025-11-02 07:14:35 +01:00
w, f := writeWith(out, roffNBSP(), errorHandler)
2025-10-14 20:46:32 +02:00
for i, e := range d.entries {
if i > 0 {
2025-11-02 07:14:35 +01:00
write(w, "\n.br\n.sp 1v\n")
2025-10-14 20:46:32 +02:00
}
switch e.typ {
case title:
2025-11-02 06:27:17 +01:00
renderRoffTitle(w, e)
2025-10-14 20:46:32 +02:00
case paragraph:
2025-11-02 06:27:17 +01:00
renderRoffParagraph(w, e)
2025-10-14 20:46:32 +02:00
case list:
2025-11-02 06:27:17 +01:00
renderRoffList(w, e)
2025-10-14 20:46:32 +02:00
case numberedList:
2025-11-02 06:27:17 +01:00
renderRoffNumberedList(w, e)
2025-10-14 20:46:32 +02:00
case definitions:
2025-11-02 06:27:17 +01:00
renderRoffDefinitions(w, e)
2025-10-14 20:46:32 +02:00
case numberedDefinitions:
2025-11-02 06:27:17 +01:00
renderRoffNumberedDefinitions(w, e)
2025-10-14 20:46:32 +02:00
case table:
2025-11-02 06:27:17 +01:00
renderRoffTable(w, e)
2025-10-14 20:46:32 +02:00
case code:
2025-11-02 06:27:17 +01:00
renderRoffCode(w, e)
2025-10-14 20:46:32 +02:00
case syntax:
2025-11-02 06:27:17 +01:00
renderRoffSyntax(w, e)
2025-10-28 02:48:55 +01:00
default:
return errors.New("invalid entry")
2025-10-14 20:46:32 +02:00
}
}
if len(d.entries) > 0 {
2025-11-02 07:14:35 +01:00
write(w, "\n")
2025-10-14 20:46:32 +02:00
}
2025-11-02 07:14:35 +01:00
_, err := f()
return err
2025-10-14 20:46:32 +02:00
}