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)
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
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
syntax SyntaxItem
indentFirst int
indentRest int
indent int
wrapWidth int
}
@ -71,8 +71,8 @@ func Text(text string) Txt {
return Txt{text: text}
}
func Link(title, uri string) Txt {
return Txt{text: title, link: uri}
func Link(label, uri string) Txt {
return Txt{text: label, link: uri}
}
func Bold(t Txt) Txt {
@ -184,11 +184,15 @@ func Choice(items ...SyntaxItem) SyntaxItem {
}
func Syntax(items ...SyntaxItem) Entry {
if len(items) == 1 {
return Entry{typ: syntax, syntax: items[0]}
}
return Entry{typ: syntax, syntax: Sequence(items...)}
}
func Indent(e Entry, first, rest int) Entry {
e.indentFirst, e.indentRest = first, rest
e.indentFirst, e.indent = first, rest
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]))
for _, row := range cells {
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)
}
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")
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
}
indent := indentFirst
if i > 0 {
indent = indentRest
value, err := ttyTextToString(di.value)
if err != nil {
return nil, nil, err
}
w.write(timesn(" ", indent))
w.write(l)
n = append(n, name)
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) {
@ -71,31 +85,6 @@ func renderTTYText(w *writer, text Txt) {
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)
@ -105,16 +94,15 @@ 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)
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) {
const bullet = "- "
indentFirst := e.indentFirst
indentRest := e.indentRest + len(bullet)
indent := e.indent + len(bullet)
for i, item := range e.items {
if i > 0 {
w.write("\n")
@ -123,19 +111,18 @@ func renderTTYList(w *writer, e Entry) {
var txt string
txt, w.err = ttyTextToString(item.text)
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)
writeLines(w, txt, 0, indentRest)
writeLines(w, txt, 0, indent)
}
}
func renderTTYNumberedList(w *writer, e Entry) {
maxDigits := maxDigits(len(e.items))
indentFirst := e.indentFirst
indentRest := e.indentRest + maxDigits + 2
maxDigits := numDigits(len(e.items))
indent := e.indent + maxDigits + 2
for i, item := range e.items {
if i > 0 {
w.write("\n")
@ -144,71 +131,89 @@ func renderTTYNumberedList(w *writer, e Entry) {
var txt string
txt, w.err = ttyTextToString(item.text)
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(fmt.Sprintf("%d. ", i))
writeLines(w, txt, 0, indentRest)
w.write(timesn(" ", e.indentFirst))
w.write(padRight(fmt.Sprintf("%d.", i+1), maxDigits+2))
writeLines(w, txt, 0, indent)
}
}
func renderTTYDefinitions(w *writer, e Entry) {
names, err := definitionNames(e.definitions)
const (
bullet = "- "
sep = ": "
)
names, values, err := definitionNamesValues(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 {
maxNameLength := maxLength(names)
nameColWidth := maxNameLength + e.indentFirst + len(bullet) + len(sep)
valueWidth := e.wrapWidth
if valueWidth > 0 {
valueWidth -= nameColWidth
}
for i := range names {
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)
w.write(timesn(" ", e.indentFirst), bullet, names[i], sep)
if valueWidth > 0 {
values[i] = wrap(values[i], valueWidth, 0, e.indent)
}
name := names[i]
w.write("- ")
w.write(name)
w.write(": ")
writeLines(w, value, 0, indentRest)
writeLines(
w,
values[i],
maxNameLength-len([]rune(names[i])),
nameColWidth+e.indent,
)
}
}
func renderTTYNumberedDefinitions(w *writer, e Entry) {
maxDigits := maxDigits(len(e.definitions))
names, err := definitionNames(e.definitions)
const (
dot = ". "
sep = ": "
)
names, values, err := definitionNamesValues(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 {
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 {
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)
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)
}
name := names[i]
w.write(fmt.Sprintf("%d. ", i))
w.write(name)
w.write(": ")
writeLines(w, value, 0, indentRest)
writeLines(
w,
values[i],
maxNameLength-len([]rune(names[i])),
nameColWidth+e.indent,
)
}
}
@ -248,7 +253,7 @@ func renderTTYTable(w *writer, e Entry) {
totalSeparatorWidth := (len(cellTexts[0]) - 1) * 3
if e.wrapWidth > 0 {
allocatedWidth := e.wrapWidth - e.indentFirst - totalSeparatorWidth
allocatedWidth := e.wrapWidth - e.indent - totalSeparatorWidth
columnWeights := columnWeights(cellTexts)
targetColumnWidths := targetColumnWidths(allocatedWidth, columnWeights)
for i := range cellTexts {
@ -273,28 +278,52 @@ func renderTTYTable(w *writer, e Entry) {
}
w.write("\n")
w.write(timesn(sep, totalWidth))
w.write(timesn(" ", e.indent), timesn(sep, totalWidth))
w.write("\n")
}
lines := make([][]string, len(cellTexts[i]))
for j := range cellTexts[i] {
if j > 0 {
w.write(" | ")
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 {
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 {
w.write(timesn("=", totalWidth))
w.write("\n", timesn("=", totalWidth))
}
}
func renderTTYCode(w *writer, e Entry) {
var txt string
txt, w.err = ttyTextToString(e.text)
writeLines(w, txt, e.indentFirst, e.indentFirst)
e.text.text = escapeTeletype(e.text.text)
writeLines(w, e.text.text, e.indent, e.indent)
}
func renderTTYMultiple(w *writer, s SyntaxItem) {
@ -346,7 +375,7 @@ func renderTTYChoice(w *writer, s SyntaxItem) {
w.write("(")
}
for i, item := range s.sequence {
for i, item := range s.choice {
if i > 0 {
separator := "|"
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
}

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)
}
func maxDigits(n int) int {
func numDigits(n int) int {
if n == 0 {
return 1
}
@ -33,6 +33,42 @@ func maxLength(names []string) int {
}
func padRight(s string, n int) string {
if len(s) >= n {
return s
}
n -= len([]rune(s))
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
}
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 {
var (
lines []string
currentLine []string
currentLen int
lineLen int
)
words := getWords(text)
for _, w := range words {
if len(currentLine) == 0 {
currentLine = []string{w}
lineLen = len(w)
continue
}
maxw := width - restIndent
if len(lines) == 0 {
maxw = width - firstIndent
}
currentLine = append(currentLine, w)
if lineLength(currentLine) > maxw {
currentLine = currentLine[:len(currentLine)-1]
if lineLen+1+len(w) > maxw {
lines = append(lines, strings.Join(currentLine, " "))
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")
}

View File

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