]> git.giacomolli.uk - onechat/blob - terminal_markdown_test.go
Give the root tools to start and message children
[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 }