8 "github.com/stretchr/testify/require"
11 func TestTerminalMarkdownBlocksAndMarks(t *testing.T) {
12 testCases := []struct {
19 name: "headings and a full width rule",
20 text: "# First\n###### Last ###\n---",
22 want: []string{"\x1b[1mFirst\x1b[0m", "\x1b[1mLast\x1b[0m", "\x1b[2m------------\x1b[0m"},
25 name: "bold italic and code",
26 text: "**bold** __also__ *lean* _also_ `code`",
28 want: []string{"\x1b[1mbold\x1b[0m \x1b[1malso\x1b[0m \x1b[3mlean\x1b[0m \x1b[3malso\x1b[0m \x1b[7mcode\x1b[0m"},
31 name: "nested marks restore the outer style",
32 text: "**bold *lean* end**",
34 want: []string{"\x1b[1mbold \x1b[0m\x1b[1m\x1b[3mlean\x1b[0m\x1b[1m end\x1b[0m"},
37 name: "links show different URLs once",
38 text: "[site](https://example.com) [https://example.com](https://example.com)",
40 want: []string{"site (https://example.com) https://example.com"},
43 name: "code lines keep literal marks and indent",
44 text: "```text\n**raw**\n code\n\n```",
46 want: []string{" \x1b[2m**raw**\x1b[0m", " \x1b[2m code\x1b[0m", " "},
49 name: "quotes repeat the bar when wrapped",
50 text: "> first next last\n> end",
52 want: []string{"| first next", "| last", "| end"},
55 name: "lists hang wrapped text after their marks",
56 text: "- first next last\n+ small\n* short\n12. first next last\n2) next",
58 want: []string{"- first next", " last", "- small", "- short", "12. first", " next", " last", "2) next"},
61 name: "unclosed marks stay literal and cannot cross a line",
62 text: "**open\n*open\n_open\n`open\n[open](link\n```text\nplain",
64 want: []string{"**open", "*open", "_open", "`open", "[open](link", "```text", "plain"},
67 name: "escaped marks and unsupported strike stay literal",
68 text: "\\*plain\\* snake_case ~~kept~~ <details> < AT&T",
70 want: []string{"*plain* snake_case ~~kept~~ <details> < AT&T"},
73 name: "styled text wraps at words and resets every line",
74 text: "**first next last**",
76 want: []string{"\x1b[1mfirst next\x1b[0m", "\x1b[1mlast\x1b[0m"},
79 name: "long words split without loss",
82 want: []string{"abcd", "efgh", "ij"},
85 name: "terminal commands are not passed through",
86 text: "safe\x1b[31m\tend",
88 want: []string{"safe[31m end"},
92 for _, tc := range testCases {
93 t.Run(tc.name, func(t *testing.T) {
94 require.Equal(t, tc.want, terminalMarkdown(tc.text, tc.columns))
99 func TestTerminalMarkdownNarrowLinesKeepQuoteAndListMarks(t *testing.T) {
100 testCases := []struct {
107 name: "quote bar repeats without its space when width is two",
110 want: []string{"|a", "|b"},
113 name: "bullet takes its own line when width is two",
116 want: []string{"-", "ab"},
119 name: "full number takes its own line even when wider than width",
122 want: []string{"12.", "ab"},
125 name: "narrow quote drops indent but keeps its bar",
128 want: []string{"|a", "|b"},
131 name: "one column keeps a quote bar and a whole glyph",
134 want: []string{"|a", "|b"},
137 name: "one column keeps a bullet then wraps the item",
140 want: []string{"-", "a", "b"},
143 name: "quote with room keeps its space on each line",
146 want: []string{"| a", "| b"},
150 for _, tc := range testCases {
151 t.Run(tc.name, func(t *testing.T) {
152 require.Equal(t, tc.want, terminalMarkdown(tc.text, tc.columns))
157 func TestTerminalMarkdownTablesFitAndWrap(t *testing.T) {
158 text := "| Name | Detail |\n| :--- | ---: |\n| Short | Some longer words |\n| `a|b` | left \\| right |"
159 lines := terminalMarkdown(text, 23)
160 require.Equal(t, "Name | Detail ", terminalMarkdownWithoutSGR(lines[0]))
161 require.Equal(t, "\x1b[2m------+----------------\x1b[0m", lines[1])
165 for _, line := range lines {
166 value := terminalMarkdownWithoutSGR(line)
167 require.LessOrEqual(t, terminalMarkdownColumns(terminalMarkdownPlain(value, 0)), 23)
169 plain = append(plain, value)
172 require.Contains(t, strings.Join(plain, "\n"), "Some")
173 require.Contains(t, strings.Join(plain, "\n"), "longer")
174 require.Contains(t, strings.Join(plain, "\n"), "a|b")
175 require.Contains(t, strings.Join(plain, "\n"), "left |")
176 require.Contains(t, lines[0], "\x1b[1mName\x1b[0m")
179 func TestTerminalMarkdownSmallTablesKeepNaturalWidths(t *testing.T) {
180 lines := terminalMarkdown("Name | Size\n--- | ---\nOne | Big", 80)
181 require.Equal(t, []string{
182 "\x1b[1mName\x1b[0m | \x1b[1mSize\x1b[0m",
183 "\x1b[2m-----+-----\x1b[0m",
188 func TestTerminalMarkdownSingleColumnTablesKeepTheirHeaderRule(t *testing.T) {
189 require.Equal(t, []string{
190 "\x1b[1mName\x1b[0m",
191 "\x1b[2m----\x1b[0m",
193 }, terminalMarkdown("| Name |\n| --- |\n| One |", 80))
196 func TestTerminalMarkdownSingleCharacterColumnsStayNarrow(t *testing.T) {
197 require.Equal(t, []string{
198 "\x1b[1mA\x1b[0m | \x1b[1mB\x1b[0m",
199 "\x1b[2m--+--\x1b[0m",
201 }, terminalMarkdown("A | B\n--- | ---\nx | y", 80))
204 func TestTerminalMarkdownCodeWrapKeepsSpacesAndMarks(t *testing.T) {
205 require.Equal(t, []string{
206 " \x1b[2m ab\x1b[0m",
207 " \x1b[2m**cd\x1b[0m",
209 }, terminalMarkdown("```\n ab**cd**\n```", 6))
212 func TestTerminalMarkdownWideCharactersUseTwoColumns(t *testing.T) {
213 require.Equal(t, []string{"界界", "界"}, terminalMarkdown("界界界", 4))
214 require.Equal(t, []string{"e界", "e\u0301"}, terminalMarkdown("e界e\u0301", 4))
216 lines := terminalMarkdown("Word | Mark\n--- | ---\n界界界 | eee", 13)
220 for _, line := range lines {
221 value := terminalMarkdownWithoutSGR(line)
222 require.LessOrEqual(t, terminalMarkdownColumns(terminalMarkdownPlain(value, 0)), 13)
224 plain = append(plain, value)
227 require.Contains(t, strings.Join(plain, "\n"), "界界")
228 require.Contains(t, strings.Join(plain, "\n"), "ee")
231 func TestTerminalMarkdownTablesStackWhenColumnsCannotFit(t *testing.T) {
232 lines := terminalMarkdown("First | Second\n--- | ---\nalpha | beta", 4)
234 var text strings.Builder
236 for _, line := range lines {
237 plain := terminalMarkdownWithoutSGR(line)
238 require.LessOrEqual(t, len(plain), 4)
239 text.WriteString(plain)
242 require.Equal(t, "FirstSecond----alphabeta", text.String())
245 func TestTerminalMarkdownWidthDefaultsToEighty(t *testing.T) {
246 require.Equal(t, terminalMarkdown(strings.Repeat("a", 81), 80), terminalMarkdown(strings.Repeat("a", 81), 0))
249 func terminalMarkdownWithoutSGR(text string) string {
250 return regexp.MustCompile("\x1b\\[[0-9]+m").ReplaceAllString(text, "")