diff options
author | Bjørn Erik Pedersen <[email protected]> | 2023-01-04 18:24:36 +0100 |
---|---|---|
committer | Bjørn Erik Pedersen <[email protected]> | 2023-05-16 18:01:29 +0200 |
commit | 241b21b0fd34d91fccb2ce69874110dceae6f926 (patch) | |
tree | d4e0118eac7e9c42f065815447a70805f8d6ad3e /markup | |
parent | 6aededf6b42011c3039f5f66487a89a8dd65e0e7 (diff) | |
download | hugo-241b21b0fd34d91fccb2ce69874110dceae6f926.tar.gz hugo-241b21b0fd34d91fccb2ce69874110dceae6f926.zip |
Create a struct with all of Hugo's config options
Primary motivation is documentation, but it will also hopefully simplify the code.
Also,
* Lower case the default output format names; this is in line with the custom ones (map keys) and how
it's treated all the places. This avoids doing `stringds.EqualFold` everywhere.
Closes #10896
Closes #10620
Diffstat (limited to 'markup')
-rw-r--r-- | markup/asciidocext/convert.go | 283 | ||||
-rw-r--r-- | markup/asciidocext/convert_test.go | 202 | ||||
-rw-r--r-- | markup/asciidocext/internal/converter.go | 274 | ||||
-rw-r--r-- | markup/converter/converter.go | 8 | ||||
-rw-r--r-- | markup/converter/hooks/hooks.go | 2 | ||||
-rw-r--r-- | markup/goldmark/convert.go | 8 | ||||
-rw-r--r-- | markup/goldmark/convert_test.go | 213 | ||||
-rw-r--r-- | markup/goldmark/toc_test.go | 34 | ||||
-rw-r--r-- | markup/highlight/config.go | 2 | ||||
-rw-r--r-- | markup/highlight/highlight.go | 4 | ||||
-rw-r--r-- | markup/markup.go | 12 | ||||
-rw-r--r-- | markup/markup_config/config.go | 14 | ||||
-rw-r--r-- | markup/markup_test.go | 11 | ||||
-rw-r--r-- | markup/org/convert_test.go | 13 | ||||
-rw-r--r-- | markup/pandoc/convert.go | 2 | ||||
-rw-r--r-- | markup/rst/convert.go | 2 | ||||
-rw-r--r-- | markup/tableofcontents/tableofcontents.go | 1 |
17 files changed, 602 insertions, 483 deletions
diff --git a/markup/asciidocext/convert.go b/markup/asciidocext/convert.go index c3bd90edd..ecf3eb9ac 100644 --- a/markup/asciidocext/convert.go +++ b/markup/asciidocext/convert.go @@ -17,26 +17,11 @@ package asciidocext import ( - "bytes" - "path/filepath" - "strings" - - "github.com/gohugoio/hugo/common/hexec" "github.com/gohugoio/hugo/htesting" - - "github.com/gohugoio/hugo/identity" - "github.com/gohugoio/hugo/markup/asciidocext/asciidocext_config" + "github.com/gohugoio/hugo/markup/asciidocext/internal" "github.com/gohugoio/hugo/markup/converter" - "github.com/gohugoio/hugo/markup/internal" - "github.com/gohugoio/hugo/markup/tableofcontents" - "golang.org/x/net/html" ) -/* ToDo: RelPermalink patch for svg posts not working*/ -type pageSubset interface { - RelPermalink() string -} - // Provider is the package entry point. var Provider converter.ProviderProvider = provider{} @@ -44,274 +29,16 @@ type provider struct{} func (p provider) New(cfg converter.ProviderConfig) (converter.Provider, error) { return converter.NewProvider("asciidocext", func(ctx converter.DocumentContext) (converter.Converter, error) { - return &asciidocConverter{ - ctx: ctx, - cfg: cfg, + return &internal.AsciidocConverter{ + Ctx: ctx, + Cfg: cfg, }, nil }), nil } -type asciidocResult struct { - converter.ResultRender - toc *tableofcontents.Fragments -} - -func (r asciidocResult) TableOfContents() *tableofcontents.Fragments { - return r.toc -} - -type asciidocConverter struct { - ctx converter.DocumentContext - cfg converter.ProviderConfig -} - -func (a *asciidocConverter) Convert(ctx converter.RenderContext) (converter.ResultRender, error) { - b, err := a.getAsciidocContent(ctx.Src, a.ctx) - if err != nil { - return nil, err - } - content, toc, err := a.extractTOC(b) - if err != nil { - return nil, err - } - return asciidocResult{ - ResultRender: converter.Bytes(content), - toc: toc, - }, nil -} - -func (a *asciidocConverter) Supports(_ identity.Identity) bool { - return false -} - -// getAsciidocContent calls asciidoctor as an external helper -// to convert AsciiDoc content to HTML. -func (a *asciidocConverter) getAsciidocContent(src []byte, ctx converter.DocumentContext) ([]byte, error) { - if !hasAsciiDoc() { - a.cfg.Logger.Errorln("asciidoctor not found in $PATH: Please install.\n", - " Leaving AsciiDoc content unrendered.") - return src, nil - } - - args := a.parseArgs(ctx) - args = append(args, "-") - - a.cfg.Logger.Infoln("Rendering", ctx.DocumentName, " using asciidoctor args", args, "...") - - return internal.ExternallyRenderContent(a.cfg, ctx, src, asciiDocBinaryName, args) -} - -func (a *asciidocConverter) parseArgs(ctx converter.DocumentContext) []string { - cfg := a.cfg.MarkupConfig.AsciidocExt - args := []string{} - - args = a.appendArg(args, "-b", cfg.Backend, asciidocext_config.CliDefault.Backend, asciidocext_config.AllowedBackend) - - for _, extension := range cfg.Extensions { - if strings.LastIndexAny(extension, `\/.`) > -1 { - a.cfg.Logger.Errorln("Unsupported asciidoctor extension was passed in. Extension `" + extension + "` ignored. Only installed asciidoctor extensions are allowed.") - continue - } - args = append(args, "-r", extension) - } - - for attributeKey, attributeValue := range cfg.Attributes { - if asciidocext_config.DisallowedAttributes[attributeKey] { - a.cfg.Logger.Errorln("Unsupported asciidoctor attribute was passed in. Attribute `" + attributeKey + "` ignored.") - continue - } - - args = append(args, "-a", attributeKey+"="+attributeValue) - } - - if cfg.WorkingFolderCurrent { - contentDir := filepath.Dir(ctx.Filename) - sourceDir := a.cfg.Cfg.GetString("source") - destinationDir := a.cfg.Cfg.GetString("destination") - - if destinationDir == "" { - a.cfg.Logger.Errorln("markup.asciidocext.workingFolderCurrent requires hugo command option --destination to be set") - } - if !filepath.IsAbs(destinationDir) && sourceDir != "" { - destinationDir = filepath.Join(sourceDir, destinationDir) - } - - var outDir string - var err error - - file := filepath.Base(ctx.Filename) - if a.cfg.Cfg.GetBool("uglyUrls") || file == "_index.adoc" || file == "index.adoc" { - outDir, err = filepath.Abs(filepath.Dir(filepath.Join(destinationDir, ctx.DocumentName))) - } else { - postDir := "" - page, ok := ctx.Document.(pageSubset) - if ok { - postDir = filepath.Base(page.RelPermalink()) - } else { - a.cfg.Logger.Errorln("unable to cast interface to pageSubset") - } - - outDir, err = filepath.Abs(filepath.Join(destinationDir, filepath.Dir(ctx.DocumentName), postDir)) - } - - if err != nil { - a.cfg.Logger.Errorln("asciidoctor outDir: ", err) - } - - args = append(args, "--base-dir", contentDir, "-a", "outdir="+outDir) - } - - if cfg.NoHeaderOrFooter { - args = append(args, "--no-header-footer") - } else { - a.cfg.Logger.Warnln("asciidoctor parameter NoHeaderOrFooter is expected for correct html rendering") - } - - if cfg.SectionNumbers { - args = append(args, "--section-numbers") - } - - if cfg.Verbose { - args = append(args, "--verbose") - } - - if cfg.Trace { - args = append(args, "--trace") - } - - args = a.appendArg(args, "--failure-level", cfg.FailureLevel, asciidocext_config.CliDefault.FailureLevel, asciidocext_config.AllowedFailureLevel) - - args = a.appendArg(args, "--safe-mode", cfg.SafeMode, asciidocext_config.CliDefault.SafeMode, asciidocext_config.AllowedSafeMode) - - return args -} - -func (a *asciidocConverter) appendArg(args []string, option, value, defaultValue string, allowedValues map[string]bool) []string { - if value != defaultValue { - if allowedValues[value] { - args = append(args, option, value) - } else { - a.cfg.Logger.Errorln("Unsupported asciidoctor value `" + value + "` for option " + option + " was passed in and will be ignored.") - } - } - return args -} - -const asciiDocBinaryName = "asciidoctor" - -func hasAsciiDoc() bool { - return hexec.InPath(asciiDocBinaryName) -} - -// extractTOC extracts the toc from the given src html. -// It returns the html without the TOC, and the TOC data -func (a *asciidocConverter) extractTOC(src []byte) ([]byte, *tableofcontents.Fragments, error) { - var buf bytes.Buffer - buf.Write(src) - node, err := html.Parse(&buf) - if err != nil { - return nil, nil, err - } - var ( - f func(*html.Node) bool - toc *tableofcontents.Fragments - toVisit []*html.Node - ) - f = func(n *html.Node) bool { - if n.Type == html.ElementNode && n.Data == "div" && attr(n, "id") == "toc" { - toc = parseTOC(n) - if !a.cfg.MarkupConfig.AsciidocExt.PreserveTOC { - n.Parent.RemoveChild(n) - } - return true - } - if n.FirstChild != nil { - toVisit = append(toVisit, n.FirstChild) - } - if n.NextSibling != nil && f(n.NextSibling) { - return true - } - for len(toVisit) > 0 { - nv := toVisit[0] - toVisit = toVisit[1:] - if f(nv) { - return true - } - } - return false - } - f(node) - if err != nil { - return nil, nil, err - } - buf.Reset() - err = html.Render(&buf, node) - if err != nil { - return nil, nil, err - } - // ltrim <html><head></head><body> and rtrim </body></html> which are added by html.Render - res := buf.Bytes()[25:] - res = res[:len(res)-14] - return res, toc, nil -} - -// parseTOC returns a TOC root from the given toc Node -func parseTOC(doc *html.Node) *tableofcontents.Fragments { - var ( - toc tableofcontents.Builder - f func(*html.Node, int, int) - ) - f = func(n *html.Node, row, level int) { - if n.Type == html.ElementNode { - switch n.Data { - case "ul": - if level == 0 { - row++ - } - level++ - f(n.FirstChild, row, level) - case "li": - for c := n.FirstChild; c != nil; c = c.NextSibling { - if c.Type != html.ElementNode || c.Data != "a" { - continue - } - href := attr(c, "href")[1:] - toc.AddAt(&tableofcontents.Heading{ - Title: nodeContent(c), - ID: href, - }, row, level) - } - f(n.FirstChild, row, level) - } - } - if n.NextSibling != nil { - f(n.NextSibling, row, level) - } - } - f(doc.FirstChild, -1, 0) - return toc.Build() -} - -func attr(node *html.Node, key string) string { - for _, a := range node.Attr { - if a.Key == key { - return a.Val - } - } - return "" -} - -func nodeContent(node *html.Node) string { - var buf bytes.Buffer - for c := node.FirstChild; c != nil; c = c.NextSibling { - html.Render(&buf, c) - } - return buf.String() -} - // Supports returns whether Asciidoctor is installed on this computer. func Supports() bool { - hasBin := hasAsciiDoc() + hasBin := internal.HasAsciiDoc() if htesting.SupportsAll() { if !hasBin { panic("asciidoctor not installed") diff --git a/markup/asciidocext/convert_test.go b/markup/asciidocext/convert_test.go index 47208c066..cdc981263 100644 --- a/markup/asciidocext/convert_test.go +++ b/markup/asciidocext/convert_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 The Hugo Authors. All rights reserved. +// Copyright 2023 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. @@ -15,7 +15,7 @@ // external binary. The `asciidoc` module is reserved for a future golang // implementation. -package asciidocext +package asciidocext_test import ( "path/filepath" @@ -26,8 +26,12 @@ import ( "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config/security" + "github.com/gohugoio/hugo/config/testconfig" + "github.com/gohugoio/hugo/markup/asciidocext" + "github.com/gohugoio/hugo/markup/asciidocext/internal" "github.com/gohugoio/hugo/markup/converter" "github.com/gohugoio/hugo/markup/markup_config" + "github.com/spf13/afero" qt "github.com/frankban/quicktest" ) @@ -35,13 +39,12 @@ import ( func TestAsciidoctorDefaultArgs(t *testing.T) { c := qt.New(t) cfg := config.New() - mconf := markup_config.Default + conf := testconfig.GetTestConfig(afero.NewMemMapFs(), cfg) - p, err := Provider.New( + p, err := asciidocext.Provider.New( converter.ProviderConfig{ - Cfg: cfg, - MarkupConfig: mconf, - Logger: loggers.NewErrorLogger(), + Conf: conf, + Logger: loggers.NewErrorLogger(), }, ) c.Assert(err, qt.IsNil) @@ -49,17 +52,16 @@ func TestAsciidoctorDefaultArgs(t *testing.T) { conv, err := p.New(converter.DocumentContext{}) c.Assert(err, qt.IsNil) - ac := conv.(*asciidocConverter) + ac := conv.(*internal.AsciidocConverter) c.Assert(ac, qt.Not(qt.IsNil)) - args := ac.parseArgs(converter.DocumentContext{}) + args := ac.ParseArgs(converter.DocumentContext{}) expected := []string{"--no-header-footer"} c.Assert(args, qt.DeepEquals, expected) } func TestAsciidoctorNonDefaultArgs(t *testing.T) { c := qt.New(t) - cfg := config.New() mconf := markup_config.Default mconf.AsciidocExt.Backend = "manpage" mconf.AsciidocExt.NoHeaderOrFooter = false @@ -68,11 +70,13 @@ func TestAsciidoctorNonDefaultArgs(t *testing.T) { mconf.AsciidocExt.Verbose = true mconf.AsciidocExt.Trace = false mconf.AsciidocExt.FailureLevel = "warn" - p, err := Provider.New( + + conf := testconfig.GetTestConfigSectionFromStruct("markup", mconf) + + p, err := asciidocext.Provider.New( converter.ProviderConfig{ - Cfg: cfg, - MarkupConfig: mconf, - Logger: loggers.NewErrorLogger(), + Conf: conf, + Logger: loggers.NewErrorLogger(), }, ) c.Assert(err, qt.IsNil) @@ -80,28 +84,29 @@ func TestAsciidoctorNonDefaultArgs(t *testing.T) { conv, err := p.New(converter.DocumentContext{}) c.Assert(err, qt.IsNil) - ac := conv.(*asciidocConverter) + ac := conv.(*internal.AsciidocConverter) c.Assert(ac, qt.Not(qt.IsNil)) - args := ac.parseArgs(converter.DocumentContext{}) + args := ac.ParseArgs(converter.DocumentContext{}) expected := []string{"-b", "manpage", "--section-numbers", "--verbose", "--failure-level", "warn", "--safe-mode", "safe"} c.Assert(args, qt.DeepEquals, expected) } func TestAsciidoctorDisallowedArgs(t *testing.T) { c := qt.New(t) - cfg := config.New() mconf := markup_config.Default mconf.AsciidocExt.Backend = "disallowed-backend" mconf.AsciidocExt.Extensions = []string{"./disallowed-extension"} mconf.AsciidocExt.Attributes = map[string]string{"outdir": "disallowed-attribute"} mconf.AsciidocExt.SafeMode = "disallowed-safemode" mconf.AsciidocExt.FailureLevel = "disallowed-failurelevel" - p, err := Provider.New( + + conf := testconfig.GetTestConfigSectionFromStruct("markup", mconf) + + p, err := asciidocext.Provider.New( converter.ProviderConfig{ - Cfg: cfg, - MarkupConfig: mconf, - Logger: loggers.NewErrorLogger(), + Conf: conf, + Logger: loggers.NewErrorLogger(), }, ) c.Assert(err, qt.IsNil) @@ -109,24 +114,23 @@ func TestAsciidoctorDisallowedArgs(t *testing.T) { conv, err := p.New(converter.DocumentContext{}) c.Assert(err, qt.IsNil) - ac := conv.(*asciidocConverter) + ac := conv.(*internal.AsciidocConverter) c.Assert(ac, qt.Not(qt.IsNil)) - args := ac.parseArgs(converter.DocumentContext{}) + args := ac.ParseArgs(converter.DocumentContext{}) expected := []string{"--no-header-footer"} c.Assert(args, qt.DeepEquals, expected) } func TestAsciidoctorArbitraryExtension(t *testing.T) { c := qt.New(t) - cfg := config.New() mconf := markup_config.Default mconf.AsciidocExt.Extensions = []string{"arbitrary-extension"} - p, err := Provider.New( + conf := testconfig.GetTestConfigSectionFromStruct("markup", mconf) + p, err := asciidocext.Provider.New( converter.ProviderConfig{ - Cfg: cfg, - MarkupConfig: mconf, - Logger: loggers.NewErrorLogger(), + Conf: conf, + Logger: loggers.NewErrorLogger(), }, ) c.Assert(err, qt.IsNil) @@ -134,17 +138,17 @@ func TestAsciidoctorArbitraryExtension(t *testing.T) { conv, err := p.New(converter.DocumentContext{}) c.Assert(err, qt.IsNil) - ac := conv.(*asciidocConverter) + ac := conv.(*internal.AsciidocConverter) c.Assert(ac, qt.Not(qt.IsNil)) - args := ac.parseArgs(converter.DocumentContext{}) + args := ac.ParseArgs(converter.DocumentContext{}) expected := []string{"-r", "arbitrary-extension", "--no-header-footer"} c.Assert(args, qt.DeepEquals, expected) } func TestAsciidoctorDisallowedExtension(t *testing.T) { c := qt.New(t) - cfg := config.New() + for _, disallowedExtension := range []string{ `foo-bar//`, `foo-bar\\ `, @@ -156,11 +160,11 @@ func TestAsciidoctorDisallowedExtension(t *testing.T) { } { mconf := markup_config.Default mconf.AsciidocExt.Extensions = []string{disallowedExtension} - p, err := Provider.New( + conf := testconfig.GetTestConfigSectionFromStruct("markup", mconf) + p, err := asciidocext.Provider.New( converter.ProviderConfig{ - Cfg: cfg, - MarkupConfig: mconf, - Logger: loggers.NewErrorLogger(), + Conf: conf, + Logger: loggers.NewErrorLogger(), }, ) c.Assert(err, qt.IsNil) @@ -168,10 +172,10 @@ func TestAsciidoctorDisallowedExtension(t *testing.T) { conv, err := p.New(converter.DocumentContext{}) c.Assert(err, qt.IsNil) - ac := conv.(*asciidocConverter) + ac := conv.(*internal.AsciidocConverter) c.Assert(ac, qt.Not(qt.IsNil)) - args := ac.parseArgs(converter.DocumentContext{}) + args := ac.ParseArgs(converter.DocumentContext{}) expected := []string{"--no-header-footer"} c.Assert(args, qt.DeepEquals, expected) } @@ -179,15 +183,19 @@ func TestAsciidoctorDisallowedExtension(t *testing.T) { func TestAsciidoctorWorkingFolderCurrent(t *testing.T) { c := qt.New(t) - cfg := config.New() - mconf := markup_config.Default - mconf.AsciidocExt.WorkingFolderCurrent = true - mconf.AsciidocExt.Trace = false - p, err := Provider.New( + cfg := config.FromTOMLConfigString(` +[markup] +[markup.asciidocext] +workingFolderCurrent = true +trace = false +`) + + conf := testconfig.GetTestConfig(afero.NewMemMapFs(), cfg) + + p, err := asciidocext.Provider.New( converter.ProviderConfig{ - Cfg: cfg, - MarkupConfig: mconf, - Logger: loggers.NewErrorLogger(), + Conf: conf, + Logger: loggers.NewErrorLogger(), }, ) c.Assert(err, qt.IsNil) @@ -196,32 +204,35 @@ func TestAsciidoctorWorkingFolderCurrent(t *testing.T) { conv, err := p.New(ctx) c.Assert(err, qt.IsNil) - ac := conv.(*asciidocConverter) + ac := conv.(*internal.AsciidocConverter) c.Assert(ac, qt.Not(qt.IsNil)) - args := ac.parseArgs(ctx) + args := ac.ParseArgs(ctx) c.Assert(len(args), qt.Equals, 5) c.Assert(args[0], qt.Equals, "--base-dir") c.Assert(filepath.ToSlash(args[1]), qt.Matches, "/tmp/hugo_asciidoc_ddd/docs/chapter2") c.Assert(args[2], qt.Equals, "-a") - c.Assert(args[3], qt.Matches, `outdir=.*[/\\]{1,2}asciidocext[/\\]{1,2}chapter2`) + c.Assert(args[3], qt.Matches, `outdir=.*chapter2`) c.Assert(args[4], qt.Equals, "--no-header-footer") } func TestAsciidoctorWorkingFolderCurrentAndExtensions(t *testing.T) { c := qt.New(t) - cfg := config.New() - mconf := markup_config.Default - mconf.AsciidocExt.NoHeaderOrFooter = true - mconf.AsciidocExt.Extensions = []string{"asciidoctor-html5s", "asciidoctor-diagram"} - mconf.AsciidocExt.Backend = "html5s" - mconf.AsciidocExt.WorkingFolderCurrent = true - mconf.AsciidocExt.Trace = false - p, err := Provider.New( + cfg := config.FromTOMLConfigString(` +[markup] +[markup.asciidocext] +backend = "html5s" +workingFolderCurrent = true +trace = false +noHeaderOrFooter = true +extensions = ["asciidoctor-html5s", "asciidoctor-diagram"] +`) + conf := testconfig.GetTestConfig(afero.NewMemMapFs(), cfg) + + p, err := asciidocext.Provider.New( converter.ProviderConfig{ - Cfg: cfg, - MarkupConfig: mconf, - Logger: loggers.NewErrorLogger(), + Conf: conf, + Logger: loggers.NewErrorLogger(), }, ) c.Assert(err, qt.IsNil) @@ -229,10 +240,10 @@ func TestAsciidoctorWorkingFolderCurrentAndExtensions(t *testing.T) { conv, err := p.New(converter.DocumentContext{}) c.Assert(err, qt.IsNil) - ac := conv.(*asciidocConverter) + ac := conv.(*internal.AsciidocConverter) c.Assert(ac, qt.Not(qt.IsNil)) - args := ac.parseArgs(converter.DocumentContext{}) + args := ac.ParseArgs(converter.DocumentContext{}) c.Assert(len(args), qt.Equals, 11) c.Assert(args[0], qt.Equals, "-b") c.Assert(args[1], qt.Equals, "html5s") @@ -249,15 +260,19 @@ func TestAsciidoctorWorkingFolderCurrentAndExtensions(t *testing.T) { func TestAsciidoctorAttributes(t *testing.T) { c := qt.New(t) - cfg := config.New() - mconf := markup_config.Default - mconf.AsciidocExt.Attributes = map[string]string{"my-base-url": "https://gohugo.io/", "my-attribute-name": "my value"} - mconf.AsciidocExt.Trace = false - p, err := Provider.New( + cfg := config.FromTOMLConfigString(` +[markup] +[markup.asciidocext] +trace = false +[markup.asciidocext.attributes] +my-base-url = "https://gohugo.io/" +my-attribute-name = "my value" +`) + conf := testconfig.GetTestConfig(nil, cfg) + p, err := asciidocext.Provider.New( converter.ProviderConfig{ - Cfg: cfg, - MarkupConfig: mconf, - Logger: loggers.NewErrorLogger(), + Conf: conf, + Logger: loggers.NewErrorLogger(), }, ) c.Assert(err, qt.IsNil) @@ -265,7 +280,7 @@ func TestAsciidoctorAttributes(t *testing.T) { conv, err := p.New(converter.DocumentContext{}) c.Assert(err, qt.IsNil) - ac := conv.(*asciidocConverter) + ac := conv.(*internal.AsciidocConverter) c.Assert(ac, qt.Not(qt.IsNil)) expectedValues := map[string]bool{ @@ -273,7 +288,7 @@ func TestAsciidoctorAttributes(t *testing.T) { "my-attribute-name=my value": true, } - args := ac.parseArgs(converter.DocumentContext{}) + args := ac.ParseArgs(converter.DocumentContext{}) c.Assert(len(args), qt.Equals, 5) c.Assert(args[0], qt.Equals, "-a") c.Assert(expectedValues[args[1]], qt.Equals, true) @@ -282,15 +297,23 @@ func TestAsciidoctorAttributes(t *testing.T) { c.Assert(args[4], qt.Equals, "--no-header-footer") } -func getProvider(c *qt.C, mconf markup_config.Config) converter.Provider { - sc := security.DefaultConfig - sc.Exec.Allow = security.NewWhitelist("asciidoctor") +func getProvider(c *qt.C, mConfStr string) converter.Provider { + confStr := ` +[security] +[security.exec] +allow = ['asciidoctor'] +` + confStr += mConfStr - p, err := Provider.New( + cfg := config.FromTOMLConfigString(confStr) + conf := testconfig.GetTestConfig(nil, cfg) + securityConfig := conf.GetConfigSection("security").(security.Config) + + p, err := asciidocext.Provider.New( converter.ProviderConfig{ - MarkupConfig: mconf, - Logger: loggers.NewErrorLogger(), - Exec: hexec.New(sc), + Logger: loggers.NewErrorLogger(), + Conf: conf, + Exec: hexec.New(securityConfig), }, ) c.Assert(err, qt.IsNil) @@ -298,12 +321,12 @@ func getProvider(c *qt.C, mconf markup_config.Config) converter.Provider { } func TestConvert(t *testing.T) { - if !Supports() { + if !asciidocext.Supports() { t.Skip("asciidoctor not installed") } c := qt.New(t) - p := getProvider(c, markup_config.Default) + p := getProvider(c, "") conv, err := p.New(converter.DocumentContext{}) c.Assert(err, qt.IsNil) @@ -314,11 +337,11 @@ func TestConvert(t *testing.T) { } func TestTableOfContents(t *testing.T) { - if !Supports() { + if !asciidocext.Supports() { t.Skip("asciidoctor not installed") } c := qt.New(t) - p := getProvider(c, markup_config.Default) + p := getProvider(c, "") conv, err := p.New(converter.DocumentContext{}) c.Assert(err, qt.IsNil) @@ -349,11 +372,11 @@ testContent } func TestTableOfContentsWithCode(t *testing.T) { - if !Supports() { + if !asciidocext.Supports() { t.Skip("asciidoctor not installed") } c := qt.New(t) - p := getProvider(c, markup_config.Default) + p := getProvider(c, "") conv, err := p.New(converter.DocumentContext{}) c.Assert(err, qt.IsNil) r, err := conv.Convert(converter.RenderContext{Src: []byte(`:toc: auto @@ -368,13 +391,16 @@ func TestTableOfContentsWithCode(t *testing.T) { } func TestTableOfContentsPreserveTOC(t *testing.T) { - if !Supports() { + if !asciidocext.Supports() { t.Skip("asciidoctor not installed") } c := qt.New(t) - mconf := markup_config.Default - mconf.AsciidocExt.PreserveTOC = true - p := getProvider(c, mconf) + confStr := ` +[markup] +[markup.asciidocExt] +preserveTOC = true + ` + p := getProvider(c, confStr) conv, err := p.New(converter.DocumentContext{}) c.Assert(err, qt.IsNil) diff --git a/markup/asciidocext/internal/converter.go b/markup/asciidocext/internal/converter.go new file mode 100644 index 000000000..5108bdd0a --- /dev/null +++ b/markup/asciidocext/internal/converter.go @@ -0,0 +1,274 @@ +package internal + +import ( + "bytes" + "path/filepath" + "strings" + + "github.com/gohugoio/hugo/common/hexec" + "github.com/gohugoio/hugo/identity" + "github.com/gohugoio/hugo/markup/asciidocext/asciidocext_config" + "github.com/gohugoio/hugo/markup/converter" + "github.com/gohugoio/hugo/markup/internal" + "github.com/gohugoio/hugo/markup/tableofcontents" + "golang.org/x/net/html" +) + +type AsciidocConverter struct { + Ctx converter.DocumentContext + Cfg converter.ProviderConfig +} + +type AsciidocResult struct { + converter.ResultRender + toc *tableofcontents.Fragments +} + +/* ToDo: RelPermalink patch for svg posts not working*/ +type pageSubset interface { + RelPermalink() string +} + +func (r AsciidocResult) TableOfContents() *tableofcontents.Fragments { + return r.toc +} + +func (a *AsciidocConverter) Convert(ctx converter.RenderContext) (converter.ResultRender, error) { + b, err := a.GetAsciidocContent(ctx.Src, a.Ctx) + if err != nil { + return nil, err + } + content, toc, err := a.extractTOC(b) + if err != nil { + return nil, err + } + return AsciidocResult{ + ResultRender: converter.Bytes(content), + toc: toc, + }, nil +} + +func (a *AsciidocConverter) Supports(_ identity.Identity) bool { + return false +} + +// GetAsciidocContent calls asciidoctor as an external helper +// to convert AsciiDoc content to HTML. +func (a *AsciidocConverter) GetAsciidocContent(src []byte, ctx converter.DocumentContext) ([]byte, error) { + if !HasAsciiDoc() { + a.Cfg.Logger.Errorln("asciidoctor not found in $PATH: Please install.\n", + " Leaving AsciiDoc content unrendered.") + return src, nil + } + + args := a.ParseArgs(ctx) + args = append(args, "-") + + a.Cfg.Logger.Infoln("Rendering", ctx.DocumentName, " using asciidoctor args", args, "...") + + return internal.ExternallyRenderContent(a.Cfg, ctx, src, asciiDocBinaryName, args) +} + +func (a *AsciidocConverter) ParseArgs(ctx converter.DocumentContext) []string { + cfg := a.Cfg.MarkupConfig().AsciidocExt + args := []string{} + + args = a.AppendArg(args, "-b", cfg.Backend, asciidocext_config.CliDefault.Backend, asciidocext_config.AllowedBackend) + + for _, extension := range cfg.Extensions { + if strings.LastIndexAny(extension, `\/.`) > -1 { + a.Cfg.Logger.Errorln("Unsupported asciidoctor extension was passed in. Extension `" + extension + "` ignored. Only installed asciidoctor extensions are allowed.") + continue + } + args = append(args, "-r", extension) + } + + for attributeKey, attributeValue := range cfg.Attributes { + if asciidocext_config.DisallowedAttributes[attributeKey] { + a.Cfg.Logger.Errorln("Unsupported asciidoctor attribute was passed in. Attribute `" + attributeKey + "` ignored.") + continue + } + + args = append(args, "-a", attributeKey+"="+attributeValue) + } + + if cfg.WorkingFolderCurrent { + contentDir := filepath.Dir(ctx.Filename) + destinationDir := a.Cfg.Conf.BaseConfig().PublishDir + + if destinationDir == "" { + a.Cfg.Logger.Errorln("markup.asciidocext.workingFolderCurrent requires hugo command option --destination to be set") + } + + var outDir string + var err error + + file := filepath.Base(ctx.Filename) + if a.Cfg.Conf.IsUglyURLs("") || file == "_index.adoc" || file == "index.adoc" { + outDir, err = filepath.Abs(filepath.Dir(filepath.Join(destinationDir, ctx.DocumentName))) + } else { + postDir := "" + page, ok := ctx.Document.(pageSubset) + if ok { + postDir = filepath.Base(page.RelPermalink()) + } else { + a.Cfg.Logger.Errorln("unable to cast interface to pageSubset") + } + + outDir, err = filepath.Abs(filepath.Join(destinationDir, filepath.Dir(ctx.DocumentName), postDir)) + } + + if err != nil { + a.Cfg.Logger.Errorln("asciidoctor outDir: ", err) + } + + args = append(args, "--base-dir", contentDir, "-a", "outdir="+outDir) + } + + if cfg.NoHeaderOrFooter { + args = append(args, "--no-header-footer") + } else { + a.Cfg.Logger.Warnln("asciidoctor parameter NoHeaderOrFooter is expected for correct html rendering") + } + + if cfg.SectionNumbers { + args = append(args, "--section-numbers") + } + + if cfg.Verbose { + args = append(args, "--verbose") + } + + if cfg.Trace { + args = append(args, "--trace") + } + + args = a.AppendArg(args, "--failure-level", cfg.FailureLevel, asciidocext_config.CliDefault.FailureLevel, asciidocext_config.AllowedFailureLevel) + + args = a.AppendArg(args, "--safe-mode", cfg.SafeMode, asciidocext_config.CliDefault.SafeMode, asciidocext_config.AllowedSafeMode) + + return args +} + +func (a *AsciidocConverter) AppendArg(args []string, option, value, defaultValue string, allowedValues map[string]bool) []string { + if value != defaultValue { + if allowedValues[value] { + args = append(args, option, value) + } else { + a.Cfg.Logger.Errorln("Unsupported asciidoctor value `" + value + "` for option " + option + " was passed in and will be ignored.") + } + } + return args +} + +const asciiDocBinaryName = "asciidoctor" + +func HasAsciiDoc() bool { + return hexec.InPath(asciiDocBinaryName) +} + +// extractTOC extracts the toc from the given src html. +// It returns the html without the TOC, and the TOC data +func (a *AsciidocConverter) extractTOC(src []byte) ([]byte, *tableofcontents.Fragments, error) { + var buf bytes.Buffer + buf.Write(src) + node, err := html.Parse(&buf) + if err != nil { + return nil, nil, err + } + var ( + f func(*html.Node) bool + toc *tableofcontents.Fragments + toVisit []*html.Node + ) + f = func(n *html.Node) bool { + if n.Type == html.ElementNode && n.Data == "div" && attr(n, "id") == "toc" { + toc = parseTOC(n) + if !a.Cfg.MarkupConfig().AsciidocExt.PreserveTOC { + n.Parent.RemoveChild(n) + } + return true + } + if n.FirstChild != nil { + toVisit = append(toVisit, n.FirstChild) + } + if n.NextSibling != nil && f(n.NextSibling) { + return true + } + for len(toVisit) > 0 { + nv := toVisit[0] + toVisit = toVisit[1:] + if f(nv) { + return true + } + } + return false + } + f(node) + if err != nil { + return nil, nil, err + } + buf.Reset() + err = html.Render(&buf, node) + if err != nil { + return nil, nil, err + } + // ltrim <html><head></head><body> and rtrim </body></html> which are added by html.Render + res := buf.Bytes()[25:] + res = res[:len(res)-14] + return res, toc, nil +} + +// parseTOC returns a TOC root from the given toc Node +func parseTOC(doc *html.Node) *tableofcontents.Fragments { + var ( + toc tableofcontents.Builder + f func(*html.Node, int, int) + ) + f = func(n *html.Node, row, level int) { + if n.Type == html.ElementNode { + switch n.Data { + case "ul": + if level == 0 { + row++ + } + level++ + f(n.FirstChild, row, level) + case "li": + for c := n.FirstChild; c != nil; c = c.NextSibling { + if c.Type != html.ElementNode || c.Data != "a" { + continue + } + href := attr(c, "href")[1:] + toc.AddAt(&tableofcontents.Heading{ + Title: nodeContent(c), + ID: href, + }, row, level) + } + f(n.FirstChild, row, level) + } + } + if n.NextSibling != nil { + f(n.NextSibling, row, level) + } + } + f(doc.FirstChild, -1, 0) + return toc.Build() +} + +func attr(node *html.Node, key string) string { + for _, a := range node.Attr { + if a.Key == key { + return a.Val + } + } + return "" +} + +func nodeContent(node *html.Node) string { + var buf bytes.Buffer + for c := node.FirstChild; c != nil; c = c.NextSibling { + html.Render(&buf, c) + } + return buf.String() +} diff --git a/markup/converter/converter.go b/markup/converter/converter.go index 544d4841a..7c4898592 100644 --- a/markup/converter/converter.go +++ b/markup/converter/converter.go @@ -30,15 +30,17 @@ import ( // ProviderConfig configures a new Provider. type ProviderConfig struct { - MarkupConfig markup_config.Config - - Cfg config.Provider // Site config + Conf config.AllProvider // Site config ContentFs afero.Fs Logger loggers.Logger Exec *hexec.Exec highlight.Highlighter } +func (p ProviderConfig) MarkupConfig() markup_config.Config { + return p.Conf.GetConfigSection("markup").(markup_config.Config) +} + // ProviderProvider creates converter providers. type ProviderProvider interface { New(cfg ProviderConfig) (Provider, error) diff --git a/markup/converter/hooks/hooks.go b/markup/converter/hooks/hooks.go index 55d7c1127..5c7b9692d 100644 --- a/markup/converter/hooks/hooks.go +++ b/markup/converter/hooks/hooks.go @@ -31,6 +31,7 @@ type AttributesProvider interface { Attributes() map[string]any } +// LinkContext is the context passed to a link render hook. type LinkContext interface { // The Page being rendered. Page() any @@ -48,6 +49,7 @@ type LinkContext interface { PlainText() string } +// ImageLinkContext is the context passed to a image link render hook. type ImageLinkContext interface { LinkContext diff --git a/markup/goldmark/convert.go b/markup/goldmark/convert.go index efcfb7142..20bbfc210 100644 --- a/markup/goldmark/convert.go +++ b/markup/goldmark/convert.go @@ -54,7 +54,7 @@ func (p provide) New(cfg converter.ProviderConfig) (converter.Provider, error) { cfg: cfg, md: md, sanitizeAnchorName: func(s string) string { - return sanitizeAnchorNameString(s, cfg.MarkupConfig.Goldmark.Parser.AutoHeadingIDType) + return sanitizeAnchorNameString(s, cfg.MarkupConfig().Goldmark.Parser.AutoHeadingIDType) }, }, nil }), nil @@ -75,8 +75,8 @@ func (c *goldmarkConverter) SanitizeAnchorName(s string) string { } func newMarkdown(pcfg converter.ProviderConfig) goldmark.Markdown { - mcfg := pcfg.MarkupConfig - cfg := pcfg.MarkupConfig.Goldmark + mcfg := pcfg.MarkupConfig() + cfg := mcfg.Goldmark var rendererOptions []renderer.Option if cfg.Renderer.HardWraps { @@ -265,7 +265,7 @@ func (c *goldmarkConverter) Supports(feature identity.Identity) bool { } func (c *goldmarkConverter) newParserContext(rctx converter.RenderContext) *parserContext { - ctx := parser.NewContext(parser.WithIDs(newIDFactory(c.cfg.MarkupConfig.Goldmark.Parser.AutoHeadingIDType))) + ctx := parser.NewContext(parser.WithIDs(newIDFactory(c.cfg.MarkupConfig().Goldmark.Parser.AutoHeadingIDType))) ctx.Set(tocEnableKey, rctx.RenderTOC) return &parserContext{ Context: ctx, diff --git a/markup/goldmark/convert_test.go b/markup/goldmark/convert_test.go index e92c651fc..05279cd2c 100644 --- a/markup/goldmark/convert_test.go +++ b/markup/goldmark/convert_test.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Hugo Authors. All rights reserved. +// Copyright 2023 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. @@ -11,37 +11,52 @@ // See the License for the specific language governing permissions and // limitations under the License. -package goldmark +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/goldmark_config" + "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" ) -func convert(c *qt.C, mconf markup_config.Config, content string) converter.ResultRender { - p, err := Provider.New( - converter.ProviderConfig{ - MarkupConfig: mconf, - Logger: loggers.NewErrorLogger(), - }, +var cfgStrHighlichgtNoClasses = ` +[markup] +[markup.highlight] +noclasses=false +` + +func convert(c *qt.C, conf config.AllProvider, content string) converter.ResultRender { + pconf := converter.ProviderConfig{ + Logger: loggers.NewErrorLogger(), + 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 { @@ -140,11 +155,17 @@ description // Code fences content = strings.Replace(content, "§§§", "```", -1) - mconf := markup_config.Default - mconf.Highlight.NoClasses = false - mconf.Goldmark.Renderer.Unsafe = true - b := convert(c, mconf, content) + 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) @@ -193,9 +214,17 @@ func TestConvertAutoIDAsciiOnly(t *testing.T) { content := ` ## God is Good: 神真美好 ` - mconf := markup_config.Default - mconf.Goldmark.Parser.AutoHeadingIDType = goldmark_config.AutoHeadingIDTypeGitHubAscii - b := convert(c, mconf, content) + + 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, "<h2 id=\"god-is-good-\">") @@ -208,9 +237,16 @@ func TestConvertAutoIDBlackfriday(t *testing.T) { ## Let's try this, shall we? ` - mconf := markup_config.Default - mconf.Goldmark.Parser.AutoHeadingIDType = goldmark_config.AutoHeadingIDTypeBlackfriday - b := convert(c, mconf, content) + + 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, "<h2 id=\"let-s-try-this-shall-we\">") @@ -356,7 +392,13 @@ func TestConvertAttributes(t *testing.T) { if test.withConfig != nil { test.withConfig(&mconf) } - b := convert(c, mconf, test.input) + 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) { @@ -378,7 +420,7 @@ func TestConvertIssues(t *testing.T) { </custom-element> ` - b := convert(c, mconf, input) + b := convert(c, unsafeConf(), input) got := string(b.Bytes()) c.Assert(got, qt.Contains, "<custom-element>\n <div>This will be \"slotted\" into the custom element.</div>\n</custom-element>\n") @@ -395,18 +437,18 @@ LINE4 LINE5 ` - convertForConfig := func(c *qt.C, conf highlight.Config, code, language string) string { - mconf := markup_config.Default - mconf.Highlight = conf - - p, err := Provider.New( - converter.ProviderConfig{ - MarkupConfig: mconf, - Logger: loggers.NewErrorLogger(), - }, + 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.NewErrorLogger(), + } + p, err := goldmark.Provider.New( + pcfg, ) - h := highlight.New(conf) + h := highlight.New(pcfg.MarkupConfig().Highlight) getRenderer := func(t hooks.RendererType, id any) any { if t == hooks.CodeBlockRendererType { @@ -427,75 +469,92 @@ LINE5 } c.Run("Basic", func(c *qt.C) { - cfg := highlight.DefaultConfig - cfg.NoClasses = false + confStr := ` +[markup] +[markup.highlight] +noclasses=false +` - result := convertForConfig(c, cfg, `echo "Hugo Rocks!"`, "bash") + 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, "<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\">"Hugo Rocks!"</span>\n</span></span></code></pre></div>") - result = convertForConfig(c, cfg, `echo "Hugo Rocks!"`, "unknown") + result = convertForConfig(c, confStr, `echo "Hugo Rocks!"`, "unknown") c.Assert(result, qt.Equals, "<pre tabindex=\"0\"><code class=\"language-unknown\" data-lang=\"unknown\">echo "Hugo Rocks!"\n</code></pre>") }) c.Run("Highlight lines, default config", func(c *qt.C) { - cfg := highlight.DefaultConfig - cfg.NoClasses = false - result := convertForConfig(c, cfg, lines, `bash {linenos=table,hl_lines=[2 "4-5"],linenostart=3}`) + result := convertForConfig(c, cfgStrHighlichgtNoClasses, lines, `bash {linenos=table,hl_lines=[2 "4-5"],linenostart=3}`) 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") c.Assert(result, qt.Contains, "<span class=\"hl\"><span class=\"lnt\">4") - result = convertForConfig(c, cfg, lines, "bash {linenos=inline,hl_lines=[2]}") + result = convertForConfig(c, cfgStrHighlichgtNoClasses, lines, "bash {linenos=inline,hl_lines=[2]}") c.Assert(result, qt.Contains, "<span class=\"ln\">2</span><span class=\"cl\">LINE2\n</span></span>") c.Assert(result, qt.Not(qt.Contains), "<table") - result = convertForConfig(c, cfg, lines, "bash {linenos=true,hl_lines=[2]}") + result = convertForConfig(c, cfgStrHighlichgtNoClasses, lines, "bash {linenos=true,hl_lines=[2]}") c.Assert(result, qt.Contains, "<table") c.Assert(result, qt.Contains, "<span class=\"hl\"><span class=\"lnt\">2\n</span>") }) c.Run("Highlight lines, linenumbers default on", func(c *qt.C) { - cfg := highlight.DefaultConfig - cfg.NoClasses = false - cfg.LineNos = true + confStr := ` +[markup] +[markup.highlight] +noclasses=false +linenos=true +` - result := convertForConfig(c, cfg, lines, "bash") + result := convertForConfig(c, confStr, lines, "bash") c.Assert(result, qt.Contains, "<span class=\"lnt\">2\n</span>") - result = convertForConfig(c, cfg, lines, "bash {linenos=false,hl_lines=[2]}") + 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) { - cfg := highlight.DefaultConfig - cfg.NoClasses = false - cfg.LineNos = true - cfg.LineNumbersInTable = false + confStr := ` +[markup] +[markup.highlight] +noClasses = false +lineNos = true +lineNumbersInTable = false +` - result := convertForConfig(c, cfg, lines, "bash") + result := convertForConfig(c, confStr, lines, "bash") c.Assert(result, qt.Contains, "<span class=\"ln\">2</span><span class=\"cl\">LINE2\n</span>") - result = convertForConfig(c, cfg, lines, "bash {linenos=table}") + result = convertForConfig(c, confStr, lines, "bash {linenos=table}") c.Assert(result, qt.Contains, "<span class=\"lnt\">1\n</span>") }) 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, cfg, lines, "") + result := convertForConfig(c, confStr, lines, "") c.Assert(result, qt.Contains, "<pre tabindex=\"0\"><code>LINE1\n") }) c.Run("No language, guess syntax", func(c *qt.C) { - cfg := highlight.DefaultConfig - cfg.NoClasses = false - cfg.GuessSyntax = true - cfg.LineNos = true - cfg.LineNumbersInTable = false + confStr := ` +[markup] +[markup.highlight] +noClasses = false +lineNos = true +lineNumbersInTable = false +guessSyntax = true +` - result := convertForConfig(c, cfg, lines, "") + result := convertForConfig(c, confStr, lines, "") c.Assert(result, qt.Contains, "<span class=\"ln\">2</span><span class=\"cl\">LINE2\n</span></span>") }) } @@ -506,11 +565,41 @@ func TestTypographerConfig(t *testing.T) { content := ` A "quote" and 'another quote' and a "quote with a 'nested' quote" and a 'quote with a "nested" quote' and an ellipsis... ` - mconf := markup_config.Default - mconf.Goldmark.Extensions.Typographer.LeftDoubleQuote = "«" - mconf.Goldmark.Extensions.Typographer.RightDoubleQuote = "»" - b := convert(c, mconf, content) + + 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, "<p>A «quote» and ‘another quote’ and a «quote with a ’nested’ quote» and a ‘quote with a «nested» quote’ and an ellipsis…</p>\n") } + +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) + +} diff --git a/markup/goldmark/toc_test.go b/markup/goldmark/toc_test.go index 947f58a36..78811cfb4 100644 --- a/markup/goldmark/toc_test.go +++ b/markup/goldmark/toc_test.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Hugo Authors. All rights reserved. +// Copyright 2023 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. @@ -11,15 +11,15 @@ // See the License for the specific language governing permissions and // limitations under the License. -// Package goldmark converts Markdown to HTML using Goldmark. -package goldmark +package goldmark_test import ( "strings" "testing" + "github.com/gohugoio/hugo/config/testconfig" "github.com/gohugoio/hugo/markup/converter/hooks" - "github.com/gohugoio/hugo/markup/markup_config" + "github.com/gohugoio/hugo/markup/goldmark" "github.com/gohugoio/hugo/common/loggers" @@ -53,10 +53,10 @@ And then some. #### First H4 ` - p, err := Provider.New( + p, err := goldmark.Provider.New( converter.ProviderConfig{ - MarkupConfig: markup_config.Default, - Logger: loggers.NewErrorLogger(), + Conf: testconfig.GetTestConfig(nil, nil), + Logger: loggers.NewErrorLogger(), }) c.Assert(err, qt.IsNil) conv, err := p.New(converter.DocumentContext{}) @@ -83,23 +83,15 @@ And then some. func TestEscapeToc(t *testing.T) { c := qt.New(t) - defaultConfig := markup_config.Default - - safeConfig := defaultConfig - unsafeConfig := defaultConfig - - safeConfig.Goldmark.Renderer.Unsafe = false - unsafeConfig.Goldmark.Renderer.Unsafe = true - - safeP, _ := Provider.New( + safeP, _ := goldmark.Provider.New( converter.ProviderConfig{ - MarkupConfig: safeConfig, - Logger: loggers.NewErrorLogger(), + Conf: safeConf(), + Logger: loggers.NewErrorLogger(), }) - unsafeP, _ := Provider.New( + unsafeP, _ := goldmark.Provider.New( converter.ProviderConfig{ - MarkupConfig: unsafeConfig, - Logger: loggers.NewErrorLogger(), + Conf: unsafeConf(), + Logger: loggers.NewErrorLogger(), }) safeConv, _ := safeP.New(converter.DocumentContext{}) unsafeConv, _ := unsafeP.New(converter.DocumentContext{}) diff --git a/markup/highlight/config.go b/markup/highlight/config.go index b1f6d4603..ca065fd2d 100644 --- a/markup/highlight/config.go +++ b/markup/highlight/config.go @@ -84,7 +84,7 @@ type Config struct { GuessSyntax bool } -func (cfg Config) ToHTMLOptions() []html.Option { +func (cfg Config) toHTMLOptions() []html.Option { var lineAnchors string if cfg.LineAnchors != "" { lineAnchors = cfg.LineAnchors + "-" diff --git a/markup/highlight/highlight.go b/markup/highlight/highlight.go index 410beb740..cb0d578de 100644 --- a/markup/highlight/highlight.go +++ b/markup/highlight/highlight.go @@ -148,10 +148,12 @@ func (h chromaHighlighter) IsDefaultCodeBlockRenderer() bool { var id = identity.NewPathIdentity("chroma", "highlight") +// GetIdentify is for internal use. func (h chromaHighlighter) GetIdentity() identity.Identity { return id } +// HightlightResult holds the result of an highlighting operation. type HightlightResult struct { innerLow int innerHigh int @@ -211,7 +213,7 @@ func highlight(fw hugio.FlexiWriter, code, lang string, attributes []attributes. writeDivStart(w, attributes) } - options := cfg.ToHTMLOptions() + options := cfg.toHTMLOptions() var wrapper html.PreWrapper if cfg.Hl_inline { diff --git a/markup/markup.go b/markup/markup.go index aefa50867..ebd86f38f 100644 --- a/markup/markup.go +++ b/markup/markup.go @@ -35,17 +35,13 @@ import ( func NewConverterProvider(cfg converter.ProviderConfig) (ConverterProvider, error) { converters := make(map[string]converter.Provider) - markupConfig, err := markup_config.Decode(cfg.Cfg) - if err != nil { - return nil, err - } + mcfg := cfg.MarkupConfig() if cfg.Highlighter == nil { - cfg.Highlighter = highlight.New(markupConfig.Highlight) + cfg.Highlighter = highlight.New(mcfg.Highlight) } - cfg.MarkupConfig = markupConfig - defaultHandler := cfg.MarkupConfig.DefaultMarkdownHandler + defaultHandler := mcfg.DefaultMarkdownHandler var defaultFound bool add := func(p converter.ProviderProvider, aliases ...string) error { @@ -123,7 +119,7 @@ func (r *converterRegistry) GetHighlighter() highlight.Highlighter { } func (r *converterRegistry) GetMarkupConfig() markup_config.Config { - return r.config.MarkupConfig + return r.config.MarkupConfig() } func addConverter(m map[string]converter.Provider, c converter.Provider, aliases ...string) { diff --git a/markup/markup_config/config.go b/markup/markup_config/config.go index 60446b9bc..0793669a7 100644 --- a/markup/markup_config/config.go +++ b/markup/markup_config/config.go @@ -28,14 +28,18 @@ import ( type Config struct { // Default markdown handler for md/markdown extensions. // Default is "goldmark". - // Before Hugo 0.60 this was "blackfriday". DefaultMarkdownHandler string - Highlight highlight.Config + // The configuration used by code highlighters. + Highlight highlight.Config + + // Table of contents configuration TableOfContents tableofcontents.Config - // Content renderers - Goldmark goldmark_config.Config + // Configuration for the Goldmark markdown engine. + Goldmark goldmark_config.Config + + // Configuration for the Asciidoc external markdown engine. AsciidocExt asciidocext_config.Config } @@ -46,6 +50,8 @@ func Decode(cfg config.Provider) (conf Config, err error) { if m == nil { return } + m = maps.CleanConfigStringMap(m) + normalizeConfig(m) err = mapstructure.WeakDecode(m, &conf) diff --git a/markup/markup_test.go b/markup/markup_test.go index 5ec27c45c..5cf08758d 100644 --- a/markup/markup_test.go +++ b/markup/markup_test.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Hugo Authors. All rights reserved. +// Copyright 2023 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. @@ -11,20 +11,21 @@ // See the License for the specific language governing permissions and // limitations under the License. -package markup +package markup_test import ( "testing" qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/config/testconfig" + "github.com/gohugoio/hugo/markup" "github.com/gohugoio/hugo/markup/converter" ) func TestConverterRegistry(t *testing.T) { c := qt.New(t) - - r, err := NewConverterProvider(converter.ProviderConfig{Cfg: config.New()}) + conf := testconfig.GetTestConfig(nil, nil) + r, err := markup.NewConverterProvider(converter.ProviderConfig{Conf: conf}) c.Assert(err, qt.IsNil) c.Assert("goldmark", qt.Equals, r.GetMarkupConfig().DefaultMarkdownHandler) diff --git a/markup/org/convert_test.go b/markup/org/convert_test.go index e3676fc34..08841b2d7 100644 --- a/markup/org/convert_test.go +++ b/markup/org/convert_test.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Hugo Authors. All rights reserved. +// Copyright 2023 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. @@ -11,25 +11,26 @@ // See the License for the specific language governing permissions and // limitations under the License. -package org +package org_test import ( "testing" - "github.com/gohugoio/hugo/config" - "github.com/gohugoio/hugo/common/loggers" + "github.com/gohugoio/hugo/config/testconfig" + "github.com/spf13/afero" "github.com/gohugoio/hugo/markup/converter" + "github.com/gohugoio/hugo/markup/org" qt "github.com/frankban/quicktest" ) func TestConvert(t *testing.T) { c := qt.New(t) - p, err := Provider.New(converter.ProviderConfig{ + p, err := org.Provider.New(converter.ProviderConfig{ Logger: loggers.NewErrorLogger(), - Cfg: config.New(), + Conf: testconfig.GetTestConfig(afero.NewMemMapFs(), nil), }) c.Assert(err, qt.IsNil) conv, err := p.New(converter.DocumentContext{}) diff --git a/markup/pandoc/convert.go b/markup/pandoc/convert.go index 386a9ff26..eaa9bfb6a 100644 --- a/markup/pandoc/convert.go +++ b/markup/pandoc/convert.go @@ -18,9 +18,9 @@ import ( "github.com/gohugoio/hugo/common/hexec" "github.com/gohugoio/hugo/htesting" "github.com/gohugoio/hugo/identity" - "github.com/gohugoio/hugo/markup/internal" "github.com/gohugoio/hugo/markup/converter" + "github.com/gohugoio/hugo/markup/internal" ) // Provider is the package entry point. diff --git a/markup/rst/convert.go b/markup/rst/convert.go index 59ce38408..b7aa5a2ce 100644 --- a/markup/rst/convert.go +++ b/markup/rst/convert.go @@ -22,9 +22,9 @@ import ( "github.com/gohugoio/hugo/htesting" "github.com/gohugoio/hugo/identity" - "github.com/gohugoio/hugo/markup/internal" "github.com/gohugoio/hugo/markup/converter" + "github.com/gohugoio/hugo/markup/internal" ) // Provider is the package entry point. diff --git a/markup/tableofcontents/tableofcontents.go b/markup/tableofcontents/tableofcontents.go index bd0aaa801..774b5c6cd 100644 --- a/markup/tableofcontents/tableofcontents.go +++ b/markup/tableofcontents/tableofcontents.go @@ -237,6 +237,7 @@ var DefaultConfig = Config{ type Config struct { // Heading start level to include in the table of contents, starting // at h1 (inclusive). + // <docsmeta>{ "identifiers": ["h1"] }</docsmeta> StartLevel int // Heading end level, inclusive, to include in the table of contents. |