mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-25 15:58:53 +00:00
Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d976efdda | |||
| 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 |
@@ -0,0 +1,13 @@
|
||||
have_fun: false
|
||||
memory_config:
|
||||
disabled: false
|
||||
code_review:
|
||||
disable: true
|
||||
comment_severity_threshold: HIGH
|
||||
max_review_comments: -1
|
||||
pull_request_opened:
|
||||
help: false
|
||||
summary: true
|
||||
code_review: true
|
||||
include_drafts: false
|
||||
ignore_patterns: []
|
||||
@@ -0,0 +1,17 @@
|
||||
You are an expert at detecting AI-generated content. Your main tasks is to analyze pull requests and determine if they were likely generated by AI and if this is disclosed in the pull request description.
|
||||
|
||||
Important: You must start your response with a confidence score line in exactly this format:
|
||||
Confidence Score: X% where X is a number between 0 and 100.
|
||||
|
||||
Then provide your analysis in this format:
|
||||
Reasoning: (explain your analysis)
|
||||
Key Indicators: (list specific elements that suggest AI or human authorship)
|
||||
|
||||
A high confidence score (>80%) means you are very confident the content was AI-generated.
|
||||
A low confidence score (<20%) means you are very confident the content was human-generated.
|
||||
Use scores between 20-80% when there is uncertainty.
|
||||
|
||||
If there's a high confidence score:
|
||||
|
||||
* Add the label "Suspect AI" to the PR
|
||||
* If it's not disclosed that it's AI generated, add a link to [AI Assistance Notice](../CONTRIBUTING.md#ai-assistance-notice) in the contribution gudelines.
|
||||
@@ -0,0 +1,17 @@
|
||||
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, reopened, ready_for_review, synchronize]
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
jobs:
|
||||
detect-ai:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
|
||||
- name: AI Watchdog
|
||||
uses: Citric-Sheep/ai-watchdog@0db4a635d309db0f8d82642130dfc33e91cf33fa # v0.2.3
|
||||
with:
|
||||
openai-api-key: ${{ secrets.OPENAI_API_KEY }}
|
||||
@@ -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'
|
||||
|
||||
@@ -34,9 +34,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 +45,28 @@ jobs:
|
||||
**/go.sum
|
||||
**/go.mod
|
||||
- name: Install Ruby
|
||||
uses: ruby/setup-ruby@44511735964dcb71245e7e55f72539531f7bc0eb # v1.257.0
|
||||
uses: ruby/setup-ruby@8aeb6ff8030dd539317f8e1769a044873b56ea71 # v1.268.0
|
||||
with:
|
||||
ruby-version: "2.7"
|
||||
bundler-cache: true #
|
||||
ruby-version: "3.4.5"
|
||||
- name: Install gems
|
||||
run: |
|
||||
gem install asciidoctor -v "2.0.26"
|
||||
gem install asciidoctor-diagram -v "3.1.0"
|
||||
gem install asciidoctor-diagram-ditaamini -v "1.0.3"
|
||||
- name: Install GoAT
|
||||
run: go install github.com/blampe/goat/cmd/goat@177de93b192b8ffae608e5d9ec421cc99bf68402
|
||||
- name: Install Java # required by asciidoctor-diagram-ditaamini
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "25"
|
||||
java-package: jre
|
||||
- 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,
|
||||
|
||||
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
+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
+8
-8
@@ -173,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 {
|
||||
@@ -190,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)
|
||||
|
||||
|
||||
+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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
}
|
||||
|
||||
+12
-3
@@ -31,6 +31,7 @@ import (
|
||||
"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"
|
||||
@@ -143,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) {
|
||||
@@ -463,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()
|
||||
@@ -827,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 {
|
||||
|
||||
+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
-3
@@ -49,6 +49,7 @@ import (
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/gohugoio/hugo/langs"
|
||||
"github.com/gohugoio/hugo/tpl/tplimpl"
|
||||
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
@@ -627,7 +628,7 @@ func (c *serverCommand) setServerInfoInConfig() error {
|
||||
panic("no server ports set")
|
||||
}
|
||||
return c.withConfE(func(conf *commonConfig) error {
|
||||
for i, language := range conf.configs.LanguagesDefaultFirst {
|
||||
for i, language := range conf.configs.Languages {
|
||||
isMultihost := conf.configs.IsMultihost
|
||||
var serverPort int
|
||||
if isMultihost {
|
||||
@@ -879,7 +880,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 +1035,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)
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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...)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
+84
-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 {
|
||||
@@ -106,14 +114,14 @@ func implementsIsZero(tp reflect.Type) bool {
|
||||
// Based on:
|
||||
// https://github.com/golang/go/blob/178a2c42254166cffed1b25fb1d3c7a5727cada6/src/text/template/exec.go#L306
|
||||
func IsTruthfulValue(val reflect.Value) (truth bool) {
|
||||
val = 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.Kind() == reflect.Pointer && val.IsNil() {
|
||||
if val.Kind() == reflect.Pointer && isNil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -150,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)
|
||||
|
||||
@@ -168,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)
|
||||
}
|
||||
@@ -177,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
|
||||
@@ -259,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
|
||||
@@ -276,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()
|
||||
|
||||
@@ -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,6 +20,7 @@ import (
|
||||
"time"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/gohugoio/hugo/htesting/hqt"
|
||||
)
|
||||
|
||||
type zeroStruct struct {
|
||||
@@ -79,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")
|
||||
}
|
||||
}
|
||||
@@ -98,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")
|
||||
}
|
||||
@@ -117,8 +190,7 @@ func BenchmarkIsTruthFulValue(b *testing.B) {
|
||||
nilPointer = reflect.ValueOf((*zeroStruct)(nil))
|
||||
)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for b.Loop() {
|
||||
IsTruthfulValue(stringHugo)
|
||||
IsTruthfulValue(stringEmpty)
|
||||
IsTruthfulValue(zero)
|
||||
@@ -151,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"
|
||||
|
||||
@@ -128,7 +129,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())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,9 +13,11 @@
|
||||
|
||||
package hugo
|
||||
|
||||
import "github.com/gohugoio/hugo/common/version"
|
||||
|
||||
// CurrentVersion represents the current build version.
|
||||
// This should be the only one.
|
||||
var CurrentVersion = Version{
|
||||
var CurrentVersion = version.Version{
|
||||
Major: 0,
|
||||
Minor: 153,
|
||||
PatchLevel: 0,
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -187,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".
|
||||
|
||||
@@ -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,39 @@ 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
+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()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -55,7 +55,7 @@ func (v Version) Version() VersionString {
|
||||
|
||||
// Compare implements the compare.Comparer interface.
|
||||
func (h Version) Compare(other any) int {
|
||||
return compareVersions(h, other)
|
||||
return CompareVersions(h, other)
|
||||
}
|
||||
|
||||
// VersionString represents a Hugo version string.
|
||||
@@ -67,7 +67,7 @@ func (h VersionString) String() string {
|
||||
|
||||
// Compare implements the compare.Comparer interface.
|
||||
func (h VersionString) Compare(other any) int {
|
||||
return compareVersions(h.Version(), other)
|
||||
return CompareVersions(h.Version(), other)
|
||||
}
|
||||
|
||||
func (h VersionString) Version() Version {
|
||||
@@ -83,18 +83,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 +140,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 +197,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
|
||||
+102
-104
@@ -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"
|
||||
@@ -99,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
|
||||
|
||||
@@ -139,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>
|
||||
@@ -200,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 {
|
||||
@@ -319,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)
|
||||
@@ -483,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,
|
||||
@@ -523,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
|
||||
@@ -586,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
|
||||
@@ -788,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
|
||||
|
||||
@@ -803,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
|
||||
}
|
||||
|
||||
@@ -833,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
|
||||
}
|
||||
@@ -897,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
|
||||
@@ -932,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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -946,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
|
||||
}
|
||||
|
||||
@@ -955,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
|
||||
}
|
||||
@@ -966,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
|
||||
}
|
||||
|
||||
@@ -983,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
|
||||
}
|
||||
}
|
||||
@@ -1060,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
|
||||
@@ -1077,6 +1068,13 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
|
||||
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) {
|
||||
@@ -1108,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
|
||||
@@ -1188,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)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
@@ -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
|
||||
}
|
||||
@@ -137,11 +143,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 +161,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 +226,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 +281,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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -44,6 +45,7 @@ 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()
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -93,7 +95,7 @@ func LoadConfig(d ConfigSourceDescriptor) (configs *Configs, err 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)
|
||||
}
|
||||
|
||||
@@ -218,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
|
||||
}
|
||||
|
||||
@@ -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,7 @@ type AllProvider interface {
|
||||
IgnoredLogs() map[string]bool
|
||||
WorkingDir() string
|
||||
EnableEmoji() bool
|
||||
ConfiguredDimensions() *sitesmatrix.ConfiguredDimensions
|
||||
}
|
||||
|
||||
// We cannot import the media package as that would create a circular dependency.
|
||||
@@ -110,3 +116,6 @@ func GetStringSlicePreserveString(cfg Provider, key string) []string {
|
||||
sd := cfg.Get(key)
|
||||
return types.ToStringSlicePreserveString(sd)
|
||||
}
|
||||
|
||||
/*func (cd ConfiguredDimensions) Language(v sitesmatrix.Vector) ConfiguredDimension {
|
||||
}*/
|
||||
|
||||
@@ -16,12 +16,10 @@ package config
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
xmaps "maps"
|
||||
|
||||
"github.com/spf13/cast"
|
||||
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
@@ -239,7 +237,12 @@ func (c *defaultConfigProvider) Merge(k string, v any) {
|
||||
func (c *defaultConfigProvider) Keys() []string {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return slices.Collect(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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
+2
-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
|
||||
}
|
||||
|
||||
+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
|
||||
|
||||
@@ -5,8 +5,8 @@ require (
|
||||
github.com/JohannesKaufmann/html-to-markdown/v2 v2.4.0
|
||||
github.com/alecthomas/chroma/v2 v2.20.0
|
||||
github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.2
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudfront v1.54.4
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.6
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudfront v1.56.0
|
||||
github.com/bep/clocks v0.5.0
|
||||
github.com/bep/debounce v1.2.0
|
||||
github.com/bep/gitmap v1.9.0
|
||||
@@ -23,11 +23,12 @@ require (
|
||||
github.com/bep/overlayfs v0.10.0
|
||||
github.com/bep/simplecobra v0.6.1
|
||||
github.com/bep/tmc v0.5.1
|
||||
github.com/bits-and-blooms/bitset v1.22.0
|
||||
github.com/cespare/xxhash/v2 v2.3.0
|
||||
github.com/clbanning/mxj/v2 v2.7.0
|
||||
github.com/disintegration/gift v1.2.1
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/evanw/esbuild v0.25.11
|
||||
github.com/evanw/esbuild v0.25.12
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/fortytw2/leaktest v1.3.0
|
||||
github.com/frankban/quicktest v1.14.6
|
||||
@@ -43,7 +44,6 @@ require (
|
||||
github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.3.1
|
||||
github.com/gohugoio/locales v0.14.0
|
||||
github.com/gohugoio/localescompressed v1.0.1
|
||||
github.com/gohugoio/testmodBuilder/mods v0.0.0-20190520184928-c56af20f2e95
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/hairyhenderson/go-codeowners v0.7.0
|
||||
@@ -62,15 +62,14 @@ require (
|
||||
github.com/pelletier/go-toml/v2 v2.2.4
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
|
||||
github.com/rogpeppe/go-internal v1.14.1
|
||||
github.com/sanity-io/litter v1.5.8
|
||||
github.com/spf13/afero v1.15.0
|
||||
github.com/spf13/cast v1.10.0
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/spf13/fsync v0.10.1
|
||||
github.com/spf13/pflag v1.0.7
|
||||
github.com/tdewolff/minify/v2 v2.24.4
|
||||
github.com/tdewolff/parse/v2 v2.8.4
|
||||
github.com/tetratelabs/wazero v1.9.0
|
||||
github.com/tdewolff/minify/v2 v2.24.5
|
||||
github.com/tdewolff/parse/v2 v2.8.5-0.20251020133559-0efcf90bef1a
|
||||
github.com/tetratelabs/wazero v1.10.0
|
||||
github.com/yuin/goldmark v1.7.13
|
||||
github.com/yuin/goldmark-emoji v1.0.6
|
||||
go.uber.org/automaxprocs v1.5.3
|
||||
@@ -81,14 +80,14 @@ require (
|
||||
golang.org/x/sync v0.17.0
|
||||
golang.org/x/text v0.30.0
|
||||
golang.org/x/tools v0.38.0
|
||||
google.golang.org/api v0.251.0
|
||||
google.golang.org/api v0.255.0
|
||||
rsc.io/qr v0.2.0
|
||||
)
|
||||
|
||||
require (
|
||||
cel.dev/expr v0.24.0 // indirect
|
||||
cloud.google.com/go v0.121.4 // indirect
|
||||
cloud.google.com/go/auth v0.16.5 // indirect
|
||||
cloud.google.com/go/auth v0.17.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
cloud.google.com/go/iam v1.5.2 // indirect
|
||||
@@ -111,8 +110,8 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.70 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 // indirect
|
||||
@@ -123,7 +122,7 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 // indirect
|
||||
github.com/aws/smithy-go v1.23.0 // indirect
|
||||
github.com/aws/smithy-go v1.23.2 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
|
||||
@@ -131,7 +130,7 @@ require (
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
|
||||
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.1 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.2 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.21.0 // indirect
|
||||
@@ -177,15 +176,15 @@ require (
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.37.0 // indirect
|
||||
golang.org/x/crypto v0.43.0 // indirect
|
||||
golang.org/x/oauth2 v0.31.0 // indirect
|
||||
golang.org/x/oauth2 v0.32.0 // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
golang.org/x/time v0.13.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
|
||||
google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250715232539-7130f93afb79 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 // indirect
|
||||
google.golang.org/grpc v1.75.1 // indirect
|
||||
google.golang.org/protobuf v1.36.9 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect
|
||||
google.golang.org/grpc v1.76.0 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
howett.net/plist v1.0.0 // indirect
|
||||
|
||||
@@ -21,8 +21,8 @@ cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmW
|
||||
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
|
||||
cloud.google.com/go v0.121.4 h1:cVvUiY0sX0xwyxPwdSU2KsF9knOVmtRyAMt8xou0iTs=
|
||||
cloud.google.com/go v0.121.4/go.mod h1:XEBchUiHFJbz4lKBZwYBDHV/rSyfFktk737TLDU089s=
|
||||
cloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI=
|
||||
cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ=
|
||||
cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4=
|
||||
cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
@@ -106,8 +106,8 @@ github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c h1:651/eoCRnQ7YtS
|
||||
github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE=
|
||||
github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.2 h1:EJLg8IdbzgeD7xgvZ+I8M1e0fL0ptn/M47lianzth0I=
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.2/go.mod h1:sDioUELIUO9Znk23YVmIk86/9DOpkbyyVb1i/gUNFXY=
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.6 h1:2JrPCVgWJm7bm83BDwY5z8ietmeJUbh3O2ACnn+Xsqk=
|
||||
github.com/aws/aws-sdk-go-v2 v1.39.6/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 h1:12SpdwU8Djs+YGklkinSSlcrPyj3H4VifVsKf78KbwA=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11/go.mod h1:dd+Lkp6YmMryke+qxW/VnKyhMBDTYP41Q2Bb+6gNZgY=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.29.17 h1:jSuiQ5jEe4SAMH6lLRMY9OVC+TqJLP5655pBGjmnjr0=
|
||||
@@ -118,16 +118,16 @@ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 h1:KAXP9JSHO1vKGCr5f4O6Wm
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32/go.mod h1:h4Sg6FQdexC1yYG9RDnOvLbW1a/P986++/Y/a+GyEM8=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84 h1:cTXRdLkpBanlDwISl+5chq5ui1d1YWg4PWMR9c3kXyw=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.84/go.mod h1:kwSy5X7tfIHN39uucmjQVs2LvDdXEjQucgQQEqCggEo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9 h1:se2vOWGD3dWQUtfn4wEjRQJb1HK1XsNIt825gskZ970=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.9/go.mod h1:hijCGH2VfbZQxqCDN7bwz/4dzxV+hkyhjawAtdPWKZA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9 h1:6RBnKZLkJM4hQ+kN6E7yWFveOTg8NLPHAkqrs4ZPlTU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.9/go.mod h1:V9rQKRmK7AWuEsOMnHzKj8WyrIir1yUJbZxDuZLFvXI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 h1:a+8/MLcWlIxo1lF9xaGt3J/u3yOZx+CdSveSNwjhD40=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13/go.mod h1:oGnKwIYZ4XttyU2JWxFrwvhF6YKiK/9/wmE3v3Iu9K8=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 h1:HBSI2kDkMdWz4ZM7FjwE7e/pWDEZ+nR95x8Ztet1ooY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13/go.mod h1:YE94ZoDArI7awZqJzBAZ3PDD2zSfuP7w6P2knOzIn8M=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 h1:GMYy2EOWfzdP3wfVAGXBNKY5vK4K8vMET4sYOYltmqs=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36/go.mod h1:gDhdAV6wL3PmPqBhiPbnlS447GoWs8HTTOYef9/9Inw=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudfront v1.54.4 h1:tVpbQcr1A0c+VTqtKEN9vfB0qer2SjfxX3LYojSGUq0=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudfront v1.54.4/go.mod h1:dYwFVhUsRZt7COcGP23ei0lY8gX8ZSHrbyX49VB93MA=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudfront v1.56.0 h1:MhbMTYraEw+I/l258s/aGqJOFdKkdcgBJr1NrydMlBw=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudfront v1.56.0/go.mod h1:UtP1sSXq2FHHO7Lvn4mNplFS4x7oP4+uMIJIQ8+3JyY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 h1:CXV68E2dNqhuynZJPB80bhPQwAKqBWVer887figW6Jc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4/go.mod h1:/xFi9KtvBXP97ppCz1TAEvU1Uf66qvid89rbem3wCzQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 h1:nAP2GYbfh8dd2zGZqFRSMlq+/F6cMPBUuCsGAMkN074=
|
||||
@@ -144,8 +144,8 @@ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 h1:BpOxT3yhLwSJ77qIY3DoHAQj
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3/go.mod h1:vq/GQR1gOFLquZMSrxUK/cpvKCNVYibNyJ1m7JrU88E=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 h1:NFOJ/NXEGV4Rq//71Hs1jC/NvPs1ezajK+yQmkwnPV0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.34.0/go.mod h1:7ph2tGpfQvwzgistp2+zga9f+bCjlQJPkPUmMgDSD7w=
|
||||
github.com/aws/smithy-go v1.23.0 h1:8n6I3gXzWJB2DxBDnfxgBaSX6oe0d/t10qGz7OKqMCE=
|
||||
github.com/aws/smithy-go v1.23.0/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
|
||||
github.com/aws/smithy-go v1.23.2 h1:Crv0eatJUQhaManss33hS5r40CG3ZFH+21XSkqMrIUM=
|
||||
github.com/aws/smithy-go v1.23.2/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/bep/clocks v0.5.0 h1:hhvKVGLPQWRVsBP/UB7ErrHYIO42gINVbvqxvYTPVps=
|
||||
@@ -182,6 +182,8 @@ github.com/bep/tmc v0.5.1 h1:CsQnSC6MsomH64gw0cT5f+EwQDcvZz4AazKunFwTpuI=
|
||||
github.com/bep/tmc v0.5.1/go.mod h1:tGYHN8fS85aJPhDLgXETVKp+PR382OvFi2+q2GkGsq0=
|
||||
github.com/bep/workers v1.0.0 h1:U+H8YmEaBCEaFZBst7GcRVEoqeRC9dzH2dWOwGmOchg=
|
||||
github.com/bep/workers v1.0.0/go.mod h1:7kIESOB86HfR2379pwoMWNy8B50D7r99fRLUyPSNyCs=
|
||||
github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4=
|
||||
github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
@@ -199,7 +201,6 @@ github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1Ig
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
@@ -226,8 +227,8 @@ github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJP
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
|
||||
github.com/evanw/esbuild v0.25.11 h1:NGtezc+xk+Mti4fgWaoD3dncZNCzcTA+r0BxMV3Koyw=
|
||||
github.com/evanw/esbuild v0.25.11/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
|
||||
github.com/evanw/esbuild v0.25.12 h1:7kIg7aG2++vhheW5YCzut1q1AjehYVQU752NcMuGVsw=
|
||||
github.com/evanw/esbuild v0.25.12/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
@@ -246,8 +247,8 @@ github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4L
|
||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-jose/go-jose/v4 v4.1.1 h1:JYhSgy4mXXzAdF3nUx3ygx347LRXJRrpgyU3adRmkAI=
|
||||
github.com/go-jose/go-jose/v4 v4.1.1/go.mod h1:BdsZGqgdO3b6tTc6LSE56wcDbMMLuPsw5d4ZD5f94kA=
|
||||
github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI=
|
||||
github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
@@ -281,8 +282,6 @@ github.com/gohugoio/locales v0.14.0 h1:Q0gpsZwfv7ATHMbcTNepFd59H7GoykzWJIxi113XG
|
||||
github.com/gohugoio/locales v0.14.0/go.mod h1:ip8cCAv/cnmVLzzXtiTpPwgJ4xhKZranqNqtoIu0b/4=
|
||||
github.com/gohugoio/localescompressed v1.0.1 h1:KTYMi8fCWYLswFyJAeOtuk/EkXR/KPTHHNN9OS+RTxo=
|
||||
github.com/gohugoio/localescompressed v1.0.1/go.mod h1:jBF6q8D7a0vaEmcWPNcAjUZLJaIVNiwvM3WlmTvooB0=
|
||||
github.com/gohugoio/testmodBuilder/mods v0.0.0-20190520184928-c56af20f2e95 h1:sgew0XCnZwnzpWxTt3V8LLiCO7OQi3C6dycaE67wfkU=
|
||||
github.com/gohugoio/testmodBuilder/mods v0.0.0-20190520184928-c56af20f2e95/go.mod h1:bOlVlCa1/RajcHpXkrUXPSHB/Re1UnlXxD1Qp8SKOd8=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3 h1:kkGXqQOBSDDWRhWNXTFpqGSCMyh/PLnqUvMGJPDJDs0=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.3/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
@@ -459,7 +458,6 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
||||
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
@@ -479,8 +477,6 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc=
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
|
||||
github.com/sanity-io/litter v1.5.8 h1:uM/2lKrWdGbRXDrIq08Lh9XtVYoeGtcQxk9rtQ7+rYg=
|
||||
github.com/sanity-io/litter v1.5.8/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=
|
||||
github.com/sebdah/goldie/v2 v2.7.1 h1:PkBHymaYdtvEkZV7TmyqKxdmn5/Vcj+8TpATWZjnG5E=
|
||||
github.com/sebdah/goldie/v2 v2.7.1/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
|
||||
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
|
||||
@@ -503,7 +499,6 @@ github.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GB
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
@@ -513,14 +508,14 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tdewolff/minify/v2 v2.24.4 h1:pQyr6eWDa+RXtAoZg+6wurh0jB9ojqw/qc5LlU7/z6c=
|
||||
github.com/tdewolff/minify/v2 v2.24.4/go.mod h1:iD9Qn7/brhKY9d0KLKMkZrqS8/bqxSxRKruBi7V6m+w=
|
||||
github.com/tdewolff/parse/v2 v2.8.4 h1:A6slgBLGGDPBMGA28KQZfHpaKffuNvhOe7zSag+x/rw=
|
||||
github.com/tdewolff/parse/v2 v2.8.4/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo=
|
||||
github.com/tdewolff/minify/v2 v2.24.5 h1:ytxthX3xSxrK3Xx5B38flg5moCKs/dB8VwiD/RzJViU=
|
||||
github.com/tdewolff/minify/v2 v2.24.5/go.mod h1:q09KtNnVai7TyEzGEZeWPAnK+c8Z+NI8prCXZW652bo=
|
||||
github.com/tdewolff/parse/v2 v2.8.5-0.20251020133559-0efcf90bef1a h1:Rmq+utdraciok/97XHRweYdsAo/M4LOswpCboo3yvN4=
|
||||
github.com/tdewolff/parse/v2 v2.8.5-0.20251020133559-0efcf90bef1a/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo=
|
||||
github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE=
|
||||
github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
||||
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
|
||||
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
|
||||
github.com/tetratelabs/wazero v1.10.0 h1:CXP3zneLDl6J4Zy8N/J+d5JsWKfrjE6GtvVK1fpnDlk=
|
||||
github.com/tetratelabs/wazero v1.10.0/go.mod h1:DRm5twOQ5Gr1AoEdSi0CLjDQF1J9ZAuyqFIjl1KKfQU=
|
||||
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
|
||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||
github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0=
|
||||
@@ -674,8 +669,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ
|
||||
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo=
|
||||
golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
|
||||
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -765,8 +760,8 @@ golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
|
||||
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
@@ -848,8 +843,8 @@ google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz513
|
||||
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
|
||||
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
|
||||
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
|
||||
google.golang.org/api v0.251.0 h1:6lea5nHRT8RUmpy9kkC2PJYnhnDAB13LqrLSVQlMIE8=
|
||||
google.golang.org/api v0.251.0/go.mod h1:Rwy0lPf/TD7+T2VhYcffCHhyyInyuxGjICxdfLqT7KI=
|
||||
google.golang.org/api v0.255.0 h1:OaF+IbRwOottVCYV2wZan7KUq7UeNUQn1BcPc4K7lE4=
|
||||
google.golang.org/api v0.255.0/go.mod h1:d1/EtvCLdtiWEV4rAEHDHGh2bCnqsWhw+M8y2ECN4a8=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
@@ -895,10 +890,10 @@ google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6D
|
||||
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79 h1:Nt6z9UHqSlIdIGJdz6KhTIs2VRx/iOsA5iE8bmQNcxs=
|
||||
google.golang.org/genproto v0.0.0-20250715232539-7130f93afb79/go.mod h1:kTmlBHMPqR5uCZPBvwa2B18mvubkjyY3CRLI0c6fj0s=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250715232539-7130f93afb79 h1:iOye66xuaAK0WnkPuhQPUFy8eJcmwUXqGGP3om6IxX8=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250715232539-7130f93afb79/go.mod h1:HKJDgKsFUnv5VAGeQjz8kxcgDP0HoE0iZNp0OdZNlhE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 h1:i8QOKZfYg6AbGVZzUAY3LrNWCKF8O6zFisU9Wl9RER4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b h1:ULiyYQ0FdsJhwwZUwbaXpZF5yUE3h+RA+gxvBu37ucc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:oDOGiMSXHL4sDTJvFvIB9nRQCGdLP1o/iVaqQK8zB+M=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda h1:i/Q+bfisr7gq6feoJnS/DlpdwEL4ihp41fvRiM3Ork0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
@@ -915,8 +910,8 @@ google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
|
||||
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
|
||||
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||
google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
|
||||
google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
@@ -927,8 +922,8 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
|
||||
@@ -54,8 +54,8 @@ func TestTrimShortHTML(t *testing.T) {
|
||||
|
||||
func BenchmarkTrimShortHTML(b *testing.B) {
|
||||
c := newTestContentSpec(nil)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
|
||||
for b.Loop() {
|
||||
c.TrimShortHTML([]byte("<p>Simple paragraph</p>"), "markdown")
|
||||
}
|
||||
}
|
||||
@@ -134,8 +134,8 @@ func TestTotalWords(t *testing.T) {
|
||||
}
|
||||
|
||||
func BenchmarkTotalWords(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
|
||||
for b.Loop() {
|
||||
wordCount := helpers.TotalWords(totalWordsBenchmarkString)
|
||||
if wordCount != 400 {
|
||||
b.Fatal("Wordcount error")
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
@@ -59,65 +58,6 @@ func FirstUpper(s string) string {
|
||||
return string(unicode.ToUpper(r)) + s[n:]
|
||||
}
|
||||
|
||||
// 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]
|
||||
}
|
||||
|
||||
// ReaderToBytes takes an io.Reader argument, reads from it
|
||||
// and returns bytes.
|
||||
func ReaderToBytes(lines io.Reader) []byte {
|
||||
|
||||
+2
-81
@@ -14,7 +14,6 @@
|
||||
package helpers_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -215,8 +214,8 @@ func TestGetTitleFunc(t *testing.T) {
|
||||
}
|
||||
|
||||
func BenchmarkReaderContains(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
|
||||
for b.Loop() {
|
||||
for i, this := range containsBenchTestData {
|
||||
result := helpers.ReaderContains(strings.NewReader(this.v1), this.v2)
|
||||
if result != this.expect {
|
||||
@@ -226,84 +225,6 @@ func BenchmarkReaderContains(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUniqueStrings(t *testing.T) {
|
||||
in := []string{"a", "b", "a", "b", "c", "", "a", "", "d"}
|
||||
output := helpers.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 := helpers.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 := helpers.UniqueStringsSorted(in)
|
||||
expected := []string{"", "a", "b", "c", "d"}
|
||||
c.Assert(output, qt.DeepEquals, expected)
|
||||
c.Assert(helpers.UniqueStringsSorted(nil), qt.IsNil)
|
||||
}
|
||||
|
||||
func BenchmarkUniqueStrings(b *testing.B) {
|
||||
input := []string{"a", "b", "d", "e", "d", "h", "a", "i"}
|
||||
|
||||
b.Run("Safe", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
result := helpers.UniqueStrings(input)
|
||||
if len(result) != 6 {
|
||||
b.Fatalf("invalid count: %d", len(result))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Reuse slice", func(b *testing.B) {
|
||||
b.StopTimer()
|
||||
inputs := make([][]string, b.N)
|
||||
for i := 0; i < b.N; i++ {
|
||||
inputc := make([]string, len(input))
|
||||
copy(inputc, input)
|
||||
inputs[i] = inputc
|
||||
}
|
||||
b.StartTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
inputc := inputs[i]
|
||||
|
||||
result := helpers.UniqueStringsReuse(inputc)
|
||||
if len(result) != 6 {
|
||||
b.Fatalf("invalid count: %d", len(result))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Reuse slice sorted", func(b *testing.B) {
|
||||
b.StopTimer()
|
||||
inputs := make([][]string, b.N)
|
||||
for i := 0; i < b.N; i++ {
|
||||
inputc := make([]string, len(input))
|
||||
copy(inputc, input)
|
||||
inputs[i] = inputc
|
||||
}
|
||||
b.StartTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
inputc := inputs[i]
|
||||
|
||||
result := helpers.UniqueStringsSorted(inputc)
|
||||
if len(result) != 6 {
|
||||
b.Fatalf("invalid count: %d", len(result))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestStringSliceToList(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
slice []string
|
||||
|
||||
+7
-6
@@ -25,6 +25,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
"github.com/gohugoio/hugo/common/hstrings"
|
||||
"github.com/gohugoio/hugo/common/text"
|
||||
"github.com/gohugoio/hugo/htesting"
|
||||
|
||||
@@ -72,8 +73,8 @@ func MakeTitle(inpath string) string {
|
||||
// MakeTitleInPath converts the path given to a suitable title, trimming whitespace
|
||||
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")
|
||||
@@ -188,7 +189,7 @@ func ExtractAndGroupRootPaths(paths []string) []NamedSlice {
|
||||
groupsStr[i] = strings.Join(g, "/")
|
||||
}
|
||||
|
||||
groupsStr = UniqueStringsSorted(groupsStr)
|
||||
groupsStr = hstrings.UniqueStringsSorted(groupsStr)
|
||||
|
||||
var result []NamedSlice
|
||||
|
||||
@@ -209,7 +210,7 @@ func ExtractAndGroupRootPaths(paths []string) []NamedSlice {
|
||||
}
|
||||
}
|
||||
|
||||
ns.Slice = UniqueStrings(ExtractRootPaths(ns.Slice))
|
||||
ns.Slice = hstrings.UniqueStrings(ExtractRootPaths(ns.Slice))
|
||||
|
||||
result = append(result, ns)
|
||||
}
|
||||
@@ -225,8 +226,8 @@ func ExtractRootPaths(paths []string) []string {
|
||||
r := make([]string, len(paths))
|
||||
for i, p := range paths {
|
||||
root := filepath.ToSlash(p)
|
||||
sections := strings.Split(root, "/")
|
||||
for _, section := range sections {
|
||||
sections := strings.SplitSeq(root, "/")
|
||||
for section := range sections {
|
||||
if section != "" {
|
||||
root = section
|
||||
break
|
||||
|
||||
+1
-10
@@ -32,19 +32,11 @@ type PathSpec struct {
|
||||
|
||||
// The file systems to use
|
||||
Fs *hugofs.Fs
|
||||
|
||||
// The config provider to use
|
||||
Cfg config.AllProvider
|
||||
}
|
||||
|
||||
// NewPathSpec creates a new PathSpec from the given filesystems and language.
|
||||
func NewPathSpec(fs *hugofs.Fs, cfg config.AllProvider, logger loggers.Logger) (*PathSpec, error) {
|
||||
return NewPathSpecWithBaseBaseFsProvided(fs, cfg, logger, nil)
|
||||
}
|
||||
|
||||
// NewPathSpecWithBaseBaseFsProvided creates a new PathSpec from the given filesystems and language.
|
||||
// If an existing BaseFs is provided, parts of that is reused.
|
||||
func NewPathSpecWithBaseBaseFsProvided(fs *hugofs.Fs, cfg config.AllProvider, logger loggers.Logger, baseBaseFs *filesystems.BaseFs) (*PathSpec, error) {
|
||||
func NewPathSpec(fs *hugofs.Fs, cfg config.AllProvider, logger loggers.Logger, baseBaseFs *filesystems.BaseFs) (*PathSpec, error) {
|
||||
p, err := paths.New(fs, cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -65,7 +57,6 @@ func NewPathSpecWithBaseBaseFsProvided(fs *hugofs.Fs, cfg config.AllProvider, lo
|
||||
Paths: p,
|
||||
BaseFs: bfs,
|
||||
Fs: fs,
|
||||
Cfg: cfg,
|
||||
ProcessingStats: NewProcessingStats(p.Lang()),
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ func newTestPathSpecFromCfgAndLang(cfg config.Provider, lang string) *helpers.Pa
|
||||
}
|
||||
}
|
||||
fs := hugofs.NewFrom(mfs, conf.BaseConfig())
|
||||
ps, err := helpers.NewPathSpec(fs, conf, loggers.NewDefault())
|
||||
ps, err := helpers.NewPathSpec(fs, conf, loggers.NewDefault(), nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
+4
-4
@@ -259,8 +259,8 @@ func BenchmarkRelURL(b *testing.B) {
|
||||
v := config.New()
|
||||
v.Set("baseURL", "https://base/")
|
||||
p := newTestPathSpecFromCfgAndLang(v, "")
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
|
||||
for b.Loop() {
|
||||
_ = p.RelURL("https://base/foo/bar", false)
|
||||
}
|
||||
}
|
||||
@@ -271,12 +271,12 @@ func BenchmarkAbsURL(b *testing.B) {
|
||||
p := newTestPathSpecFromCfgAndLang(v, "")
|
||||
b.ResetTimer()
|
||||
b.Run("relurl", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
for b.Loop() {
|
||||
_ = p.AbsURL("foo/bar", false)
|
||||
}
|
||||
})
|
||||
b.Run("absurl", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
for b.Loop() {
|
||||
_ = p.AbsURL("https://base/foo/bar", false)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -44,6 +44,46 @@ var IsSameFloat64 = qt.CmpEquals(cmp.Comparer(func(a, b float64) bool {
|
||||
return math.Abs(a-b) < 0.0001
|
||||
}))
|
||||
|
||||
// IsSameNumber asserts that two number values are equal within a small delta.
|
||||
var IsSameNumber = qt.CmpEquals(
|
||||
cmp.Comparer(func(a, b float64) bool {
|
||||
return math.Abs(a-b) < 0.0001
|
||||
}),
|
||||
cmp.Comparer(func(a, b float32) bool {
|
||||
return math.Abs(float64(a)-float64(b)) < 0.0001
|
||||
}),
|
||||
cmp.Comparer(func(a, b int) bool {
|
||||
return a == b
|
||||
}),
|
||||
cmp.Comparer(func(a, b int8) bool {
|
||||
return a == b
|
||||
}),
|
||||
cmp.Comparer(func(a, b int16) bool {
|
||||
return a == b
|
||||
}),
|
||||
cmp.Comparer(func(a, b int32) bool {
|
||||
return a == b
|
||||
}),
|
||||
cmp.Comparer(func(a, b int64) bool {
|
||||
return a == b
|
||||
}),
|
||||
cmp.Comparer(func(a, b uint) bool {
|
||||
return a == b
|
||||
}),
|
||||
cmp.Comparer(func(a, b uint8) bool {
|
||||
return a == b
|
||||
}),
|
||||
cmp.Comparer(func(a, b uint16) bool {
|
||||
return a == b
|
||||
}),
|
||||
cmp.Comparer(func(a, b uint32) bool {
|
||||
return a == b
|
||||
}),
|
||||
cmp.Comparer(func(a, b uint64) bool {
|
||||
return a == b
|
||||
}),
|
||||
)
|
||||
|
||||
type argNames []string
|
||||
|
||||
func (a argNames) ArgNames() []string {
|
||||
|
||||
+162
-32
@@ -14,15 +14,19 @@
|
||||
package hugofs
|
||||
|
||||
import (
|
||||
"context"
|
||||
iofs "io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
"sort"
|
||||
|
||||
"github.com/bep/helpers/contexthelpers"
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
"github.com/gohugoio/hugo/common/paths"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/hugofs/files"
|
||||
"github.com/gohugoio/hugo/hugolib/sitesmatrix"
|
||||
"github.com/spf13/afero"
|
||||
"golang.org/x/text/unicode/norm"
|
||||
)
|
||||
@@ -39,7 +43,10 @@ func NewComponentFs(opts ComponentFsOptions) *componentFs {
|
||||
return &componentFs{Fs: bfs, opts: opts}
|
||||
}
|
||||
|
||||
var _ FilesystemUnwrapper = (*componentFs)(nil)
|
||||
var (
|
||||
_ FilesystemUnwrapper = (*componentFs)(nil)
|
||||
_ ReadDirWithContextDir = (*componentFsDir)(nil)
|
||||
)
|
||||
|
||||
// componentFs is a filesystem that holds one of the Hugo components, e.g. content, layouts etc.
|
||||
type componentFs struct {
|
||||
@@ -59,12 +66,24 @@ type componentFsDir struct {
|
||||
fs *componentFs
|
||||
}
|
||||
|
||||
// ReadDir reads count entries from this virtual directory and
|
||||
// sorts the entries according to the component filesystem rules.
|
||||
func (f *componentFsDir) ReadDir(count int) ([]iofs.DirEntry, error) {
|
||||
type (
|
||||
contextKey uint8
|
||||
)
|
||||
|
||||
const (
|
||||
contextKeyIsInLeafBundle contextKey = iota
|
||||
)
|
||||
|
||||
var componentFsContext = struct {
|
||||
IsInLeafBundle contexthelpers.ContextDispatcher[bool]
|
||||
}{
|
||||
IsInLeafBundle: contexthelpers.NewContextDispatcher[bool](contextKeyIsInLeafBundle),
|
||||
}
|
||||
|
||||
func (f *componentFsDir) ReadDirWithContext(ctx context.Context, count int) ([]iofs.DirEntry, context.Context, error) {
|
||||
fis, err := f.DirOnlyOps.(iofs.ReadDirFile).ReadDir(-1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, ctx, err
|
||||
}
|
||||
|
||||
// Filter out any symlinks.
|
||||
@@ -79,7 +98,7 @@ func (f *componentFsDir) ReadDir(count int) ([]iofs.DirEntry, error) {
|
||||
if herrors.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
return nil, ctx, err
|
||||
}
|
||||
if info.Mode()&os.ModeSymlink == 0 {
|
||||
keep = true
|
||||
@@ -92,8 +111,8 @@ func (f *componentFsDir) ReadDir(count int) ([]iofs.DirEntry, error) {
|
||||
}
|
||||
|
||||
fis = fis[:n]
|
||||
|
||||
n = 0
|
||||
|
||||
for _, fi := range fis {
|
||||
s := path.Join(f.name, fi.Name())
|
||||
if _, ok := f.fs.applyMeta(fi, s); ok {
|
||||
@@ -101,7 +120,9 @@ func (f *componentFsDir) ReadDir(count int) ([]iofs.DirEntry, error) {
|
||||
n++
|
||||
}
|
||||
}
|
||||
|
||||
fis = fis[:n]
|
||||
n = 0
|
||||
|
||||
sort.Slice(fis, func(i, j int) bool {
|
||||
fimi, fimj := fis[i].(FileMetaInfo), fis[j].(FileMetaInfo)
|
||||
@@ -110,6 +131,16 @@ func (f *componentFsDir) ReadDir(count int) ([]iofs.DirEntry, error) {
|
||||
}
|
||||
fimim, fimjm := fimi.Meta(), fimj.Meta()
|
||||
|
||||
bi, bj := fimim.PathInfo.Base(), fimjm.PathInfo.Base()
|
||||
if bi == bj {
|
||||
matrixi, matrixj := fimim.SitesMatrix, fimjm.SitesMatrix
|
||||
l1, l2 := matrixi.LenVectors(), matrixj.LenVectors()
|
||||
if l1 != l2 {
|
||||
// Pull the ones with the least number of sites defined to the top.
|
||||
return l1 < l2
|
||||
}
|
||||
}
|
||||
|
||||
if fimim.ModuleOrdinal != fimjm.ModuleOrdinal {
|
||||
switch f.fs.opts.Component {
|
||||
case files.ComponentFolderI18n:
|
||||
@@ -133,7 +164,6 @@ func (f *componentFsDir) ReadDir(count int) ([]iofs.DirEntry, error) {
|
||||
}
|
||||
|
||||
if exti != extj {
|
||||
// This pulls .md above .html.
|
||||
return exti > extj
|
||||
}
|
||||
|
||||
@@ -149,7 +179,108 @@ func (f *componentFsDir) ReadDir(count int) ([]iofs.DirEntry, error) {
|
||||
return fimi.Name() < fimj.Name()
|
||||
})
|
||||
|
||||
return fis, nil
|
||||
if f.fs.opts.Component == files.ComponentFolderContent {
|
||||
isInLeafBundle := componentFsContext.IsInLeafBundle.Get(ctx)
|
||||
var isCurrentLeafBundle bool
|
||||
for _, fi := range fis {
|
||||
if fi.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
pi := fi.(FileMetaInfo).Meta().PathInfo
|
||||
|
||||
if pi.IsLeafBundle() {
|
||||
isCurrentLeafBundle = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !isInLeafBundle && isCurrentLeafBundle {
|
||||
ctx = componentFsContext.IsInLeafBundle.Set(ctx, true)
|
||||
}
|
||||
|
||||
if isInLeafBundle || isCurrentLeafBundle {
|
||||
for _, fi := range fis {
|
||||
if fi.IsDir() {
|
||||
continue
|
||||
}
|
||||
pi := fi.(FileMetaInfo).Meta().PathInfo
|
||||
|
||||
// Everything below a leaf bundle is a resource.
|
||||
isResource := isInLeafBundle && pi.Type() > paths.TypeFile
|
||||
// Every sibling of a leaf bundle is a resource.
|
||||
isResource = isResource || (isCurrentLeafBundle && !pi.IsLeafBundle())
|
||||
|
||||
if isResource {
|
||||
paths.ModifyPathBundleTypeResource(pi)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type typeBase struct {
|
||||
Type paths.Type
|
||||
Base string
|
||||
}
|
||||
|
||||
variants := make(map[typeBase][]sitesmatrix.VectorProvider)
|
||||
|
||||
for _, fi := range fis {
|
||||
|
||||
if !fi.IsDir() {
|
||||
meta := fi.(FileMetaInfo).Meta()
|
||||
|
||||
pi := meta.PathInfo
|
||||
|
||||
if pi.Component() == files.ComponentFolderLayouts || pi.Component() == files.ComponentFolderContent {
|
||||
|
||||
var base string
|
||||
switch pi.Component() {
|
||||
case files.ComponentFolderContent:
|
||||
base = pi.Base() + pi.Custom()
|
||||
default:
|
||||
base = pi.PathNoLang()
|
||||
}
|
||||
|
||||
baseName := typeBase{pi.Type(), base}
|
||||
|
||||
// There may be multiple languge/version/role combinations for the same file.
|
||||
// The most important come early.
|
||||
matrixes, found := variants[baseName]
|
||||
|
||||
if found {
|
||||
complement := meta.SitesMatrix.Complement(matrixes...)
|
||||
if complement == nil || complement.LenVectors() == 0 {
|
||||
continue
|
||||
}
|
||||
matrixes = append(matrixes, meta.SitesMatrix)
|
||||
meta.SitesMatrix = complement
|
||||
|
||||
variants[baseName] = matrixes
|
||||
|
||||
} else {
|
||||
matrixes = []sitesmatrix.VectorProvider{meta.SitesMatrix}
|
||||
variants[baseName] = matrixes
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fis[n] = fi
|
||||
n++
|
||||
|
||||
}
|
||||
fis = fis[:n]
|
||||
|
||||
return fis, ctx, nil
|
||||
}
|
||||
|
||||
// ReadDir reads count entries from this virtual directory and
|
||||
// sorts the entries according to the component filesystem rules.
|
||||
func (f *componentFsDir) ReadDir(count int) ([]iofs.DirEntry, error) {
|
||||
v, _, err := ReadDirWithContext(context.Background(), f.DirOnlyOps, count)
|
||||
return v, err
|
||||
}
|
||||
|
||||
func (f *componentFsDir) Stat() (iofs.FileInfo, error) {
|
||||
@@ -176,35 +307,37 @@ func (fs *componentFs) applyMeta(fi FileNameIsDir, name string) (FileMetaInfo, b
|
||||
}
|
||||
fim := fi.(FileMetaInfo)
|
||||
meta := fim.Meta()
|
||||
pi := fs.opts.PathParser.Parse(fs.opts.Component, name)
|
||||
pi := fs.opts.Cfg.PathParser().Parse(fs.opts.Component, name)
|
||||
if pi.Disabled() {
|
||||
return fim, false
|
||||
}
|
||||
if meta.Lang != "" {
|
||||
if isLangDisabled := fs.opts.PathParser.IsLangDisabled; isLangDisabled != nil && isLangDisabled(meta.Lang) {
|
||||
return fim, false
|
||||
}
|
||||
}
|
||||
|
||||
meta.PathInfo = pi
|
||||
if !fim.IsDir() {
|
||||
if fileLang := meta.PathInfo.Lang(); fileLang != "" {
|
||||
// A valid lang set in filename.
|
||||
// Give priority to myfile.sv.txt inside the sv filesystem.
|
||||
meta.Weight++
|
||||
meta.Lang = fileLang
|
||||
if idx, ok := fs.opts.Cfg.PathParser().LanguageIndex[fileLang]; ok {
|
||||
// A valid lang set in filename.
|
||||
// Give priority to myfile.sv.txt inside the sv filesystem.
|
||||
meta.Weight++
|
||||
meta.SitesMatrix = meta.SitesMatrix.WithLanguageIndices(idx)
|
||||
if idx > 0 {
|
||||
// Not the default language, add some weight.
|
||||
meta.SitesMatrix = sitesmatrix.NewWeightedVectorStore(meta.SitesMatrix, 10)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
switch meta.Component {
|
||||
case files.ComponentFolderLayouts:
|
||||
// Eg. index.fr.html when French isn't defined,
|
||||
// we want e.g. index.html to be used instead.
|
||||
if len(pi.IdentifiersUnknown()) > 0 {
|
||||
meta.Weight--
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if meta.Lang == "" {
|
||||
meta.Lang = fs.opts.DefaultContentLanguage
|
||||
}
|
||||
|
||||
langIdx, found := fs.opts.PathParser.LanguageIndex[meta.Lang]
|
||||
if !found {
|
||||
panic("no language found for " + meta.Lang)
|
||||
}
|
||||
meta.LangIndex = langIdx
|
||||
|
||||
if fi.IsDir() {
|
||||
meta.OpenFunc = func() (afero.File, error) {
|
||||
return fs.Open(name)
|
||||
@@ -238,10 +371,7 @@ type ComponentFsOptions struct {
|
||||
// The component name, e.g. "content", "layouts" etc.
|
||||
Component string
|
||||
|
||||
DefaultContentLanguage string
|
||||
|
||||
// The parser used to parse paths provided by this filesystem.
|
||||
PathParser *paths.PathParser
|
||||
Cfg config.AllProvider
|
||||
}
|
||||
|
||||
func (fs *componentFs) Open(name string) (afero.File, error) {
|
||||
|
||||
@@ -19,31 +19,9 @@ import (
|
||||
"github.com/bep/overlayfs"
|
||||
)
|
||||
|
||||
// LanguageDirsMerger implements the overlayfs.DirsMerger func, which is used
|
||||
// to merge two directories.
|
||||
var LanguageDirsMerger overlayfs.DirsMerger = func(lofi, bofi []fs.DirEntry) []fs.DirEntry {
|
||||
for _, fi1 := range bofi {
|
||||
fim1 := fi1.(FileMetaInfo)
|
||||
var found bool
|
||||
for _, fi2 := range lofi {
|
||||
fim2 := fi2.(FileMetaInfo)
|
||||
if fi1.Name() == fi2.Name() && fim1.Meta().Lang == fim2.Meta().Lang {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
lofi = append(lofi, fi1)
|
||||
}
|
||||
}
|
||||
|
||||
return lofi
|
||||
}
|
||||
|
||||
// AppendDirsMerger merges two directories keeping all regular files
|
||||
// with the first slice as the base.
|
||||
// Duplicate directories in the second slice will be ignored.
|
||||
// This strategy is used for the i18n and data fs where we need all entries.
|
||||
var AppendDirsMerger overlayfs.DirsMerger = func(lofi, bofi []fs.DirEntry) []fs.DirEntry {
|
||||
for _, fi1 := range bofi {
|
||||
var found bool
|
||||
|
||||
+8
-9
@@ -27,7 +27,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/hugofs/glob"
|
||||
"github.com/gohugoio/hugo/hugofs/hglob"
|
||||
"github.com/gohugoio/hugo/hugolib/sitesmatrix"
|
||||
|
||||
"golang.org/x/text/unicode/norm"
|
||||
|
||||
@@ -58,19 +59,17 @@ type FileMeta struct {
|
||||
IsProject bool
|
||||
Watch bool
|
||||
|
||||
// The lang associated with this file. This may be
|
||||
// either the language set in the filename or
|
||||
// the language defined in the source mount configuration.
|
||||
Lang string
|
||||
// The language index for the above lang. This is the index
|
||||
// in the sorted list of languages/sites.
|
||||
LangIndex int
|
||||
// This file/directory will be built for these sites.
|
||||
SitesMatrix sitesmatrix.VectorStore
|
||||
|
||||
// This file/directory complements these other sites.
|
||||
SitesComplements sitesmatrix.VectorStore
|
||||
|
||||
OpenFunc func() (afero.File, error)
|
||||
JoinStatFunc func(name string) (FileMetaInfo, error)
|
||||
|
||||
// Include only files or directories that match.
|
||||
InclusionFilter *glob.FilenameFilter
|
||||
InclusionFilter *hglob.FilenameFilter
|
||||
|
||||
// Rename the name part of the file (not the directory).
|
||||
// Returns the new name and a boolean indicating if the file
|
||||
|
||||
@@ -20,13 +20,13 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gohugoio/hugo/hugofs/glob"
|
||||
"github.com/gohugoio/hugo/hugofs/hglob"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
var _ FilesystemUnwrapper = (*filenameFilterFs)(nil)
|
||||
|
||||
func newFilenameFilterFs(fs afero.Fs, base string, filter *glob.FilenameFilter) afero.Fs {
|
||||
func newFilenameFilterFs(fs afero.Fs, base string, filter *hglob.FilenameFilter) afero.Fs {
|
||||
return &filenameFilterFs{
|
||||
fs: fs,
|
||||
base: base,
|
||||
@@ -39,7 +39,7 @@ type filenameFilterFs struct {
|
||||
base string
|
||||
fs afero.Fs
|
||||
|
||||
filter *glob.FilenameFilter
|
||||
filter *hglob.FilenameFilter
|
||||
}
|
||||
|
||||
func (fs *filenameFilterFs) UnwrapFilesystem() afero.Fs {
|
||||
@@ -90,7 +90,7 @@ func (fs *filenameFilterFs) Stat(name string) (os.FileInfo, error) {
|
||||
type filenameFilterDir struct {
|
||||
afero.File
|
||||
base string
|
||||
filter *glob.FilenameFilter
|
||||
filter *hglob.FilenameFilter
|
||||
}
|
||||
|
||||
func (f *filenameFilterDir) ReadDir(n int) ([]fs.DirEntry, error) {
|
||||
|
||||
@@ -20,8 +20,7 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/hugofs/glob"
|
||||
|
||||
"github.com/gohugoio/hugo/hugofs/hglob"
|
||||
"github.com/spf13/afero"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
@@ -43,7 +42,7 @@ func TestFilenameFilterFs(t *testing.T) {
|
||||
|
||||
fs = NewBasePathFs(fs, base)
|
||||
|
||||
filter, err := glob.NewFilenameFilter(nil, []string{"/b/**.txt"})
|
||||
filter, err := hglob.NewFilenameFilter(nil, []string{"/b/**.txt"})
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
fs = newFilenameFilterFs(fs, base, filter)
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
package hugofs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
iofs "io/fs"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
@@ -250,3 +252,18 @@ type filesystemsWrapper struct {
|
||||
func (w filesystemsWrapper) UnwrapFilesystem() afero.Fs {
|
||||
return w.content
|
||||
}
|
||||
|
||||
type ReadDirWithContextDir interface {
|
||||
ReadDirWithContext(context context.Context, count int) ([]iofs.DirEntry, context.Context, error)
|
||||
}
|
||||
|
||||
func ReadDirWithContext(ctx context.Context, f DirOnlyOps, count int) ([]iofs.DirEntry, context.Context, error) {
|
||||
if ff, ok := f.(ReadDirWithContextDir); ok {
|
||||
return ff.ReadDirWithContext(ctx, count)
|
||||
}
|
||||
v, err := f.(iofs.ReadDirFile).ReadDir(count)
|
||||
if err != nil {
|
||||
return nil, ctx, err
|
||||
}
|
||||
return v, ctx, nil
|
||||
}
|
||||
|
||||
+7
-7
@@ -14,29 +14,29 @@
|
||||
package hugofs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/hugofs/glob"
|
||||
|
||||
"github.com/gohugoio/hugo/hugofs/hglob"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
// Glob walks the fs and passes all matches to the handle func.
|
||||
// The handle func can return true to signal a stop.
|
||||
func Glob(fs afero.Fs, pattern string, handle func(fi FileMetaInfo) (bool, error)) error {
|
||||
pattern = glob.NormalizePathNoLower(pattern)
|
||||
pattern = hglob.NormalizePathNoLower(pattern)
|
||||
if pattern == "" {
|
||||
return nil
|
||||
}
|
||||
root := glob.ResolveRootDir(pattern)
|
||||
root := hglob.ResolveRootDir(pattern)
|
||||
if !strings.HasPrefix(root, "/") {
|
||||
root = "/" + root
|
||||
}
|
||||
pattern = strings.ToLower(pattern)
|
||||
|
||||
g, err := glob.GetGlob(pattern)
|
||||
g, err := hglob.GetGlob(pattern)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -47,8 +47,8 @@ func Glob(fs afero.Fs, pattern string, handle func(fi FileMetaInfo) (bool, error
|
||||
// Signals that we're done.
|
||||
done := errors.New("done")
|
||||
|
||||
wfn := func(p string, info FileMetaInfo) error {
|
||||
p = glob.NormalizePath(p)
|
||||
wfn := func(ctx context.Context, p string, info FileMetaInfo) error {
|
||||
p = hglob.NormalizePath(p)
|
||||
if info.IsDir() {
|
||||
if !hasSuperAsterisk {
|
||||
// Avoid walking to the bottom if we can avoid it.
|
||||
|
||||
@@ -59,9 +59,7 @@ func BenchmarkHashingFs(b *testing.B) {
|
||||
ofs := NewHashingFs(fs, observer)
|
||||
content := []byte(strings.Repeat("lorem ipsum ", 1000))
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
for i := 0; b.Loop(); i++ {
|
||||
f, err := ofs.Create(fmt.Sprintf("file%d", i))
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
|
||||
@@ -11,11 +11,12 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package glob
|
||||
package hglob
|
||||
|
||||
import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/gobwas/glob"
|
||||
@@ -23,14 +24,30 @@ import (
|
||||
|
||||
type FilenameFilter struct {
|
||||
shouldInclude func(filename string) bool
|
||||
inclusions []glob.Glob
|
||||
dirInclusions []glob.Glob
|
||||
exclusions []glob.Glob
|
||||
isWindows bool
|
||||
|
||||
entries []globFilenameFilterEntry
|
||||
|
||||
isWindows bool
|
||||
|
||||
nested []*FilenameFilter
|
||||
}
|
||||
|
||||
type globFilenameFilterEntryType int
|
||||
|
||||
const (
|
||||
globFilenameFilterEntryTypeInclusion globFilenameFilterEntryType = iota
|
||||
globFilenameFilterEntryTypeInclusionDir
|
||||
globFilenameFilterEntryTypeExclusion
|
||||
)
|
||||
|
||||
// NegationPrefix is the prefix that makes a pattern an exclusion.
|
||||
const NegationPrefix = "! "
|
||||
|
||||
type globFilenameFilterEntry struct {
|
||||
g glob.Glob
|
||||
t globFilenameFilterEntryType
|
||||
}
|
||||
|
||||
func normalizeFilenameGlobPattern(s string) string {
|
||||
// Use Unix separators even on Windows.
|
||||
s = filepath.ToSlash(s)
|
||||
@@ -40,51 +57,62 @@ func normalizeFilenameGlobPattern(s string) string {
|
||||
return s
|
||||
}
|
||||
|
||||
// NewFilenameFilter creates a new Glob where the Match method will
|
||||
// return true if the file should be included.
|
||||
// Note that the inclusions will be checked first.
|
||||
func NewFilenameFilter(inclusions, exclusions []string) (*FilenameFilter, error) {
|
||||
if inclusions == nil && exclusions == nil {
|
||||
func NewFilenameFilterV2(patterns []string) (*FilenameFilter, error) {
|
||||
if len(patterns) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
filter := &FilenameFilter{isWindows: isWindows}
|
||||
|
||||
for _, include := range inclusions {
|
||||
include = normalizeFilenameGlobPattern(include)
|
||||
g, err := GetGlob(include)
|
||||
for _, p := range patterns {
|
||||
var t globFilenameFilterEntryType
|
||||
if strings.HasPrefix(p, NegationPrefix) {
|
||||
t = globFilenameFilterEntryTypeExclusion
|
||||
p = strings.TrimPrefix(p, NegationPrefix)
|
||||
} else {
|
||||
t = globFilenameFilterEntryTypeInclusion
|
||||
}
|
||||
p = normalizeFilenameGlobPattern(p)
|
||||
g, err := GetGlob(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
filter.inclusions = append(filter.inclusions, g)
|
||||
|
||||
// For mounts that do directory walking (e.g. content) we
|
||||
// must make sure that all directories up to this inclusion also
|
||||
// gets included.
|
||||
dir := path.Dir(include)
|
||||
parts := strings.Split(dir, "/")
|
||||
for i := range parts {
|
||||
pattern := "/" + filepath.Join(parts[:i+1]...)
|
||||
g, err := GetGlob(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
filter.entries = append(filter.entries, globFilenameFilterEntry{t: t, g: g})
|
||||
if t == globFilenameFilterEntryTypeInclusion {
|
||||
// For mounts that do directory walking (e.g. content) we
|
||||
// must make sure that all directories up to this inclusion also
|
||||
// gets included.
|
||||
dir := path.Dir(p)
|
||||
parts := strings.Split(dir, "/")
|
||||
for i := range parts {
|
||||
pattern := "/" + filepath.Join(parts[:i+1]...)
|
||||
g, err := GetGlob(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
filter.entries = append(filter.entries, globFilenameFilterEntry{t: globFilenameFilterEntryTypeInclusionDir, g: g})
|
||||
}
|
||||
filter.dirInclusions = append(filter.dirInclusions, g)
|
||||
}
|
||||
}
|
||||
|
||||
for _, exclude := range exclusions {
|
||||
exclude = normalizeFilenameGlobPattern(exclude)
|
||||
g, err := GetGlob(exclude)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
filter.exclusions = append(filter.exclusions, g)
|
||||
}
|
||||
|
||||
return filter, nil
|
||||
}
|
||||
|
||||
// NewFilenameFilter creates a new Glob where the Match method will
|
||||
// return true if the file should be included.
|
||||
// Note that the exclusions will be checked first.
|
||||
// Deprecated: Use NewFilenameFilterV2.
|
||||
func NewFilenameFilter(inclusions, exclusions []string) (*FilenameFilter, error) {
|
||||
for i, p := range exclusions {
|
||||
if !strings.HasPrefix(p, NegationPrefix) {
|
||||
exclusions[i] = NegationPrefix + p
|
||||
}
|
||||
}
|
||||
all := slices.Concat(inclusions, exclusions)
|
||||
return NewFilenameFilterV2(all)
|
||||
}
|
||||
|
||||
// MustNewFilenameFilter invokes NewFilenameFilter and panics on error.
|
||||
// Deprecated: Use NewFilenameFilterV2.
|
||||
func MustNewFilenameFilter(inclusions, exclusions []string) *FilenameFilter {
|
||||
filter, err := NewFilenameFilter(inclusions, exclusions)
|
||||
if err != nil {
|
||||
@@ -155,28 +183,26 @@ func (f *FilenameFilter) doMatch(filename string, isDir bool) bool {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for _, inclusion := range f.inclusions {
|
||||
if inclusion.Match(filename) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if isDir && f.inclusions != nil {
|
||||
for _, inclusion := range f.dirInclusions {
|
||||
if inclusion.Match(filename) {
|
||||
var hasInclude bool
|
||||
for _, entry := range f.entries {
|
||||
switch entry.t {
|
||||
case globFilenameFilterEntryTypeExclusion:
|
||||
if entry.g.Match(filename) {
|
||||
return false
|
||||
}
|
||||
case globFilenameFilterEntryTypeInclusion:
|
||||
if entry.g.Match(filename) {
|
||||
return true
|
||||
}
|
||||
hasInclude = true
|
||||
case globFilenameFilterEntryTypeInclusionDir:
|
||||
if isDir && entry.g.Match(filename) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, exclusion := range f.exclusions {
|
||||
if exclusion.Match(filename) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return f.inclusions == nil && f.shouldInclude == nil
|
||||
return !hasInclude && f.shouldInclude == nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2021 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 glob
|
||||
package hglob
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user