"fmt"
"reflect"
"regexp"
+ "slices"
"sort"
"strconv"
"strings"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/paths"
- "github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/common/urls"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/config/privacy"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugolib/segments"
"github.com/gohugoio/hugo/langs"
+ gc "github.com/gohugoio/hugo/markup/goldmark/goldmark_config"
"github.com/gohugoio/hugo/markup/markup_config"
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/minifiers"
hugo.DeprecateWithLogger("site config key paginate", "Use pagination.pagerSize instead.", "v0.128.0", logger.Logger())
c.Pagination.PagerSize = c.Paginate
}
-
if c.PaginatePath != "" {
hugo.DeprecateWithLogger("site config key paginatePath", "Use pagination.path instead.", "v0.128.0", logger.Logger())
c.Pagination.Path = c.PaginatePath
hugo.DeprecateWithLogger("site config key privacy.twitter.disable", "Use privacy.x.disable instead.", "v0.141.0", logger.Logger())
c.Privacy.X.Disable = c.Privacy.Twitter.Disable
}
-
if c.Privacy.Twitter.EnableDNT {
hugo.DeprecateWithLogger("site config key privacy.twitter.enableDNT", "Use privacy.x.enableDNT instead.", "v0.141.0", logger.Logger())
c.Privacy.X.EnableDNT = c.Privacy.Twitter.EnableDNT
}
-
if c.Privacy.Twitter.Simple {
hugo.DeprecateWithLogger("site config key privacy.twitter.simple", "Use privacy.x.simple instead.", "v0.141.0", logger.Logger())
c.Privacy.X.Simple = c.Privacy.Twitter.Simple
hugo.DeprecateWithLogger("the \":slugorfilename\" permalink token", "Use \":slugorcontentbasename\" instead.", "0.144.0", logger.Logger())
}
+ // Legacy render hook values.
+ alternativeDetails := fmt.Sprintf(
+ "Set to %q if previous value was false, or set to %q if previous value was true.",
+ gc.RenderHookUseEmbeddedNever,
+ gc.RenderHookUseEmbeddedFallback,
+ )
+ if c.Markup.Goldmark.RenderHooks.Image.EnableDefault != nil {
+ alternative := "Use markup.goldmark.renderHooks.image.useEmbedded instead." + " " + alternativeDetails
+ hugo.DeprecateWithLogger("site config key markup.goldmark.renderHooks.image.enableDefault", alternative, "0.148.0", logger.Logger())
+ if *c.Markup.Goldmark.RenderHooks.Image.EnableDefault {
+ c.Markup.Goldmark.RenderHooks.Image.UseEmbedded = gc.RenderHookUseEmbeddedFallback
+ } else {
+ c.Markup.Goldmark.RenderHooks.Image.UseEmbedded = gc.RenderHookUseEmbeddedNever
+ }
+ }
+ if c.Markup.Goldmark.RenderHooks.Link.EnableDefault != nil {
+ alternative := "Use markup.goldmark.renderHooks.link.useEmbedded instead." + " " + alternativeDetails
+ hugo.DeprecateWithLogger("site config key markup.goldmark.renderHooks.link.enableDefault", alternative, "0.148.0", logger.Logger())
+ if *c.Markup.Goldmark.RenderHooks.Link.EnableDefault {
+ c.Markup.Goldmark.RenderHooks.Link.UseEmbedded = gc.RenderHookUseEmbeddedFallback
+ } else {
+ c.Markup.Goldmark.RenderHooks.Link.UseEmbedded = gc.RenderHookUseEmbeddedNever
+ }
+ }
+
+ // Validate render hook configuration.
+ renderHookUseEmbeddedModes := []string{
+ gc.RenderHookUseEmbeddedAlways,
+ gc.RenderHookUseEmbeddedAuto,
+ gc.RenderHookUseEmbeddedFallback,
+ gc.RenderHookUseEmbeddedNever,
+ }
+ if !slices.Contains(renderHookUseEmbeddedModes, c.Markup.Goldmark.RenderHooks.Image.UseEmbedded) {
+ return fmt.Errorf("site config markup.goldmark.renderHooks.image must be one of %s", helpers.StringSliceToList(renderHookUseEmbeddedModes, "or"))
+ }
+ if !slices.Contains(renderHookUseEmbeddedModes, c.Markup.Goldmark.RenderHooks.Link.UseEmbedded) {
+ return fmt.Errorf("site config markup.goldmark.renderHooks.link must be one of %s", helpers.StringSliceToList(renderHookUseEmbeddedModes, "or"))
+ }
+
c.C = &ConfigCompiled{
Timeout: timeout,
BaseURL: baseURL,
// Adjust Goldmark config defaults for multilingual, single-host sites.
if len(languagesConfig) > 1 && !isMultihost && !clone.Markup.Goldmark.DuplicateResourceFiles {
- if !clone.Markup.Goldmark.DuplicateResourceFiles {
- if clone.Markup.Goldmark.RenderHooks.Link.EnableDefault == nil {
- clone.Markup.Goldmark.RenderHooks.Link.EnableDefault = types.NewBool(true)
- }
- if clone.Markup.Goldmark.RenderHooks.Image.EnableDefault == nil {
- clone.Markup.Goldmark.RenderHooks.Image.EnableDefault = types.NewBool(true)
- }
+ if clone.Markup.Goldmark.RenderHooks.Image.UseEmbedded == gc.RenderHookUseEmbeddedAuto {
+ clone.Markup.Goldmark.RenderHooks.Image.UseEmbedded = gc.RenderHookUseEmbeddedFallback
+ }
+ if clone.Markup.Goldmark.RenderHooks.Link.UseEmbedded == gc.RenderHookUseEmbeddedAuto {
+ clone.Markup.Goldmark.RenderHooks.Link.UseEmbedded = gc.RenderHookUseEmbeddedFallback
}
}
import (
"path/filepath"
+ "strings"
"testing"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/config/allconfig"
"github.com/gohugoio/hugo/hugolib"
+ gc "github.com/gohugoio/hugo/markup/goldmark/goldmark_config"
"github.com/gohugoio/hugo/media"
)
b.Assert(len(b.H.Sites), qt.Equals, 1)
}
+
+// Issue 13535
+// We changed enablement of the embedded link and image render hooks from
+// booleans to enums in v0.148.0.
+func TestLegacyEmbeddedRenderHookEnablement(t *testing.T) {
+ files := `
+-- hugo.toml --
+[markup.goldmark.renderHooks.image]
+#KEY_VALUE
+
+[markup.goldmark.renderHooks.link]
+#KEY_VALUE
+`
+ f := strings.ReplaceAll(files, "#KEY_VALUE", "enableDefault = false")
+ b := hugolib.Test(t, f)
+ c := b.H.Configs.Base.Markup.Goldmark.RenderHooks
+ b.Assert(c.Link.UseEmbedded, qt.Equals, gc.RenderHookUseEmbeddedNever)
+ b.Assert(c.Image.UseEmbedded, qt.Equals, gc.RenderHookUseEmbeddedNever)
+
+ f = strings.ReplaceAll(files, "#KEY_VALUE", "enableDefault = true")
+ b = hugolib.Test(t, f)
+ c = b.H.Configs.Base.Markup.Goldmark.RenderHooks
+ b.Assert(c.Link.UseEmbedded, qt.Equals, gc.RenderHookUseEmbeddedFallback)
+ b.Assert(c.Image.UseEmbedded, qt.Equals, gc.RenderHookUseEmbeddedFallback)
+}
return l
}
+// StringSliceToList formats a string slice into a human-readable list.
+// It joins the elements of the slice s with commas, using an Oxford comma,
+// and precedes the final element with the conjunction c.
+func StringSliceToList(s []string, c string) string {
+ const defaultConjunction = "and"
+
+ if c == "" {
+ c = defaultConjunction
+ }
+ if len(s) == 0 {
+ return ""
+ }
+ if len(s) == 1 {
+ return s[0]
+ }
+ if len(s) == 2 {
+ return fmt.Sprintf("%s %s %s", s[0], c, s[1])
+ }
+ return fmt.Sprintf("%s, %s %s", strings.Join(s[:len(s)-1], ", "), c, s[len(s)-1])
+}
+
// IsWhitespace determines if the given rune is whitespace.
func IsWhitespace(r rune) bool {
return r == ' ' || r == '\t' || r == '\n' || r == '\r'
"strings"
"testing"
- "github.com/gohugoio/hugo/helpers"
-
qt "github.com/frankban/quicktest"
+ "github.com/gohugoio/hugo/helpers"
)
func TestResolveMarkup(t *testing.T) {
}
})
}
+
+func TestStringSliceToList(t *testing.T) {
+ for _, tt := range []struct {
+ slice []string
+ conjunction string
+ want string
+ }{
+ {[]string{}, "", ""},
+ {[]string{"foo"}, "", "foo"},
+ {[]string{"foo"}, "and", "foo"},
+ {[]string{"foo", "bar"}, "", "foo and bar"},
+ {[]string{"foo", "bar"}, "and", "foo and bar"},
+ {[]string{"foo", "bar"}, "or", "foo or bar"},
+ {[]string{"foo", "bar", "baz"}, "", "foo, bar, and baz"},
+ {[]string{"foo", "bar", "baz"}, "and", "foo, bar, and baz"},
+ {[]string{"foo", "bar", "baz"}, "or", "foo, bar, or baz"},
+ } {
+ got := helpers.StringSliceToList(tt.slice, tt.conjunction)
+ if got != tt.want {
+ t.Errorf("StringSliceToList() got: %q, want: %q", got, tt.want)
+ }
+ }
+}
"github.com/spf13/cast"
"github.com/gohugoio/hugo/markup/converter/hooks"
+ gc "github.com/gohugoio/hugo/markup/goldmark/goldmark_config"
"github.com/gohugoio/hugo/markup/tableofcontents"
"github.com/gohugoio/hugo/markup/converter"
}
renderHookConfig := pco.po.p.s.conf.Markup.Goldmark.RenderHooks
- var ignoreInternal bool
+ var ignoreEmbedded bool
+
+ // For multilingual single-host sites, "auto" becomes "fallback"
+ // earlier in the process.
switch layoutDescriptor.Variant1 {
case "link":
- ignoreInternal = !renderHookConfig.Link.IsEnableDefault()
+ ignoreEmbedded = renderHookConfig.Link.UseEmbedded == gc.RenderHookUseEmbeddedNever ||
+ renderHookConfig.Link.UseEmbedded == gc.RenderHookUseEmbeddedAuto
case "image":
- ignoreInternal = !renderHookConfig.Image.IsEnableDefault()
+ ignoreEmbedded = renderHookConfig.Image.UseEmbedded == gc.RenderHookUseEmbeddedNever ||
+ renderHookConfig.Image.UseEmbedded == gc.RenderHookUseEmbeddedAuto
}
candidates := pco.po.p.s.renderFormats
return false
}
- if ignoreInternal && candidate.SubCategory() == tplimpl.SubCategoryEmbedded {
- // Don't consider the internal hook templates.
+ if ignoreEmbedded && candidate.SubCategory() == tplimpl.SubCategoryEmbedded {
+ // Don't consider the embedded hook templates.
return false
}
MediaTypes: s.conf.MediaTypes.Config,
DefaultOutputFormat: s.conf.DefaultOutputFormat,
TaxonomySingularPlural: s.conf.Taxonomies,
+ RenderHooks: s.conf.Markup.Goldmark.RenderHooks,
}, tplimpl.SiteOptions{
Site: s,
TemplateFuncs: tplimplinit.CreateFuncMap(s.Deps),
package goldmark_config
const (
- AutoIDTypeGitHub = "github"
- AutoIDTypeGitHubAscii = "github-ascii"
- AutoIDTypeBlackfriday = "blackfriday"
+ AutoIDTypeBlackfriday = "blackfriday"
+ AutoIDTypeGitHub = "github"
+ AutoIDTypeGitHubAscii = "github-ascii"
+ RenderHookUseEmbeddedAlways = "always"
+ RenderHookUseEmbeddedAuto = "auto"
+ RenderHookUseEmbeddedFallback = "fallback"
+ RenderHookUseEmbeddedNever = "never"
)
// Default holds the default Goldmark configuration.
Block: false,
},
},
+ RenderHooks: RenderHooks{
+ Image: ImageRenderHook{
+ UseEmbedded: RenderHookUseEmbeddedAuto,
+ },
+ Link: LinkRenderHook{
+ UseEmbedded: RenderHookUseEmbeddedAuto,
+ },
+ },
}
// Config configures Goldmark.
type ImageRenderHook struct {
// Enable the default image render hook.
// We need to know if it is set or not, hence the pointer.
+ // Deprecated: Use UseEmbedded instead.
EnableDefault *bool
-}
-func (h ImageRenderHook) IsEnableDefault() bool {
- return h.EnableDefault != nil && *h.EnableDefault
+ // When to use the embedded image render hook.
+ // One of auto, never, always, or fallback. Default is auto.
+ UseEmbedded string
}
// LinkRenderHook contains configuration for the link render hook.
type LinkRenderHook struct {
// Disable the default image render hook.
// We need to know if it is set or not, hence the pointer.
+ // Deprecated: Use UseEmbedded instead.
EnableDefault *bool
-}
-func (h LinkRenderHook) IsEnableDefault() bool {
- return h.EnableDefault != nil && *h.EnableDefault
+ // When to use the embedded link render hook.
+ // One of auto, never, always, or fallback. Default is auto.
+ UseEmbedded string
}
type Extensions struct {
`<img src="/dir/p1/pixel.png" alt="alt4" id=""><script>alert()</script>">`,
)
}
+
+// Issue 13535
+func TestEmbeddedLinkAndImageRenderHookConfig(t *testing.T) {
+ t.Parallel()
+
+ files := `
+-- hugo.toml --
+disableKinds = ['home','rss','section','sitemap','taxonomy','term']
+
+[markup.goldmark]
+duplicateResourceFiles = false
+
+[markup.goldmark.renderHooks.image]
+#KEY_VALUE
+
+[markup.goldmark.renderHooks.link]
+#KEY_VALUE
+
+#LANGUAGES
+-- content/s1/p1/index.md --
+---
+title: p1
+---
+[p2](p2)
+
+[a](a.txt)
+
+
+-- content/s1/p1/a.txt --
+-- content/s1/p1/b.jpg --
+-- content/s1/p2.md --
+---
+title: p2
+---
+-- layouts/all.html --
+{{ .Content }}
+`
+
+ const customHooks string = `
+-- layouts/s1/_markup/render-link.html --
+custom link render hook: {{ .Text }}|{{ .Destination }}
+-- layouts/s1/_markup/render-image.html --
+custom image render hook: {{ .Text }}|{{ .Destination }}
+`
+
+ const languages string = `
+[languages.en]
+[languages.fr]
+`
+
+ const (
+ fileToCheck = "public/s1/p1/index.html"
+ wantCustom string = "<p>custom link render hook: p2|p2</p>\n<p>custom link render hook: a|a.txt</p>\n<p>custom image render hook: b|b.jpg</p>"
+ wantEmbedded string = "<p><a href=\"/s1/p2/\">p2</a></p>\n<p><a href=\"/s1/p1/a.txt\">a</a></p>\n<p><img src=\"/s1/p1/b.jpg\" alt=\"b\"></p>"
+ wantGoldmark string = "<p><a href=\"p2\">p2</a></p>\n<p><a href=\"a.txt\">a</a></p>\n<p><img src=\"b.jpg\" alt=\"b\"></p>"
+ )
+
+ tests := []struct {
+ id string // the test id
+ isMultilingual bool // whether the site is multilingual single-host
+ hasCustomHooks bool // whether the site has custom link and image render hooks
+ keyValuePair string // the enableDefault (deprecated in v0.148.0) or useEmbedded key-value pair
+ want string // the expected content of public/s1/p1/index.html
+ }{
+ {"01", false, false, "", wantGoldmark}, // monolingual
+ {"02", false, false, "enableDefault = false", wantGoldmark}, // monolingual, enableDefault = false
+ {"03", false, false, "enableDefault = true", wantEmbedded}, // monolingual, enableDefault = true
+ {"04", false, false, "useEmbedded = 'always'", wantEmbedded}, // monolingual, useEmbedded = 'always'
+ {"05", false, false, "useEmbedded = 'auto'", wantGoldmark}, // monolingual, useEmbedded = 'auto'
+ {"06", false, false, "useEmbedded = 'fallback'", wantEmbedded}, // monolingual, useEmbedded = 'fallback'
+ {"07", false, false, "useEmbedded = 'never'", wantGoldmark}, // monolingual, useEmbedded = 'never'
+ {"08", false, true, "", wantCustom}, // monolingual, with custom hooks
+ {"09", false, true, "enableDefault = false", wantCustom}, // monolingual, with custom hooks, enableDefault = false
+ {"10", false, true, "enableDefault = true", wantCustom}, // monolingual, with custom hooks, enableDefault = true
+ {"11", false, true, "useEmbedded = 'always'", wantEmbedded}, // monolingual, with custom hooks, useEmbedded = 'always'
+ {"12", false, true, "useEmbedded = 'auto'", wantCustom}, // monolingual, with custom hooks, useEmbedded = 'auto'
+ {"13", false, true, "useEmbedded = 'fallback'", wantCustom}, // monolingual, with custom hooks, useEmbedded = 'fallback'
+ {"14", false, true, "useEmbedded = 'never'", wantCustom}, // monolingual, with custom hooks, useEmbedded = 'never'
+ {"15", true, false, "", wantEmbedded}, // multilingual
+ {"16", true, false, "enableDefault = false", wantGoldmark}, // multilingual, enableDefault = false
+ {"17", true, false, "enableDefault = true", wantEmbedded}, // multilingual, enableDefault = true
+ {"18", true, false, "useEmbedded = 'always'", wantEmbedded}, // multilingual, useEmbedded = 'always'
+ {"19", true, false, "useEmbedded = 'auto'", wantEmbedded}, // multilingual, useEmbedded = 'auto'
+ {"20", true, false, "useEmbedded = 'fallback'", wantEmbedded}, // multilingual, useEmbedded = 'fallback'
+ {"21", true, false, "useEmbedded = 'never'", wantGoldmark}, // multilingual, useEmbedded = 'never'
+ {"22", true, true, "", wantCustom}, // multilingual, with custom hooks
+ {"23", true, true, "enableDefault = false", wantCustom}, // multilingual, with custom hooks, enableDefault = false
+ {"24", true, true, "enableDefault = true", wantCustom}, // multilingual, with custom hooks, enableDefault = true
+ {"25", true, true, "useEmbedded = 'always'", wantEmbedded}, // multilingual, with custom hooks, useEmbedded = 'always'
+ {"26", true, true, "useEmbedded = 'auto'", wantCustom}, // multilingual, with custom hooks, useEmbedded = 'auto'
+ {"27", true, true, "useEmbedded = 'fallback'", wantCustom}, // multilingual, with custom hooks, useEmbedded = 'fallback'
+ {"28", true, true, "useEmbedded = 'never'", wantCustom}, // multilingual, with custom hooks, useEmbedded = 'never'
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.id, func(t *testing.T) {
+ t.Parallel()
+
+ f := files
+ if tt.isMultilingual {
+ f = strings.ReplaceAll(f, "#LANGUAGES", languages)
+ }
+ if tt.hasCustomHooks {
+ f = f + customHooks
+ }
+ f = strings.ReplaceAll(f, "#KEY_VALUE", tt.keyValuePair)
+
+ b := hugolib.Test(t, f)
+ b.AssertFileContent(fileToCheck, tt.want)
+ })
+ }
+}
"github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/hugolib/doctree"
"github.com/gohugoio/hugo/identity"
+ gc "github.com/gohugoio/hugo/markup/goldmark/goldmark_config"
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/metrics"
"github.com/gohugoio/hugo/output"
// Whether we are in watch or server mode.
Watching bool
+ // The render hook configuration.
+ RenderHooks gc.RenderHooks
+
// compiled.
legacyMappingTaxonomy map[string]legacyOrdinalMapping
legacyMappingTerm map[string]legacyOrdinalMapping
var ti *TemplInfo
var err error
- if pi.Type() == paths.TypeShortcode {
- ti, err = s.insertShortcode(pi, fi, partialRebuild, s.treeShortcodes)
- if err != nil || ti == nil {
- return err
+ var insertFunc func() (*TemplInfo, error)
+
+ insertFunc = func() (*TemplInfo, error) {
+ return s.insertTemplate(pi, fi, SubCategoryMain, partialRebuild, s.treeMain)
+ }
+
+ switch pi.Type() {
+ case paths.TypeShortcode:
+ insertFunc = func() (*TemplInfo, error) {
+ return s.insertShortcode(pi, fi, partialRebuild, s.treeShortcodes)
}
- } else {
- ti, err = s.insertTemplate(pi, fi, SubCategoryMain, partialRebuild, s.treeMain)
+ case paths.TypeMarkup:
+ skipImageRenderHook := pi.Name() == "render-image.html" && s.opts.RenderHooks.Image.UseEmbedded == "always"
+ skipLinkRenderHook := pi.Name() == "render-link.html" && s.opts.RenderHooks.Link.UseEmbedded == "always"
+ if skipImageRenderHook || skipLinkRenderHook {
+ insertFunc = nil
+ }
+ }
+
+ if insertFunc != nil {
+ ti, err = insertFunc()
if err != nil || ti == nil {
return err
}
}
- if err := s.tns.readTemplateInto(ti); err != nil {
- return err
+ if ti != nil {
+ if err := s.tns.readTemplateInto(ti); err != nil {
+ return err
+ }
}
return nil