Compare commits

..

1 Commits

Author SHA1 Message Date
Bjørn Erik Pedersen 518eefee9b Skip flakey test on CI 2025-08-05 17:03:12 +02:00
701 changed files with 21843 additions and 30492 deletions
+2 -2
View File
@@ -4,7 +4,7 @@ parameters:
defaults: &defaults
resource_class: large
docker:
- image: bepsays/ci-hugoreleaser:1.22500.20400
- image: bepsays/ci-hugoreleaser:1.22400.20000
environment: &buildenv
GOMODCACHE: /root/project/gomodcache
version: 2
@@ -58,7 +58,7 @@ jobs:
environment:
<<: [*buildenv]
docker:
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22500.20400
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22400.20000
steps:
- *restore-cache
- &attach-workspace
-13
View File
@@ -1,13 +0,0 @@
have_fun: false
memory_config:
disabled: false
code_review:
disable: false
comment_severity_threshold: HIGH
max_review_comments: -1
pull_request_opened:
help: true
summary: false
code_review: false
include_drafts: false
ignore_patterns: []
-10
View File
@@ -1,10 +0,0 @@
🛑 Before submitting this PR
All PRs must be linked to an Issue in this repository, and that Issue must be approved by a project maintainer:
- For a bug, approval means that the "needs triage" label is removed.
- For a proposal, approval means that the "proposal" label is replaced by an "enhancement" label.
PRs without a linked, approved Issue will be closed immediately.
Please review the contribution guide (CONTRIBUTING.md) to understand commit message guidelines and AI assistance disclosures.
-22
View File
@@ -1,22 +0,0 @@
name: AI Detection
on:
# Use pull_request_target instead of pull_request (we need the secrets.OPENAI_API_KEY to be available for forks)
pull_request_target:
types: [opened, ready_for_review]
permissions:
contents: read
pull-requests: write
jobs:
detect-ai:
runs-on: ubuntu-latest
if: ${{ !github.event.pull_request.draft }}
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- name: AI Watchdog
uses: bep/ai-watchdog@995475dd81767f8035dc2c943e9230918d06caca # temporary fork (probably)
with:
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
pr-label: AI Suspect
fail-when-confident: false
# CSV list.
pr-user-logins-skip: dependabot[bot]
+5 -5
View File
@@ -16,20 +16,20 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Docker meta
id: meta
uses: docker/metadata-action@318604b99e75e41977312d83839a89be02ca4893 # v5.9.0
uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1
with:
images: ${{ env.REGISTRY_IMAGE }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
uses: docker/setup-buildx-action@988b5a0280414f521da01fcc63a27aeeb4b104db # v3.6.1
- name: Login to GHCR
# Login is only needed when the image is pushed
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -37,7 +37,7 @@ jobs:
- name: Build and push
id: build
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
uses: docker/build-push-action@16ebe778df0e7752d2cfcbd924afdbbd89c1a755 # v6.6.1
with:
context: .
provenance: mode=max
+2 -2
View File
@@ -24,7 +24,7 @@ jobs:
This pull request has been automatically locked since there
has not been any recent activity after it was closed.
Please open a new issue for related bugs.
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
- uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0
with:
operations-per-run: 999
days-before-issue-stale: 365
@@ -43,7 +43,7 @@ jobs:
stale-pr-message: This PR has been automatically marked as stale because it has not had
recent activity. The resources of the Hugo team are limited, and so we are asking for your help.
Please check https://github.com/gohugoio/hugo/blob/master/CONTRIBUTING.md#code-contribution and verify that this code contribution fits with the description. If yes, tell us in a comment.
Please check https://github.com/gohugoio/hugo/blob/master/CONTRIBUTING.md#code-contribution and verify that this code contribution fits with the description. If yes, tell is in a comment.
This PR will automatically be closed in the near future if no further activity occurs. Thank you for all your contributions.
stale-issue-label: 'Stale'
+12 -13
View File
@@ -16,7 +16,7 @@ jobs:
test:
strategy:
matrix:
go-version: [1.24.x, 1.25.x]
go-version: [1.23.x, 1.24.x]
os: [ubuntu-latest, windows-latest] # macos disabled for now because of disk space issues.
runs-on: ${{ matrix.os }}
steps:
@@ -24,7 +24,9 @@ jobs:
name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1
with:
tool-cache: true
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: false
android: true
dotnet: true
haskell: true
@@ -32,9 +34,9 @@ jobs:
docker-images: true
swap-storage: true
- name: Checkout code
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Install Go
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32 # v5.0.2
with:
go-version: ${{ matrix.go-version }}
check-latest: true
@@ -43,21 +45,18 @@ jobs:
**/go.sum
**/go.mod
- name: Install Ruby
uses: ruby/setup-ruby@8aeb6ff8030dd539317f8e1769a044873b56ea71 # v1.268.0
uses: ruby/setup-ruby@a6e6f86333f0a2523ece813039b8b4be04560854 # v1.190.0
with:
ruby-version: "3.4.5"
- name: Install Ruby gems
run: |
gem install asciidoctor -v "2.0.26"
gem install asciidoctor-diagram -v "3.1.0"
- name: Install GoAT
run: go install github.com/blampe/goat/cmd/goat@177de93b192b8ffae608e5d9ec421cc99bf68402
ruby-version: "2.7"
bundler-cache: true #
- name: Install Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
uses: actions/setup-python@39cd14951b08e74b54015e9e001cdefcf80e669f # v5.1.1
with:
python-version: "3.x"
- name: Install Mage
run: go install github.com/magefile/mage@v1.15.0
- name: Install asciidoctor
uses: reitzig/actions-asciidoctor@c642db5eedd1d729bb8c92034770d0b2f769eda6 # v2.0.2
- name: Install docutils
run: |
pip install docutils
+1 -12
View File
@@ -19,7 +19,6 @@ The Hugo community and maintainers are [very active](https://github.com/gohugoio
* [Reporting Issues](#reporting-issues)
* [Submitting Patches](#submitting-patches)
* [Code Contribution Guidelines](#code-contribution-guidelines)
* [AI Assistance Notice](#ai-assistance-notice)
* [Git Commit Message Guidelines](#git-commit-message-guidelines)
* [Fetching the Sources From GitHub](#fetching-the-sources-from-github)
* [Building Hugo with Your Changes](#building-hugo-with-your-changes)
@@ -55,7 +54,7 @@ If it is of some complexity, the contributor is expected to maintain and support
Any non-trivial code change needs to update an open [issue](https://github.com/gohugoio/hugo/issues). A non-trivial code change without an issue reference with one of the labels `bug` or `enhancement` will not be merged.
Note that we do not accept new features that require [CGO](https://go.dev/wiki/cgo).
Note that we do not accept new features that require [CGO](https://github.com/golang/go/wiki/cgo).
We have one exception to this rule which is LibSASS.
**Bug fixes are, of course, always welcome.**
@@ -80,16 +79,6 @@ To make the contribution process as seamless as possible, we ask for the followi
* Ensure that `mage check` succeeds. [Travis CI](https://travis-ci.org/gohugoio/hugo) (Windows, Linux and macOS) will fail the build if `mage check` fails.
* Follow the **Git Commit Message Guidelines** below.
## AI Assistance Notice
If a substantial part of your contribution is autogenerated with AI, **this must be disclosed in the pull request**, along with the extent to which AI assistance was used.
An example disclosure:
> This PR was written primarily by Claude Code.
When using AI assistance, we expect contributors to understand the code that is produced and be able to answer critical questions about it.
### Git Commit Message Guidelines
This [blog article](https://cbea.ms/git-commit/) is a good resource for learning how to write good commit messages,
+1 -1
View File
@@ -2,7 +2,7 @@
# Twitter: https://twitter.com/gohugoio
# Website: https://gohugo.io/
ARG GO_VERSION="1.25"
ARG GO_VERSION="1.24"
ARG ALPINE_VERSION="3.22"
ARG DART_SASS_VERSION="1.79.3"
+4 -4
View File
@@ -69,7 +69,7 @@ See the [features] section of the documentation for a comprehensive summary of H
&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://cloudcannon.com/hugo-cms/?utm_campaign=HugoSponsorship&utm_source=sponsor&utm_content=gohugo" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/hugoDocs/master/assets/images/sponsors/cloudcannon-cms-logo.svg" width="200" alt="CloudCannon"></a>
<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>
</p>
## Editions
@@ -102,9 +102,9 @@ Install Hugo from a [prebuilt binary], package manager, or package repository. P
Prerequisites to build Hugo from source:
- Standard edition: Go 1.24.0 or later
- Extended edition: Go 1.24.0 or later, and GCC
- Extended/deploy edition: Go 1.24.0 or later, and GCC
- Standard edition: Go 1.23.0 or later
- Extended edition: Go 1.23.0 or later, and GCC
- Extended/deploy edition: Go 1.23.0 or later, and GCC
Build the standard edition:
+1 -1
View File
@@ -340,7 +340,7 @@ func GetOrCreatePartition[K comparable, V any](c *Cache, name string, opts Optio
panic("invalid Weight, must be between 1 and 100")
}
if !partitionNameRe.MatchString(name) {
if partitionNameRe.FindString(name) != name {
panic(fmt.Sprintf("invalid partition name %q", name))
}
-7
View File
@@ -51,18 +51,11 @@ const (
type Configs map[string]FileCacheConfig
// CacheDirModules returns the compiled path to the modules cache.
// For internal use.
func (c Configs) CacheDirModules() string {
return c[CacheKeyModules].DirCompiled
}
// CacheDirMisc returns the compiled path to the misc cache.
// For internal use.
func (c Configs) CacheDirMisc() string {
return c[CacheKeyMisc].DirCompiled
}
var defaultCacheConfigs = Configs{
CacheKeyModules: {
MaxAge: -1,
+1 -1
View File
@@ -67,7 +67,7 @@ title: "Home"
---
-- assets/a/pixel.png --
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
-- layouts/home.html --
-- layouts/index.html --
{{ warnf "HOME!" }}
{{ $img := resources.GetMatch "**.png" }}
{{ $img = $img.Resize "3x3" }}
+1 -1
View File
@@ -270,7 +270,7 @@ func newPathsSpec(t *testing.T, fs afero.Fs, configStr string) *helpers.PathSpec
cfg, err := config.FromConfigString(configStr, "toml")
c.Assert(err, qt.IsNil)
acfg := testconfig.GetTestConfig(fs, cfg)
p, err := helpers.NewPathSpec(hugofs.NewFrom(fs, acfg.BaseConfig()), acfg, nil, nil)
p, err := helpers.NewPathSpec(hugofs.NewFrom(fs, acfg.BaseConfig()), acfg, nil)
c.Assert(err, qt.IsNil)
return p
}
+10 -21
View File
@@ -25,8 +25,6 @@ import (
// DefaultConfig holds the default configuration for the HTTP cache.
var DefaultConfig = Config{
RespectCacheControlNoStoreInRequest: true,
RespectCacheControlNoStoreInResponse: false,
Cache: Cache{
For: GlobMatcher{
Excludes: []string{"**"},
@@ -44,13 +42,7 @@ var DefaultConfig = Config{
// Config holds the configuration for the HTTP cache.
type Config struct {
// When enabled and there's a Cache-Control: no-store directive in the request, response will never be stored in disk cache.
RespectCacheControlNoStoreInRequest bool
// When enabled and there's a Cache-Control: no-store directive in the response, response will never be stored in disk cache.
RespectCacheControlNoStoreInResponse bool
// Enables HTTP cache behavior (RFC 9111) for these resources.
// Configures the HTTP cache behavior (RFC 9111).
// When this is not enabled for a resource, Hugo will go straight to the file cache.
Cache Cache
@@ -65,9 +57,7 @@ type Cache struct {
}
func (c *Config) Compile() (ConfigCompiled, error) {
cc := ConfigCompiled{
Base: *c,
}
var cc ConfigCompiled
p, err := c.Cache.For.CompilePredicate()
if err != nil {
@@ -137,7 +127,6 @@ func (gm GlobMatcher) IsZero() bool {
}
type ConfigCompiled struct {
Base Config
For predicate.P[string]
PollConfigs []PollConfigCompiled
}
@@ -173,16 +162,16 @@ func (gm *GlobMatcher) CompilePredicate() (func(string) bool, error) {
if gm.IsZero() {
panic("no includes or excludes")
}
var b predicate.PR[string]
var p predicate.P[string]
for _, include := range gm.Includes {
g, err := glob.Compile(include, '/')
if err != nil {
return nil, err
}
fn := func(s string) predicate.Match {
return predicate.BoolMatch(g.Match(s))
fn := func(s string) bool {
return g.Match(s)
}
b = b.Or(fn)
p = p.Or(fn)
}
for _, exclude := range gm.Excludes {
@@ -190,13 +179,13 @@ func (gm *GlobMatcher) CompilePredicate() (func(string) bool, error) {
if err != nil {
return nil, err
}
fn := func(s string) predicate.Match {
return predicate.BoolMatch(!g.Match(s))
fn := func(s string) bool {
return !g.Match(s)
}
b = b.And(fn)
p = p.And(fn)
}
return b.BoolFunc(), nil
return p, nil
}
func DecodeConfig(_ config.BaseConfig, m map[string]any) (Config, error) {
+1 -1
View File
@@ -52,7 +52,7 @@ func TestDefaultConfig(t *testing.T) {
func TestDecodeConfigInjectsDefaultAndCompiles(t *testing.T) {
c := qt.New(t)
cfg, err := DecodeConfig(config.BaseConfig{}, map[string]any{})
cfg, err := DecodeConfig(config.BaseConfig{}, map[string]interface{}{})
c.Assert(err, qt.IsNil)
c.Assert(cfg, qt.DeepEquals, DefaultConfig)
+3 -1
View File
@@ -401,7 +401,9 @@ func (r *rootCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args
watchGroups := helpers.ExtractAndGroupRootPaths(watchDirs)
r.Printf("Watching for changes in %s\n", strings.Join(watchGroups, ", "))
for _, group := range watchGroups {
r.Printf("Watching for changes in %s\n", group)
}
watcher, err := b.newWatcher(r.poll, watchDirs...)
if err != nil {
return err
+5 -6
View File
@@ -25,7 +25,6 @@ import (
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/config/allconfig"
"github.com/gohugoio/hugo/hugolib/sitesmatrix"
"github.com/gohugoio/hugo/modules"
"github.com/gohugoio/hugo/parser"
"github.com/gohugoio/hugo/parser/metadecoders"
@@ -72,7 +71,7 @@ func (c *configCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
return fmt.Errorf("language %q not found", c.lang)
}
} else {
config = conf.configs.LanguageConfigMap[conf.configs.Base.DefaultContentLanguage]
config = conf.configs.LanguageConfigSlice[0]
}
var buf bytes.Buffer
@@ -129,9 +128,9 @@ func (c *configCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
}
type configModMount struct {
Source string `json:"source"`
Target string `json:"target"`
Sites sitesmatrix.Sites `json:"sites,omitzero"`
Source string `json:"source"`
Target string `json:"target"`
Lang string `json:"lang,omitempty"`
}
type configModMounts struct {
@@ -147,7 +146,7 @@ func (m *configModMounts) MarshalJSON() ([]byte, error) {
mounts = append(mounts, configModMount{
Source: mount.Source,
Target: mount.Target,
Sites: mount.Sites,
Lang: mount.Lang,
})
}
+7 -12
View File
@@ -28,7 +28,6 @@ import (
"github.com/alecthomas/chroma/v2/formatters/html"
"github.com/alecthomas/chroma/v2/styles"
"github.com/bep/simplecobra"
"github.com/goccy/go-yaml"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/docshelper"
"github.com/gohugoio/hugo/helpers"
@@ -37,6 +36,7 @@ import (
"github.com/gohugoio/hugo/parser"
"github.com/spf13/cobra"
"github.com/spf13/cobra/doc"
"gopkg.in/yaml.v2"
)
func newGenCommand() *genCommand {
@@ -51,7 +51,6 @@ func newGenCommand() *genCommand {
lineNumbersInlineStyle string
lineNumbersTableStyle string
omitEmpty bool
omitClassComments bool
)
newChromaStyles := func() simplecobra.Commander {
@@ -82,14 +81,12 @@ See https://xyproto.github.io/splash/docs/all.html for a preview of the availabl
return err
}
var formatter *html.Formatter
if omitEmpty {
// See https://github.com/alecthomas/chroma/commit/5b2a4c5a26c503c79bc86ba3c4ae5b330028bd3d
hugo.Deprecate("--omitEmpty", "Flag is no longer needed, empty classes are now always omitted.", "v0.149.0")
formatter = html.New(html.WithClasses(true))
} else {
formatter = html.New(html.WithAllClasses(true))
}
options := []html.Option{
html.WithCSSComments(!omitClassComments),
}
formatter := html.New(options...)
w := os.Stdout
fmt.Fprintf(w, "/* Generated using: hugo %s */\n\n", strings.Join(os.Args[1:], " "))
@@ -106,10 +103,8 @@ 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 (deprecated, no longer needed)`)
cmd.PersistentFlags().BoolVar(&omitEmpty, "omitEmpty", false, `omit empty CSS rules`)
_ = cmd.RegisterFlagCompletionFunc("omitEmpty", cobra.NoFileCompletions)
cmd.PersistentFlags().BoolVar(&omitClassComments, "omitClassComments", false, `omit CSS class comment prefixes in the generated CSS`)
_ = cmd.RegisterFlagCompletionFunc("omitClassComments", cobra.NoFileCompletions)
},
}
}
@@ -249,7 +244,7 @@ url: %s
return err
}
defer f.Close()
yamlEnc := yaml.NewEncoder(f, yaml.UseSingleQuote(true), yaml.AutoInt())
yamlEnc := yaml.NewEncoder(f)
if err := yamlEnc.Encode(m); err != nil {
return err
}
+1 -1
View File
@@ -76,7 +76,7 @@ func flagsToCfgWithAdditionalConfigBase(cd *simplecobra.Commandeer, cfg config.P
// Flags with a different name in the config.
keyMap := map[string]string{
"minify": "minify.minifyOutput",
"minify": "minifyOutput",
"destination": "publishDir",
"editor": "newContentEditor",
}
+3 -32
View File
@@ -27,11 +27,9 @@ import (
"sync/atomic"
"time"
"github.com/bep/debounce"
"github.com/bep/simplecobra"
"github.com/fsnotify/fsnotify"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/hstrings"
"github.com/gohugoio/hugo/common/htime"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/common/loggers"
@@ -144,7 +142,7 @@ func (c *hugoBuilder) getDirList() ([]string, error) {
return nil, err
}
return hstrings.UniqueStringsSorted(h.PathSpec.BaseFs.WatchFilenames()), nil
return helpers.UniqueStringsSorted(h.PathSpec.BaseFs.WatchFilenames()), nil
}
func (c *hugoBuilder) initCPUProfile() (func(), error) {
@@ -464,15 +462,7 @@ func (c *hugoBuilder) copyStaticTo(sourceFs *filesystems.SourceFilesystem) (uint
infol.Logf("removing all files from destination that don't exist in static dirs")
syncer.DeleteFilter = func(f fsync.FileInfo) bool {
name := f.Name()
// Keep .gitignore and .gitattributes anywhere
if name == ".gitignore" || name == ".gitattributes" {
return true
}
// Keep Hugo's original dot-directory behavior
return f.IsDir() && strings.HasPrefix(name, ".")
return f.IsDir() && strings.HasPrefix(f.Name(), ".")
}
}
start := time.Now()
@@ -525,14 +515,6 @@ func (c *hugoBuilder) doWithPublishDirs(f func(sourceFs *filesystems.SourceFiles
return langCount, nil
}
func (c *hugoBuilder) progressIntermediate() {
terminal.ReportProgress(c.r.StdOut, terminal.ProgressIntermediate, 0)
}
func (c *hugoBuilder) progressHidden() {
terminal.ReportProgress(c.r.StdOut, terminal.ProgressHidden, 0)
}
func (c *hugoBuilder) fullBuild(noBuildLock bool) error {
var (
g errgroup.Group
@@ -836,7 +818,7 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
continue
}
walkAdder := func(ctx context.Context, path string, f hugofs.FileMetaInfo) error {
walkAdder := func(path string, f hugofs.FileMetaInfo) error {
if f.IsDir() {
c.r.logger.Println("adding created directory to watchlist", path)
if err := watcher.Add(path); err != nil {
@@ -1045,17 +1027,6 @@ func (c *hugoBuilder) hugoTry() *hugolib.HugoSites {
}
func (c *hugoBuilder) loadConfig(cd *simplecobra.Commandeer, running bool) error {
if terminal.PrintANSIColors(os.Stdout) {
defer c.progressHidden()
// If the configuration takes a while to load, we want to show some progress.
// This is typically loading of external modules.
d := debounce.New(500 * time.Millisecond)
d(func() {
c.progressIntermediate()
})
defer d(func() {})
}
cfg := config.New()
cfg.Set("renderToMemory", c.r.renderToMemory)
watch := c.r.buildWatch || (c.s != nil && c.s.serverWatch)
+1 -1
View File
@@ -427,7 +427,7 @@ func (c *importCommand) importFromJekyll(args []string) error {
c.r.Println("Importing...")
fileCount := 0
callback := func(ctx context.Context, path string, fi hugofs.FileMetaInfo) error {
callback := func(path string, fi hugofs.FileMetaInfo) error {
if fi.IsDir() {
return nil
}
+1 -3
View File
@@ -53,9 +53,7 @@ Ensure you run this within the root directory of your site.`,
if len(args) < 1 {
return newUserError("path needs to be provided")
}
cfg := flagsToCfg(cd, nil)
cfg.Set("BuildFuture", true)
h, err := r.Hugo(cfg)
h, err := r.Hugo(flagsToCfg(cd, nil))
if err != nil {
return err
}
+1 -3
View File
@@ -28,14 +28,13 @@ func newReleaseCommand() simplecobra.Commander {
step int
skipPush bool
try bool
version string
)
return &simpleCommand{
name: "release",
short: "Release a new version of Hugo",
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
rel, err := releaser.New(skipPush, try, step, version)
rel, err := releaser.New(skipPush, try, step)
if err != nil {
return err
}
@@ -48,7 +47,6 @@ func newReleaseCommand() simplecobra.Commander {
cmd.PersistentFlags().BoolVarP(&skipPush, "skip-push", "", false, "skip pushing to remote")
cmd.PersistentFlags().BoolVarP(&try, "try", "", false, "no changes")
cmd.PersistentFlags().IntVarP(&step, "step", "", 0, "step to run (1: set new version 2: prepare next dev version)")
cmd.PersistentFlags().StringVarP(&version, "version", "", "", "version to release (derived from branch name if not set)")
_ = cmd.RegisterFlagCompletionFunc("step", cobra.FixedCompletions([]string{"1", "2"}, cobra.ShellCompDirectiveNoFileComp))
},
}
+7 -11
View File
@@ -49,8 +49,6 @@ import (
"github.com/fsnotify/fsnotify"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/langs"
"github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/gohugoio/hugo/common/types"
@@ -371,12 +369,6 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
if f.c.fastRenderMode && f.c.errState.buildErr() == nil {
if isNavigation(requestURI, r) {
// See issue 14240.
// Hugo escapes the URL paths when generating them,
// that may not be the case when we receive it back from the browser.
// PathEscape will escape if it is not already escaped.
requestURI = paths.PathEscape(requestURI)
if !f.c.visitedURLs.Contains(requestURI) {
// If not already on stack, re-render that single page.
if err := f.c.partialReRender(requestURI); err != nil {
@@ -499,7 +491,9 @@ func (c *serverCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
watchGroups := helpers.ExtractAndGroupRootPaths(watchDirs)
c.r.Printf("Watching for changes in %s\n", strings.Join(watchGroups, ", "))
for _, group := range watchGroups {
c.r.Printf("Watching for changes in %s\n", group)
}
watcher, err := c.newWatcher(c.r.poll, watchDirs...)
if err != nil {
return err
@@ -633,7 +627,7 @@ func (c *serverCommand) setServerInfoInConfig() error {
panic("no server ports set")
}
return c.withConfE(func(conf *commonConfig) error {
for i, language := range conf.configs.Languages {
for i, language := range conf.configs.LanguagesDefaultFirst {
isMultihost := conf.configs.IsMultihost
var serverPort int
if isMultihost {
@@ -885,7 +879,7 @@ func (c *serverCommand) serve() error {
if isMultihost {
for _, l := range conf.configs.ConfigLangs() {
baseURLs = append(baseURLs, l.BaseURL())
roots = append(roots, l.Language().(*langs.Language).Lang)
roots = append(roots, l.Language().Lang)
}
} else {
l := conf.configs.GetFirstLanguageConfig()
@@ -1040,6 +1034,7 @@ func (c *serverCommand) serve() error {
defer cancel()
wg2, ctx := errgroup.WithContext(ctx)
for _, srv := range servers {
srv := srv
wg2.Go(func() error {
return srv.Shutdown(ctx)
})
@@ -1171,6 +1166,7 @@ func chmodFilter(dst, src os.FileInfo) bool {
}
func cleanErrorLog(content string) string {
content = strings.ReplaceAll(content, "\n", " ")
content = logReplacer.Replace(content)
content = logDuplicateTemplateExecuteRe.ReplaceAllString(content, "")
content = logDuplicateTemplateParseRe.ReplaceAllString(content, "")
+16 -4
View File
@@ -16,8 +16,6 @@ package collections
import (
"fmt"
"reflect"
"github.com/gohugoio/hugo/common/hreflect"
)
// Append appends from to a slice to and returns the resulting slice.
@@ -27,7 +25,7 @@ func Append(to any, from ...any) (any, error) {
if len(from) == 0 {
return to, nil
}
tov, toIsNil := hreflect.Indirect(reflect.ValueOf(to))
tov, toIsNil := indirect(reflect.ValueOf(to))
toIsNil = toIsNil || to == nil
var tot reflect.Type
@@ -102,7 +100,7 @@ func Append(to any, from ...any) (any, error) {
fv := reflect.ValueOf(f)
if !fv.IsValid() || !fv.Type().AssignableTo(tot) {
// Fall back to a []interface{} slice.
tov, _ := hreflect.Indirect(reflect.ValueOf(to))
tov, _ := indirect(reflect.ValueOf(to))
return appendToInterfaceSlice(tov, from...)
}
tov = reflect.Append(tov, fv)
@@ -138,3 +136,17 @@ func appendToInterfaceSlice(tov reflect.Value, from ...any) ([]any, error) {
return tos, nil
}
// indirect is borrowed from the Go stdlib: 'text/template/exec.go'
// TODO(bep) consolidate
func indirect(v reflect.Value) (rv reflect.Value, isNil bool) {
for ; v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface; v = v.Elem() {
if v.IsNil() {
return v, true
}
if v.Kind() == reflect.Interface && v.NumMethod() > 0 {
break
}
}
return v, false
}
+64
View File
@@ -15,6 +15,7 @@ package collections
import (
"html/template"
"reflect"
"testing"
qt "github.com/frankban/quicktest"
@@ -147,3 +148,66 @@ 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)
})
}
}
+2 -11
View File
@@ -13,13 +13,9 @@
package collections
import (
"iter"
"slices"
"sync"
import "slices"
"github.com/gohugoio/hugo/common/hiter"
)
import "sync"
// Stack is a simple LIFO stack that is safe for concurrent use.
type Stack[T any] struct {
@@ -64,11 +60,6 @@ func (s *Stack[T]) Len() int {
return len(s.items)
}
// All returns all items in the stack, from bottom to top.
func (s *Stack[T]) All() iter.Seq2[int, T] {
return hiter.Lock2(slices.All(s.items), s.mu.RLock, s.mu.RUnlock)
}
func (s *Stack[T]) Drain() []T {
s.mu.Lock()
defer s.mu.Unlock()
-29
View File
@@ -23,7 +23,6 @@ import (
"github.com/cespare/xxhash/v2"
"github.com/gohugoio/hashstructure"
"github.com/gohugoio/hugo/common/hugio"
"github.com/gohugoio/hugo/identity"
)
@@ -39,34 +38,6 @@ func XXHashFromReader(r io.Reader) (uint64, int64, error) {
return h.Sum64(), size, nil
}
type Hasher interface {
io.StringWriter
io.Writer
io.ReaderFrom
Sum64() uint64
}
type HashCloser interface {
Hasher
io.Closer
}
// XxHasher returns a Hasher that uses xxHash.
// Remember to call Close when done.
func XxHasher() HashCloser {
h := getXxHashReadFrom()
return struct {
Hasher
io.Closer
}{
Hasher: h,
Closer: hugio.CloserFunc(func() error {
putXxHashReadFrom(h)
return nil
}),
}
}
// XxHashFromReaderHexEncoded calculates the xxHash for the given reader
// and returns the hash as a hex encoded string.
func XxHashFromReaderHexEncoded(r io.Reader) (string, error) {
+11 -8
View File
@@ -38,6 +38,7 @@ func TestXxHashFromReaderPara(t *testing.T) {
var wg sync.WaitGroup
for i := range 10 {
i := i
wg.Add(1)
go func() {
defer wg.Done()
@@ -74,8 +75,8 @@ func TestXxHashFromStringHexEncoded(t *testing.T) {
func BenchmarkXXHashFromReader(b *testing.B) {
r := strings.NewReader("Hello World")
for b.Loop() {
b.ResetTimer()
for i := 0; i < b.N; i++ {
XXHashFromReader(r)
r.Seek(0, 0)
}
@@ -83,16 +84,16 @@ func BenchmarkXXHashFromReader(b *testing.B) {
func BenchmarkXXHashFromString(b *testing.B) {
s := "Hello World"
for b.Loop() {
b.ResetTimer()
for i := 0; i < b.N; i++ {
XXHashFromString(s)
}
}
func BenchmarkXXHashFromStringHexEncoded(b *testing.B) {
s := "The quick brown fox jumps over the lazy dog"
for b.Loop() {
b.ResetTimer()
for i := 0; i < b.N; i++ {
XxHashFromStringHexEncoded(s)
}
}
@@ -135,7 +136,7 @@ func BenchmarkHashString(b *testing.B) {
for _, test := range tests {
b.Run(fmt.Sprintf("n%d", len(test)), func(b *testing.B) {
for b.Loop() {
for i := 0; i < b.N; i++ {
HashString(test)
}
})
@@ -148,7 +149,9 @@ func BenchmarkHashMap(b *testing.B) {
m[fmt.Sprintf("key%d", i)] = i
}
for b.Loop() {
b.ResetTimer()
for i := 0; i < b.N; i++ {
HashString(m)
}
}
+46
View File
@@ -0,0 +1,46 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hcontext
import "context"
// ContextDispatcher is a generic interface for setting and getting values from a context.
type ContextDispatcher[T any] interface {
Set(ctx context.Context, value T) context.Context
Get(ctx context.Context) T
}
// NewContextDispatcher creates a new ContextDispatcher with the given key.
func NewContextDispatcher[T any, R comparable](key R) ContextDispatcher[T] {
return keyInContext[T, R]{
id: key,
}
}
type keyInContext[T any, R comparable] struct {
zero T
id R
}
func (f keyInContext[T, R]) Get(ctx context.Context) T {
v := ctx.Value(f.id)
if v == nil {
return f.zero
}
return v.(T)
}
func (f keyInContext[T, R]) Set(ctx context.Context, value T) context.Context {
return context.WithValue(ctx, f.id, value)
}
-61
View File
@@ -1,61 +0,0 @@
// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hdebug
import (
"fmt"
"strings"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/htesting"
)
// Printf is a debug print function that should be removed before committing code to the repository.
func Printf(format string, args ...any) {
panicIfRealCI()
if len(args) == 1 && !strings.Contains(format, "%") {
format = format + ": %v"
}
if !strings.HasSuffix(format, "\n") {
format = format + "\n"
}
fmt.Printf(format, args...)
}
func AssertNotNil(a ...any) {
panicIfRealCI()
for _, v := range a {
if types.IsNil(v) {
panic("hdebug.AssertNotNil: value is nil")
}
}
}
func Panicf(format string, args ...any) {
panicIfRealCI()
// fmt.Println(stack())
if len(args) == 1 && !strings.Contains(format, "%") {
format = format + ": %v"
}
if !strings.HasSuffix(format, "\n") {
format = format + "\n"
}
panic(fmt.Sprintf(format, args...))
}
func panicIfRealCI() {
if htesting.IsRealCI() {
panic("This debug statement should be removed before committing code!")
}
}
+10 -3
View File
@@ -17,13 +17,22 @@ package herrors
import (
"errors"
"fmt"
"io"
"os"
"regexp"
"runtime"
"runtime/debug"
"strings"
"time"
)
// PrintStackTrace prints the current stacktrace to w.
func PrintStackTrace(w io.Writer) {
buf := make([]byte, 1<<16)
runtime.Stack(buf, true)
fmt.Fprintf(w, "%s", buf)
}
// ErrorSender is a, typically, non-blocking error handler.
type ErrorSender interface {
SendError(err error)
@@ -36,9 +45,7 @@ type ErrorSender interface {
func Recover(args ...any) {
if r := recover(); r != nil {
fmt.Println("ERR:", r)
buf := make([]byte, 64<<10)
buf = buf[:runtime.Stack(buf, false)]
args = append(args, "stacktrace from panic: \n"+string(buf), "\n")
args = append(args, "stacktrace from panic: \n"+string(debug.Stack()), "\n")
fmt.Println(args...)
}
}
+10 -9
View File
@@ -110,11 +110,11 @@ func (fe *fileError) UpdateContent(r io.Reader, linematcher LineMatcherFn) FileE
fe.errorContext = ectx
if ectx.Position.LineNumber > 0 && ectx.Position.LineNumber > fe.position.LineNumber {
if ectx.Position.LineNumber > 0 {
fe.position.LineNumber = ectx.Position.LineNumber
}
if ectx.Position.ColumnNumber > 0 && ectx.Position.ColumnNumber > fe.position.ColumnNumber {
if ectx.Position.ColumnNumber > 0 {
fe.position.ColumnNumber = ectx.Position.ColumnNumber
}
@@ -177,7 +177,6 @@ func NewFileErrorFromName(err error, name string) FileError {
// Filetype is used to determine the Chroma lexer to use.
fileType, pos := extractFileTypePos(err)
pos.Filename = name
if fileType == "" {
_, fileType = paths.FileAndExtNoDelimiter(filepath.Clean(name))
}
@@ -235,9 +234,7 @@ func NewFileErrorFromFile(err error, filename string, fs afero.Fs, linematcher L
return NewFileErrorFromName(err, realFilename)
}
defer f.Close()
fe := NewFileErrorFromName(err, realFilename)
fe = fe.UpdateContent(f, linematcher)
return fe
return NewFileErrorFromName(err, realFilename).UpdateContent(f, linematcher)
}
func openFile(filename string, fs afero.Fs) (afero.File, string, error) {
@@ -324,9 +321,13 @@ func extractFileTypePos(err error) (string, text.Position) {
}
// Look in the error message for the line number.
if lno, col := commonLineNumberExtractor(err); lno > 0 {
pos.ColumnNumber = col
pos.LineNumber = lno
for _, handle := range lineNumberExtractors {
lno, col := handle(err)
if lno > 0 {
pos.ColumnNumber = col
pos.LineNumber = lno
break
}
}
if fileType == "" && pos.Filename != "" {
+3 -13
View File
@@ -19,27 +19,17 @@ import (
)
var lineNumberExtractors = []lineNumberExtractor{
// YAML parse errors.
newLineNumberErrHandlerFromRegexp(`\[(\d+):(\d+)\]`),
// Template/shortcode parse errors
newLineNumberErrHandlerFromRegexp(`:(\d+):(\d*):`),
newLineNumberErrHandlerFromRegexp(`:(\d+):`),
// YAML parse errors
newLineNumberErrHandlerFromRegexp(`line (\d+):`),
// i18n bundle errors
newLineNumberErrHandlerFromRegexp(`\((\d+),\s(\d*)`),
}
func commonLineNumberExtractor(e error) (int, int) {
for _, handler := range lineNumberExtractors {
lno, col := handler(e)
if lno > 0 {
return lno, col
}
}
return 0, 0
}
type lineNumberExtractor func(e error) (int, int)
func newLineNumberErrHandlerFromRegexp(expression string) lineNumberExtractor {
-66
View File
@@ -1,66 +0,0 @@
package hiter
// Common iterator functions.
// Some of these are are based on this discsussion: https://github.com/golang/go/issues/61898
import "iter"
// Concat returns an iterator over the concatenation of the sequences.
// Any nil sequences are ignored.
func Concat[V any](seqs ...iter.Seq[V]) iter.Seq[V] {
return func(yield func(V) bool) {
for _, seq := range seqs {
if seq == nil {
continue
}
for e := range seq {
if !yield(e) {
return
}
}
}
}
}
// Concat2 returns an iterator over the concatenation of the sequences.
// Any nil sequences are ignored.
func Concat2[K, V any](seqs ...iter.Seq2[K, V]) iter.Seq2[K, V] {
return func(yield func(K, V) bool) {
for _, seq := range seqs {
if seq == nil {
continue
}
for k, v := range seq {
if !yield(k, v) {
return
}
}
}
}
}
// Lock returns an iterator that locks before iterating and unlocks after.
func Lock[V any](seq iter.Seq[V], lock, unlock func()) iter.Seq[V] {
return func(yield func(V) bool) {
lock()
defer unlock()
for e := range seq {
if !yield(e) {
return
}
}
}
}
// Lock2 returns an iterator that locks before iterating and unlocks after.
func Lock2[K, V any](seq iter.Seq2[K, V], lock, unlock func()) iter.Seq2[K, V] {
return func(yield func(K, V) bool) {
lock()
defer unlock()
for k, v := range seq {
if !yield(k, v) {
return
}
}
}
}
-222
View File
@@ -1,222 +0,0 @@
// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hreflect
import (
"fmt"
"math"
"reflect"
)
var (
typeInt64 = reflect.TypeFor[int64]()
typeFloat64 = reflect.TypeFor[float64]()
typeString = reflect.TypeFor[string]()
)
// ToInt64 converts v to int64 if possible, returning an error if not.
func ToInt64E(v reflect.Value) (int64, error) {
if v, ok := ConvertIfPossible(v, typeInt64); ok {
return v.Int(), nil
}
return 0, errConvert(v, "int64")
}
// ToInt64 converts v to int64 if possible. It panics if the conversion is not possible.
func ToInt64(v reflect.Value) int64 {
vv, err := ToInt64E(v)
if err != nil {
panic(err)
}
return vv
}
// ToFloat64E converts v to float64 if possible, returning an error if not.
func ToFloat64E(v reflect.Value) (float64, error) {
if v, ok := ConvertIfPossible(v, typeFloat64); ok {
return v.Float(), nil
}
return 0, errConvert(v, "float64")
}
// ToFloat64 converts v to float64 if possible, panicking if not.
func ToFloat64(v reflect.Value) float64 {
vv, err := ToFloat64E(v)
if err != nil {
panic(err)
}
return vv
}
// ToStringE converts v to string if possible, returning an error if not.
func ToStringE(v reflect.Value) (string, error) {
vv, err := ToStringValueE(v)
if err != nil {
return "", err
}
return vv.String(), nil
}
func ToStringValueE(v reflect.Value) (reflect.Value, error) {
if v, ok := ConvertIfPossible(v, typeString); ok {
return v, nil
}
return reflect.Value{}, errConvert(v, "string")
}
// ToString converts v to string if possible, panicking if not.
func ToString(v reflect.Value) string {
vv, err := ToStringE(v)
if err != nil {
panic(err)
}
return vv
}
func errConvert(v reflect.Value, s string) error {
return fmt.Errorf("unable to convert value of type %q to %q", v.Type().String(), s)
}
// ConvertIfPossible tries to convert val to typ if possible.
// This is currently only implemented for int kinds,
// added to handle the move to a new YAML library which produces uint64 for unsigned integers.
// We can expand on this later if needed.
// This conversion is lossless.
// See Issue 14079.
func ConvertIfPossible(val reflect.Value, typ reflect.Type) (reflect.Value, bool) {
switch val.Kind() {
case reflect.Ptr, reflect.Interface:
if val.IsNil() {
// Return typ's zero value.
return reflect.Zero(typ), true
}
val = val.Elem()
}
if val.Type().AssignableTo(typ) {
// No conversion needed.
return val, true
}
if IsInt(typ.Kind()) {
return convertToIntIfPossible(val, typ)
}
if IsFloat(typ.Kind()) {
return convertToFloatIfPossible(val, typ)
}
if IsUint(typ.Kind()) {
return convertToUintIfPossible(val, typ)
}
if IsString(typ.Kind()) && IsString(val.Kind()) {
return val.Convert(typ), true
}
return reflect.Value{}, false
}
func convertToUintIfPossible(val reflect.Value, typ reflect.Type) (reflect.Value, bool) {
if IsInt(val.Kind()) {
i := val.Int()
if i < 0 {
return reflect.Value{}, false
}
u := uint64(i)
if typ.OverflowUint(u) {
return reflect.Value{}, false
}
return reflect.ValueOf(u).Convert(typ), true
}
if IsUint(val.Kind()) {
if typ.OverflowUint(val.Uint()) {
return reflect.Value{}, false
}
return val.Convert(typ), true
}
if IsFloat(val.Kind()) {
f := val.Float()
if f < 0 || f > float64(math.MaxUint64) {
return reflect.Value{}, false
}
if f != math.Trunc(f) {
return reflect.Value{}, false
}
u := uint64(f)
if typ.OverflowUint(u) {
return reflect.Value{}, false
}
return reflect.ValueOf(u).Convert(typ), true
}
return reflect.Value{}, false
}
func convertToFloatIfPossible(val reflect.Value, typ reflect.Type) (reflect.Value, bool) {
if IsInt(val.Kind()) {
i := val.Int()
f := float64(i)
if typ.OverflowFloat(f) {
return reflect.Value{}, false
}
return reflect.ValueOf(f).Convert(typ), true
}
if IsUint(val.Kind()) {
u := val.Uint()
f := float64(u)
if typ.OverflowFloat(f) {
return reflect.Value{}, false
}
return reflect.ValueOf(f).Convert(typ), true
}
if IsFloat(val.Kind()) {
if typ.OverflowFloat(val.Float()) {
return reflect.Value{}, false
}
return val.Convert(typ), true
}
return reflect.Value{}, false
}
func convertToIntIfPossible(val reflect.Value, typ reflect.Type) (reflect.Value, bool) {
if IsInt(val.Kind()) {
if typ.OverflowInt(val.Int()) {
return reflect.Value{}, false
}
return val.Convert(typ), true
}
if IsUint(val.Kind()) {
if val.Uint() > uint64(math.MaxInt64) {
return reflect.Value{}, false
}
if typ.OverflowInt(int64(val.Uint())) {
return reflect.Value{}, false
}
return val.Convert(typ), true
}
if IsFloat(val.Kind()) {
f := val.Float()
if f < float64(math.MinInt64) || f > float64(math.MaxInt64) {
return reflect.Value{}, false
}
if f != math.Trunc(f) {
return reflect.Value{}, false
}
if typ.OverflowInt(int64(f)) {
return reflect.Value{}, false
}
return reflect.ValueOf(int64(f)).Convert(typ), true
}
return reflect.Value{}, false
}
-284
View File
@@ -1,284 +0,0 @@
// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hreflect
import (
"math"
"reflect"
"testing"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/htesting/hqt"
)
func TestToFuncs(t *testing.T) {
c := qt.New(t)
c.Assert(ToInt64(reflect.ValueOf(int(42))), qt.Equals, int64(42))
c.Assert(ToFloat64(reflect.ValueOf(float32(3.14))), hqt.IsSameFloat64, float64(3.14))
c.Assert(ToString(reflect.ValueOf("hello")), qt.Equals, "hello")
}
func TestConvertIfPossible(t *testing.T) {
c := qt.New(t)
for _, test := range []struct {
name string
value any
typ any
expected any
ok bool
}{
// From uint to int.
{
name: "uint64(math.MaxUint64) to int16",
value: uint64(math.MaxUint64),
typ: int16(0),
ok: false, // overflow
},
{
name: "uint64(math.MaxUint64) to int64",
value: uint64(math.MaxUint64),
typ: int64(0),
ok: false, // overflow
},
{
name: "uint64(math.MaxInt16) to int16",
value: uint64(math.MaxInt16),
typ: int64(0),
ok: true,
expected: int64(math.MaxInt16),
},
// From int to int.
{
name: "int64(math.MaxInt64) to int16",
value: int64(math.MaxInt64),
typ: int16(0),
ok: false, // overflow
},
{
name: "int64(math.MaxInt16) to int",
value: int64(math.MaxInt16),
typ: int(0),
ok: true,
expected: int(math.MaxInt16),
},
{
name: "int64(math.MaxInt16) to int",
value: int64(math.MaxInt16),
typ: int(0),
ok: true,
expected: int(math.MaxInt16),
},
// From float64 to int.
{
name: "float64(1.5) to int",
value: float64(1.5),
typ: int(0),
ok: false, // loss of precision
},
{
name: "float64(1.0) to int",
value: float64(1.0),
typ: int(0),
ok: true,
expected: int(1),
},
{
name: "float64(math.MaxInt16+1) to int16",
value: float64(math.MaxInt16 + 1),
typ: int16(0),
ok: false, // overflow
},
{
name: "float64(math.MaxFloat64) to int64",
value: float64(math.MaxFloat64),
typ: int64(0),
ok: false, // overflow
},
{
name: "float64(32767) to int16",
value: float64(32767),
typ: int16(0),
ok: true,
expected: int16(32767),
},
// From float32 to int.
{
name: "float32(1.5) to int",
value: float32(1.5),
typ: int(0),
ok: false, // loss of precision
},
{
name: "float32(1.0) to int",
value: float32(1.0),
typ: int(0),
ok: true,
expected: int(1),
},
{
name: "float32(math.MaxFloat32) to int16",
value: float32(math.MaxFloat32),
typ: int16(0),
ok: false, // overflow
},
{
name: "float32(math.MaxFloat32) to int64",
value: float32(math.MaxFloat32),
typ: int64(0),
ok: false, // overflow
},
{
name: "float32(math.MaxInt16) to int16",
value: float32(math.MaxInt16),
typ: int16(0),
ok: true,
expected: int16(32767),
},
{
name: "float32(math.MaxInt16+1) to int16",
value: float32(math.MaxInt16 + 1),
typ: int16(0),
ok: false, // overflow
},
// Int to float.
{
name: "int16(32767) to float32",
value: int16(32767),
typ: float32(0),
ok: true,
expected: float32(32767),
},
{
name: "int64(32767) to float32",
value: int64(32767),
typ: float32(0),
ok: true,
expected: float32(32767),
},
{
name: "int64(math.MaxInt64) to float32",
value: int64(math.MaxInt64),
typ: float32(0),
ok: true,
expected: float32(math.MaxInt64),
},
{
name: "int64(math.MaxInt64) to float64",
value: int64(math.MaxInt64),
typ: float64(0),
ok: true,
expected: float64(math.MaxInt64),
},
// Int to uint.
{
name: "int16(32767) to uint16",
value: int16(32767),
typ: uint16(0),
ok: true,
expected: uint16(32767),
},
{
name: "int16(32767) to uint8",
value: int16(32767),
typ: uint8(0),
ok: false,
},
{
name: "float64(3.14) to uint64",
value: float64(3.14),
typ: uint64(0),
ok: false,
},
{
name: "float64(3.0) to uint64",
value: float64(3.0),
typ: uint64(0),
ok: true,
expected: uint64(3),
},
// From uint to float.
{
name: "uint64(math.MaxInt16) to float64",
value: uint64(math.MaxInt16),
typ: float64(0),
ok: true,
expected: float64(math.MaxInt16),
},
// Float to float.
{
name: "float64(3.14) to float32",
value: float64(3.14),
typ: float32(0),
ok: true,
expected: float32(3.14),
},
{
name: "float32(3.14) to float64",
value: float32(3.14),
typ: float64(0),
ok: true,
expected: float64(3.14),
},
{
name: "float64(3.14) to float64",
value: float64(3.14),
typ: float64(0),
ok: true,
expected: float64(3.14),
},
} {
v, ok := ConvertIfPossible(reflect.ValueOf(test.value), reflect.TypeOf(test.typ))
c.Assert(ok, qt.Equals, test.ok, qt.Commentf("test case: %s", test.name))
if test.ok {
c.Assert(v.Interface(), hqt.IsSameNumber, test.expected, qt.Commentf("test case: %s", test.name))
}
}
}
func TestConvertIfPossibleMisc(t *testing.T) {
c := qt.New(t)
type s string
var (
i = int32(42)
i64 = int64(i)
iv any = i
ip = &i
inil any = (*int32)(nil)
shello = s("hello")
)
convertOK := func(v any, typ any) any {
rv, ok := ConvertIfPossible(reflect.ValueOf(v), reflect.TypeOf(typ))
c.Assert(ok, qt.IsTrue)
return rv.Interface()
}
c.Assert(convertOK(shello, ""), qt.Equals, "hello")
c.Assert(convertOK(ip, int64(0)), qt.Equals, i64)
c.Assert(convertOK(iv, int64(0)), qt.Equals, i64)
c.Assert(convertOK(inil, int64(0)), qt.Equals, int64(0))
}
func BenchmarkToInt64(b *testing.B) {
v := reflect.ValueOf(int(42))
for b.Loop() {
ToInt64(v)
}
}
+18 -100
View File
@@ -1,4 +1,6 @@
// Copyright 2025 The Hugo Authors. All rights reserved.
// Copyright 2024 The Hugo Authors. All rights reserved.
// Some functions in this file (see comments) is based on the Go source code,
// copyright The Go Authors and governed by a BSD-style license.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -25,11 +27,6 @@ import (
"github.com/gohugoio/hugo/common/types"
)
// IsInterfaceOrPointer returns whether the given kind is an interface or a pointer.
func IsInterfaceOrPointer(kind reflect.Kind) bool {
return kind == reflect.Interface || kind == reflect.Ptr
}
// TODO(bep) replace the private versions in /tpl with these.
// IsNumber returns whether the given kind is a number.
func IsNumber(kind reflect.Kind) bool {
@@ -66,11 +63,6 @@ func IsFloat(kind reflect.Kind) bool {
}
}
// IsString returns whether the given kind is a string.
func IsString(kind reflect.Kind) bool {
return kind == reflect.String
}
// IsTruthful returns whether in represents a truthful value.
// See IsTruthfulValue
func IsTruthful(in any) bool {
@@ -94,18 +86,6 @@ func IsSlice(v any) bool {
var zeroType = reflect.TypeOf((*types.Zeroer)(nil)).Elem()
var isZeroCache sync.Map
func implementsIsZero(tp reflect.Type) bool {
v, ok := isZeroCache.Load(tp)
if ok {
return v.(bool)
}
implements := tp.Implements(zeroType)
isZeroCache.Store(tp, implements)
return implements
}
// IsTruthfulValue returns whether the given value has a meaningful truth value.
// This is based on template.IsTrue in Go's stdlib, but also considers
// IsZero and any interface value will be unwrapped before it's considered
@@ -114,18 +94,14 @@ func implementsIsZero(tp reflect.Type) bool {
// Based on:
// https://github.com/golang/go/blob/178a2c42254166cffed1b25fb1d3c7a5727cada6/src/text/template/exec.go#L306
func IsTruthfulValue(val reflect.Value) (truth bool) {
val, isNil := Indirect(val)
val = indirectInterface(val)
if !val.IsValid() {
// Something like: var x any, never set. It's a form of nil.
// Something like var x interface{}, never set. It's a form of nil.
return
}
if val.Kind() == reflect.Pointer && isNil {
return
}
if implementsIsZero(val.Type()) {
if val.Type().Implements(zeroType) {
return !val.Interface().(types.Zeroer).IsZero()
}
@@ -158,31 +134,10 @@ type methodKey struct {
name string
}
var (
methodIndexCache sync.Map
methodCache sync.Map
)
var methodCache sync.Map
// GetMethodByNameForType returns the method with the given name for the given type,
// or a zero Method if no such method exists.
// It panics if tp is an interface type.
// It caches the lookup.
func GetMethodByNameForType(tp reflect.Type, name string) reflect.Method {
if tp.Kind() == reflect.Interface {
// Func field is nil for interface types.
panic("not supported for interface types")
}
k := methodKey{tp, name}
v, found := methodCache.Load(k)
if found {
return v.(reflect.Method)
}
m, _ := tp.MethodByName(name)
methodCache.Store(k, m)
return m
}
// GetMethodByName is the same as reflect.Value.MethodByName, but it caches the lookup.
// GetMethodByName is the same as reflect.Value.MethodByName, but it caches the
// type lookup.
func GetMethodByName(v reflect.Value, name string) reflect.Value {
index := GetMethodIndexByName(v.Type(), name)
@@ -197,7 +152,7 @@ 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 := methodIndexCache.Load(k)
v, found := methodCache.Load(k)
if found {
return v.(int)
}
@@ -206,7 +161,7 @@ func GetMethodIndexByName(tp reflect.Type, name string) int {
if !ok {
index = -1
}
methodIndexCache.Store(k, index)
methodCache.Store(k, index)
if !ok {
return -1
@@ -288,7 +243,6 @@ func ToSliceAny(v any) ([]any, bool) {
return nil, false
}
// CallMethodByName calls the method with the given name on v.
func CallMethodByName(cxt context.Context, name string, v reflect.Value) []reflect.Value {
fn := v.MethodByName(name)
var args []reflect.Value
@@ -306,51 +260,15 @@ func CallMethodByName(cxt context.Context, name string, v reflect.Value) []refle
return fn.Call(args)
}
// Indirect unwraps interfaces and pointers until it finds a non-interface/pointer value.
// If a nil is encountered, the second return value is true.
// If a pointer to a struct is encountered, it is not unwrapped.
func Indirect(v reflect.Value) (vv reflect.Value, isNil bool) {
for ; IsInterfaceOrPointer(v.Kind()); v = v.Elem() {
if IsNil(v) {
return v, true
}
if v.Kind() != reflect.Interface {
// A pointer.
if v.NumMethod() > 0 {
break
}
if v.Elem().Kind() == reflect.Struct {
// Avoid unwrapping pointers to structs.
break
}
}
// Based on: https://github.com/golang/go/blob/178a2c42254166cffed1b25fb1d3c7a5727cada6/src/text/template/exec.go#L931
func indirectInterface(v reflect.Value) reflect.Value {
if v.Kind() != reflect.Interface {
return v
}
return v, false
}
// IndirectElem is like Indirect, but if the final value is a pointer, it unwraps it.
func IndirectElem(v reflect.Value) (vv reflect.Value, isNil bool) {
vv, isNil = Indirect(v)
if isNil {
return vv, isNil
if v.IsNil() {
return reflect.Value{}
}
if vv.Kind() == reflect.Pointer {
vv = vv.Elem()
}
return vv, isNil
}
// IsNil reports whether v is nil.
// Based on reflect.Value.IsNil, but also considers invalid values as nil.
func IsNil(v reflect.Value) bool {
if !v.IsValid() {
return true
}
switch v.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
return v.IsNil()
}
return false
return v.Elem()
}
var contextInterface = reflect.TypeOf((*context.Context)(nil)).Elem()
+16 -126
View File
@@ -1,4 +1,4 @@
// Copyright 2025 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -20,32 +20,15 @@ import (
"time"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/htesting/hqt"
)
type zeroStruct struct {
zero bool
}
func (z zeroStruct) IsZero() bool {
return z.zero
}
func TestIsTruthful(t *testing.T) {
c := qt.New(t)
var nilpointerZero *zeroStruct
c.Assert(IsTruthful(true), qt.Equals, true)
c.Assert(IsTruthful(false), qt.Equals, false)
c.Assert(IsTruthful(time.Now()), qt.Equals, true)
c.Assert(IsTruthful(time.Time{}), qt.Equals, false)
c.Assert(IsTruthful(&zeroStruct{zero: false}), qt.Equals, true)
c.Assert(IsTruthful(&zeroStruct{zero: true}), qt.Equals, false)
c.Assert(IsTruthful(zeroStruct{zero: false}), qt.Equals, true)
c.Assert(IsTruthful(zeroStruct{zero: true}), qt.Equals, false)
c.Assert(IsTruthful(nil), qt.Equals, false)
c.Assert(IsTruthful(nilpointerZero), qt.Equals, false)
}
func TestGetMethodByName(t *testing.T) {
@@ -80,90 +63,18 @@ func TestToSliceAny(t *testing.T) {
checkOK([]int{1, 2, 3}, []any{1, 2, 3})
}
type testIndirectStruct struct {
S string
}
func (t *testIndirectStruct) GetS() string {
return t.S
}
func (t testIndirectStruct) Foo() string {
return "bar"
}
type testIndirectStructNoMethods struct {
S string
}
func TestIsNil(t *testing.T) {
c := qt.New(t)
var (
nilPtr *testIndirectStruct
nilIface any = nil
nonNilIface any = &testIndirectStruct{S: "hello"}
)
c.Assert(IsNil(reflect.ValueOf(nilPtr)), qt.Equals, true)
c.Assert(IsNil(reflect.ValueOf(nilIface)), qt.Equals, true)
c.Assert(IsNil(reflect.ValueOf(nonNilIface)), qt.Equals, false)
}
func TestIndirectInterface(t *testing.T) {
c := qt.New(t)
var (
structWithMethods = testIndirectStruct{S: "hello"}
structWithMethodsPointer = &testIndirectStruct{S: "hello"}
structWithMethodsPointerAny any = structWithMethodsPointer
structPointerToPointer = &structWithMethodsPointer
structNoMethodsPtr = &testIndirectStructNoMethods{S: "no methods"}
structNoMethods = testIndirectStructNoMethods{S: "no methods"}
intValue = 32
intPtr = &intValue
nilPtr *testIndirectStruct
nilIface any = nil
)
ind := func(v any) any {
c.Helper()
vv, isNil := Indirect(reflect.ValueOf(v))
c.Assert(isNil, qt.IsFalse)
return vv.Interface()
}
c.Assert(ind(intValue), hqt.IsSameType, 32)
c.Assert(ind(intPtr), hqt.IsSameType, 32)
c.Assert(ind(structNoMethodsPtr), hqt.IsSameType, structNoMethodsPtr)
c.Assert(ind(structWithMethods), hqt.IsSameType, structWithMethods)
c.Assert(ind(structNoMethods), hqt.IsSameType, structNoMethods)
c.Assert(ind(structPointerToPointer), hqt.IsSameType, &testIndirectStruct{})
c.Assert(ind(structWithMethodsPointer), hqt.IsSameType, &testIndirectStruct{})
c.Assert(ind(structWithMethodsPointerAny), hqt.IsSameType, structWithMethodsPointer)
vv, isNil := Indirect(reflect.ValueOf(nilPtr))
c.Assert(isNil, qt.IsTrue)
c.Assert(vv, qt.Equals, reflect.ValueOf(nilPtr))
vv, isNil = Indirect(reflect.ValueOf(nilIface))
c.Assert(isNil, qt.IsFalse)
c.Assert(vv, qt.Equals, reflect.ValueOf(nilIface))
}
func BenchmarkIsContextType(b *testing.B) {
const size = 1000
type k string
b.Run("value", func(b *testing.B) {
ctx := context.Background()
ctxs := make([]reflect.Type, size)
for i := range size {
ctxs := make([]reflect.Type, b.N)
for i := 0; i < b.N; i++ {
ctxs[i] = reflect.TypeOf(context.WithValue(ctx, k("key"), i))
}
for i := 0; b.Loop(); i++ {
idx := i % size
if !IsContextType(ctxs[idx]) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
if !IsContextType(ctxs[i]) {
b.Fatal("not context")
}
}
@@ -171,7 +82,7 @@ func BenchmarkIsContextType(b *testing.B) {
b.Run("background", func(b *testing.B) {
var ctxt reflect.Type = reflect.TypeOf(context.Background())
for b.Loop() {
for i := 0; i < b.N; i++ {
if !IsContextType(ctxt) {
b.Fatal("not context")
}
@@ -179,25 +90,14 @@ func BenchmarkIsContextType(b *testing.B) {
})
}
func BenchmarkIsTruthFulValue(b *testing.B) {
var (
stringHugo = reflect.ValueOf("Hugo")
stringEmpty = reflect.ValueOf("")
zero = reflect.ValueOf(time.Time{})
timeNow = reflect.ValueOf(time.Now())
boolTrue = reflect.ValueOf(true)
boolFalse = reflect.ValueOf(false)
nilPointer = reflect.ValueOf((*zeroStruct)(nil))
)
func BenchmarkIsTruthFul(b *testing.B) {
v := reflect.ValueOf("Hugo")
for b.Loop() {
IsTruthfulValue(stringHugo)
IsTruthfulValue(stringEmpty)
IsTruthfulValue(zero)
IsTruthfulValue(timeNow)
IsTruthfulValue(boolTrue)
IsTruthfulValue(boolFalse)
IsTruthfulValue(nilPointer)
b.ResetTimer()
for i := 0; i < b.N; i++ {
if !IsTruthfulValue(v) {
b.Fatal("not truthful")
}
}
}
@@ -223,22 +123,12 @@ func (t *testStruct) Method5() string {
return "Hugo"
}
func BenchmarkGetMethodByNameForType(b *testing.B) {
tp := reflect.TypeFor[*testStruct]()
methods := []string{"Method1", "Method2", "Method3", "Method4", "Method5"}
for b.Loop() {
for _, method := range methods {
_ = GetMethodByNameForType(tp, method)
}
}
}
func BenchmarkGetMethodByName(b *testing.B) {
v := reflect.ValueOf(&testStruct{})
methods := []string{"Method1", "Method2", "Method3", "Method4", "Method5"}
for b.Loop() {
b.ResetTimer()
for i := 0; i < b.N; i++ {
for _, method := range methods {
_ = GetMethodByName(v, method)
}
+4 -69
View File
@@ -17,7 +17,6 @@ import (
"fmt"
"regexp"
"slices"
"sort"
"strings"
"sync"
@@ -97,16 +96,6 @@ func GetOrCompileRegexp(pattern string) (re *regexp.Regexp, err error) {
return reCache.getOrCompileRegexp(pattern)
}
// HasAnyPrefix checks if the string s has any of the prefixes given.
func HasAnyPrefix(s string, prefixes ...string) bool {
for _, p := range prefixes {
if strings.HasPrefix(s, p) {
return true
}
}
return false
}
// 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 {
@@ -139,61 +128,7 @@ func ToString(v any) (string, bool) {
return "", false
}
// UniqueStrings returns a new slice with any duplicates removed.
func UniqueStrings(s []string) []string {
unique := make([]string, 0, len(s))
for i, val := range s {
var seen bool
for j := range i {
if s[j] == val {
seen = true
break
}
}
if !seen {
unique = append(unique, val)
}
}
return unique
}
// UniqueStringsReuse returns a slice with any duplicates removed.
// It will modify the input slice.
func UniqueStringsReuse(s []string) []string {
result := s[:0]
for i, val := range s {
var seen bool
for j := range i {
if s[j] == val {
seen = true
break
}
}
if !seen {
result = append(result, val)
}
}
return result
}
// UniqueStringsSorted returns a sorted slice with any duplicates removed.
// It will modify the input slice.
func UniqueStringsSorted(s []string) []string {
if len(s) == 0 {
return nil
}
ss := sort.StringSlice(s)
ss.Sort()
i := 0
for j := 1; j < len(s); j++ {
if !ss.Less(i, j) {
continue
}
i++
s[i] = s[j]
}
return s[:i+1]
}
type (
Strings2 [2]string
Strings3 [3]string
)
+3 -81
View File
@@ -1,4 +1,4 @@
// Copyright 2025 The Hugo Authors. All rights reserved.
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -14,7 +14,6 @@
package hstrings
import (
"reflect"
"regexp"
"testing"
@@ -44,91 +43,14 @@ func TestGetOrCompileRegexp(t *testing.T) {
c.Assert(re.MatchString("123"), qt.Equals, true)
}
func TestUniqueStrings(t *testing.T) {
in := []string{"a", "b", "a", "b", "c", "", "a", "", "d"}
output := UniqueStrings(in)
expected := []string{"a", "b", "c", "", "d"}
if !reflect.DeepEqual(output, expected) {
t.Errorf("Expected %#v, got %#v\n", expected, output)
}
}
func TestUniqueStringsReuse(t *testing.T) {
in := []string{"a", "b", "a", "b", "c", "", "a", "", "d"}
output := UniqueStringsReuse(in)
expected := []string{"a", "b", "c", "", "d"}
if !reflect.DeepEqual(output, expected) {
t.Errorf("Expected %#v, got %#v\n", expected, output)
}
}
func TestUniqueStringsSorted(t *testing.T) {
c := qt.New(t)
in := []string{"a", "a", "b", "c", "b", "", "a", "", "d"}
output := UniqueStringsSorted(in)
expected := []string{"", "a", "b", "c", "d"}
c.Assert(output, qt.DeepEquals, expected)
c.Assert(UniqueStringsSorted(nil), qt.IsNil)
}
// Note that these cannot use b.Loop() because of golang/go#27217.
func BenchmarkUniqueStrings(b *testing.B) {
input := []string{"a", "b", "d", "e", "d", "h", "a", "i"}
b.Run("Safe", func(b *testing.B) {
for b.Loop() {
result := UniqueStrings(input)
if len(result) != 6 {
b.Fatalf("invalid count: %d", len(result))
}
}
})
b.Run("Reuse slice", func(b *testing.B) {
inputs := make([][]string, b.N)
for i := 0; i < b.N; i++ {
inputc := make([]string, len(input))
copy(inputc, input)
inputs[i] = inputc
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
inputc := inputs[i]
result := UniqueStringsReuse(inputc)
if len(result) != 6 {
b.Fatalf("invalid count: %d", len(result))
}
}
})
b.Run("Reuse slice sorted", func(b *testing.B) {
inputs := make([][]string, b.N)
for i := 0; i < b.N; i++ {
inputc := make([]string, len(input))
copy(inputc, input)
inputs[i] = inputc
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
inputc := inputs[i]
result := UniqueStringsSorted(inputc)
if len(result) != 6 {
b.Fatalf("invalid count: %d", len(result))
}
}
})
}
func BenchmarkGetOrCompileRegexp(b *testing.B) {
for b.Loop() {
for i := 0; i < b.N; i++ {
GetOrCompileRegexp(`\d+`)
}
}
func BenchmarkCompileRegexp(b *testing.B) {
for b.Loop() {
for i := 0; i < b.N; i++ {
regexp.MustCompile(`\d+`)
}
}
-162
View File
@@ -1,162 +0,0 @@
// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hsync
import (
"context"
"sync"
"sync/atomic"
)
// OnceMore is similar to sync.Once.
//
// Additional features are:
// * it can be reset, so the action can be repeated if needed
// * it has methods to check if it's done or in progress
type OnceMore struct {
_ doNotCopy
done atomic.Bool
mu sync.Mutex
}
func (t *OnceMore) Do(f func()) {
if t.Done() {
return
}
t.mu.Lock()
defer t.mu.Unlock()
// Double check
if t.Done() {
return
}
defer t.done.Store(true)
f()
}
func (t *OnceMore) Done() bool {
return t.done.Load()
}
func (t *OnceMore) Reset() {
t.mu.Lock()
t.done.Store(false)
t.mu.Unlock()
}
type ValueResetter[T any] struct {
reset func()
f func(context.Context) T
}
func (v *ValueResetter[T]) Value(ctx context.Context) T {
return v.f(ctx)
}
func (v *ValueResetter[T]) Reset() {
v.reset()
}
// OnceMoreValue returns a function that invokes f only once and returns the value
// returned by f. The returned function may be called concurrently.
//
// If f panics, the returned function will panic with the same value on every call.
func OnceMoreValue[T any](f func(context.Context) T) ValueResetter[T] {
v := struct {
f func(context.Context) T
once OnceMore
ok bool
p any
result T
}{
f: f,
}
ff := func(ctx context.Context) T {
v.once.Do(func() {
v.ok = false
defer func() {
v.p = recover()
if !v.ok {
panic(v.p)
}
}()
v.result = v.f(ctx)
v.ok = true
})
if !v.ok {
panic(v.p)
}
return v.result
}
return ValueResetter[T]{
reset: v.once.Reset,
f: ff,
}
}
type FuncResetter struct {
f func(context.Context) error
reset func()
}
func (v *FuncResetter) Do(ctx context.Context) error {
return v.f(ctx)
}
func (v *FuncResetter) Reset() {
v.reset()
}
func OnceMoreFunc(f func(context.Context) error) FuncResetter {
v := struct {
f func(context.Context) error
once OnceMore
ok bool
err error
p any
}{
f: f,
}
ff := func(ctx context.Context) error {
v.once.Do(func() {
v.ok = false
defer func() {
v.p = recover()
if !v.ok {
panic(v.p)
}
}()
v.err = v.f(ctx)
v.ok = true
})
if !v.ok {
panic(v.p)
}
return v.err
}
return FuncResetter{
f: ff,
reset: v.once.Reset,
}
}
type doNotCopy struct{}
// Lock is a no-op used by -copylocks checker from `go vet`.
func (*doNotCopy) Lock() {}
func (*doNotCopy) Unlock() {}
-80
View File
@@ -1,80 +0,0 @@
// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hsync
import (
"context"
"testing"
qt "github.com/frankban/quicktest"
)
func TestOnceMoreValue(t *testing.T) {
c := qt.New(t)
var counter int
f := func(context.Context) int {
counter++
return counter
}
omf := OnceMoreValue(f)
for range 10 {
c.Assert(omf.Value(context.Background()), qt.Equals, 1)
}
omf.Reset()
for range 10 {
c.Assert(omf.Value(context.Background()), qt.Equals, 2)
}
}
func TestOnceMoreFunc(t *testing.T) {
c := qt.New(t)
var counter int
f := func(context.Context) error {
counter++
return nil
}
omf := OnceMoreFunc(f)
for range 10 {
c.Assert(omf.Do(context.Background()), qt.IsNil)
c.Assert(counter, qt.Equals, 1)
}
omf.Reset()
for range 10 {
c.Assert(omf.Do(context.Background()), qt.IsNil)
c.Assert(counter, qt.Equals, 2)
}
}
func BenchmarkOnceMoreValue(b *testing.B) {
var counter int
f := func(context.Context) int {
counter++
return counter
}
for b.Loop() {
omf := OnceMoreValue(f)
for range 10 {
omf.Value(context.Background())
}
omf.Reset()
for range 10 {
omf.Value(context.Background())
}
}
}
+2 -2
View File
@@ -24,9 +24,9 @@ func TestApplyWithContext(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
-- config.toml --
defaultContentLanguage = 'it'
-- layouts/home.html --
-- layouts/index.html --
{{ $dates := slice
"2022-01-03"
"2022-02-01"
+3 -3
View File
@@ -112,7 +112,7 @@ func BenchmarkTimeFormatter(b *testing.B) {
june06, _ := time.Parse("2006-Jan-02", "2018-Jun-06")
b.Run("Native", func(b *testing.B) {
for b.Loop() {
for i := 0; i < b.N; i++ {
got := june06.Format("Monday Jan 2 2006")
if got != "Wednesday Jun 6 2018" {
b.Fatalf("invalid format, got %q", got)
@@ -123,7 +123,7 @@ func BenchmarkTimeFormatter(b *testing.B) {
b.Run("Localized", func(b *testing.B) {
f := NewTimeFormatter(translators.GetTranslator("nn"))
b.ResetTimer()
for b.Loop() {
for i := 0; i < b.N; i++ {
got := f.Format(june06, "Monday Jan 2 2006")
if got != "onsdag juni 6 2018" {
b.Fatalf("invalid format, got %q", got)
@@ -134,7 +134,7 @@ func BenchmarkTimeFormatter(b *testing.B) {
b.Run("Localized Custom", func(b *testing.B) {
f := NewTimeFormatter(translators.GetTranslator("nn"))
b.ResetTimer()
for b.Loop() {
for i := 0; i < b.N; i++ {
got := f.Format(june06, ":date_medium")
if got != "6. juni 2018" {
b.Fatalf("invalid format, got %q", got)
-7
View File
@@ -32,13 +32,6 @@ type ReadSeekCloser interface {
io.Closer
}
// CloserFunc is an adapter to allow the use of ordinary functions as io.Closers.
type CloserFunc func() error
func (f CloserFunc) Close() error {
return f()
}
// ReadSeekCloserProvider provides a ReadSeekCloser.
type ReadSeekCloserProvider interface {
ReadSeekCloser() (ReadSeekCloser, error)
+9 -54
View File
@@ -28,14 +28,12 @@ import (
"github.com/bep/logg"
"github.com/bep/godartsass/v2"
"github.com/gohugoio/hugo/common/hcontext"
"github.com/gohugoio/hugo/common/hexec"
"github.com/gohugoio/hugo/common/hstore"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/version"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/hugofs/files"
"github.com/bep/helpers/contexthelpers"
"github.com/spf13/afero"
iofs "io/fs"
@@ -56,7 +54,7 @@ var (
vendorInfo string
)
var _ hstore.StoreProvider = (*HugoInfo)(nil)
var _ maps.StoreProvider = (*HugoInfo)(nil)
// HugoInfo contains information about the current Hugo environment
type HugoInfo struct {
@@ -75,14 +73,14 @@ type HugoInfo struct {
conf ConfigProvider
deps []*Dependency
store *hstore.Scratch
store *maps.Scratch
// Context gives access to some of the context scoped variables.
Context Context
}
// Version returns the current version as a comparable version string.
func (i HugoInfo) Version() version.VersionString {
func (i HugoInfo) Version() VersionString {
return CurrentVersion.Version()
}
@@ -121,7 +119,7 @@ func (i HugoInfo) Deps() []*Dependency {
return i.deps
}
func (i HugoInfo) Store() *hstore.Scratch {
func (i HugoInfo) Store() *maps.Scratch {
return i.store
}
@@ -147,7 +145,7 @@ const (
contextKeyMarkupScope contextKey = iota
)
var markupScope = contexthelpers.NewContextDispatcher[string](contextKeyMarkupScope)
var markupScope = hcontext.NewContextDispatcher[string](contextKeyMarkupScope)
type Context struct{}
@@ -198,7 +196,7 @@ func NewInfo(conf ConfigProvider, deps []*Dependency) HugoInfo {
Environment: conf.Environment(),
conf: conf,
deps: deps,
store: hstore.NewScratch(),
store: maps.NewScratch(),
GoVersion: goVersion,
}
}
@@ -453,7 +451,7 @@ func deprecateLevelWithLogger(item, alternative, version string, level logg.Leve
// We want people to run at least the current and previous version without any warnings.
// We want people who don't update Hugo that often to see the warnings and errors before we remove the feature.
func deprecationLogLevelFromVersion(ver string) logg.Level {
from := version.MustParseVersion(ver)
from := MustParseVersion(ver)
to := CurrentVersion
minorDiff := to.Minor - from.Minor
switch {
@@ -467,46 +465,3 @@ func deprecationLogLevelFromVersion(ver string) logg.Level {
return logg.LevelInfo
}
}
// BuildVersionString creates a version string. This is what you see when
// running "hugo version".
func BuildVersionString() string {
// program := "Hugo Static Site Generator"
program := "hugo"
version := "v" + CurrentVersion.String()
bi := getBuildInfo()
if bi == nil {
return version
}
if bi.Revision != "" {
version += "-" + bi.Revision
}
if IsExtended {
version += "+extended"
}
if IsWithdeploy {
version += "+withdeploy"
}
osArch := bi.GoOS + "/" + bi.GoArch
date := bi.RevisionTime
if date == "" {
// Accept vendor-specified build date if .git/ is unavailable.
date = buildDate
}
if date == "" {
date = "unknown"
}
versionString := fmt.Sprintf("%s %s %s BuildDate=%s",
program, version, osArch, date)
if vendorInfo != "" {
versionString += " VendorInfo=" + vendorInfo
}
return versionString
}
+1 -1
View File
@@ -35,7 +35,7 @@ baseURL = 'https://en.example.org/'
---
title: home
---
-- layouts/home.html --
-- layouts/index.html --
multilingual={{ hugo.IsMultilingual }}
multihost={{ hugo.IsMultihost }}
`
+1 -2
View File
@@ -20,7 +20,6 @@ import (
"github.com/bep/logg"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/common/version"
)
func TestHugoInfo(t *testing.T) {
@@ -30,7 +29,7 @@ func TestHugoInfo(t *testing.T) {
hugoInfo := NewInfo(conf, nil)
c.Assert(hugoInfo.Version(), qt.Equals, CurrentVersion.Version())
c.Assert(fmt.Sprintf("%T", version.VersionString("")), qt.Equals, fmt.Sprintf("%T", hugoInfo.Version()))
c.Assert(fmt.Sprintf("%T", VersionString("")), qt.Equals, fmt.Sprintf("%T", hugoInfo.Version()))
c.Assert(hugoInfo.WorkingDir(), qt.Equals, "/mywork")
bi := getBuildInfo()
@@ -1,4 +1,4 @@
// Copyright 2025 The Hugo Authors. All rights reserved.
// Copyright 2018 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package version
package hugo
import (
"fmt"
@@ -44,13 +44,6 @@ var (
_ compare.Comparer = (*VersionString)(nil)
)
// IsAlphaBetaOrRC returns whether this version is an alpha, beta, or release candidate.
func (v Version) IsAlphaBetaOrRC() bool {
s := strings.ToLower(v.Suffix)
// e.g. "alpha.1", "beta.2", "rc.3"
return strings.Contains(s, "alpha.") || strings.Contains(s, "beta.") || strings.Contains(s, "rc.")
}
func (v Version) String() string {
return version(v.Major, v.Minor, v.PatchLevel, v.Suffix)
}
@@ -62,7 +55,7 @@ func (v Version) Version() VersionString {
// Compare implements the compare.Comparer interface.
func (h Version) Compare(other any) int {
return CompareVersions(h, other)
return compareVersions(h, other)
}
// VersionString represents a Hugo version string.
@@ -74,7 +67,7 @@ func (h VersionString) String() string {
// Compare implements the compare.Comparer interface.
func (h VersionString) Compare(other any) int {
return CompareVersions(h.Version(), other)
return compareVersions(h.Version(), other)
}
func (h VersionString) Version() Version {
@@ -90,24 +83,18 @@ func (h VersionString) Eq(other any) bool {
return s == h.String()
}
var versionSuffixes = []string{"-test", "-DEV"}
// ParseVersion parses a version string.
func ParseVersion(s string) (Version, error) {
s = strings.TrimPrefix(strings.TrimSpace(s), "v")
var vv Version
hyphen := strings.Index(s, "-")
if hyphen > 0 {
suffix := s[hyphen:]
if len(suffix) > 1 {
if suffix[0] == '-' {
suffix = suffix[1:]
}
if len(suffix) > 0 {
vv.Suffix = suffix
s = s[:hyphen]
}
for _, suffix := range versionSuffixes {
if strings.HasSuffix(s, suffix) {
vv.Suffix = suffix
s = strings.TrimSuffix(s, suffix)
}
vv.Suffix = suffix
}
vv.Major, vv.Minor, vv.PatchLevel = parseVersion(s)
return vv, nil
@@ -147,55 +134,98 @@ func (v Version) NextPatchLevel(level int) Version {
return prev
}
func version(major, minor, patch int, suffix string) string {
if suffix != "" {
if suffix[0] != '-' {
suffix = "-" + suffix
}
// BuildVersionString creates a version string. This is what you see when
// running "hugo version".
func BuildVersionString() string {
// program := "Hugo Static Site Generator"
program := "hugo"
version := "v" + CurrentVersion.String()
bi := getBuildInfo()
if bi == nil {
return version
}
if bi.Revision != "" {
version += "-" + bi.Revision
}
if IsExtended {
version += "+extended"
}
if IsWithdeploy {
version += "+withdeploy"
}
osArch := bi.GoOS + "/" + bi.GoArch
date := bi.RevisionTime
if date == "" {
// Accept vendor-specified build date if .git/ is unavailable.
date = buildDate
}
if date == "" {
date = "unknown"
}
versionString := fmt.Sprintf("%s %s %s BuildDate=%s",
program, version, osArch, date)
if vendorInfo != "" {
versionString += " VendorInfo=" + vendorInfo
}
return versionString
}
func version(major, minor, patch int, suffix string) string {
if patch > 0 || minor > 53 {
return fmt.Sprintf("%d.%d.%d%s", major, minor, patch, suffix)
}
return fmt.Sprintf("%d.%d%s", major, minor, suffix)
}
// CompareVersion compares v1 with v2.
// It returns -1 if the v2 is less than, 0 if equal and 1 if greater than
// v1.
func CompareVersions(v1 Version, v2 any) int {
// CompareVersion compares the given version string or number against the
// running Hugo version.
// It returns -1 if the given version is less than, 0 if equal and 1 if greater than
// the running version.
func CompareVersion(version any) int {
return compareVersions(CurrentVersion, version)
}
func compareVersions(inVersion Version, in any) int {
var c int
switch d := v2.(type) {
switch d := in.(type) {
case float64:
c = compareFloatWithVersion(d, v1)
c = compareFloatWithVersion(d, inVersion)
case float32:
c = compareFloatWithVersion(float64(d), v1)
c = compareFloatWithVersion(float64(d), inVersion)
case int:
c = compareFloatWithVersion(float64(d), v1)
c = compareFloatWithVersion(float64(d), inVersion)
case int32:
c = compareFloatWithVersion(float64(d), v1)
c = compareFloatWithVersion(float64(d), inVersion)
case int64:
c = compareFloatWithVersion(float64(d), v1)
c = compareFloatWithVersion(float64(d), inVersion)
case Version:
if d.Major == v1.Major && d.Minor == v1.Minor && d.PatchLevel == v1.PatchLevel {
return strings.Compare(v1.Suffix, d.Suffix)
if d.Major == inVersion.Major && d.Minor == inVersion.Minor && d.PatchLevel == inVersion.PatchLevel {
return strings.Compare(inVersion.Suffix, d.Suffix)
}
if d.Major > v1.Major {
if d.Major > inVersion.Major {
return 1
} else if d.Major < v1.Major {
} else if d.Major < inVersion.Major {
return -1
}
if d.Minor > v1.Minor {
if d.Minor > inVersion.Minor {
return 1
} else if d.Minor < v1.Minor {
} else if d.Minor < inVersion.Minor {
return -1
}
if d.PatchLevel > v1.PatchLevel {
if d.PatchLevel > inVersion.PatchLevel {
return 1
} else if d.PatchLevel < v1.PatchLevel {
} else if d.PatchLevel < inVersion.PatchLevel {
return -1
}
default:
s, err := cast.ToStringE(v2)
s, err := cast.ToStringE(in)
if err != nil {
return -1
}
@@ -204,7 +234,7 @@ func CompareVersions(v1 Version, v2 any) int {
if err != nil {
return -1
}
return v1.Compare(v)
return inVersion.Compare(v)
}
+3 -5
View File
@@ -1,4 +1,4 @@
// Copyright 2025 The Hugo Authors. All rights reserved.
// Copyright 2018 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -13,13 +13,11 @@
package hugo
import "github.com/gohugoio/hugo/common/version"
// CurrentVersion represents the current build version.
// This should be the only one.
var CurrentVersion = version.Version{
var CurrentVersion = Version{
Major: 0,
Minor: 153,
Minor: 149,
PatchLevel: 0,
Suffix: "-DEV",
}
@@ -1,4 +1,4 @@
// Copyright 2025 The Hugo Authors. All rights reserved.
// Copyright 2015 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package version
package hugo
import (
"testing"
@@ -50,31 +50,30 @@ func TestHugoVersion(t *testing.T) {
func TestCompareVersions(t *testing.T) {
c := qt.New(t)
c.Assert(CompareVersions(MustParseVersion("0.20.0"), 0.20), qt.Equals, 0)
c.Assert(CompareVersions(MustParseVersion("0.20.0"), float32(0.20)), qt.Equals, 0)
c.Assert(CompareVersions(MustParseVersion("0.20.0"), float64(0.20)), qt.Equals, 0)
c.Assert(CompareVersions(MustParseVersion("0.19.1"), 0.20), qt.Equals, 1)
c.Assert(CompareVersions(MustParseVersion("0.19.3"), "0.20.2"), qt.Equals, 1)
c.Assert(CompareVersions(MustParseVersion("0.1"), 3), qt.Equals, 1)
c.Assert(CompareVersions(MustParseVersion("0.1"), int32(3)), qt.Equals, 1)
c.Assert(CompareVersions(MustParseVersion("0.1"), int64(3)), qt.Equals, 1)
c.Assert(CompareVersions(MustParseVersion("0.20"), "0.20"), qt.Equals, 0)
c.Assert(CompareVersions(MustParseVersion("0.20.1"), "0.20.1"), qt.Equals, 0)
c.Assert(CompareVersions(MustParseVersion("0.20.1"), "0.20"), qt.Equals, -1)
c.Assert(CompareVersions(MustParseVersion("0.20.0"), "0.20.1"), qt.Equals, 1)
c.Assert(CompareVersions(MustParseVersion("0.20.1"), "0.20.2"), qt.Equals, 1)
c.Assert(CompareVersions(MustParseVersion("0.21.1"), "0.22.1"), qt.Equals, 1)
c.Assert(CompareVersions(MustParseVersion("0.22.0"), "0.22-DEV"), qt.Equals, -1)
c.Assert(CompareVersions(MustParseVersion("0.22.0"), "0.22.1-DEV"), qt.Equals, 1)
c.Assert(CompareVersions(MustParseVersion("0.22.0-DEV"), "0.22"), qt.Equals, 1)
c.Assert(CompareVersions(MustParseVersion("0.22.1-DEV"), "0.22"), qt.Equals, -1)
c.Assert(CompareVersions(MustParseVersion("0.22.1-DEV"), "0.22.1-DEV"), qt.Equals, 0)
c.Assert(compareVersions(MustParseVersion("0.20.0"), 0.20), qt.Equals, 0)
c.Assert(compareVersions(MustParseVersion("0.20.0"), float32(0.20)), qt.Equals, 0)
c.Assert(compareVersions(MustParseVersion("0.20.0"), float64(0.20)), qt.Equals, 0)
c.Assert(compareVersions(MustParseVersion("0.19.1"), 0.20), qt.Equals, 1)
c.Assert(compareVersions(MustParseVersion("0.19.3"), "0.20.2"), qt.Equals, 1)
c.Assert(compareVersions(MustParseVersion("0.1"), 3), qt.Equals, 1)
c.Assert(compareVersions(MustParseVersion("0.1"), int32(3)), qt.Equals, 1)
c.Assert(compareVersions(MustParseVersion("0.1"), int64(3)), qt.Equals, 1)
c.Assert(compareVersions(MustParseVersion("0.20"), "0.20"), qt.Equals, 0)
c.Assert(compareVersions(MustParseVersion("0.20.1"), "0.20.1"), qt.Equals, 0)
c.Assert(compareVersions(MustParseVersion("0.20.1"), "0.20"), qt.Equals, -1)
c.Assert(compareVersions(MustParseVersion("0.20.0"), "0.20.1"), qt.Equals, 1)
c.Assert(compareVersions(MustParseVersion("0.20.1"), "0.20.2"), qt.Equals, 1)
c.Assert(compareVersions(MustParseVersion("0.21.1"), "0.22.1"), qt.Equals, 1)
c.Assert(compareVersions(MustParseVersion("0.22.0"), "0.22-DEV"), qt.Equals, -1)
c.Assert(compareVersions(MustParseVersion("0.22.0"), "0.22.1-DEV"), qt.Equals, 1)
c.Assert(compareVersions(MustParseVersion("0.22.0-DEV"), "0.22"), qt.Equals, 1)
c.Assert(compareVersions(MustParseVersion("0.22.1-DEV"), "0.22"), qt.Equals, -1)
c.Assert(compareVersions(MustParseVersion("0.22.1-DEV"), "0.22.1-DEV"), qt.Equals, 0)
}
func TestParseHugoVersion(t *testing.T) {
c := qt.New(t)
c.Assert(MustParseVersion("v2.3.2").String(), qt.Equals, "2.3.2")
c.Assert(MustParseVersion("0.25").String(), qt.Equals, "0.25")
c.Assert(MustParseVersion("0.25.2").String(), qt.Equals, "0.25.2")
c.Assert(MustParseVersion("0.25-test").String(), qt.Equals, "0.25-test")
+2 -25
View File
@@ -20,27 +20,13 @@ import (
// Cache is a simple thread safe cache backed by a map.
type Cache[K comparable, T any] struct {
m map[K]T
opts CacheOptions
hasBeenInitialized bool
sync.RWMutex
}
// CacheOptions are the options for the Cache.
type CacheOptions struct {
// If set, the cache will not grow beyond this size.
Size uint64
}
var defaultCacheOptions = CacheOptions{}
// NewCache creates a new Cache with default options.
// NewCache creates a new Cache.
func NewCache[K comparable, T any]() *Cache[K, T] {
return &Cache[K, T]{m: make(map[K]T), opts: defaultCacheOptions}
}
// NewCacheWithOptions creates a new Cache with the given options.
func NewCacheWithOptions[K comparable, T any](opts CacheOptions) *Cache[K, T] {
return &Cache[K, T]{m: make(map[K]T), opts: opts}
return &Cache[K, T]{m: make(map[K]T)}
}
// Delete deletes the given key from the cache.
@@ -79,7 +65,6 @@ func (c *Cache[K, T]) GetOrCreate(key K, create func() (T, error)) (T, error) {
if err != nil {
return v, err
}
c.clearIfNeeded()
c.m[key] = v
return v, nil
}
@@ -142,15 +127,7 @@ func (c *Cache[K, T]) SetIfAbsent(key K, value T) {
}
}
func (c *Cache[K, T]) clearIfNeeded() {
if c.opts.Size > 0 && uint64(len(c.m)) >= c.opts.Size {
// clear the map
clear(c.m)
}
}
func (c *Cache[K, T]) set(key K, value T) {
c.clearIfNeeded()
c.m[key] = value
}
-57
View File
@@ -1,57 +0,0 @@
// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package maps
import (
"testing"
qt "github.com/frankban/quicktest"
)
func TestCacheSize(t *testing.T) {
c := qt.New(t)
cache := NewCacheWithOptions[string, string](CacheOptions{Size: 10})
for i := range 30 {
cache.Set(string(rune('a'+i)), "value")
}
c.Assert(len(cache.m), qt.Equals, 10)
for i := 20; i < 50; i++ {
cache.GetOrCreate(string(rune('a'+i)), func() (string, error) {
return "value", nil
})
}
c.Assert(len(cache.m), qt.Equals, 10)
for i := 100; i < 200; i++ {
cache.SetIfAbsent(string(rune('a'+i)), "value")
}
c.Assert(len(cache.m), qt.Equals, 10)
cache.InitAndGet("foo", func(
get func(key string) (string, bool), set func(key string, value string),
) error {
for i := 50; i < 100; i++ {
set(string(rune('a'+i)), "value")
}
return nil
})
c.Assert(len(cache.m), qt.Equals, 10)
}
-117
View File
@@ -1,117 +0,0 @@
// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package maps
import (
"iter"
"sync"
)
func NewMap[K comparable, T any]() *Map[K, T] {
return &Map[K, T]{
m: make(map[K]T),
}
}
// Map is a thread safe map backed by a Go map.
type Map[K comparable, T any] struct {
m map[K]T
mu sync.RWMutex
}
// Get gets the value for the given key.
// It returns the zero value of T if the key is not found.
func (m *Map[K, T]) Get(key K) T {
v, _ := m.Lookup(key)
return v
}
// Lookup looks up the given key in the map.
// It returns the value and a boolean indicating whether the key was found.
func (m *Map[K, T]) Lookup(key K) (T, bool) {
m.mu.RLock()
v, found := m.m[key]
m.mu.RUnlock()
return v, found
}
// GetOrCreate gets the value for the given key if it exists, or creates it if not.
func (m *Map[K, T]) GetOrCreate(key K, create func() (T, error)) (T, error) {
v, found := m.Lookup(key)
if found {
return v, nil
}
m.mu.Lock()
defer m.mu.Unlock()
v, found = m.m[key]
if found {
return v, nil
}
v, err := create()
if err != nil {
return v, err
}
m.m[key] = v
return v, nil
}
// Set sets the given key to the given value.
func (m *Map[K, T]) Set(key K, value T) {
m.mu.Lock()
m.m[key] = value
m.mu.Unlock()
}
// WithWriteLock executes the given function with a write lock on the map.
func (m *Map[K, T]) WithWriteLock(f func(m map[K]T)) {
m.mu.Lock()
defer m.mu.Unlock()
f(m.m)
}
// SetIfAbsent sets the given key to the given value if the key does not already exist in the map.
// It returns true if the value was set, false otherwise.
func (m *Map[K, T]) SetIfAbsent(key K, value T) bool {
m.mu.RLock()
if _, found := m.m[key]; !found {
m.mu.RUnlock()
return m.doSetIfAbsent(key, value)
}
m.mu.RUnlock()
return false
}
func (m *Map[K, T]) doSetIfAbsent(key K, value T) bool {
m.mu.Lock()
defer m.mu.Unlock()
if _, found := m.m[key]; !found {
m.m[key] = value
return true
}
return false
}
// All returns an iterator over all key/value pairs in the map.
// A read lock is held during the iteration.
func (m *Map[K, T]) All() iter.Seq2[K, T] {
return func(yield func(K, T) bool) {
m.mu.RLock()
defer m.mu.RUnlock()
for k, v := range m.m {
if !yield(k, v) {
return
}
}
}
}
-71
View File
@@ -1,71 +0,0 @@
// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package maps
import (
"testing"
qt "github.com/frankban/quicktest"
)
func TestMap(t *testing.T) {
c := qt.New(t)
m := NewMap[string, int]()
m.Set("b", 42)
v, found := m.Lookup("b")
c.Assert(found, qt.Equals, true)
c.Assert(v, qt.Equals, 42)
v = m.Get("b")
c.Assert(v, qt.Equals, 42)
v, found = m.Lookup("c")
c.Assert(found, qt.Equals, false)
c.Assert(v, qt.Equals, 0)
v = m.Get("c")
c.Assert(v, qt.Equals, 0)
v, err := m.GetOrCreate("d", func() (int, error) {
return 100, nil
})
c.Assert(err, qt.IsNil)
c.Assert(v, qt.Equals, 100)
v, found = m.Lookup("d")
c.Assert(found, qt.Equals, true)
c.Assert(v, qt.Equals, 100)
v, err = m.GetOrCreate("d", func() (int, error) {
return 200, nil
})
c.Assert(err, qt.IsNil)
c.Assert(v, qt.Equals, 100)
wasSet := m.SetIfAbsent("e", 300)
c.Assert(wasSet, qt.Equals, true)
v, found = m.Lookup("e")
c.Assert(found, qt.Equals, true)
c.Assert(v, qt.Equals, 300)
wasSet = m.SetIfAbsent("e", 400)
c.Assert(wasSet, qt.Equals, false)
v, found = m.Lookup("e")
c.Assert(found, qt.Equals, true)
c.Assert(v, qt.Equals, 300)
m.WithWriteLock(func(m map[string]int) {
m["f"] = 500
})
v, found = m.Lookup("f")
c.Assert(found, qt.Equals, true)
c.Assert(v, qt.Equals, 500)
}
-2
View File
@@ -98,8 +98,6 @@ func ToSliceStringMap(in any) ([]map[string]any, error) {
return v, nil
case Params:
return []map[string]any{v}, nil
case map[string]any:
return []map[string]any{v}, nil
case []any:
var s []map[string]any
for _, entry := range v {
-154
View File
@@ -1,154 +0,0 @@
// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package maps
import (
"fmt"
"slices"
"github.com/bits-and-blooms/bitset"
)
type OrderedIntSet struct {
keys []int
values *bitset.BitSet
}
// NewOrderedIntSet creates a new OrderedIntSet.
// Note that this is backed by https://github.com/bits-and-blooms/bitset
func NewOrderedIntSet(vals ...int) *OrderedIntSet {
m := &OrderedIntSet{
keys: make([]int, 0, len(vals)),
values: bitset.New(uint(len(vals))),
}
for _, v := range vals {
m.Set(v)
}
return m
}
// Set sets the value for the given key.
// Note that insertion order is not affected if a key is re-inserted into the set.
func (m *OrderedIntSet) Set(key int) {
if m == nil {
panic("nil OrderedIntSet")
}
keyu := uint(key)
if m.values.Test(keyu) {
return
}
m.values.Set(keyu)
m.keys = append(m.keys, key)
}
// SetFrom sets the values from another OrderedIntSet.
func (m *OrderedIntSet) SetFrom(other *OrderedIntSet) {
if m == nil || other == nil {
return
}
for _, key := range other.keys {
m.Set(key)
}
}
func (m *OrderedIntSet) Clone() *OrderedIntSet {
if m == nil {
return nil
}
newSet := &OrderedIntSet{
keys: slices.Clone(m.keys),
values: m.values.Clone(),
}
return newSet
}
// Next returns the next key in the set possibly including the given key.
// It returns -1 if the key is not found or if there are no keys greater than the given key.
func (m *OrderedIntSet) Next(i int) int {
n, ok := m.values.NextSet(uint(i))
if !ok {
return -1
}
return int(n)
}
// The reason we don't use iter.Seq is https://github.com/golang/go/issues/69015
// This is 70% faster than using iter.Seq2[int, int] for the keys.
// It returns false if the iteration was stopped early.
func (m *OrderedIntSet) ForEachKey(yield func(int) bool) bool {
if m == nil {
return true
}
for _, key := range m.keys {
if !yield(key) {
return false
}
}
return true
}
func (m *OrderedIntSet) Has(key int) bool {
if m == nil {
return false
}
return m.values.Test(uint(key))
}
func (m *OrderedIntSet) Len() int {
if m == nil {
return 0
}
return len(m.keys)
}
// KeysSorted returns the keys in sorted order.
func (m *OrderedIntSet) KeysSorted() []int {
if m == nil {
return nil
}
keys := slices.Clone(m.keys)
slices.Sort(keys)
return m.keys
}
func (m *OrderedIntSet) String() string {
if m == nil {
return "[]"
}
return fmt.Sprintf("%v", m.keys)
}
func (m *OrderedIntSet) Values() *bitset.BitSet {
if m == nil {
return nil
}
return m.values
}
func (m *OrderedIntSet) IsSuperSet(other *OrderedIntSet) bool {
if m == nil || other == nil {
return false
}
return m.values.IsSuperSet(other.values)
}
// Words returns the bitset as array of 64-bit words, giving direct access to the internal representation.
// It is not a copy, so changes to the returned slice will affect the bitset.
// It is meant for advanced users.
func (m *OrderedIntSet) Words() []uint64 {
if m == nil {
return nil
}
return m.values.Words()
}
-117
View File
@@ -1,117 +0,0 @@
// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package maps
import (
"testing"
qt "github.com/frankban/quicktest"
)
func TestOrderedIntSet(t *testing.T) {
c := qt.New(t)
m := NewOrderedIntSet(2, 1, 3, 7)
c.Assert(m.Len(), qt.Equals, 4)
c.Assert(m.Has(1), qt.Equals, true)
c.Assert(m.Has(4), qt.Equals, false)
c.Assert(m.String(), qt.Equals, "[2 1 3 7]")
m.Set(4)
c.Assert(m.Len(), qt.Equals, 5)
c.Assert(m.Has(4), qt.Equals, true)
c.Assert(m.Next(0), qt.Equals, 1)
c.Assert(m.Next(1), qt.Equals, 1)
c.Assert(m.Next(2), qt.Equals, 2)
c.Assert(m.Next(3), qt.Equals, 3)
c.Assert(m.Next(4), qt.Equals, 4)
c.Assert(m.Next(7), qt.Equals, 7)
c.Assert(m.Next(8), qt.Equals, -1)
c.Assert(m.String(), qt.Equals, "[2 1 3 7 4]")
var nilset *OrderedIntSet
c.Assert(nilset.Len(), qt.Equals, 0)
c.Assert(nilset.Has(1), qt.Equals, false)
c.Assert(nilset.String(), qt.Equals, "[]")
var collected []int
m.ForEachKey(func(key int) bool {
collected = append(collected, key)
return true
})
c.Assert(collected, qt.DeepEquals, []int{2, 1, 3, 7, 4})
}
func BenchmarkOrderedIntSet(b *testing.B) {
smallSet := NewOrderedIntSet()
for i := range 8 {
smallSet.Set(i)
}
mediumSet := NewOrderedIntSet()
for i := range 64 {
mediumSet.Set(i)
}
largeSet := NewOrderedIntSet()
for i := range 1024 {
largeSet.Set(i)
}
b.Run("New", func(b *testing.B) {
for b.Loop() {
NewOrderedIntSet(1, 2, 3, 4, 5, 6, 7, 8)
}
})
b.Run("Has small", func(b *testing.B) {
for i := 0; b.Loop(); i++ {
smallSet.Has(i % 32)
}
})
b.Run("Has medium", func(b *testing.B) {
for i := 0; b.Loop(); i++ {
mediumSet.Has(i % 32)
}
})
b.Run("Next", func(b *testing.B) {
for i := 0; b.Loop(); i++ {
mediumSet.Next(i % 32)
}
})
b.Run("ForEachKey small", func(b *testing.B) {
for b.Loop() {
smallSet.ForEachKey(func(key int) bool {
return true
})
}
})
b.Run("ForEachKey medium", func(b *testing.B) {
for b.Loop() {
mediumSet.ForEachKey(func(key int) bool {
return true
})
}
})
b.Run("ForEachKey large", func(b *testing.B) {
for b.Loop() {
largeSet.ForEachKey(func(key int) bool {
return true
})
}
})
}
+4 -93
View File
@@ -14,7 +14,6 @@
package maps
import (
"errors"
"fmt"
"strings"
@@ -41,14 +40,6 @@ func (p Params) GetNested(indices ...string) any {
// SetParams overwrites values in dst with values in src for common or new keys.
// This is done recursively.
func SetParams(dst, src Params) {
setParams(dst, src, 0)
}
func setParams(dst, src Params, depth int) {
const maxDepth = 1000
if depth > maxDepth {
panic(errors.New("max depth exceeded"))
}
for k, v := range src {
vv, found := dst[k]
if !found {
@@ -57,7 +48,7 @@ func setParams(dst, src Params, depth int) {
switch vvv := vv.(type) {
case Params:
if pv, ok := v.(Params); ok {
setParams(vvv, pv, depth+1)
SetParams(vvv, pv)
} else {
dst[k] = v
}
@@ -110,6 +101,7 @@ func (p Params) merge(ps ParamsMergeStrategy, pp Params) {
noUpdate = noUpdate || (ps != "" && ps == ParamsMergeStrategyShallow)
for k, v := range pp {
if k == MergeStrategyKey {
continue
}
@@ -187,56 +179,6 @@ func getNested(m map[string]any, indices []string) (any, string, map[string]any)
}
}
// CreateNestedParamsFromSegements creates empty nested maps for the given keySegments in the target map.
func CreateNestedParamsFromSegements(target Params, keySegments ...string) Params {
if len(keySegments) == 0 {
return target
}
m := target
for i, key := range keySegments {
v, found := m[key]
if !found {
nm := Params{}
m[key] = nm
m = nm
if i == len(keySegments)-1 {
return nm
}
continue
}
m = v.(Params)
}
return m
}
// CreateNestedParamsSepString creates empty nested maps for the given keyStr in the target map
// It returns the last map created.
func CreateNestedParamsSepString(keyStr, separator string, target Params) Params {
keySegments := strings.Split(keyStr, separator)
return CreateNestedParamsFromSegements(target, keySegments...)
}
// SetNestedParamIfNotSet sets the value for the given keyStr in the target map if it does not exist.
// It assumes that all but the last key in keyStr is a Params map or should be one.
func SetNestedParamIfNotSet(keyStr, separator string, value any, target Params) Params {
keySegments := strings.Split(keyStr, separator)
if len(keySegments) == 0 {
return target
}
base := keySegments[:len(keySegments)-1]
last := keySegments[len(keySegments)-1]
m := CreateNestedParamsFromSegements(target, base...)
if _, ok := m[last]; !ok {
m[last] = value
}
return target
}
// GetNestedParam gets the first match of the keyStr in the candidates given.
// It will first try the exact match and then try to find it as a nested map value,
// using the given separator, e.g. "mymap.name".
@@ -324,17 +266,9 @@ func CleanConfigStringMapString(m map[string]string) map[string]string {
// CleanConfigStringMap is the same as CleanConfigStringMapString but for
// map[string]any.
func CleanConfigStringMap(m map[string]any) map[string]any {
return doCleanConfigStringMap(m, 0)
}
func doCleanConfigStringMap(m map[string]any, depth int) map[string]any {
if len(m) == 0 {
return m
}
const maxDepth = 1000
if depth > maxDepth {
panic(errors.New("max depth exceeded"))
}
if _, found := m[MergeStrategyKey]; !found {
return m
}
@@ -346,9 +280,9 @@ func doCleanConfigStringMap(m map[string]any, depth int) map[string]any {
}
switch v2 := v.(type) {
case map[string]any:
m2[k] = doCleanConfigStringMap(v2, depth+1)
m2[k] = CleanConfigStringMap(v2)
case Params:
var p Params = doCleanConfigStringMap(v2, depth+1)
var p Params = CleanConfigStringMap(v2)
m2[k] = p
case map[string]string:
m2[k] = CleanConfigStringMapString(v2)
@@ -410,29 +344,6 @@ func PrepareParams(m Params) {
}
}
// CloneParamsDeep does a deep clone of the given Params,
// meaning that any nested Params will be cloned as well.
func CloneParamsDeep(m Params) Params {
return cloneParamsDeep(m, 0)
}
func cloneParamsDeep(m Params, depth int) Params {
const maxDepth = 1000
if depth > maxDepth {
panic(errors.New("max depth exceeded"))
}
m2 := make(Params)
for k, v := range m {
switch vv := v.(type) {
case Params:
m2[k] = cloneParamsDeep(vv, depth+1)
default:
m2[k] = v
}
}
return m2
}
// PrepareParamsClone is like PrepareParams, but it does not modify the input.
func PrepareParamsClone(m Params) Params {
m2 := make(Params)
-32
View File
@@ -167,35 +167,3 @@ func TestParamsIsZero(t *testing.T) {
c.Assert(Params{"_merge": "foo", "foo": "bar"}.IsZero(), qt.IsFalse)
c.Assert(Params{"_merge": "foo"}.IsZero(), qt.IsTrue)
}
func TestSetNestedParamIfNotSet(t *testing.T) {
c := qt.New(t)
m := Params{}
SetNestedParamIfNotSet("a.b.c", ".", "value", m)
c.Assert(m, qt.DeepEquals, Params{
"a": Params{
"b": Params{
"c": "value",
},
},
})
m = Params{
"a": Params{
"b": Params{
"c": "existingValue",
},
},
}
SetNestedParamIfNotSet("a.b.c", ".", "value", m)
c.Assert(m, qt.DeepEquals, Params{
"a": Params{
"b": Params{
"c": "existingValue",
},
},
})
}
@@ -1,4 +1,4 @@
// Copyright 2025 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package hstore
package maps
import (
"reflect"
@@ -1,4 +1,4 @@
// Copyright 2025 The Hugo Authors. All rights reserved.
// Copyright 2018 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package hstore
package maps
import (
"reflect"
@@ -214,8 +214,8 @@ func TestScratchGetSortedMapValues(t *testing.T) {
func BenchmarkScratchGet(b *testing.B) {
scratch := NewScratch()
scratch.Add("A", 1)
for b.Loop() {
b.ResetTimer()
for i := 0; i < b.N; i++ {
scratch.Get("A")
}
}
+1
View File
@@ -52,6 +52,7 @@ func TestPara(t *testing.T) {
var result []int
var mu sync.Mutex
for i := range n {
i := i
r.Run(func() error {
mu.Lock()
defer mu.Unlock()
+2 -2
View File
@@ -115,8 +115,8 @@ func ReplaceExtension(path string, newExt string) string {
func makePathRelative(inPath string, possibleDirectories ...string) (string, error) {
for _, currentPath := range possibleDirectories {
if after, ok := strings.CutPrefix(inPath, currentPath); ok {
return after, nil
if strings.HasPrefix(inPath, currentPath) {
return strings.TrimPrefix(inPath, currentPath), nil
}
}
return inPath, errors.New("can't extract relative path, unknown prefix")
+2 -60
View File
@@ -225,7 +225,7 @@ func BenchmarkSanitize(b *testing.B) {
// This should not allocate any memory.
b.Run("All allowed", func(b *testing.B) {
for b.Loop() {
for i := 0; i < b.N; i++ {
got := Sanitize(allAlowedPath)
if got != allAlowedPath {
b.Fatal(got)
@@ -235,7 +235,7 @@ func BenchmarkSanitize(b *testing.B) {
// This will allocate some memory.
b.Run("Spaces", func(b *testing.B) {
for b.Loop() {
for i := 0; i < b.N; i++ {
got := Sanitize(spacePath)
if got != "foo-bar" {
b.Fatal(got)
@@ -311,61 +311,3 @@ func TestIsSameFilePath(t *testing.T) {
c.Assert(IsSameFilePath(filepath.FromSlash(this.a), filepath.FromSlash(this.b)), qt.Equals, this.expected, qt.Commentf("a: %s b: %s", this.a, this.b))
}
}
func BenchmarkAddLeadingSlash(b *testing.B) {
const (
noLeadingSlash = "a/b/c"
withLeadingSlash = "/a/b/c"
)
// This should not allocate any memory.
b.Run("With leading slash", func(b *testing.B) {
for b.Loop() {
got := AddLeadingSlash(withLeadingSlash)
if got != withLeadingSlash {
b.Fatal(got)
}
}
})
// This will allocate some memory.
b.Run("Without leading slash", func(b *testing.B) {
for b.Loop() {
got := AddLeadingSlash(noLeadingSlash)
if got != "/a/b/c" {
b.Fatal(got)
}
}
})
b.Run("Blank string", func(b *testing.B) {
for b.Loop() {
got := AddLeadingSlash("")
if got != "/" {
b.Fatal(got)
}
}
})
}
func TestPathEscape(t *testing.T) {
c := qt.New(t)
for _, this := range []struct {
input string
expected string
}{
{"/tags/欢迎", "/tags/%E6%AC%A2%E8%BF%8E"},
{"/path with spaces", "/path%20with%20spaces"},
{"/simple-path", "/simple-path"},
{"/path/with/slash", "/path/with/slash"},
{"/path/with special&chars", "/path/with%20special&chars"},
} {
in := this.input
for range 2 {
result := PathEscape(in)
c.Assert(result, qt.Equals, this.expected, qt.Commentf("input: %q", this.input))
in = result // test idempotency
}
}
}
+7 -86
View File
@@ -14,27 +14,23 @@
package paths
import (
"fmt"
"path"
"path/filepath"
"runtime"
"strings"
"sync"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/hugolib/sitesmatrix"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/resources/kinds"
)
const (
identifierBaseof = "baseof"
identifierCurstomWrapper = "_"
identifierBaseof = "baseof"
)
// PathParser parses and manages paths.
// PathParser parses a path into a Path.
type PathParser struct {
// Maps the language code to its index in the languages/sites slice.
LanguageIndex map[string]int
@@ -48,19 +44,6 @@ type PathParser struct {
// Reports whether the given ext is a content file.
IsContentExt func(string) bool
// The configured sites matrix.
ConfiguredDimensions *sitesmatrix.ConfiguredDimensions
// Below gets created on demand.
initOnce sync.Once
sitesMatrixCache *maps.Cache[string, sitesmatrix.VectorStore] // Maps language index to sites matrix vector store.
}
func (pp *PathParser) init() {
pp.initOnce.Do(func() {
pp.sitesMatrixCache = maps.NewCache[string, sitesmatrix.VectorStore]()
})
}
// NormalizePathString returns a normalized path string using the very basic Hugo rules.
@@ -74,32 +57,6 @@ func NormalizePathStringBasic(s string) string {
return s
}
func (pp *PathParser) SitesMatrixFromPath(p *Path) sitesmatrix.VectorStore {
pp.init()
lang := p.Lang()
v, _ := pp.sitesMatrixCache.GetOrCreate(lang, func() (sitesmatrix.VectorStore, error) {
builder := sitesmatrix.NewIntSetsBuilder(pp.ConfiguredDimensions)
if lang != "" {
if idx, ok := pp.LanguageIndex[lang]; ok {
builder.WithLanguageIndices(idx)
}
}
switch p.Component() {
case files.ComponentFolderContent:
builder.WithDefaultsIfNotSet()
case files.ComponentFolderLayouts:
builder.WithAllIfNotSet()
case files.ComponentFolderStatic:
builder.WithDefaultsAndAllLanguagesIfNotSet()
}
return builder.Build(), nil
})
return v
}
// ParseIdentity parses component c with path s into a StringIdentity.
func (pp *PathParser) ParseIdentity(c, s string) identity.StringIdentity {
p := pp.parsePooled(c, s)
@@ -188,12 +145,6 @@ func (pp *PathParser) parseIdentifier(component, s string, p *Path, i, lastDot,
id := types.LowHigh[string]{Low: i + 1, High: high}
sid := p.s[id.Low:id.High]
if strings.HasPrefix(sid, identifierCurstomWrapper) && strings.HasSuffix(sid, identifierCurstomWrapper) {
p.identifiersKnown = append(p.identifiersKnown, id)
p.posIdentifierCustom = len(p.identifiersKnown) - 1
found = true
}
if len(p.identifiersKnown) == 0 {
// The first is always the extension.
p.identifiersKnown = append(p.identifiersKnown, id)
@@ -204,12 +155,12 @@ func (pp *PathParser) parseIdentifier(component, s string, p *Path, i, lastDot,
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 {
@@ -222,7 +173,6 @@ func (pp *PathParser) parseIdentifier(component, s string, p *Path, i, lastDot,
p.identifiersKnown = append(p.identifiersKnown, id)
p.posIdentifierLanguage = len(p.identifiersKnown) - 1
}
}
if !found && mayHaveOutputFormat {
@@ -252,14 +202,8 @@ func (pp *PathParser) parseIdentifier(component, s string, p *Path, i, lastDot,
}
if !found && mayHaveLayout {
if p.posIdentifierLayout != -1 {
// Move it to identifiersUnknown.
p.identifiersUnknown = append(p.identifiersUnknown, p.identifiersKnown[p.posIdentifierLayout])
p.identifiersKnown[p.posIdentifierLayout] = id
} else {
p.identifiersKnown = append(p.identifiersKnown, id)
p.posIdentifierLayout = len(p.identifiersKnown) - 1
}
p.identifiersKnown = append(p.identifiersKnown, id)
p.posIdentifierLayout = len(p.identifiersKnown) - 1
found = true
}
@@ -389,7 +333,7 @@ type Type int
const (
// A generic file, e.g. a JSON file.
// A generic resource, e.g. a JSON file.
TypeFile Type = iota
// All below are content files.
@@ -436,7 +380,6 @@ type Path struct {
posIdentifierKind int
posIdentifierLayout int
posIdentifierBaseof int
posIdentifierCustom int
disabled bool
trimLeadingSlash bool
@@ -474,7 +417,6 @@ func (p *Path) reset() {
p.posIdentifierKind = -1
p.posIdentifierLayout = -1
p.posIdentifierBaseof = -1
p.posIdentifierCustom = -1
p.disabled = false
p.trimLeadingSlash = false
p.unnormalized = nil
@@ -638,9 +580,6 @@ func (p *Path) Unnormalized() *Path {
// PathNoLang returns the Path but with any language identifier removed.
func (p *Path) PathNoLang() string {
if p.identifierIndex(p.posIdentifierLanguage) == -1 {
return p.Path()
}
return p.base(true, false)
}
@@ -694,12 +633,7 @@ func (p *Path) BaseRel(owner *Path) string {
//
// For other files (Resources), any extension is kept.
func (p *Path) Base() string {
s := p.base(!p.isContentPage(), p.IsBundle())
if s == "/" && p.isContentPage() {
// The content home page is represented as "".
s = ""
}
return s
return p.base(!p.isContentPage(), p.IsBundle())
}
// Used in template lookups.
@@ -774,10 +708,6 @@ func (p *Path) Lang() string {
return p.identifierAsString(p.posIdentifierLanguage)
}
func (p *Path) Custom() string {
return strings.TrimSuffix(strings.TrimPrefix(p.identifierAsString(p.posIdentifierCustom), identifierCurstomWrapper), identifierCurstomWrapper)
}
func (p *Path) Identifier(i int) string {
return p.identifierAsString(i)
}
@@ -856,12 +786,3 @@ func HasExt(p string) bool {
}
return false
}
// ValidateIdentifier returns true if the given string is a valid identifier according
// to Hugo's basic path normalization rules.
func ValidateIdentifier(s string) error {
if s == NormalizePathStringBasic(s) {
return nil
}
return fmt.Errorf("must be all lower case and no spaces")
}
+30 -81
View File
@@ -18,33 +18,27 @@ import (
"testing"
"github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/hugolib/sitesmatrix"
"github.com/gohugoio/hugo/resources/kinds"
qt "github.com/frankban/quicktest"
)
func newTestParser() *PathParser {
dims := sitesmatrix.NewTestingDimensions([]string{"en", "no", "fr"}, []string{"v1", "v2", "v3"}, []string{"admin", "editor", "viewer", "guest"})
return &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
},
ConfiguredDimensions: dims,
}
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) {
@@ -196,7 +190,7 @@ func TestParse(t *testing.T) {
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.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, "")
@@ -272,7 +266,7 @@ func TestParse(t *testing.T) {
"Index root no slash",
"_index.md",
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "")
c.Assert(p.Base(), qt.Equals, "/")
c.Assert(p.Ext(), qt.Equals, "md")
c.Assert(p.Name(), qt.Equals, "_index.md")
},
@@ -281,7 +275,7 @@ func TestParse(t *testing.T) {
"Index root",
"/_index.md",
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "")
c.Assert(p.Base(), qt.Equals, "/")
c.Assert(p.Ext(), qt.Equals, "md")
c.Assert(p.Name(), qt.Equals, "_index.md")
},
@@ -379,24 +373,13 @@ func TestParse(t *testing.T) {
c.Assert(p.IsContentData(), qt.IsFalse)
},
},
{
"Custom identifier",
"/a/b/p1._myid_.no.md",
func(c *qt.C, p *Path) {
c.Assert(p.Base(), qt.Equals, "/a/b/p1")
c.Assert(p.Lang(), qt.Equals, "no")
c.Assert(p.Ext(), qt.Equals, "md")
c.Assert(p.Custom(), qt.Equals, "myid")
},
},
}
parser := newTestParser()
for _, test := range tests {
c.Run(test.name, func(c *qt.C) {
if test.name != "Caret up identifier" {
// return
if test.name != "Home branch cundle" {
// return
}
test.assert(c, parser.Parse(files.ComponentFolderContent, test.path))
test.assert(c, testParser.Parse(files.ComponentFolderContent, test.path))
})
}
}
@@ -454,8 +437,8 @@ func TestParseLayouts(t *testing.T) {
"/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", "mylayout"})
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"list"})
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")
},
@@ -478,8 +461,7 @@ func TestParseLayouts(t *testing.T) {
"Lang and output format",
"/list.no.amp.not.html",
func(c *qt.C, p *Path) {
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "list", "amp", "no"})
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"not"})
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")
@@ -601,27 +583,14 @@ func TestParseLayouts(t *testing.T) {
c.Assert(p.NameNoIdentifier(), qt.Equals, "myshortcode")
},
},
{
"Not lang",
"/foo/index.xy.html",
func(c *qt.C, p *Path) {
c.Assert(p.Lang(), qt.Equals, "")
c.Assert(p.Layout(), qt.Equals, "index")
c.Assert(p.NameNoLang(), qt.Equals, "index.xy.html")
c.Assert(p.PathNoLang(), qt.Equals, "/foo/index.xy.html")
c.Assert(p.Identifiers(), qt.DeepEquals, []string{"html", "index"})
c.Assert(p.IdentifiersUnknown(), qt.DeepEquals, []string{"xy"})
},
},
}
parser := newTestParser()
for _, test := range tests {
c.Run(test.name, func(c *qt.C) {
if test.name != "Not lang" {
return
if test.name != "Shortcode lang layout" {
// return
}
test.assert(c, parser.Parse(files.ComponentFolderLayouts, test.path))
test.assert(c, testParser.Parse(files.ComponentFolderLayouts, test.path))
})
}
}
@@ -636,27 +605,7 @@ func TestHasExt(t *testing.T) {
}
func BenchmarkParseIdentity(b *testing.B) {
parser := newTestParser()
for b.Loop() {
parser.ParseIdentity(files.ComponentFolderAssets, "/a/b.css")
}
}
func TestSitesMatrixFromPath(t *testing.T) {
c := qt.New(t)
parser := newTestParser()
p := parser.Parse(files.ComponentFolderContent, "/a/b/c.fr.md")
v := parser.SitesMatrixFromPath(p)
c.Assert(v.HasLanguage(2), qt.IsTrue)
c.Assert(v.LenVectors(), qt.Equals, 1)
c.Assert(v.VectorSample(), qt.Equals, sitesmatrix.Vector{2, 0, 0})
}
func BenchmarkSitesMatrixFromPath(b *testing.B) {
parser := newTestParser()
p := parser.Parse(files.ComponentFolderContent, "/a/b/c.fr.md")
for b.Loop() {
parser.SitesMatrixFromPath(p)
for i := 0; i < b.N; i++ {
testParser.ParseIdentity(files.ComponentFolderAssets, "/a/b.css")
}
}
+4 -4
View File
@@ -35,9 +35,9 @@ weight = 2
removePathAccents = true
-- content/διακριτικός.md --
-- content/διακριτικός.fr.md --
-- layouts/single.html --
-- layouts/_default/single.html --
{{ .Language.Lang }}|Single.
-- layouts/list.html --
-- layouts/_default/list.html --
List
`
b := hugolib.Test(t, files)
@@ -64,9 +64,9 @@ disablePathToLower = true
-- content/MySection/MyPage.fr.md --
-- content/MySection/MyBundle/index.md --
-- content/MySection/MyBundle/index.fr.md --
-- layouts/single.html --
-- layouts/_default/single.html --
{{ .Language.Lang }}|Single.
-- layouts/list.html --
-- layouts/_default/list.html --
{{ .Language.Lang }}|List.
`
b := hugolib.Test(t, files)
+25 -139
View File
@@ -1,4 +1,4 @@
// Copyright 2025 The Hugo Authors. All rights reserved.
// Copyright 2024 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -13,128 +13,52 @@
package predicate
import (
"iter"
"strings"
"github.com/gobwas/glob"
"github.com/gohugoio/hugo/hugofs/hglob"
)
// Match represents the result of a predicate evaluation.
type Match interface {
OK() bool
}
var (
// Predefined Match values for common cases.
True = BoolMatch(true)
False = BoolMatch(false)
)
// BoolMatch is a simple Match implementation based on a boolean value.
type BoolMatch bool
func (b BoolMatch) OK() bool {
return bool(b)
}
// breakMatch is a Match implementation that always returns false for OK() and signals to break evaluation.
type breakMatch struct{}
func (b breakMatch) OK() bool {
return false
}
var matchBreak = breakMatch{}
// P is a predicate function that tests whether a value of type T satisfies some condition.
type P[T any] func(T) bool
// Or returns a predicate that is a short-circuiting logical OR of this and the given predicates.
// Note that P[T] only supports Or. For chained AND/OR logic, use PR[T].
func (p P[T]) Or(ps ...P[T]) P[T] {
// And returns a predicate that is a short-circuiting logical AND of this and the given predicates.
func (p P[T]) And(ps ...P[T]) P[T] {
return func(v T) bool {
if p != nil && p(v) {
for _, pp := range ps {
if !pp(v) {
return false
}
}
if p == nil {
return true
}
return p(v)
}
}
// Or returns a predicate that is a short-circuiting logical OR of this and the given predicates.
func (p P[T]) Or(ps ...P[T]) P[T] {
return func(v T) bool {
for _, pp := range ps {
if pp(v) {
return true
}
}
return false
}
}
// PR is a predicate function that tests whether a value of type T satisfies some condition and returns a Match result.
type PR[T any] func(T) Match
// BoolFunc returns a P[T] version of this predicate.
func (p PR[T]) BoolFunc() P[T] {
return func(v T) bool {
if p == nil {
return false
}
return p(v).OK()
return p(v)
}
}
// And returns a predicate that is a short-circuiting logical AND of this and the given predicates.
func (p PR[T]) And(ps ...PR[T]) PR[T] {
return func(v T) Match {
if p != nil {
m := p(v)
if !m.OK() || shouldBreak(m) {
return matchBreak
}
}
for _, pp := range ps {
m := pp(v)
if !m.OK() || shouldBreak(m) {
return matchBreak
}
}
return BoolMatch(true)
// Negate returns a predicate that is a logical negation of this predicate.
func (p P[T]) Negate() P[T] {
return func(v T) bool {
return !p(v)
}
}
// Or returns a predicate that is a short-circuiting logical OR of this and the given predicates.
func (p PR[T]) Or(ps ...PR[T]) PR[T] {
return func(v T) Match {
if p != nil {
m := p(v)
if m.OK() {
return m
}
if shouldBreak(m) {
return matchBreak
}
}
for _, pp := range ps {
m := pp(v)
if m.OK() {
return m
}
if shouldBreak(m) {
return matchBreak
}
}
return BoolMatch(false)
}
}
func shouldBreak(m Match) bool {
_, ok := m.(breakMatch)
return ok
}
// Filter returns a new slice holding only the elements of s that satisfy p.
// Filter modifies the contents of the slice s and returns the modified slice, which may have a smaller length.
func (p PR[T]) Filter(s []T) []T {
func (p P[T]) Filter(s []T) []T {
var n int
for _, v := range s {
if p(v).OK() {
if p(v) {
s[n] = v
n++
}
@@ -143,50 +67,12 @@ func (p PR[T]) Filter(s []T) []T {
}
// FilterCopy returns a new slice holding only the elements of s that satisfy p.
func (p PR[T]) FilterCopy(s []T) []T {
func (p P[T]) FilterCopy(s []T) []T {
var result []T
for _, v := range s {
if p(v).OK() {
if p(v) {
result = append(result, v)
}
}
return result
}
// NewStringPredicateFromGlobs creates a string predicate from the given glob patterns.
// A glob pattern starting with "!" is a negation pattern which will be ANDed with the rest.
func NewStringPredicateFromGlobs(patterns []string, getGlob func(pattern string) (glob.Glob, error)) (P[string], error) {
var p PR[string]
for _, pattern := range patterns {
pattern = strings.TrimSpace(pattern)
if pattern == "" {
continue
}
negate := strings.HasPrefix(pattern, hglob.NegationPrefix)
if negate {
pattern = pattern[2:]
g, err := getGlob(pattern)
if err != nil {
return nil, err
}
p = p.And(func(s string) Match {
return BoolMatch(!g.Match(s))
})
} else {
g, err := getGlob(pattern)
if err != nil {
return nil, err
}
p = p.Or(func(s string) Match {
return BoolMatch(g.Match(s))
})
}
}
return p.BoolFunc(), nil
}
type IndexMatcher interface {
IndexMatch(match P[string]) (iter.Seq[int], error)
}
+28 -126
View File
@@ -17,66 +17,38 @@ import (
"testing"
qt "github.com/frankban/quicktest"
"github.com/gobwas/glob"
"github.com/gohugoio/hugo/common/predicate"
)
func TestPredicate(t *testing.T) {
func TestAdd(t *testing.T) {
c := qt.New(t)
n := func() predicate.PR[int] {
var pr predicate.PR[int]
return pr
}
var p predicate.P[int] = intP1
var pr predicate.PR[int]
p := pr.BoolFunc()
c.Assert(p(1), qt.IsFalse)
pr = n().Or(intP1).Or(intP2)
p = pr.BoolFunc()
c.Assert(p(1), qt.IsTrue) // true || false
c.Assert(p(2), qt.IsTrue) // false || true
c.Assert(p(3), qt.IsFalse) // false || false
pr = pr.And(intP3)
p = pr.BoolFunc()
c.Assert(p(2), qt.IsFalse) // true || true && false
c.Assert(pr(10), qt.IsTrue) // true || true && true
pr = pr.And(intP4)
p = pr.BoolFunc()
c.Assert(p(10), qt.IsTrue) // true || true && true && true
c.Assert(p(2), qt.IsFalse) // true || true && false && false
c.Assert(p(1), qt.IsFalse) // true || false && false && false
c.Assert(p(3), qt.IsFalse) // false || false && false && false
c.Assert(p(4), qt.IsFalse) // false || false && false && false
c.Assert(p(42), qt.IsFalse) // false || false && false && false
pr = n().And(intP1).And(intP2).And(intP3).And(intP4)
p = pr.BoolFunc()
c.Assert(p(1), qt.IsFalse)
c.Assert(p(2), qt.IsFalse)
c.Assert(p(10), qt.IsTrue)
pr = n().And(intP1).And(intP2).And(intP3).And(intP4)
p = pr.BoolFunc()
c.Assert(p(1), qt.IsFalse)
c.Assert(p(2), qt.IsFalse)
c.Assert(p(10), qt.IsTrue)
pr = n().Or(intP1).Or(intP2).Or(intP3)
p = pr.BoolFunc()
c.Assert(p(1), qt.IsTrue)
c.Assert(p(10), qt.IsTrue)
c.Assert(p(4), qt.IsFalse)
c.Assert(p(2), qt.IsFalse)
neg := p.Negate()
c.Assert(neg(1), qt.IsFalse)
c.Assert(neg(2), qt.IsTrue)
and := p.And(intP2)
c.Assert(and(1), qt.IsFalse)
c.Assert(and(2), qt.IsFalse)
c.Assert(and(10), qt.IsTrue)
or := p.Or(intP2)
c.Assert(or(1), qt.IsTrue)
c.Assert(or(2), qt.IsTrue)
c.Assert(or(10), qt.IsTrue)
c.Assert(or(11), qt.IsFalse)
}
func TestFilter(t *testing.T) {
c := qt.New(t)
var p predicate.PR[int]
p = p.Or(intP1).Or(intP2)
var p predicate.P[int] = intP1
p = p.Or(intP2)
ints := []int{1, 2, 3, 4, 1, 6, 7, 8, 2}
@@ -87,8 +59,8 @@ func TestFilter(t *testing.T) {
func TestFilterCopy(t *testing.T) {
c := qt.New(t)
var p predicate.PR[int]
p = p.Or(intP1).Or(intP2)
var p predicate.P[int] = intP1
p = p.Or(intP2)
ints := []int{1, 2, 3, 4, 1, 6, 7, 8, 2}
@@ -96,86 +68,16 @@ func TestFilterCopy(t *testing.T) {
c.Assert(ints, qt.DeepEquals, []int{1, 2, 3, 4, 1, 6, 7, 8, 2})
}
var intP1 = func(i int) predicate.Match {
var intP1 = func(i int) bool {
if i == 10 {
return predicate.True
return true
}
return predicate.BoolMatch(i == 1)
return i == 1
}
var intP2 = func(i int) predicate.Match {
var intP2 = func(i int) bool {
if i == 10 {
return predicate.True
return true
}
return predicate.BoolMatch(i == 2)
}
var intP3 = func(i int) predicate.Match {
if i == 10 {
return predicate.True
}
return predicate.BoolMatch(i == 3)
}
var intP4 = func(i int) predicate.Match {
if i == 10 {
return predicate.True
}
return predicate.BoolMatch(i == 4)
}
func TestNewStringPredicateFromGlobs(t *testing.T) {
c := qt.New(t)
getGlob := func(pattern string) (glob.Glob, error) {
return glob.Compile(pattern)
}
n := func(patterns ...string) predicate.P[string] {
p, err := predicate.NewStringPredicateFromGlobs(patterns, getGlob)
c.Assert(err, qt.IsNil)
return p
}
m := n("a", "! ab*", "abc")
c.Assert(m("a"), qt.IsTrue)
c.Assert(m("ab"), qt.IsFalse)
c.Assert(m("abc"), qt.IsFalse)
m = n()
c.Assert(m("anything"), qt.IsFalse)
}
func BenchmarkPredicate(b *testing.B) {
b.Run("and or no match", func(b *testing.B) {
var p predicate.PR[int] = intP1
p = p.And(intP2).Or(intP3)
for b.Loop() {
_ = p(3).OK()
}
})
b.Run("and and no match", func(b *testing.B) {
var p predicate.PR[int] = intP1
p = p.And(intP2)
for b.Loop() {
_ = p(3).OK()
}
})
b.Run("and and match", func(b *testing.B) {
var p predicate.PR[int] = intP1
p = p.And(intP2)
for b.Loop() {
_ = p(10).OK()
}
})
b.Run("or or match", func(b *testing.B) {
var p predicate.PR[int] = intP1
p = p.Or(intP2).Or(intP3)
for b.Loop() {
_ = p(2).OK()
}
})
return i == 2
}
-25
View File
@@ -16,7 +16,6 @@ package terminal
import (
"fmt"
"io"
"os"
"strings"
@@ -73,27 +72,3 @@ func doublePercent(str string) string {
func singlePercent(str string) string {
return strings.Replace(str, "%%", "%", -1)
}
type ProgressState int
const (
ProgressHidden ProgressState = iota
ProgressNormal
ProgressError
ProgressIntermediate
ProgressWarning
)
// ReportProgress writes OSC 9;4 sequence to w.
func ReportProgress(w io.Writer, state ProgressState, progress float64) {
if progress < 0 {
progress = 0.0
}
if progress > 1 {
progress = 1.0
}
pi := int(progress * 100)
fmt.Fprintf(w, "\033]9;4;%d;%d\007", state, pi)
}
+1 -1
View File
@@ -65,7 +65,7 @@ func BenchmarkVisitLinesAfter(b *testing.B) {
line 3`
for b.Loop() {
for i := 0; i < b.N; i++ {
VisitLinesAfter(lines, func(s string) {
})
}
-18
View File
@@ -139,25 +139,7 @@ func NewBool(b bool) *bool {
return &b
}
// WeightProvider provides a weight.
type WeightProvider interface {
Weight() int
}
// Weight0Provider provides a weight that's considered before the WeightProvider in sorting.
// This allows the weight set on a given term to win.
type Weight0Provider interface {
Weight0() int
}
// PrintableValueProvider is implemented by types that can provide a printable value.
type PrintableValueProvider interface {
PrintableValue() any
}
type (
Strings2 [2]string
Strings3 [3]string
Ints2 [2]int
Ints3 [3]int
)
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright 2025 The Hugo Authors. All rights reserved.
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
-1
View File
@@ -63,7 +63,6 @@ func TestLexicographicSort(t *testing.T) {
c.Assert(s, qt.DeepEquals, []string{"A", "b", "Ba", "ba", "ba", "Bz"})
}
// // Note that this cannot use b.Loop() because of golang/go#27217.
func BenchmarkStringSort(b *testing.B) {
prototype := []string{"b", "Bz", "zz", "ba", "αβδ αβδ αβδ", "A", "Ba", "ba", "nnnnasdfnnn", "AAgæåz", "αβδC"}
b.Run("LessStrings", func(b *testing.B) {
-14
View File
@@ -1,14 +0,0 @@
// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package compare
+108 -115
View File
@@ -29,7 +29,6 @@ import (
"github.com/gohugoio/hugo/cache/filecache"
"github.com/gohugoio/hugo/cache/httpcache"
"github.com/gohugoio/hugo/common/hstrings"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/maps"
@@ -41,10 +40,7 @@ import (
"github.com/gohugoio/hugo/config/services"
"github.com/gohugoio/hugo/deploy/deployconfig"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugolib/roles"
"github.com/gohugoio/hugo/hugolib/segments"
"github.com/gohugoio/hugo/hugolib/sitesmatrix"
"github.com/gohugoio/hugo/hugolib/versions"
"github.com/gohugoio/hugo/langs"
gc "github.com/gohugoio/hugo/markup/goldmark/goldmark_config"
"github.com/gohugoio/hugo/markup/markup_config"
@@ -59,6 +55,8 @@ import (
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/page/pagemeta"
"github.com/spf13/afero"
xmaps "golang.org/x/exp/maps"
)
// InternalConfig is the internal configuration for Hugo, not read from any user provided config file.
@@ -103,8 +101,7 @@ type Config struct {
// For internal use only.
Internal InternalConfig `mapstructure:"-" json:"-"`
// For internal use only.
C *ConfigCompiled `mapstructure:"-" json:"-"`
isLanguageClone bool
C *ConfigCompiled `mapstructure:"-" json:"-"`
RootConfig
@@ -144,25 +141,16 @@ type Config struct {
// The outputformats configuration sections maps a format name (a string) to a configuration object for that format.
OutputFormats *config.ConfigNamespace[map[string]output.OutputFormatConfig, output.Formats] `mapstructure:"-"`
// The languages configuration sections maps a language code (a string) to a configuration object for that language.
Languages *config.ConfigNamespace[map[string]langs.LanguageConfig, langs.LanguagesInternal] `mapstructure:"-"`
// The versions configuration section contains the top level versions configuration options.
Versions *config.ConfigNamespace[map[string]versions.VersionConfig, versions.VersionsInternal] `mapstructure:"-"`
// The roles configuration section contains the top level roles configuration options.
Roles *config.ConfigNamespace[map[string]roles.RoleConfig, roles.RolesInternal] `mapstructure:"-"`
// The outputs configuration section maps a Page Kind (a string) to a slice of output formats.
// This can be overridden in the front matter.
Outputs map[string][]string `mapstructure:"-"`
// 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 *page.PageMatcherParamsConfigs `mapstructure:"-"`
Cascade *config.ConfigNamespace[[]page.PageMatcherParamsConfig, *maps.Ordered[page.PageMatcher, page.PageMatcherParamsConfig]] `mapstructure:"-"`
// The segments defines segments for the site. Used for partial/segmented builds.
Segments *config.ConfigNamespace[map[string]segments.SegmentConfig, *segments.Segments] `mapstructure:"-"`
Segments *config.ConfigNamespace[map[string]segments.SegmentConfig, segments.Segments] `mapstructure:"-"`
// Menu configuration.
// <docsmeta>{"refs": ["config:languages:menus"] }</docsmeta>
@@ -214,23 +202,19 @@ type Config struct {
// <docsmeta>{"refs": ["config:languages:params"] }</docsmeta>
Params maps.Params `mapstructure:"-"`
// The languages configuration sections maps a language code (a string) to a configuration object for that language.
Languages map[string]langs.LanguageConfig `mapstructure:"-"`
// UglyURLs configuration. Either a boolean or a sections map.
UglyURLs any `mapstructure:"-"`
}
// Early initialization of config.
type configCompiler interface {
CompileConfig(logger loggers.Logger) error
}
// Late initialization of config.
type configInitializer interface {
InitConfig(logger loggers.Logger, defaultSitesMatrix sitesmatrix.VectorStore, configuredDimensions *sitesmatrix.ConfiguredDimensions) error
}
func (c Config) cloneForLang() *Config {
x := c
x.isLanguageClone = true
x.C = nil
copyStringSlice := func(in []string) []string {
if in == nil {
@@ -337,6 +321,18 @@ func (c *Config) CompileConfig(logger loggers.Logger) error {
for _, lang := range c.DisableLanguages {
disabledLangs[lang] = true
}
for lang, language := range c.Languages {
if !language.Disabled && disabledLangs[lang] {
language.Disabled = true
c.Languages[lang] = language
}
if language.Disabled {
disabledLangs[lang] = true
if lang == c.DefaultContentLanguage {
return fmt.Errorf("cannot disable default content language %q", lang)
}
}
}
for i, s := range c.IgnoreLogs {
c.IgnoreLogs[i] = strings.ToLower(s)
@@ -489,6 +485,7 @@ func (c *Config) CompileConfig(logger loggers.Logger) error {
CreateTitle: helpers.GetTitleFunc(c.TitleCaseStyle),
IsUglyURLSection: isUglyURL,
IgnoreFile: ignoreFile,
SegmentFilter: c.Segments.Config.Get(func(s string) { logger.Warnf("Render segment %q not found in configuration", s) }, c.RootConfig.RenderSegments...),
MainSections: c.MainSections,
Clock: clock,
HTTPCache: httpCache,
@@ -528,6 +525,7 @@ type ConfigCompiled struct {
CreateTitle func(s string) string
IsUglyURLSection func(section string) bool
IgnoreFile func(filename string) bool
SegmentFilter segments.SegmentFilter
MainSections []string
Clock time.Time
HTTPCache httpcache.ConfigCompiled
@@ -590,18 +588,6 @@ type RootConfig struct {
// Set this to true to put all languages below their language ID.
DefaultContentLanguageInSubdir bool
// The default content role to use for the site.
DefaultContentRole string
// Set this to true to put the default role in a subdirectory.
DefaultContentRoleInSubdir bool
// The default content version to use for the site.
DefaultContentVersion string
// Set to true to render the default version in a subdirectory.
DefaultContentVersionInSubdir bool
// The default output format to use for the site.
// If not set, we will use the first output format.
DefaultOutputFormat string
@@ -804,13 +790,14 @@ func (c RootConfig) staticDirs() []string {
dirs = append(dirs, c.StaticDir8...)
dirs = append(dirs, c.StaticDir9...)
dirs = append(dirs, c.StaticDir10...)
return hstrings.UniqueStringsReuse(dirs)
return helpers.UniqueStringsReuse(dirs)
}
type Configs struct {
Base *Config
LoadingInfo config.LoadConfigResult
LanguageConfigMap map[string]*Config
Base *Config
LoadingInfo config.LoadConfigResult
LanguageConfigMap map[string]*Config
LanguageConfigSlice []*Config
IsMultihost bool
@@ -818,16 +805,18 @@ type Configs struct {
ModulesClient *modules.Client
// All below is set in Init.
Languages langs.Languages
ContentPathParser *paths.PathParser
ConfiguredDimensions *sitesmatrix.ConfiguredDimensions
DefaultContentSitesMatrix *sitesmatrix.IntSets
AllSitesMatrix *sitesmatrix.IntSets
Languages langs.Languages
LanguagesDefaultFirst langs.Languages
ContentPathParser *paths.PathParser
configLangs []config.AllProvider
}
func (c *Configs) Validate(logger loggers.Logger) error {
c.Base.Cascade.Config.Range(func(p page.PageMatcher, cfg page.PageMatcherParamsConfig) bool {
page.CheckCascadePattern(logger, p)
return true
})
return nil
}
@@ -846,15 +835,54 @@ func (c *Configs) IsZero() bool {
return c == nil || len(c.Languages) == 0
}
func (c *Configs) Init(logger loggers.Logger) error {
func (c *Configs) Init() error {
var languages langs.Languages
for _, f := range c.Base.Languages.Config.Sorted {
v, found := c.LanguageConfigMap[f.Name]
if !found {
return fmt.Errorf("invalid language configuration for %q", f.Name)
var langKeys []string
var hasEn bool
const en = "en"
for k := range c.LanguageConfigMap {
langKeys = append(langKeys, k)
if k == en {
hasEn = true
}
language, err := langs.NewLanguage(f.Name, c.Base.DefaultContentLanguage, v.TimeZone, f.LanguageConfig)
}
// 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)
languageConf := v.Languages[k]
language, err := langs.NewLanguage(k, c.Base.DefaultContentLanguage, v.TimeZone, languageConf)
if err != nil {
return err
}
@@ -871,29 +899,25 @@ func (c *Configs) Init(logger loggers.Logger) error {
}
languages = languages[:n]
var languagesDefaultFirst langs.Languages
for _, l := range languages {
if l.Lang == c.Base.DefaultContentLanguage {
languagesDefaultFirst = append(languagesDefaultFirst, l)
}
}
for _, l := range languages {
if l.Lang != c.Base.DefaultContentLanguage {
languagesDefaultFirst = append(languagesDefaultFirst, l)
}
}
c.Languages = languages
c.ConfiguredDimensions = &sitesmatrix.ConfiguredDimensions{
ConfiguredLanguages: c.Base.Languages.Config,
ConfiguredVersions: c.Base.Versions.Config,
ConfiguredRoles: c.Base.Roles.Config,
}
if err := c.ConfiguredDimensions.Init(); err != nil {
return err
}
intSetsCfg := sitesmatrix.IntSetsConfig{
ApplyDefaults: sitesmatrix.IntSetsConfigApplyDefaultsIfNotSet,
}
matrix := sitesmatrix.NewIntSetsBuilder(c.ConfiguredDimensions).WithConfig(intSetsCfg)
c.DefaultContentSitesMatrix = matrix.Build()
c.AllSitesMatrix = sitesmatrix.NewIntSetsBuilder(c.ConfiguredDimensions).WithAllIfNotSet().Build()
c.LanguagesDefaultFirst = languagesDefaultFirst
c.ContentPathParser = &paths.PathParser{
ConfiguredDimensions: c.ConfiguredDimensions,
LanguageIndex: languages.AsIndexSet(),
IsLangDisabled: c.Base.IsLangDisabled,
IsContentExt: c.Base.ContentTypes.Config.IsContentSuffix,
LanguageIndex: languagesDefaultFirst.AsIndexSet(),
IsLangDisabled: c.Base.IsLangDisabled,
IsContentExt: c.Base.ContentTypes.Config.IsContentSuffix,
IsOutputFormat: func(name, ext string) bool {
if name == "" {
return false
@@ -910,26 +934,12 @@ func (c *Configs) Init(logger loggers.Logger) error {
}
c.configLangs = make([]config.AllProvider, len(c.Languages))
// Config can be shared between languages,
// avoid initializing the same config more than once.
for i, l := range c.Languages {
langConfig := c.LanguageConfigMap[l.Lang]
sitesMatrix := sitesmatrix.NewIntSetsBuilder(c.ConfiguredDimensions).WithLanguageIndices(i).WithAllIfNotSet().Build()
for _, s := range allDecoderSetups {
if getInitializer := s.getInitializer; getInitializer != nil {
if err := getInitializer(langConfig).InitConfig(logger, sitesMatrix, c.ConfiguredDimensions); err != nil {
return err
}
}
}
for i, l := range c.LanguagesDefaultFirst {
c.configLangs[i] = ConfigLanguage{
m: c,
config: langConfig,
baseConfig: c.LoadingInfo.BaseConfig,
language: l,
languageIndex: i,
m: c,
config: c.LanguageConfigMap[l.Lang],
baseConfig: c.LoadingInfo.BaseConfig,
language: l,
}
}
@@ -938,7 +948,7 @@ func (c *Configs) Init(logger loggers.Logger) error {
}
// Apply default project mounts.
if err := modules.ApplyProjectConfigDefaults(logger.Logger(), c.Modules[0], c.configLangs...); err != nil {
if err := modules.ApplyProjectConfigDefaults(c.Modules[0], c.configLangs...); err != nil {
return err
}
@@ -947,7 +957,7 @@ func (c *Configs) Init(logger loggers.Logger) error {
for _, m := range c.Modules[0].Mounts() {
var found bool
for _, cm := range c.Base.Module.Mounts {
if cm.Equal(m) {
if cm.Source == m.Source && cm.Target == m.Target && cm.Lang == m.Lang {
found = true
break
}
@@ -958,7 +968,7 @@ func (c *Configs) Init(logger loggers.Logger) error {
}
// Transfer the changed mounts to the language versions (all share the same mount set, but can be displayed in different languages).
for _, l := range c.LanguageConfigMap {
for _, l := range c.LanguageConfigSlice {
l.Module.Mounts = c.Base.Module.Mounts
}
@@ -975,7 +985,7 @@ func (c Configs) GetFirstLanguageConfig() config.AllProvider {
func (c Configs) GetByLang(lang string) config.AllProvider {
for _, l := range c.configLangs {
if l.Language().(*langs.Language).Lang == lang {
if l.Language().Lang == lang {
return l
}
}
@@ -1044,7 +1054,6 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
mergedConfig := config.New()
var differentRootKeys []string
switch x := v.(type) {
case nil:
case maps.Params:
_, found := x["params"]
if !found {
@@ -1052,6 +1061,7 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
maps.MergeStrategyKey: maps.ParamsMergeStrategyDeep,
}
}
for kk, vv := range x {
if kk == "_merge" {
continue
@@ -1060,21 +1070,6 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
// baseURL configure don the language level is a multihost setup.
isMultihost = true
}
if p, ok := vv.(maps.Params); ok {
// With the introduction of YAML anchor and alias support, language config entries
// may be contain shared references.
// This also break potential cycles.
vv = maps.CloneParamsDeep(p)
}
if kk == "cascade" {
// If not set, add the current language to make sure it does not get applied to other languages.
page.AddLangToCascadeTargetMap(k, vv.(maps.Params))
// Always clone cascade config to get the sites matrix right.
differentRootKeys = append(differentRootKeys, kk)
}
mergedConfig.Set(kk, vv)
rootv := cfg.Get(kk)
if rootv != nil && cfg.IsSet(kk) {
@@ -1085,8 +1080,7 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
differentRootKeys = append(differentRootKeys, kk)
// Use the language value as base.
// Note that this is already cloned above.
mergedConfigEntry := vvv
mergedConfigEntry := xmaps.Clone(vvv)
// Merge in the root value.
maps.MergeParams(mergedConfigEntry, rootv.(maps.Params))
@@ -1106,7 +1100,7 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
}
}
}
differentRootKeys = hstrings.UniqueStringsSorted(differentRootKeys)
differentRootKeys = helpers.UniqueStringsSorted(differentRootKeys)
if len(differentRootKeys) == 0 {
langConfigMap[k] = all
@@ -1135,7 +1129,6 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
langConfigMap[k] = clone
case maps.ParamsMergeStrategy:
default:
panic(fmt.Sprintf("unknown type in languages config: %T", v))
@@ -1186,7 +1179,7 @@ func decodeConfigFromParams(fs afero.Fs, logger loggers.Logger, bcfg config.Base
})
for _, v := range decoderSetups {
p := decodeConfig{p: p, c: target, fs: fs, bcfg: bcfg, logger: logger}
p := decodeConfig{p: p, c: target, fs: fs, bcfg: bcfg}
if err := v.decode(v, p); err != nil {
return fmt.Errorf("failed to decode %q: %w", v.key, err)
}
+27 -7
View File
@@ -39,7 +39,7 @@ title: "p1"
---
title: "p1"
---
-- layouts/single.html --
-- layouts/_default/single.html --
Title: {{ .Title }}
`
@@ -66,12 +66,10 @@ Title: {{ .Title }}
b.Assert(modConf.Mounts, qt.HasLen, 8)
b.Assert(modConf.Mounts[0].Source, qt.Equals, filepath.FromSlash("content/en"))
b.Assert(modConf.Mounts[0].Target, qt.Equals, "content")
b.Assert(modConf.Mounts[0].Lang, qt.Equals, "")
b.Assert(modConf.Mounts[0].Sites.Matrix.Languages, qt.DeepEquals, []string{"en"})
b.Assert(modConf.Mounts[0].Lang, qt.Equals, "en")
b.Assert(modConf.Mounts[1].Source, qt.Equals, filepath.FromSlash("content/sv"))
b.Assert(modConf.Mounts[1].Target, qt.Equals, "content")
b.Assert(modConf.Mounts[1].Lang, qt.Equals, "")
b.Assert(modConf.Mounts[1].Sites.Matrix.Languages, qt.DeepEquals, []string{"sv"})
b.Assert(modConf.Mounts[1].Lang, qt.Equals, "sv")
}
func TestConfigAliases(t *testing.T) {
@@ -141,7 +139,7 @@ disableKinds = ["taxonomy", "term"]
[pagination]
disableAliases = true
pagerSize = 2
-- layouts/list.html --
-- layouts/_default/list.html --
{{ $paginator := .Paginate site.RegularPages }}
{{ template "_internal/pagination.html" . }}
{{ range $paginator.Pages }}
@@ -191,7 +189,7 @@ func TestInvalidOutputFormat(t *testing.T) {
disableKinds = ['page','rss','section','sitemap','taxonomy','term']
[outputs]
home = ['html','foo']
-- layouts/home.html --
-- layouts/index.html --
x
`
@@ -200,6 +198,28 @@ x
b.Assert(err.Error(), qt.Contains, `failed to create config: unknown output format "foo" for kind "home"`)
}
// Issue 13201
func TestLanguageConfigSlice(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ['page','rss','section','sitemap','taxonomy','term']
[languages.en]
title = 'TITLE_EN'
weight = 2
[languages.de]
title = 'TITLE_DE'
weight = 1
[languages.fr]
title = 'TITLE_FR'
weight = 3
`
b := hugolib.Test(t, files)
b.Assert(b.H.Configs.LanguageConfigSlice[0].Title, qt.Equals, `TITLE_DE`)
}
func TestContentTypesDefault(t *testing.T) {
files := `
-- hugo.toml --
+51 -86
View File
@@ -15,15 +15,11 @@ package allconfig
import (
"fmt"
"sort"
"strings"
"github.com/gohugoio/hugo/cache/filecache"
"github.com/gohugoio/hugo/langs"
"github.com/gohugoio/hugo/cache/httpcache"
"github.com/gohugoio/hugo/common/hstrings"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/config"
@@ -31,9 +27,8 @@ import (
"github.com/gohugoio/hugo/config/security"
"github.com/gohugoio/hugo/config/services"
"github.com/gohugoio/hugo/deploy/deployconfig"
"github.com/gohugoio/hugo/hugolib/roles"
"github.com/gohugoio/hugo/hugolib/segments"
"github.com/gohugoio/hugo/hugolib/versions"
"github.com/gohugoio/hugo/langs"
"github.com/gohugoio/hugo/markup/markup_config"
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/minifiers"
@@ -51,18 +46,16 @@ import (
)
type decodeConfig struct {
p config.Provider
c *Config
fs afero.Fs
bcfg config.BaseConfig
logger loggers.Logger
p config.Provider
c *Config
fs afero.Fs
bcfg config.BaseConfig
}
type decodeWeight struct {
key string
decode func(decodeWeight, decodeConfig) error
getCompiler func(c *Config) configCompiler
getInitializer func(c *Config) configInitializer
weight int
internalOrDeprecated bool // Hide it from the docs.
}
@@ -76,10 +69,8 @@ var allDecoderSetups = map[string]decodeWeight{
return err
}
// This need to match with the map keys, always lower case.
// This need to match with Lang which is always lower case.
p.c.RootConfig.DefaultContentLanguage = strings.ToLower(p.c.RootConfig.DefaultContentLanguage)
p.c.RootConfig.DefaultContentRole = strings.ToLower(p.c.RootConfig.DefaultContentRole)
p.c.RootConfig.DefaultContentVersion = strings.ToLower(p.c.RootConfig.DefaultContentVersion)
return nil
},
@@ -149,12 +140,9 @@ var allDecoderSetups = map[string]decodeWeight{
key: "segments",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.Segments, err = segments.DecodeSegments(p.p.GetStringMap(d.key), p.c.RenderSegments, p.logger)
p.c.Segments, err = segments.DecodeSegments(p.p.GetStringMap(d.key))
return err
},
getInitializer: func(c *Config) configInitializer {
return c.Segments.Config
},
},
"server": {
key: "server",
@@ -222,70 +210,6 @@ var allDecoderSetups = map[string]decodeWeight{
return err
},
},
"languages": {
key: "languages",
decode: func(d decodeWeight, p decodeConfig) error {
m := maps.CleanConfigStringMap(p.p.GetStringMap(d.key))
if len(m) == 1 {
// In v0.112.4 we moved this to the language config, but it's very commmon for mono language sites to have this at the top level.
var first maps.Params
var ok bool
for _, v := range m {
first, ok = v.(maps.Params)
if ok {
break
}
}
if first != nil {
if _, found := first["languagecode"]; !found {
first["languagecode"] = p.p.GetString("languagecode")
}
}
}
var (
err error
defaultContentLanguage string
)
p.c.Languages, defaultContentLanguage, err = langs.DecodeConfig(p.c.RootConfig.DefaultContentLanguage, p.c.RootConfig.DisableLanguages, m)
if err != nil {
return fmt.Errorf("failed to decode languages config: %w", err)
}
for k, v := range p.c.Languages.Config.LanguageConfigs {
if v.Disabled {
p.c.RootConfig.DisableLanguages = append(p.c.RootConfig.DisableLanguages, k)
}
}
p.c.RootConfig.DisableLanguages = hstrings.UniqueStringsReuse(p.c.RootConfig.DisableLanguages)
sort.Strings(p.c.RootConfig.DisableLanguages)
p.c.RootConfig.DefaultContentLanguage = defaultContentLanguage
return nil
},
},
"versions": {
key: "versions",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
m := maps.CleanConfigStringMap(p.p.GetStringMap(d.key))
p.c.Versions, err = versions.DecodeConfig(p.c.RootConfig.DefaultContentVersion, m)
return err
},
},
"roles": {
key: "roles",
decode: func(d decodeWeight, p decodeConfig) error {
var (
err error
defaultContentRole string
)
m := maps.CleanConfigStringMap(p.p.GetStringMap(d.key))
p.c.Roles, defaultContentRole, err = roles.DecodeConfig(p.c.RootConfig.DefaultContentRole, m)
p.c.RootConfig.DefaultContentRole = defaultContentRole
return err
},
},
"params": {
key: "params",
decode: func(d decodeWeight, p decodeConfig) error {
@@ -309,7 +233,7 @@ var allDecoderSetups = map[string]decodeWeight{
key: "module",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.Module, err = modules.DecodeConfig(p.logger.Logger(), p.p)
p.c.Module, err = modules.DecodeConfig(p.p)
return err
},
},
@@ -359,15 +283,56 @@ var allDecoderSetups = map[string]decodeWeight{
return nil
},
},
"languages": {
key: "languages",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
m := p.p.GetStringMap(d.key)
if len(m) == 1 {
// In v0.112.4 we moved this to the language config, but it's very commmon for mono language sites to have this at the top level.
var first maps.Params
var ok bool
for _, v := range m {
first, ok = v.(maps.Params)
if ok {
break
}
}
if first != nil {
if _, found := first["languagecode"]; !found {
first["languagecode"] = p.p.GetString("languagecode")
}
}
}
p.c.Languages, err = langs.DecodeConfig(m)
if err != nil {
return err
}
// 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)
}
}
return nil
},
},
"cascade": {
key: "cascade",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.Cascade, err = page.DecodeCascadeConfig(p.p.Get(d.key))
p.c.Cascade, err = page.DecodeCascadeConfig(nil, true, p.p.Get(d.key))
return err
},
getInitializer: func(c *Config) configInitializer { return c.Cascade },
},
"menus": {
key: "menus",
+13 -51
View File
@@ -19,7 +19,6 @@ import (
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/common/urls"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/hugolib/sitesmatrix"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/langs"
)
@@ -28,21 +27,20 @@ type ConfigLanguage struct {
config *Config
baseConfig config.BaseConfig
m *Configs
language *langs.Language
languageIndex int
m *Configs
language *langs.Language
}
func (c ConfigLanguage) Language() any {
func (c ConfigLanguage) Language() *langs.Language {
return c.language
}
func (c ConfigLanguage) LanguageIndex() int {
return c.languageIndex
func (c ConfigLanguage) Languages() langs.Languages {
return c.m.Languages
}
func (c ConfigLanguage) Languages() any {
return c.m.Languages
func (c ConfigLanguage) LanguagesDefaultFirst() langs.Languages {
return c.m.LanguagesDefaultFirst
}
func (c ConfigLanguage) PathParser() *paths.PathParser {
@@ -50,14 +48,14 @@ func (c ConfigLanguage) PathParser() *paths.PathParser {
}
func (c ConfigLanguage) LanguagePrefix() string {
if c.DefaultContentLanguageInSubdir() && c.DefaultContentLanguage() == c.language.Lang {
return c.language.Lang
if c.DefaultContentLanguageInSubdir() && c.DefaultContentLanguage() == c.Language().Lang {
return c.Language().Lang
}
if !c.IsMultilingual() || c.DefaultContentLanguage() == c.language.Lang {
if !c.IsMultilingual() || c.DefaultContentLanguage() == c.Language().Lang {
return ""
}
return c.language.Lang
return c.Language().Lang
}
func (c ConfigLanguage) BaseURL() urls.BaseURL {
@@ -99,10 +97,6 @@ func (c ConfigLanguage) IsLangDisabled(lang string) bool {
return c.config.C.DisabledLanguages[lang]
}
func (c ConfigLanguage) IsKindEnabled(kind string) bool {
return !c.config.C.DisabledKinds[kind]
}
func (c ConfigLanguage) IgnoredLogs() map[string]bool {
return c.config.C.IgnoredLogs
}
@@ -135,10 +129,6 @@ func (c ConfigLanguage) WorkingDir() string {
return c.m.Base.WorkingDir
}
func (c ConfigLanguage) CacheDirMisc() string {
return c.config.Caches.CacheDirMisc()
}
func (c ConfigLanguage) Quiet() bool {
return c.m.Base.Internal.Quiet
}
@@ -147,11 +137,11 @@ func (c ConfigLanguage) Watching() bool {
return c.m.Base.Internal.Watch
}
func (c ConfigLanguage) NewIdentityManager(opts ...identity.ManagerOption) identity.Manager {
func (c ConfigLanguage) NewIdentityManager(name string, opts ...identity.ManagerOption) identity.Manager {
if !c.Watching() {
return identity.NopManager
}
return identity.NewManager(opts...)
return identity.NewManager(name, opts...)
}
func (c ConfigLanguage) ContentTypes() config.ContentTypesProvider {
@@ -165,24 +155,16 @@ func (c ConfigLanguage) GetConfigSection(s string) any {
return c.config.Security
case "build":
return c.config.Build
case "cascade":
return c.config.Cascade
case "frontmatter":
return c.config.Frontmatter
case "caches":
return c.config.Caches
case "markup":
return c.config.Markup
case "module":
return c.config.Module
case "mediaTypes":
return c.config.MediaTypes.Config
case "outputFormats":
return c.config.OutputFormats.Config
case "roles":
return c.config.Roles.Config
case "versions":
return c.config.Versions.Config
case "permalinks":
return c.config.Permalinks
case "minify":
@@ -230,14 +212,6 @@ func (c ConfigLanguage) DefaultContentLanguageInSubdir() bool {
return c.config.DefaultContentLanguageInSubdir
}
func (c ConfigLanguage) DefaultContentRoleInSubdir() bool {
return c.config.DefaultContentRoleInSubdir
}
func (c ConfigLanguage) DefaultContentVersionInSubdir() bool {
return c.config.DefaultContentVersionInSubdir
}
func (c ConfigLanguage) SummaryLength() int {
return c.config.SummaryLength
}
@@ -285,15 +259,3 @@ func (c ConfigLanguage) StaticDirs() []string {
func (c ConfigLanguage) EnableEmoji() bool {
return c.config.EnableEmoji
}
func (c ConfigLanguage) ConfiguredDimensions() *sitesmatrix.ConfiguredDimensions {
return c.m.ConfiguredDimensions
}
func (c ConfigLanguage) DefaultContentsitesMatrix() *sitesmatrix.IntSets {
return c.m.DefaultContentSitesMatrix
}
func (c ConfigLanguage) AllSitesMatrix() *sitesmatrix.IntSets {
return c.m.AllSitesMatrix
}
+13 -35
View File
@@ -19,7 +19,6 @@ import (
"fmt"
"os"
"path/filepath"
"runtime/debug"
"strings"
"github.com/gobwas/glob"
@@ -32,7 +31,7 @@ import (
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/helpers"
hglob "github.com/gohugoio/hugo/hugofs/hglob"
hglob "github.com/gohugoio/hugo/hugofs/glob"
"github.com/gohugoio/hugo/modules"
"github.com/gohugoio/hugo/parser/metadecoders"
"github.com/spf13/afero"
@@ -41,14 +40,7 @@ import (
//lint:ignore ST1005 end user message.
var ErrNoConfigFile = errors.New("Unable to locate config file or config directory. Perhaps you need to create a new site.\n Run `hugo help new` for details.\n")
func LoadConfig(d ConfigSourceDescriptor) (configs *Configs, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("failed to load config: %v", r)
debug.PrintStack()
}
}()
func LoadConfig(d ConfigSourceDescriptor) (*Configs, error) {
if len(d.Environ) == 0 && !hugo.IsRunningAsTest() {
d.Environ = os.Environ()
}
@@ -67,7 +59,7 @@ func LoadConfig(d ConfigSourceDescriptor) (configs *Configs, err error) {
return nil, fmt.Errorf("failed to load config: %w", err)
}
configs, err = fromLoadConfigResult(d.Fs, d.Logger, res)
configs, err := fromLoadConfigResult(d.Fs, d.Logger, res)
if err != nil {
return nil, fmt.Errorf("failed to create config from result: %w", err)
}
@@ -95,13 +87,13 @@ func LoadConfig(d ConfigSourceDescriptor) (configs *Configs, err error) {
configs.Modules = moduleConfig.AllModules
configs.ModulesClient = modulesClient
if err := configs.Init(d.Logger); err != nil {
if err := configs.Init(); err != nil {
return nil, fmt.Errorf("failed to init config: %w", err)
}
loggers.SetGlobalLogger(d.Logger)
return
return configs, nil
}
// ConfigSourceDescriptor describes where to find the config (e.g. config.toml etc.).
@@ -178,16 +170,10 @@ func (l configLoader) applyDefaultConfig() error {
}
func (l configLoader) normalizeCfg(cfg config.Provider) error {
if b, ok := cfg.Get("minifyOutput").(bool); ok {
hugo.Deprecate("site config minifyOutput", "Use minify.minifyOutput instead.", "v0.150.0")
if b {
cfg.Set("minify.minifyOutput", true)
}
} else if b, ok := cfg.Get("minify").(bool); ok {
hugo.Deprecate("site config minify", "Use minify.minifyOutput instead.", "v0.150.0")
if b {
cfg.Set("minify", maps.Params{"minifyOutput": true})
}
if b, ok := cfg.Get("minifyOutput").(bool); ok && b {
cfg.Set("minify.minifyOutput", true)
} else if b, ok := cfg.Get("minify").(bool); ok && b {
cfg.Set("minify", maps.Params{"minifyOutput": true})
}
return nil
@@ -220,8 +206,8 @@ func (l configLoader) applyOsEnvOverrides(environ []string) error {
var hugoEnv []types.KeyValueStr
for _, v := range environ {
key, val := config.SplitEnvVar(v)
if after, ok := strings.CutPrefix(key, hugoEnvPrefix); ok {
delimiterAndKey := after
if strings.HasPrefix(key, hugoEnvPrefix) {
delimiterAndKey := strings.TrimPrefix(key, hugoEnvPrefix)
if len(delimiterAndKey) < 2 {
continue
}
@@ -294,14 +280,7 @@ func (l *configLoader) envValToVal(k string, v any) any {
func (l *configLoader) envStringToVal(k, v string) any {
switch k {
case "disablekinds", "disablelanguages", "ignorefiles", "ignorelogs":
v = strings.TrimSpace(v)
if strings.HasPrefix(v, "[") && strings.HasSuffix(v, "]") {
if parsed, err := metadecoders.Default.UnmarshalStringTo(v, []any{}); err == nil {
return parsed
}
}
case "disablekinds", "disablelanguages":
if strings.Contains(v, ",") {
return strings.Split(v, ",")
} else {
@@ -546,12 +525,11 @@ func (l configLoader) loadConfig(configName string) (string, error) {
return filename, nil
}
func (l configLoader) deleteMergeStrategies() (err error) {
func (l configLoader) deleteMergeStrategies() {
l.cfg.WalkParams(func(params ...maps.KeyParams) bool {
params[len(params)-1].Params.DeleteMergeStrategy()
return false
})
return
}
func (l configLoader) wrapFileError(err error, filename string) error {
+1 -1
View File
@@ -58,7 +58,7 @@ weight = 4
Filename: configFilename,
}
for b.Loop() {
for i := 0; i < b.N; i++ {
_, err := LoadConfig(d)
if err != nil {
b.Fatal(err)
+2 -13
View File
@@ -1,4 +1,4 @@
// Copyright 2025 The Hugo Authors. All rights reserved.
// Copyright 2018 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -20,7 +20,6 @@ import (
"strings"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/parser"
"github.com/gohugoio/hugo/common/paths"
@@ -58,7 +57,6 @@ func IsValidConfigFilename(filename string) bool {
return validConfigFileExtensionsMap[ext]
}
// FromTOMLConfigString creates a config from the given TOML config. This is useful in tests.
func FromTOMLConfigString(config string) Provider {
cfg, err := FromConfigString(config, "toml")
if err != nil {
@@ -67,16 +65,6 @@ func FromTOMLConfigString(config string) Provider {
return cfg
}
// FromMapToTOMLString converts the given map to a TOML string. This is useful in tests.
func FromMapToTOMLString(v map[string]any) string {
var sb strings.Builder
err := parser.InterfaceToConfig(v, metadecoders.TOML, &sb)
if err != nil {
panic(err)
}
return sb.String()
}
// FromConfigString creates a config from the given YAML, JSON or TOML config. This is useful in tests.
func FromConfigString(config, configType string) (Provider, error) {
m, err := readConfig(metadecoders.FormatFromString(configType), []byte(config))
@@ -230,6 +218,7 @@ func init() {
// Before 0.53 we used singular for "menu".
"{menu,languages/*/menu}", "menus",
)
if err != nil {
panic(err)
}
+5 -15
View File
@@ -20,15 +20,15 @@ import (
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/common/urls"
"github.com/gohugoio/hugo/hugolib/sitesmatrix"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/langs"
)
// AllProvider is a sub set of all config settings.
type AllProvider interface {
Language() any
LanguageIndex() int
Languages() any
Language() *langs.Language
Languages() langs.Languages
LanguagesDefaultFirst() langs.Languages
LanguagePrefix() string
BaseURL() urls.BaseURL
BaseURLLiveReload() urls.BaseURL
@@ -50,11 +50,6 @@ type AllProvider interface {
IsUglyURLs(section string) bool
DefaultContentLanguage() string
DefaultContentLanguageInSubdir() bool
DefaultContentRoleInSubdir() bool
DefaultContentVersionInSubdir() bool
DefaultContentsitesMatrix() *sitesmatrix.IntSets
AllSitesMatrix() *sitesmatrix.IntSets
IsKindEnabled(string) bool
IsLangDisabled(string) bool
SummaryLength() int
Pagination() Pagination
@@ -63,7 +58,7 @@ type AllProvider interface {
BuildDrafts() bool
Running() bool
Watching() bool
NewIdentityManager(opts ...identity.ManagerOption) identity.Manager
NewIdentityManager(name string, opts ...identity.ManagerOption) identity.Manager
FastRenderMode() bool
PrintUnusedTemplates() bool
EnableMissingTranslationPlaceholders() bool
@@ -78,8 +73,6 @@ type AllProvider interface {
IgnoredLogs() map[string]bool
WorkingDir() string
EnableEmoji() bool
ConfiguredDimensions() *sitesmatrix.ConfiguredDimensions
CacheDirMisc() string
}
// We cannot import the media package as that would create a circular dependency.
@@ -117,6 +110,3 @@ func GetStringSlicePreserveString(cfg Provider, key string) []string {
sd := cfg.Get(key)
return types.ToStringSlicePreserveString(sd)
}
/*func (cd ConfiguredDimensions) Language(v sitesmatrix.Vector) ConfiguredDimension {
}*/
+7 -16
View File
@@ -14,12 +14,12 @@
package config
import (
"errors"
"fmt"
"sort"
"strings"
"sync"
xmaps "golang.org/x/exp/maps"
"github.com/spf13/cast"
"github.com/gohugoio/hugo/common/maps"
@@ -237,21 +237,12 @@ func (c *defaultConfigProvider) Merge(k string, v any) {
func (c *defaultConfigProvider) Keys() []string {
c.mu.RLock()
defer c.mu.RUnlock()
var keys []string
for k := range c.root {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
return xmaps.Keys(c.root)
}
func (c *defaultConfigProvider) WalkParams(walkFn func(params ...maps.KeyParams) bool) {
maxDepth := 1000
var walk func(depth int, params ...maps.KeyParams)
walk = func(depth int, params ...maps.KeyParams) {
if depth > maxDepth {
panic(errors.New("max depth exceeded"))
}
var walk func(params ...maps.KeyParams)
walk = func(params ...maps.KeyParams) {
if walkFn(params...) {
return
}
@@ -262,11 +253,11 @@ func (c *defaultConfigProvider) WalkParams(walkFn func(params ...maps.KeyParams)
paramsplus1 := make([]maps.KeyParams, i+1)
copy(paramsplus1, params)
paramsplus1[i] = maps.KeyParams{Key: k, Params: p2}
walk(depth+1, paramsplus1...)
walk(paramsplus1...)
}
}
}
walk(0, maps.KeyParams{Key: "", Params: c.root})
walk(maps.KeyParams{Key: "", Params: c.root})
}
func (c *defaultConfigProvider) determineMergeStrategy(params ...maps.KeyParams) maps.ParamsMergeStrategy {
+2 -1
View File
@@ -334,6 +334,7 @@ func TestDefaultConfigProvider(t *testing.T) {
}
for i := range 20 {
i := i
r.Run(func() error {
const v = 42
k := fmt.Sprintf("k%d", i)
@@ -477,7 +478,7 @@ func BenchmarkDefaultConfigProvider(b *testing.B) {
b.Run("Custom", func(b *testing.B) {
cfg := New()
for b.Loop() {
for i := 0; i < b.N; i++ {
runMethods(b, cfg)
}
})
+6 -2
View File
@@ -30,7 +30,7 @@ type Config struct {
Disqus Disqus
GoogleAnalytics GoogleAnalytics
Instagram Instagram
Twitter Twitter `json:"-"` // deprecated in favor of X in v0.141.0
Twitter Twitter // deprecated in favor of X in v0.141.0
Vimeo Vimeo
YouTube YouTube
X X
@@ -112,8 +112,12 @@ type X struct {
// DecodeConfig creates a privacy Config from a given Hugo configuration.
func DecodeConfig(cfg config.Provider) (pc Config, err error) {
pc.GoogleAnalytics.RespectDoNotTrack = true
if !cfg.IsSet(privacyConfigKey) {
return
}
m := cfg.GetStringMap(privacyConfigKey)
err = mapstructure.WeakDecode(m, &pc)
return
+1 -1
View File
@@ -44,7 +44,7 @@ var DefaultConfig = Config{
),
// These have been tested to work with Hugo's external programs
// on Windows, Linux and MacOS.
OsEnv: MustNewWhitelist(`(?i)^((HTTPS?|NO)_PROXY|PATH(EXT)?|APPDATA|TE?MP|TERM|GO\w+|(XDG_CONFIG_)?HOME|USERPROFILE|SSH_AUTH_SOCK|DISPLAY|LANG|SYSTEMDRIVE|PROGRAMDATA)$`),
OsEnv: MustNewWhitelist(`(?i)^((HTTPS?|NO)_PROXY|PATH(EXT)?|APPDATA|TE?MP|TERM|GO\w+|(XDG_CONFIG_)?HOME|USERPROFILE|SSH_AUTH_SOCK|DISPLAY|LANG|SYSTEMDRIVE)$`),
},
Funcs: Funcs{
Getenv: MustNewWhitelist("^HUGO_", "^CI$"),
+1 -1
View File
@@ -135,7 +135,7 @@ func TestToTOML(t *testing.T) {
got := DefaultConfig.ToTOML()
c.Assert(got, qt.Equals,
"[security]\n enableInlineShortcodes = false\n\n [security.exec]\n allow = ['^(dart-)?sass(-embedded)?$', '^go$', '^git$', '^npx$', '^postcss$', '^tailwindcss$']\n osEnv = ['(?i)^((HTTPS?|NO)_PROXY|PATH(EXT)?|APPDATA|TE?MP|TERM|GO\\w+|(XDG_CONFIG_)?HOME|USERPROFILE|SSH_AUTH_SOCK|DISPLAY|LANG|SYSTEMDRIVE|PROGRAMDATA)$']\n\n [security.funcs]\n getenv = ['^HUGO_', '^CI$']\n\n [security.http]\n methods = ['(?i)GET|POST']\n urls = ['.*']",
"[security]\n enableInlineShortcodes = false\n\n [security.exec]\n allow = ['^(dart-)?sass(-embedded)?$', '^go$', '^git$', '^npx$', '^postcss$', '^tailwindcss$']\n osEnv = ['(?i)^((HTTPS?|NO)_PROXY|PATH(EXT)?|APPDATA|TE?MP|TERM|GO\\w+|(XDG_CONFIG_)?HOME|USERPROFILE|SSH_AUTH_SOCK|DISPLAY|LANG|SYSTEMDRIVE)$']\n\n [security.funcs]\n getenv = ['^HUGO_', '^CI$']\n\n [security.http]\n methods = ['(?i)GET|POST']\n urls = ['.*']",
)
}
+5 -5
View File
@@ -26,12 +26,12 @@ const (
rssLimitKey = "rssLimit"
)
// Config is a services configuration for all the relevant services in Hugo.
// Config is a privacy configuration for all the relevant services in Hugo.
type Config struct {
Disqus Disqus
GoogleAnalytics GoogleAnalytics
Instagram Instagram `json:"-"` // the embedded instagram shortcode no longer uses this
Twitter Twitter `json:"-"` // deprecated in favor of X in v0.141.0
Instagram Instagram
Twitter Twitter // deprecated in favor of X in v0.141.0
X X
RSS RSS
}
@@ -53,12 +53,12 @@ type Instagram struct {
// The Simple variant of the Instagram is decorated with Bootstrap 4 card classes.
// This means that if you use Bootstrap 4 or want to provide your own CSS, you want
// to disable the inline CSS provided by Hugo.
DisableInlineCSS bool // this is no longer used by the embedded instagram shortcode
DisableInlineCSS bool
// App or Client Access Token.
// If you are using a Client Access Token, remember that you must combine it with your App ID
// using a pipe symbol (<APPID>|<CLIENTTOKEN>) otherwise the request will fail.
AccessToken string // this is no longer used by the embedded instagram shortcode
AccessToken string
}
// Twitter holds the functional configuration settings related to the Twitter shortcodes.
+19 -7
View File
@@ -16,7 +16,6 @@ package create
import (
"bytes"
"context"
"errors"
"fmt"
"io"
@@ -24,9 +23,11 @@ import (
"path/filepath"
"strings"
"github.com/gohugoio/hugo/hugofs/glob"
"github.com/gohugoio/hugo/common/hexec"
"github.com/gohugoio/hugo/common/hstrings"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/hugofs/hglob"
"github.com/gohugoio/hugo/hugofs"
@@ -158,10 +159,10 @@ func (b *contentBuilder) buildDir() error {
contentTargetFilenames = append(contentTargetFilenames, abs)
}
var contentInclusionFilter *hglob.FilenameFilter
var contentInclusionFilter *glob.FilenameFilter
if !b.dirMap.siteUsed {
// We don't need to build everything.
contentInclusionFilter = hglob.NewFilenameFilterForInclusionFunc(func(filename string) bool {
contentInclusionFilter = glob.NewFilenameFilterForInclusionFunc(func(filename string) bool {
filename = strings.TrimPrefix(filename, string(os.PathSeparator))
for _, cn := range contentTargetFilenames {
if strings.Contains(cn, filename) {
@@ -227,10 +228,10 @@ func (b *contentBuilder) buildFile() (string, error) {
return "", err
}
var contentInclusionFilter *hglob.FilenameFilter
var contentInclusionFilter *glob.FilenameFilter
if !usesSite {
// We don't need to build everything.
contentInclusionFilter = hglob.NewFilenameFilterForInclusionFunc(func(filename string) bool {
contentInclusionFilter = glob.NewFilenameFilterForInclusionFunc(func(filename string) bool {
filename = strings.TrimPrefix(filename, string(os.PathSeparator))
return strings.Contains(contentPlaceholderAbsFilename, filename)
})
@@ -290,7 +291,9 @@ func (b *contentBuilder) applyArcheType(contentFilename string, archetypeFi hugo
func (b *contentBuilder) mapArcheTypeDir() error {
var m archetypeMap
walkFn := func(ctx context.Context, path string, fim hugofs.FileMetaInfo) error {
seen := map[hstrings.Strings2]bool{}
walkFn := func(path string, fim hugofs.FileMetaInfo) error {
if fim.IsDir() {
return nil
}
@@ -298,6 +301,15 @@ func (b *contentBuilder) mapArcheTypeDir() error {
pi := fim.Meta().PathInfo
if pi.IsContent() {
pathLang := hstrings.Strings2{pi.PathBeforeLangAndOutputFormatAndExt(), 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
// for archetype folders these are ambiguous and we need to
// fail.
return fmt.Errorf("duplicate content file found in archetype folder: %q; having both e.g. %s.md and %s.html is ambigous", path, pi.BaseNameNoIdentifier(), pi.BaseNameNoIdentifier())
}
seen[pathLang] = true
m.contentFiles = append(m.contentFiles, fim)
if !m.siteUsed {
var err error
+8 -11
View File
@@ -74,6 +74,7 @@ func TestNewContentFromFile(t *testing.T) {
c := qt.New(t)
for i, cas := range cases {
cas := cas
c.Run(cas.name, func(c *qt.C) {
c.Parallel()
@@ -135,27 +136,23 @@ site RegularPages: {{ len site.RegularPages }}
cfg, fs := newTestCfg(c, mm)
conf := testconfig.GetTestConfigs(fs.Source, cfg)
h, err := hugolib.NewHugoSites(deps.DepsCfg{Configs: conf, Fs: fs})
c.Assert(err, qt.IsNil)
c.Assert(len(h.Sites), qt.Equals, 2)
h := func() *hugolib.HugoSites {
h, err := hugolib.NewHugoSites(deps.DepsCfg{Configs: conf, Fs: fs})
c.Assert(err, qt.IsNil)
c.Assert(len(h.Sites), qt.Equals, 2)
return h
}
c.Assert(create.NewContent(h(), "my-bundle", "post/my-post", false), qt.IsNil)
c.Assert(create.NewContent(h, "my-bundle", "post/my-post", false), qt.IsNil)
cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/index.md")), `site RegularPages: 10`)
// Default bundle archetype
c.Assert(create.NewContent(h(), "", "post/my-post2", false), qt.IsNil)
c.Assert(create.NewContent(h, "", "post/my-post2", false), qt.IsNil)
cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post2/index.md")), `default archetype index.md`)
// Regular file with bundle kind.
c.Assert(create.NewContent(h(), "my-bundle", "post/foo.md", false), qt.IsNil)
c.Assert(create.NewContent(h, "my-bundle", "post/foo.md", false), qt.IsNil)
cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/foo.md")), `draft: true`)
// Regular files should fall back to the default archetype (we have no regular file archetype).
c.Assert(create.NewContent(h(), "my-bundle", "mypage.md", false), qt.IsNil)
c.Assert(create.NewContent(h, "my-bundle", "mypage.md", false), qt.IsNil)
cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "mypage.md")), `draft: true`)
}
+2 -4
View File
@@ -1,9 +1,7 @@
{{ define "main" }}
{{ .Content }}
{{ range site.RegularPages }}
<section>
<h2><a href="{{ .RelPermalink }}">{{ .LinkTitle }}</a></h2>
{{ .Summary }}
</section>
<h2><a href="{{ .RelPermalink }}">{{ .LinkTitle }}</a></h2>
{{ .Summary }}
{{ end }}
{{ end }}
+2 -4
View File
@@ -2,9 +2,7 @@
<h1>{{ .Title }}</h1>
{{ .Content }}
{{ range .Pages }}
<section>
<h2><a href="{{ .RelPermalink }}">{{ .LinkTitle }}</a></h2>
{{ .Summary }}
</section>
<h2><a href="{{ .RelPermalink }}">{{ .LinkTitle }}</a></h2>
{{ .Summary }}
{{ end }}
{{ end }}
+1 -1
View File
@@ -20,7 +20,7 @@ import (
"github.com/gobwas/glob"
"github.com/gohugoio/hugo/config"
hglob "github.com/gohugoio/hugo/hugofs/hglob"
hglob "github.com/gohugoio/hugo/hugofs/glob"
"github.com/mitchellh/mapstructure"
)
+2 -5
View File
@@ -217,14 +217,14 @@ func (d *Deps) Init() error {
[]byte(tpl.HugoDeferredTemplatePrefix),
[]byte(postpub.PostProcessPrefix))
pathSpec, err := helpers.NewPathSpec(d.Fs, d.Conf, d.Log, nil)
pathSpec, err := helpers.NewPathSpec(d.Fs, d.Conf, d.Log)
if err != nil {
return err
}
d.PathSpec = pathSpec
} else {
var err error
d.PathSpec, err = helpers.NewPathSpec(d.Fs, d.Conf, d.Log, d.PathSpec.BaseFs)
d.PathSpec, err = helpers.NewPathSpecWithBaseBaseFsProvided(d.Fs, d.Conf, d.Log, d.PathSpec.BaseFs)
if err != nil {
return err
}
@@ -417,9 +417,6 @@ type DepsCfg struct {
// i18n handling.
TranslationProvider ResourceProvider
// Build triggered by the IntegrationTest framework.
IsIntegrationTest bool
// ChangesFromBuild for changes passed back to the server/watch process.
ChangesFromBuild chan []identity.Identity
}
+2 -2
View File
@@ -4,10 +4,10 @@
[codespell]
# Comma separated list of dirs to be skipped.
skip = *.ai,chroma.css,chroma_dark.css,.cspell.json
skip = _vendor,.cspell.json,chroma.css,chroma_dark.css
# Comma separated list of words to be ignored. Words must be lowercased.
ignore-words-list = abl,edn,januar,te,trys,ue,womens
ignore-words-list = abl,edn,te,ue,trys,januar,womens,crossreferences
# Check file names as well.
check-filenames = true
+2 -13
View File
@@ -1,15 +1,8 @@
{
"version": "0.2",
"allowCompoundWords": true,
"overrides": [
{
"filename": "**/*",
"enabled": false
},
{
"filename": "**/*.md",
"enabled": true
}
"files": [
"**/*.md"
],
"flagWords": [
"alot",
@@ -71,7 +64,6 @@
"templating",
"transpile",
"unmarshal",
"unmarshaled",
"unmarshaling",
"unmarshals",
"# ----------------------------------------------------------------------",
@@ -108,8 +100,6 @@
"descripción",
"dokumentation",
"erklärungen",
"español",
"français",
"libros",
"mercredi",
"miesiąc",
@@ -157,7 +147,6 @@
"dpkg",
"doas",
"eopkg",
"forgejo",
"gitee",
"goldmark",
"katex",

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