forked from
tangled.org/core
Monorepo for Tangled
1package markup
2
3import (
4 "bytes"
5 "strings"
6 "testing"
7
8 textension "tangled.org/core/appview/pages/markup/extension"
9)
10
11func TestMermaidExtension(t *testing.T) {
12 tests := []struct {
13 name string
14 markdown string
15 contains string
16 notContains string
17 }{
18 {
19 name: "mermaid block produces pre.mermaid",
20 markdown: "```mermaid\ngraph TD\n A-->B\n```",
21 contains: `<pre class="mermaid">`,
22 notContains: `<code class="language-mermaid"`,
23 },
24 {
25 name: "mermaid block contains diagram source",
26 markdown: "```mermaid\ngraph TD\n A-->B\n```",
27 contains: "graph TD",
28 },
29 {
30 name: "non-mermaid code block is not affected",
31 markdown: "```go\nfunc main() {}\n```",
32 contains: `<pre class="chroma">`,
33 },
34 }
35
36 for _, tt := range tests {
37 t.Run(tt.name, func(t *testing.T) {
38 md := NewMarkdown("tangled.org")
39
40 var buf bytes.Buffer
41 if err := md.Convert([]byte(tt.markdown), &buf); err != nil {
42 t.Fatalf("failed to convert markdown: %v", err)
43 }
44
45 result := buf.String()
46 if !strings.Contains(result, tt.contains) {
47 t.Errorf("expected output to contain:\n%s\ngot:\n%s", tt.contains, result)
48 }
49 if tt.notContains != "" && strings.Contains(result, tt.notContains) {
50 t.Errorf("expected output NOT to contain:\n%s\ngot:\n%s", tt.notContains, result)
51 }
52 })
53 }
54}
55
56func TestAtExtension_Rendering(t *testing.T) {
57 tests := []struct {
58 name string
59 markdown string
60 expected string
61 }{
62 {
63 name: "renders simple at mention",
64 markdown: "Hello @user.tngl.sh!",
65 expected: `<p>Hello <a href="/user.tngl.sh" class="mention">@user.tngl.sh</a>!</p>`,
66 },
67 {
68 name: "renders multiple at mentions",
69 markdown: "Hi @alice.tngl.sh and @bob.example.com",
70 expected: `<p>Hi <a href="/alice.tngl.sh" class="mention">@alice.tngl.sh</a> and <a href="/bob.example.com" class="mention">@bob.example.com</a></p>`,
71 },
72 {
73 name: "renders at mention in parentheses",
74 markdown: "Check this out (@user.tngl.sh)",
75 expected: `<p>Check this out (<a href="/user.tngl.sh" class="mention">@user.tngl.sh</a>)</p>`,
76 },
77 {
78 name: "does not render email",
79 markdown: "Contact me at test@example.com",
80 expected: `<p>Contact me at <a href="mailto:test@example.com">test@example.com</a></p>`,
81 },
82 {
83 name: "renders at mention with hyphen",
84 markdown: "Follow @user-name.tngl.sh",
85 expected: `<p>Follow <a href="/user-name.tngl.sh" class="mention">@user-name.tngl.sh</a></p>`,
86 },
87 {
88 name: "renders at mention with numbers",
89 markdown: "@user123.test456.social",
90 expected: `<p><a href="/user123.test456.social" class="mention">@user123.test456.social</a></p>`,
91 },
92 {
93 name: "at mention at start of line",
94 markdown: "@user.tngl.sh is cool",
95 expected: `<p><a href="/user.tngl.sh" class="mention">@user.tngl.sh</a> is cool</p>`,
96 },
97 }
98
99 for _, tt := range tests {
100 t.Run(tt.name, func(t *testing.T) {
101 md := NewMarkdown("tangled.org")
102
103 var buf bytes.Buffer
104 if err := md.Convert([]byte(tt.markdown), &buf); err != nil {
105 t.Fatalf("failed to convert markdown: %v", err)
106 }
107
108 result := buf.String()
109 if result != tt.expected+"\n" {
110 t.Errorf("expected:\n%s\ngot:\n%s", tt.expected, result)
111 }
112 })
113 }
114}
115
116func TestAtExtension_WithOtherMarkdown(t *testing.T) {
117 tests := []struct {
118 name string
119 markdown string
120 contains string
121 }{
122 {
123 name: "at mention with bold",
124 markdown: "**Hello @user.tngl.sh**",
125 contains: `<strong>Hello <a href="/user.tngl.sh" class="mention">@user.tngl.sh</a></strong>`,
126 },
127 {
128 name: "at mention with italic",
129 markdown: "*Check @user.tngl.sh*",
130 contains: `<em>Check <a href="/user.tngl.sh" class="mention">@user.tngl.sh</a></em>`,
131 },
132 {
133 name: "at mention in list",
134 markdown: "- Item 1\n- @user.tngl.sh\n- Item 3",
135 contains: `<a href="/user.tngl.sh" class="mention">@user.tngl.sh</a>`,
136 },
137 {
138 name: "at mention in link",
139 markdown: "[@regnault.dev](https://regnault.dev)",
140 contains: `<a href="https://regnault.dev">@regnault.dev</a>`,
141 },
142 {
143 name: "at mention in link again",
144 markdown: "[check out @regnault.dev](https://regnault.dev)",
145 contains: `<a href="https://regnault.dev">check out @regnault.dev</a>`,
146 },
147 {
148 name: "at mention in link again, multiline",
149 markdown: "[\ncheck out @regnault.dev](https://regnault.dev)",
150 contains: "<a href=\"https://regnault.dev\">\ncheck out @regnault.dev</a>",
151 },
152 }
153
154 for _, tt := range tests {
155 t.Run(tt.name, func(t *testing.T) {
156 md := NewMarkdown("tangled.org")
157
158 var buf bytes.Buffer
159 if err := md.Convert([]byte(tt.markdown), &buf); err != nil {
160 t.Fatalf("failed to convert markdown: %v", err)
161 }
162
163 result := buf.String()
164 if !bytes.Contains([]byte(result), []byte(tt.contains)) {
165 t.Errorf("expected output to contain:\n%s\ngot:\n%s", tt.contains, result)
166 }
167 })
168 }
169}
170
171func TestCodeCopyExtension(t *testing.T) {
172 tests := []struct {
173 name string
174 markdown string
175 contains string
176 notContains string
177 }{
178 {
179 name: "fenced code block gets copy wrapper",
180 markdown: "```go\nfunc main() {}\n```",
181 contains: `<div class="code-copy-wrapper">`,
182 },
183 {
184 name: "fenced code block gets copy button",
185 markdown: "```go\nfunc main() {}\n```",
186 contains: `<button class="code-copy-btn"`,
187 },
188 {
189 name: "copy button has both icon spans",
190 markdown: "```go\nfunc main() {}\n```",
191 contains: `<span class="copy-icon">`,
192 },
193 {
194 name: "mermaid blocks are not wrapped",
195 markdown: "```mermaid\ngraph TD\n A-->B\n```",
196 notContains: `code-copy-wrapper`,
197 },
198 {
199 name: "inline code is not affected",
200 markdown: "use `fmt.Println` here",
201 notContains: `code-copy-wrapper`,
202 },
203 {
204 name: "fenced block without language gets wrapper",
205 markdown: "```\nsome text\n```",
206 contains: `<div class="code-copy-wrapper">`,
207 },
208 }
209
210 for _, tt := range tests {
211 t.Run(tt.name, func(t *testing.T) {
212 md := NewMarkdown("tangled.org")
213 textension.NewCodeCopyExt(nil).Extend(md)
214
215 var buf bytes.Buffer
216 if err := md.Convert([]byte(tt.markdown), &buf); err != nil {
217 t.Fatalf("failed to convert markdown: %v", err)
218 }
219
220 result := buf.String()
221 if tt.contains != "" && !strings.Contains(result, tt.contains) {
222 t.Errorf("expected output to contain:\n%s\ngot:\n%s", tt.contains, result)
223 }
224 if tt.notContains != "" && strings.Contains(result, tt.notContains) {
225 t.Errorf("expected output NOT to contain:\n%s\ngot:\n%s", tt.notContains, result)
226 }
227 })
228 }
229}
230
231func TestCodeCopyButtonSurvivesSanitization(t *testing.T) {
232 md := NewMarkdown("tangled.org")
233 textension.NewCodeCopyExt(nil).Extend(md)
234
235 var buf bytes.Buffer
236 if err := md.Convert([]byte("```go\nfunc main() {}\n```"), &buf); err != nil {
237 t.Fatalf("failed to convert markdown: %v", err)
238 }
239
240 sanitizer := NewSanitizer()
241 result := sanitizer.SanitizeDefault(buf.String())
242
243 for _, expected := range []string{
244 `<div class="code-copy-wrapper">`,
245 `<button class="code-copy-btn"`,
246 `<span class="copy-icon">`,
247 `<span class="check-icon">`,
248 } {
249 if !strings.Contains(result, expected) {
250 t.Errorf("expected sanitized output to contain:\n%s\ngot:\n%s", expected, result)
251 }
252 }
253}