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

23
lib.go
View File

@ -62,18 +62,17 @@ type SyntaxItem struct {
} }
type Entry struct { type Entry struct {
typ int typ int
text Txt text Txt
titleLevel int titleLevel int
items []ListItem items []ListItem
definitions []DefinitionItem definitions []DefinitionItem
rows []TableRow rows []TableRow
syntax SyntaxItem syntax SyntaxItem
wrapWidth int wrapWidth int
wrapWidthFirst int indent int
indent int indentFirst int
indentFirst int man struct {
man struct {
section int section int
date time.Time date time.Time
version string version string

214
runoff.go
View File

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

View File

@ -144,39 +144,39 @@ textfmt supports the following entries:
.sp 1v .sp 1v
.in 2 .in 2
.ti 0 .ti 0
\(bu CodeBlock \(bu\~CodeBlock
.br .br
.in 2 .in 2
.ti 0 .ti 0
\(bu DefinitionList \(bu\~DefinitionList
.br .br
.in 2 .in 2
.ti 0 .ti 0
\(bu List \(bu\~List
.br .br
.in 2 .in 2
.ti 0 .ti 0
\(bu NumberedDefinitionList \(bu\~NumberedDefinitionList
.br .br
.in 2 .in 2
.ti 0 .ti 0
\(bu NumberedList \(bu\~NumberedList
.br .br
.in 2 .in 2
.ti 0 .ti 0
\(bu Paragraph \(bu\~Paragraph
.br .br
.in 2 .in 2
.ti 0 .ti 0
\(bu Syntax \(bu\~Syntax
.br .br
.in 2 .in 2
.ti 0 .ti 0
\(bu Table \(bu\~Table
.br .br
.in 2 .in 2
.ti 0 .ti 0
\(bu Title \(bu\~Title
.br .br
.sp 1v .sp 1v
.in 0 .in 0
@ -186,39 +186,39 @@ textfmt supports the following entries:
.sp 1v .sp 1v
.in 26 .in 26
.ti 0 .ti 0
\(bu CodeBlock:\~\~\~\~\~\~\~\~\~\~\~\~\~\~a multiline block of code \(bu\~CodeBlock:\~\~\~\~\~\~\~\~\~\~\~\~\~\~a multiline block of code
.br .br
.in 26 .in 26
.ti 0 .ti 0
\(bu DefinitionList:\~\~\~\~\~\~\~\~\~a list of definitions like this one \(bu\~DefinitionList:\~\~\~\~\~\~\~\~\~a list of definitions like this one
.br .br
.in 26 .in 26
.ti 0 .ti 0
\(bu List:\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~a list of items \(bu\~List:\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~a list of items
.br .br
.in 26 .in 26
.ti 0 .ti 0
\(bu NumberedDefinitionList:\~numbered definitions \(bu\~NumberedDefinitionList:\~numbered definitions
.br .br
.in 26 .in 26
.ti 0 .ti 0
\(bu NumberedList:\~\~\~\~\~\~\~\~\~\~\~numbered list \(bu\~NumberedList:\~\~\~\~\~\~\~\~\~\~\~numbered list
.br .br
.in 26 .in 26
.ti 0 .ti 0
\(bu Paragraph:\~\~\~\~\~\~\~\~\~\~\~\~\~\~paragraph of text \(bu\~Paragraph:\~\~\~\~\~\~\~\~\~\~\~\~\~\~paragraph of text
.br .br
.in 26 .in 26
.ti 0 .ti 0
\(bu Syntax:\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~a syntax expression \(bu\~Syntax:\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~a syntax expression
.br .br
.in 26 .in 26
.ti 0 .ti 0
\(bu Table:\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~a table \(bu\~Table:\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~a table
.br .br
.in 26 .in 26
.ti 0 .ti 0
\(bu Title:\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~a title \(bu\~Title:\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~a title
` `
if b.String() != expect { if b.String() != expect {
@ -354,39 +354,39 @@ textfmt supports the following entries:
.sp 1v .sp 1v
.in 2 .in 2
.ti 0 .ti 0
\(bu CodeBlock \(bu\~CodeBlock
.br .br
.in 2 .in 2
.ti 0 .ti 0
\(bu DefinitionList \(bu\~DefinitionList
.br .br
.in 2 .in 2
.ti 0 .ti 0
\(bu List \(bu\~List
.br .br
.in 2 .in 2
.ti 0 .ti 0
\(bu NumberedDefinitionList \(bu\~NumberedDefinitionList
.br .br
.in 2 .in 2
.ti 0 .ti 0
\(bu NumberedList \(bu\~NumberedList
.br .br
.in 2 .in 2
.ti 0 .ti 0
\(bu Paragraph \(bu\~Paragraph
.br .br
.in 2 .in 2
.ti 0 .ti 0
\(bu Syntax \(bu\~Syntax
.br .br
.in 2 .in 2
.ti 0 .ti 0
\(bu Table \(bu\~Table
.br .br
.in 2 .in 2
.ti 0 .ti 0
\(bu Title \(bu\~Title
.br .br
.sp 1v .sp 1v
.in 0 .in 0
@ -396,39 +396,39 @@ textfmt supports the following entries:
.sp 1v .sp 1v
.in 26 .in 26
.ti 0 .ti 0
\(bu CodeBlock:\~\~\~\~\~\~\~\~\~\~\~\~\~\~a multiline block of code \(bu\~CodeBlock:\~\~\~\~\~\~\~\~\~\~\~\~\~\~a multiline block of code
.br .br
.in 26 .in 26
.ti 0 .ti 0
\(bu DefinitionList:\~\~\~\~\~\~\~\~\~a list of definitions like this one \(bu\~DefinitionList:\~\~\~\~\~\~\~\~\~a list of definitions like this one
.br .br
.in 26 .in 26
.ti 0 .ti 0
\(bu List:\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~a list of items \(bu\~List:\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~a list of items
.br .br
.in 26 .in 26
.ti 0 .ti 0
\(bu NumberedDefinitionList:\~numbered definitions \(bu\~NumberedDefinitionList:\~numbered definitions
.br .br
.in 26 .in 26
.ti 0 .ti 0
\(bu NumberedList:\~\~\~\~\~\~\~\~\~\~\~numbered list \(bu\~NumberedList:\~\~\~\~\~\~\~\~\~\~\~numbered list
.br .br
.in 26 .in 26
.ti 0 .ti 0
\(bu Paragraph:\~\~\~\~\~\~\~\~\~\~\~\~\~\~paragraph of text \(bu\~Paragraph:\~\~\~\~\~\~\~\~\~\~\~\~\~\~paragraph of text
.br .br
.in 26 .in 26
.ti 0 .ti 0
\(bu Syntax:\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~a syntax expression \(bu\~Syntax:\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~a syntax expression
.br .br
.in 26 .in 26
.ti 0 .ti 0
\(bu Table:\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~a table \(bu\~Table:\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~a table
.br .br
.in 26 .in 26
.ti 0 .ti 0
\(bu Title:\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~a title \(bu\~Title:\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~\~a title
` `
if b.String() != expect { if b.String() != expect {
@ -781,15 +781,15 @@ This is a paragraph.
const expect = `.in 2 const expect = `.in 2
.ti 0 .ti 0
\(bu this is an item \(bu\~this is an item
.br .br
.in 2 .in 2
.ti 0 .ti 0
\(bu this is another item \(bu\~this is another item
.br .br
.in 2 .in 2
.ti 0 .ti 0
\(bu this is a third item \(bu\~this is a third item
` `
if b.String() != expect { if b.String() != expect {
@ -817,15 +817,15 @@ This is a paragraph.
const expect = `.in 6 const expect = `.in 6
.ti 4 .ti 4
\(bu this is an item \(bu\~this is an item
.br .br
.in 6 .in 6
.ti 4 .ti 4
\(bu this is another item \(bu\~this is another item
.br .br
.in 6 .in 6
.ti 4 .ti 4
\(bu this is a third item \(bu\~this is a third item
` `
if b.String() != expect { if b.String() != expect {
@ -853,15 +853,15 @@ This is a paragraph.
const expect = `.in 0 const expect = `.in 0
.ti 4 .ti 4
\(bu this is an item \(bu\~this is an item
.br .br
.in 0 .in 0
.ti 4 .ti 4
\(bu this is another item \(bu\~this is another item
.br .br
.in 0 .in 0
.ti 4 .ti 4
\(bu this is a third item \(bu\~this is a third item
` `
if b.String() != expect { if b.String() != expect {
@ -889,15 +889,15 @@ This is a paragraph.
const expect = `.in 4 const expect = `.in 4
.ti 0 .ti 0
\(bu this is an item \(bu\~this is an item
.br .br
.in 4 .in 4
.ti 0 .ti 0
\(bu this is another item \(bu\~this is another item
.br .br
.in 4 .in 4
.ti 0 .ti 0
\(bu this is a third item \(bu\~this is a third item
` `
if b.String() != expect { if b.String() != expect {
@ -922,15 +922,15 @@ This is a paragraph.
const expect = ` const expect = `
.in 2 .in 2
.ti 0 .ti 0
* foo bar baz *\~foo bar baz
.br .br
.in 2 .in 2
.ti 0 .ti 0
* qux *\~qux
.br .br
.in 2 .in 2
.ti 0 .ti 0
* quux *\~quux
` `
if "\n"+b.String() != expect { if "\n"+b.String() != expect {
@ -955,15 +955,15 @@ This is a paragraph.
const expect = ` const expect = `
.in 3 .in 3
.ti 0 .ti 0
=> foo bar baz =>\~foo bar baz
.br .br
.in 3 .in 3
.ti 0 .ti 0
=> qux =>\~qux
.br .br
.in 3 .in 3
.ti 0 .ti 0
=> quux =>\~quux
` `
if "\n"+b.String() != expect { if "\n"+b.String() != expect {
@ -997,6 +997,72 @@ qux
.in 0 .in 0
.ti 0 .ti 0
quux 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 { if "\n"+b.String() != expect {
@ -1241,15 +1307,15 @@ quux
const expect = `.in 9 const expect = `.in 9
.ti 0 .ti 0
\(bu red:\~\~\~looks like strawberry \(bu\~red:\~\~\~looks like strawberry
.br .br
.in 9 .in 9
.ti 0 .ti 0
\(bu green:\~looks like grass \(bu\~green:\~looks like grass
.br .br
.in 9 .in 9
.ti 0 .ti 0
\(bu blue:\~\~looks like sky \(bu\~blue:\~\~looks like sky
` `
if b.String() != expect { if b.String() != expect {
@ -1277,15 +1343,15 @@ quux
const expect = `.in 13 const expect = `.in 13
.ti 4 .ti 4
\(bu red:\~\~\~looks like strawberry \(bu\~red:\~\~\~looks like strawberry
.br .br
.in 13 .in 13
.ti 4 .ti 4
\(bu green:\~looks like grass \(bu\~green:\~looks like grass
.br .br
.in 13 .in 13
.ti 4 .ti 4
\(bu blue:\~\~looks like sky \(bu\~blue:\~\~looks like sky
` `
if b.String() != expect { if b.String() != expect {
@ -1314,15 +1380,15 @@ quux
const expect = ` const expect = `
.in 3 .in 3
.ti 3 .ti 3
\(bu red:\~\~\~looks like strawberry \(bu\~red:\~\~\~looks like strawberry
.br .br
.in 3 .in 3
.ti 3 .ti 3
\(bu green:\~looks like grass \(bu\~green:\~looks like grass
.br .br
.in 3 .in 3
.ti 3 .ti 3
\(bu blue:\~\~looks like sky \(bu\~blue:\~\~looks like sky
` `
if "\n"+b.String() != expect { if "\n"+b.String() != expect {
@ -1351,15 +1417,15 @@ quux
const expect = ` const expect = `
.in 13 .in 13
.ti 0 .ti 0
\(bu red:\~\~\~looks like strawberry \(bu\~red:\~\~\~looks like strawberry
.br .br
.in 13 .in 13
.ti 0 .ti 0
\(bu green:\~looks like grass \(bu\~green:\~looks like grass
.br .br
.in 13 .in 13
.ti 0 .ti 0
\(bu blue:\~\~looks like sky \(bu\~blue:\~\~looks like sky
` `
if "\n"+b.String() != expect { if "\n"+b.String() != expect {
@ -1396,15 +1462,15 @@ quux
const expect = ` const expect = `
.in 9 .in 9
.ti 0 .ti 0
* one:\~\~\~foo bar baz *\~one:\~\~\~foo bar baz
.br .br
.in 9 .in 9
.ti 0 .ti 0
* two:\~\~\~qux *\~two:\~\~\~qux
.br .br
.in 9 .in 9
.ti 0 .ti 0
* three:\~quux *\~three:\~quux
` `
if "\n"+b.String() != expect { if "\n"+b.String() != expect {
@ -1441,15 +1507,15 @@ quux
const expect = ` const expect = `
.in 10 .in 10
.ti 0 .ti 0
=> one:\~\~\~foo bar baz =>\~one:\~\~\~foo bar baz
.br .br
.in 10 .in 10
.ti 0 .ti 0
=> two:\~\~\~qux =>\~two:\~\~\~qux
.br .br
.in 10 .in 10
.ti 0 .ti 0
=> three:\~quux =>\~three:\~quux
` `
if "\n"+b.String() != expect { if "\n"+b.String() != expect {
@ -1495,6 +1561,96 @@ two:\~\~\~qux
.in 7 .in 7
.ti 0 .ti 0
three:\~quux 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 { 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) { func renderTTYParagraph(w io.Writer, e Entry) {
var indentation wrapper var indentation wrapper
indentFirst := e.indent + e.indentFirst indentFirst := e.indent + e.indentFirst
wrapWidthFirst := e.wrapWidth + e.wrapWidthFirst
if e.wrapWidth == 0 { if e.wrapWidth == 0 {
indentation = indent(indentFirst, e.indent) indentation = indent(indentFirst, e.indent)
} else { } else {
indentation = wrapIndent(indentFirst, e.indent, wrapWidthFirst, e.wrapWidth) indentation = wrapIndent(indentFirst, e.indent, e.wrapWidth, e.wrapWidth)
} }
w, f := writeWith(w, indentation) w, f := writeWith(w, indentation)
@ -78,95 +77,111 @@ func renderTTYParagraph(w io.Writer, e Entry) {
} }
func renderTTYList(w io.Writer, e Entry) { func renderTTYList(w io.Writer, e Entry) {
for i, item := range e.items { var bullets []string
if i > 0 { for _, item := range e.items {
write(w, "\n")
}
var p Entry
itemStyle := mergeItemStyles(item.style) itemStyle := mergeItemStyles(item.style)
if itemStyle.noBullet { if itemStyle.noBullet {
p = itemToParagraph(e, item.text) bullets = append(bullets, "")
} else { } else {
bullet := "-" bullet := "-"
if itemStyle.bullet != "" { if itemStyle.bullet != "" {
bullet = itemStyle.bullet bullet = itemStyle.bullet
} }
p = itemToParagraph(e, item.text, bullet) bullets = append(bullets, bullet)
} }
renderTTYParagraph(w, p)
} }
}
func renderTTYNumberedList(w io.Writer, e Entry) { maxBulletLength := maxLength(bullets)
maxDigits := numDigits(len(e.items)) for i := range bullets {
bullets[i] = padRight(bullets[i], maxBulletLength)
}
for i, item := range e.items { for i, item := range e.items {
if i > 0 { if i > 0 {
write(w, "\n") 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) 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) { func renderTTYDefinitions(w io.Writer, e Entry) {
names := ttyDefinitionNames(e.definitions) itemStyles := make([]ItemStyle, len(e.definitions))
maxNameLength := maxLength(names) for i := range e.definitions {
for i, definition := range e.definitions { itemStyles[i] = mergeItemStyles(e.definitions[i].style)
if i > 0 {
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%s:", bullet, names[i]), maxNameLength+padLength),
)
renderTTYParagraph(w, p)
} }
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
if maxBulletLength == 0 {
bullet = fmt.Sprintf("%s:", ttyTextToString(e.definitions[i].name))
} else {
bullet = fmt.Sprintf(
"%s %s:",
padRight(bullets[i], maxBulletLength),
ttyTextToString(e.definitions[i].name),
)
}
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) { func renderTTYNumberedDefinitions(w io.Writer, e Entry) {
names := ttyDefinitionNames(e.definitions) var defs []DefinitionItem
maxNameLength := maxLength(names)
maxDigits := numDigits(len(e.definitions))
for i, definition := range e.definitions { for i, definition := range e.definitions {
if i > 0 { defs = append(
write(w, "\n") defs,
} Definition(
definition.name,
p := itemToParagraph( definition.value,
e, append(definition.style, Bullet(fmt.Sprintf("%d.", i+1)))...,
definition.value,
padRight(
fmt.Sprintf(
"%s %s:",
padRight(fmt.Sprintf("%d.", i+1), maxDigits+1),
names[i],
),
maxNameLength+maxDigits+3,
), ),
) )
renderTTYParagraph(w, p)
} }
e.typ = definitions
e.definitions = defs
renderTTYDefinitions(w, e)
} }
func ttyCellTexts(rows []TableRow) [][]string { func ttyCellTexts(rows []TableRow) [][]string {

View File

@ -724,6 +724,65 @@ foo bar
baz baz
qux qux
quux 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 { if "\n"+b.String() != expect {
@ -1193,6 +1252,88 @@ one: foo bar
baz baz
two: qux two: qux
three: quux 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 { if "\n"+b.String() != expect {

View File

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