This repository has no description
1package markup
2
3import (
4 "bytes"
5 "strings"
6 "testing"
7
8 "tangled.org/core/appview/pages/markup/sanitizer"
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 TestMathExtension(t *testing.T) {
57 tests := []struct {
58 name string
59 markdown string
60 contains string
61 notContains string
62 }{
63 {
64 name: "inline math produces span with mathjax delimiters",
65 markdown: "the famous $E = mc^2$ equation",
66 contains: `<span class="math inline">\(E = mc^2\)</span>`,
67 },
68 {
69 name: "block math produces display span",
70 markdown: "$$\n\\frac{a}{b}\n$$",
71 contains: `<span class="math display">\[`,
72 },
73 {
74 name: "underscores inside math are not treated as emphasis",
75 markdown: "$a_1 + a_2$",
76 contains: `\(a_1 + a_2\)`,
77 notContains: "<em>",
78 },
79 {
80 name: "non-math dollar usage is left alone",
81 markdown: "it costs $5 today",
82 notContains: `class="math`,
83 },
84 {
85 // regression: two currency amounts must not be parsed as one
86 // inline math span (the "$5 and $" .. "10" case).
87 name: "currency pair is not math",
88 markdown: "it costs $5 today and $10 tomorrow",
89 contains: "it costs $5 today and $10 tomorrow",
90 notContains: `class="math`,
91 },
92 {
93 // regression: single-line $$...$$ must keep both the math and the
94 // trailing prose.
95 name: "single-line block keeps trailing prose",
96 markdown: "$$x^2$$ and then prose",
97 contains: "and then prose",
98 },
99 {
100 // math content with < / & must be escaped so the sanitizer keeps
101 // the span and MathJax reads the literal source.
102 name: "angle brackets in math are escaped",
103 markdown: "$a < b$",
104 contains: `\(a < b\)`,
105 },
106 }
107
108 for _, tt := range tests {
109 t.Run(tt.name, func(t *testing.T) {
110 md := NewMarkdown("tangled.org")
111
112 var buf bytes.Buffer
113 if err := md.Convert([]byte(tt.markdown), &buf); err != nil {
114 t.Fatalf("failed to convert markdown: %v", err)
115 }
116
117 result := buf.String()
118 if tt.contains != "" && !strings.Contains(result, tt.contains) {
119 t.Errorf("expected output to contain:\n%s\ngot:\n%s", tt.contains, result)
120 }
121 if tt.notContains != "" && strings.Contains(result, tt.notContains) {
122 t.Errorf("expected output NOT to contain:\n%s\ngot:\n%s", tt.notContains, result)
123 }
124 })
125 }
126}
127
128// The sanitizer must preserve the carrier spans that MathJax renders client-side.
129func TestMathSurvivesSanitizer(t *testing.T) {
130 md := NewMarkdown("tangled.org")
131
132 var buf bytes.Buffer
133 if err := md.Convert([]byte("inline $x^2$ and block\n\n$$\ny^2\n$$"), &buf); err != nil {
134 t.Fatalf("failed to convert markdown: %v", err)
135 }
136
137 out := sanitizer.SanitizeDefault(buf.String())
138
139 for _, want := range []string{`class="math inline"`, `class="math display"`} {
140 if !strings.Contains(out, want) {
141 t.Errorf("sanitizer stripped math span; expected %q in:\n%s", want, out)
142 }
143 }
144}
145
146func TestAtExtension_Rendering(t *testing.T) {
147 tests := []struct {
148 name string
149 markdown string
150 expected string
151 }{
152 {
153 name: "renders simple at mention",
154 markdown: "Hello @user.tngl.sh!",
155 expected: `<p>Hello <a href="/user.tngl.sh" class="mention">@user.tngl.sh</a>!</p>`,
156 },
157 {
158 name: "renders multiple at mentions",
159 markdown: "Hi @alice.tngl.sh and @bob.example.com",
160 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>`,
161 },
162 {
163 name: "renders at mention in parentheses",
164 markdown: "Check this out (@user.tngl.sh)",
165 expected: `<p>Check this out (<a href="/user.tngl.sh" class="mention">@user.tngl.sh</a>)</p>`,
166 },
167 {
168 name: "does not render email",
169 markdown: "Contact me at test@example.com",
170 expected: `<p>Contact me at <a href="mailto:test@example.com">test@example.com</a></p>`,
171 },
172 {
173 name: "renders at mention with hyphen",
174 markdown: "Follow @user-name.tngl.sh",
175 expected: `<p>Follow <a href="/user-name.tngl.sh" class="mention">@user-name.tngl.sh</a></p>`,
176 },
177 {
178 name: "renders at mention with numbers",
179 markdown: "@user123.test456.social",
180 expected: `<p><a href="/user123.test456.social" class="mention">@user123.test456.social</a></p>`,
181 },
182 {
183 name: "at mention at start of line",
184 markdown: "@user.tngl.sh is cool",
185 expected: `<p><a href="/user.tngl.sh" class="mention">@user.tngl.sh</a> is cool</p>`,
186 },
187 }
188
189 for _, tt := range tests {
190 t.Run(tt.name, func(t *testing.T) {
191 md := NewMarkdown("tangled.org")
192
193 var buf bytes.Buffer
194 if err := md.Convert([]byte(tt.markdown), &buf); err != nil {
195 t.Fatalf("failed to convert markdown: %v", err)
196 }
197
198 result := buf.String()
199 if result != tt.expected+"\n" {
200 t.Errorf("expected:\n%s\ngot:\n%s", tt.expected, result)
201 }
202 })
203 }
204}
205
206func TestAtExtension_WithOtherMarkdown(t *testing.T) {
207 tests := []struct {
208 name string
209 markdown string
210 contains string
211 }{
212 {
213 name: "at mention with bold",
214 markdown: "**Hello @user.tngl.sh**",
215 contains: `<strong>Hello <a href="/user.tngl.sh" class="mention">@user.tngl.sh</a></strong>`,
216 },
217 {
218 name: "at mention with italic",
219 markdown: "*Check @user.tngl.sh*",
220 contains: `<em>Check <a href="/user.tngl.sh" class="mention">@user.tngl.sh</a></em>`,
221 },
222 {
223 name: "at mention in list",
224 markdown: "- Item 1\n- @user.tngl.sh\n- Item 3",
225 contains: `<a href="/user.tngl.sh" class="mention">@user.tngl.sh</a>`,
226 },
227 {
228 name: "at mention in link",
229 markdown: "[@regnault.dev](https://regnault.dev)",
230 contains: `<a href="https://regnault.dev">@regnault.dev</a>`,
231 },
232 {
233 name: "at mention in link again",
234 markdown: "[check out @regnault.dev](https://regnault.dev)",
235 contains: `<a href="https://regnault.dev">check out @regnault.dev</a>`,
236 },
237 {
238 name: "at mention in link again, multiline",
239 markdown: "[\ncheck out @regnault.dev](https://regnault.dev)",
240 contains: "<a href=\"https://regnault.dev\">\ncheck out @regnault.dev</a>",
241 },
242 }
243
244 for _, tt := range tests {
245 t.Run(tt.name, func(t *testing.T) {
246 md := NewMarkdown("tangled.org")
247
248 var buf bytes.Buffer
249 if err := md.Convert([]byte(tt.markdown), &buf); err != nil {
250 t.Fatalf("failed to convert markdown: %v", err)
251 }
252
253 result := buf.String()
254 if !bytes.Contains([]byte(result), []byte(tt.contains)) {
255 t.Errorf("expected output to contain:\n%s\ngot:\n%s", tt.contains, result)
256 }
257 })
258 }
259}