diff --git a/lib.go b/lib.go index 9c1ff64..5185588 100644 --- a/lib.go +++ b/lib.go @@ -62,18 +62,17 @@ type SyntaxItem struct { } type Entry struct { - typ int - text Txt - titleLevel int - items []ListItem - definitions []DefinitionItem - rows []TableRow - syntax SyntaxItem - wrapWidth int - wrapWidthFirst int - indent int - indentFirst int - man struct { + typ int + text Txt + titleLevel int + items []ListItem + definitions []DefinitionItem + rows []TableRow + syntax SyntaxItem + wrapWidth int + indent int + indentFirst int + man struct { section int date time.Time version string diff --git a/runoff.go b/runoff.go index 109ff38..86aa4a0 100644 --- a/runoff.go +++ b/runoff.go @@ -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") - } - - 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) + 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)))..., + ) } + + 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") - } - - 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) + 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)))..., + ) } + + e.typ = definitions + e.definitions = defs + renderRoffDefinitions(w, e) } func renderRoffTable(w io.Writer, e Entry) { diff --git a/runoff_test.go b/runoff_test.go index 0fffc16..9ebf358 100644 --- a/runoff_test.go +++ b/runoff_test.go @@ -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 { diff --git a/teletype.go b/teletype.go index 8bbbe00..c177e5d 100644 --- a/teletype.go +++ b/teletype.go @@ -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) + bullets = append(bullets, bullet) } - - renderTTYParagraph(w, p) } -} -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") - } - - 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) + 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 + 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) { - 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, - definition.value, - padRight( - fmt.Sprintf( - "%s %s:", - padRight(fmt.Sprintf("%d.", i+1), maxDigits+1), - names[i], - ), - maxNameLength+maxDigits+3, + defs = append( + defs, + Definition( + definition.name, + definition.value, + 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 { diff --git a/teletype_test.go b/teletype_test.go index 9bb90f8..d8295b8 100644 --- a/teletype_test.go +++ b/teletype_test.go @@ -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 { diff --git a/text.go b/text.go index ec5de6c..ab471ea 100644 --- a/text.go +++ b/text.go @@ -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)) }