1
0

test teletype formatting

This commit is contained in:
Arpad Ryszka 2025-10-10 15:47:36 +02:00
parent dda5dc6884
commit 57ef6b1267
13 changed files with 2162 additions and 138 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.cover

View File

@ -7,3 +7,19 @@ build: $(SOURCES)
fmt: $(SOURCES) fmt: $(SOURCES)
go fmt go fmt
check: $(SOURCES)
go test -count 1
.cover: $(SOURCES)
go test -count 1 -coverprofile .cover
cover: .cover
go tool cover -func .cover
showcover: .cover
go tool cover -html .cover
clean:
go clean
rm .cover

2
go.mod
View File

@ -1,3 +1,5 @@
module code.squareroundforest.org/arpio/textfmt module code.squareroundforest.org/arpio/textfmt
go 1.25.0 go 1.25.0
require code.squareroundforest.org/arpio/notation v0.0.0-20250826181910-5140794b16b2

2
go.sum Normal file
View File

@ -0,0 +1,2 @@
code.squareroundforest.org/arpio/notation v0.0.0-20250826181910-5140794b16b2 h1:S4mjQHL70CuzFg1AGkr0o0d+4M+ZWM0sbnlYq6f0b3I=
code.squareroundforest.org/arpio/notation v0.0.0-20250826181910-5140794b16b2/go.mod h1:ait4Fvg9o0+bq5hlxi9dAcPL5a+/sr33qsZPNpToMLY=

12
lib.go
View File

@ -59,7 +59,7 @@ type Entry struct {
rows []TableRow rows []TableRow
syntax SyntaxItem syntax SyntaxItem
indentFirst int indentFirst int
indentRest int indent int
wrapWidth int wrapWidth int
} }
@ -71,8 +71,8 @@ func Text(text string) Txt {
return Txt{text: text} return Txt{text: text}
} }
func Link(title, uri string) Txt { func Link(label, uri string) Txt {
return Txt{text: title, link: uri} return Txt{text: label, link: uri}
} }
func Bold(t Txt) Txt { func Bold(t Txt) Txt {
@ -184,11 +184,15 @@ func Choice(items ...SyntaxItem) SyntaxItem {
} }
func Syntax(items ...SyntaxItem) Entry { func Syntax(items ...SyntaxItem) Entry {
if len(items) == 1 {
return Entry{typ: syntax, syntax: items[0]}
}
return Entry{typ: syntax, syntax: Sequence(items...)} return Entry{typ: syntax, syntax: Sequence(items...)}
} }
func Indent(e Entry, first, rest int) Entry { func Indent(e Entry, first, rest int) Entry {
e.indentFirst, e.indentRest = first, rest e.indentFirst, e.indent = first, rest
return e return e
} }

10
print_test.go Normal file
View File

@ -0,0 +1,10 @@
package textfmt_test
import (
"code.squareroundforest.org/arpio/notation"
"testing"
)
func logBytes(t *testing.T, s string) {
t.Log(notation.Sprint([]byte(s)))
}

View File

@ -31,7 +31,12 @@ func columnWeights(cells [][]string) []int {
w := make([]int, len(cells[0])) w := make([]int, len(cells[0]))
for _, row := range cells { for _, row := range cells {
for i, cell := range row { for i, cell := range row {
w[i] += len([]rune(cell)) weight := len([]rune(cell))
if weight == 0 {
weight = 1
}
w[i] += weight
} }
} }

View File

@ -23,21 +23,35 @@ func escapeTeletype(s string) string {
return string(r) return string(r)
} }
func writeLines(w *writer, txt string, indentFirst, indentRest int) { func definitionNamesValues(d []DefinitionItem) ([]string, []string, error) {
lines := strings.Split(txt, "\n") var n, v []string
for i, l := range lines { for _, di := range d {
if i > 0 { name, err := ttyTextToString(di.name)
w.write("\n") if err != nil {
return nil, nil, err
} }
indent := indentFirst value, err := ttyTextToString(di.value)
if i > 0 { if err != nil {
indent = indentRest return nil, nil, err
} }
w.write(timesn(" ", indent)) n = append(n, name)
w.write(l) v = append(v, value)
} }
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
} }
func renderTTYText(w *writer, text Txt) { func renderTTYText(w *writer, text Txt) {
@ -71,31 +85,6 @@ func renderTTYText(w *writer, text Txt) {
w.write(text.text) 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) { func renderTTYTitle(w *writer, e Entry) {
w.write(timesn(" ", e.indentFirst)) w.write(timesn(" ", e.indentFirst))
renderTTYText(w, e.text) renderTTYText(w, e.text)
@ -105,16 +94,15 @@ func renderTTYParagraph(w *writer, e Entry) {
var txt string var txt string
txt, w.err = ttyTextToString(e.text) txt, w.err = ttyTextToString(e.text)
if e.wrapWidth > 0 { if e.wrapWidth > 0 {
txt = wrap(txt, e.wrapWidth, e.indentFirst, e.indentRest) txt = wrap(txt, e.wrapWidth, e.indentFirst, e.indent)
} }
writeLines(w, txt, e.indentFirst, e.indentRest) writeLines(w, txt, e.indentFirst, e.indent)
} }
func renderTTYList(w *writer, e Entry) { func renderTTYList(w *writer, e Entry) {
const bullet = "- " const bullet = "- "
indentFirst := e.indentFirst indent := e.indent + len(bullet)
indentRest := e.indentRest + len(bullet)
for i, item := range e.items { for i, item := range e.items {
if i > 0 { if i > 0 {
w.write("\n") w.write("\n")
@ -123,19 +111,18 @@ func renderTTYList(w *writer, e Entry) {
var txt string var txt string
txt, w.err = ttyTextToString(item.text) txt, w.err = ttyTextToString(item.text)
if e.wrapWidth > 0 { if e.wrapWidth > 0 {
txt = wrap(txt, e.wrapWidth-len(bullet), indentFirst, indentRest) txt = wrap(txt, e.wrapWidth-len(bullet), e.indentFirst, indent)
} }
w.write(timesn(" ", indentFirst)) w.write(timesn(" ", e.indentFirst))
w.write(bullet) w.write(bullet)
writeLines(w, txt, 0, indentRest) writeLines(w, txt, 0, indent)
} }
} }
func renderTTYNumberedList(w *writer, e Entry) { func renderTTYNumberedList(w *writer, e Entry) {
maxDigits := maxDigits(len(e.items)) maxDigits := numDigits(len(e.items))
indentFirst := e.indentFirst indent := e.indent + maxDigits + 2
indentRest := e.indentRest + maxDigits + 2
for i, item := range e.items { for i, item := range e.items {
if i > 0 { if i > 0 {
w.write("\n") w.write("\n")
@ -144,71 +131,89 @@ func renderTTYNumberedList(w *writer, e Entry) {
var txt string var txt string
txt, w.err = ttyTextToString(item.text) txt, w.err = ttyTextToString(item.text)
if e.wrapWidth > 0 { if e.wrapWidth > 0 {
txt = wrap(txt, e.wrapWidth-maxDigits-2, indentFirst, indentRest) txt = wrap(txt, e.wrapWidth-maxDigits-2, e.indentFirst, indent)
} }
w.write(timesn(" ", indentFirst)) w.write(timesn(" ", e.indentFirst))
w.write(fmt.Sprintf("%d. ", i)) w.write(padRight(fmt.Sprintf("%d.", i+1), maxDigits+2))
writeLines(w, txt, 0, indentRest) writeLines(w, txt, 0, indent)
} }
} }
func renderTTYDefinitions(w *writer, e Entry) { func renderTTYDefinitions(w *writer, e Entry) {
names, err := definitionNames(e.definitions) const (
bullet = "- "
sep = ": "
)
names, values, err := definitionNamesValues(e.definitions)
if err != nil { if err != nil {
w.err = err w.err = err
return return
} }
maxLength := maxLength(names) maxNameLength := maxLength(names)
indentFirst := e.indentFirst nameColWidth := maxNameLength + e.indentFirst + len(bullet) + len(sep)
indentRest := e.indentRest + maxLength + 4 valueWidth := e.wrapWidth
for i, def := range e.definitions { if valueWidth > 0 {
valueWidth -= nameColWidth
}
for i := range names {
if i > 0 { if i > 0 {
w.write("\n") w.write("\n")
} }
var value string w.write(timesn(" ", e.indentFirst), bullet, names[i], sep)
value, w.err = ttyTextToString(def.value) if valueWidth > 0 {
if e.wrapWidth > 0 { values[i] = wrap(values[i], valueWidth, 0, e.indent)
value = wrap(value, e.wrapWidth-maxLength-4, indentFirst, indentRest)
} }
name := names[i] writeLines(
w.write("- ") w,
w.write(name) values[i],
w.write(": ") maxNameLength-len([]rune(names[i])),
writeLines(w, value, 0, indentRest) nameColWidth+e.indent,
)
} }
} }
func renderTTYNumberedDefinitions(w *writer, e Entry) { func renderTTYNumberedDefinitions(w *writer, e Entry) {
maxDigits := maxDigits(len(e.definitions)) const (
names, err := definitionNames(e.definitions) dot = ". "
sep = ": "
)
names, values, err := definitionNamesValues(e.definitions)
if err != nil { if err != nil {
w.err = err w.err = err
return return
} }
maxLength := maxLength(names) maxDigits := numDigits(len(e.definitions))
indentFirst := e.indentFirst maxNameLength := maxLength(names)
indentRest := e.indentRest + maxLength + maxDigits + 4 nameColWidth := maxNameLength + e.indentFirst + maxDigits + len(dot) + len(sep)
for i, def := range e.definitions { valueWidth := e.wrapWidth
if valueWidth > 0 {
valueWidth -= nameColWidth
}
for i := range names {
if i > 0 { if i > 0 {
w.write("\n") w.write("\n")
} }
var value string w.write(timesn(" ", e.indentFirst), padRight(fmt.Sprintf("%d.", i+1), maxDigits+2), names[i], sep)
value, w.err = ttyTextToString(def.value) if valueWidth > 0 {
if e.wrapWidth > 0 { values[i] = wrap(values[i], valueWidth, 0, e.indent)
value = wrap(value, e.wrapWidth-maxLength-4, indentFirst, indentRest)
} }
name := names[i] writeLines(
w.write(fmt.Sprintf("%d. ", i)) w,
w.write(name) values[i],
w.write(": ") maxNameLength-len([]rune(names[i])),
writeLines(w, value, 0, indentRest) nameColWidth+e.indent,
)
} }
} }
@ -248,7 +253,7 @@ func renderTTYTable(w *writer, e Entry) {
totalSeparatorWidth := (len(cellTexts[0]) - 1) * 3 totalSeparatorWidth := (len(cellTexts[0]) - 1) * 3
if e.wrapWidth > 0 { if e.wrapWidth > 0 {
allocatedWidth := e.wrapWidth - e.indentFirst - totalSeparatorWidth allocatedWidth := e.wrapWidth - e.indent - totalSeparatorWidth
columnWeights := columnWeights(cellTexts) columnWeights := columnWeights(cellTexts)
targetColumnWidths := targetColumnWidths(allocatedWidth, columnWeights) targetColumnWidths := targetColumnWidths(allocatedWidth, columnWeights)
for i := range cellTexts { for i := range cellTexts {
@ -273,28 +278,52 @@ func renderTTYTable(w *writer, e Entry) {
} }
w.write("\n") w.write("\n")
w.write(timesn(sep, totalWidth)) w.write(timesn(" ", e.indent), timesn(sep, totalWidth))
w.write("\n") w.write("\n")
} }
lines := make([][]string, len(cellTexts[i]))
for j := range cellTexts[i] { for j := range cellTexts[i] {
if j > 0 { lines[j] = strings.Split(cellTexts[i][j], "\n")
w.write(" | ") }
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 {
w.write("\n")
} }
w.write(padRight(cellTexts[i][j], columnWidths[j])) for j := range lines {
if j == 0 {
w.write(timesn(" ", e.indent))
} else {
w.write(" | ")
}
var l string
if k < len(lines[j]) {
l = lines[j][k]
}
w.write(padRight(l, columnWidths[j]))
}
} }
} }
if hasHeader && len(cellTexts) == 1 { if hasHeader && len(cellTexts) == 1 {
w.write(timesn("=", totalWidth)) w.write("\n", timesn("=", totalWidth))
} }
} }
func renderTTYCode(w *writer, e Entry) { func renderTTYCode(w *writer, e Entry) {
var txt string e.text.text = escapeTeletype(e.text.text)
txt, w.err = ttyTextToString(e.text) writeLines(w, e.text.text, e.indent, e.indent)
writeLines(w, txt, e.indentFirst, e.indentFirst)
} }
func renderTTYMultiple(w *writer, s SyntaxItem) { func renderTTYMultiple(w *writer, s SyntaxItem) {
@ -346,7 +375,7 @@ func renderTTYChoice(w *writer, s SyntaxItem) {
w.write("(") w.write("(")
} }
for i, item := range s.sequence { for i, item := range s.choice {
if i > 0 { if i > 0 {
separator := "|" separator := "|"
if s.topLevel { if s.topLevel {
@ -435,6 +464,9 @@ func renderTeletype(out io.Writer, d Document) error {
} }
} }
w.write("\n") if len(d.entries) > 0 {
w.write("\n")
}
return w.err return w.err
} }

1901
teletype_test.go Normal file

File diff suppressed because it is too large Load Diff

38
text.go
View File

@ -7,7 +7,7 @@ func timesn(s string, n int) string {
return strings.Join(ss, s) return strings.Join(ss, s)
} }
func maxDigits(n int) int { func numDigits(n int) int {
if n == 0 { if n == 0 {
return 1 return 1
} }
@ -33,6 +33,42 @@ func maxLength(names []string) int {
} }
func padRight(s string, n int) string { func padRight(s string, n int) string {
if len(s) >= n {
return s
}
n -= len([]rune(s)) n -= len([]rune(s))
return s + timesn(" ", n) return s + timesn(" ", n)
} }
func singleLine(text string) string {
var l []string
p := strings.Split(text, "\n")
for _, part := range p {
part = strings.TrimSpace(part)
if part == "" {
continue
}
l = append(l, part)
}
return strings.Join(l, " ")
}
func writeLines(w *writer, txt string, indentFirst, indent int) {
lines := strings.Split(txt, "\n")
for i, l := range lines {
if i > 0 {
w.write("\n")
}
ind := indentFirst
if i > 0 {
ind = indent
}
w.write(timesn(" ", ind))
w.write(l)
}
}

51
wrap.go
View File

@ -16,60 +16,37 @@ func getWords(text string) []string {
return words return words
} }
func lineLength(words []string) int {
if len(words) == 0 {
return 0
}
var l int
for _, w := range words {
r := []rune(w)
l += len(r)
}
return l + len(words) - 1
}
func singleLine(text string) string {
var l []string
p := strings.Split(text, "\n")
for _, part := range p {
part = strings.TrimSpace(part)
if part == "" {
continue
}
l = append(l, part)
}
return strings.Join(l, " ")
}
func wrap(text string, width, firstIndent, restIndent int) string { func wrap(text string, width, firstIndent, restIndent int) string {
var ( var (
lines []string lines []string
currentLine []string currentLine []string
currentLen int lineLen int
) )
words := getWords(text) words := getWords(text)
for _, w := range words { for _, w := range words {
if len(currentLine) == 0 {
currentLine = []string{w}
lineLen = len(w)
continue
}
maxw := width - restIndent maxw := width - restIndent
if len(lines) == 0 { if len(lines) == 0 {
maxw = width - firstIndent maxw = width - firstIndent
} }
currentLine = append(currentLine, w) if lineLen+1+len(w) > maxw {
if lineLength(currentLine) > maxw {
currentLine = currentLine[:len(currentLine)-1]
lines = append(lines, strings.Join(currentLine, " ")) lines = append(lines, strings.Join(currentLine, " "))
currentLine = []string{w} currentLine = []string{w}
lineLen = len(w)
continue
} }
currentLine = append(currentLine, w)
lineLen += 1 + len(w)
} }
if len(currentLine) > 0 { lines = append(lines, strings.Join(currentLine, " "))
lines = append(lines, strings.Join(currentLine, " "))
}
return strings.Join(lines, "\n") return strings.Join(lines, "\n")
} }

View File

@ -7,12 +7,14 @@ type writer struct {
err error err error
} }
func (w *writer) write(s string) { func (w *writer) write(s ...string) {
if w.err != nil { for _, si := range s {
return if w.err != nil {
} return
}
if _, err := w.w.Write([]byte(s)); err != nil { if _, err := w.w.Write([]byte(si)); err != nil {
w.err = err w.err = err
}
} }
} }

36
writer_test.go Normal file
View File

@ -0,0 +1,36 @@
package textfmt_test
import (
"errors"
"io"
)
type failingWriter struct {
out io.Writer
failAfter int
err error
}
func (w *failingWriter) Write(p []byte) (int, error) {
if w.err != nil {
return 0, w.err
}
if w.failAfter <= len(p) {
p = p[:w.failAfter]
}
if len(p) > 0 && w.out != nil {
if n, err := w.out.Write(p); err != nil {
w.err = err
return n, w.err
}
}
w.failAfter -= len(p)
if w.failAfter == 0 {
w.err = errors.New("test write error")
}
return len(p), w.err
}