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