Monorepo for Tangled
at add-code-block-copy-button 253 lines 7.3 kB view raw
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}