1
0
textfmt/runoff_test.go

2328 lines
49 KiB
Go
Raw Normal View History

2025-10-23 02:55:38 +02:00
package textfmt_test
import (
"bytes"
"code.squareroundforest.org/arpio/textfmt"
"testing"
2025-11-02 22:15:31 +01:00
"time"
2025-10-23 02:55:38 +02:00
)
func TestRoff(t *testing.T) {
t.Run("invalid", func(t *testing.T) {
var b bytes.Buffer
if err := textfmt.Runoff(&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.Runoff(&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.Indent(
textfmt.Paragraph(textfmt.Text("Below you can find some test text, with various text items.")),
0,
8,
),
textfmt.Title(1, "Document syntax:"),
textfmt.Indent(
textfmt.Syntax(
textfmt.Symbol("textfmt.Doc"),
textfmt.Symbol("("),
textfmt.ZeroOrMore(textfmt.Symbol("Entry")),
textfmt.Symbol(")"),
),
8,
2025-11-02 06:27:17 +01:00
0,
2025-10-23 02:55:38 +02:00
),
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.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"),
),
),
)
var b bytes.Buffer
if err := textfmt.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
const expect = `.in 0
.ti 0
\fBExample Text\fR
.br
.sp 1v
.in 0
.ti 8
Below you can find some test text, with various text items.
.br
.sp 1v
.in 0
.ti 0
\fBDocument syntax:\fR
.br
.sp 1v
.nf
2025-11-02 07:14:35 +01:00
textfmt.Doc ( [Entry]... )
2025-10-23 02:55:38 +02:00
.fi
.br
.sp 1v
.in 0
.ti 0
\fBEntries:\fR
.br
.sp 1v
.in 0
.ti 0
textfmt supports the following entries:
.br
.sp 1v
.in 2
.ti 0
\(bu CodeBlock
.br
.in 2
.ti 0
\(bu DefinitionList
.br
.in 2
.ti 0
\(bu List
.br
.in 2
.ti 0
\(bu NumberedDefinitionList
.br
.in 2
.ti 0
\(bu NumberedList
.br
.in 2
.ti 0
\(bu Paragraph
.br
.in 2
.ti 0
\(bu Syntax
.br
.in 2
.ti 0
\(bu Table
.br
.in 2
.ti 0
\(bu Title
.br
.sp 1v
.in 0
.ti 0
\fBEntry explanations:\fR
.br
.sp 1v
.in 26
.ti 0
\(bu CodeBlock:\~\~\~\~\~\~\~\~\~\~\~\~\~\~a multiline block of code
.br
.in 26
.ti 0
\(bu DefinitionList:\~\~\~\~\~\~\~\~\~a list of definitions like this one
.br
.in 26
.ti 0
\(bu List:\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~a list of items
.br
.in 26
.ti 0
\(bu NumberedDefinitionList:\~numbered definitions
.br
.in 26
.ti 0
\(bu NumberedList:\~\~\~\~\~\~\~\~\~\~\~numbered list
.br
.in 26
.ti 0
\(bu Paragraph:\~\~\~\~\~\~\~\~\~\~\~\~\~\~paragraph of text
.br
.in 26
.ti 0
\(bu Syntax:\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~a syntax expression
.br
.in 26
.ti 0
\(bu Table:\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~a table
.br
.in 26
.ti 0
\(bu 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("example man", func(t *testing.T) {
2025-11-02 22:15:31 +01:00
releaseDate := time.Date(2025, 11, 2, 15, 36, 18, 0, time.FixedZone("CET", 3600))
2025-10-23 02:55:38 +02:00
doc := textfmt.Doc(
2025-11-02 22:15:31 +01:00
textfmt.Title(
0,
"Example Text",
textfmt.ManSection(1),
textfmt.ManCategory("User Command"),
textfmt.ReleaseDate(releaseDate),
textfmt.ReleaseVersion("v1"),
),
2025-10-23 02:55:38 +02:00
textfmt.Indent(
textfmt.Paragraph(textfmt.Text("Below you can find some test text, with various text items.")),
0,
8,
),
textfmt.Title(1, "Document syntax:"),
textfmt.Indent(
textfmt.Syntax(
textfmt.Symbol("textfmt.Doc"),
textfmt.Symbol("("),
textfmt.ZeroOrMore(textfmt.Symbol("Entry")),
textfmt.Symbol(")"),
),
8,
2025-11-02 06:27:17 +01:00
0,
2025-10-23 02:55:38 +02:00
),
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.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"),
),
),
)
var b bytes.Buffer
if err := textfmt.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
2025-11-02 22:15:31 +01:00
const expect = `.TH "Example Text" 1 "November 2025" "v1" "User Command"
2025-10-23 02:55:38 +02:00
.br
.sp 1v
.in 0
.ti 8
Below you can find some test text, with various text items.
.br
.sp 1v
.in 0
.ti 0
\fBDocument syntax:\fR
.br
.sp 1v
.nf
2025-11-02 07:14:35 +01:00
textfmt.Doc ( [Entry]... )
2025-10-23 02:55:38 +02:00
.fi
.br
.sp 1v
.in 0
.ti 0
\fBEntries:\fR
.br
.sp 1v
.in 0
.ti 0
textfmt supports the following entries:
.br
.sp 1v
.in 2
.ti 0
\(bu CodeBlock
.br
.in 2
.ti 0
\(bu DefinitionList
.br
.in 2
.ti 0
\(bu List
.br
.in 2
.ti 0
\(bu NumberedDefinitionList
.br
.in 2
.ti 0
\(bu NumberedList
.br
.in 2
.ti 0
\(bu Paragraph
.br
.in 2
.ti 0
\(bu Syntax
.br
.in 2
.ti 0
\(bu Table
.br
.in 2
.ti 0
\(bu Title
.br
.sp 1v
.in 0
.ti 0
\fBEntry explanations:\fR
.br
.sp 1v
.in 26
.ti 0
\(bu CodeBlock:\~\~\~\~\~\~\~\~\~\~\~\~\~\~a multiline block of code
.br
.in 26
.ti 0
\(bu DefinitionList:\~\~\~\~\~\~\~\~\~a list of definitions like this one
.br
.in 26
.ti 0
\(bu List:\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~a list of items
.br
.in 26
.ti 0
\(bu NumberedDefinitionList:\~numbered definitions
.br
.in 26
.ti 0
\(bu NumberedList:\~\~\~\~\~\~\~\~\~\~\~numbered list
.br
.in 26
.ti 0
\(bu Paragraph:\~\~\~\~\~\~\~\~\~\~\~\~\~\~paragraph of text
.br
.in 26
.ti 0
\(bu Syntax:\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~a syntax expression
.br
.in 26
.ti 0
\(bu Table:\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~a table
.br
.in 26
.ti 0
\(bu 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\n...with invalid chars.")))
if err := textfmt.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
const expect = ".in 0\n.ti 0\n\\&...some sample text...\x00 ...with invalid chars.\n"
if b.String() != expect {
logBytes(t, b.String())
logBytes(t, expect)
t.Fatal(b.String())
}
})
t.Run("styling", 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.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
const expect = `.in 0
.ti 0
\fISome sample text... with\fR \fBsome\fR \fIstyling.\fR
`
if b.String() != expect {
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.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
const expect = `.in 0
.ti 0
Some sample text... on multiple lines.
`
if b.String() != expect {
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.Paragraph(textfmt.Text("Some sample text...\non multiple lines.")),
2,
0,
),
)
if err := textfmt.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
const expect = `.in 2
.ti 2
Some sample text... on multiple lines.
`
if b.String() != expect {
t.Fatal(b.String())
}
})
t.Run("indent first in", func(t *testing.T) {
var b bytes.Buffer
doc := textfmt.Doc(
textfmt.Indent(
textfmt.Paragraph(textfmt.Text("Some sample text...\non multiple lines.")),
2,
2,
),
)
if err := textfmt.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
const expect = `.in 2
.ti 4
Some sample text... on multiple lines.
`
if b.String() != expect {
t.Fatal(b.String())
}
})
t.Run("indent first out", func(t *testing.T) {
var b bytes.Buffer
doc := textfmt.Doc(
textfmt.Indent(
textfmt.Paragraph(textfmt.Text("Some sample text...\non multiple lines.")),
2,
-2,
),
)
if err := textfmt.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
const expect = `.in 2
.ti 0
Some sample text... on multiple lines.
`
if b.String() != expect {
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.Runoff(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.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
if b.String() != ".in 0\n.ti 0\nText 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.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
if b.String() != ".in 0\n.ti 0\nhttps://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.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
if b.String() != ".in 0\n.ti 0\na link (https://sqrndfst.org)\n" {
t.Fatal(b.String())
}
})
t.Run("newline in link", func(t *testing.T) {
var b bytes.Buffer
doc := textfmt.Doc(textfmt.Paragraph(textfmt.Link("a\nlink", "https://sqrndfst.org\n/foo")))
if err := textfmt.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
if b.String() != ".in 0\n.ti 0\na link (https://sqrndfst.org /foo)\n" {
t.Fatal(b.String())
}
})
})
t.Run("title", func(t *testing.T) {
2025-11-02 22:15:31 +01:00
t.Run("basic", func(t *testing.T) {
var b bytes.Buffer
doc := textfmt.Doc(textfmt.Title(0, "This is a title"))
if err := textfmt.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
2025-10-23 02:55:38 +02:00
2025-11-02 22:15:31 +01:00
const expect = `.in 0
2025-10-23 02:55:38 +02:00
.ti 0
\fBThis is a title\fR
`
2025-11-02 22:15:31 +01:00
if b.String() != expect {
t.Fatal(b.String())
}
})
t.Run("escaped man", func(t *testing.T) {
var b bytes.Buffer
doc := textfmt.Doc(textfmt.Title(0, "This is a title \"\\\"", textfmt.ManSection(1)))
if err := textfmt.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
const expect = `
.TH "This is a title \(dq\\\(dq" 1 "" "" ""
`
if "\n" + b.String() != expect {
t.Fatal("\n" + b.String())
}
})
2025-10-23 02:55:38 +02:00
})
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.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
const expect = `.in 0
.ti 0
This is a paragraph.
`
if b.String() != expect {
t.Fatal(b.String())
}
})
t.Run("wrap ignored", 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.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
const expect = `.in 0
.ti 0
This is a paragraph.
`
if b.String() != expect {
t.Fatal(b.String())
}
})
t.Run("indent in", func(t *testing.T) {
var b bytes.Buffer
doc := textfmt.Doc(
textfmt.Indent(
textfmt.Paragraph(textfmt.Text("This is a paragraph.")),
0,
4,
),
)
if err := textfmt.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
const expect = `.in 0
.ti 4
This is a paragraph.
`
if b.String() != expect {
t.Fatal(b.String())
}
})
t.Run("indent out", func(t *testing.T) {
var b bytes.Buffer
doc := textfmt.Doc(
textfmt.Indent(
textfmt.Paragraph(textfmt.Text("This is a paragraph.")),
4,
-4,
),
)
if err := textfmt.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
const expect = `.in 4
.ti 0
This is a paragraph.
`
if b.String() != expect {
t.Fatal(b.String())
}
})
})
t.Run("list", func(t *testing.T) {
t.Run("simple", 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.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
const expect = `.in 2
.ti 0
\(bu this is an item
.br
.in 2
.ti 0
\(bu this is another item
.br
.in 2
.ti 0
\(bu this is a third item
`
if b.String() != expect {
t.Fatal(b.String())
}
})
t.Run("indent", 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.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
const expect = `.in 6
.ti 4
\(bu this is an item
.br
.in 6
.ti 4
\(bu this is another item
.br
.in 6
.ti 4
\(bu 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.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")),
),
-2,
6,
),
)
var b bytes.Buffer
if err := textfmt.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
const expect = `.in 0
.ti 4
\(bu this is an item
.br
.in 0
.ti 4
\(bu this is another item
.br
.in 0
.ti 4
\(bu 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.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")),
),
2,
-2,
),
)
var b bytes.Buffer
if err := textfmt.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
const expect = `.in 4
.ti 0
\(bu this is an item
.br
.in 4
.ti 0
\(bu this is another item
.br
.in 4
.ti 0
\(bu this is a third item
`
if b.String() != expect {
t.Fatal(b.String())
}
})
})
t.Run("numbered list", func(t *testing.T) {
t.Run("simple", 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.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
const expect = `.in 3
.ti 0
1.\~this is an item
.br
.in 3
.ti 0
2.\~this is another item
.br
.in 3
.ti 0
3.\~this is a third item
`
if b.String() != expect {
t.Fatal(b.String())
}
})
t.Run("indent", 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.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
const expect = `.in 7
.ti 4
1.\~this is an item
.br
.in 7
.ti 4
2.\~this is another item
.br
.in 7
.ti 4
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.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")),
),
-3,
7,
),
)
var b bytes.Buffer
if err := textfmt.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
const expect = `.in 0
.ti 4
1.\~this is an item
.br
.in 0
.ti 4
2.\~this is another item
.br
.in 0
.ti 4
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.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")),
),
3,
-3,
),
)
var b bytes.Buffer
if err := textfmt.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
const expect = `.in 6
.ti 0
1.\~this is an item
.br
.in 6
.ti 0
2.\~this is another item
.br
.in 6
.ti 0
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")),
2025-10-28 00:47:41 +01:00
textfmt.Item(textfmt.Text("this is the twelfth item")),
2025-10-23 02:55:38 +02:00
),
)
var b bytes.Buffer
if err := textfmt.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
const expect = `.in 4
.ti 0
1.\~\~this is an item
.br
.in 4
.ti 0
2.\~\~this is another item
.br
.in 4
.ti 0
3.\~\~this is the third item
.br
.in 4
.ti 0
4.\~\~this is the fourth item
.br
.in 4
.ti 0
5.\~\~this is the fifth item
.br
.in 4
.ti 0
6.\~\~this is the sixth item
.br
.in 4
.ti 0
7.\~\~this is the seventh item
.br
.in 4
.ti 0
8.\~\~this is the eighth item
.br
.in 4
.ti 0
9.\~\~this is the nineth item
.br
.in 4
.ti 0
10.\~this is the tenth item
.br
.in 4
.ti 0
11.\~this is the eleventh item
.br
.in 4
.ti 0
2025-10-28 00:47:41 +01:00
12.\~this is the twelfth item
2025-10-23 02:55:38 +02:00
`
if b.String() != expect {
t.Fatal(b.String())
}
})
})
t.Run("definition list", func(t *testing.T) {
t.Run("simple", 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.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
const expect = `.in 9
.ti 0
\(bu red:\~\~\~looks like strawberry
.br
.in 9
.ti 0
\(bu green:\~looks like grass
.br
.in 9
.ti 0
\(bu blue:\~\~looks like sky
`
if b.String() != expect {
t.Fatal(b.String())
}
})
t.Run("indent", 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.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
const expect = `.in 13
.ti 4
\(bu red:\~\~\~looks like strawberry
.br
.in 13
.ti 4
\(bu green:\~looks like grass
.br
.in 13
.ti 4
\(bu 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.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")),
),
-6,
9,
),
)
var b bytes.Buffer
if err := textfmt.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
const expect = `
.in 3
.ti 3
\(bu red:\~\~\~looks like strawberry
.br
.in 3
.ti 3
\(bu green:\~looks like grass
.br
.in 3
.ti 3
\(bu blue:\~\~looks like sky
`
2025-10-23 02:55:49 +02:00
if "\n"+b.String() != expect {
2025-10-23 02:55:38 +02:00
t.Fatal("\n" + b.String())
}
})
t.Run("indent out", 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,
-4,
),
)
var b bytes.Buffer
if err := textfmt.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
const expect = `
.in 13
.ti 0
\(bu red:\~\~\~looks like strawberry
.br
.in 13
.ti 0
\(bu green:\~looks like grass
.br
.in 13
.ti 0
\(bu blue:\~\~looks like sky
`
2025-10-23 02:55:49 +02:00
if "\n"+b.String() != expect {
2025-10-23 02:55:38 +02:00
t.Fatal("\n" + b.String())
}
})
})
t.Run("numbered definition list", func(t *testing.T) {
t.Run("simple", 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.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
const expect = `.in 10
.ti 0
1.\~red:\~\~\~looks like strawberry
.br
.in 10
.ti 0
2.\~green:\~looks like grass
.br
.in 10
.ti 0
3.\~blue:\~\~looks like sky
`
if b.String() != expect {
t.Fatal(b.String())
}
})
t.Run("indent", 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")),
),
0,
4,
),
)
var b bytes.Buffer
if err := textfmt.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
const expect = `.in 10
.ti 4
1.\~red:\~\~\~looks like strawberry
.br
.in 10
.ti 4
2.\~green:\~looks like grass
.br
.in 10
.ti 4
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.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")),
),
-6,
9,
),
)
var b bytes.Buffer
if err := textfmt.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
const expect = `
.in 4
.ti 3
1.\~red:\~\~\~looks like strawberry
.br
.in 4
.ti 3
2.\~green:\~looks like grass
.br
.in 4
.ti 3
3.\~blue:\~\~looks like sky
`
2025-10-23 02:55:49 +02:00
if "\n"+b.String() != expect {
2025-10-23 02:55:38 +02:00
t.Fatal("\n" + b.String())
}
})
t.Run("indent out", 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,
-4,
),
)
var b bytes.Buffer
if err := textfmt.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
const expect = `
.in 14
.ti 0
1.\~red:\~\~\~looks like strawberry
.br
.in 14
.ti 0
2.\~green:\~looks like grass
.br
.in 14
.ti 0
3.\~blue:\~\~looks like sky
`
2025-10-23 02:55:49 +02:00
if "\n"+b.String() != expect {
2025-10-23 02:55:38 +02:00
t.Fatal("\n" + 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")),
2025-10-28 00:47:41 +01:00
textfmt.Definition(textfmt.Text("twelve"), textfmt.Text("this is the twelfth item")),
2025-10-23 02:55:38 +02:00
),
)
var b bytes.Buffer
if err := textfmt.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
const expect = `.in 12
.ti 0
1.\~\~one:\~\~\~\~this is an item
.br
.in 12
.ti 0
2.\~\~two:\~\~\~\~this is another item
.br
.in 12
.ti 0
3.\~\~three:\~\~this is the third item
.br
.in 12
.ti 0
4.\~\~four:\~\~\~this is the fourth item
.br
.in 12
.ti 0
5.\~\~five:\~\~\~this is the fifth item
.br
.in 12
.ti 0
6.\~\~six:\~\~\~\~this is the sixth item
.br
.in 12
.ti 0
7.\~\~seven:\~\~this is the seventh item
.br
.in 12
.ti 0
8.\~\~eight:\~\~this is the eighth item
.br
.in 12
.ti 0
9.\~\~nine:\~\~\~this is the nineth item
.br
.in 12
.ti 0
10.\~ten:\~\~\~\~this is the tenth item
.br
.in 12
.ti 0
11.\~eleven:\~this is the eleventh item
.br
.in 12
.ti 0
2025-10-28 00:47:41 +01:00
12.\~twelve:\~this is the twelfth item
2025-10-23 02:55:38 +02:00
`
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.Runoff(&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.Runoff(&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.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
const expect = `.nf
1 | 2 | 3
---------
4 | 5 | 6
---------
7 | 8 | 9
.fi
`
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.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
const expect = `.nf
1 | -1 | 0
==========
1 | 2\~ | 3
----------
4 | 5\~ | 6
----------
7 | 8\~ | 9
.fi
`
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.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
if b.String() != ".nf\nfoo | bar | baz\n===============\n.fi\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.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
if b.String() != ".nf\n1 | 2 | 3\n.fi\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.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
if b.String() != ".nf\nfoo | bar | baz\n===============\n1\\~\\~ | 2\\~\\~ | 3\\~\\~\n.fi\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.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
if b.String() != ".nf\n1\n-\n4\n-\n7\n.fi\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.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
if b.String() != ".nf\nfoo\n===\n1\\~\\~\n---\n4\\~\\~\n---\n7\\~\\~\n.fi\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.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
if b.String() != ".nf\n1\n.fi\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.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
if b.String() != ".nf\nfoo\n===\n1\\~\\~\n.fi\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.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
const expect = `.nf
1 | 2 | \~
---------
4 | \~ | \~
---------
7 | 8 | 9
.fi
`
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.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
const expect = `.nf
1 | -1 | \~
==========
1 | 2\~ | 3
----------
4 | \~\~ | \~
----------
\~ | 8\~ | 9
.fi
`
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.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
if b.String() != ".nf\n | \n===\n | \n---\n | \n.fi\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.Runoff(&b, doc); err != nil {
t.Fatal()
}
// <<
const expect = `.nf
Forest\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~
=====================================================================================================================================
Walking through the mixed forests of Brandenburg in early autumn, one notices the dominant presence of Scots pine (Pinus sylvestris).
.fi
`
if b.String() != expect {
t.Fatal(b.String())
}
})
t.Run("indent", 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")),
),
),
4,
0,
),
)
var b bytes.Buffer
if err := textfmt.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
const expect = `.nf
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\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~
.fi
`
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.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
const expect = `.nf
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\~\~\~\~\~
.fi
`
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,
),
4,
0,
),
)
var b bytes.Buffer
if err := textfmt.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
const expect = `.nf
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\~\~\~\~\~
.fi
`
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) {
t.Run("unindented", 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.Runoff(&b, textfmt.Doc(textfmt.CodeBlock(code))); err != nil {
t.Fatal(err)
}
2025-10-23 02:55:49 +02:00
if b.String() != ".nf\n"+code+"\n.fi\n" {
2025-10-23 02:55:38 +02:00
t.Fatal(b.String())
}
})
t.Run("indented", 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.Runoff(&b, textfmt.Doc(textfmt.Indent(textfmt.CodeBlock(code), 4, 0))); err != nil {
t.Fatal(err)
}
const expect = `.nf
func() textfmt.Document {
return textfmt.Document(
textfmt.Paragraph(textfmt.Text("Hello, world!")),
)
}
.fi
`
if b.String() != expect {
t.Fatal(b.String())
}
})
t.Run("wrap has no effect", 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.Runoff(&b, textfmt.Doc(textfmt.Wrap(textfmt.CodeBlock(code), 12))); err != nil {
t.Fatal(err)
}
2025-10-23 02:55:49 +02:00
if b.String() != ".nf\n"+code+"\n.fi\n" {
2025-10-23 02:55:38 +02:00
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.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
if b.String() != ".nf\nfoo\n.fi\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.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
if b.String() != ".nf\n[foo]...\n.fi\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.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
if b.String() != ".nf\n<foo>...\n.fi\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.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
if b.String() != ".nf\n<foo>\n.fi\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.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
if b.String() != ".nf\n[foo]\n.fi\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.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
if b.String() != ".nf\nfoo bar baz\n.fi\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.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
if b.String() != ".nf\nfoo bar baz\n.fi\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.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
if b.String() != ".nf\ncorge (foo bar baz) garply\n.fi\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.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
if b.String() != ".nf\nfoo\nbar\nbaz\n.fi\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.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
if b.String() != ".nf\ncorge (foo|bar|baz) garply\n.fi\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.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
if b.String() != ".nf\nfoo [options]... <filename> [string|number]...\n.fi\n" {
t.Fatal(b.String())
}
})
t.Run("example indented", func(t *testing.T) {
doc := textfmt.Doc(
textfmt.Indent(
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"),
),
),
),
4,
0,
),
)
var b bytes.Buffer
if err := textfmt.Runoff(&b, doc); err != nil {
t.Fatal(err)
}
2025-11-02 07:14:35 +01:00
if b.String() != ".nf\n foo [options]... <filename> [string|number]...\n.fi\n" {
2025-10-23 02:55:38 +02:00
t.Fatal(b.String())
}
})
})
}