test teletype formatting
This commit is contained in:
parent
dda5dc6884
commit
57ef6b1267
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
.cover
|
||||
16
Makefile
16
Makefile
@ -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
2
go.mod
@ -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
2
go.sum
Normal 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
12
lib.go
@ -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
10
print_test.go
Normal 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)))
|
||||
}
|
||||
7
table.go
7
table.go
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
206
teletype.go
206
teletype.go
@ -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 {
|
||||
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")
|
||||
}
|
||||
|
||||
for j := range lines {
|
||||
if j == 0 {
|
||||
w.write(timesn(" ", e.indent))
|
||||
} else {
|
||||
w.write(" | ")
|
||||
}
|
||||
|
||||
w.write(padRight(cellTexts[i][j], columnWidths[j]))
|
||||
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 {
|
||||
}
|
||||
}
|
||||
|
||||
if len(d.entries) > 0 {
|
||||
w.write("\n")
|
||||
}
|
||||
|
||||
return w.err
|
||||
}
|
||||
|
||||
1901
teletype_test.go
Normal file
1901
teletype_test.go
Normal file
File diff suppressed because it is too large
Load Diff
38
text.go
38
text.go
@ -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
51
wrap.go
@ -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, " "))
|
||||
}
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
@ -7,12 +7,14 @@ type writer struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (w *writer) write(s string) {
|
||||
func (w *writer) write(s ...string) {
|
||||
for _, si := range s {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
36
writer_test.go
Normal file
36
writer_test.go
Normal 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
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user