441 lines
7.9 KiB
Go
441 lines
7.9 KiB
Go
package textfmt
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
)
|
|
|
|
func escapeTeletype(s string) string {
|
|
r := []rune(s)
|
|
for i := range r {
|
|
if r[i] >= 0x00 && r[i] <= 0x1f && r[i] != '\n' && r[i] != '\t' {
|
|
r[i] = 0xb7
|
|
}
|
|
|
|
if r[i] >= 0x7f && r[i] <= 0x9f {
|
|
r[i] = 0xb7
|
|
}
|
|
}
|
|
|
|
return string(r)
|
|
}
|
|
|
|
func writeLines(w *writer, txt string, indentFirst, indentRest int) {
|
|
lines := strings.Split(txt, "\n")
|
|
for i, l := range lines {
|
|
if i > 0 {
|
|
w.write("\n")
|
|
}
|
|
|
|
indent := indentFirst
|
|
if i > 0 {
|
|
indent = indentRest
|
|
}
|
|
|
|
w.write(timesn(" ", indent))
|
|
w.write(l)
|
|
}
|
|
}
|
|
|
|
func renderTTYText(w *writer, text Txt) {
|
|
if len(text.cat) > 0 {
|
|
for i, tc := range text.cat {
|
|
if i > 0 {
|
|
w.write(" ")
|
|
}
|
|
|
|
renderTTYText(w, tc)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
text.text = singleLine(text.text)
|
|
text.text = escapeTeletype(text.text)
|
|
if text.link != "" {
|
|
if text.text != "" {
|
|
w.write(text.text)
|
|
w.write(" (")
|
|
w.write(text.link)
|
|
w.write(")")
|
|
return
|
|
}
|
|
|
|
w.write(text.link)
|
|
return
|
|
}
|
|
|
|
w.write(text.text)
|
|
}
|
|
|
|
func ttyTextToString(text Txt) (string, error) {
|
|
var b bytes.Buffer
|
|
w := writer{w: &b}
|
|
renderTTYText(&w, text)
|
|
if w.err != nil {
|
|
return "", w.err
|
|
}
|
|
|
|
return b.String(), nil
|
|
}
|
|
|
|
func definitionNames(d []DefinitionItem) ([]string, error) {
|
|
var n []string
|
|
for _, di := range d {
|
|
name, err := ttyTextToString(di.name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
n = append(n, name)
|
|
}
|
|
|
|
return n, nil
|
|
}
|
|
|
|
func renderTTYTitle(w *writer, e Entry) {
|
|
w.write(timesn(" ", e.indentFirst))
|
|
renderTTYText(w, e.text)
|
|
}
|
|
|
|
func renderTTYParagraph(w *writer, e Entry) {
|
|
var txt string
|
|
txt, w.err = ttyTextToString(e.text)
|
|
if e.wrapWidth > 0 {
|
|
txt = wrap(txt, e.wrapWidth, e.indentFirst, e.indentRest)
|
|
}
|
|
|
|
writeLines(w, txt, e.indentFirst, e.indentRest)
|
|
}
|
|
|
|
func renderTTYList(w *writer, e Entry) {
|
|
const bullet = "- "
|
|
indentFirst := e.indentFirst
|
|
indentRest := e.indentRest + len(bullet)
|
|
for i, item := range e.items {
|
|
if i > 0 {
|
|
w.write("\n")
|
|
}
|
|
|
|
var txt string
|
|
txt, w.err = ttyTextToString(item.text)
|
|
if e.wrapWidth > 0 {
|
|
txt = wrap(txt, e.wrapWidth-len(bullet), indentFirst, indentRest)
|
|
}
|
|
|
|
w.write(timesn(" ", indentFirst))
|
|
w.write(bullet)
|
|
writeLines(w, txt, 0, indentRest)
|
|
}
|
|
}
|
|
|
|
func renderTTYNumberedList(w *writer, e Entry) {
|
|
maxDigits := maxDigits(len(e.items))
|
|
indentFirst := e.indentFirst
|
|
indentRest := e.indentRest + maxDigits + 2
|
|
for i, item := range e.items {
|
|
if i > 0 {
|
|
w.write("\n")
|
|
}
|
|
|
|
var txt string
|
|
txt, w.err = ttyTextToString(item.text)
|
|
if e.wrapWidth > 0 {
|
|
txt = wrap(txt, e.wrapWidth-maxDigits-2, indentFirst, indentRest)
|
|
}
|
|
|
|
w.write(timesn(" ", indentFirst))
|
|
w.write(fmt.Sprintf("%d. ", i))
|
|
writeLines(w, txt, 0, indentRest)
|
|
}
|
|
}
|
|
|
|
func renderTTYDefinitions(w *writer, e Entry) {
|
|
names, err := definitionNames(e.definitions)
|
|
if err != nil {
|
|
w.err = err
|
|
return
|
|
}
|
|
|
|
maxLength := maxLength(names)
|
|
indentFirst := e.indentFirst
|
|
indentRest := e.indentRest + maxLength + 4
|
|
for i, def := range e.definitions {
|
|
if i > 0 {
|
|
w.write("\n")
|
|
}
|
|
|
|
var value string
|
|
value, w.err = ttyTextToString(def.value)
|
|
if e.wrapWidth > 0 {
|
|
value = wrap(value, e.wrapWidth-maxLength-4, indentFirst, indentRest)
|
|
}
|
|
|
|
name := names[i]
|
|
w.write("- ")
|
|
w.write(name)
|
|
w.write(": ")
|
|
writeLines(w, value, 0, indentRest)
|
|
}
|
|
}
|
|
|
|
func renderTTYNumberedDefinitions(w *writer, e Entry) {
|
|
maxDigits := maxDigits(len(e.definitions))
|
|
names, err := definitionNames(e.definitions)
|
|
if err != nil {
|
|
w.err = err
|
|
return
|
|
}
|
|
|
|
maxLength := maxLength(names)
|
|
indentFirst := e.indentFirst
|
|
indentRest := e.indentRest + maxLength + maxDigits + 4
|
|
for i, def := range e.definitions {
|
|
if i > 0 {
|
|
w.write("\n")
|
|
}
|
|
|
|
var value string
|
|
value, w.err = ttyTextToString(def.value)
|
|
if e.wrapWidth > 0 {
|
|
value = wrap(value, e.wrapWidth-maxLength-4, indentFirst, indentRest)
|
|
}
|
|
|
|
name := names[i]
|
|
w.write(fmt.Sprintf("%d. ", i))
|
|
w.write(name)
|
|
w.write(": ")
|
|
writeLines(w, value, 0, indentRest)
|
|
}
|
|
}
|
|
|
|
func ttyCellTexts(rows []TableRow) ([][]string, error) {
|
|
var cellTexts [][]string
|
|
for _, row := range rows {
|
|
var c []string
|
|
for _, cell := range row.cells {
|
|
txt, err := ttyTextToString(cell.text)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
c = append(c, txt)
|
|
}
|
|
|
|
cellTexts = append(cellTexts, c)
|
|
}
|
|
|
|
return cellTexts, nil
|
|
}
|
|
|
|
func renderTTYTable(w *writer, e Entry) {
|
|
if len(e.rows) == 0 {
|
|
return
|
|
}
|
|
|
|
e.rows = normalizeTable(e.rows)
|
|
if len(e.rows[0].cells) == 0 {
|
|
return
|
|
}
|
|
|
|
cellTexts, err := ttyCellTexts(e.rows)
|
|
if err != nil {
|
|
w.err = err
|
|
}
|
|
|
|
totalSeparatorWidth := (len(cellTexts[0]) - 1) * 3
|
|
if e.wrapWidth > 0 {
|
|
allocatedWidth := e.wrapWidth - e.indentFirst - totalSeparatorWidth
|
|
columnWeights := columnWeights(cellTexts)
|
|
targetColumnWidths := targetColumnWidths(allocatedWidth, columnWeights)
|
|
for i := range cellTexts {
|
|
for j := range cellTexts[i] {
|
|
cellTexts[i][j] = wrap(cellTexts[i][j], targetColumnWidths[j], 0, 0)
|
|
}
|
|
}
|
|
}
|
|
|
|
columnWidths := columnWidths(cellTexts)
|
|
totalWidth := totalSeparatorWidth
|
|
for i := range columnWidths {
|
|
totalWidth += columnWidths[i]
|
|
}
|
|
|
|
hasHeader := e.rows[0].header
|
|
for i := range cellTexts {
|
|
if i > 0 {
|
|
sep := "-"
|
|
if hasHeader && i == 1 {
|
|
sep = "="
|
|
}
|
|
|
|
w.write("\n")
|
|
w.write(timesn(sep, totalWidth))
|
|
w.write("\n")
|
|
}
|
|
|
|
for j := range cellTexts[i] {
|
|
if j > 0 {
|
|
w.write(" | ")
|
|
}
|
|
|
|
w.write(padRight(cellTexts[i][j], columnWidths[j]))
|
|
}
|
|
}
|
|
|
|
if hasHeader && len(cellTexts) == 1 {
|
|
w.write(timesn("=", totalWidth))
|
|
}
|
|
}
|
|
|
|
func renderTTYCode(w *writer, e Entry) {
|
|
var txt string
|
|
txt, w.err = ttyTextToString(e.text)
|
|
writeLines(w, txt, e.indentFirst, e.indentFirst)
|
|
}
|
|
|
|
func renderTTYMultiple(w *writer, s SyntaxItem) {
|
|
s.topLevel = false
|
|
s.multiple = false
|
|
renderTTYSyntaxItem(w, s)
|
|
w.write("...")
|
|
}
|
|
|
|
func renderTTYRequired(w *writer, s SyntaxItem) {
|
|
s.delimited = true
|
|
s.topLevel = false
|
|
s.required = false
|
|
w.write("<")
|
|
renderTTYSyntaxItem(w, s)
|
|
w.write(">")
|
|
}
|
|
|
|
func renderTTYOptional(w *writer, s SyntaxItem) {
|
|
s.delimited = true
|
|
s.topLevel = false
|
|
s.optional = false
|
|
w.write("[")
|
|
renderTTYSyntaxItem(w, s)
|
|
w.write("]")
|
|
}
|
|
|
|
func renderTTYSequence(w *writer, s SyntaxItem) {
|
|
if !s.delimited && !s.topLevel {
|
|
w.write("(")
|
|
}
|
|
|
|
for i, item := range s.sequence {
|
|
if i > 0 {
|
|
w.write(" ")
|
|
}
|
|
|
|
item.delimited = false
|
|
renderTTYSyntaxItem(w, item)
|
|
}
|
|
|
|
if !s.delimited && !s.topLevel {
|
|
w.write(")")
|
|
}
|
|
}
|
|
|
|
func renderTTYChoice(w *writer, s SyntaxItem) {
|
|
if !s.delimited && !s.topLevel {
|
|
w.write("(")
|
|
}
|
|
|
|
for i, item := range s.sequence {
|
|
if i > 0 {
|
|
separator := "|"
|
|
if s.topLevel {
|
|
separator = "\n"
|
|
}
|
|
|
|
w.write(separator)
|
|
}
|
|
|
|
item.delimited = false
|
|
renderTTYSyntaxItem(w, item)
|
|
}
|
|
|
|
if !s.delimited && !s.topLevel {
|
|
w.write(")")
|
|
}
|
|
}
|
|
|
|
func renderTTYSymbol(w *writer, s SyntaxItem) {
|
|
w.write(escapeTeletype(s.symbol))
|
|
}
|
|
|
|
func renderTTYSyntaxItem(w *writer, s SyntaxItem) {
|
|
switch {
|
|
|
|
// foo...
|
|
case s.multiple:
|
|
renderTTYMultiple(w, s)
|
|
|
|
// <foo>
|
|
case s.required:
|
|
renderTTYRequired(w, s)
|
|
|
|
// [foo]
|
|
case s.optional:
|
|
renderTTYOptional(w, s)
|
|
|
|
// foo bar baz or (foo bar baz)
|
|
case len(s.sequence) > 0:
|
|
renderTTYSequence(w, s)
|
|
|
|
// foo|bar|baz or (foo|bar|baz)
|
|
case len(s.choice) > 0:
|
|
renderTTYChoice(w, s)
|
|
|
|
// foo
|
|
default:
|
|
renderTTYSymbol(w, s)
|
|
}
|
|
}
|
|
|
|
func renderTTYSyntax(w *writer, e Entry) {
|
|
s := e.syntax
|
|
s.topLevel = true
|
|
renderTTYSyntaxItem(w, s)
|
|
}
|
|
|
|
func renderTeletype(out io.Writer, d Document) error {
|
|
w := writer{w: out}
|
|
for i, e := range d.entries {
|
|
if i > 0 {
|
|
w.write("\n\n")
|
|
}
|
|
|
|
switch e.typ {
|
|
case invalid:
|
|
return errors.New("invalid entry")
|
|
case title:
|
|
renderTTYTitle(&w, e)
|
|
case paragraph:
|
|
renderTTYParagraph(&w, e)
|
|
case list:
|
|
renderTTYList(&w, e)
|
|
case numberedList:
|
|
renderTTYNumberedList(&w, e)
|
|
case definitions:
|
|
renderTTYDefinitions(&w, e)
|
|
case numberedDefinitions:
|
|
renderTTYNumberedDefinitions(&w, e)
|
|
case table:
|
|
renderTTYTable(&w, e)
|
|
case code:
|
|
renderTTYCode(&w, e)
|
|
case syntax:
|
|
renderTTYSyntax(&w, e)
|
|
}
|
|
}
|
|
|
|
w.write("\n")
|
|
return w.err
|
|
}
|