convert_test.go (16373B)
1 // Copyright 2019 The Hugo Authors. All rights reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // http://www.apache.org/licenses/LICENSE-2.0 7 // 8 // Unless required by applicable law or agreed to in writing, software 9 // distributed under the License is distributed on an "AS IS" BASIS, 10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 package goldmark 15 16 import ( 17 "fmt" 18 "strings" 19 "testing" 20 21 "github.com/spf13/cast" 22 23 "github.com/gohugoio/hugo/markup/converter/hooks" 24 "github.com/gohugoio/hugo/markup/goldmark/goldmark_config" 25 26 "github.com/gohugoio/hugo/markup/highlight" 27 28 "github.com/gohugoio/hugo/markup/markup_config" 29 30 "github.com/gohugoio/hugo/common/loggers" 31 32 "github.com/gohugoio/hugo/markup/converter" 33 34 qt "github.com/frankban/quicktest" 35 ) 36 37 func convert(c *qt.C, mconf markup_config.Config, content string) converter.Result { 38 p, err := Provider.New( 39 converter.ProviderConfig{ 40 MarkupConfig: mconf, 41 Logger: loggers.NewErrorLogger(), 42 }, 43 ) 44 c.Assert(err, qt.IsNil) 45 h := highlight.New(mconf.Highlight) 46 47 getRenderer := func(t hooks.RendererType, id any) any { 48 if t == hooks.CodeBlockRendererType { 49 return h 50 } 51 return nil 52 } 53 54 conv, err := p.New(converter.DocumentContext{DocumentID: "thedoc"}) 55 c.Assert(err, qt.IsNil) 56 b, err := conv.Convert(converter.RenderContext{RenderTOC: true, Src: []byte(content), GetRenderer: getRenderer}) 57 c.Assert(err, qt.IsNil) 58 59 return b 60 } 61 62 func TestConvert(t *testing.T) { 63 c := qt.New(t) 64 65 // Smoke test of the default configuration. 66 content := ` 67 ## Links 68 69 https://github.com/gohugoio/hugo/issues/6528 70 [Live Demo here!](https://docuapi.netlify.com/) 71 72 [I'm an inline-style link with title](https://www.google.com "Google's Homepage") 73 <https://foo.bar/> 74 https://bar.baz/ 75 <fake@example.com> 76 <mailto:fake2@example.com> 77 78 79 ## Code Fences 80 81 §§§bash 82 LINE1 83 §§§ 84 85 ## Code Fences No Lexer 86 87 §§§moo 88 LINE1 89 §§§ 90 91 ## Custom ID {#custom} 92 93 ## Auto ID 94 95 * Autolink: https://gohugo.io/ 96 * Strikethrough:~~Hi~~ Hello, world! 97 98 ## Table 99 100 | foo | bar | 101 | --- | --- | 102 | baz | bim | 103 104 ## Task Lists (default on) 105 106 - [x] Finish my changes[^1] 107 - [ ] Push my commits to GitHub 108 - [ ] Open a pull request 109 110 111 ## Smartypants (default on) 112 113 * Straight double "quotes" and single 'quotes' into “curly” quote HTML entities 114 * Dashes (“--” and “---”) into en- and em-dash entities 115 * Three consecutive dots (“...”) into an ellipsis entity 116 * Apostrophes are also converted: "That was back in the '90s, that's a long time ago" 117 118 ## Footnotes 119 120 That's some text with a footnote.[^1] 121 122 ## Definition Lists 123 124 date 125 : the datetime assigned to this page. 126 127 description 128 : the description for the content. 129 130 131 ## 神真美好 132 133 ## 神真美好 134 135 ## 神真美好 136 137 [^1]: And that's the footnote. 138 139 ` 140 141 // Code fences 142 content = strings.Replace(content, "§§§", "```", -1) 143 mconf := markup_config.Default 144 mconf.Highlight.NoClasses = false 145 mconf.Goldmark.Renderer.Unsafe = true 146 147 b := convert(c, mconf, content) 148 got := string(b.Bytes()) 149 150 fmt.Println(got) 151 152 // Links 153 c.Assert(got, qt.Contains, `<a href="https://docuapi.netlify.com/">Live Demo here!</a>`) 154 c.Assert(got, qt.Contains, `<a href="https://foo.bar/">https://foo.bar/</a>`) 155 c.Assert(got, qt.Contains, `<a href="https://bar.baz/">https://bar.baz/</a>`) 156 c.Assert(got, qt.Contains, `<a href="mailto:fake@example.com">fake@example.com</a>`) 157 c.Assert(got, qt.Contains, `<a href="mailto:fake2@example.com">mailto:fake2@example.com</a></p>`) 158 159 // Header IDs 160 c.Assert(got, qt.Contains, `<h2 id="custom">Custom ID</h2>`, qt.Commentf(got)) 161 c.Assert(got, qt.Contains, `<h2 id="auto-id">Auto ID</h2>`, qt.Commentf(got)) 162 c.Assert(got, qt.Contains, `<h2 id="神真美好">神真美好</h2>`, qt.Commentf(got)) 163 c.Assert(got, qt.Contains, `<h2 id="神真美好-1">神真美好</h2>`, qt.Commentf(got)) 164 c.Assert(got, qt.Contains, `<h2 id="神真美好-2">神真美好</h2>`, qt.Commentf(got)) 165 166 // Code fences 167 c.Assert(got, qt.Contains, "<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-bash\" data-lang=\"bash\"><span class=\"line\"><span class=\"cl\">LINE1\n</span></span></code></pre></div>") 168 c.Assert(got, qt.Contains, "Code Fences No Lexer</h2>\n<pre tabindex=\"0\"><code class=\"language-moo\" data-lang=\"moo\">LINE1\n</code></pre>") 169 170 // Extensions 171 c.Assert(got, qt.Contains, `Autolink: <a href="https://gohugo.io/">https://gohugo.io/</a>`) 172 c.Assert(got, qt.Contains, `Strikethrough:<del>Hi</del> Hello, world`) 173 c.Assert(got, qt.Contains, `<th>foo</th>`) 174 c.Assert(got, qt.Contains, `<li><input disabled="" type="checkbox"> Push my commits to GitHub</li>`) 175 176 c.Assert(got, qt.Contains, `Straight double “quotes” and single ‘quotes’`) 177 c.Assert(got, qt.Contains, `Dashes (“–” and “—”) `) 178 c.Assert(got, qt.Contains, `Three consecutive dots (“…”)`) 179 c.Assert(got, qt.Contains, `“That was back in the ’90s, that’s a long time ago”`) 180 c.Assert(got, qt.Contains, `footnote.<sup id="fnref1:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>`) 181 c.Assert(got, qt.Contains, `<div class="footnotes" role="doc-endnotes">`) 182 c.Assert(got, qt.Contains, `<dt>date</dt>`) 183 184 toc, ok := b.(converter.TableOfContentsProvider) 185 c.Assert(ok, qt.Equals, true) 186 tocHTML := toc.TableOfContents().ToHTML(1, 2, false) 187 c.Assert(tocHTML, qt.Contains, "TableOfContents") 188 } 189 190 func TestConvertAutoIDAsciiOnly(t *testing.T) { 191 c := qt.New(t) 192 193 content := ` 194 ## God is Good: 神真美好 195 ` 196 mconf := markup_config.Default 197 mconf.Goldmark.Parser.AutoHeadingIDType = goldmark_config.AutoHeadingIDTypeGitHubAscii 198 b := convert(c, mconf, content) 199 got := string(b.Bytes()) 200 201 c.Assert(got, qt.Contains, "<h2 id=\"god-is-good-\">") 202 } 203 204 func TestConvertAutoIDBlackfriday(t *testing.T) { 205 c := qt.New(t) 206 207 content := ` 208 ## Let's try this, shall we? 209 210 ` 211 mconf := markup_config.Default 212 mconf.Goldmark.Parser.AutoHeadingIDType = goldmark_config.AutoHeadingIDTypeBlackfriday 213 b := convert(c, mconf, content) 214 got := string(b.Bytes()) 215 216 c.Assert(got, qt.Contains, "<h2 id=\"let-s-try-this-shall-we\">") 217 } 218 219 func TestConvertAttributes(t *testing.T) { 220 c := qt.New(t) 221 222 withBlockAttributes := func(conf *markup_config.Config) { 223 conf.Goldmark.Parser.Attribute.Block = true 224 conf.Goldmark.Parser.Attribute.Title = false 225 } 226 227 withTitleAndBlockAttributes := func(conf *markup_config.Config) { 228 conf.Goldmark.Parser.Attribute.Block = true 229 conf.Goldmark.Parser.Attribute.Title = true 230 } 231 232 for _, test := range []struct { 233 name string 234 withConfig func(conf *markup_config.Config) 235 input string 236 expect any 237 }{ 238 { 239 "Title", 240 nil, 241 "## heading {#id .className attrName=attrValue class=\"class1 class2\"}", 242 "<h2 id=\"id\" class=\"className class1 class2\" attrName=\"attrValue\">heading</h2>\n", 243 }, 244 { 245 "Blockquote", 246 withBlockAttributes, 247 "> foo\n> bar\n{#id .className attrName=attrValue class=\"class1 class2\"}\n", 248 "<blockquote id=\"id\" class=\"className class1 class2\"><p>foo\nbar</p>\n</blockquote>\n", 249 }, 250 /*{ 251 // TODO(bep) this needs an upstream fix, see https://github.com/yuin/goldmark/issues/195 252 "Code block, CodeFences=false", 253 func(conf *markup_config.Config) { 254 withBlockAttributes(conf) 255 conf.Highlight.CodeFences = false 256 }, 257 "```bash\necho 'foo';\n```\n{.myclass}", 258 "TODO", 259 },*/ 260 { 261 "Code block, CodeFences=true", 262 func(conf *markup_config.Config) { 263 withBlockAttributes(conf) 264 conf.Highlight.CodeFences = true 265 }, 266 "```bash {.myclass id=\"myid\"}\necho 'foo';\n````\n", 267 "<div class=\"highlight myclass\" id=\"myid\"><pre style", 268 }, 269 { 270 "Code block, CodeFences=true,linenos=table", 271 func(conf *markup_config.Config) { 272 withBlockAttributes(conf) 273 conf.Highlight.CodeFences = true 274 }, 275 "```bash {linenos=table .myclass id=\"myid\"}\necho 'foo';\n````\n{ .adfadf }", 276 []string{ 277 "div class=\"highlight myclass\" id=\"myid\"><div s", 278 "table style", 279 }, 280 }, 281 { 282 "Code block, CodeFences=true,lineanchors", 283 func(conf *markup_config.Config) { 284 withBlockAttributes(conf) 285 conf.Highlight.CodeFences = true 286 conf.Highlight.NoClasses = false 287 }, 288 "```bash {linenos=table, anchorlinenos=true, lineanchors=org-coderef--xyz}\necho 'foo';\n```", 289 "<div class=\"highlight\"><div class=\"chroma\">\n<table class=\"lntable\"><tr><td class=\"lntd\">\n<pre tabindex=\"0\" class=\"chroma\"><code><span class=\"lnt\" id=\"org-coderef--xyz-1\"><a style=\"outline: none; text-decoration:none; color:inherit\" href=\"#org-coderef--xyz-1\">1</a>\n</span></code></pre></td>\n<td class=\"lntd\">\n<pre tabindex=\"0\" class=\"chroma\"><code class=\"language-bash\" data-lang=\"bash\"><span class=\"line\"><span class=\"cl\"><span class=\"nb\">echo</span> <span class=\"s1\">'foo'</span><span class=\"p\">;</span>\n</span></span></code></pre></td></tr></table>\n</div>\n</div>", 290 }, 291 { 292 "Code block, CodeFences=true,lineanchors, default ordinal", 293 func(conf *markup_config.Config) { 294 withBlockAttributes(conf) 295 conf.Highlight.CodeFences = true 296 conf.Highlight.NoClasses = false 297 }, 298 "```bash {linenos=inline, anchorlinenos=true}\necho 'foo';\nnecho 'bar';\n```\n\n```bash {linenos=inline, anchorlinenos=true}\necho 'baz';\nnecho 'qux';\n```", 299 []string{ 300 "<span class=\"ln\" id=\"hl-0-1\"><a style=\"outline: none; text-decoration:none; color:inherit\" href=\"#hl-0-1\">1</a></span><span class=\"cl\"><span class=\"nb\">echo</span> <span class=\"s1\">'foo'</span>", 301 "<span class=\"ln\" id=\"hl-0-2\"><a style=\"outline: none; text-decoration:none; color:inherit\" href=\"#hl-0-2\">2</a></span><span class=\"cl\">necho <span class=\"s1\">'bar'</span>", 302 "<span class=\"ln\" id=\"hl-1-2\"><a style=\"outline: none; text-decoration:none; color:inherit\" href=\"#hl-1-2\">2</a></span><span class=\"cl\">necho <span class=\"s1\">'qux'</span>", 303 }, 304 }, 305 { 306 "Paragraph", 307 withBlockAttributes, 308 "\nHi there.\n{.myclass }", 309 "<p class=\"myclass\">Hi there.</p>\n", 310 }, 311 { 312 "Ordered list", 313 withBlockAttributes, 314 "\n1. First\n2. Second\n{.myclass }", 315 "<ol class=\"myclass\">\n<li>First</li>\n<li>Second</li>\n</ol>\n", 316 }, 317 { 318 "Unordered list", 319 withBlockAttributes, 320 "\n* First\n* Second\n{.myclass }", 321 "<ul class=\"myclass\">\n<li>First</li>\n<li>Second</li>\n</ul>\n", 322 }, 323 { 324 "Unordered list, indented", 325 withBlockAttributes, 326 `* Fruit 327 * Apple 328 * Orange 329 * Banana 330 {.fruits} 331 * Dairy 332 * Milk 333 * Cheese 334 {.dairies} 335 {.list}`, 336 []string{"<ul class=\"list\">\n<li>Fruit\n<ul class=\"fruits\">", "<li>Dairy\n<ul class=\"dairies\">"}, 337 }, 338 { 339 "Table", 340 withBlockAttributes, 341 `| A | B | 342 | ------------- |:-------------:| -----:| 343 | AV | BV | 344 {.myclass }`, 345 "<table class=\"myclass\">\n<thead>", 346 }, 347 { 348 "Title and Blockquote", 349 withTitleAndBlockAttributes, 350 "## heading {#id .className attrName=attrValue class=\"class1 class2\"}\n> foo\n> bar\n{.myclass}", 351 "<h2 id=\"id\" class=\"className class1 class2\" attrName=\"attrValue\">heading</h2>\n<blockquote class=\"myclass\"><p>foo\nbar</p>\n</blockquote>\n", 352 }, 353 } { 354 c.Run(test.name, func(c *qt.C) { 355 mconf := markup_config.Default 356 if test.withConfig != nil { 357 test.withConfig(&mconf) 358 } 359 b := convert(c, mconf, test.input) 360 got := string(b.Bytes()) 361 362 for _, s := range cast.ToStringSlice(test.expect) { 363 c.Assert(got, qt.Contains, s) 364 } 365 }) 366 } 367 } 368 369 func TestConvertIssues(t *testing.T) { 370 c := qt.New(t) 371 372 // https://github.com/gohugoio/hugo/issues/7619 373 c.Run("Hyphen in HTML attributes", func(c *qt.C) { 374 mconf := markup_config.Default 375 mconf.Goldmark.Renderer.Unsafe = true 376 input := `<custom-element> 377 <div>This will be "slotted" into the custom element.</div> 378 </custom-element> 379 ` 380 381 b := convert(c, mconf, input) 382 got := string(b.Bytes()) 383 384 c.Assert(got, qt.Contains, "<custom-element>\n <div>This will be \"slotted\" into the custom element.</div>\n</custom-element>\n") 385 }) 386 } 387 388 func TestCodeFence(t *testing.T) { 389 c := qt.New(t) 390 391 lines := `LINE1 392 LINE2 393 LINE3 394 LINE4 395 LINE5 396 ` 397 398 convertForConfig := func(c *qt.C, conf highlight.Config, code, language string) string { 399 mconf := markup_config.Default 400 mconf.Highlight = conf 401 402 p, err := Provider.New( 403 converter.ProviderConfig{ 404 MarkupConfig: mconf, 405 Logger: loggers.NewErrorLogger(), 406 }, 407 ) 408 409 h := highlight.New(conf) 410 411 getRenderer := func(t hooks.RendererType, id any) any { 412 if t == hooks.CodeBlockRendererType { 413 return h 414 } 415 return nil 416 } 417 418 content := "```" + language + "\n" + code + "\n```" 419 420 c.Assert(err, qt.IsNil) 421 conv, err := p.New(converter.DocumentContext{}) 422 c.Assert(err, qt.IsNil) 423 b, err := conv.Convert(converter.RenderContext{Src: []byte(content), GetRenderer: getRenderer}) 424 c.Assert(err, qt.IsNil) 425 426 return string(b.Bytes()) 427 } 428 429 c.Run("Basic", func(c *qt.C) { 430 cfg := highlight.DefaultConfig 431 cfg.NoClasses = false 432 433 result := convertForConfig(c, cfg, `echo "Hugo Rocks!"`, "bash") 434 // TODO(bep) there is a whitespace mismatch (\n) between this and the highlight template func. 435 c.Assert(result, qt.Equals, "<div class=\"highlight\"><pre tabindex=\"0\" class=\"chroma\"><code class=\"language-bash\" data-lang=\"bash\"><span class=\"line\"><span class=\"cl\"><span class=\"nb\">echo</span> <span class=\"s2\">"Hugo Rocks!"</span>\n</span></span></code></pre></div>") 436 result = convertForConfig(c, cfg, `echo "Hugo Rocks!"`, "unknown") 437 c.Assert(result, qt.Equals, "<pre tabindex=\"0\"><code class=\"language-unknown\" data-lang=\"unknown\">echo "Hugo Rocks!"\n</code></pre>") 438 }) 439 440 c.Run("Highlight lines, default config", func(c *qt.C) { 441 cfg := highlight.DefaultConfig 442 cfg.NoClasses = false 443 444 result := convertForConfig(c, cfg, lines, `bash {linenos=table,hl_lines=[2 "4-5"],linenostart=3}`) 445 c.Assert(result, qt.Contains, "<div class=\"highlight\"><div class=\"chroma\">\n<table class=\"lntable\"><tr><td class=\"lntd\">\n<pre tabindex=\"0\" class=\"chroma\"><code><span class") 446 c.Assert(result, qt.Contains, "<span class=\"hl\"><span class=\"lnt\">4") 447 448 result = convertForConfig(c, cfg, lines, "bash {linenos=inline,hl_lines=[2]}") 449 c.Assert(result, qt.Contains, "<span class=\"ln\">2</span><span class=\"cl\">LINE2\n</span></span>") 450 c.Assert(result, qt.Not(qt.Contains), "<table") 451 452 result = convertForConfig(c, cfg, lines, "bash {linenos=true,hl_lines=[2]}") 453 c.Assert(result, qt.Contains, "<table") 454 c.Assert(result, qt.Contains, "<span class=\"hl\"><span class=\"lnt\">2\n</span>") 455 }) 456 457 c.Run("Highlight lines, linenumbers default on", func(c *qt.C) { 458 cfg := highlight.DefaultConfig 459 cfg.NoClasses = false 460 cfg.LineNos = true 461 462 result := convertForConfig(c, cfg, lines, "bash") 463 c.Assert(result, qt.Contains, "<span class=\"lnt\">2\n</span>") 464 465 result = convertForConfig(c, cfg, lines, "bash {linenos=false,hl_lines=[2]}") 466 c.Assert(result, qt.Not(qt.Contains), "class=\"lnt\"") 467 }) 468 469 c.Run("Highlight lines, linenumbers default on, linenumbers in table default off", func(c *qt.C) { 470 cfg := highlight.DefaultConfig 471 cfg.NoClasses = false 472 cfg.LineNos = true 473 cfg.LineNumbersInTable = false 474 475 result := convertForConfig(c, cfg, lines, "bash") 476 c.Assert(result, qt.Contains, "<span class=\"ln\">2</span><span class=\"cl\">LINE2\n</span>") 477 result = convertForConfig(c, cfg, lines, "bash {linenos=table}") 478 c.Assert(result, qt.Contains, "<span class=\"lnt\">1\n</span>") 479 }) 480 481 c.Run("No language", func(c *qt.C) { 482 cfg := highlight.DefaultConfig 483 cfg.NoClasses = false 484 cfg.LineNos = true 485 cfg.LineNumbersInTable = false 486 487 result := convertForConfig(c, cfg, lines, "") 488 c.Assert(result, qt.Contains, "<pre tabindex=\"0\"><code>LINE1\n") 489 }) 490 491 c.Run("No language, guess syntax", func(c *qt.C) { 492 cfg := highlight.DefaultConfig 493 cfg.NoClasses = false 494 cfg.GuessSyntax = true 495 cfg.LineNos = true 496 cfg.LineNumbersInTable = false 497 498 result := convertForConfig(c, cfg, lines, "") 499 c.Assert(result, qt.Contains, "<span class=\"ln\">2</span><span class=\"cl\">LINE2\n</span></span>") 500 }) 501 }