diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ebf0f2e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.cover diff --git a/Makefile b/Makefile index 8c7cbe3..7704820 100644 --- a/Makefile +++ b/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 diff --git a/go.mod b/go.mod index e0efcfa..d03268c 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a66f3d0 --- /dev/null +++ b/go.sum @@ -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= diff --git a/lib.go b/lib.go index ccc0ce2..fdb7148 100644 --- a/lib.go +++ b/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 } diff --git a/print_test.go b/print_test.go new file mode 100644 index 0000000..5da8ae3 --- /dev/null +++ b/print_test.go @@ -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))) +} diff --git a/table.go b/table.go index 201a315..633cb72 100644 --- a/table.go +++ b/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 } } diff --git a/teletype.go b/teletype.go index cf2523f..bbeffde 100644 --- a/teletype.go +++ b/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 { - 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 } diff --git a/teletype_test.go b/teletype_test.go new file mode 100644 index 0000000..f8a8113 --- /dev/null +++ b/teletype_test.go @@ -0,0 +1,1901 @@ +package textfmt_test + +import ( + "bytes" + "code.squareroundforest.org/arpio/textfmt" + "testing" +) + +func TestTeletype(t *testing.T) { + t.Run("invalid", func(t *testing.T) { + var b bytes.Buffer + if err := textfmt.Teletype(&b, textfmt.Doc(textfmt.Entry{})); err == nil { + t.Fatal("failed to fail") + } + }) + + t.Run("empty", func(t *testing.T) { + var b bytes.Buffer + if err := textfmt.Teletype(&b, textfmt.Doc()); err != nil { + t.Fatal(err) + } + + if b.String() != "" { + t.Fatal(b.String()) + } + }) + + t.Run("example", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Title(0, "Example Text"), + + textfmt.Wrap( + textfmt.Indent( + textfmt.Paragraph(textfmt.Text("Below you can find some test text, with various text items.")), + 8, + 0, + ), + 30, + ), + + textfmt.Title(1, "Document syntax:"), + + textfmt.Indent( + textfmt.Syntax( + textfmt.Symbol("textfmt.Doc"), + textfmt.Symbol("("), + textfmt.ZeroOrMore(textfmt.Symbol("Entry")), + textfmt.Symbol(")"), + ), + 8, + 0, + ), + + textfmt.Title(1, "Entries:"), + + textfmt.Paragraph(textfmt.Text("textfmt supports the following entries:")), + + textfmt.List( + textfmt.Item(textfmt.Text("CodeBlock")), + textfmt.Item(textfmt.Text("DefinitionList")), + textfmt.Item(textfmt.Text("List")), + textfmt.Item(textfmt.Text("NumberedDefinitionList")), + textfmt.Item(textfmt.Text("NumberedList")), + textfmt.Item(textfmt.Text("Paragraph")), + textfmt.Item(textfmt.Text("Syntax")), + textfmt.Item(textfmt.Text("Table")), + textfmt.Item(textfmt.Text("Title")), + ), + + textfmt.Title(1, "Entry explanations:"), + + textfmt.Wrap( + textfmt.DefinitionList( + textfmt.Definition( + textfmt.Text("CodeBlock"), + textfmt.Text("a multiline block of code"), + ), + textfmt.Definition( + textfmt.Text("DefinitionList"), + textfmt.Text("a list of definitions like this one"), + ), + textfmt.Definition( + textfmt.Text("List"), + textfmt.Text("a list of items"), + ), + textfmt.Definition( + textfmt.Text("NumberedDefinitionList"), + textfmt.Text("numbered definitions"), + ), + textfmt.Definition( + textfmt.Text("NumberedList"), + textfmt.Text("numbered list"), + ), + textfmt.Definition( + textfmt.Text("Paragraph"), + textfmt.Text("paragraph of text"), + ), + textfmt.Definition( + textfmt.Text("Syntax"), + textfmt.Text("a syntax expression"), + ), + textfmt.Definition( + textfmt.Text("Table"), + textfmt.Text("a table"), + ), + textfmt.Definition( + textfmt.Text("Title"), + textfmt.Text("a title"), + ), + ), + 48, + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = `Example Text + + Below you can find +some test text, with various +text items. + +Document syntax: + +textfmt.Doc ( [Entry]... ) + +Entries: + +textfmt supports the following entries: + +- CodeBlock +- DefinitionList +- List +- NumberedDefinitionList +- NumberedList +- Paragraph +- Syntax +- Table +- Title + +Entry explanations: + +- CodeBlock: a multiline block of + code +- DefinitionList: a list of definitions + like this one +- List: a list of items +- NumberedDefinitionList: numbered definitions +- NumberedList: numbered list +- Paragraph: paragraph of text +- Syntax: a syntax expression +- Table: a table +- Title: a title +` + + if b.String() != expect { + t.Log(b.String()) + t.Log(expect) + logBytes(t, b.String()) + logBytes(t, expect) + t.Fatal() + } + }) + + t.Run("escape", func(t *testing.T) { + var b bytes.Buffer + doc := textfmt.Doc(textfmt.Paragraph(textfmt.Text("Some sample text...\x00\nwith invalid chars."))) + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = "Some sample text...ยท with invalid chars.\n" + if b.String() != expect { + logBytes(t, b.String()) + logBytes(t, expect) + t.Fatal(b.String()) + } + }) + + t.Run("styling ignored", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Paragraph( + textfmt.Cat( + textfmt.Italic(textfmt.Text("Some sample text... with")), + textfmt.Bold(textfmt.Text("some")), + textfmt.Italic(textfmt.Text("styling.")), + ), + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "Some sample text... with some styling.\n" { + t.Fatal(b.String()) + } + }) + + t.Run("single line by default", func(t *testing.T) { + var b bytes.Buffer + doc := textfmt.Doc(textfmt.Paragraph(textfmt.Text("Some sample text...\n\non multiple lines."))) + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "Some sample text... on multiple lines.\n" { + t.Fatal(b.String()) + } + }) + + t.Run("wrap", func(t *testing.T) { + t.Run("unindented", func(t *testing.T) { + var b bytes.Buffer + doc := textfmt.Doc( + textfmt.Wrap(textfmt.Paragraph(textfmt.Text("Some sample text...\n on multiple lines.")), 15), + ) + + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "Some sample\ntext... on\nmultiple lines.\n" { + t.Fatal(b.String()) + } + }) + + t.Run("indented", func(t *testing.T) { + var b bytes.Buffer + doc := textfmt.Doc( + textfmt.Indent( + textfmt.Wrap(textfmt.Paragraph(textfmt.Text("Some sample text...\n on multiple lines.")), 15), + 4, + 2, + ), + ) + + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != " Some sample\n text... on\n multiple\n lines.\n" { + t.Fatal(b.String()) + } + }) + + }) + + t.Run("indent", func(t *testing.T) { + t.Run("indent uniform", func(t *testing.T) { + var b bytes.Buffer + doc := textfmt.Doc( + textfmt.Indent( + textfmt.Wrap(textfmt.Paragraph(textfmt.Text("Some sample text...\non multiple lines.")), 15), + 2, + 2, + ), + ) + + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != " Some sample\n text... on\n multiple\n lines.\n" { + t.Fatal(b.String()) + } + }) + + t.Run("indent first in", func(t *testing.T) { + var b bytes.Buffer + doc := textfmt.Doc( + textfmt.Indent( + textfmt.Wrap(textfmt.Paragraph(textfmt.Text("Some sample text...\non multiple lines.")), 15), + 4, + 2, + ), + ) + + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != " Some sample\n text... on\n multiple\n lines.\n" { + t.Fatal(b.String()) + } + }) + + t.Run("indent first out", func(t *testing.T) { + var b bytes.Buffer + doc := textfmt.Doc( + textfmt.Indent( + textfmt.Wrap(textfmt.Paragraph(textfmt.Text("Some sample text...\non multiple lines.")), 15), + 0, + 2, + ), + ) + + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "Some sample\n text... on\n multiple\n lines.\n" { + t.Fatal(b.String()) + } + }) + }) + + t.Run("failing writer", func(t *testing.T) { + w := &failingWriter{failAfter: 15} + doc := textfmt.Doc( + textfmt.Paragraph(textfmt.Text("Some sample text...\non multiple lines.")), + ) + + if err := textfmt.Teletype(w, doc); err == nil { + t.Fatal("failed to fail") + } + }) + + t.Run("concatenate", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Paragraph( + textfmt.Cat( + textfmt.Text("Text from"), + textfmt.Text("multiple"), + textfmt.Text("pieces."), + ), + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "Text from multiple pieces.\n" { + t.Fatal(b.String()) + } + }) + + t.Run("link", func(t *testing.T) { + t.Run("without label", func(t *testing.T) { + var b bytes.Buffer + doc := textfmt.Doc(textfmt.Paragraph(textfmt.Link("", "https://sqrndfst.org"))) + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "https://sqrndfst.org\n" { + t.Fatal(b.String()) + } + }) + + t.Run("with label", func(t *testing.T) { + var b bytes.Buffer + doc := textfmt.Doc(textfmt.Paragraph(textfmt.Link("a link", "https://sqrndfst.org"))) + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "a link (https://sqrndfst.org)\n" { + t.Fatal(b.String()) + } + }) + }) + + t.Run("title", func(t *testing.T) { + var b bytes.Buffer + doc := textfmt.Doc(textfmt.Title(0, "This is a title")) + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "This is a title\n" { + t.Fatal(b.String()) + } + }) + + t.Run("paragraph", func(t *testing.T) { + t.Run("unwrapped", func(t *testing.T) { + var b bytes.Buffer + doc := textfmt.Doc(textfmt.Paragraph(textfmt.Text("This is a paragraph."))) + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "This is a paragraph.\n" { + t.Fatal(b.String()) + } + }) + + t.Run("wrapped", func(t *testing.T) { + var b bytes.Buffer + doc := textfmt.Doc( + textfmt.Wrap(textfmt.Paragraph(textfmt.Text("This is a paragraph.")), 8), + ) + + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "This is\na\nparagraph.\n" { + t.Fatal(b.String()) + } + }) + + t.Run("indent without wrapping", func(t *testing.T) { + var b bytes.Buffer + doc := textfmt.Doc( + textfmt.Indent(textfmt.Paragraph(textfmt.Text("This is a paragraph.")), 4, 0), + ) + + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != " This is a paragraph.\n" { + t.Fatal(b.String()) + } + }) + + t.Run("indent in", func(t *testing.T) { + var b bytes.Buffer + doc := textfmt.Doc( + textfmt.Indent( + textfmt.Wrap(textfmt.Paragraph(textfmt.Text("This is a paragraph.")), 12), + 4, + 0, + ), + ) + + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != " This is\na paragraph.\n" { + t.Fatal(b.String()) + } + }) + + t.Run("indent out", func(t *testing.T) { + var b bytes.Buffer + doc := textfmt.Doc( + textfmt.Indent( + textfmt.Wrap(textfmt.Paragraph(textfmt.Text("This is a paragraph.")), 12), + 0, + 4, + ), + ) + + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "This is a\n paragraph.\n" { + t.Fatal(b.String()) + } + }) + }) + + t.Run("list", func(t *testing.T) { + t.Run("unwrapped", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.List( + textfmt.Item(textfmt.Text("this is an item")), + textfmt.Item(textfmt.Text("this is another item")), + textfmt.Item(textfmt.Text("this is a third item")), + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = `- this is an item +- this is another item +- this is a third item +` + + if b.String() != expect { + t.Fatal(b.String()) + } + }) + + t.Run("wrapped", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Wrap( + textfmt.List( + textfmt.Item(textfmt.Text("this is an item")), + textfmt.Item(textfmt.Text("this is another item")), + textfmt.Item(textfmt.Text("this is a third item")), + ), + 12, + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = `- this is an + item +- this is + another + item +- this is a + third + item +` + + if b.String() != expect { + t.Fatal(b.String()) + } + }) + + t.Run("indent without wrapping", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Indent( + textfmt.List( + textfmt.Item(textfmt.Text("this is an item")), + textfmt.Item(textfmt.Text("this is another item")), + textfmt.Item(textfmt.Text("this is a third item")), + ), + 4, + 0, + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = ` - this is an item + - this is another item + - this is a third item +` + + if b.String() != expect { + t.Fatal(b.String()) + } + }) + + t.Run("indent in", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Indent( + textfmt.Wrap( + textfmt.List( + textfmt.Item(textfmt.Text("this is an item")), + textfmt.Item(textfmt.Text("this is another item")), + textfmt.Item(textfmt.Text("this is a third item")), + ), + 18, + ), + 4, + -2, + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = ` - this is an +item + - this is +another item + - this is a +third item +` + + if b.String() != expect { + t.Fatal(b.String()) + } + }) + + t.Run("indent out", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Indent( + textfmt.Wrap( + textfmt.List( + textfmt.Item(textfmt.Text("this is an item")), + textfmt.Item(textfmt.Text("this is another item")), + textfmt.Item(textfmt.Text("this is a third item")), + ), + 18, + ), + 0, + 2, + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = `- this is an item +- this is another + item +- this is a third + item +` + + if b.String() != expect { + t.Fatal(b.String()) + } + }) + }) + + t.Run("numbered list", func(t *testing.T) { + t.Run("unwrapped", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.NumberedList( + textfmt.Item(textfmt.Text("this is an item")), + textfmt.Item(textfmt.Text("this is another item")), + textfmt.Item(textfmt.Text("this is a third item")), + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = `1. this is an item +2. this is another item +3. this is a third item +` + + if b.String() != expect { + t.Fatal(b.String()) + } + }) + + t.Run("wrapped", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Wrap( + textfmt.NumberedList( + textfmt.Item(textfmt.Text("this is an item")), + textfmt.Item(textfmt.Text("this is another item")), + textfmt.Item(textfmt.Text("this is a third item")), + ), + 15, + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = `1. this is an + item +2. this is + another + item +3. this is a + third + item +` + + if b.String() != expect { + t.Fatal(b.String()) + } + }) + + t.Run("indent without wrapping", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Indent( + textfmt.NumberedList( + textfmt.Item(textfmt.Text("this is an item")), + textfmt.Item(textfmt.Text("this is another item")), + textfmt.Item(textfmt.Text("this is a third item")), + ), + 4, + 0, + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = ` 1. this is an item + 2. this is another item + 3. this is a third item +` + + if b.String() != expect { + t.Fatal(b.String()) + } + }) + + t.Run("indent in", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Indent( + textfmt.Wrap( + textfmt.NumberedList( + textfmt.Item(textfmt.Text("this is an item")), + textfmt.Item(textfmt.Text("this is another item")), + textfmt.Item(textfmt.Text("this is a third item")), + ), + 21, + ), + 4, + -3, + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = ` 1. this is an +item + 2. this is +another item + 3. this is a +third item +` + + if b.String() != expect { + t.Fatal(b.String()) + } + }) + + t.Run("indent out", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Indent( + textfmt.Wrap( + textfmt.NumberedList( + textfmt.Item(textfmt.Text("this is an item")), + textfmt.Item(textfmt.Text("this is another item")), + textfmt.Item(textfmt.Text("this is a third item")), + ), + 21, + ), + 0, + 3, + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = `1. this is an item +2. this is another + item +3. this is a third + item +` + + if b.String() != expect { + t.Fatal(b.String()) + } + }) + + t.Run("long numbered list", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.NumberedList( + textfmt.Item(textfmt.Text("this is an item")), + textfmt.Item(textfmt.Text("this is another item")), + textfmt.Item(textfmt.Text("this is the third item")), + textfmt.Item(textfmt.Text("this is the fourth item")), + textfmt.Item(textfmt.Text("this is the fifth item")), + textfmt.Item(textfmt.Text("this is the sixth item")), + textfmt.Item(textfmt.Text("this is the seventh item")), + textfmt.Item(textfmt.Text("this is the eighth item")), + textfmt.Item(textfmt.Text("this is the nineth item")), + textfmt.Item(textfmt.Text("this is the tenth item")), + textfmt.Item(textfmt.Text("this is the eleventh item")), + textfmt.Item(textfmt.Text("this is the twelveth item")), + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = `1. this is an item +2. this is another item +3. this is the third item +4. this is the fourth item +5. this is the fifth item +6. this is the sixth item +7. this is the seventh item +8. this is the eighth item +9. this is the nineth item +10. this is the tenth item +11. this is the eleventh item +12. this is the twelveth item +` + + if b.String() != expect { + t.Fatal(b.String()) + } + }) + }) + + t.Run("definition list", func(t *testing.T) { + t.Run("unwrapped", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.DefinitionList( + textfmt.Definition(textfmt.Text("red"), textfmt.Text("looks like strawberry")), + textfmt.Definition(textfmt.Text("green"), textfmt.Text("looks like grass")), + textfmt.Definition(textfmt.Text("blue"), textfmt.Text("looks like sky")), + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = `- red: looks like strawberry +- green: looks like grass +- blue: looks like sky +` + + if b.String() != expect { + t.Fatal(b.String()) + } + }) + + t.Run("wrapped", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Wrap( + textfmt.DefinitionList( + textfmt.Definition(textfmt.Text("red"), textfmt.Text("looks like strawberry")), + textfmt.Definition(textfmt.Text("green"), textfmt.Text("looks like grass")), + textfmt.Definition(textfmt.Text("blue"), textfmt.Text("looks like sky")), + ), + 24, + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = `- red: looks like + strawberry +- green: looks like + grass +- blue: looks like sky +` + + if b.String() != expect { + t.Fatal(b.String()) + } + }) + + t.Run("indent without wrapping", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Indent( + textfmt.DefinitionList( + textfmt.Definition(textfmt.Text("red"), textfmt.Text("looks like strawberry")), + textfmt.Definition(textfmt.Text("green"), textfmt.Text("looks like grass")), + textfmt.Definition(textfmt.Text("blue"), textfmt.Text("looks like sky")), + ), + 4, + 0, + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = ` - red: looks like strawberry + - green: looks like grass + - blue: looks like sky +` + + if b.String() != expect { + t.Fatal(b.String()) + } + }) + + t.Run("indent in", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Indent( + textfmt.Wrap( + textfmt.DefinitionList( + textfmt.Definition(textfmt.Text("red"), textfmt.Text("looks like strawberry")), + textfmt.Definition(textfmt.Text("green"), textfmt.Text("looks like grass")), + textfmt.Definition(textfmt.Text("blue"), textfmt.Text("looks like sky")), + ), + 21, + ), + 3, + -9, + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = ` - red: looks + like strawberry + - green: looks + like grass + - blue: looks + like sky +` + + if b.String() != expect { + t.Fatal(b.String()) + } + }) + + t.Run("indent out", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Indent( + textfmt.Wrap( + textfmt.DefinitionList( + textfmt.Definition(textfmt.Text("red"), textfmt.Text("looks like strawberry")), + textfmt.Definition(textfmt.Text("green"), textfmt.Text("looks like grass")), + textfmt.Definition(textfmt.Text("blue"), textfmt.Text("looks like sky")), + ), + 21, + ), + 0, + 4, + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = `- red: looks like + strawberry +- green: looks like + grass +- blue: looks like + sky +` + + if b.String() != expect { + t.Fatal(b.String()) + } + }) + }) + + t.Run("numbered definition list", func(t *testing.T) { + t.Run("unwrapped", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.NumberedDefinitionList( + textfmt.Definition(textfmt.Text("red"), textfmt.Text("looks like strawberry")), + textfmt.Definition(textfmt.Text("green"), textfmt.Text("looks like grass")), + textfmt.Definition(textfmt.Text("blue"), textfmt.Text("looks like sky")), + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = `1. red: looks like strawberry +2. green: looks like grass +3. blue: looks like sky +` + + if b.String() != expect { + t.Fatal(b.String()) + } + }) + + t.Run("wrapped", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Wrap( + textfmt.NumberedDefinitionList( + textfmt.Definition(textfmt.Text("red"), textfmt.Text("looks like strawberry")), + textfmt.Definition(textfmt.Text("green"), textfmt.Text("looks like grass")), + textfmt.Definition(textfmt.Text("blue"), textfmt.Text("looks like sky")), + ), + 24, + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = `1. red: looks like + strawberry +2. green: looks like + grass +3. blue: looks like sky +` + + if b.String() != expect { + t.Fatal(b.String()) + } + }) + + t.Run("indent without wrapping", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Indent( + textfmt.NumberedDefinitionList( + textfmt.Definition(textfmt.Text("red"), textfmt.Text("looks like strawberry")), + textfmt.Definition(textfmt.Text("green"), textfmt.Text("looks like grass")), + textfmt.Definition(textfmt.Text("blue"), textfmt.Text("looks like sky")), + ), + 4, + 0, + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = ` 1. red: looks like strawberry + 2. green: looks like grass + 3. blue: looks like sky +` + + if b.String() != expect { + t.Fatal(b.String()) + } + }) + + t.Run("indent in", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Indent( + textfmt.Wrap( + textfmt.NumberedDefinitionList( + textfmt.Definition(textfmt.Text("red"), textfmt.Text("looks like strawberry")), + textfmt.Definition(textfmt.Text("green"), textfmt.Text("looks like grass")), + textfmt.Definition(textfmt.Text("blue"), textfmt.Text("looks like sky")), + ), + 21, + ), + 3, + -9, + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = ` 1. red: looks + like strawberry + 2. green: looks + like grass + 3. blue: looks + like sky +` + + if b.String() != expect { + t.Fatal(b.String()) + } + }) + + t.Run("indent out", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Indent( + textfmt.Wrap( + textfmt.NumberedDefinitionList( + textfmt.Definition(textfmt.Text("red"), textfmt.Text("looks like strawberry")), + textfmt.Definition(textfmt.Text("green"), textfmt.Text("looks like grass")), + textfmt.Definition(textfmt.Text("blue"), textfmt.Text("looks like sky")), + ), + 21, + ), + 0, + 4, + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = `1. red: looks like + strawberry +2. green: looks like + grass +3. blue: looks like + sky +` + + if b.String() != expect { + t.Fatal(b.String()) + } + }) + + t.Run("long numbered definition list", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.NumberedDefinitionList( + textfmt.Definition(textfmt.Text("one"), textfmt.Text("this is an item")), + textfmt.Definition(textfmt.Text("two"), textfmt.Text("this is another item")), + textfmt.Definition(textfmt.Text("three"), textfmt.Text("this is the third item")), + textfmt.Definition(textfmt.Text("four"), textfmt.Text("this is the fourth item")), + textfmt.Definition(textfmt.Text("five"), textfmt.Text("this is the fifth item")), + textfmt.Definition(textfmt.Text("six"), textfmt.Text("this is the sixth item")), + textfmt.Definition(textfmt.Text("seven"), textfmt.Text("this is the seventh item")), + textfmt.Definition(textfmt.Text("eight"), textfmt.Text("this is the eighth item")), + textfmt.Definition(textfmt.Text("nine"), textfmt.Text("this is the nineth item")), + textfmt.Definition(textfmt.Text("ten"), textfmt.Text("this is the tenth item")), + textfmt.Definition(textfmt.Text("eleven"), textfmt.Text("this is the eleventh item")), + textfmt.Definition(textfmt.Text("twelve"), textfmt.Text("this is the twelveth item")), + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = `1. one: this is an item +2. two: this is another item +3. three: this is the third item +4. four: this is the fourth item +5. five: this is the fifth item +6. six: this is the sixth item +7. seven: this is the seventh item +8. eight: this is the eighth item +9. nine: this is the nineth item +10. ten: this is the tenth item +11. eleven: this is the eleventh item +12. twelve: this is the twelveth item +` + + if b.String() != expect { + t.Fatal(b.String()) + } + }) + }) + + t.Run("table", func(t *testing.T) { + t.Run("now rows", func(t *testing.T) { + var b bytes.Buffer + doc := textfmt.Doc(textfmt.Table()) + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "\n" { + t.Fatal(b.String()) + } + }) + + t.Run("no columns", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Table( + textfmt.Row(), + textfmt.Row(), + textfmt.Row(), + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "\n" { + t.Fatal(b.String()) + } + }) + + t.Run("basic", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Table( + textfmt.Row( + textfmt.Cell(textfmt.Text("1")), textfmt.Cell(textfmt.Text("2")), textfmt.Cell(textfmt.Text("3")), + ), + textfmt.Row( + textfmt.Cell(textfmt.Text("4")), textfmt.Cell(textfmt.Text("5")), textfmt.Cell(textfmt.Text("6")), + ), + textfmt.Row( + textfmt.Cell(textfmt.Text("7")), textfmt.Cell(textfmt.Text("8")), textfmt.Cell(textfmt.Text("9")), + ), + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = `1 | 2 | 3 +--------- +4 | 5 | 6 +--------- +7 | 8 | 9 +` + + if b.String() != expect { + t.Fatal(b.String()) + } + }) + + t.Run("basic with header", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Table( + textfmt.Header( + textfmt.Cell(textfmt.Text("1")), textfmt.Cell(textfmt.Text("-1")), textfmt.Cell(textfmt.Text("0")), + ), + textfmt.Row( + textfmt.Cell(textfmt.Text("1")), textfmt.Cell(textfmt.Text("2")), textfmt.Cell(textfmt.Text("3")), + ), + textfmt.Row( + textfmt.Cell(textfmt.Text("4")), textfmt.Cell(textfmt.Text("5")), textfmt.Cell(textfmt.Text("6")), + ), + textfmt.Row( + textfmt.Cell(textfmt.Text("7")), textfmt.Cell(textfmt.Text("8")), textfmt.Cell(textfmt.Text("9")), + ), + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = `1 | -1 | 0 +========== +1 | 2 | 3 +---------- +4 | 5 | 6 +---------- +7 | 8 | 9 +` + + if b.String() != expect { + t.Fatal(b.String()) + } + }) + + t.Run("header without rows", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Table( + textfmt.Header( + textfmt.Cell(textfmt.Text("foo")), + textfmt.Cell(textfmt.Text("bar")), + textfmt.Cell(textfmt.Text("baz")), + ), + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "foo | bar | baz\n===============\n" { + t.Fatal(b.String()) + } + }) + + t.Run("single row", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Table( + textfmt.Row( + textfmt.Cell(textfmt.Text("1")), + textfmt.Cell(textfmt.Text("2")), + textfmt.Cell(textfmt.Text("3")), + ), + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "1 | 2 | 3\n" { + t.Fatal(b.String()) + } + }) + + t.Run("single row with header", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Table( + textfmt.Header( + textfmt.Cell(textfmt.Text("foo")), + textfmt.Cell(textfmt.Text("bar")), + textfmt.Cell(textfmt.Text("baz")), + ), + textfmt.Row( + textfmt.Cell(textfmt.Text("1")), + textfmt.Cell(textfmt.Text("2")), + textfmt.Cell(textfmt.Text("3")), + ), + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "foo | bar | baz\n===============\n1 | 2 | 3 \n" { + t.Fatal(b.String()) + } + }) + + t.Run("single column", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Table( + textfmt.Row( + textfmt.Cell(textfmt.Text("1")), + ), + textfmt.Row( + textfmt.Cell(textfmt.Text("4")), + ), + textfmt.Row( + textfmt.Cell(textfmt.Text("7")), + ), + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "1\n-\n4\n-\n7\n" { + t.Fatal(b.String()) + } + }) + + t.Run("single column with header", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Table( + textfmt.Header( + textfmt.Cell(textfmt.Text("foo")), + ), + textfmt.Row( + textfmt.Cell(textfmt.Text("1")), + ), + textfmt.Row( + textfmt.Cell(textfmt.Text("4")), + ), + textfmt.Row( + textfmt.Cell(textfmt.Text("7")), + ), + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "foo\n===\n1 \n---\n4 \n---\n7 \n" { + t.Fatal(b.String()) + } + }) + + t.Run("single row and single column", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Table( + textfmt.Row( + textfmt.Cell(textfmt.Text("1")), + ), + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "1\n" { + t.Fatal(b.String()) + } + }) + + t.Run("single row and single column with header", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Table( + textfmt.Header( + textfmt.Cell(textfmt.Text("foo")), + ), + textfmt.Row( + textfmt.Cell(textfmt.Text("1")), + ), + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "foo\n===\n1 \n" { + t.Fatal(b.String()) + } + }) + + t.Run("unequal number of row cells", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Table( + textfmt.Row( + textfmt.Cell(textfmt.Text("1")), textfmt.Cell(textfmt.Text("2")), + ), + textfmt.Row( + textfmt.Cell(textfmt.Text("4")), + ), + textfmt.Row( + textfmt.Cell(textfmt.Text("7")), textfmt.Cell(textfmt.Text("8")), textfmt.Cell(textfmt.Text("9")), + ), + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = `1 | 2 | +--------- +4 | | +--------- +7 | 8 | 9 +` + + if b.String() != expect { + t.Fatal(b.String()) + } + }) + + t.Run("unequal number of row cells with header", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Table( + textfmt.Header( + textfmt.Cell(textfmt.Text("1")), textfmt.Cell(textfmt.Text("-1")), + ), + textfmt.Row( + textfmt.Cell(textfmt.Text("1")), textfmt.Cell(textfmt.Text("2")), textfmt.Cell(textfmt.Text("3")), + ), + textfmt.Row( + textfmt.Cell(textfmt.Text("4")), + ), + textfmt.Row( + textfmt.Cell(textfmt.Text("")), textfmt.Cell(textfmt.Text("8")), textfmt.Cell(textfmt.Text("9")), + ), + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = `1 | -1 | +========== +1 | 2 | 3 +---------- +4 | | +---------- + | 8 | 9 +` + + if b.String() != expect { + t.Fatal(b.String()) + } + }) + + t.Run("all empty cells", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Wrap( + textfmt.Table( + textfmt.Header(textfmt.Cell(textfmt.Text("")), textfmt.Cell(textfmt.Text(""))), + textfmt.Row(textfmt.Cell(textfmt.Text("")), textfmt.Cell(textfmt.Text(""))), + textfmt.Row(textfmt.Cell(textfmt.Text("")), textfmt.Cell(textfmt.Text(""))), + ), + 72, + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != " | \n===\n | \n---\n | \n" { + t.Fatal(b.String()) + } + }) + + t.Run("no new lines by default", func(t *testing.T) { + const txt = ` + Walking through the mixed forests of Brandenburg in early autumn, + one notices the dominant presence of Scots pine (Pinus sylvestris). + ` + + doc := textfmt.Doc( + textfmt.Table( + textfmt.Header(textfmt.Cell(textfmt.Text("Forest"))), + textfmt.Row(textfmt.Cell(textfmt.Text(txt))), + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal() + } + + // << + const expect = `Forest +===================================================================================================================================== +Walking through the mixed forests of Brandenburg in early autumn, one notices the dominant presence of Scots pine (Pinus sylvestris). +` + + if b.String() != expect { + t.Fatal(b.String()) + } + }) + + t.Run("indent without wrap", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Indent( + textfmt.Table( + textfmt.Header( + textfmt.Cell(textfmt.Text("one")), + textfmt.Cell(textfmt.Text("two")), + textfmt.Cell(textfmt.Text("three")), + ), + textfmt.Row( + textfmt.Cell(textfmt.Text("Walking through the mixed forests of Brandenburg in early autumn")), + textfmt.Cell(textfmt.Text("one notices the dominant presence of Scots pine (Pinus sylvestris)")), + textfmt.Cell(textfmt.Text("interspersed with sessile oak (Quercus petraea)")), + ), + textfmt.Row( + textfmt.Cell(textfmt.Text("and silver birch (Betula pendula)")), + textfmt.Cell(textfmt.Text("their canopies creating a mosaic of light")), + textfmt.Cell(textfmt.Text("and shadow on the forest floor")), + ), + ), + 0, + 4, + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = ` one | two | three + ======================================================================================================================================================================================= + Walking through the mixed forests of Brandenburg in early autumn | one notices the dominant presence of Scots pine (Pinus sylvestris) | interspersed with sessile oak (Quercus petraea) + --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + and silver birch (Betula pendula) | their canopies creating a mosaic of light | and shadow on the forest floor +` + + if b.String() != expect { + logBytes(t, expect) + logBytes(t, b.String()) + t.Log("\n" + expect) + t.Fatal("\n" + b.String()) + } + }) + + t.Run("wrap without indent", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Wrap( + textfmt.Table( + textfmt.Header( + textfmt.Cell(textfmt.Text("one")), + textfmt.Cell(textfmt.Text("two")), + textfmt.Cell(textfmt.Text("three")), + ), + textfmt.Row( + textfmt.Cell(textfmt.Text("Walking through the mixed forests of Brandenburg in early autumn")), + textfmt.Cell(textfmt.Text("one notices the dominant presence of Scots pine (Pinus sylvestris)")), + textfmt.Cell(textfmt.Text("interspersed with sessile oak (Quercus petraea)")), + ), + textfmt.Row( + textfmt.Cell(textfmt.Text("and silver birch (Betula pendula)")), + textfmt.Cell(textfmt.Text("their canopies creating a mosaic of light")), + textfmt.Cell(textfmt.Text("and shadow on the forest floor")), + ), + ), + 72, + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = `one | two | three +=================================================================== +Walking through the | one notices the dominant | interspersed with +mixed forests of | presence of Scots pine | sessile oak +Brandenburg in early | (Pinus sylvestris) | (Quercus petraea) +autumn | | +------------------------------------------------------------------- +and silver birch | their canopies creating | and shadow on the +(Betula pendula) | a mosaic of light | forest floor +` + + if b.String() != expect { + logBytes(t, expect) + logBytes(t, b.String()) + t.Log("\n" + expect) + t.Fatal("\n" + b.String()) + } + }) + + t.Run("indent and wrap", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Indent( + textfmt.Wrap( + textfmt.Table( + textfmt.Header( + textfmt.Cell(textfmt.Text("one")), + textfmt.Cell(textfmt.Text("two")), + textfmt.Cell(textfmt.Text("three")), + ), + textfmt.Row( + textfmt.Cell(textfmt.Text("Walking through the mixed forests of Brandenburg in early autumn")), + textfmt.Cell(textfmt.Text("one notices the dominant presence of Scots pine (Pinus sylvestris)")), + textfmt.Cell(textfmt.Text("interspersed with sessile oak (Quercus petraea)")), + ), + textfmt.Row( + textfmt.Cell(textfmt.Text("and silver birch (Betula pendula)")), + textfmt.Cell(textfmt.Text("their canopies creating a mosaic of light")), + textfmt.Cell(textfmt.Text("and shadow on the forest floor")), + ), + ), + 72, + ), + 0, + 4, + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = ` one | two | three + ================================================================== + Walking through the | one notices the | interspersed with + mixed forests of | dominant presence of | sessile oak + Brandenburg in early | Scots pine (Pinus | (Quercus petraea) + autumn | sylvestris) | + ------------------------------------------------------------------ + and silver birch | their canopies creating | and shadow on the + (Betula pendula) | a mosaic of light | forest floor +` + + if b.String() != expect { + logBytes(t, expect) + logBytes(t, b.String()) + t.Log("\n" + expect) + t.Fatal("\n" + b.String()) + } + }) + }) + + t.Run("code", func(t *testing.T) { + const code = `func() textfmt.Document { + return textfmt.Document( + textfmt.Paragraph(textfmt.Text("Hello, world!")), + ) +}` + + var b bytes.Buffer + if err := textfmt.Teletype(&b, textfmt.Doc(textfmt.CodeBlock(code))); err != nil { + t.Fatal(err) + } + + if b.String() != code+"\n" { + t.Fatal(b.String()) + } + }) + + t.Run("syntax", func(t *testing.T) { + t.Run("symbol", func(t *testing.T) { + doc := textfmt.Doc(textfmt.Syntax(textfmt.Symbol("foo"))) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "foo\n" { + t.Fatal(b.String()) + } + }) + + t.Run("zero or more symbols", func(t *testing.T) { + doc := textfmt.Doc(textfmt.Syntax(textfmt.ZeroOrMore(textfmt.Symbol("foo")))) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "[foo]...\n" { + t.Fatal(b.String()) + } + }) + + t.Run("one or more", func(t *testing.T) { + doc := textfmt.Doc(textfmt.Syntax(textfmt.OneOrMore(textfmt.Symbol("foo")))) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "...\n" { + t.Fatal(b.String()) + } + }) + + t.Run("required symbol", func(t *testing.T) { + doc := textfmt.Doc(textfmt.Syntax(textfmt.Required(textfmt.Symbol("foo")))) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "\n" { + t.Fatal(b.String()) + } + }) + + t.Run("optional symbol", func(t *testing.T) { + doc := textfmt.Doc(textfmt.Syntax(textfmt.Optional(textfmt.Symbol("foo")))) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "[foo]\n" { + t.Fatal(b.String()) + } + }) + + t.Run("sequence implicit", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Syntax( + textfmt.Symbol("foo"), + textfmt.Symbol("bar"), + textfmt.Symbol("baz"), + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "foo bar baz\n" { + t.Fatal(b.String()) + } + }) + + t.Run("sequence", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Syntax( + textfmt.Sequence( + textfmt.Symbol("foo"), + textfmt.Symbol("bar"), + textfmt.Symbol("baz"), + ), + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "foo bar baz\n" { + t.Fatal(b.String()) + } + }) + + t.Run("subsequence", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Syntax( + textfmt.Symbol("corge"), + textfmt.Sequence( + textfmt.Symbol("foo"), + textfmt.Symbol("bar"), + textfmt.Symbol("baz"), + ), + textfmt.Symbol("garply"), + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "corge (foo bar baz) garply\n" { + t.Fatal(b.String()) + } + }) + + t.Run("top level choice", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Syntax( + textfmt.Choice( + textfmt.Symbol("foo"), + textfmt.Symbol("bar"), + textfmt.Symbol("baz"), + ), + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "foo\nbar\nbaz\n" { + t.Fatal(b.String()) + } + }) + + t.Run("choice", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Syntax( + textfmt.Symbol("corge"), + textfmt.Choice( + textfmt.Symbol("foo"), + textfmt.Symbol("bar"), + textfmt.Symbol("baz"), + ), + textfmt.Symbol("garply"), + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "corge (foo|bar|baz) garply\n" { + t.Fatal(b.String()) + } + }) + + t.Run("example", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Syntax( + textfmt.Symbol("foo"), + textfmt.ZeroOrMore(textfmt.Symbol("options")), + textfmt.Required(textfmt.Symbol("filename")), + textfmt.ZeroOrMore( + textfmt.Choice( + textfmt.Symbol("string"), + textfmt.Symbol("number"), + ), + ), + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "foo [options]... [string|number]...\n" { + t.Fatal(b.String()) + } + }) + + }) +} diff --git a/text.go b/text.go index 3d1807a..2cd3932 100644 --- a/text.go +++ b/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) + } +} diff --git a/wrap.go b/wrap.go index 03c268c..45b2851 100644 --- a/wrap.go +++ b/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, " ")) - } - + lines = append(lines, strings.Join(currentLine, " ")) return strings.Join(lines, "\n") } diff --git a/writer.go b/writer.go index 357882d..eb94aa9 100644 --- a/writer.go +++ b/writer.go @@ -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 + } } } diff --git a/writer_test.go b/writer_test.go new file mode 100644 index 0000000..4c45474 --- /dev/null +++ b/writer_test.go @@ -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 +}