1
0

refactor roff rendering

This commit is contained in:
Arpad Ryszka 2025-11-02 07:14:35 +01:00
parent 4c0d034620
commit a6332946e5
3 changed files with 119 additions and 161 deletions

267
runoff.go
View File

@ -7,91 +7,46 @@ import (
"io" "io"
"strings" "strings"
"time" "time"
"unicode"
) )
func trim(s string) string {
return strings.TrimFunc(
s,
func(r rune) bool { return r != '\u00a0' && unicode.IsSpace(r) },
)
}
func textToString(t Txt) string {
if len(t.cat) == 0 && t.link == "" {
return trim(t.text)
}
if len(t.cat) == 0 && t.text == "" {
return trim(t.link)
}
if len(t.cat) == 0 {
return fmt.Sprintf("%s (%s)", t.text, t.link)
}
b := bytes.NewBuffer(nil)
for i := range t.cat {
if i > 0 {
b.WriteRune(' ')
}
b.WriteString(textToString(t.cat[i]))
}
return editString(b.String(), singleLine())
}
func manPageDate(d time.Time) string { func manPageDate(d time.Time) string {
return fmt.Sprintf("%v %d", d.Month(), d.Year()) return fmt.Sprintf("%v %d", d.Month(), d.Year())
} }
func roffString(s string, additionalEscape ...string) string { func roffTextToString(t Txt) string {
s = editString(s, singleLine()) var b bytes.Buffer
return editString(s, escapeRoff(additionalEscape...)) renderRoffText(&b, t)
} return b.String()
func renderRoffString(w writer, s string, additionalEscape ...string) {
s = roffString(s, additionalEscape...)
w.write(s)
} }
func roffDefinitionNames(d []DefinitionItem) []string { func roffDefinitionNames(d []DefinitionItem) []string {
var n []string var n []string
for _, di := range d { for _, di := range d {
n = append(n, textToString(di.name)) n = append(n, roffTextToString(di.name))
} }
return n return n
} }
func roffCellTexts(r []TableRow) ([][]string, error) { func roffCellTexts(r []TableRow) [][]string {
var cellTexts [][]string var cellTexts [][]string
for _, row := range r { for _, row := range r {
var c []string var c []string
for _, cell := range row.cells { for _, cell := range row.cells {
var b bytes.Buffer c = append(c, roffTextToString(cell.text))
w := newRoffWriter(&b, true)
renderRoffText(w, cell.text)
w.flush()
if w.err != nil {
return nil, w.err
}
c = append(c, b.String())
} }
cellTexts = append(cellTexts, c) cellTexts = append(cellTexts, c)
} }
return cellTexts, nil return cellTexts
} }
func renderRoffText(w writer, text Txt, additionalEscape ...string) { func renderRoffText(w io.Writer, text Txt, additionalEscape ...string) {
if len(text.cat) > 0 { if len(text.cat) > 0 {
for i, tc := range text.cat { for i, tc := range text.cat {
if i > 0 { if i > 0 {
w.write(" ") write(w, " ")
} }
renderRoffText(w, tc) renderRoffText(w, tc)
@ -101,34 +56,38 @@ func renderRoffText(w writer, text Txt, additionalEscape ...string) {
} }
if text.bold { if text.bold {
w.write("\\fB") write(w, "\\fB")
} }
if text.italic { if text.italic {
w.write("\\fI") write(w, "\\fI")
} }
if text.bold || text.italic { if text.bold || text.italic {
defer w.write("\\fR") defer write(w, "\\fR")
} }
if text.link != "" { w, f := writeWith(w, singleLine(), escapeRoff(additionalEscape...))
if text.text != "" { if text.link == "" {
renderRoffString(w, text.text, additionalEscape...) write(w, text.text)
w.write(" (") f()
renderRoffString(w, text.link, additionalEscape...)
w.write(")")
return return
} }
renderRoffString(w, text.link, additionalEscape...) if text.text == "" {
write(w, text.link)
f()
return return
} }
renderRoffString(w, text.text, additionalEscape...) write(w, text.text)
write(w, " (")
write(w, text.link)
write(w, ")")
f()
} }
func renderRoffTitle(w writer, e Entry) { func renderRoffTitle(w io.Writer, e Entry) {
if e.titleLevel != 0 || e.man.section == 0 { if e.titleLevel != 0 || e.man.section == 0 {
e.text.bold = true e.text.bold = true
p := Entry{ p := Entry{
@ -142,86 +101,92 @@ func renderRoffTitle(w writer, e Entry) {
return return
} }
w.write(".TH \"") write(w, ".TH \"")
renderRoffText(w, e.text, "\"", "\\(dq") renderRoffText(w, e.text, "\"", "\\(dq")
w.write("\" ") write(w, "\" ")
renderRoffString(w, fmt.Sprint(e.man.section), "\"", "\\(dq") w, f := writeWith(w, singleLine(), escapeRoff("\"", "\\(dq"))
w.write(" \"") write(w, fmt.Sprint(e.man.section))
w, _ = f()
write(w, " \"")
if !e.man.date.IsZero() { if !e.man.date.IsZero() {
w.write(manPageDate(e.man.date)) write(w, manPageDate(e.man.date))
} }
w.write("\" \"") write(w, "\" \"")
renderRoffString(w, e.man.version, "\"", "\\(dq") w, f = writeWith(w, singleLine(), escapeRoff("\"", "\\(dq"))
w.write("\" \"") write(w, e.man.version)
renderRoffString(w, e.man.category, "\"", "\\(dq") w, _ = f()
w.write("\"") write(w, "\" \"")
w, f = writeWith(w, singleLine(), escapeRoff("\"", "\\(dq"))
write(w, e.man.category)
w, _ = f()
write(w, "\"")
} }
func renderRoffParagraph(w writer, e Entry) { func renderRoffParagraph(w io.Writer, e Entry) {
w.write(".in ", e.indent, "\n.ti ", e.indent+e.indentFirst, "\n") write(w, ".in ", e.indent, "\n.ti ", e.indent+e.indentFirst, "\n")
renderRoffText(w, e.text) renderRoffText(w, e.text)
} }
func renderRoffList(w writer, e Entry) { func renderRoffList(w io.Writer, e Entry) {
for i, item := range e.items { for i, item := range e.items {
if i > 0 { if i > 0 {
w.write("\n.br\n") write(w, "\n.br\n")
} }
w.write(".in ", e.indent+2, "\n.ti ", e.indent+e.indentFirst, "\n") write(w, ".in ", e.indent+2, "\n.ti ", e.indent+e.indentFirst, "\n")
w.write("\\(bu ") write(w, "\\(bu ")
renderRoffText(w, item.text) renderRoffText(w, item.text)
} }
} }
func renderRoffNumberedList(w writer, e Entry) { func renderRoffNumberedList(w io.Writer, e Entry) {
maxDigits := numDigits(len(e.items)) maxDigits := numDigits(len(e.items))
for i, item := range e.items { for i, item := range e.items {
if i > 0 { if i > 0 {
w.write("\n.br\n") write(w, "\n.br\n")
} }
w.write(".in ", e.indent+maxDigits+2, "\n.ti ", e.indent+e.indentFirst, "\n") write(w, ".in ", e.indent+maxDigits+2, "\n.ti ", e.indent+e.indentFirst, "\n")
w.write(padRight(fmt.Sprintf("%d.", i+1), maxDigits+2)) write(w, padRight(fmt.Sprintf("%d.", i+1), maxDigits+2))
renderRoffText(w, item.text) renderRoffText(w, item.text)
} }
} }
func renderRoffDefinitions(w writer, e Entry) { func renderRoffDefinitions(w io.Writer, e Entry) {
names := roffDefinitionNames(e.definitions) names := roffDefinitionNames(e.definitions)
maxNameLength := maxLength(names) maxNameLength := maxLength(names)
for i, definition := range e.definitions { for i, definition := range e.definitions {
if i > 0 { if i > 0 {
w.write("\n.br\n") write(w, "\n.br\n")
} }
w.write(".in ", e.indent+maxNameLength+4, "\n.ti ", e.indent+e.indentFirst, "\n") write(w, ".in ", e.indent+maxNameLength+4, "\n.ti ", e.indent+e.indentFirst, "\n")
w.write("\\(bu ") write(w, "\\(bu ")
renderRoffText(w, definition.name) renderRoffText(w, definition.name)
w.write(":", timesn("\\~", maxNameLength-len([]rune(names[i]))+1)) write(w, ":", timesn("\\~", maxNameLength-len([]rune(names[i]))+1))
renderRoffText(w, definition.value) renderRoffText(w, definition.value)
} }
} }
func renderRoffNumberedDefinitions(w writer, e Entry) { func renderRoffNumberedDefinitions(w io.Writer, e Entry) {
maxDigits := numDigits(len(e.definitions)) maxDigits := numDigits(len(e.definitions))
names := roffDefinitionNames(e.definitions) names := roffDefinitionNames(e.definitions)
maxNameLength := maxLength(names) maxNameLength := maxLength(names)
for i, definition := range e.definitions { for i, definition := range e.definitions {
if i > 0 { if i > 0 {
w.write("\n.br\n") write(w, "\n.br\n")
} }
w.write(".in ", e.indent+maxDigits+maxNameLength+4, "\n.ti ", e.indent+e.indentFirst, "\n") write(w, ".in ", e.indent+maxDigits+maxNameLength+4, "\n.ti ", e.indent+e.indentFirst, "\n")
w.write(padRight(fmt.Sprintf("%d.", i+1), maxDigits+2)) write(w, padRight(fmt.Sprintf("%d.", i+1), maxDigits+2))
renderRoffText(w, definition.name) renderRoffText(w, definition.name)
w.write(":", timesn("\\~", maxNameLength-len([]rune(names[i]))+1)) write(w, ":", timesn("\\~", maxNameLength-len([]rune(names[i]))+1))
renderRoffText(w, definition.value) renderRoffText(w, definition.value)
} }
} }
func renderRoffTable(w writer, e Entry) { func renderRoffTable(w io.Writer, e Entry) {
if len(e.rows) == 0 { if len(e.rows) == 0 {
return return
} }
@ -231,14 +196,10 @@ func renderRoffTable(w writer, e Entry) {
return return
} }
w.write(".nf\n") write(w, ".nf\n")
defer w.write("\n.fi") defer write(w, "\n.fi")
cellTexts, err := roffCellTexts(e.rows)
if err != nil {
w.setErr(err)
}
cellTexts := roffCellTexts(e.rows)
totalSeparatorWidth := (len(cellTexts[0]) - 1) * 3 totalSeparatorWidth := (len(cellTexts[0]) - 1) * 3
if e.wrapWidth > 0 { if e.wrapWidth > 0 {
allocatedWidth := e.wrapWidth - e.indent - totalSeparatorWidth allocatedWidth := e.wrapWidth - e.indent - totalSeparatorWidth
@ -257,6 +218,7 @@ func renderRoffTable(w writer, e Entry) {
totalWidth += columnWidths[i] totalWidth += columnWidths[i]
} }
w, f := writeWith(w, indent(e.indent, e.indent))
hasHeader := e.rows[0].header hasHeader := e.rows[0].header
for i := range cellTexts { for i := range cellTexts {
if i > 0 { if i > 0 {
@ -265,9 +227,9 @@ func renderRoffTable(w writer, e Entry) {
sep = "=" sep = "="
} }
w.write("\n") write(w, "\n")
w.write(timesn(" ", e.indent), timesn(sep, totalWidth)) write(w, timesn(sep, totalWidth))
w.write("\n") write(w, "\n")
} }
lines := make([][]string, len(cellTexts[i])) lines := make([][]string, len(cellTexts[i]))
@ -284,14 +246,12 @@ func renderRoffTable(w writer, e Entry) {
for k := 0; k < maxLines; k++ { for k := 0; k < maxLines; k++ {
if k > 0 { if k > 0 {
w.write("\n") write(w, "\n")
} }
for j := range lines { for j := range lines {
if j == 0 { if j > 0 {
w.write(timesn(" ", e.indent)) write(w, " | ")
} else {
w.write(" | ")
} }
var l string var l string
@ -299,57 +259,59 @@ func renderRoffTable(w writer, e Entry) {
l = lines[j][k] l = lines[j][k]
} }
w.write(padRight(l, columnWidths[j])) write(w, padRight(l, columnWidths[j]))
} }
} }
} }
if hasHeader && len(cellTexts) == 1 { if hasHeader && len(cellTexts) == 1 {
w.write("\n", timesn("=", totalWidth)) write(w, "\n", timesn("=", totalWidth))
}
} }
func renderRoffCode(w writer, e Entry) { f()
w.write(".nf\n")
defer w.write("\n.fi")
txt := editString(e.text.text, escapeRoff())
txt = editString(txt, indent(e.indent, e.indent))
w.write(txt)
} }
func renderRoffMultiple(w writer, s SyntaxItem) { 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()
}
func renderRoffMultiple(w io.Writer, s SyntaxItem) {
s.topLevel = false s.topLevel = false
s.multiple = false s.multiple = false
renderRoffSyntaxItem(w, s) renderRoffSyntaxItem(w, s)
w.write("...") write(w, "...")
} }
func renderRoffRequired(w writer, s SyntaxItem) { func renderRoffRequired(w io.Writer, s SyntaxItem) {
s.delimited = true s.delimited = true
s.topLevel = false s.topLevel = false
s.required = false s.required = false
w.write("<") write(w, "<")
renderRoffSyntaxItem(w, s) renderRoffSyntaxItem(w, s)
w.write(">") write(w, ">")
} }
func renderRoffOptional(w writer, s SyntaxItem) { func renderRoffOptional(w io.Writer, s SyntaxItem) {
s.delimited = true s.delimited = true
s.topLevel = false s.topLevel = false
s.optional = false s.optional = false
w.write("[") write(w, "[")
renderRoffSyntaxItem(w, s) renderRoffSyntaxItem(w, s)
w.write("]") write(w, "]")
} }
func renderRoffSequence(w writer, s SyntaxItem) { func renderRoffSequence(w io.Writer, s SyntaxItem) {
if !s.delimited && !s.topLevel { if !s.delimited && !s.topLevel {
w.write("(") write(w, "(")
} }
for i, item := range s.sequence { for i, item := range s.sequence {
if i > 0 { if i > 0 {
w.write(" ") write(w, " ")
} }
item.delimited = false item.delimited = false
@ -357,13 +319,13 @@ func renderRoffSequence(w writer, s SyntaxItem) {
} }
if !s.delimited && !s.topLevel { if !s.delimited && !s.topLevel {
w.write(")") write(w, ")")
} }
} }
func renderRoffChoice(w writer, s SyntaxItem) { func renderRoffChoice(w io.Writer, s SyntaxItem) {
if !s.delimited && !s.topLevel { if !s.delimited && !s.topLevel {
w.write("(") write(w, "(")
} }
for i, item := range s.choice { for i, item := range s.choice {
@ -373,7 +335,7 @@ func renderRoffChoice(w writer, s SyntaxItem) {
separator = "\n" separator = "\n"
} }
w.write(separator) write(w, separator)
} }
item.delimited = false item.delimited = false
@ -381,15 +343,15 @@ func renderRoffChoice(w writer, s SyntaxItem) {
} }
if !s.delimited && !s.topLevel { if !s.delimited && !s.topLevel {
w.write(")") write(w, ")")
} }
} }
func renderRoffSymbol(w writer, s SyntaxItem) { func renderRoffSymbol(w io.Writer, s SyntaxItem) {
w.write(editString(s.symbol, escapeRoff())) write(w, s.symbol)
} }
func renderRoffSyntaxItem(w writer, s SyntaxItem) { func renderRoffSyntaxItem(w io.Writer, s SyntaxItem) {
switch { switch {
// foo... // foo...
@ -418,24 +380,21 @@ func renderRoffSyntaxItem(w writer, s SyntaxItem) {
} }
} }
func renderRoffSyntax(w writer, e Entry) { func renderRoffSyntax(w io.Writer, e Entry) {
s := e.syntax s := e.syntax
s.topLevel = true s.topLevel = true
w.write(".nf\n") write(w, ".nf\n")
defer w.write("\n.fi") defer write(w, "\n.fi")
w.write(timesn("\u00a0", e.indent)) w, f := writeWith(w, escapeRoff(), indent(e.indent, e.indent))
renderRoffSyntaxItem(w, s) renderRoffSyntaxItem(w, s)
f()
} }
func renderRoff(out io.Writer, d Document) error { func renderRoff(out io.Writer, d Document) error {
w := newRoffWriter(out, false) w, f := writeWith(out, roffNBSP(), errorHandler)
for i, e := range d.entries { for i, e := range d.entries {
if err := w.error(); err != nil {
return err
}
if i > 0 { if i > 0 {
w.write("\n.br\n.sp 1v\n") write(w, "\n.br\n.sp 1v\n")
} }
switch e.typ { switch e.typ {
@ -463,9 +422,9 @@ func renderRoff(out io.Writer, d Document) error {
} }
if len(d.entries) > 0 { if len(d.entries) > 0 {
w.write("\n") write(w, "\n")
} }
w.flush() _, err := f()
return w.err return err
} }

View File

@ -127,7 +127,7 @@ Below you can find some test text, with various text items.
.br .br
.sp 1v .sp 1v
.nf .nf
\~\~\~\~\~\~\~\~textfmt.Doc ( [Entry]... ) textfmt.Doc ( [Entry]... )
.fi .fi
.br .br
.sp 1v .sp 1v
@ -329,7 +329,7 @@ Below you can find some test text, with various text items.
.br .br
.sp 1v .sp 1v
.nf .nf
\~\~\~\~\~\~\~\~textfmt.Doc ( [Entry]... ) textfmt.Doc ( [Entry]... )
.fi .fi
.br .br
.sp 1v .sp 1v
@ -2292,7 +2292,7 @@ and silver birch\~\~\~\~ | their canopies creating\~ | and shadow on the
t.Fatal(err) t.Fatal(err)
} }
if b.String() != ".nf\n\\~\\~\\~\\~foo [options]... <filename> [string|number]...\n.fi\n" { if b.String() != ".nf\n foo [options]... <filename> [string|number]...\n.fi\n" {
t.Fatal(b.String()) t.Fatal(b.String())
} }
}) })

View File

@ -149,8 +149,7 @@ func ttyCellTexts(rows []TableRow) [][]string {
for _, row := range rows { for _, row := range rows {
var c []string var c []string
for _, cell := range row.cells { for _, cell := range row.cells {
txt := ttyTextToString(cell.text) c = append(c, ttyTextToString(cell.text))
c = append(c, txt)
} }
cellTexts = append(cellTexts, c) cellTexts = append(cellTexts, c)