code.gitea.io/gitea@v1.19.3/modules/markup/markdown/markdown_test.go (about) 1 // Copyright 2017 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package markdown_test 5 6 import ( 7 "context" 8 "os" 9 "strings" 10 "testing" 11 12 "code.gitea.io/gitea/modules/git" 13 "code.gitea.io/gitea/modules/log" 14 "code.gitea.io/gitea/modules/markup" 15 . "code.gitea.io/gitea/modules/markup/markdown" 16 "code.gitea.io/gitea/modules/setting" 17 "code.gitea.io/gitea/modules/util" 18 19 "github.com/stretchr/testify/assert" 20 ) 21 22 const ( 23 AppURL = "http://localhost:3000/" 24 Repo = "gogits/gogs" 25 AppSubURL = AppURL + Repo + "/" 26 ) 27 28 // these values should match the Repo const above 29 var localMetas = map[string]string{ 30 "user": "gogits", 31 "repo": "gogs", 32 "repoPath": "../../../tests/gitea-repositories-meta/user13/repo11.git/", 33 } 34 35 func TestMain(m *testing.M) { 36 setting.InitProviderAllowEmpty() 37 setting.LoadCommonSettings() 38 if err := git.InitSimple(context.Background()); err != nil { 39 log.Fatal("git init failed, err: %v", err) 40 } 41 markup.Init(&markup.ProcessorHelper{ 42 IsUsernameMentionable: func(ctx context.Context, username string) bool { 43 return username == "r-lyeh" 44 }, 45 }) 46 os.Exit(m.Run()) 47 } 48 49 func TestRender_StandardLinks(t *testing.T) { 50 setting.AppURL = AppURL 51 setting.AppSubURL = AppSubURL 52 53 test := func(input, expected, expectedWiki string) { 54 buffer, err := RenderString(&markup.RenderContext{ 55 Ctx: git.DefaultContext, 56 URLPrefix: setting.AppSubURL, 57 }, input) 58 assert.NoError(t, err) 59 assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) 60 61 buffer, err = RenderString(&markup.RenderContext{ 62 Ctx: git.DefaultContext, 63 URLPrefix: setting.AppSubURL, 64 IsWiki: true, 65 }, input) 66 assert.NoError(t, err) 67 assert.Equal(t, strings.TrimSpace(expectedWiki), strings.TrimSpace(buffer)) 68 } 69 70 googleRendered := `<p><a href="https://google.com/" rel="nofollow">https://google.com/</a></p>` 71 test("<https://google.com/>", googleRendered, googleRendered) 72 73 lnk := util.URLJoin(AppSubURL, "WikiPage") 74 lnkWiki := util.URLJoin(AppSubURL, "wiki", "WikiPage") 75 test("[WikiPage](WikiPage)", 76 `<p><a href="`+lnk+`" rel="nofollow">WikiPage</a></p>`, 77 `<p><a href="`+lnkWiki+`" rel="nofollow">WikiPage</a></p>`) 78 } 79 80 func TestRender_Images(t *testing.T) { 81 setting.AppURL = AppURL 82 setting.AppSubURL = AppSubURL 83 84 test := func(input, expected string) { 85 buffer, err := RenderString(&markup.RenderContext{ 86 Ctx: git.DefaultContext, 87 URLPrefix: setting.AppSubURL, 88 }, input) 89 assert.NoError(t, err) 90 assert.Equal(t, strings.TrimSpace(expected), strings.TrimSpace(buffer)) 91 } 92 93 url := "../../.images/src/02/train.jpg" 94 title := "Train" 95 href := "https://gitea.io" 96 result := util.URLJoin(AppSubURL, url) 97 // hint: With Markdown v2.5.2, there is a new syntax: [link](URL){:target="_blank"} , but we do not support it now 98 99 test( 100 "", 101 `<p><a href="`+result+`" target="_blank" rel="nofollow noopener"><img src="`+result+`" alt="`+title+`"/></a></p>`) 102 103 test( 104 "[["+title+"|"+url+"]]", 105 `<p><a href="`+result+`" rel="nofollow"><img src="`+result+`" title="`+title+`" alt="`+title+`"/></a></p>`) 106 test( 107 "[]("+href+")", 108 `<p><a href="`+href+`" rel="nofollow"><img src="`+result+`" alt="`+title+`"/></a></p>`) 109 110 url = "/../../.images/src/02/train.jpg" 111 test( 112 "", 113 `<p><a href="`+result+`" target="_blank" rel="nofollow noopener"><img src="`+result+`" alt="`+title+`"/></a></p>`) 114 115 test( 116 "[["+title+"|"+url+"]]", 117 `<p><a href="`+result+`" rel="nofollow"><img src="`+result+`" title="`+title+`" alt="`+title+`"/></a></p>`) 118 test( 119 "[]("+href+")", 120 `<p><a href="`+href+`" rel="nofollow"><img src="`+result+`" alt="`+title+`"/></a></p>`) 121 } 122 123 func testAnswers(baseURLContent, baseURLImages string) []string { 124 return []string{ 125 `<p>Wiki! Enjoy :)</p> 126 <ul> 127 <li><a href="` + baseURLContent + `/Links" rel="nofollow">Links, Language bindings, Engine bindings</a></li> 128 <li><a href="` + baseURLContent + `/Tips" rel="nofollow">Tips</a></li> 129 </ul> 130 <p>See commit <a href="http://localhost:3000/gogits/gogs/commit/65f1bf27bc" rel="nofollow"><code>65f1bf27bc</code></a></p> 131 <p>Ideas and codes</p> 132 <ul> 133 <li>Bezier widget (by <a href="` + AppURL + `r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="http://localhost:3000/ocornut/imgui/issues/786" class="ref-issue" rel="nofollow">ocornut/imgui#786</a></li> 134 <li>Bezier widget (by <a href="` + AppURL + `r-lyeh" rel="nofollow">@r-lyeh</a>) <a href="http://localhost:3000/gogits/gogs/issues/786" class="ref-issue" rel="nofollow">#786</a></li> 135 <li>Node graph editors <a href="https://github.com/ocornut/imgui/issues/306" rel="nofollow">https://github.com/ocornut/imgui/issues/306</a></li> 136 <li><a href="` + baseURLContent + `/memory_editor_example" rel="nofollow">Memory Editor</a></li> 137 <li><a href="` + baseURLContent + `/plot_var_example" rel="nofollow">Plot var helper</a></li> 138 </ul> 139 `, 140 `<h2 id="user-content-what-is-wine-staging">What is Wine Staging?</h2> 141 <p><strong>Wine Staging</strong> on website <a href="http://wine-staging.com" rel="nofollow">wine-staging.com</a>.</p> 142 <h2 id="user-content-quick-links">Quick Links</h2> 143 <p>Here are some links to the most important topics. You can find the full list of pages at the sidebar.</p> 144 <table> 145 <thead> 146 <tr> 147 <th><a href="` + baseURLImages + `/images/icon-install.png" rel="nofollow"><img src="` + baseURLImages + `/images/icon-install.png" title="icon-install.png" alt="images/icon-install.png"/></a></th> 148 <th><a href="` + baseURLContent + `/Installation" rel="nofollow">Installation</a></th> 149 </tr> 150 </thead> 151 <tbody> 152 <tr> 153 <td><a href="` + baseURLImages + `/images/icon-usage.png" rel="nofollow"><img src="` + baseURLImages + `/images/icon-usage.png" title="icon-usage.png" alt="images/icon-usage.png"/></a></td> 154 <td><a href="` + baseURLContent + `/Usage" rel="nofollow">Usage</a></td> 155 </tr> 156 </tbody> 157 </table> 158 `, 159 `<p><a href="http://www.excelsiorjet.com/" rel="nofollow">Excelsior JET</a> allows you to create native executables for Windows, Linux and Mac OS X.</p> 160 <ol> 161 <li><a href="https://github.com/libgdx/libgdx/wiki/Gradle-on-the-Commandline#packaging-for-the-desktop" rel="nofollow">Package your libGDX application</a><br/> 162 <a href="` + baseURLImages + `/images/1.png" rel="nofollow"><img src="` + baseURLImages + `/images/1.png" title="1.png" alt="images/1.png"/></a></li> 163 <li>Perform a test run by hitting the Run! button.<br/> 164 <a href="` + baseURLImages + `/images/2.png" rel="nofollow"><img src="` + baseURLImages + `/images/2.png" title="2.png" alt="images/2.png"/></a></li> 165 </ol> 166 <h2 id="user-content-custom-id">More tests</h2> 167 <p>(from <a href="https://www.markdownguide.org/extended-syntax/" rel="nofollow">https://www.markdownguide.org/extended-syntax/</a>)</p> 168 <h3 id="user-content-checkboxes">Checkboxes</h3> 169 <ul> 170 <li class="task-list-item"><input type="checkbox" disabled="" data-source-position="434"/>unchecked</li> 171 <li class="task-list-item"><input type="checkbox" disabled="" data-source-position="450" checked=""/>checked</li> 172 <li class="task-list-item"><input type="checkbox" disabled="" data-source-position="464"/>still unchecked</li> 173 </ul> 174 <h3 id="user-content-definition-list">Definition list</h3> 175 <dl> 176 <dt>First Term</dt> 177 <dd>This is the definition of the first term.</dd> 178 <dt>Second Term</dt> 179 <dd>This is one definition of the second term.</dd> 180 <dd>This is another definition of the second term.</dd> 181 </dl> 182 <h3 id="user-content-footnotes">Footnotes</h3> 183 <p>Here is a simple footnote,<sup id="fnref:user-content-1"><a href="#fn:user-content-1" rel="nofollow">1</a></sup> and here is a longer one.<sup id="fnref:user-content-bignote"><a href="#fn:user-content-bignote" rel="nofollow">2</a></sup></p> 184 <div> 185 <hr/> 186 <ol> 187 <li id="fn:user-content-1"> 188 <p>This is the first footnote. <a href="#fnref:user-content-1" rel="nofollow">↩︎</a></p> 189 </li> 190 <li id="fn:user-content-bignote"> 191 <p>Here is one with multiple paragraphs and code.</p> 192 <p>Indent paragraphs to include them in the footnote.</p> 193 <p><code>{ my code }</code></p> 194 <p>Add as many paragraphs as you like. <a href="#fnref:user-content-bignote" rel="nofollow">↩︎</a></p> 195 </li> 196 </ol> 197 </div> 198 `, `<ul> 199 <li class="task-list-item"><input type="checkbox" disabled="" data-source-position="3"/> If you want to rebase/retry this PR, click this checkbox.</li> 200 </ul> 201 <hr/> 202 <p>This PR has been generated by <a href="https://github.com/renovatebot/renovate" rel="nofollow">Renovate Bot</a>.</p> 203 `, 204 } 205 } 206 207 // Test cases without ambiguous links 208 var sameCases = []string{ 209 // dear imgui wiki markdown extract: special wiki syntax 210 `Wiki! Enjoy :) 211 - [[Links, Language bindings, Engine bindings|Links]] 212 - [[Tips]] 213 214 See commit 65f1bf27bc 215 216 Ideas and codes 217 218 - Bezier widget (by @r-lyeh) ` + AppURL + `ocornut/imgui/issues/786 219 - Bezier widget (by @r-lyeh) ` + AppURL + `gogits/gogs/issues/786 220 - Node graph editors https://github.com/ocornut/imgui/issues/306 221 - [[Memory Editor|memory_editor_example]] 222 - [[Plot var helper|plot_var_example]]`, 223 // wine-staging wiki home extract: tables, special wiki syntax, images 224 `## What is Wine Staging? 225 **Wine Staging** on website [wine-staging.com](http://wine-staging.com). 226 227 ## Quick Links 228 Here are some links to the most important topics. You can find the full list of pages at the sidebar. 229 230 | [[images/icon-install.png]] | [[Installation]] | 231 |--------------------------------|----------------------------------------------------------| 232 | [[images/icon-usage.png]] | [[Usage]] | 233 `, 234 // libgdx wiki page: inline images with special syntax 235 `[Excelsior JET](http://www.excelsiorjet.com/) allows you to create native executables for Windows, Linux and Mac OS X. 236 237 1. [Package your libGDX application](https://github.com/libgdx/libgdx/wiki/Gradle-on-the-Commandline#packaging-for-the-desktop) 238 [[images/1.png]] 239 2. Perform a test run by hitting the Run! button. 240 [[images/2.png]] 241 242 ## More tests {#custom-id} 243 244 (from https://www.markdownguide.org/extended-syntax/) 245 246 ### Checkboxes 247 248 - [ ] unchecked 249 - [x] checked 250 - [ ] still unchecked 251 252 ### Definition list 253 254 First Term 255 : This is the definition of the first term. 256 257 Second Term 258 : This is one definition of the second term. 259 : This is another definition of the second term. 260 261 ### Footnotes 262 263 Here is a simple footnote,[^1] and here is a longer one.[^bignote] 264 265 [^1]: This is the first footnote. 266 267 [^bignote]: Here is one with multiple paragraphs and code. 268 269 Indent paragraphs to include them in the footnote. 270 271 ` + "`{ my code }`" + ` 272 273 Add as many paragraphs as you like. 274 `, 275 ` 276 - [ ] <!-- rebase-check --> If you want to rebase/retry this PR, click this checkbox. 277 278 --- 279 280 This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). 281 282 <!-- test-comment -->`, 283 } 284 285 func TestTotal_RenderWiki(t *testing.T) { 286 setting.AppURL = AppURL 287 setting.AppSubURL = AppSubURL 288 289 answers := testAnswers(util.URLJoin(AppSubURL, "wiki/"), util.URLJoin(AppSubURL, "wiki", "raw/")) 290 291 for i := 0; i < len(sameCases); i++ { 292 line, err := RenderString(&markup.RenderContext{ 293 Ctx: git.DefaultContext, 294 URLPrefix: AppSubURL, 295 Metas: localMetas, 296 IsWiki: true, 297 }, sameCases[i]) 298 assert.NoError(t, err) 299 assert.Equal(t, answers[i], line) 300 } 301 302 testCases := []string{ 303 // Guard wiki sidebar: special syntax 304 `[[Guardfile-DSL / Configuring-Guard|Guardfile-DSL---Configuring-Guard]]`, 305 // rendered 306 `<p><a href="` + AppSubURL + `wiki/Guardfile-DSL---Configuring-Guard" rel="nofollow">Guardfile-DSL / Configuring-Guard</a></p> 307 `, 308 // special syntax 309 `[[Name|Link]]`, 310 // rendered 311 `<p><a href="` + AppSubURL + `wiki/Link" rel="nofollow">Name</a></p> 312 `, 313 } 314 315 for i := 0; i < len(testCases); i += 2 { 316 line, err := RenderString(&markup.RenderContext{ 317 Ctx: git.DefaultContext, 318 URLPrefix: AppSubURL, 319 IsWiki: true, 320 }, testCases[i]) 321 assert.NoError(t, err) 322 assert.Equal(t, testCases[i+1], line) 323 } 324 } 325 326 func TestTotal_RenderString(t *testing.T) { 327 setting.AppURL = AppURL 328 setting.AppSubURL = AppSubURL 329 330 answers := testAnswers(util.URLJoin(AppSubURL, "src", "master/"), util.URLJoin(AppSubURL, "raw", "master/")) 331 332 for i := 0; i < len(sameCases); i++ { 333 line, err := RenderString(&markup.RenderContext{ 334 Ctx: git.DefaultContext, 335 URLPrefix: util.URLJoin(AppSubURL, "src", "master/"), 336 Metas: localMetas, 337 }, sameCases[i]) 338 assert.NoError(t, err) 339 assert.Equal(t, answers[i], line) 340 } 341 342 testCases := []string{} 343 344 for i := 0; i < len(testCases); i += 2 { 345 line, err := RenderString(&markup.RenderContext{ 346 Ctx: git.DefaultContext, 347 URLPrefix: AppSubURL, 348 }, testCases[i]) 349 assert.NoError(t, err) 350 assert.Equal(t, testCases[i+1], line) 351 } 352 } 353 354 func TestRender_RenderParagraphs(t *testing.T) { 355 test := func(t *testing.T, str string, cnt int) { 356 res, err := RenderRawString(&markup.RenderContext{Ctx: git.DefaultContext}, str) 357 assert.NoError(t, err) 358 assert.Equal(t, cnt, strings.Count(res, "<p"), "Rendered result for unix should have %d paragraph(s) but has %d:\n%s\n", cnt, strings.Count(res, "<p"), res) 359 360 mac := strings.ReplaceAll(str, "\n", "\r") 361 res, err = RenderRawString(&markup.RenderContext{Ctx: git.DefaultContext}, mac) 362 assert.NoError(t, err) 363 assert.Equal(t, cnt, strings.Count(res, "<p"), "Rendered result for mac should have %d paragraph(s) but has %d:\n%s\n", cnt, strings.Count(res, "<p"), res) 364 365 dos := strings.ReplaceAll(str, "\n", "\r\n") 366 res, err = RenderRawString(&markup.RenderContext{Ctx: git.DefaultContext}, dos) 367 assert.NoError(t, err) 368 assert.Equal(t, cnt, strings.Count(res, "<p"), "Rendered result for windows should have %d paragraph(s) but has %d:\n%s\n", cnt, strings.Count(res, "<p"), res) 369 } 370 371 test(t, "\nOne\nTwo\nThree", 1) 372 test(t, "\n\nOne\nTwo\nThree", 1) 373 test(t, "\n\nOne\nTwo\nThree\n\n\n", 1) 374 test(t, "A\n\nB\nC\n", 2) 375 test(t, "A\n\n\nB\nC\n", 2) 376 } 377 378 func TestMarkdownRenderRaw(t *testing.T) { 379 testcases := [][]byte{ 380 { // clusterfuzz_testcase_minimized_fuzz_markdown_render_raw_6267570554535936 381 0x2a, 0x20, 0x2d, 0x0a, 0x09, 0x20, 0x60, 0x5b, 0x0a, 0x09, 0x20, 0x60, 382 0x5b, 383 }, 384 { // clusterfuzz_testcase_minimized_fuzz_markdown_render_raw_6278827345051648 385 0x2d, 0x20, 0x2d, 0x0d, 0x09, 0x60, 0x0d, 0x09, 0x60, 386 }, 387 { // clusterfuzz_testcase_minimized_fuzz_markdown_render_raw_6016973788020736[] = { 388 0x7b, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x3d, 0x35, 0x7d, 0x0a, 0x3d, 389 }, 390 } 391 392 for _, testcase := range testcases { 393 log.Info("Test markdown render error with fuzzy data: %x, the following errors can be recovered", testcase) 394 _, err := RenderRawString(&markup.RenderContext{Ctx: git.DefaultContext}, string(testcase)) 395 assert.NoError(t, err) 396 } 397 } 398 399 func TestRenderSiblingImages_Issue12925(t *testing.T) { 400 testcase := ` 401  402 ` 403 expected := `<p><a href="/image1" target="_blank" rel="nofollow noopener"><img src="/image1" alt="image1"></a><br> 404 <a href="/image2" target="_blank" rel="nofollow noopener"><img src="/image2" alt="image2"></a></p> 405 ` 406 res, err := RenderRawString(&markup.RenderContext{Ctx: git.DefaultContext}, testcase) 407 assert.NoError(t, err) 408 assert.Equal(t, expected, res) 409 } 410 411 func TestRenderEmojiInLinks_Issue12331(t *testing.T) { 412 testcase := `[Link with emoji :moon: in text](https://gitea.io)` 413 expected := `<p><a href="https://gitea.io" rel="nofollow">Link with emoji <span class="emoji" aria-label="waxing gibbous moon">🌔</span> in text</a></p> 414 ` 415 res, err := RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, testcase) 416 assert.NoError(t, err) 417 assert.Equal(t, expected, res) 418 } 419 420 func TestColorPreview(t *testing.T) { 421 const nl = "\n" 422 positiveTests := []struct { 423 testcase string 424 expected string 425 }{ 426 { // hex 427 "`#FF0000`", 428 `<p><code>#FF0000<span class="color-preview" style="background-color: #FF0000"></span></code></p>` + nl, 429 }, 430 { // rgb 431 "`rgb(16, 32, 64)`", 432 `<p><code>rgb(16, 32, 64)<span class="color-preview" style="background-color: rgb(16, 32, 64)"></span></code></p>` + nl, 433 }, 434 { // short hex 435 "This is the color white `#000`", 436 `<p>This is the color white <code>#000<span class="color-preview" style="background-color: #000"></span></code></p>` + nl, 437 }, 438 { // hsl 439 "HSL stands for hue, saturation, and lightness. An example: `hsl(0, 100%, 50%)`.", 440 `<p>HSL stands for hue, saturation, and lightness. An example: <code>hsl(0, 100%, 50%)<span class="color-preview" style="background-color: hsl(0, 100%, 50%)"></span></code>.</p>` + nl, 441 }, 442 { // uppercase hsl 443 "HSL stands for hue, saturation, and lightness. An example: `HSL(0, 100%, 50%)`.", 444 `<p>HSL stands for hue, saturation, and lightness. An example: <code>HSL(0, 100%, 50%)<span class="color-preview" style="background-color: HSL(0, 100%, 50%)"></span></code>.</p>` + nl, 445 }, 446 } 447 448 for _, test := range positiveTests { 449 res, err := RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase) 450 assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase) 451 assert.Equal(t, test.expected, res, "Unexpected result in testcase %q", test.testcase) 452 453 } 454 455 negativeTests := []string{ 456 // not a color code 457 "`FF0000`", 458 // inside a code block 459 "```javascript" + nl + `const red = "#FF0000";` + nl + "```", 460 // no backticks 461 "rgb(166, 32, 64)", 462 // typo 463 "`hsI(0, 100%, 50%)`", 464 // looks like a color but not really 465 "`hsl(40, 60, 80)`", 466 } 467 468 for _, test := range negativeTests { 469 res, err := RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test) 470 assert.NoError(t, err, "Unexpected error in testcase: %q", test) 471 assert.NotContains(t, res, `<span class="color-preview" style="background-color: `, "Unexpected result in testcase %q", test) 472 } 473 } 474 475 func TestMathBlock(t *testing.T) { 476 const nl = "\n" 477 testcases := []struct { 478 testcase string 479 expected string 480 }{ 481 { 482 "$a$", 483 `<p><code class="language-math is-loading">a</code></p>` + nl, 484 }, 485 { 486 "$ a $", 487 `<p><code class="language-math is-loading">a</code></p>` + nl, 488 }, 489 { 490 "$a$ $b$", 491 `<p><code class="language-math is-loading">a</code> <code class="language-math is-loading">b</code></p>` + nl, 492 }, 493 { 494 `\(a\) \(b\)`, 495 `<p><code class="language-math is-loading">a</code> <code class="language-math is-loading">b</code></p>` + nl, 496 }, 497 { 498 `$a a$b b$`, 499 `<p><code class="language-math is-loading">a a$b b</code></p>` + nl, 500 }, 501 { 502 `a a$b b`, 503 `<p>a a$b b</p>` + nl, 504 }, 505 { 506 `a$b $a a$b b$`, 507 `<p>a$b <code class="language-math is-loading">a a$b b</code></p>` + nl, 508 }, 509 { 510 "$$a$$", 511 `<pre class="code-block is-loading"><code class="chroma language-math display">a</code></pre>` + nl, 512 }, 513 } 514 515 for _, test := range testcases { 516 res, err := RenderString(&markup.RenderContext{Ctx: git.DefaultContext}, test.testcase) 517 assert.NoError(t, err, "Unexpected error in testcase: %q", test.testcase) 518 assert.Equal(t, test.expected, res, "Unexpected result in testcase %q", test.testcase) 519 520 } 521 }