diff --git a/lib.go b/lib.go index bf403d9..4baff9e 100644 --- a/lib.go +++ b/lib.go @@ -197,6 +197,7 @@ func Row(cells ...TableCell) TableRow { return TableRow{cells: cells} } +// when no header and markdown, header automatic func Table(rows ...TableRow) Entry { return Entry{typ: table, rows: rows} } @@ -279,8 +280,8 @@ func Runoff(out io.Writer, d Document) error { return renderRoff(out, d) } -func Markdown(io.Writer, Document) error { - return nil +func Markdown(out io.Writer, d Document) error { + return renderMarkdown(out, d) } func HTML(io.Writer, Document) error { diff --git a/markdown.go b/markdown.go new file mode 100644 index 0000000..6d0187e --- /dev/null +++ b/markdown.go @@ -0,0 +1,460 @@ +package textfmt + +import ( + "bytes" + "errors" + "fmt" + "io" + "slices" + "strings" +) + +func escapeMarkdown(s string, additional ...rune) string { + var ( + rr []rune + isNumberOnNewLine bool + isLinkOpen, isLinkClosed, isLinkLabel bool + ) + + isNewLine := true + r := []rune(s) + for _, ri := range r { + switch ri { + case '\\', '`', '*', '_', '[', ']', '#', '<', '>': + rr = append(rr, '\\', ri) + default: + switch { + case isNewLine: + switch ri { + case '+', '-': + rr = append(rr, '\\', ri) + default: + rr = append(rr, ri) + } + case isNumberOnNewLine: + switch ri { + case '.': + rr = append(rr, '\\', ri) + default: + rr = append(rr, ri) + } + case isLinkClosed: + switch ri { + case '(': + rr = append(rr, '\\', ri) + default: + rr = append(rr, ri) + } + case isLinkLabel: + switch ri { + case ')': + rr = append(rr, '\\', ri) + default: + rr = append(rr, ri) + } + default: + if slices.Contains(additional, ri) { + rr = append(rr, '\\', ri) + } else { + rr = append(rr, ri) + } + } + } + + isNumberOnNewLine = (isNewLine || isNumberOnNewLine) && ri >= '0' && ri <= '9' + isNewLine = ri == '\n' + isLinkOpen = !isLinkLabel && ri == '[' || isLinkOpen && ri != ']' + isLinkClosed = isLinkOpen && ri == ']' + isLinkLabel = isLinkClosed && ri == '(' || isLinkLabel && ri != ')' + } + + return string(rr) +} + +func mdTextToString(text Txt) (string, error) { + var b bytes.Buffer + w := mdWriter{w: &b, internal: true} + renderMDText(&w, text) + if w.err != nil { + return "", w.err + } + + return b.String(), nil +} + +func mdCellTexts(rows []TableRow) ([][]string, error) { + var texts [][]string + for _, row := range rows { + var rowTexts []string + for _, cell := range row.cells { + txt, err := mdTextToString(cell.text) + if err != nil { + return nil, err + } + + rowTexts = append(rowTexts, txt) + } + + texts = append(texts, rowTexts) + } + + return texts, nil +} + +func mdEnsureHeaderTexts(h []string) []string { + var hh []string + for _, t := range h { + if strings.TrimSpace(t) == "" { + t = "\\-" + } + + hh = append(hh, t) + } + + return hh +} + +func renderMDText(w writer, text Txt) { + if len(text.cat) > 0 { + for i, tc := range text.cat { + if i > 0 { + w.write(" ") + } + + renderMDText(w, tc) + } + + return + } + + text.text = singleLine(text.text) + text.text = escapeMarkdown(text.text) + text.link = singleLine(text.link) + text.link = escapeMarkdown(text.link) + if text.bold { + w.write("**") + } + + if text.italic { + w.write("_") + } + + defer func() { + if text.italic { + w.write("_") + } + + if text.bold { + w.write("**") + } + }() + + if text.link != "" { + if text.text != "" { + w.write("[") + w.write(text.text) + w.write("](") + w.write(text.link) + w.write(")") + return + } + + w.write(text.link) + return + } + + w.write(text.text) +} + +func renderMDTitle(w writer, e Entry) { + hashes := e.titleLevel + 1 + if hashes > 6 { + hashes = 6 + } + + w.write(timesn("#", hashes), " ") + renderMDText(w, e.text) +} + +func renderMDParagraphIndent(w writer, e Entry) { + txt, err := mdTextToString(e.text) + if err != nil { + w.setErr(err) + } + + indentFirst := e.indent + e.indentFirst + if e.wrapWidth > 0 { + txt = wrap(txt, e.wrapWidth, indentFirst, e.indent) + } + + writeLines(w, txt, indentFirst, e.indent) +} + +func renderMDParagraph(w writer, e Entry) { + e.indent = 0 + e.indentFirst = 0 + renderMDParagraphIndent(w, e) +} + +func renderMDList(w writer, e Entry) { + e.indent = 2 + e.indentFirst = -2 + if e.wrapWidth > 2 { + e.wrapWidth -= 2 + } + + for i, item := range e.items { + if i > 0 { + w.write("\n") + } + + w.write("- ") + p := itemToParagraph(e, item.text) + renderMDParagraphIndent(w, p) + } +} + +func renderMDNumberedList(w writer, e Entry) { + maxDigits := numDigits(len(e.items)) + e.indent = maxDigits + 2 + e.indentFirst = 0 - maxDigits - 2 + if e.wrapWidth > maxDigits+2 { + e.wrapWidth -= maxDigits + 2 + } + + for i, item := range e.items { + if i > 0 { + w.write("\n") + } + + w.write(padRight(fmt.Sprintf("%d.", i+1), maxDigits+2)) + p := itemToParagraph(e, item.text) + renderMDParagraphIndent(w, p) + } +} + +func renderMDDefinitions(w writer, e Entry) { + for _, d := range e.definitions { + e.items = append( + e.items, + Item(Cat(Text(fmt.Sprintf("%s:", d.name.text)), d.value)), + ) + } + + renderMDList(w, e) +} + +func renderMDNumberedDefinitions(w writer, e Entry) { + for _, d := range e.definitions { + e.items = append( + e.items, + Item(Cat(Text(fmt.Sprintf("%s:", d.name.text)), d.value)), + ) + } + + renderMDNumberedList(w, e) +} + +func renderMDTable(w writer, e Entry) { + e.rows = normalizeTable(e.rows) + e.rows = ensureHeader(e.rows) + if len(e.rows) == 0 || len(e.rows[0].cells) == 0 { + return + } + + headerTexts, err := mdCellTexts(e.rows[:1]) + if err != nil { + w.setErr(err) + return + } + + cellTexts, err := mdCellTexts(e.rows[1:]) + if err != nil { + w.setErr(err) + return + } + + headerTexts[0] = mdEnsureHeaderTexts(headerTexts[0]) + columns := columnWidths(headerTexts) + cellColumns := columnWidths(cellTexts) + if len(cellColumns) > 0 { + for i := range columns { + if cellColumns[i] > columns[i] { + columns[i] = cellColumns[i] + } + } + } + + w.write("|") + for i, h := range headerTexts[0] { + w.write(" ", padRight(h, columns[i])) + w.write(" |") + } + + w.write("\n|") + for _, c := range columns { + w.write(timesn("-", c+1)) + w.write("-|") + } + + for _, row := range cellTexts { + w.write("\n|") + for i, cell := range row { + w.write(" ", padRight(cell, columns[i])) + w.write(" |") + } + } +} + +func renderMDCode(w writer, e Entry) { + w.write("```\n") + w.write(e.text.text) + w.write("\n```") +} + +func renderMDMultiple(w writer, s SyntaxItem) { + s.topLevel = false + s.multiple = false + renderMDSyntaxItem(w, s) + w.write("...") +} + +func renderMDRequired(w writer, s SyntaxItem) { + s.delimited = true + s.topLevel = false + s.required = false + w.write("<") + renderMDSyntaxItem(w, s) + w.write(">") +} + +func renderMDOptional(w writer, s SyntaxItem) { + s.delimited = true + s.topLevel = false + s.optional = false + w.write("[") + renderMDSyntaxItem(w, s) + w.write("]") +} + +func renderMDSequence(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 + renderMDSyntaxItem(w, item) + } + + if !s.delimited && !s.topLevel { + w.write(")") + } +} + +func renderMDChoice(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 + renderMDSyntaxItem(w, item) + } + + if !s.delimited && !s.topLevel { + w.write(")") + } +} + +func renderMDSymbol(w writer, s SyntaxItem) { + w.write(escapeTeletype(s.symbol)) +} + +func renderMDSyntaxItem(w writer, s SyntaxItem) { + switch { + + // foo... + case s.multiple: + renderMDMultiple(w, s) + + // + case s.required: + renderMDRequired(w, s) + + // [foo] + case s.optional: + renderMDOptional(w, s) + + // foo bar baz or (foo bar baz) + case len(s.sequence) > 0: + renderMDSequence(w, s) + + // foo|bar|baz or (foo|bar|baz) + case len(s.choice) > 0: + renderMDChoice(w, s) + + // foo + default: + renderMDSymbol(w, s) + } +} + +func renderMDSyntax(w writer, e Entry) { + s := e.syntax + s.topLevel = true + w.write("```\n") + renderMDSyntaxItem(w, s) + w.write("\n```") +} + +func renderMarkdown(out io.Writer, d Document) error { + w := mdWriter{w: out} + for i, e := range d.entries { + if i > 0 { + w.write("\n\n") + } + + switch e.typ { + case invalid: + return errors.New("invalid entry") + case title: + renderMDTitle(&w, e) + case paragraph: + renderMDParagraph(&w, e) + case list: + renderMDList(&w, e) + case numberedList: + renderMDNumberedList(&w, e) + case definitions: + renderMDDefinitions(&w, e) + case numberedDefinitions: + renderMDNumberedDefinitions(&w, e) + case table: + renderMDTable(&w, e) + case code: + renderMDCode(&w, e) + case syntax: + renderMDSyntax(&w, e) + } + } + + if len(d.entries) > 0 { + w.write("\n") + } + + return w.err +} diff --git a/markdown_test.go b/markdown_test.go new file mode 100644 index 0000000..cb02e9f --- /dev/null +++ b/markdown_test.go @@ -0,0 +1,1490 @@ +package textfmt_test + +import ( + "bytes" + "code.squareroundforest.org/arpio/textfmt" + "testing" +) + +func TestMarkdown(t *testing.T) { + t.Run("invalid", func(t *testing.T) { + t.Fatal() + }) + + t.Run("empty", func(t *testing.T) { + t.Fatal() + }) + + t.Run("example", func(t *testing.T) { + t.Fatal() + }) + + t.Run("write error", func(t *testing.T) { + t.Fatal() + }) + + t.Run("title", func(t *testing.T) { + t.Run("top level", func(t *testing.T) { + doc := textfmt.Doc(textfmt.Title(0, "This is a title")) + + var b bytes.Buffer + if err := textfmt.Markdown(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "# This is a title\n" { + t.Fatal(b.String()) + } + }) + + t.Run("second level", func(t *testing.T) { + doc := textfmt.Doc(textfmt.Title(1, "This is a title")) + + var b bytes.Buffer + if err := textfmt.Markdown(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "## This is a title\n" { + t.Fatal(b.String()) + } + }) + + t.Run("fourth level", func(t *testing.T) { + doc := textfmt.Doc(textfmt.Title(3, "This is a title")) + + var b bytes.Buffer + if err := textfmt.Markdown(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "#### This is a title\n" { + t.Fatal(b.String()) + } + }) + + t.Run("sixth level", func(t *testing.T) { + doc := textfmt.Doc(textfmt.Title(5, "This is a title")) + + var b bytes.Buffer + if err := textfmt.Markdown(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "###### This is a title\n" { + t.Fatal(b.String()) + } + }) + + t.Run("overflow level", func(t *testing.T) { + doc := textfmt.Doc(textfmt.Title(6, "This is a title")) + + var b bytes.Buffer + if err := textfmt.Markdown(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "###### This is a title\n" { + t.Fatal(b.String()) + } + }) + + t.Run("indent ignored", func(t *testing.T) { + doc := textfmt.Doc(textfmt.Indent(textfmt.Title(1, "This is a title"), 4, 0)) + + var b bytes.Buffer + if err := textfmt.Markdown(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "## This is a title\n" { + t.Fatal(b.String()) + } + }) + + t.Run("wrap ignored", func(t *testing.T) { + doc := textfmt.Doc(textfmt.Wrap(textfmt.Title(1, "This is a title"), 6)) + + var b bytes.Buffer + if err := textfmt.Markdown(&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("simple", func(t *testing.T) { + doc := textfmt.Doc(textfmt.Paragraph(textfmt.Text("This is a sample paragraph."))) + + var b bytes.Buffer + if err := textfmt.Markdown(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "This is a sample paragraph.\n" { + t.Fatal(b.String()) + } + }) + + t.Run("wrap", func(t *testing.T) { + doc := textfmt.Doc(textfmt.Wrap(textfmt.Paragraph(textfmt.Text("This is a sample paragraph.")), 6)) + + var b bytes.Buffer + if err := textfmt.Markdown(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "This\nis a\nsample\nparagraph.\n" { + t.Fatal(b.String()) + } + }) + + t.Run("ignore indent", func(t *testing.T) { + doc := textfmt.Doc(textfmt.Indent(textfmt.Paragraph(textfmt.Text("This is a sample paragraph.")), 4, 0)) + + var b bytes.Buffer + if err := textfmt.Markdown(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "This is a sample paragraph.\n" { + t.Fatal(b.String()) + } + }) + + t.Run("cat", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Paragraph( + textfmt.Cat( + textfmt.Text("This is"), + textfmt.Text("a sample"), + textfmt.Text("paragraph."), + ), + ), + ) + + var b bytes.Buffer + if err := textfmt.Markdown(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "This is a sample paragraph.\n" { + t.Fatal(b.String()) + } + }) + + t.Run("bold", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Paragraph( + textfmt.Cat( + textfmt.Text("This is"), + textfmt.Bold(textfmt.Text("a sample")), + textfmt.Text("paragraph."), + ), + ), + ) + + var b bytes.Buffer + if err := textfmt.Markdown(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "This is **a sample** paragraph.\n" { + t.Fatal(b.String()) + } + }) + + t.Run("italic", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Paragraph( + textfmt.Cat( + textfmt.Text("This is"), + textfmt.Italic(textfmt.Text("a sample")), + textfmt.Text("paragraph."), + ), + ), + ) + + var b bytes.Buffer + if err := textfmt.Markdown(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "This is _a sample_ paragraph.\n" { + t.Fatal(b.String()) + } + }) + + t.Run("bold italic", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Paragraph( + textfmt.Cat( + textfmt.Italic(textfmt.Text("This is")), + textfmt.Italic(textfmt.Bold(textfmt.Text("a sample"))), + textfmt.Italic(textfmt.Text("paragraph.")), + ), + ), + ) + + var b bytes.Buffer + if err := textfmt.Markdown(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "_This is_ **_a sample_** _paragraph._\n" { + t.Fatal(b.String()) + } + }) + + t.Run("escape", func(t *testing.T) { + t.Fatal() + }) + + t.Run("escape link", func(t *testing.T) { + t.Fatal() + }) + + t.Run("escape negative number on line start", func(t *testing.T) { + t.Fatal() + }) + + t.Run("escape year on line start", func(t *testing.T) { + t.Fatal() + }) + }) + + t.Run("list", func(t *testing.T) { + t.Run("simple", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.List( + textfmt.Item(textfmt.Text("item one")), + textfmt.Item(textfmt.Text("item two")), + textfmt.Item(textfmt.Text("item three")), + ), + ) + + var b bytes.Buffer + if err := textfmt.Markdown(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = ` +- item one +- item two +- item three +` + + if "\n"+b.String() != expect { + t.Fatal("\n" + b.String()) + } + }) + + t.Run("indent ignored", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Indent( + textfmt.List( + textfmt.Item(textfmt.Text("item one")), + textfmt.Item(textfmt.Text("item two")), + textfmt.Item(textfmt.Text("item three")), + ), + 4, + 0, + ), + ) + + var b bytes.Buffer + if err := textfmt.Markdown(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = ` +- item one +- item two +- item three +` + + 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("item one")), + textfmt.Item(textfmt.Text("item two")), + textfmt.Item(textfmt.Text("item three")), + ), + 6, + ), + ) + + var b bytes.Buffer + if err := textfmt.Markdown(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = ` +- item + one +- item + two +- item + three +` + + if "\n"+b.String() != expect { + t.Fatal("\n" + 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("item one")), + textfmt.Item(textfmt.Text("item two")), + textfmt.Item(textfmt.Text("item three")), + ), + ) + + var b bytes.Buffer + if err := textfmt.Markdown(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = ` +1. item one +2. item two +3. item three +` + + if "\n"+b.String() != expect { + logBytes(t, expect) + logBytes(t, "\n"+b.String()) + t.Log(expect) + t.Fatal("\n" + b.String()) + } + }) + + t.Run("indent ignored", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Indent( + textfmt.NumberedList( + textfmt.Item(textfmt.Text("item one")), + textfmt.Item(textfmt.Text("item two")), + textfmt.Item(textfmt.Text("item three")), + ), + 4, + 0, + ), + ) + + var b bytes.Buffer + if err := textfmt.Markdown(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = ` +1. item one +2. item two +3. item three +` + + if "\n"+b.String() != expect { + logBytes(t, expect) + logBytes(t, "\n"+b.String()) + t.Log(expect) + t.Fatal("\n" + b.String()) + } + }) + + t.Run("wrap", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Wrap( + textfmt.NumberedList( + textfmt.Item(textfmt.Text("item one")), + textfmt.Item(textfmt.Text("item two")), + textfmt.Item(textfmt.Text("item three")), + ), + 9, + ), + ) + + var b bytes.Buffer + if err := textfmt.Markdown(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = ` +1. item + one +2. item + two +3. item + three +` + + if "\n"+b.String() != expect { + logBytes(t, expect) + logBytes(t, "\n"+b.String()) + t.Log(expect) + t.Fatal("\n" + b.String()) + } + }) + + t.Run("long list", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Wrap( + textfmt.NumberedList( + textfmt.Item(textfmt.Text("item one")), + textfmt.Item(textfmt.Text("item two")), + textfmt.Item(textfmt.Text("item three")), + textfmt.Item(textfmt.Text("item four")), + textfmt.Item(textfmt.Text("item five")), + textfmt.Item(textfmt.Text("item six")), + textfmt.Item(textfmt.Text("item seven")), + textfmt.Item(textfmt.Text("item eight")), + textfmt.Item(textfmt.Text("item nine")), + textfmt.Item(textfmt.Text("item ten")), + textfmt.Item(textfmt.Text("item eleven")), + textfmt.Item(textfmt.Text("item twelve")), + ), + 9, + ), + ) + + var b bytes.Buffer + if err := textfmt.Markdown(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = ` +1. item + one +2. item + two +3. item + three +4. item + four +5. item + five +6. item + six +7. item + seven +8. item + eight +9. item + nine +10. item + ten +11. item + eleven +12. item + twelve +` + + if "\n"+b.String() != expect { + logBytes(t, expect) + logBytes(t, "\n"+b.String()) + t.Log(expect) + t.Fatal("\n" + b.String()) + } + }) + }) + + t.Run("definitions", 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.Markdown(&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("indent ignored", 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.Markdown(&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("wrap", 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")), + ), + 15, + ), + ) + + var b bytes.Buffer + if err := textfmt.Markdown(&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 definitions", 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.Markdown(&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("indent ignored", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Indent( + textfmt.NumberedDefinitionList( + textfmt.Definition(textfmt.Text("red"), textfmt.Text("looks like strawberry")), + textfmt.Definition(textfmt.Text("green"), textfmt.Text("looks like grass")), + textfmt.Definition(textfmt.Text("blue"), textfmt.Text("looks like sky")), + ), + 4, + 0, + ), + ) + + var b bytes.Buffer + if err := textfmt.Markdown(&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("wrap", 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")), + ), + 16, + ), + ) + + var b bytes.Buffer + if err := textfmt.Markdown(&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 list", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Wrap( + 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")), + ), + 21, + ), + ) + + var b bytes.Buffer + if err := textfmt.Markdown(&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("no rows", func(t *testing.T) { + var b bytes.Buffer + doc := textfmt.Doc(textfmt.Table()) + if err := textfmt.Markdown(&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.Markdown(&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.Markdown(&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.Markdown(&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.Markdown(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = ` +| foo | bar | baz | +|-----|-----|-----| +` + + 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.Markdown(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = ` +| \- | \- | \- | +|----|----|----| +| 1 | 2 | 3 | +` + + 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.Markdown(&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.Markdown(&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.Markdown(&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.Markdown(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = ` +| \- | +|----| +| 1 | +` + + 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.Markdown(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = ` +| foo | +|-----| +| 1 | +` + + 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.Markdown(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = ` +| \- | \- | \- | +|----|----|----| +| 1 | 2 | | +| 4 | | | +| 7 | 8 | 9 | +` + + if "\n"+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.Markdown(&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.Markdown(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = ` +| \- | \- | +|----|----| +| | | +| | | +` + + if "\n"+b.String() != expect { + t.Fatal("\n" + b.String()) + } + }) + + t.Run("ignore 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.Markdown(&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(expect) + t.Fatal("\n" + b.String()) + } + }) + + t.Run("ignore 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.Markdown(&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(expect) + t.Fatal("\n" + b.String()) + } + }) + }) + + t.Run("code", func(t *testing.T) { + t.Run("basic", 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.Markdown(&b, textfmt.Doc(textfmt.CodeBlock(code))); err != nil { + t.Fatal(err) + } + + if b.String() != "```\n"+code+"\n```\n" { + t.Fatal(b.String()) + } + }) + + t.Run("indent ignored", 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.Markdown(&b, textfmt.Doc(textfmt.Indent(textfmt.CodeBlock(code), 4, 0))); 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.Markdown(&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.Markdown(&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.Markdown(&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.Markdown(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "```\n...\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.Markdown(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "```\n\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.Markdown(&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.Markdown(&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.Markdown(&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.Markdown(&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.Markdown(&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.Markdown(&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.Markdown(&b, doc); err != nil { + t.Fatal(err) + } + + if b.String() != "```\nfoo [options]... [string|number]...\n```\n" { + t.Fatal(b.String()) + } + }) + }) +} diff --git a/notes.txt b/notes.txt index c1b8daa..c4c6fcd 100644 --- a/notes.txt +++ b/notes.txt @@ -1,5 +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 +test empty cat +show top level choice on separate lines in the same block diff --git a/runoff_test.go b/runoff_test.go index 208c271..53dd386 100644 --- a/runoff_test.go +++ b/runoff_test.go @@ -1034,7 +1034,7 @@ This is a paragraph. 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")), + textfmt.Item(textfmt.Text("this is the twelfth item")), ), ) @@ -1089,7 +1089,7 @@ This is a paragraph. .br .in 4 .ti 0 -12.\~this is the twelveth item +12.\~this is the twelfth item ` if b.String() != expect { @@ -1399,7 +1399,7 @@ This is a paragraph. 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")), + textfmt.Definition(textfmt.Text("twelve"), textfmt.Text("this is the twelfth item")), ), ) @@ -1454,7 +1454,7 @@ This is a paragraph. .br .in 12 .ti 0 -12.\~twelve:\~this is the twelveth item +12.\~twelve:\~this is the twelfth item ` if b.String() != expect { diff --git a/table.go b/table.go index 633cb72..bffb878 100644 --- a/table.go +++ b/table.go @@ -81,3 +81,12 @@ func columnWidths(rows [][]string) []int { return widths } + +func ensureHeader(rows []TableRow) []TableRow { + if len(rows) == 0 || len(rows[0].cells) == 0 || rows[0].header { + return rows + } + + h := []TableRow{{header: true, cells: make([]TableCell, len(rows[0].cells))}} + return append(h, rows...) +} diff --git a/teletype.go b/teletype.go index 65f0c47..458554a 100644 --- a/teletype.go +++ b/teletype.go @@ -23,6 +23,17 @@ func escapeTeletype(s string) string { return string(r) } +func ttyTextToString(text Txt) (string, error) { + var b bytes.Buffer + w := ttyWriter{w: &b, internal: true} + renderTTYText(&w, text) + if w.err != nil { + return "", w.err + } + + return b.String(), nil +} + func ttyDefinitionNames(d []DefinitionItem) ([]string, error) { var n []string for _, di := range d { @@ -37,17 +48,6 @@ func ttyDefinitionNames(d []DefinitionItem) ([]string, error) { return n, nil } -func ttyTextToString(text Txt) (string, error) { - var b bytes.Buffer - w := ttyWriter{w: &b, internal: true} - renderTTYText(&w, text) - if w.err != nil { - return "", w.err - } - - return b.String(), nil -} - func renderTTYText(w writer, text Txt) { if len(text.cat) > 0 { for i, tc := range text.cat { @@ -63,6 +63,8 @@ func renderTTYText(w writer, text Txt) { text.text = singleLine(text.text) text.text = escapeTeletype(text.text) + text.link = singleLine(text.link) + text.link = escapeTeletype(text.link) if text.link != "" { if text.text != "" { w.write(text.text) @@ -79,18 +81,6 @@ func renderTTYText(w writer, text Txt) { w.write(text.text) } -func itemToParagraph(list Entry, itemText Txt, prefix string) Entry { - p := Entry{ - typ: paragraph, - wrapWidth: list.wrapWidth, - indent: list.indent + len([]rune(prefix)) + 1, - indentFirst: list.indentFirst - len([]rune(prefix)) - 1, - } - - p.text.cat = []Txt{Text(prefix), itemText} - return p -} - func renderTTYTitle(w writer, e Entry) { w.write(timesn(" ", e.indent)) renderTTYText(w, e.text) diff --git a/teletype_test.go b/teletype_test.go index 1c6a41d..a18f125 100644 --- a/teletype_test.go +++ b/teletype_test.go @@ -373,7 +373,7 @@ Entry explanations: t.Fatal(err) } - if b.String() != "a link (https://sqrndfst.org\n/foo)\n" { + if b.String() != "a link (https://sqrndfst.org /foo)\n" { t.Fatal(b.String()) } }) @@ -789,7 +789,7 @@ third 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")), + textfmt.Item(textfmt.Text("this is the twelfth item")), ), ) @@ -809,7 +809,7 @@ third item 9. this is the nineth item 10. this is the tenth item 11. this is the eleventh item -12. this is the twelveth item +12. this is the twelfth item ` if b.String() != expect { @@ -1137,7 +1137,7 @@ third 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")), + textfmt.Definition(textfmt.Text("twelve"), textfmt.Text("this is the twelfth item")), ), ) @@ -1157,7 +1157,7 @@ third item 9. nine: this is the nineth item 10. ten: this is the tenth item 11. eleven: this is the eleventh item -12. twelve: this is the twelveth item +12. twelve: this is the twelfth item ` if b.String() != expect { diff --git a/text.go b/text.go index 1f98e57..a168754 100644 --- a/text.go +++ b/text.go @@ -97,3 +97,32 @@ func textToString(t Txt) string { return singleLine(b.String()) } + +func itemToParagraph(list Entry, itemText Txt, prefix ...string) Entry { + p := Entry{ + typ: paragraph, + wrapWidth: list.wrapWidth, + indent: list.indent, + indentFirst: list.indentFirst, + } + + if len(prefix) == 0 { + p.text = itemText + return p + } + + var prefixLength int + for _, p := range prefix { + prefixLength += len([]rune(p)) + 1 + } + + var prefixText []Txt + for _, p := range prefix { + prefixText = append(prefixText, Text(p)) + } + + p.indent += prefixLength + p.indentFirst -= prefixLength + p.text.cat = append(prefixText, itemText) + return p +} diff --git a/write.go b/write.go index 1b1fc09..73749ec 100644 --- a/write.go +++ b/write.go @@ -24,6 +24,12 @@ type roffWriter struct { err error } +type mdWriter struct { + w io.Writer + internal bool + err error +} + func (w *ttyWriter) write(a ...any) { for _, ai := range a { if w.err != nil { @@ -90,6 +96,35 @@ func (w *roffWriter) setErr(err error) { w.err = err } +func (w *mdWriter) write(a ...any) { + for _, ai := range a { + if w.err != nil { + return + } + + s := fmt.Sprint(ai) + r := []rune(s) + if !w.internal { + for i := range r { + if r[i] == '\u00a0' { + r[i] = ' ' + } + } + } + + s = string(r) + _, w.err = w.w.Write([]byte(s)) + } +} + +func (w *mdWriter) error() error { + return w.err +} + +func (w *mdWriter) setErr(err error) { + w.err = err +} + func writeLines(w writer, txt string, indentFirst, indentRest int) { lines := strings.Split(txt, "\n") for i, l := range lines {