hugo

Fork of github.com/gohugoio/hugo with reverse pagination support

git clone git://git.shimmy1996.com/hugo.git

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 &ldquo;quotes&rdquo; and single &lsquo;quotes&rsquo;`)
  177 	c.Assert(got, qt.Contains, `Dashes (“&ndash;” and “&mdash;”) `)
  178 	c.Assert(got, qt.Contains, `Three consecutive dots (“&hellip;”)`)
  179 	c.Assert(got, qt.Contains, `&ldquo;That was back in the &rsquo;90s, that&rsquo;s a long time ago&rdquo;`)
  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\">&#39;foo&#39;</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\">&#39;foo&#39;</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\">&#39;bar&#39;</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\">&#39;qux&#39;</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\">&#34;Hugo Rocks!&#34;</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 &#34;Hugo Rocks!&#34;\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 }