Compare commits

..

1 Commits

Author SHA1 Message Date
Bjørn Erik Pedersen e04f7cd378 js/esbuild: Add an integration test for multiple options for the same input JS file
See #14254
2025-12-12 11:01:55 +01:00
1110 changed files with 10052 additions and 24331 deletions
+2 -2
View File
@@ -4,7 +4,7 @@ parameters:
defaults: &defaults
resource_class: large
docker:
- image: bepsays/ci-hugoreleaser:1.22600.20100
- image: bepsays/ci-hugoreleaser:1.22500.20400
environment: &buildenv
GOMODCACHE: /root/project/gomodcache
version: 2
@@ -58,7 +58,7 @@ jobs:
environment:
<<: [*buildenv]
docker:
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22600.20100
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22500.20400
steps:
- *restore-cache
- &attach-workspace
+10
View File
@@ -0,0 +1,10 @@
🛑 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
@@ -0,0 +1,22 @@
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]
+6 -7
View File
@@ -16,7 +16,7 @@ jobs:
test:
strategy:
matrix:
go-version: [1.25.x, 1.26.x]
go-version: [1.24.x, 1.25.x]
os: [ubuntu-latest, windows-latest] # macos disabled for now because of disk space issues.
runs-on: ${{ matrix.os }}
steps:
@@ -106,10 +106,7 @@ jobs:
run: go install honnef.co/go/tools/cmd/staticcheck@latest
- if: matrix.os == 'ubuntu-latest'
name: Run staticcheck
run: |
export STATICCHECK_CACHE="${{ runner.temp }}/staticcheck"
staticcheck ./...
rm -rf ${{ runner.temp }}/staticcheck
run: staticcheck ./...
- if: matrix.os != 'windows-latest'
name: Check
run: |
@@ -121,14 +118,16 @@ jobs:
# See issue #11052. We limit the build to regular test (no -race flag) on Windows for now.
name: Test
run: |
mage -v test
mage -v test;
env:
HUGO_BUILD_TAGS: extended,withdeploy
- name: Build tags
run: |
go install -tags extended
- if: matrix.os == 'ubuntu-latest'
name: Build for dragonfly
run: |
go install
go clean -i -cache
env:
GOARCH: amd64
GOOS: dragonfly
+1 -2
View File
@@ -3,5 +3,4 @@
imports.*
dist/
public/
.DS_Store
cache/filecache/_gen/
.DS_Store
-1
View File
@@ -1 +0,0 @@
tpl/tplimpl/embedded/templates/**
-17
View File
@@ -1,17 +0,0 @@
* Brevity is good.
* Assume that the maintainers and readers of the code you write are Go experts:
* Don't use comments to explain the obvious.
* Use self-explanatory variable and function names.
* Use short variable names when the context is clear.
* If you need to add temporary debug printing, use `hdebug.Printf`.[^1]
* Never export symbols that's not needed outside of the package.
* Avoid global state at (almost) all cost.
* This is a project with a long history; assume that a similiar problem has been solved before, look hard for helper functions before creating new ones.
* In tests, use `qt` matchers (e.g. `b.Assert(err, qt.ErrorMatches, ...)`) instead of raw `if`/`t.Fatal` checks.
* In tests, always use the latest Hugo specification, e.g. for layouts, it's `layouts/page.html` and not `layouts/_default/single.html`, `layouts/list.html` and not `layouts/_default/list.html`
* Brevity is good. This applies to code, comments and commit messages. Don't write a novel.
* Use `./check.sh ./somepackage/...` when iterating.
* Use `./check.sh` when you're done.
[^1]: CI build fail if you forget to remove the debug printing.
-1
View File
@@ -1 +0,0 @@
@AGENTS.md
+2 -2
View File
@@ -77,7 +77,7 @@ To make the contribution process as seamless as possible, we ask for the followi
* Run `go fmt`.
* Add documentation if you are adding new features or changing functionality. The docs site lives in `/docs`.
* Squash your commits into a single commit. `git rebase -i`. Its okay to force update your pull request with `git push -f`.
* Ensure that `./check.sh` succeeds. Note that some tests are skipped when running locally, some because they are slow. To run these locally, do `CI_LOCAL=true ./check.sh ./somepackage/...`.
* 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
@@ -88,7 +88,7 @@ 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. Also, AI contributions from non-maintainers needs to have a fairly narrow scope (e.g. a bug fix), as we have limited review capacity.
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
+2 -4
View File
@@ -2,7 +2,7 @@
# Twitter: https://twitter.com/gohugoio
# Website: https://gohugo.io/
ARG GO_VERSION="1.26"
ARG GO_VERSION="1.25"
ARG ALPINE_VERSION="3.22"
ARG DART_SASS_VERSION="1.79.3"
@@ -65,9 +65,7 @@ RUN apk add --no-cache \
git \
runuser \
nodejs \
npm \
openssh-client \
tar
npm
RUN mkdir -p /var/hugo/bin /cache && \
addgroup -Sg 1000 hugo && \
+74 -81
View File
@@ -52,7 +52,7 @@ Use Hugo's embedded web server during development to instantly see changes to co
Hugo's fast asset pipelines include:
- Image processing &ndash; Convert, resize, crop, rotate, adjust colors, apply filters, overlay text and images, and extract metadata
- Image processing &ndash; Convert, resize, crop, rotate, adjust colors, apply filters, overlay text and images, and extract EXIF data
- JavaScript bundling &ndash; Transpile TypeScript and JSX to JavaScript, bundle, tree shake, minify, create source maps, and perform SRI hashing.
- Sass processing &ndash; Transpile Sass to CSS, bundle, tree shake, minify, create source maps, perform SRI hashing, and integrate with PostCSS
- Tailwind CSS processing &ndash; Compile Tailwind CSS utility classes into standard CSS, bundle, tree shake, optimize, minify, perform SRI hashing, and integrate with PostCSS
@@ -65,6 +65,8 @@ See the [features] section of the documentation for a comprehensive summary of H
<p>&nbsp;</p>
<p float="left">
<a href="https://www.linode.com/?utm_campaign=hugosponsor&utm_medium=banner&utm_source=hugogithub" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/hugoDocs/master/assets/images/sponsors/linode-logo_standard_light_medium.png" width="200" alt="Linode"></a>
&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>
@@ -72,17 +74,21 @@ See the [features] section of the documentation for a comprehensive summary of H
## Editions
Hugo offers a standard edition with core features, plus extended and extended/deploy editions with more. Use the standard edition unless you need the features below.
Hugo is available in three editions: standard, extended, and extended/deploy. While the standard edition provides core functionality, the extended and extended/deploy editions offer advanced features.
Feature|extended edition|extended/deploy edition
:--|:-:|:-:
[Transpile Sass to CSS] via embedded LibSass. Note that embedded LibSass was deprecated in v0.153.0 and will be removed in a future release. Use the [Dart Sass] transpiler instead, which is compatible with any edition.|:heavy_check_mark:|:heavy_check_mark:
Encode to the WebP format when [processing images]. You can decode WebP images with any edition.|:heavy_check_mark:|:heavy_check_mark:
[Transpile Sass to CSS] using the embedded LibSass transpiler. You can use the [Dart Sass] transpiler with any edition.|:heavy_check_mark:|:heavy_check_mark:
Deploy your site directly to a Google Cloud Storage bucket, an AWS S3 bucket, or an Azure Storage container. See&nbsp;[details].|:x:|:heavy_check_mark:
[dart sass]: https://gohugo.io/functions/css/sass/#dart-sass
[processing images]: https://gohugo.io/content-management/image-processing/
[transpile sass to css]: https://gohugo.io/functions/css/sass/
[details]: https://gohugo.io/hosting-and-deployment/hugo-deploy/
Unless your specific deployment needs require the extended/deploy edition, we recommend the extended edition.
## Installation
Install Hugo from a [prebuilt binary], package manager, or package repository. Please see the installation instructions for your operating system:
@@ -96,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.25.0 or later
- Extended edition: Go 1.25.0 or later, and GCC
- Extended/deploy edition: Go 1.25.0 or later, and GCC
- 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
Build the standard edition:
@@ -156,16 +162,6 @@ If there is sufficient interest, [create a proposal]. Do not submit a pull reque
For a complete guide to contributing to Hugo, see the [Contribution Guide](CONTRIBUTING.md).
## License
For the Hugo source code, see [LICENSE](/LICENSE).
We also bundle some libraries in binary/WASM form:
* [libwebp](https://github.com/webmproject/libwebp), [BSD-3-Clause license](https://github.com/webmproject/libwebp?tab=BSD-3-Clause-1-ov-file#readme)
* [Katex](https://github.com/KaTeX/KaTeX), [MIT license](https://github.com/KaTeX/KaTeX?tab=MIT-1-ov-file#readme)
* [QuickJS](https://github.com/bellard/quickjs?tab=License-1-ov-file#readme), [License](https://github.com/bellard/quickjs?tab=License-1-ov-file#readme)
## Dependencies
Hugo stands on the shoulders of great open source libraries. Run `hugo env --logLevel info` to display a list of dependencies.
@@ -175,55 +171,51 @@ Hugo stands on the shoulders of great open source libraries. Run `hugo env --log
```text
github.com/BurntSushi/locker="v0.0.0-20171006230638-a6e239ea1c69"
github.com/JohannesKaufmann/dom="v0.2.0"
github.com/JohannesKaufmann/html-to-markdown/v2="v2.5.0"
github.com/alecthomas/chroma/v2="v2.21.1"
github.com/aymerick/douceur="v0.2.0"
github.com/PuerkitoBio/goquery="v1.10.1"
github.com/alecthomas/chroma/v2="v2.15.0"
github.com/andybalholm/cascadia="v1.3.3"
github.com/armon/go-radix="v1.0.1-0.20221118154546-54df44f2176c"
github.com/bep/clocks="v0.5.0"
github.com/bep/debounce="v1.2.0"
github.com/bep/gitmap="v1.9.0"
github.com/bep/gitmap="v1.6.0"
github.com/bep/goat="v0.5.0"
github.com/bep/godartsass/v2="v2.5.0"
github.com/bep/godartsass/v2="v2.3.2"
github.com/bep/golibsass="v1.2.0"
github.com/bep/goportabletext="v0.1.0"
github.com/bep/helpers="v0.6.0"
github.com/bep/imagemeta="v0.12.0"
github.com/bep/lazycache="v0.8.0"
github.com/bep/gowebp="v0.3.0"
github.com/bep/imagemeta="v0.8.4"
github.com/bep/lazycache="v0.7.0"
github.com/bep/logg="v0.4.0"
github.com/bep/mclib="v1.20400.20402"
github.com/bep/overlayfs="v0.10.0"
github.com/bep/simplecobra="v0.6.1"
github.com/bep/textandbinarywriter="v0.0.0-20251212174530-cd9f0732f60f"
github.com/bep/overlayfs="v0.9.2"
github.com/bep/simplecobra="v0.5.0"
github.com/bep/tmc="v0.5.1"
github.com/bits-and-blooms/bitset="v1.24.4"
github.com/cespare/xxhash/v2="v2.3.0"
github.com/clbanning/mxj/v2="v2.7.0"
github.com/clipperhouse/displaywidth="v0.6.0"
github.com/clipperhouse/stringish="v0.1.1"
github.com/clipperhouse/uax29/v2="v2.3.0"
github.com/cpuguy83/go-md2man/v2="v2.0.6"
github.com/cpuguy83/go-md2man/v2="v2.0.4"
github.com/disintegration/gift="v1.2.1"
github.com/dlclark/regexp2="v1.11.5"
github.com/evanw/esbuild="v0.27.2"
github.com/dop251/goja="v0.0.0-20250125213203-5ef83b82af17"
github.com/evanw/esbuild="v0.24.2"
github.com/fatih/color="v1.18.0"
github.com/frankban/quicktest="v1.14.6"
github.com/fsnotify/fsnotify="v1.9.0"
github.com/getkin/kin-openapi="v0.133.0"
github.com/fsnotify/fsnotify="v1.8.0"
github.com/getkin/kin-openapi="v0.129.0"
github.com/ghodss/yaml="v1.0.0"
github.com/go-openapi/jsonpointer="v0.21.0"
github.com/go-openapi/swag="v0.23.0"
github.com/go-sourcemap/sourcemap="v2.1.4+incompatible"
github.com/gobuffalo/flect="v1.0.3"
github.com/gobwas/glob="v0.2.3"
github.com/goccy/go-yaml="v1.19.1"
github.com/gohugoio/go-i18n/v2="v2.1.3-0.20251018145728-cfcc22d823c6"
github.com/gohugoio/go-radix="v1.2.0"
github.com/gohugoio/hashstructure="v0.6.0"
github.com/gohugoio/httpcache="v0.8.0"
github.com/gohugoio/hugo-goldmark-extensions/extras="v0.5.0"
github.com/gohugoio/hugo-goldmark-extensions/passthrough="v0.3.1"
github.com/gohugoio/go-i18n/v2="v2.1.3-0.20230805085216-e63c13218d0e"
github.com/gohugoio/hashstructure="v0.5.0"
github.com/gohugoio/httpcache="v0.7.0"
github.com/gohugoio/hugo-goldmark-extensions/extras="v0.2.0"
github.com/gohugoio/hugo-goldmark-extensions/passthrough="v0.3.0"
github.com/gohugoio/locales="v0.14.0"
github.com/gohugoio/localescompressed="v1.0.1"
github.com/google/go-cmp="v0.7.0"
github.com/gorilla/css="v1.0.1"
github.com/golang/freetype="v0.0.0-20170609003504-e2365dfdc4a0"
github.com/google/go-cmp="v0.6.0"
github.com/google/pprof="v0.0.0-20250208200701-d0013a598941"
github.com/gorilla/websocket="v1.5.3"
github.com/hairyhenderson/go-codeowners="v0.7.0"
github.com/hashicorp/golang-lru/v2="v2.0.7"
@@ -232,57 +224,58 @@ github.com/josharian/intern="v1.0.0"
github.com/kr/pretty="v0.3.1"
github.com/kr/text="v0.2.0"
github.com/kyokomi/emoji/v2="v2.2.13"
github.com/lucasb-eyer/go-colorful="v1.2.0"
github.com/mailru/easyjson="v0.7.7"
github.com/makeworld-the-better-one/dither/v2="v2.4.0"
github.com/marekm4/color-extractor="v1.2.1"
github.com/mattn/go-colorable="v0.1.13"
github.com/mattn/go-isatty="v0.0.20"
github.com/mattn/go-runewidth="v0.0.19"
github.com/microcosm-cc/bluemonday="v1.0.27"
github.com/mattn/go-runewidth="v0.0.9"
github.com/mazznoer/csscolorparser="v0.1.5"
github.com/mitchellh/mapstructure="v1.5.1-0.20231216201459-8508981c8b6c"
github.com/mohae/deepcopy="v0.0.0-20170929034955-c48cc78d4826"
github.com/muesli/smartcrop="v0.3.0"
github.com/niklasfasching/go-org="v1.9.1"
github.com/oasdiff/yaml3="v0.0.0-20250309153720-d2182401db90"
github.com/oasdiff/yaml="v0.0.0-20250309154309-f31be36b4037"
github.com/olekukonko/cat="v0.0.0-20250911104152-50322a0618f6"
github.com/olekukonko/errors="v1.1.0"
github.com/olekukonko/ll="v0.1.3"
github.com/olekukonko/tablewriter="v1.1.2"
github.com/niklasfasching/go-org="v1.7.0"
github.com/oasdiff/yaml3="v0.0.0-20241210130736-a94c01f36349"
github.com/oasdiff/yaml="v0.0.0-20241210131133-6b86fb107d80"
github.com/olekukonko/tablewriter="v0.0.5"
github.com/pbnjay/memory="v0.0.0-20210728143218-7b4eea64cf58"
github.com/pelletier/go-toml/v2="v2.2.4"
github.com/pelletier/go-toml/v2="v2.2.3"
github.com/perimeterx/marshmallow="v1.1.5"
github.com/pkg/browser="v0.0.0-20240102092130-5ac0b6a4141c"
github.com/pkg/errors="v0.9.1"
github.com/rogpeppe/go-internal="v1.14.1"
github.com/rivo/uniseg="v0.4.7"
github.com/rogpeppe/go-internal="v1.13.1"
github.com/russross/blackfriday/v2="v2.1.0"
github.com/sass/dart-sass/compiler="1.97.1"
github.com/sass/dart-sass/implementation="1.97.1"
github.com/sass/dart-sass/protocol="3.2.0"
github.com/spf13/afero="v1.15.0"
github.com/spf13/cast="v1.10.0"
github.com/spf13/cobra="v1.10.2"
github.com/sass/libsass="3.6.6"
github.com/spf13/afero="v1.11.0"
github.com/spf13/cast="v1.7.1"
github.com/spf13/cobra="v1.8.1"
github.com/spf13/fsync="v0.10.1"
github.com/spf13/pflag="v1.0.9"
github.com/tdewolff/minify/v2="v2.24.8"
github.com/tdewolff/parse/v2="v2.8.5"
github.com/tetratelabs/wazero="v1.10.1"
github.com/webmproject/libwebp="v1.6.0"
github.com/woodsbury/decimal128="v1.3.0"
github.com/yuin/goldmark-emoji="v1.0.6"
github.com/yuin/goldmark="v1.7.13"
github.com/spf13/pflag="v1.0.6"
github.com/tdewolff/minify/v2="v2.20.37"
github.com/tdewolff/parse/v2="v2.7.15"
github.com/tetratelabs/wazero="v1.8.2"
github.com/webmproject/libwebp="v1.3.2"
github.com/yuin/goldmark-emoji="v1.0.4"
github.com/yuin/goldmark="v1.7.8"
go.uber.org/automaxprocs="v1.5.3"
go.yaml.in/yaml/v3="v3.0.4"
golang.org/x/crypto="v0.46.0"
golang.org/x/image="v0.34.0"
golang.org/x/mod="v0.31.0"
golang.org/x/net="v0.48.0"
golang.org/x/sync="v0.19.0"
golang.org/x/sys="v0.39.0"
golang.org/x/text="v0.32.0"
golang.org/x/tools="v0.40.0"
google.golang.org/protobuf="v1.36.10"
golang.org/x/crypto="v0.33.0"
golang.org/x/exp="v0.0.0-20250210185358-939b2ce775ac"
golang.org/x/image="v0.24.0"
golang.org/x/mod="v0.23.0"
golang.org/x/net="v0.35.0"
golang.org/x/sync="v0.11.0"
golang.org/x/sys="v0.30.0"
golang.org/x/text="v0.22.0"
golang.org/x/tools="v0.30.0"
golang.org/x/xerrors="v0.0.0-20240903120638-7835f813f4da"
gonum.org/v1/plot="v0.15.0"
google.golang.org/protobuf="v1.36.5"
gopkg.in/yaml.v2="v2.4.0"
gopkg.in/yaml.v3="v3.0.1"
oss.terrastruct.com/d2="v0.6.9"
oss.terrastruct.com/util-go="v0.0.0-20241005222610-44c011a04896"
rsc.io/qr="v0.2.0"
software.sslmate.com/src/go-pkcs12="v0.2.0"
```
+2 -2
View File
@@ -69,7 +69,7 @@ func New(opts Options) *Cache {
infol := opts.Log.InfoCommand("dynacache")
evictedIdentities := collections.NewStackThreadSafe[KeyIdentity]()
evictedIdentities := collections.NewStack[KeyIdentity]()
onEvict := func(k, v any) {
if !opts.Watching {
@@ -129,7 +129,7 @@ type Cache struct {
partitions map[string]PartitionManager
onEvict func(k, v any)
evictedIdentities *collections.StackThreadSafe[KeyIdentity]
evictedIdentities *collections.Stack[KeyIdentity]
opts Options
infol logg.LevelLogger
+59 -142
View File
@@ -1,4 +1,4 @@
// Copyright 2026 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.
@@ -16,7 +16,6 @@ package filecache
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"path/filepath"
@@ -46,12 +45,16 @@ const (
type Cache struct {
Fs afero.Fs
cfg FileCacheConfig
// Max age for items in this cache. Negative duration means forever,
// 0 is effectively turning this cache off.
maxAge time.Duration
entryLocker *lockTracker
// When set, we just remove this entire root directory on expiration.
pruneAllRootDir string
nlocker *lockTracker
initOnce sync.Once
isInited bool
initErr error
}
@@ -85,15 +88,12 @@ type ItemInfo struct {
}
// NewCache creates a new file cache with the given filesystem and max age.
func NewCache(fs afero.Fs, cfg FileCacheConfig) *Cache {
if err := cfg.init(); err != nil {
panic(fmt.Sprintf("invalid cache config: %s", err))
}
func NewCache(fs afero.Fs, maxAge time.Duration, pruneAllRootDir string) *Cache {
return &Cache{
Fs: fs,
entryLocker: &lockTracker{Locker: locker.NewLocker(), seen: make(map[string]struct{})},
cfg: cfg,
Fs: fs,
nlocker: &lockTracker{Locker: locker.NewLocker(), seen: make(map[string]struct{})},
maxAge: maxAge,
pruneAllRootDir: pruneAllRootDir,
}
}
@@ -109,15 +109,9 @@ func (l *lockedFile) Close() error {
}
func (c *Cache) init() error {
if c == nil {
panic("cache is nil")
}
c.initOnce.Do(func() {
c.isInited = true
// Create the base dir if it does not exist.
if err := c.Fs.MkdirAll("", 0o777); err != nil && !os.IsExist(err) {
err = fmt.Errorf("failled to create base cache directory: %s", err)
c.initErr = err
}
})
@@ -132,19 +126,19 @@ func (c *Cache) WriteCloser(id string) (ItemInfo, io.WriteCloser, error) {
}
id = cleanID(id)
c.entryLocker.Lock(id)
c.nlocker.Lock(id)
info := ItemInfo{Name: id}
f, err := helpers.OpenFileForWriting(c.Fs, id)
if err != nil {
c.entryLocker.Unlock(id)
c.nlocker.Unlock(id)
return info, nil, err
}
return info, &lockedFile{
File: f,
unlock: func() { c.entryLocker.Unlock(id) },
unlock: func() { c.nlocker.Unlock(id) },
}, nil
}
@@ -162,8 +156,8 @@ func (c *Cache) ReadOrCreate(id string,
id = cleanID(id)
c.entryLocker.Lock(id)
defer c.entryLocker.Unlock(id)
c.nlocker.Lock(id)
defer c.nlocker.Unlock(id)
info = ItemInfo{Name: id}
@@ -192,9 +186,9 @@ func (c *Cache) ReadOrCreate(id string,
// NamedLock locks the given id. The lock is released when the returned function is called.
func (c *Cache) NamedLock(id string) func() {
id = cleanID(id)
c.entryLocker.Lock(id)
c.nlocker.Lock(id)
return func() {
c.entryLocker.Unlock(id)
c.nlocker.Unlock(id)
}
}
@@ -207,8 +201,8 @@ func (c *Cache) GetOrCreate(id string, create func() (io.ReadCloser, error)) (It
}
id = cleanID(id)
c.entryLocker.Lock(id)
defer c.entryLocker.Unlock(id)
c.nlocker.Lock(id)
defer c.nlocker.Unlock(id)
info := ItemInfo{Name: id}
@@ -226,7 +220,7 @@ func (c *Cache) GetOrCreate(id string, create func() (io.ReadCloser, error)) (It
return info, nil, err
}
if c.cfg.MaxAge == 0 {
if c.maxAge == 0 {
// No caching.
return info, hugio.ToReadCloser(r), nil
}
@@ -237,40 +231,6 @@ func (c *Cache) GetOrCreate(id string, create func() (io.ReadCloser, error)) (It
c.writeReader(id, io.TeeReader(r, &buff))
}
// AbsFilenameFromID returns the filename for the given id in the cache.
// This will be an absolute path.
func (c *Cache) AbsFilenameFromID(id string) string {
return filepath.Join(c.cfg.DirCompiled, cleanID(id))
}
// GetOrCreateInfo tries to get the item info with the given id from cache. If not found or expired, create will
// be invoked with the id. The create function is expected to create the cache item with the given id. The returned ItemInfo will have the id as Name.
// This method is protected by a named lock using the given id as identifier.
func (c *Cache) GetOrCreateInfo(id string, create func(id string) error) (ItemInfo, error) {
if err := c.init(); err != nil {
return ItemInfo{}, err
}
id = cleanID(id)
c.entryLocker.Lock(id)
defer c.entryLocker.Unlock(id)
info := ItemInfo{Name: id}
if !c.removeIfNeeded(id) {
// The file exists and is not expired, so we consider it a cache hit.
return info, nil
}
if err := create(id); err != nil {
c.remove(id)
return info, err
}
return info, nil
}
func (c *Cache) writeReader(id string, r io.Reader) error {
dir := filepath.Dir(id)
if dir != "" {
@@ -294,8 +254,8 @@ func (c *Cache) GetOrCreateBytes(id string, create func() ([]byte, error)) (Item
}
id = cleanID(id)
c.entryLocker.Lock(id)
defer c.entryLocker.Unlock(id)
c.nlocker.Lock(id)
defer c.nlocker.Unlock(id)
info := ItemInfo{Name: id}
@@ -315,7 +275,7 @@ func (c *Cache) GetOrCreateBytes(id string, create func() ([]byte, error)) (Item
return info, nil, err
}
if c.cfg.MaxAge == 0 {
if c.maxAge == 0 {
return info, b, nil
}
@@ -326,39 +286,15 @@ func (c *Cache) GetOrCreateBytes(id string, create func() ([]byte, error)) (Item
return info, b, nil
}
// SetBytes sets the file content with the given id in the cache.
func (c *Cache) SetBytes(id string, data []byte) error {
if err := c.init(); err != nil {
return err
}
id = cleanID(id)
c.entryLocker.Lock(id)
defer c.entryLocker.Unlock(id)
if c.cfg.MaxAge == 0 {
// No caching.
return nil
}
return c.writeReader(id, bytes.NewReader(data))
}
// GetBytes gets the file content with the given id from the cache, nil if none found.
func (c *Cache) GetBytes(id string) ([]byte, error) {
_, b, err := c.GetItemBytes(id)
return b, err
}
// GetItemBytes gets the ItemInfo and file content with the given id from the cache, nil if none found.
func (c *Cache) GetItemBytes(id string) (ItemInfo, []byte, error) {
func (c *Cache) GetBytes(id string) (ItemInfo, []byte, error) {
if err := c.init(); err != nil {
return ItemInfo{}, nil, err
}
id = cleanID(id)
c.entryLocker.Lock(id)
defer c.entryLocker.Unlock(id)
c.nlocker.Lock(id)
defer c.nlocker.Unlock(id)
info := ItemInfo{Name: id}
@@ -378,8 +314,8 @@ func (c *Cache) Get(id string) (ItemInfo, io.ReadCloser, error) {
}
id = cleanID(id)
c.entryLocker.Lock(id)
defer c.entryLocker.Unlock(id)
c.nlocker.Lock(id)
defer c.nlocker.Unlock(id)
info := ItemInfo{Name: id}
@@ -388,23 +324,10 @@ func (c *Cache) Get(id string) (ItemInfo, io.ReadCloser, error) {
return info, r, nil
}
// removeIfNeeded checks if the file with the given id should be re-created.
func (c *Cache) removeIfNeeded(id string) bool {
if c.cfg.MaxAge == 0 {
// No caching, remove.
c.remove(id)
return true
}
if removed, err := c.removeIfExpired(id); err != nil || removed {
return true
}
return false
}
// getOrRemove gets the file with the given id. If it's expired, it will
// be removed.
func (c *Cache) getOrRemove(id string) hugio.ReadSeekCloser {
if c.cfg.MaxAge == 0 {
if c.maxAge == 0 {
// No caching.
return nil
}
@@ -422,7 +345,7 @@ func (c *Cache) getOrRemove(id string) hugio.ReadSeekCloser {
}
func (c *Cache) getBytesAndRemoveIfExpired(id string) ([]byte, bool) {
if c.cfg.MaxAge == 0 {
if c.maxAge == 0 {
// No caching.
return nil, false
}
@@ -447,7 +370,7 @@ func (c *Cache) getBytesAndRemoveIfExpired(id string) ([]byte, bool) {
}
func (c *Cache) removeIfExpired(id string) (bool, error) {
if c.cfg.MaxAge <= 0 {
if c.maxAge <= 0 {
return false, nil
}
@@ -457,37 +380,29 @@ func (c *Cache) removeIfExpired(id string) (bool, error) {
}
if c.isExpired(fi.ModTime()) {
c.remove(id)
c.Fs.Remove(id)
return true, nil
}
return false, nil
}
func (c *Cache) remove(id string) {
if c.cfg.entryIsDir {
c.Fs.RemoveAll(id)
} else {
c.Fs.Remove(id)
}
}
func (c *Cache) isExpired(modTime time.Time) bool {
if c.cfg.MaxAge < 0 {
if c.maxAge < 0 {
return false
}
// Note the use of time.Since here.
// We cannot use Hugo's global Clock for this.
return c.cfg.MaxAge == 0 || time.Since(modTime) > c.cfg.MaxAge
return c.maxAge == 0 || time.Since(modTime) > c.maxAge
}
// For testing
func (c *Cache) GetString(id string) string {
id = cleanID(id)
c.entryLocker.Lock(id)
defer c.entryLocker.Unlock(id)
c.nlocker.Lock(id)
defer c.nlocker.Unlock(id)
f, err := c.Fs.Open(id)
if err != nil {
@@ -502,17 +417,6 @@ func (c *Cache) GetString(id string) string {
// Caches is a named set of caches.
type Caches map[string]*Cache
func (f Caches) SetResourceFs(fs afero.Fs) {
for _, c := range f {
if c.cfg.IsResourceDir {
if c.isInited {
panic("cannot set resource fs after init")
}
c.Fs = hugofs.NewBasePathFs(fs, c.cfg.DirCompiled)
}
}
}
// Get gets a named cache, nil if none found.
func (f Caches) Get(name string) *Cache {
return f[strings.ToLower(name)]
@@ -520,21 +424,34 @@ func (f Caches) Get(name string) *Cache {
// NewCaches creates a new set of file caches from the given
// configuration.
func NewCaches(dcfg Configs, sourceFs afero.Fs) (Caches, error) {
fs := sourceFs
func NewCaches(p *helpers.PathSpec) (Caches, error) {
dcfg := p.Cfg.GetConfigSection("caches").(Configs)
fs := p.Fs.Source
m := make(Caches)
for k, v := range dcfg {
var cfs afero.Fs
if v.IsResourceDir {
cfs = nil // Set later. TODO(bep) this needs to be cleanded up.
cfs = p.BaseFs.ResourcesCache
} else {
cfs = hugofs.NewBasePathFs(fs, v.DirCompiled)
cfs = fs
}
c := NewCache(cfs, v)
if cfs == nil {
panic("nil fs")
}
m[k] = c
baseDir := v.DirCompiled
bfs := hugofs.NewBasePathFs(cfs, baseDir)
var pruneAllRootDir string
if k == CacheKeyModules {
pruneAllRootDir = "pkg"
}
m[k] = NewCache(bfs, v.MaxAge, pruneAllRootDir)
}
return m, nil
@@ -563,7 +480,7 @@ func (h *httpCache) Get(id string) (resp []byte, ok bool) {
}
func (h *httpCache) Set(id string, resp []byte) {
if h.c.cfg.MaxAge == 0 {
if h.c.maxAge == 0 {
return
}
+33 -96
View File
@@ -15,7 +15,6 @@
package filecache
import (
"encoding/json"
"errors"
"fmt"
"path"
@@ -23,7 +22,7 @@ import (
"strings"
"time"
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/config"
"github.com/mitchellh/mapstructure"
@@ -35,14 +34,19 @@ const (
cacheDirProject = ":cacheDir/:project"
)
var defaultCacheConfig = FileCacheConfig{
MaxAge: -1, // Never expire
Dir: cacheDirProject,
}
const (
CacheKeyImages = "images"
CacheKeyAssets = "assets"
CacheKeyModules = "modules"
CacheKeyModuleQueries = "modulequeries"
CacheKeyModuleGitInfo = "modulegitinfo"
CacheKeyGetResource = "getresource"
CacheKeyMisc = "misc"
CacheKeyGetJSON = "getjson"
CacheKeyGetCSV = "getcsv"
CacheKeyImages = "images"
CacheKeyAssets = "assets"
CacheKeyModules = "modules"
CacheKeyGetResource = "getresource"
CacheKeyMisc = "misc"
)
type Configs map[string]FileCacheConfig
@@ -63,22 +67,9 @@ var defaultCacheConfigs = Configs{
CacheKeyModules: {
MaxAge: -1,
Dir: ":cacheDir/modules",
fileCacheConfigInternal: fileCacheConfigInternal{
entryIsDir: true,
isReadOnly: true, // we need to make it writable when pruning.
},
},
CacheKeyModuleQueries: {
MaxAge: 24 * time.Hour,
Dir: ":cacheDir/modules",
},
CacheKeyModuleGitInfo: {
MaxAge: 24 * time.Hour,
Dir: ":cacheDir/modules",
fileCacheConfigInternal: fileCacheConfigInternal{
entryIsDir: true,
},
},
CacheKeyGetJSON: defaultCacheConfig,
CacheKeyGetCSV: defaultCacheConfig,
CacheKeyImages: {
MaxAge: -1,
Dir: resourcesGenDir,
@@ -97,13 +88,6 @@ var defaultCacheConfigs = Configs{
},
}
func init() {
for k, v := range defaultCacheConfigs {
v.name = k
defaultCacheConfigs[k] = v
}
}
type FileCacheConfig struct {
// Max age of cache entries in this cache. Any items older than this will
// be removed and not returned from the cache.
@@ -115,50 +99,22 @@ type FileCacheConfig struct {
MaxAge time.Duration
// The directory where files are stored.
Dir string
Dir string
DirCompiled string `json:"-"`
fileCacheConfigInternal `json:"-"`
// Will resources/_gen will get its own composite filesystem that
// also checks any theme.
IsResourceDir bool `json:"-"`
}
func (cfg *FileCacheConfig) init() error {
if cfg.DirCompiled == "" {
// From unit tests. Just check that it does not contain any placeholders.
if strings.Contains(cfg.Dir, ":") {
return fmt.Errorf("cache dir %q contains unresolved placeholders", cfg.Dir)
}
cfg.DirCompiled = cfg.Dir
}
// Sanity check the config.
if len(cfg.DirCompiled) < 5 {
panic(fmt.Sprintf("invalid cache dir: %q", cfg.DirCompiled))
}
return nil
// GetJSONCache gets the file cache for getJSON.
func (f Caches) GetJSONCache() *Cache {
return f[CacheKeyGetJSON]
}
type fileCacheConfigInternal struct {
DirCompiled string
name string // The name of this cache, e.g. "images", "modules" etc.
entryIsDir bool // when set, the cache entries represents directories directly below the base dir.
isReadOnly bool // when set, the cache is read only and needs to be pruned differently. This is used for the Go modules cache.
IsResourceDir bool // resources/_gen will get its own composite filesystem that also checks any theme. TODO(bep) unexport this.
}
// MarshalJSON marshals FileCacheConfig to JSON with MaxAge as a human-readable string.
func (c FileCacheConfig) MarshalJSON() ([]byte, error) {
var maxAge any
if c.MaxAge == -1 {
maxAge = -1
} else {
maxAge = strings.TrimSuffix(c.MaxAge.String(), "0m0s")
}
return json.Marshal(&struct {
MaxAge any `json:"maxAge"`
Dir string `json:"dir"`
}{
MaxAge: maxAge,
Dir: c.Dir,
})
// GetCSVCache gets the file cache for getCSV.
func (f Caches) GetCSVCache() *Cache {
return f[CacheKeyGetCSV]
}
// ImageCache gets the file cache for processed images.
@@ -171,25 +127,6 @@ func (f Caches) ModulesCache() *Cache {
return f[CacheKeyModules]
}
// ModuleQueriesCache gets the file cache for Hugo Module version queries.
// Returns nil if not found.
func (f Caches) ModuleQueriesCache() *Cache {
c, ok := f[CacheKeyModuleQueries]
if !ok {
panic("module queries cache not set")
}
return c
}
// ModuleGitInfoCache gets the file cache for Hugo Module git info.
func (f Caches) ModuleGitInfoCache() *Cache {
c, ok := f[CacheKeyModuleGitInfo]
if !ok {
panic("module git info cache not set")
}
return c
}
// AssetsCache gets the file cache for assets (processed resources, SCSS etc.).
func (f Caches) AssetsCache() *Cache {
return f[CacheKeyAssets]
@@ -217,14 +154,10 @@ func DecodeConfig(fs afero.Fs, bcfg config.BaseConfig, m map[string]any) (Config
_, isOsFs := fs.(*afero.OsFs)
for k, v := range m {
if _, ok := v.(hmaps.Params); !ok {
if _, ok := v.(maps.Params); !ok {
continue
}
var ok bool
cc, ok := c[k]
if !ok {
return nil, fmt.Errorf("%q is not a valid cache name", k)
}
cc := defaultCacheConfig
dc := &mapstructure.DecoderConfig{
Result: &cc,
@@ -245,8 +178,12 @@ func DecodeConfig(fs afero.Fs, bcfg config.BaseConfig, m map[string]any) (Config
return c, errors.New("must provide cache Dir")
}
c[k] = cc
name := strings.ToLower(k)
if !valid[name] {
return nil, fmt.Errorf("%q is not a valid cache name", name)
}
c[name] = cc
}
for k, v := range c {
+14 -39
View File
@@ -14,7 +14,6 @@
package filecache_test
import (
"encoding/json"
"path/filepath"
"runtime"
"testing"
@@ -44,9 +43,12 @@ assetDir = "assets"
archetypeDir = "archetypes"
[caches]
[caches.misc]
[caches.getJSON]
maxAge = "10m"
dir = "/path/to/c1"
[caches.getCSV]
maxAge = "11h"
dir = "/path/to/c2"
[caches.images]
dir = "/path/to/c3"
[caches.getResource]
@@ -59,9 +61,9 @@ dir = "/path/to/c4"
decoded := testconfig.GetTestConfigs(fs, cfg).Base.Caches
c.Assert(len(decoded), qt.Equals, 7)
c2 := decoded["misc"]
c.Assert(c2.MaxAge.String(), qt.Equals, "10m0s")
c.Assert(c2.DirCompiled, qt.Equals, filepath.FromSlash("/path/to/c1/filecache/misc"))
c2 := decoded["getcsv"]
c.Assert(c2.MaxAge.String(), qt.Equals, "11h0m0s")
c.Assert(c2.DirCompiled, qt.Equals, filepath.FromSlash("/path/to/c2/filecache/getcsv"))
c3 := decoded["images"]
c.Assert(c3.MaxAge, qt.Equals, time.Duration(-1))
@@ -88,9 +90,12 @@ archeTypedir = "archetypes"
ignoreCache = true
[caches]
[caches.misc]
[caches.getJSON]
maxAge = 1234
dir = "/path/to/c1"
[caches.getCSV]
maxAge = 3456
dir = "/path/to/c2"
[caches.images]
dir = "/path/to/c3"
[caches.getResource]
@@ -127,45 +132,15 @@ func TestDecodeConfigDefault(t *testing.T) {
c.Assert(len(decoded), qt.Equals, 7)
imgConfig := decoded[filecache.CacheKeyImages]
miscConfig := decoded[filecache.CacheKeyMisc]
jsonConfig := decoded[filecache.CacheKeyGetJSON]
if runtime.GOOS == "windows" {
c.Assert(imgConfig.DirCompiled, qt.Equals, filepath.FromSlash("_gen/images"))
} else {
c.Assert(imgConfig.DirCompiled, qt.Equals, "_gen/images")
c.Assert(miscConfig.DirCompiled, qt.Equals, "/cache/thecache/hugoproject/filecache/misc")
c.Assert(jsonConfig.DirCompiled, qt.Equals, "/cache/thecache/hugoproject/filecache/getjson")
}
c.Assert(imgConfig.IsResourceDir, qt.Equals, true)
c.Assert(miscConfig.IsResourceDir, qt.Equals, false)
}
func TestFileCacheConfigMarshalJSON(t *testing.T) {
c := qt.New(t)
cfg := config.New()
cfg.Set("cacheDir", "/cache")
cfg.Set("workingDir", "/my/project")
fs := afero.NewMemMapFs()
decoded := testconfig.GetTestConfigs(fs, cfg).Base.Caches
moduleQueriesConfig := decoded[filecache.CacheKeyModuleQueries]
c.Assert(moduleQueriesConfig.MaxAge, qt.Equals, 24*time.Hour)
// Also verify the new moduleGitInfo cache.
moduleGitInfoConfig := decoded[filecache.CacheKeyModuleGitInfo]
c.Assert(moduleGitInfoConfig.MaxAge, qt.Equals, 24*time.Hour)
b, err := json.Marshal(moduleQueriesConfig)
c.Assert(err, qt.IsNil)
c.Assert(string(b), qt.Contains, `"maxAge":"24h"`)
c.Assert(string(b), qt.Not(qt.Contains), "86400000000000")
c.Assert(string(b), qt.Not(qt.Contains), "8.64e")
moduleQueriesConfig.MaxAge = -1
b, err = json.Marshal(moduleQueriesConfig)
c.Assert(err, qt.IsNil)
c.Assert(string(b), qt.Contains, `"maxAge":-1`)
c.Assert(jsonConfig.IsResourceDir, qt.Equals, false)
}
+7 -43
View File
@@ -50,8 +50,8 @@ func (c Caches) Prune() (int, error) {
// Prune removes expired and unused items from this cache.
// If force is set, everything will be removed not considering expiry time.
func (c *Cache) Prune(force bool) (int, error) {
if c.cfg.entryIsDir {
return c.pruneRootDirs(force)
if c.pruneAllRootDir != "" {
return c.pruneRootDir(force)
}
if err := c.init(); err != nil {
return 0, err
@@ -93,9 +93,9 @@ func (c *Cache) Prune(force bool) (int, error) {
shouldRemove := force || c.isExpired(info.ModTime())
if !shouldRemove && len(c.entryLocker.seen) > 0 {
if !shouldRemove && len(c.nlocker.seen) > 0 {
// Remove it if it's not been touched/used in the last build.
_, seen := c.entryLocker.seen[name]
_, seen := c.nlocker.seen[name]
shouldRemove = !seen
}
@@ -117,43 +117,11 @@ func (c *Cache) Prune(force bool) (int, error) {
return counter, err
}
func (c *Cache) pruneRootDirs(force bool) (int, error) {
dirs, err := afero.ReadDir(c.Fs, "")
if err != nil {
if herrors.IsNotExist(err) {
return 0, nil
}
return 0, err
}
counter := 0
for _, dir := range dirs {
if !dir.IsDir() {
continue
}
count, err := c.pruneRootDir(dir.Name(), force)
if err != nil {
return counter, err
}
counter += count
}
return counter, nil
}
func (c *Cache) pruneRootDir(dirname string, force bool) (int, error) {
func (c *Cache) pruneRootDir(force bool) (int, error) {
if err := c.init(); err != nil {
return 0, err
}
// Sanity check.
if dirname != "pkg" && len(dirname) < 5 {
panic(fmt.Sprintf("invalid cache dir name: %q", dirname))
}
info, err := c.Fs.Stat(dirname)
info, err := c.Fs.Stat(c.pruneAllRootDir)
if err != nil {
if herrors.IsNotExist(err) {
return 0, nil
@@ -165,9 +133,5 @@ func (c *Cache) pruneRootDir(dirname string, force bool) (int, error) {
return 0, nil
}
if c.cfg.isReadOnly {
return hugofs.MakeReadableAndRemoveAllModulePkgDir(c.Fs, dirname)
}
return 1, c.Fs.RemoveAll(dirname)
return hugofs.MakeReadableAndRemoveAllModulePkgDir(c.Fs, c.pruneAllRootDir)
}
+8 -9
View File
@@ -39,9 +39,12 @@ assetDir = "assets"
archeTypedir = "archetypes"
[caches]
[caches.misc]
[caches.getjson]
maxAge = "200ms"
dir = "/cache/c"
[caches.getcsv]
maxAge = "200ms"
dir = "/cache/d"
[caches.assets]
maxAge = "200ms"
dir = ":resourceDir/_gen"
@@ -50,14 +53,11 @@ maxAge = "200ms"
dir = ":resourceDir/_gen"
`
for _, name := range []string{filecache.CacheKeyAssets, filecache.CacheKeyImages} {
for _, name := range []string{filecache.CacheKeyGetCSV, filecache.CacheKeyGetJSON, filecache.CacheKeyAssets, filecache.CacheKeyImages} {
msg := qt.Commentf("cache: %s", name)
fs := afero.NewMemMapFs()
p := newPathsSpec(t, fs, configStr)
fileCachConfig := p.Cfg.GetConfigSection("caches").(filecache.Configs)
caches, err := filecache.NewCaches(fileCachConfig, fs)
p := newPathsSpec(t, afero.NewMemMapFs(), configStr)
caches, err := filecache.NewCaches(p)
c.Assert(err, qt.IsNil)
caches.SetResourceFs(fs)
cache := caches[name]
for i := range 10 {
id := fmt.Sprintf("i%d", i)
@@ -84,9 +84,8 @@ dir = ":resourceDir/_gen"
}
}
caches, err = filecache.NewCaches(fileCachConfig, fs)
caches, err = filecache.NewCaches(p)
c.Assert(err, qt.IsNil)
caches.SetResourceFs(fs)
cache = caches[name]
// Touch one and then prune.
cache.GetOrCreateBytes("i5", func() ([]byte, error) {
+13 -22
View File
@@ -22,8 +22,6 @@ import (
"testing"
"time"
"github.com/gohugoio/hugo/htesting"
"github.com/gohugoio/hugo/cache/filecache"
"github.com/gohugoio/hugo/common/hugio"
"github.com/gohugoio/hugo/config"
@@ -66,7 +64,7 @@ assetDir = "assets"
archeTypedir = "archetypes"
[caches]
[caches.misc]
[caches.getJSON]
maxAge = "10h"
dir = ":cacheDir/c"
@@ -80,13 +78,11 @@ dir = ":cacheDir/c"
configStr = strings.Replace(configStr, "\\", winPathSep, -1)
p := newPathsSpec(t, osfs, configStr)
fileCachConfig := p.Cfg.GetConfigSection("caches").(filecache.Configs)
caches, err := filecache.NewCaches(fileCachConfig, p.Fs.Source)
caches, err := filecache.NewCaches(p)
c.Assert(err, qt.IsNil)
caches.SetResourceFs(p.SourceFs)
cache := caches.Get("Misc")
cache := caches.Get("GetJSON")
c.Assert(cache, qt.Not(qt.IsNil))
cache = caches.Get("Images")
@@ -108,7 +104,7 @@ dir = ":cacheDir/c"
return []byte("bcd"), nil
}
for _, ca := range []*filecache.Cache{caches.ImageCache(), caches.AssetsCache()} {
for _, ca := range []*filecache.Cache{caches.ImageCache(), caches.AssetsCache(), caches.GetJSONCache(), caches.GetCSVCache()} {
for range 2 {
info, r, err := ca.GetOrCreate("a", rf("abc"))
c.Assert(err, qt.IsNil)
@@ -136,6 +132,8 @@ dir = ":cacheDir/c"
}
}
c.Assert(caches.Get("getJSON"), qt.Not(qt.IsNil))
info, w, err := caches.ImageCache().WriteCloser("mykey")
c.Assert(err, qt.IsNil)
c.Assert(info.Name, qt.Equals, "mykey")
@@ -151,7 +149,7 @@ dir = ":cacheDir/c"
r.Close()
c.Assert(string(b), qt.Equals, "Hugo is great!")
info, b, err = caches.ImageCache().GetItemBytes("mykey")
info, b, err = caches.ImageCache().GetBytes("mykey")
c.Assert(err, qt.IsNil)
c.Assert(info.Name, qt.Equals, "mykey")
c.Assert(string(b), qt.Equals, "Hugo is great!")
@@ -160,7 +158,6 @@ dir = ":cacheDir/c"
}
func TestFileCacheConcurrent(t *testing.T) {
htesting.SkipSlowTestUnlessCI(t)
t.Parallel()
c := qt.New(t)
@@ -175,19 +172,18 @@ assetDir = "assets"
archeTypedir = "archetypes"
[caches]
[caches.misc]
[caches.getjson]
maxAge = "1s"
dir = "/cache/c"
`
p := newPathsSpec(t, afero.NewMemMapFs(), configStr)
fileCachConfig := p.Cfg.GetConfigSection("caches").(filecache.Configs)
caches, err := filecache.NewCaches(fileCachConfig, p.Fs.Source)
c.Assert(err, qt.IsNil)
caches.SetResourceFs(p.Fs.Source)
const cacheName = "misc"
caches, err := filecache.NewCaches(p)
c.Assert(err, qt.IsNil)
const cacheName = "getjson"
filenameData := func(i int) (string, string) {
data := fmt.Sprintf("data: %d", i)
@@ -252,12 +248,7 @@ func TestFileCacheReadOrCreateErrorInRead(t *testing.T) {
}
}
cfg := filecache.FileCacheConfig{
MaxAge: 100 * time.Hour,
Dir: "cache/c",
}
cache := filecache.NewCache(afero.NewMemMapFs(), cfg)
cache := filecache.NewCache(afero.NewMemMapFs(), 100*time.Hour, "")
const id = "a32"
-82
View File
@@ -1,82 +0,0 @@
#!/bin/bash
set -e
# Default to all packages if none specified
PACKAGES="${1:-./...}"
echo "==> Checking packages: $PACKAGES"
# Timing arrays
declare -a STEP_NAMES
declare -a STEP_TIMES
time_step() {
local name="$1"
shift
local start=$(date +%s.%N)
"$@"
local end=$(date +%s.%N)
local elapsed=$(echo "$end - $start" | bc)
STEP_NAMES+=("$name")
STEP_TIMES+=("$elapsed")
}
# Check gofmt
run_gofmt() {
echo "==> Running gofmt..."
# Convert package pattern to path (e.g., ./hugolib/... -> ./hugolib)
local path="${PACKAGES%/...}"
GOFMT_OUTPUT=$(gofmt -l "$path" 2>&1) || true
if [ -n "$GOFMT_OUTPUT" ]; then
echo "gofmt found issues in:"
echo "$GOFMT_OUTPUT"
exit 1
fi
echo " OK"
}
# Run staticcheck
run_staticcheck() {
# Check if staticcheck is installed, install if not
if ! command -v staticcheck &> /dev/null; then
echo "==> Installing staticcheck..."
go install honnef.co/go/tools/cmd/staticcheck@latest
fi
echo "==> Running staticcheck..."
staticcheck $PACKAGES
echo " OK"
}
# Run tests
run_tests() {
echo "==> Running tests..."
local output
if ! output=$(go test -failfast $PACKAGES 2>&1); then
echo "$output"
exit 1
fi
echo " OK"
}
# Run all steps with timing
TOTAL_START=$(date +%s.%N)
time_step "gofmt" run_gofmt
time_step "staticcheck" run_staticcheck
time_step "tests" run_tests
TOTAL_END=$(date +%s.%N)
TOTAL_ELAPSED=$(echo "$TOTAL_END - $TOTAL_START" | bc)
# Print timing summary
echo ""
echo "==> All checks passed!"
echo ""
echo "Timing summary:"
echo "---------------"
for i in "${!STEP_NAMES[@]}"; do
printf " %-15s %6.2fs\n" "${STEP_NAMES[$i]}" "${STEP_TIMES[$i]}"
done
echo "---------------"
printf " %-15s %6.2fs\n" "Total" "$TOTAL_ELAPSED"
+1 -1
View File
@@ -73,7 +73,7 @@ func (c *Inspector) MethodsFromTypes(include []reflect.Type, exclude []reflect.T
nameAndPackage := func(t reflect.Type) (string, string) {
var name, pkg string
isPointer := t.Kind() == reflect.Pointer
isPointer := t.Kind() == reflect.Ptr
if isPointer {
t = t.Elem()
+3 -3
View File
@@ -26,9 +26,9 @@ import (
func TestMethods(t *testing.T) {
var (
zeroIE = reflect.TypeFor[IEmbed]()
zeroIEOnly = reflect.TypeFor[IEOnly]()
zeroI = reflect.TypeFor[I]()
zeroIE = reflect.TypeOf((*IEmbed)(nil)).Elem()
zeroIEOnly = reflect.TypeOf((*IEOnly)(nil)).Elem()
zeroI = reflect.TypeOf((*I)(nil)).Elem()
)
dir, _ := os.Getwd()
+11 -26
View File
@@ -97,7 +97,6 @@ type commonConfig struct {
type configKey struct {
counter int32
ignoreModulesDoesNotExists bool
skipNpmCheck bool
}
// This is the root command.
@@ -132,7 +131,6 @@ type rootCommand struct {
gc bool
poll string
forceSyncStatic bool
panicOnWarning bool
// Profile flags (for debugging of performance problems)
cpuprofile string
@@ -196,7 +194,6 @@ func (r *rootCommand) ConfigFromConfig(key configKey, oldConf *commonConfig) (*c
Logger: r.logger,
Environment: r.environment,
IgnoreModuleDoesNotExist: key.ignoreModulesDoesNotExists,
SkipNpmCheck: key.skipNpmCheck,
},
)
if err != nil {
@@ -253,7 +250,6 @@ func (r *rootCommand) ConfigFromProvider(key configKey, cfg config.Provider) (*c
Environment: r.environment,
Logger: r.logger,
IgnoreModuleDoesNotExist: key.ignoreModulesDoesNotExists,
SkipNpmCheck: key.skipNpmCheck,
},
)
if err != nil {
@@ -335,9 +331,6 @@ func (r *rootCommand) ConfigFromProvider(key configKey, cfg config.Provider) (*c
}
func (r *rootCommand) HugFromConfig(conf *commonConfig) (*hugolib.HugoSites, error) {
if conf == nil {
return nil, fmt.Errorf("conf must be set")
}
k := configKey{counter: r.configVersionID.Load()}
h, _, err := r.hugoSites.GetOrCreate(k, func(key configKey) (*hugolib.HugoSites, error) {
depsCfg := r.newDepsConfig(conf)
@@ -492,18 +485,12 @@ func (r *rootCommand) createLogger(running bool) (loggers.Logger, error) {
}
}
var logHookLast func(e *logg.Entry) error
if r.panicOnWarning {
logHookLast = loggers.PanicOnWarningHook
}
optsLogger := loggers.Options{
DistinctLevel: logg.LevelWarn,
Level: level,
StdOut: r.StdOut,
StdErr: r.StdErr,
StoreErrors: running,
HandlerPost: logHookLast,
}
return loggers.New(optsLogger), nil
@@ -530,8 +517,8 @@ func (r *rootCommand) initRootCommand(subCommandName string, cd *simplecobra.Com
commandName = subCommandName
}
cmd.Use = fmt.Sprintf("%s [flags]", commandName)
cmd.Short = "Build your project"
cmd.Long = `COMMAND_NAME is the main command, used to build your Hugo project.
cmd.Short = "Build your site"
cmd.Long = `COMMAND_NAME is the main command, used to build your Hugo site.
Hugo is a Fast and Flexible Static Site Generator
built with love by spf13 and friends in Go.
@@ -601,7 +588,7 @@ func applyLocalFlagsBuild(cmd *cobra.Command, r *rootCommand) {
cmd.Flags().BoolVar(&r.gc, "gc", false, "enable to run some cleanup tasks (remove unused cache files) after the build")
cmd.Flags().StringVar(&r.poll, "poll", "", "set this to a poll interval, e.g --poll 700ms, to use a poll based approach to watch for file system changes")
_ = cmd.RegisterFlagCompletionFunc("poll", cobra.NoFileCompletions)
cmd.Flags().BoolVar(&r.panicOnWarning, "panicOnWarning", false, "panic on first WARNING log")
cmd.Flags().Bool("panicOnWarning", false, "panic on first WARNING log")
cmd.Flags().Bool("templateMetrics", false, "display metrics about template executions")
cmd.Flags().Bool("templateMetricsHints", false, "calculate some improvement hints when combined with --templateMetrics")
cmd.Flags().BoolVar(&r.forceSyncStatic, "forceSyncStatic", false, "copy all files when static is changed.")
@@ -632,14 +619,13 @@ func (r *rootCommand) timeTrack(start time.Time, name string) {
}
type simpleCommand struct {
use string
name string
short string
long string
aliases []string
run func(ctx context.Context, cd *simplecobra.Commandeer, rootCmd *rootCommand, args []string) error
withc func(cmd *cobra.Command, r *rootCommand)
initc func(cd *simplecobra.Commandeer) error
use string
name string
short string
long string
run func(ctx context.Context, cd *simplecobra.Commandeer, rootCmd *rootCommand, args []string) error
withc func(cmd *cobra.Command, r *rootCommand)
initc func(cd *simplecobra.Commandeer) error
commands []simplecobra.Commander
@@ -666,7 +652,6 @@ func (c *simpleCommand) Init(cd *simplecobra.Commandeer) error {
cmd := cd.CobraCommand
cmd.Short = c.short
cmd.Long = c.long
cmd.Aliases = c.aliases
if c.use != "" {
cmd.Use = c.use
}
@@ -684,7 +669,7 @@ func (c *simpleCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
}
func mapLegacyArgs(args []string) []string {
if len(args) > 1 && args[0] == "new" && !hstrings.EqualAny(args[1], "project", "site", "theme", "content") {
if len(args) > 1 && args[0] == "new" && !hstrings.EqualAny(args[1], "site", "theme", "content") {
// Insert "content" as the second argument
args = append(args[:1], append([]string{"content"}, args[1:]...)...)
}
+4 -4
View File
@@ -23,7 +23,7 @@ import (
"time"
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/config/allconfig"
"github.com/gohugoio/hugo/hugolib/sitesmatrix"
"github.com/gohugoio/hugo/modules"
@@ -95,7 +95,7 @@ func (c *configCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
if err := json.Unmarshal(buf.Bytes(), &m); err != nil {
return err
}
hmaps.ConvertFloat64WithNoDecimalsToInt(m)
maps.ConvertFloat64WithNoDecimalsToInt(m)
switch format {
case "yaml":
return parser.InterfaceToConfig(m, metadecoders.YAML, os.Stdout)
@@ -112,8 +112,8 @@ func (c *configCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
func (c *configCommand) Init(cd *simplecobra.Commandeer) error {
c.r = cd.Root.Command.(*rootCommand)
cmd := cd.CobraCommand
cmd.Short = "Display project configuration"
cmd.Long = `Display project configuration, both default and custom settings.`
cmd.Short = "Display site configuration"
cmd.Long = `Display site configuration, both default and custom settings.`
cmd.Flags().StringVar(&c.format, "format", "toml", "preferred file format (toml, yaml or json)")
_ = cmd.RegisterFlagCompletionFunc("format", cobra.FixedCompletions([]string{"toml", "yaml", "json"}, cobra.ShellCompDirectiveNoFileComp))
cmd.Flags().StringVar(&c.lang, "lang", "", "the language to display config for. Defaults to the first language defined.")
-58
View File
@@ -18,12 +18,10 @@ import (
"context"
"fmt"
"path/filepath"
"slices"
"strings"
"time"
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/common/hugio"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs"
@@ -202,56 +200,6 @@ func (c *convertCommand) convertAndSavePage(p page.Page, site *hugolib.Site, tar
return nil
}
func (c *convertCommand) copyContentDirsForOutput(pagesBackedByFile page.Pages) error {
contentDirs := make(map[string]bool)
for _, p := range pagesBackedByFile {
filename := p.File().Filename()
contentDir := strings.TrimSuffix(filename, p.File().Path())
if contentDir == filename {
continue
}
contentDirs[filepath.Clean(contentDir)] = true
}
var contentDirList []string
for contentDir := range contentDirs {
contentDirList = append(contentDirList, contentDir)
}
slices.Sort(contentDirList)
outputDirAbs, err := filepath.Abs(c.outputDir)
if err != nil {
return fmt.Errorf("failed to resolve output path %q: %w", c.outputDir, err)
}
for _, contentDir := range contentDirList {
outputContentDirAbs := filepath.Join(outputDirAbs, filepath.Base(contentDir))
skipDirs := make(map[string]bool)
relToOutputDir, err := filepath.Rel(contentDir, outputDirAbs)
if err == nil && relToOutputDir != ".." && !strings.HasPrefix(relToOutputDir, ".."+string(filepath.Separator)) {
skipDirs[filepath.Clean(outputDirAbs)] = true
}
relToOutputContentDir, err := filepath.Rel(contentDir, outputContentDirAbs)
if err == nil && relToOutputContentDir != ".." && !strings.HasPrefix(relToOutputContentDir, ".."+string(filepath.Separator)) {
skipDirs[filepath.Clean(outputContentDirAbs)] = true
}
var shouldCopy func(filename string) bool
if len(skipDirs) > 0 {
shouldCopy = func(filename string) bool {
return !skipDirs[filepath.Clean(filename)]
}
}
if err := hugio.CopyDir(hugofs.Os, contentDir, outputContentDirAbs, shouldCopy); err != nil {
return fmt.Errorf("failed to copy %q to %q: %w", contentDir, outputContentDirAbs, err)
}
}
return nil
}
func (c *convertCommand) convertContents(format metadecoders.Format) error {
if c.outputDir == "" && !c.unsafe {
return newUserError("Unsafe operation not allowed, use --unsafe or set a different output path")
@@ -271,12 +219,6 @@ func (c *convertCommand) convertContents(format metadecoders.Format) error {
pagesBackedByFile = append(pagesBackedByFile, p)
}
if c.outputDir != "" {
if err := c.copyContentDirsForOutput(pagesBackedByFile); err != nil {
return err
}
}
site.Log.Println("processing", len(pagesBackedByFile), "content files")
for _, p := range site.AllPages() {
if err := c.convertAndSavePage(p, site, format); err != nil {
+2 -2
View File
@@ -27,8 +27,8 @@ import (
func newDeployCommand() simplecobra.Commander {
return &simpleCommand{
name: "deploy",
short: "Deploy your project to a cloud provider",
long: `Deploy your project to a cloud provider
short: "Deploy your site to a cloud provider",
long: `Deploy your site to a cloud provider
See https://gohugo.io/hosting-and-deployment/hugo-deploy/ for detailed
documentation.
+2 -2
View File
@@ -60,7 +60,7 @@ func newGenCommand() *genCommand {
short: "Generate CSS stylesheet for the Chroma code highlighter",
long: `Generate CSS stylesheet for the Chroma code highlighter for a given style. This stylesheet is needed if markup.highlight.noClasses is disabled in config.
See https://gohugo.io/quick-reference/syntax-highlighting-styles/ for a preview of the available styles.`,
See https://xyproto.github.io/splash/docs/all.html for a preview of the available styles`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
style = strings.ToLower(style)
@@ -98,7 +98,7 @@ See https://gohugo.io/quick-reference/syntax-highlighting-styles/ for a preview
},
withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.ValidArgsFunction = cobra.NoFileCompletions
cmd.PersistentFlags().StringVar(&style, "style", "friendly", "highlighter style")
cmd.PersistentFlags().StringVar(&style, "style", "friendly", "highlighter style (see https://xyproto.github.io/splash/docs/)")
_ = cmd.RegisterFlagCompletionFunc("style", cobra.NoFileCompletions)
cmd.PersistentFlags().StringVar(&highlightStyle, "highlightStyle", "", `foreground and background colors for highlighted lines, e.g. --highlightStyle "#fff000 bg:#000fff"`)
_ = cmd.RegisterFlagCompletionFunc("highlightStyle", cobra.NoFileCompletions)
+1 -1
View File
@@ -81,7 +81,7 @@ func flagsToCfgWithAdditionalConfigBase(cd *simplecobra.Commandeer, cfg config.P
"editor": "newContentEditor",
}
// Flags that we for some reason don't want to expose in the project config.
// Flags that we for some reason don't want to expose in the site config.
internalKeySet := map[string]bool{
"quiet": true,
"verbose": true,
+6 -31
View File
@@ -31,11 +31,11 @@ import (
"github.com/bep/simplecobra"
"github.com/fsnotify/fsnotify"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/common/hstrings"
"github.com/gohugoio/hugo/common/htime"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/common/terminal"
"github.com/gohugoio/hugo/common/types"
@@ -56,9 +56,8 @@ import (
type hugoBuilder struct {
r *rootCommand
confmu sync.Mutex
confOld *commonConfig
conf *commonConfig
confmu sync.Mutex
conf *commonConfig
// May be nil.
s *serverCommand
@@ -95,27 +94,6 @@ func (c *hugoBuilder) withConf(fn func(conf *commonConfig)) {
fn(c.conf)
}
func (c *hugoBuilder) withConfOrOldConf(fn func(conf *commonConfig)) {
c.confmu.Lock()
defer c.confmu.Unlock()
if c.conf != nil {
fn(c.conf)
} else if c.confOld != nil {
fn(c.confOld)
}
}
func (c *hugoBuilder) withConfOrOldConfE(fn func(conf *commonConfig) error) error {
c.confmu.Lock()
defer c.confmu.Unlock()
if c.conf != nil {
return fn(c.conf)
} else if c.confOld != nil {
return fn(c.confOld)
}
return errConfigNotSet
}
type hugoBuilderErrState struct {
mu sync.Mutex
paused bool
@@ -179,7 +157,6 @@ func (c *hugoBuilder) initCPUProfile() (func(), error) {
return nil, fmt.Errorf("failed to create CPU profile: %w", err)
}
if err := pprof.StartCPUProfile(f); err != nil {
f.Close()
return nil, fmt.Errorf("failed to start CPU profile: %w", err)
}
return func() {
@@ -1100,7 +1077,7 @@ func (c *hugoBuilder) loadConfig(cd *simplecobra.Commandeer, running bool) error
}
cfg.Set("environment", c.r.environment)
cfg.Set("internal", hmaps.Params{
cfg.Set("internal", maps.Params{
"running": running,
"watch": watch,
"verbose": c.r.isVerbose(),
@@ -1114,11 +1091,10 @@ func (c *hugoBuilder) loadConfig(cd *simplecobra.Commandeer, running bool) error
if len(conf.configs.LoadingInfo.ConfigFiles) == 0 {
//lint:ignore ST1005 end user message.
return errors.New("Unable to locate config file or config directory. Perhaps you need to create a new project.\nRun `hugo help new` for details.")
return errors.New("Unable to locate config file or config directory. Perhaps you need to create a new site.\nRun `hugo help new` for details.")
}
c.conf = conf
c.confOld = conf
if c.onConfigLoaded != nil {
if err := c.onConfigLoaded(false); err != nil {
return err
@@ -1182,9 +1158,8 @@ func (c *hugoBuilder) reloadConfig() error {
c.r.resetLogs()
c.r.configVersionID.Add(1)
if err := c.withConfOrOldConfE(func(conf *commonConfig) error {
if err := c.withConfE(func(conf *commonConfig) error {
oldConf := conf
c.conf = nil
newConf, err := c.r.ConfigFromConfig(configKey{counter: c.r.configVersionID.Load()}, conf)
if err != nil {
return err
+11 -11
View File
@@ -29,9 +29,9 @@ import (
"unicode"
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/common/htime"
"github.com/gohugoio/hugo/common/hugio"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/parser"
@@ -49,7 +49,7 @@ func newImportCommand() *importCommand {
name: "jekyll",
short: "hugo import from Jekyll",
long: `hugo import from Jekyll.
Import from Jekyll requires two paths, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`.",
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
if len(args) < 2 {
@@ -90,8 +90,8 @@ func (c *importCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
func (c *importCommand) Init(cd *simplecobra.Commandeer) error {
cmd := cd.CobraCommand
cmd.Short = "Import a project from another system"
cmd.Long = `Import a project from another system.
cmd.Short = "Import a site from another system"
cmd.Long = `Import a site from another system.
Import requires a subcommand, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`."
@@ -105,7 +105,7 @@ func (c *importCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
}
func (i *importCommand) createConfigFromJekyll(fs afero.Fs, inpath string, kind metadecoders.Format, jekyllConfig map[string]any) (err error) {
title := "My New Hugo Project"
title := "My New Hugo Site"
baseURL := "http://example.org/"
for key, value := range jekyllConfig {
@@ -127,7 +127,7 @@ func (i *importCommand) createConfigFromJekyll(fs afero.Fs, inpath string, kind
in := map[string]any{
"baseURL": baseURL,
"title": title,
"locale": "en-us",
"languageCode": "en-us",
"disablePathToLower": true,
}
@@ -159,7 +159,7 @@ func (c *importCommand) getJekyllDirInfo(fs afero.Fs, jekyllRoot string) (map[st
return postDirs, hasAnyPost
}
func (c *importCommand) createProjectFromJekyll(jekyllRoot, targetDir string, jekyllPostDirs map[string]bool) error {
func (c *importCommand) createSiteFromJekyll(jekyllRoot, targetDir string, jekyllPostDirs map[string]bool) error {
fs := &afero.OsFs{}
if exists, _ := helpers.Exists(targetDir, fs); exists {
if isDir, _ := helpers.IsDir(targetDir, fs); !isDir {
@@ -190,7 +190,7 @@ func (c *importCommand) createProjectFromJekyll(jekyllRoot, targetDir string, je
}
func (c *importCommand) convertJekyllContent(m any, content string) (string, error) {
metadata, _ := hmaps.ToStringMapE(m)
metadata, _ := maps.ToStringMapE(m)
lines := strings.Split(content, "\n")
var resultLines []string
@@ -246,7 +246,7 @@ func (c *importCommand) convertJekyllContent(m any, content string) (string, err
}
func (c *importCommand) convertJekyllMetaData(m any, postName string, postDate time.Time, draft bool) (any, error) {
metadata, err := hmaps.ToStringMapE(m)
metadata, err := maps.ToStringMapE(m)
if err != nil {
return nil, err
}
@@ -419,7 +419,7 @@ func (c *importCommand) importFromJekyll(args []string) error {
return errors.New("abort: jekyll root contains neither posts nor drafts")
}
err = c.createProjectFromJekyll(jekyllRoot, targetDir, jekyllPostDirs)
err = c.createSiteFromJekyll(jekyllRoot, targetDir, jekyllPostDirs)
if err != nil {
return newUserError(err)
}
@@ -463,7 +463,7 @@ func (c *importCommand) importFromJekyll(args []string) error {
}
c.r.Println("Congratulations!", fileCount, "post(s) imported!")
c.r.Println("Now, start Hugo by yourself:")
c.r.Println("Now, start Hugo by yourself:\n")
c.r.Println("cd " + args[1])
c.r.Println("git init")
c.r.Println("git submodule add https://github.com/theNewDynamic/gohugo-theme-ananke themes/ananke")
+13 -23
View File
@@ -21,13 +21,12 @@ import (
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/modules/npm"
"github.com/spf13/cobra"
)
const commonUsageMod = `
Note that Hugo will always start out by resolving the components defined in the project
Note that Hugo will always start out by resolving the components defined in the site
configuration, provided by a _vendor directory (if no --ignoreVendorPaths flag provided),
Go Modules, or a folder inside the themes directory, in that order.
@@ -50,34 +49,28 @@ func newModCommands() *modCommands {
commands: []simplecobra.Commander{
&simpleCommand{
name: "pack",
short: "Merges module Node.js dependencies into an npm workspace",
long: `Merges Node.js dependencies from all Hugo modules into a "packages/hugoautogen" npm workspace.
short: "Experimental: Prepares and writes a composite package.json file for your project",
long: `Prepares and writes a composite package.json file for your project.
The merged dependencies are written to packages/hugoautogen/package.json, and the root package.json
is updated with a "workspaces" entry pointing to "packages/hugoautogen".
On first run it creates a "package.hugo.json" in the project root if not already there. This file will be used as a template file
with the base dependency set.
The source entries are read from either package.hugo.json or package.json in the module root, with package.hugo.json taking precedence if both exist.
This set will be merged with all "package.hugo.json" files found in the dependency tree, picking the version closest to the project.
See [Node.js dependencies](/hugo-modules/nodejs-dependencies/) for more information.
This command is marked as 'Experimental'. We think it's a great idea, so it's not likely to be
removed from Hugo, but we need to test this out in "real life" to get a feel of it,
so this may/will change in future versions of Hugo.
`,
withc: func(cmd *cobra.Command, r *rootCommand) {
cmd.ValidArgsFunction = cobra.NoFileCompletions
applyLocalFlagsBuildConfig(cmd, r)
},
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
cfg := flagsToCfg(cd, nil)
k := configKey{counter: r.configVersionID.Load(), skipNpmCheck: true}
h, _, err := r.hugoSites.GetOrCreate(k, func(key configKey) (*hugolib.HugoSites, error) {
conf, err := r.ConfigFromProvider(key, cfg)
if err != nil {
return nil, err
}
return hugolib.NewHugoSites(r.newDepsConfig(conf))
})
h, err := r.Hugo(flagsToCfg(cd, nil))
if err != nil {
return err
}
return npm.Pack(h.BaseFs.ProjectSourceFs, h.BaseFs.AssetsWithDuplicatesPreserved.Fs, h.Configs.Modules)
return npm.Pack(h.BaseFs.ProjectSourceFs, h.BaseFs.AssetsWithDuplicatesPreserved.Fs)
},
},
},
@@ -173,7 +166,7 @@ Note that for vendored modules, that is the version listed and not the one from
if all {
modCache := h.ResourceSpec.FileCaches.ModulesCache()
count, err := modCache.Prune(true)
r.Printf("Deleted %d directories from module cache.", count)
r.Printf("Deleted %d files from module cache.", count)
return err
}
@@ -300,10 +293,7 @@ Run "go help get" for more information. All flags available for "go get" is also
return err
}
client := conf.configs.ModulesClient
if err := client.Get(args...); err != nil {
return err
}
return nil
return client.Get(args...)
}
},
},
+12 -13
View File
@@ -46,9 +46,9 @@ It will guess which kind of file to create based on the path provided.
You can also specify the kind with ` + "`-k KIND`" + `.
If archetypes are provided in your theme or project, they will be used.
If archetypes are provided in your theme or site, they will be used.
Ensure you run this within the root directory of your project.`,
Ensure you run this within the root directory of your site.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
if len(args) < 1 {
return newUserError("path needs to be provided")
@@ -76,11 +76,10 @@ Ensure you run this within the root directory of your project.`,
},
},
&simpleCommand{
name: "project",
use: "project [path]",
short: "Create a new project",
long: `Create a new project at the specified path.`,
aliases: []string{"site"},
name: "site",
use: "site [path]",
short: "Create a new site",
long: `Create a new site at the specified path.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
if len(args) < 1 {
return newUserError("path needs to be provided")
@@ -100,13 +99,13 @@ Ensure you run this within the root directory of your project.`,
}
sourceFs := conf.fs.Source
err = skeletons.CreateProject(createpath, sourceFs, force, format)
err = skeletons.CreateSite(createpath, sourceFs, force, format)
if err != nil {
return err
}
r.Printf("Congratulations! Your new Hugo project was created in %s.\n\n", createpath)
r.Println(c.newProjectNextStepsText(createpath, format))
r.Printf("Congratulations! Your new Hugo site was created in %s.\n\n", createpath)
r.Println(c.newSiteNextStepsText(createpath, format))
return nil
},
@@ -193,9 +192,9 @@ It will guess which kind of file to create based on the path provided.
You can also specify the kind with ` + "`-k KIND`" + `.
If archetypes are provided in your theme or project, they will be used.
If archetypes are provided in your theme or site, they will be used.
Ensure you run this within the root directory of your project.`
Ensure you run this within the root directory of your site.`
cmd.RunE = nil
return nil
@@ -206,7 +205,7 @@ func (c *newCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
return nil
}
func (c *newCommand) newProjectNextStepsText(path string, format string) string {
func (c *newCommand) newSiteNextStepsText(path string, format string) string {
format = strings.ToLower(format)
var nextStepsText bytes.Buffer
+13 -13
View File
@@ -246,7 +246,7 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
logger := f.c.r.logger
if i == 0 {
r.Printf("Environment: %q\n", f.c.hugoTry().Deps.Site.Hugo().Environment())
r.Printf("Environment: %q\n", f.c.hugoTry().Deps.Site.Hugo().Environment)
mainTarget := "disk"
if f.c.r.renderToMemory {
mainTarget = "memory"
@@ -282,7 +282,7 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string
}
port = 1313
f.c.withConfOrOldConf(func(conf *commonConfig) {
f.c.withConf(func(conf *commonConfig) {
if lrport := conf.configs.GetFirstLanguageConfig().BaseURLLiveReload().Port(); lrport != 0 {
port = lrport
}
@@ -523,7 +523,7 @@ func (c *serverCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, arg
func (c *serverCommand) Init(cd *simplecobra.Commandeer) error {
cmd := cd.CobraCommand
cmd.Short = "Start the embedded web server"
cmd.Long = `Hugo provides its own webserver which builds and serves the project.
cmd.Long = `Hugo provides its own webserver which builds and serves the site.
While hugo server is high performance, it is a webserver with limited options.
The ` + "`" + `hugo server` + "`" + ` command will by default write and serve files from disk, but
@@ -531,8 +531,8 @@ you can render to memory by using the ` + "`" + `--renderToMemory` + "`" + ` fla
faster in some cases, but it will consume more memory.
By default hugo will also watch your files for any changes you make and
automatically rebuild the project. It will then live reload any open browser pages
and push the latest content to them. As most Hugo projects are built in a fraction
automatically rebuild the site. It will then live reload any open browser pages
and push the latest content to them. As most Hugo sites are built in a fraction
of a second, you will be able to save and see your changes nearly instantly.`
cmd.Aliases = []string{"serve"}
@@ -553,7 +553,7 @@ of a second, you will be able to save and see your changes nearly instantly.`
cmd.Flags().BoolVarP(&c.serverAppend, "appendPort", "", true, "append port to baseURL")
cmd.Flags().BoolVar(&c.disableLiveReload, "disableLiveReload", false, "watch without enabling live browser reload on rebuild")
cmd.Flags().BoolVarP(&c.navigateToChanged, "navigateToChanged", "N", false, "navigate to changed content file on live browser reload")
cmd.Flags().BoolVarP(&c.openBrowser, "openBrowser", "O", false, "open the project in a browser after server startup")
cmd.Flags().BoolVarP(&c.openBrowser, "openBrowser", "O", false, "open the site in a browser after server startup")
cmd.Flags().BoolVar(&c.renderStaticToDisk, "renderStaticToDisk", false, "serve static files from disk and dynamic files from memory")
cmd.Flags().BoolVar(&c.disableFastRender, "disableFastRender", false, "enables full re-renders on changes")
cmd.Flags().BoolVar(&c.disableBrowserError, "disableBrowserError", false, "do not show build errors in the browser")
@@ -1160,14 +1160,14 @@ func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error {
return err
}
// chmodFilter is a ChmodFilter for static syncing.
// Returns true to skip syncing permissions for directories and files without
// owner-write permission. The primary use case is files from the module cache (0444).
func chmodFilter(dst, src os.FileInfo) bool {
if src.IsDir() {
return true
}
return src.Mode().Perm()&0o200 == 0
// Hugo publishes data from multiple sources, potentially
// with overlapping directory structures. We cannot sync permissions
// for directories as that would mean that we might end up with write-protected
// directories inside /public.
// One example of this would be syncing from the Go Module cache,
// which have 0555 directories.
return src.IsDir()
}
func cleanErrorLog(content string) string {
+11 -55
View File
@@ -21,24 +21,24 @@ import (
"github.com/gohugoio/hugo/common/hiter"
)
// StackThreadSafe is a simple LIFO stack that is safe for concurrent use.
type StackThreadSafe[T any] struct {
// Stack is a simple LIFO stack that is safe for concurrent use.
type Stack[T any] struct {
items []T
zero T
mu sync.RWMutex
}
func NewStackThreadSafe[T any]() *StackThreadSafe[T] {
return &StackThreadSafe[T]{}
func NewStack[T any]() *Stack[T] {
return &Stack[T]{}
}
func (s *StackThreadSafe[T]) Push(item T) {
func (s *Stack[T]) Push(item T) {
s.mu.Lock()
defer s.mu.Unlock()
s.items = append(s.items, item)
}
func (s *StackThreadSafe[T]) Pop() (T, bool) {
func (s *Stack[T]) Pop() (T, bool) {
s.mu.Lock()
defer s.mu.Unlock()
if len(s.items) == 0 {
@@ -49,7 +49,7 @@ func (s *StackThreadSafe[T]) Pop() (T, bool) {
return item, true
}
func (s *StackThreadSafe[T]) Peek() (T, bool) {
func (s *Stack[T]) Peek() (T, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
if len(s.items) == 0 {
@@ -58,18 +58,18 @@ func (s *StackThreadSafe[T]) Peek() (T, bool) {
return s.items[len(s.items)-1], true
}
func (s *StackThreadSafe[T]) Len() int {
func (s *Stack[T]) Len() int {
s.mu.RLock()
defer s.mu.RUnlock()
return len(s.items)
}
// All returns all items in the stack, from bottom to top.
func (s *StackThreadSafe[T]) All() iter.Seq2[int, T] {
func (s *Stack[T]) All() iter.Seq2[int, T] {
return hiter.Lock2(slices.All(s.items), s.mu.RLock, s.mu.RUnlock)
}
func (s *StackThreadSafe[T]) Drain() []T {
func (s *Stack[T]) Drain() []T {
s.mu.Lock()
defer s.mu.Unlock()
items := s.items
@@ -77,7 +77,7 @@ func (s *StackThreadSafe[T]) Drain() []T {
return items
}
func (s *StackThreadSafe[T]) DrainMatching(predicate func(T) bool) []T {
func (s *Stack[T]) DrainMatching(predicate func(T) bool) []T {
s.mu.Lock()
defer s.mu.Unlock()
var items []T
@@ -89,47 +89,3 @@ func (s *StackThreadSafe[T]) DrainMatching(predicate func(T) bool) []T {
}
return items
}
// Stack is a simple LIFO stack that is not safe for concurrent use.
type Stack[T any] struct {
items []T
zero T
}
func NewStack[T any]() *Stack[T] {
return &Stack[T]{}
}
func (s *Stack[T]) Push(item T) {
s.items = append(s.items, item)
}
func (s *Stack[T]) Pop() (T, bool) {
if len(s.items) == 0 {
return s.zero, false
}
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item, true
}
func (s *Stack[T]) Peek() (T, bool) {
if len(s.items) == 0 {
return s.zero, false
}
return s.items[len(s.items)-1], true
}
func (s *Stack[T]) Len() int {
return len(s.items)
}
func (s *Stack[T]) All() iter.Seq2[int, T] {
return slices.All(s.items)
}
func (s *Stack[T]) Drain() []T {
items := s.items
s.items = nil
return items
}
+4 -4
View File
@@ -10,7 +10,7 @@ func TestNewStack(t *testing.T) {
t.Parallel()
c := qt.New(t)
s := NewStackThreadSafe[int]()
s := NewStack[int]()
c.Assert(s, qt.IsNotNil)
}
@@ -19,7 +19,7 @@ func TestStackBasic(t *testing.T) {
t.Parallel()
c := qt.New(t)
s := NewStackThreadSafe[int]()
s := NewStack[int]()
c.Assert(s.Len(), qt.Equals, 0)
@@ -50,7 +50,7 @@ func TestStackDrain(t *testing.T) {
t.Parallel()
c := qt.New(t)
s := NewStackThreadSafe[string]()
s := NewStack[string]()
s.Push("a")
s.Push("b")
@@ -64,7 +64,7 @@ func TestStackDrainMatching(t *testing.T) {
t.Parallel()
c := qt.New(t)
s := NewStackThreadSafe[int]()
s := NewStack[int]()
s.Push(1)
s.Push(2)
s.Push(3)
+3 -5
View File
@@ -87,13 +87,11 @@ func XXHashFromString(s string) (uint64, error) {
return h.Sum64(), nil
}
// XxHashFromStringHexEncoded calculates the xxHash for the given strings
// XxHashFromStringHexEncoded calculates the xxHash for the given string
// and returns the hash as a hex encoded string.
func XxHashFromStringHexEncoded(s ...string) string {
func XxHashFromStringHexEncoded(f string) string {
h := xxhash.New()
for _, f := range s {
h.WriteString(f)
}
h.WriteString(f)
hash := h.Sum(nil)
return hex.EncodeToString(hash)
}
+4 -2
View File
@@ -38,7 +38,9 @@ func TestXxHashFromReaderPara(t *testing.T) {
var wg sync.WaitGroup
for i := range 10 {
wg.Go(func() {
wg.Add(1)
go func() {
defer wg.Done()
for j := range 100 {
s := strings.Repeat("Hello ", i+j+1*42)
r := strings.NewReader(s)
@@ -48,7 +50,7 @@ func TestXxHashFromReaderPara(t *testing.T) {
expect, _ := XXHashFromString(s)
c.Assert(got, qt.Equals, expect)
}
})
}()
}
wg.Wait()
+24 -44
View File
@@ -288,50 +288,6 @@ func Unwrap(err error) error {
return err
}
// UnwrapFileErrors returns all FileError contained in err.
func UnwrapFileErrors(err error) []FileError {
if err == nil {
return nil
}
errs := Errors(err)
var fileErrors []FileError
for _, e := range errs {
if v, ok := e.(FileError); ok {
fileErrors = append(fileErrors, v)
}
fileErrors = append(fileErrors, UnwrapFileErrors(errors.Unwrap(e))...)
}
return fileErrors
}
// UnwrapFileErrorsWithErrorContext tries to unwrap all FileError in err that has an ErrorContext.
func UnwrapFileErrorsWithErrorContext(err error) []FileError {
errs := UnwrapFileErrors(err)
var n int
for _, e := range errs {
if e.ErrorContext() != nil {
errs[n] = e
n++
}
}
return errs[:n]
}
// Errors returns the list of errors contained in err.
func Errors(err error) []error {
if err == nil {
return nil
}
type unwrapper interface {
Unwrap() []error
}
if u, ok := err.(unwrapper); ok {
return u.Unwrap()
}
return []error{err}
}
func extractFileTypePos(err error) (string, text.Position) {
err = Unwrap(err)
@@ -394,6 +350,30 @@ func UnwrapFileError(err error) FileError {
return nil
}
// UnwrapFileErrors tries to unwrap all FileError.
func UnwrapFileErrors(err error) []FileError {
var errs []FileError
for err != nil {
if v, ok := err.(FileError); ok {
errs = append(errs, v)
}
err = errors.Unwrap(err)
}
return errs
}
// UnwrapFileErrorsWithErrorContext tries to unwrap all FileError in err that has an ErrorContext.
func UnwrapFileErrorsWithErrorContext(err error) []FileError {
var errs []FileError
for err != nil {
if v, ok := err.(FileError); ok && v.ErrorContext() != nil {
errs = append(errs, v)
}
err = errors.Unwrap(err)
}
return errs
}
func extractOffsetAndType(e error) (int, string) {
switch v := e.(type) {
case *json.UnmarshalTypeError:
+3 -3
View File
@@ -32,15 +32,15 @@ func TestNewFileError(t *testing.T) {
fe := NewFileErrorFromName(errors.New("bar"), "foo.html")
c.Assert(fe.Error(), qt.Equals, `"foo.html:1:1": bar`)
var lines strings.Builder
lines := ""
for i := 1; i <= 100; i++ {
lines.WriteString(fmt.Sprintf("line %d\n", i))
lines += fmt.Sprintf("line %d\n", i)
}
fe.UpdatePosition(text.Position{LineNumber: 32, ColumnNumber: 2})
c.Assert(fe.Error(), qt.Equals, `"foo.html:32:2": bar`)
fe.UpdatePosition(text.Position{LineNumber: 0, ColumnNumber: 0, Offset: 212})
fe.UpdateContent(strings.NewReader(lines.String()), nil)
fe.UpdateContent(strings.NewReader(lines), nil)
c.Assert(fe.Error(), qt.Equals, `"foo.html:32:0": bar`)
errorContext := fe.ErrorContext()
c.Assert(errorContext, qt.IsNotNil)
+3 -3
View File
@@ -27,8 +27,8 @@ import (
"sync"
"github.com/bep/logg"
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/config/security"
)
@@ -102,7 +102,7 @@ func New(cfg security.Config, workingDir string, log loggers.Logger) *Exec {
workingDir: workingDir,
infol: log.InfoCommand("exec"),
baseEnviron: baseEnviron,
newNPXRunnerCache: hmaps.NewCache[string, func(arg ...any) (Runner, error)](),
newNPXRunnerCache: maps.NewCache[string, func(arg ...any) (Runner, error)](),
}
}
@@ -121,7 +121,7 @@ type Exec struct {
// os.Environ filtered by the Exec.OsEnviron whitelist filter.
baseEnviron []string
newNPXRunnerCache *hmaps.Cache[string, func(arg ...any) (Runner, error)]
newNPXRunnerCache *maps.Cache[string, func(arg ...any) (Runner, error)]
npxInit sync.Once
npxAvailable bool
}
-67
View File
@@ -1,67 +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 himage provides some high level image types and interfaces.
package himage
import "image"
// AnimatedImage represents an animated image.
// This is currently supported for GIF and WebP images.
type AnimatedImage interface {
image.Image // The first frame.
GetRaw() any // *gif.GIF or *WEBP.
GetLoopCount() int // Number of times to loop the animation. 0 means infinite.
ImageFrames
}
// ImageFrames provides access to the frames of an animated image.
type ImageFrames interface {
GetFrames() []image.Image
// Frame durations in milliseconds.
// Note that Gif frame durations are in 100ths of a second,
// so they need to be multiplied by 10 to get milliseconds and vice versa.
GetFrameDurations() []int
SetFrames(frames []image.Image)
SetWidthHeight(width, height int)
}
// ImageConfigProvider provides access to the image.Config of an image.
type ImageConfigProvider interface {
GetImageConfig() image.Config
}
// FrameDurationsToGifDelays converts frame durations in milliseconds to
// GIF delays in 100ths of a second.
func FrameDurationsToGifDelays(frameDurations []int) []int {
delays := make([]int, len(frameDurations))
for i, fd := range frameDurations {
delays[i] = fd / 10
if delays[i] == 0 && fd > 0 {
delays[i] = 1
}
}
return delays
}
// GifDelaysToFrameDurations converts GIF delays in 100ths of a second to
// frame durations in milliseconds.
func GifDelaysToFrameDurations(delays []int) []int {
frameDurations := make([]int, len(delays))
for i, d := range delays {
frameDurations[i] = d * 10
}
return frameDurations
}
+1 -1
View File
@@ -96,7 +96,7 @@ func errConvert(v reflect.Value, s string) error {
// See Issue 14079.
func ConvertIfPossible(val reflect.Value, typ reflect.Type) (reflect.Value, bool) {
switch val.Kind() {
case reflect.Pointer, reflect.Interface:
case reflect.Ptr, reflect.Interface:
if val.IsNil() {
// Return typ's zero value.
return reflect.Zero(typ), true
+10 -10
View File
@@ -20,14 +20,14 @@ import (
"sync"
"time"
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/common/htime"
"github.com/gohugoio/hugo/common/maps"
"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.Pointer
return kind == reflect.Interface || kind == reflect.Ptr
}
// TODO(bep) replace the private versions in /tpl with these.
@@ -92,7 +92,7 @@ func IsSlice(v any) bool {
return reflect.ValueOf(v).Kind() == reflect.Slice
}
var zeroType = reflect.TypeFor[types.Zeroer]()
var zeroType = reflect.TypeOf((*types.Zeroer)(nil)).Elem()
var isZeroCache sync.Map
@@ -136,7 +136,7 @@ func IsTruthfulValue(val reflect.Value) (truth bool) {
truth = val.Bool()
case reflect.Complex64, reflect.Complex128:
truth = val.Complex() != 0
case reflect.Chan, reflect.Func, reflect.Pointer, reflect.UnsafePointer, reflect.Interface:
case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Interface:
truth = !val.IsNil()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
truth = val.Int() != 0
@@ -216,8 +216,8 @@ func GetMethodIndexByName(tp reflect.Type, name string) int {
}
var (
timeType = reflect.TypeFor[time.Time]()
asTimeProviderType = reflect.TypeFor[htime.AsTimeProvider]()
timeType = reflect.TypeOf((*time.Time)(nil)).Elem()
asTimeProviderType = reflect.TypeOf((*htime.AsTimeProvider)(nil)).Elem()
)
// IsTime returns whether tp is a time.Time type or if it can be converted into one
@@ -240,7 +240,7 @@ func IsValid(v reflect.Value) bool {
}
switch v.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
return !v.IsNil()
}
@@ -347,15 +347,15 @@ func IsNil(v reflect.Value) bool {
return true
}
switch v.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
return v.IsNil()
}
return false
}
var contextInterface = reflect.TypeFor[context.Context]()
var contextInterface = reflect.TypeOf((*context.Context)(nil)).Elem()
var isContextCache = hmaps.NewCache[reflect.Type, bool]()
var isContextCache = maps.NewCache[reflect.Type, bool]()
type k string
+18 -18
View File
@@ -18,10 +18,10 @@ import (
"strings"
"time"
"github.com/bep/golocales"
"github.com/bep/clocks"
"github.com/spf13/cast"
"github.com/gohugoio/locales"
)
var (
@@ -78,9 +78,9 @@ var (
Clock = clocks.System()
)
func NewTimeFormatter(ltr golocales.Translator) TimeFormatter {
func NewTimeFormatter(ltr locales.Translator) TimeFormatter {
if ltr == nil {
panic("must provide a golocales.Translator")
panic("must provide a locales.Translator")
}
return TimeFormatter{
ltr: ltr,
@@ -89,7 +89,7 @@ func NewTimeFormatter(ltr golocales.Translator) TimeFormatter {
// TimeFormatter is locale aware.
type TimeFormatter struct {
ltr golocales.Translator
ltr locales.Translator
}
func (f TimeFormatter) Format(t time.Time, layout string) string {
@@ -101,39 +101,39 @@ func (f TimeFormatter) Format(t time.Time, layout string) string {
// It may be one of Hugo's custom layouts.
switch strings.ToLower(layout[1:]) {
case "date_full":
return f.ltr.FormatDateFull(t)
return f.ltr.FmtDateFull(t)
case "date_long":
return f.ltr.FormatDateLong(t)
return f.ltr.FmtDateLong(t)
case "date_medium":
return f.ltr.FormatDateMedium(t)
return f.ltr.FmtDateMedium(t)
case "date_short":
return f.ltr.FormatDateShort(t)
return f.ltr.FmtDateShort(t)
case "time_full":
return f.ltr.FormatTimeFull(t)
return f.ltr.FmtTimeFull(t)
case "time_long":
return f.ltr.FormatTimeLong(t)
return f.ltr.FmtTimeLong(t)
case "time_medium":
return f.ltr.FormatTimeMedium(t)
return f.ltr.FmtTimeMedium(t)
case "time_short":
return f.ltr.FormatTimeShort(t)
return f.ltr.FmtTimeShort(t)
}
}
s := t.Format(layout)
monthIdx := t.Month() - 1 // time.Month is 1-based, but our month name slices are 0-based.
monthIdx := t.Month() - 1 // Month() starts at 1.
dayIdx := t.Weekday()
if strings.Contains(layout, "January") {
s = strings.ReplaceAll(s, longMonthNames[monthIdx], f.ltr.MonthsWide()[monthIdx])
s = strings.ReplaceAll(s, longMonthNames[monthIdx], f.ltr.MonthWide(t.Month()))
} else if strings.Contains(layout, "Jan") {
s = strings.ReplaceAll(s, shortMonthNames[monthIdx], f.ltr.MonthsAbbreviated()[monthIdx])
s = strings.ReplaceAll(s, shortMonthNames[monthIdx], f.ltr.MonthAbbreviated(t.Month()))
}
if strings.Contains(layout, "Monday") {
s = strings.ReplaceAll(s, longDayNames[dayIdx], f.ltr.WeekdaysWide()[dayIdx])
s = strings.ReplaceAll(s, longDayNames[dayIdx], f.ltr.WeekdayWide(t.Weekday()))
} else if strings.Contains(layout, "Mon") {
s = strings.ReplaceAll(s, shortDayNames[dayIdx], f.ltr.WeekdaysAbbreviated()[dayIdx])
s = strings.ReplaceAll(s, shortDayNames[dayIdx], f.ltr.WeekdayAbbreviated(t.Weekday()))
}
return s
+18 -19
View File
@@ -17,9 +17,8 @@ import (
"testing"
"time"
"github.com/bep/golocales"
qt "github.com/frankban/quicktest"
translators "github.com/gohugoio/localescompressed"
)
func TestTimeFormatter(t *testing.T) {
@@ -35,7 +34,7 @@ func TestTimeFormatter(t *testing.T) {
mondayNovemberFirst = mondayNovemberFirst.Add(33 * time.Second)
c.Run("Norsk nynorsk", func(c *qt.C) {
f := NewTimeFormatter(golocales.New("nn"))
f := NewTimeFormatter(translators.GetTranslator("nn"))
c.Assert(f.Format(june06, "Monday Jan 2 2006"), qt.Equals, "onsdag juni 6 2018")
c.Assert(f.Format(june06, "Mon January 2 2006"), qt.Equals, "on. juni 6 2018")
@@ -43,35 +42,35 @@ func TestTimeFormatter(t *testing.T) {
})
c.Run("Custom layouts Norsk nynorsk", func(c *qt.C) {
f := NewTimeFormatter(golocales.New("nn"))
f := NewTimeFormatter(translators.GetTranslator("nn"))
c.Assert(f.Format(june06, ":date_full"), qt.Equals, "onsdag 6. juni 2018")
c.Assert(f.Format(june06, ":date_long"), qt.Equals, "6. juni 2018")
c.Assert(f.Format(june06, ":date_medium"), qt.Equals, "6. juni 2018")
c.Assert(f.Format(june06, ":date_short"), qt.Equals, "06.06.18")
c.Assert(f.Format(june06, ":date_short"), qt.Equals, "06.06.2018")
c.Assert(f.Format(june06, ":time_full"), qt.Equals, "02:09:37 UTC")
c.Assert(f.Format(june06, ":time_full"), qt.Equals, "kl. 02:09:37 UTC")
c.Assert(f.Format(june06, ":time_long"), qt.Equals, "02:09:37 UTC")
c.Assert(f.Format(june06, ":time_medium"), qt.Equals, "02:09:37")
c.Assert(f.Format(june06, ":time_short"), qt.Equals, "02:09")
})
c.Run("Custom layouts English", func(c *qt.C) {
f := NewTimeFormatter(golocales.New("en"))
f := NewTimeFormatter(translators.GetTranslator("en"))
c.Assert(f.Format(june06, ":date_full"), qt.Equals, "Wednesday, June 6, 2018")
c.Assert(f.Format(june06, ":date_long"), qt.Equals, "June 6, 2018")
c.Assert(f.Format(june06, ":date_medium"), qt.Equals, "Jun 6, 2018")
c.Assert(f.Format(june06, ":date_short"), qt.Equals, "6/6/18")
c.Assert(f.Format(june06, ":time_full"), qt.Equals, "2:09:37\u202fam UTC")
c.Assert(f.Format(june06, ":time_long"), qt.Equals, "2:09:37\u202fam UTC")
c.Assert(f.Format(june06, ":time_medium"), qt.Equals, "2:09:37\u202fam")
c.Assert(f.Format(june06, ":time_short"), qt.Equals, "2:09\u202fam")
c.Assert(f.Format(june06, ":time_full"), qt.Equals, "2:09:37 am UTC")
c.Assert(f.Format(june06, ":time_long"), qt.Equals, "2:09:37 am UTC")
c.Assert(f.Format(june06, ":time_medium"), qt.Equals, "2:09:37 am")
c.Assert(f.Format(june06, ":time_short"), qt.Equals, "2:09 am")
})
c.Run("English", func(c *qt.C) {
f := NewTimeFormatter(golocales.New("en"))
f := NewTimeFormatter(translators.GetTranslator("en"))
c.Assert(f.Format(june06, "Monday Jan 2 2006"), qt.Equals, "Wednesday Jun 6 2018")
c.Assert(f.Format(june06, "Mon January 2 2006"), qt.Equals, "Wed June 6 2018")
@@ -79,31 +78,31 @@ func TestTimeFormatter(t *testing.T) {
})
c.Run("Weekdays German", func(c *qt.C) {
tr := golocales.New("de")
tr := translators.GetTranslator("de")
f := NewTimeFormatter(tr)
// Issue #9107
for i, weekDayWideGerman := range []string{"Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag", "Sonntag"} {
date := mondayNovemberFirst.Add(time.Duration(i*24) * time.Hour)
c.Assert(tr.WeekdaysWide()[date.Weekday()], qt.Equals, weekDayWideGerman)
c.Assert(tr.WeekdayWide(date.Weekday()), qt.Equals, weekDayWideGerman)
c.Assert(f.Format(date, "Monday"), qt.Equals, weekDayWideGerman)
}
for i, weekDayAbbreviatedGerman := range []string{"Mo.", "Di.", "Mi.", "Do.", "Fr.", "Sa.", "So."} {
date := mondayNovemberFirst.Add(time.Duration(i*24) * time.Hour)
c.Assert(tr.WeekdaysAbbreviated()[date.Weekday()], qt.Equals, weekDayAbbreviatedGerman)
c.Assert(tr.WeekdayAbbreviated(date.Weekday()), qt.Equals, weekDayAbbreviatedGerman)
c.Assert(f.Format(date, "Mon"), qt.Equals, weekDayAbbreviatedGerman)
}
})
c.Run("Months German", func(c *qt.C) {
tr := golocales.New("de")
tr := translators.GetTranslator("de")
f := NewTimeFormatter(tr)
// Issue #9107
for i, monthWideNorway := range []string{"Januar", "Februar", "März", "April", "Mai", "Juni", "Juli"} {
date := jan06.Add(time.Duration(i*24*31) * time.Hour)
c.Assert(tr.MonthsWide()[date.Month()-1], qt.Equals, monthWideNorway)
c.Assert(tr.MonthWide(date.Month()), qt.Equals, monthWideNorway)
c.Assert(f.Format(date, "January"), qt.Equals, monthWideNorway)
}
})
@@ -122,7 +121,7 @@ func BenchmarkTimeFormatter(b *testing.B) {
})
b.Run("Localized", func(b *testing.B) {
f := NewTimeFormatter(golocales.New("nn"))
f := NewTimeFormatter(translators.GetTranslator("nn"))
b.ResetTimer()
for b.Loop() {
got := f.Format(june06, "Monday Jan 2 2006")
@@ -133,7 +132,7 @@ func BenchmarkTimeFormatter(b *testing.B) {
})
b.Run("Localized Custom", func(b *testing.B) {
f := NewTimeFormatter(golocales.New("nn"))
f := NewTimeFormatter(translators.GetTranslator("nn"))
b.ResetTimer()
for b.Loop() {
got := f.Format(june06, ":date_medium")
+9 -43
View File
@@ -19,37 +19,19 @@ import (
"strings"
)
// ReadSeeker wraps io.Reader and io.Seeker.
type ReadSeeker interface {
io.Reader
io.Seeker
}
// ReadSeekCloser is implemented by afero.File. We use this as the common type for
// content in Resource objects, even for strings.
type ReadSeekCloser interface {
io.ReadSeeker
ReadSeeker
io.Closer
}
// Sizer provides the size of, typically, a io.Reader.
// As implemented by e.g. os.File and io.SectionReader.
type Sizer interface {
Size() int64
}
type SizeReader interface {
io.Reader
Sizer
}
// ToSizeReader converts the given io.Reader to a SizeReader.
// Note that if r is not a SizeReader, the entire content will be read into memory
func ToSizeReader(r io.Reader) (SizeReader, error) {
if sr, ok := r.(SizeReader); ok {
return sr, nil
}
b, err := io.ReadAll(r)
if err != nil {
return nil, err
}
return bytes.NewReader(b), nil
}
// CloserFunc is an adapter to allow the use of ordinary functions as io.Closers.
type CloserFunc func() error
@@ -64,7 +46,7 @@ type ReadSeekCloserProvider interface {
// readSeekerNopCloser implements ReadSeekCloser by doing nothing in Close.
type readSeekerNopCloser struct {
io.ReadSeeker
ReadSeeker
}
// Close does nothing.
@@ -73,7 +55,7 @@ func (r readSeekerNopCloser) Close() error {
}
// NewReadSeekerNoOpCloser creates a new ReadSeekerNoOpCloser with the given ReadSeeker.
func NewReadSeekerNoOpCloser(r io.ReadSeeker) ReadSeekCloser {
func NewReadSeekerNoOpCloser(r ReadSeeker) ReadSeekCloser {
return readSeekerNopCloser{r}
}
@@ -105,22 +87,6 @@ func NewReadSeekerNoOpCloserFromBytes(content []byte) readSeekerNopCloser {
return readSeekerNopCloser{bytes.NewReader(content)}
}
// NewReadSeekerNoOpCloserFromReader creates a new ReadSeekerNoOpCloser from the given io.Reader.
// If the given io.Reader is not an io.ReadSeeker, the entire content will be read into memory.
func NewReadSeekerNoOpCloserFromReader(r io.Reader) (readSeekerNopCloser, error) {
var rs io.ReadSeeker
if s, ok := r.(io.ReadSeeker); ok {
rs = s
} else {
b, err := io.ReadAll(r)
if err != nil {
return readSeekerNopCloser{rs}, err
}
rs = bytes.NewReader(b)
}
return readSeekerNopCloser{rs}, nil
}
// NewOpenReadSeekCloser creates a new ReadSeekCloser from the given ReadSeeker.
// The ReadSeeker will be seeked to the beginning before returned.
func NewOpenReadSeekCloser(r ReadSeekCloser) OpenReadSeekCloser {
+124 -24
View File
@@ -16,6 +16,7 @@ package hugo
import (
"context"
"fmt"
"html/template"
"os"
"path/filepath"
"runtime/debug"
@@ -24,16 +25,17 @@ import (
"sync"
"time"
"github.com/bep/helpers/contexthelpers"
"github.com/bep/logg"
"github.com/bep/godartsass/v2"
"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/hugofs/files"
"github.com/bep/helpers/contexthelpers"
"github.com/spf13/afero"
iofs "io/fs"
@@ -54,24 +56,89 @@ var (
vendorInfo string
)
// BuildInfo holds build information extracted from runtime/debug.
type BuildInfo struct {
Revision string
RevisionTime string
GoVersion string
var _ hstore.StoreProvider = (*HugoInfo)(nil)
// HugoInfo contains information about the current Hugo environment
type HugoInfo struct {
CommitHash string
BuildDate string
// The build environment.
// Defaults are "production" (hugo) and "development" (hugo server).
// This can also be set by the user.
// It can be any string, but it will be all lower case.
Environment string
// version of go that the Hugo binary was built with
GoVersion string
conf ConfigProvider
deps []*Dependency
store *hstore.Scratch
// Context gives access to some of the context scoped variables.
Context Context
}
// GetBuildInfo returns the build info for the current binary.
func GetBuildInfo() *BuildInfo {
bi := getBuildInfo()
if bi == nil {
return nil
}
return &BuildInfo{
Revision: bi.Revision,
RevisionTime: bi.RevisionTime,
GoVersion: bi.GoVersion,
}
// Version returns the current version as a comparable version string.
func (i HugoInfo) Version() version.VersionString {
return CurrentVersion.Version()
}
// Generator a Hugo meta generator HTML tag.
func (i HugoInfo) Generator() template.HTML {
return template.HTML(fmt.Sprintf(`<meta name="generator" content="Hugo %s">`, CurrentVersion.String()))
}
// IsDevelopment reports whether the current running environment is "development".
func (i HugoInfo) IsDevelopment() bool {
return i.Environment == EnvironmentDevelopment
}
// IsProduction reports whether the current running environment is "production".
func (i HugoInfo) IsProduction() bool {
return i.Environment == EnvironmentProduction
}
// IsServer reports whether the built-in server is running.
func (i HugoInfo) IsServer() bool {
return i.conf.Running()
}
// IsExtended reports whether the Hugo binary is the extended version.
func (i HugoInfo) IsExtended() bool {
return IsExtended
}
// WorkingDir returns the project working directory.
func (i HugoInfo) WorkingDir() string {
return i.conf.WorkingDir()
}
// Deps gets a list of dependencies for this Hugo build.
func (i HugoInfo) Deps() []*Dependency {
return i.deps
}
func (i HugoInfo) Store() *hstore.Scratch {
return i.store
}
// Deprecated: Use hugo.IsMultihost instead.
func (i HugoInfo) IsMultiHost() bool {
Deprecate("hugo.IsMultiHost", "Use hugo.IsMultihost instead.", "v0.124.0")
return i.conf.IsMultihost()
}
// IsMultihost reports whether each configured language has a unique baseURL.
func (i HugoInfo) IsMultihost() bool {
return i.conf.IsMultihost()
}
// IsMultilingual reports whether there are two or more configured languages.
func (i HugoInfo) IsMultilingual() bool {
return i.conf.IsMultilingual()
}
type contextKey uint8
@@ -82,7 +149,6 @@ const (
var markupScope = contexthelpers.NewContextDispatcher[string](contextKeyMarkupScope)
// Context gives access to some of the context scoped variables.
type Context struct{}
func (c Context) MarkupScope(ctx context.Context) string {
@@ -99,6 +165,44 @@ func GetMarkupScope(ctx context.Context) string {
return markupScope.Get(ctx)
}
// ConfigProvider represents the config options that are relevant for HugoInfo.
type ConfigProvider interface {
Environment() string
Running() bool
WorkingDir() string
IsMultihost() bool
IsMultilingual() bool
}
// NewInfo creates a new Hugo Info object.
func NewInfo(conf ConfigProvider, deps []*Dependency) HugoInfo {
if conf.Environment() == "" {
panic("environment not set")
}
var (
commitHash string
buildDate string
goVersion string
)
bi := getBuildInfo()
if bi != nil {
commitHash = bi.Revision
buildDate = bi.RevisionTime
goVersion = bi.GoVersion
}
return HugoInfo{
CommitHash: commitHash,
BuildDate: buildDate,
Environment: conf.Environment(),
conf: conf,
deps: deps,
store: hstore.NewScratch(),
GoVersion: goVersion,
}
}
// GetExecEnviron creates and gets the common os/exec environment used in the
// external programs we interact with via os/exec, e.g. postcss.
func GetExecEnviron(workDir string, cfg config.AllProvider, fs afero.Fs) []string {
@@ -206,12 +310,13 @@ func GetDependencyList() []string {
// GetDependencyListNonGo returns a list of non-Go dependencies.
func GetDependencyListNonGo() []string {
deps := []string{formatDep("github.com/webmproject/libwebp", "v1.6.0")} // via WASM. TODO(bep) get versions from the plugin setup.
var deps []string
if IsExtended {
deps = append(
deps,
formatDep("github.com/sass/libsass", "3.6.6"),
formatDep("github.com/webmproject/libwebp", "v1.3.2"),
)
}
@@ -334,13 +439,8 @@ func deprecateLevel(item, alternative, version string, level logg.Level) {
// DeprecateLevel informs about a deprecation logging at the given level.
func deprecateLevelWithLogger(item, alternative, version string, level logg.Level, log logg.Logger) {
//if strings.Contains(item, "module.mounts.lang") || strings.Contains(item, "includeFiles") {
// hdebug.Panicf("Deprecated")
//}
var msg string
if level == logg.LevelError {
// Useful to debug deprecation errors that needs to be removedor fixed. Comment out when done debugging.
// hdebug.Panicf("deprecation error: %s was removed in Hugo %s. %s", item, version, alternative)
msg = fmt.Sprintf("%s was deprecated in Hugo %s and subsequently removed. %s", item, version, alternative)
} else {
msg = fmt.Sprintf("%s was deprecated in Hugo %s and will be removed in a future release. %s", item, version, alternative)
-36
View File
@@ -75,39 +75,3 @@ multihost={{ hugo.IsMultihost }}
"multihost=false",
)
}
func TestHugoSites(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ['page','rss','section','sitemap','taxonomy','term']
defaultContentLanguage = 'fr'
defaultContentLanguageInSubdir = true
defaultContentVersionInSubdir = true
defaultContentRoleInSubdir = true
[languages]
[languages.en]
weight = 1
[languages.fr]
weight = 2
[languages.de]
weight = 3
[roles]
[roles.guest]
weight = 1
[roles.member]
weight = 2
[versions]
[versions.'v1.0.0']
weight = 1
[versions.'v2.0.0']
weight = 2
-- layouts/home.html --
{{ range hugo.Sites }}{{ .Language.Name }}-{{ .Role.Name }}-{{ .Version.Name }}|{{ end }}
`
b := hugolib.Test(t, files)
b.AssertFileContent("public/guest/v1.0.0/en/index.html", "en-guest-v1.0.0|en-member-v1.0.0|en-guest-v2.0.0|en-member-v2.0.0|fr-guest-v1.0.0|fr-member-v1.0.0|fr-guest-v2.0.0|fr-member-v2.0.0|de-guest-v1.0.0|de-member-v1.0.0|de-guest-v2.0.0|de-member-v2.0.0|")
}
+64 -13
View File
@@ -15,12 +15,43 @@ package hugo
import (
"context"
"fmt"
"testing"
"github.com/bep/logg"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/common/version"
)
func TestHugoInfo(t *testing.T) {
c := qt.New(t)
conf := testConfig{environment: "production", workingDir: "/mywork", running: false}
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(hugoInfo.WorkingDir(), qt.Equals, "/mywork")
bi := getBuildInfo()
if bi != nil {
c.Assert(hugoInfo.CommitHash, qt.Equals, bi.Revision)
c.Assert(hugoInfo.BuildDate, qt.Equals, bi.RevisionTime)
c.Assert(hugoInfo.GoVersion, qt.Equals, bi.GoVersion)
}
c.Assert(hugoInfo.Environment, qt.Equals, "production")
c.Assert(string(hugoInfo.Generator()), qt.Contains, fmt.Sprintf("Hugo %s", hugoInfo.Version()))
c.Assert(hugoInfo.IsDevelopment(), qt.Equals, false)
c.Assert(hugoInfo.IsProduction(), qt.Equals, true)
c.Assert(hugoInfo.IsExtended(), qt.Equals, IsExtended)
c.Assert(hugoInfo.IsServer(), qt.Equals, false)
devHugoInfo := NewInfo(testConfig{environment: "development", running: true}, nil)
c.Assert(devHugoInfo.IsDevelopment(), qt.Equals, true)
c.Assert(devHugoInfo.IsProduction(), qt.Equals, false)
c.Assert(devHugoInfo.IsServer(), qt.Equals, true)
}
func TestDeprecationLogLevelFromVersion(t *testing.T) {
c := qt.New(t)
@@ -35,27 +66,47 @@ func TestDeprecationLogLevelFromVersion(t *testing.T) {
c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelError)
// Added just to find the threshold for where we can remove deprecated items.
// Subtract 5 from the minor version of the first ERRORed version => 0.136.0.
c.Assert(deprecationLogLevelFromVersion("0.141.0"), qt.Equals, logg.LevelError)
// Subtract 5 from the minor version of the first ERRORed version => 0.122.0.
c.Assert(deprecationLogLevelFromVersion("0.127.0"), qt.Equals, logg.LevelError)
}
func TestMarkupScope(t *testing.T) {
c := qt.New(t)
conf := testConfig{environment: "production", workingDir: "/mywork", running: false}
info := NewInfo(conf, nil)
ctx := context.Background()
ctx = SetMarkupScope(ctx, "foo")
var hugoCtx Context
c.Assert(hugoCtx.MarkupScope(ctx), qt.Equals, "foo")
c.Assert(GetMarkupScope(ctx), qt.Equals, "foo")
c.Assert(info.Context.MarkupScope(ctx), qt.Equals, "foo")
}
func TestGetBuildInfo(t *testing.T) {
c := qt.New(t)
bi := GetBuildInfo()
// In test mode, build info may or may not be available.
if bi != nil {
c.Assert(bi.GoVersion, qt.Not(qt.Equals), "")
}
type testConfig struct {
environment string
running bool
workingDir string
multihost bool
multilingual bool
}
func (c testConfig) Environment() string {
return c.environment
}
func (c testConfig) Running() bool {
return c.running
}
func (c testConfig) WorkingDir() string {
return c.workingDir
}
func (c testConfig) IsMultihost() bool {
return c.multihost
}
func (c testConfig) IsMultilingual() bool {
return c.multilingual
}
+3 -3
View File
@@ -19,7 +19,7 @@ import "github.com/gohugoio/hugo/common/version"
// This should be the only one.
var CurrentVersion = version.Version{
Major: 0,
Minor: 159,
PatchLevel: 2,
Suffix: "",
Minor: 153,
PatchLevel: 0,
Suffix: "-DEV",
}
+2 -15
View File
@@ -20,7 +20,6 @@ import (
"io"
"os"
"strings"
"sync"
"time"
"github.com/bep/logg"
@@ -124,11 +123,8 @@ func New(opts Options) Logger {
)
l := logger.WithLevel(opts.Level)
logMu := &sync.Mutex{}
reset := func() {
logMu.Lock()
defer logMu.Unlock()
logCounters.mu.Lock()
defer logCounters.mu.Unlock()
logCounters.counters = make(map[logg.Level]int)
@@ -139,7 +135,6 @@ func New(opts Options) Logger {
}
return &logAdapter{
mu: logMu,
logCounters: logCounters,
errors: errorsw,
reset: reset,
@@ -178,7 +173,6 @@ func LevelLoggerToWriter(l logg.LevelLogger) io.Writer {
type Logger interface {
Debug() logg.LevelLogger
DebugCommand(command string) logg.LevelLogger
Debugf(format string, v ...any)
Debugln(v ...any)
Error() logg.LevelLogger
@@ -209,7 +203,6 @@ type Logger interface {
}
type logAdapter struct {
mu *sync.Mutex
logCounters *logLevelCounter
errors *strings.Builder
reset func()
@@ -228,10 +221,6 @@ func (l *logAdapter) Debug() logg.LevelLogger {
return l.debugl
}
func (l *logAdapter) DebugCommand(command string) logg.LevelLogger {
return l.debugl.WithField(FieldNameCmd, command)
}
func (l *logAdapter) Debugf(format string, v ...any) {
l.debugl.Logf(format, v...)
}
@@ -286,7 +275,7 @@ func (l *logAdapter) PrintTimerIfDelayed(start time.Time, name string) {
if milli < 500 {
return
}
fmt.Fprintf(l.stdErr, "%s in %v ms\n", name, milli)
fmt.Fprintf(l.stdErr, "%s in %v ms", name, milli)
}
func (l *logAdapter) Printf(format string, v ...any) {
@@ -334,8 +323,6 @@ func (l *logAdapter) Errorln(v ...any) {
}
func (l *logAdapter) Errors() string {
l.mu.Lock()
defer l.mu.Unlock()
return l.errors.String()
}
@@ -352,7 +339,7 @@ func (l *logAdapter) Warnidf(id, format string, v ...any) {
}
func (l *logAdapter) idfInfoStatement(what, id, format string) string {
return fmt.Sprintf("\nYou can suppress this %s by adding the following to your project configuration:\nignoreLogs = ['%s']", what, id)
return fmt.Sprintf("\nYou can suppress this %s by adding the following to your site configuration:\nignoreLogs = ['%s']", what, id)
}
func (l *logAdapter) Trace(s logg.StringFunc) {
@@ -1,4 +1,4 @@
// Copyright 2026 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.
@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package hmaps
package maps
import (
"sync"
@@ -1,4 +1,4 @@
// Copyright 2026 The Hugo Authors. All rights reserved.
// 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.
@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package hmaps
package maps
import (
"testing"
+4 -16
View File
@@ -1,4 +1,4 @@
// Copyright 2026 The Hugo Authors. All rights reserved.
// 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.
@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package hmaps
package maps
import (
"iter"
@@ -73,23 +73,11 @@ func (m *Map[K, T]) Set(key K, value T) {
m.mu.Unlock()
}
// Delete deletes the given key from the map.
// It returns true if the key was found and deleted, false otherwise.
func (m *Map[K, T]) Delete(key K) bool {
m.mu.Lock()
defer m.mu.Unlock()
if _, found := m.m[key]; found {
delete(m.m, key)
return true
}
return false
}
// WithWriteLock executes the given function with a write lock on the map.
func (m *Map[K, T]) WithWriteLock(f func(m map[K]T) error) error {
func (m *Map[K, T]) WithWriteLock(f func(m map[K]T)) {
m.mu.Lock()
defer m.mu.Unlock()
return f(m.m)
f(m.m)
}
// SetIfAbsent sets the given key to the given value if the key does not already exist in the map.
@@ -1,4 +1,4 @@
// Copyright 2026 The Hugo Authors. All rights reserved.
// 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.
@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package hmaps
package maps
import (
"testing"
@@ -62,9 +62,8 @@ func TestMap(t *testing.T) {
c.Assert(found, qt.Equals, true)
c.Assert(v, qt.Equals, 300)
m.WithWriteLock(func(m map[string]int) error {
m.WithWriteLock(func(m map[string]int) {
m["f"] = 500
return nil
})
v, found = m.Lookup("f")
c.Assert(found, qt.Equals, true)
+2 -2
View File
@@ -1,4 +1,4 @@
// Copyright 2026 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 hmaps
package maps
import (
"fmt"
@@ -1,4 +1,4 @@
// Copyright 2026 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 hmaps
package maps
import (
"fmt"
@@ -1,4 +1,4 @@
// Copyright 2026 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.
@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package hmaps
package maps
import (
"slices"
@@ -1,4 +1,4 @@
// Copyright 2026 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.
@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package hmaps
package maps
import (
"testing"
@@ -1,4 +1,4 @@
// Copyright 2026 The Hugo Authors. All rights reserved.
// 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.
@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package hmaps
package maps
import (
"fmt"
@@ -1,4 +1,4 @@
// Copyright 2026 The Hugo Authors. All rights reserved.
// 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.
@@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package hmaps
package maps
import (
"testing"
@@ -1,4 +1,4 @@
// Copyright 2026 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 hmaps
package maps
import (
"errors"
@@ -1,4 +1,4 @@
// Copyright 2026 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 hmaps
package maps
import (
"testing"
+7 -15
View File
@@ -21,7 +21,7 @@ import (
"strings"
"sync"
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/hugolib/sitesmatrix"
@@ -30,18 +30,10 @@ import (
)
const (
identifierBaseof = "baseof"
identifierCustomWrapper = "_"
identifierBaseof = "baseof"
identifierCurstomWrapper = "_"
)
// isCustomWrapperIdentifier tells whether a supplied path is of the form _xyz_.
// must have non-empty content between the identifierCustomWrapper's to pass.
func isCustomWrapperIdentifier(s string) bool {
return len(s) > 2*len(identifierCustomWrapper) &&
strings.HasPrefix(s, identifierCustomWrapper) &&
strings.HasSuffix(s, identifierCustomWrapper)
}
// PathParser parses and manages paths.
type PathParser struct {
// Maps the language code to its index in the languages/sites slice.
@@ -62,12 +54,12 @@ type PathParser struct {
// Below gets created on demand.
initOnce sync.Once
sitesMatrixCache *hmaps.Cache[string, sitesmatrix.VectorStore] // Maps language index to sites matrix vector store.
sitesMatrixCache *maps.Cache[string, sitesmatrix.VectorStore] // Maps language index to sites matrix vector store.
}
func (pp *PathParser) init() {
pp.initOnce.Do(func() {
pp.sitesMatrixCache = hmaps.NewCache[string, sitesmatrix.VectorStore]()
pp.sitesMatrixCache = maps.NewCache[string, sitesmatrix.VectorStore]()
})
}
@@ -196,7 +188,7 @@ 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 isCustomWrapperIdentifier(sid) {
if strings.HasPrefix(sid, identifierCurstomWrapper) && strings.HasSuffix(sid, identifierCurstomWrapper) {
p.identifiersKnown = append(p.identifiersKnown, id)
p.posIdentifierCustom = len(p.identifiersKnown) - 1
found = true
@@ -783,7 +775,7 @@ func (p *Path) Lang() string {
}
func (p *Path) Custom() string {
return strings.TrimSuffix(strings.TrimPrefix(p.identifierAsString(p.posIdentifierCustom), identifierCustomWrapper), identifierCustomWrapper)
return strings.TrimSuffix(strings.TrimPrefix(p.identifierAsString(p.posIdentifierCustom), identifierCurstomWrapper), identifierCurstomWrapper)
}
func (p *Path) Identifier(i int) string {
+1 -110
View File
@@ -153,35 +153,6 @@ func (p PR[T]) FilterCopy(s []T) []T {
return result
}
const (
rangeOpNone = iota
rangeOpLT
rangeOpLTE
rangeOpGT
rangeOpGTE
rangeOpEQ
rangeOpNE
)
func cutRangeOp(s string) (op int, rest string) {
switch {
case strings.HasPrefix(s, ">= "):
return rangeOpGTE, s[3:]
case strings.HasPrefix(s, "<= "):
return rangeOpLTE, s[3:]
case strings.HasPrefix(s, "== "):
return rangeOpEQ, s[3:]
case strings.HasPrefix(s, "!= "):
return rangeOpNE, s[3:]
case strings.HasPrefix(s, "> "):
return rangeOpGT, s[2:]
case strings.HasPrefix(s, "< "):
return rangeOpLT, s[2:]
default:
return rangeOpNone, s
}
}
// 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) {
@@ -216,86 +187,6 @@ func NewStringPredicateFromGlobs(patterns []string, getGlob func(pattern string)
return p.BoolFunc(), nil
}
// NewIndexStringPredicateFromGlobsAndRanges creates an IndexString predicate from the given glob patterns and range patterns.
// A glob pattern starting with "!" is a negation pattern which will be ANDed with the rest.
// A range pattern is one of "> value", ">= value", "< value" or "<= value".
func NewIndexStringPredicateFromGlobsAndRanges(patterns []string, getIndex func(s string) int, getGlob func(pattern string) (glob.Glob, error)) (P[IndexString], error) {
var p PR[IndexString]
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 IndexString) Match {
return BoolMatch(!g.Match(s.String))
})
} else {
// This can be either a glob or a value prefixed with one of >, >=, < or <=.
o, v := cutRangeOp(pattern)
if o != rangeOpNone {
i := getIndex(v)
if i == -1 {
// No match possible.
p = p.And(func(s IndexString) Match {
return BoolMatch(false)
})
continue
}
switch o {
// The greater values starts at the top with index 0.
case rangeOpGT:
p = p.And(func(s IndexString) Match {
return BoolMatch(s.Index < i)
})
case rangeOpGTE:
p = p.And(func(s IndexString) Match {
return BoolMatch(s.Index <= i)
})
case rangeOpLT:
p = p.And(func(s IndexString) Match {
return BoolMatch(s.Index > i)
})
case rangeOpLTE:
p = p.And(func(s IndexString) Match {
return BoolMatch(s.Index >= i)
})
case rangeOpEQ:
p = p.And(func(s IndexString) Match {
return BoolMatch(s.Index == i)
})
case rangeOpNE:
p = p.And(func(s IndexString) Match {
return BoolMatch(s.Index != i)
})
}
} else {
g, err := getGlob(pattern)
if err != nil {
return nil, err
}
p = p.Or(func(s IndexString) Match {
return BoolMatch(g.Match(s.String))
})
}
}
}
return p.BoolFunc(), nil
}
type IndexString struct {
Index int
String string
}
type IndexMatcher interface {
IndexMatch(match P[IndexString]) (iter.Seq[int], error)
IndexMatch(match P[string]) (iter.Seq[int], error)
}
-101
View File
@@ -146,107 +146,6 @@ func TestNewStringPredicateFromGlobs(t *testing.T) {
c.Assert(m("anything"), qt.IsFalse)
}
func TestNewIndexStringPredicateFromGlobsAndRanges(t *testing.T) {
c := qt.New(t)
// Simulate versions: v4.0.0=0, v3.0.0=1, v2.0.0=2, v1.0.0=3
// Lower index = greater value.
versions := []string{"v4.0.0", "v3.0.0", "v2.0.0", "v1.0.0"}
getIndex := func(s string) int {
for i, v := range versions {
if v == s {
return i
}
}
return -1
}
getGlob := func(pattern string) (glob.Glob, error) {
return glob.Compile(pattern)
}
n := func(patterns ...string) predicate.P[predicate.IndexString] {
p, err := predicate.NewIndexStringPredicateFromGlobsAndRanges(patterns, getIndex, getGlob)
c.Assert(err, qt.IsNil)
return p
}
is := func(i int) predicate.IndexString {
return predicate.IndexString{Index: i, String: versions[i]}
}
// Test >= v2.0.0 (index 2): should match indices <= 2
m := n(">= v2.0.0")
c.Assert(m(is(0)), qt.IsTrue) // v4.0.0
c.Assert(m(is(1)), qt.IsTrue) // v3.0.0
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
c.Assert(m(is(3)), qt.IsFalse) // v1.0.0
// Test > v2.0.0 (index 2): should match indices < 2
m = n("> v2.0.0")
c.Assert(m(is(0)), qt.IsTrue) // v4.0.0
c.Assert(m(is(1)), qt.IsTrue) // v3.0.0
c.Assert(m(is(2)), qt.IsFalse) // v2.0.0
c.Assert(m(is(3)), qt.IsFalse) // v1.0.0
// Test < v3.0.0 (index 1): should match indices > 1
m = n("< v3.0.0")
c.Assert(m(is(0)), qt.IsFalse) // v4.0.0
c.Assert(m(is(1)), qt.IsFalse) // v3.0.0
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
c.Assert(m(is(3)), qt.IsTrue) // v1.0.0
// Test range: >= v2.0.0 AND <= v3.0.0
m = n(">= v2.0.0", "<= v3.0.0")
c.Assert(m(is(0)), qt.IsFalse) // v4.0.0 - too high
c.Assert(m(is(1)), qt.IsTrue) // v3.0.0
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
c.Assert(m(is(3)), qt.IsFalse) // v1.0.0 - too low
// Test glob pattern
m = n("v2.*.*")
c.Assert(m(is(0)), qt.IsFalse) // v4.0.0
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
// Test glob with negation
m = n("v*.*.*", "! v3.*.*")
c.Assert(m(is(0)), qt.IsTrue) // v4.0.0
c.Assert(m(is(1)), qt.IsFalse) // v3.0.0 - negated
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
// Test range with negation: >= v2.0.0 but not v3.0.0
m = n(">= v2.0.0", "! v3.0.0")
c.Assert(m(is(0)), qt.IsTrue) // v4.0.0
c.Assert(m(is(1)), qt.IsFalse) // v3.0.0 - negated
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
c.Assert(m(is(3)), qt.IsFalse) // v1.0.0 - out of range
// Test unknown value in range returns no match
m = n(">= v99.0.0")
c.Assert(m(is(0)), qt.IsFalse)
c.Assert(m(is(3)), qt.IsFalse)
// Test == v2.0.0: should only match v2.0.0
m = n("== v2.0.0")
c.Assert(m(is(0)), qt.IsFalse) // v4.0.0
c.Assert(m(is(1)), qt.IsFalse) // v3.0.0
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
c.Assert(m(is(3)), qt.IsFalse) // v1.0.0
// Test != v2.0.0: should match everything except v2.0.0
m = n("!= v2.0.0")
c.Assert(m(is(0)), qt.IsTrue) // v4.0.0
c.Assert(m(is(1)), qt.IsTrue) // v3.0.0
c.Assert(m(is(2)), qt.IsFalse) // v2.0.0
c.Assert(m(is(3)), qt.IsTrue) // v1.0.0
// Test != with range: >= v2.0.0 AND != v3.0.0
m = n(">= v2.0.0", "!= v3.0.0")
c.Assert(m(is(0)), qt.IsTrue) // v4.0.0
c.Assert(m(is(1)), qt.IsFalse) // v3.0.0 - excluded by !=
c.Assert(m(is(2)), qt.IsTrue) // v2.0.0
c.Assert(m(is(3)), qt.IsFalse) // v1.0.0 - out of range
}
func BenchmarkPredicate(b *testing.B) {
b.Run("and or no match", func(b *testing.B) {
var p predicate.PR[int] = intP1
-52
View File
@@ -1,52 +0,0 @@
// Copyright 2026 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 predicate
import (
"testing"
qt "github.com/frankban/quicktest"
)
func TestCutRangeOp(t *testing.T) {
c := qt.New(t)
op, rest := cutRangeOp(">= value")
c.Assert(op, qt.Equals, rangeOpGTE)
c.Assert(rest, qt.Equals, "value")
op, rest = cutRangeOp("<= value")
c.Assert(op, qt.Equals, rangeOpLTE)
c.Assert(rest, qt.Equals, "value")
op, rest = cutRangeOp("> value")
c.Assert(op, qt.Equals, rangeOpGT)
c.Assert(rest, qt.Equals, "value")
op, rest = cutRangeOp("< value")
c.Assert(op, qt.Equals, rangeOpLT)
c.Assert(rest, qt.Equals, "value")
op, rest = cutRangeOp("== value")
c.Assert(op, qt.Equals, rangeOpEQ)
c.Assert(rest, qt.Equals, "value")
op, rest = cutRangeOp("!= value")
c.Assert(op, qt.Equals, rangeOpNE)
c.Assert(rest, qt.Equals, "value")
op, rest = cutRangeOp("value")
c.Assert(op, qt.Equals, rangeOpNone)
c.Assert(rest, qt.Equals, "value")
}
+4 -2
View File
@@ -56,7 +56,9 @@ func TestEvictingStringQueueConcurrent(t *testing.T) {
queue := NewEvictingQueue[string](3)
for range 100 {
wg.Go(func() {
wg.Add(1)
go func() {
defer wg.Done()
queue.Add(val)
v := queue.Peek()
if v != val {
@@ -66,7 +68,7 @@ func TestEvictingStringQueueConcurrent(t *testing.T) {
if len(vals) != 1 || vals[0] != val {
t.Error("wrong val")
}
})
}()
}
wg.Wait()
}
+1 -1
View File
@@ -89,7 +89,7 @@ func IsNil(v any) bool {
value := reflect.ValueOf(v)
switch value.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Ptr, reflect.Slice:
return value.IsNil()
}
+4 -18
View File
@@ -37,9 +37,6 @@ type Version struct {
// HugoVersionSuffix is the suffix used in the Hugo version string.
// It will be blank for release versions.
Suffix string
// Set when parsed from a string,
source string
}
var (
@@ -54,10 +51,6 @@ func (v Version) IsAlphaBetaOrRC() bool {
return strings.Contains(s, "alpha.") || strings.Contains(s, "beta.") || strings.Contains(s, "rc.")
}
func (v Version) IsZero() bool {
return v.Major == 0 && v.Minor == 0 && v.PatchLevel == 0 && v.Suffix == ""
}
func (v Version) String() string {
return version(v.Major, v.Minor, v.PatchLevel, v.Suffix)
}
@@ -116,7 +109,6 @@ func ParseVersion(s string) (Version, error) {
vv.Suffix = suffix
}
vv.Major, vv.Minor, vv.PatchLevel = parseVersion(s)
vv.source = s
return vv, nil
}
@@ -184,10 +176,6 @@ func CompareVersions(v1 Version, v2 any) int {
case int64:
c = compareFloatWithVersion(float64(d), v1)
case Version:
if v1.IsZero() && d.IsZero() {
// Fall back to source comparison.
return strings.Compare(v1.source, d.source)
}
if d.Major == v1.Major && d.Minor == v1.Minor && d.PatchLevel == v1.PatchLevel {
return strings.Compare(v1.Suffix, d.Suffix)
}
@@ -212,13 +200,11 @@ func CompareVersions(v1 Version, v2 any) int {
return -1
}
v2v, _ := ParseVersion(s)
if v1.IsZero() && v2v.IsZero() {
// Fall back to source comparison.
return strings.Compare(v1.source, v2v.source)
v, err := ParseVersion(s)
if err != nil {
return -1
}
return v1.Compare(v2v)
return v1.Compare(v)
}
-14
View File
@@ -50,11 +50,6 @@ func TestHugoVersion(t *testing.T) {
func TestCompareVersions(t *testing.T) {
c := qt.New(t)
parseIgnoreErr := func(s string) Version {
v, _ := ParseVersion(s)
return v
}
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)
@@ -74,15 +69,6 @@ func TestCompareVersions(t *testing.T) {
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(parseIgnoreErr("foobar"), "v1.0.0"), qt.Equals, 1)
c.Assert(CompareVersions(parseIgnoreErr("v1.0.0"), "foobar"), qt.Equals, -1)
c.Assert(CompareVersions(parseIgnoreErr("foobar"), "foobar"), qt.Equals, 0)
c.Assert(CompareVersions(parseIgnoreErr("foobar"), parseIgnoreErr("foobar")), qt.Equals, 0)
c.Assert(CompareVersions(parseIgnoreErr("a"), "b"), qt.Equals, -1)
c.Assert(CompareVersions(parseIgnoreErr("a"), parseIgnoreErr("b")), qt.Equals, -1)
c.Assert(CompareVersions(parseIgnoreErr("b"), "a"), qt.Equals, 1)
c.Assert(CompareVersions(parseIgnoreErr("b"), parseIgnoreErr("a")), qt.Equals, 1)
}
func TestParseHugoVersion(t *testing.T) {
+42 -98
View File
@@ -29,10 +29,10 @@ import (
"github.com/gohugoio/hugo/cache/filecache"
"github.com/gohugoio/hugo/cache/httpcache"
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/common/hstrings"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/common/urls"
"github.com/gohugoio/hugo/config"
@@ -212,7 +212,7 @@ type Config struct {
// User provided parameters.
// <docsmeta>{"refs": ["config:languages:params"] }</docsmeta>
Params hmaps.Params `mapstructure:"-"`
Params maps.Params `mapstructure:"-"`
// UglyURLs configuration. Either a boolean or a sections map.
UglyURLs any `mapstructure:"-"`
@@ -399,82 +399,33 @@ func (c *Config) CompileConfig(logger loggers.Logger) error {
return err
}
// Legacy language values.
if c.LanguageCode != "" {
if !c.isLanguageClone {
hugo.DeprecateWithLogger("project config key languageCode", "Use locale instead.", "v0.158.0", logger.Logger())
}
if c.Locale == "" {
c.Locale = c.LanguageCode
}
c.LanguageCode = ""
// Legacy paginate values.
if c.Paginate != 0 {
hugo.DeprecateWithLogger("site config key paginate", "Use pagination.pagerSize instead.", "v0.128.0", logger.Logger())
c.Pagination.PagerSize = c.Paginate
}
for k, v := range c.Languages.Config.LanguageConfigs {
//lint:ignore SA1019 Keep as adapter for now.
if v.LanguageCode != "" {
if !c.isLanguageClone {
hugo.DeprecateWithLogger(fmt.Sprintf("project config key languages.%s.languageCode", k), fmt.Sprintf("Use languages.%s.locale instead.", k), "v0.158.0", logger.Logger())
}
if v.Locale == "" {
//lint:ignore SA1019 Keep as adapter for now.
v.Locale = v.LanguageCode
}
//lint:ignore SA1019 Keep as adapter for now.
v.LanguageCode = ""
}
//lint:ignore SA1019 Keep as adapter for now.
if v.LanguageName != "" {
if !c.isLanguageClone {
hugo.DeprecateWithLogger(fmt.Sprintf("project config key languages.%s.languageName", k), fmt.Sprintf("Use languages.%s.label instead.", k), "v0.158.0", logger.Logger())
}
if v.Label == "" {
//lint:ignore SA1019 Keep as adapter for now.
v.Label = v.LanguageName
}
//lint:ignore SA1019 Keep as adapter for now.
v.LanguageName = ""
}
//lint:ignore SA1019 Keep as adapter for now.
if v.LanguageDirection != "" {
if !c.isLanguageClone {
hugo.DeprecateWithLogger(fmt.Sprintf("project config key languages.%s.languageDirection", k), fmt.Sprintf("Use languages.%s.direction instead.", k), "v0.158.0", logger.Logger())
}
if v.Direction == "" {
//lint:ignore SA1019 Keep as adapter for now.
v.Direction = v.LanguageDirection
}
//lint:ignore SA1019 Keep as adapter for now.
v.LanguageDirection = ""
}
c.Languages.Config.LanguageConfigs[k] = v
// Sorted is a snapshot of LanguageConfigs taken at decode time; keep it
// in sync so Configs.Init, which reads from Sorted, sees the migrated values.
for i, s := range c.Languages.Config.Sorted {
if s.Name == k {
c.Languages.Config.Sorted[i].LanguageConfig = v
break
}
}
if c.PaginatePath != "" {
hugo.DeprecateWithLogger("site config key paginatePath", "Use pagination.path instead.", "v0.128.0", logger.Logger())
c.Pagination.Path = c.PaginatePath
}
// Legacy privacy values.
if c.Privacy.Twitter.Disable {
hugo.DeprecateWithLogger("project config key privacy.twitter.disable", "Use privacy.x.disable instead.", "v0.141.0", logger.Logger())
hugo.DeprecateWithLogger("site config key privacy.twitter.disable", "Use privacy.x.disable instead.", "v0.141.0", logger.Logger())
c.Privacy.X.Disable = c.Privacy.Twitter.Disable
}
if c.Privacy.Twitter.EnableDNT {
hugo.DeprecateWithLogger("project config key privacy.twitter.enableDNT", "Use privacy.x.enableDNT instead.", "v0.141.0", logger.Logger())
hugo.DeprecateWithLogger("site config key privacy.twitter.enableDNT", "Use privacy.x.enableDNT instead.", "v0.141.0", logger.Logger())
c.Privacy.X.EnableDNT = c.Privacy.Twitter.EnableDNT
}
if c.Privacy.Twitter.Simple {
hugo.DeprecateWithLogger("project config key privacy.twitter.simple", "Use privacy.x.simple instead.", "v0.141.0", logger.Logger())
hugo.DeprecateWithLogger("site config key privacy.twitter.simple", "Use privacy.x.simple instead.", "v0.141.0", logger.Logger())
c.Privacy.X.Simple = c.Privacy.Twitter.Simple
}
// Legacy services values.
if c.Services.Twitter.DisableInlineCSS {
hugo.DeprecateWithLogger("project config key services.twitter.disableInlineCSS", "Use services.x.disableInlineCSS instead.", "v0.141.0", logger.Logger())
hugo.DeprecateWithLogger("site config key services.twitter.disableInlineCSS", "Use services.x.disableInlineCSS instead.", "v0.141.0", logger.Logger())
c.Services.X.DisableInlineCSS = c.Services.Twitter.DisableInlineCSS
}
@@ -495,7 +446,7 @@ func (c *Config) CompileConfig(logger loggers.Logger) error {
)
if c.Markup.Goldmark.RenderHooks.Image.EnableDefault != nil {
alternative := "Use markup.goldmark.renderHooks.image.useEmbedded instead." + " " + alternativeDetails
hugo.DeprecateWithLogger("project config key markup.goldmark.renderHooks.image.enableDefault", alternative, "0.148.0", logger.Logger())
hugo.DeprecateWithLogger("site config key markup.goldmark.renderHooks.image.enableDefault", alternative, "0.148.0", logger.Logger())
if *c.Markup.Goldmark.RenderHooks.Image.EnableDefault {
c.Markup.Goldmark.RenderHooks.Image.UseEmbedded = gc.RenderHookUseEmbeddedFallback
} else {
@@ -504,7 +455,7 @@ func (c *Config) CompileConfig(logger loggers.Logger) error {
}
if c.Markup.Goldmark.RenderHooks.Link.EnableDefault != nil {
alternative := "Use markup.goldmark.renderHooks.link.useEmbedded instead." + " " + alternativeDetails
hugo.DeprecateWithLogger("project config key markup.goldmark.renderHooks.link.enableDefault", alternative, "0.148.0", logger.Logger())
hugo.DeprecateWithLogger("site config key markup.goldmark.renderHooks.link.enableDefault", alternative, "0.148.0", logger.Logger())
if *c.Markup.Goldmark.RenderHooks.Link.EnableDefault {
c.Markup.Goldmark.RenderHooks.Link.UseEmbedded = gc.RenderHookUseEmbeddedFallback
} else {
@@ -520,10 +471,10 @@ func (c *Config) CompileConfig(logger loggers.Logger) error {
gc.RenderHookUseEmbeddedNever,
}
if !slices.Contains(renderHookUseEmbeddedModes, c.Markup.Goldmark.RenderHooks.Image.UseEmbedded) {
return fmt.Errorf("project config markup.goldmark.renderHooks.image.useEmbedded must be one of %s", helpers.StringSliceToList(renderHookUseEmbeddedModes, "or"))
return fmt.Errorf("site config markup.goldmark.renderHooks.image must be one of %s", helpers.StringSliceToList(renderHookUseEmbeddedModes, "or"))
}
if !slices.Contains(renderHookUseEmbeddedModes, c.Markup.Goldmark.RenderHooks.Link.UseEmbedded) {
return fmt.Errorf("project config markup.goldmark.renderHooks.link.useEmbedded must be one of %s", helpers.StringSliceToList(renderHookUseEmbeddedModes, "or"))
return fmt.Errorf("site config markup.goldmark.renderHooks.link must be one of %s", helpers.StringSliceToList(renderHookUseEmbeddedModes, "or"))
}
c.C = &ConfigCompiled{
@@ -656,14 +607,8 @@ type RootConfig struct {
DefaultOutputFormat string
// Disable generation of redirect to the default language when DefaultContentLanguageInSubdir is enabled.
// Note that this currently is an alias for DisableDefaultSiteRedirect introduced in v0.154.5.
// It's not obvious how a more fine grained setup would work.
DisableDefaultLanguageRedirect bool
// Disable generation of redirects to the default site when DefaultContentRoleInSubdir or DefaultContentVersionInSubdir or DefaultContentLanguageInSubdir is enabled.
// The default site is the site is the combination of defaultContentLanguage, defaultContentVersion and defaultContentRole.
DisableDefaultSiteRedirect bool
// Disable creation of alias redirect pages.
DisableAliases bool
@@ -730,15 +675,20 @@ type RootConfig struct {
// The configured environment. Default is "development" for server and "production" for build.
Environment string
// Deprecated: Use Locale instead.
LanguageCode string `json:"-"`
// The default locale.
Locale string
// The default language code.
LanguageCode string
// Enable if the site content has CJK language (Chinese, Japanese, or Korean). This affects how Hugo counts words.
HasCJKLanguage bool
// The default number of pages per page when paginating.
// Deprecated: Use the Pagination struct.
Paginate int
// The path to use when creating pagination URLs, e.g. "page" in /page/2/.
// Deprecated: Use the Pagination struct.
PaginatePath string
// Whether to pluralize default list titles.
// Note that this currently only works for English, but you can provide your own title in the content file's front matter.
PluralizeListTitles bool
@@ -866,7 +816,6 @@ type Configs struct {
Modules modules.Modules
ModulesClient *modules.Client
FileCaches filecache.Caches
// All below is set in Init.
Languages langs.Languages
@@ -897,7 +846,7 @@ func (c *Configs) IsZero() bool {
return c == nil || len(c.Languages) == 0
}
func (c *Configs) Init(sourceFs afero.Fs, logger loggers.Logger) error {
func (c *Configs) Init(logger loggers.Logger) error {
var languages langs.Languages
for _, f := range c.Base.Languages.Config.Sorted {
@@ -905,7 +854,7 @@ func (c *Configs) Init(sourceFs afero.Fs, logger loggers.Logger) error {
if !found {
return fmt.Errorf("invalid language configuration for %q", f.Name)
}
language, err := langs.NewLanguage(f.Name, c.Base.DefaultContentLanguage, v.TimeZone, f.LanguageConfig, logger)
language, err := langs.NewLanguage(f.Name, c.Base.DefaultContentLanguage, v.TimeZone, f.LanguageConfig)
if err != nil {
return err
}
@@ -966,9 +915,10 @@ func (c *Configs) Init(sourceFs afero.Fs, logger loggers.Logger) error {
// 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, nil, c.ConfiguredDimensions); err != nil {
if err := getInitializer(langConfig).InitConfig(logger, sitesMatrix, c.ConfiguredDimensions); err != nil {
return err
}
}
@@ -1068,7 +1018,7 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
if lang == "" {
lang = "en"
}
res.Cfg.Set("languages", hmaps.Params{lang: hmaps.Params{}})
res.Cfg.Set("languages", maps.Params{lang: maps.Params{}})
}
bcfg := res.BaseConfig
cfg := res.Cfg
@@ -1095,11 +1045,11 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
var differentRootKeys []string
switch x := v.(type) {
case nil:
case hmaps.Params:
case maps.Params:
_, found := x["params"]
if !found {
x["params"] = hmaps.Params{
hmaps.MergeStrategyKey: hmaps.ParamsMergeStrategyDeep,
x["params"] = maps.Params{
maps.MergeStrategyKey: maps.ParamsMergeStrategyDeep,
}
}
for kk, vv := range x {
@@ -1111,16 +1061,16 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
isMultihost = true
}
if p, ok := vv.(hmaps.Params); ok {
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 = hmaps.CloneParamsDeep(p)
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.(hmaps.Params))
page.AddLangToCascadeTargetMap(k, vv.(maps.Params))
// Always clone cascade config to get the sites matrix right.
differentRootKeys = append(differentRootKeys, kk)
}
@@ -1131,14 +1081,14 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
// This overrides a root key and potentially needs a merge.
if !reflect.DeepEqual(rootv, vv) {
switch vvv := vv.(type) {
case hmaps.Params:
case maps.Params:
differentRootKeys = append(differentRootKeys, kk)
// Use the language value as base.
// Note that this is already cloned above.
mergedConfigEntry := vvv
// Merge in the root value.
hmaps.MergeParams(mergedConfigEntry, rootv.(hmaps.Params))
maps.MergeParams(mergedConfigEntry, rootv.(maps.Params))
mergedConfig.Set(kk, mergedConfigEntry)
default:
@@ -1148,7 +1098,7 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
}
} else {
switch vv.(type) {
case hmaps.Params:
case maps.Params:
differentRootKeys = append(differentRootKeys, kk)
default:
// Apply new values to the root.
@@ -1184,7 +1134,7 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
}
langConfigMap[k] = clone
case hmaps.ParamsMergeStrategy:
case maps.ParamsMergeStrategy:
default:
panic(fmt.Sprintf("unknown type in languages config: %T", v))
@@ -1199,16 +1149,10 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
l.CommonDirs.CacheDir = bcfg.CacheDir
}
caches, err := filecache.NewCaches(all.Caches, fs)
if err != nil {
return nil, fmt.Errorf("failed to create file caches from configuration: %w", err)
}
cm := &Configs{
Base: all,
LanguageConfigMap: langConfigMap,
LoadingInfo: res,
FileCaches: caches,
IsMultihost: isMultihost,
}
+28 -25
View File
@@ -22,9 +22,9 @@ import (
"github.com/gohugoio/hugo/langs"
"github.com/gohugoio/hugo/cache/httpcache"
"github.com/gohugoio/hugo/common/hmaps"
"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"
"github.com/gohugoio/hugo/config/privacy"
@@ -196,7 +196,7 @@ var allDecoderSetups = map[string]decodeWeight{
key: "outputs",
decode: func(d decodeWeight, p decodeConfig) error {
defaults := createDefaultOutputFormats(p.c.OutputFormats.Config)
m := hmaps.CleanConfigStringMap(p.p.GetStringMap("outputs"))
m := maps.CleanConfigStringMap(p.p.GetStringMap("outputs"))
p.c.Outputs = make(map[string][]string)
for k, v := range m {
s := types.ToStringSlicePreserveString(v)
@@ -225,18 +225,28 @@ var allDecoderSetups = map[string]decodeWeight{
"languages": {
key: "languages",
decode: func(d decodeWeight, p decodeConfig) error {
m := hmaps.CleanConfigStringMap(p.p.GetStringMap(d.key))
// Root-level locale/languageCode is passed to DecodeConfig so it can
// be applied to the default content language inside langs.DecodeConfig.
// They are passed separately so that an explicit per-lang languageCode
// can override a root-level languageCode (but not a root-level locale).
rootLocale := p.p.GetString("locale")
rootLanguageCode := p.p.GetString("languagecode")
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, rootLocale, rootLanguageCode, p.c.RootConfig.DisableLanguages, m)
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)
}
@@ -256,7 +266,7 @@ var allDecoderSetups = map[string]decodeWeight{
key: "versions",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
m := hmaps.CleanConfigStringMap(p.p.GetStringMap(d.key))
m := maps.CleanConfigStringMap(p.p.GetStringMap(d.key))
p.c.Versions, err = versions.DecodeConfig(p.c.RootConfig.DefaultContentVersion, m)
return err
},
@@ -268,7 +278,7 @@ var allDecoderSetups = map[string]decodeWeight{
err error
defaultContentRole string
)
m := hmaps.CleanConfigStringMap(p.p.GetStringMap(d.key))
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
@@ -279,7 +289,7 @@ var allDecoderSetups = map[string]decodeWeight{
"params": {
key: "params",
decode: func(d decodeWeight, p decodeConfig) error {
p.c.Params = hmaps.CleanConfigStringMap(p.p.GetStringMap("params"))
p.c.Params = maps.CleanConfigStringMap(p.p.GetStringMap("params"))
if p.c.Params == nil {
p.c.Params = make(map[string]any)
}
@@ -325,14 +335,7 @@ var allDecoderSetups = map[string]decodeWeight{
key: "taxonomies",
decode: func(d decodeWeight, p decodeConfig) error {
if p.p.IsSet(d.key) {
m := hmaps.CleanConfigStringMapString(p.p.GetStringMapString(d.key))
// Remove invalid entries (e.g. non-taxonomy keys placed inside [taxonomies] in TOML).
for k, v := range m {
if k == "" || v == "" {
delete(m, k)
}
}
p.c.Taxonomies = m
p.c.Taxonomies = maps.CleanConfigStringMapString(p.p.GetStringMapString(d.key))
}
return nil
},
@@ -444,7 +447,7 @@ var allDecoderSetups = map[string]decodeWeight{
"author": {
key: "author",
decode: func(d decodeWeight, p decodeConfig) error {
p.c.Author = hmaps.CleanConfigStringMap(p.p.GetStringMap(d.key))
p.c.Author = maps.CleanConfigStringMap(p.p.GetStringMap(d.key))
return nil
},
internalOrDeprecated: true,
@@ -452,7 +455,7 @@ var allDecoderSetups = map[string]decodeWeight{
"social": {
key: "social",
decode: func(d decodeWeight, p decodeConfig) error {
p.c.Social = hmaps.CleanConfigStringMapString(p.p.GetStringMapString(d.key))
p.c.Social = maps.CleanConfigStringMapString(p.p.GetStringMapString(d.key))
return nil
},
internalOrDeprecated: true,
@@ -466,8 +469,8 @@ var allDecoderSetups = map[string]decodeWeight{
p.c.UglyURLs = vv
case string:
p.c.UglyURLs = vv == "true"
case hmaps.Params:
p.c.UglyURLs = cast.ToStringMapBool(hmaps.CleanConfigStringMap(vv))
case maps.Params:
p.c.UglyURLs = cast.ToStringMapBool(maps.CleanConfigStringMap(vv))
default:
p.c.UglyURLs = cast.ToStringMapBool(v)
}
+3 -6
View File
@@ -25,8 +25,9 @@ import (
)
type ConfigLanguage struct {
config *Config
baseConfig config.BaseConfig
config *Config
baseConfig config.BaseConfig
m *Configs
language *langs.Language
languageIndex int
@@ -122,10 +123,6 @@ func (c ConfigLanguage) BaseConfig() config.BaseConfig {
return c.baseConfig
}
func (c ConfigLanguage) FileCaches() any {
return c.m.FileCaches
}
func (c ConfigLanguage) Dirs() config.CommonDirs {
return c.config.CommonDirs
}
+6 -6
View File
@@ -14,7 +14,7 @@
package allconfig
import (
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/docshelper"
)
@@ -27,12 +27,12 @@ func init() {
if v.internalOrDeprecated {
continue
}
cfg.Set(configRoot, make(hmaps.Params))
cfg.Set(configRoot, make(maps.Params))
}
lang := hmaps.Params{
"en": hmaps.Params{
"menus": hmaps.Params{},
"params": hmaps.Params{},
lang := maps.Params{
"en": maps.Params{
"menus": maps.Params{},
"params": maps.Params{},
},
}
cfg.Set("languages", lang)
+9 -18
View File
@@ -25,22 +25,21 @@ import (
"github.com/gobwas/glob"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/hexec"
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/helpers"
hglob "github.com/gohugoio/hugo/hugofs/hglob"
"github.com/gohugoio/hugo/modules"
"github.com/gohugoio/hugo/modules/npm"
"github.com/gohugoio/hugo/parser/metadecoders"
"github.com/spf13/afero"
)
//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 project.\n Run `hugo help new` for details.\n")
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() {
@@ -96,11 +95,7 @@ func LoadConfig(d ConfigSourceDescriptor) (configs *Configs, err error) {
configs.Modules = moduleConfig.AllModules
configs.ModulesClient = modulesClient
if !d.SkipNpmCheck && npm.NpmPackNeedsUpdate(d.Fs, configs.Modules) {
d.Logger.Warnln(`npm dependencies are out of sync, please run "hugo mod npm pack" (you may also want to run "npm install" after that)`)
}
if err := configs.Init(d.Fs, d.Logger); err != nil {
if err := configs.Init(d.Logger); err != nil {
return nil, fmt.Errorf("failed to init config: %w", err)
}
@@ -132,9 +127,6 @@ type ConfigSourceDescriptor struct {
// If set, this will be used to ignore the module does not exist error.
IgnoreModuleDoesNotExist bool
// If set, skip the npm pack staleness check (used by hugo mod npm pack).
SkipNpmCheck bool
}
func (d ConfigSourceDescriptor) configFilenames() []string {
@@ -174,7 +166,7 @@ func (l configLoader) applyConfigAliases() error {
}
func (l configLoader) applyDefaultConfig() error {
defaultSettings := hmaps.Params{
defaultSettings := maps.Params{
// These dirs are used early/before we build the config struct.
"themesDir": "themes",
"configDir": "config",
@@ -187,14 +179,14 @@ func (l configLoader) applyDefaultConfig() error {
func (l configLoader) normalizeCfg(cfg config.Provider) error {
if b, ok := cfg.Get("minifyOutput").(bool); ok {
hugo.Deprecate("project config minifyOutput", "Use minify.minifyOutput instead.", "v0.150.0")
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("project config minify", "Use minify.minifyOutput instead.", "v0.150.0")
hugo.Deprecate("site config minify", "Use minify.minifyOutput instead.", "v0.150.0")
if b {
cfg.Set("minify", hmaps.Params{"minifyOutput": true})
cfg.Set("minify", maps.Params{"minifyOutput": true})
}
}
@@ -248,7 +240,7 @@ func (l configLoader) applyOsEnvOverrides(environ []string) error {
}
for _, env := range hugoEnv {
existing, nestedKey, owner, err := hmaps.GetNestedParamFn(env.Key, delim, l.cfg.Get)
existing, nestedKey, owner, err := maps.GetNestedParamFn(env.Key, delim, l.cfg.Get)
if err != nil {
return err
}
@@ -485,7 +477,6 @@ func (l *configLoader) loadModules(configs *Configs, ignoreModuleDoesNotExist bo
PublishDir: publishDir,
Environment: l.Environment,
CacheDir: conf.Caches.CacheDirModules(),
ModuleQueriesCache: configs.FileCaches.ModuleQueriesCache(),
ModuleConfig: conf.Module,
IgnoreVendor: ignoreVendor,
IgnoreModuleDoesNotExist: ignoreModuleDoesNotExist,
@@ -556,7 +547,7 @@ func (l configLoader) loadConfig(configName string) (string, error) {
}
func (l configLoader) deleteMergeStrategies() (err error) {
l.cfg.WalkParams(func(params ...hmaps.KeyParams) bool {
l.cfg.WalkParams(func(params ...maps.KeyParams) bool {
params[len(params)-1].Params.DeleteMergeStrategy()
return false
})
+1 -1
View File
@@ -437,7 +437,7 @@ func (c *CacheBuster) CompileConfig(logger loggers.Logger) error {
return nil
}
return func(ss string) bool {
match := targetRe.MatchString(ss)
match = targetRe.MatchString(ss)
matchString := "no match"
if match {
matchString = "match!"
+3 -3
View File
@@ -20,11 +20,11 @@ import (
"strings"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/parser"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/parser/metadecoders"
"github.com/spf13/afero"
)
@@ -222,11 +222,11 @@ func LoadConfigFromDir(sourceFs afero.Fs, configDir, environment string) (Provid
return cfg, dirnames, nil
}
var keyAliases hmaps.KeyRenamer
var keyAliases maps.KeyRenamer
func init() {
var err error
keyAliases, err = hmaps.NewKeyRenamer(
keyAliases, err = maps.NewKeyRenamer(
// Before 0.53 we used singular for "menu".
"{menu,languages/*/menu}", "menus",
)
+4 -5
View File
@@ -16,7 +16,7 @@ package config
import (
"time"
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/common/urls"
@@ -41,7 +41,6 @@ type AllProvider interface {
Dirs() CommonDirs
Quiet() bool
DirsBase() CommonDirs
FileCaches() any
ContentTypes() ContentTypesProvider
GetConfigSection(string) any
GetConfig() any
@@ -97,7 +96,7 @@ type Provider interface {
GetString(key string) string
GetInt(key string) int
GetBool(key string) bool
GetParams(key string) hmaps.Params
GetParams(key string) maps.Params
GetStringMap(key string) map[string]any
GetStringMapString(key string) map[string]string
GetStringSlice(key string) []string
@@ -105,9 +104,9 @@ type Provider interface {
Set(key string, value any)
Keys() []string
Merge(key string, value any)
SetDefaults(params hmaps.Params)
SetDefaults(params maps.Params)
SetDefaultMergeStrategy()
WalkParams(walkFn func(params ...hmaps.KeyParams) bool)
WalkParams(walkFn func(params ...maps.KeyParams) bool)
IsSet(key string) bool
}
+50 -49
View File
@@ -20,20 +20,21 @@ import (
"strings"
"sync"
"github.com/gohugoio/hugo/common/hmaps"
"github.com/spf13/cast"
"github.com/gohugoio/hugo/common/maps"
)
// New creates a Provider backed by an empty hmaps.Params.
// New creates a Provider backed by an empty maps.Params.
func New() Provider {
return &defaultConfigProvider{
root: make(hmaps.Params),
root: make(maps.Params),
}
}
// NewFrom creates a Provider backed by params.
func NewFrom(params hmaps.Params) Provider {
hmaps.PrepareParams(params)
func NewFrom(params maps.Params) Provider {
maps.PrepareParams(params)
return &defaultConfigProvider{
root: params,
}
@@ -43,7 +44,7 @@ func NewFrom(params hmaps.Params) Provider {
// All methods are thread safe.
type defaultConfigProvider struct {
mu sync.RWMutex
root hmaps.Params
root maps.Params
keyCache sync.Map
}
@@ -89,22 +90,22 @@ func (c *defaultConfigProvider) GetString(k string) string {
return cast.ToString(v)
}
func (c *defaultConfigProvider) GetParams(k string) hmaps.Params {
func (c *defaultConfigProvider) GetParams(k string) maps.Params {
v := c.Get(k)
if v == nil {
return nil
}
return v.(hmaps.Params)
return v.(maps.Params)
}
func (c *defaultConfigProvider) GetStringMap(k string) map[string]any {
v := c.Get(k)
return hmaps.ToStringMap(v)
return maps.ToStringMap(v)
}
func (c *defaultConfigProvider) GetStringMapString(k string) map[string]string {
v := c.Get(k)
return hmaps.ToStringMapString(v)
return maps.ToStringMapString(v)
}
func (c *defaultConfigProvider) GetStringSlice(k string) []string {
@@ -119,9 +120,9 @@ func (c *defaultConfigProvider) Set(k string, v any) {
k = strings.ToLower(k)
if k == "" {
if p, err := hmaps.ToParamsAndPrepare(v); err == nil {
if p, err := maps.ToParamsAndPrepare(v); err == nil {
// Set the values directly in root.
hmaps.SetParams(c.root, p)
maps.SetParams(c.root, p)
} else {
c.root[k] = v
}
@@ -131,7 +132,7 @@ func (c *defaultConfigProvider) Set(k string, v any) {
switch vv := v.(type) {
case map[string]any, map[any]any, map[string]string:
p := hmaps.MustToParamsAndPrepare(vv)
p := maps.MustToParamsAndPrepare(vv)
v = p
}
@@ -141,9 +142,9 @@ func (c *defaultConfigProvider) Set(k string, v any) {
}
if existing, found := m[key]; found {
if p1, ok := existing.(hmaps.Params); ok {
if p2, ok := v.(hmaps.Params); ok {
hmaps.SetParams(p1, p2)
if p1, ok := existing.(maps.Params); ok {
if p2, ok := v.(maps.Params); ok {
maps.SetParams(p1, p2)
return
}
}
@@ -153,8 +154,8 @@ func (c *defaultConfigProvider) Set(k string, v any) {
}
// SetDefaults will set values from params if not already set.
func (c *defaultConfigProvider) SetDefaults(params hmaps.Params) {
hmaps.PrepareParams(params)
func (c *defaultConfigProvider) SetDefaults(params maps.Params) {
maps.PrepareParams(params)
for k, v := range params {
if _, found := c.root[k]; !found {
c.root[k] = v
@@ -169,28 +170,28 @@ func (c *defaultConfigProvider) Merge(k string, v any) {
if k == "" {
rs, f := c.root.GetMergeStrategy()
if f && rs == hmaps.ParamsMergeStrategyNone {
if f && rs == maps.ParamsMergeStrategyNone {
// The user has set a "no merge" strategy on this,
// nothing more to do.
return
}
if p, err := hmaps.ToParamsAndPrepare(v); err == nil {
if p, err := maps.ToParamsAndPrepare(v); err == nil {
// As there may be keys in p not in root, we need to handle
// those as a special case.
var keysToDelete []string
for kk, vv := range p {
if pp, ok := vv.(hmaps.Params); ok {
if pp, ok := vv.(maps.Params); ok {
if pppi, ok := c.root[kk]; ok {
ppp := pppi.(hmaps.Params)
hmaps.MergeParamsWithStrategy("", ppp, pp)
ppp := pppi.(maps.Params)
maps.MergeParamsWithStrategy("", ppp, pp)
} else {
// We need to use the default merge strategy for
// this key.
np := make(hmaps.Params)
strategy := c.determineMergeStrategy(hmaps.KeyParams{Key: "", Params: c.root}, hmaps.KeyParams{Key: kk, Params: np})
np := make(maps.Params)
strategy := c.determineMergeStrategy(maps.KeyParams{Key: "", Params: c.root}, maps.KeyParams{Key: kk, Params: np})
np.SetMergeStrategy(strategy)
hmaps.MergeParamsWithStrategy("", np, pp)
maps.MergeParamsWithStrategy("", np, pp)
c.root[kk] = np
if np.IsZero() {
// Just keep it until merge is done.
@@ -200,7 +201,7 @@ func (c *defaultConfigProvider) Merge(k string, v any) {
}
}
// Merge the rest.
hmaps.MergeParams(c.root, p)
maps.MergeParams(c.root, p)
for _, k := range keysToDelete {
delete(c.root, k)
}
@@ -213,7 +214,7 @@ func (c *defaultConfigProvider) Merge(k string, v any) {
switch vv := v.(type) {
case map[string]any, map[any]any, map[string]string:
p := hmaps.MustToParamsAndPrepare(vv)
p := maps.MustToParamsAndPrepare(vv)
v = p
}
@@ -223,9 +224,9 @@ func (c *defaultConfigProvider) Merge(k string, v any) {
}
if existing, found := m[key]; found {
if p1, ok := existing.(hmaps.Params); ok {
if p2, ok := v.(hmaps.Params); ok {
hmaps.MergeParamsWithStrategy("", p1, p2)
if p1, ok := existing.(maps.Params); ok {
if p2, ok := v.(maps.Params); ok {
maps.MergeParamsWithStrategy("", p1, p2)
}
}
} else {
@@ -244,10 +245,10 @@ func (c *defaultConfigProvider) Keys() []string {
return keys
}
func (c *defaultConfigProvider) WalkParams(walkFn func(params ...hmaps.KeyParams) bool) {
func (c *defaultConfigProvider) WalkParams(walkFn func(params ...maps.KeyParams) bool) {
maxDepth := 1000
var walk func(depth int, params ...hmaps.KeyParams)
walk = func(depth int, params ...hmaps.KeyParams) {
var walk func(depth int, params ...maps.KeyParams)
walk = func(depth int, params ...maps.KeyParams) {
if depth > maxDepth {
panic(errors.New("max depth exceeded"))
}
@@ -257,24 +258,24 @@ func (c *defaultConfigProvider) WalkParams(walkFn func(params ...hmaps.KeyParams
p1 := params[len(params)-1]
i := len(params)
for k, v := range p1.Params {
if p2, ok := v.(hmaps.Params); ok {
paramsplus1 := make([]hmaps.KeyParams, i+1)
if p2, ok := v.(maps.Params); ok {
paramsplus1 := make([]maps.KeyParams, i+1)
copy(paramsplus1, params)
paramsplus1[i] = hmaps.KeyParams{Key: k, Params: p2}
paramsplus1[i] = maps.KeyParams{Key: k, Params: p2}
walk(depth+1, paramsplus1...)
}
}
}
walk(0, hmaps.KeyParams{Key: "", Params: c.root})
walk(0, maps.KeyParams{Key: "", Params: c.root})
}
func (c *defaultConfigProvider) determineMergeStrategy(params ...hmaps.KeyParams) hmaps.ParamsMergeStrategy {
func (c *defaultConfigProvider) determineMergeStrategy(params ...maps.KeyParams) maps.ParamsMergeStrategy {
if len(params) == 0 {
return hmaps.ParamsMergeStrategyNone
return maps.ParamsMergeStrategyNone
}
var (
strategy hmaps.ParamsMergeStrategy
strategy maps.ParamsMergeStrategy
prevIsRoot bool
curr = params[len(params)-1]
)
@@ -298,10 +299,10 @@ func (c *defaultConfigProvider) determineMergeStrategy(params ...hmaps.KeyParams
// Don't set a merge strategy on the root unless set by user.
// This will be handled as a special case.
case "params":
strategy = hmaps.ParamsMergeStrategyDeep
strategy = maps.ParamsMergeStrategyDeep
case "outputformats", "mediatypes":
if prevIsRoot {
strategy = hmaps.ParamsMergeStrategyShallow
strategy = maps.ParamsMergeStrategyShallow
}
case "menus":
isMenuKey := prevIsRoot
@@ -313,11 +314,11 @@ func (c *defaultConfigProvider) determineMergeStrategy(params ...hmaps.KeyParams
}
}
if isMenuKey {
strategy = hmaps.ParamsMergeStrategyShallow
strategy = maps.ParamsMergeStrategyShallow
}
default:
if strategy == "" {
strategy = hmaps.ParamsMergeStrategyNone
strategy = maps.ParamsMergeStrategyNone
}
}
@@ -325,7 +326,7 @@ func (c *defaultConfigProvider) determineMergeStrategy(params ...hmaps.KeyParams
}
func (c *defaultConfigProvider) SetDefaultMergeStrategy() {
c.WalkParams(func(params ...hmaps.KeyParams) bool {
c.WalkParams(func(params ...maps.KeyParams) bool {
if len(params) == 0 {
return false
}
@@ -343,7 +344,7 @@ func (c *defaultConfigProvider) SetDefaultMergeStrategy() {
})
}
func (c *defaultConfigProvider) getNestedKeyAndMap(key string, create bool) (string, hmaps.Params) {
func (c *defaultConfigProvider) getNestedKeyAndMap(key string, create bool) (string, maps.Params) {
var parts []string
v, ok := c.keyCache.Load(key)
if ok {
@@ -357,14 +358,14 @@ func (c *defaultConfigProvider) getNestedKeyAndMap(key string, create bool) (str
next, found := current[parts[i]]
if !found {
if create {
next = make(hmaps.Params)
next = make(maps.Params)
current[parts[i]] = next
} else {
return "", nil
}
}
var ok bool
current, ok = next.(hmaps.Params)
current, ok = next.(maps.Params)
if !ok {
// E.g. a string, not a map that we can store values in.
return "", nil
+32 -31
View File
@@ -22,9 +22,10 @@ import (
"strings"
"testing"
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/common/para"
"github.com/gohugoio/hugo/common/maps"
qt "github.com/frankban/quicktest"
)
@@ -47,7 +48,7 @@ func TestDefaultConfigProvider(t *testing.T) {
c.Assert(cfg.Get(k), qt.Equals, v)
c.Assert(cfg.GetInt(k), qt.Equals, v)
c.Assert(cfg.Get(""), qt.DeepEquals, hmaps.Params{
c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
"foo": 42,
})
})
@@ -59,7 +60,7 @@ func TestDefaultConfigProvider(t *testing.T) {
"bar": "baz",
})
c.Assert(cfg.Get("foo"), qt.DeepEquals, hmaps.Params{
c.Assert(cfg.Get("foo"), qt.DeepEquals, maps.Params{
"bar": "baz",
})
@@ -75,14 +76,14 @@ func TestDefaultConfigProvider(t *testing.T) {
})
cfg.Set("a.c", "cv")
c.Assert(cfg.Get("a"), qt.DeepEquals, hmaps.Params{
c.Assert(cfg.Get("a"), qt.DeepEquals, maps.Params{
"b": "bv",
"c": "cv",
})
c.Assert(cfg.Get("a.c"), qt.Equals, "cv")
cfg.Set("b.a", "av")
c.Assert(cfg.Get("b"), qt.DeepEquals, hmaps.Params{
c.Assert(cfg.Get("b"), qt.DeepEquals, maps.Params{
"a": "av",
})
@@ -90,7 +91,7 @@ func TestDefaultConfigProvider(t *testing.T) {
"b": "bv",
})
c.Assert(cfg.Get("b"), qt.DeepEquals, hmaps.Params{
c.Assert(cfg.Get("b"), qt.DeepEquals, maps.Params{
"a": "av",
"b": "bv",
})
@@ -104,7 +105,7 @@ func TestDefaultConfigProvider(t *testing.T) {
"b": "bv2",
})
c.Assert(cfg.Get(""), qt.DeepEquals, hmaps.Params{
c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
"a": "av2",
"b": "bv2",
})
@@ -117,7 +118,7 @@ func TestDefaultConfigProvider(t *testing.T) {
"b": "bv2",
})
c.Assert(cfg.Get(""), qt.DeepEquals, hmaps.Params{
c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
"a": "av",
"b": "bv2",
})
@@ -136,7 +137,7 @@ func TestDefaultConfigProvider(t *testing.T) {
},
})
c.Assert(cfg.Get("foo"), qt.DeepEquals, hmaps.Params{
c.Assert(cfg.Get("foo"), qt.DeepEquals, maps.Params{
"a": "av",
"b": "bv2",
})
@@ -154,7 +155,7 @@ func TestDefaultConfigProvider(t *testing.T) {
"c": "cv2",
})
c.Assert(cfg.Get("a"), qt.DeepEquals, hmaps.Params{
c.Assert(cfg.Get("a"), qt.DeepEquals, maps.Params{
"b": "bv",
"c": "cv2",
})
@@ -168,7 +169,7 @@ func TestDefaultConfigProvider(t *testing.T) {
"b": "bv2",
})
c.Assert(cfg.Get(""), qt.DeepEquals, hmaps.Params{
c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
"a": "av",
})
})
@@ -191,11 +192,11 @@ func TestDefaultConfigProvider(t *testing.T) {
"e": "ev2",
})
c.Assert(cfg.Get("a"), qt.DeepEquals, hmaps.Params{
c.Assert(cfg.Get("a"), qt.DeepEquals, maps.Params{
"e": "ev2",
"_merge": hmaps.ParamsMergeStrategyShallow,
"_merge": maps.ParamsMergeStrategyShallow,
"b": "bv",
"c": hmaps.Params{
"c": maps.Params{
"b": "bv",
},
})
@@ -220,15 +221,15 @@ func TestDefaultConfigProvider(t *testing.T) {
"b": left,
})
cfg.Merge("", hmaps.Params{
"b": hmaps.Params{
cfg.Merge("", maps.Params{
"b": maps.Params{
"c": "cv2",
"d": "dv2",
},
})
c.Assert(cfg.Get(""), qt.DeepEquals, hmaps.Params{
"b": hmaps.Params{
c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
"b": maps.Params{
"c": "cv1",
"d": "dv2",
},
@@ -266,8 +267,8 @@ func TestDefaultConfigProvider(t *testing.T) {
cfg.Merge("a", right)
c.Assert(cfg.Get(""), qt.DeepEquals, hmaps.Params{
"a": hmaps.Params{
c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
"a": maps.Params{
"b": "bv1",
"c": "cv2",
},
@@ -291,7 +292,7 @@ func TestDefaultConfigProvider(t *testing.T) {
},
})
c.Assert(cfg.Get(""), qt.DeepEquals, hmaps.Params{
c.Assert(cfg.Get(""), qt.DeepEquals, maps.Params{
"b": "bv",
})
})
@@ -340,7 +341,7 @@ func TestDefaultConfigProvider(t *testing.T) {
return err
}
m := hmaps.Params{
m := maps.Params{
"new": 42,
}
@@ -370,8 +371,8 @@ func TestDefaultConfigProvider(t *testing.T) {
cfg := New()
k := "foo"
cfg.Set(k, hmaps.Params{k: true})
c.Assert(cfg.GetParams(k), qt.DeepEquals, hmaps.Params{
cfg.Set(k, maps.Params{k: true})
c.Assert(cfg.GetParams(k), qt.DeepEquals, maps.Params{
k: true,
})
@@ -383,8 +384,8 @@ func TestDefaultConfigProvider(t *testing.T) {
k := "foo"
k2 := "bar"
cfg.Set(k, hmaps.Params{k: struct{}{}})
cfg.Set(k2, hmaps.Params{k2: struct{}{}})
cfg.Set(k, maps.Params{k: struct{}{}})
cfg.Set(k2, maps.Params{k2: struct{}{}})
c.Assert(len(cfg.Keys()), qt.Equals, 2)
@@ -400,11 +401,11 @@ func TestDefaultConfigProvider(t *testing.T) {
c.Run("WalkParams", func(c *qt.C) {
cfg := New()
cfg.Set("x", hmaps.Params{})
cfg.Set("y", hmaps.Params{})
cfg.Set("x", maps.Params{})
cfg.Set("y", maps.Params{})
var got []string
cfg.WalkParams(func(params ...hmaps.KeyParams) bool {
cfg.WalkParams(func(params ...maps.KeyParams) bool {
got = append(got, params[len(params)-1].Key)
return false
})
@@ -416,7 +417,7 @@ func TestDefaultConfigProvider(t *testing.T) {
c.Assert(got, qt.DeepEquals, want)
cfg = New()
cfg.WalkParams(func(params ...hmaps.KeyParams) bool {
cfg.WalkParams(func(params ...maps.KeyParams) bool {
return true
})
@@ -428,7 +429,7 @@ func TestDefaultConfigProvider(t *testing.T) {
c.Run("SetDefaults", func(c *qt.C) {
cfg := New()
cfg.SetDefaults(hmaps.Params{
cfg.SetDefaults(maps.Params{
"foo": "bar",
"bar": "baz",
})
+2 -2
View File
@@ -18,7 +18,7 @@ import (
"testing"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/common/maps"
"github.com/mitchellh/mapstructure"
)
@@ -32,7 +32,7 @@ func TestNamespace(t *testing.T) {
map[string]any{"foo": "bar"},
func(v any) (*tstNsExt, any, error) {
t := &tstNsExt{}
m, err := hmaps.ToStringMapE(v)
m, err := maps.ToStringMapE(v)
if err != nil {
return nil, nil, err
}
+1 -1
View File
@@ -202,7 +202,7 @@ func stringSliceToWhitelistHook() mapstructure.DecodeHookFuncType {
t reflect.Type,
data any,
) (any, error) {
if t != reflect.TypeFor[Whitelist]() {
if t != reflect.TypeOf(Whitelist{}) {
return data, nil
}
+2 -11
View File
@@ -17,12 +17,11 @@ package testconfig
import (
_ "unsafe"
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/config/allconfig"
"github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/internal/warpc"
toml "github.com/pelletier/go-toml/v2"
"github.com/spf13/afero"
)
@@ -61,14 +60,6 @@ func GetTestDeps(fs afero.Fs, cfg config.Provider, beforeInit ...func(*deps.Deps
d := &deps.Deps{
Conf: conf,
Fs: hugofs.NewFrom(fs, conf.BaseConfig()),
WasmDispatchers: warpc.AllDispatchers(
warpc.Options{
PoolSize: 1,
},
warpc.Options{
PoolSize: 1,
},
),
}
for _, f := range beforeInit {
f(d)
@@ -84,7 +75,7 @@ func GetTestConfigSectionFromStruct(section string, v any) config.AllProvider {
if err != nil {
panic(err)
}
p := hmaps.Params{
p := maps.Params{
section: config.FromTOMLConfigString(string(data)).Get(""),
}
cfg := config.NewFrom(p)
+2 -2
View File
@@ -36,7 +36,7 @@ import (
)
const (
// DefaultArchetypeTemplateTemplate is the template used in 'hugo new project'
// DefaultArchetypeTemplateTemplate is the template used in 'hugo new site'
// and the template we use as a fall back.
DefaultArchetypeTemplateTemplate = `---
title: "{{ replace .File.ContentBaseName "-" " " | title }}"
@@ -271,7 +271,7 @@ func (b *contentBuilder) setArcheTypeFilenameToUse(ext string) {
func (b *contentBuilder) applyArcheType(contentFilename string, archetypeFi hugofs.FileMetaInfo) error {
p := b.h.GetContentPage(contentFilename)
if p == nil {
return fmt.Errorf("no page found for %q; if a file with the same name but different case already exists, please use a different filename or remove the existing file", contentFilename)
panic(fmt.Sprintf("[BUG] no Page found for %q", contentFilename))
}
f, err := b.sourceFs.Create(contentFilename)
+17 -17
View File
@@ -27,8 +27,8 @@ import (
"github.com/spf13/afero"
)
//go:embed all:project/*
var projectFs embed.FS
//go:embed all:site/*
var siteFs embed.FS
//go:embed all:theme/*
var themeFs embed.FS
@@ -41,10 +41,10 @@ func CreateTheme(createpath string, sourceFs afero.Fs, format string) error {
format = strings.ToLower(format)
projectConfig := map[string]any{
"baseURL": "https://example.org/",
"locale": "en-US",
"title": "My New Hugo Project",
siteConfig := map[string]any{
"baseURL": "https://example.org/",
"languageCode": "en-US",
"title": "My New Hugo Site",
"menus": map[string]any{
"main": []any{
map[string]any{
@@ -72,7 +72,7 @@ func CreateTheme(createpath string, sourceFs afero.Fs, format string) error {
},
}
err := createProjectConfig(sourceFs, createpath, projectConfig, format)
err := createSiteConfig(sourceFs, createpath, siteConfig, format)
if err != nil {
return err
}
@@ -91,8 +91,8 @@ func CreateTheme(createpath string, sourceFs afero.Fs, format string) error {
return copyFiles(createpath, sourceFs, themeFs)
}
// CreateProject creates a project skeleton.
func CreateProject(createpath string, sourceFs afero.Fs, force bool, format string) error {
// CreateSite creates a site skeleton.
func CreateSite(createpath string, sourceFs afero.Fs, force bool, format string) error {
format = strings.ToLower(format)
if exists, _ := helpers.Exists(createpath, sourceFs); exists {
if isDir, _ := helpers.IsDir(createpath, sourceFs); !isDir {
@@ -106,7 +106,7 @@ func CreateProject(createpath string, sourceFs afero.Fs, force bool, format stri
return errors.New(createpath + " already exists and is not empty. See --force.")
case !isEmpty && force:
var all []string
fs.WalkDir(projectFs, ".", func(path string, d fs.DirEntry, err error) error {
fs.WalkDir(siteFs, ".", func(path string, d fs.DirEntry, err error) error {
if d.IsDir() && path != "." {
all = append(all, path)
}
@@ -121,13 +121,13 @@ func CreateProject(createpath string, sourceFs afero.Fs, force bool, format stri
}
}
projectConfig := map[string]any{
"baseURL": "https://example.org/",
"locale": "en-us",
"title": "My New Hugo Project",
siteConfig := map[string]any{
"baseURL": "https://example.org/",
"title": "My New Hugo Site",
"languageCode": "en-us",
}
err := createProjectConfig(sourceFs, createpath, projectConfig, format)
err := createSiteConfig(sourceFs, createpath, siteConfig, format)
if err != nil {
return err
}
@@ -143,7 +143,7 @@ func CreateProject(createpath string, sourceFs afero.Fs, force bool, format stri
return err
}
return copyFiles(createpath, sourceFs, projectFs)
return copyFiles(createpath, sourceFs, siteFs)
}
func copyFiles(createpath string, sourceFs afero.Fs, skeleton embed.FS) error {
@@ -161,7 +161,7 @@ func copyFiles(createpath string, sourceFs afero.Fs, skeleton embed.FS) error {
})
}
func createProjectConfig(fs afero.Fs, createpath string, in map[string]any, format string) (err error) {
func createSiteConfig(fs afero.Fs, createpath string, in map[string]any, format string) (err error) {
var buf bytes.Buffer
err = parser.InterfaceToConfig(in, metadecoders.FormatFromString(format), &buf)
if err != nil {
@@ -1,4 +0,0 @@
footer {
border-top: 1px solid #222;
margin-top: 1rem;
}
@@ -1,4 +0,0 @@
header {
border-bottom: 1px solid #222;
margin-bottom: 1rem;
}
+10 -3
View File
@@ -1,6 +1,3 @@
@import "components/header.css";
@import "components/footer.css";
body {
color: #222;
font-family: sans-serif;
@@ -9,6 +6,16 @@ body {
max-width: 768px;
}
header {
border-bottom: 1px solid #222;
margin-bottom: 1rem;
}
footer {
border-top: 1px solid #222;
margin-top: 1rem;
}
a {
color: #00e;
text-decoration: none;
@@ -1,15 +1,9 @@
{{- with resources.Get "css/main.css" }}
{{- $opts := dict
"minify" (cond hugo.IsDevelopment false true)
"sourceMap" (cond hugo.IsDevelopment "linked" "none")
}}
{{- with . | css.Build $opts }}
{{- if hugo.IsDevelopment }}
<link rel="stylesheet" href="{{ .RelPermalink }}">
{{- else }}
{{- with . | fingerprint }}
<link rel="stylesheet" href="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous">
{{- end }}
{{- if hugo.IsDevelopment }}
<link rel="stylesheet" href="{{ .RelPermalink }}">
{{- else }}
{{- with . | minify | fingerprint }}
<link rel="stylesheet" href="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous">
{{- end }}
{{- end }}
{{- end }}
@@ -1,7 +1,8 @@
{{- with resources.Get "js/main.js" }}
{{- $opts := dict
"minify" (cond hugo.IsDevelopment false true)
"sourceMap" (cond hugo.IsDevelopment "linked" "none")
"minify" (not hugo.IsDevelopment)
"sourceMap" (cond hugo.IsDevelopment "external" "")
"targetPath" "js/main.js"
}}
{{- with . | js.Build $opts }}
{{- if hugo.IsDevelopment }}
+1 -1
View File
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="{{ site.Language.Locale }}" dir="{{ or site.Language.Direction `ltr` }}">
<html lang="{{ site.Language.LanguageCode }}" dir="{{ or site.Language.LanguageDirection `ltr` }}">
<head>
{{ partial "head.html" . }}
</head>
+12 -33
View File
@@ -1,16 +1,3 @@
// 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 deps
import (
@@ -28,8 +15,8 @@ import (
"github.com/gohugoio/hugo/cache/dynacache"
"github.com/gohugoio/hugo/cache/filecache"
"github.com/gohugoio/hugo/common/hexec"
"github.com/gohugoio/hugo/common/hmaps"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/config/allconfig"
@@ -173,8 +160,8 @@ func (d *Deps) Init() error {
d.BuildState.DeferredExecutionsGroupedByRenderingContext = make(map[tpl.RenderingContext]*DeferredExecutions)
}
d.BuildState.DeferredExecutions = &DeferredExecutions{
Executions: hmaps.NewCache[string, *tpl.DeferredExecution](),
FilenamesWithPostPrefix: hmaps.NewCache[string, bool](),
Executions: maps.NewCache[string, *tpl.DeferredExecution](),
FilenamesWithPostPrefix: maps.NewCache[string, bool](),
}
}
@@ -260,12 +247,12 @@ func (d *Deps) Init() error {
common = d.ResourceSpec.SpecCommon
}
d.Cfg.BaseConfig()
fileCaches, err := filecache.NewCaches(d.PathSpec)
if err != nil {
return fmt.Errorf("failed to create file caches from configuration: %w", err)
}
fileCaches := d.Cfg.FileCaches().(filecache.Caches)
fileCaches.SetResourceFs(d.BaseFs.ResourcesCache)
resourceSpec, err := resources.NewSpec(d.PathSpec, common, d.WasmDispatchers, fileCaches, d.MemCache, d.BuildState, d.Log, d, d.ExecHelper, d.BuildClosers, d.BuildState)
resourceSpec, err := resources.NewSpec(d.PathSpec, common, fileCaches, d.MemCache, d.BuildState, d.Log, d, d.ExecHelper, d.BuildClosers, d.BuildState)
if err != nil {
return fmt.Errorf("failed to create resource spec: %w", err)
}
@@ -441,9 +428,6 @@ type DepsCfg struct {
type BuildState struct {
counter uint64
// Tracks invocations of the Build method.
BuildCounter atomic.Uint64
mu sync.Mutex // protects state below.
OnSignalRebuild func(ids ...identity.Identity)
@@ -467,19 +451,14 @@ type Counters struct {
type DeferredExecutions struct {
// A set of filenames in /public that
// contains a post-processing prefix.
FilenamesWithPostPrefix *hmaps.Cache[string, bool]
FilenamesWithPostPrefix *maps.Cache[string, bool]
// Maps a placeholder to a deferred execution.
Executions *hmaps.Cache[string, *tpl.DeferredExecution]
Executions *maps.Cache[string, *tpl.DeferredExecution]
}
var _ identity.SignalRebuilder = (*BuildState)(nil)
// IsRebuild reports whether this is a rebuild.
func (b *BuildState) IsRebuild() bool {
return b.BuildCounter.Load() > 0
}
// StartStageRender will be called before a stage is rendered.
func (b *BuildState) StartStageRender(stage tpl.RenderingContext) {
}
@@ -488,8 +467,8 @@ func (b *BuildState) StartStageRender(stage tpl.RenderingContext) {
func (b *BuildState) StopStageRender(stage tpl.RenderingContext) {
b.DeferredExecutionsGroupedByRenderingContext[stage] = b.DeferredExecutions
b.DeferredExecutions = &DeferredExecutions{
Executions: hmaps.NewCache[string, *tpl.DeferredExecution](),
FilenamesWithPostPrefix: hmaps.NewCache[string, bool](),
Executions: maps.NewCache[string, *tpl.DeferredExecution](),
FilenamesWithPostPrefix: maps.NewCache[string, bool](),
}
}
+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,./data/docs.yaml
skip = *.ai,chroma.css,chroma_dark.css,.cspell.json
# Comma separated list of words to be ignored. Words must be lowercased.
ignore-words-list = abl,edn,ist,januar,te,trys,ue,womens
ignore-words-list = abl,edn,januar,te,trys,ue,womens
# Check file names as well.
check-filenames = true

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