Compare commits

...

12 Commits

Author SHA1 Message Date
Bjørn Erik Pedersen 10ddc2ef92 Improve error messages for PostCSS etc.
Fixes #9730
2023-07-17 19:42:13 +02:00
Bjørn Erik Pedersen c406fd3a0e Fix setting config from env with complex (e.g. YAML) strings
So you can do

```
HUGO_OUTPUTS="home: [rss]"  hugo
```

And similar.

Fixes #11249
2023-07-16 18:08:23 +02:00
David Karlsson 286821e360 Fix for data mounts in sub folders
Before this change, data files from Hugo modules were always mounted at the
root of the `data` directory. The File and FileMetaInfo structs for modules
are different from 'native' data directories.

This changes how the keyParts for data files are generated so that data
from modules or native directories are treated the same.
2023-07-15 11:13:08 +02:00
hugoreleaser 79f15be5b0 releaser: Prepare repository for 0.116.0-DEV
[ci skip]
2023-07-13 16:23:50 +00:00
hugoreleaser 5c2e014a51 releaser: Bump versions for release of 0.115.3
[ci skip]
2023-07-13 16:11:34 +00:00
Bjørn Erik Pedersen cc44583cc3 Improve behavior of defaultContentLanguageInSubdir when only the default language is enabled
1 .Create sitemapindex in root of publishDir (will contain link to sitemap.xml in defaultContentLanguage site)
2. Create index.html in root of publishDir (will redirect to defaultContentLanguage site)

Fixes #11229
2023-07-13 18:06:36 +02:00
Bjørn Erik Pedersen 4da672af88 Return error when .Render is invoked without arg
Fixes #11243
2023-07-13 18:06:36 +02:00
Joe Mooring f1886f8c37 js: Pass tsconfig.json to esBuild
Note that esBuild only inspects/honors certain fields.
See https://esbuild.github.io/content-types/#tsconfig-json.

Fixes #11232
2023-07-13 15:19:04 +02:00
Bjørn Erik Pedersen 5bec50838c tpl/collections: Fix WordCount (etc.) regression in Where, Sort, Delimit
Fixes #11234
2023-07-11 12:11:39 +02:00
Joe Mooring f650e4d751 config/allconfig: Update timeout description 2023-07-08 23:22:26 +02:00
Joe Mooring c934a45069 docs: Refresh docs.json 2023-07-08 23:20:11 +02:00
hugoreleaser 91b02091a5 releaser: Prepare repository for 0.116.0-DEV
[ci skip]
2023-07-08 17:19:03 +00:00
30 changed files with 396 additions and 63 deletions
+1 -1
View File
@@ -348,7 +348,7 @@ description = ""
homepage = "http://example.com/"
tags = []
features = []
min_version = "0.115.2"
min_version = "0.115.0"
[author]
name = ""
+24 -1
View File
@@ -59,11 +59,34 @@ func GetGID() uint64 {
return n
}
// IsFeatureNotAvailableError returns true if the given error is or contains a FeatureNotAvailableError.
func IsFeatureNotAvailableError(err error) bool {
return errors.Is(err, &FeatureNotAvailableError{})
}
// ErrFeatureNotAvailable denotes that a feature is unavailable.
//
// We will, at least to begin with, make some Hugo features (SCSS with libsass) optional,
// and this error is used to signal those situations.
var ErrFeatureNotAvailable = errors.New("this feature is not available in your current Hugo version, see https://goo.gl/YMrWcn for more information")
var ErrFeatureNotAvailable = &FeatureNotAvailableError{Cause: errors.New("this feature is not available in your current Hugo version, see https://goo.gl/YMrWcn for more information")}
// FeatureNotAvailableError is an error type used to signal that a feature is not available.
type FeatureNotAvailableError struct {
Cause error
}
func (e *FeatureNotAvailableError) Unwrap() error {
return e.Cause
}
func (e *FeatureNotAvailableError) Error() string {
return e.Cause.Error()
}
func (e *FeatureNotAvailableError) Is(target error) bool {
_, ok := target.(*FeatureNotAvailableError)
return ok
}
// Must panics if err != nil.
func Must(err error) {
+10
View File
@@ -14,6 +14,7 @@
package herrors
import (
"errors"
"fmt"
"testing"
@@ -34,3 +35,12 @@ func TestIsNotExist(t *testing.T) {
// os.IsNotExist returns false for wrapped errors.
c.Assert(IsNotExist(fmt.Errorf("foo: %w", afero.ErrFileNotFound)), qt.Equals, true)
}
func TestIsFeatureNotAvailableError(t *testing.T) {
c := qt.New(t)
c.Assert(IsFeatureNotAvailableError(ErrFeatureNotAvailable), qt.Equals, true)
c.Assert(IsFeatureNotAvailableError(&FeatureNotAvailableError{}), qt.Equals, true)
c.Assert(IsFeatureNotAvailableError(errors.New("asdf")), qt.Equals, false)
}
+3 -3
View File
@@ -17,7 +17,7 @@ package hugo
// This should be the only one.
var CurrentVersion = Version{
Major: 0,
Minor: 115,
PatchLevel: 2,
Suffix: "",
Minor: 116,
PatchLevel: 0,
Suffix: "-DEV",
}
+1 -1
View File
@@ -553,7 +553,7 @@ type RootConfig struct {
// See Modules for more a more flexible way to load themes.
Theme []string
// Timeout for generating page contents, specified as a duration or in milliseconds.
// Timeout for generating page contents, specified as a duration or in seconds.
Timeout string
// The time zone (or location), e.g. Europe/Oslo, used to parse front matter dates without such information and in the time function.
+1 -1
View File
@@ -150,7 +150,7 @@ var allDecoderSetups = map[string]decodeWeight{
key: "outputs",
decode: func(d decodeWeight, p decodeConfig) error {
defaults := createDefaultOutputFormats(p.c.OutputFormats.Config)
m := p.p.GetStringMap("outputs")
m := maps.CleanConfigStringMap(p.p.GetStringMap("outputs"))
p.c.Outputs = make(map[string][]string)
for k, v := range m {
s := types.ToStringSlicePreserveString(v)
+12 -4
View File
@@ -293,11 +293,19 @@ func (l configLoader) applyOsEnvOverrides(environ []string) error {
} else {
l.cfg.Set(env.Key, val)
}
} else if nestedKey != "" {
owner[nestedKey] = env.Value
} else {
// The container does not exist yet.
l.cfg.Set(strings.ReplaceAll(env.Key, delim, "."), env.Value)
if nestedKey != "" {
owner[nestedKey] = env.Value
} else {
var val any = env.Value
if _, ok := allDecoderSetups[env.Key]; ok {
// A map.
val, err = metadecoders.Default.UnmarshalStringTo(env.Value, map[string]interface{}{})
}
if err == nil {
l.cfg.Set(strings.ReplaceAll(env.Key, delim, "."), val)
}
}
}
}
+1 -1
View File
@@ -148,7 +148,7 @@ func (b BuildConfig) UseResourceCache(err error) bool {
}
if b.UseResourceCacheWhen == "fallback" {
return err == herrors.ErrFeatureNotAvailable
return herrors.IsFeatureNotAvailableError(err)
}
return true
+1 -1
View File
@@ -1792,7 +1792,7 @@
"^postcss$"
],
"osEnv": [
"(?i)^((HTTPS?|NO)_PROXY|PATH(EXT)?|APPDATA|TE?MP|TERM|GO\\w+)$"
"(?i)^((HTTPS?|NO)_PROXY|PATH(EXT)?|APPDATA|TE?MP|TERM|GO\\w+|(XDG_CONFIG_)?HOME|USERPROFILE|SSH_AUTH_SOCK|DISPLAY|LANG)$"
]
},
"funcs": {
+123
View File
@@ -1321,6 +1321,9 @@ Home.
b.AssertFileContent("public/en/index.html", "Home.")
b.AssertFileContent("public/en/foo/bar.txt", "Foo.")
b.AssertFileContent("public/index.html", "refresh")
b.AssertFileContent("public/sitemap.xml", "sitemapindex")
b.AssertFileContent("public/en/sitemap.xml", "urlset")
})
t.Run("Two languages, default in sub dir", func(t *testing.T) {
@@ -1351,6 +1354,9 @@ Home.
b.AssertFileContent("public/en/index.html", "Home.")
b.AssertFileContent("public/en/foo/bar.txt", "Foo.")
b.AssertFileContent("public/index.html", "refresh")
b.AssertFileContent("public/sitemap.xml", "sitemapindex")
b.AssertFileContent("public/en/sitemap.xml", "urlset")
})
t.Run("Two languages, default in root", func(t *testing.T) {
@@ -1381,6 +1387,8 @@ Home.
b.AssertFileContent("public/index.html", "Home.")
b.AssertFileContent("public/foo/bar.txt", "Foo.")
b.AssertFileContent("public/sitemap.xml", "sitemapindex")
b.AssertFileContent("public/en/sitemap.xml", "urlset")
})
}
@@ -1411,3 +1419,118 @@ Home.
b.Assert(len(b.H.Sites), qt.Equals, 1)
}
func TestLoadConfigYamlEnvVar(t *testing.T) {
defaultEnv := []string{`HUGO_OUTPUTS=home: ['json']`}
runVariant := func(t testing.TB, files string, env []string) *IntegrationTestBuilder {
if env == nil {
env = defaultEnv
}
b := NewIntegrationTestBuilder(
IntegrationTestConfig{
T: t,
TxtarString: files,
Environ: env,
BuildCfg: BuildCfg{SkipRender: true},
},
).Build()
outputs := b.H.Configs.Base.Outputs
if env == nil {
home := outputs["home"]
b.Assert(home, qt.Not(qt.IsNil))
b.Assert(home, qt.DeepEquals, []string{"json"})
}
return b
}
t.Run("with empty slice", func(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
baseURL = "https://example.com"
disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "page", "section"]
[outputs]
home = ["html"]
`
b := runVariant(t, files, []string{`HUGO_OUTPUTS=section: []`})
outputs := b.H.Configs.Base.Outputs
b.Assert(outputs, qt.DeepEquals, map[string][]string{
"home": {"html"},
"page": {"html"},
"rss": {"rss"},
"section": nil,
"taxonomy": {"html", "rss"},
"term": {"html", "rss"},
})
})
t.Run("with existing outputs", func(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
baseURL = "https://example.com"
disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "page", "section"]
[outputs]
home = ["html"]
`
runVariant(t, files, nil)
})
{
t.Run("with existing outputs direct", func(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
baseURL = "https://example.com"
disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "page", "section"]
[outputs]
home = ["html"]
`
runVariant(t, files, []string{"HUGO_OUTPUTS_HOME=json"})
})
}
t.Run("without existing outputs", func(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
baseURL = "https://example.com"
disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "page", "section"]
`
runVariant(t, files, nil)
})
t.Run("without existing outputs direct", func(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
baseURL = "https://example.com"
disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "page", "section"]
`
runVariant(t, files, []string{"HUGO_OUTPUTS_HOME=json"})
})
}
+29
View File
@@ -1178,3 +1178,32 @@ target = "content/resources-b"
b.AssertFileContent("public/resources-a/subdir/about/index.html", "Single")
b.AssertFileContent("public/resources-b/subdir/about/index.html", "Single")
}
func TestMountData(t *testing.T) {
files := `
-- hugo.toml --
baseURL = 'https://example.org/'
disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT", "page", "section"]
[[module.mounts]]
source = "data"
target = "data"
[[module.mounts]]
source = "extra-data"
target = "data/extra"
-- extra-data/test.yaml --
message: Hugo Rocks
-- layouts/index.html --
{{ site.Data.extra.test.message }}
`
b := NewIntegrationTestBuilder(
IntegrationTestConfig{
T: t,
TxtarString: files,
},
).Build()
b.AssertFileContent("public/index.html", "Hugo Rocks")
}
+6 -4
View File
@@ -437,7 +437,7 @@ func (cfg *BuildCfg) shouldRender(p *pageState) bool {
}
func (h *HugoSites) renderCrossSitesSitemap() error {
if !h.isMultiLingual() || h.Conf.IsMultihost() {
if h.Conf.IsMultihost() || !(h.Conf.DefaultContentLanguageInSubdir() || h.Conf.IsMultiLingual()) {
return nil
}
@@ -555,13 +555,14 @@ func (h *HugoSites) loadData(fis []hugofs.FileMetaInfo) (err error) {
h.data = make(map[string]any)
for _, fi := range fis {
basePath := fi.Meta().Path
fileSystem := spec.NewFilesystemFromFileMetaInfo(fi)
files, err := fileSystem.Files()
if err != nil {
return err
}
for _, r := range files {
if err := h.handleDataFile(r); err != nil {
if err := h.handleDataFile(basePath, r); err != nil {
return err
}
}
@@ -570,7 +571,7 @@ func (h *HugoSites) loadData(fis []hugofs.FileMetaInfo) (err error) {
return
}
func (h *HugoSites) handleDataFile(r source.File) error {
func (h *HugoSites) handleDataFile(basePath string, r source.File) error {
var current map[string]any
f, err := r.FileInfo().Meta().Open()
@@ -581,7 +582,8 @@ func (h *HugoSites) handleDataFile(r source.File) error {
// Crawl in data tree to insert data
current = h.data
keyParts := strings.Split(r.Dir(), helpers.FilePathSeparator)
dataPath := filepath.Join(basePath, r.Dir())
keyParts := strings.Split(dataPath, helpers.FilePathSeparator)
for _, key := range keyParts {
if key != "" {
+3
View File
@@ -557,6 +557,9 @@ func (p *pageContentOutput) RenderWithTemplateInfo(ctx context.Context, info tpl
}
func (p *pageContentOutput) Render(ctx context.Context, layout ...string) (template.HTML, error) {
if len(layout) == 0 {
return "", errors.New("no layout given")
}
templ, found, err := p.p.resolveTemplate(layout...)
if err != nil {
return "", p.p.wrapError(err)
+22
View File
@@ -1983,3 +1983,25 @@ title: "p2"
b.Assert(identity.HashString(p1), qt.Not(qt.Equals), identity.HashString(p2))
b.Assert(identity.HashString(sites[0]), qt.Not(qt.Equals), identity.HashString(sites[1]))
}
// Issue #11243
func TestRenderWithoutArgument(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
-- layouts/index.html --
{{ .Render }}
`
b, err := NewIntegrationTestBuilder(
IntegrationTestConfig{
T: t,
TxtarString: files,
Running: true,
},
).BuildE()
b.Assert(err, qt.IsNotNil)
}
+1 -1
View File
@@ -384,7 +384,7 @@ func (s *Site) renderAliases() error {
// renderMainLanguageRedirect creates a redirect to the main language home,
// depending on if it lives in sub folder (e.g. /en) or not.
func (s *Site) renderMainLanguageRedirect() error {
if !s.h.isMultiLingual() || s.h.Conf.IsMultihost() {
if s.h.Conf.IsMultihost() || !(s.h.Conf.DefaultContentLanguageInSubdir() || s.h.Conf.IsMultiLingual()) {
// No need for a redirect
return nil
}
+4 -2
View File
@@ -1,7 +1,9 @@
# Release env.
# These will be replaced by script before release.
HUGORELEASER_TAG=v0.115.1
HUGORELEASER_COMMITISH=857374e69358f788bd31ddc55255c5c8e3dcfd80
HUGORELEASER_TAG=v0.115.3
HUGORELEASER_COMMITISH=5c2e014a5150553a9fa4f9c1eb7dc4db89c0f1ab
+2 -1
View File
@@ -23,6 +23,7 @@ import (
"strings"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/maps"
"github.com/niklasfasching/go-org/org"
xml "github.com/clbanning/mxj/v2"
@@ -90,7 +91,7 @@ func (d Decoder) UnmarshalStringTo(data string, typ any) (any, error) {
switch typ.(type) {
case string:
return data, nil
case map[string]any:
case map[string]any, maps.Params:
format := d.FormatFromContentString(data)
return d.UnmarshalToMap([]byte(data), format)
case []any:
@@ -181,7 +181,7 @@ func (t *babelTransformation) Transform(ctx *resources.ResourceTransformationCtx
if err != nil {
if hexec.IsNotFound(err) {
// This may be on a CI server etc. Will fall back to pre-built assets.
return herrors.ErrFeatureNotAvailable
return &herrors.FeatureNotAvailableError{Cause: err}
}
return err
}
@@ -200,7 +200,7 @@ func (t *babelTransformation) Transform(ctx *resources.ResourceTransformationCtx
err = cmd.Run()
if err != nil {
if hexec.IsNotFound(err) {
return herrors.ErrFeatureNotAvailable
return &herrors.FeatureNotAvailableError{Cause: err}
}
return fmt.Errorf(errBuf.String()+": %w", err)
}
@@ -86,6 +86,7 @@ func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx
opts.resolveDir = t.c.rs.Cfg.BaseConfig().WorkingDir // where node_modules gets resolved
opts.contents = string(src)
opts.mediaType = ctx.InMediaType
opts.tsConfig = t.c.rs.ResolveJSConfigFile("tsconfig.json")
buildOptions, err := toBuildOptions(opts)
if err != nil {
@@ -48,7 +48,7 @@ export function hello3() {
-- layouts/index.html --
{{ $js := resources.Get "js/main.js" | js.Build }}
JS Content:{{ $js.Content }}:End:
`
c.Run("Basic", func(c *qt.C) {
@@ -90,9 +90,9 @@ disableKinds=["page", "section", "taxonomy", "term", "sitemap", "robotsTXT"]
path="github.com/gohugoio/hugoTestProjectJSModImports"
-- go.mod --
module github.com/gohugoio/tests/testHugoModules
go 1.16
require github.com/gohugoio/hugoTestProjectJSModImports v0.10.0 // indirect
-- package.json --
{
@@ -100,7 +100,7 @@ require github.com/gohugoio/hugoTestProjectJSModImports v0.10.0 // indirect
"date-fns": "^2.16.1"
}
}
`
b := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{
@@ -142,7 +142,7 @@ console.log("included");
-- assets/js/main.js --
import "./included";
import { toCamelCase } from "to-camel-case";
console.log("main");
console.log("To camel:", toCamelCase("space case"));
-- assets/js/myjsx.jsx --
@@ -222,7 +222,7 @@ import { hello1, hello2 } from './util1';
hello1();
hello2();
-- assets/js/util1.js --
/* Some
/* Some
comments.
*/
import { hello3 } from './util2';
@@ -239,7 +239,7 @@ export function hello3() {
-- layouts/index.html --
{{ $js := resources.Get "js/main.js" | js.Build }}
JS Content:{{ $js.Content }}:End:
`
c.Run("Import from main not found", func(c *qt.C) {
@@ -292,9 +292,9 @@ import 'imp3/foo.js';
}).Build()
expected := `
IMPORT_SRC_DIR:imp1/index.js
IMPORT_SRC_DIR:imp1/index.js
IMPORT_SRC_DIR:imp2/index.ts
IMPORT_SRC_DIR:imp3/foo.ts
IMPORT_SRC_DIR:imp3/foo.ts
`
expected = strings.ReplaceAll(expected, "IMPORT_SRC_DIR", importSrcDir)
@@ -340,7 +340,36 @@ console.log("Hello 2");
License util1
License util2
Main license
`)
}
// Issue #11232
func TestTypeScriptExperimentalDecorators(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ['RSS','sitemap','taxonomy','term']
-- tsconfig.json --
{
"compilerOptions": {
"experimentalDecorators": true,
}
}
-- assets/ts/main.ts --
function addFoo(target: any) {target.prototype.foo = 'bar'}
@addFoo
class A {}
-- layouts/index.html --
{{ $opts := dict "target" "es2020" "targetPath" "js/main.js" }}
{{ (resources.Get "ts/main.ts" | js.Build $opts).Publish }}
`
b := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{
T: t,
NeedsOsFS: true,
TxtarString: files,
}).Build()
b.AssertFileContent("public/js/main.js", "__decorateClass")
}
@@ -168,6 +168,25 @@ func TestTransformPostCSSError(t *testing.T) {
}
func TestTransformPostCSSNotInstalledError(t *testing.T) {
if !htesting.IsCI() {
t.Skip("Skip long running test when running locally")
}
c := qt.New(t)
s, err := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{
T: c,
NeedsOsFS: true,
TxtarString: postCSSIntegrationTestFiles,
}).BuildE()
s.AssertIsFileError(err)
c.Assert(err.Error(), qt.Contains, `binary with name "npx" not found`)
}
// #9895
func TestTransformPostCSSImportError(t *testing.T) {
if !htesting.IsCI() {
@@ -205,7 +205,7 @@ func (t *postcssTransformation) Transform(ctx *resources.ResourceTransformationC
if err != nil {
if hexec.IsNotFound(err) {
// This may be on a CI server etc. Will fall back to pre-built assets.
return herrors.ErrFeatureNotAvailable
return &herrors.FeatureNotAvailableError{Cause: err}
}
return err
}
@@ -240,7 +240,9 @@ func (t *postcssTransformation) Transform(ctx *resources.ResourceTransformationC
err = cmd.Run()
if err != nil {
if hexec.IsNotFound(err) {
return herrors.ErrFeatureNotAvailable
return &herrors.FeatureNotAvailableError{
Cause: err,
}
}
return imp.toFileError(errBuf.String())
}
+1 -1
View File
@@ -449,7 +449,7 @@ func (r *resourceAdapter) transform(publish, setContent bool) error {
newErr := func(err error) error {
msg := fmt.Sprintf("%s: failed to transform %q (%s)", strings.ToUpper(tr.Key().Name), tctx.InPath, tctx.InMediaType.Type)
if err == herrors.ErrFeatureNotAvailable {
if herrors.IsFeatureNotAvailableError(err) {
var errMsg string
if tr.Key().Name == "postcss" {
// This transformation is not available in this
+3 -2
View File
@@ -16,6 +16,7 @@
package collections
import (
"context"
"fmt"
"html/template"
"math/rand"
@@ -99,7 +100,7 @@ func (ns *Namespace) After(n any, l any) (any, error) {
// Delimit takes a given list l and returns a string delimited by sep.
// If last is passed to the function, it will be used as the final delimiter.
func (ns *Namespace) Delimit(l, sep any, last ...any) (template.HTML, error) {
func (ns *Namespace) Delimit(ctx context.Context, l, sep any, last ...any) (template.HTML, error) {
d, err := cast.ToStringE(sep)
if err != nil {
return "", err
@@ -125,7 +126,7 @@ func (ns *Namespace) Delimit(l, sep any, last ...any) (template.HTML, error) {
var str string
switch lv.Kind() {
case reflect.Map:
sortSeq, err := ns.Sort(l)
sortSeq, err := ns.Sort(ctx, l)
if err != nil {
return "", err
}
+3 -2
View File
@@ -14,6 +14,7 @@
package collections
import (
"context"
"errors"
"fmt"
"html/template"
@@ -166,9 +167,9 @@ func TestDelimit(t *testing.T) {
var err error
if test.last == nil {
result, err = ns.Delimit(test.seq, test.delimiter)
result, err = ns.Delimit(context.Background(), test.seq, test.delimiter)
} else {
result, err = ns.Delimit(test.seq, test.delimiter, test.last)
result, err = ns.Delimit(context.Background(), test.seq, test.delimiter, test.last)
}
c.Assert(err, qt.IsNil, errMsg)
+41
View File
@@ -155,3 +155,44 @@ func TestAppendNilsToSliceWithNils(t *testing.T) {
}
}
// Issue 11234.
func TestWhereWithWordCount(t *testing.T) {
t.Parallel()
files := `
-- config.toml --
baseURL = 'http://example.com/'
-- layouts/index.html --
Home: {{ range where site.RegularPages "WordCount" "gt" 50 }}{{ .Title }}|{{ end }}
-- layouts/shortcodes/lorem.html --
{{ "ipsum " | strings.Repeat (.Get 0 | int) }}
-- content/p1.md --
---
title: "p1"
---
{{< lorem 100 >}}
-- content/p2.md --
---
title: "p2"
---
{{< lorem 20 >}}
-- content/p3.md --
---
title: "p3"
---
{{< lorem 60 >}}
`
b := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{
T: t,
TxtarString: files,
},
).Build()
b.AssertFileContent("public/index.html", `
Home: p1|p3|
`)
}
+6 -3
View File
@@ -14,6 +14,7 @@
package collections
import (
"context"
"errors"
"reflect"
"sort"
@@ -26,7 +27,7 @@ import (
)
// Sort returns a sorted copy of the list l.
func (ns *Namespace) Sort(l any, args ...any) (any, error) {
func (ns *Namespace) Sort(ctx context.Context, l any, args ...any) (any, error) {
if l == nil {
return nil, errors.New("sequence must be provided")
}
@@ -36,6 +37,8 @@ func (ns *Namespace) Sort(l any, args ...any) (any, error) {
return nil, errors.New("can't iterate over a nil value")
}
ctxv := reflect.ValueOf(ctx)
var sliceType reflect.Type
switch seqv.Kind() {
case reflect.Array, reflect.Slice:
@@ -78,7 +81,7 @@ func (ns *Namespace) Sort(l any, args ...any) (any, error) {
v := p.Pairs[i].Value
var err error
for i, elemName := range path {
v, err = evaluateSubElem(v, elemName)
v, err = evaluateSubElem(ctxv, v, elemName)
if err != nil {
return nil, err
}
@@ -108,7 +111,7 @@ func (ns *Namespace) Sort(l any, args ...any) (any, error) {
v := p.Pairs[i].Value
var err error
for i, elemName := range path {
v, err = evaluateSubElem(v, elemName)
v, err = evaluateSubElem(ctxv, v, elemName)
if err != nil {
return nil, err
}
+3 -2
View File
@@ -14,6 +14,7 @@
package collections
import (
"context"
"fmt"
"reflect"
"testing"
@@ -240,9 +241,9 @@ func TestSort(t *testing.T) {
var result any
var err error
if test.sortByField == nil {
result, err = ns.Sort(test.seq)
result, err = ns.Sort(context.Background(), test.seq)
} else {
result, err = ns.Sort(test.seq, test.sortByField, test.sortAsc)
result, err = ns.Sort(context.Background(), test.seq, test.sortByField, test.sortAsc)
}
if b, ok := test.expect.(bool); ok && !b {
+23 -12
View File
@@ -14,6 +14,7 @@
package collections
import (
"context"
"errors"
"fmt"
"reflect"
@@ -24,7 +25,7 @@ import (
)
// Where returns a filtered subset of collection c.
func (ns *Namespace) Where(c, key any, args ...any) (any, error) {
func (ns *Namespace) Where(ctx context.Context, c, key any, args ...any) (any, error) {
seqv, isNil := indirect(reflect.ValueOf(c))
if isNil {
return nil, errors.New("can't iterate over a nil value of type " + reflect.ValueOf(c).Type().String())
@@ -35,6 +36,8 @@ func (ns *Namespace) Where(c, key any, args ...any) (any, error) {
return nil, err
}
ctxv := reflect.ValueOf(ctx)
var path []string
kv := reflect.ValueOf(key)
if kv.Kind() == reflect.String {
@@ -43,9 +46,9 @@ func (ns *Namespace) Where(c, key any, args ...any) (any, error) {
switch seqv.Kind() {
case reflect.Array, reflect.Slice:
return ns.checkWhereArray(seqv, kv, mv, path, op)
return ns.checkWhereArray(ctxv, seqv, kv, mv, path, op)
case reflect.Map:
return ns.checkWhereMap(seqv, kv, mv, path, op)
return ns.checkWhereMap(ctxv, seqv, kv, mv, path, op)
default:
return nil, fmt.Errorf("can't iterate over %v", c)
}
@@ -275,7 +278,7 @@ func (ns *Namespace) checkCondition(v, mv reflect.Value, op string) (bool, error
return false, nil
}
func evaluateSubElem(obj reflect.Value, elemName string) (reflect.Value, error) {
func evaluateSubElem(ctx, obj reflect.Value, elemName string) (reflect.Value, error) {
if !obj.IsValid() {
return zero, errors.New("can't evaluate an invalid value")
}
@@ -301,12 +304,20 @@ func evaluateSubElem(obj reflect.Value, elemName string) (reflect.Value, error)
index := hreflect.GetMethodIndexByName(objPtr.Type(), elemName)
if index != -1 {
var args []reflect.Value
mt := objPtr.Type().Method(index)
num := mt.Type.NumIn()
maxNumIn := 1
if num > 1 && mt.Type.In(1).Implements(hreflect.ContextInterface) {
args = []reflect.Value{ctx}
maxNumIn = 2
}
switch {
case mt.PkgPath != "":
return zero, fmt.Errorf("%s is an unexported method of type %s", elemName, typ)
case mt.Type.NumIn() > 1:
return zero, fmt.Errorf("%s is a method of type %s but requires more than 1 parameter", elemName, typ)
case mt.Type.NumIn() > maxNumIn:
return zero, fmt.Errorf("%s is a method of type %s but requires more than %d parameter", elemName, typ, maxNumIn)
case mt.Type.NumOut() == 0:
return zero, fmt.Errorf("%s is a method of type %s but returns no output", elemName, typ)
case mt.Type.NumOut() > 2:
@@ -316,7 +327,7 @@ func evaluateSubElem(obj reflect.Value, elemName string) (reflect.Value, error)
case mt.Type.NumOut() == 2 && !mt.Type.Out(1).Implements(errorType):
return zero, fmt.Errorf("%s is a method of type %s returning two values but the second value is not an error type", elemName, typ)
}
res := objPtr.Method(mt.Index).Call([]reflect.Value{})
res := objPtr.Method(mt.Index).Call(args)
if len(res) == 2 && !res[1].IsNil() {
return zero, fmt.Errorf("error at calling a method %s of type %s: %s", elemName, typ, res[1].Interface().(error))
}
@@ -371,7 +382,7 @@ func parseWhereArgs(args ...any) (mv reflect.Value, op string, err error) {
// checkWhereArray handles the where-matching logic when the seqv value is an
// Array or Slice.
func (ns *Namespace) checkWhereArray(seqv, kv, mv reflect.Value, path []string, op string) (any, error) {
func (ns *Namespace) checkWhereArray(ctxv, seqv, kv, mv reflect.Value, path []string, op string) (any, error) {
rv := reflect.MakeSlice(seqv.Type(), 0, 0)
for i := 0; i < seqv.Len(); i++ {
@@ -385,7 +396,7 @@ func (ns *Namespace) checkWhereArray(seqv, kv, mv reflect.Value, path []string,
vvv = rvv
for i, elemName := range path {
var err error
vvv, err = evaluateSubElem(vvv, elemName)
vvv, err = evaluateSubElem(ctxv, vvv, elemName)
if err != nil {
continue
@@ -417,14 +428,14 @@ func (ns *Namespace) checkWhereArray(seqv, kv, mv reflect.Value, path []string,
}
// checkWhereMap handles the where-matching logic when the seqv value is a Map.
func (ns *Namespace) checkWhereMap(seqv, kv, mv reflect.Value, path []string, op string) (any, error) {
func (ns *Namespace) checkWhereMap(ctxv, seqv, kv, mv reflect.Value, path []string, op string) (any, error) {
rv := reflect.MakeMap(seqv.Type())
keys := seqv.MapKeys()
for _, k := range keys {
elemv := seqv.MapIndex(k)
switch elemv.Kind() {
case reflect.Array, reflect.Slice:
r, err := ns.checkWhereArray(elemv, kv, mv, path, op)
r, err := ns.checkWhereArray(ctxv, elemv, kv, mv, path, op)
if err != nil {
return nil, err
}
@@ -443,7 +454,7 @@ func (ns *Namespace) checkWhereMap(seqv, kv, mv reflect.Value, path []string, op
switch elemvv.Kind() {
case reflect.Array, reflect.Slice:
r, err := ns.checkWhereArray(elemvv, kv, mv, path, op)
r, err := ns.checkWhereArray(ctxv, elemvv, kv, mv, path, op)
if err != nil {
return nil, err
}
+7 -6
View File
@@ -14,6 +14,7 @@
package collections
import (
"context"
"fmt"
"html/template"
"reflect"
@@ -641,9 +642,9 @@ func TestWhere(t *testing.T) {
var err error
if len(test.op) > 0 {
results, err = ns.Where(test.seq, test.key, test.op, test.match)
results, err = ns.Where(context.Background(), test.seq, test.key, test.op, test.match)
} else {
results, err = ns.Where(test.seq, test.key, test.match)
results, err = ns.Where(context.Background(), test.seq, test.key, test.match)
}
if b, ok := test.expect.(bool); ok && !b {
if err == nil {
@@ -662,17 +663,17 @@ func TestWhere(t *testing.T) {
}
var err error
_, err = ns.Where(map[string]int{"a": 1, "b": 2}, "a", []byte("="), 1)
_, err = ns.Where(context.Background(), map[string]int{"a": 1, "b": 2}, "a", []byte("="), 1)
if err == nil {
t.Errorf("Where called with none string op value didn't return an expected error")
}
_, err = ns.Where(map[string]int{"a": 1, "b": 2}, "a", []byte("="), 1, 2)
_, err = ns.Where(context.Background(), map[string]int{"a": 1, "b": 2}, "a", []byte("="), 1, 2)
if err == nil {
t.Errorf("Where called with more than two variable arguments didn't return an expected error")
}
_, err = ns.Where(map[string]int{"a": 1, "b": 2}, "a")
_, err = ns.Where(context.Background(), map[string]int{"a": 1, "b": 2}, "a")
if err == nil {
t.Errorf("Where called with no variable arguments didn't return an expected error")
}
@@ -842,7 +843,7 @@ func TestEvaluateSubElem(t *testing.T) {
{reflect.ValueOf(map[int]string{1: "foo", 2: "bar"}), "1", false},
{reflect.ValueOf([]string{"foo", "bar"}), "1", false},
} {
result, err := evaluateSubElem(test.value, test.key)
result, err := evaluateSubElem(reflect.ValueOf(context.Background()), test.value, test.key)
if b, ok := test.expect.(bool); ok && !b {
if err == nil {
t.Errorf("[%d] evaluateSubElem didn't return an expected error", i)