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