Compare commits

..

2 Commits

Author SHA1 Message Date
hugoreleaser 378762b789 releaser: Prepare repository for 0.145.0-DEV
[ci skip]
2025-02-19 12:31:09 +00:00
hugoreleaser 098c68fd18 releaser: Bump versions for release of 0.144.2
[ci skip]
2025-02-19 12:17:04 +00:00
1398 changed files with 22562 additions and 25485 deletions
-1
View File
@@ -46,4 +46,3 @@ jobs:
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: HUGO_BUILD_TAGS=extended,withdeploy
+1 -1
View File
@@ -1,4 +1,4 @@
>**Note:** We would appreciate if you hold on with any big refactoring (like renaming deprecated Go packages), mainly because of potential for extra merge work for future coming in in the near future.
>**Note:** We would apprecitate if you hold on with any big refactorings (like renaming deprecated Go packages), mainly because of potential for extra merge work for future coming in in the near future.
# Contributing to Hugo
+4 -4
View File
@@ -2,8 +2,8 @@
# Twitter: https://twitter.com/gohugoio
# Website: https://gohugo.io/
ARG GO_VERSION="1.24"
ARG ALPINE_VERSION="3.22"
ARG GO_VERSION="1.23.2"
ARG ALPINE_VERSION="3.20"
ARG DART_SASS_VERSION="1.79.3"
FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.5.0 AS xx
@@ -19,7 +19,7 @@ RUN apk add clang lld
COPY --from=xx / /
ARG TARGETPLATFORM
RUN xx-apk add musl-dev gcc g++
RUN xx-apk add musl-dev gcc g++
# Optionally set HUGO_BUILD_TAGS to "none" or "withdeploy" when building like so:
# docker build --build-arg HUGO_BUILD_TAGS=withdeploy .
@@ -72,7 +72,7 @@ RUN mkdir -p /var/hugo/bin /cache && \
adduser -Sg hugo -u 1000 -h /var/hugo hugo && \
chown -R hugo: /var/hugo /cache && \
# For the Hugo's Git integration to work.
runuser -u hugo -- git config --global --add safe.directory /project && \
runuser -u hugo -- git config --global --add safe.directory /project && \
# See https://github.com/gohugoio/hugo/issues/9810
runuser -u hugo -- git config --global core.quotepath false
+7 -8
View File
@@ -65,11 +65,9 @@ See the [features] section of the documentation for a comprehensive summary of H
<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/hugoDocs/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;
<a href="https://www.jetbrains.com/go/?utm_source=OSS&utm_medium=referral&utm_campaign=hugo" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/hugoDocs/master/assets/images/sponsors/goland.svg" width="200" alt="The complete IDE crafted for professional Go developers."></a>
&nbsp;&nbsp;&nbsp;
<a href="https://pinme.eth.limo/?s=hugo" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/hugoDocs/master/assets/images/sponsors/logo-pinme.svg" width="200" alt="PinMe."></a>
<a href="https://www.jetbrains.com/go/?utm_source=OSS&utm_medium=referral&utm_campaign=hugo" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/gohugoioTheme/master/assets/images/sponsors/goland.svg" width="200" alt="The complete IDE crafted for professional Go developers."></a>
</p>
## Editions
@@ -82,10 +80,10 @@ Encode to the WebP format when [processing images]. You can decode WebP images w
[Transpile Sass to CSS] using the embedded LibSass transpiler. You can use the [Dart Sass] transpiler with any edition.|:heavy_check_mark:|:heavy_check_mark:
Deploy your site directly to a Google Cloud Storage bucket, an AWS S3 bucket, or an Azure Storage container. See&nbsp;[details].|:x:|:heavy_check_mark:
[dart sass]: https://gohugo.io/functions/css/sass/#dart-sass
[processing images]: https://gohugo.io/content-management/image-processing/
[transpile sass to css]: https://gohugo.io/functions/css/sass/
[details]: https://gohugo.io/hosting-and-deployment/hugo-deploy/
[dart sass]: /functions/css/sass/#dart-sass
[processing images]: /content-management/image-processing/
[transpile sass to css]: /functions/css/sass/
[details]: /hosting-and-deployment/hugo-deploy/
Unless your specific deployment needs require the extended/deploy edition, we recommend the extended edition.
@@ -191,6 +189,7 @@ github.com/bep/simplecobra="v0.5.0"
github.com/bep/tmc="v0.5.1"
github.com/cespare/xxhash/v2="v2.3.0"
github.com/clbanning/mxj/v2="v2.7.0"
github.com/cli/safeexec="v1.0.1"
github.com/cpuguy83/go-md2man/v2="v2.0.4"
github.com/disintegration/gift="v1.2.1"
github.com/dlclark/regexp2="v1.11.5"
+4 -4
View File
@@ -191,16 +191,16 @@ func TestPanicInCreate(t *testing.T) {
return err
}
for i := range 3 {
for range 3 {
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
c.Assert(willPanic(i), qt.PanicMatches, fmt.Sprintf("panic-%d", i))
c.Assert(willErr(i), qt.ErrorMatches, fmt.Sprintf("error-%d", i))
}
}
// Test the same keys again without the panic.
for i := range 3 {
for range 3 {
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
v, err := p1.GetOrCreate(fmt.Sprintf("panic-%d", i), func(key string) (testItem, error) {
return testItem{
name: key,
+3 -3
View File
@@ -59,7 +59,7 @@ dir = ":resourceDir/_gen"
caches, err := filecache.NewCaches(p)
c.Assert(err, qt.IsNil)
cache := caches[name]
for i := range 10 {
for i := 0; i < 10; i++ {
id := fmt.Sprintf("i%d", i)
cache.GetOrCreateBytes(id, func() ([]byte, error) {
return []byte("abc"), nil
@@ -74,7 +74,7 @@ dir = ":resourceDir/_gen"
c.Assert(err, qt.IsNil)
c.Assert(count, qt.Equals, 5, msg)
for i := range 10 {
for i := 0; i < 10; i++ {
id := fmt.Sprintf("i%d", i)
v := cache.GetString(id)
if i < 5 {
@@ -97,7 +97,7 @@ dir = ":resourceDir/_gen"
c.Assert(count, qt.Equals, 4)
// Now only the i5 should be left.
for i := range 10 {
for i := 0; i < 10; i++ {
id := fmt.Sprintf("i%d", i)
v := cache.GetString(id)
if i != 5 {
+3 -3
View File
@@ -105,7 +105,7 @@ dir = ":cacheDir/c"
}
for _, ca := range []*filecache.Cache{caches.ImageCache(), caches.AssetsCache(), caches.GetJSONCache(), caches.GetCSVCache()} {
for range 2 {
for i := 0; i < 2; i++ {
info, r, err := ca.GetOrCreate("a", rf("abc"))
c.Assert(err, qt.IsNil)
c.Assert(r, qt.Not(qt.IsNil))
@@ -193,11 +193,11 @@ dir = "/cache/c"
var wg sync.WaitGroup
for i := range 50 {
for i := 0; i < 50; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
for range 20 {
for j := 0; j < 20; j++ {
ca := caches.Get(cacheName)
c.Assert(ca, qt.Not(qt.IsNil))
filename, data := filenameData(i)
+3 -25
View File
@@ -42,7 +42,7 @@ var DefaultConfig = Config{
// Config holds the configuration for the HTTP cache.
type Config struct {
// Configures the HTTP cache behavior (RFC 9111).
// Configures the HTTP cache behaviour (RFC 9111).
// When this is not enabled for a resource, Hugo will go straight to the file cache.
Cache Cache
@@ -52,7 +52,7 @@ type Config struct {
}
type Cache struct {
// Enable HTTP cache behavior (RFC 9111) for these resources.
// Enable HTTP cache behaviour (RFC 9111) for these rsources.
For GlobMatcher
}
@@ -122,10 +122,6 @@ type GlobMatcher struct {
Includes []string
}
func (gm GlobMatcher) IsZero() bool {
return len(gm.Includes) == 0 && len(gm.Excludes) == 0
}
type ConfigCompiled struct {
For predicate.P[string]
PollConfigs []PollConfigCompiled
@@ -159,9 +155,6 @@ func (p PollConfigCompiled) IsZero() bool {
}
func (gm *GlobMatcher) CompilePredicate() (func(string) bool, error) {
if gm.IsZero() {
panic("no includes or excludes")
}
var p predicate.P[string]
for _, include := range gm.Includes {
g, err := glob.Compile(include, '/')
@@ -188,7 +181,7 @@ func (gm *GlobMatcher) CompilePredicate() (func(string) bool, error) {
return p, nil
}
func DecodeConfig(_ config.BaseConfig, m map[string]any) (Config, error) {
func DecodeConfig(bcfg config.BaseConfig, m map[string]any) (Config, error) {
if len(m) == 0 {
return DefaultConfig, nil
}
@@ -210,20 +203,5 @@ func DecodeConfig(_ config.BaseConfig, m map[string]any) (Config, error) {
return c, err
}
if c.Cache.For.IsZero() {
c.Cache.For = DefaultConfig.Cache.For
}
for pci := range c.Polls {
if c.Polls[pci].For.IsZero() {
c.Polls[pci].For = DefaultConfig.Cache.For
c.Polls[pci].Disable = true
}
}
if len(c.Polls) == 0 {
c.Polls = DefaultConfig.Polls
}
return c, nil
}
-31
View File
@@ -22,8 +22,6 @@ import (
)
func TestConfigCustom(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
[httpcache]
@@ -53,8 +51,6 @@ includes = ["**gohugo.io**"]
}
func TestConfigDefault(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
`
@@ -66,30 +62,3 @@ func TestConfigDefault(t *testing.T) {
b.Assert(compiled.For("https://gohugo.io/foo.jpg"), qt.IsFalse)
b.Assert(compiled.PollConfigFor("https://gohugo.io/foo.jpg").Config.Disable, qt.IsTrue)
}
func TestConfigPollsOnly(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
[httpcache]
[[httpcache.polls]]
low = "5s"
high = "32s"
[httpcache.polls.for]
includes = ["**gohugo.io**"]
`
b := hugolib.Test(t, files)
compiled := b.H.Configs.Base.C.HTTPCache
b.Assert(compiled.For("https://gohugo.io/posts.json"), qt.IsFalse)
b.Assert(compiled.For("https://gohugo.io/foo.jpg"), qt.IsFalse)
pc := compiled.PollConfigFor("https://gohugo.io/foo.jpg")
b.Assert(pc.Config.Low, qt.Equals, 5*time.Second)
b.Assert(pc.Config.High, qt.Equals, 32*time.Second)
b.Assert(compiled.PollConfigFor("https://example.com/foo.jpg").IsZero(), qt.IsTrue)
}
-31
View File
@@ -17,7 +17,6 @@ import (
"testing"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/config"
)
func TestGlobMatcher(t *testing.T) {
@@ -41,33 +40,3 @@ func TestGlobMatcher(t *testing.T) {
c.Assert(p("foo/bar/foo.css"), qt.IsFalse)
c.Assert(p("foo/bar/foo.xml"), qt.IsTrue)
}
func TestDefaultConfig(t *testing.T) {
c := qt.New(t)
_, err := DefaultConfig.Compile()
c.Assert(err, qt.IsNil)
}
func TestDecodeConfigInjectsDefaultAndCompiles(t *testing.T) {
c := qt.New(t)
cfg, err := DecodeConfig(config.BaseConfig{}, map[string]interface{}{})
c.Assert(err, qt.IsNil)
c.Assert(cfg, qt.DeepEquals, DefaultConfig)
_, err = cfg.Compile()
c.Assert(err, qt.IsNil)
cfg, err = DecodeConfig(config.BaseConfig{}, map[string]any{
"cache": map[string]any{
"polls": []map[string]any{
{"disable": true},
},
},
})
c.Assert(err, qt.IsNil)
_, err = cfg.Compile()
c.Assert(err, qt.IsNil)
}
+7 -8
View File
@@ -26,7 +26,6 @@ import (
"path/filepath"
"reflect"
"regexp"
"slices"
"sort"
"strings"
"sync"
@@ -103,7 +102,7 @@ func (c *Inspector) MethodsFromTypes(include []reflect.Type, exclude []reflect.T
}
for _, t := range include {
for i := range t.NumMethod() {
for i := 0; i < t.NumMethod(); i++ {
m := t.Method(i)
if excludes[m.Name] || seen[m.Name] {
@@ -123,7 +122,7 @@ func (c *Inspector) MethodsFromTypes(include []reflect.Type, exclude []reflect.T
method := Method{Owner: t, OwnerName: ownerName, Name: m.Name}
for i := range numIn {
for i := 0; i < numIn; i++ {
in := m.Type.In(i)
name, pkg := nameAndPackage(in)
@@ -138,7 +137,7 @@ func (c *Inspector) MethodsFromTypes(include []reflect.Type, exclude []reflect.T
numOut := m.Type.NumOut()
if numOut > 0 {
for i := range numOut {
for i := 0; i < numOut; i++ {
out := m.Type.Out(i)
name, pkg := nameAndPackage(out)
@@ -305,7 +304,7 @@ func (m Method) inOutStr() string {
}
args := make([]string, len(m.In))
for i := range args {
for i := 0; i < len(args); i++ {
args[i] = fmt.Sprintf("arg%d", i)
}
return "(" + strings.Join(args, ", ") + ")"
@@ -317,7 +316,7 @@ func (m Method) inStr() string {
}
args := make([]string, len(m.In))
for i := range args {
for i := 0; i < len(args); i++ {
args[i] = fmt.Sprintf("arg%d %s", i, m.In[i])
}
return "(" + strings.Join(args, ", ") + ")"
@@ -340,7 +339,7 @@ func (m Method) outStrNamed() string {
}
outs := make([]string, len(m.Out))
for i := range outs {
for i := 0; i < len(outs); i++ {
outs[i] = fmt.Sprintf("o%d %s", i, m.Out[i])
}
@@ -436,7 +435,7 @@ func (m Methods) ToMarshalJSON(receiver, pkgPath string, excludes ...string) (st
// Exclude self
for i, pkgImp := range pkgImports {
if pkgImp == pkgPath {
pkgImports = slices.Delete(pkgImports, i, i+1)
pkgImports = append(pkgImports[:i], pkgImports[i+1:]...)
}
}
}
+4 -4
View File
@@ -101,8 +101,8 @@ type configKey struct {
// This is the root command.
type rootCommand struct {
Printf func(format string, v ...any)
Println func(a ...any)
Printf func(format string, v ...interface{})
Println func(a ...interface{})
StdOut io.Writer
StdErr io.Writer
@@ -431,12 +431,12 @@ func (r *rootCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
// Used by mkcert (server).
log.SetOutput(r.StdOut)
r.Printf = func(format string, v ...any) {
r.Printf = func(format string, v ...interface{}) {
if !r.quiet {
fmt.Fprintf(r.StdOut, format, v...)
}
}
r.Println = func(a ...any) {
r.Println = func(a ...interface{}) {
if !r.quiet {
fmt.Fprintln(r.StdOut, a...)
}
+1 -1
View File
@@ -90,7 +90,7 @@ func (c *configCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
os.Stdout.Write(buf.Bytes())
default:
// Decode the JSON to a map[string]interface{} and then unmarshal it again to the correct format.
var m map[string]any
var m map[string]interface{}
if err := json.Unmarshal(buf.Bytes(), &m); err != nil {
return err
}
+4 -14
View File
@@ -50,7 +50,6 @@ func newGenCommand() *genCommand {
highlightStyle string
lineNumbersInlineStyle string
lineNumbersTableStyle string
omitEmpty bool
)
newChromaStyles := func() simplecobra.Commander {
@@ -80,14 +79,7 @@ See https://xyproto.github.io/splash/docs/all.html for a preview of the availabl
if err != nil {
return err
}
var formatter *html.Formatter
if omitEmpty {
formatter = html.New(html.WithClasses(true))
} else {
formatter = html.New(html.WithAllClasses(true))
}
formatter := html.New(html.WithAllClasses(true))
w := os.Stdout
fmt.Fprintf(w, "/* Generated using: hugo %s */\n\n", strings.Join(os.Args[1:], " "))
formatter.WriteCSS(w, style)
@@ -103,8 +95,6 @@ See https://xyproto.github.io/splash/docs/all.html for a preview of the availabl
_ = 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().BoolVar(&omitEmpty, "omitEmpty", false, `omit empty CSS rules`)
_ = cmd.RegisterFlagCompletionFunc("omitEmpty", cobra.NoFileCompletions)
},
}
}
@@ -184,13 +174,13 @@ url: %s
prepender := func(filename string) string {
name := filepath.Base(filename)
base := strings.TrimSuffix(name, path.Ext(name))
url := "/docs/reference/commands/" + strings.ToLower(base) + "/"
url := "/commands/" + strings.ToLower(base) + "/"
return fmt.Sprintf(gendocFrontmatterTemplate, strings.Replace(base, "_", " ", -1), base, url)
}
linkHandler := func(name string) string {
base := strings.TrimSuffix(name, path.Ext(name))
return "/docs/reference/commands/" + strings.ToLower(base) + "/"
return "/commands/" + strings.ToLower(base) + "/"
}
r.Println("Generating Hugo command-line documentation in", gendocdir, "...")
doc.GenMarkdownTreeCustom(cd.CobraCommand.Root(), gendocdir, prepender, linkHandler)
@@ -232,7 +222,7 @@ url: %s
}
// Decode the JSON to a map[string]interface{} and then unmarshal it again to the correct format.
var m map[string]any
var m map[string]interface{}
if err := json.Unmarshal(buf.Bytes(), &m); err != nil {
return err
}
+1 -4
View File
@@ -962,7 +962,7 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
lrl.Logf("no page to navigate to, force refresh")
livereload.ForceRefresh()
}
} else if len(otherChanges) > 0 || len(cssChanges) > 0 {
} else if len(otherChanges) > 0 {
if len(otherChanges) == 1 {
// Allow single changes to be refreshed without a full page reload.
pathToRefresh := h.PathSpec.RelURL(paths.ToSlashTrimLeading(otherChanges[0]), false)
@@ -972,9 +972,6 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
lrl.Logf("force refresh")
livereload.ForceRefresh()
}
} else {
lrl.Logf("force refresh")
livereload.ForceRefresh()
}
if len(cssChanges) > 0 {
+11 -14
View File
@@ -76,8 +76,10 @@ Ensure you run this within the root directory of your site.`,
&simpleCommand{
name: "site",
use: "site [path]",
short: "Create a new site",
long: `Create a new site at the specified path.`,
short: "Create a new site (skeleton)",
long: `Create a new site in the provided directory.
The new site will have the correct structure, but no content or theme yet.
Use ` + "`hugo new [contentPath]`" + ` to create new content.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
if len(args) < 1 {
return newUserError("path needs to be provided")
@@ -122,9 +124,11 @@ Ensure you run this within the root directory of your site.`,
&simpleCommand{
name: "theme",
use: "theme [name]",
short: "Create a new theme",
long: `Create a new theme with the specified name in the ./themes directory.
This generates a functional theme including template examples and sample content.`,
short: "Create a new theme (skeleton)",
long: `Create a new theme (skeleton) called [name] in ./themes.
New theme is a skeleton. Please add content to the touched files. Add your
name to the copyright line in the license and adjust the theme.toml file
according to your needs.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
if len(args) < 1 {
return newUserError("theme name needs to be provided")
@@ -140,7 +144,7 @@ This generates a functional theme including template examples and sample content
createpath := paths.AbsPathify(conf.configs.Base.WorkingDir, filepath.Join(conf.configs.Base.ThemesDir, args[0]))
r.Println("Creating new theme in", createpath)
err = skeletons.CreateTheme(createpath, sourceFs, format)
err = skeletons.CreateTheme(createpath, sourceFs)
if err != nil {
return err
}
@@ -148,14 +152,7 @@ This generates a functional theme including template examples and sample content
return nil
},
withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.ValidArgsFunction = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
if len(args) != 0 {
return []string{}, cobra.ShellCompDirectiveNoFileComp
}
return []string{}, cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveFilterDirs
}
cmd.Flags().StringVar(&format, "format", "toml", "preferred file format (toml, yaml or json)")
_ = cmd.RegisterFlagCompletionFunc("format", cobra.FixedCompletions([]string{"toml", "yaml", "json"}, cobra.ShellCompDirectiveNoFileComp))
cmd.ValidArgsFunction = cobra.NoFileCompletions
},
},
},
+12 -11
View File
@@ -23,7 +23,6 @@ import (
"errors"
"fmt"
"io"
"maps"
"net"
"net/http"
_ "net/http/pprof"
@@ -49,7 +48,6 @@ import (
"github.com/fsnotify/fsnotify"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/common/urls"
@@ -59,6 +57,7 @@ import (
"github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/hugolib/filesystems"
"github.com/gohugoio/hugo/livereload"
"github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/transform"
"github.com/gohugoio/hugo/transform/livereloadinject"
"github.com/spf13/afero"
@@ -196,7 +195,9 @@ func (f *fileChangeDetector) PrepareNew() {
}
f.prev = make(map[string]uint64)
maps.Copy(f.prev, f.current)
for k, v := range f.current {
f.prev[k] = v
}
f.current = make(map[string]uint64)
}
@@ -627,7 +628,7 @@ func (c *serverCommand) setServerInfoInConfig() error {
panic("no server ports set")
}
return c.withConfE(func(conf *commonConfig) error {
for i, language := range conf.configs.LanguagesDefaultFirst {
for i, language := range conf.configs.Languages {
isMultihost := conf.configs.IsMultihost
var serverPort int
if isMultihost {
@@ -758,7 +759,7 @@ func (c *serverCommand) createServerPorts(cd *simplecobra.Commandeer) error {
c.serverPorts = make([]serverPortListener, len(conf.configs.Languages))
}
currentServerPort := c.serverPort
for i := range c.serverPorts {
for i := 0; i < len(c.serverPorts); i++ {
l, err := net.Listen("tcp", net.JoinHostPort(c.serverInterface, strconv.Itoa(currentServerPort)))
if err == nil {
c.serverPorts[i] = serverPortListener{ln: l, p: currentServerPort}
@@ -897,16 +898,16 @@ func (c *serverCommand) serve() error {
// To allow the en user to change the error template while the server is running, we use
// the freshest template we can provide.
var (
errTempl *tplimpl.TemplInfo
templHandler *tplimpl.TemplateStore
errTempl tpl.Template
templHandler tpl.TemplateHandler
)
getErrorTemplateAndHandler := func(h *hugolib.HugoSites) (*tplimpl.TemplInfo, *tplimpl.TemplateStore) {
getErrorTemplateAndHandler := func(h *hugolib.HugoSites) (tpl.Template, tpl.TemplateHandler) {
if h == nil {
return errTempl, templHandler
}
templHandler := h.GetTemplateStore()
errTempl := templHandler.LookupByPath("/_server/error.html")
if errTempl == nil {
templHandler := h.Tmpl()
errTempl, found := templHandler.Lookup("_server/error.html")
if !found {
panic("template server/error.html not found")
}
return errTempl, templHandler
+2 -2
View File
@@ -117,7 +117,7 @@ func appendToInterfaceSliceFromValues(slice1, slice2 reflect.Value) ([]any, erro
tos = append(tos, nil)
continue
}
for i := range slice.Len() {
for i := 0; i < slice.Len(); i++ {
tos = append(tos, slice.Index(i).Interface())
}
}
@@ -128,7 +128,7 @@ func appendToInterfaceSliceFromValues(slice1, slice2 reflect.Value) ([]any, erro
func appendToInterfaceSlice(tov reflect.Value, from ...any) ([]any, error) {
var tos []any
for i := range tov.Len() {
for i := 0; i < tov.Len(); i++ {
tos = append(tos, tov.Index(i).Interface())
}
-65
View File
@@ -15,7 +15,6 @@ package collections
import (
"html/template"
"reflect"
"testing"
qt "github.com/frankban/quicktest"
@@ -78,7 +77,6 @@ func TestAppend(t *testing.T) {
{[]string{"a", "b"}, []any{nil}, []any{"a", "b", nil}},
{[]string{"a", "b"}, []any{nil, "d", nil}, []any{"a", "b", nil, "d", nil}},
{[]any{"a", nil, "c"}, []any{"d", nil, "f"}, []any{"a", nil, "c", "d", nil, "f"}},
{[]string{"a", "b"}, []any{}, []string{"a", "b"}},
} {
result, err := Append(test.start, test.addend...)
@@ -148,66 +146,3 @@ func TestAppendShouldMakeACopyOfTheInputSlice(t *testing.T) {
c.Assert(result, qt.DeepEquals, []string{"a", "b", "c"})
c.Assert(slice, qt.DeepEquals, []string{"d", "b"})
}
func TestIndirect(t *testing.T) {
t.Parallel()
c := qt.New(t)
type testStruct struct {
Field string
}
var (
nilPtr *testStruct
nilIface interface{} = nil
nonNilIface interface{} = &testStruct{Field: "hello"}
)
tests := []struct {
name string
input any
wantKind reflect.Kind
wantNil bool
}{
{
name: "nil pointer",
input: nilPtr,
wantKind: reflect.Ptr,
wantNil: true,
},
{
name: "nil interface",
input: nilIface,
wantKind: reflect.Invalid,
wantNil: false,
},
{
name: "non-nil pointer to struct",
input: &testStruct{Field: "abc"},
wantKind: reflect.Struct,
wantNil: false,
},
{
name: "non-nil interface holding pointer",
input: nonNilIface,
wantKind: reflect.Struct,
wantNil: false,
},
{
name: "plain value",
input: testStruct{Field: "xyz"},
wantKind: reflect.Struct,
wantNil: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
v := reflect.ValueOf(tt.input)
got, isNil := indirect(v)
c.Assert(got.Kind(), qt.Equals, tt.wantKind)
c.Assert(isNil, qt.Equals, tt.wantNil)
})
}
}
-34
View File
@@ -136,37 +136,3 @@ func TestSortedStringSlice(t *testing.T) {
c.Assert(s.Count("z"), qt.Equals, 0)
c.Assert(s.Count("a"), qt.Equals, 1)
}
func TestStringSliceToInterfaceSlice(t *testing.T) {
t.Parallel()
c := qt.New(t)
tests := []struct {
name string
in []string
want []any
}{
{
name: "empty slice",
in: []string{},
want: []any{},
},
{
name: "single element",
in: []string{"hello"},
want: []any{"hello"},
},
{
name: "multiple elements",
in: []string{"a", "b", "c"},
want: []any{"a", "b", "c"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := StringSliceToInterfaceSlice(tt.in)
c.Assert(got, qt.DeepEquals, tt.want)
})
}
}
+1 -3
View File
@@ -13,8 +13,6 @@
package collections
import "slices"
import "sync"
// Stack is a simple LIFO stack that is safe for concurrent use.
@@ -75,7 +73,7 @@ func (s *Stack[T]) DrainMatching(predicate func(T) bool) []T {
for i := len(s.items) - 1; i >= 0; i-- {
if predicate(s.items[i]) {
items = append(items, s.items[i])
s.items = slices.Delete(s.items, i, i+1)
s.items = append(s.items[:i], s.items[i+1:]...)
}
}
return items
-77
View File
@@ -1,77 +0,0 @@
package collections
import (
"testing"
qt "github.com/frankban/quicktest"
)
func TestNewStack(t *testing.T) {
t.Parallel()
c := qt.New(t)
s := NewStack[int]()
c.Assert(s, qt.IsNotNil)
}
func TestStackBasic(t *testing.T) {
t.Parallel()
c := qt.New(t)
s := NewStack[int]()
c.Assert(s.Len(), qt.Equals, 0)
s.Push(1)
s.Push(2)
s.Push(3)
c.Assert(s.Len(), qt.Equals, 3)
top, ok := s.Peek()
c.Assert(ok, qt.Equals, true)
c.Assert(top, qt.Equals, 3)
popped, ok := s.Pop()
c.Assert(ok, qt.Equals, true)
c.Assert(popped, qt.Equals, 3)
c.Assert(s.Len(), qt.Equals, 2)
_, _ = s.Pop()
_, _ = s.Pop()
_, ok = s.Pop()
c.Assert(ok, qt.Equals, false)
}
func TestStackDrain(t *testing.T) {
t.Parallel()
c := qt.New(t)
s := NewStack[string]()
s.Push("a")
s.Push("b")
got := s.Drain()
c.Assert(got, qt.DeepEquals, []string{"a", "b"})
c.Assert(s.Len(), qt.Equals, 0)
}
func TestStackDrainMatching(t *testing.T) {
t.Parallel()
c := qt.New(t)
s := NewStack[int]()
s.Push(1)
s.Push(2)
s.Push(3)
s.Push(4)
got := s.DrainMatching(func(v int) bool { return v%2 == 0 })
c.Assert(got, qt.DeepEquals, []int{4, 2})
c.Assert(s.Drain(), qt.DeepEquals, []int{1, 3})
}
-2
View File
@@ -23,8 +23,6 @@ const (
WarnFrontMatterParamsOverrides = "warning-frontmatter-params-overrides"
WarnRenderShortcodesInHTML = "warning-rendershortcodes-in-html"
WarnGoldmarkRawHTML = "warning-goldmark-raw-html"
WarnPartialSuperfluousPrefix = "warning-partial-superfluous-prefix"
WarnHomePageIsLeafBundle = "warning-home-page-is-leaf-bundle"
)
// Field/method names with special meaning.
+4 -4
View File
@@ -37,12 +37,12 @@ func TestXxHashFromReaderPara(t *testing.T) {
c := qt.New(t)
var wg sync.WaitGroup
for i := range 10 {
for i := 0; i < 10; i++ {
i := i
wg.Add(1)
go func() {
defer wg.Done()
for j := range 100 {
for j := 0; j < 100; j++ {
s := strings.Repeat("Hello ", i+j+1*42)
r := strings.NewReader(s)
got, size, err := XXHashFromReader(r)
@@ -144,8 +144,8 @@ func BenchmarkHashString(b *testing.B) {
}
func BenchmarkHashMap(b *testing.B) {
m := map[string]any{}
for i := range 1000 {
m := map[string]interface{}{}
for i := 0; i < 1000; i++ {
m[fmt.Sprintf("key%d", i)] = i
}
+8 -2
View File
@@ -152,7 +152,10 @@ func locateError(r io.Reader, le FileError, matches LineMatcherFn) *ErrorContext
}
if ectx.Position.LineNumber > 0 {
low := max(ectx.Position.LineNumber-3, 0)
low := ectx.Position.LineNumber - 3
if low < 0 {
low = 0
}
if ectx.Position.LineNumber > 2 {
ectx.LinesPos = 2
@@ -160,7 +163,10 @@ func locateError(r io.Reader, le FileError, matches LineMatcherFn) *ErrorContext
ectx.LinesPos = ectx.Position.LineNumber - 1
}
high := min(ectx.Position.LineNumber+2, len(lines))
high := ectx.Position.LineNumber + 2
if high > len(lines) {
high = len(lines)
}
ectx.Lines = lines[low:high]
+18 -5
View File
@@ -27,6 +27,7 @@ import (
"sync"
"github.com/bep/logg"
"github.com/cli/safeexec"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/config"
@@ -112,6 +113,18 @@ func IsNotFound(err error) bool {
return errors.As(err, &notFoundErr)
}
// SafeCommand is a wrapper around os/exec Command which uses a LookPath
// implementation that does not search in current directory before looking in PATH.
// See https://github.com/cli/safeexec and the linked issues.
func SafeCommand(name string, arg ...string) (*exec.Cmd, error) {
bin, err := safeexec.LookPath(name)
if err != nil {
return nil, err
}
return exec.Command(bin, arg...), nil
}
// Exec enforces a security policy for commands run via os/exec.
type Exec struct {
sc security.Config
@@ -184,7 +197,7 @@ func (e *Exec) Npx(name string, arg ...any) (Runner, error) {
tryFuncs := map[binaryLocation]tryFunc{
binaryLocationNodeModules: func() func(...any) (Runner, error) {
nodeBinFilename := filepath.Join(e.workingDir, nodeModulesBinPath, name)
_, err := exec.LookPath(nodeBinFilename)
_, err := safeexec.LookPath(nodeBinFilename)
if err != nil {
return nil
}
@@ -202,7 +215,7 @@ func (e *Exec) Npx(name string, arg ...any) (Runner, error) {
}
},
binaryLocationPath: func() func(...any) (Runner, error) {
if _, err := exec.LookPath(name); err != nil {
if _, err := safeexec.LookPath(name); err != nil {
return nil
}
return func(arg2 ...any) (Runner, error) {
@@ -333,7 +346,7 @@ func (c *commandeer) command(arg ...any) (*cmdWrapper, error) {
bin = c.fullyQualifiedName
} else {
var err error
bin, err = exec.LookPath(c.name)
bin, err = safeexec.LookPath(c.name)
if err != nil {
return nil, &NotFoundError{
name: c.name,
@@ -371,7 +384,7 @@ func InPath(binaryName string) bool {
if strings.Contains(binaryName, "/") {
panic("binary name should not contain any slash")
}
_, err := exec.LookPath(binaryName)
_, err := safeexec.LookPath(binaryName)
return err == nil
}
@@ -381,7 +394,7 @@ func LookPath(binaryName string) string {
if strings.Contains(binaryName, "/") {
panic("binary name should not contain any slash")
}
s, err := exec.LookPath(binaryName)
s, err := safeexec.LookPath(binaryName)
if err != nil {
return ""
}
+17 -6
View File
@@ -134,7 +134,12 @@ type methodKey struct {
name string
}
var methodCache sync.Map
type methods struct {
sync.RWMutex
cache map[methodKey]int
}
var methodCache = &methods{cache: make(map[methodKey]int)}
// GetMethodByName is the same as reflect.Value.MethodByName, but it caches the
// type lookup.
@@ -152,16 +157,22 @@ func GetMethodByName(v reflect.Value, name string) reflect.Value {
// -1 if no such method exists.
func GetMethodIndexByName(tp reflect.Type, name string) int {
k := methodKey{tp, name}
v, found := methodCache.Load(k)
methodCache.RLock()
index, found := methodCache.cache[k]
methodCache.RUnlock()
if found {
return v.(int)
return index
}
methodCache.Lock()
defer methodCache.Unlock()
m, ok := tp.MethodByName(name)
index := m.Index
index = m.Index
if !ok {
index = -1
}
methodCache.Store(k, index)
methodCache.cache[k] = index
if !ok {
return -1
@@ -234,7 +245,7 @@ func ToSliceAny(v any) ([]any, bool) {
vvv := reflect.ValueOf(v)
if vvv.Kind() == reflect.Slice {
out := make([]any, vvv.Len())
for i := range vvv.Len() {
for i := 0; i < vvv.Len(); i++ {
out[i] = vvv.Index(i).Interface()
}
return out, true
-14
View File
@@ -134,17 +134,3 @@ func BenchmarkGetMethodByName(b *testing.B) {
}
}
}
func BenchmarkGetMethodByNamePara(b *testing.B) {
v := reflect.ValueOf(&testStruct{})
methods := []string{"Method1", "Method2", "Method3", "Method4", "Method5"}
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
for _, method := range methods {
_ = GetMethodByName(v, method)
}
}
})
}
+16 -7
View File
@@ -16,7 +16,6 @@ package hstrings
import (
"fmt"
"regexp"
"slices"
"strings"
"sync"
@@ -51,7 +50,12 @@ func (s StringEqualFold) Eq(s2 any) bool {
// EqualAny returns whether a string is equal to any of the given strings.
func EqualAny(a string, b ...string) bool {
return slices.Contains(b, a)
for _, s := range b {
if a == s {
return true
}
}
return false
}
// regexpCache represents a cache of regexp objects protected by a mutex.
@@ -99,7 +103,12 @@ func GetOrCompileRegexp(pattern string) (re *regexp.Regexp, err error) {
// InSlice checks if a string is an element of a slice of strings
// and returns a boolean value.
func InSlice(arr []string, el string) bool {
return slices.Contains(arr, el)
for _, v := range arr {
if v == el {
return true
}
}
return false
}
// InSlicEqualFold checks if a string is an element of a slice of strings
@@ -128,7 +137,7 @@ func ToString(v any) (string, bool) {
return "", false
}
type (
Strings2 [2]string
Strings3 [3]string
)
type Tuple struct {
First string
Second string
}
+1 -1
View File
@@ -46,7 +46,7 @@ func TestHasBytesWriter(t *testing.T) {
return strings.Repeat("ab cfo", r.Intn(33))
}
for range 22 {
for i := 0; i < 22; i++ {
h, w := neww()
fmt.Fprint(w, rndStr()+"abc __foobar"+rndStr())
c.Assert(h.Patterns[0].Match, qt.Equals, true)
+10 -22
View File
@@ -139,13 +139,9 @@ func (i HugoInfo) IsMultilingual() bool {
return i.conf.IsMultilingual()
}
type contextKey uint8
type contextKey string
const (
contextKeyMarkupScope contextKey = iota
)
var markupScope = hcontext.NewContextDispatcher[string](contextKeyMarkupScope)
var markupScope = hcontext.NewContextDispatcher[string](contextKey("markupScope"))
type Context struct{}
@@ -415,28 +411,20 @@ func IsDartSassGeV2() bool {
// 2. Their theme to work for at least the last few Hugo versions.
func Deprecate(item, alternative string, version string) {
level := deprecationLogLevelFromVersion(version)
deprecateLevel(item, alternative, version, level)
}
// See Deprecate for details.
func DeprecateWithLogger(item, alternative string, version string, log logg.Logger) {
level := deprecationLogLevelFromVersion(version)
deprecateLevelWithLogger(item, alternative, version, level, log)
DeprecateLevel(item, alternative, version, level)
}
// DeprecateLevelMin informs about a deprecation starting at the given version, but with a minimum log level.
func DeprecateLevelMin(item, alternative string, version string, minLevel logg.Level) {
level := max(deprecationLogLevelFromVersion(version), minLevel)
deprecateLevel(item, alternative, version, level)
}
// deprecateLevel informs about a deprecation logging at the given level.
func deprecateLevel(item, alternative, version string, level logg.Level) {
deprecateLevelWithLogger(item, alternative, version, level, loggers.Log().Logger())
level := deprecationLogLevelFromVersion(version)
if level < minLevel {
level = minLevel
}
DeprecateLevel(item, alternative, version, level)
}
// DeprecateLevel informs about a deprecation logging at the given level.
func deprecateLevelWithLogger(item, alternative, version string, level logg.Level, log logg.Logger) {
func DeprecateLevel(item, alternative, version string, level logg.Level) {
var msg string
if level == logg.LevelError {
msg = fmt.Sprintf("%s was deprecated in Hugo %s and subsequently removed. %s", item, version, alternative)
@@ -444,7 +432,7 @@ func deprecateLevelWithLogger(item, alternative, version string, level logg.Leve
msg = fmt.Sprintf("%s was deprecated in Hugo %s and will be removed in a future release. %s", item, version, alternative)
}
log.WithLevel(level).WithField(loggers.FieldNameCmd, "deprecated").Logf("%s", msg)
loggers.Log().Logger().WithLevel(level).WithField(loggers.FieldNameCmd, "deprecated").Logf("%s", msg)
}
// We usually do about one minor version a month.
+1 -1
View File
@@ -17,7 +17,7 @@ package hugo
// This should be the only one.
var CurrentVersion = Version{
Major: 0,
Minor: 149,
Minor: 145,
PatchLevel: 0,
Suffix: "-DEV",
}
+2 -2
View File
@@ -37,7 +37,7 @@ func TestLogDistinct(t *testing.T) {
l := loggers.New(opts)
for range 10 {
for i := 0; i < 10; i++ {
l.Errorln("error 1")
l.Errorln("error 2")
l.Warnln("warn 1")
@@ -137,7 +137,7 @@ func TestReset(t *testing.T) {
l := loggers.New(opts)
for range 3 {
for i := 0; i < 3; i++ {
l.Errorln("error 1")
l.Errorln("error 2")
l.Errorln("error 1")
+1 -20
View File
@@ -69,14 +69,6 @@ func (c *Cache[K, T]) GetOrCreate(key K, create func() (T, error)) (T, error) {
return v, nil
}
// Contains returns whether the given key exists in the cache.
func (c *Cache[K, T]) Contains(key K) bool {
c.RLock()
_, found := c.m[key]
c.RUnlock()
return found
}
// InitAndGet initializes the cache if not already done and returns the value for the given key.
// The init state will be reset on Reset or Drain.
func (c *Cache[K, T]) InitAndGet(key K, init func(get func(key K) (T, bool), set func(key K, value T)) error) (T, error) {
@@ -116,17 +108,6 @@ func (c *Cache[K, T]) Set(key K, value T) {
c.Unlock()
}
// SetIfAbsent sets the given key to the given value if the key does not already exist in the cache.
func (c *Cache[K, T]) SetIfAbsent(key K, value T) {
c.RLock()
if _, found := c.get(key); !found {
c.RUnlock()
c.Set(key, value)
} else {
c.RUnlock()
}
}
func (c *Cache[K, T]) set(key K, value T) {
c.m[key] = value
}
@@ -160,7 +141,7 @@ func (c *Cache[K, T]) Len() int {
func (c *Cache[K, T]) Reset() {
c.Lock()
clear(c.m)
c.m = make(map[K]T)
c.hasBeenInitialized = false
c.Unlock()
}
+1 -12
View File
@@ -14,8 +14,6 @@
package maps
import (
"slices"
"github.com/gohugoio/hugo/common/hashing"
)
@@ -58,15 +56,6 @@ func (m *Ordered[K, T]) Get(key K) (T, bool) {
return value, found
}
// Has returns whether the given key exists in the map.
func (m *Ordered[K, T]) Has(key K) bool {
if m == nil {
return false
}
_, found := m.values[key]
return found
}
// Delete deletes the value for the given key.
func (m *Ordered[K, T]) Delete(key K) {
if m == nil {
@@ -75,7 +64,7 @@ func (m *Ordered[K, T]) Delete(key K) {
delete(m.values, key)
for i, k := range m.keys {
if k == key {
m.keys = slices.Delete(m.keys, i, i+1)
m.keys = append(m.keys[:i], m.keys[i+1:]...)
break
}
}
+1 -1
View File
@@ -140,7 +140,7 @@ func TestScratchInParallel(t *testing.T) {
for i := 1; i <= 10; i++ {
wg.Add(1)
go func(j int) {
for k := range 10 {
for k := 0; k < 10; k++ {
newVal := int64(k + j)
_, err := scratch.Add(key, newVal)
+3 -3
View File
@@ -42,7 +42,7 @@ func TestPara(t *testing.T) {
c.Run("Order", func(c *qt.C) {
n := 500
ints := make([]int, n)
for i := range n {
for i := 0; i < n; i++ {
ints[i] = i
}
@@ -51,7 +51,7 @@ func TestPara(t *testing.T) {
var result []int
var mu sync.Mutex
for i := range n {
for i := 0; i < n; i++ {
i := i
r.Run(func() error {
mu.Lock()
@@ -78,7 +78,7 @@ func TestPara(t *testing.T) {
var counter int64
for range n {
for i := 0; i < n; i++ {
r.Run(func() error {
atomic.AddInt64(&counter, 1)
time.Sleep(1 * time.Millisecond)
+97 -307
View File
@@ -23,11 +23,6 @@ import (
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/resources/kinds"
)
const (
identifierBaseof = "baseof"
)
// PathParser parses a path into a Path.
@@ -38,10 +33,6 @@ type PathParser struct {
// Reports whether the given language is disabled.
IsLangDisabled func(string) bool
// IsOutputFormat reports whether the given name is a valid output format.
// The second argument is optional.
IsOutputFormat func(name, ext string) bool
// Reports whether the given ext is a content file.
IsContentExt func(string) bool
}
@@ -92,10 +83,13 @@ func (pp *PathParser) Parse(c, s string) *Path {
}
func (pp *PathParser) newPath(component string) *Path {
p := &Path{}
p.reset()
p.component = component
return p
return &Path{
component: component,
posContainerLow: -1,
posContainerHigh: -1,
posSectionHigh: -1,
posIdentifierLanguage: -1,
}
}
func (pp *PathParser) parse(component, s string) (*Path, error) {
@@ -120,101 +114,10 @@ func (pp *PathParser) parse(component, s string) (*Path, error) {
return p, nil
}
func (pp *PathParser) parseIdentifier(component, s string, p *Path, i, lastDot, numDots int, isLast bool) {
if p.posContainerHigh != -1 {
return
}
mayHaveLang := numDots > 1 && p.posIdentifierLanguage == -1 && pp.LanguageIndex != nil
mayHaveLang = mayHaveLang && (component == files.ComponentFolderContent || component == files.ComponentFolderLayouts)
mayHaveOutputFormat := component == files.ComponentFolderLayouts
mayHaveKind := p.posIdentifierKind == -1 && mayHaveOutputFormat
var mayHaveLayout bool
if p.pathType == TypeShortcode {
mayHaveLayout = !isLast && component == files.ComponentFolderLayouts
} else {
mayHaveLayout = component == files.ComponentFolderLayouts
}
var found bool
var high int
if len(p.identifiersKnown) > 0 {
high = lastDot
} else {
high = len(p.s)
}
id := types.LowHigh[string]{Low: i + 1, High: high}
sid := p.s[id.Low:id.High]
if len(p.identifiersKnown) == 0 {
// The first is always the extension.
p.identifiersKnown = append(p.identifiersKnown, id)
found = true
// May also be the output format.
if mayHaveOutputFormat && pp.IsOutputFormat(sid, "") {
p.posIdentifierOutputFormat = 0
}
} else {
var langFound bool
if mayHaveLang {
var disabled bool
_, langFound = pp.LanguageIndex[sid]
if !langFound {
disabled = pp.IsLangDisabled != nil && pp.IsLangDisabled(sid)
if disabled {
p.disabled = true
langFound = true
}
}
found = langFound
if langFound {
p.identifiersKnown = append(p.identifiersKnown, id)
p.posIdentifierLanguage = len(p.identifiersKnown) - 1
}
}
if !found && mayHaveOutputFormat {
// At this point we may already have resolved an output format,
// but we need to keep looking for a more specific one, e.g. amp before html.
// Use both name and extension to prevent
// false positives on the form css.html.
if pp.IsOutputFormat(sid, p.Ext()) {
found = true
p.identifiersKnown = append(p.identifiersKnown, id)
p.posIdentifierOutputFormat = len(p.identifiersKnown) - 1
}
}
if !found && mayHaveKind {
if kinds.GetKindMain(sid) != "" {
found = true
p.identifiersKnown = append(p.identifiersKnown, id)
p.posIdentifierKind = len(p.identifiersKnown) - 1
}
}
if !found && sid == identifierBaseof {
found = true
p.identifiersKnown = append(p.identifiersKnown, id)
p.posIdentifierBaseof = len(p.identifiersKnown) - 1
}
if !found && mayHaveLayout {
p.identifiersKnown = append(p.identifiersKnown, id)
p.posIdentifierLayout = len(p.identifiersKnown) - 1
found = true
}
if !found {
p.identifiersUnknown = append(p.identifiersUnknown, id)
}
}
}
func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
hasLang := pp.LanguageIndex != nil
hasLang = hasLang && (component == files.ComponentFolderContent || component == files.ComponentFolderLayouts)
if runtime.GOOS == "windows" {
s = path.Clean(filepath.ToSlash(s))
if s == "." {
@@ -237,26 +140,46 @@ func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
p.s = s
slashCount := 0
lastDot := 0
lastSlashIdx := strings.LastIndex(s, "/")
numDots := strings.Count(s[lastSlashIdx+1:], ".")
if strings.Contains(s, "/_shortcodes/") {
p.pathType = TypeShortcode
}
for i := len(s) - 1; i >= 0; i-- {
c := s[i]
switch c {
case '.':
pp.parseIdentifier(component, s, p, i, lastDot, numDots, false)
lastDot = i
if p.posContainerHigh == -1 {
var high int
if len(p.identifiers) > 0 {
high = p.identifiers[len(p.identifiers)-1].Low - 1
} else {
high = len(p.s)
}
id := types.LowHigh[string]{Low: i + 1, High: high}
if len(p.identifiers) == 0 {
p.identifiers = append(p.identifiers, id)
} else if len(p.identifiers) == 1 {
// Check for a valid language.
s := p.s[id.Low:id.High]
if hasLang {
var disabled bool
_, langFound := pp.LanguageIndex[s]
if !langFound {
disabled = pp.IsLangDisabled != nil && pp.IsLangDisabled(s)
if disabled {
p.disabled = true
langFound = true
}
}
if langFound {
p.posIdentifierLanguage = 1
p.identifiers = append(p.identifiers, id)
}
}
}
}
case '/':
slashCount++
if p.posContainerHigh == -1 {
if lastDot > 0 {
pp.parseIdentifier(component, s, p, i, lastDot, numDots, true)
}
p.posContainerHigh = i + 1
} else if p.posContainerLow == -1 {
p.posContainerLow = i + 1
@@ -267,52 +190,26 @@ func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
}
}
if len(p.identifiersKnown) > 0 {
if len(p.identifiers) > 0 {
isContentComponent := p.component == files.ComponentFolderContent || p.component == files.ComponentFolderArchetypes
isContent := isContentComponent && pp.IsContentExt(p.Ext())
id := p.identifiersKnown[len(p.identifiersKnown)-1]
if id.Low > p.posContainerHigh {
b := p.s[p.posContainerHigh : id.Low-1]
if isContent {
switch b {
case "index":
p.pathType = TypeLeaf
case "_index":
p.pathType = TypeBranch
default:
p.pathType = TypeContentSingle
}
if slashCount == 2 && p.IsLeafBundle() {
p.posSectionHigh = 0
}
} else if b == files.NameContentData && files.IsContentDataExt(p.Ext()) {
p.pathType = TypeContentData
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
}
}
}
if p.pathType < TypeMarkup && component == files.ComponentFolderLayouts {
if p.posIdentifierBaseof != -1 {
p.pathType = TypeBaseof
} else {
pth := p.Path()
if strings.Contains(pth, "/_shortcodes/") {
p.pathType = TypeShortcode
} else if strings.Contains(pth, "/_markup/") {
p.pathType = TypeMarkup
} else if strings.HasPrefix(pth, "/_partials/") {
p.pathType = TypePartial
if slashCount == 2 && p.IsLeafBundle() {
p.posSectionHigh = 0
}
}
}
if p.pathType == TypeShortcode && p.posIdentifierLayout != -1 {
id := p.identifiersKnown[p.posIdentifierLayout]
if id.Low == p.posContainerHigh {
// First identifier is shortcode name.
p.posIdentifierLayout = -1
} else if b == files.NameContentData && files.IsContentDataExt(p.Ext()) {
p.bundleType = PathTypeContentData
}
}
@@ -321,44 +218,35 @@ func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
func ModifyPathBundleTypeResource(p *Path) {
if p.IsContent() {
p.pathType = TypeContentResource
p.bundleType = PathTypeContentResource
} else {
p.pathType = TypeFile
p.bundleType = PathTypeFile
}
}
//go:generate stringer -type Type
type Type int
type PathType int
const (
// A generic resource, e.g. a JSON file.
TypeFile Type = iota
PathTypeFile PathType = iota
// All below are content files.
// A resource of a content type with front matter.
TypeContentResource
PathTypeContentResource
// E.g. /blog/my-post.md
TypeContentSingle
PathTypeContentSingle
// All below are bundled content files.
// Leaf bundles, e.g. /blog/my-post/index.md
TypeLeaf
PathTypeLeaf
// Branch bundles, e.g. /blog/_index.md
TypeBranch
PathTypeBranch
// Content data file, _content.gotmpl.
TypeContentData
// Layout types.
TypeMarkup
TypeShortcode
TypePartial
TypeBaseof
PathTypeContentData
)
type Path struct {
@@ -369,18 +257,13 @@ type Path struct {
posContainerHigh int
posSectionHigh int
component string
pathType Type
component string
bundleType PathType
identifiersKnown []types.LowHigh[string]
identifiersUnknown []types.LowHigh[string]
identifiers []types.LowHigh[string]
posIdentifierLanguage int
posIdentifierOutputFormat int
posIdentifierKind int
posIdentifierLayout int
posIdentifierBaseof int
disabled bool
posIdentifierLanguage int
disabled bool
trimLeadingSlash bool
@@ -410,13 +293,9 @@ func (p *Path) reset() {
p.posContainerHigh = -1
p.posSectionHigh = -1
p.component = ""
p.pathType = 0
p.identifiersKnown = p.identifiersKnown[:0]
p.bundleType = 0
p.identifiers = p.identifiers[:0]
p.posIdentifierLanguage = -1
p.posIdentifierOutputFormat = -1
p.posIdentifierKind = -1
p.posIdentifierLayout = -1
p.posIdentifierBaseof = -1
p.disabled = false
p.trimLeadingSlash = false
p.unnormalized = nil
@@ -437,9 +316,6 @@ func (p *Path) norm(s string) string {
// IdentifierBase satisfies identity.Identity.
func (p *Path) IdentifierBase() string {
if p.Component() == files.ComponentFolderLayouts {
return p.Path()
}
return p.Base()
}
@@ -456,13 +332,6 @@ func (p *Path) Container() string {
return p.norm(p.s[p.posContainerLow : p.posContainerHigh-1])
}
func (p *Path) String() string {
if p == nil {
return "<nil>"
}
return p.Path()
}
// ContainerDir returns the container directory for this path.
// For content bundles this will be the parent directory.
func (p *Path) ContainerDir() string {
@@ -483,13 +352,13 @@ func (p *Path) Section() string {
// IsContent returns true if the path is a content file (e.g. mypost.md).
// Note that this will also return true for content files in a bundle.
func (p *Path) IsContent() bool {
return p.Type() >= TypeContentResource && p.Type() <= TypeContentData
return p.BundleType() >= PathTypeContentResource
}
// isContentPage returns true if the path is a content file (e.g. mypost.md),
// but nof if inside a leaf bundle.
func (p *Path) isContentPage() bool {
return p.Type() >= TypeContentSingle && p.Type() <= TypeContentData
return p.BundleType() >= PathTypeContentSingle
}
// Name returns the last element of path.
@@ -503,7 +372,7 @@ func (p *Path) Name() string {
// Name returns the last element of path without any extension.
func (p *Path) NameNoExt() string {
if i := p.identifierIndex(0); i != -1 {
return p.s[p.posContainerHigh : p.identifiersKnown[i].Low-1]
return p.s[p.posContainerHigh : p.identifiers[i].Low-1]
}
return p.s[p.posContainerHigh:]
}
@@ -515,7 +384,7 @@ func (p *Path) NameNoLang() string {
return p.Name()
}
return p.s[p.posContainerHigh:p.identifiersKnown[i].Low-1] + p.s[p.identifiersKnown[i].High:]
return p.s[p.posContainerHigh:p.identifiers[i].Low-1] + p.s[p.identifiers[i].High:]
}
// BaseNameNoIdentifier returns the logical base name for a resource without any identifier (e.g. no extension).
@@ -529,26 +398,10 @@ func (p *Path) BaseNameNoIdentifier() string {
// NameNoIdentifier returns the last element of path without any identifier (e.g. no extension).
func (p *Path) NameNoIdentifier() string {
lowHigh := p.nameLowHigh()
return p.s[lowHigh.Low:lowHigh.High]
}
func (p *Path) nameLowHigh() types.LowHigh[string] {
if len(p.identifiersKnown) > 0 {
lastID := p.identifiersKnown[len(p.identifiersKnown)-1]
if p.posContainerHigh == lastID.Low {
// The last identifier is the name.
return lastID
}
return types.LowHigh[string]{
Low: p.posContainerHigh,
High: p.identifiersKnown[len(p.identifiersKnown)-1].Low - 1,
}
}
return types.LowHigh[string]{
Low: p.posContainerHigh,
High: len(p.s),
if len(p.identifiers) > 0 {
return p.s[p.posContainerHigh : p.identifiers[len(p.identifiers)-1].Low-1]
}
return p.s[p.posContainerHigh:]
}
// Dir returns all but the last element of path, typically the path's directory.
@@ -568,11 +421,6 @@ func (p *Path) Path() (d string) {
return p.norm(p.s)
}
// PathNoLeadingSlash returns the full path without the leading slash.
func (p *Path) PathNoLeadingSlash() string {
return p.Path()[1:]
}
// Unnormalized returns the Path with the original case preserved.
func (p *Path) Unnormalized() *Path {
return p.unnormalized
@@ -588,28 +436,6 @@ func (p *Path) PathNoIdentifier() string {
return p.base(false, false)
}
// PathBeforeLangAndOutputFormatAndExt returns the path up to the first identifier that is not a language or output format.
func (p *Path) PathBeforeLangAndOutputFormatAndExt() string {
if len(p.identifiersKnown) == 0 {
return p.norm(p.s)
}
i := p.identifierIndex(0)
if j := p.posIdentifierOutputFormat; i == -1 || (j != -1 && j < i) {
i = j
}
if j := p.posIdentifierLanguage; i == -1 || (j != -1 && j < i) {
i = j
}
if i == -1 {
return p.norm(p.s)
}
id := p.identifiersKnown[i]
return p.norm(p.s[:id.Low-1])
}
// PathRel returns the path relative to the given owner.
func (p *Path) PathRel(owner *Path) string {
ob := owner.Base()
@@ -636,42 +462,26 @@ func (p *Path) Base() string {
return p.base(!p.isContentPage(), p.IsBundle())
}
// Used in template lookups.
// For pages with Type set, we treat that as the section.
func (p *Path) BaseReTyped(typ string) (d string) {
base := p.Base()
if typ == "" || p.Section() == typ {
return base
}
d = "/" + typ
if p.posSectionHigh != -1 {
d += base[p.posSectionHigh:]
}
d = p.norm(d)
return
}
// BaseNoLeadingSlash returns the base path without the leading slash.
func (p *Path) BaseNoLeadingSlash() string {
return p.Base()[1:]
}
func (p *Path) base(preserveExt, isBundle bool) string {
if len(p.identifiersKnown) == 0 {
if len(p.identifiers) == 0 {
return p.norm(p.s)
}
if preserveExt && len(p.identifiersKnown) == 1 {
if preserveExt && len(p.identifiers) == 1 {
// Preserve extension.
return p.norm(p.s)
}
var high int
id := p.identifiers[len(p.identifiers)-1]
high := id.Low - 1
if isBundle {
high = p.posContainerHigh - 1
} else {
high = p.nameLowHigh().High
}
if high == 0 {
@@ -683,7 +493,7 @@ func (p *Path) base(preserveExt, isBundle bool) string {
}
// For txt files etc. we want to preserve the extension.
id := p.identifiersKnown[0]
id = p.identifiers[0]
return p.norm(p.s[:high] + p.s[id.Low-1:id.High])
}
@@ -692,20 +502,8 @@ func (p *Path) Ext() string {
return p.identifierAsString(0)
}
func (p *Path) OutputFormat() string {
return p.identifierAsString(p.posIdentifierOutputFormat)
}
func (p *Path) Kind() string {
return p.identifierAsString(p.posIdentifierKind)
}
func (p *Path) Layout() string {
return p.identifierAsString(p.posIdentifierLayout)
}
func (p *Path) Lang() string {
return p.identifierAsString(p.posIdentifierLanguage)
return p.identifierAsString(1)
}
func (p *Path) Identifier(i int) string {
@@ -717,43 +515,35 @@ func (p *Path) Disabled() bool {
}
func (p *Path) Identifiers() []string {
ids := make([]string, len(p.identifiersKnown))
for i, id := range p.identifiersKnown {
ids := make([]string, len(p.identifiers))
for i, id := range p.identifiers {
ids[i] = p.s[id.Low:id.High]
}
return ids
}
func (p *Path) IdentifiersUnknown() []string {
ids := make([]string, len(p.identifiersUnknown))
for i, id := range p.identifiersUnknown {
ids[i] = p.s[id.Low:id.High]
}
return ids
}
func (p *Path) Type() Type {
return p.pathType
func (p *Path) BundleType() PathType {
return p.bundleType
}
func (p *Path) IsBundle() bool {
return p.pathType >= TypeLeaf && p.pathType <= TypeContentData
return p.bundleType >= PathTypeLeaf
}
func (p *Path) IsBranchBundle() bool {
return p.pathType == TypeBranch
return p.bundleType == PathTypeBranch
}
func (p *Path) IsLeafBundle() bool {
return p.pathType == TypeLeaf
return p.bundleType == PathTypeLeaf
}
func (p *Path) IsContentData() bool {
return p.pathType == TypeContentData
return p.bundleType == PathTypeContentData
}
func (p Path) ForType(t Type) *Path {
p.pathType = t
func (p Path) ForBundleType(t PathType) *Path {
p.bundleType = t
return &p
}
@@ -763,12 +553,12 @@ func (p *Path) identifierAsString(i int) string {
return ""
}
id := p.identifiersKnown[i]
id := p.identifiers[i]
return p.s[id.Low:id.High]
}
func (p *Path) identifierIndex(i int) int {
if i < 0 || i >= len(p.identifiersKnown) {
if i < 0 || i >= len(p.identifiers) {
return -1
}
return i
+2 -237
View File
@@ -18,7 +18,6 @@ import (
"testing"
"github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/resources/kinds"
qt "github.com/frankban/quicktest"
)
@@ -27,18 +26,10 @@ var testParser = &PathParser{
LanguageIndex: map[string]int{
"no": 0,
"en": 1,
"fr": 2,
},
IsContentExt: func(ext string) bool {
return ext == "md"
},
IsOutputFormat: func(name, ext string) bool {
switch name {
case "html", "amp", "csv", "rss":
return true
}
return false
},
}
func TestParse(t *testing.T) {
@@ -114,19 +105,17 @@ func TestParse(t *testing.T) {
"Basic Markdown file",
"/a/b/c.md",
func(c *qt.C, p *Path) {
c.Assert(p.Ext(), qt.Equals, "md")
c.Assert(p.Type(), qt.Equals, TypeContentSingle)
c.Assert(p.IsContent(), qt.IsTrue)
c.Assert(p.IsLeafBundle(), qt.IsFalse)
c.Assert(p.Name(), qt.Equals, "c.md")
c.Assert(p.Base(), qt.Equals, "/a/b/c")
c.Assert(p.BaseReTyped("foo"), qt.Equals, "/foo/b/c")
c.Assert(p.Section(), qt.Equals, "a")
c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "c")
c.Assert(p.Path(), qt.Equals, "/a/b/c.md")
c.Assert(p.Dir(), qt.Equals, "/a/b")
c.Assert(p.Container(), qt.Equals, "b")
c.Assert(p.ContainerDir(), qt.Equals, "/a/b")
c.Assert(p.Ext(), qt.Equals, "md")
},
},
{
@@ -141,7 +130,7 @@ func TestParse(t *testing.T) {
// Reclassify it as a content resource.
ModifyPathBundleTypeResource(p)
c.Assert(p.Type(), qt.Equals, TypeContentResource)
c.Assert(p.BundleType(), qt.Equals, PathTypeContentResource)
c.Assert(p.IsContent(), qt.IsTrue)
c.Assert(p.Name(), qt.Equals, "b.md")
c.Assert(p.Base(), qt.Equals, "/a/b.md")
@@ -174,10 +163,8 @@ func TestParse(t *testing.T) {
c.Assert(p.NameNoIdentifier(), qt.Equals, "b.a.b")
c.Assert(p.NameNoLang(), qt.Equals, "b.a.b.txt")
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"txt", "no"})
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"b", "a", "b"})
c.Assert(p.Base(), qt.Equals, "/a/b.a.b.txt")
c.Assert(p.BaseNoLeadingSlash(), qt.Equals, "a/b.a.b.txt")
c.Assert(p.Path(), qt.Equals, "/a/b.a.b.no.txt")
c.Assert(p.PathNoLang(), qt.Equals, "/a/b.a.b.txt")
c.Assert(p.Ext(), qt.Equals, "txt")
c.Assert(p.PathNoIdentifier(), qt.Equals, "/a/b.a.b")
@@ -187,11 +174,7 @@ func TestParse(t *testing.T) {
"Home branch cundle",
"/_index.md",
func(c *qt.C, p *Path) {
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md"})
c.Assert(p.IsBranchBundle(), qt.IsTrue)
c.Assert(p.IsBundle(), qt.IsTrue)
c.Assert(p.Base(), qt.Equals, "/")
c.Assert(p.BaseReTyped("foo"), qt.Equals, "/foo")
c.Assert(p.Path(), qt.Equals, "/_index.md")
c.Assert(p.Container(), qt.Equals, "")
c.Assert(p.ContainerDir(), qt.Equals, "/")
@@ -202,14 +185,12 @@ func TestParse(t *testing.T) {
"/a/index.md",
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/a")
c.Assert(p.BaseReTyped("foo"), qt.Equals, "/foo/a")
c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "a")
c.Assert(p.Container(), qt.Equals, "a")
c.Assert(p.Container(), qt.Equals, "a")
c.Assert(p.ContainerDir(), qt.Equals, "")
c.Assert(p.Dir(), qt.Equals, "/a")
c.Assert(p.Ext(), qt.Equals, "md")
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"index"})
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"md"})
c.Assert(p.IsBranchBundle(), qt.IsFalse)
c.Assert(p.IsBundle(), qt.IsTrue)
@@ -227,7 +208,6 @@ func TestParse(t *testing.T) {
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/a/b")
c.Assert(p.BaseNameNoIdentifier(), qt.Equals, "b")
c.Assert(p.BaseReTyped("foo"), qt.Equals, "/foo/b")
c.Assert(p.Container(), qt.Equals, "b")
c.Assert(p.ContainerDir(), qt.Equals, "/a")
c.Assert(p.Dir(), qt.Equals, "/a/b")
@@ -240,7 +220,6 @@ func TestParse(t *testing.T) {
c.Assert(p.NameNoExt(), qt.Equals, "index.no")
c.Assert(p.NameNoIdentifier(), qt.Equals, "index")
c.Assert(p.NameNoLang(), qt.Equals, "index.md")
c.Assert(p.Path(), qt.Equals, "/a/b/index.no.md")
c.Assert(p.PathNoLang(), qt.Equals, "/a/b/index.md")
c.Assert(p.Section(), qt.Equals, "a")
},
@@ -376,225 +355,11 @@ func TestParse(t *testing.T) {
}
for _, test := range tests {
c.Run(test.name, func(c *qt.C) {
if test.name != "Home branch cundle" {
// return
}
test.assert(c, testParser.Parse(files.ComponentFolderContent, test.path))
})
}
}
func TestParseLayouts(t *testing.T) {
c := qt.New(t)
tests := []struct {
name string
path string
assert func(c *qt.C, p *Path)
}{
{
"Basic",
"/list.html",
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/list.html")
c.Assert(p.OutputFormat(), qt.Equals, "html")
},
},
{
"Lang",
"/list.no.html",
func(c *qt.C, p *Path) {
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "no", "list"})
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{})
c.Assert(p.Base(), qt.Equals, "/list.html")
c.Assert(p.Lang(), qt.Equals, "no")
},
},
{
"Kind",
"/section.no.html",
func(c *qt.C, p *Path) {
c.Assert(p.Kind(), qt.Equals, kinds.KindSection)
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "no", "section"})
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{})
c.Assert(p.Base(), qt.Equals, "/section.html")
c.Assert(p.Lang(), qt.Equals, "no")
},
},
{
"Layout",
"/list.section.no.html",
func(c *qt.C, p *Path) {
c.Assert(p.Layout(), qt.Equals, "list")
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "no", "section", "list"})
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{})
c.Assert(p.Base(), qt.Equals, "/list.html")
c.Assert(p.Lang(), qt.Equals, "no")
},
},
{
"Layout multiple",
"/mylayout.list.section.no.html",
func(c *qt.C, p *Path) {
c.Assert(p.Layout(), qt.Equals, "mylayout")
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "no", "section", "list", "mylayout"})
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{})
c.Assert(p.Base(), qt.Equals, "/mylayout.html")
c.Assert(p.Lang(), qt.Equals, "no")
},
},
{
"Layout shortcode",
"/_shortcodes/myshort.list.no.html",
func(c *qt.C, p *Path) {
c.Assert(p.Layout(), qt.Equals, "list")
},
},
{
"Layout baseof",
"/baseof.list.no.html",
func(c *qt.C, p *Path) {
c.Assert(p.Layout(), qt.Equals, "list")
},
},
{
"Lang and output format",
"/list.no.amp.not.html",
func(c *qt.C, p *Path) {
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "not", "amp", "no", "list"})
c.Assert(p.OutputFormat(), qt.Equals, "amp")
c.Assert(p.Ext(), qt.Equals, "html")
c.Assert(p.Lang(), qt.Equals, "no")
c.Assert(p.Base(), qt.Equals, "/list.html")
},
},
{
"Term",
"/term.html",
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/term.html")
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "term"})
c.Assert(p.PathNoIdentifier(), qt.Equals, "/term")
c.Assert(p.PathBeforeLangAndOutputFormatAndExt(), qt.Equals, "/term")
c.Assert(p.Lang(), qt.Equals, "")
c.Assert(p.Kind(), qt.Equals, "term")
c.Assert(p.OutputFormat(), qt.Equals, "html")
},
},
{
"Shortcode with layout",
"/_shortcodes/myshortcode.list.html",
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/_shortcodes/myshortcode.html")
c.Assert(p.Type(), qt.Equals, TypeShortcode)
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "list"})
c.Assert(p.Layout(), qt.Equals, "list")
c.Assert(p.PathNoIdentifier(), qt.Equals, "/_shortcodes/myshortcode")
c.Assert(p.PathBeforeLangAndOutputFormatAndExt(), qt.Equals, "/_shortcodes/myshortcode.list")
c.Assert(p.Lang(), qt.Equals, "")
c.Assert(p.Kind(), qt.Equals, "")
c.Assert(p.OutputFormat(), qt.Equals, "html")
},
},
{
"Sub dir",
"/pages/home.html",
func(c *qt.C, p *Path) {
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "home"})
c.Assert(p.Lang(), qt.Equals, "")
c.Assert(p.Kind(), qt.Equals, "home")
c.Assert(p.OutputFormat(), qt.Equals, "html")
c.Assert(p.Dir(), qt.Equals, "/pages")
},
},
{
"Baseof",
"/pages/baseof.list.section.fr.amp.html",
func(c *qt.C, p *Path) {
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "amp", "fr", "section", "list", "baseof"})
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{})
c.Assert(p.Kind(), qt.Equals, kinds.KindSection)
c.Assert(p.Lang(), qt.Equals, "fr")
c.Assert(p.OutputFormat(), qt.Equals, "amp")
c.Assert(p.Dir(), qt.Equals, "/pages")
c.Assert(p.NameNoIdentifier(), qt.Equals, "baseof")
c.Assert(p.Type(), qt.Equals, TypeBaseof)
c.Assert(p.IdentifierBase(), qt.Equals, "/pages/baseof.list.section.fr.amp.html")
},
},
{
"Markup",
"/_markup/render-link.html",
func(c *qt.C, p *Path) {
c.Assert(p.Type(), qt.Equals, TypeMarkup)
},
},
{
"Markup nested",
"/foo/_markup/render-link.html",
func(c *qt.C, p *Path) {
c.Assert(p.Type(), qt.Equals, TypeMarkup)
},
},
{
"Shortcode",
"/_shortcodes/myshortcode.html",
func(c *qt.C, p *Path) {
c.Assert(p.Type(), qt.Equals, TypeShortcode)
},
},
{
"Shortcode nested",
"/foo/_shortcodes/myshortcode.html",
func(c *qt.C, p *Path) {
c.Assert(p.Type(), qt.Equals, TypeShortcode)
},
},
{
"Shortcode nested sub",
"/foo/_shortcodes/foo/myshortcode.html",
func(c *qt.C, p *Path) {
c.Assert(p.Type(), qt.Equals, TypeShortcode)
},
},
{
"Partials",
"/_partials/foo.bar",
func(c *qt.C, p *Path) {
c.Assert(p.Type(), qt.Equals, TypePartial)
},
},
{
"Shortcode lang in root",
"/_shortcodes/no.html",
func(c *qt.C, p *Path) {
c.Assert(p.Type(), qt.Equals, TypeShortcode)
c.Assert(p.Lang(), qt.Equals, "")
c.Assert(p.NameNoIdentifier(), qt.Equals, "no")
},
},
{
"Shortcode lang layout",
"/_shortcodes/myshortcode.no.html",
func(c *qt.C, p *Path) {
c.Assert(p.Type(), qt.Equals, TypeShortcode)
c.Assert(p.Lang(), qt.Equals, "no")
c.Assert(p.Layout(), qt.Equals, "")
c.Assert(p.NameNoIdentifier(), qt.Equals, "myshortcode")
},
},
}
for _, test := range tests {
c.Run(test.name, func(c *qt.C) {
if test.name != "Shortcode lang layout" {
// return
}
test.assert(c, testParser.Parse(files.ComponentFolderLayouts, test.path))
})
}
}
func TestHasExt(t *testing.T) {
c := qt.New(t)
-23
View File
@@ -78,26 +78,3 @@ disablePathToLower = true
b.AssertFileContent("public/en/mysection/mybundle/index.html", "en|Single")
b.AssertFileContent("public/fr/MySection/MyBundle/index.html", "fr|Single")
}
func TestIssue13596(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ['home','rss','section','sitemap','taxonomy','term']
-- content/p1/index.md --
---
title: p1
---
-- content/p1/a.1.txt --
-- content/p1/a.2.txt --
-- layouts/all.html --
{{ range .Resources.Match "*" }}{{ .Name }}|{{ end }}
`
b := hugolib.Test(t, files)
b.AssertFileContent("public/p1/index.html", "a.1.txt|a.2.txt|")
b.AssertFileExists("public/p1/a.1.txt", true)
b.AssertFileExists("public/p1/a.2.txt", true) // fails
}
+27
View File
@@ -0,0 +1,27 @@
// Code generated by "stringer -type=PathType"; DO NOT EDIT.
package paths
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[PathTypeFile-0]
_ = x[PathTypeContentResource-1]
_ = x[PathTypeContentSingle-2]
_ = x[PathTypeLeaf-3]
_ = x[PathTypeBranch-4]
}
const _PathType_name = "PathTypeFilePathTypeContentResourcePathTypeContentSinglePathTypeLeafPathTypeBranch"
var _PathType_index = [...]uint8{0, 12, 35, 56, 68, 82}
func (i PathType) String() string {
if i < 0 || i >= PathType(len(_PathType_index)-1) {
return "PathType(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _PathType_name[_PathType_index[i]:_PathType_index[i+1]]
}
-32
View File
@@ -1,32 +0,0 @@
// Code generated by "stringer -type Type"; DO NOT EDIT.
package paths
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[TypeFile-0]
_ = x[TypeContentResource-1]
_ = x[TypeContentSingle-2]
_ = x[TypeLeaf-3]
_ = x[TypeBranch-4]
_ = x[TypeContentData-5]
_ = x[TypeMarkup-6]
_ = x[TypeShortcode-7]
_ = x[TypePartial-8]
_ = x[TypeBaseof-9]
}
const _Type_name = "TypeFileTypeContentResourceTypeContentSingleTypeLeafTypeBranchTypeContentDataTypeMarkupTypeShortcodeTypePartialTypeBaseof"
var _Type_index = [...]uint8{0, 8, 27, 44, 52, 62, 77, 87, 100, 111, 121}
func (i Type) String() string {
if i < 0 || i >= Type(len(_Type_index)-1) {
return "Type(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _Type_name[_Type_index[i]:_Type_index[i+1]]
}
+1 -1
View File
@@ -51,7 +51,7 @@ func Run[T any](ctx context.Context, cfg Config[T]) Group[T] {
// Buffered for performance.
ch := make(chan T, cfg.NumWorkers)
for range cfg.NumWorkers {
for i := 0; i < cfg.NumWorkers; i++ {
g.Go(func() error {
for {
select {
+4 -1
View File
@@ -103,7 +103,10 @@ func (r *RunEvery) Add(name string, f Func) {
f.IntervalHigh = 20 * time.Second
}
start := max(f.IntervalHigh/3, f.IntervalLow)
start := f.IntervalHigh / 3
if start < f.IntervalLow {
start = f.IntervalLow
}
f.interval = start
f.last = time.Now()
+5
View File
@@ -17,6 +17,7 @@ package terminal
import (
"fmt"
"os"
"runtime"
"strings"
isatty "github.com/mattn/go-isatty"
@@ -40,6 +41,10 @@ func PrintANSIColors(f *os.File) bool {
// IsTerminal return true if the file descriptor is terminal and the TERM
// environment variable isn't a dumb one.
func IsTerminal(f *os.File) bool {
if runtime.GOOS == "windows" {
return false
}
fd := f.Fd()
return os.Getenv("TERM") != "dumb" && (isatty.IsTerminal(fd) || isatty.IsCygwinTerminal(fd))
}
+1 -1
View File
@@ -69,7 +69,7 @@ func ToStringSlicePreserveStringE(v any) ([]string, error) {
switch vv.Kind() {
case reflect.Slice, reflect.Array:
result = make([]string, vv.Len())
for i := range vv.Len() {
for i := 0; i < vv.Len(); i++ {
s, err := cast.ToStringE(vv.Index(i).Interface())
if err != nil {
return nil, err
+1 -2
View File
@@ -15,7 +15,6 @@
package types
import (
"slices"
"sync"
)
@@ -46,7 +45,7 @@ func (q *EvictingQueue[T]) Add(v T) *EvictingQueue[T] {
if len(q.set) == q.size {
// Full
delete(q.set, q.vals[0])
q.vals = slices.Delete(q.vals, 0, 1)
q.vals = append(q.vals[:0], q.vals[1:]...)
}
q.set[v] = true
q.vals = append(q.vals, v)
+1 -1
View File
@@ -55,7 +55,7 @@ func TestEvictingStringQueueConcurrent(t *testing.T) {
queue := NewEvictingQueue[string](3)
for range 100 {
for j := 0; j < 100; j++ {
wg.Add(1)
go func() {
defer wg.Done()
+1 -11
View File
@@ -28,16 +28,6 @@ type RLocker interface {
RUnlock()
}
type Locker interface {
Lock()
Unlock()
}
type RWLocker interface {
RLocker
Locker
}
// KeyValue is a interface{} tuple.
type KeyValue struct {
Key any
@@ -69,7 +59,7 @@ func (k KeyValues) String() string {
// KeyValues struct.
func NewKeyValuesStrings(key string, values ...string) KeyValues {
iv := make([]any, len(values))
for i := range values {
for i := 0; i < len(values); i++ {
iv[i] = values[i]
}
return KeyValues{Key: key, Values: iv}
+43 -155
View File
@@ -20,7 +20,6 @@ import (
"fmt"
"reflect"
"regexp"
"slices"
"sort"
"strconv"
"strings"
@@ -33,6 +32,7 @@ import (
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/common/urls"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/config/privacy"
@@ -42,7 +42,6 @@ import (
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugolib/segments"
"github.com/gohugoio/hugo/langs"
gc "github.com/gohugoio/hugo/markup/goldmark/goldmark_config"
"github.com/gohugoio/hugo/markup/markup_config"
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/minifiers"
@@ -83,7 +82,7 @@ func init() {
}
configLanguageKeys = make(map[string]bool)
addKeys := func(v reflect.Value) {
for i := range v.NumField() {
for i := 0; i < v.NumField(); i++ {
name := strings.ToLower(v.Type().Field(i).Name)
if skip[name] {
continue
@@ -147,7 +146,7 @@ type Config struct {
// The cascade configuration section contains the top level front matter cascade configuration options,
// a slice of page matcher and params to apply to those pages.
Cascade *config.ConfigNamespace[[]page.PageMatcherParamsConfig, *maps.Ordered[page.PageMatcher, page.PageMatcherParamsConfig]] `mapstructure:"-"`
Cascade *config.ConfigNamespace[[]page.PageMatcherParamsConfig, *maps.Ordered[page.PageMatcher, maps.Params]] `mapstructure:"-"`
// The segments defines segments for the site. Used for partial/segmented builds.
Segments *config.ConfigNamespace[map[string]segments.SegmentConfig, segments.Segments] `mapstructure:"-"`
@@ -397,80 +396,44 @@ func (c *Config) CompileConfig(logger loggers.Logger) error {
// Legacy paginate values.
if c.Paginate != 0 {
hugo.DeprecateWithLogger("site config key paginate", "Use pagination.pagerSize instead.", "v0.128.0", logger.Logger())
hugo.Deprecate("site config key paginate", "Use pagination.pagerSize instead.", "v0.128.0")
c.Pagination.PagerSize = c.Paginate
}
if c.PaginatePath != "" {
hugo.DeprecateWithLogger("site config key paginatePath", "Use pagination.path instead.", "v0.128.0", logger.Logger())
hugo.Deprecate("site config key paginatePath", "Use pagination.path instead.", "v0.128.0")
c.Pagination.Path = c.PaginatePath
}
// Legacy privacy values.
if c.Privacy.Twitter.Disable {
hugo.DeprecateWithLogger("site config key privacy.twitter.disable", "Use privacy.x.disable instead.", "v0.141.0", logger.Logger())
hugo.Deprecate("site config key privacy.twitter.disable", "Use privacy.x.disable instead.", "v0.141.0")
c.Privacy.X.Disable = c.Privacy.Twitter.Disable
}
if c.Privacy.Twitter.EnableDNT {
hugo.DeprecateWithLogger("site config key privacy.twitter.enableDNT", "Use privacy.x.enableDNT instead.", "v0.141.0", logger.Logger())
hugo.Deprecate("site config key privacy.twitter.enableDNT", "Use privacy.x.enableDNT instead.", "v0.141.0")
c.Privacy.X.EnableDNT = c.Privacy.Twitter.EnableDNT
}
if c.Privacy.Twitter.Simple {
hugo.DeprecateWithLogger("site config key privacy.twitter.simple", "Use privacy.x.simple instead.", "v0.141.0", logger.Logger())
hugo.Deprecate("site config key privacy.twitter.simple", "Use privacy.x.simple instead.", "v0.141.0")
c.Privacy.X.Simple = c.Privacy.Twitter.Simple
}
// Legacy services values.
if c.Services.Twitter.DisableInlineCSS {
hugo.DeprecateWithLogger("site config key services.twitter.disableInlineCSS", "Use services.x.disableInlineCSS instead.", "v0.141.0", logger.Logger())
hugo.Deprecate("site config key services.twitter.disableInlineCSS", "Use services.x.disableInlineCSS instead.", "v0.141.0")
c.Services.X.DisableInlineCSS = c.Services.Twitter.DisableInlineCSS
}
// Legacy permalink tokens
vs := fmt.Sprintf("%v", c.Permalinks)
if strings.Contains(vs, ":filename") {
hugo.DeprecateWithLogger("the \":filename\" permalink token", "Use \":contentbasename\" instead.", "0.144.0", logger.Logger())
hugo.Deprecate("the \":filename\" permalink token", "Use \":contentbasename\" instead.", "0.144.0")
}
if strings.Contains(vs, ":slugorfilename") {
hugo.DeprecateWithLogger("the \":slugorfilename\" permalink token", "Use \":slugorcontentbasename\" instead.", "0.144.0", logger.Logger())
}
// Legacy render hook values.
alternativeDetails := fmt.Sprintf(
"Set to %q if previous value was false, or set to %q if previous value was true.",
gc.RenderHookUseEmbeddedNever,
gc.RenderHookUseEmbeddedFallback,
)
if c.Markup.Goldmark.RenderHooks.Image.EnableDefault != nil {
alternative := "Use markup.goldmark.renderHooks.image.useEmbedded instead." + " " + alternativeDetails
hugo.DeprecateWithLogger("site config key markup.goldmark.renderHooks.image.enableDefault", alternative, "0.148.0", logger.Logger())
if *c.Markup.Goldmark.RenderHooks.Image.EnableDefault {
c.Markup.Goldmark.RenderHooks.Image.UseEmbedded = gc.RenderHookUseEmbeddedFallback
} else {
c.Markup.Goldmark.RenderHooks.Image.UseEmbedded = gc.RenderHookUseEmbeddedNever
}
}
if c.Markup.Goldmark.RenderHooks.Link.EnableDefault != nil {
alternative := "Use markup.goldmark.renderHooks.link.useEmbedded instead." + " " + alternativeDetails
hugo.DeprecateWithLogger("site config key markup.goldmark.renderHooks.link.enableDefault", alternative, "0.148.0", logger.Logger())
if *c.Markup.Goldmark.RenderHooks.Link.EnableDefault {
c.Markup.Goldmark.RenderHooks.Link.UseEmbedded = gc.RenderHookUseEmbeddedFallback
} else {
c.Markup.Goldmark.RenderHooks.Link.UseEmbedded = gc.RenderHookUseEmbeddedNever
}
}
// Validate render hook configuration.
renderHookUseEmbeddedModes := []string{
gc.RenderHookUseEmbeddedAlways,
gc.RenderHookUseEmbeddedAuto,
gc.RenderHookUseEmbeddedFallback,
gc.RenderHookUseEmbeddedNever,
}
if !slices.Contains(renderHookUseEmbeddedModes, c.Markup.Goldmark.RenderHooks.Image.UseEmbedded) {
return fmt.Errorf("site config markup.goldmark.renderHooks.image must be one of %s", helpers.StringSliceToList(renderHookUseEmbeddedModes, "or"))
}
if !slices.Contains(renderHookUseEmbeddedModes, c.Markup.Goldmark.RenderHooks.Link.UseEmbedded) {
return fmt.Errorf("site config markup.goldmark.renderHooks.link must be one of %s", helpers.StringSliceToList(renderHookUseEmbeddedModes, "or"))
hugo.Deprecate("the \":slugorfilename\" permalink token", "Use \":slugorcontentbasename\" instead.", "0.144.0")
}
c.C = &ConfigCompiled{
@@ -813,7 +776,7 @@ type Configs struct {
}
func (c *Configs) Validate(logger loggers.Logger) error {
c.Base.Cascade.Config.Range(func(p page.PageMatcher, cfg page.PageMatcherParamsConfig) bool {
c.Base.Cascade.Config.Range(func(p page.PageMatcher, params maps.Params) bool {
page.CheckCascadePattern(logger, p)
return true
})
@@ -837,58 +800,30 @@ func (c *Configs) IsZero() bool {
func (c *Configs) Init() error {
var languages langs.Languages
var langKeys []string
var hasEn bool
const en = "en"
for k := range c.LanguageConfigMap {
langKeys = append(langKeys, k)
if k == en {
hasEn = true
}
}
// Sort the LanguageConfigSlice by language weight (if set) or lang.
sort.Slice(langKeys, func(i, j int) bool {
ki := langKeys[i]
kj := langKeys[j]
lki := c.LanguageConfigMap[ki]
lkj := c.LanguageConfigMap[kj]
li := lki.Languages[ki]
lj := lkj.Languages[kj]
if li.Weight != lj.Weight {
return li.Weight < lj.Weight
}
return ki < kj
})
// See issue #13646.
defaultConfigLanguageFallback := en
if !hasEn {
// Pick the first one.
defaultConfigLanguageFallback = langKeys[0]
}
if c.Base.DefaultContentLanguage == "" {
c.Base.DefaultContentLanguage = defaultConfigLanguageFallback
}
for _, k := range langKeys {
v := c.LanguageConfigMap[k]
if v.DefaultContentLanguage == "" {
v.DefaultContentLanguage = defaultConfigLanguageFallback
}
c.LanguageConfigSlice = append(c.LanguageConfigSlice, v)
defaultContentLanguage := c.Base.DefaultContentLanguage
for k, v := range c.LanguageConfigMap {
languageConf := v.Languages[k]
language, err := langs.NewLanguage(k, c.Base.DefaultContentLanguage, v.TimeZone, languageConf)
language, err := langs.NewLanguage(k, defaultContentLanguage, v.TimeZone, languageConf)
if err != nil {
return err
}
languages = append(languages, language)
}
// Sort the sites by language weight (if set) or lang.
sort.Slice(languages, func(i, j int) bool {
li := languages[i]
lj := languages[j]
if li.Weight != lj.Weight {
return li.Weight < lj.Weight
}
return li.Lang < lj.Lang
})
for _, l := range languages {
c.LanguageConfigSlice = append(c.LanguageConfigSlice, c.LanguageConfigMap[l.Lang])
}
// Filter out disabled languages.
var n int
for _, l := range languages {
@@ -901,12 +836,12 @@ func (c *Configs) Init() error {
var languagesDefaultFirst langs.Languages
for _, l := range languages {
if l.Lang == c.Base.DefaultContentLanguage {
if l.Lang == defaultContentLanguage {
languagesDefaultFirst = append(languagesDefaultFirst, l)
}
}
for _, l := range languages {
if l.Lang != c.Base.DefaultContentLanguage {
if l.Lang != defaultContentLanguage {
languagesDefaultFirst = append(languagesDefaultFirst, l)
}
}
@@ -914,24 +849,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.ContentTypes.Config.IsContentSuffix,
IsOutputFormat: func(name, ext string) bool {
if name == "" {
return false
}
if of, ok := c.Base.OutputFormats.Config.GetByName(name); ok {
if ext != "" && !of.MediaType.HasSuffix(ext) {
return false
}
return true
}
return false
},
}
c.ContentPathParser = &paths.PathParser{LanguageIndex: languagesDefaultFirst.AsIndexSet(), IsLangDisabled: c.Base.IsLangDisabled, IsContentExt: c.Base.ContentTypes.Config.IsContentSuffix}
c.configLangs = make([]config.AllProvider, len(c.Languages))
for i, l := range c.LanguagesDefaultFirst {
@@ -992,48 +910,17 @@ func (c Configs) GetByLang(lang string) config.AllProvider {
return nil
}
func newDefaultConfig() *Config {
return &Config{
Taxonomies: map[string]string{"tag": "tags", "category": "categories"},
Sitemap: config.SitemapConfig{Priority: -1, Filename: "sitemap.xml"},
RootConfig: RootConfig{
Environment: hugo.EnvironmentProduction,
TitleCaseStyle: "AP",
PluralizeListTitles: true,
CapitalizeListTitles: true,
StaticDir: []string{"static"},
SummaryLength: 70,
Timeout: "60s",
CommonDirs: config.CommonDirs{
ArcheTypeDir: "archetypes",
ContentDir: "content",
ResourceDir: "resources",
PublishDir: "public",
ThemesDir: "themes",
AssetDir: "assets",
LayoutDir: "layouts",
I18nDir: "i18n",
DataDir: "data",
},
},
}
}
// fromLoadConfigResult creates a new Config from res.
func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadConfigResult) (*Configs, error) {
if !res.Cfg.IsSet("languages") {
// We need at least one
lang := res.Cfg.GetString("defaultContentLanguage")
if lang == "" {
lang = "en"
}
res.Cfg.Set("languages", maps.Params{lang: maps.Params{}})
}
bcfg := res.BaseConfig
cfg := res.Cfg
all := newDefaultConfig()
all := &Config{}
err := decodeConfigFromParams(fs, logger, bcfg, cfg, all, nil)
if err != nil {
@@ -1043,7 +930,6 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
langConfigMap := make(map[string]*Config)
languagesConfig := cfg.GetStringMap("languages")
var isMultihost bool
if err := all.CompileConfig(logger); err != nil {
@@ -1119,11 +1005,13 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
// Adjust Goldmark config defaults for multilingual, single-host sites.
if len(languagesConfig) > 1 && !isMultihost && !clone.Markup.Goldmark.DuplicateResourceFiles {
if clone.Markup.Goldmark.RenderHooks.Image.UseEmbedded == gc.RenderHookUseEmbeddedAuto {
clone.Markup.Goldmark.RenderHooks.Image.UseEmbedded = gc.RenderHookUseEmbeddedFallback
}
if clone.Markup.Goldmark.RenderHooks.Link.UseEmbedded == gc.RenderHookUseEmbeddedAuto {
clone.Markup.Goldmark.RenderHooks.Link.UseEmbedded = gc.RenderHookUseEmbeddedFallback
if !clone.Markup.Goldmark.DuplicateResourceFiles {
if clone.Markup.Goldmark.RenderHooks.Link.EnableDefault == nil {
clone.Markup.Goldmark.RenderHooks.Link.EnableDefault = types.NewBool(true)
}
if clone.Markup.Goldmark.RenderHooks.Image.EnableDefault == nil {
clone.Markup.Goldmark.RenderHooks.Image.EnableDefault = types.NewBool(true)
}
}
}
@@ -2,14 +2,11 @@ package allconfig_test
import (
"path/filepath"
"strings"
"testing"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/config/allconfig"
"github.com/gohugoio/hugo/hugolib"
gc "github.com/gohugoio/hugo/markup/goldmark/goldmark_config"
"github.com/gohugoio/hugo/media"
)
@@ -237,172 +234,3 @@ baseURL = "https://example.com"
b.Assert(c.IsContentFile("foo.md"), qt.Equals, true)
b.Assert(len(s), qt.Equals, 6)
}
func TestMergeDeep(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
baseURL = "https://example.com"
theme = ["theme1", "theme2"]
_merge = "deep"
-- themes/theme1/hugo.toml --
[sitemap]
filename = 'mysitemap.xml'
[services]
[services.googleAnalytics]
id = 'foo bar'
[taxonomies]
foo = 'bars'
-- themes/theme2/config/_default/hugo.toml --
[taxonomies]
bar = 'baz'
-- layouts/home.html --
GA ID: {{ site.Config.Services.GoogleAnalytics.ID }}.
`
b := hugolib.Test(t, files)
conf := b.H.Configs
base := conf.Base
b.Assert(base.Environment, qt.Equals, hugo.EnvironmentProduction)
b.Assert(base.BaseURL, qt.Equals, "https://example.com")
b.Assert(base.Sitemap.Filename, qt.Equals, "mysitemap.xml")
b.Assert(base.Taxonomies, qt.DeepEquals, map[string]string{"bar": "baz", "foo": "bars"})
b.AssertFileContent("public/index.html", "GA ID: foo bar.")
}
func TestMergeDeepBuildStats(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
baseURL = "https://example.com"
title = "Theme 1"
_merge = "deep"
[module]
[module.hugoVersion]
[[module.imports]]
path = "theme1"
-- themes/theme1/hugo.toml --
[build]
[build.buildStats]
disableIDs = true
enable = true
-- layouts/home.html --
Home.
`
b := hugolib.Test(t, files, hugolib.TestOptOsFs())
conf := b.H.Configs
base := conf.Base
b.Assert(base.Title, qt.Equals, "Theme 1")
b.Assert(len(base.Module.Imports), qt.Equals, 1)
b.Assert(base.Build.BuildStats.Enable, qt.Equals, true)
b.AssertFileExists("/hugo_stats.json", true)
}
func TestMergeDeepBuildStatsTheme(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
baseURL = "https://example.com"
_merge = "deep"
theme = ["theme1"]
-- themes/theme1/hugo.toml --
title = "Theme 1"
[build]
[build.buildStats]
disableIDs = true
enable = true
-- layouts/home.html --
Home.
`
b := hugolib.Test(t, files, hugolib.TestOptOsFs())
conf := b.H.Configs
base := conf.Base
b.Assert(base.Title, qt.Equals, "Theme 1")
b.Assert(len(base.Module.Imports), qt.Equals, 1)
b.Assert(base.Build.BuildStats.Enable, qt.Equals, true)
b.AssertFileExists("/hugo_stats.json", true)
}
func TestDefaultConfigLanguageBlankWhenNoEnglishExists(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
baseURL = "https://example.com"
[languages]
[languages.nn]
weight = 20
[languages.sv]
weight = 10
[languages.sv.taxonomies]
tag = "taggar"
-- layouts/all.html --
All.
`
b := hugolib.Test(t, files)
b.Assert(b.H.Conf.DefaultContentLanguage(), qt.Equals, "sv")
}
func TestDefaultConfigEnvDisableLanguagesIssue13707(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableLanguages = []
[languages]
[languages.en]
weight = 1
[languages.nn]
weight = 2
[languages.sv]
weight = 3
`
b := hugolib.Test(t, files, hugolib.TestOptWithConfig(func(conf *hugolib.IntegrationTestConfig) {
conf.Environ = []string{`HUGO_DISABLELANGUAGES=sv nn`}
}))
b.Assert(len(b.H.Sites), qt.Equals, 1)
}
// Issue 13535
// We changed enablement of the embedded link and image render hooks from
// booleans to enums in v0.148.0.
func TestLegacyEmbeddedRenderHookEnablement(t *testing.T) {
files := `
-- hugo.toml --
[markup.goldmark.renderHooks.image]
#KEY_VALUE
[markup.goldmark.renderHooks.link]
#KEY_VALUE
`
f := strings.ReplaceAll(files, "#KEY_VALUE", "enableDefault = false")
b := hugolib.Test(t, f)
c := b.H.Configs.Base.Markup.Goldmark.RenderHooks
b.Assert(c.Link.UseEmbedded, qt.Equals, gc.RenderHookUseEmbeddedNever)
b.Assert(c.Image.UseEmbedded, qt.Equals, gc.RenderHookUseEmbeddedNever)
f = strings.ReplaceAll(files, "#KEY_VALUE", "enableDefault = true")
b = hugolib.Test(t, f)
c = b.H.Configs.Base.Markup.Goldmark.RenderHooks
b.Assert(c.Link.UseEmbedded, qt.Equals, gc.RenderHookUseEmbeddedFallback)
b.Assert(c.Image.UseEmbedded, qt.Equals, gc.RenderHookUseEmbeddedFallback)
}
+11 -17
View File
@@ -249,18 +249,14 @@ var allDecoderSetups = map[string]decodeWeight{
key: "sitemap",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
if p.p.IsSet(d.key) {
p.c.Sitemap, err = config.DecodeSitemap(p.c.Sitemap, p.p.GetStringMap(d.key))
}
p.c.Sitemap, err = config.DecodeSitemap(config.SitemapConfig{Priority: -1, Filename: "sitemap.xml"}, p.p.GetStringMap(d.key))
return err
},
},
"taxonomies": {
key: "taxonomies",
decode: func(d decodeWeight, p decodeConfig) error {
if p.p.IsSet(d.key) {
p.c.Taxonomies = maps.CleanConfigStringMapString(p.p.GetStringMapString(d.key))
}
p.c.Taxonomies = maps.CleanConfigStringMapString(p.p.GetStringMapString(d.key))
return nil
},
},
@@ -310,18 +306,16 @@ var allDecoderSetups = map[string]decodeWeight{
}
// Validate defaultContentLanguage.
if p.c.DefaultContentLanguage != "" {
var found bool
for lang := range p.c.Languages {
if lang == p.c.DefaultContentLanguage {
found = true
break
}
}
if !found {
return fmt.Errorf("config value %q for defaultContentLanguage does not match any language definition", p.c.DefaultContentLanguage)
var found bool
for lang := range p.c.Languages {
if lang == p.c.DefaultContentLanguage {
found = true
break
}
}
if !found {
return fmt.Errorf("config value %q for defaultContentLanguage does not match any language definition", p.c.DefaultContentLanguage)
}
return nil
},
@@ -330,7 +324,7 @@ var allDecoderSetups = map[string]decodeWeight{
key: "cascade",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.Cascade, err = page.DecodeCascadeConfig(nil, true, p.p.Get(d.key))
p.c.Cascade, err = page.DecodeCascadeConfig(nil, p.p.Get(d.key))
return err
},
},
+79 -36
View File
@@ -159,9 +159,63 @@ func (l configLoader) applyConfigAliases() error {
func (l configLoader) applyDefaultConfig() error {
defaultSettings := maps.Params{
// These dirs are used early/before we build the config struct.
"themesDir": "themes",
"configDir": "config",
"baseURL": "",
"cleanDestinationDir": false,
"watch": false,
"contentDir": "content",
"resourceDir": "resources",
"publishDir": "public",
"publishDirOrig": "public",
"themesDir": "themes",
"assetDir": "assets",
"layoutDir": "layouts",
"i18nDir": "i18n",
"dataDir": "data",
"archetypeDir": "archetypes",
"configDir": "config",
"staticDir": "static",
"buildDrafts": false,
"buildFuture": false,
"buildExpired": false,
"params": maps.Params{},
"environment": hugo.EnvironmentProduction,
"uglyURLs": false,
"verbose": false,
"ignoreCache": false,
"canonifyURLs": false,
"relativeURLs": false,
"removePathAccents": false,
"titleCaseStyle": "AP",
"taxonomies": maps.Params{"tag": "tags", "category": "categories"},
"permalinks": maps.Params{},
"sitemap": maps.Params{"priority": -1, "filename": "sitemap.xml"},
"menus": maps.Params{},
"disableLiveReload": false,
"pluralizeListTitles": true,
"capitalizeListTitles": true,
"forceSyncStatic": false,
"footnoteAnchorPrefix": "",
"footnoteReturnLinkContents": "",
"newContentEditor": "",
"paginate": 0, // Moved into the paginator struct in Hugo v0.128.0.
"paginatePath": "", // Moved into the paginator struct in Hugo v0.128.0.
"summaryLength": 70,
"rssLimit": -1,
"sectionPagesMenu": "",
"disablePathToLower": false,
"hasCJKLanguage": false,
"enableEmoji": false,
"defaultContentLanguage": "en",
"defaultContentLanguageInSubdir": false,
"enableMissingTranslationPlaceholders": false,
"enableGitInfo": false,
"ignoreFiles": make([]string, 0),
"disableAliases": false,
"debug": false,
"disableFastRender": false,
"timeout": "30s",
"timeZone": "",
"enableInlineShortcodes": false,
}
l.cfg.SetDefaults(defaultSettings)
@@ -233,51 +287,40 @@ func (l configLoader) applyOsEnvOverrides(environ []string) error {
if existing != nil {
val, err := metadecoders.Default.UnmarshalStringTo(env.Value, existing)
if err == nil {
val = l.envValToVal(env.Key, val)
if owner != nil {
owner[nestedKey] = val
} else {
l.cfg.Set(env.Key, val)
}
if err != nil {
continue
}
}
if owner != nil && nestedKey != "" {
owner[nestedKey] = env.Value
if owner != nil {
owner[nestedKey] = val
} else {
l.cfg.Set(env.Key, val)
}
} else {
var val any
key := strings.ReplaceAll(env.Key, delim, ".")
_, ok := allDecoderSetups[key]
if ok {
// A map.
if v, err := metadecoders.Default.UnmarshalStringTo(env.Value, map[string]any{}); err == nil {
val = v
if nestedKey != "" {
owner[nestedKey] = env.Value
} else {
var val any
key := strings.ReplaceAll(env.Key, delim, ".")
_, ok := allDecoderSetups[key]
if ok {
// A map.
if v, err := metadecoders.Default.UnmarshalStringTo(env.Value, map[string]interface{}{}); err == nil {
val = v
}
}
if val == nil {
// A string.
val = l.envStringToVal(key, env.Value)
}
l.cfg.Set(key, val)
}
if val == nil {
// A string.
val = l.envStringToVal(key, env.Value)
}
l.cfg.Set(key, val)
}
}
return nil
}
func (l *configLoader) envValToVal(k string, v any) any {
switch v := v.(type) {
case string:
return l.envStringToVal(k, v)
default:
return v
}
}
func (l *configLoader) envStringToVal(k, v string) any {
switch k {
case "disablekinds", "disablelanguages":
+1 -2
View File
@@ -17,7 +17,6 @@ import (
"fmt"
"net/http"
"regexp"
"slices"
"sort"
"strings"
@@ -129,7 +128,7 @@ func (w BuildStats) Enabled() bool {
}
func (b BuildConfig) clone() BuildConfig {
b.CacheBusters = slices.Clone(b.CacheBusters)
b.CacheBusters = append([]CacheBuster{}, b.CacheBusters...)
return b
}
+1 -1
View File
@@ -166,7 +166,7 @@ func TestBuildConfigCacheBusters(t *testing.T) {
func TestBuildConfigCacheBusterstTailwindSetup(t *testing.T) {
c := qt.New(t)
cfg := New()
cfg.Set("build", map[string]any{
cfg.Set("build", map[string]interface{}{
"cacheBusters": []map[string]string{
{
"source": "assets/watching/hugo_stats\\.json",
-11
View File
@@ -14,7 +14,6 @@
package config
import (
"regexp"
"strings"
"testing"
@@ -33,13 +32,3 @@ func TestIsValidConfigFileName(t *testing.T) {
c.Assert(IsValidConfigFilename(""), qt.Equals, false)
c.Assert(IsValidConfigFilename("config.toml.swp"), qt.Equals, false)
}
func TestFromTOMLConfigString(t *testing.T) {
c := qt.New(t)
c.Assert(
func() { FromTOMLConfigString("cfg") },
qt.PanicMatches,
regexp.MustCompile("_stream.toml:.*"),
)
}
+1 -1
View File
@@ -76,7 +76,7 @@ type AllProvider interface {
}
// We cannot import the media package as that would create a circular dependency.
// This interface defines a subset of what media.ContentTypes provides.
// This interface defineds a sub set of what media.ContentTypes provides.
type ContentTypesProvider interface {
IsContentSuffix(suffix string) bool
IsContentFile(filename string) bool
+1 -1
View File
@@ -345,7 +345,7 @@ func (c *defaultConfigProvider) getNestedKeyAndMap(key string, create bool) (str
c.keyCache.Store(key, parts)
}
current := c.root
for i := range len(parts) - 1 {
for i := 0; i < len(parts)-1; i++ {
next, found := current[parts[i]]
if !found {
if create {
+1 -86
View File
@@ -17,7 +17,6 @@ import (
"context"
"errors"
"fmt"
"slices"
"strconv"
"strings"
"testing"
@@ -333,7 +332,7 @@ func TestDefaultConfigProvider(t *testing.T) {
return nil
}
for i := range 20 {
for i := 0; i < 20; i++ {
i := i
r.Run(func() error {
const v = 42
@@ -354,90 +353,6 @@ func TestDefaultConfigProvider(t *testing.T) {
c.Assert(r.Wait(), qt.IsNil)
})
c.Run("GetBool", func(c *qt.C) {
cfg := New()
var k string
var v bool
k, v = "foo", true
cfg.Set(k, v)
c.Assert(cfg.Get(k), qt.Equals, v)
c.Assert(cfg.GetBool(k), qt.Equals, v)
})
c.Run("GetParams", func(c *qt.C) {
cfg := New()
k := "foo"
cfg.Set(k, maps.Params{k: true})
c.Assert(cfg.GetParams(k), qt.DeepEquals, maps.Params{
k: true,
})
c.Assert(cfg.GetParams("bar"), qt.IsNil)
})
c.Run("Keys", func(c *qt.C) {
cfg := New()
k := "foo"
k2 := "bar"
cfg.Set(k, maps.Params{k: struct{}{}})
cfg.Set(k2, maps.Params{k2: struct{}{}})
c.Assert(len(cfg.Keys()), qt.Equals, 2)
got := cfg.Keys()
slices.Sort(got)
want := []string{k, k2}
slices.Sort(want)
c.Assert(got, qt.DeepEquals, want)
})
c.Run("WalkParams", func(c *qt.C) {
cfg := New()
cfg.Set("x", maps.Params{})
cfg.Set("y", maps.Params{})
var got []string
cfg.WalkParams(func(params ...maps.KeyParams) bool {
got = append(got, params[len(params)-1].Key)
return false
})
want := []string{"", "x", "y"}
slices.Sort(got)
slices.Sort(want)
c.Assert(got, qt.DeepEquals, want)
cfg = New()
cfg.WalkParams(func(params ...maps.KeyParams) bool {
return true
})
got = []string{""}
want = []string{""}
c.Assert(got, qt.DeepEquals, want)
})
c.Run("SetDefaults", func(c *qt.C) {
cfg := New()
cfg.SetDefaults(maps.Params{
"foo": "bar",
"bar": "baz",
})
c.Assert(cfg.Get("foo"), qt.Equals, "bar")
c.Assert(cfg.Get("bar"), qt.Equals, "baz")
})
}
func BenchmarkDefaultConfigProvider(b *testing.B) {
+2 -2
View File
@@ -29,7 +29,7 @@ func TestNamespace(t *testing.T) {
// ns, err := config.DecodeNamespace[map[string]DocsMediaTypeConfig](in, defaultMediaTypesConfig, buildConfig)
ns, err := DecodeNamespace[[]*tstNsExt](
map[string]any{"foo": "bar"},
map[string]interface{}{"foo": "bar"},
func(v any) (*tstNsExt, any, error) {
t := &tstNsExt{}
m, err := maps.ToStringMapE(v)
@@ -42,7 +42,7 @@ func TestNamespace(t *testing.T) {
c.Assert(err, qt.IsNil)
c.Assert(ns, qt.Not(qt.IsNil))
c.Assert(ns.SourceStructure, qt.DeepEquals, map[string]any{"foo": "bar"})
c.Assert(ns.SourceStructure, qt.DeepEquals, map[string]interface{}{"foo": "bar"})
c.Assert(ns.SourceHash, qt.Equals, "1420f6c7782f7459")
c.Assert(ns.Config, qt.DeepEquals, &tstNsExt{Foo: "bar"})
c.Assert(ns.Signature(), qt.DeepEquals, []*tstNsExt(nil))
+1 -1
View File
@@ -73,7 +73,7 @@ func NewWhitelist(patterns ...string) (Whitelist, error) {
var patternsr []*regexp.Regexp
for i := range patterns {
for i := 0; i < len(patterns); i++ {
p := strings.TrimSpace(patterns[i])
if p == "" {
continue
-3
View File
@@ -101,9 +101,6 @@ func DecodeConfig(cfg config.Provider) (c Config, err error) {
if c.RSS.Limit == 0 {
c.RSS.Limit = cfg.GetInt(rssLimitKey)
if c.RSS.Limit == 0 {
c.RSS.Limit = -1
}
}
return
+2 -2
View File
@@ -291,7 +291,7 @@ func (b *contentBuilder) applyArcheType(contentFilename string, archetypeFi hugo
func (b *contentBuilder) mapArcheTypeDir() error {
var m archetypeMap
seen := map[hstrings.Strings2]bool{}
seen := map[hstrings.Tuple]bool{}
walkFn := func(path string, fim hugofs.FileMetaInfo) error {
if fim.IsDir() {
@@ -301,7 +301,7 @@ func (b *contentBuilder) mapArcheTypeDir() error {
pi := fim.Meta().PathInfo
if pi.IsContent() {
pathLang := hstrings.Strings2{pi.PathBeforeLangAndOutputFormatAndExt(), fim.Meta().Lang}
pathLang := hstrings.Tuple{First: pi.PathNoIdentifier(), Second: fim.Meta().Lang}
if seen[pathLang] {
// Duplicate content file, e.g. page.md and page.html.
// In the regular build, we will filter out the duplicates, but
+1 -1
View File
@@ -129,7 +129,7 @@ site RegularPages: {{ len site.RegularPages }}
`
c.Assert(afero.WriteFile(mm, filepath.Join(archetypeDir, "index.md"), fmt.Appendf(nil, contentFile, "index.md"), 0o755), qt.IsNil)
c.Assert(afero.WriteFile(mm, filepath.Join(archetypeDir, "index.md"), []byte(fmt.Sprintf(contentFile, "index.md")), 0o755), qt.IsNil)
c.Assert(afero.WriteFile(mm, filepath.Join(defaultArchetypeDir, "index.md"), []byte("default archetype index.md"), 0o755), qt.IsNil)
c.Assert(initFs(mm), qt.IsNil)
+17 -67
View File
@@ -34,60 +34,10 @@ var siteFs embed.FS
var themeFs embed.FS
// CreateTheme creates a theme skeleton.
func CreateTheme(createpath string, sourceFs afero.Fs, format string) error {
func CreateTheme(createpath string, sourceFs afero.Fs) error {
if exists, _ := helpers.Exists(createpath, sourceFs); exists {
return errors.New(createpath + " already exists")
}
format = strings.ToLower(format)
siteConfig := map[string]any{
"baseURL": "https://example.org/",
"languageCode": "en-US",
"title": "My New Hugo Site",
"menus": map[string]any{
"main": []any{
map[string]any{
"name": "Home",
"pageRef": "/",
"weight": 10,
},
map[string]any{
"name": "Posts",
"pageRef": "/posts",
"weight": 20,
},
map[string]any{
"name": "Tags",
"pageRef": "/tags",
"weight": 30,
},
},
},
"module": map[string]any{
"hugoVersion": map[string]any{
"extended": false,
"min": "0.146.0",
},
},
}
err := createSiteConfig(sourceFs, createpath, siteConfig, format)
if err != nil {
return err
}
defaultArchetype := map[string]any{
"title": "{{ replace .File.ContentBaseName \"-\" \" \" | title }}",
"date": "{{ .Date }}",
"draft": true,
}
err = createDefaultArchetype(sourceFs, createpath, defaultArchetype, format)
if err != nil {
return err
}
return copyFiles(createpath, sourceFs, themeFs)
}
@@ -121,24 +71,12 @@ func CreateSite(createpath string, sourceFs afero.Fs, force bool, format string)
}
}
siteConfig := map[string]any{
"baseURL": "https://example.org/",
"title": "My New Hugo Site",
"languageCode": "en-us",
}
err := createSiteConfig(sourceFs, createpath, siteConfig, format)
err := newSiteCreateConfig(sourceFs, createpath, format)
if err != nil {
return err
}
defaultArchetype := map[string]any{
"title": "{{ replace .File.ContentBaseName \"-\" \" \" | title }}",
"date": "{{ .Date }}",
"draft": true,
}
err = createDefaultArchetype(sourceFs, createpath, defaultArchetype, format)
err = newSiteCreateArchetype(sourceFs, createpath, format)
if err != nil {
return err
}
@@ -161,7 +99,13 @@ func copyFiles(createpath string, sourceFs afero.Fs, skeleton embed.FS) error {
})
}
func createSiteConfig(fs afero.Fs, createpath string, in map[string]any, format string) (err error) {
func newSiteCreateConfig(fs afero.Fs, createpath string, format string) (err error) {
in := map[string]string{
"baseURL": "https://example.org/",
"title": "My New Hugo Site",
"languageCode": "en-us",
}
var buf bytes.Buffer
err = parser.InterfaceToConfig(in, metadecoders.FormatFromString(format), &buf)
if err != nil {
@@ -171,7 +115,13 @@ func createSiteConfig(fs afero.Fs, createpath string, in map[string]any, format
return helpers.WriteToDisk(filepath.Join(createpath, "hugo."+format), &buf, fs)
}
func createDefaultArchetype(fs afero.Fs, createpath string, in map[string]any, format string) (err error) {
func newSiteCreateArchetype(fs afero.Fs, createpath string, format string) (err error) {
in := map[string]any{
"title": "{{ replace .File.ContentBaseName \"-\" \" \" | title }}",
"date": "{{ .Date }}",
"draft": true,
}
var buf bytes.Buffer
err = parser.InterfaceToFrontMatter(in, metadecoders.FormatFromString(format), &buf)
if err != nil {
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) [year] [fullname]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+7
View File
@@ -0,0 +1,7 @@
# Theme Name
## Features
## Installation
## Configuration
@@ -0,0 +1,5 @@
+++
title = '{{ replace .File.ContentBaseName "-" " " | title }}'
date = {{ .Date }}
draft = true
+++
+23
View File
@@ -0,0 +1,23 @@
baseURL = 'https://example.org/'
languageCode = 'en-US'
title = 'My New Hugo Site'
[[menus.main]]
name = 'Home'
pageRef = '/'
weight = 10
[[menus.main]]
name = 'Posts'
pageRef = '/posts'
weight = 20
[[menus.main]]
name = 'Tags'
pageRef = '/tags'
weight = 30
[module]
[module.hugoVersion]
extended = false
min = "0.116.0"
@@ -1,16 +0,0 @@
{{- with resources.Get "js/main.js" }}
{{- $opts := dict
"minify" (not hugo.IsDevelopment)
"sourceMap" (cond hugo.IsDevelopment "external" "")
"targetPath" "js/main.js"
}}
{{- with . | js.Build $opts }}
{{- if hugo.IsDevelopment }}
<script src="{{ .RelPermalink }}"></script>
{{- else }}
{{- with . | fingerprint }}
<script src="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous"></script>
{{- end }}
{{- end }}
{{- end }}
{{- end }}
@@ -1,5 +1,5 @@
{{- with resources.Get "css/main.css" }}
{{- if hugo.IsDevelopment }}
{{- if eq hugo.Environment "development" }}
<link rel="stylesheet" href="{{ .RelPermalink }}">
{{- else }}
{{- with . | minify | fingerprint }}
@@ -0,0 +1,12 @@
{{- with resources.Get "js/main.js" }}
{{- if eq hugo.Environment "development" }}
{{- with . | js.Build }}
<script src="{{ .RelPermalink }}"></script>
{{- end }}
{{- else }}
{{- $opts := dict "minify" true }}
{{- with . | js.Build $opts | fingerprint }}
<script src="{{ .RelPermalink }}" integrity="{{- .Data.Integrity }}" crossorigin="anonymous"></script>
{{- end }}
{{- end }}
{{- end }}
@@ -18,7 +18,7 @@ Renders a menu for the given menu ID.
</nav>
{{- end }}
{{- define "_partials/inline/menu/walk.html" }}
{{- define "partials/inline/menu/walk.html" }}
{{- $page := .page }}
{{- range .menuEntries }}
{{- $attrs := dict "href" .URL }}
@@ -1,7 +0,0 @@
{{ define "main" }}
<h1>{{ .Title }}</h1>
{{ .Content }}
{{ range .Pages }}
<h2><a href="{{ .RelPermalink }}">{{ .LinkTitle }}</a></h2>
{{ end }}
{{ end }}
-7
View File
@@ -1,7 +0,0 @@
{{ define "main" }}
<h1>{{ .Title }}</h1>
{{ .Content }}
{{ range .Pages }}
<h2><a href="{{ .RelPermalink }}">{{ .LinkTitle }}</a></h2>
{{ end }}
{{ end }}
+31
View File
@@ -0,0 +1,31 @@
name = 'Theme name'
license = 'MIT'
licenselink = 'https://github.com/owner/repo/LICENSE'
description = 'Theme description'
# The home page of the theme, where the source can be found
homepage = 'https://github.com/owner/repo'
# If you have a running demo of the theme
demosite = 'https://owner.github.io/repo'
# Taxonomy terms
tags = ['blog', 'company']
features = ['some', 'awesome', 'features']
# If the theme has multiple authors
authors = [
{name = 'Name of author', homepage = 'Website of author'},
{name = 'Name of author', homepage = 'Website of author'}
]
# If the theme has a single author
[author]
name = 'Your name'
homepage = 'Your website'
# If porting an existing theme
[original]
author = 'Name of original author'
homepage = 'Website of original author'
repo = 'https://github.com/owner/repo'
+33 -48
View File
@@ -36,7 +36,6 @@ import (
"github.com/dustin/go-humanize"
"github.com/gobwas/glob"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/para"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/deploy/deployconfig"
"github.com/gohugoio/hugo/media"
@@ -488,12 +487,7 @@ func knownHiddenDirectory(name string) bool {
// walkLocal walks the source directory and returns a flat list of files,
// using localFile.SlashPath as the map keys.
func (d *Deployer) walkLocal(fs afero.Fs, matchers []*deployconfig.Matcher, include, exclude glob.Glob, mediaTypes media.Types, mappath func(string) string) (map[string]*localFile, error) {
retval := make(map[string]*localFile)
var mu sync.Mutex
workers := para.New(d.cfg.Workers)
g, _ := workers.Start(context.Background())
retval := map[string]*localFile{}
err := afero.Walk(fs, "", func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
@@ -514,54 +508,45 @@ func (d *Deployer) walkLocal(fs afero.Fs, matchers []*deployconfig.Matcher, incl
return nil
}
// Process each file in a worker
g.Run(func() error {
// When a file system is HFS+, its filepath is in NFD form.
if runtime.GOOS == "darwin" {
path = norm.NFC.String(path)
}
// When a file system is HFS+, its filepath is in NFD form.
if runtime.GOOS == "darwin" {
path = norm.NFC.String(path)
}
// Check include/exclude matchers.
slashpath := filepath.ToSlash(path)
if include != nil && !include.Match(slashpath) {
d.logger.Infof(" dropping %q due to include\n", slashpath)
return nil
}
if exclude != nil && exclude.Match(slashpath) {
d.logger.Infof(" dropping %q due to exclude\n", slashpath)
return nil
}
// Find the first matching matcher (if any).
var m *deployconfig.Matcher
for _, cur := range matchers {
if cur.Matches(slashpath) {
m = cur
break
}
}
// Apply any additional modifications to the local path, to map it to
// the remote path.
if mappath != nil {
slashpath = mappath(slashpath)
}
lf, err := newLocalFile(fs, path, slashpath, m, mediaTypes)
if err != nil {
return err
}
mu.Lock()
retval[lf.SlashPath] = lf
mu.Unlock()
// Check include/exclude matchers.
slashpath := filepath.ToSlash(path)
if include != nil && !include.Match(slashpath) {
d.logger.Infof(" dropping %q due to include\n", slashpath)
return nil
})
}
if exclude != nil && exclude.Match(slashpath) {
d.logger.Infof(" dropping %q due to exclude\n", slashpath)
return nil
}
// Find the first matching matcher (if any).
var m *deployconfig.Matcher
for _, cur := range matchers {
if cur.Matches(slashpath) {
m = cur
break
}
}
// Apply any additional modifications to the local path, to map it to
// the remote path.
if mappath != nil {
slashpath = mappath(slashpath)
}
lf, err := newLocalFile(fs, path, slashpath, m, mediaTypes)
if err != nil {
return err
}
retval[lf.SlashPath] = lf
return nil
})
if err != nil {
return nil, err
}
if err := g.Wait(); err != nil {
return nil, err
}
return retval, nil
}
+6 -7
View File
@@ -623,7 +623,7 @@ func TestEndToEndSync(t *testing.T) {
localFs: test.fs,
bucket: test.bucket,
mediaTypes: media.DefaultTypes,
cfg: deployconfig.DeployConfig{Workers: 2, MaxDeletes: -1},
cfg: deployconfig.DeployConfig{MaxDeletes: -1},
}
// Initial deployment should sync remote with local.
@@ -706,7 +706,7 @@ func TestMaxDeletes(t *testing.T) {
localFs: test.fs,
bucket: test.bucket,
mediaTypes: media.DefaultTypes,
cfg: deployconfig.DeployConfig{Workers: 2, MaxDeletes: -1},
cfg: deployconfig.DeployConfig{MaxDeletes: -1},
}
// Sync remote with local.
@@ -836,7 +836,7 @@ func TestIncludeExclude(t *testing.T) {
}
deployer := &Deployer{
localFs: fsTest.fs,
cfg: deployconfig.DeployConfig{Workers: 2, MaxDeletes: -1}, bucket: fsTest.bucket,
cfg: deployconfig.DeployConfig{MaxDeletes: -1}, bucket: fsTest.bucket,
target: tgt,
mediaTypes: media.DefaultTypes,
}
@@ -893,7 +893,7 @@ func TestIncludeExcludeRemoteDelete(t *testing.T) {
}
deployer := &Deployer{
localFs: fsTest.fs,
cfg: deployconfig.DeployConfig{Workers: 2, MaxDeletes: -1}, bucket: fsTest.bucket,
cfg: deployconfig.DeployConfig{MaxDeletes: -1}, bucket: fsTest.bucket,
mediaTypes: media.DefaultTypes,
}
@@ -945,7 +945,7 @@ func TestCompression(t *testing.T) {
deployer := &Deployer{
localFs: test.fs,
bucket: test.bucket,
cfg: deployconfig.DeployConfig{Workers: 2, MaxDeletes: -1, Matchers: []*deployconfig.Matcher{{Pattern: ".*", Gzip: true, Re: regexp.MustCompile(".*")}}},
cfg: deployconfig.DeployConfig{MaxDeletes: -1, Matchers: []*deployconfig.Matcher{{Pattern: ".*", Gzip: true, Re: regexp.MustCompile(".*")}}},
mediaTypes: media.DefaultTypes,
}
@@ -1000,7 +1000,7 @@ func TestMatching(t *testing.T) {
deployer := &Deployer{
localFs: test.fs,
bucket: test.bucket,
cfg: deployconfig.DeployConfig{Workers: 2, MaxDeletes: -1, Matchers: []*deployconfig.Matcher{{Pattern: "^subdir/aaa$", Force: true, Re: regexp.MustCompile("^subdir/aaa$")}}},
cfg: deployconfig.DeployConfig{MaxDeletes: -1, Matchers: []*deployconfig.Matcher{{Pattern: "^subdir/aaa$", Force: true, Re: regexp.MustCompile("^subdir/aaa$")}}},
mediaTypes: media.DefaultTypes,
}
@@ -1097,6 +1097,5 @@ func verifyRemote(ctx context.Context, bucket *blob.Bucket, local []*fileData) (
func newDeployer() *Deployer {
return &Deployer{
logger: loggers.NewDefault(),
cfg: deployconfig.DeployConfig{Workers: 2},
}
}
+28 -19
View File
@@ -29,7 +29,6 @@ import (
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/postpub"
"github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/gohugoio/hugo/metrics"
"github.com/gohugoio/hugo/resources"
@@ -47,6 +46,12 @@ type Deps struct {
ExecHelper *hexec.Exec
// The templates to use. This will usually implement the full tpl.TemplateManager.
tmplHandlers *tpl.TemplateHandlers
// The template funcs.
TmplFuncMap map[string]any
// The file systems to use.
Fs *hugofs.Fs `json:"-"`
@@ -74,8 +79,7 @@ type Deps struct {
// The site building.
Site page.Site
TemplateStore *tplimpl.TemplateStore
TemplateProvider ResourceProvider
// Used in tests
OverloadedTemplateFuncs map[string]any
@@ -98,9 +102,6 @@ type Deps struct {
// This is common/global for all sites.
BuildState *BuildState
// Misc counters.
Counters *Counters
// Holds RPC dispatchers for Katex etc.
// TODO(bep) rethink this re. a plugin setup, but this will have to do for now.
WasmDispatchers *warpc.Dispatchers
@@ -108,6 +109,9 @@ type Deps struct {
// The JS batcher client.
JSBatcherClient js.BatcherClient
// The JS batcher client.
// JSBatcherClient *esbuild.BatcherClient
isClosed bool
*globalErrHandler
@@ -126,8 +130,8 @@ func (d Deps) Clone(s page.Site, conf config.AllProvider) (*Deps, error) {
return &d, nil
}
func (d *Deps) GetTemplateStore() *tplimpl.TemplateStore {
return d.TemplateStore
func (d *Deps) SetTempl(t *tpl.TemplateHandlers) {
d.tmplHandlers = t
}
func (d *Deps) Init() error {
@@ -149,12 +153,10 @@ func (d *Deps) Init() error {
logger: d.Log,
}
}
if d.BuildState == nil {
d.BuildState = &BuildState{}
}
if d.Counters == nil {
d.Counters = &Counters{}
}
if d.BuildState.DeferredExecutions == nil {
if d.BuildState.DeferredExecutionsGroupedByRenderingContext == nil {
d.BuildState.DeferredExecutionsGroupedByRenderingContext = make(map[tpl.RenderingContext]*DeferredExecutions)
@@ -261,17 +263,22 @@ func (d *Deps) Init() error {
return nil
}
// TODO(bep) rework this to get it in line with how we manage templates.
func (d *Deps) Compile(prototype *Deps) error {
var err error
if prototype == nil {
if err = d.TemplateProvider.NewResource(d); err != nil {
return err
}
if err = d.TranslationProvider.NewResource(d); err != nil {
return err
}
return nil
}
if err = d.TemplateProvider.CloneResource(d, prototype); err != nil {
return err
}
if err = d.TranslationProvider.CloneResource(d, prototype); err != nil {
return err
}
@@ -371,6 +378,14 @@ type ResourceProvider interface {
CloneResource(dst, src *Deps) error
}
func (d *Deps) Tmpl() tpl.TemplateHandler {
return d.tmplHandlers.Tmpl
}
func (d *Deps) TextTmpl() tpl.TemplateParseFinder {
return d.tmplHandlers.TxtTmpl
}
func (d *Deps) Close() error {
if d.isClosed {
return nil
@@ -439,12 +454,6 @@ type BuildState struct {
DeferredExecutionsGroupedByRenderingContext map[tpl.RenderingContext]*DeferredExecutions
}
// Misc counters.
type Counters struct {
// Counter for the math.Counter function.
MathCounter atomic.Uint64
}
type DeferredExecutions struct {
// A set of filenames in /public that
// contains a post-processing prefix.
+1 -5
View File
@@ -60,7 +60,6 @@
"redirections",
"subexpression",
"suppressible",
"synchronisation",
"templating",
"transpile",
"unmarshal",
@@ -70,7 +69,6 @@
"# cspell: ignore hugo terminology",
"# ----------------------------------------------------------------------",
"alignx",
"aligny",
"attrlink",
"canonify",
"codeowners",
@@ -163,7 +161,7 @@
"# ----------------------------------------------------------------------",
"achristie",
"ccpa",
"cpra",
"crpa",
"ddmaurier",
"dring",
"fleqn",
@@ -173,8 +171,6 @@
"jsmith",
"leqno",
"milli",
"monokai",
"mysanityprojectid",
"rgba",
"rsmith",
"tdewolff",
+3 -2
View File
@@ -4,9 +4,10 @@
/.vscode
/dist
/public
/resources
hugo_stats.json
node_modules/
nohup.out
package-lock.json
trace.out
public/
resources/
trace.out
-1
View File
@@ -22,6 +22,5 @@ MD041: false
MD046: false
MD049: false
MD050: false
MD051: false
MD053: false
MD055: false
-18
View File
@@ -1,18 +0,0 @@
# Ignore all SVG icons.
**/icons.html
# These are whitespace sensitive.
layouts/_markup/render-code*
layouts/_markup/render-table*
layouts/_shortcodes/glossary-term.html
layouts/_shortcodes/glossary.html
layouts/_shortcodes/highlighting-styles.html
layouts/_shortcodes/list-pages-in-section.html
layouts/_shortcodes/quick-reference.html
# No root node.
layouts/_partials/layouts/head/head.html
# Auto generated.
assets/css/components/chroma*.css
assets/jsconfig.json
-24
View File
@@ -1,24 +0,0 @@
{
"plugins": [
"prettier-plugin-go-template",
"@awmottaz/prettier-plugin-void-html"
],
"overrides": [
{
"files": ["*.html"],
"options": {
"parser": "go-template",
"goTemplateBracketSpacing": true,
"bracketSameLine": true
}
},
{
"files": ["*.js", "*.ts"],
"options": {
"useTabs": true,
"printWidth": 120,
"singleQuote": true
}
}
]
}
+1 -1
View File
@@ -1,3 +1,3 @@
See [content/LICENSE.md](content/LICENSE.md) for the license of the content of this repository.
The theme (layouts, CSS, JavaScript etc.) of this repository has no open source license. It is custom made for the Hugo sites and is not meant for reuse.
The theme (layouts, CSS, JavaScript etc.) of this repository has no open source license. It is custom made for the Hugo sites and is not meant for reuse.
+2 -2
View File
@@ -19,9 +19,9 @@ Please see the [contributing] section for guidelines, examples, and process.
# Install
```sh
```bash
npm i
hugo server
```
**Note:** We're working on removing the need to run `npm i` for local development. Stay tuned.
**Note:** We're working on removing the need to run `npm i` for local development. Stay tuned.
-6
View File
@@ -1,6 +0,0 @@
---
title: {{ replace .File.ContentBaseName "-" " " | strings.FirstUpper }}
description:
categories: []
keywords: []
---
+5 -5
View File
@@ -3,9 +3,9 @@ title: {{ replace .File.ContentBaseName "-" " " | title }}
description:
categories: []
keywords: []
params:
functions_and_methods:
aliases: []
returnType:
signatures: []
action:
aliases: []
related: []
returnType:
signatures: []
---
+1 -2
View File
@@ -1,7 +1,6 @@
---
title: {{ replace .File.ContentBaseName "-" " " }}
params:
reference:
reference:
---
<!--
+4 -4
View File
@@ -3,8 +3,8 @@ title: {{ replace .File.ContentBaseName "-" " " | title }}
description:
categories: []
keywords: []
params:
functions_and_methods:
returnType:
signatures: []
action:
related: []
returnType:
signatures: []
---
-7
View File
@@ -1,7 +0,0 @@
---
title: {{ replace .File.ContentBaseName "-" " " | strings.FirstUpper }}
description:
categories: []
keywords: []
publishDate: {{ .Date }}
---
+5
View File
@@ -0,0 +1,5 @@
---
title: {{ replace .File.ContentBaseName "-" " " | title }}
description:
date: {{ .Date }}
---

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