// Copyright 2024 The Hugo Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package goldmark_test import ( "fmt" "strings" "testing" "github.com/pelletier/go-toml/v2" "github.com/spf13/cast" "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config/testconfig" "github.com/gohugoio/hugo/markup/converter/hooks" "github.com/gohugoio/hugo/markup/goldmark" "github.com/gohugoio/hugo/markup/highlight" "github.com/gohugoio/hugo/markup/markup_config" "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/markup/converter" qt "github.com/frankban/quicktest" ) var cfgStrHighlichgtNoClasses = ` [markup] [markup.highlight] noclasses=false ` func convert(c *qt.C, conf config.AllProvider, content string) converter.ResultRender { pconf := converter.ProviderConfig{ Logger: loggers.NewDefault(), Conf: conf, } p, err := goldmark.Provider.New( pconf, ) c.Assert(err, qt.IsNil) mconf := pconf.MarkupConfig() h := highlight.New(mconf.Highlight) getRenderer := func(t hooks.RendererType, id any) any { if t == hooks.CodeBlockRendererType { return h } return nil } conv, err := p.New(converter.DocumentContext{DocumentID: "thedoc"}) c.Assert(err, qt.IsNil) b, err := conv.Convert(converter.RenderContext{RenderTOC: true, Src: []byte(content), GetRenderer: getRenderer}) c.Assert(err, qt.IsNil) return b } func TestConvert(t *testing.T) { c := qt.New(t) // Smoke test of the default configuration. content := ` ## Links https://github.com/gohugoio/hugo/issues/6528 [Live Demo here!](https://docuapi.netlify.com/) [I'm an inline-style link with title](https://www.google.com "Google's Homepage") https://bar.baz/ ## Code Fences §§§bash LINE1 §§§ ## Code Fences No Lexer §§§moo LINE1 §§§ ## Custom ID {#custom} ## Auto ID * Autolink: https://gohugo.io/ * Strikethrough:~~Hi~~ Hello, world! ## Table | foo | bar | | --- | --- | | baz | bim | ## Task Lists (default on) - [x] Finish my changes[^1] - [ ] Push my commits to GitHub - [ ] Open a pull request ## Smartypants (default on) * Straight double "quotes" and single 'quotes' into “curly” quote HTML entities * Dashes (“--” and “---”) into en- and em-dash entities * Three consecutive dots (“...”) into an ellipsis entity * Apostrophes are also converted: "That was back in the '90s, that's a long time ago" ## Footnotes That's some text with a footnote.[^1] ## Definition Lists date : the datetime assigned to this page. description : the description for the content. ## 神真美好 ## 神真美好 ## 神真美好 [^1]: And that's the footnote. ` // Code fences content = strings.Replace(content, "§§§", "```", -1) cfg := config.FromTOMLConfigString(` [markup] [markup.highlight] noClasses = false [markup.goldmark.renderer] unsafe = true `) b := convert(c, testconfig.GetTestConfig(nil, cfg), content) got := string(b.Bytes()) fmt.Println(got) // Links c.Assert(got, qt.Contains, `Live Demo here!`) c.Assert(got, qt.Contains, `https://foo.bar/`) c.Assert(got, qt.Contains, `https://bar.baz/`) c.Assert(got, qt.Contains, `fake@example.com`) c.Assert(got, qt.Contains, `mailto:fake2@example.com

`) // Header IDs c.Assert(got, qt.Contains, `

Custom ID

`, qt.Commentf(got)) c.Assert(got, qt.Contains, `

Auto ID

`, qt.Commentf(got)) c.Assert(got, qt.Contains, `

神真美好

`, qt.Commentf(got)) c.Assert(got, qt.Contains, `

神真美好

`, qt.Commentf(got)) c.Assert(got, qt.Contains, `

神真美好

`, qt.Commentf(got)) // Code fences c.Assert(got, qt.Contains, "
LINE1\n
") c.Assert(got, qt.Contains, "Code Fences No Lexer\n
LINE1\n
") // Extensions c.Assert(got, qt.Contains, `Autolink: https://gohugo.io/`) c.Assert(got, qt.Contains, `Strikethrough:Hi Hello, world`) c.Assert(got, qt.Contains, `foo`) c.Assert(got, qt.Contains, `
  • Push my commits to GitHub
  • `) c.Assert(got, qt.Contains, `Straight double “quotes” and single ‘quotes’`) c.Assert(got, qt.Contains, `Dashes (“–” and “—”) `) c.Assert(got, qt.Contains, `Three consecutive dots (“…”)`) c.Assert(got, qt.Contains, `“That was back in the ’90s, that’s a long time ago”`) c.Assert(got, qt.Contains, `footnote.1`) c.Assert(got, qt.Contains, `
    `) c.Assert(got, qt.Contains, `
    date
    `) toc, ok := b.(converter.TableOfContentsProvider) c.Assert(ok, qt.Equals, true) tocString := string(toc.TableOfContents().ToHTML(1, 2, false)) c.Assert(tocString, qt.Contains, "TableOfContents") } func TestConvertAutoIDAsciiOnly(t *testing.T) { c := qt.New(t) content := ` ## God is Good: 神真美好 ` cfg := config.FromTOMLConfigString(` [markup] [markup.goldmark] [markup.goldmark.parser] autoHeadingIDType = 'github-ascii' `) b := convert(c, testconfig.GetTestConfig(nil, cfg), content) got := string(b.Bytes()) c.Assert(got, qt.Contains, "

    ") } func TestConvertAutoIDBlackfriday(t *testing.T) { c := qt.New(t) content := ` ## Let's try this, shall we? ` cfg := config.FromTOMLConfigString(` [markup] [markup.goldmark] [markup.goldmark.parser] autoHeadingIDType = 'blackfriday' `) b := convert(c, testconfig.GetTestConfig(nil, cfg), content) got := string(b.Bytes()) c.Assert(got, qt.Contains, "

    ") } func TestConvertAttributes(t *testing.T) { c := qt.New(t) withBlockAttributes := func(conf *markup_config.Config) { conf.Goldmark.Parser.Attribute.Block = true conf.Goldmark.Parser.Attribute.Title = false } withTitleAndBlockAttributes := func(conf *markup_config.Config) { conf.Goldmark.Parser.Attribute.Block = true conf.Goldmark.Parser.Attribute.Title = true } for _, test := range []struct { name string withConfig func(conf *markup_config.Config) input string expect any }{ { "Title", nil, "## heading {#id .className attrName=attrValue class=\"class1 class2\"}", "

    heading

    \n", }, { "Blockquote", withBlockAttributes, "> foo\n> bar\n{#id .className attrName=attrValue class=\"class1 class2\"}\n", "

    foo\nbar

    \n
    \n", }, /*{ // TODO(bep) this needs an upstream fix, see https://github.com/yuin/goldmark/issues/195 "Code block, CodeFences=false", func(conf *markup_config.Config) { withBlockAttributes(conf) conf.Highlight.CodeFences = false }, "```bash\necho 'foo';\n```\n{.myclass}", "TODO", },*/ { "Code block, CodeFences=true", func(conf *markup_config.Config) { withBlockAttributes(conf) conf.Highlight.CodeFences = true }, "```bash {.myclass id=\"myid\"}\necho 'foo';\n````\n", "
    \n\n
    \n
    1\n
    \n
    echo 'foo';\n
    \n
    \n
    ", }, { "Code block, CodeFences=true,lineanchors, default ordinal", func(conf *markup_config.Config) { withBlockAttributes(conf) conf.Highlight.CodeFences = true conf.Highlight.NoClasses = false }, "```bash {linenos=inline, anchorlinenos=true}\necho 'foo';\nnecho 'bar';\n```\n\n```bash {linenos=inline, anchorlinenos=true}\necho 'baz';\nnecho 'qux';\n```", []string{ "1echo 'foo'", "2necho 'bar'", "2necho 'qux'", }, }, { "Paragraph", withBlockAttributes, "\nHi there.\n{.myclass }", "

    Hi there.

    \n", }, { "Ordered list", withBlockAttributes, "\n1. First\n2. Second\n{.myclass }", "
      \n
    1. First
    2. \n
    3. Second
    4. \n
    \n", }, { "Unordered list", withBlockAttributes, "\n* First\n* Second\n{.myclass }", "
      \n
    • First
    • \n
    • Second
    • \n
    \n", }, { "Unordered list, indented", withBlockAttributes, `* Fruit * Apple * Orange * Banana {.fruits} * Dairy * Milk * Cheese {.dairies} {.list}`, []string{"
      \n
    • Fruit\n
        ", "
      • Dairy\n
          "}, }, { "Table", withBlockAttributes, `| A | B | | ------------- |:-------------:| -----:| | AV | BV | {.myclass }`, "\n", }, { "Title and Blockquote", withTitleAndBlockAttributes, "## heading {#id .className attrName=attrValue class=\"class1 class2\"}\n> foo\n> bar\n{.myclass}", "

          heading

          \n

          foo\nbar

          \n
          \n", }, } { c.Run(test.name, func(c *qt.C) { mconf := markup_config.Default if test.withConfig != nil { test.withConfig(&mconf) } data, err := toml.Marshal(mconf) c.Assert(err, qt.IsNil) m := maps.Params{ "markup": config.FromTOMLConfigString(string(data)).Get(""), } conf := testconfig.GetTestConfig(nil, config.NewFrom(m)) b := convert(c, conf, test.input) got := string(b.Bytes()) for _, s := range cast.ToStringSlice(test.expect) { c.Assert(got, qt.Contains, s) } }) } } func TestConvertIssues(t *testing.T) { c := qt.New(t) // https://github.com/gohugoio/hugo/issues/7619 c.Run("Hyphen in HTML attributes", func(c *qt.C) { mconf := markup_config.Default mconf.Goldmark.Renderer.Unsafe = true input := `
          This will be "slotted" into the custom element.
          ` b := convert(c, unsafeConf(), input) got := string(b.Bytes()) c.Assert(got, qt.Contains, "\n
          This will be \"slotted\" into the custom element.
          \n
          \n") }) } func TestCodeFence(t *testing.T) { c := qt.New(t) lines := `LINE1 LINE2 LINE3 LINE4 LINE5 ` convertForConfig := func(c *qt.C, confStr, code, language string) string { cfg := config.FromTOMLConfigString(confStr) conf := testconfig.GetTestConfig(nil, cfg) pcfg := converter.ProviderConfig{ Conf: conf, Logger: loggers.NewDefault(), } p, err := goldmark.Provider.New( pcfg, ) h := highlight.New(pcfg.MarkupConfig().Highlight) getRenderer := func(t hooks.RendererType, id any) any { if t == hooks.CodeBlockRendererType { return h } return nil } content := "```" + language + "\n" + code + "\n```" c.Assert(err, qt.IsNil) conv, err := p.New(converter.DocumentContext{}) c.Assert(err, qt.IsNil) b, err := conv.Convert(converter.RenderContext{Src: []byte(content), GetRenderer: getRenderer}) c.Assert(err, qt.IsNil) return string(b.Bytes()) } c.Run("Basic", func(c *qt.C) { confStr := ` [markup] [markup.highlight] noclasses=false ` result := convertForConfig(c, confStr, `echo "Hugo Rocks!"`, "bash") // TODO(bep) there is a whitespace mismatch (\n) between this and the highlight template func. c.Assert(result, qt.Equals, "
          echo "Hugo Rocks!"\n
          ") result = convertForConfig(c, confStr, `echo "Hugo Rocks!"`, "unknown") c.Assert(result, qt.Equals, "
          echo "Hugo Rocks!"\n
          ") }) c.Run("Highlight lines, default config", func(c *qt.C) { result := convertForConfig(c, cfgStrHighlichgtNoClasses, lines, `bash {linenos=table,hl_lines=[2 "4-5"],linenostart=3}`) c.Assert(result, qt.Contains, "
          \n
          \n
          4")
          
          		result = convertForConfig(c, cfgStrHighlichgtNoClasses, lines, "bash {linenos=inline,hl_lines=[2]}")
          		c.Assert(result, qt.Contains, "2LINE2\n")
          		c.Assert(result, qt.Not(qt.Contains), "2\n")
          	})
          
          	c.Run("Highlight lines, linenumbers default on", func(c *qt.C) {
          		confStr := `
          [markup]
          [markup.highlight]
          noclasses=false
          linenos=true
          `
          
          		result := convertForConfig(c, confStr, lines, "bash")
          		c.Assert(result, qt.Contains, "2\n")
          
          		result = convertForConfig(c, confStr, lines, "bash {linenos=false,hl_lines=[2]}")
          		c.Assert(result, qt.Not(qt.Contains), "class=\"lnt\"")
          	})
          
          	c.Run("Highlight lines, linenumbers default on, linenumbers in table default off", func(c *qt.C) {
          		confStr := `
          [markup]
          [markup.highlight]
          noClasses = false
          lineNos = true
          lineNumbersInTable = false
          `
          
          		result := convertForConfig(c, confStr, lines, "bash")
          		c.Assert(result, qt.Contains, "2LINE2\n")
          		result = convertForConfig(c, confStr, lines, "bash {linenos=table}")
          		c.Assert(result, qt.Contains, "1\n")
          	})
          
          	c.Run("No language", func(c *qt.C) {
          		confStr := `
          [markup]
          [markup.highlight]
          noClasses = false
          lineNos = true
          lineNumbersInTable = false
          `
          		cfg := highlight.DefaultConfig
          		cfg.NoClasses = false
          		cfg.LineNos = true
          		cfg.LineNumbersInTable = false
          
          		result := convertForConfig(c, confStr, lines, "")
          		c.Assert(result, qt.Contains, "
          LINE1\n")
          	})
          
          	c.Run("No language, guess syntax", func(c *qt.C) {
          		confStr := `
          [markup]
          [markup.highlight]
          noClasses = false
          lineNos = true
          lineNumbersInTable = false
          guessSyntax = true
          `
          
          		result := convertForConfig(c, confStr, lines, "")
          		c.Assert(result, qt.Contains, "2LINE2\n")
          	})
          }
          
          func TestTypographerConfig(t *testing.T) {
          	c := qt.New(t)
          
          	content := `
          A "quote" and 'another quote' and a "quote with a 'nested' quote" and a 'quote with a "nested" quote' and an ellipsis...
          `
          
          	confStr := `
          [markup]
          [markup.goldmark]
          [markup.goldmark.extensions]
          [markup.goldmark.extensions.typographer]
          leftDoubleQuote = "«"
          rightDoubleQuote = "»"
          `
          
          	cfg := config.FromTOMLConfigString(confStr)
          	conf := testconfig.GetTestConfig(nil, cfg)
          
          	b := convert(c, conf, content)
          	got := string(b.Bytes())
          
          	c.Assert(got, qt.Contains, "

          A «quote» and ‘another quote’ and a «quote with a ’nested’ quote» and a ‘quote with a «nested» quote’ and an ellipsis…

          \n") } // Issue #11045 func TestTypographerImageAltText(t *testing.T) { c := qt.New(t) content := ` !["They didn't even say 'hello'!" I exclaimed.](https://example.com/image.jpg) ` confStr := ` [markup] [markup.goldmark] ` cfg := config.FromTOMLConfigString(confStr) conf := testconfig.GetTestConfig(nil, cfg) b := convert(c, conf, content) got := string(b.Bytes()) c.Assert(got, qt.Contains, "“They didn’t even say ‘hello’!” I exclaimed.") } func unsafeConf() config.AllProvider { cfg := config.FromTOMLConfigString(` [markup] [markup.goldmark.renderer] unsafe = true `) return testconfig.GetTestConfig(nil, cfg) } func TestConvertCJK(t *testing.T) { c := qt.New(t) content := ` 私は太郎です。 プログラミングが好きです。\ 運動が苦手です。 ` confStr := ` [markup] [markup.goldmark] ` cfg := config.FromTOMLConfigString(confStr) conf := testconfig.GetTestConfig(nil, cfg) b := convert(c, conf, content) got := string(b.Bytes()) c.Assert(got, qt.Contains, "

          私は太郎です。\nプログラミングが好きです。\\ 運動が苦手です。

          \n") } func TestConvertCJKWithExtensionWithEastAsianLineBreaksOption(t *testing.T) { c := qt.New(t) content := ` 私は太郎です。 プログラミングが好きで、 運動が苦手です。 ` confStr := ` [markup] [markup.goldmark] [markup.goldmark.extensions.CJK] enable=true eastAsianLineBreaks=true ` cfg := config.FromTOMLConfigString(confStr) conf := testconfig.GetTestConfig(nil, cfg) b := convert(c, conf, content) got := string(b.Bytes()) c.Assert(got, qt.Contains, "

          私は太郎です。プログラミングが好きで、運動が苦手です。

          \n") } func TestConvertCJKWithExtensionWithEastAsianLineBreaksOptionWithSimple(t *testing.T) { c := qt.New(t) content := ` 私は太郎です。 Programming が好きで、 運動が苦手です。 ` confStr := ` [markup] [markup.goldmark] [markup.goldmark.extensions.CJK] enable=true eastAsianLineBreaks=true eastAsianLineBreaksStyle="simple" ` cfg := config.FromTOMLConfigString(confStr) conf := testconfig.GetTestConfig(nil, cfg) b := convert(c, conf, content) got := string(b.Bytes()) c.Assert(got, qt.Contains, "

          私は太郎です。\nProgramming が好きで、運動が苦手です。

          \n") } func TestConvertCJKWithExtensionWithEastAsianLineBreaksOptionWithStyle(t *testing.T) { c := qt.New(t) content := ` 私は太郎です。 Programming が好きで、 運動が苦手です。 ` confStr := ` [markup] [markup.goldmark] [markup.goldmark.extensions.CJK] enable=true eastAsianLineBreaks=true eastAsianLineBreaksStyle="css3draft" ` cfg := config.FromTOMLConfigString(confStr) conf := testconfig.GetTestConfig(nil, cfg) b := convert(c, conf, content) got := string(b.Bytes()) c.Assert(got, qt.Contains, "

          私は太郎です。Programming が好きで、運動が苦手です。

          \n") } func TestConvertCJKWithExtensionWithEscapedSpaceOption(t *testing.T) { c := qt.New(t) content := ` 私は太郎です。 プログラミングが好きです。\ 運動が苦手です。 ` confStr := ` [markup] [markup.goldmark] [markup.goldmark.extensions.CJK] enable=true escapedSpace=true ` cfg := config.FromTOMLConfigString(confStr) conf := testconfig.GetTestConfig(nil, cfg) b := convert(c, conf, content) got := string(b.Bytes()) c.Assert(got, qt.Contains, "

          私は太郎です。\nプログラミングが好きです。運動が苦手です。

          \n") }