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 &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="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\">&#39;foo&#39;</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\">&#34;Hugo Rocks!&#34;</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 &#34;Hugo Rocks!&#34;\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  }