diff --git a/lib.go b/lib.go index 04b8488..9c1ff64 100644 --- a/lib.go +++ b/lib.go @@ -26,12 +26,19 @@ type Txt struct { cat []Txt } +type ItemStyle struct { + bullet string + noBullet bool +} + type ListItem struct { - text Txt + text Txt + style []ItemStyle } type DefinitionItem struct { name, value Txt + style []ItemStyle } type TableCell struct { @@ -168,8 +175,18 @@ func Paragraph(t Txt) Entry { return Entry{typ: paragraph, text: t} } -func Item(text Txt) ListItem { - return ListItem{text: text} +func Bullet(b string) ItemStyle { + return ItemStyle{bullet: b} +} + +func NoBullet() ItemStyle { + return ItemStyle{noBullet: true} +} + +// Item creates a list item for bulleted or numbered lists. Item style may behave different depending ont the +// output format. +func Item(text Txt, style ...ItemStyle) ListItem { + return ListItem{text: text, style: style} } func List(items ...ListItem) Entry { @@ -180,8 +197,10 @@ func NumberedList(items ...ListItem) Entry { return Entry{typ: numberedList, items: items} } -func Definition(name, value Txt) DefinitionItem { - return DefinitionItem{name: name, value: value} +// Definition creates a definition list item for bulleted or numbered definition lists. Item style may behave +// different depending ont the output format. +func Definition(name, value Txt, style ...ItemStyle) DefinitionItem { + return DefinitionItem{name: name, value: value, style: style} } func DefinitionList(items ...DefinitionItem) Entry { diff --git a/runoff.go b/runoff.go index 4211a5b..109ff38 100644 --- a/runoff.go +++ b/runoff.go @@ -134,8 +134,24 @@ func renderRoffList(w io.Writer, e Entry) { write(w, "\n.br\n") } - write(w, ".in ", e.indent+2, "\n.ti ", e.indent+e.indentFirst, "\n") - write(w, "\\(bu ") + var ( + bullet string + bulletIndent int + ) + + itemStyle := mergeItemStyles(item.style) + if !itemStyle.noBullet { + if itemStyle.bullet == "" { + bullet = "\\(bu " + bulletIndent = 2 + } else { + bullet = itemStyle.bullet + " " + bulletIndent = len([]rune(bullet)) + } + } + + write(w, ".in ", e.indent+bulletIndent, "\n.ti ", e.indent+e.indentFirst, "\n") + write(w, bullet) renderRoffText(w, item.text) } } @@ -161,8 +177,21 @@ func renderRoffDefinitions(w io.Writer, e Entry) { write(w, "\n.br\n") } - write(w, ".in ", e.indent+maxNameLength+4, "\n.ti ", e.indent+e.indentFirst, "\n") - write(w, "\\(bu ") + var bullet string + padLength := 2 + itemStyle := mergeItemStyles(definition.style) + if !itemStyle.noBullet { + if itemStyle.bullet == "" { + bullet = "\\(bu " + padLength = 4 + } else { + bullet = itemStyle.bullet + " " + padLength = len([]rune(bullet)) + 2 + } + } + + write(w, ".in ", e.indent+maxNameLength+padLength, "\n.ti ", e.indent+e.indentFirst, "\n") + write(w, bullet) renderRoffText(w, definition.name) write(w, ":", timesn("\\~", maxNameLength-len([]rune(names[i]))+1)) renderRoffText(w, definition.value) diff --git a/runoff_test.go b/runoff_test.go index d28097f..0fffc16 100644 --- a/runoff_test.go +++ b/runoff_test.go @@ -904,6 +904,105 @@ This is a paragraph. t.Fatal(b.String()) } }) + + t.Run("custom bullet", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.List( + textfmt.Item(textfmt.Text("foo bar baz"), textfmt.Bullet("*")), + textfmt.Item(textfmt.Text("qux"), textfmt.Bullet("*")), + textfmt.Item(textfmt.Text("quux"), textfmt.Bullet("*")), + ), + ) + + var b bytes.Buffer + if err := textfmt.Runoff(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = ` +.in 2 +.ti 0 +* foo bar baz +.br +.in 2 +.ti 0 +* qux +.br +.in 2 +.ti 0 +* quux +` + + if "\n"+b.String() != expect { + t.Fatal("\n" + b.String()) + } + }) + + t.Run("custom bullet longer", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.List( + textfmt.Item(textfmt.Text("foo bar baz"), textfmt.Bullet("=>")), + textfmt.Item(textfmt.Text("qux"), textfmt.Bullet("=>")), + textfmt.Item(textfmt.Text("quux"), textfmt.Bullet("=>")), + ), + ) + + var b bytes.Buffer + if err := textfmt.Runoff(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = ` +.in 3 +.ti 0 +=> foo bar baz +.br +.in 3 +.ti 0 +=> qux +.br +.in 3 +.ti 0 +=> quux +` + + if "\n"+b.String() != expect { + t.Fatal("\n" + b.String()) + } + }) + + t.Run("no bullet", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.List( + textfmt.Item(textfmt.Text("foo bar baz"), textfmt.NoBullet()), + textfmt.Item(textfmt.Text("qux"), textfmt.NoBullet()), + textfmt.Item(textfmt.Text("quux"), textfmt.NoBullet()), + ), + ) + + var b bytes.Buffer + if err := textfmt.Runoff(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = ` +.in 0 +.ti 0 +foo bar baz +.br +.in 0 +.ti 0 +qux +.br +.in 0 +.ti 0 +quux +` + + if "\n"+b.String() != expect { + t.Fatal("\n" + b.String()) + } + }) }) t.Run("numbered list", func(t *testing.T) { @@ -1261,6 +1360,141 @@ This is a paragraph. .in 13 .ti 0 \(bu blue:\~\~looks like sky +` + + if "\n"+b.String() != expect { + t.Fatal("\n" + b.String()) + } + }) + + t.Run("custom bullet", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.DefinitionList( + textfmt.Definition( + textfmt.Text("one"), + textfmt.Text("foo bar baz"), + textfmt.Bullet("*"), + ), + textfmt.Definition( + textfmt.Text("two"), + textfmt.Text("qux"), + textfmt.Bullet("*"), + ), + textfmt.Definition( + textfmt.Text("three"), + textfmt.Text("quux"), + textfmt.Bullet("*"), + ), + ), + ) + + var b bytes.Buffer + if err := textfmt.Runoff(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = ` +.in 9 +.ti 0 +* one:\~\~\~foo bar baz +.br +.in 9 +.ti 0 +* two:\~\~\~qux +.br +.in 9 +.ti 0 +* three:\~quux +` + + if "\n"+b.String() != expect { + t.Fatal("\n" + b.String()) + } + }) + + t.Run("custom bullet longer", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.DefinitionList( + textfmt.Definition( + textfmt.Text("one"), + textfmt.Text("foo bar baz"), + textfmt.Bullet("=>"), + ), + textfmt.Definition( + textfmt.Text("two"), + textfmt.Text("qux"), + textfmt.Bullet("=>"), + ), + textfmt.Definition( + textfmt.Text("three"), + textfmt.Text("quux"), + textfmt.Bullet("=>"), + ), + ), + ) + + var b bytes.Buffer + if err := textfmt.Runoff(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = ` +.in 10 +.ti 0 +=> one:\~\~\~foo bar baz +.br +.in 10 +.ti 0 +=> two:\~\~\~qux +.br +.in 10 +.ti 0 +=> three:\~quux +` + + if "\n"+b.String() != expect { + t.Fatal("\n" + b.String()) + } + }) + + t.Run("no bullet", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.DefinitionList( + textfmt.Definition( + textfmt.Text("one"), + textfmt.Text("foo bar baz"), + textfmt.NoBullet(), + ), + textfmt.Definition( + textfmt.Text("two"), + textfmt.Text("qux"), + textfmt.NoBullet(), + ), + textfmt.Definition( + textfmt.Text("three"), + textfmt.Text("quux"), + textfmt.NoBullet(), + ), + ), + ) + + var b bytes.Buffer + if err := textfmt.Runoff(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = ` +.in 7 +.ti 0 +one:\~\~\~foo bar baz +.br +.in 7 +.ti 0 +two:\~\~\~qux +.br +.in 7 +.ti 0 +three:\~quux ` if "\n"+b.String() != expect { diff --git a/teletype.go b/teletype.go index ca57e9b..8bbbe00 100644 --- a/teletype.go +++ b/teletype.go @@ -83,7 +83,19 @@ func renderTTYList(w io.Writer, e Entry) { write(w, "\n") } - p := itemToParagraph(e, item.text, "-") + var p Entry + itemStyle := mergeItemStyles(item.style) + if itemStyle.noBullet { + p = itemToParagraph(e, item.text) + } else { + bullet := "-" + if itemStyle.bullet != "" { + bullet = itemStyle.bullet + } + + p = itemToParagraph(e, item.text, bullet) + } + renderTTYParagraph(w, p) } } @@ -108,10 +120,23 @@ func renderTTYDefinitions(w io.Writer, e Entry) { write(w, "\n") } + var bullet string + padLength := 1 + itemStyle := mergeItemStyles(definition.style) + if !itemStyle.noBullet { + if itemStyle.bullet == "" { + bullet = "- " + padLength = 3 + } else { + bullet = itemStyle.bullet + " " + padLength = len([]rune(bullet)) + 1 + } + } + p := itemToParagraph( e, definition.value, - padRight(fmt.Sprintf("- %s:", names[i]), maxNameLength+3), + padRight(fmt.Sprintf("%s%s:", bullet, names[i]), maxNameLength+padLength), ) renderTTYParagraph(w, p) diff --git a/teletype_test.go b/teletype_test.go index e282788..9bb90f8 100644 --- a/teletype_test.go +++ b/teletype_test.go @@ -643,6 +643,93 @@ third item t.Fatal(b.String()) } }) + + t.Run("custom bullet", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Wrap( + textfmt.List( + textfmt.Item(textfmt.Text("foo bar baz"), textfmt.Bullet("*")), + textfmt.Item(textfmt.Text("qux"), textfmt.Bullet("*")), + textfmt.Item(textfmt.Text("quux"), textfmt.Bullet("*")), + ), + 10, + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = ` +* foo bar + baz +* qux +* quux +` + + if "\n"+b.String() != expect { + t.Fatal("\n" + b.String()) + } + }) + + t.Run("custom bullet longer", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Wrap( + textfmt.List( + textfmt.Item(textfmt.Text("foo bar baz"), textfmt.Bullet("=>")), + textfmt.Item(textfmt.Text("qux"), textfmt.Bullet("=>")), + textfmt.Item(textfmt.Text("quux"), textfmt.Bullet("=>")), + ), + 10, + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = ` +=> foo bar + baz +=> qux +=> quux +` + + if "\n"+b.String() != expect { + t.Fatal("\n" + b.String()) + } + }) + + t.Run("no bullet", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Wrap( + textfmt.List( + textfmt.Item(textfmt.Text("foo bar baz"), textfmt.NoBullet()), + textfmt.Item(textfmt.Text("qux"), textfmt.NoBullet()), + textfmt.Item(textfmt.Text("quux"), textfmt.NoBullet()), + ), + 10, + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = ` +foo bar +baz +qux +quux +` + + if "\n"+b.String() != expect { + t.Fatal("\n" + b.String()) + } + }) }) t.Run("numbered list", func(t *testing.T) { @@ -989,6 +1076,129 @@ third item t.Fatal("\n" + b.String()) } }) + + t.Run("custom bullet", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Wrap( + textfmt.DefinitionList( + textfmt.Definition( + textfmt.Text("one"), + textfmt.Text("foo bar baz"), + textfmt.Bullet("*"), + ), + textfmt.Definition( + textfmt.Text("two"), + textfmt.Text("qux"), + textfmt.Bullet("*"), + ), + textfmt.Definition( + textfmt.Text("three"), + textfmt.Text("quux"), + textfmt.Bullet("*"), + ), + ), + 18, + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = ` +* one: foo bar + baz +* two: qux +* three: quux +` + + if "\n"+b.String() != expect { + t.Fatal("\n" + b.String()) + } + }) + + t.Run("custom bullet longer", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Wrap( + textfmt.DefinitionList( + textfmt.Definition( + textfmt.Text("one"), + textfmt.Text("foo bar baz"), + textfmt.Bullet("=>"), + ), + textfmt.Definition( + textfmt.Text("two"), + textfmt.Text("qux"), + textfmt.Bullet("=>"), + ), + textfmt.Definition( + textfmt.Text("three"), + textfmt.Text("quux"), + textfmt.Bullet("=>"), + ), + ), + 18, + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = ` +=> one: foo bar + baz +=> two: qux +=> three: quux +` + + if "\n"+b.String() != expect { + t.Fatal("\n" + b.String()) + } + }) + + t.Run("no bullet", func(t *testing.T) { + doc := textfmt.Doc( + textfmt.Wrap( + textfmt.DefinitionList( + textfmt.Definition( + textfmt.Text("one"), + textfmt.Text("foo bar baz"), + textfmt.NoBullet(), + ), + textfmt.Definition( + textfmt.Text("two"), + textfmt.Text("qux"), + textfmt.NoBullet(), + ), + textfmt.Definition( + textfmt.Text("three"), + textfmt.Text("quux"), + textfmt.NoBullet(), + ), + ), + 15, + ), + ) + + var b bytes.Buffer + if err := textfmt.Teletype(&b, doc); err != nil { + t.Fatal(err) + } + + const expect = ` +one: foo bar + baz +two: qux +three: quux +` + + if "\n"+b.String() != expect { + t.Fatal("\n" + b.String()) + } + }) }) t.Run("numbered definition list", func(t *testing.T) { diff --git a/text.go b/text.go index 0bbfd04..ec5de6c 100644 --- a/text.go +++ b/text.go @@ -73,3 +73,13 @@ func itemToParagraph(list Entry, itemText Txt, prefix ...string) Entry { p.text.cat = append(prefixText, itemText) return p } + +func mergeItemStyles(s []ItemStyle) ItemStyle { + var m ItemStyle + for _, si := range s { + m.bullet = si.bullet + m.noBullet = m.noBullet && si.bullet == "" || si.noBullet + } + + return m +}