]> git.giacomolli.uk - onechat/blob - terminal_markdown_test.go
Write private data archives with safe extraction
[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 for _, tc := range testCases {
92 t.Run(tc.name, func(t *testing.T) {
93 require.Equal(t, tc.want, terminalMarkdown(tc.text, tc.columns))
94 })
95 }
96 }
97
98 func TestTerminalMarkdownNarrowLinesKeepQuoteAndListMarks(t *testing.T) {
99 testCases := []struct {
100 name string
101 text string
102 columns int
103 want []string
104 }{
105 {
106 name: "quote bar repeats without its space when width is two",
107 text: "> ab",
108 columns: 2,
109 want: []string{"|a", "|b"},
110 },
111 {
112 name: "bullet takes its own line when width is two",
113 text: "- ab",
114 columns: 2,
115 want: []string{"-", "ab"},
116 },
117 {
118 name: "full number takes its own line even when wider than width",
119 text: "12. ab",
120 columns: 2,
121 want: []string{"12.", "ab"},
122 },
123 {
124 name: "narrow quote drops indent but keeps its bar",
125 text: " > ab",
126 columns: 2,
127 want: []string{"|a", "|b"},
128 },
129 {
130 name: "one column keeps a quote bar and a whole glyph",
131 text: "> ab",
132 columns: 1,
133 want: []string{"|a", "|b"},
134 },
135 {
136 name: "one column keeps a bullet then wraps the item",
137 text: "- ab",
138 columns: 1,
139 want: []string{"-", "a", "b"},
140 },
141 {
142 name: "quote with room keeps its space on each line",
143 text: "> ab",
144 columns: 3,
145 want: []string{"| a", "| b"},
146 },
147 }
148 for _, tc := range testCases {
149 t.Run(tc.name, func(t *testing.T) {
150 require.Equal(t, tc.want, terminalMarkdown(tc.text, tc.columns))
151 })
152 }
153 }
154
155 func TestTerminalMarkdownTablesFitAndWrap(t *testing.T) {
156 text := "| Name | Detail |\n| :--- | ---: |\n| Short | Some longer words |\n| `a|b` | left \\| right |"
157 lines := terminalMarkdown(text, 23)
158 require.Equal(t, "Name | Detail ", terminalMarkdownWithoutSGR(lines[0]))
159 require.Equal(t, "\x1b[2m------+----------------\x1b[0m", lines[1])
160
161 var plain []string
162
163 for _, line := range lines {
164 value := terminalMarkdownWithoutSGR(line)
165 require.LessOrEqual(t, terminalMarkdownColumns(terminalMarkdownPlain(value, 0)), 23)
166
167 plain = append(plain, value)
168 }
169
170 require.Contains(t, strings.Join(plain, "\n"), "Some")
171 require.Contains(t, strings.Join(plain, "\n"), "longer")
172 require.Contains(t, strings.Join(plain, "\n"), "a|b")
173 require.Contains(t, strings.Join(plain, "\n"), "left |")
174 require.Contains(t, lines[0], "\x1b[1mName\x1b[0m")
175 }
176
177 func TestTerminalMarkdownSmallTablesKeepNaturalWidths(t *testing.T) {
178 lines := terminalMarkdown("Name | Size\n--- | ---\nOne | Big", 80)
179 require.Equal(t, []string{
180 "\x1b[1mName\x1b[0m | \x1b[1mSize\x1b[0m",
181 "\x1b[2m-----+-----\x1b[0m",
182 "One | Big ",
183 }, lines)
184 }
185
186 func TestTerminalMarkdownSingleColumnTablesKeepTheirHeaderRule(t *testing.T) {
187 require.Equal(t, []string{
188 "\x1b[1mName\x1b[0m",
189 "\x1b[2m----\x1b[0m",
190 "One ",
191 }, terminalMarkdown("| Name |\n| --- |\n| One |", 80))
192 }
193
194 func TestTerminalMarkdownSingleCharacterColumnsStayNarrow(t *testing.T) {
195 require.Equal(t, []string{
196 "\x1b[1mA\x1b[0m | \x1b[1mB\x1b[0m",
197 "\x1b[2m--+--\x1b[0m",
198 "x | y",
199 }, terminalMarkdown("A | B\n--- | ---\nx | y", 80))
200 }
201
202 func TestTerminalMarkdownCodeWrapKeepsSpacesAndMarks(t *testing.T) {
203 require.Equal(t, []string{
204 " \x1b[2m ab\x1b[0m",
205 " \x1b[2m**cd\x1b[0m",
206 " \x1b[2m**\x1b[0m",
207 }, terminalMarkdown("```\n ab**cd**\n```", 6))
208 }
209
210 func TestTerminalMarkdownWideCharactersUseTwoColumns(t *testing.T) {
211 require.Equal(t, []string{"界界", "界"}, terminalMarkdown("界界界", 4))
212 require.Equal(t, []string{"e界", "e\u0301"}, terminalMarkdown("e界e\u0301", 4))
213
214 lines := terminalMarkdown("Word | Mark\n--- | ---\n界界界 | eee", 13)
215
216 var plain []string
217
218 for _, line := range lines {
219 value := terminalMarkdownWithoutSGR(line)
220 require.LessOrEqual(t, terminalMarkdownColumns(terminalMarkdownPlain(value, 0)), 13)
221
222 plain = append(plain, value)
223 }
224
225 require.Contains(t, strings.Join(plain, "\n"), "界界")
226 require.Contains(t, strings.Join(plain, "\n"), "ee")
227 }
228
229 func TestTerminalMarkdownTablesStackWhenColumnsCannotFit(t *testing.T) {
230 lines := terminalMarkdown("First | Second\n--- | ---\nalpha | beta", 4)
231
232 var text strings.Builder
233
234 for _, line := range lines {
235 plain := terminalMarkdownWithoutSGR(line)
236 require.LessOrEqual(t, len(plain), 4)
237 text.WriteString(plain)
238 }
239
240 require.Equal(t, "FirstSecond----alphabeta", text.String())
241 }
242
243 func TestTerminalMarkdownWidthDefaultsToEighty(t *testing.T) {
244 require.Equal(t, terminalMarkdown(strings.Repeat("a", 81), 80), terminalMarkdown(strings.Repeat("a", 81), 0))
245 }
246
247 func terminalMarkdownWithoutSGR(text string) string {
248 return regexp.MustCompile("\x1b\\[[0-9]+m").ReplaceAllString(text, "")
249 }