Compare commits

..

1 Commits

Author SHA1 Message Date
Bjørn Erik Pedersen 0c8232e3bd github: Add a "free space" step on Ubuntu 2024-04-14 16:14:16 +02:00
130 changed files with 840 additions and 8285 deletions
+2 -2
View File
@@ -4,7 +4,7 @@ parameters:
defaults: &defaults
resource_class: large
docker:
- image: bepsays/ci-hugoreleaser:1.22200.20201
- image: bepsays/ci-hugoreleaser:1.22200.20200
environment: &buildenv
GOMODCACHE: /root/project/gomodcache
version: 2
@@ -60,7 +60,7 @@ jobs:
environment:
<<: [*buildenv]
docker:
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22200.20201
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22200.20200
steps:
- *restore-cache
- &attach-workspace
+1 -1
View File
@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Copyright 2022 The Hugo Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+2 -1
View File
@@ -62,7 +62,8 @@ And with [Hugo Modules], you can share content, assets, data, translations, them
<p>&nbsp;</p>
<p float="left">
<a href="https://www.linode.com/?utm_campaign=hugosponsor&utm_medium=banner&utm_source=hugogithub" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/gohugoioTheme/master/assets/images/sponsors/linode-logo_standard_light_medium.png" width="200" alt="Linode"></a>
<a href="https://www.linode.com/?utm_campaign=hugosponsor&utm_medium=banner&utm_source=hugogithub" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/gohugoioTheme/master/assets/images/sponsors/linode-logo_standard_light_medium.png" width="200" alt="Linode"></a>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://cloudcannon.com/hugo-cms/?utm_campaign=HugoSponsorship&utm_source=sponsor&utm_content=gohugo" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/gohugoioTheme/master/assets/images/sponsors/cloudcannon-blue.svg" width="220" alt="CloudCannon"></a>
<p>&nbsp;</p>
## Installation
+1 -1
View File
@@ -4,4 +4,4 @@
Please report (suspected) security vulnerabilities to **[bjorn.erik.pedersen@gmail.com](mailto:bjorn.erik.pedersen@gmail.com)**. You will receive a response from us within 48 hours. If we can confirm the issue, we will release a patch as soon as possible depending on the complexity of the issue but historically within days.
Also see [Hugo's Security Model](https://gohugo.io/about/security/).
Also see [Hugo's Security Model](https://gohugo.io/about/security-model/).
+10 -44
View File
@@ -67,7 +67,7 @@ func New(opts Options) *Cache {
evictedIdentities := collections.NewStack[identity.Identity]()
onEvict := func(k, v any) {
if !opts.Watching {
if !opts.Running {
return
}
identity.WalkIdentitiesShallow(v, func(level int, id identity.Identity) bool {
@@ -97,7 +97,7 @@ type Options struct {
CheckInterval time.Duration
MaxSize int
MinMaxSize int
Watching bool
Running bool
}
// Options for a partition.
@@ -140,25 +140,16 @@ func (c *Cache) DrainEvictedIdentities() []identity.Identity {
}
// ClearMatching clears all partition for which the predicate returns true.
func (c *Cache) ClearMatching(predicatePartition func(k string, p PartitionManager) bool, predicateValue func(k, v any) bool) {
if predicatePartition == nil {
predicatePartition = func(k string, p PartitionManager) bool { return true }
}
if predicateValue == nil {
panic("nil predicateValue")
}
func (c *Cache) ClearMatching(predicate func(k, v any) bool) {
g := rungroup.Run[PartitionManager](context.Background(), rungroup.Config[PartitionManager]{
NumWorkers: len(c.partitions),
Handle: func(ctx context.Context, partition PartitionManager) error {
partition.clearMatching(predicateValue)
partition.clearMatching(predicate)
return nil
},
})
for k, p := range c.partitions {
if !predicatePartition(k, p) {
continue
}
for _, p := range c.partitions {
g.Enqueue(p)
}
@@ -365,7 +356,6 @@ func GetOrCreatePartition[K comparable, V any](c *Cache, name string, opts Optio
trace: c.opts.Log.Logger().WithLevel(logg.LevelTrace).WithField("partition", name),
opts: opts,
}
c.partitions[name] = partition
return partition
@@ -385,37 +375,13 @@ type Partition[K comparable, V any] struct {
// GetOrCreate gets or creates a value for the given key.
func (p *Partition[K, V]) GetOrCreate(key K, create func(key K) (V, error)) (V, error) {
v, err := p.doGetOrCreate(key, create)
if err != nil {
return p.zero, err
}
if resource.StaleVersion(v) > 0 {
p.c.Delete(key)
return p.doGetOrCreate(key, create)
}
return v, err
}
func (p *Partition[K, V]) doGetOrCreate(key K, create func(key K) (V, error)) (V, error) {
v, _, err := p.c.GetOrCreate(key, create)
return v, err
}
func (p *Partition[K, V]) GetOrCreateWitTimeout(key K, duration time.Duration, create func(key K) (V, error)) (V, error) {
v, err := p.doGetOrCreateWitTimeout(key, duration, create)
if err != nil {
return p.zero, err
}
if resource.StaleVersion(v) > 0 {
p.c.Delete(key)
return p.doGetOrCreateWitTimeout(key, duration, create)
}
return v, err
}
// GetOrCreateWitTimeout gets or creates a value for the given key and times out if the create function
// takes too long.
func (p *Partition[K, V]) doGetOrCreateWitTimeout(key K, duration time.Duration, create func(key K) (V, error)) (V, error) {
func (p *Partition[K, V]) GetOrCreateWitTimeout(key K, duration time.Duration, create func(key K) (V, error)) (V, error) {
resultch := make(chan V, 1)
errch := make(chan error, 1)
@@ -472,7 +438,7 @@ func (p *Partition[K, V]) clearOnRebuild(changeset ...identity.Identity) {
shouldDelete := func(key K, v V) bool {
// We always clear elements marked as stale.
if resource.StaleVersion(v) > 0 {
if resource.IsStaleAny(v) {
return true
}
@@ -527,8 +493,8 @@ func (p *Partition[K, V]) Keys() []K {
func (p *Partition[K, V]) clearStale() {
p.c.DeleteFunc(func(key K, v V) bool {
staleVersion := resource.StaleVersion(v)
if staleVersion > 0 {
isStale := resource.IsStaleAny(v)
if isStale {
p.trace.Log(
logg.StringFunc(
func() string {
@@ -538,7 +504,7 @@ func (p *Partition[K, V]) clearStale() {
)
}
return staleVersion > 0
return isStale
})
}
+7 -7
View File
@@ -29,12 +29,12 @@ var (
)
type testItem struct {
name string
staleVersion uint32
name string
isStale bool
}
func (t testItem) StaleVersion() uint32 {
return t.staleVersion
func (t testItem) IsStale() bool {
return t.isStale
}
func (t testItem) IdentifierBase() string {
@@ -109,7 +109,7 @@ func newTestCache(t *testing.T) *Cache {
p2.GetOrCreate("clearBecauseStale", func(string) (testItem, error) {
return testItem{
staleVersion: 32,
isStale: true,
}, nil
})
@@ -121,7 +121,7 @@ func newTestCache(t *testing.T) *Cache {
p2.GetOrCreate("clearNever", func(string) (testItem, error) {
return testItem{
staleVersion: 0,
isStale: false,
}, nil
})
@@ -156,7 +156,7 @@ func TestClear(t *testing.T) {
cache = newTestCache(t)
cache.ClearMatching(nil, func(k, v any) bool {
cache.ClearMatching(func(k, v any) bool {
return k.(string) == "clearOnRebuild"
})
+24 -32
View File
@@ -128,7 +128,6 @@ type rootCommand struct {
verbose bool
debug bool
quiet bool
devMode bool // Hidden flag.
renderToMemory bool
@@ -327,12 +326,12 @@ func (r *rootCommand) Name() string {
}
func (r *rootCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
b := newHugoBuilder(r, nil)
if !r.buildWatch {
defer b.postBuild("Total", time.Now())
defer r.timeTrack(time.Now(), "Total")
}
b := newHugoBuilder(r, nil)
if err := b.loadConfig(cd, false); err != nil {
return err
}
@@ -424,33 +423,29 @@ func (r *rootCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
func (r *rootCommand) createLogger(running bool) (loggers.Logger, error) {
level := logg.LevelWarn
if r.devMode {
level = logg.LevelTrace
if r.logLevel != "" {
switch strings.ToLower(r.logLevel) {
case "debug":
level = logg.LevelDebug
case "info":
level = logg.LevelInfo
case "warn", "warning":
level = logg.LevelWarn
case "error":
level = logg.LevelError
default:
return nil, fmt.Errorf("invalid log level: %q, must be one of debug, warn, info or error", r.logLevel)
}
} else {
if r.logLevel != "" {
switch strings.ToLower(r.logLevel) {
case "debug":
level = logg.LevelDebug
case "info":
level = logg.LevelInfo
case "warn", "warning":
level = logg.LevelWarn
case "error":
level = logg.LevelError
default:
return nil, fmt.Errorf("invalid log level: %q, must be one of debug, warn, info or error", r.logLevel)
}
} else {
if r.verbose {
hugo.Deprecate("--verbose", "use --logLevel info", "v0.114.0")
hugo.Deprecate("--verbose", "use --logLevel info", "v0.114.0")
level = logg.LevelInfo
}
if r.verbose {
hugo.Deprecate("--verbose", "use --logLevel info", "v0.114.0")
hugo.Deprecate("--verbose", "use --logLevel info", "v0.114.0")
level = logg.LevelInfo
}
if r.debug {
hugo.Deprecate("--debug", "use --logLevel debug", "v0.114.0")
level = logg.LevelDebug
}
if r.debug {
hugo.Deprecate("--debug", "use --logLevel debug", "v0.114.0")
level = logg.LevelDebug
}
}
@@ -510,13 +505,10 @@ Complete documentation is available at https://gohugo.io/.`
cmd.PersistentFlags().BoolVarP(&r.verbose, "verbose", "v", false, "verbose output")
cmd.PersistentFlags().BoolVarP(&r.debug, "debug", "", false, "debug output")
cmd.PersistentFlags().BoolVarP(&r.devMode, "devMode", "", false, "only used for internal testing, flag hidden.")
cmd.PersistentFlags().StringVar(&r.logLevel, "logLevel", "", "log level (debug|info|warn|error)")
_ = cmd.RegisterFlagCompletionFunc("logLevel", cobra.FixedCompletions([]string{"debug", "info", "warn", "error"}, cobra.ShellCompDirectiveNoFileComp))
cmd.Flags().BoolVarP(&r.buildWatch, "watch", "w", false, "watch filesystem for changes and recreate as needed")
cmd.PersistentFlags().MarkHidden("devMode")
// Configure local flags
applyLocalFlagsBuild(cmd, r)
+8 -14
View File
@@ -45,10 +45,9 @@ func newGenCommand() *genCommand {
genmandir string
// Chroma flags.
style string
highlightStyle string
lineNumbersInlineStyle string
lineNumbersTableStyle string
style string
highlightStyle string
linesStyle string
)
newChromaStyles := func() simplecobra.Commander {
@@ -64,11 +63,8 @@ See https://xyproto.github.io/splash/docs/all.html for a preview of the availabl
if highlightStyle != "" {
builder.Add(chroma.LineHighlight, highlightStyle)
}
if lineNumbersInlineStyle != "" {
builder.Add(chroma.LineNumbers, lineNumbersInlineStyle)
}
if lineNumbersTableStyle != "" {
builder.Add(chroma.LineNumbersTable, lineNumbersTableStyle)
if linesStyle != "" {
builder.Add(chroma.LineNumbers, linesStyle)
}
style, err := builder.Build()
if err != nil {
@@ -82,12 +78,10 @@ See https://xyproto.github.io/splash/docs/all.html for a preview of the availabl
cmd.ValidArgsFunction = cobra.NoFileCompletions
cmd.PersistentFlags().StringVar(&style, "style", "friendly", "highlighter style (see https://xyproto.github.io/splash/docs/)")
_ = cmd.RegisterFlagCompletionFunc("style", cobra.NoFileCompletions)
cmd.PersistentFlags().StringVar(&highlightStyle, "highlightStyle", "", `foreground and background colors for highlighted lines, e.g. --highlightStyle "#fff000 bg:#000fff"`)
cmd.PersistentFlags().StringVar(&highlightStyle, "highlightStyle", "", "style used for highlighting lines (see https://github.com/alecthomas/chroma)")
_ = cmd.RegisterFlagCompletionFunc("highlightStyle", cobra.NoFileCompletions)
cmd.PersistentFlags().StringVar(&lineNumbersInlineStyle, "lineNumbersInlineStyle", "", `foreground and background colors for inline line numbers, e.g. --lineNumbersInlineStyle "#fff000 bg:#000fff"`)
_ = cmd.RegisterFlagCompletionFunc("lineNumbersInlineStyle", cobra.NoFileCompletions)
cmd.PersistentFlags().StringVar(&lineNumbersTableStyle, "lineNumbersTableStyle", "", `foreground and background colors for table line numbers, e.g. --lineNumbersTableStyle "#fff000 bg:#000fff"`)
_ = cmd.RegisterFlagCompletionFunc("lineNumbersTableStyle", cobra.NoFileCompletions)
cmd.PersistentFlags().StringVar(&linesStyle, "linesStyle", "", "style used for line numbers (see https://github.com/alecthomas/chroma)")
_ = cmd.RegisterFlagCompletionFunc("linesStyle", cobra.NoFileCompletions)
},
}
}
+3 -15
View File
@@ -75,14 +75,9 @@ type hugoBuilder struct {
errState hugoBuilderErrState
}
var errConfigNotSet = errors.New("config not set")
func (c *hugoBuilder) withConfE(fn func(conf *commonConfig) error) error {
c.confmu.Lock()
defer c.confmu.Unlock()
if c.conf == nil {
return errConfigNotSet
}
return fn(c.conf)
}
@@ -590,7 +585,7 @@ func (c *hugoBuilder) fullRebuild(changeType string) {
time.Sleep(2 * time.Second)
}()
defer c.postBuild("Rebuilt", time.Now())
defer c.r.timeTrack(time.Now(), "Rebuilt")
err := c.reloadConfig()
if err != nil {
@@ -854,13 +849,13 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
h.BaseFs.SourceFilesystems,
dynamicEvents)
onePageName := pickOneWriteOrCreatePath(h.Conf.ContentTypes(), partitionedEvents.ContentEvents)
onePageName := pickOneWriteOrCreatePath(partitionedEvents.ContentEvents)
c.printChangeDetected("")
c.changeDetector.PrepareNew()
func() {
defer c.postBuild("Total", time.Now())
defer c.r.timeTrack(time.Now(), "Total")
if err := c.rebuildSites(dynamicEvents); err != nil {
c.handleBuildErr(err, "Rebuild failed")
}
@@ -906,13 +901,6 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
}
}
func (c *hugoBuilder) postBuild(what string, start time.Time) {
if h, err := c.hugo(); err == nil && h.Conf.Running() {
h.LogServerAddresses()
}
c.r.timeTrack(start, what)
}
func (c *hugoBuilder) hugo() (*hugolib.HugoSites, error) {
var h *hugolib.HugoSites
if err := c.withConfE(func(conf *commonConfig) error {
+1 -1
View File
@@ -209,7 +209,7 @@ func (c *newCommand) newSiteNextStepsText(path string, format string) string {
1. Change the current directory to ` + path + `.
2. Create or install a theme:
- Create a new theme with the command "hugo new theme <THEMENAME>"
- Or, install a theme from https://themes.gohugo.io/
- Install a theme from https://themes.gohugo.io/
3. Edit hugo.` + format + `, setting the "theme" property to the theme name.
4. Create new content with the command "hugo new content `)
+4 -4
View File
@@ -46,12 +46,12 @@ import (
"github.com/fsnotify/fsnotify"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/common/urls"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/hugolib/filesystems"
"github.com/gohugoio/hugo/livereload"
@@ -1188,16 +1188,16 @@ func partitionDynamicEvents(sourceFs *filesystems.SourceFilesystems, events []fs
return
}
func pickOneWriteOrCreatePath(contentTypes config.ContentTypesProvider, events []fsnotify.Event) string {
func pickOneWriteOrCreatePath(events []fsnotify.Event) string {
name := ""
for _, ev := range events {
if ev.Op&fsnotify.Write == fsnotify.Write || ev.Op&fsnotify.Create == fsnotify.Create {
if contentTypes.IsIndexContentFile(ev.Name) {
if files.IsIndexContentFile(ev.Name) {
return ev.Name
}
if contentTypes.IsContentFile(ev.Name) {
if files.IsContentFile(ev.Name) {
name = ev.Name
}
-14
View File
@@ -123,20 +123,6 @@ func InSlicEqualFold(arr []string, el string) bool {
return false
}
// ToString converts the given value to a string.
// Note that this is a more strict version compared to cast.ToString,
// as it will not try to convert numeric values to strings,
// but only accept strings or fmt.Stringer.
func ToString(v any) (string, bool) {
switch vv := v.(type) {
case string:
return vv, true
case fmt.Stringer:
return vv.String(), true
}
return "", false
}
type Tuple struct {
First string
Second string
+1 -1
View File
@@ -17,7 +17,7 @@ package hugo
// This should be the only one.
var CurrentVersion = Version{
Major: 0,
Minor: 127,
Minor: 125,
PatchLevel: 0,
Suffix: "-DEV",
}
-14
View File
@@ -27,12 +27,7 @@ func NewCache[K comparable, T any]() *Cache[K, T] {
}
// Delete deletes the given key from the cache.
// If c is nil, this method is a no-op.
func (c *Cache[K, T]) Get(key K) (T, bool) {
if c == nil {
var zero T
return zero, false
}
c.RLock()
v, found := c.m[key]
c.RUnlock()
@@ -65,15 +60,6 @@ func (c *Cache[K, T]) Set(key K, value T) {
c.Unlock()
}
// ForEeach calls the given function for each key/value pair in the cache.
func (c *Cache[K, T]) ForEeach(f func(K, T)) {
c.RLock()
defer c.RUnlock()
for k, v := range c.m {
f(k, v)
}
}
// SliceCache is a simple thread safe cache backed by a map.
type SliceCache[T any] struct {
m map[string][]T
+25 -26
View File
@@ -25,6 +25,8 @@ import (
"github.com/gohugoio/hugo/identity"
)
var defaultPathParser PathParser
// PathParser parses a path into a Path.
type PathParser struct {
// Maps the language code to its index in the languages/sites slice.
@@ -32,9 +34,11 @@ type PathParser struct {
// Reports whether the given language is disabled.
IsLangDisabled func(string) bool
}
// Reports whether the given ext is a content file.
IsContentExt func(string) bool
// Parse parses component c with path s into Path using the default path parser.
func Parse(c, s string) *Path {
return defaultPathParser.Parse(c, s)
}
// NormalizePathString returns a normalized path string using the very basic Hugo rules.
@@ -104,6 +108,7 @@ func (pp *PathParser) parse(component, s string) (*Path, error) {
var err error
// Preserve the original case for titles etc.
p.unnormalized, err = pp.doParse(component, s, pp.newPath(component))
if err != nil {
return nil, err
}
@@ -190,26 +195,23 @@ func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
}
}
if len(p.identifiers) > 0 {
isContentComponent := p.component == files.ComponentFolderContent || p.component == files.ComponentFolderArchetypes
isContent := isContentComponent && pp.IsContentExt(p.Ext())
isContentComponent := p.component == files.ComponentFolderContent || p.component == files.ComponentFolderArchetypes
isContent := isContentComponent && files.IsContentExt(p.Ext())
if isContent {
id := p.identifiers[len(p.identifiers)-1]
b := p.s[p.posContainerHigh : id.Low-1]
if isContent {
switch b {
case "index":
p.bundleType = PathTypeLeaf
case "_index":
p.bundleType = PathTypeBranch
default:
p.bundleType = PathTypeContentSingle
}
switch b {
case "index":
p.bundleType = PathTypeLeaf
case "_index":
p.bundleType = PathTypeBranch
default:
p.bundleType = PathTypeContentSingle
}
if slashCount == 2 && p.IsLeafBundle() {
p.posSectionHigh = 0
}
} else if b == files.NameContentData && files.IsContentDataExt(p.Ext()) {
p.bundleType = PathTypeContentData
if slashCount == 2 && p.IsLeafBundle() {
p.posSectionHigh = 0
}
}
@@ -244,9 +246,6 @@ const (
// Branch bundles, e.g. /blog/_index.md
PathTypeBranch
// Content data file, _content.gotmpl.
PathTypeContentData
)
type Path struct {
@@ -522,6 +521,10 @@ func (p *Path) Identifiers() []string {
return ids
}
func (p *Path) IsHTML() bool {
return files.IsHTML(p.Ext())
}
func (p *Path) BundleType() PathType {
return p.bundleType
}
@@ -538,10 +541,6 @@ func (p *Path) IsLeafBundle() bool {
return p.bundleType == PathTypeLeaf
}
func (p *Path) IsContentData() bool {
return p.bundleType == PathTypeContentData
}
func (p Path) ForBundleType(t PathType) *Path {
p.bundleType = t
return &p
-19
View File
@@ -27,9 +27,6 @@ var testParser = &PathParser{
"no": 0,
"en": 1,
},
IsContentExt: func(ext string) bool {
return ext == "md"
},
}
func TestParse(t *testing.T) {
@@ -336,22 +333,6 @@ func TestParse(t *testing.T) {
c.Assert(p.Path(), qt.Equals, "/a/b/c.txt")
},
},
{
"Content data file gotmpl",
"/a/b/_content.gotmpl",
func(c *qt.C, p *Path) {
c.Assert(p.Path(), qt.Equals, "/a/b/_content.gotmpl")
c.Assert(p.Ext(), qt.Equals, "gotmpl")
c.Assert(p.IsContentData(), qt.IsTrue)
},
},
{
"Content data file yaml",
"/a/b/_content.yaml",
func(c *qt.C, p *Path) {
c.Assert(p.IsContentData(), qt.IsFalse)
},
},
}
for _, test := range tests {
c.Run(test.name, func(c *qt.C) {
+1 -3
View File
@@ -367,7 +367,6 @@ func (c *Config) CompileConfig(logger loggers.Logger) error {
DisabledLanguages: disabledLangs,
IgnoredLogs: ignoredLogIDs,
KindOutputFormats: kindOutputFormats,
ContentTypes: media.DefaultContentTypes.FromTypes(c.MediaTypes.Config),
CreateTitle: helpers.GetTitleFunc(c.TitleCaseStyle),
IsUglyURLSection: isUglyURL,
IgnoreFile: ignoreFile,
@@ -403,7 +402,6 @@ type ConfigCompiled struct {
BaseURLLiveReload urls.BaseURL
ServerInterface string
KindOutputFormats map[string]output.Formats
ContentTypes media.ContentTypes
DisabledKinds map[string]bool
DisabledLanguages map[string]bool
IgnoredLogs map[string]bool
@@ -761,7 +759,7 @@ func (c *Configs) Init() error {
c.Languages = languages
c.LanguagesDefaultFirst = languagesDefaultFirst
c.ContentPathParser = &paths.PathParser{LanguageIndex: languagesDefaultFirst.AsIndexSet(), IsLangDisabled: c.Base.IsLangDisabled, IsContentExt: c.Base.C.ContentTypes.IsContentSuffix}
c.ContentPathParser = &paths.PathParser{LanguageIndex: languagesDefaultFirst.AsIndexSet(), IsLangDisabled: c.Base.IsLangDisabled}
c.configLangs = make([]config.AllProvider, len(c.Languages))
for i, l := range c.LanguagesDefaultFirst {
@@ -84,21 +84,3 @@ logPathWarnings = true
b.Assert(conf.PrintI18nWarnings, qt.Equals, true)
b.Assert(conf.PrintPathWarnings, qt.Equals, true)
}
func TestRedefineContentTypes(t *testing.T) {
files := `
-- hugo.toml --
baseURL = "https://example.com"
[mediaTypes]
[mediaTypes."text/html"]
suffixes = ["html", "xhtml"]
`
b := hugolib.Test(t, files)
conf := b.H.Configs.Base
contentTypes := conf.C.ContentTypes
b.Assert(contentTypes.HTML.Suffixes(), qt.DeepEquals, []string{"html", "xhtml"})
b.Assert(contentTypes.Markdown.Suffixes(), qt.DeepEquals, []string{"md", "mdown", "markdown"})
}
-7
View File
@@ -71,9 +71,6 @@ func (c ConfigLanguage) Environment() string {
}
func (c ConfigLanguage) IsMultihost() bool {
if len(c.m.Languages)-len(c.config.C.DisabledLanguages) <= 1 {
return false
}
return c.m.IsMultihost
}
@@ -144,10 +141,6 @@ func (c ConfigLanguage) NewIdentityManager(name string) identity.Manager {
return identity.NewManager(name)
}
func (c ConfigLanguage) ContentTypes() config.ContentTypesProvider {
return c.config.C.ContentTypes
}
// GetConfigSection is mostly used in tests. The switch statement isn't complete, but what's in use.
func (c ConfigLanguage) GetConfigSection(s string) any {
switch s {
-10
View File
@@ -41,7 +41,6 @@ type AllProvider interface {
Dirs() CommonDirs
Quiet() bool
DirsBase() CommonDirs
ContentTypes() ContentTypesProvider
GetConfigSection(string) any
GetConfig() any
CanonifyURLs() bool
@@ -76,15 +75,6 @@ type AllProvider interface {
EnableEmoji() bool
}
// We cannot import the media package as that would create a circular dependency.
// This interface defineds a sub set of what media.ContentTypes provides.
type ContentTypesProvider interface {
IsContentSuffix(suffix string) bool
IsContentFile(filename string) bool
IsIndexContentFile(filename string) bool
IsHTMLSuffix(suffix string) bool
}
// Provider provides the configuration settings for Hugo.
type Provider interface {
GetString(key string) string
+3 -1
View File
@@ -29,6 +29,8 @@ import (
"github.com/gohugoio/hugo/common/hstrings"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/helpers"
@@ -96,7 +98,7 @@ func NewContent(h *hugolib.HugoSites, kind, targetPath string, force bool) error
return "", fmt.Errorf("failed to resolve %q to an archetype template", targetPath)
}
if !h.Conf.ContentTypes().IsContentFile(b.targetPath) {
if !files.IsContentFile(b.targetPath) {
return "", fmt.Errorf("target path %q is not a known content format", b.targetPath)
}
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="{{ or site.Language.LanguageCode }}" dir="{{ or site.Language.LanguageDirection `ltr` }}">
<html lang="{{ or site.Language.LanguageCode site.Language.Lang }}" dir="{{ or site.Language.LanguageDirection `ltr` }}">
<head>
{{ partial "head.html" . }}
</head>
+1 -1
View File
@@ -155,7 +155,7 @@ func (d *Deps) Init() error {
}
if d.MemCache == nil {
d.MemCache = dynacache.New(dynacache.Options{Watching: d.Conf.Watching(), Log: d.Log})
d.MemCache = dynacache.New(dynacache.Options{Running: d.Conf.Running(), Log: d.Log})
}
if d.PathSpec == nil {
@@ -20,11 +20,10 @@ hugo gen chromastyles [flags] [args]
### Options
```
-h, --help help for chromastyles
--highlightStyle string foreground and background colors for highlighted lines, e.g. --highlightStyle "#fff000 bg:#000fff"
--lineNumbersInlineStyle string foreground and background colors for inline line numbers, e.g. --lineNumbersInlineStyle "#fff000 bg:#000fff"
--lineNumbersTableStyle string foreground and background colors for table line numbers, e.g. --lineNumbersTableStyle "#fff000 bg:#000fff"
--style string highlighter style (see https://xyproto.github.io/splash/docs/) (default "friendly")
-h, --help help for chromastyles
--highlightStyle string style used for highlighting lines (see https://github.com/alecthomas/chroma)
--linesStyle string style used for line numbers (see https://github.com/alecthomas/chroma)
--style string highlighter style (see https://xyproto.github.io/splash/docs/) (default "friendly")
```
### Options inherited from parent commands
-35
View File
@@ -1065,15 +1065,6 @@ config:
enable: false
escapedSpace: false
definitionList: true
extras:
insert:
enable: false
mark:
enable: false
subscript:
enable: false
superscript:
enable: false
footnote: true
linkify: true
linkifyProtocol: https
@@ -1209,12 +1200,6 @@ config:
delimiter: .
suffixes:
- webp
text/asciidoc:
delimiter: .
suffixes:
- adoc
- asciidoc
- ad
text/calendar:
delimiter: .
suffixes:
@@ -1231,7 +1216,6 @@ config:
delimiter: .
suffixes:
- html
- htm
text/javascript:
delimiter: .
suffixes:
@@ -1246,25 +1230,11 @@ config:
delimiter: .
suffixes:
- md
- mdown
- markdown
text/org:
delimiter: .
suffixes:
- org
text/pandoc:
delimiter: .
suffixes:
- pandoc
- pdc
text/plain:
delimiter: .
suffixes:
- txt
text/rst:
delimiter: .
suffixes:
- rst
text/tsx:
delimiter: .
suffixes:
@@ -4047,11 +4017,6 @@ tpl:
- s
Description: CountWords returns the approximate word count in s.
Examples: []
Diff:
Aliases: null
Args: null
Description: ""
Examples: null
FindRE:
Aliases:
- findRE
+10 -11
View File
@@ -30,12 +30,11 @@ require (
github.com/fortytw2/leaktest v1.3.0
github.com/frankban/quicktest v1.14.6
github.com/fsnotify/fsnotify v1.7.0
github.com/getkin/kin-openapi v0.123.0
github.com/getkin/kin-openapi v0.124.0
github.com/ghodss/yaml v1.0.0
github.com/gobuffalo/flect v1.0.2
github.com/gobwas/glob v0.2.3
github.com/gohugoio/go-i18n/v2 v2.1.3-0.20230805085216-e63c13218d0e
github.com/gohugoio/hugo-goldmark-extensions/extras v0.1.0
github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.2.0
github.com/gohugoio/locales v0.14.0
github.com/gohugoio/localescompressed v1.0.1
@@ -56,7 +55,7 @@ require (
github.com/niklasfasching/go-org v1.7.0
github.com/olekukonko/tablewriter v0.0.5
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58
github.com/pelletier/go-toml/v2 v2.2.2
github.com/pelletier/go-toml/v2 v2.2.0
github.com/rogpeppe/go-internal v1.12.0
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd
github.com/sanity-io/litter v1.5.5
@@ -65,18 +64,18 @@ require (
github.com/spf13/cobra v1.8.0
github.com/spf13/fsync v0.10.1
github.com/spf13/pflag v1.0.5
github.com/tdewolff/minify/v2 v2.20.20
github.com/tdewolff/parse/v2 v2.7.13
github.com/tdewolff/minify/v2 v2.20.19
github.com/tdewolff/parse/v2 v2.7.12
github.com/yuin/goldmark v1.7.1
github.com/yuin/goldmark-emoji v1.0.2
go.uber.org/automaxprocs v1.5.3
gocloud.dev v0.36.0
golang.org/x/exp v0.0.0-20221031165847-c99f073a8326
golang.org/x/image v0.16.0
golang.org/x/image v0.15.0
golang.org/x/mod v0.17.0
golang.org/x/net v0.25.0
golang.org/x/net v0.24.0
golang.org/x/sync v0.7.0
golang.org/x/text v0.15.0
golang.org/x/text v0.14.0
golang.org/x/tools v0.20.0
google.golang.org/api v0.152.0
gopkg.in/yaml.v2 v2.4.0
@@ -143,9 +142,9 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/oauth2 v0.15.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
google.golang.org/appengine v1.6.8 // indirect
@@ -153,7 +152,7 @@ require (
google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect
google.golang.org/grpc v1.59.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
howett.net/plist v1.0.0 // indirect
software.sslmate.com/src/go-pkcs12 v0.2.0 // indirect
+18 -22
View File
@@ -193,8 +193,6 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/getkin/kin-openapi v0.123.0 h1:zIik0mRwFNLyvtXK274Q6ut+dPh6nlxBp0x7mNrPhs8=
github.com/getkin/kin-openapi v0.123.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM=
github.com/getkin/kin-openapi v0.124.0 h1:VSFNMB9C9rTKBnQ/fpyDU8ytMTr4dWI9QovSKj9kz/M=
github.com/getkin/kin-openapi v0.124.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
@@ -213,8 +211,6 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/gohugoio/go-i18n/v2 v2.1.3-0.20230805085216-e63c13218d0e h1:QArsSubW7eDh8APMXkByjQWvuljwPGAGQpJEFn0F0wY=
github.com/gohugoio/go-i18n/v2 v2.1.3-0.20230805085216-e63c13218d0e/go.mod h1:3Ltoo9Banwq0gOtcOwxuHG6omk+AwsQPADyw2vQYOJQ=
github.com/gohugoio/hugo-goldmark-extensions/extras v0.1.0 h1:YhxZNU8y2vxV6Ibr7QJzzUlpr8oHHWX/l+Q1R/a5Zao=
github.com/gohugoio/hugo-goldmark-extensions/extras v0.1.0/go.mod h1:0cuvOnGKW7WeXA3i7qK6IS07FH1bgJ2XzOjQ7BMJYH4=
github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.2.0 h1:PCtO5l++psZf48yen2LxQ3JiOXxaRC6v0594NeHvGZg=
github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.2.0/go.mod h1:g9CCh+Ci2IMbPUrVJuXbBTrA+rIIx5+hDQ4EXYaQDoM=
github.com/gohugoio/locales v0.14.0 h1:Q0gpsZwfv7ATHMbcTNepFd59H7GoykzWJIxi113XGDc=
@@ -380,8 +376,8 @@ github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo=
github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
@@ -433,10 +429,10 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tdewolff/minify/v2 v2.20.20 h1:vhULb+VsW2twkplgsawAoUY957efb+EdiZ7zu5fUhhk=
github.com/tdewolff/minify/v2 v2.20.20/go.mod h1:GYaLXFpIIwsX99apQHXfGdISUdlA98wmaoWxjT9C37k=
github.com/tdewolff/parse/v2 v2.7.13 h1:iSiwOUkCYLNfapHoqdLcqZVgvQ0jrsao8YYKP/UJYTI=
github.com/tdewolff/parse/v2 v2.7.13/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA=
github.com/tdewolff/minify/v2 v2.20.19 h1:tX0SR0LUrIqGoLjXnkIzRSIbKJ7PaNnSENLD4CyH6Xo=
github.com/tdewolff/minify/v2 v2.20.19/go.mod h1:ulkFoeAVWMLEyjuDz1ZIWOA31g5aWOawCFRp9R/MudM=
github.com/tdewolff/parse/v2 v2.7.12 h1:tgavkHc2ZDEQVKy1oWxwIyh5bP4F5fEh/JmBwPP/3LQ=
github.com/tdewolff/parse/v2 v2.7.12/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA=
github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo=
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
@@ -472,8 +468,8 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -489,8 +485,8 @@ golang.org/x/exp v0.0.0-20221031165847-c99f073a8326/go.mod h1:CxIveKay+FTh1D0yPZ
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.16.0 h1:9kloLAKhUufZhA12l5fwnx2NZW39/we1UhBesW433jw=
golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs=
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -551,8 +547,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -621,8 +617,8 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -634,8 +630,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -801,8 +797,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+26 -25
View File
@@ -26,7 +26,6 @@ import (
"github.com/gohugoio/hugo/common/hexec"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/media"
"github.com/spf13/afero"
@@ -37,6 +36,11 @@ import (
"github.com/gohugoio/hugo/config"
)
var (
openingPTag = []byte("<p>")
closingPTag = []byte("</p>")
)
// ContentSpec provides functionality to render markdown content.
type ContentSpec struct {
Converters markup.ConverterProvider
@@ -136,16 +140,20 @@ func (c *ContentSpec) SanitizeAnchorName(s string) string {
}
func (c *ContentSpec) ResolveMarkup(in string) string {
if c == nil {
panic("nil ContentSpec")
}
in = strings.ToLower(in)
if mediaType, found := c.Cfg.ContentTypes().(media.ContentTypes).Types().GetBestMatch(markup.ResolveMarkup(in)); found {
return mediaType.SubType
switch in {
case "md", "markdown", "mdown":
return "markdown"
case "html", "htm":
return "html"
default:
if conv := c.Converters.Get(in); conv != nil {
return conv.Name()
}
}
if conv := c.Converters.Get(in); conv != nil {
return markup.ResolveMarkup(conv.Name())
}
return ""
}
@@ -234,26 +242,19 @@ func (c *ContentSpec) TruncateWordsToWholeSentence(s string) (string, bool) {
return strings.TrimSpace(s[:endIndex]), endIndex < len(s)
}
// TrimShortHTML removes the outer tags from HTML input where (a) the opening
// tag is present only once with the input, and (b) the opening and closing
// tags wrap the input after white space removal.
func (c *ContentSpec) TrimShortHTML(input []byte, markup string) []byte {
openingTag := []byte("<p>")
closingTag := []byte("</p>")
if markup == media.DefaultContentTypes.AsciiDoc.SubType {
openingTag = []byte("<div class=\"paragraph\">\n<p>")
closingTag = []byte("</p>\n</div>")
}
if bytes.Count(input, openingTag) == 1 {
// TrimShortHTML removes the <p>/</p> tags from HTML input in the situation
// where said tags are the only <p> tags in the input and enclose the content
// of the input (whitespace excluded).
func (c *ContentSpec) TrimShortHTML(input []byte) []byte {
if bytes.Count(input, openingPTag) == 1 {
input = bytes.TrimSpace(input)
if bytes.HasPrefix(input, openingTag) && bytes.HasSuffix(input, closingTag) {
input = bytes.TrimPrefix(input, openingTag)
input = bytes.TrimSuffix(input, closingTag)
if bytes.HasPrefix(input, openingPTag) && bytes.HasSuffix(input, closingPTag) {
input = bytes.TrimPrefix(input, openingPTag)
input = bytes.TrimSuffix(input, closingPTag)
input = bytes.TrimSpace(input)
}
}
return input
}
+14 -17
View File
@@ -26,27 +26,24 @@ import (
func TestTrimShortHTML(t *testing.T) {
tests := []struct {
markup string
input []byte
output []byte
input, output []byte
}{
{"markdown", []byte(""), []byte("")},
{"markdown", []byte("Plain text"), []byte("Plain text")},
{"markdown", []byte("<p>Simple paragraph</p>"), []byte("Simple paragraph")},
{"markdown", []byte("\n \n \t <p> \t Whitespace\nHTML \n\t </p>\n\t"), []byte("Whitespace\nHTML")},
{"markdown", []byte("<p>Multiple</p><p>paragraphs</p>"), []byte("<p>Multiple</p><p>paragraphs</p>")},
{"markdown", []byte("<p>Nested<p>paragraphs</p></p>"), []byte("<p>Nested<p>paragraphs</p></p>")},
{"markdown", []byte("<p>Hello</p>\n<ul>\n<li>list1</li>\n<li>list2</li>\n</ul>"), []byte("<p>Hello</p>\n<ul>\n<li>list1</li>\n<li>list2</li>\n</ul>")},
// Issue 11698
{"markdown", []byte("<h2 id=`a`>b</h2>\n\n<p>c</p>"), []byte("<h2 id=`a`>b</h2>\n\n<p>c</p>")},
// Issue 12369
{"markdown", []byte("<div class=\"paragraph\">\n<p>foo</p>\n</div>"), []byte("<div class=\"paragraph\">\n<p>foo</p>\n</div>")},
{"asciidoc", []byte("<div class=\"paragraph\">\n<p>foo</p>\n</div>"), []byte("foo")},
{[]byte(""), []byte("")},
{[]byte("Plain text"), []byte("Plain text")},
// This seems wrong. Why touch it if it doesn't have p tag?
// {[]byte(" \t\n Whitespace text\n\n"), []byte("Whitespace text")},
{[]byte("<p>Simple paragraph</p>"), []byte("Simple paragraph")},
{[]byte("\n \n \t <p> \t Whitespace\nHTML \n\t </p>\n\t"), []byte("Whitespace\nHTML")},
{[]byte("<p>Multiple</p><p>paragraphs</p>"), []byte("<p>Multiple</p><p>paragraphs</p>")},
{[]byte("<p>Nested<p>paragraphs</p></p>"), []byte("<p>Nested<p>paragraphs</p></p>")},
{[]byte("<p>Hello</p>\n<ul>\n<li>list1</li>\n<li>list2</li>\n</ul>"), []byte("<p>Hello</p>\n<ul>\n<li>list1</li>\n<li>list2</li>\n</ul>")},
// Issue #11698
{[]byte("<h2 id=`a`>b</h2>\n\n<p>c</p>"), []byte("<h2 id=`a`>b</h2>\n\n<p>c</p>")},
}
c := newTestContentSpec(nil)
for i, test := range tests {
output := c.TrimShortHTML(test.input, test.markup)
output := c.TrimShortHTML(test.input)
if !bytes.Equal(test.output, output) {
t.Errorf("Test %d failed. Expected %q got %q", i, test.output, output)
}
@@ -57,7 +54,7 @@ func BenchmarkTrimShortHTML(b *testing.B) {
c := newTestContentSpec(nil)
b.ResetTimer()
for i := 0; i < b.N; i++ {
c.TrimShortHTML([]byte("<p>Simple paragraph</p>"), "markdown")
c.TrimShortHTML([]byte("<p>Simple paragraph</p>"))
}
}
+3 -3
View File
@@ -35,9 +35,9 @@ func TestResolveMarkup(t *testing.T) {
{"md", "markdown"},
{"markdown", "markdown"},
{"mdown", "markdown"},
{"asciidocext", "asciidoc"},
{"adoc", "asciidoc"},
{"ad", "asciidoc"},
{"asciidocext", "asciidocext"},
{"adoc", "asciidocext"},
{"ad", "asciidocext"},
{"rst", "rst"},
{"pandoc", "pandoc"},
{"pdc", "pandoc"},
-6
View File
@@ -16,7 +16,6 @@ package hqt
import (
"errors"
"fmt"
"math"
"reflect"
"strings"
@@ -39,11 +38,6 @@ var IsSameType qt.Checker = &typeChecker{
argNames: []string{"got", "want"},
}
// IsSameFloat64 asserts that two float64 values are equal within a small delta.
var IsSameFloat64 = qt.CmpEquals(cmp.Comparer(func(a, b float64) bool {
return math.Abs(a-b) < 0.0001
}))
type argNames []string
func (a argNames) ArgNames() []string {
+49 -7
View File
@@ -29,13 +29,57 @@ const (
FilenameHugoStatsJSON = "hugo_stats.json"
)
func IsGoTmplExt(ext string) bool {
return ext == "gotmpl"
var (
// This should be the only list of valid extensions for content files.
contentFileExtensions = []string{
"html", "htm",
"mdown", "markdown", "md",
"asciidoc", "adoc", "ad",
"rest", "rst",
"org",
"pandoc", "pdc",
}
contentFileExtensionsSet map[string]bool
htmlFileExtensions = []string{
"html", "htm",
}
htmlFileExtensionsSet map[string]bool
)
func init() {
contentFileExtensionsSet = make(map[string]bool)
for _, ext := range contentFileExtensions {
contentFileExtensionsSet[ext] = true
}
htmlFileExtensionsSet = make(map[string]bool)
for _, ext := range htmlFileExtensions {
htmlFileExtensionsSet[ext] = true
}
}
// Supported data file extensions for _content.* files.
func IsContentDataExt(ext string) bool {
return IsGoTmplExt(ext)
func IsContentFile(filename string) bool {
return contentFileExtensionsSet[strings.TrimPrefix(filepath.Ext(filename), ".")]
}
func IsIndexContentFile(filename string) bool {
if !IsContentFile(filename) {
return false
}
base := filepath.Base(filename)
return strings.HasPrefix(base, "index.") || strings.HasPrefix(base, "_index.")
}
func IsHTML(ext string) bool {
return htmlFileExtensionsSet[ext]
}
func IsContentExt(ext string) bool {
return contentFileExtensionsSet[ext]
}
const (
@@ -49,8 +93,6 @@ const (
FolderResources = "resources"
FolderJSConfig = "_jsconfig" // Mounted below /assets with postcss.config.js etc.
NameContentData = "_content"
)
var (
+11
View File
@@ -14,11 +14,22 @@
package files
import (
"path/filepath"
"testing"
qt "github.com/frankban/quicktest"
)
func TestIsContentFile(t *testing.T) {
c := qt.New(t)
c.Assert(IsContentFile(filepath.FromSlash("my/file.md")), qt.Equals, true)
c.Assert(IsContentFile(filepath.FromSlash("my/file.ad")), qt.Equals, true)
c.Assert(IsContentFile(filepath.FromSlash("textfile.txt")), qt.Equals, false)
c.Assert(IsContentExt("md"), qt.Equals, true)
c.Assert(IsContentExt("json"), qt.Equals, false)
}
func TestComponentFolders(t *testing.T) {
c := qt.New(t)
+2 -8
View File
@@ -23,7 +23,6 @@ import (
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/media"
"github.com/spf13/afero"
)
@@ -51,8 +50,7 @@ type WalkwayConfig struct {
Root string
// The logger to use.
Logger loggers.Logger
PathParser *paths.PathParser
Logger loggers.Logger
// One or both of these may be pre-set.
Info FileMetaInfo // The start info.
@@ -74,10 +72,6 @@ func NewWalkway(cfg WalkwayConfig) *Walkway {
panic("fs must be set")
}
if cfg.PathParser == nil {
cfg.PathParser = media.DefaultPathParser
}
logger := cfg.Logger
if logger == nil {
logger = loggers.NewDefault()
@@ -167,7 +161,7 @@ func (w *Walkway) walk(path string, info FileMetaInfo, dirEntries []FileMetaInfo
dirEntries = DirEntriesToFileMetaInfos(fis)
for _, fi := range dirEntries {
if fi.Meta().PathInfo == nil {
fi.Meta().PathInfo = w.cfg.PathParser.Parse("", filepath.Join(pathRel, fi.Name()))
fi.Meta().PathInfo = paths.Parse("", filepath.Join(pathRel, fi.Name()))
}
}
+1 -50
View File
@@ -329,7 +329,7 @@ cascade:
counters := &buildCounters{}
b.Build(BuildCfg{testCounters: counters})
b.Assert(int(counters.contentRenderCounter.Load()), qt.Equals, 1)
b.Assert(int(counters.contentRenderCounter.Load()), qt.Equals, 2)
b.AssertFileContent("public/post/index.html", `Banner: post.jpg|Layout: postlayout|Type: posttype|Content: <p>content edit</p>`)
b.AssertFileContent("public/post/dir/p1/index.html", `Banner: post.jpg|Layout: postlayout|`)
@@ -672,55 +672,6 @@ S1|p1:|p2:p2|
})
}
func TestCascadeEditIssue12449(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
baseURL = "https://example.com"
disableKinds = ['sitemap','rss', 'home', 'taxonomy','term']
disableLiveReload = true
-- layouts/_default/list.html --
Title: {{ .Title }}|{{ .Content }}|cascadeparam: {{ .Params.cascadeparam }}|
-- layouts/_default/single.html --
Title: {{ .Title }}|{{ .Content }}|cascadeparam: {{ .Params.cascadeparam }}|
-- content/mysect/_index.md --
---
title: mysect
cascade:
description: descriptionvalue
params:
cascadeparam: cascadeparamvalue
---
mysect-content|
-- content/mysect/p1/index.md --
---
slug: p1
---
p1-content|
-- content/mysect/subsect/_index.md --
---
slug: subsect
---
subsect-content|
`
b := TestRunning(t, files)
// Make the cascade set the title.
b.EditFileReplaceAll("content/mysect/_index.md", "description: descriptionvalue", "title: cascadetitle").Build()
b.AssertFileContent("public/mysect/subsect/index.html", "Title: cascadetitle|")
// Edit cascade title.
b.EditFileReplaceAll("content/mysect/_index.md", "title: cascadetitle", "title: cascadetitle-edit").Build()
b.AssertFileContent("public/mysect/subsect/index.html", "Title: cascadetitle-edit|")
// Revert title change.
// The step below failed in #12449.
b.EditFileReplaceAll("content/mysect/_index.md", "title: cascadetitle-edit", "description: descriptionvalue").Build()
b.AssertFileContent("public/mysect/subsect/index.html", "Title: |")
}
// Issue 11977.
func TestCascadeExtensionInPath(t *testing.T) {
t.Parallel()
+1 -1
View File
@@ -1144,7 +1144,7 @@ Home.
enConfig := b.H.Sites[0].conf
m, _ := enConfig.MediaTypes.Config.GetByType("text/html")
b.Assert(m.Suffixes(), qt.DeepEquals, []string{"html", "htm"})
b.Assert(m.Suffixes(), qt.DeepEquals, []string{"html"})
svConfig := b.H.Sites[1].conf
f, _ := svConfig.OutputFormats.Config.GetByName("html")
+17 -180
View File
@@ -14,7 +14,6 @@
package hugolib
import (
"context"
"fmt"
"path"
"path/filepath"
@@ -24,13 +23,10 @@ import (
"github.com/bep/logg"
"github.com/gohugoio/hugo/common/hugio"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/hugolib/pagesfromdata"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/source"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/page/pagemeta"
"github.com/gohugoio/hugo/resources/resource"
"github.com/gohugoio/hugo/hugofs"
@@ -55,11 +51,9 @@ type contentMapConfig struct {
var _ contentNodeI = (*resourceSource)(nil)
type resourceSource struct {
langIndex int
path *paths.Path
opener hugio.OpenReadSeekCloser
fi hugofs.FileMetaInfo
rc *pagemeta.ResourceConfig
path *paths.Path
opener hugio.OpenReadSeekCloser
fi hugofs.FileMetaInfo
r resource.Resource
}
@@ -70,7 +64,11 @@ func (r resourceSource) clone() *resourceSource {
}
func (r *resourceSource) LangIndex() int {
return r.langIndex
if r.r != nil && r.isPage() {
return r.r.(*pageState).s.languagei
}
return r.fi.Meta().LangIndex
}
func (r *resourceSource) MarkStale() {
@@ -164,13 +162,12 @@ func (cfg contentMapConfig) getTaxonomyConfig(s string) (v viewName) {
return
}
func (m *pageMap) AddFi(fi hugofs.FileMetaInfo, buildConfig *BuildCfg) (pageCount uint64, resourceCount uint64, addErr error) {
func (m *pageMap) AddFi(fi hugofs.FileMetaInfo) error {
if fi.IsDir() {
return
return nil
}
insertResource := func(fim hugofs.FileMetaInfo) error {
resourceCount++
pi := fi.Meta().PathInfo
key := pi.Base()
tree := m.treeResources
@@ -202,9 +199,9 @@ func (m *pageMap) AddFi(fi hugofs.FileMetaInfo, buildConfig *BuildCfg) (pageCoun
}
key = pi.Base()
rs = &resourceSource{r: pageResource, langIndex: pageResource.s.languagei}
rs = &resourceSource{r: pageResource}
} else {
rs = &resourceSource{path: pi, opener: r, fi: fim, langIndex: fim.Meta().LangIndex}
rs = &resourceSource{path: pi, opener: r, fi: fim}
}
tree.InsertIntoValuesDimension(key, rs)
@@ -223,27 +220,14 @@ func (m *pageMap) AddFi(fi hugofs.FileMetaInfo, buildConfig *BuildCfg) (pageCoun
},
))
if err := insertResource(fi); err != nil {
addErr = err
return
return err
}
case paths.PathTypeContentData:
pc, rc, err := m.addPagesFromGoTmplFi(fi, buildConfig)
pageCount += pc
resourceCount += rc
if err != nil {
addErr = err
return
}
default:
m.s.Log.Trace(logg.StringFunc(
func() string {
return fmt.Sprintf("insert bundle: %q", fi.Meta().Filename)
},
))
pageCount++
// A content file.
p, pi, err := m.s.h.newPage(
&pageMeta{
@@ -253,164 +237,17 @@ func (m *pageMap) AddFi(fi hugofs.FileMetaInfo, buildConfig *BuildCfg) (pageCoun
},
)
if err != nil {
addErr = err
return
return err
}
if p == nil {
// Disabled page.
return
return nil
}
m.treePages.InsertIntoValuesDimensionWithLock(pi.Base(), p)
m.treePages.InsertWithLock(pi.Base(), p)
}
return
}
func (m *pageMap) addPagesFromGoTmplFi(fi hugofs.FileMetaInfo, buildConfig *BuildCfg) (pageCount uint64, resourceCount uint64, addErr error) {
meta := fi.Meta()
pi := meta.PathInfo
m.s.Log.Trace(logg.StringFunc(
func() string {
return fmt.Sprintf("insert pages from data file: %q", fi.Meta().Filename)
},
))
if !files.IsGoTmplExt(pi.Ext()) {
addErr = fmt.Errorf("unsupported data file extension %q", pi.Ext())
return
}
s := m.s.h.resolveSite(fi.Meta().Lang)
f := source.NewFileInfo(fi)
h := s.h
// Make sure the layouts are initialized.
if _, err := h.init.layouts.Do(context.Background()); err != nil {
addErr = err
return
}
contentAdapter := s.pageMap.treePagesFromTemplateAdapters.Get(pi.Base())
var rebuild bool
if contentAdapter != nil {
// Rebuild
contentAdapter = contentAdapter.CloneForGoTmpl(fi)
rebuild = true
} else {
contentAdapter = pagesfromdata.NewPagesFromTemplate(
pagesfromdata.PagesFromTemplateOptions{
GoTmplFi: fi,
Site: s,
DepsFromSite: func(s page.Site) pagesfromdata.PagesFromTemplateDeps {
ss := s.(*Site)
return pagesfromdata.PagesFromTemplateDeps{
TmplFinder: ss.TextTmpl(),
TmplExec: ss.Tmpl(),
}
},
DependencyManager: s.Conf.NewIdentityManager("pagesfromdata"),
Watching: s.Conf.Watching(),
HandlePage: func(pt *pagesfromdata.PagesFromTemplate, pc *pagemeta.PageConfig) error {
s := pt.Site.(*Site)
if err := pc.Compile(pt.GoTmplFi.Meta().PathInfo.Base(), true, "", s.Log, s.conf.MediaTypes.Config); err != nil {
return err
}
ps, pi, err := h.newPage(
&pageMeta{
f: f,
s: s,
pageMetaParams: &pageMetaParams{
pageConfig: pc,
},
},
)
if err != nil {
return err
}
if ps == nil {
// Disabled page.
return nil
}
u, n, replaced := s.pageMap.treePages.InsertIntoValuesDimensionWithLock(pi.Base(), ps)
if h.isRebuild() {
if replaced {
pt.AddChange(n.GetIdentity())
} else {
pt.AddChange(u.GetIdentity())
}
}
return nil
},
HandleResource: func(pt *pagesfromdata.PagesFromTemplate, rc *pagemeta.ResourceConfig) error {
s := pt.Site.(*Site)
if err := rc.Compile(
pt.GoTmplFi.Meta().PathInfo.Base(),
s.Conf.PathParser(),
s.conf.MediaTypes.Config,
); err != nil {
return err
}
rs := &resourceSource{path: rc.PathInfo, rc: rc, opener: nil, fi: nil, langIndex: s.languagei}
_, n, replaced := s.pageMap.treeResources.InsertIntoValuesDimensionWithLock(rc.PathInfo.Base(), rs)
if h.isRebuild() && replaced {
pt.AddChange(n.GetIdentity())
}
return nil
},
},
)
s.pageMap.treePagesFromTemplateAdapters.Insert(pi.Base(), contentAdapter)
}
handleBuildInfo := func(s *Site, bi pagesfromdata.BuildInfo) {
resourceCount += bi.NumResourcesAdded
pageCount += bi.NumPagesAdded
s.handleContentAdapterChanges(bi, buildConfig)
}
bi, err := contentAdapter.Execute(context.Background())
if err != nil {
addErr = err
return
}
handleBuildInfo(s, bi)
if !rebuild && bi.EnableAllLanguages {
// Clone and insert the adapter for the other sites.
for _, ss := range s.h.Sites {
if s == ss {
continue
}
clone := contentAdapter.CloneForSite(ss)
// Make sure it gets executed for the first time.
bi, err := clone.Execute(context.Background())
if err != nil {
addErr = err
return
}
handleBuildInfo(ss, bi)
// Insert into the correct language tree so it get rebuilt on changes.
ss.pageMap.treePagesFromTemplateAdapters.Insert(pi.Base(), clone)
}
}
return
return nil
}
// The home page is represented with the zero string.
+58 -146
View File
@@ -34,9 +34,7 @@ import (
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/hugolib/doctree"
"github.com/gohugoio/hugo/hugolib/pagesfromdata"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/output"
"github.com/gohugoio/hugo/resources"
"github.com/spf13/cast"
@@ -102,8 +100,6 @@ type pageMap struct {
cacheContentPlain *dynacache.Partition[string, *resources.StaleValue[contentPlainPlainWords]]
contentTableOfContents *dynacache.Partition[string, *resources.StaleValue[contentTableOfContents]]
contentDataFileSeenItems *maps.Cache[string, map[uint64]bool]
cfg contentMapConfig
}
@@ -126,10 +122,6 @@ type pageTrees struct {
// This tree contains all taxonomy entries, e.g "/tags/blue/page1"
treeTaxonomyEntries *doctree.TreeShiftTree[*weightedContentNode]
// Stores the state for _content.gotmpl files.
// Mostly releveant for rebuilds.
treePagesFromTemplateAdapters *doctree.TreeShiftTree[*pagesfromdata.PagesFromTemplate]
// A slice of the resource trees.
resourceTrees doctree.MutableTrees
}
@@ -230,7 +222,6 @@ func (t pageTrees) Shape(d, v int) *pageTrees {
t.treePages = t.treePages.Shape(d, v)
t.treeResources = t.treeResources.Shape(d, v)
t.treeTaxonomyEntries = t.treeTaxonomyEntries.Shape(d, v)
t.treePagesFromTemplateAdapters = t.treePagesFromTemplateAdapters.Shape(d, v)
t.createMutableTrees()
return &t
@@ -596,9 +587,9 @@ func (m *pageMap) getOrCreateResourcesForPage(ps *pageState) resource.Resources
sort.SliceStable(res, lessFunc)
if len(ps.m.pageConfig.ResourcesMeta) > 0 {
if len(ps.m.pageConfig.Resources) > 0 {
for i, r := range res {
res[i] = resources.CloneWithMetadataFromMapIfNeeded(ps.m.pageConfig.ResourcesMeta, r)
res[i] = resources.CloneWithMetadataIfNeeded(ps.m.pageConfig.Resources, r)
}
sort.SliceStable(res, lessFunc)
}
@@ -676,13 +667,12 @@ type contentNodeShifter struct {
numLanguages int
}
func (s *contentNodeShifter) Delete(n contentNodeI, dimension doctree.Dimension) (contentNodeI, bool, bool) {
func (s *contentNodeShifter) Delete(n contentNodeI, dimension doctree.Dimension) (bool, bool) {
lidx := dimension[0]
switch v := n.(type) {
case contentNodeIs:
deleted := v[lidx]
resource.MarkStale(deleted)
wasDeleted := deleted != nil
resource.MarkStale(v[lidx])
wasDeleted := v[lidx] != nil
v[lidx] = nil
isEmpty := true
for _, vv := range v {
@@ -691,11 +681,10 @@ func (s *contentNodeShifter) Delete(n contentNodeI, dimension doctree.Dimension)
break
}
}
return deleted, wasDeleted, isEmpty
return wasDeleted, isEmpty
case resourceSources:
deleted := v[lidx]
resource.MarkStale(deleted)
wasDeleted := deleted != nil
resource.MarkStale(v[lidx])
wasDeleted := v[lidx] != nil
v[lidx] = nil
isEmpty := true
for _, vv := range v {
@@ -704,19 +693,19 @@ func (s *contentNodeShifter) Delete(n contentNodeI, dimension doctree.Dimension)
break
}
}
return deleted, wasDeleted, isEmpty
return wasDeleted, isEmpty
case *resourceSource:
if lidx != v.LangIndex() {
return nil, false, false
return false, false
}
resource.MarkStale(v)
return v, true, true
return true, true
case *pageState:
if lidx != v.s.languagei {
return nil, false, false
return false, false
}
resource.MarkStale(v)
return v, true, true
return true, true
default:
panic(fmt.Sprintf("unknown type %T", n))
}
@@ -789,7 +778,7 @@ func (s *contentNodeShifter) ForEeachInDimension(n contentNodeI, d int, f func(c
}
}
func (s *contentNodeShifter) InsertInto(old, new contentNodeI, dimension doctree.Dimension) (contentNodeI, contentNodeI, bool) {
func (s *contentNodeShifter) InsertInto(old, new contentNodeI, dimension doctree.Dimension) contentNodeI {
langi := dimension[doctree.DimensionLanguage.Index()]
switch vv := old.(type) {
case *pageState:
@@ -798,39 +787,37 @@ func (s *contentNodeShifter) InsertInto(old, new contentNodeI, dimension doctree
panic(fmt.Sprintf("unknown type %T", new))
}
if vv.s.languagei == newp.s.languagei && newp.s.languagei == langi {
return new, vv, true
return new
}
is := make(contentNodeIs, s.numLanguages)
is[vv.s.languagei] = old
is[langi] = new
return is, old, false
return is
case contentNodeIs:
oldv := vv[langi]
vv[langi] = new
return vv, oldv, oldv != nil
return vv
case resourceSources:
oldv := vv[langi]
vv[langi] = new.(*resourceSource)
return vv, oldv, oldv != nil
return vv
case *resourceSource:
newp, ok := new.(*resourceSource)
if !ok {
panic(fmt.Sprintf("unknown type %T", new))
}
if vv.LangIndex() == newp.LangIndex() && newp.LangIndex() == langi {
return new, vv, true
return new
}
rs := make(resourceSources, s.numLanguages)
rs[vv.LangIndex()] = vv
rs[langi] = newp
return rs, vv, false
return rs
default:
panic(fmt.Sprintf("unknown type %T", old))
}
}
func (s *contentNodeShifter) Insert(old, new contentNodeI) (contentNodeI, contentNodeI, bool) {
func (s *contentNodeShifter) Insert(old, new contentNodeI) contentNodeI {
switch vv := old.(type) {
case *pageState:
newp, ok := new.(*pageState)
@@ -838,52 +825,40 @@ func (s *contentNodeShifter) Insert(old, new contentNodeI) (contentNodeI, conten
panic(fmt.Sprintf("unknown type %T", new))
}
if vv.s.languagei == newp.s.languagei {
if newp != old {
resource.MarkStale(old)
}
return new, vv, true
return new
}
is := make(contentNodeIs, s.numLanguages)
is[newp.s.languagei] = new
is[vv.s.languagei] = old
return is, old, false
return is
case contentNodeIs:
newp, ok := new.(*pageState)
if !ok {
panic(fmt.Sprintf("unknown type %T", new))
}
oldp := vv[newp.s.languagei]
if oldp != newp {
resource.MarkStale(oldp)
}
resource.MarkStale(vv[newp.s.languagei])
vv[newp.s.languagei] = new
return vv, oldp, oldp != nil
return vv
case *resourceSource:
newp, ok := new.(*resourceSource)
if !ok {
panic(fmt.Sprintf("unknown type %T", new))
}
if vv.LangIndex() == newp.LangIndex() {
if vv != newp {
resource.MarkStale(vv)
}
return new, vv, true
return new
}
rs := make(resourceSources, s.numLanguages)
rs[newp.LangIndex()] = newp
rs[vv.LangIndex()] = vv
return rs, vv, false
return rs
case resourceSources:
newp, ok := new.(*resourceSource)
if !ok {
panic(fmt.Sprintf("unknown type %T", new))
}
oldp := vv[newp.LangIndex()]
if oldp != newp {
resource.MarkStale(oldp)
}
resource.MarkStale(vv[newp.LangIndex()])
vv[newp.LangIndex()] = newp
return vv, oldp, oldp != nil
return vv
default:
panic(fmt.Sprintf("unknown type %T", old))
}
@@ -903,8 +878,6 @@ func newPageMap(i int, s *Site, mcache *dynacache.Cache, pageTrees *pageTrees) *
cacheContentPlain: dynacache.GetOrCreatePartition[string, *resources.StaleValue[contentPlainPlainWords]](mcache, fmt.Sprintf("/cont/pla/%d", i), dynacache.OptionsPartition{Weight: 70, ClearWhen: dynacache.ClearOnChange}),
contentTableOfContents: dynacache.GetOrCreatePartition[string, *resources.StaleValue[contentTableOfContents]](mcache, fmt.Sprintf("/cont/toc/%d", i), dynacache.OptionsPartition{Weight: 70, ClearWhen: dynacache.ClearOnChange}),
contentDataFileSeenItems: maps.NewCache[string, map[uint64]bool](),
cfg: contentMapConfig{
lang: s.Lang(),
taxonomyConfig: taxonomiesConfig.Values(),
@@ -975,6 +948,8 @@ type contentTreeReverseIndexMap struct {
type sitePagesAssembler struct {
*Site
watching bool
incomingChanges *whatChanged
assembleChanges *whatChanged
ctx context.Context
}
@@ -1079,7 +1054,7 @@ func (h *HugoSites) resolveAndClearStateForIdentities(
)
for _, id := range changes {
if staler, ok := id.(resource.Staler); ok {
if staler, ok := id.(resource.Staler); ok && !staler.IsStale() {
var msgDetail string
if p, ok := id.(*pageState); ok && p.File() != nil {
msgDetail = fmt.Sprintf(" (%s)", p.File().Filename())
@@ -1093,7 +1068,6 @@ func (h *HugoSites) resolveAndClearStateForIdentities(
// 1. Handle the cache busters first, as those may produce identities for the page reset step.
// 2. Then reset the page outputs, which may mark some resources as stale.
// 3. Then GC the cache.
// TOOD1
if cachebuster != nil {
if err := loggers.TimeTrackfn(func() (logg.LevelLogger, error) {
ll := l.WithField("substep", "gc dynacache cachebuster")
@@ -1110,7 +1084,7 @@ func (h *HugoSites) resolveAndClearStateForIdentities(
return b
}
h.MemCache.ClearMatching(nil, shouldDelete)
h.MemCache.ClearMatching(shouldDelete)
return ll, nil
}); err != nil {
@@ -1139,33 +1113,6 @@ func (h *HugoSites) resolveAndClearStateForIdentities(
}
changes = changes[:n]
if h.pageTrees.treePagesFromTemplateAdapters.LenRaw() > 0 {
if err := loggers.TimeTrackfn(func() (logg.LevelLogger, error) {
ll := l.WithField("substep", "resolve content adapter change set").WithField("changes", len(changes))
checkedCount := 0
matchCount := 0
depsFinder := identity.NewFinder(identity.FinderConfig{})
h.pageTrees.treePagesFromTemplateAdapters.WalkPrefixRaw(doctree.LockTypeRead, "",
func(s string, n *pagesfromdata.PagesFromTemplate) (bool, error) {
for _, id := range changes {
checkedCount++
if r := depsFinder.Contains(id, n.DependencyManager, 2); r > identity.FinderNotFound {
n.MarkStale()
matchCount++
break
}
}
return false, nil
})
ll = ll.WithField("checked", checkedCount).WithField("matches", matchCount)
return ll, nil
}); err != nil {
return err
}
}
if err := loggers.TimeTrackfn(func() (logg.LevelLogger, error) {
// changesLeft: The IDs that the pages is dependent on.
// changesRight: The IDs that the pages depend on.
@@ -1310,7 +1257,6 @@ func (sa *sitePagesAssembler) applyAggregates() error {
rw := pw.Extend()
rw.Tree = sa.pageMap.treeResources
sa.lastmod = time.Time{}
rebuild := sa.s.h.isRebuild()
pw.Handle = func(keyPage string, n contentNodeI, match doctree.DimensionFlag) (bool, error) {
pageBundle := n.(*pageState)
@@ -1331,7 +1277,7 @@ func (sa *sitePagesAssembler) applyAggregates() error {
// Home page gets it's cascade from the site config.
cascade = sa.conf.Cascade.Config
if pageBundle.m.pageConfig.CascadeCompiled == nil {
if pageBundle.m.pageConfig.Cascade == nil {
// Pass the site cascade downwards.
pw.WalkContext.Data().Insert(keyPage, cascade)
}
@@ -1342,20 +1288,18 @@ func (sa *sitePagesAssembler) applyAggregates() error {
}
}
if rebuild {
if (pageBundle.IsHome() || pageBundle.IsSection()) && pageBundle.m.setMetaPostCount > 0 {
oldDates := pageBundle.m.pageConfig.Dates
if (pageBundle.IsHome() || pageBundle.IsSection()) && pageBundle.m.setMetaPostCount > 0 {
oldDates := pageBundle.m.pageConfig.Dates
// We need to wait until after the walk to determine if any of the dates have changed.
pw.WalkContext.AddPostHook(
func() error {
if oldDates != pageBundle.m.pageConfig.Dates {
sa.assembleChanges.Add(pageBundle)
}
return nil
},
)
}
// We need to wait until after the walk to determine if any of the dates have changed.
pw.WalkContext.AddPostHook(
func() error {
if oldDates != pageBundle.m.pageConfig.Dates {
sa.assembleChanges.Add(pageBundle)
}
return nil
},
)
}
// Combine the cascade map with front matter.
@@ -1365,15 +1309,15 @@ func (sa *sitePagesAssembler) applyAggregates() error {
// We receive cascade values from above. If this leads to a change compared
// to the previous value, we need to mark the page and its dependencies as changed.
if rebuild && pageBundle.m.setMetaPostCascadeChanged {
if pageBundle.m.setMetaPostCascadeChanged {
sa.assembleChanges.Add(pageBundle)
}
const eventName = "dates"
if n.isContentNodeBranch() {
if pageBundle.m.pageConfig.CascadeCompiled != nil {
if pageBundle.m.pageConfig.Cascade != nil {
// Pass it down.
pw.WalkContext.Data().Insert(keyPage, pageBundle.m.pageConfig.CascadeCompiled)
pw.WalkContext.Data().Insert(keyPage, pageBundle.m.pageConfig.Cascade)
}
wasZeroDates := pageBundle.m.pageConfig.Dates.IsAllDatesZero()
@@ -1474,9 +1418,9 @@ func (sa *sitePagesAssembler) applyAggregatesToTaxonomiesAndTerms() error {
p := n.(*pageState)
if p.Kind() != kinds.KindTerm {
// The other kinds were handled in applyAggregates.
if p.m.pageConfig.CascadeCompiled != nil {
if p.m.pageConfig.Cascade != nil {
// Pass it down.
pw.WalkContext.Data().Insert(s, p.m.pageConfig.CascadeCompiled)
pw.WalkContext.Data().Insert(s, p.m.pageConfig.Cascade)
}
}
@@ -1597,7 +1541,7 @@ func (sa *sitePagesAssembler) assembleTermsAndTranslations() error {
singular: viewName.singular,
s: sa.Site,
pathInfo: pi,
pageMetaParams: &pageMetaParams{
pageMetaParams: pageMetaParams{
pageConfig: &pagemeta.PageConfig{
Kind: kinds.KindTerm,
},
@@ -1659,13 +1603,13 @@ func (sa *sitePagesAssembler) assembleResources() error {
targetPaths := ps.targetPaths()
baseTarget := targetPaths.SubResourceBaseTarget
duplicateResourceFiles := true
if ps.m.pageConfig.ContentMediaType.IsMarkdown() {
if ps.s.ContentSpec.Converters.IsGoldmark(ps.m.pageConfig.Markup) {
duplicateResourceFiles = ps.s.ContentSpec.Converters.GetMarkupConfig().Goldmark.DuplicateResourceFiles
}
duplicateResourceFiles = duplicateResourceFiles || ps.s.Conf.IsMultihost()
err := sa.pageMap.forEachResourceInPage(
sa.pageMap.forEachResourceInPage(
ps, lockType,
!duplicateResourceFiles,
func(resourceKey string, n contentNodeI, match doctree.DimensionFlag) (bool, error) {
@@ -1691,23 +1635,6 @@ func (sa *sitePagesAssembler) assembleResources() error {
}
if rs.rc != nil && rs.rc.Content.IsResourceValue() {
if rs.rc.Name == "" {
rs.rc.Name = relPathOriginal
}
r, err := ps.m.s.ResourceSpec.NewResourceWrapperFromResourceConfig(rs.rc)
if err != nil {
return false, err
}
rs.r = r
return false, nil
}
var mt media.Type
if rs.rc != nil {
mt = rs.rc.ContentMediaType
}
rd := resources.ResourceSourceDescriptor{
OpenReadSeekCloser: rs.opener,
Path: rs.path,
@@ -1718,23 +1645,8 @@ func (sa *sitePagesAssembler) assembleResources() error {
BasePathTargetPath: baseTarget,
NameNormalized: relPath,
NameOriginal: relPathOriginal,
MediaType: mt,
LazyPublish: !ps.m.pageConfig.Build.PublishResources,
}
if rs.rc != nil {
rc := rs.rc
rd.OpenReadSeekCloser = rc.Content.ValueAsOpenReadSeekCloser()
if rc.Name != "" {
rd.NameNormalized = rc.Name
rd.NameOriginal = rc.Name
}
if rc.Title != "" {
rd.Title = rc.Title
}
rd.Params = rc.Params
}
r, err := ps.m.s.ResourceSpec.NewResource(rd)
if err != nil {
return false, err
@@ -1744,7 +1656,7 @@ func (sa *sitePagesAssembler) assembleResources() error {
},
)
return false, err
return false, nil
},
}
@@ -1851,7 +1763,7 @@ func (sa *sitePagesAssembler) addStandalonePages() error {
m := &pageMeta{
s: s,
pathInfo: s.Conf.PathParser().Parse(files.ComponentFolderContent, key+f.MediaType.FirstSuffix.FullSuffix),
pageMetaParams: &pageMetaParams{
pageMetaParams: pageMetaParams{
pageConfig: &pagemeta.PageConfig{
Kind: kind,
},
@@ -1969,7 +1881,7 @@ func (sa *sitePagesAssembler) addMissingRootSections() error {
m := &pageMeta{
s: sa.Site,
pathInfo: p,
pageMetaParams: &pageMetaParams{
pageMetaParams: pageMetaParams{
pageConfig: &pagemeta.PageConfig{
Kind: kinds.KindHome,
},
@@ -1979,7 +1891,7 @@ func (sa *sitePagesAssembler) addMissingRootSections() error {
if err != nil {
return err
}
w.Tree.InsertIntoValuesDimensionWithLock(p.Base(), n)
w.Tree.InsertWithLock(p.Base(), n)
sa.home = n
}
@@ -2002,7 +1914,7 @@ func (sa *sitePagesAssembler) addMissingTaxonomies() error {
m := &pageMeta{
s: sa.Site,
pathInfo: sa.Conf.PathParser().Parse(files.ComponentFolderContent, key+"/_index.md"),
pageMetaParams: &pageMetaParams{
pageMetaParams: pageMetaParams{
pageConfig: &pagemeta.PageConfig{
Kind: kinds.KindTaxonomy,
},
-50
View File
@@ -14,7 +14,6 @@
package hugolib
import (
"fmt"
"strings"
"testing"
)
@@ -242,52 +241,3 @@ iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAA
"p1|<p><a href=\"p2\">P2</a>", "<img src=\"pixel.png\" alt=\"Pixel\">")
})
}
func TestRenderHooksDefaultEscape(t *testing.T) {
files := `
-- hugo.toml --
[markup.goldmark.renderHooks]
[markup.goldmark.renderHooks.image]
enableDefault = ENABLE
[markup.goldmark.renderHooks.link]
enableDefault = ENABLE
[markup.goldmark.parser]
wrapStandAloneImageWithinParagraph = false
[markup.goldmark.parser.attribute]
block = true
title = true
-- content/_index.md --
---
title: "Home"
---
Link: [text-"<>&](/destination-"<> 'title-"<>&')
Image: ![alt-"<>&](/destination-"<> 'title-"<>&')
{class="><script>alert()</script>" id="baz"}
-- layouts/index.html --
{{ .Content }}
`
for _, enabled := range []bool{true, false} {
enabled := enabled
t.Run(fmt.Sprint(enabled), func(t *testing.T) {
t.Parallel()
b := Test(t, strings.ReplaceAll(files, "ENABLE", fmt.Sprint(enabled)))
// The escaping is slightly different between the two.
if enabled {
b.AssertFileContent("public/index.html",
"Link: <a href=\"/destination-%22%3C%3E\" title=\"title-&#34;&lt;&gt;&amp;\">text-&quot;&lt;&gt;&amp;</a>",
"img alt=\"alt-&quot;&lt;&gt;&amp;\" src=\"/destination-%22%3C%3E\" title=\"title-&#34;&lt;&gt;&amp;\">",
"&gt;&lt;script&gt;",
)
} else {
b.AssertFileContent("public/index.html",
"Link: <a href=\"/destination-%22%3C%3E\" title=\"title-&quot;&lt;&gt;&amp;\">text-&quot;&lt;&gt;&amp;</a>",
"Image: <img src=\"/destination-%22%3C%3E\" alt=\"alt-&quot;&lt;&gt;&amp;\" title=\"title-&quot;&lt;&gt;&amp;\">",
)
}
})
}
}
+7 -7
View File
@@ -173,7 +173,7 @@ func TestTreeInsert(t *testing.T) {
c.Assert(tree.Get("/notfound"), qt.IsNil)
ab2 := &testValue{ID: "/a/b", Lang: 0}
v, _, ok := tree.InsertIntoValuesDimension("/a/b", ab2)
v, ok := tree.InsertIntoValuesDimension("/a/b", ab2)
c.Assert(ok, qt.IsTrue)
c.Assert(v, qt.DeepEquals, ab2)
@@ -239,16 +239,16 @@ func (s *testShifter) ForEeachInDimension(n *testValue, d int, f func(n *testVal
f(n)
}
func (s *testShifter) Insert(old, new *testValue) (*testValue, *testValue, bool) {
return new, old, true
func (s *testShifter) Insert(old, new *testValue) *testValue {
return new
}
func (s *testShifter) InsertInto(old, new *testValue, dimension doctree.Dimension) (*testValue, *testValue, bool) {
return new, old, true
func (s *testShifter) InsertInto(old, new *testValue, dimension doctree.Dimension) *testValue {
return new
}
func (s *testShifter) Delete(n *testValue, dimension doctree.Dimension) (*testValue, bool, bool) {
return nil, true, true
func (s *testShifter) Delete(n *testValue, dimension doctree.Dimension) (bool, bool) {
return true, true
}
func (s *testShifter) Shift(n *testValue, dimension doctree.Dimension, exact bool) (*testValue, bool, doctree.DimensionFlag) {
+18 -37
View File
@@ -38,18 +38,16 @@ type (
// Insert inserts new into the tree into the dimension it provides.
// It may replace old.
// It returns the updated and existing T
// and a bool indicating if an existing record is updated.
Insert(old, new T) (T, T, bool)
// It returns a T (can be the same as old).
Insert(old, new T) T
// Insert inserts new into the given dimension.
// It may replace old.
// It returns the updated and existing T
// and a bool indicating if an existing record is updated.
InsertInto(old, new T, dimension Dimension) (T, T, bool)
// It returns a T (can be the same as old).
InsertInto(old, new T, dimension Dimension) T
// Delete deletes T from the given dimension and returns the deleted T and whether the dimension was deleted and if it's empty after the delete.
Delete(v T, dimension Dimension) (T, bool, bool)
// Delete deletes T from the given dimension and returns whether the dimension was deleted and if it's empty after the delete.
Delete(v T, dimension Dimension) (bool, bool)
// Shift shifts T into the given dimension
// and returns the shifted T and a bool indicating if the shift was successful and
@@ -83,11 +81,7 @@ func New[T any](cfg Config[T]) *NodeShiftTree[T] {
}
}
func (r *NodeShiftTree[T]) Delete(key string) (T, bool) {
return r.delete(key)
}
func (r *NodeShiftTree[T]) DeleteRaw(key string) {
func (r *NodeShiftTree[T]) Delete(key string) {
r.delete(key)
}
@@ -109,24 +103,23 @@ func (r *NodeShiftTree[T]) DeletePrefix(prefix string) int {
return false
})
for _, key := range keys {
if _, ok := r.delete(key); ok {
if ok := r.delete(key); ok {
count++
}
}
return count
}
func (r *NodeShiftTree[T]) delete(key string) (T, bool) {
func (r *NodeShiftTree[T]) delete(key string) bool {
var wasDeleted bool
var deleted T
if v, ok := r.tree.Get(key); ok {
var isEmpty bool
deleted, wasDeleted, isEmpty = r.shifter.Delete(v.(T), r.dims)
wasDeleted, isEmpty = r.shifter.Delete(v.(T), r.dims)
if isEmpty {
r.tree.Delete(key)
}
}
return deleted, wasDeleted
return wasDeleted
}
func (t *NodeShiftTree[T]) DeletePrefixAll(prefix string) int {
@@ -148,33 +141,22 @@ func (t *NodeShiftTree[T]) Increment(d int) *NodeShiftTree[T] {
return t.Shape(d, t.dims[d]+1)
}
func (r *NodeShiftTree[T]) InsertIntoCurrentDimension(s string, v T) (T, T, bool) {
func (r *NodeShiftTree[T]) InsertIntoCurrentDimension(s string, v T) (T, bool) {
s = mustValidateKey(cleanKey(s))
var (
updated bool
existing T
)
if vv, ok := r.tree.Get(s); ok {
v, existing, updated = r.shifter.InsertInto(vv.(T), v, r.dims)
v = r.shifter.InsertInto(vv.(T), v, r.dims)
}
r.tree.Insert(s, v)
return v, existing, updated
return v, true
}
// InsertIntoValuesDimension inserts v into the tree at the given key and the
// dimension defined by the value.
// It returns the updated and existing T and a bool indicating if an existing record is updated.
func (r *NodeShiftTree[T]) InsertIntoValuesDimension(s string, v T) (T, T, bool) {
func (r *NodeShiftTree[T]) InsertIntoValuesDimension(s string, v T) (T, bool) {
s = mustValidateKey(cleanKey(s))
var (
updated bool
existing T
)
if vv, ok := r.tree.Get(s); ok {
v, existing, updated = r.shifter.Insert(vv.(T), v)
v = r.shifter.Insert(vv.(T), v)
}
r.tree.Insert(s, v)
return v, existing, updated
return v, true
}
func (r *NodeShiftTree[T]) InsertRawWithLock(s string, v any) (any, bool) {
@@ -183,8 +165,7 @@ func (r *NodeShiftTree[T]) InsertRawWithLock(s string, v any) (any, bool) {
return r.tree.Insert(s, v)
}
// It returns the updated and existing T and a bool indicating if an existing record is updated.
func (r *NodeShiftTree[T]) InsertIntoValuesDimensionWithLock(s string, v T) (T, T, bool) {
func (r *NodeShiftTree[T]) InsertWithLock(s string, v T) (T, bool) {
r.mu.Lock()
defer r.mu.Unlock()
return r.InsertIntoValuesDimension(s, v)
+5 -12
View File
@@ -28,12 +28,12 @@ type Tree[T any] interface {
}
// NewSimpleTree creates a new SimpleTree.
func NewSimpleTree[T comparable]() *SimpleTree[T] {
func NewSimpleTree[T any]() *SimpleTree[T] {
return &SimpleTree[T]{tree: radix.New()}
}
// SimpleTree is a thread safe radix tree that holds T.
type SimpleTree[T comparable] struct {
type SimpleTree[T any] struct {
mu sync.RWMutex
tree *radix.Tree
zero T
@@ -67,23 +67,16 @@ func (tree *SimpleTree[T]) Insert(s string, v T) T {
return v
}
func (tree *SimpleTree[T]) Lock(lockType LockType) func() {
func (tree *SimpleTree[T]) WalkPrefix(lockType LockType, s string, f func(s string, v T) (bool, error)) error {
switch lockType {
case LockTypeNone:
return func() {}
case LockTypeRead:
tree.mu.RLock()
return tree.mu.RUnlock
defer tree.mu.RUnlock()
case LockTypeWrite:
tree.mu.Lock()
return tree.mu.Unlock
defer tree.mu.Unlock()
}
return func() {}
}
func (tree *SimpleTree[T]) WalkPrefix(lockType LockType, s string, f func(s string, v T) (bool, error)) error {
commit := tree.Lock(lockType)
defer commit()
var err error
tree.tree.WalkPrefix(s, func(s string, v any) bool {
var b bool
+3 -3
View File
@@ -113,7 +113,7 @@ type LockType int
// MutableTree is a tree that can be modified.
type MutableTree interface {
DeleteRaw(key string)
Delete(key string)
DeleteAll(key string)
DeletePrefix(prefix string) int
DeletePrefixAll(prefix string) int
@@ -140,9 +140,9 @@ var _ MutableTree = MutableTrees(nil)
type MutableTrees []MutableTree
func (t MutableTrees) DeleteRaw(key string) {
func (t MutableTrees) Delete(key string) {
for _, tree := range t {
tree.DeleteRaw(key)
tree.Delete(key)
}
}
+24 -37
View File
@@ -15,21 +15,18 @@ package doctree
var _ Tree[string] = (*TreeShiftTree[string])(nil)
type TreeShiftTree[T comparable] struct {
type TreeShiftTree[T any] struct {
// This tree is shiftable in one dimension.
d int
// The value of the current dimension.
v int
// The zero value of T.
zero T
// Will be of length equal to the length of the dimension.
trees []*SimpleTree[T]
}
func NewTreeShiftTree[T comparable](d, length int) *TreeShiftTree[T] {
func NewTreeShiftTree[T any](d, length int) *TreeShiftTree[T] {
if length <= 0 {
panic("length must be > 0")
}
@@ -55,17 +52,6 @@ func (t *TreeShiftTree[T]) Get(s string) T {
return t.trees[t.v].Get(s)
}
func (t *TreeShiftTree[T]) DeleteAllFunc(s string, f func(s string, v T) bool) {
for _, tt := range t.trees {
if v := tt.Get(s); v != t.zero {
if f(s, v) {
// Delete.
tt.tree.Delete(s)
}
}
}
}
func (t *TreeShiftTree[T]) LongestPrefix(s string) (string, T) {
return t.trees[t.v].LongestPrefix(s)
}
@@ -74,31 +60,10 @@ func (t *TreeShiftTree[T]) Insert(s string, v T) T {
return t.trees[t.v].Insert(s, v)
}
func (t *TreeShiftTree[T]) Lock(lockType LockType) func() {
return t.trees[t.v].Lock(lockType)
}
func (t *TreeShiftTree[T]) WalkPrefix(lockType LockType, s string, f func(s string, v T) (bool, error)) error {
return t.trees[t.v].WalkPrefix(lockType, s, f)
}
func (t *TreeShiftTree[T]) WalkPrefixRaw(lockType LockType, s string, f func(s string, v T) (bool, error)) error {
for _, tt := range t.trees {
if err := tt.WalkPrefix(lockType, s, f); err != nil {
return err
}
}
return nil
}
func (t *TreeShiftTree[T]) LenRaw() int {
var count int
for _, tt := range t.trees {
count += tt.tree.Len()
}
return count
}
func (t *TreeShiftTree[T]) Delete(key string) {
for _, tt := range t.trees {
tt.tree.Delete(key)
@@ -112,3 +77,25 @@ func (t *TreeShiftTree[T]) DeletePrefix(prefix string) int {
}
return count
}
func (t *TreeShiftTree[T]) Lock(writable bool) (commit func()) {
if writable {
for _, tt := range t.trees {
tt.mu.Lock()
}
return func() {
for _, tt := range t.trees {
tt.mu.Unlock()
}
}
}
for _, tt := range t.trees {
tt.mu.RLock()
}
return func() {
for _, tt := range t.trees {
tt.mu.RUnlock()
}
}
}
-19
View File
@@ -111,24 +111,6 @@ func (h *HugoSites) ShouldSkipFileChangeEvent(ev fsnotify.Event) bool {
return h.skipRebuildForFilenames[ev.Name]
}
func (h *HugoSites) isRebuild() bool {
return h.buildCounter.Load() > 0
}
func (h *HugoSites) resolveSite(lang string) *Site {
if lang == "" {
lang = h.Conf.DefaultContentLanguage()
}
for _, s := range h.Sites {
if s.Lang() == lang {
return s
}
}
return nil
}
// Only used in tests.
type buildCounters struct {
contentRenderCounter atomic.Uint64
@@ -497,7 +479,6 @@ func (h *HugoSites) loadData() error {
hugofs.WalkwayConfig{
Fs: h.PathSpec.BaseFs.Data.Fs,
IgnoreFile: h.SourceSpec.IgnoreFile,
PathParser: h.Conf.PathParser(),
WalkFn: func(path string, fi hugofs.FileMetaInfo) error {
if fi.IsDir() {
return nil
+45 -116
View File
@@ -26,12 +26,11 @@ import (
"time"
"github.com/bep/logg"
"github.com/gohugoio/hugo/cache/dynacache"
"github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/hugofs/glob"
"github.com/gohugoio/hugo/hugolib/doctree"
"github.com/gohugoio/hugo/hugolib/pagesfromdata"
"github.com/gohugoio/hugo/hugolib/segments"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/output"
@@ -43,7 +42,6 @@ import (
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/para"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/common/rungroup"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/page/siteidentities"
@@ -99,10 +97,6 @@ func (h *HugoSites) Build(config BuildCfg, events ...fsnotify.Event) error {
close(to)
}(errCollector, errs)
for _, s := range h.Sites {
s.state = siteStateInit
}
if h.Metrics != nil {
h.Metrics.Reset()
}
@@ -116,7 +110,7 @@ func (h *HugoSites) Build(config BuildCfg, events ...fsnotify.Event) error {
conf := &config
if conf.whatChanged == nil {
// Assume everything has changed
conf.whatChanged = &whatChanged{needsPagesAssembly: true}
conf.whatChanged = &whatChanged{contentChanged: true}
}
var prepareErr error
@@ -160,10 +154,6 @@ func (h *HugoSites) Build(config BuildCfg, events ...fsnotify.Event) error {
}
}
for _, s := range h.Sites {
s.state = siteStateReady
}
if prepareErr == nil {
if err := h.render(infol, conf); err != nil {
h.SendError(fmt.Errorf("render: %w", err))
@@ -224,7 +214,7 @@ func (h *HugoSites) initRebuild(config *BuildCfg) error {
})
for _, s := range h.Sites {
s.resetBuildState(config.whatChanged.needsPagesAssembly)
s.resetBuildState(config.whatChanged.contentChanged)
}
h.reset(config)
@@ -243,7 +233,7 @@ func (h *HugoSites) process(ctx context.Context, l logg.LevelLogger, config *Bui
// This is a rebuild
return h.processPartial(ctx, l, config, init, events)
}
return h.processFull(ctx, l, config)
return h.processFull(ctx, l, *config)
}
// assemble creates missing sections, applies aggregate values (e.g. dates, cascading params),
@@ -252,24 +242,22 @@ func (h *HugoSites) assemble(ctx context.Context, l logg.LevelLogger, bcfg *Buil
l = l.WithField("step", "assemble")
defer loggers.TimeTrackf(l, time.Now(), nil, "")
if !bcfg.whatChanged.needsPagesAssembly {
changes := bcfg.whatChanged.Drain()
if len(changes) > 0 {
if err := h.resolveAndClearStateForIdentities(ctx, l, nil, changes); err != nil {
return err
}
}
if !bcfg.whatChanged.contentChanged {
return nil
}
h.translationKeyPages.Reset()
assemblers := make([]*sitePagesAssembler, len(h.Sites))
// Changes detected during assembly (e.g. aggregate date changes)
assembleChanges := &whatChanged{
identitySet: make(map[identity.Identity]bool),
}
for i, s := range h.Sites {
assemblers[i] = &sitePagesAssembler{
Site: s,
assembleChanges: bcfg.whatChanged,
watching: s.watching(),
incomingChanges: bcfg.whatChanged,
assembleChanges: assembleChanges,
ctx: ctx,
}
}
@@ -285,9 +273,9 @@ func (h *HugoSites) assemble(ctx context.Context, l logg.LevelLogger, bcfg *Buil
return err
}
changes := bcfg.whatChanged.Drain()
changes := assembleChanges.Changes()
// Changes from the assemble step (e.g. lastMod, cascade) needs a re-calculation
// Changes from the assemble step (e.g. lastMod, cascase) needs a re-calculation
// of what needs to be re-built.
if len(changes) > 0 {
if err := h.resolveAndClearStateForIdentities(ctx, l, nil, changes); err != nil {
@@ -608,10 +596,6 @@ type pathChange struct {
isDir bool
}
func (p pathChange) isStructuralChange() bool {
return p.delete || p.isDir
}
// processPartial prepares the Sites' sources for a partial rebuild.
func (h *HugoSites) processPartial(ctx context.Context, l logg.LevelLogger, config *BuildCfg, init func(config *BuildCfg) error, events []fsnotify.Event) error {
h.Log.Trace(logg.StringFunc(func() string {
@@ -626,16 +610,16 @@ func (h *HugoSites) processPartial(ctx context.Context, l logg.LevelLogger, conf
// For a list of events for the different OSes, see the test output in https://github.com/bep/fsnotifyeventlister/.
events = h.fileEventsFilter(events)
events = h.fileEventsTrim(events)
events = h.fileEventsTranslate(events)
eventInfos := h.fileEventsApplyInfo(events)
logger := h.Log
var (
tmplAdded bool
tmplChanged bool
i18nChanged bool
needsPagesAssemble bool
tmplAdded bool
tmplChanged bool
i18nChanged bool
contentChanged bool
)
changedPaths := struct {
@@ -709,33 +693,11 @@ func (h *HugoSites) processPartial(ctx context.Context, l logg.LevelLogger, conf
switch pathInfo.Component() {
case files.ComponentFolderContent:
logger.Println("Source changed", pathInfo.Path())
isContentDataFile := pathInfo.IsContentData()
if !isContentDataFile {
if ids := h.pageTrees.collectAndMarkStaleIdentities(pathInfo); len(ids) > 0 {
changes = append(changes, ids...)
}
} else {
h.pageTrees.treePagesFromTemplateAdapters.DeleteAllFunc(pathInfo.Base(),
func(s string, n *pagesfromdata.PagesFromTemplate) bool {
changes = append(changes, n.DependencyManager)
// Try to open the file to see if has been deleted.
f, err := n.GoTmplFi.Meta().Open()
if err == nil {
f.Close()
}
if err != nil {
// Remove all pages and resources below.
prefix := pathInfo.Base() + "/"
h.pageTrees.treePages.DeletePrefixAll(prefix)
h.pageTrees.resourceTrees.DeletePrefixAll(prefix)
changes = append(changes, identity.NewGlobIdentity(prefix+"*"))
}
return err != nil
})
if ids := h.pageTrees.collectAndMarkStaleIdentities(pathInfo); len(ids) > 0 {
changes = append(changes, ids...)
}
needsPagesAssemble = true
contentChanged = true
if config.RecentlyVisited != nil {
// Fast render mode. Adding them to the visited queue
@@ -749,7 +711,7 @@ func (h *HugoSites) processPartial(ctx context.Context, l logg.LevelLogger, conf
h.pageTrees.treeTaxonomyEntries.DeletePrefix("")
if delete && !isContentDataFile {
if delete {
_, ok := h.pageTrees.treePages.LongestPrefixAll(pathInfo.Base())
if ok {
h.pageTrees.treePages.DeleteAll(pathInfo.Base())
@@ -797,7 +759,17 @@ func (h *HugoSites) processPartial(ctx context.Context, l logg.LevelLogger, conf
}
case files.ComponentFolderAssets:
logger.Println("Asset changed", pathInfo.Path())
changes = append(changes, pathInfo)
var hasID bool
r, _ := h.ResourceSpec.ResourceCache.Get(context.Background(), dynacache.CleanKey(pathInfo.Base()))
identity.WalkIdentitiesShallow(r, func(level int, rid identity.Identity) bool {
hasID = true
changes = append(changes, rid)
return false
})
if !hasID {
changes = append(changes, pathInfo)
}
case files.ComponentFolderData:
logger.Println("Data changed", pathInfo.Path())
@@ -888,8 +860,8 @@ func (h *HugoSites) processPartial(ctx context.Context, l logg.LevelLogger, conf
resourceFiles := h.fileEventsContentPaths(addedOrChangedContent)
changed := &whatChanged{
needsPagesAssembly: needsPagesAssemble,
identitySet: make(identity.Identities),
contentChanged: contentChanged,
identitySet: make(identity.Identities),
}
changed.Add(changes...)
@@ -911,7 +883,10 @@ func (h *HugoSites) processPartial(ctx context.Context, l logg.LevelLogger, conf
}
}
if err := h.resolveAndClearStateForIdentities(ctx, l, cacheBusterOr, changed.Drain()); err != nil {
// Removes duplicates.
changes = changed.identitySet.AsSlice()
if err := h.resolveAndClearStateForIdentities(ctx, l, cacheBusterOr, changes); err != nil {
return err
}
@@ -939,21 +914,17 @@ func (h *HugoSites) processPartial(ctx context.Context, l logg.LevelLogger, conf
}
if resourceFiles != nil {
if err := h.processFiles(ctx, l, config, resourceFiles...); err != nil {
if err := h.processFiles(ctx, l, *config, resourceFiles...); err != nil {
return err
}
}
if h.isRebuild() {
if err := h.processContentAdaptersOnRebuild(ctx, config); err != nil {
return err
}
}
h.logServerAddresses()
return nil
}
func (h *HugoSites) LogServerAddresses() {
func (h *HugoSites) logServerAddresses() {
if h.hugoInfo.IsMultihost() {
for _, s := range h.Sites {
h.Log.Printf("Web Server is available at %s (bind address %s) %s\n", s.conf.C.BaseURL, s.conf.C.ServerInterface, s.Language().Lang)
@@ -964,7 +935,7 @@ func (h *HugoSites) LogServerAddresses() {
}
}
func (h *HugoSites) processFull(ctx context.Context, l logg.LevelLogger, config *BuildCfg) (err error) {
func (h *HugoSites) processFull(ctx context.Context, l logg.LevelLogger, config BuildCfg) (err error) {
if err = h.processFiles(ctx, l, config); err != nil {
err = fmt.Errorf("readAndProcessContent: %w", err)
return
@@ -972,49 +943,7 @@ func (h *HugoSites) processFull(ctx context.Context, l logg.LevelLogger, config
return err
}
func (s *Site) handleContentAdapterChanges(bi pagesfromdata.BuildInfo, buildConfig *BuildCfg) {
if !s.h.isRebuild() {
return
}
if len(bi.ChangedIdentities) > 0 {
buildConfig.whatChanged.Add(bi.ChangedIdentities...)
buildConfig.whatChanged.needsPagesAssembly = true
}
for _, p := range bi.DeletedPaths {
pp := path.Join(bi.Path.Base(), p)
if v, ok := s.pageMap.treePages.Delete(pp); ok {
buildConfig.whatChanged.Add(v.GetIdentity())
}
}
}
func (h *HugoSites) processContentAdaptersOnRebuild(ctx context.Context, buildConfig *BuildCfg) error {
g := rungroup.Run[*pagesfromdata.PagesFromTemplate](ctx, rungroup.Config[*pagesfromdata.PagesFromTemplate]{
NumWorkers: h.numWorkers,
Handle: func(ctx context.Context, p *pagesfromdata.PagesFromTemplate) error {
bi, err := p.Execute(ctx)
if err != nil {
return err
}
s := p.Site.(*Site)
s.handleContentAdapterChanges(bi, buildConfig)
return nil
},
})
h.pageTrees.treePagesFromTemplateAdapters.WalkPrefixRaw(doctree.LockTypeRead, "", func(key string, p *pagesfromdata.PagesFromTemplate) (bool, error) {
if p.StaleVersion() > 0 {
g.Enqueue(p)
}
return false, nil
})
return g.Wait()
}
func (s *HugoSites) processFiles(ctx context.Context, l logg.LevelLogger, buildConfig *BuildCfg, filenames ...pathChange) error {
func (s *HugoSites) processFiles(ctx context.Context, l logg.LevelLogger, buildConfig BuildCfg, filenames ...pathChange) error {
if s.Deps == nil {
panic("nil deps on site")
}
@@ -1024,7 +953,7 @@ func (s *HugoSites) processFiles(ctx context.Context, l logg.LevelLogger, buildC
// For inserts, we can pick an arbitrary pageMap.
pageMap := s.Sites[0].pageMap
c := newPagesCollector(ctx, s.h, sourceSpec, s.Log, l, pageMap, buildConfig, filenames)
c := newPagesCollector(ctx, s.h, sourceSpec, s.Log, l, pageMap, filenames)
if err := c.Collect(); err != nil {
return err
-28
View File
@@ -252,31 +252,3 @@ Files: {{ range $files }}{{ .Permalink }}|{{ end }}$
b.AssertFileContent("public/en/enpages/mybundle-en/file2.txt", "File 2 en.")
b.AssertFileContent("public/fr/section/mybundle/file2.txt", "File 2 en.")
}
func TestMultihostAllButOneLanguageDisabledIssue12288(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
defaultContentLanguage = "en"
disableLanguages = ["fr"]
#baseURL = "https://example.com"
[languages]
[languages.en]
baseURL = "https://example.en"
weight = 1
[languages.fr]
baseURL = "https://example.fr"
weight = 2
-- assets/css/main.css --
body { color: red; }
-- layouts/index.html --
{{ $css := resources.Get "css/main.css" | minify }}
CSS: {{ $css.Permalink }}|{{ $css.RelPermalink }}|
`
b := Test(t, files)
b.AssertFileContent("public/css/main.min.css", "body{color:red}")
b.AssertFileContent("public/index.html", "CSS: https://example.en/css/main.min.css|/css/main.min.css|")
}
+1 -1
View File
@@ -41,7 +41,7 @@ Home: {{ .Title }}
IntegrationTestConfig{
T: t,
TxtarString: files,
// LogLevel: logg.LevelTrace,
LogLevel: logg.LevelTrace,
},
).Build()
+1 -48
View File
@@ -10,7 +10,6 @@ import (
"os"
"path/filepath"
"regexp"
"runtime"
"sort"
"strings"
"sync"
@@ -38,20 +37,12 @@ import (
type TestOpt func(*IntegrationTestConfig)
// TestOptRunning will enable running in integration tests.
func TestOptRunning() TestOpt {
return func(c *IntegrationTestConfig) {
c.Running = true
}
}
// TestOptWatching will enable watching in integration tests.
func TestOptWatching() TestOpt {
return func(c *IntegrationTestConfig) {
c.Watching = true
}
}
// Enable tracing in integration tests.
// THis should only be used during development and not committed to the repo.
func TestOptTrace() TestOpt {
@@ -578,10 +569,6 @@ func (s *IntegrationTestBuilder) initBuilder() error {
"running": s.Cfg.Running,
"watch": s.Cfg.Running,
})
} else if s.Cfg.Watching {
flags.Set("internal", maps.Params{
"watch": s.Cfg.Watching,
})
}
if s.Cfg.WorkingDir != "" {
@@ -698,17 +685,8 @@ func (s *IntegrationTestBuilder) build(cfg BuildCfg) error {
return nil
}
// We simulate the fsnotify events.
// See the test output in https://github.com/bep/fsnotifyeventlister for what events gets produced
// by the different OSes.
func (s *IntegrationTestBuilder) changeEvents() []fsnotify.Event {
var (
events []fsnotify.Event
isLinux = runtime.GOOS == "linux"
isMacOs = runtime.GOOS == "darwin"
isWindows = runtime.GOOS == "windows"
)
var events []fsnotify.Event
for _, v := range s.removedFiles {
events = append(events, fsnotify.Event{
Name: v,
@@ -735,32 +713,12 @@ func (s *IntegrationTestBuilder) changeEvents() []fsnotify.Event {
Name: v,
Op: fsnotify.Write,
})
if isLinux || isWindows {
// Duplicate write events, for some reason.
events = append(events, fsnotify.Event{
Name: v,
Op: fsnotify.Write,
})
}
if isMacOs {
events = append(events, fsnotify.Event{
Name: v,
Op: fsnotify.Chmod,
})
}
}
for _, v := range s.createdFiles {
events = append(events, fsnotify.Event{
Name: v,
Op: fsnotify.Create,
})
if isLinux || isWindows {
events = append(events, fsnotify.Event{
Name: v,
Op: fsnotify.Write,
})
}
}
// Shuffle events.
@@ -829,11 +787,6 @@ type IntegrationTestConfig struct {
// Whether to simulate server mode.
Running bool
// Watch for changes.
// This is (currently) always set to true when Running is set.
// Note that the CLI for the server does allow for --watch=false, but that is not used in these test.
Watching bool
// Will print the log buffer after the build
Verbose bool
-34
View File
@@ -676,37 +676,3 @@ menu: main
b.AssertFileContent("public/fr/index.html", `<a href="/fr/p1/">p1</a>`)
b.AssertLogNotContains("WARN")
}
func TestSectionPagesIssue12399(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ['rss','sitemap','taxonomy','term']
capitalizeListTitles = false
pluralizeListTitles = false
sectionPagesMenu = 'main'
-- content/p1.md --
---
title: p1
---
-- content/s1/p2.md --
---
title: p2
menus: main
---
-- content/s1/p3.md --
---
title: p3
---
-- layouts/_default/list.html --
{{ range site.Menus.main }}<a href="{{ .URL }}">{{ .Name }}</a>{{ end }}
-- layouts/_default/single.html --
{{ .Title }}
`
b := Test(t, files)
b.AssertFileExists("public/index.html", true)
b.AssertFileContent("public/index.html", `<a href="/s1/p2/">p2</a><a href="/s1/">s1</a>`)
}
+2 -12
View File
@@ -510,15 +510,9 @@ func (p *pageState) renderResources() error {
continue
}
if _, isWrapper := r.(resource.ResourceWrapper); isWrapper {
// Skip resources that are wrapped.
// These gets published on its own.
continue
}
src, ok := r.(resource.Source)
if !ok {
initErr = fmt.Errorf("resource %T does not support resource.Source", r)
initErr = fmt.Errorf("resource %T does not support resource.Source", src)
return
}
@@ -587,11 +581,7 @@ func (p *pageState) getPageInfoForError() string {
func (p *pageState) getContentConverter() converter.Converter {
var err error
p.contentConverterInit.Do(func() {
if p.m.pageConfig.ContentMediaType.IsZero() {
panic("ContentMediaType not set")
}
markup := p.m.pageConfig.ContentMediaType.SubType
markup := p.m.pageConfig.Markup
if markup == "html" {
// Only used for shortcode inner content.
markup = "markdown"
-1
View File
@@ -67,7 +67,6 @@ type pageCommon struct {
page.InSectionPositioner
page.OutputFormatsProvider
page.PageMetaProvider
page.PageMetaInternalProvider
page.Positioner
page.RawContentProvider
page.RelatedKeywordsProvider
+27 -52
View File
@@ -20,7 +20,6 @@ import (
"fmt"
"html/template"
"io"
"path/filepath"
"strconv"
"strings"
"unicode/utf8"
@@ -55,31 +54,21 @@ type pageContentReplacement struct {
source pageparser.Item
}
func (m *pageMeta) parseFrontMatter(h *HugoSites, pid uint64) (*contentParseInfo, error) {
var (
sourceKey string
openSource hugio.OpenReadSeekCloser
hasContent = m.pageConfig.IsFromContentAdapter
)
if m.f != nil && !hasContent {
sourceKey = filepath.ToSlash(m.f.Filename())
if !hasContent {
meta := m.f.FileInfo().Meta()
openSource = func() (hugio.ReadSeekCloser, error) {
r, err := meta.Open()
if err != nil {
return nil, fmt.Errorf("failed to open file %q: %w", meta.Filename, err)
}
return r, nil
func (m *pageMeta) parseFrontMatter(h *HugoSites, pid uint64, sourceKey string) (*contentParseInfo, error) {
var openSource hugio.OpenReadSeekCloser
if m.f != nil {
meta := m.f.FileInfo().Meta()
openSource = func() (hugio.ReadSeekCloser, error) {
r, err := meta.Open()
if err != nil {
return nil, fmt.Errorf("failed to open file %q: %w", meta.Filename, err)
}
return r, nil
}
} else if hasContent {
openSource = m.pageConfig.Content.ValueAsOpenReadSeekCloser()
}
if sourceKey == "" {
sourceKey = strconv.FormatUint(pid, 10)
sourceKey = strconv.Itoa(int(pid))
}
pi := &contentParseInfo{
@@ -104,11 +93,6 @@ func (m *pageMeta) parseFrontMatter(h *HugoSites, pid uint64) (*contentParseInfo
pi.itemsStep1 = items
if hasContent {
// No front matter.
return pi, nil
}
if err := pi.mapFrontMatter(source); err != nil {
return nil, err
}
@@ -434,8 +418,6 @@ func (c *cachedContent) mustSource() []byte {
func (c *contentParseInfo) contentSource(s resource.StaleInfo) ([]byte, error) {
key := c.sourceKey
versionv := s.StaleVersion()
v, err := c.h.cacheContentSource.GetOrCreate(key, func(string) (*resources.StaleValue[[]byte], error) {
b, err := c.readSourceAll()
if err != nil {
@@ -444,8 +426,8 @@ func (c *contentParseInfo) contentSource(s resource.StaleInfo) ([]byte, error) {
return &resources.StaleValue[[]byte]{
Value: b,
StaleVersionFunc: func() uint32 {
return s.StaleVersion() - versionv
IsStaleFunc: func() bool {
return s.IsStale()
},
}, nil
})
@@ -505,7 +487,7 @@ type contentPlainPlainWords struct {
func (c *cachedContent) contentRendered(ctx context.Context, cp *pageContentOutput) (contentSummary, error) {
ctx = tpl.Context.DependencyScope.Set(ctx, pageDependencyScopeGlobal)
key := c.pi.sourceKey + "/" + cp.po.f.Name
versionv := c.version(cp)
versionv := cp.contentRenderedVersion
v, err := c.pm.cacheContentRendered.GetOrCreate(key, func(string) (*resources.StaleValue[contentSummary], error) {
cp.po.p.s.Log.Trace(logg.StringFunc(func() string {
@@ -522,8 +504,8 @@ func (c *cachedContent) contentRendered(ctx context.Context, cp *pageContentOutp
}
rs := &resources.StaleValue[contentSummary]{
StaleVersionFunc: func() uint32 {
return c.version(cp) - versionv
IsStaleFunc: func() bool {
return c.IsStale() || cp.contentRenderedVersion != versionv
},
}
@@ -540,7 +522,6 @@ func (c *cachedContent) contentRendered(ctx context.Context, cp *pageContentOutp
if err != nil {
return nil, err
}
if !ok {
return nil, errors.New("invalid state: astDoc is set but RenderContent returned false")
}
@@ -583,14 +564,15 @@ func (c *cachedContent) contentRendered(ctx context.Context, cp *pageContentOutp
var result contentSummary // hasVariants bool
if c.pi.hasSummaryDivider {
if cp.po.p.m.pageConfig.ContentMediaType.IsHTML() {
isHTML := cp.po.p.m.pageConfig.Markup == "html"
if isHTML {
// Use the summary sections as provided by the user.
i := bytes.Index(b, internalSummaryDividerPre)
result.summary = helpers.BytesToHTML(b[:i])
b = b[i+len(internalSummaryDividerPre):]
} else {
summary, content, err := splitUserDefinedSummaryAndContent(cp.po.p.m.pageConfig.Content.Markup, b)
summary, content, err := splitUserDefinedSummaryAndContent(cp.po.p.m.pageConfig.Markup, b)
if err != nil {
cp.po.p.s.Log.Errorf("Failed to set user defined summary for page %q: %s", cp.po.p.pathOrTitle(), err)
} else {
@@ -624,7 +606,7 @@ var setGetContentCallbackInContext = hcontext.NewContextDispatcher[func(*pageCon
func (c *cachedContent) contentToC(ctx context.Context, cp *pageContentOutput) (contentTableOfContents, error) {
key := c.pi.sourceKey + "/" + cp.po.f.Name
versionv := c.version(cp)
versionv := cp.contentRenderedVersion
v, err := c.pm.contentTableOfContents.GetOrCreate(key, func(string) (*resources.StaleValue[contentTableOfContents], error) {
source, err := c.pi.contentSource(c)
@@ -644,10 +626,8 @@ func (c *cachedContent) contentToC(ctx context.Context, cp *pageContentOutput) (
return nil, err
}
// Callback called from below (e.g. in .RenderString)
// Callback called from above (e.g. in .RenderString)
ctxCallback := func(cp2 *pageContentOutput, ct2 contentTableOfContents) {
cp.otherOutputs[cp2.po.p.pid] = cp2
// Merge content placeholders
for k, v := range ct2.contentPlaceholders {
ct.contentPlaceholders[k] = v
@@ -680,7 +660,7 @@ func (c *cachedContent) contentToC(ctx context.Context, cp *pageContentOutput) (
p.pageOutputTemplateVariationsState.Add(1)
}
isHTML := cp.po.p.m.pageConfig.ContentMediaType.IsHTML()
isHTML := cp.po.p.m.pageConfig.Markup == "html"
if !isHTML {
createAndSetToC := func(tocProvider converter.TableOfContentsProvider) {
@@ -730,8 +710,8 @@ func (c *cachedContent) contentToC(ctx context.Context, cp *pageContentOutput) (
return &resources.StaleValue[contentTableOfContents]{
Value: ct,
StaleVersionFunc: func() uint32 {
return c.version(cp) - versionv
IsStaleFunc: func() bool {
return c.IsStale() || cp.contentRenderedVersion != versionv
},
}, nil
})
@@ -742,21 +722,16 @@ func (c *cachedContent) contentToC(ctx context.Context, cp *pageContentOutput) (
return v.Value, nil
}
func (c *cachedContent) version(cp *pageContentOutput) uint32 {
// Both of these gets incremented on change.
return c.StaleVersion() + cp.contentRenderedVersion
}
func (c *cachedContent) contentPlain(ctx context.Context, cp *pageContentOutput) (contentPlainPlainWords, error) {
key := c.pi.sourceKey + "/" + cp.po.f.Name
versionv := c.version(cp)
versionv := cp.contentRenderedVersion
v, err := c.pm.cacheContentPlain.GetOrCreateWitTimeout(key, cp.po.p.s.Conf.Timeout(), func(string) (*resources.StaleValue[contentPlainPlainWords], error) {
var result contentPlainPlainWords
rs := &resources.StaleValue[contentPlainPlainWords]{
StaleVersionFunc: func() uint32 {
return c.version(cp) - versionv
IsStaleFunc: func() bool {
return c.IsStale() || cp.contentRenderedVersion != versionv
},
}
@@ -803,7 +778,7 @@ func (c *cachedContent) contentPlain(ctx context.Context, cp *pageContentOutput)
if err != nil {
return nil, err
}
html := cp.po.p.s.ContentSpec.TrimShortHTML(b.Bytes(), cp.po.p.m.pageConfig.Content.Markup)
html := cp.po.p.s.ContentSpec.TrimShortHTML(b.Bytes())
result.summary = helpers.BytesToHTML(html)
} else {
var summary string
+34 -68
View File
@@ -54,7 +54,7 @@ type pageMeta struct {
singular string // Set for kind == KindTerm and kind == KindTaxonomy.
resource.Staler
*pageMetaParams
pageMetaParams
pageMetaFrontMatter
// Set for standalone pages, e.g. robotsTXT.
@@ -74,9 +74,7 @@ type pageMeta struct {
// Prepare for a rebuild of the data passed in from front matter.
func (m *pageMeta) setMetaPostPrepareRebuild() {
params := xmaps.Clone[map[string]any](m.paramsOriginal)
m.pageMetaParams.pageConfig = &pagemeta.PageConfig{
Params: params,
}
m.pageMetaParams.pageConfig.Params = params
m.pageMetaFrontMatter = pageMetaFrontMatter{}
}
@@ -100,7 +98,7 @@ type pageMetaFrontMatter struct {
func (m *pageMetaParams) init(preserveOringal bool) {
if preserveOringal {
m.paramsOriginal = xmaps.Clone[maps.Params](m.pageConfig.Params)
m.cascadeOriginal = xmaps.Clone[map[page.PageMatcher]maps.Params](m.pageConfig.CascadeCompiled)
m.cascadeOriginal = xmaps.Clone[map[page.PageMatcher]maps.Params](m.pageConfig.Cascade)
}
}
@@ -137,19 +135,19 @@ func (p *pageMeta) BundleType() string {
}
func (p *pageMeta) Date() time.Time {
return p.pageConfig.Dates.Date
return p.pageConfig.Date
}
func (p *pageMeta) PublishDate() time.Time {
return p.pageConfig.Dates.PublishDate
return p.pageConfig.PublishDate
}
func (p *pageMeta) Lastmod() time.Time {
return p.pageConfig.Dates.Lastmod
return p.pageConfig.Lastmod
}
func (p *pageMeta) ExpiryDate() time.Time {
return p.pageConfig.Dates.ExpiryDate
return p.pageConfig.ExpiryDate
}
func (p *pageMeta) Description() string {
@@ -277,9 +275,11 @@ func (p *pageMeta) Weight() int {
func (p *pageMeta) setMetaPre(pi *contentParseInfo, logger loggers.Logger, conf config.AllProvider) error {
frontmatter := pi.frontMatter
if frontmatter != nil {
pcfg := p.pageConfig
if pcfg == nil {
panic("pageConfig not set")
}
// Needed for case insensitive fetching of params values
maps.PrepareParams(frontmatter)
pcfg.Params = frontmatter
@@ -290,7 +290,7 @@ func (p *pageMeta) setMetaPre(pi *contentParseInfo, logger loggers.Logger, conf
if err != nil {
return err
}
pcfg.CascadeCompiled = cascade
pcfg.Cascade = cascade
}
// Look for path, lang and kind, all of which values we need early on.
@@ -328,18 +328,18 @@ func (ps *pageState) setMetaPost(cascade map[page.PageMatcher]maps.Params) error
ps.m.setMetaPostCount++
var cascadeHashPre uint64
if ps.m.setMetaPostCount > 1 {
cascadeHashPre = identity.HashUint64(ps.m.pageConfig.CascadeCompiled)
ps.m.pageConfig.CascadeCompiled = xmaps.Clone[map[page.PageMatcher]maps.Params](ps.m.cascadeOriginal)
cascadeHashPre = identity.HashUint64(ps.m.pageConfig.Cascade)
ps.m.pageConfig.Cascade = xmaps.Clone[map[page.PageMatcher]maps.Params](ps.m.cascadeOriginal)
}
// Apply cascades first so they can be overridden later.
if cascade != nil {
if ps.m.pageConfig.CascadeCompiled != nil {
if ps.m.pageConfig.Cascade != nil {
for k, v := range cascade {
vv, found := ps.m.pageConfig.CascadeCompiled[k]
vv, found := ps.m.pageConfig.Cascade[k]
if !found {
ps.m.pageConfig.CascadeCompiled[k] = v
ps.m.pageConfig.Cascade[k] = v
} else {
// Merge
for ck, cv := range v {
@@ -349,20 +349,19 @@ func (ps *pageState) setMetaPost(cascade map[page.PageMatcher]maps.Params) error
}
}
}
cascade = ps.m.pageConfig.CascadeCompiled
cascade = ps.m.pageConfig.Cascade
} else {
ps.m.pageConfig.CascadeCompiled = cascade
ps.m.pageConfig.Cascade = cascade
}
}
if cascade == nil {
cascade = ps.m.pageConfig.CascadeCompiled
cascade = ps.m.pageConfig.Cascade
}
if ps.m.setMetaPostCount > 1 {
ps.m.setMetaPostCascadeChanged = cascadeHashPre != identity.HashUint64(ps.m.pageConfig.CascadeCompiled)
ps.m.setMetaPostCascadeChanged = cascadeHashPre != identity.HashUint64(ps.m.pageConfig.Cascade)
if !ps.m.setMetaPostCascadeChanged {
// No changes, restore any value that may be changed by aggregation.
ps.m.pageConfig.Dates = ps.m.datesOriginal
return nil
@@ -401,17 +400,11 @@ func (p *pageState) setMetaPostParams() error {
pm := p.m
var mtime time.Time
var contentBaseName string
var ext string
var isContentAdapter bool
if p.File() != nil {
isContentAdapter = p.File().IsContentAdapter()
contentBaseName = p.File().ContentBaseName()
if p.File().FileInfo() != nil {
mtime = p.File().FileInfo().ModTime()
}
if !isContentAdapter {
ext = p.File().Ext()
}
}
var gitAuthorDate time.Time
@@ -435,11 +428,6 @@ func (p *pageState) setMetaPostParams() error {
p.s.Log.Errorf("Failed to handle dates for page %q: %s", p.pathOrTitle(), err)
}
if isContentAdapter {
// Done.
return nil
}
var buildConfig any
var isNewBuildKeyword bool
if v, ok := pm.pageConfig.Params["_build"]; ok {
@@ -468,10 +456,8 @@ params:
var sitemapSet bool
pcfg := pm.pageConfig
params := pcfg.Params
if params == nil {
panic("params not set for " + p.Title())
}
var draft, published, isCJKLanguage *bool
var userParams map[string]any
@@ -564,8 +550,8 @@ params:
pcfg.Layout = cast.ToString(v)
params[loki] = pcfg.Layout
case "markup":
pcfg.Content.Markup = cast.ToString(v)
params[loki] = pcfg.Content.Markup
pcfg.Markup = cast.ToString(v)
params[loki] = pcfg.Markup
case "weight":
pcfg.Weight = cast.ToInt(v)
params[loki] = pcfg.Weight
@@ -615,7 +601,7 @@ params:
}
if handled {
pcfg.ResourcesMeta = resources
pcfg.Resources = resources
break
}
fallthrough
@@ -662,6 +648,8 @@ params:
pcfg.Sitemap = p.s.conf.Sitemap
}
pcfg.Markup = p.s.ContentSpec.ResolveMarkup(pcfg.Markup)
if draft != nil && published != nil {
pcfg.Draft = *draft
p.m.s.Log.Warnf("page %q has both draft and published settings in its frontmatter. Using draft.", p.File().Filename())
@@ -684,14 +672,6 @@ params:
params["iscjklanguage"] = pcfg.IsCJKLanguage
if err := pcfg.Validate(false); err != nil {
return err
}
if err := pcfg.Compile("", false, ext, p.s.Log, p.s.conf.MediaTypes.Config); err != nil {
return err
}
return nil
}
@@ -747,13 +727,13 @@ func (p *pageMeta) applyDefaultValues() error {
(&p.pageConfig.Build).Disable()
}
if p.pageConfig.Content.Markup == "" {
if p.pageConfig.Markup == "" {
if p.File() != nil {
// Fall back to file extension
p.pageConfig.Content.Markup = p.s.ContentSpec.ResolveMarkup(p.File().Ext())
p.pageConfig.Markup = p.s.ContentSpec.ResolveMarkup(p.File().Ext())
}
if p.pageConfig.Content.Markup == "" {
p.pageConfig.Content.Markup = "markdown"
if p.pageConfig.Markup == "" {
p.pageConfig.Markup = "markdown"
}
}
@@ -814,26 +794,12 @@ func (p *pageMeta) newContentConverter(ps *pageState, markup string) (converter.
path = p.Path()
}
doc := newPageForRenderHook(ps)
documentLookup := func(id uint64) any {
if id == ps.pid {
// This prevents infinite recursion in some cases.
return doc
}
if v, ok := ps.pageOutput.pco.otherOutputs[id]; ok {
return v.po.p
}
return nil
}
cpp, err := cp.New(
converter.DocumentContext{
Document: doc,
DocumentLookup: documentLookup,
DocumentID: id,
DocumentName: path,
Filename: filename,
Document: newPageForRenderHook(ps),
DocumentID: id,
DocumentName: path,
Filename: filename,
},
)
if err != nil {
+37 -36
View File
@@ -15,6 +15,7 @@ package hugolib
import (
"fmt"
"path/filepath"
"sync"
"sync/atomic"
@@ -35,17 +36,21 @@ var pageIDCounter atomic.Uint64
func (h *HugoSites) newPage(m *pageMeta) (*pageState, *paths.Path, error) {
m.Staler = &resources.AtomicStaler{}
if m.pageMetaParams == nil {
m.pageMetaParams = &pageMetaParams{
pageConfig: &pagemeta.PageConfig{},
if m.pageConfig == nil {
m.pageMetaParams = pageMetaParams{
pageConfig: &pagemeta.PageConfig{
Params: maps.Params{},
},
}
}
if m.pageConfig.Params == nil {
m.pageConfig.Params = maps.Params{}
var sourceKey string
if m.f != nil {
sourceKey = filepath.ToSlash(m.f.Filename())
}
pid := pageIDCounter.Add(1)
pi, err := m.parseFrontMatter(h, pid)
pi, err := m.parseFrontMatter(h, pid, sourceKey)
if err != nil {
return nil, nil, err
}
@@ -62,43 +67,30 @@ func (h *HugoSites) newPage(m *pageMeta) (*pageState, *paths.Path, error) {
if pcfg.Path != "" {
s := m.pageConfig.Path
// Paths from content adapters should never have any extension.
if pcfg.IsFromContentAdapter || !paths.HasExt(s) {
if !paths.HasExt(s) {
var (
isBranch bool
isBranchSet bool
ext string = m.pageConfig.ContentMediaType.FirstSuffix.Suffix
isBranch bool
ext string = "md"
)
if pcfg.Kind != "" {
isBranch = kinds.IsBranch(pcfg.Kind)
isBranchSet = true
}
if !pcfg.IsFromContentAdapter {
if m.pathInfo != nil {
if !isBranchSet {
isBranch = m.pathInfo.IsBranchBundle()
}
if m.pathInfo.Ext() != "" {
ext = m.pathInfo.Ext()
}
} else if m.f != nil {
pi := m.f.FileInfo().Meta().PathInfo
if !isBranchSet {
isBranch = pi.IsBranchBundle()
}
if pi.Ext() != "" {
ext = pi.Ext()
}
} else if m.pathInfo != nil {
isBranch = m.pathInfo.IsBranchBundle()
if m.pathInfo.Ext() != "" {
ext = m.pathInfo.Ext()
}
} else if m.f != nil {
pi := m.f.FileInfo().Meta().PathInfo
isBranch = pi.IsBranchBundle()
if pi.Ext() != "" {
ext = pi.Ext()
}
}
if isBranch {
s += "/_index." + ext
} else {
s += "/index." + ext
}
}
m.pathInfo = h.Conf.PathParser().Parse(files.ComponentFolderContent, s)
} else if m.pathInfo == nil {
@@ -120,13 +112,23 @@ func (h *HugoSites) newPage(m *pageMeta) (*pageState, *paths.Path, error) {
} else if m.f != nil {
meta := m.f.FileInfo().Meta()
lang = meta.Lang
m.s = h.Sites[meta.LangIndex]
} else {
lang = m.pathInfo.Lang()
}
if lang == "" {
lang = h.Conf.DefaultContentLanguage()
}
var found bool
for _, ss := range h.Sites {
if ss.Lang() == lang {
m.s = ss
found = true
break
}
}
m.s = h.resolveSite(lang)
if m.s == nil {
if !found {
return nil, fmt.Errorf("no site found for language %q", lang)
}
}
@@ -182,7 +184,6 @@ func (h *HugoSites) newPage(m *pageMeta) (*pageState, *paths.Path, error) {
ResourceNameTitleProvider: m,
ResourceParamsProvider: m,
PageMetaProvider: m,
PageMetaInternalProvider: m,
RelatedKeywordsProvider: m,
OutputFormatsProvider: page.NopPage,
ResourceTypeProvider: pageTypesProvider,
+9 -30
View File
@@ -25,14 +25,11 @@ import (
"github.com/gohugoio/hugo/common/text"
"github.com/gohugoio/hugo/common/types/hstring"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/markup"
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/parser/pageparser"
"github.com/mitchellh/mapstructure"
"github.com/spf13/cast"
"github.com/gohugoio/hugo/markup/converter/hooks"
"github.com/gohugoio/hugo/markup/goldmark/hugocontext"
"github.com/gohugoio/hugo/markup/highlight/chromalexers"
"github.com/gohugoio/hugo/markup/tableofcontents"
@@ -71,9 +68,8 @@ var (
func newPageContentOutput(po *pageOutput) (*pageContentOutput, error) {
cp := &pageContentOutput{
po: po,
renderHooks: &renderHooks{},
otherOutputs: make(map[uint64]*pageContentOutput),
po: po,
renderHooks: &renderHooks{},
}
return cp, nil
}
@@ -87,12 +83,8 @@ type renderHooks struct {
type pageContentOutput struct {
po *pageOutput
// Other pages involved in rendering of this page,
// typically included with .RenderShortcodes.
otherOutputs map[uint64]*pageContentOutput
contentRenderedVersion uint32 // Incremented on reset.
contentRendered bool // Set on content render.
contentRenderedVersion int // Incremented on reset.
contentRendered bool // Set on content render.
// Renders Markdown hooks.
renderHooks *renderHooks
@@ -173,13 +165,6 @@ func (pco *pageContentOutput) RenderShortcodes(ctx context.Context) (template.HT
cb(pco, ct)
}
if tpl.Context.IsInGoldmark.Get(ctx) {
// This content will be parsed and rendered by Goldmark.
// Wrap it in a special Hugo markup to assign the correct Page from
// the stack.
return template.HTML(hugocontext.Wrap(c, pco.po.p.pid)), nil
}
return helpers.BytesToHTML(c), nil
}
@@ -264,9 +249,6 @@ func (pco *pageContentOutput) RenderString(ctx context.Context, args ...any) (te
if err := mapstructure.WeakDecode(m, &opts); err != nil {
return "", fmt.Errorf("failed to decode options: %w", err)
}
if opts.Markup != "" {
opts.Markup = markup.ResolveMarkup(opts.Markup)
}
}
contentToRenderv := args[sidx]
@@ -288,8 +270,7 @@ func (pco *pageContentOutput) RenderString(ctx context.Context, args ...any) (te
}
conv := pco.po.p.getContentConverter()
if opts.Markup != "" && opts.Markup != pco.po.p.m.pageConfig.ContentMediaType.SubType {
if opts.Markup != "" && opts.Markup != pco.po.p.m.pageConfig.Markup {
var err error
conv, err = pco.po.p.m.newContentConverter(pco.po.p, opts.Markup)
if err != nil {
@@ -382,11 +363,9 @@ func (pco *pageContentOutput) RenderString(ctx context.Context, args ...any) (te
}
if opts.Display == "inline" {
markup := pco.po.p.m.pageConfig.Content.Markup
if opts.Markup != "" {
markup = pco.po.p.s.ContentSpec.ResolveMarkup(opts.Markup)
}
rendered = pco.po.p.s.ContentSpec.TrimShortHTML(rendered, markup)
// We may have to rethink this in the future when we get other
// renderers.
rendered = pco.po.p.s.ContentSpec.TrimShortHTML(rendered)
}
return template.HTML(string(rendered)), nil
@@ -663,7 +642,7 @@ func splitUserDefinedSummaryAndContent(markup string, c []byte) (summary []byte,
startTag := "p"
switch markup {
case media.DefaultContentTypes.AsciiDoc.SubType:
case "asciidocext":
startTag = "div"
}
+12 -41
View File
@@ -42,20 +42,18 @@ func newPagesCollector(
logger loggers.Logger,
infoLogger logg.LevelLogger,
m *pageMap,
buildConfig *BuildCfg,
ids []pathChange,
) *pagesCollector {
return &pagesCollector{
ctx: ctx,
h: h,
fs: sp.BaseFs.Content.Fs,
m: m,
sp: sp,
logger: logger,
infoLogger: infoLogger,
buildConfig: buildConfig,
ids: ids,
seenDirs: make(map[string]bool),
ctx: ctx,
h: h,
fs: sp.BaseFs.Content.Fs,
m: m,
sp: sp,
logger: logger,
infoLogger: infoLogger,
ids: ids,
seenDirs: make(map[string]bool),
}
}
@@ -70,8 +68,6 @@ type pagesCollector struct {
fs afero.Fs
buildConfig *BuildCfg
// List of paths that have changed. Used in partial builds.
ids []pathChange
seenDirs map[string]bool
@@ -86,8 +82,6 @@ func (c *pagesCollector) Collect() (collectErr error) {
var (
numWorkers = c.h.numWorkers
numFilesProcessedTotal atomic.Uint64
numPagesProcessedTotal atomic.Uint64
numResourcesProcessed atomic.Uint64
numFilesProcessedLast uint64
fileBatchTimer = time.Now()
fileBatchTimerMu sync.Mutex
@@ -104,8 +98,6 @@ func (c *pagesCollector) Collect() (collectErr error) {
logg.Fields{
logg.Field{Name: "files", Value: numFilesProcessedBatch},
logg.Field{Name: "files_total", Value: numFilesProcessedTotal.Load()},
logg.Field{Name: "pages_total", Value: numPagesProcessedTotal.Load()},
logg.Field{Name: "resources_total", Value: numResourcesProcessed.Load()},
},
"",
)
@@ -121,13 +113,10 @@ func (c *pagesCollector) Collect() (collectErr error) {
c.g = rungroup.Run[hugofs.FileMetaInfo](c.ctx, rungroup.Config[hugofs.FileMetaInfo]{
NumWorkers: numWorkers,
Handle: func(ctx context.Context, fi hugofs.FileMetaInfo) error {
numPages, numResources, err := c.m.AddFi(fi, c.buildConfig)
if err != nil {
if err := c.m.AddFi(fi); err != nil {
return hugofs.AddFileInfoToError(err, fi, c.fs)
}
numFilesProcessedTotal.Add(1)
numPagesProcessedTotal.Add(numPages)
numResourcesProcessed.Add(numResources)
if numFilesProcessedTotal.Load()%1000 == 0 {
logFilesProcessed(false)
}
@@ -158,7 +147,7 @@ func (c *pagesCollector) Collect() (collectErr error) {
false,
func(fim hugofs.FileMetaInfo) bool {
if fim.IsDir() {
return id.isStructuralChange()
return true
}
fimp := fim.Meta().PathInfo
if fimp == nil {
@@ -171,7 +160,7 @@ func (c *pagesCollector) Collect() (collectErr error) {
} else {
// We always start from a directory.
collectErr = c.collectDir(id.p, id.isDir, func(fim hugofs.FileMetaInfo) bool {
if id.isStructuralChange() {
if id.delete || id.isDir {
if id.isDir && fim.Meta().PathInfo.IsLeafBundle() {
return strings.HasPrefix(fim.Meta().PathInfo.Path(), paths.AddTrailingSlash(id.p.Path()))
}
@@ -254,21 +243,6 @@ func (c *pagesCollector) collectDirDir(path string, root hugofs.FileMetaInfo, in
return nil, nil
}
n := 0
for _, fi := range readdir {
if fi.Meta().PathInfo.IsContentData() {
// _content.json
// These are not part of any bundle, so just add them directly and remove them from the readdir slice.
if err := c.g.Enqueue(fi); err != nil {
return nil, err
}
} else {
readdir[n] = fi
n++
}
}
readdir = readdir[:n]
// Pick the first regular file.
var first hugofs.FileMetaInfo
for _, fi := range readdir {
@@ -286,7 +260,6 @@ func (c *pagesCollector) collectDirDir(path string, root hugofs.FileMetaInfo, in
// Any bundle file will always be first.
firstPi := first.Meta().PathInfo
if firstPi == nil {
panic(fmt.Sprintf("collectDirDir: no path info for %q", first.Meta().Filename))
}
@@ -347,7 +320,6 @@ func (c *pagesCollector) collectDirDir(path string, root hugofs.FileMetaInfo, in
Info: root,
Fs: c.fs,
IgnoreFile: c.h.SourceSpec.IgnoreFile,
PathParser: c.h.Conf.PathParser(),
HookPre: preHook,
HookPost: postHook,
WalkFn: wfn,
@@ -398,7 +370,6 @@ func (c *pagesCollector) handleBundleLeaf(dir, bundle hugofs.FileMetaInfo, inPat
Info: dir,
DirEntries: readdir,
IgnoreFile: c.h.SourceSpec.IgnoreFile,
PathParser: c.h.Conf.PathParser(),
WalkFn: walk,
})
-331
View File
@@ -1,331 +0,0 @@
// 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 pagesfromdata
import (
"context"
"fmt"
"io"
"path/filepath"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/page/pagemeta"
"github.com/gohugoio/hugo/resources/resource"
"github.com/gohugoio/hugo/tpl"
"github.com/mitchellh/mapstructure"
"github.com/spf13/cast"
)
type PagesFromDataTemplateContext interface {
// AddPage adds a new page to the site.
// The first return value will always be an empty string.
AddPage(any) (string, error)
// AddResource adds a new resource to the site.
// The first return value will always be an empty string.
AddResource(any) (string, error)
// The site to which the pages will be added.
Site() page.Site
// The same template may be executed multiple times for multiple languages.
// The Store can be used to store state between these invocations.
Store() *maps.Scratch
// By default, the template will be executed for the language
// defined by the _content.gotmpl file (e.g. its mount definition).
// This method can be used to activate the template for all languages.
// The return value will always be an empty string.
EnableAllLanguages() string
}
var _ PagesFromDataTemplateContext = (*pagesFromDataTemplateContext)(nil)
type pagesFromDataTemplateContext struct {
p *PagesFromTemplate
}
func (p *pagesFromDataTemplateContext) toPathMap(v any) (string, map[string]any, error) {
m, err := maps.ToStringMapE(v)
if err != nil {
return "", nil, err
}
pathv, ok := m["path"]
if !ok {
return "", nil, fmt.Errorf("path not set")
}
path, err := cast.ToStringE(pathv)
if err != nil || path == "" {
return "", nil, fmt.Errorf("invalid path %q", path)
}
return path, m, nil
}
func (p *pagesFromDataTemplateContext) AddPage(v any) (string, error) {
path, m, err := p.toPathMap(v)
if err != nil {
return "", err
}
if !p.p.buildState.checkHasChangedAndSetSourceInfo(path, m) {
return "", nil
}
pd := pagemeta.DefaultPageConfig
pd.IsFromContentAdapter = true
if err := mapstructure.WeakDecode(m, &pd); err != nil {
return "", fmt.Errorf("failed to decode page map: %w", err)
}
p.p.buildState.NumPagesAdded++
if err := pd.Validate(true); err != nil {
return "", err
}
return "", p.p.HandlePage(p.p, &pd)
}
func (p *pagesFromDataTemplateContext) AddResource(v any) (string, error) {
path, m, err := p.toPathMap(v)
if err != nil {
return "", err
}
if !p.p.buildState.checkHasChangedAndSetSourceInfo(path, m) {
return "", nil
}
var rd pagemeta.ResourceConfig
if err := mapstructure.WeakDecode(m, &rd); err != nil {
return "", err
}
p.p.buildState.NumResourcesAdded++
if err := rd.Validate(); err != nil {
return "", err
}
return "", p.p.HandleResource(p.p, &rd)
}
func (p *pagesFromDataTemplateContext) Site() page.Site {
return p.p.Site
}
func (p *pagesFromDataTemplateContext) Store() *maps.Scratch {
return p.p.store
}
func (p *pagesFromDataTemplateContext) EnableAllLanguages() string {
p.p.buildState.EnableAllLanguages = true
return ""
}
func NewPagesFromTemplate(opts PagesFromTemplateOptions) *PagesFromTemplate {
return &PagesFromTemplate{
PagesFromTemplateOptions: opts,
PagesFromTemplateDeps: opts.DepsFromSite(opts.Site),
buildState: &BuildState{
sourceInfosCurrent: maps.NewCache[string, *sourceInfo](),
},
store: maps.NewScratch(),
}
}
type PagesFromTemplateOptions struct {
Site page.Site
DepsFromSite func(page.Site) PagesFromTemplateDeps
DependencyManager identity.Manager
Watching bool
HandlePage func(pt *PagesFromTemplate, p *pagemeta.PageConfig) error
HandleResource func(pt *PagesFromTemplate, p *pagemeta.ResourceConfig) error
GoTmplFi hugofs.FileMetaInfo
}
type PagesFromTemplateDeps struct {
TmplFinder tpl.TemplateParseFinder
TmplExec tpl.TemplateExecutor
}
var _ resource.Staler = (*PagesFromTemplate)(nil)
type PagesFromTemplate struct {
PagesFromTemplateOptions
PagesFromTemplateDeps
buildState *BuildState
store *maps.Scratch
}
func (b *PagesFromTemplate) AddChange(id identity.Identity) {
b.buildState.ChangedIdentities = append(b.buildState.ChangedIdentities, id)
}
func (b *PagesFromTemplate) MarkStale() {
b.buildState.StaleVersion++
}
func (b *PagesFromTemplate) StaleVersion() uint32 {
return b.buildState.StaleVersion
}
type BuildInfo struct {
NumPagesAdded uint64
NumResourcesAdded uint64
EnableAllLanguages bool
ChangedIdentities []identity.Identity
DeletedPaths []string
Path *paths.Path
}
type BuildState struct {
StaleVersion uint32
EnableAllLanguages bool
// Paths deleted in the current build.
DeletedPaths []string
// Changed identities in the current build.
ChangedIdentities []identity.Identity
NumPagesAdded uint64
NumResourcesAdded uint64
sourceInfosCurrent *maps.Cache[string, *sourceInfo]
sourceInfosPrevious *maps.Cache[string, *sourceInfo]
}
func (b *BuildState) hash(v any) uint64 {
return identity.HashUint64(v)
}
func (b *BuildState) checkHasChangedAndSetSourceInfo(changedPath string, v any) bool {
h := b.hash(v)
si, found := b.sourceInfosPrevious.Get(changedPath)
if found {
b.sourceInfosCurrent.Set(changedPath, si)
if si.hash == h {
return false
}
} else {
si = &sourceInfo{}
b.sourceInfosCurrent.Set(changedPath, si)
}
si.hash = h
return true
}
func (b *BuildState) resolveDeletedPaths() {
if b.sourceInfosPrevious == nil {
b.DeletedPaths = nil
return
}
var paths []string
b.sourceInfosPrevious.ForEeach(func(k string, _ *sourceInfo) {
if _, found := b.sourceInfosCurrent.Get(k); !found {
paths = append(paths, k)
}
})
b.DeletedPaths = paths
}
func (b *BuildState) PrepareNextBuild() {
b.sourceInfosPrevious = b.sourceInfosCurrent
b.sourceInfosCurrent = maps.NewCache[string, *sourceInfo]()
b.StaleVersion = 0
b.DeletedPaths = nil
b.ChangedIdentities = nil
b.NumPagesAdded = 0
b.NumResourcesAdded = 0
}
type sourceInfo struct {
hash uint64
}
func (p PagesFromTemplate) CloneForSite(s page.Site) *PagesFromTemplate {
// We deliberately make them share the same DepenencyManager and Store.
p.PagesFromTemplateOptions.Site = s
p.PagesFromTemplateDeps = p.PagesFromTemplateOptions.DepsFromSite(s)
p.buildState = &BuildState{
sourceInfosCurrent: maps.NewCache[string, *sourceInfo](),
}
return &p
}
func (p PagesFromTemplate) CloneForGoTmpl(fi hugofs.FileMetaInfo) *PagesFromTemplate {
p.PagesFromTemplateOptions.GoTmplFi = fi
return &p
}
func (p *PagesFromTemplate) GetDependencyManagerForScope(scope int) identity.Manager {
return p.DependencyManager
}
func (p *PagesFromTemplate) Execute(ctx context.Context) (BuildInfo, error) {
defer func() {
p.buildState.PrepareNextBuild()
}()
f, err := p.GoTmplFi.Meta().Open()
if err != nil {
return BuildInfo{}, err
}
defer f.Close()
tmpl, err := p.TmplFinder.Parse(filepath.ToSlash(p.GoTmplFi.Meta().Filename), helpers.ReaderToString(f))
if err != nil {
return BuildInfo{}, err
}
data := &pagesFromDataTemplateContext{
p: p,
}
ctx = tpl.Context.DependencyManagerScopedProvider.Set(ctx, p)
if err := p.TmplExec.ExecuteWithContext(ctx, tmpl, io.Discard, data); err != nil {
return BuildInfo{}, err
}
if p.Watching {
p.buildState.resolveDeletedPaths()
}
bi := BuildInfo{
NumPagesAdded: p.buildState.NumPagesAdded,
NumResourcesAdded: p.buildState.NumResourcesAdded,
EnableAllLanguages: p.buildState.EnableAllLanguages,
ChangedIdentities: p.buildState.ChangedIdentities,
DeletedPaths: p.buildState.DeletedPaths,
Path: p.GoTmplFi.Meta().PathInfo,
}
return bi, nil
}
//////////////
@@ -1,520 +0,0 @@
// 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 pagesfromdata_test
import (
"fmt"
"strings"
"testing"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/markup/asciidocext"
"github.com/gohugoio/hugo/markup/pandoc"
"github.com/gohugoio/hugo/markup/rst"
)
const filesPagesFromDataTempleBasic = `
-- hugo.toml --
disableKinds = ["taxonomy", "term", "rss", "sitemap"]
baseURL = "https://example.com"
disableLiveReload = true
-- assets/a/pixel.png --
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
-- assets/mydata.yaml --
p1: "p1"
draft: false
-- layouts/partials/get-value.html --
{{ $val := "p1" }}
{{ return $val }}
-- layouts/_default/single.html --
Single: {{ .Title }}|{{ .Content }}|Params: {{ .Params.param1 }}|Path: {{ .Path }}|
Dates: Date: {{ .Date.Format "2006-01-02" }}|Lastmod: {{ .Lastmod.Format "2006-01-02" }}|PublishDate: {{ .PublishDate.Format "2006-01-02" }}|ExpiryDate: {{ .ExpiryDate.Format "2006-01-02" }}|
Len Resources: {{ .Resources | len }}
Resources: {{ range .Resources }}RelPermalink: {{ .RelPermalink }}|Name: {{ .Name }}|Title: {{ .Title }}|Params: {{ .Params }}|{{ end }}$
{{ with .Resources.Get "featured.png" }}
Featured Image: {{ .RelPermalink }}|{{ .Name }}|
{{ with .Resize "10x10" }}
Resized Featured Image: {{ .RelPermalink }}|{{ .Width }}|
{{ end}}
{{ end }}
-- layouts/_default/list.html --
List: {{ .Title }}|{{ .Content }}|
RegularPagesRecursive: {{ range .RegularPagesRecursive }}{{ .Title }}:{{ .Path }}|{{ end }}$
Sections: {{ range .Sections }}{{ .Title }}:{{ .Path }}|{{ end }}$
-- content/docs/pfile.md --
---
title: "pfile"
date: 2023-03-01
---
Pfile Content
-- content/docs/_content.gotmpl --
{{ $pixel := resources.Get "a/pixel.png" }}
{{ $dataResource := resources.Get "mydata.yaml" }}
{{ $data := $dataResource | transform.Unmarshal }}
{{ $pd := $data.p1 }}
{{ $pp := partial "get-value.html" }}
{{ $title := printf "%s:%s" $pd $pp }}
{{ $date := "2023-03-01" | time.AsTime }}
{{ $dates := dict "date" $date }}
{{ $contentMarkdown := dict "value" "**Hello World**" "mediaType" "text/markdown" }}
{{ $contentMarkdownDefault := dict "value" "**Hello World Default**" }}
{{ $contentHTML := dict "value" "<b>Hello World!</b> No **markdown** here." "mediaType" "text/html" }}
{{ $.AddPage (dict "kind" "page" "path" "P1" "title" $title "dates" $dates "content" $contentMarkdown "params" (dict "param1" "param1v" ) ) }}
{{ $.AddPage (dict "kind" "page" "path" "p2" "title" "p2title" "dates" $dates "content" $contentHTML ) }}
{{ $.AddPage (dict "kind" "page" "path" "p3" "title" "p3title" "dates" $dates "content" $contentMarkdownDefault "draft" false ) }}
{{ $.AddPage (dict "kind" "page" "path" "p4" "title" "p4title" "dates" $dates "content" $contentMarkdownDefault "draft" $data.draft ) }}
{{ $resourceContent := dict "value" $dataResource }}
{{ $.AddResource (dict "path" "p1/data1.yaml" "content" $resourceContent) }}
{{ $.AddResource (dict "path" "p1/mytext.txt" "content" (dict "value" "some text") "name" "textresource" "title" "My Text Resource" "params" (dict "param1" "param1v") )}}
{{ $.AddResource (dict "path" "p1/sub/mytex2.txt" "content" (dict "value" "some text") "title" "My Text Sub Resource" ) }}
{{ $.AddResource (dict "path" "P1/Sub/MyMixCaseText2.txt" "content" (dict "value" "some text") "title" "My Text Sub Mixed Case Path Resource" ) }}
{{ $.AddResource (dict "path" "p1/sub/data1.yaml" "content" $resourceContent "title" "Sub data") }}
{{ $resourceParams := dict "data2ParaM1" "data2Param1v" }}
{{ $.AddResource (dict "path" "p1/data2.yaml" "name" "data2.yaml" "title" "My data 2" "params" $resourceParams "content" $resourceContent) }}
{{ $.AddResource (dict "path" "p1/featuredimage.png" "name" "featured.png" "title" "My Featured Image" "params" $resourceParams "content" (dict "value" $pixel ))}}
`
func TestPagesFromGoTmplMisc(t *testing.T) {
t.Parallel()
b := hugolib.Test(t, filesPagesFromDataTempleBasic)
b.AssertPublishDir(`
docs/p1/mytext.txt
docs/p1/sub/mytex2.tx
docs/p1/sub/mymixcasetext2.txt
`)
// Page from markdown file.
b.AssertFileContent("public/docs/pfile/index.html", "Dates: Date: 2023-03-01|Lastmod: 2023-03-01|PublishDate: 2023-03-01|ExpiryDate: 0001-01-01|")
// Pages from gotmpl.
b.AssertFileContent("public/docs/p1/index.html",
"Single: p1:p1|",
"Path: /docs/p1|",
"<strong>Hello World</strong>",
"Params: param1v|",
"Len Resources: 7",
"RelPermalink: /mydata.yaml|Name: data1.yaml|Title: data1.yaml|Params: map[]|",
"RelPermalink: /mydata.yaml|Name: data2.yaml|Title: My data 2|Params: map[data2param1:data2Param1v]|",
"RelPermalink: /a/pixel.png|Name: featured.png|Title: My Featured Image|Params: map[data2param1:data2Param1v]|",
"RelPermalink: /docs/p1/sub/mytex2.txt|Name: sub/mytex2.txt|",
"RelPermalink: /docs/p1/sub/mymixcasetext2.txt|Name: sub/mymixcasetext2.txt|",
"RelPermalink: /mydata.yaml|Name: sub/data1.yaml|Title: Sub data|Params: map[]|",
"Featured Image: /a/pixel.png|featured.png|",
"Resized Featured Image: /a/pixel_hu8aa3346827e49d756ff4e630147c42b5_70_10x10_resize_box_3.png|10|",
// Resource from string
"RelPermalink: /docs/p1/mytext.txt|Name: textresource|Title: My Text Resource|Params: map[param1:param1v]|",
// Dates
"Dates: Date: 2023-03-01|Lastmod: 2023-03-01|PublishDate: 2023-03-01|ExpiryDate: 0001-01-01|",
)
b.AssertFileContent("public/docs/p2/index.html", "Single: p2title|", "<b>Hello World!</b> No **markdown** here.")
b.AssertFileContent("public/docs/p3/index.html", "<strong>Hello World Default</strong>")
}
func TestPagesFromGoTmplAsciidocAndSimilar(t *testing.T) {
files := `
-- hugo.toml --
disableKinds = ["taxonomy", "term", "rss", "sitemap"]
baseURL = "https://example.com"
[security]
[security.exec]
allow = ['asciidoctor', 'pandoc','rst2html', 'python']
-- layouts/_default/single.html --
|Content: {{ .Content }}|Title: {{ .Title }}|Path: {{ .Path }}|
-- content/docs/_content.gotmpl --
{{ $.AddPage (dict "path" "asciidoc" "content" (dict "value" "Mark my words, #automation is essential#." "mediaType" "text/asciidoc" )) }}
{{ $.AddPage (dict "path" "pandoc" "content" (dict "value" "This ~~is deleted text.~~" "mediaType" "text/pandoc" )) }}
{{ $.AddPage (dict "path" "rst" "content" (dict "value" "This is *bold*." "mediaType" "text/rst" )) }}
{{ $.AddPage (dict "path" "org" "content" (dict "value" "the ability to use +strikethrough+ is a plus" "mediaType" "text/org" )) }}
{{ $.AddPage (dict "path" "nocontent" "title" "No Content" ) }}
`
b := hugolib.Test(t, files)
if asciidocext.Supports() {
b.AssertFileContent("public/docs/asciidoc/index.html",
"Mark my words, <mark>automation is essential</mark>",
"Path: /docs/asciidoc|",
)
}
if pandoc.Supports() {
b.AssertFileContent("public/docs/pandoc/index.html",
"This <del>is deleted text.</del>",
"Path: /docs/pandoc|",
)
}
if rst.Supports() {
b.AssertFileContent("public/docs/rst/index.html",
"This is <em>bold</em>",
"Path: /docs/rst|",
)
}
b.AssertFileContent("public/docs/org/index.html",
"the ability to use <del>strikethrough</del> is a plus",
"Path: /docs/org|",
)
b.AssertFileContent("public/docs/nocontent/index.html", "|Content: |Title: No Content|Path: /docs/nocontent|")
}
func TestPagesFromGoTmplAddPageErrors(t *testing.T) {
filesTemplate := `
-- hugo.toml --
disableKinds = ["taxonomy", "term", "rss", "sitemap"]
baseURL = "https://example.com"
-- content/docs/_content.gotmpl --
{{ $.AddPage DICT }}
`
t.Run("AddPage, missing Path", func(t *testing.T) {
files := strings.ReplaceAll(filesTemplate, "DICT", `(dict "kind" "page" "title" "p1")`)
b, err := hugolib.TestE(t, files)
b.Assert(err, qt.IsNotNil)
b.Assert(err.Error(), qt.Contains, "_content.gotmpl:1:4")
b.Assert(err.Error(), qt.Contains, "error calling AddPage: path not set")
})
t.Run("AddPage, path starting with slash", func(t *testing.T) {
files := strings.ReplaceAll(filesTemplate, "DICT", `(dict "kind" "page" "title" "p1" "path" "/foo")`)
b, err := hugolib.TestE(t, files)
b.Assert(err, qt.IsNotNil)
b.Assert(err.Error(), qt.Contains, `path "/foo" must not start with a /`)
})
t.Run("AddPage, lang set", func(t *testing.T) {
files := strings.ReplaceAll(filesTemplate, "DICT", `(dict "kind" "page" "path" "p1" "lang" "en")`)
b, err := hugolib.TestE(t, files)
b.Assert(err, qt.IsNotNil)
b.Assert(err.Error(), qt.Contains, "_content.gotmpl:1:4")
b.Assert(err.Error(), qt.Contains, "error calling AddPage: lang must not be set")
})
t.Run("Site methods not ready", func(t *testing.T) {
filesTemplate := `
-- hugo.toml --
disableKinds = ["taxonomy", "term", "rss", "sitemap"]
baseURL = "https://example.com"
-- content/docs/_content.gotmpl --
{{ .Site.METHOD }}
`
for _, method := range []string{"RegularPages", "Pages", "AllPages", "AllRegularPages", "Home", "Sections", "GetPage", "Menus", "MainSections", "Taxonomies"} {
t.Run(method, func(t *testing.T) {
files := strings.ReplaceAll(filesTemplate, "METHOD", method)
b, err := hugolib.TestE(t, files)
b.Assert(err, qt.IsNotNil)
b.Assert(err.Error(), qt.Contains, fmt.Sprintf("error calling %s: this method cannot be called before the site is fully initialized", method))
})
}
})
}
func TestPagesFromGoTmplAddResourceErrors(t *testing.T) {
filesTemplate := `
-- hugo.toml --
disableKinds = ["taxonomy", "term", "rss", "sitemap"]
baseURL = "https://example.com"
-- content/docs/_content.gotmpl --
{{ $.AddResource DICT }}
`
t.Run("missing Path", func(t *testing.T) {
files := strings.ReplaceAll(filesTemplate, "DICT", `(dict "name" "r1")`)
b, err := hugolib.TestE(t, files)
b.Assert(err, qt.IsNotNil)
b.Assert(err.Error(), qt.Contains, "error calling AddResource: path not set")
})
}
func TestPagesFromGoTmplEditGoTmpl(t *testing.T) {
t.Parallel()
b := hugolib.TestRunning(t, filesPagesFromDataTempleBasic)
b.EditFileReplaceAll("content/docs/_content.gotmpl", `"title" "p2title"`, `"title" "p2titleedited"`).Build()
b.AssertFileContent("public/docs/p2/index.html", "Single: p2titleedited|")
b.AssertFileContent("public/docs/index.html", "p2titleedited")
}
func TestPagesFromGoTmplEditDataResource(t *testing.T) {
t.Parallel()
b := hugolib.TestRunning(t, filesPagesFromDataTempleBasic)
b.AssertRenderCountPage(7)
b.EditFileReplaceAll("assets/mydata.yaml", "p1: \"p1\"", "p1: \"p1edited\"").Build()
b.AssertFileContent("public/docs/p1/index.html", "Single: p1edited:p1|")
b.AssertFileContent("public/docs/index.html", "p1edited")
b.AssertRenderCountPage(3)
}
func TestPagesFromGoTmplEditPartial(t *testing.T) {
t.Parallel()
b := hugolib.TestRunning(t, filesPagesFromDataTempleBasic)
b.EditFileReplaceAll("layouts/partials/get-value.html", "p1", "p1edited").Build()
b.AssertFileContent("public/docs/p1/index.html", "Single: p1:p1edited|")
b.AssertFileContent("public/docs/index.html", "p1edited")
}
func TestPagesFromGoTmplRemovePage(t *testing.T) {
t.Parallel()
b := hugolib.TestRunning(t, filesPagesFromDataTempleBasic)
b.EditFileReplaceAll("content/docs/_content.gotmpl", `{{ $.AddPage (dict "kind" "page" "path" "p2" "title" "p2title" "dates" $dates "content" $contentHTML ) }}`, "").Build()
b.AssertFileContent("public/index.html", "RegularPagesRecursive: p1:p1:/docs/p1|p3title:/docs/p3|p4title:/docs/p4|pfile:/docs/pfile|$")
}
func TestPagesFromGoTmplDraftPage(t *testing.T) {
t.Parallel()
b := hugolib.TestRunning(t, filesPagesFromDataTempleBasic)
b.EditFileReplaceAll("content/docs/_content.gotmpl", `"draft" false`, `"draft" true`).Build()
b.AssertFileContent("public/index.html", "RegularPagesRecursive: p1:p1:/docs/p1|p2title:/docs/p2|p4title:/docs/p4|pfile:/docs/pfile|$")
}
func TestPagesFromGoTmplDraftFlagFromResource(t *testing.T) {
t.Parallel()
b := hugolib.TestRunning(t, filesPagesFromDataTempleBasic)
b.EditFileReplaceAll("assets/mydata.yaml", `draft: false`, `draft: true`).Build()
b.AssertFileContent("public/index.html", "RegularPagesRecursive: p1:p1:/docs/p1|p2title:/docs/p2|p3title:/docs/p3|pfile:/docs/pfile|$")
b.EditFileReplaceAll("assets/mydata.yaml", `draft: true`, `draft: false`).Build()
b.AssertFileContent("public/index.html", "RegularPagesRecursive: p1:p1:/docs/p1|p2title:/docs/p2|p3title:/docs/p3|p4title:/docs/p4|pfile:/docs/pfile|$")
}
func TestPagesFromGoTmplMovePage(t *testing.T) {
t.Parallel()
b := hugolib.TestRunning(t, filesPagesFromDataTempleBasic)
b.AssertFileContent("public/index.html", "RegularPagesRecursive: p1:p1:/docs/p1|p2title:/docs/p2|p3title:/docs/p3|p4title:/docs/p4|pfile:/docs/pfile|$")
b.EditFileReplaceAll("content/docs/_content.gotmpl", `"path" "p2"`, `"path" "p2moved"`).Build()
b.AssertFileContent("public/index.html", "RegularPagesRecursive: p1:p1:/docs/p1|p2title:/docs/p2moved|p3title:/docs/p3|p4title:/docs/p4|pfile:/docs/pfile|$")
}
func TestPagesFromGoTmplRemoveGoTmpl(t *testing.T) {
t.Parallel()
b := hugolib.TestRunning(t, filesPagesFromDataTempleBasic)
b.AssertFileContent("public/index.html",
"RegularPagesRecursive: p1:p1:/docs/p1|p2title:/docs/p2|p3title:/docs/p3|p4title:/docs/p4|pfile:/docs/pfile|$",
"Sections: Docs:/docs|",
)
b.AssertFileContent("public/docs/index.html", "RegularPagesRecursive: p1:p1:/docs/p1|p2title:/docs/p2|p3title:/docs/p3|p4title:/docs/p4|pfile:/docs/pfile|$")
b.RemoveFiles("content/docs/_content.gotmpl").Build()
// One regular page left.
b.AssertFileContent("public/index.html",
"RegularPagesRecursive: pfile:/docs/pfile|$",
"Sections: Docs:/docs|",
)
b.AssertFileContent("public/docs/index.html", "RegularPagesRecursive: pfile:/docs/pfile|$")
}
func TestPagesFromGoTmplLanguagePerFile(t *testing.T) {
filesTemplate := `
-- hugo.toml --
defaultContentLanguage = "en"
defaultContentLanguageInSubdir = true
[languages]
[languages.en]
weight = 1
title = "Title"
[languages.fr]
weight = 2
title = "Titre"
disabled = DISABLE
-- layouts/_default/single.html --
Single: {{ .Title }}|{{ .Content }}|
-- content/docs/_content.gotmpl --
{{ $.AddPage (dict "kind" "page" "path" "p1" "title" "Title" ) }}
-- content/docs/_content.fr.gotmpl --
{{ $.AddPage (dict "kind" "page" "path" "p1" "title" "Titre" ) }}
`
for _, disable := range []bool{false, true} {
t.Run(fmt.Sprintf("disable=%t", disable), func(t *testing.T) {
b := hugolib.Test(t, strings.ReplaceAll(filesTemplate, "DISABLE", fmt.Sprintf("%t", disable)))
b.AssertFileContent("public/en/docs/p1/index.html", "Single: Title||")
b.AssertFileExists("public/fr/docs/p1/index.html", !disable)
if !disable {
b.AssertFileContent("public/fr/docs/p1/index.html", "Single: Titre||")
}
})
}
}
func TestPagesFromGoTmplEnableAllLanguages(t *testing.T) {
t.Parallel()
filesTemplate := `
-- hugo.toml --
defaultContentLanguage = "en"
defaultContentLanguageInSubdir = true
[languages]
[languages.en]
weight = 1
title = "Title"
[languages.fr]
title = "Titre"
weight = 2
disabled = DISABLE
-- i18n/en.yaml --
title: Title
-- i18n/fr.yaml --
title: Titre
-- content/docs/_content.gotmpl --
{{ .EnableAllLanguages }}
{{ $titleFromStore := .Store.Get "title" }}
{{ if not $titleFromStore }}
{{ $titleFromStore = "notfound"}}
{{ .Store.Set "title" site.Title }}
{{ end }}
{{ $title := printf "%s:%s:%s" site.Title (i18n "title") $titleFromStore }}
{{ $.AddPage (dict "kind" "page" "path" "p1" "title" $title ) }}
-- layouts/_default/single.html --
Single: {{ .Title }}|{{ .Content }}|
`
for _, disable := range []bool{false, true} {
t.Run(fmt.Sprintf("disable=%t", disable), func(t *testing.T) {
b := hugolib.Test(t, strings.ReplaceAll(filesTemplate, "DISABLE", fmt.Sprintf("%t", disable)))
b.AssertFileExists("public/fr/docs/p1/index.html", !disable)
if !disable {
b.AssertFileContent("public/en/docs/p1/index.html", "Single: Title:Title:notfound||")
b.AssertFileContent("public/fr/docs/p1/index.html", "Single: Titre:Titre:Title||")
}
})
}
}
func TestPagesFromGoTmplMarkdownify(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ["taxonomy", "term", "rss", "sitemap"]
baseURL = "https://example.com"
-- layouts/_default/single.html --
|Content: {{ .Content }}|Title: {{ .Title }}|Path: {{ .Path }}|
-- content/docs/_content.gotmpl --
{{ $content := "**Hello World**" | markdownify }}
{{ $.AddPage (dict "path" "p1" "content" (dict "value" $content "mediaType" "text/html" )) }}
`
b, err := hugolib.TestE(t, files)
// This currently fails. We should fix this, but that is not a trivial task, so do it later.
b.Assert(err, qt.IsNotNil)
b.Assert(err.Error(), qt.Contains, "error calling markdownify: this method cannot be called before the site is fully initialized")
}
func TestPagesFromGoTmplResourceWithoutExtensionWithMediaTypeProvided(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ["taxonomy", "term", "rss", "sitemap"]
baseURL = "https://example.com"
-- layouts/_default/single.html --
|Content: {{ .Content }}|Title: {{ .Title }}|Path: {{ .Path }}|
{{ range .Resources }}
|RelPermalink: {{ .RelPermalink }}|Name: {{ .Name }}|Title: {{ .Title }}|Params: {{ .Params }}|MediaType: {{ .MediaType }}|
{{ end }}
-- content/docs/_content.gotmpl --
{{ $.AddPage (dict "path" "p1" "content" (dict "value" "**Hello World**" "mediaType" "text/markdown" )) }}
{{ $.AddResource (dict "path" "p1/myresource" "content" (dict "value" "abcde" "mediaType" "text/plain" )) }}
`
b := hugolib.Test(t, files)
b.AssertFileContent("public/docs/p1/index.html", "RelPermalink: /docs/p1/myresource|Name: myresource|Title: myresource|Params: map[]|MediaType: text/plain|")
}
func TestPagesFromGoTmplCascade(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ["taxonomy", "term", "rss", "sitemap"]
baseURL = "https://example.com"
-- layouts/_default/single.html --
|Content: {{ .Content }}|Title: {{ .Title }}|Path: {{ .Path }}|Params: {{ .Params }}|
-- content/_content.gotmpl --
{{ $cascade := dict "params" (dict "cascadeparam1" "cascadeparam1value" ) }}
{{ $.AddPage (dict "path" "docs" "kind" "section" "cascade" $cascade ) }}
{{ $.AddPage (dict "path" "docs/p1" "content" (dict "value" "**Hello World**" "mediaType" "text/markdown" )) }}
`
b := hugolib.Test(t, files)
b.AssertFileContent("public/docs/p1/index.html", "|Path: /docs/p1|Params: map[cascadeparam1:cascadeparam1value")
}
func TestPagesFromGoBuildOptions(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ["taxonomy", "term", "rss", "sitemap"]
baseURL = "https://example.com"
-- layouts/_default/single.html --
|Content: {{ .Content }}|Title: {{ .Title }}|Path: {{ .Path }}|Params: {{ .Params }}|
-- content/_content.gotmpl --
{{ $.AddPage (dict "path" "docs/p1" "content" (dict "value" "**Hello World**" "mediaType" "text/markdown" )) }}
{{ $never := dict "list" "never" "publishResources" false "render" "never" }}
{{ $.AddPage (dict "path" "docs/p2" "content" (dict "value" "**Hello World**" "mediaType" "text/markdown" ) "build" $never ) }}
`
b := hugolib.Test(t, files)
b.AssertFileExists("public/docs/p1/index.html", true)
b.AssertFileExists("public/docs/p2/index.html", false)
}
func TestPagesFromGoPathsWithDotsIssue12493(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ['home','section','rss','sitemap','taxonomy','term']
-- content/_content.gotmpl --
{{ .AddPage (dict "path" "s-1.2.3/p-4.5.6" "title" "p-4.5.6") }}
-- layouts/_default/single.html --
{{ .Title }}
`
b := hugolib.Test(t, files)
b.AssertFileExists("public/s-1.2.3/p-4.5.6/index.html", true)
}
func TestPagesFromGoParamsIssue12497(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ['home','section','rss','sitemap','taxonomy','term']
-- content/_content.gotmpl --
{{ .AddPage (dict "path" "p1" "title" "p1" "params" (dict "paraM1" "param1v" )) }}
{{ .AddResource (dict "path" "p1/data1.yaml" "content" (dict "value" "data1" ) "params" (dict "paraM1" "param1v" )) }}
-- layouts/_default/single.html --
{{ .Title }}|{{ .Params.paraM1 }}
{{ range .Resources }}
{{ .Name }}|{{ .Params.paraM1 }}
{{ end }}
`
b := hugolib.Test(t, files)
b.AssertFileContent("public/p1/index.html",
"p1|param1v",
"data1.yaml|param1v",
)
}
@@ -1,32 +0,0 @@
// 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 pagesfromdata
import "testing"
func BenchmarkHash(b *testing.B) {
m := map[string]any{
"foo": "bar",
"bar": "foo",
"stringSlice": []any{"a", "b", "c"},
"intSlice": []any{1, 2, 3},
"largeText": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit.",
}
bs := BuildState{}
for i := 0; i < b.N; i++ {
bs.hash(m)
}
}
+17 -144
View File
@@ -11,7 +11,6 @@ import (
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/htesting"
"github.com/gohugoio/hugo/markup/asciidocext"
"github.com/gohugoio/hugo/resources/resource_transformers/tocss/dartsass"
"github.com/gohugoio/hugo/resources/resource_transformers/tocss/scss"
)
@@ -53,11 +52,6 @@ title: "Home"
Home Content.
-- content/hometext.txt --
Home Text Content.
-- content/myothersection/myothersectionpage.md --
---
title: "myothersectionpage"
---
myothersectionpage Content.
-- layouts/_default/single.html --
Single: {{ .Title }}|{{ .Content }}$
Resources: {{ range $i, $e := .Resources }}{{ $i }}:{{ .RelPermalink }}|{{ .Content }}|{{ end }}$
@@ -126,23 +120,14 @@ func TestRebuildEditTextFileInBranchBundle(t *testing.T) {
b.AssertRenderCountContent(1)
}
func testRebuildBothWatchingAndRunning(t *testing.T, files string, withB func(b *IntegrationTestBuilder)) {
t.Helper()
for _, opt := range []TestOpt{TestOptWatching(), TestOptRunning()} {
b := Test(t, files, opt)
withB(b)
}
}
func TestRebuildRenameTextFileInLeafBundle(t *testing.T) {
testRebuildBothWatchingAndRunning(t, rebuildFilesSimple, func(b *IntegrationTestBuilder) {
b.AssertFileContent("public/mysection/mysectionbundle/index.html", "My Section Bundle Text 2 Content.", "Len Resources: 2|")
b := TestRunning(t, rebuildFilesSimple)
b.AssertFileContent("public/mysection/mysectionbundle/index.html", "My Section Bundle Text 2 Content.", "Len Resources: 2|")
b.RenameFile("content/mysection/mysectionbundle/mysectionbundletext.txt", "content/mysection/mysectionbundle/mysectionbundletext2.txt").Build()
b.AssertFileContent("public/mysection/mysectionbundle/index.html", "mysectionbundletext2", "My Section Bundle Text 2 Content.", "Len Resources: 2|")
b.AssertRenderCountPage(5)
b.AssertRenderCountContent(6)
})
b.RenameFile("content/mysection/mysectionbundle/mysectionbundletext.txt", "content/mysection/mysectionbundle/mysectionbundletext2.txt").Build()
b.AssertFileContent("public/mysection/mysectionbundle/index.html", "mysectionbundletext2", "My Section Bundle Text 2 Content.", "Len Resources: 2|")
b.AssertRenderCountPage(3)
b.AssertRenderCountContent(3)
}
func TestRebuilEditContentFileInLeafBundle(t *testing.T) {
@@ -152,19 +137,6 @@ func TestRebuilEditContentFileInLeafBundle(t *testing.T) {
b.AssertFileContent("public/mysection/mysectionbundle/index.html", "My Section Bundle Content Content Edited.")
}
func TestRebuilEditContentFileThenAnother(t *testing.T) {
b := TestRunning(t, rebuildFilesSimple)
b.EditFileReplaceAll("content/mysection/mysectionbundle/mysectionbundlecontent.md", "Content Content.", "Content Content Edited.").Build()
b.AssertFileContent("public/mysection/mysectionbundle/index.html", "My Section Bundle Content Content Edited.")
b.AssertRenderCountPage(1)
b.AssertRenderCountContent(2)
b.EditFileReplaceAll("content/myothersection/myothersectionpage.md", "myothersectionpage Content.", "myothersectionpage Content Edited.").Build()
b.AssertFileContent("public/myothersection/myothersectionpage/index.html", "myothersectionpage Content Edited")
b.AssertRenderCountPage(1)
b.AssertRenderCountContent(1)
}
func TestRebuildRenameTextFileInBranchBundle(t *testing.T) {
b := TestRunning(t, rebuildFilesSimple)
b.AssertFileContent("public/mysection/index.html", "My Section")
@@ -181,7 +153,7 @@ func TestRebuildRenameTextFileInHomeBundle(t *testing.T) {
b.RenameFile("content/hometext.txt", "content/hometext2.txt").Build()
b.AssertFileContent("public/index.html", "hometext2", "Home Text Content.")
b.AssertRenderCountPage(3)
b.AssertRenderCountPage(2)
}
func TestRebuildRenameDirectoryWithLeafBundle(t *testing.T) {
@@ -197,7 +169,7 @@ func TestRebuildRenameDirectoryWithBranchBundle(t *testing.T) {
b.AssertFileContent("public/mysectionrenamed/index.html", "My Section")
b.AssertFileContent("public/mysectionrenamed/mysectionbundle/index.html", "My Section Bundle")
b.AssertFileContent("public/mysectionrenamed/mysectionbundle/mysectionbundletext.txt", "My Section Bundle Text 2 Content.")
b.AssertRenderCountPage(3)
b.AssertRenderCountPage(2)
}
func TestRebuildRenameDirectoryWithRegularPageUsedInHome(t *testing.T) {
@@ -296,7 +268,7 @@ func TestRebuildRenameDirectoryWithBranchBundleFastRender(t *testing.T) {
b.AssertFileContent("public/mysectionrenamed/index.html", "My Section")
b.AssertFileContent("public/mysectionrenamed/mysectionbundle/index.html", "My Section Bundle")
b.AssertFileContent("public/mysectionrenamed/mysectionbundle/mysectionbundletext.txt", "My Section Bundle Text 2 Content.")
b.AssertRenderCountPage(3)
b.AssertRenderCountPage(2)
}
func TestRebuilErrorRecovery(t *testing.T) {
@@ -394,6 +366,8 @@ My short.
}
func TestRebuildBaseof(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
title = "Hugo Site"
@@ -408,13 +382,12 @@ Baseof: {{ .Title }}|
Home: {{ .Title }}|{{ .Content }}|
{{ end }}
`
testRebuildBothWatchingAndRunning(t, files, func(b *IntegrationTestBuilder) {
b.AssertFileContent("public/index.html", "Baseof: Hugo Site|", "Home: Hugo Site||")
b.EditFileReplaceFunc("layouts/_default/baseof.html", func(s string) string {
return strings.Replace(s, "Baseof", "Baseof Edited", 1)
}).Build()
b.AssertFileContent("public/index.html", "Baseof Edited: Hugo Site|", "Home: Hugo Site||")
})
b := Test(t, files, TestOptRunning())
b.AssertFileContent("public/index.html", "Baseof: Hugo Site|", "Home: Hugo Site||")
b.EditFileReplaceFunc("layouts/_default/baseof.html", func(s string) string {
return strings.Replace(s, "Baseof", "Baseof Edited", 1)
}).Build()
b.AssertFileContent("public/index.html", "Baseof Edited: Hugo Site|", "Home: Hugo Site||")
}
func TestRebuildSingleWithBaseof(t *testing.T) {
@@ -1541,103 +1514,3 @@ MyTemplate: {{ partial "MyTemplate.html" . }}|
b.AssertFileContent("public/index.html", "MyTemplate: MyTemplate Edited")
}
func TestRebuildEditAsciidocContentFile(t *testing.T) {
if !asciidocext.Supports() {
t.Skip("skip asciidoc")
}
files := `
-- hugo.toml --
baseURL = "https://example.com"
disableLiveReload = true
disableKinds = ["taxonomy", "term", "sitemap", "robotsTXT", "404", "rss", "home", "section"]
[security]
[security.exec]
allow = ['^python$', '^rst2html.*', '^asciidoctor$']
-- content/posts/p1.adoc --
---
title: "P1"
---
P1 Content.
-- content/posts/p2.adoc --
---
title: "P2"
---
P2 Content.
-- layouts/_default/single.html --
Single: {{ .Title }}|{{ .Content }}|
`
b := TestRunning(t, files)
b.AssertFileContent("public/posts/p1/index.html",
"Single: P1|<div class=\"paragraph\">\n<p>P1 Content.</p>\n</div>\n|")
b.AssertRenderCountPage(2)
b.AssertRenderCountContent(2)
b.EditFileReplaceAll("content/posts/p1.adoc", "P1 Content.", "P1 Content Edited.").Build()
b.AssertFileContent("public/posts/p1/index.html", "Single: P1|<div class=\"paragraph\">\n<p>P1 Content Edited.</p>\n</div>\n|")
b.AssertRenderCountPage(1)
b.AssertRenderCountContent(1)
}
func TestRebuildEditSingleListChangeUbuntuIssue12362(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ['rss','section','sitemap','taxonomy','term']
disableLiveReload = true
-- layouts/_default/list.html --
{{ range .Pages }}{{ .Title }}|{{ end }}
-- layouts/_default/single.html --
{{ .Title }}
-- content/p1.md --
---
title: p1
---
`
b := TestRunning(t, files)
b.AssertFileContent("public/index.html", "p1|")
b.AddFiles("content/p2.md", "---\ntitle: p2\n---").Build()
b.AssertFileContent("public/index.html", "p1|p2|") // this test passes, which doesn't match reality
}
func TestRebuildHomeThenPageIssue12436(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
baseURL = "https://example.com"
disableKinds = ['sitemap','taxonomy','term']
disableLiveReload = true
-- layouts/_default/list.html --
{{ .Content }}
-- layouts/_default/single.html --
{{ .Content }}
-- content/_index.md --
---
title: home
---
home-content|
-- content/p1/index.md --
---
title: p1
---
p1-content|
`
b := TestRunning(t, files)
b.AssertFileContent("public/index.html", "home-content|")
b.AssertFileContent("public/p1/index.html", "p1-content|")
b.AssertRenderCountPage(3)
b.EditFileReplaceAll("content/_index.md", "home-content", "home-content-foo").Build()
b.AssertFileContent("public/index.html", "home-content-foo")
b.AssertRenderCountPage(2) // Home page rss + html
b.EditFileReplaceAll("content/p1/index.md", "p1-content", "p1-content-foo").Build()
b.AssertFileContent("public/p1/index.html", "p1-content-foo")
}
-133
View File
@@ -200,136 +200,3 @@ Myshort Original.
b.Build()
b.AssertFileContent("public/p1/index.html", "Edited")
}
func TestRenderShortcodesEditSectionContentWithShortcodeInIncludedPageIssue12458(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableLiveReload = true
disableKinds = ["home", "taxonomy", "term", "rss", "sitemap", "robotsTXT", "404"]
-- content/mysection/_index.md --
---
title: "My Section"
---
## p1-h1
{{% include "p2" %}}
-- content/mysection/p2.md --
---
title: "p2"
---
### Original
{{% myshort %}}
-- layouts/shortcodes/include.html --
{{ $p := .Page.GetPage (.Get 0) }}
{{ $p.RenderShortcodes }}
-- layouts/shortcodes/myshort.html --
Myshort Original.
-- layouts/_default/list.html --
{{ .Content }}
`
b := TestRunning(t, files)
b.AssertFileContent("public/mysection/index.html", "p1-h1")
b.EditFileReplaceAll("content/mysection/_index.md", "p1-h1", "p1-h1 Edited").Build()
b.AssertFileContent("public/mysection/index.html", "p1-h1 Edited")
}
func TestRenderShortcodesNestedPageContextIssue12356(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ["taxonomy", "term", "rss", "sitemap", "robotsTXT", "404"]
-- layouts/_default/_markup/render-image.html --
{{- with .PageInner.Resources.Get .Destination -}}Image: {{ .RelPermalink }}|{{- end -}}
-- layouts/_default/_markup/render-link.html --
{{- with .PageInner.GetPage .Destination -}}Link: {{ .RelPermalink }}|{{- end -}}
-- layouts/_default/_markup/render-heading.html --
Heading: {{ .PageInner.Title }}: {{ .PlainText }}|
-- layouts/_default/_markup/render-codeblock.html --
CodeBlock: {{ .PageInner.Title }}: {{ .Type }}|
-- layouts/_default/list.html --
Content:{{ .Content }}|
Fragments: {{ with .Fragments }}{{.Identifiers }}{{ end }}|
-- layouts/_default/single.html --
Content:{{ .Content }}|
-- layouts/shortcodes/include.html --
{{ with site.GetPage (.Get 0) }}
{{ .RenderShortcodes }}
{{ end }}
-- content/markdown/_index.md --
---
title: "Markdown"
---
# H1
|{{% include "/posts/p1" %}}|
![kitten](pixel3.png "Pixel 3")
§§§go
fmt.Println("Hello")
§§§
-- content/markdown2/_index.md --
---
title: "Markdown 2"
---
|{{< include "/posts/p1" >}}|
-- content/html/_index.html --
---
title: "HTML"
---
|{{% include "/posts/p1" %}}|
-- content/posts/p1/index.md --
---
title: "p1"
---
## H2-p1
![kitten](pixel1.png "Pixel 1")
![kitten](pixel2.png "Pixel 2")
[p2](p2)
§§§bash
echo "Hello"
§§§
-- content/posts/p2/index.md --
---
title: "p2"
---
-- content/posts/p1/pixel1.png --
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
-- content/posts/p1/pixel2.png --
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
-- content/markdown/pixel3.png --
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
-- content/html/pixel4.png --
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
`
b := Test(t, files)
b.AssertFileContent("public/markdown/index.html",
// Images.
"Image: /posts/p1/pixel1.png|\nImage: /posts/p1/pixel2.png|\n|\nImage: /markdown/pixel3.png|</p>\n|",
// Links.
"Link: /posts/p2/|",
// Code blocks
"CodeBlock: p1: bash|", "CodeBlock: Markdown: go|",
// Headings.
"Heading: Markdown: H1|", "Heading: p1: H2-p1|",
// Fragments.
"Fragments: [h1 h2-p1]|",
// Check that the special context markup is not rendered.
"! hugo_ctx",
)
b.AssertFileContent("public/markdown2/index.html", "! hugo_ctx", "Content:<p>|\n ![kitten](pixel1.png \"Pixel 1\")\n![kitten](pixel2.png \"Pixel 2\")\n|</p>\n|")
b.AssertFileContent("public/html/index.html", "! hugo_ctx")
}
+1 -7
View File
@@ -321,16 +321,10 @@ func prepareShortcode(
// Allow the caller to delay the rendering of the shortcode if needed.
var fn shortcodeRenderFunc = func(ctx context.Context) ([]byte, bool, error) {
if p.m.pageConfig.ContentMediaType.IsMarkdown() && sc.doMarkup {
// Signal downwards that the content rendered will be
// parsed and rendered by Goldmark.
ctx = tpl.Context.IsInGoldmark.Set(ctx, true)
}
r, err := doRenderShortcode(ctx, level, s, tplVariants, sc, parent, p, isRenderString)
if err != nil {
return nil, false, toParseErr(err)
}
b, hasVariants, err := r.renderShortcode(ctx)
if err != nil {
return nil, false, toParseErr(err)
@@ -449,7 +443,7 @@ func doRenderShortcode(
// unchanged.
// 2 If inner does not have a newline, strip the wrapping <p> block and
// the newline.
switch p.m.pageConfig.Content.Markup {
switch p.m.pageConfig.Markup {
case "", "markdown":
if match, _ := regexp.MatchString(innerNewlineRegexp, inner); !match {
cleaner, err := regexp.Compile(innerCleanupRegexp)
+34 -63
View File
@@ -62,7 +62,6 @@ import (
)
func (s *Site) Taxonomies() page.TaxonomyList {
s.checkReady()
s.init.taxonomies.Do(context.Background())
return s.taxonomies
}
@@ -205,7 +204,6 @@ type siteRenderingContext struct {
}
func (s *Site) Menus() navigation.Menus {
s.checkReady()
s.init.menus.Do(context.Background())
return s.menus
}
@@ -374,33 +372,19 @@ func (s *Site) watching() bool {
type whatChanged struct {
mu sync.Mutex
needsPagesAssembly bool
identitySet identity.Identities
contentChanged bool
identitySet identity.Identities
}
func (w *whatChanged) Add(ids ...identity.Identity) {
w.mu.Lock()
defer w.mu.Unlock()
if w.identitySet == nil {
w.identitySet = make(identity.Identities)
}
for _, id := range ids {
w.identitySet[id] = true
}
}
func (w *whatChanged) Clear() {
w.mu.Lock()
defer w.mu.Unlock()
w.clear()
}
func (w *whatChanged) clear() {
w.identitySet = identity.Identities{}
}
func (w *whatChanged) Changes() []identity.Identity {
if w == nil || w.identitySet == nil {
return nil
@@ -408,14 +392,6 @@ func (w *whatChanged) Changes() []identity.Identity {
return w.identitySet.AsSlice()
}
func (w *whatChanged) Drain() []identity.Identity {
w.mu.Lock()
defer w.mu.Unlock()
ids := w.identitySet.AsSlice()
w.clear()
return ids
}
// RegisterMediaTypes will register the Site's media types in the mime
// package, so it will behave correctly with Hugo's built-in server.
func (s *Site) RegisterMediaTypes() {
@@ -448,35 +424,7 @@ func (h *HugoSites) fileEventsFilter(events []fsnotify.Event) []fsnotify.Event {
events[n] = ev
n++
}
events = events[:n]
eventOrdinal := func(e fsnotify.Event) int {
// Pull the structural changes to the top.
if e.Op.Has(fsnotify.Create) {
return 1
}
if e.Op.Has(fsnotify.Remove) {
return 2
}
if e.Op.Has(fsnotify.Rename) {
return 3
}
if e.Op.Has(fsnotify.Write) {
return 4
}
return 5
}
sort.Slice(events, func(i, j int) bool {
// First sort by event type.
if eventOrdinal(events[i]) != eventOrdinal(events[j]) {
return eventOrdinal(events[i]) < eventOrdinal(events[j])
}
// Then sort by name.
return events[i].Name < events[j].Name
})
return events
return events[:n]
}
type fileEventInfo struct {
@@ -546,17 +494,41 @@ func (h *HugoSites) fileEventsApplyInfo(events []fsnotify.Event) []fileEventInfo
return infos
}
func (h *HugoSites) fileEventsTrim(events []fsnotify.Event) []fsnotify.Event {
seen := make(map[string]bool)
func (h *HugoSites) fileEventsTranslate(events []fsnotify.Event) []fsnotify.Event {
eventMap := make(map[string][]fsnotify.Event)
// We often get a Remove etc. followed by a Create, a Create followed by a Write.
// Remove the superfluous events to make the update logic simpler.
for _, ev := range events {
eventMap[ev.Name] = append(eventMap[ev.Name], ev)
}
n := 0
for _, ev := range events {
if seen[ev.Name] {
continue
mapped := eventMap[ev.Name]
// Keep one
found := false
var kept fsnotify.Event
for i, ev2 := range mapped {
if i == 0 {
kept = ev2
}
if ev2.Op&fsnotify.Write == fsnotify.Write {
kept = ev2
found = true
}
if !found && ev2.Op&fsnotify.Create == fsnotify.Create {
kept = ev2
}
}
seen[ev.Name] = true
events[n] = ev
events[n] = kept
n++
}
return events
}
@@ -683,7 +655,7 @@ func (s *Site) assembleMenus() error {
if sectionPagesMenu != "" {
if err := s.pageMap.forEachPage(pagePredicates.ShouldListGlobal, func(p *pageState) (bool, error) {
if p.Kind() != kinds.KindSection || !p.m.shouldBeCheckedForMenuDefinitions() {
if p.IsHome() || !p.m.shouldBeCheckedForMenuDefinitions() {
return false, nil
}
@@ -810,7 +782,6 @@ func (s *Site) errorCollator(results <-chan error, errs chan<- error) {
// as possible for existing sites. Most sites will use {{ .Site.GetPage "section" "my/section" }},
// i.e. 2 arguments, so we test for that.
func (s *Site) GetPage(ref ...string) (page.Page, error) {
s.checkReady()
p, err := s.s.getPageForRefs(ref...)
if p == nil {
+3 -25
View File
@@ -32,7 +32,6 @@ import (
"github.com/gohugoio/hugo/config/allconfig"
"github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/hugolib/doctree"
"github.com/gohugoio/hugo/hugolib/pagesfromdata"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/langs"
"github.com/gohugoio/hugo/langs/i18n"
@@ -52,15 +51,7 @@ import (
var _ page.Site = (*Site)(nil)
type siteState int
const (
siteStateInit siteState = iota
siteStateReady
)
type Site struct {
state siteState
conf *allconfig.Config
language *langs.Language
languagei int
@@ -132,14 +123,14 @@ func NewHugoSites(cfg deps.DepsCfg) (*HugoSites, error) {
HandlerPost: logHookLast,
Stdout: cfg.LogOut,
Stderr: cfg.LogOut,
StoreErrors: conf.Watching(),
StoreErrors: conf.Running(),
SuppressStatements: conf.IgnoredLogs(),
}
logger = loggers.New(logOpts)
}
memCache := dynacache.New(dynacache.Options{Watching: conf.Watching(), Log: logger})
memCache := dynacache.New(dynacache.Options{Running: conf.Running(), Log: logger})
firstSiteDeps := &deps.Deps{
Fs: cfg.Fs,
@@ -175,8 +166,7 @@ func NewHugoSites(cfg deps.DepsCfg) (*HugoSites, error) {
treeResources: doctree.New(
treeConfig,
),
treeTaxonomyEntries: doctree.NewTreeShiftTree[*weightedContentNode](doctree.DimensionLanguage.Index(), len(confm.Languages)),
treePagesFromTemplateAdapters: doctree.NewTreeShiftTree[*pagesfromdata.PagesFromTemplate](doctree.DimensionLanguage.Index(), len(confm.Languages)),
treeTaxonomyEntries: doctree.NewTreeShiftTree[*weightedContentNode](doctree.DimensionLanguage.Index(), len(confm.Languages)),
}
pageTrees.createMutableTrees()
@@ -425,7 +415,6 @@ func (s *Site) Current() page.Site {
// MainSections returns the list of main sections.
func (s *Site) MainSections() []string {
s.checkReady()
return s.conf.C.MainSections
}
@@ -444,7 +433,6 @@ func (s *Site) BaseURL() string {
// Deprecated: Use .Site.Lastmod instead.
func (s *Site) LastChange() time.Time {
s.checkReady()
hugo.Deprecate(".Site.LastChange", "Use .Site.Lastmod instead.", "v0.123.0")
return s.lastmod
}
@@ -533,7 +521,6 @@ func (s *Site) ForEeachIdentityByName(name string, f func(identity.Identity) boo
// Pages returns all pages.
// This is for the current language only.
func (s *Site) Pages() page.Pages {
s.checkReady()
return s.pageMap.getPagesInSection(
pageMapQueryPagesInSection{
pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{
@@ -550,7 +537,6 @@ func (s *Site) Pages() page.Pages {
// RegularPages returns all the regular pages.
// This is for the current language only.
func (s *Site) RegularPages() page.Pages {
s.checkReady()
return s.pageMap.getPagesInSection(
pageMapQueryPagesInSection{
pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{
@@ -565,18 +551,10 @@ func (s *Site) RegularPages() page.Pages {
// AllPages returns all pages for all sites.
func (s *Site) AllPages() page.Pages {
s.checkReady()
return s.h.Pages()
}
// AllRegularPages returns all regular pages for all sites.
func (s *Site) AllRegularPages() page.Pages {
s.checkReady()
return s.h.RegularPages()
}
func (s *Site) checkReady() {
if s.state != siteStateReady {
panic("this method cannot be called before the site is fully initialized")
}
}
-2
View File
@@ -19,12 +19,10 @@ import (
// Sections returns the top level sections.
func (s *Site) Sections() page.Pages {
s.checkReady()
return s.Home().Sections()
}
// Home is a shortcut to the home page, equivalent to .Site.GetPage "home".
func (s *Site) Home() page.Page {
s.checkReady()
return s.s.home
}
+2 -9
View File
@@ -1,14 +1,7 @@
# Release env.
# These will be replaced by script before release.
HUGORELEASER_TAG=v0.126.1
HUGORELEASER_COMMITISH=3d40aba512931031921463dafc172c0d124437b8
HUGORELEASER_TAG=v0.124.1
HUGORELEASER_COMMITISH=db083b05f16c945fec04f745f0ca8640560cf1ec
-1
View File
@@ -61,7 +61,6 @@ func (tp *TranslationProvider) NewResource(dst *deps.Deps) error {
hugofs.WalkwayConfig{
Fs: dst.BaseFs.I18n.Fs,
IgnoreFile: dst.SourceSpec.IgnoreFile,
PathParser: dst.SourceSpec.Cfg.PathParser(),
WalkFn: func(path string, info hugofs.FileMetaInfo) error {
if info.IsDir() {
return nil
-34
View File
@@ -1,34 +0,0 @@
/*
Hugo adds a specific prefix, "__hugo_navigate", to the path in certain situations to signal
navigation to another content page.
*/
function HugoReload() {}
HugoReload.identifier = 'hugoReloader';
HugoReload.version = '0.9';
HugoReload.prototype.reload = function (path, options) {
var prefix = '__hugo_navigate';
if (path.lastIndexOf(prefix, 0) !== 0) {
return false;
}
path = path.substring(prefix.length);
var portChanged = options.overrideURL && options.overrideURL != window.location.port;
if (!portChanged && window.location.pathname === path) {
window.location.reload();
} else {
if (portChanged) {
window.location = location.protocol + '//' + location.hostname + ':' + options.overrideURL + path;
} else {
window.location.pathname = path;
}
}
return true;
};
LiveReload.addPlugin(HugoReload);
-61
View File
@@ -1,61 +0,0 @@
//go:generate go run main.go
package main
import (
_ "embed"
"fmt"
"io"
"log"
"net/http"
"os"
"github.com/evanw/esbuild/pkg/api"
)
//go:embed livereload-hugo-plugin.js
var livereloadHugoPluginJS string
func main() {
// 4.0.2
// To upgrade to a new version, change to the commit hash of the version you want to upgrade to
// then run mage generate from the root.
const liveReloadCommit = "d803a41804d2d71e0814c4e9e3233e78991024d9"
liveReloadSourceURL := fmt.Sprintf("https://raw.githubusercontent.com/livereload/livereload-js/%s/dist/livereload.js", liveReloadCommit)
func() {
resp, err := http.Get(liveReloadSourceURL)
must(err)
defer resp.Body.Close()
b, err := io.ReadAll(resp.Body)
must(err)
// Write the unminified livereload.js file.
err = os.WriteFile("../livereload.js", b, 0o644)
must(err)
// Bundle and minify with ESBuild.
result := api.Build(api.BuildOptions{
Stdin: &api.StdinOptions{
Contents: string(b) + livereloadHugoPluginJS,
},
Outfile: "../livereload.min.js",
Bundle: true,
Target: api.ES2015,
Write: true,
MinifyWhitespace: true,
MinifyIdentifiers: true,
MinifySyntax: true,
})
if len(result.Errors) > 0 {
log.Fatal(result.Errors)
}
}()
}
func must(err error) {
if err != nil {
log.Fatal(err)
}
}
+44 -5
View File
@@ -1,4 +1,4 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
// Copyright 2015 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.
@@ -50,7 +50,6 @@ import (
)
// Prefix to signal to LiveReload that we need to navigate to another path.
// Do not change this.
const hugoNavigatePrefix = "__hugo_navigate"
var upgrader = &websocket.Upgrader{
@@ -145,8 +144,48 @@ func ServeJS(w http.ResponseWriter, r *http.Request) {
}
func liveReloadJS() []byte {
return []byte(livereloadJS)
return []byte(livereloadJS + hugoLiveReloadPlugin)
}
//go:embed livereload.min.js
var livereloadJS string
var (
// This is a patched version, see https://github.com/livereload/livereload-js/pull/84
//go:embed livereload.js
livereloadJS string
hugoLiveReloadPlugin = fmt.Sprintf(`
/*
Hugo adds a specific prefix, "__hugo_navigate", to the path in certain situations to signal
navigation to another content page.
*/
function HugoReload() {}
HugoReload.identifier = 'hugoReloader';
HugoReload.version = '0.9';
HugoReload.prototype.reload = function(path, options) {
var prefix = %q;
if (path.lastIndexOf(prefix, 0) !== 0) {
return false
}
path = path.substring(prefix.length);
var portChanged = options.overrideURL && options.overrideURL != window.location.port
if (!portChanged && window.location.pathname === path) {
window.location.reload();
} else {
if (portChanged) {
window.location = location.protocol + "//" + location.hostname + ":" + options.overrideURL + path;
} else {
window.location.pathname = path;
}
}
return true;
};
LiveReload.addPlugin(HugoReload)
`, hugoNavigatePrefix)
)
+1 -3795
View File
File diff suppressed because one or more lines are too long
-7
View File
File diff suppressed because one or more lines are too long
+2 -1
View File
@@ -79,7 +79,8 @@ func flagEnv() map[string]string {
// Generate autogen packages
func Generate() error {
generatorPackages := []string{
"livereload/gen",
//"tpl/tplimpl/embedded/generate",
//"resources/page/generate",
}
for _, pkg := range generatorPackages {
+4 -5
View File
@@ -135,11 +135,10 @@ func (b Bytes) Bytes() []byte {
// DocumentContext holds contextual information about the document to convert.
type DocumentContext struct {
Document any // May be nil. Usually a page.Page
DocumentLookup func(uint64) any // May be nil.
DocumentID string
DocumentName string
Filename string
Document any // May be nil. Usually a page.Page
DocumentID string
DocumentName string
Filename string
}
// RenderContext holds contextual information about the content to render.
+7 -13
View File
@@ -32,7 +32,8 @@ type AttributesProvider interface {
// LinkContext is the context passed to a link render hook.
type LinkContext interface {
PageProvider
// The Page being rendered.
Page() any
// The link URL.
Destination() string
@@ -63,7 +64,6 @@ type ImageLinkContext interface {
type CodeblockContext interface {
AttributesProvider
text.Positioner
PageProvider
// Chroma highlighting processing options. This will only be filled if Type is a known Chroma Lexer.
Options() map[string]any
@@ -76,6 +76,9 @@ type CodeblockContext interface {
// Zero-based ordinal for all code blocks in the current document.
Ordinal() int
// The owning Page.
Page() any
}
type AttributesOptionsSliceProvider interface {
@@ -98,7 +101,8 @@ type IsDefaultCodeBlockRendererProvider interface {
// HeadingContext contains accessors to all attributes that a HeadingRenderer
// can use to render a heading.
type HeadingContext interface {
PageProvider
// Page is the page containing the heading.
Page() any
// Level is the level of the header (i.e. 1 for top-level, 2 for sub-level, etc.).
Level() int
// Anchor is the HTML id assigned to the heading.
@@ -112,16 +116,6 @@ type HeadingContext interface {
AttributesProvider
}
type PageProvider interface {
// Page is the page being rendered.
Page() any
// PageInner may be different than Page when .RenderShortcodes is in play.
// The main use case for this is to include other pages' markdown into the current page
// but resolve resources and pages relative to the original.
PageInner() any
}
// HeadingRenderer describes a uniquely identifiable rendering hook.
type HeadingRenderer interface {
// RenderHeading writes the rendered content to w using the data in w.
+5 -22
View File
@@ -108,7 +108,6 @@ func (r *htmlRenderer) renderCodeBlock(w util.BufWriter, src []byte, node ast.No
}
cbctx := &codeBlockContext{
page: ctx.DocumentContext().Document,
pageInner: r.getPageInner(ctx),
lang: lang,
code: s,
ordinal: ordinal,
@@ -133,6 +132,7 @@ func (r *htmlRenderer) renderCodeBlock(w util.BufWriter, src []byte, node ast.No
w,
cbctx,
)
if err != nil {
return ast.WalkContinue, herrors.NewFileErrorFromPos(err, cbctx.createPos())
}
@@ -140,24 +140,11 @@ func (r *htmlRenderer) renderCodeBlock(w util.BufWriter, src []byte, node ast.No
return ast.WalkContinue, nil
}
func (r *htmlRenderer) getPageInner(rctx *render.Context) any {
pid := rctx.PeekPid()
if pid > 0 {
if lookup := rctx.DocumentContext().DocumentLookup; lookup != nil {
if v := rctx.DocumentContext().DocumentLookup(pid); v != nil {
return v
}
}
}
return rctx.DocumentContext().Document
}
type codeBlockContext struct {
page any
pageInner any
lang string
code string
ordinal int
page any
lang string
code string
ordinal int
// This is only used in error situations and is expensive to create,
// to delay creation until needed.
@@ -172,10 +159,6 @@ func (c *codeBlockContext) Page() any {
return c.page
}
func (c *codeBlockContext) PageInner() any {
return c.pageInner
}
func (c *codeBlockContext) Type() string {
return c.lang
}
-12
View File
@@ -17,9 +17,7 @@ package goldmark
import (
"bytes"
"github.com/gohugoio/hugo-goldmark-extensions/extras"
"github.com/gohugoio/hugo-goldmark-extensions/passthrough"
"github.com/gohugoio/hugo/markup/goldmark/hugocontext"
"github.com/yuin/goldmark/util"
"github.com/gohugoio/hugo/markup/goldmark/codeblocks"
@@ -105,7 +103,6 @@ func newMarkdown(pcfg converter.ProviderConfig) goldmark.Markdown {
renderer.WithNodeRenderers(util.Prioritized(emoji.NewHTMLRenderer(), 200)))
var (
extensions = []goldmark.Extender{
hugocontext.New(),
newLinks(cfg),
newTocExtension(tocRendererOptions),
}
@@ -114,15 +111,6 @@ func newMarkdown(pcfg converter.ProviderConfig) goldmark.Markdown {
extensions = append(extensions, images.New(cfg.Parser.WrapStandAloneImageWithinParagraph))
extensions = append(extensions, extras.New(
extras.Config{
Insert: extras.InsertConfig{Enable: cfg.Extensions.Extras.Insert.Enable},
Mark: extras.MarkConfig{Enable: cfg.Extensions.Extras.Mark.Enable},
Subscript: extras.SubscriptConfig{Enable: cfg.Extensions.Extras.Subscript.Enable},
Superscript: extras.SuperscriptConfig{Enable: cfg.Extensions.Extras.Superscript.Enable},
},
))
if mcfg.Highlight.CodeFences {
extensions = append(extensions, codeblocks.New())
}
+1 -41
View File
@@ -49,20 +49,6 @@ var Default = Config{
EastAsianLineBreaksStyle: "simple",
EscapedSpace: false,
},
Extras: Extras{
Superscript: Superscript{
Enable: false,
},
Subscript: Subscript{
Enable: false,
},
Insert: Insert{
Enable: false,
},
Mark: Mark{
Enable: false,
},
},
Passthrough: Passthrough{
Enable: false,
Delimiters: DelimitersConfig{
@@ -126,7 +112,6 @@ type Extensions struct {
Typographer Typographer
Footnote bool
DefinitionList bool
Extras Extras
Passthrough Passthrough
// GitHub flavored markdown
@@ -165,32 +150,7 @@ type Typographer struct {
Apostrophe string
}
// Extras holds extras configuration.
// github.com/hugoio/hugo-goldmark-extensions/extras
type Extras struct {
Insert Insert
Mark Mark
Subscript Subscript
Superscript Superscript
}
type Insert struct {
Enable bool
}
type Mark struct {
Enable bool
}
type Subscript struct {
Enable bool
}
type Superscript struct {
Enable bool
}
// Passthrough holds passthrough configuration.
// Passthrough hold passthrough configuration.
// github.com/hugoio/hugo-goldmark-extensions/passthrough
type Passthrough struct {
// Whether to enable the extension
@@ -744,53 +744,3 @@ a^*=x-b^*
%!%
`)
}
func TestExtrasExtension(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ['page','rss','section','sitemap','taxonomy','term']
[markup.goldmark.extensions.extras.insert]
enable = false
[markup.goldmark.extensions.extras.mark]
enable = false
[markup.goldmark.extensions.extras.subscript]
enable = false
[markup.goldmark.extensions.extras.superscript]
enable = false
-- layouts/index.html --
{{ .Content }}
-- content/_index.md --
---
title: home
---
++insert++
==mark==
H~2~0
1^st^
`
b := hugolib.Test(t, files)
b.AssertFileContent("public/index.html",
"<p>++insert++</p>",
"<p>==mark==</p>",
"<p>H~2~0</p>",
"<p>1^st^</p>",
)
files = strings.ReplaceAll(files, "enable = false", "enable = true")
b = hugolib.Test(t, files)
b.AssertFileContent("public/index.html",
"<p><ins>insert</ins></p>",
"<p><mark>mark</mark></p>",
"<p>H<sub>2</sub>0</p>",
"<p>1<sup>st</sup></p>",
)
}
-165
View File
@@ -1,165 +0,0 @@
// 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 hugocontext
import (
"bytes"
"fmt"
"strconv"
"github.com/gohugoio/hugo/bufferpool"
"github.com/gohugoio/hugo/markup/goldmark/internal/render"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
)
func New() goldmark.Extender {
return &hugoContextExtension{}
}
// Wrap wraps the given byte slice in a Hugo context that used to determine the correct Page
// in .RenderShortcodes.
func Wrap(b []byte, pid uint64) string {
buf := bufferpool.GetBuffer()
defer bufferpool.PutBuffer(buf)
buf.Write(prefix)
buf.WriteString(" pid=")
buf.WriteString(strconv.FormatUint(pid, 10))
buf.Write(endDelim)
buf.WriteByte('\n')
buf.Write(b)
buf.Write(prefix)
buf.Write(closingDelimAndNewline)
return buf.String()
}
var kindHugoContext = ast.NewNodeKind("HugoContext")
// HugoContext is a node that represents a Hugo context.
type HugoContext struct {
ast.BaseInline
Closing bool
// Internal page ID. Not persisted.
Pid uint64
}
// Dump implements Node.Dump.
func (n *HugoContext) Dump(source []byte, level int) {
m := map[string]string{}
m["Pid"] = fmt.Sprintf("%v", n.Pid)
ast.DumpHelper(n, source, level, m, nil)
}
func (n *HugoContext) parseAttrs(attrBytes []byte) {
keyPairs := bytes.Split(attrBytes, []byte(" "))
for _, keyPair := range keyPairs {
kv := bytes.Split(keyPair, []byte("="))
if len(kv) != 2 {
continue
}
key := string(kv[0])
val := string(kv[1])
switch key {
case "pid":
pid, _ := strconv.ParseUint(val, 10, 64)
n.Pid = pid
}
}
}
func (h *HugoContext) Kind() ast.NodeKind {
return kindHugoContext
}
var (
prefix = []byte("{{__hugo_ctx")
endDelim = []byte("}}")
closingDelimAndNewline = []byte("/}}\n")
)
var _ parser.InlineParser = (*hugoContextParser)(nil)
type hugoContextParser struct{}
func (s *hugoContextParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node {
line, _ := block.PeekLine()
if !bytes.HasPrefix(line, prefix) {
return nil
}
end := bytes.Index(line, endDelim)
if end == -1 {
return nil
}
block.Advance(end + len(endDelim) + 1) // +1 for the newline
if line[end-1] == '/' {
return &HugoContext{Closing: true}
}
attrBytes := line[len(prefix)+1 : end]
h := &HugoContext{}
h.parseAttrs(attrBytes)
return h
}
func (a *hugoContextParser) Trigger() []byte {
return []byte{'{'}
}
type hugoContextRenderer struct{}
func (r *hugoContextRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(kindHugoContext, r.handleHugoContext)
}
func (r *hugoContextRenderer) handleHugoContext(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
hctx := node.(*HugoContext)
ctx, ok := w.(*render.Context)
if !ok {
return ast.WalkContinue, nil
}
if hctx.Closing {
_ = ctx.PopPid()
} else {
ctx.PushPid(hctx.Pid)
}
return ast.WalkContinue, nil
}
type hugoContextExtension struct{}
func (a *hugoContextExtension) Extend(m goldmark.Markdown) {
m.Parser().AddOptions(
parser.WithInlineParsers(
util.Prioritized(&hugoContextParser{}, 50),
),
)
m.Renderer().AddOptions(
renderer.WithNodeRenderers(
util.Prioritized(&hugoContextRenderer{}, 50),
),
)
}
@@ -1,34 +0,0 @@
// 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 hugocontext
import (
"testing"
qt "github.com/frankban/quicktest"
)
func TestWrap(t *testing.T) {
c := qt.New(t)
b := []byte("test")
c.Assert(Wrap(b, 42), qt.Equals, "{{__hugo_ctx pid=42}}\ntest{{__hugo_ctx/}}\n")
}
func BenchmarkWrap(b *testing.B) {
for i := 0; i < b.N; i++ {
Wrap([]byte("test"), 42)
}
}
@@ -41,7 +41,6 @@ func (b *BufWriter) Flush() error {
type Context struct {
*BufWriter
positions []int
pids []uint64
ContextData
}
@@ -56,30 +55,6 @@ func (ctx *Context) PopPos() int {
return p
}
// PushPid pushes a new page ID to the stack.
func (ctx *Context) PushPid(pid uint64) {
ctx.pids = append(ctx.pids, pid)
}
// PeekPid returns the current page ID without removing it from the stack.
func (ctx *Context) PeekPid() uint64 {
if len(ctx.pids) == 0 {
return 0
}
return ctx.pids[len(ctx.pids)-1]
}
// PopPid pops the last page ID from the stack.
func (ctx *Context) PopPid() uint64 {
if len(ctx.pids) == 0 {
return 0
}
i := len(ctx.pids) - 1
p := ctx.pids[i]
ctx.pids = ctx.pids[:i]
return p
}
type ContextData interface {
RenderContext() converter.RenderContext
DocumentContext() converter.DocumentContext
-26
View File
@@ -49,7 +49,6 @@ func newLinks(cfg goldmark_config.Config) goldmark.Extender {
type linkContext struct {
page any
pageInner any
destination string
title string
text hstring.RenderedString
@@ -65,10 +64,6 @@ func (ctx linkContext) Page() any {
return ctx.page
}
func (ctx linkContext) PageInner() any {
return ctx.pageInner
}
func (ctx linkContext) Text() hstring.RenderedString {
return ctx.text
}
@@ -97,7 +92,6 @@ func (ctx imageLinkContext) Ordinal() int {
type headingContext struct {
page any
pageInner any
level int
anchor string
text hstring.RenderedString
@@ -109,10 +103,6 @@ func (ctx headingContext) Page() any {
return ctx.page
}
func (ctx headingContext) PageInner() any {
return ctx.pageInner
}
func (ctx headingContext) Level() int {
return ctx.level
}
@@ -196,7 +186,6 @@ func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.N
imageLinkContext{
linkContext: linkContext{
page: ctx.DocumentContext().Document,
pageInner: r.getPageInner(ctx),
destination: string(n.Destination),
title: string(n.Title),
text: hstring.RenderedString(text),
@@ -211,18 +200,6 @@ func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.N
return ast.WalkContinue, err
}
func (r *hookedRenderer) getPageInner(rctx *render.Context) any {
pid := rctx.PeekPid()
if pid > 0 {
if lookup := rctx.DocumentContext().DocumentLookup; lookup != nil {
if v := rctx.DocumentContext().DocumentLookup(pid); v != nil {
return v
}
}
}
return rctx.DocumentContext().Document
}
func (r *hookedRenderer) filterInternalAttributes(attrs []ast.Attribute) []ast.Attribute {
n := 0
for _, x := range attrs {
@@ -297,7 +274,6 @@ func (r *hookedRenderer) renderLink(w util.BufWriter, source []byte, node ast.No
w,
linkContext{
page: ctx.DocumentContext().Document,
pageInner: r.getPageInner(ctx),
destination: string(n.Destination),
title: string(n.Title),
text: hstring.RenderedString(text),
@@ -363,7 +339,6 @@ func (r *hookedRenderer) renderAutoLink(w util.BufWriter, source []byte, node as
w,
linkContext{
page: ctx.DocumentContext().Document,
pageInner: r.getPageInner(ctx),
destination: url,
text: hstring.RenderedString(label),
plainText: label,
@@ -448,7 +423,6 @@ func (r *hookedRenderer) renderHeading(w util.BufWriter, source []byte, node ast
w,
headingContext{
page: ctx.DocumentContext().Document,
pageInner: r.getPageInner(ctx),
level: n.Level,
anchor: string(anchor),
text: hstring.RenderedString(text),
+6 -23
View File
@@ -19,7 +19,6 @@ import (
"strings"
"github.com/gohugoio/hugo/markup/highlight"
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/markup/markup_config"
@@ -45,7 +44,7 @@ func NewConverterProvider(cfg converter.ProviderConfig) (ConverterProvider, erro
defaultHandler := mcfg.DefaultMarkdownHandler
var defaultFound bool
add := func(p converter.ProviderProvider, subType string, aliases ...string) error {
add := func(p converter.ProviderProvider, aliases ...string) error {
c, err := p.New(cfg)
if err != nil {
return err
@@ -54,7 +53,6 @@ func NewConverterProvider(cfg converter.ProviderConfig) (ConverterProvider, erro
name := c.Name()
aliases = append(aliases, name)
aliases = append(aliases, subType)
if strings.EqualFold(name, defaultHandler) {
aliases = append(aliases, "markdown")
@@ -65,21 +63,19 @@ func NewConverterProvider(cfg converter.ProviderConfig) (ConverterProvider, erro
return nil
}
contentTypes := cfg.Conf.ContentTypes().(media.ContentTypes)
if err := add(goldmark.Provider, contentTypes.Markdown.SubType, contentTypes.Markdown.Suffixes()...); err != nil {
if err := add(goldmark.Provider); err != nil {
return nil, err
}
if err := add(asciidocext.Provider, contentTypes.AsciiDoc.SubType, contentTypes.AsciiDoc.Suffixes()...); err != nil {
if err := add(asciidocext.Provider, "ad", "adoc"); err != nil {
return nil, err
}
if err := add(rst.Provider, contentTypes.ReStructuredText.SubType, contentTypes.ReStructuredText.Suffixes()...); err != nil {
if err := add(rst.Provider); err != nil {
return nil, err
}
if err := add(pandoc.Provider, contentTypes.Pandoc.SubType, contentTypes.Pandoc.Suffixes()...); err != nil {
if err := add(pandoc.Provider, "pdc"); err != nil {
return nil, err
}
if err := add(org.Provider, contentTypes.EmacsOrgMode.SubType, contentTypes.EmacsOrgMode.Suffixes()...); err != nil {
if err := add(org.Provider); err != nil {
return nil, err
}
@@ -137,16 +133,3 @@ func addConverter(m map[string]converter.Provider, c converter.Provider, aliases
m[alias] = c
}
}
// ResolveMarkup returns the markup type.
func ResolveMarkup(s string) string {
s = strings.ToLower(s)
switch s {
case "goldmark":
return media.DefaultContentTypes.Markdown.SubType
case "asciidocext":
return media.DefaultContentTypes.AsciiDoc.SubType
default:
return s
}
}
+13 -18
View File
@@ -34,12 +34,8 @@ type BuiltinTypes struct {
OpenTypeFontType Type
// Common document types
PDFType Type
MarkdownType Type
EmacsOrgModeType Type
AsciiDocType Type
PandocType Type
ReStructuredTextType Type
PDFType Type
MarkdownType Type
// Common video types
AVIType Type
@@ -89,12 +85,8 @@ var Builtin = BuiltinTypes{
OpenTypeFontType: Type{Type: "font/otf"},
// Common document types
PDFType: Type{Type: "application/pdf"},
MarkdownType: Type{Type: "text/markdown"},
AsciiDocType: Type{Type: "text/asciidoc"}, // https://github.com/asciidoctor/asciidoctor/issues/2502
PandocType: Type{Type: "text/pandoc"},
ReStructuredTextType: Type{Type: "text/rst"}, // https://docutils.sourceforge.io/FAQ.html#what-s-the-official-mime-type-for-restructuredtext-data
EmacsOrgModeType: Type{Type: "text/org"},
PDFType: Type{Type: "application/pdf"},
MarkdownType: Type{Type: "text/markdown"},
// Common video types
AVIType: Type{Type: "video/x-msvideo"},
@@ -116,7 +108,7 @@ var defaultMediaTypesConfig = map[string]any{
"text/x-scss": map[string]any{"suffixes": []string{"scss"}},
"text/x-sass": map[string]any{"suffixes": []string{"sass"}},
"text/csv": map[string]any{"suffixes": []string{"csv"}},
"text/html": map[string]any{"suffixes": []string{"html", "htm"}},
"text/html": map[string]any{"suffixes": []string{"html"}},
"text/javascript": map[string]any{"suffixes": []string{"js", "jsm", "mjs"}},
"text/typescript": map[string]any{"suffixes": []string{"ts"}},
"text/tsx": map[string]any{"suffixes": []string{"tsx"}},
@@ -145,11 +137,7 @@ var defaultMediaTypesConfig = map[string]any{
// Common document types
"application/pdf": map[string]any{"suffixes": []string{"pdf"}},
"text/markdown": map[string]any{"suffixes": []string{"md", "mdown", "markdown"}},
"text/asciidoc": map[string]any{"suffixes": []string{"adoc", "asciidoc", "ad"}},
"text/pandoc": map[string]any{"suffixes": []string{"pandoc", "pdc"}},
"text/rst": map[string]any{"suffixes": []string{"rst"}},
"text/org": map[string]any{"suffixes": []string{"org"}},
"text/markdown": map[string]any{"suffixes": []string{"md", "markdown"}},
// Common video types
"video/x-msvideo": map[string]any{"suffixes": []string{"avi"}},
@@ -164,3 +152,10 @@ var defaultMediaTypesConfig = map[string]any{
"application/octet-stream": map[string]any{},
}
func init() {
// Apply delimiter to all.
for _, m := range defaultMediaTypesConfig {
m.(map[string]any)["delimiter"] = "."
}
}
+2 -120
View File
@@ -14,14 +14,13 @@
package media
import (
"errors"
"fmt"
"path/filepath"
"reflect"
"sort"
"strings"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/config"
"github.com/mitchellh/mapstructure"
@@ -32,11 +31,6 @@ import (
var DefaultTypes Types
func init() {
// Apply delimiter to all.
for _, m := range defaultMediaTypesConfig {
m.(map[string]any)["delimiter"] = "."
}
ns, err := DecodeTypes(nil)
if err != nil {
panic(err)
@@ -45,122 +39,17 @@ func init() {
// Initialize the Builtin types with values from DefaultTypes.
v := reflect.ValueOf(&Builtin).Elem()
for i := 0; i < v.NumField(); i++ {
f := v.Field(i)
fieldName := v.Type().Field(i).Name
builtinType := f.Interface().(Type)
if builtinType.Type == "" {
panic(fmt.Errorf("builtin type %q is empty", fieldName))
}
defaultType, found := DefaultTypes.GetByType(builtinType.Type)
if !found {
panic(fmt.Errorf("missing default type for field builtin type: %q", fieldName))
panic(errors.New("missing default type for builtin type: " + builtinType.Type))
}
f.Set(reflect.ValueOf(defaultType))
}
}
func init() {
DefaultContentTypes = ContentTypes{
HTML: Builtin.HTMLType,
Markdown: Builtin.MarkdownType,
AsciiDoc: Builtin.AsciiDocType,
Pandoc: Builtin.PandocType,
ReStructuredText: Builtin.ReStructuredTextType,
EmacsOrgMode: Builtin.EmacsOrgModeType,
}
DefaultContentTypes.init()
}
var DefaultContentTypes ContentTypes
// ContentTypes holds the media types that are considered content in Hugo.
type ContentTypes struct {
HTML Type
Markdown Type
AsciiDoc Type
Pandoc Type
ReStructuredText Type
EmacsOrgMode Type
// Created in init().
types Types
extensionSet map[string]bool
}
func (t *ContentTypes) init() {
t.types = Types{t.HTML, t.Markdown, t.AsciiDoc, t.Pandoc, t.ReStructuredText, t.EmacsOrgMode}
t.extensionSet = make(map[string]bool)
for _, mt := range t.types {
for _, suffix := range mt.Suffixes() {
t.extensionSet[suffix] = true
}
}
}
func (t ContentTypes) IsContentSuffix(suffix string) bool {
return t.extensionSet[suffix]
}
// IsContentFile returns whether the given filename is a content file.
func (t ContentTypes) IsContentFile(filename string) bool {
return t.IsContentSuffix(strings.TrimPrefix(filepath.Ext(filename), "."))
}
// IsIndexContentFile returns whether the given filename is an index content file.
func (t ContentTypes) IsIndexContentFile(filename string) bool {
if !t.IsContentFile(filename) {
return false
}
base := filepath.Base(filename)
return strings.HasPrefix(base, "index.") || strings.HasPrefix(base, "_index.")
}
// IsHTMLSuffix returns whether the given suffix is a HTML media type.
func (t ContentTypes) IsHTMLSuffix(suffix string) bool {
for _, s := range t.HTML.Suffixes() {
if s == suffix {
return true
}
}
return false
}
// Types is a slice of media types.
func (t ContentTypes) Types() Types {
return t.types
}
// FromTypes creates a new ContentTypes updated with the values from the given Types.
func (t ContentTypes) FromTypes(types Types) ContentTypes {
if tt, ok := types.GetByType(t.HTML.Type); ok {
t.HTML = tt
}
if tt, ok := types.GetByType(t.Markdown.Type); ok {
t.Markdown = tt
}
if tt, ok := types.GetByType(t.AsciiDoc.Type); ok {
t.AsciiDoc = tt
}
if tt, ok := types.GetByType(t.Pandoc.Type); ok {
t.Pandoc = tt
}
if tt, ok := types.GetByType(t.ReStructuredText.Type); ok {
t.ReStructuredText = tt
}
if tt, ok := types.GetByType(t.EmacsOrgMode.Type); ok {
t.EmacsOrgMode = tt
}
t.init()
return t
}
// Hold the configuration for a given media type.
type MediaTypeConfig struct {
// The file suffixes used for this media type.
@@ -216,10 +105,3 @@ func DecodeTypes(in map[string]any) (*config.ConfigNamespace[map[string]MediaTyp
}
return ns, nil
}
// TODO(bep) get rid of this.
var DefaultPathParser = &paths.PathParser{
IsContentExt: func(ext string) bool {
return DefaultContentTypes.IsContentSuffix(ext)
},
}
+7 -12
View File
@@ -114,7 +114,7 @@ func TestDefaultTypes(t *testing.T) {
tp Type
expectedMainType string
expectedSubType string
expectedSuffixes string
expectedSuffix string
expectedType string
expectedString string
}{
@@ -122,34 +122,29 @@ func TestDefaultTypes(t *testing.T) {
{Builtin.CSSType, "text", "css", "css", "text/css", "text/css"},
{Builtin.SCSSType, "text", "x-scss", "scss", "text/x-scss", "text/x-scss"},
{Builtin.CSVType, "text", "csv", "csv", "text/csv", "text/csv"},
{Builtin.HTMLType, "text", "html", "html,htm", "text/html", "text/html"},
{Builtin.MarkdownType, "text", "markdown", "md,mdown,markdown", "text/markdown", "text/markdown"},
{Builtin.EmacsOrgModeType, "text", "org", "org", "text/org", "text/org"},
{Builtin.PandocType, "text", "pandoc", "pandoc,pdc", "text/pandoc", "text/pandoc"},
{Builtin.ReStructuredTextType, "text", "rst", "rst", "text/rst", "text/rst"},
{Builtin.AsciiDocType, "text", "asciidoc", "adoc,asciidoc,ad", "text/asciidoc", "text/asciidoc"},
{Builtin.JavascriptType, "text", "javascript", "js,jsm,mjs", "text/javascript", "text/javascript"},
{Builtin.HTMLType, "text", "html", "html", "text/html", "text/html"},
{Builtin.JavascriptType, "text", "javascript", "js", "text/javascript", "text/javascript"},
{Builtin.TypeScriptType, "text", "typescript", "ts", "text/typescript", "text/typescript"},
{Builtin.TSXType, "text", "tsx", "tsx", "text/tsx", "text/tsx"},
{Builtin.JSXType, "text", "jsx", "jsx", "text/jsx", "text/jsx"},
{Builtin.JSONType, "application", "json", "json", "application/json", "application/json"},
{Builtin.RSSType, "application", "rss", "xml,rss", "application/rss+xml", "application/rss+xml"},
{Builtin.RSSType, "application", "rss", "xml", "application/rss+xml", "application/rss+xml"},
{Builtin.SVGType, "image", "svg", "svg", "image/svg+xml", "image/svg+xml"},
{Builtin.TextType, "text", "plain", "txt", "text/plain", "text/plain"},
{Builtin.XMLType, "application", "xml", "xml", "application/xml", "application/xml"},
{Builtin.TOMLType, "application", "toml", "toml", "application/toml", "application/toml"},
{Builtin.YAMLType, "application", "yaml", "yaml,yml", "application/yaml", "application/yaml"},
{Builtin.YAMLType, "application", "yaml", "yaml", "application/yaml", "application/yaml"},
{Builtin.PDFType, "application", "pdf", "pdf", "application/pdf", "application/pdf"},
{Builtin.TrueTypeFontType, "font", "ttf", "ttf", "font/ttf", "font/ttf"},
{Builtin.OpenTypeFontType, "font", "otf", "otf", "font/otf", "font/otf"},
} {
c.Assert(test.tp.MainType, qt.Equals, test.expectedMainType)
c.Assert(test.tp.SubType, qt.Equals, test.expectedSubType)
c.Assert(test.tp.SuffixesCSV, qt.Equals, test.expectedSuffixes)
c.Assert(test.tp.Type, qt.Equals, test.expectedType)
c.Assert(test.tp.String(), qt.Equals, test.expectedString)
}
c.Assert(len(DefaultTypes), qt.Equals, 40)
c.Assert(len(DefaultTypes), qt.Equals, 36)
}
+3 -52
View File
@@ -117,16 +117,13 @@ func FromContent(types Types, extensionHints []string, content []byte) Type {
return m
}
// FromStringAndExt creates a Type from a MIME string and a given extensions
func FromStringAndExt(t string, ext ...string) (Type, error) {
// FromStringAndExt creates a Type from a MIME string and a given extension.
func FromStringAndExt(t, ext string) (Type, error) {
tp, err := FromString(t)
if err != nil {
return tp, err
}
for i, e := range ext {
ext[i] = strings.TrimPrefix(e, ".")
}
tp.SuffixesCSV = strings.Join(ext, ",")
tp.SuffixesCSV = strings.TrimPrefix(ext, ".")
tp.Delimiter = DefaultDelimiter
tp.init()
return tp, nil
@@ -190,16 +187,6 @@ func (m Type) IsText() bool {
return false
}
// For internal use.
func (m Type) IsHTML() bool {
return m.SubType == Builtin.HTMLType.SubType
}
// For internal use.
func (m Type) IsMarkdown() bool {
return m.SubType == Builtin.MarkdownType.SubType
}
func InitMediaType(m *Type) {
m.init()
}
@@ -234,26 +221,6 @@ func (t Types) Len() int { return len(t) }
func (t Types) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
func (t Types) Less(i, j int) bool { return t[i].Type < t[j].Type }
// GetBestMatch returns the best match for the given media type string.
func (t Types) GetBestMatch(s string) (Type, bool) {
// First try an exact match.
if mt, found := t.GetByType(s); found {
return mt, true
}
// Try main type.
if mt, found := t.GetBySubType(s); found {
return mt, true
}
// Try extension.
if mt, _, found := t.GetFirstBySuffix(s); found {
return mt, true
}
return Type{}, false
}
// GetByType returns a media type for tp.
func (t Types) GetByType(tp string) (Type, bool) {
for _, tt := range t {
@@ -357,22 +324,6 @@ func (t Types) GetByMainSubType(mainType, subType string) (tp Type, found bool)
return
}
// GetBySubType gets a media type given a sub type e.g. "plain".
func (t Types) GetBySubType(subType string) (tp Type, found bool) {
for _, tt := range t {
if strings.EqualFold(subType, tt.SubType) {
if found {
// ambiguous
found = false
return
}
tp = tt
found = true
}
}
return
}
// IsZero reports whether this Type represents a zero value.
// For internal use.
func (m Type) IsZero() bool {
+2 -10
View File
@@ -115,10 +115,10 @@ func TestFromTypeString(t *testing.T) {
func TestFromStringAndExt(t *testing.T) {
c := qt.New(t)
f, err := FromStringAndExt("text/html", "html", "htm")
f, err := FromStringAndExt("text/html", "html")
c.Assert(err, qt.IsNil)
c.Assert(f, qt.Equals, Builtin.HTMLType)
f, err = FromStringAndExt("text/html", ".html", ".htm")
f, err = FromStringAndExt("text/html", ".html")
c.Assert(err, qt.IsNil)
c.Assert(f, qt.Equals, Builtin.HTMLType)
}
@@ -214,11 +214,3 @@ func BenchmarkTypeOps(b *testing.B) {
}
}
func TestIsContentFile(t *testing.T) {
c := qt.New(t)
c.Assert(DefaultContentTypes.IsContentFile(filepath.FromSlash("my/file.md")), qt.Equals, true)
c.Assert(DefaultContentTypes.IsContentFile(filepath.FromSlash("my/file.ad")), qt.Equals, true)
c.Assert(DefaultContentTypes.IsContentFile(filepath.FromSlash("textfile.txt")), qt.Equals, false)
}
+1 -4
View File
@@ -261,10 +261,7 @@ func (c *collector) add(owner *moduleAdapter, moduleImport Import) (*moduleAdapt
// This will select the latest release-version (not beta etc.).
versionQuery = "upgrade"
}
// Note that we cannot use c.Get for this, as that may
// trigger a new module collection and potentially create a infinite loop.
if err := c.get(fmt.Sprintf("%s@%s", modulePath, versionQuery)); err != nil {
if err := c.Get(fmt.Sprintf("%s@%s", modulePath, versionQuery)); err != nil {
return nil, err
}
if err := c.loadModules(); err != nil {
+1
View File
@@ -104,6 +104,7 @@ func InterfaceToFrontMatter(in any, format metadecoders.Format, w io.Writer) err
}
err = InterfaceToConfig(in, format, w)
if err != nil {
return err
}
+1 -1
View File
@@ -128,7 +128,7 @@ func (e *errorResource) Exif() *exif.ExifInfo {
panic(e.ResourceError)
}
func (e *errorResource) Colors() ([]images.Color, error) {
func (e *errorResource) Colors() ([]string, error) {
panic(e.ResourceError)
}
+3 -3
View File
@@ -67,7 +67,7 @@ type imageResource struct {
meta *imageMeta
dominantColorInit sync.Once
dominantColors []images.Color
dominantColors []string
baseResource
}
@@ -143,7 +143,7 @@ func (i *imageResource) getExif() *exif.ExifInfo {
// Colors returns a slice of the most dominant colors in an image
// using a simple histogram method.
func (i *imageResource) Colors() ([]images.Color, error) {
func (i *imageResource) Colors() ([]string, error) {
var err error
i.dominantColorInit.Do(func() {
var img image.Image
@@ -153,7 +153,7 @@ func (i *imageResource) Colors() ([]images.Color, error) {
}
colors := color_extractor.ExtractColors(img)
for _, c := range colors {
i.dominantColors = append(i.dominantColors, images.ColorGoToColor(c))
i.dominantColors = append(i.dominantColors, images.ColorToHexString(c))
}
})
return i.dominantColors, nil
+2 -27
View File
@@ -85,16 +85,9 @@ func TestImageTransformBasic(t *testing.T) {
assertWidthHeight(c, img, w, h)
}
gotColors, err := image.Colors()
colors, err := image.Colors()
c.Assert(err, qt.IsNil)
expectedColors := images.HexStringsToColors("#2d2f33", "#a49e93", "#d39e59", "#a76936", "#737a84", "#7c838b")
c.Assert(len(gotColors), qt.Equals, len(expectedColors))
for i := range gotColors {
c1, c2 := gotColors[i], expectedColors[i]
c.Assert(c1.ColorHex(), qt.Equals, c2.ColorHex())
c.Assert(c1.ColorGo(), qt.DeepEquals, c2.ColorGo())
c.Assert(c1.Luminance(), qt.Equals, c2.Luminance())
}
c.Assert(colors, qt.DeepEquals, []string{"#2d2f33", "#a49e93", "#d39e59", "#a76936", "#737a84", "#7c838b"})
c.Assert(image.RelPermalink(), qt.Equals, "/a/sunset.jpg")
c.Assert(image.ResourceType(), qt.Equals, "image")
@@ -452,24 +445,6 @@ func TestImageExif(t *testing.T) {
getAndCheckExif(c, image)
}
func TestImageColorsLuminance(t *testing.T) {
c := qt.New(t)
_, image := fetchSunset(c)
c.Assert(image, qt.Not(qt.IsNil))
colors, err := image.Colors()
c.Assert(err, qt.IsNil)
c.Assert(len(colors), qt.Equals, 6)
var prevLuminance float64
for i, color := range colors {
luminance := color.Luminance()
c.Assert(err, qt.IsNil)
c.Assert(luminance > 0, qt.IsTrue)
c.Assert(luminance, qt.Not(qt.Equals), prevLuminance, qt.Commentf("i=%d", i))
prevLuminance = luminance
}
}
func BenchmarkImageExif(b *testing.B) {
getImages := func(c *qt.C, b *testing.B, fs afero.Fs) []images.ImageResource {
spec := newTestResourceSpec(specDescriptor{fs: fs, c: c})
+4 -116
View File
@@ -16,76 +16,10 @@ package images
import (
"encoding/hex"
"fmt"
"hash/fnv"
"image/color"
"math"
"strings"
"github.com/gohugoio/hugo/common/hstrings"
)
type colorGoProvider interface {
ColorGo() color.Color
}
type Color struct {
// The color.
color color.Color
// The color prefixed with a #.
hex string
// The relative luminance of the color.
luminance float64
}
// Luminance as defined by w3.org.
// See https://www.w3.org/TR/WCAG21/#dfn-relative-luminance
func (c Color) Luminance() float64 {
return c.luminance
}
// ColorGo returns the color as a color.Color.
// For internal use only.
func (c Color) ColorGo() color.Color {
return c.color
}
// ColorHex returns the color as a hex string prefixed with a #.
func (c Color) ColorHex() string {
return c.hex
}
// String returns the color as a hex string prefixed with a #.
func (c Color) String() string {
return c.hex
}
// For hashstructure. This struct is used in template func options
// that needs to be able to hash a Color.
// For internal use only.
func (c Color) Hash() (uint64, error) {
h := fnv.New64a()
h.Write([]byte(c.hex))
return h.Sum64(), nil
}
func (c *Color) init() error {
c.hex = ColorGoToHexString(c.color)
r, g, b, _ := c.color.RGBA()
c.luminance = 0.2126*c.toSRGB(uint8(r)) + 0.7152*c.toSRGB(uint8(g)) + 0.0722*c.toSRGB(uint8(b))
return nil
}
func (c Color) toSRGB(i uint8) float64 {
v := float64(i) / 255
if v <= 0.04045 {
return v / 12.92
} else {
return math.Pow((v+0.055)/1.055, 2.4)
}
}
// AddColorToPalette adds c as the first color in p if not already there.
// Note that it does no additional checks, so callers must make sure
// that the palette is valid for the relevant format.
@@ -111,60 +45,14 @@ func ReplaceColorInPalette(c color.Color, p color.Palette) {
p[p.Index(c)] = c
}
// ColorGoToHexString converts a color.Color to a hex string.
func ColorGoToHexString(c color.Color) string {
// ColorToHexString converts a color to a hex string.
func ColorToHexString(c color.Color) string {
r, g, b, a := c.RGBA()
rgba := color.RGBA{uint8(r), uint8(g), uint8(b), uint8(a)}
if rgba.A == 0xff {
return fmt.Sprintf("#%.2x%.2x%.2x", rgba.R, rgba.G, rgba.B)
}
return fmt.Sprintf("#%.2x%.2x%.2x%.2x", rgba.R, rgba.G, rgba.B, rgba.A)
return fmt.Sprintf("#%.2x%.2x%.2x", rgba.R, rgba.G, rgba.B)
}
// ColorGoToColor converts a color.Color to a Color.
func ColorGoToColor(c color.Color) Color {
cc := Color{color: c}
if err := cc.init(); err != nil {
panic(err)
}
return cc
}
func hexStringToColor(s string) Color {
c, err := hexStringToColorGo(s)
if err != nil {
panic(err)
}
return ColorGoToColor(c)
}
// HexStringsToColors converts a slice of hex strings to a slice of Colors.
func HexStringsToColors(s ...string) []Color {
var colors []Color
for _, v := range s {
colors = append(colors, hexStringToColor(v))
}
return colors
}
func toColorGo(v any) (color.Color, bool, error) {
switch vv := v.(type) {
case colorGoProvider:
return vv.ColorGo(), true, nil
default:
s, ok := hstrings.ToString(v)
if !ok {
return nil, false, nil
}
c, err := hexStringToColorGo(s)
if err != nil {
return nil, false, err
}
return c, true, nil
}
}
func hexStringToColorGo(s string) (color.Color, error) {
func hexStringToColor(s string) (color.Color, error) {
s = strings.TrimPrefix(s, "#")
if len(s) != 3 && len(s) != 4 && len(s) != 6 && len(s) != 8 {
+6 -20
View File
@@ -18,7 +18,6 @@ import (
"testing"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/htesting/hqt"
)
func TestHexStringToColor(t *testing.T) {
@@ -47,7 +46,7 @@ func TestHexStringToColor(t *testing.T) {
c.Run(test.arg, func(c *qt.C) {
c.Parallel()
result, err := hexStringToColorGo(test.arg)
result, err := hexStringToColor(test.arg)
if b, ok := test.expect.(bool); ok && !b {
c.Assert(err, qt.Not(qt.IsNil))
@@ -71,18 +70,13 @@ func TestColorToHexString(t *testing.T) {
{color.White, "#ffffff"},
{color.Black, "#000000"},
{color.RGBA{R: 0x42, G: 0x87, B: 0xf5, A: 0xff}, "#4287f5"},
// 50% opacity.
// Note that the .Colors (dominant colors) received from the Image resource
// will always have an alpha value of 0xff.
{color.RGBA{R: 0x42, G: 0x87, B: 0xf5, A: 0x80}, "#4287f580"},
} {
test := test
c.Run(test.expect, func(c *qt.C) {
c.Parallel()
result := ColorGoToHexString(test.arg)
result := ColorToHexString(test.arg)
c.Assert(result, qt.Equals, test.expect)
})
@@ -97,9 +91,9 @@ func TestAddColorToPalette(t *testing.T) {
c.Assert(AddColorToPalette(color.White, palette), qt.HasLen, 2)
blue1, _ := hexStringToColorGo("34c3eb")
blue2, _ := hexStringToColorGo("34c3eb")
white, _ := hexStringToColorGo("fff")
blue1, _ := hexStringToColor("34c3eb")
blue2, _ := hexStringToColor("34c3eb")
white, _ := hexStringToColor("fff")
c.Assert(AddColorToPalette(white, palette), qt.HasLen, 2)
c.Assert(AddColorToPalette(blue1, palette), qt.HasLen, 3)
@@ -110,18 +104,10 @@ func TestReplaceColorInPalette(t *testing.T) {
c := qt.New(t)
palette := color.Palette{color.White, color.Black}
offWhite, _ := hexStringToColorGo("fcfcfc")
offWhite, _ := hexStringToColor("fcfcfc")
ReplaceColorInPalette(offWhite, palette)
c.Assert(palette, qt.HasLen, 2)
c.Assert(palette[0], qt.Equals, offWhite)
}
func TestColorLuminance(t *testing.T) {
c := qt.New(t)
c.Assert(hexStringToColor("#000000").Luminance(), hqt.IsSameFloat64, 0.0)
c.Assert(hexStringToColor("#768a9a").Luminance(), hqt.IsSameFloat64, 0.24361603589088263)
c.Assert(hexStringToColor("#d5bc9f").Luminance(), hqt.IsSameFloat64, 0.5261577672685374)
c.Assert(hexStringToColor("#ffffff").Luminance(), hqt.IsSameFloat64, 1.0)
}
+3 -3
View File
@@ -171,7 +171,7 @@ func DecodeConfig(in map[string]any) (*config.ConfigNamespace[ImagingConfig, Ima
return i, nil, err
}
i.BgColor, err = hexStringToColorGo(i.Imaging.BgColor)
i.BgColor, err = hexStringToColor(i.Imaging.BgColor)
if err != nil {
return i, nil, err
}
@@ -230,7 +230,7 @@ func DecodeImageConfig(action string, options []string, defaults *config.ConfigN
c.Hint = hint
} else if part[0] == '#' {
c.BgColorStr = part[1:]
c.BgColor, err = hexStringToColorGo(c.BgColorStr)
c.BgColor, err = hexStringToColor(c.BgColorStr)
if err != nil {
return c, err
}
@@ -424,7 +424,7 @@ type ImagingConfigInternal struct {
func (i *ImagingConfigInternal) Compile(externalCfg *ImagingConfig) error {
var err error
i.BgColor, err = hexStringToColorGo(externalCfg.BgColor)
i.BgColor, err = hexStringToColor(externalCfg.BgColor)
if err != nil {
return err
}
+1 -1
View File
@@ -132,7 +132,7 @@ func newImageConfig(action string, width, height, quality, rotate int, filter, a
c.qualitySetForImage = quality != 75
c.Rotate = rotate
c.BgColorStr = bgColor
c.BgColor, _ = hexStringToColorGo(bgColor)
c.BgColor, _ = hexStringToColor(bgColor)
if filter != "" {
filter = strings.ToLower(filter)
-7
View File
@@ -17,7 +17,6 @@ import (
"bytes"
"fmt"
"io"
"math"
"math/big"
"regexp"
"strings"
@@ -141,12 +140,6 @@ func (d *Decoder) Decode(r io.Reader) (ex *ExifInfo, err error) {
if !d.noLatLong {
lat, long, _ = x.LatLong()
if math.IsNaN(lat) {
lat = 0
}
if math.IsNaN(long) {
long = 0
}
}
walker := &exifWalker{x: x, vals: make(map[string]any), includeMatcher: d.includeFieldsRe, excludeMatcher: d.excludeFieldsrRe}

Some files were not shown because too many files have changed in this diff Show More