From ace14e534cd64fcf3eb16c02d5ee3aa144d22742 Mon Sep 17 00:00:00 2001 From: Arpad Ryszka Date: Thu, 23 Oct 2025 02:55:38 +0200 Subject: [PATCH] roff --- lib.go | 8 +- notes.txt | 5 + runoff.go | 236 ++++- runoff_test.go | 2300 ++++++++++++++++++++++++++++++++++++++++++++++ teletype.go | 1 + teletype_test.go | 36 +- text.go | 2 + write.go | 18 +- 8 files changed, 2579 insertions(+), 27 deletions(-) create mode 100644 notes.txt create mode 100644 runoff_test.go diff --git a/lib.go b/lib.go index 930cefe..d8a9b47 100644 --- a/lib.go +++ b/lib.go @@ -141,7 +141,7 @@ func Title(level int, text string, manInfo ...TitleInfo) Entry { return e } -func ManualSection(s int) TitleInfo { +func ManSection(s int) TitleInfo { return TitleInfo{section: s} } @@ -153,7 +153,7 @@ func ReleaseVersion(v string) TitleInfo { return TitleInfo{version: v} } -func ManualCategory(c string) TitleInfo { +func ManCategory(c string) TitleInfo { return TitleInfo{category: c} } @@ -275,8 +275,8 @@ func Teletype(out io.Writer, d Document) error { // // Text is always wrapped, or as controlled by the roff processor, except for tables. The Wrap instrunction has // no effect, except for tables. -func Runoff(io.Writer, Document) error { - return nil +func Runoff(out io.Writer, d Document) error { + return renderRoff(out, d) } func Markdown(io.Writer, Document) error { diff --git a/notes.txt b/notes.txt new file mode 100644 index 0000000..c1b8daa --- /dev/null +++ b/notes.txt @@ -0,0 +1,5 @@ +indentation for syntax in tty and roff +verify if empty document doesn't print even an empty line. E.g. empty table, empty paragraph. What should those +print? +does the table need the non-breaking space for the filling in roff? +indentation for syntax may not require non-break spaces diff --git a/runoff.go b/runoff.go index b4b5161..3104766 100644 --- a/runoff.go +++ b/runoff.go @@ -5,6 +5,8 @@ import ( "errors" "time" "fmt" + "bytes" + "strings" ) func escapeRoff(s string, additional ...string) string { @@ -98,6 +100,22 @@ func roffDefinitionNames(d []DefinitionItem) []string { return n } +func roffCellTexts(r []TableRow) ([][]string, error) { + var cellTexts [][]string + for _, row := range r { + var c []string + for _, cell := range row.cells { + var b bytes.Buffer + renderRoffText(&roffWriter{w: &b, internal: true}, cell.text) + c = append(c, b.String()) + } + + cellTexts = append(cellTexts, c) + } + + return cellTexts, nil +} + func renderRoffText(w writer, text Txt, additionalEscape ...string) { if len(text.cat) > 0 { for i, tc := range text.cat { @@ -125,10 +143,8 @@ func renderRoffText(w writer, text Txt, additionalEscape ...string) { if text.link != "" { if text.text != "" { - w.write(text.text) renderRoffString(w, text.text, additionalEscape...) w.write(" (") - w.write(text.link) renderRoffString(w, text.link, additionalEscape...) w.write(")") return @@ -143,12 +159,12 @@ func renderRoffText(w writer, text Txt, additionalEscape ...string) { func renderRoffTitle(w writer, e Entry) { if e.titleLevel != 0 || e.man.section == 0 { + e.text.bold = true p := Entry{ typ: paragraph, text: e.text, indent: e.indent, indentFirst: e.indentFirst, - bold: true, } renderRoffParagraph(w, p) @@ -172,17 +188,17 @@ func renderRoffTitle(w writer, e Entry) { } func renderRoffParagraph(w writer, e Entry) { - w.write(".in ", e.indent, "\n.tin ", e.indent + e.indentFirst, "\n") + w.write(".in ", e.indent, "\n.ti ", e.indent + e.indentFirst, "\n") renderRoffText(w, e.text) } func renderRoffList(w writer, e Entry) { for i, item := range e.items { if i > 0 { - w.write(".br\n") + w.write("\n.br\n") } - w.write(".in ", e.indent + 2, "\n.tin ", e.indent + e.indentFirst, "\n") + w.write(".in ", e.indent + 2, "\n.ti ", e.indent + e.indentFirst, "\n") w.write("\\(bu ") renderRoffText(w, item.text) } @@ -192,10 +208,10 @@ func renderRoffNumberedList(w writer, e Entry) { maxDigits := numDigits(len(e.items)) for i, item := range e.items { if i > 0 { - w.write(".br\n") + w.write("\n.br\n") } - w.write(".in ", e.indent + maxDigits + 2, "\n.tin ", e.indent + e.indentFirst, "\n") + w.write(".in ", e.indent + maxDigits + 2, "\n.ti ", e.indent + e.indentFirst, "\n") w.write(padRight(fmt.Sprintf("%d.", i + 1), maxDigits + 2)) renderRoffText(w, item.text) } @@ -206,10 +222,10 @@ func renderRoffDefinitions(w writer, e Entry) { maxNameLength := maxLength(names) for i, definition := range e.definitions { if i > 0 { - w.write(".br\n") + w.write("\n.br\n") } - w.write(".in ", e.indent + maxNameLength + 4, "\n.tin ", e.indent + e.indentFirst, "\n") + w.write(".in ", e.indent + maxNameLength + 4, "\n.ti ", e.indent + e.indentFirst, "\n") w.write("\\(bu ") renderRoffText(w, definition.name) w.write(":", timesn("\\~", maxNameLength - len([]rune(names[i])) + 1)) @@ -223,10 +239,10 @@ func renderRoffNumberedDefinitions(w writer, e Entry) { maxNameLength := maxLength(names) for i, definition := range e.definitions { if i > 0 { - w.write(".br\n") + w.write("\n.br\n") } - w.write(".in ", e.indent + maxDigits + maxNameLength + 4, "\n.tin ", e.indent + e.indentFirst, "\n") + w.write(".in ", e.indent + maxDigits + maxNameLength + 4, "\n.ti ", e.indent + e.indentFirst, "\n") w.write(padRight(fmt.Sprintf("%d.", i + 1), maxDigits + 2)) renderRoffText(w, definition.name) w.write(":", timesn("\\~", maxNameLength - len([]rune(names[i])) + 1)) @@ -235,12 +251,208 @@ func renderRoffNumberedDefinitions(w writer, e Entry) { } func renderRoffTable(w writer, e Entry) { + if len(e.rows) == 0 { + return + } + + e.rows = normalizeTable(e.rows) + if len(e.rows[0].cells) == 0 { + return + } + + w.write(".nf\n") + defer w.write("\n.fi") + + cellTexts, err := roffCellTexts(e.rows) + if err != nil { + w.setErr(err) + } + + totalSeparatorWidth := (len(cellTexts[0]) - 1) * 3 + if e.wrapWidth > 0 { + allocatedWidth := e.wrapWidth - e.indent - totalSeparatorWidth + columnWeights := columnWeights(cellTexts) + targetColumnWidths := targetColumnWidths(allocatedWidth, columnWeights) + for i := range cellTexts { + for j := range cellTexts[i] { + cellTexts[i][j] = wrap(cellTexts[i][j], targetColumnWidths[j], 0, 0) + } + } + } + + columnWidths := columnWidths(cellTexts) + totalWidth := totalSeparatorWidth + for i := range columnWidths { + totalWidth += columnWidths[i] + } + + hasHeader := e.rows[0].header + for i := range cellTexts { + if i > 0 { + sep := "-" + if hasHeader && i == 1 { + sep = "=" + } + + w.write("\n") + w.write(timesn(" ", e.indent), timesn(sep, totalWidth)) + w.write("\n") + } + + lines := make([][]string, len(cellTexts[i])) + for j := range cellTexts[i] { + lines[j] = strings.Split(cellTexts[i][j], "\n") + } + + var maxLines int + for j := range lines { + if len(lines[j]) > maxLines { + maxLines = len(lines[j]) + } + } + + for k := 0; k < maxLines; k++ { + if k > 0 { + w.write("\n") + } + + for j := range lines { + if j == 0 { + w.write(timesn(" ", e.indent)) + } else { + w.write(" | ") + } + + 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("\n", timesn("=", totalWidth)) + } } func renderRoffCode(w writer, e Entry) { + w.write(".nf\n") + defer w.write("\n.fi") + e.text.text = escapeRoff(e.text.text) + writeLines(w, e.text.text, e.indent, e.indent) +} + +func renderRoffMultiple(w writer, s SyntaxItem) { + s.topLevel = false + s.multiple = false + renderRoffSyntaxItem(w, s) + w.write("...") +} + +func renderRoffRequired(w writer, s SyntaxItem) { + s.delimited = true + s.topLevel = false + s.required = false + w.write("<") + renderRoffSyntaxItem(w, s) + w.write(">") +} + +func renderRoffOptional(w writer, s SyntaxItem) { + s.delimited = true + s.topLevel = false + s.optional = false + w.write("[") + renderRoffSyntaxItem(w, s) + w.write("]") +} + +func renderRoffSequence(w writer, s SyntaxItem) { + if !s.delimited && !s.topLevel { + w.write("(") + } + + for i, item := range s.sequence { + if i > 0 { + w.write(" ") + } + + item.delimited = false + renderRoffSyntaxItem(w, item) + } + + if !s.delimited && !s.topLevel { + w.write(")") + } +} + +func renderRoffChoice(w writer, s SyntaxItem) { + if !s.delimited && !s.topLevel { + w.write("(") + } + + for i, item := range s.choice { + if i > 0 { + separator := "|" + if s.topLevel { + separator = "\n" + } + + w.write(separator) + } + + item.delimited = false + renderRoffSyntaxItem(w, item) + } + + if !s.delimited && !s.topLevel { + w.write(")") + } +} + +func renderRoffSymbol(w writer, s SyntaxItem) { + w.write(escapeRoff(s.symbol)) +} + +func renderRoffSyntaxItem(w writer, s SyntaxItem) { + switch { + + // foo... + case s.multiple: + renderRoffMultiple(w, s) + + // + case s.required: + renderRoffRequired(w, s) + + // [foo] + case s.optional: + renderRoffOptional(w, s) + + // foo bar baz or (foo bar baz) + case len(s.sequence) > 0: + renderRoffSequence(w, s) + + // foo|bar|baz or (foo|bar|baz) + case len(s.choice) > 0: + renderRoffChoice(w, s) + + // foo + default: + renderRoffSymbol(w, s) + } } func renderRoffSyntax(w writer, e Entry) { + s := e.syntax + s.topLevel = true + w.write(".nf\n") + defer w.write("\n.fi") + w.write(timesn("\u00a0", e.indent + e.indentFirst)) + renderRoffSyntaxItem(w, s) } func renderRoff(out io.Writer, d Document) error { diff --git a/runoff_test.go b/runoff_test.go new file mode 100644 index 0000000..9d06bb3 --- /dev/null +++ b/runoff_test.go @@ -0,0 +1,2300 @@ +package textfmt_test + +import ( + "bytes" + "code.squareroundforest.org/arpio/textfmt" + "testing" +) + +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(")"), + ), + 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.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 +\~\~\~\~\~\~\~\~textfmt.Doc ( [Entry]... ) +.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) { + doc := textfmt.Doc( + textfmt.Title(0, "Example Text", textfmt.ManSection(1)), + + 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(")"), + ), + 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.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 = `.TH "Example Text" 1 "" "" "" +.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 +\~\~\~\~\~\~\~\~textfmt.Doc ( [Entry]... ) +.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) { + 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) + } + + const expect = `.in 0 +.ti 0 +\fBThis is a title\fR +` + + if b.String() != expect { + 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.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")), + textfmt.Item(textfmt.Text("this is the twelveth item")), + ), + ) + + 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 +12.\~this is the twelveth item +` + + 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 +` + + if "\n" + b.String() != expect { + 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 +` + + if "\n" + b.String() != expect { + 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 +` + + if "\n" + b.String() != expect { + 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 +` + + 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 twelveth item")), + ), + ) + + 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 +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.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) + } + + if b.String() != ".nf\n" + code+"\n.fi\n" { + 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) + } + + if b.String() != ".nf\n" + code+"\n.fi\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.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...\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\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]... [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) + } + + if b.String() != ".nf\n\\~\\~\\~\\~foo [options]... [string|number]...\n.fi\n" { + t.Fatal(b.String()) + } + }) + }) +} diff --git a/teletype.go b/teletype.go index 57b1fc4..ea0fc3c 100644 --- a/teletype.go +++ b/teletype.go @@ -400,6 +400,7 @@ func renderTTYSyntaxItem(w writer, s SyntaxItem) { func renderTTYSyntax(w writer, e Entry) { s := e.syntax s.topLevel = true + w.write(timesn(" ", e.indent + e.indentFirst)) renderTTYSyntaxItem(w, s) } diff --git a/teletype_test.go b/teletype_test.go index 0519bea..56335b3 100644 --- a/teletype_test.go +++ b/teletype_test.go @@ -125,7 +125,7 @@ text items. Document syntax: -textfmt.Doc ( [Entry]... ) + textfmt.Doc ( [Entry]... ) Entries: @@ -537,8 +537,8 @@ Entry explanations: textfmt.Item(textfmt.Text("this is another item")), textfmt.Item(textfmt.Text("this is a third item")), ), - 0, 4, + 0, ), ) @@ -688,8 +688,8 @@ third item textfmt.Item(textfmt.Text("this is another item")), textfmt.Item(textfmt.Text("this is a third item")), ), - 0, 4, + 0, ), ) @@ -880,8 +880,8 @@ third item textfmt.Definition(textfmt.Text("green"), textfmt.Text("looks like grass")), textfmt.Definition(textfmt.Text("blue"), textfmt.Text("looks like sky")), ), - 0, 4, + 0, ), ) @@ -1953,5 +1953,33 @@ and silver birch | their canopies creating | and shadow on the } }) + 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.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 0e0ed0d..856302a 100644 --- a/text.go +++ b/text.go @@ -3,6 +3,8 @@ package textfmt import ( "strings" "unicode" + "fmt" + "bytes" ) func timesn(s string, n int) string { diff --git a/write.go b/write.go index 08136fc..25a6c0b 100644 --- a/write.go +++ b/write.go @@ -19,7 +19,7 @@ type ttyWriter struct { } type roffWriter struct { - w *bufio.Writer + w io.Writer internal bool err error } @@ -63,13 +63,17 @@ func (w *roffWriter) write(a ...any) { var rr []rune s := fmt.Sprint(ai) r := []rune(s) - for i := range r { - if r[i] == '\u00a0' { - rr = append(rr, []rune("\\~")...) - continue - } + if w.internal { + rr = r + } else { + for i := range r { + if r[i] == '\u00a0' { + rr = append(rr, []rune("\\~")...) + continue + } - rr = append(rr, r[i]) + rr = append(rr, r[i]) + } } if _, err := w.w.Write([]byte(string(rr))); err != nil {