]> git.giacomolli.uk - onechat/blob - terminal_markdown_test.go
Separate multiline Go statements
[onechat] / terminal_markdown_test.go
1 package main
2
3 import (
4 "regexp"
5 "strings"
6 "testing"
7
8 "github.com/stretchr/testify/require"
9 )
10
11 func TestTerminalMarkdownBlocksAndMarks(t *testing.T) {
12 testCases := []struct {
13 name string
14 text string
15 columns int
16 want []string
17 }{
18 {
19 name: "headings and a full width rule",
20 text: "# First\n###### Last ###\n---",
21 columns: 12,
22 want: []string{"\x1b[1mFirst\x1b[0m", "\x1b[1mLast\x1b[0m", "\x1b[2m------------\x1b[0m"},
23 },
24 {
25 name: "bold italic and code",
26 text: "**bold** __also__ *lean* _also_ `code`",
27 columns: 80,
28 want: []string{"\x1b[1mbold\x1b[0m \x1b[1malso\x1b[0m \x1b[3mlean\x1b[0m \x1b[3malso\x1b[0m \x1b[7mcode\x1b[0m"},
29 },
30 {
31 name: "nested marks restore the outer style",
32 text: "**bold *lean* end**",
33 columns: 80,
34 want: []string{"\x1b[1mbold \x1b[0m\x1b[1m\x1b[3mlean\x1b[0m\x1b[1m end\x1b[0m"},
35 },
36 {
37 name: "links show different URLs once",
38 text: "[site](https://example.com) [https://example.com](https://example.com)",
39 columns: 80,
40 want: []string{"site (https://example.com) https://example.com"},
41 },
42 {
43 name: "code lines keep literal marks and indent",
44 text: "```text\n**raw**\n code\n\n```",
45 columns: 80,
46 want: []string{" \x1b[2m**raw**\x1b[0m", " \x1b[2m code\x1b[0m", " "},
47 },
48 {
49 name: "quotes repeat the bar when wrapped",
50 text: "> first next last\n> end",
51 columns: 12,
52 want: []string{"| first next", "| last", "| end"},
53 },
54 {
55 name: "lists hang wrapped text after their marks",
56 text: "- first next last\n+ small\n* short\n12. first next last\n2) next",
57 columns: 12,
58 want: []string{"- first next", " last", "- small", "- short", "12. first", " next", " last", "2) next"},
59 },
60 {
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",
63 columns: 80,
64 want: []string{"**open", "*open", "_open", "`open", "[open](link", "```text", "plain"},
65 },
66 {
67 name: "escaped marks and unsupported strike stay literal",
68 text: "\\*plain\\* snake_case ~~kept~~ <details> &lt; AT&T",
69 columns: 80,
70 want: []string{"*plain* snake_case ~~kept~~ <details> &lt; AT&T"},
71 },
72 {
73 name: "styled text wraps at words and resets every line",
74 text: "**first next last**",
75 columns: 10,
76 want: []string{"\x1b[1mfirst next\x1b[0m", "\x1b[1mlast\x1b[0m"},
77 },
78 {
79 name: "long words split without loss",
80 text: "abcdefghij",
81 columns: 4,
82 want: []string{"abcd", "efgh", "ij"},
83 },
84 {
85 name: "terminal commands are not passed through",
86 text: "safe\x1b[31m\tend",
87 columns: 80,
88 want: []string{"safe[31m end"},
89 },
90 }
91
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))
95 })
96 }
97 }
98
99 func TestTerminalMarkdownNarrowLinesKeepQuoteAndListMarks(t *testing.T) {
100 testCases := []struct {
101 name string
102 text string
103 columns int
104 want []string
105 }{
106 {
107 name: "quote bar repeats without its space when width is two",
108 text: "> ab",
109 columns: 2,
110 want: []string{"|a", "|b"},
111 },
112 {
113 name: "bullet takes its own line when width is two",
114 text: "- ab",
115 columns: 2,
116 want: []string{"-", "ab"},
117 },
118 {
119 name: "full number takes its own line even when wider than width",
120 text: "12. ab",
121 columns: 2,
122 want: []string{"12.", "ab"},
123 },
124 {
125 name: "narrow quote drops indent but keeps its bar",
126 text: " > ab",
127 columns: 2,
128 want: []string{"|a", "|b"},
129 },
130 {
131 name: "one column keeps a quote bar and a whole glyph",
132 text: "> ab",
133 columns: 1,
134 want: []string{"|a", "|b"},
135 },
136 {
137 name: "one column keeps a bullet then wraps the item",
138 text: "- ab",
139 columns: 1,
140 want: []string{"-", "a", "b"},
141 },
142 {
143 name: "quote with room keeps its space on each line",
144 text: "> ab",
145 columns: 3,
146 want: []string{"| a", "| b"},
147 },
148 }
149
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))
153 })
154 }
155 }
156
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])
162
163 var plain []string
164
165 for _, line := range lines {
166 value := terminalMarkdownWithoutSGR(line)
167 require.LessOrEqual(t, terminalMarkdownColumns(terminalMarkdownPlain(value, 0)), 23)
168
169 plain = append(plain, value)
170 }
171
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")
177 }
178
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",
184 "One | Big ",
185 }, lines)
186 }
187
188 func TestTerminalMarkdownSingleColumnTablesKeepTheirHeaderRule(t *testing.T) {
189 require.Equal(t, []string{
190 "\x1b[1mName\x1b[0m",
191 "\x1b[2m----\x1b[0m",
192 "One ",
193 }, terminalMarkdown("| Name |\n| --- |\n| One |", 80))
194 }
195
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",
200 "x | y",
201 }, terminalMarkdown("A | B\n--- | ---\nx | y", 80))
202 }
203
204 func TestTerminalMarkdownCodeWrapKeepsSpacesAndMarks(t *testing.T) {
205 require.Equal(t, []string{
206 " \x1b[2m ab\x1b[0m",
207 " \x1b[2m**cd\x1b[0m",
208 " \x1b[2m**\x1b[0m",
209 }, terminalMarkdown("```\n ab**cd**\n```", 6))
210 }
211
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))
215
216 lines := terminalMarkdown("Word | Mark\n--- | ---\n界界界 | eee", 13)
217
218 var plain []string
219
220 for _, line := range lines {
221 value := terminalMarkdownWithoutSGR(line)
222 require.LessOrEqual(t, terminalMarkdownColumns(terminalMarkdownPlain(value, 0)), 13)
223
224 plain = append(plain, value)
225 }
226
227 require.Contains(t, strings.Join(plain, "\n"), "界界")
228 require.Contains(t, strings.Join(plain, "\n"), "ee")
229 }
230
231 func TestTerminalMarkdownTablesStackWhenColumnsCannotFit(t *testing.T) {
232 lines := terminalMarkdown("First | Second\n--- | ---\nalpha | beta", 4)
233
234 var text strings.Builder
235
236 for _, line := range lines {
237 plain := terminalMarkdownWithoutSGR(line)
238 require.LessOrEqual(t, len(plain), 4)
239 text.WriteString(plain)
240 }
241
242 require.Equal(t, "FirstSecond----alphabeta", text.String())
243 }
244
245 func TestTerminalMarkdownWidthDefaultsToEighty(t *testing.T) {
246 require.Equal(t, terminalMarkdown(strings.Repeat("a", 81), 80), terminalMarkdown(strings.Repeat("a", 81), 0))
247 }
248
249 func terminalMarkdownWithoutSGR(text string) string {
250 return regexp.MustCompile("\x1b\\[[0-9]+m").ReplaceAllString(text, "")
251 }