1
0
textfmt/teletype.go

473 lines
8.4 KiB
Go
Raw Normal View History

2025-09-11 21:16:09 +02:00
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)
}
2025-10-10 15:47:36 +02:00
func definitionNamesValues(d []DefinitionItem) ([]string, []string, error) {
var n, v []string
for _, di := range d {
name, err := ttyTextToString(di.name)
if err != nil {
return nil, nil, err
2025-09-11 21:16:09 +02:00
}
2025-10-10 15:47:36 +02:00
value, err := ttyTextToString(di.value)
if err != nil {
return nil, nil, err
2025-09-11 21:16:09 +02:00
}
2025-10-10 15:47:36 +02:00
n = append(n, name)
v = append(v, value)
2025-09-11 21:16:09 +02:00
}
2025-10-10 15:47:36 +02:00
return n, v, nil
}
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
2025-09-11 21:16:09 +02:00
}
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 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 {
2025-10-10 15:47:36 +02:00
txt = wrap(txt, e.wrapWidth, e.indentFirst, e.indent)
2025-09-11 21:16:09 +02:00
}
2025-10-10 15:47:36 +02:00
writeLines(w, txt, e.indentFirst, e.indent)
2025-09-11 21:16:09 +02:00
}
func renderTTYList(w *writer, e Entry) {
const bullet = "- "
2025-10-10 15:47:36 +02:00
indent := e.indent + len(bullet)
2025-09-11 21:16:09 +02:00
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 {
2025-10-10 15:47:36 +02:00
txt = wrap(txt, e.wrapWidth-len(bullet), e.indentFirst, indent)
2025-09-11 21:16:09 +02:00
}
2025-10-10 15:47:36 +02:00
w.write(timesn(" ", e.indentFirst))
2025-09-11 21:16:09 +02:00
w.write(bullet)
2025-10-10 15:47:36 +02:00
writeLines(w, txt, 0, indent)
2025-09-11 21:16:09 +02:00
}
}
func renderTTYNumberedList(w *writer, e Entry) {
2025-10-10 15:47:36 +02:00
maxDigits := numDigits(len(e.items))
indent := e.indent + maxDigits + 2
2025-09-11 21:16:09 +02:00
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 {
2025-10-10 15:47:36 +02:00
txt = wrap(txt, e.wrapWidth-maxDigits-2, e.indentFirst, indent)
2025-09-11 21:16:09 +02:00
}
2025-10-10 15:47:36 +02:00
w.write(timesn(" ", e.indentFirst))
w.write(padRight(fmt.Sprintf("%d.", i+1), maxDigits+2))
writeLines(w, txt, 0, indent)
2025-09-11 21:16:09 +02:00
}
}
func renderTTYDefinitions(w *writer, e Entry) {
2025-10-10 15:47:36 +02:00
const (
bullet = "- "
sep = ": "
)
names, values, err := definitionNamesValues(e.definitions)
2025-09-11 21:16:09 +02:00
if err != nil {
w.err = err
return
}
2025-10-10 15:47:36 +02:00
maxNameLength := maxLength(names)
nameColWidth := maxNameLength + e.indentFirst + len(bullet) + len(sep)
valueWidth := e.wrapWidth
if valueWidth > 0 {
valueWidth -= nameColWidth
}
for i := range names {
2025-09-11 21:16:09 +02:00
if i > 0 {
w.write("\n")
}
2025-10-10 15:47:36 +02:00
w.write(timesn(" ", e.indentFirst), bullet, names[i], sep)
if valueWidth > 0 {
values[i] = wrap(values[i], valueWidth, 0, e.indent)
2025-09-11 21:16:09 +02:00
}
2025-10-10 15:47:36 +02:00
writeLines(
w,
values[i],
maxNameLength-len([]rune(names[i])),
nameColWidth+e.indent,
)
2025-09-11 21:16:09 +02:00
}
}
func renderTTYNumberedDefinitions(w *writer, e Entry) {
2025-10-10 15:47:36 +02:00
const (
dot = ". "
sep = ": "
)
names, values, err := definitionNamesValues(e.definitions)
2025-09-11 21:16:09 +02:00
if err != nil {
w.err = err
return
}
2025-10-10 15:47:36 +02:00
maxDigits := numDigits(len(e.definitions))
maxNameLength := maxLength(names)
nameColWidth := maxNameLength + e.indentFirst + maxDigits + len(dot) + len(sep)
valueWidth := e.wrapWidth
if valueWidth > 0 {
valueWidth -= nameColWidth
}
for i := range names {
2025-09-11 21:16:09 +02:00
if i > 0 {
w.write("\n")
}
2025-10-10 15:47:36 +02:00
w.write(timesn(" ", e.indentFirst), padRight(fmt.Sprintf("%d.", i+1), maxDigits+2), names[i], sep)
if valueWidth > 0 {
values[i] = wrap(values[i], valueWidth, 0, e.indent)
2025-09-11 21:16:09 +02:00
}
2025-10-10 15:47:36 +02:00
writeLines(
w,
values[i],
maxNameLength-len([]rune(names[i])),
nameColWidth+e.indent,
)
2025-09-11 21:16:09 +02:00
}
}
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 {
2025-10-10 15:47:36 +02:00
allocatedWidth := e.wrapWidth - e.indent - totalSeparatorWidth
2025-09-11 21:16:09 +02:00
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")
2025-10-10 15:47:36 +02:00
w.write(timesn(" ", e.indent), timesn(sep, totalWidth))
2025-09-11 21:16:09 +02:00
w.write("\n")
}
2025-10-10 15:47:36 +02:00
lines := make([][]string, len(cellTexts[i]))
2025-09-11 21:16:09 +02:00
for j := range cellTexts[i] {
2025-10-10 15:47:36 +02:00
lines[j] = strings.Split(cellTexts[i][j], "\n")
}
var maxLines int
for j := range lines {
if len(lines[j]) > maxLines {
maxLines = len(lines[j])
2025-09-11 21:16:09 +02:00
}
2025-10-10 15:47:36 +02:00
}
for k := 0; k < maxLines; k++ {
if k > 0 {
w.write("\n")
}
for j := range lines {
if j == 0 {
w.write(timesn(" ", e.indent))
} else {
w.write(" | ")
}
2025-09-11 21:16:09 +02:00
2025-10-10 15:47:36 +02:00
var l string
if k < len(lines[j]) {
l = lines[j][k]
}
w.write(padRight(l, columnWidths[j]))
}
2025-09-11 21:16:09 +02:00
}
}
if hasHeader && len(cellTexts) == 1 {
2025-10-10 15:47:36 +02:00
w.write("\n", timesn("=", totalWidth))
2025-09-11 21:16:09 +02:00
}
}
func renderTTYCode(w *writer, e Entry) {
2025-10-10 15:47:36 +02:00
e.text.text = escapeTeletype(e.text.text)
writeLines(w, e.text.text, e.indent, e.indent)
2025-09-11 21:16:09 +02:00
}
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("(")
}
2025-10-10 15:47:36 +02:00
for i, item := range s.choice {
2025-09-11 21:16:09 +02:00
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)
}
}
2025-10-10 15:47:36 +02:00
if len(d.entries) > 0 {
w.write("\n")
}
2025-09-11 21:16:09 +02:00
return w.err
}