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  		"!["+title+"]("+url+")",
   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  		"[!["+title+"]("+url+")]("+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  		"!["+title+"]("+url+")",
   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  		"[!["+title+"]("+url+")]("+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 := `![image1](/image1)
   401  ![image2](/image2)
   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  }