1
0

support custom bullet lengths in tty and roff

This commit is contained in:
Arpad Ryszka 2025-11-16 18:15:12 +01:00
parent cff44cc943
commit 0ddb884813
6 changed files with 616 additions and 209 deletions

1
lib.go
View File

@ -70,7 +70,6 @@ type Entry struct {
rows []TableRow
syntax SyntaxItem
wrapWidth int
wrapWidthFirst int
indent int
indentFirst int
man struct {

210
runoff.go
View File

@ -13,21 +13,45 @@ func manPageDate(d time.Time) string {
return fmt.Sprintf("%v %d", d.Month(), d.Year())
}
func escapeRoffString(s string, additionalEscape ...string) string {
var b bytes.Buffer
w, f := writeWith(&b, escapeRoff(additionalEscape...))
write(w, s)
f()
return b.String()
}
func roffTextLength(t Txt) int {
var l int
if len(t.cat) > 0 {
for i, tc := range t.cat {
if i > 0 {
l++
}
l += roffTextLength(tc)
}
return l
}
if t.link == "" {
return len([]rune(t.text))
}
if t.text == "" {
return len([]rune(t.link))
}
return len([]rune(t.text)) + len([]rune(t.link)) + 3
}
func roffTextToString(t Txt) string {
var b bytes.Buffer
renderRoffText(&b, t)
return b.String()
}
func roffDefinitionNames(d []DefinitionItem) []string {
var n []string
for _, di := range d {
n = append(n, roffTextToString(di.name))
}
return n
}
func roffCellTexts(r []TableRow) [][]string {
var cellTexts [][]string
for _, row := range r {
@ -129,90 +153,154 @@ func renderRoffParagraph(w io.Writer, e Entry) {
}
func renderRoffList(w io.Writer, e Entry) {
bullets := make([]string, len(e.items))
bulletLengths := make([]int, len(e.items))
for i := range e.items {
itemStyle := mergeItemStyles(e.items[i].style)
if itemStyle.noBullet {
continue
}
if itemStyle.bullet == "" {
bullets[i] = "\\(bu"
bulletLengths[i] = 1
continue
}
bullets[i] = escapeRoffString(itemStyle.bullet, " ", "\\~")
bulletLengths[i] = len([]rune(itemStyle.bullet))
}
var maxBulletLength int
for _, l := range bulletLengths {
if l > maxBulletLength {
maxBulletLength = l
}
}
for i := range bullets {
if bulletLengths[i] == maxBulletLength {
continue
}
bullets[i] = fmt.Sprintf(
"%s%s",
bullets[i],
timesn("\\~", maxBulletLength-bulletLengths[i]),
)
}
for i, item := range e.items {
if i > 0 {
write(w, "\n.br\n")
}
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))
}
indent := e.indent
if maxBulletLength > 0 {
indent += maxBulletLength + 1
}
write(w, ".in ", indent, "\n.ti ", e.indent+e.indentFirst, "\n")
write(w, bullets[i])
if maxBulletLength > 0 {
write(w, "\\~")
}
write(w, ".in ", e.indent+bulletIndent, "\n.ti ", e.indent+e.indentFirst, "\n")
write(w, bullet)
renderRoffText(w, item.text)
}
}
func renderRoffNumberedList(w io.Writer, e Entry) {
maxDigits := numDigits(len(e.items))
for i, item := range e.items {
if i > 0 {
write(w, "\n.br\n")
items := make([]ListItem, len(e.items))
for i := range e.items {
items[i] = Item(
e.items[i].text,
append(e.items[i].style, Bullet(fmt.Sprintf("%d.", i+1)))...,
)
}
write(w, ".in ", e.indent+maxDigits+2, "\n.ti ", e.indent+e.indentFirst, "\n")
write(w, padRight(fmt.Sprintf("%d.", i+1), maxDigits+2))
renderRoffText(w, item.text)
}
e.typ = list
e.items = items
renderRoffList(w, e)
}
func renderRoffDefinitions(w io.Writer, e Entry) {
names := roffDefinitionNames(e.definitions)
maxNameLength := maxLength(names)
itemStyles := make([]ItemStyle, len(e.definitions))
for i := range e.definitions {
itemStyles[i] = mergeItemStyles(e.definitions[i].style)
}
bullets := make([]string, len(itemStyles))
bulletLengths := make([]int, len(itemStyles))
for i := range itemStyles {
if itemStyles[i].noBullet {
continue
}
if itemStyles[i].bullet == "" {
bullets[i] = "\\(bu"
bulletLengths[i] = 1
continue
}
bullets[i] = escapeRoffString(itemStyles[i].bullet, " ", "\\~")
bulletLengths[i] = len([]rune(itemStyles[i].bullet))
}
var maxBulletLength int
for i := range bulletLengths {
if bulletLengths[i] > maxBulletLength {
maxBulletLength = bulletLengths[i]
}
}
nameLengths := make([]int, len(e.definitions))
for i := range e.definitions {
nameLengths[i] = roffTextLength(e.definitions[i].name)
}
var maxNameLength int
for i := range nameLengths {
if nameLengths[i] > maxNameLength {
maxNameLength = nameLengths[i]
}
}
for i, definition := range e.definitions {
if i > 0 {
write(w, "\n.br\n")
}
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
}
indent := e.indent + maxNameLength + 2
if maxBulletLength > 0 {
indent += maxBulletLength + 1
}
write(w, ".in ", indent, "\n.ti ", e.indent+e.indentFirst, "\n")
write(w, bullets[i])
if maxBulletLength > 0 {
write(w, timesn("\\~", maxBulletLength-bulletLengths[i]+1))
}
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))
write(w, ":", timesn("\\~", maxNameLength-nameLengths[i]+1))
renderRoffText(w, definition.value)
}
}
func renderRoffNumberedDefinitions(w io.Writer, e Entry) {
maxDigits := numDigits(len(e.definitions))
names := roffDefinitionNames(e.definitions)
maxNameLength := maxLength(names)
for i, definition := range e.definitions {
if i > 0 {
write(w, "\n.br\n")
defs := make([]DefinitionItem, len(e.definitions))
for i := range e.definitions {
defs[i] = Definition(
e.definitions[i].name,
e.definitions[i].value,
append(e.definitions[i].style, Bullet(fmt.Sprintf("%d.", i+1)))...,
)
}
write(w, ".in ", e.indent+maxDigits+maxNameLength+4, "\n.ti ", e.indent+e.indentFirst, "\n")
write(w, padRight(fmt.Sprintf("%d.", i+1), maxDigits+2))
renderRoffText(w, definition.name)
write(w, ":", timesn("\\~", maxNameLength-len([]rune(names[i]))+1))
renderRoffText(w, definition.value)
}
e.typ = definitions
e.definitions = defs
renderRoffDefinitions(w, e)
}
func renderRoffTable(w io.Writer, e Entry) {

View File

@ -144,39 +144,39 @@ textfmt supports the following entries:
.sp 1v
.in 2
.ti 0
\(bu CodeBlock
\(bu\~CodeBlock
.br
.in 2
.ti 0
\(bu DefinitionList
\(bu\~DefinitionList
.br
.in 2
.ti 0
\(bu List
\(bu\~List
.br
.in 2
.ti 0
\(bu NumberedDefinitionList
\(bu\~NumberedDefinitionList
.br
.in 2
.ti 0
\(bu NumberedList
\(bu\~NumberedList
.br
.in 2
.ti 0
\(bu Paragraph
\(bu\~Paragraph
.br
.in 2
.ti 0
\(bu Syntax
\(bu\~Syntax
.br
.in 2
.ti 0
\(bu Table
\(bu\~Table
.br
.in 2
.ti 0
\(bu Title
\(bu\~Title
.br
.sp 1v
.in 0
@ -186,39 +186,39 @@ textfmt supports the following entries:
.sp 1v
.in 26
.ti 0
\(bu CodeBlock:\~\~\~\~\~\~\~\~\~\~\~\~\~\~a multiline block of code
\(bu\~CodeBlock:\~\~\~\~\~\~\~\~\~\~\~\~\~\~a multiline block of code
.br
.in 26
.ti 0
\(bu DefinitionList:\~\~\~\~\~\~\~\~\~a list of definitions like this one
\(bu\~DefinitionList:\~\~\~\~\~\~\~\~\~a list of definitions like this one
.br
.in 26
.ti 0
\(bu List:\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~a list of items
\(bu\~List:\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~a list of items
.br
.in 26
.ti 0
\(bu NumberedDefinitionList:\~numbered definitions
\(bu\~NumberedDefinitionList:\~numbered definitions
.br
.in 26
.ti 0
\(bu NumberedList:\~\~\~\~\~\~\~\~\~\~\~numbered list
\(bu\~NumberedList:\~\~\~\~\~\~\~\~\~\~\~numbered list
.br
.in 26
.ti 0
\(bu Paragraph:\~\~\~\~\~\~\~\~\~\~\~\~\~\~paragraph of text
\(bu\~Paragraph:\~\~\~\~\~\~\~\~\~\~\~\~\~\~paragraph of text
.br
.in 26
.ti 0
\(bu Syntax:\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~a syntax expression
\(bu\~Syntax:\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~a syntax expression
.br
.in 26
.ti 0
\(bu Table:\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~a table
\(bu\~Table:\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~a table
.br
.in 26
.ti 0
\(bu Title:\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~a title
\(bu\~Title:\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~a title
`
if b.String() != expect {
@ -354,39 +354,39 @@ textfmt supports the following entries:
.sp 1v
.in 2
.ti 0
\(bu CodeBlock
\(bu\~CodeBlock
.br
.in 2
.ti 0
\(bu DefinitionList
\(bu\~DefinitionList
.br
.in 2
.ti 0
\(bu List
\(bu\~List
.br
.in 2
.ti 0
\(bu NumberedDefinitionList
\(bu\~NumberedDefinitionList
.br
.in 2
.ti 0
\(bu NumberedList
\(bu\~NumberedList
.br
.in 2
.ti 0
\(bu Paragraph
\(bu\~Paragraph
.br
.in 2
.ti 0
\(bu Syntax
\(bu\~Syntax
.br
.in 2
.ti 0
\(bu Table
\(bu\~Table
.br
.in 2
.ti 0
\(bu Title
\(bu\~Title
.br
.sp 1v
.in 0
@ -396,39 +396,39 @@ textfmt supports the following entries:
.sp 1v
.in 26
.ti 0
\(bu CodeBlock:\~\~\~\~\~\~\~\~\~\~\~\~\~\~a multiline block of code
\(bu\~CodeBlock:\~\~\~\~\~\~\~\~\~\~\~\~\~\~a multiline block of code
.br
.in 26
.ti 0
\(bu DefinitionList:\~\~\~\~\~\~\~\~\~a list of definitions like this one
\(bu\~DefinitionList:\~\~\~\~\~\~\~\~\~a list of definitions like this one
.br
.in 26
.ti 0
\(bu List:\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~a list of items
\(bu\~List:\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~a list of items
.br
.in 26
.ti 0
\(bu NumberedDefinitionList:\~numbered definitions
\(bu\~NumberedDefinitionList:\~numbered definitions
.br
.in 26
.ti 0
\(bu NumberedList:\~\~\~\~\~\~\~\~\~\~\~numbered list
\(bu\~NumberedList:\~\~\~\~\~\~\~\~\~\~\~numbered list
.br
.in 26
.ti 0
\(bu Paragraph:\~\~\~\~\~\~\~\~\~\~\~\~\~\~paragraph of text
\(bu\~Paragraph:\~\~\~\~\~\~\~\~\~\~\~\~\~\~paragraph of text
.br
.in 26
.ti 0
\(bu Syntax:\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~a syntax expression
\(bu\~Syntax:\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~a syntax expression
.br
.in 26
.ti 0
\(bu Table:\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~a table
\(bu\~Table:\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~a table
.br
.in 26
.ti 0
\(bu Title:\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~a title
\(bu\~Title:\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~a title
`
if b.String() != expect {
@ -781,15 +781,15 @@ This is a paragraph.
const expect = `.in 2
.ti 0
\(bu this is an item
\(bu\~this is an item
.br
.in 2
.ti 0
\(bu this is another item
\(bu\~this is another item
.br
.in 2
.ti 0
\(bu this is a third item
\(bu\~this is a third item
`
if b.String() != expect {
@ -817,15 +817,15 @@ This is a paragraph.
const expect = `.in 6
.ti 4
\(bu this is an item
\(bu\~this is an item
.br
.in 6
.ti 4
\(bu this is another item
\(bu\~this is another item
.br
.in 6
.ti 4
\(bu this is a third item
\(bu\~this is a third item
`
if b.String() != expect {
@ -853,15 +853,15 @@ This is a paragraph.
const expect = `.in 0
.ti 4
\(bu this is an item
\(bu\~this is an item
.br
.in 0
.ti 4
\(bu this is another item
\(bu\~this is another item
.br
.in 0
.ti 4
\(bu this is a third item
\(bu\~this is a third item
`
if b.String() != expect {
@ -889,15 +889,15 @@ This is a paragraph.
const expect = `.in 4
.ti 0
\(bu this is an item
\(bu\~this is an item
.br
.in 4
.ti 0
\(bu this is another item
\(bu\~this is another item
.br
.in 4
.ti 0
\(bu this is a third item
\(bu\~this is a third item
`
if b.String() != expect {
@ -922,15 +922,15 @@ This is a paragraph.
const expect = `
.in 2
.ti 0
* foo bar baz
*\~foo bar baz
.br
.in 2
.ti 0
* qux
*\~qux
.br
.in 2
.ti 0
* quux
*\~quux
`
if "\n"+b.String() != expect {
@ -955,15 +955,15 @@ This is a paragraph.
const expect = `
.in 3
.ti 0
=> foo bar baz
=>\~foo bar baz
.br
.in 3
.ti 0
=> qux
=>\~qux
.br
.in 3
.ti 0
=> quux
=>\~quux
`
if "\n"+b.String() != expect {
@ -997,6 +997,72 @@ qux
.in 0
.ti 0
quux
`
if "\n"+b.String() != expect {
t.Fatal("\n" + b.String())
}
})
t.Run("varying bullet length", 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 4
.ti 0
=>\~\~foo bar baz
.br
.in 4
.ti 0
==>\~qux
.br
.in 4
.ti 0
=>\~\~quux
`
if "\n"+b.String() != expect {
t.Fatal("\n" + b.String())
}
})
t.Run("escape 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 3
.ti 0
\&.>\~foo bar baz
.br
.in 3
.ti 0
=>\~qux
.br
.in 3
.ti 0
=>\~quux
`
if "\n"+b.String() != expect {
@ -1241,15 +1307,15 @@ quux
const expect = `.in 9
.ti 0
\(bu red:\~\~\~looks like strawberry
\(bu\~red:\~\~\~looks like strawberry
.br
.in 9
.ti 0
\(bu green:\~looks like grass
\(bu\~green:\~looks like grass
.br
.in 9
.ti 0
\(bu blue:\~\~looks like sky
\(bu\~blue:\~\~looks like sky
`
if b.String() != expect {
@ -1277,15 +1343,15 @@ quux
const expect = `.in 13
.ti 4
\(bu red:\~\~\~looks like strawberry
\(bu\~red:\~\~\~looks like strawberry
.br
.in 13
.ti 4
\(bu green:\~looks like grass
\(bu\~green:\~looks like grass
.br
.in 13
.ti 4
\(bu blue:\~\~looks like sky
\(bu\~blue:\~\~looks like sky
`
if b.String() != expect {
@ -1314,15 +1380,15 @@ quux
const expect = `
.in 3
.ti 3
\(bu red:\~\~\~looks like strawberry
\(bu\~red:\~\~\~looks like strawberry
.br
.in 3
.ti 3
\(bu green:\~looks like grass
\(bu\~green:\~looks like grass
.br
.in 3
.ti 3
\(bu blue:\~\~looks like sky
\(bu\~blue:\~\~looks like sky
`
if "\n"+b.String() != expect {
@ -1351,15 +1417,15 @@ quux
const expect = `
.in 13
.ti 0
\(bu red:\~\~\~looks like strawberry
\(bu\~red:\~\~\~looks like strawberry
.br
.in 13
.ti 0
\(bu green:\~looks like grass
\(bu\~green:\~looks like grass
.br
.in 13
.ti 0
\(bu blue:\~\~looks like sky
\(bu\~blue:\~\~looks like sky
`
if "\n"+b.String() != expect {
@ -1396,15 +1462,15 @@ quux
const expect = `
.in 9
.ti 0
* one:\~\~\~foo bar baz
*\~one:\~\~\~foo bar baz
.br
.in 9
.ti 0
* two:\~\~\~qux
*\~two:\~\~\~qux
.br
.in 9
.ti 0
* three:\~quux
*\~three:\~quux
`
if "\n"+b.String() != expect {
@ -1441,15 +1507,15 @@ quux
const expect = `
.in 10
.ti 0
=> one:\~\~\~foo bar baz
=>\~one:\~\~\~foo bar baz
.br
.in 10
.ti 0
=> two:\~\~\~qux
=>\~two:\~\~\~qux
.br
.in 10
.ti 0
=> three:\~quux
=>\~three:\~quux
`
if "\n"+b.String() != expect {
@ -1495,6 +1561,96 @@ two:\~\~\~qux
.in 7
.ti 0
three:\~quux
`
if "\n"+b.String() != expect {
t.Fatal("\n" + b.String())
}
})
t.Run("varying bullet length", 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 11
.ti 0
=>\~\~one:\~\~\~foo bar baz
.br
.in 11
.ti 0
==>\~two:\~\~\~qux
.br
.in 11
.ti 0
=>\~\~three:\~quux
`
if "\n"+b.String() != expect {
t.Fatal("\n" + b.String())
}
})
t.Run("escape 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 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 {

View File

@ -65,11 +65,10 @@ func renderTTYTitle(w io.Writer, e Entry) {
func renderTTYParagraph(w io.Writer, e Entry) {
var indentation wrapper
indentFirst := e.indent + e.indentFirst
wrapWidthFirst := e.wrapWidth + e.wrapWidthFirst
if e.wrapWidth == 0 {
indentation = indent(indentFirst, e.indent)
} else {
indentation = wrapIndent(indentFirst, e.indent, wrapWidthFirst, e.wrapWidth)
indentation = wrapIndent(indentFirst, e.indent, e.wrapWidth, e.wrapWidth)
}
w, f := writeWith(w, indentation)
@ -78,95 +77,111 @@ func renderTTYParagraph(w io.Writer, e Entry) {
}
func renderTTYList(w io.Writer, e Entry) {
for i, item := range e.items {
if i > 0 {
write(w, "\n")
}
var p Entry
var bullets []string
for _, item := range e.items {
itemStyle := mergeItemStyles(item.style)
if itemStyle.noBullet {
p = itemToParagraph(e, item.text)
bullets = append(bullets, "")
} else {
bullet := "-"
if itemStyle.bullet != "" {
bullet = itemStyle.bullet
}
p = itemToParagraph(e, item.text, bullet)
}
renderTTYParagraph(w, p)
bullets = append(bullets, bullet)
}
}
func renderTTYNumberedList(w io.Writer, e Entry) {
maxDigits := numDigits(len(e.items))
maxBulletLength := maxLength(bullets)
for i := range bullets {
bullets[i] = padRight(bullets[i], maxBulletLength)
}
for i, item := range e.items {
if i > 0 {
write(w, "\n")
}
p := itemToParagraph(e, item.text, padRight(fmt.Sprintf("%d.", i+1), maxDigits+1))
p := itemToParagraph(e, item.text, bullets[i])
renderTTYParagraph(w, p)
}
}
func renderTTYNumberedList(w io.Writer, e Entry) {
var items []ListItem
for i, item := range e.items {
items = append(
items,
Item(
item.text,
append(item.style, Bullet(fmt.Sprintf("%d.", i+1)))...,
),
)
}
e.typ = list
e.items = items
renderTTYList(w, e)
}
func renderTTYDefinitions(w io.Writer, e Entry) {
names := ttyDefinitionNames(e.definitions)
maxNameLength := maxLength(names)
for i, definition := range e.definitions {
if i > 0 {
write(w, "\n")
itemStyles := make([]ItemStyle, len(e.definitions))
for i := range e.definitions {
itemStyles[i] = mergeItemStyles(e.definitions[i].style)
}
bullets := make([]string, len(e.definitions))
for i := range e.definitions {
if itemStyles[i].noBullet {
continue
}
if itemStyles[i].bullet == "" {
bullets[i] = "-"
continue
}
bullets[i] = itemStyles[i].bullet
}
maxBulletLength := maxLength(bullets)
items := make([]ListItem, len(e.definitions))
for i := range e.definitions {
var bullet string
padLength := 1
itemStyle := mergeItemStyles(definition.style)
if !itemStyle.noBullet {
if itemStyle.bullet == "" {
bullet = "- "
padLength = 3
if maxBulletLength == 0 {
bullet = fmt.Sprintf("%s:", ttyTextToString(e.definitions[i].name))
} else {
bullet = itemStyle.bullet + " "
padLength = len([]rune(bullet)) + 1
}
}
p := itemToParagraph(
e,
definition.value,
padRight(fmt.Sprintf("%s%s:", bullet, names[i]), maxNameLength+padLength),
bullet = fmt.Sprintf(
"%s %s:",
padRight(bullets[i], maxBulletLength),
ttyTextToString(e.definitions[i].name),
)
renderTTYParagraph(w, p)
}
items[i] = Item(e.definitions[i].value, Bullet(bullet))
}
e.typ = list
e.items = items
renderTTYList(w, e)
}
func renderTTYNumberedDefinitions(w io.Writer, e Entry) {
names := ttyDefinitionNames(e.definitions)
maxNameLength := maxLength(names)
maxDigits := numDigits(len(e.definitions))
var defs []DefinitionItem
for i, definition := range e.definitions {
if i > 0 {
write(w, "\n")
}
p := itemToParagraph(
e,
defs = append(
defs,
Definition(
definition.name,
definition.value,
padRight(
fmt.Sprintf(
"%s %s:",
padRight(fmt.Sprintf("%d.", i+1), maxDigits+1),
names[i],
),
maxNameLength+maxDigits+3,
append(definition.style, Bullet(fmt.Sprintf("%d.", i+1)))...,
),
)
renderTTYParagraph(w, p)
}
e.typ = definitions
e.definitions = defs
renderTTYDefinitions(w, e)
}
func ttyCellTexts(rows []TableRow) [][]string {

View File

@ -724,6 +724,65 @@ foo bar
baz
qux
quux
`
if "\n"+b.String() != expect {
t.Fatal("\n" + b.String())
}
})
t.Run("varying length bullets", 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("escape custom bullet", func(t *testing.T) {
doc := textfmt.Doc(
textfmt.Wrap(
textfmt.List(
textfmt.Item(textfmt.Text("foo bar baz"), textfmt.Bullet("\x00>")),
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 {
@ -1193,6 +1252,88 @@ one: foo bar
baz
two: qux
three: quux
`
if "\n"+b.String() != expect {
t.Fatal("\n" + b.String())
}
})
t.Run("varying length bullets", 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("escape 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("\x00>"),
),
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 {

View File

@ -60,11 +60,19 @@ func itemToParagraph(list Entry, itemText Txt, prefix ...string) Entry {
var prefixLength int
for _, p := range prefix {
if len(p) == 0 {
continue
}
prefixLength += len([]rune(p)) + 1
}
var prefixText []Txt
for _, p := range prefix {
if len(p) == 0 {
continue
}
prefixText = append(prefixText, Text(p))
}