github.com/kovansky/hugo@v0.92.3-0.20220224232819-63076e4ff19f/markup/goldmark/convert_test.go (about) 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 interface{}) interface{} { 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="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>`) 181 c.Assert(got, qt.Contains, `<section 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 interface{} 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 "Paragraph", 293 withBlockAttributes, 294 "\nHi there.\n{.myclass }", 295 "<p class=\"myclass\">Hi there.</p>\n", 296 }, 297 { 298 "Ordered list", 299 withBlockAttributes, 300 "\n1. First\n2. Second\n{.myclass }", 301 "<ol class=\"myclass\">\n<li>First</li>\n<li>Second</li>\n</ol>\n", 302 }, 303 { 304 "Unordered list", 305 withBlockAttributes, 306 "\n* First\n* Second\n{.myclass }", 307 "<ul class=\"myclass\">\n<li>First</li>\n<li>Second</li>\n</ul>\n", 308 }, 309 { 310 "Unordered list, indented", 311 withBlockAttributes, 312 `* Fruit 313 * Apple 314 * Orange 315 * Banana 316 {.fruits} 317 * Dairy 318 * Milk 319 * Cheese 320 {.dairies} 321 {.list}`, 322 []string{"<ul class=\"list\">\n<li>Fruit\n<ul class=\"fruits\">", "<li>Dairy\n<ul class=\"dairies\">"}, 323 }, 324 { 325 "Table", 326 withBlockAttributes, 327 `| A | B | 328 | ------------- |:-------------:| -----:| 329 | AV | BV | 330 {.myclass }`, 331 "<table class=\"myclass\">\n<thead>", 332 }, 333 { 334 "Title and Blockquote", 335 withTitleAndBlockAttributes, 336 "## heading {#id .className attrName=attrValue class=\"class1 class2\"}\n> foo\n> bar\n{.myclass}", 337 "<h2 id=\"id\" class=\"className class1 class2\" attrName=\"attrValue\">heading</h2>\n<blockquote class=\"myclass\"><p>foo\nbar</p>\n</blockquote>\n", 338 }, 339 } { 340 c.Run(test.name, func(c *qt.C) { 341 mconf := markup_config.Default 342 if test.withConfig != nil { 343 test.withConfig(&mconf) 344 } 345 b := convert(c, mconf, test.input) 346 got := string(b.Bytes()) 347 348 for _, s := range cast.ToStringSlice(test.expect) { 349 c.Assert(got, qt.Contains, s) 350 } 351 }) 352 } 353 } 354 355 func TestConvertIssues(t *testing.T) { 356 c := qt.New(t) 357 358 // https://github.com/gohugoio/hugo/issues/7619 359 c.Run("Hyphen in HTML attributes", func(c *qt.C) { 360 mconf := markup_config.Default 361 mconf.Goldmark.Renderer.Unsafe = true 362 input := `<custom-element> 363 <div>This will be "slotted" into the custom element.</div> 364 </custom-element> 365 ` 366 367 b := convert(c, mconf, input) 368 got := string(b.Bytes()) 369 370 c.Assert(got, qt.Contains, "<custom-element>\n <div>This will be \"slotted\" into the custom element.</div>\n</custom-element>\n") 371 }) 372 } 373 374 func TestCodeFence(t *testing.T) { 375 c := qt.New(t) 376 377 lines := `LINE1 378 LINE2 379 LINE3 380 LINE4 381 LINE5 382 ` 383 384 convertForConfig := func(c *qt.C, conf highlight.Config, code, language string) string { 385 mconf := markup_config.Default 386 mconf.Highlight = conf 387 388 p, err := Provider.New( 389 converter.ProviderConfig{ 390 MarkupConfig: mconf, 391 Logger: loggers.NewErrorLogger(), 392 }, 393 ) 394 395 h := highlight.New(conf) 396 397 getRenderer := func(t hooks.RendererType, id interface{}) interface{} { 398 if t == hooks.CodeBlockRendererType { 399 return h 400 } 401 return nil 402 } 403 404 content := "```" + language + "\n" + code + "\n```" 405 406 c.Assert(err, qt.IsNil) 407 conv, err := p.New(converter.DocumentContext{}) 408 c.Assert(err, qt.IsNil) 409 b, err := conv.Convert(converter.RenderContext{Src: []byte(content), GetRenderer: getRenderer}) 410 c.Assert(err, qt.IsNil) 411 412 return string(b.Bytes()) 413 } 414 415 c.Run("Basic", func(c *qt.C) { 416 cfg := highlight.DefaultConfig 417 cfg.NoClasses = false 418 419 result := convertForConfig(c, cfg, `echo "Hugo Rocks!"`, "bash") 420 // TODO(bep) there is a whitespace mismatch (\n) between this and the highlight template func. 421 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>") 422 result = convertForConfig(c, cfg, `echo "Hugo Rocks!"`, "unknown") 423 c.Assert(result, qt.Equals, "<pre tabindex=\"0\"><code class=\"language-unknown\" data-lang=\"unknown\">echo "Hugo Rocks!"\n</code></pre>") 424 }) 425 426 c.Run("Highlight lines, default config", func(c *qt.C) { 427 cfg := highlight.DefaultConfig 428 cfg.NoClasses = false 429 430 result := convertForConfig(c, cfg, lines, `bash {linenos=table,hl_lines=[2 "4-5"],linenostart=3}`) 431 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") 432 c.Assert(result, qt.Contains, "<span class=\"hl\"><span class=\"lnt\">4") 433 434 result = convertForConfig(c, cfg, lines, "bash {linenos=inline,hl_lines=[2]}") 435 c.Assert(result, qt.Contains, "<span class=\"ln\">2</span><span class=\"cl\">LINE2\n</span></span>") 436 c.Assert(result, qt.Not(qt.Contains), "<table") 437 438 result = convertForConfig(c, cfg, lines, "bash {linenos=true,hl_lines=[2]}") 439 c.Assert(result, qt.Contains, "<table") 440 c.Assert(result, qt.Contains, "<span class=\"hl\"><span class=\"lnt\">2\n</span>") 441 }) 442 443 c.Run("Highlight lines, linenumbers default on", func(c *qt.C) { 444 cfg := highlight.DefaultConfig 445 cfg.NoClasses = false 446 cfg.LineNos = true 447 448 result := convertForConfig(c, cfg, lines, "bash") 449 c.Assert(result, qt.Contains, "<span class=\"lnt\">2\n</span>") 450 451 result = convertForConfig(c, cfg, lines, "bash {linenos=false,hl_lines=[2]}") 452 c.Assert(result, qt.Not(qt.Contains), "class=\"lnt\"") 453 }) 454 455 c.Run("Highlight lines, linenumbers default on, linenumbers in table default off", func(c *qt.C) { 456 cfg := highlight.DefaultConfig 457 cfg.NoClasses = false 458 cfg.LineNos = true 459 cfg.LineNumbersInTable = false 460 461 result := convertForConfig(c, cfg, lines, "bash") 462 c.Assert(result, qt.Contains, "<span class=\"ln\">2</span><span class=\"cl\">LINE2\n</span>") 463 result = convertForConfig(c, cfg, lines, "bash {linenos=table}") 464 c.Assert(result, qt.Contains, "<span class=\"lnt\">1\n</span>") 465 }) 466 467 c.Run("No language", func(c *qt.C) { 468 cfg := highlight.DefaultConfig 469 cfg.NoClasses = false 470 cfg.LineNos = true 471 cfg.LineNumbersInTable = false 472 473 result := convertForConfig(c, cfg, lines, "") 474 c.Assert(result, qt.Contains, "<pre tabindex=\"0\"><code>LINE1\n") 475 }) 476 477 c.Run("No language, guess syntax", func(c *qt.C) { 478 cfg := highlight.DefaultConfig 479 cfg.NoClasses = false 480 cfg.GuessSyntax = true 481 cfg.LineNos = true 482 cfg.LineNumbersInTable = false 483 484 result := convertForConfig(c, cfg, lines, "") 485 c.Assert(result, qt.Contains, "<span class=\"ln\">2</span><span class=\"cl\">LINE2\n</span></span>") 486 }) 487 }