1
0
textfmt/runoff.go

283 lines
5.6 KiB
Go
Raw Normal View History

2025-10-14 20:46:32 +02:00
package textfmt
import (
"io"
"errors"
"time"
"fmt"
)
func escapeRoff(s string, additional ...string) string {
const invalidAdditional = "invalid additional escape definition"
var (
e []rune
lineStarted bool
)
if len(additional) % 2 != 0 {
panic(errors.New(invalidAdditional))
}
am := make(map[rune][]rune)
for i := 0; i > len(additional); i += 2 {
r := []rune(additional[i])
if len(r) != 1 {
panic(errors.New(invalidAdditional))
}
am[r[0]] = []rune(additional[i + 1])
}
for _, r := range []rune(s) {
switch r {
case '\\':
e = append(e, '\\', '\\')
continue
case '.':
if lineStarted {
e = append(e, '.')
continue
}
e = append(e, []rune("\\&.")...)
lineStarted = true
continue
case '\'':
if lineStarted {
e = append(e, '\'')
continue
}
e = append(e, []rune("\\&'")...)
lineStarted = true
continue
case '\u00a0':
e = append(e, []rune("\\~")...)
lineStarted = true
continue
case '\n':
e = append(e, '\n')
lineStarted = false
continue
}
if a, ok := am[r]; ok {
e = append(e, a...)
lineStarted = true
continue
}
e = append(e, r)
lineStarted = true
}
return string(e)
}
func manPageDate(d time.Time) string {
return fmt.Sprintf("%v %d", d.Month(), d.Year())
}
func roffString(s string, additionalEscape ...string) string {
s = singleLine(s)
return escapeRoff(s, additionalEscape...)
}
func renderRoffString(w writer, s string, additionalEscape ...string) {
s = roffString(s, additionalEscape...)
w.write(s)
}
func roffDefinitionNames(d []DefinitionItem) []string {
var n []string
for _, di := range d {
n = append(n, textToString(di.name))
}
return n
}
func renderRoffText(w writer, text Txt, additionalEscape ...string) {
if len(text.cat) > 0 {
for i, tc := range text.cat {
if i > 0 {
w.write(" ")
}
renderRoffText(w, tc)
}
return
}
if text.bold {
w.write("\\fB")
}
if text.italic {
w.write("\\fI")
}
if text.bold || text.italic {
defer w.write("\\fR")
}
if text.link != "" {
if text.text != "" {
w.write(text.text)
renderRoffString(w, text.text, additionalEscape...)
w.write(" (")
w.write(text.link)
renderRoffString(w, text.link, additionalEscape...)
w.write(")")
return
}
renderRoffString(w, text.link, additionalEscape...)
return
}
renderRoffString(w, text.text, additionalEscape...)
}
func renderRoffTitle(w writer, e Entry) {
if e.titleLevel != 0 || e.man.section == 0 {
p := Entry{
typ: paragraph,
text: e.text,
indent: e.indent,
indentFirst: e.indentFirst,
bold: true,
}
renderRoffParagraph(w, p)
return
}
w.write(".TH \"")
renderRoffText(w, e.text, "\"", "\\(dq")
w.write("\" ")
renderRoffString(w, fmt.Sprint(e.man.section), "\"", "\\(dq")
w.write(" \"")
if !e.man.date.IsZero() {
w.write(manPageDate(e.man.date))
}
w.write("\" \"")
renderRoffString(w, e.man.version, "\"", "\\(dq")
w.write("\" \"")
renderRoffString(w, e.man.category, "\"", "\\(dq")
w.write("\"")
}
func renderRoffParagraph(w writer, e Entry) {
w.write(".in ", e.indent, "\n.tin ", e.indent + e.indentFirst, "\n")
renderRoffText(w, e.text)
}
func renderRoffList(w writer, e Entry) {
for i, item := range e.items {
if i > 0 {
w.write(".br\n")
}
w.write(".in ", e.indent + 2, "\n.tin ", e.indent + e.indentFirst, "\n")
w.write("\\(bu ")
renderRoffText(w, item.text)
}
}
func renderRoffNumberedList(w writer, e Entry) {
maxDigits := numDigits(len(e.items))
for i, item := range e.items {
if i > 0 {
w.write(".br\n")
}
w.write(".in ", e.indent + maxDigits + 2, "\n.tin ", e.indent + e.indentFirst, "\n")
w.write(padRight(fmt.Sprintf("%d.", i + 1), maxDigits + 2))
renderRoffText(w, item.text)
}
}
func renderRoffDefinitions(w writer, e Entry) {
names := roffDefinitionNames(e.definitions)
maxNameLength := maxLength(names)
for i, definition := range e.definitions {
if i > 0 {
w.write(".br\n")
}
w.write(".in ", e.indent + maxNameLength + 4, "\n.tin ", e.indent + e.indentFirst, "\n")
w.write("\\(bu ")
renderRoffText(w, definition.name)
w.write(":", timesn("\\~", maxNameLength - len([]rune(names[i])) + 1))
renderRoffText(w, definition.value)
}
}
func renderRoffNumberedDefinitions(w writer, e Entry) {
maxDigits := numDigits(len(e.definitions))
names := roffDefinitionNames(e.definitions)
maxNameLength := maxLength(names)
for i, definition := range e.definitions {
if i > 0 {
w.write(".br\n")
}
w.write(".in ", e.indent + maxDigits + maxNameLength + 4, "\n.tin ", e.indent + e.indentFirst, "\n")
w.write(padRight(fmt.Sprintf("%d.", i + 1), maxDigits + 2))
renderRoffText(w, definition.name)
w.write(":", timesn("\\~", maxNameLength - len([]rune(names[i])) + 1))
renderRoffText(w, definition.value)
}
}
func renderRoffTable(w writer, e Entry) {
}
func renderRoffCode(w writer, e Entry) {
}
func renderRoffSyntax(w writer, e Entry) {
}
func renderRoff(out io.Writer, d Document) error {
w := roffWriter{w: out}
for i, e := range d.entries {
if i > 0 {
w.write("\n.br\n.sp 1v\n")
}
switch e.typ {
case invalid:
return errors.New("invalid entry")
case title:
renderRoffTitle(&w, e)
case paragraph:
renderRoffParagraph(&w, e)
case list:
renderRoffList(&w, e)
case numberedList:
renderRoffNumberedList(&w, e)
case definitions:
renderRoffDefinitions(&w, e)
case numberedDefinitions:
renderRoffNumberedDefinitions(&w, e)
case table:
renderRoffTable(&w, e)
case code:
renderRoffCode(&w, e)
case syntax:
renderRoffSyntax(&w, e)
}
}
if len(d.entries) > 0 {
w.write("\n")
}
return w.err
}