// 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")
LINE1\n
LINE1\n
")
// Extensions
c.Assert(got, qt.Contains, `Autolink: https://gohugo.io/`)
c.Assert(got, qt.Contains, `Strikethrough:\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", "foo\nbar
\n
\n\n1\n
\n\necho '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- First
\n- Second
\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
\nfoo\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\n4")
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 safeConf() config.AllProvider {
cfg := config.FromTOMLConfigString(`
[markup]
[markup.goldmark.renderer]
unsafe = false
`)
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")
}