diff --git a/go.mod b/go.mod
index 45b0bdc..49150c3 100644
--- a/go.mod
+++ b/go.mod
@@ -4,4 +4,4 @@ go 1.25.0
require code.squareroundforest.org/arpio/notation v0.0.0-20250826181910-5140794b16b2
-require code.squareroundforest.org/arpio/html v0.0.0-20251011102613-70f77954001f // indirect
+require code.squareroundforest.org/arpio/html v0.0.0-20251029200407-effffeadf9f8 // indirect
diff --git a/go.sum b/go.sum
index 017d044..70c8d6a 100644
--- a/go.sum
+++ b/go.sum
@@ -1,4 +1,6 @@
code.squareroundforest.org/arpio/html v0.0.0-20251011102613-70f77954001f h1:Ep/POhkmvOfSkQklPIpeA4n2FTD2SoFxthjF0SJbsCU=
code.squareroundforest.org/arpio/html v0.0.0-20251011102613-70f77954001f/go.mod h1:LX+Fwqu/a7nDayuDNhXA56cVb+BNrkz4M/WCqvw9YFQ=
+code.squareroundforest.org/arpio/html v0.0.0-20251029200407-effffeadf9f8 h1:6OwHDturRjOeIxoc2Zlfkhf4InnMnNKKDb3LtrbIJjg=
+code.squareroundforest.org/arpio/html v0.0.0-20251029200407-effffeadf9f8/go.mod h1:LX+Fwqu/a7nDayuDNhXA56cVb+BNrkz4M/WCqvw9YFQ=
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/html.go b/html.go
index 13dd097..8d5d131 100644
--- a/html.go
+++ b/html.go
@@ -19,11 +19,20 @@ func htmlText(t Txt) []any {
return c
}
+ var text any = t.text
if t.link != "" {
- return []any{tag.A(html.Attr("href", t.link), t.text)}
+ text = tag.A(html.Attr("href", t.link), text)
}
- return []any{t.text}
+ if t.bold {
+ text = tag.B(text)
+ }
+
+ if t.italic {
+ text = tag.I(text)
+ }
+
+ return []any{text}
}
func htmlTitle(e Entry) html.Tag {
@@ -92,6 +101,7 @@ func htmlNumberedDefinitions(e Entry) html.Tag {
func htmlTable(e Entry) html.Tag {
table := tag.Table
+ e.rows = normalizeTable(e.rows)
for _, r := range e.rows {
row := tag.Tr
cell := tag.Td
@@ -143,7 +153,7 @@ func htmlSequence(s SyntaxItem) string {
}
func htmlChoice(s SyntaxItem) string {
- ss := htmlSyntaxItems(s.sequence)
+ ss := htmlSyntaxItems(s.choice)
if s.topLevel {
return strings.Join(ss, "\n")
}
@@ -251,6 +261,10 @@ func renderHTMLFragment(out io.Writer, doc Document) error {
return err
}
+ if len(tags) == 0 {
+ return nil
+ }
+
for i, tag := range tags {
indent := html.Indentation{
Indent: "\t",
@@ -267,11 +281,15 @@ func renderHTMLFragment(out io.Writer, doc Document) error {
indent.Indent = timesn(" ", doc.entries[i].indent)
}
- if err := html.RenderIndent(out, indent, tag); err != nil {
+ if err := html.WriteIndent(out, indent, tag); err != nil {
return err
}
}
+ if _, err := fmt.Fprintln(out); err != nil {
+ return err
+ }
+
return nil
}
@@ -302,5 +320,13 @@ func renderHTML(out io.Writer, doc Document, lang string) error {
MinPWidth: 60,
}
- return html.RenderIndent(out, indent, tag.Doctype("html"), htmlDoc)
+ if err := html.WriteIndent(out, indent, tag.Doctype("html"), htmlDoc); err != nil {
+ return err
+ }
+
+ if _, err := fmt.Fprintln(out); err != nil {
+ return err
+ }
+
+ return nil
}
diff --git a/html_test.go b/html_test.go
index 49b8ef8..d5284cc 100644
--- a/html_test.go
+++ b/html_test.go
@@ -1 +1,1541 @@
package textfmt_test
+
+import (
+ "bytes"
+ "code.squareroundforest.org/arpio/textfmt"
+ "testing"
+)
+
+func TestHTML(t *testing.T) {
+ t.Run("invalid", func(t *testing.T) {
+ var b bytes.Buffer
+ if err := textfmt.HTMLFragment(&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.HTMLFragment(&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.")),
+ 0,
+ 8,
+ ),
+ 30,
+ ),
+
+ textfmt.Title(1, "Document syntax:"),
+
+ textfmt.Indent(
+ textfmt.Syntax(
+ textfmt.Symbol("textfmt.Doc"),
+ textfmt.Symbol("("),
+ textfmt.ZeroOrMore(textfmt.Symbol("Entry")),
+ textfmt.Symbol(")"),
+ ),
+ 0,
+ 8,
+ ),
+
+ 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.HTML(&b, doc, "en"); err != nil {
+ t.Fatal(err)
+ }
+
+ const expect = `
+
+
+
+
+ Example Text
+
+
+ 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 "\n"+b.String() != expect {
+ t.Log("\n" + b.String())
+ t.Log(expect)
+ logBytes(t, "\n"+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... with invalid chars.")))
+ if err := textfmt.HTMLFragment(&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.Log(b.String())
+ t.Log(expect)
+ t.Fatal()
+ }
+ })
+
+ 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.HTMLFragment(&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.HTMLFragment(&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) {
+ var b bytes.Buffer
+ doc := textfmt.Doc(
+ textfmt.Wrap(textfmt.Paragraph(textfmt.Text("Some sample text...\n on multiple lines.")), 15),
+ )
+
+ if err := textfmt.HTMLFragment(&b, doc); err != nil {
+ t.Fatal(err)
+ }
+
+ const expect = `
+Some sample
+text...
+on multiple
+lines.
+`
+
+ if "\n"+b.String() != expect {
+ t.Fatal("\n" + 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.HTMLFragment(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.HTMLFragment(&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.HTMLFragment(&b, doc); err != nil {
+ t.Fatal(err)
+ }
+
+ if b.String() != "
\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.HTMLFragment(&b, doc); err != nil {
+ t.Fatal(err)
+ }
+
+ if b.String() != "a link
\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.HTMLFragment(&b, doc); err != nil {
+ t.Fatal(err)
+ }
+
+ if b.String() != "a link
\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.HTMLFragment(&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.HTMLFragment(&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.")), 12),
+ )
+
+ if err := textfmt.HTMLFragment(&b, doc); err != nil {
+ t.Fatal(err)
+ }
+
+ if b.String() != "This is a\nparagraph.\n
\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.HTMLFragment(&b, doc); err != nil {
+ t.Fatal(err)
+ }
+
+ const expect = `
+
+ - this is an item
+ - this is another item
+ - this is a third item
+
+`
+
+ if "\n"+b.String() != expect {
+ t.Fatal("\n" + 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")),
+ ),
+ 18,
+ ),
+ )
+
+ var b bytes.Buffer
+ if err := textfmt.HTMLFragment(&b, doc); err != nil {
+ t.Fatal(err)
+ }
+
+ const expect = `
+
+ - this
+ is an item
+
+ - this
+ is another
+ item
+ - this
+ is a third
+ item
+
+`
+
+ if "\n"+b.String() != expect {
+ t.Fatal("\n" + 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.HTMLFragment(&b, doc); err != nil {
+ t.Fatal(err)
+ }
+
+ const expect = `
+
+ - this is an item
+ - this is another item
+ - this is a third item
+
+`
+
+ if "\n"+b.String() != expect {
+ t.Fatal("\n" + 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")),
+ ),
+ 18,
+ ),
+ )
+
+ var b bytes.Buffer
+ if err := textfmt.HTMLFragment(&b, doc); err != nil {
+ t.Fatal(err)
+ }
+
+ const expect = `
+
+ - this
+ is an item
+
+ - this
+ is another
+ item
+ - this
+ is a third
+ item
+
+`
+
+ if "\n"+b.String() != expect {
+ t.Fatal("\n" + 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 twelfth item")),
+ ),
+ )
+
+ var b bytes.Buffer
+ if err := textfmt.HTMLFragment(&b, doc); err != nil {
+ t.Fatal(err)
+ }
+
+ const expect = `
+
+ - this is an item
+ - this is another item
+ - this is the third item
+ - this is the fourth item
+ - this is the fifth item
+ - this is the sixth item
+ - this is the seventh item
+ - this is the eighth item
+ - this is the nineth item
+ - this is the tenth item
+ - this is the eleventh item
+ - this is the twelfth item
+
+`
+
+ if "\n"+b.String() != expect {
+ t.Fatal("\n" + 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.HTMLFragment(&b, doc); err != nil {
+ t.Fatal(err)
+ }
+
+ const expect = `
+
+ - red
+ - looks like strawberry
+ - green
+ - looks like grass
+ - blue
+ - looks like sky
+
+`
+
+ if "\n"+b.String() != expect {
+ t.Fatal("\n" + 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.HTMLFragment(&b, doc); err != nil {
+ t.Fatal(err)
+ }
+
+ const expect = `
+
+ - red
+ - looks like
+ strawberry
+ - green
+ - looks like
+ grass
+ - blue
+ - looks like
+ sky
+
+`
+
+ if "\n"+b.String() != expect {
+ t.Fatal("\n" + 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.HTMLFragment(&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 "\n"+b.String() != expect {
+ t.Fatal("\n" + 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.HTMLFragment(&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 "\n"+b.String() != expect {
+ 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")),
+ textfmt.Definition(textfmt.Text("twelve"), textfmt.Text("this is the twelfth item")),
+ ),
+ )
+
+ var b bytes.Buffer
+ if err := textfmt.HTMLFragment(&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 twelfth item
+
+`
+
+ if "\n"+b.String() != expect {
+ t.Fatal("\n" + 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.HTMLFragment(&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.HTMLFragment(&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.HTMLFragment(&b, doc); err != nil {
+ t.Fatal(err)
+ }
+
+ const expect = `
+
+
+ | 1 |
+ 2 |
+ 3 |
+
+
+ | 4 |
+ 5 |
+ 6 |
+
+
+ | 7 |
+ 8 |
+ 9 |
+
+
+`
+
+ if "\n"+b.String() != expect {
+ t.Fatal("\n" + 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.HTMLFragment(&b, doc); err != nil {
+ t.Fatal(err)
+ }
+
+ const expect = `
+
+
+ | 1 |
+ -1 |
+ 0 |
+
+
+ | 1 |
+ 2 |
+ 3 |
+
+
+ | 4 |
+ 5 |
+ 6 |
+
+
+ | 7 |
+ 8 |
+ 9 |
+
+
+`
+
+ if "\n"+b.String() != expect {
+ t.Fatal("\n" + 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.HTMLFragment(&b, doc); err != nil {
+ t.Fatal(err)
+ }
+
+ const expect = `
+
+`
+
+ if "\n"+b.String() != expect {
+ t.Fatal("\n" + 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.HTMLFragment(&b, doc); err != nil {
+ t.Fatal(err)
+ }
+
+ const expect = `
+
+`
+
+ if "\n"+b.String() != expect {
+ t.Fatal("\n" + 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.HTMLFragment(&b, doc); err != nil {
+ t.Fatal(err)
+ }
+
+ const expect = `
+
+
+ | foo |
+ bar |
+ baz |
+
+
+ | 1 |
+ 2 |
+ 3 |
+
+
+`
+
+ if "\n"+b.String() != expect {
+ t.Fatal("\n" + 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.HTMLFragment(&b, doc); err != nil {
+ t.Fatal(err)
+ }
+
+ const expect = `
+
+
+ | 1 |
+
+
+ | 4 |
+
+
+ | 7 |
+
+
+`
+
+ if "\n"+b.String() != expect {
+ t.Fatal("\n" + 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.HTMLFragment(&b, doc); err != nil {
+ t.Fatal(err)
+ }
+
+ const expect = `
+
+
+ | foo |
+
+
+ | 1 |
+
+
+ | 4 |
+
+
+ | 7 |
+
+
+`
+
+ if "\n"+b.String() != expect {
+ t.Fatal("\n" + 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.HTMLFragment(&b, doc); err != nil {
+ t.Fatal(err)
+ }
+
+ const expect = `
+
+`
+
+ if "\n"+b.String() != expect {
+ t.Fatal("\n" + 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.HTMLFragment(&b, doc); err != nil {
+ t.Fatal(err)
+ }
+
+ const expect = `
+
+`
+
+ if "\n"+b.String() != expect {
+ t.Fatal("\n" + 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.HTMLFragment(&b, doc); err != nil {
+ t.Fatal(err)
+ }
+
+ const expect = `
+
+
+ | 1 |
+ 2 |
+ |
+
+
+ | 4 |
+ |
+ |
+
+
+ | 7 |
+ 8 |
+ 9 |
+
+
+`
+
+ if "\n"+b.String() != expect {
+ t.Fatal("\n" + 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.HTMLFragment(&b, doc); err != nil {
+ t.Fatal(err)
+ }
+
+ const expect = `
+
+
+ | 1 |
+ -1 |
+ |
+
+
+ | 1 |
+ 2 |
+ 3 |
+
+
+ | 4 |
+ |
+ |
+
+
+ |
+ 8 |
+ 9 |
+
+
+`
+
+ if "\n"+b.String() != expect {
+ t.Fatal("\n" + 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.HTMLFragment(&b, doc); err != nil {
+ t.Fatal(err)
+ }
+
+ const expect = `
+
+
+ |
+ |
+
+
+ |
+ |
+
+
+ |
+ |
+
+
+`
+
+ if "\n"+b.String() != expect {
+ t.Fatal("\n" + b.String())
+ }
+ })
+
+ t.Run("wrap", 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.HTMLFragment(&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 "\n"+b.String() != expect {
+ logBytes(t, expect)
+ logBytes(t, "\n"+b.String())
+ t.Log("\n" + expect)
+ t.Fatal("\n" + "\n" + b.String())
+ }
+ })
+ })
+
+ t.Run("code", func(t *testing.T) {
+ t.Run("simple", 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.HTMLFragment(&b, textfmt.Doc(textfmt.CodeBlock(code))); err != nil {
+ t.Fatal(err)
+ }
+
+ if b.String() != "\n"+code+"\n
\n" {
+ 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.HTMLFragment(&b, textfmt.Doc(textfmt.Wrap(textfmt.CodeBlock(code), 12))); err != nil {
+ t.Fatal(err)
+ }
+
+ if b.String() != "\n"+code+"\n
\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.HTMLFragment(&b, doc); err != nil {
+ t.Fatal(err)
+ }
+
+ if b.String() != "\nfoo\n
\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.HTMLFragment(&b, doc); err != nil {
+ t.Fatal(err)
+ }
+
+ if b.String() != "\n[foo]...\n
\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.HTMLFragment(&b, doc); err != nil {
+ t.Fatal(err)
+ }
+
+ if b.String() != "\n<foo>...\n
\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.HTMLFragment(&b, doc); err != nil {
+ t.Fatal(err)
+ }
+
+ if b.String() != "\n<foo>\n
\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.HTMLFragment(&b, doc); err != nil {
+ t.Fatal(err)
+ }
+
+ if b.String() != "\n[foo]\n
\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.HTMLFragment(&b, doc); err != nil {
+ t.Fatal(err)
+ }
+
+ if b.String() != "\nfoo bar baz\n
\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.HTMLFragment(&b, doc); err != nil {
+ t.Fatal(err)
+ }
+
+ if b.String() != "\nfoo bar baz\n
\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.HTMLFragment(&b, doc); err != nil {
+ t.Fatal(err)
+ }
+
+ if b.String() != "\ncorge (foo bar baz) garply\n
\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.HTMLFragment(&b, doc); err != nil {
+ t.Fatal(err)
+ }
+
+ if b.String() != "\nfoo\nbar\nbaz\n
\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.HTMLFragment(&b, doc); err != nil {
+ t.Fatal(err)
+ }
+
+ if b.String() != "\ncorge (foo|bar|baz) garply\n
\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.HTMLFragment(&b, doc); err != nil {
+ t.Fatal(err)
+ }
+
+ if b.String() != "\nfoo [options]... <filename> [string|number]...\n
\n" {
+ t.Fatal(b.String())
+ }
+ })
+ })
+}