283 lines
5.6 KiB
Go
283 lines
5.6 KiB
Go
|
|
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
|
||
|
|
}
|