package main import ( "regexp" "strings" "testing" "github.com/stretchr/testify/require" ) func TestTerminalMarkdownBlocksAndMarks(t *testing.T) { testCases := []struct { name string text string columns int want []string }{ { name: "headings and a full width rule", text: "# First\n###### Last ###\n---", columns: 12, want: []string{"\x1b[1mFirst\x1b[0m", "\x1b[1mLast\x1b[0m", "\x1b[2m------------\x1b[0m"}, }, { name: "bold italic and code", text: "**bold** __also__ *lean* _also_ `code`", columns: 80, want: []string{"\x1b[1mbold\x1b[0m \x1b[1malso\x1b[0m \x1b[3mlean\x1b[0m \x1b[3malso\x1b[0m \x1b[7mcode\x1b[0m"}, }, { name: "nested marks restore the outer style", text: "**bold *lean* end**", columns: 80, want: []string{"\x1b[1mbold \x1b[0m\x1b[1m\x1b[3mlean\x1b[0m\x1b[1m end\x1b[0m"}, }, { name: "links show different URLs once", text: "[site](https://example.com) [https://example.com](https://example.com)", columns: 80, want: []string{"site (https://example.com) https://example.com"}, }, { name: "code lines keep literal marks and indent", text: "```text\n**raw**\n code\n\n```", columns: 80, want: []string{" \x1b[2m**raw**\x1b[0m", " \x1b[2m code\x1b[0m", " "}, }, { name: "quotes repeat the bar when wrapped", text: "> first next last\n> end", columns: 12, want: []string{"| first next", "| last", "| end"}, }, { name: "lists hang wrapped text after their marks", text: "- first next last\n+ small\n* short\n12. first next last\n2) next", columns: 12, want: []string{"- first next", " last", "- small", "- short", "12. first", " next", " last", "2) next"}, }, { name: "unclosed marks stay literal and cannot cross a line", text: "**open\n*open\n_open\n`open\n[open](link\n```text\nplain", columns: 80, want: []string{"**open", "*open", "_open", "`open", "[open](link", "```text", "plain"}, }, { name: "escaped marks and unsupported strike stay literal", text: "\\*plain\\* snake_case ~~kept~~
< AT&T", columns: 80, want: []string{"*plain* snake_case ~~kept~~
< AT&T"}, }, { name: "styled text wraps at words and resets every line", text: "**first next last**", columns: 10, want: []string{"\x1b[1mfirst next\x1b[0m", "\x1b[1mlast\x1b[0m"}, }, { name: "long words split without loss", text: "abcdefghij", columns: 4, want: []string{"abcd", "efgh", "ij"}, }, { name: "terminal commands are not passed through", text: "safe\x1b[31m\tend", columns: 80, want: []string{"safe[31m end"}, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { require.Equal(t, tc.want, terminalMarkdown(tc.text, tc.columns)) }) } } func TestTerminalMarkdownNarrowLinesKeepQuoteAndListMarks(t *testing.T) { testCases := []struct { name string text string columns int want []string }{ { name: "quote bar repeats without its space when width is two", text: "> ab", columns: 2, want: []string{"|a", "|b"}, }, { name: "bullet takes its own line when width is two", text: "- ab", columns: 2, want: []string{"-", "ab"}, }, { name: "full number takes its own line even when wider than width", text: "12. ab", columns: 2, want: []string{"12.", "ab"}, }, { name: "narrow quote drops indent but keeps its bar", text: " > ab", columns: 2, want: []string{"|a", "|b"}, }, { name: "one column keeps a quote bar and a whole glyph", text: "> ab", columns: 1, want: []string{"|a", "|b"}, }, { name: "one column keeps a bullet then wraps the item", text: "- ab", columns: 1, want: []string{"-", "a", "b"}, }, { name: "quote with room keeps its space on each line", text: "> ab", columns: 3, want: []string{"| a", "| b"}, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { require.Equal(t, tc.want, terminalMarkdown(tc.text, tc.columns)) }) } } func TestTerminalMarkdownTablesFitAndWrap(t *testing.T) { text := "| Name | Detail |\n| :--- | ---: |\n| Short | Some longer words |\n| `a|b` | left \\| right |" lines := terminalMarkdown(text, 23) require.Equal(t, "Name | Detail ", terminalMarkdownWithoutSGR(lines[0])) require.Equal(t, "\x1b[2m------+----------------\x1b[0m", lines[1]) var plain []string for _, line := range lines { value := terminalMarkdownWithoutSGR(line) require.LessOrEqual(t, terminalMarkdownColumns(terminalMarkdownPlain(value, 0)), 23) plain = append(plain, value) } require.Contains(t, strings.Join(plain, "\n"), "Some") require.Contains(t, strings.Join(plain, "\n"), "longer") require.Contains(t, strings.Join(plain, "\n"), "a|b") require.Contains(t, strings.Join(plain, "\n"), "left |") require.Contains(t, lines[0], "\x1b[1mName\x1b[0m") } func TestTerminalMarkdownSmallTablesKeepNaturalWidths(t *testing.T) { lines := terminalMarkdown("Name | Size\n--- | ---\nOne | Big", 80) require.Equal(t, []string{ "\x1b[1mName\x1b[0m | \x1b[1mSize\x1b[0m", "\x1b[2m-----+-----\x1b[0m", "One | Big ", }, lines) } func TestTerminalMarkdownSingleColumnTablesKeepTheirHeaderRule(t *testing.T) { require.Equal(t, []string{ "\x1b[1mName\x1b[0m", "\x1b[2m----\x1b[0m", "One ", }, terminalMarkdown("| Name |\n| --- |\n| One |", 80)) } func TestTerminalMarkdownSingleCharacterColumnsStayNarrow(t *testing.T) { require.Equal(t, []string{ "\x1b[1mA\x1b[0m | \x1b[1mB\x1b[0m", "\x1b[2m--+--\x1b[0m", "x | y", }, terminalMarkdown("A | B\n--- | ---\nx | y", 80)) } func TestTerminalMarkdownCodeWrapKeepsSpacesAndMarks(t *testing.T) { require.Equal(t, []string{ " \x1b[2m ab\x1b[0m", " \x1b[2m**cd\x1b[0m", " \x1b[2m**\x1b[0m", }, terminalMarkdown("```\n ab**cd**\n```", 6)) } func TestTerminalMarkdownWideCharactersUseTwoColumns(t *testing.T) { require.Equal(t, []string{"界界", "界"}, terminalMarkdown("界界界", 4)) require.Equal(t, []string{"e界", "e\u0301"}, terminalMarkdown("e界e\u0301", 4)) lines := terminalMarkdown("Word | Mark\n--- | ---\n界界界 | eee", 13) var plain []string for _, line := range lines { value := terminalMarkdownWithoutSGR(line) require.LessOrEqual(t, terminalMarkdownColumns(terminalMarkdownPlain(value, 0)), 13) plain = append(plain, value) } require.Contains(t, strings.Join(plain, "\n"), "界界") require.Contains(t, strings.Join(plain, "\n"), "ee") } func TestTerminalMarkdownTablesStackWhenColumnsCannotFit(t *testing.T) { lines := terminalMarkdown("First | Second\n--- | ---\nalpha | beta", 4) var text strings.Builder for _, line := range lines { plain := terminalMarkdownWithoutSGR(line) require.LessOrEqual(t, len(plain), 4) text.WriteString(plain) } require.Equal(t, "FirstSecond----alphabeta", text.String()) } func TestTerminalMarkdownWidthDefaultsToEighty(t *testing.T) { require.Equal(t, terminalMarkdown(strings.Repeat("a", 81), 80), terminalMarkdown(strings.Repeat("a", 81), 0)) } func terminalMarkdownWithoutSGR(text string) string { return regexp.MustCompile("\x1b\\[[0-9]+m").ReplaceAllString(text, "") }