548 lines
10 KiB
Go
548 lines
10 KiB
Go
package textfmt
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func manPageDate(d time.Time) string {
|
|
return fmt.Sprintf("%v %d", d.Month(), d.Year())
|
|
}
|
|
|
|
func escapeRoffString(s string, additionalEscape ...string) string {
|
|
var b bytes.Buffer
|
|
w, f := writeWith(&b, escapeRoff(additionalEscape...))
|
|
write(w, s)
|
|
f()
|
|
return b.String()
|
|
}
|
|
|
|
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))
|
|
}
|
|
|
|
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()
|
|
}
|
|
|
|
func roffCellTexts(r []TableRow) [][]string {
|
|
var cellTexts [][]string
|
|
for _, row := range r {
|
|
var c []string
|
|
for _, cell := range row.cells {
|
|
c = append(c, roffTextToString(cell.text))
|
|
}
|
|
|
|
cellTexts = append(cellTexts, c)
|
|
}
|
|
|
|
return cellTexts
|
|
}
|
|
|
|
func renderRoffText(w io.Writer, text Txt, additionalEscape ...string) {
|
|
if len(text.cat) > 0 {
|
|
for i, tc := range text.cat {
|
|
if i > 0 {
|
|
write(w, " ")
|
|
}
|
|
|
|
renderRoffText(w, tc)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
if text.bold {
|
|
write(w, "\\fB")
|
|
}
|
|
|
|
if text.italic {
|
|
write(w, "\\fI")
|
|
}
|
|
|
|
if text.bold || text.italic {
|
|
defer write(w, "\\fR")
|
|
}
|
|
|
|
w, f := writeWith(w, singleLine(), escapeRoff(additionalEscape...))
|
|
if text.link == "" {
|
|
write(w, text.text)
|
|
f()
|
|
return
|
|
}
|
|
|
|
if text.text == "" {
|
|
write(w, text.link)
|
|
f()
|
|
return
|
|
}
|
|
|
|
write(w, text.text)
|
|
write(w, " (")
|
|
write(w, text.link)
|
|
write(w, ")")
|
|
f()
|
|
}
|
|
|
|
func renderRoffTitle(w io.Writer, e Entry) {
|
|
if e.titleLevel != 0 || e.man.section == 0 {
|
|
e.text.bold = true
|
|
p := Entry{
|
|
typ: paragraph,
|
|
text: e.text,
|
|
indent: e.indent,
|
|
indentFirst: e.indentFirst,
|
|
}
|
|
|
|
renderRoffParagraph(w, p)
|
|
return
|
|
}
|
|
|
|
write(w, ".TH \"")
|
|
renderRoffText(w, e.text, "\"", "\\(dq")
|
|
write(w, "\" ")
|
|
w, f := writeWith(w, singleLine(), escapeRoff("\"", "\\(dq"))
|
|
write(w, fmt.Sprint(e.man.section))
|
|
w, _ = f()
|
|
write(w, " \"")
|
|
if !e.man.date.IsZero() {
|
|
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, "\"")
|
|
}
|
|
|
|
func renderRoffParagraph(w io.Writer, e Entry) {
|
|
write(w, ".in ", e.indent, "\n.ti ", e.indent+e.indentFirst, "\n")
|
|
renderRoffText(w, e.text)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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]),
|
|
)
|
|
}
|
|
|
|
for i, item := range e.items {
|
|
if i > 0 {
|
|
write(w, "\n.br\n")
|
|
}
|
|
|
|
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, "\\~")
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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]
|
|
}
|
|
}
|
|
|
|
for i, definition := range e.definitions {
|
|
if i > 0 {
|
|
write(w, "\n.br\n")
|
|
}
|
|
|
|
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))
|
|
}
|
|
|
|
renderRoffText(w, definition.name)
|
|
write(w, ":", timesn("\\~", maxNameLength-nameLengths[i]+1))
|
|
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)
|
|
}
|
|
|
|
func renderRoffTable(w io.Writer, e Entry) {
|
|
if len(e.rows) == 0 {
|
|
return
|
|
}
|
|
|
|
e.rows = normalizeTable(e.rows)
|
|
if len(e.rows[0].cells) == 0 {
|
|
return
|
|
}
|
|
|
|
write(w, ".nf\n")
|
|
defer write(w, "\n.fi")
|
|
|
|
cellTexts := roffCellTexts(e.rows)
|
|
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] {
|
|
cellTexts[i][j] = editString(cellTexts[i][j], wrap(targetColumnWidths[j], targetColumnWidths[j]))
|
|
}
|
|
}
|
|
}
|
|
|
|
columnWidths := columnWidths(cellTexts)
|
|
totalWidth := totalSeparatorWidth
|
|
for i := range columnWidths {
|
|
totalWidth += columnWidths[i]
|
|
}
|
|
|
|
w, f := writeWith(w, indent(e.indent, e.indent))
|
|
hasHeader := e.rows[0].header
|
|
for i := range cellTexts {
|
|
if i > 0 {
|
|
sep := "-"
|
|
if hasHeader && i == 1 {
|
|
sep = "="
|
|
}
|
|
|
|
write(w, "\n")
|
|
write(w, timesn(sep, totalWidth))
|
|
write(w, "\n")
|
|
}
|
|
|
|
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 {
|
|
write(w, "\n")
|
|
}
|
|
|
|
for j := range lines {
|
|
if j > 0 {
|
|
write(w, " | ")
|
|
}
|
|
|
|
var l string
|
|
if k < len(lines[j]) {
|
|
l = lines[j][k]
|
|
}
|
|
|
|
write(w, padRight(l, columnWidths[j]))
|
|
}
|
|
}
|
|
}
|
|
|
|
if hasHeader && len(cellTexts) == 1 {
|
|
write(w, "\n", timesn("=", totalWidth))
|
|
}
|
|
|
|
f()
|
|
}
|
|
|
|
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.multiple = false
|
|
renderRoffSyntaxItem(w, s)
|
|
write(w, "...")
|
|
}
|
|
|
|
func renderRoffRequired(w io.Writer, s SyntaxItem) {
|
|
s.delimited = true
|
|
s.topLevel = false
|
|
s.required = false
|
|
write(w, "<")
|
|
renderRoffSyntaxItem(w, s)
|
|
write(w, ">")
|
|
}
|
|
|
|
func renderRoffOptional(w io.Writer, s SyntaxItem) {
|
|
s.delimited = true
|
|
s.topLevel = false
|
|
s.optional = false
|
|
write(w, "[")
|
|
renderRoffSyntaxItem(w, s)
|
|
write(w, "]")
|
|
}
|
|
|
|
func renderRoffSequence(w io.Writer, s SyntaxItem) {
|
|
if !s.delimited && !s.topLevel {
|
|
write(w, "(")
|
|
}
|
|
|
|
for i, item := range s.sequence {
|
|
if i > 0 {
|
|
write(w, " ")
|
|
}
|
|
|
|
item.delimited = false
|
|
renderRoffSyntaxItem(w, item)
|
|
}
|
|
|
|
if !s.delimited && !s.topLevel {
|
|
write(w, ")")
|
|
}
|
|
}
|
|
|
|
func renderRoffChoice(w io.Writer, s SyntaxItem) {
|
|
if !s.delimited && !s.topLevel {
|
|
write(w, "(")
|
|
}
|
|
|
|
for i, item := range s.choice {
|
|
if i > 0 {
|
|
separator := "|"
|
|
if s.topLevel {
|
|
separator = "\n"
|
|
}
|
|
|
|
write(w, separator)
|
|
}
|
|
|
|
item.delimited = false
|
|
renderRoffSyntaxItem(w, item)
|
|
}
|
|
|
|
if !s.delimited && !s.topLevel {
|
|
write(w, ")")
|
|
}
|
|
}
|
|
|
|
func renderRoffSymbol(w io.Writer, s SyntaxItem) {
|
|
write(w, s.symbol)
|
|
}
|
|
|
|
func renderRoffSyntaxItem(w io.Writer, s SyntaxItem) {
|
|
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)
|
|
}
|
|
}
|
|
|
|
func renderRoffSyntax(w io.Writer, e Entry) {
|
|
s := e.syntax
|
|
s.topLevel = true
|
|
write(w, ".nf\n")
|
|
defer write(w, "\n.fi")
|
|
w, f := writeWith(w, escapeRoff(), indent(e.indent, e.indent))
|
|
renderRoffSyntaxItem(w, s)
|
|
f()
|
|
}
|
|
|
|
func renderRoff(out io.Writer, d Document) error {
|
|
w, f := writeWith(out, roffNBSP(), errorHandler)
|
|
for i, e := range d.entries {
|
|
if i > 0 {
|
|
write(w, "\n.br\n.sp 1v\n")
|
|
}
|
|
|
|
switch e.typ {
|
|
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)
|
|
default:
|
|
return errors.New("invalid entry")
|
|
}
|
|
}
|
|
|
|
if len(d.entries) > 0 {
|
|
write(w, "\n")
|
|
}
|
|
|
|
_, err := f()
|
|
return err
|
|
}
|