github.com/linchen2chris/hugo@v0.0.0-20230307053224-cec209389705/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.ResultRender {
    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 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 class=\"lnlinks\" 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 class=\"lnlinks\" 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 class=\"lnlinks\" 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  }