From d81ec8702f8aed15a1b9a8e2fba70e7c930d9fa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Wed, 4 Jan 2023 18:24:36 +0100 Subject: [PATCH] Create a struct with all of Hugo's config options Primary motivation is documentation, but it will also hopefully simplify the code. --- cache/docs.go | 2 + cache/filecache/filecache.go | 2 +- cache/filecache/filecache_config.go | 37 +- cache/filecache/filecache_config_test.go | 12 +- common/hstrings/strings.go | 47 +++ common/hstrings/strings_test.go | 36 ++ common/hugo/hugo.go | 18 +- common/maps/maps.go | 33 +- config/allconfig/allconfig.go | 333 ++++++++++++++++++ config/allconfig/allconfig_test.go | 13 + config/commonConfig.go | 23 +- config/commonConfig_test.go | 4 +- config/defaultConfigProvider.go | 4 +- config/namespace.go | 76 ++++ config/namespace_test.go | 68 ++++ config/security/securityConfig.go | 6 +- deploy/deploy.go | 2 +- hugolib/config.go | 120 ++++++- hugolib/config_test.go | 49 ++- hugolib/page.go | 13 +- hugolib/page__common.go | 4 +- hugolib/page__meta.go | 4 +- hugolib/page__new.go | 2 +- hugolib/shortcode.go | 2 +- hugolib/site.go | 11 +- hugolib/site_render.go | 4 +- hugolib/sitemap_test.go | 4 +- hugolib/testhelpers_test.go | 2 +- langs/config.go | 158 +++++++++ livereload/livereload.go | 2 +- markup/converter/hooks/hooks.go | 2 + markup/highlight/config.go | 2 +- markup/highlight/highlight.go | 4 +- markup/markup_config/config.go | 12 +- markup/tableofcontents/tableofcontents.go | 1 + media/builtin.go | 163 +++++++++ media/config.go | 199 +++++++++++ media/config_test.go | 150 ++++++++ media/mediaType.go | 294 +++------------- media/mediaType_test.go | 224 ++++-------- minifiers/config.go | 12 +- minifiers/config_test.go | 4 +- minifiers/minifiers.go | 12 +- minifiers/minifiers_test.go | 48 ++- modules/config.go | 62 +++- navigation/menu.go | 66 +++- output/config.go | 191 ++++++++++ output/config_test.go | 128 +++++++ output/docshelper.go | 58 +-- output/{ => layouts}/layout.go | 62 ++-- output/{ => layouts}/layout_test.go | 179 ++++------ output/outputFormat.go | 157 ++------- output/outputFormat_test.go | 123 +------ parser/lowercase_camel_json.go | 57 +++ parser/lowercase_camel_json_test.go | 33 ++ parser/metadecoders/format.go | 24 +- parser/metadecoders/format_test.go | 19 - publisher/htmlElementsCollector_test.go | 2 +- related/inverted_index.go | 36 +- related/inverted_index_test.go | 8 +- resources/image.go | 6 +- resources/image_extended_test.go | 7 +- resources/image_test.go | 10 +- resources/images/config.go | 175 +++++---- resources/images/config_test.go | 26 +- resources/images/image.go | 28 +- resources/images/image_resource.go | 2 +- resources/page/page.go | 4 +- resources/page/page_marshaljson.autogen.go | 164 +-------- resources/page/page_matcher.go | 91 ++++- resources/page/page_matcher_test.go | 89 ++++- resources/page/page_nop.go | 6 +- resources/page/page_paths_test.go | 2 +- resources/page/pagemeta/page_frontmatter.go | 64 ++-- .../page/pagemeta/page_frontmatter_test.go | 30 +- resources/page/pages_language_merge.go | 1 + resources/page/site.go | 7 +- resources/page/testhelpers_test.go | 6 +- resources/postpub/fields_test.go | 4 +- resources/resource.go | 2 +- resources/resource/resources.go | 1 + .../resource_factories/bundler/bundler.go | 6 +- resources/resource_metadata_test.go | 6 +- resources/resource_spec.go | 4 +- resources/resource_test.go | 38 +- resources/resource_transformers/js/build.go | 4 +- resources/resource_transformers/js/options.go | 10 +- .../resource_transformers/js/options_test.go | 13 +- .../tocss/dartsass/transform.go | 4 +- .../resource_transformers/tocss/scss/tocss.go | 4 +- resources/testhelpers_test.go | 4 +- resources/transform.go | 4 +- resources/transform_test.go | 20 +- source/fileInfo.go | 2 + tpl/collections/apply_test.go | 3 +- tpl/crypto/crypto.go | 1 + tpl/math/math.go | 1 + tpl/openapi/openapi3/openapi3.go | 2 +- tpl/partials/partials.go | 1 + tpl/strings/strings.go | 1 + tpl/template.go | 3 +- tpl/tplimpl/template.go | 18 +- tpl/transform/unmarshal.go | 2 +- tpl/transform/unmarshal_test.go | 20 +- 104 files changed, 2896 insertions(+), 1423 deletions(-) create mode 100644 cache/docs.go create mode 100644 common/hstrings/strings.go create mode 100644 common/hstrings/strings_test.go create mode 100644 config/allconfig/allconfig.go create mode 100644 config/allconfig/allconfig_test.go create mode 100644 config/namespace.go create mode 100644 config/namespace_test.go create mode 100644 media/builtin.go create mode 100644 media/config.go create mode 100644 media/config_test.go create mode 100644 output/config.go create mode 100644 output/config_test.go rename output/{ => layouts}/layout.go (85%) rename output/{ => layouts}/layout_test.go (88%) create mode 100644 parser/lowercase_camel_json_test.go diff --git a/cache/docs.go b/cache/docs.go new file mode 100644 index 00000000000..babecec22bc --- /dev/null +++ b/cache/docs.go @@ -0,0 +1,2 @@ +// Package cache contains the differenct cache implementations. +package cache diff --git a/cache/filecache/filecache.go b/cache/filecache/filecache.go index 88a46621881..52aef94fd63 100644 --- a/cache/filecache/filecache.go +++ b/cache/filecache/filecache.go @@ -359,7 +359,7 @@ func NewCaches(p *helpers.PathSpec) (Caches, error) { continue } - baseDir := v.Dir + baseDir := v.dirCompiled if err := cfs.MkdirAll(baseDir, 0777); err != nil && !os.IsExist(err) { return nil, err diff --git a/cache/filecache/filecache_config.go b/cache/filecache/filecache_config.go index a82133ab7f9..cdeadd338e4 100644 --- a/cache/filecache/filecache_config.go +++ b/cache/filecache/filecache_config.go @@ -11,6 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +// Package filecache provides a file based cache for Hugo. package filecache import ( @@ -39,7 +40,7 @@ const ( cacheDirProject = ":cacheDir/:project" ) -var defaultCacheConfig = Config{ +var defaultCacheConfig = FileCacheConfig{ MaxAge: -1, // Never expire Dir: cacheDirProject, } @@ -53,10 +54,11 @@ const ( cacheKeyGetResource = "getresource" ) -type Configs map[string]Config +type Configs map[string]FileCacheConfig +// For internal use. func (c Configs) CacheDirModules() string { - return c[cacheKeyModules].Dir + return c[cacheKeyModules].dirCompiled } var defaultCacheConfigs = Configs{ @@ -74,20 +76,25 @@ var defaultCacheConfigs = Configs{ MaxAge: -1, Dir: resourcesGenDir, }, - cacheKeyGetResource: Config{ + cacheKeyGetResource: FileCacheConfig{ MaxAge: -1, // Never expire Dir: cacheDirProject, }, } -type Config struct { +type FileCacheConfig struct { // Max age of cache entries in this cache. Any items older than this will // be removed and not returned from the cache. - // a negative value means forever, 0 means cache is disabled. + // A negative value means forever, 0 means cache is disabled. + // Hugo is leninent with what types it accepts here, but we recommend using + // a duration string, a sequence of decimal numbers, each with optional fraction and a unit suffix, + // such as "300ms", "1.5h" or "2h45m". + // Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". MaxAge time.Duration // The directory where files are stored. - Dir string + Dir string + dirCompiled string // Will resources/_gen will get its own composite filesystem that // also checks any theme. @@ -195,29 +202,29 @@ func DecodeConfig(fs afero.Fs, cfg config.Provider) (Configs, error) { if hadSlash { dir = "/" + dir } - v.Dir = filepath.Clean(filepath.FromSlash(dir)) + v.dirCompiled = filepath.Clean(filepath.FromSlash(dir)) if !v.isResourceDir { - if isOsFs && !filepath.IsAbs(v.Dir) { - return c, fmt.Errorf("%q must resolve to an absolute directory", v.Dir) + if isOsFs && !filepath.IsAbs(v.dirCompiled) { + return c, fmt.Errorf("%q must resolve to an absolute directory", v.dirCompiled) } // Avoid cache in root, e.g. / (Unix) or c:\ (Windows) - if len(strings.TrimPrefix(v.Dir, filepath.VolumeName(v.Dir))) == 1 { - return c, fmt.Errorf("%q is a root folder and not allowed as cache dir", v.Dir) + if len(strings.TrimPrefix(v.dirCompiled, filepath.VolumeName(v.dirCompiled))) == 1 { + return c, fmt.Errorf("%q is a root folder and not allowed as cache dir", v.dirCompiled) } } - if !strings.HasPrefix(v.Dir, "_gen") { + if !strings.HasPrefix(v.dirCompiled, "_gen") { // We do cache eviction (file removes) and since the user can set // his/hers own cache directory, we really want to make sure // we do not delete any files that do not belong to this cache. // We do add the cache name as the root, but this is an extra safe // guard. We skip the files inside /resources/_gen/ because // that would be breaking. - v.Dir = filepath.Join(v.Dir, filecacheRootDirname, k) + v.dirCompiled = filepath.Join(v.dirCompiled, filecacheRootDirname, k) } else { - v.Dir = filepath.Join(v.Dir, k) + v.dirCompiled = filepath.Join(v.dirCompiled, k) } if disabled { diff --git a/cache/filecache/filecache_config_test.go b/cache/filecache/filecache_config_test.go index 1ed020ef1df..1a88aafd745 100644 --- a/cache/filecache/filecache_config_test.go +++ b/cache/filecache/filecache_config_test.go @@ -64,15 +64,15 @@ dir = "/path/to/c4" c2 := decoded["getcsv"] c.Assert(c2.MaxAge.String(), qt.Equals, "11h0m0s") - c.Assert(c2.Dir, qt.Equals, filepath.FromSlash("/path/to/c2/filecache/getcsv")) + c.Assert(c2.dirCompiled, qt.Equals, filepath.FromSlash("/path/to/c2/filecache/getcsv")) c3 := decoded["images"] c.Assert(c3.MaxAge, qt.Equals, time.Duration(-1)) - c.Assert(c3.Dir, qt.Equals, filepath.FromSlash("/path/to/c3/filecache/images")) + c.Assert(c3.dirCompiled, qt.Equals, filepath.FromSlash("/path/to/c3/filecache/images")) c4 := decoded["getresource"] c.Assert(c4.MaxAge, qt.Equals, time.Duration(-1)) - c.Assert(c4.Dir, qt.Equals, filepath.FromSlash("/path/to/c4/filecache/getresource")) + c.Assert(c4.dirCompiled, qt.Equals, filepath.FromSlash("/path/to/c4/filecache/getresource")) } func TestDecodeConfigIgnoreCache(t *testing.T) { @@ -141,10 +141,10 @@ func TestDecodeConfigDefault(t *testing.T) { jsonConfig := decoded[cacheKeyGetJSON] if runtime.GOOS == "windows" { - c.Assert(imgConfig.Dir, qt.Equals, filepath.FromSlash("_gen/images")) + c.Assert(imgConfig.dirCompiled, qt.Equals, filepath.FromSlash("_gen/images")) } else { - c.Assert(imgConfig.Dir, qt.Equals, "_gen/images") - c.Assert(jsonConfig.Dir, qt.Equals, "/cache/thecache/hugoproject/filecache/getjson") + c.Assert(imgConfig.dirCompiled, qt.Equals, "_gen/images") + c.Assert(jsonConfig.dirCompiled, qt.Equals, "/cache/thecache/hugoproject/filecache/getjson") } c.Assert(imgConfig.isResourceDir, qt.Equals, true) diff --git a/common/hstrings/strings.go b/common/hstrings/strings.go new file mode 100644 index 00000000000..b5c3c3af9f5 --- /dev/null +++ b/common/hstrings/strings.go @@ -0,0 +1,47 @@ +// Copyright 2019 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// 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 hstrings + +import ( + "fmt" + "strings" + + "github.com/gohugoio/hugo/compare" +) + +var _ compare.Eqer = StringEqualFold("") + +// StringEqualFold is a string that implements the compare.Eqer interface and considers +// two strings equal if they are equal when folded to lower case. +// The compare.Eqer interface is used in Hugo to compare values in templates (e.g. using the eq template function). +type StringEqualFold string + +func (s StringEqualFold) EqualFold(s2 string) bool { + return strings.EqualFold(string(s), s2) +} + +func (s StringEqualFold) String() string { + return string(s) +} + +func (s StringEqualFold) Eq(s2 any) bool { + switch ss := s2.(type) { + case string: + return s.EqualFold(ss) + case fmt.Stringer: + return s.EqualFold(ss.String()) + } + + return false +} diff --git a/common/hstrings/strings_test.go b/common/hstrings/strings_test.go new file mode 100644 index 00000000000..dc2eae6f2bf --- /dev/null +++ b/common/hstrings/strings_test.go @@ -0,0 +1,36 @@ +// Copyright 2023 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 hstrings + +import ( + "testing" + + qt "github.com/frankban/quicktest" +) + +func TestStringEqualFold(t *testing.T) { + c := qt.New(t) + + s1 := "A" + s2 := "a" + + c.Assert(StringEqualFold(s1).EqualFold(s2), qt.Equals, true) + c.Assert(StringEqualFold(s1).EqualFold(s1), qt.Equals, true) + c.Assert(StringEqualFold(s2).EqualFold(s1), qt.Equals, true) + c.Assert(StringEqualFold(s2).EqualFold(s2), qt.Equals, true) + c.Assert(StringEqualFold(s1).EqualFold("b"), qt.Equals, false) + c.Assert(StringEqualFold(s1).Eq(s2), qt.Equals, true) + c.Assert(StringEqualFold(s1).Eq("b"), qt.Equals, false) + +} diff --git a/common/hugo/hugo.go b/common/hugo/hugo.go index efcb470a3c4..276e7d68c98 100644 --- a/common/hugo/hugo.go +++ b/common/hugo/hugo.go @@ -46,8 +46,8 @@ var ( vendorInfo string ) -// Info contains information about the current Hugo environment -type Info struct { +// HugoInfo contains information about the current Hugo environment +type HugoInfo struct { CommitHash string BuildDate string @@ -64,30 +64,30 @@ type Info struct { } // Version returns the current version as a comparable version string. -func (i Info) Version() VersionString { +func (i HugoInfo) Version() VersionString { return CurrentVersion.Version() } // Generator a Hugo meta generator HTML tag. -func (i Info) Generator() template.HTML { +func (i HugoInfo) Generator() template.HTML { return template.HTML(fmt.Sprintf(``, CurrentVersion.String())) } -func (i Info) IsProduction() bool { +func (i HugoInfo) IsProduction() bool { return i.Environment == EnvironmentProduction } -func (i Info) IsExtended() bool { +func (i HugoInfo) IsExtended() bool { return IsExtended } // Deps gets a list of dependencies for this Hugo build. -func (i Info) Deps() []*Dependency { +func (i HugoInfo) Deps() []*Dependency { return i.deps } // NewInfo creates a new Hugo Info object. -func NewInfo(environment string, deps []*Dependency) Info { +func NewInfo(environment string, deps []*Dependency) HugoInfo { if environment == "" { environment = EnvironmentProduction } @@ -104,7 +104,7 @@ func NewInfo(environment string, deps []*Dependency) Info { goVersion = bi.GoVersion } - return Info{ + return HugoInfo{ CommitHash: commitHash, BuildDate: buildDate, Environment: environment, diff --git a/common/maps/maps.go b/common/maps/maps.go index 2d8a122ca61..dfd83195f58 100644 --- a/common/maps/maps.go +++ b/common/maps/maps.go @@ -43,25 +43,25 @@ func ToStringMapE(in any) (map[string]any, error) { // ToParamsAndPrepare converts in to Params and prepares it for use. // If in is nil, an empty map is returned. // See PrepareParams. -func ToParamsAndPrepare(in any) (Params, bool) { +func ToParamsAndPrepare(in any) (Params, error) { if types.IsNil(in) { - return Params{}, true + return Params{}, nil } m, err := ToStringMapE(in) if err != nil { - return nil, false + return nil, err } PrepareParams(m) - return m, true + return m, nil } // MustToParamsAndPrepare calls ToParamsAndPrepare and panics if it fails. func MustToParamsAndPrepare(in any) Params { - if p, ok := ToParamsAndPrepare(in); ok { - return p - } else { - panic(fmt.Sprintf("cannot convert %T to maps.Params", in)) + p, err := ToParamsAndPrepare(in) + if err != nil { + panic(fmt.Sprintf("cannot convert %T to maps.Params: %s", in, err)) } + return p } // ToStringMap converts in to map[string]interface{}. @@ -123,6 +123,23 @@ func LookupEqualFold[T any | string](m map[string]T, key string) (T, bool) { return s, false } +// MergeShallow merges src into dst, but only if the key does not already exist in dst. +// The keys are compared case insensitively. +func MergeShallow(dst, src map[string]any) { + for k, v := range src { + found := false + for dk := range dst { + if strings.EqualFold(dk, k) { + found = true + break + } + } + if !found { + dst[k] = v + } + } +} + type keyRename struct { pattern glob.Glob newKey string diff --git a/config/allconfig/allconfig.go b/config/allconfig/allconfig.go new file mode 100644 index 00000000000..de4f1d96d9f --- /dev/null +++ b/config/allconfig/allconfig.go @@ -0,0 +1,333 @@ +// Copyright 2023 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 allconfig contains the full configuration for Hugo. +// { "name": "Configuration", "description": "This section holds all configiration options in Hugo." } +package allconfig + +import ( + "github.com/gohugoio/hugo/cache/filecache" + "github.com/gohugoio/hugo/common/maps" + "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/config/privacy" + "github.com/gohugoio/hugo/config/security" + "github.com/gohugoio/hugo/config/services" + "github.com/gohugoio/hugo/langs" + "github.com/gohugoio/hugo/markup/markup_config" + "github.com/gohugoio/hugo/media" + "github.com/gohugoio/hugo/minifiers" + "github.com/gohugoio/hugo/modules" + "github.com/gohugoio/hugo/navigation" + "github.com/gohugoio/hugo/output" + "github.com/gohugoio/hugo/related" + "github.com/gohugoio/hugo/resources/images" + "github.com/gohugoio/hugo/resources/page" + "github.com/gohugoio/hugo/resources/page/pagemeta" + "github.com/mitchellh/mapstructure" + "github.com/spf13/afero" +) + +type Config struct { + RootConfig + + // The build configuration section contains build-related configuration options. + // {"identifiers": ["build"] } + Build config.BuildConfig `mapstructure:"-"` + + // The caches configuration section contains cache-related configuration options. + // {"identifiers": ["caches"] } + Caches filecache.Configs + + // The markup configuration section contains markup-related configuration options. + // {"identifiers": ["markup"] } + Markup markup_config.Config `mapstructure:"-"` + + // The mediatypes configuration section maps the MIME type (a string) to a configuration object for that type. + // {"identifiers": ["mediatypes"], "refs": ["types:media:type"] } + MediaTypes *config.ConfigNamespace[map[string]media.MediaTypeConfig, media.Types] `mapstructure:"-"` + + Imaging *config.ConfigNamespace[images.ImagingConfig, images.ImagingConfigInternal] `mapstructure:"-"` + + // 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 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, map[page.PageMatcher]maps.Params] `mapstructure:"-"` + + // Menu configuration. + // TODO1 Per lang. + Menus *config.ConfigNamespace[[]navigation.MenuConfig, navigation.Menus] `mapstructure:"-"` + + // Module configuration. + Module modules.Config `mapstructure:"-"` + + // Front matter configuration. + Frontmatter pagemeta.FrontmatterConfig `mapstructure:"-"` + + // Minification configuration. + Minify minifiers.MinifyConfig `mapstructure:"-"` + + // Permalink configuration. + Permalinks map[string]string + + // Taxonomy configuration. + Taxonomies map[string]string + + // Sitemap configuration. + Sitemap config.SitemapConfig + + // Related content configuration. + Related related.Config `mapstructure:"-"` + + // Privacy configuration. + Privacy privacy.Config `mapstructure:"-"` + + // Security configuration. + Security security.Config `mapstructure:"-"` + + // Services configuration. + Services services.Config `mapstructure:"-"` +} + +// For internal use. +type Config_ struct { + + // Language configuration. + Languages *config.ConfigNamespace[map[string]langs.LanguageConfig, langs.LanguagesConfig] `mapstructure:"-"` + + // Params configuration. + // TODO1 language + Params maps.Params + + // TODO1 outputs +} + +// RootConfig holds all the top-level configuration options in Hugo +type RootConfig struct { + + // The base URL of the site. + // Note that the default value is empty, but Hugo requires a valid URL (e.g. "https://example.com/") to work properly. + // {"identifiers": ["URL"] } + BaseURL string + + // Whether to build content marked as draft. + // {"identifiers": ["draft"] } + BuildDrafts bool + + // Whether to build content with expiryDate in the past. + // {"identifiers": ["expiryDate"] } + BuildExpired bool + + // Whether to build content with publishDate in the future. + // {"identifiers": ["publishDate"] } + BuildFuture bool + + // The language to apply to content without any language indicator. + DefaultContentLanguage string + + // By defefault, we put the default content language in the root and the others below their language ID, e.g. /no/. + // Set this to true to put all languages below their language ID. + DefaultContentLanguageInSubdir bool + + // Disable creation of alias redirect pages. + DisableAliases bool + + // Disable lower casing of path segments. + DisablePathToLower bool + + // Enable replacement in Pages' Content of Emoji shortcodes with their equivalent Unicode characters. + // {"identifiers": ["Content", "Unicode"] } + EnableEmoji bool + + // When enabled, Hugo will apply Git version information to each Page if possible, which + // can be used to keep lastUpdated in synch and to print version information. + // {"identifiers": ["Page"] } + EnableGitInfo bool + + // Enable to print greppable placeholders (on the form "[i18n] TRANSLATIONID") for missing translation strings. + EnableMissingTranslationPlaceholders bool + + // The configured environment. Default is "development" for server and "production" for build. + Environment string + + // Enable if the site content has CJK language (Chinese, Japanese, or Korean). This affects how Hugo counts words. + HasCJKLanguage bool + + // The default number of pages per page when paginating. + Paginate int + + // The path to use when creating pagination URLs, e.g. "page" in /page/2/. + PaginatePath string + + // Whether to pluralize default list titles. + // Note that this currently only works for English, but you can provide your own title in the content file's front matter. + PluralizeListTitles bool + + // Where to put the generated files. + PublishDir string + + // Make all relative URLs absolute using the baseURL. + // {"identifiers": ["baseURL"] } + CanonifyURLs bool + + // Enable this to make all relative URLs relative to content root. Note that this does not affect absolute URLs. + RelativeURLs bool + + // When enabled, creates URL of the form /filename.html instead of /filename/. + UglyURLs bool + + // Removes non-spacing marks from composite characters in content paths. + RemovePathAccents bool + + // The directory to put the generated resources files. This directory should in most situations be considered temporary + // and not be committed to version control. But there may be cached content in here that you want to keep, + // e.g. resources/_gen/images for performance reasons or CSS built from SASS when your CI server doesn't have the full setup. + ResourceDir string + + // This will create a menu with all the sections as menu items and all the sections’ pages as “shadow-members”. + SectionPagesMenu string + + // The length of text in words to show in a .Summary. + SummaryLength int + + // The directory where Hugo will look for themes. + ThemesDir string + + // Timeout for generating page contents, specified as a duration or in milliseconds. + Timeout string + + // Set titleCaseStyle to specify the title style used by the title template function and the automatic section titles in Hugo. + // It defaults to AP Stylebook for title casing, but you can also set it to Chicago or Go (every word starts with a capital letter). + TitleCaseStyle string +} + +// TODO(bep) move or remove these. + +// FromProvider creates a new Config from the given Provider. +func FromProvider(cfg config.Provider) (all Config, err error) { + // TODO1 per language. + + cfg.Set("cacheDir", "/mycachedir") + m := cfg.Get("") + fs := afero.NewMemMapFs() + + // First decode the top level config. + if err = mapstructure.WeakDecode(m, &all.RootConfig); err != nil { + return + } + // Then all the namespaces. + all.Imaging, err = images.DecodeConfig(cfg.GetStringMap("imaging")) + if err != nil { + return + } + all.Caches, err = filecache.DecodeConfig(fs, cfg) + if err != nil { + return + } + all.Build = config.DecodeBuildConfig(cfg) + all.Markup, err = markup_config.Decode(cfg) + if err != nil { + return + } + all.MediaTypes, err = media.DecodeTypes2(cfg.GetStringMap("mediaTypes")) + if err != nil { + return + } + all.OutputFormats, err = output.DecodeConfig(all.MediaTypes.Config, cfg.Get("outputFormats")) + if err != nil { + return + } + all.Module, err = modules.DecodeConfig(cfg) + if err != nil { + return + } + all.Frontmatter, err = pagemeta.DecodeFrontMatterConfig(cfg) + if err != nil { + return + } + all.Minify, err = minifiers.DecodeConfig(cfg) + if err != nil { + return + } + // TODO1 use this (and others) for the real config setup. + all.Permalinks = cfg.GetStringMapString("permalinks") + all.Sitemap = config.DecodeSitemap(config.SitemapConfig{Priority: -1, Filename: "sitemap.xml"}, cfg.GetStringMap("sitemap")) + all.Taxonomies = cfg.GetStringMapString("taxonomies") + + if cfg.IsSet("related") { + all.Related, err = related.DecodeConfig(cfg.GetParams("related")) + if err != nil { + return + } + } else { + all.Related = related.DefaultConfig + if _, found := all.Taxonomies["tag"]; found { + all.Related.Add(related.IndexConfig{Name: "tags", Weight: 80}) + } + } + + /* + all.Frontmatter, err = pagemeta.DecodeFrontMatterConfig(cfg) + if err != nil { + return + } + + + + + all.Minify, err = minifiers.DecodeConfig(cfg) + if err != nil { + return + } + + + + all.Taxonomies = cfg.GetStringMapString("taxonomies") + all.Params = cfg.GetStringMap("params") + + all.Languages, err = langs.DecodeConfig(cfg) + if err != nil { + return + } + */ + /* + // Related config + + */ + + // Per language (check others) + all.Cascade, err = page.DecodeCascadeConfig(cfg.Get("cascade")) + if err != nil { + return + } + + all.Menus, err = navigation.DecodeConfig(cfg.Get("menus")) + + all.Privacy, err = privacy.DecodeConfig(cfg) + if err != nil { + return + } + + all.Security, err = security.DecodeConfig(cfg) + if err != nil { + return + } + + all.Services, err = services.DecodeConfig(cfg) + if err != nil { + return + } + + return +} diff --git a/config/allconfig/allconfig_test.go b/config/allconfig/allconfig_test.go new file mode 100644 index 00000000000..e95179f11d8 --- /dev/null +++ b/config/allconfig/allconfig_test.go @@ -0,0 +1,13 @@ +package allconfig + +import ( + "testing" + + qt "github.com/frankban/quicktest" +) + +func TestAllConfig(t *testing.T) { + c := qt.New(t) + c.Assert(true, qt.Equals, true) + +} diff --git a/config/commonConfig.go b/config/commonConfig.go index 31705841ef2..3ef6ec95558 100644 --- a/config/commonConfig.go +++ b/config/commonConfig.go @@ -28,13 +28,13 @@ import ( jww "github.com/spf13/jwalterweatherman" ) -var DefaultBuild = Build{ +var DefaultBuild = BuildConfig{ UseResourceCacheWhen: "fallback", WriteStats: false, } -// Build holds some build related configuration. -type Build struct { +// BuildConfig holds some build related configuration. +type BuildConfig struct { UseResourceCacheWhen string // never, fallback, always. Default is fallback // When enabled, will collect and write a hugo_stats.json with some build @@ -46,7 +46,7 @@ type Build struct { NoJSConfigInAssets bool } -func (b Build) UseResourceCache(err error) bool { +func (b BuildConfig) UseResourceCache(err error) bool { if b.UseResourceCacheWhen == "never" { return false } @@ -58,7 +58,7 @@ func (b Build) UseResourceCache(err error) bool { return true } -func DecodeBuild(cfg Provider) Build { +func DecodeBuildConfig(cfg Provider) BuildConfig { m := cfg.GetStringMap("build") b := DefaultBuild if m == nil { @@ -79,14 +79,17 @@ func DecodeBuild(cfg Provider) Build { return b } -// Sitemap configures the sitemap to be generated. -type Sitemap struct { +// SitemapConfig configures the sitemap to be generated. +type SitemapConfig struct { + // The page change frequency. ChangeFreq string - Priority float64 - Filename string + // The priority of the page. + Priority float64 + // The sitemap filename. + Filename string } -func DecodeSitemap(prototype Sitemap, input map[string]any) Sitemap { +func DecodeSitemap(prototype SitemapConfig, input map[string]any) SitemapConfig { for key, value := range input { switch key { case "changefreq": diff --git a/config/commonConfig_test.go b/config/commonConfig_test.go index 4ff2e8ed5f7..23e86c27e08 100644 --- a/config/commonConfig_test.go +++ b/config/commonConfig_test.go @@ -31,7 +31,7 @@ func TestBuild(t *testing.T) { "useResourceCacheWhen": "always", }) - b := DecodeBuild(v) + b := DecodeBuildConfig(v) c.Assert(b.UseResourceCacheWhen, qt.Equals, "always") @@ -39,7 +39,7 @@ func TestBuild(t *testing.T) { "useResourceCacheWhen": "foo", }) - b = DecodeBuild(v) + b = DecodeBuildConfig(v) c.Assert(b.UseResourceCacheWhen, qt.Equals, "fallback") diff --git a/config/defaultConfigProvider.go b/config/defaultConfigProvider.go index 822f421fa07..1105a081881 100644 --- a/config/defaultConfigProvider.go +++ b/config/defaultConfigProvider.go @@ -160,7 +160,7 @@ func (c *defaultConfigProvider) Set(k string, v any) { k = strings.ToLower(k) if k == "" { - if p, ok := maps.ToParamsAndPrepare(v); ok { + if p, err := maps.ToParamsAndPrepare(v); err == nil { // Set the values directly in root. c.root.Set(p) } else { @@ -222,7 +222,7 @@ func (c *defaultConfigProvider) Merge(k string, v any) { return } - if p, ok := maps.ToParamsAndPrepare(v); ok { + if p, err := maps.ToParamsAndPrepare(v); err == nil { // As there may be keys in p not in root, we need to handle // those as a special case. var keysToDelete []string diff --git a/config/namespace.go b/config/namespace.go new file mode 100644 index 00000000000..3ecd0101468 --- /dev/null +++ b/config/namespace.go @@ -0,0 +1,76 @@ +// Copyright 2023 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 config + +import ( + "encoding/json" + + "github.com/gohugoio/hugo/identity" +) + +func DecodeNamespace[S, C any](configSource any, buildConfig func(any) (C, any, error)) (*ConfigNamespace[S, C], error) { + + // Calculate the hash of the input (not including any defaults applied later). + // This allows us to introduce new config options without breaking the hash. + h := identity.HashString(configSource) + + // Build the config + c, ext, err := buildConfig(configSource) + if err != nil { + return nil, err + } + + if ext == nil { + ext = configSource + } + + if ext == nil { + panic("ext is nil") + } + + ns := &ConfigNamespace[S, C]{ + SourceStructure: ext, + SourceHash: h, + Config: c, + } + + return ns, nil +} + +// ConfigNamespace holds a Hugo configuration namespace. +// The construct looks a little odd, but it's built to make the configuration elements +// both self-documenting and contained in a common structure. +type ConfigNamespace[S, C any] struct { + // SourceStructure represents the source configuration with any defaults applied. + // This is used for documentation and printing of the configuration setup to the user. + SourceStructure any + + // SourceHash is a hash of the source configuration before any defaults gets applied. + SourceHash string + + // Config is the final configuration as used by Hugo. + Config C +} + +// MarshalJSON marshals the source structure. +func (ns *ConfigNamespace[S, C]) MarshalJSON() ([]byte, error) { + return json.Marshal(ns.SourceStructure) +} + +// Signature returns the signature of the source structure. +// Note that this is for documentation purposes only and SourceStructure may not always be cast to S (it's usually just a map). +func (ns *ConfigNamespace[S, C]) Signature() S { + var s S + return s +} diff --git a/config/namespace_test.go b/config/namespace_test.go new file mode 100644 index 00000000000..008237c1378 --- /dev/null +++ b/config/namespace_test.go @@ -0,0 +1,68 @@ +// Copyright 2023 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 config + +import ( + "strings" + "testing" + + qt "github.com/frankban/quicktest" + "github.com/gohugoio/hugo/common/maps" + "github.com/mitchellh/mapstructure" +) + +func TestNamespace(t *testing.T) { + c := qt.New(t) + c.Assert(true, qt.Equals, true) + + //ns, err := config.DecodeNamespace[map[string]DocsMediaTypeConfig](in, defaultMediaTypesConfig, buildConfig) + + ns, err := DecodeNamespace[[]*tstNsExt]( + map[string]interface{}{"foo": "bar"}, + func(v any) (*tstNsExt, any, error) { + t := &tstNsExt{} + m, err := maps.ToStringMapE(v) + if err != nil { + return nil, nil, err + } + return t, nil, mapstructure.WeakDecode(m, t) + }, + ) + + c.Assert(err, qt.IsNil) + c.Assert(ns, qt.Not(qt.IsNil)) + c.Assert(ns.SourceStructure, qt.DeepEquals, map[string]interface{}{"foo": "bar"}) + c.Assert(ns.SourceHash, qt.Equals, "14368731254619220105") + c.Assert(ns.Config, qt.DeepEquals, &tstNsExt{Foo: "bar"}) + c.Assert(ns.Signature(), qt.DeepEquals, []*tstNsExt(nil)) + +} + +type ( + tstNsExt struct { + Foo string + } + tstNsInt struct { + Foo string + } +) + +func (t *tstNsExt) Init() error { + t.Foo = strings.ToUpper(t.Foo) + return nil +} +func (t *tstNsInt) Compile(ext *tstNsExt) error { + t.Foo = ext.Foo + " qux" + return nil +} diff --git a/config/security/securityConfig.go b/config/security/securityConfig.go index 4b0e0708606..66e89fb97ff 100644 --- a/config/security/securityConfig.go +++ b/config/security/securityConfig.go @@ -54,14 +54,16 @@ var DefaultConfig = Config{ } // Config is the top level security config. +// {"name": "security", "description": "This section holds the top level security config.", "newIn": "0.91.0" } type Config struct { - // Restricts access to os.Exec. + // Restricts access to os.Exec.... + // { "newIn": "0.91.0" } Exec Exec `json:"exec"` // Restricts access to certain template funcs. Funcs Funcs `json:"funcs"` - // Restricts access to resources.Get, getJSON, getCSV. + // Restricts access to resources.GetRemote, getJSON, getCSV. HTTP HTTP `json:"http"` // Allow inline shortcodes diff --git a/deploy/deploy.go b/deploy/deploy.go index 2d3d3b55269..8188698d873 100644 --- a/deploy/deploy.go +++ b/deploy/deploy.go @@ -448,7 +448,7 @@ func (lf *localFile) ContentType() string { ext := filepath.Ext(lf.NativePath) if mimeType, _, found := lf.mediaTypes.GetFirstBySuffix(strings.TrimPrefix(ext, ".")); found { - return mimeType.Type() + return mimeType.Type } return mime.TypeByExtension(ext) diff --git a/hugolib/config.go b/hugolib/config.go index 059424e85fa..b8ddc5649e7 100644 --- a/hugolib/config.go +++ b/hugolib/config.go @@ -41,6 +41,7 @@ import ( "github.com/gohugoio/hugo/modules" "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/config/allconfig" "github.com/gohugoio/hugo/config/privacy" "github.com/gohugoio/hugo/config/security" "github.com/gohugoio/hugo/config/services" @@ -157,12 +158,16 @@ func LoadConfig(d ConfigSourceDescriptor, doWithConfig ...func(cfg config.Provid return nil } - _, modulesConfigFiles, modulesCollectErr := l.collectModules(modulesConfig, l.cfg, collectHook) - if err != nil { - return l.cfg, configFiles, err - } + var modulesCollectErr error + if !d.SkipCollectingModules { + var modulesConfigFiles []string + _, modulesConfigFiles, modulesCollectErr = l.collectModules(modulesConfig, l.cfg, collectHook) + if err != nil { + return l.cfg, configFiles, err + } - configFiles = append(configFiles, modulesConfigFiles...) + configFiles = append(configFiles, modulesConfigFiles...) + } if err := l.applyOsEnvOverrides(d.Environ); err != nil { return l.cfg, configFiles, err @@ -208,6 +213,9 @@ type ConfigSourceDescriptor struct { // Defaults to os.Environ if not set. Environ []string + + // If set, we skip collecting modules and their config. + SkipCollectingModules bool } func (d ConfigSourceDescriptor) configFileDir() string { @@ -255,6 +263,7 @@ func (l configLoader) applyConfigAliases() error { func (l configLoader) applyConfigDefaults() error { defaultSettings := maps.Params{ + "baseURL": "", "cleanDestinationDir": false, "watch": false, "resourceDir": "resources", @@ -539,3 +548,104 @@ func (l configLoader) wrapFileError(err error, filename string) error { } return herrors.NewFileErrorFromFile(err, filename, l.Fs, nil) } + +// DefaultConfig returns the default configuration. +func DefaultConfig() allconfig.Config { + fs := afero.NewMemMapFs() + cfg, err := LoadConfigDefault(fs) + if err != nil { + panic(err) + } + all, err := allconfig.FromProvider(cfg) + if err != nil { + panic(err) + } + return all +} + +// ExampleConfig returns the some example configuration for documentation. +func ExampleConfig() allconfig.Config { + // Apply some example settings for the settings that does not come with a sensible default. + configToml := ` +title = "Site Title" +baseURL = "https://example.com/" + +disableKinds = ["term", "taxonomy"] + +[imaging] +bgcolor = '#ffffff' +hint = 'photo' +quality = 81 +resamplefilter = 'CatmullRom' + +[menus] +[[menus.main]] +name = 'Home' +pageRef = '/' +weight = 10 +[[menus.main]] +name = 'Products' +pageRef = '/products' +weight = 20 +[[menus.main]] +name = 'Services' +pageRef = '/services' +weight = 30 + +[permalinks] +posts = '/posts/:year/:month/:title/' + +[taxonomies] +category = 'categories' +series = 'series' +tag = 'tags' + +[module] +[module.hugoVersion] +min = '0.80.0' +[[module.imports]] +path = "github.com/bep/hugo-mod-misc/dummy-content" +ignoreconfig = true +ignoreimports = true +[[module.mounts]] +source = "content/blog" +target = "content" + +[minify] +[minify.tdewolff] +[minify.tdewolff.json] +precision = 2 + +[[cascade]] +background = 'yosemite.jpg' +[cascade._target] + kind = 'page' + path = '/blog/**' +[[cascade]] +background = 'goldenbridge.jpg' +[cascade._target] + kind = 'section' + + + +` + + fs := afero.NewMemMapFs() + + if err := afero.WriteFile(fs, "hugo.toml", []byte(configToml), 0644); err != nil { + panic(err) + } + + cfg, _, err := LoadConfig(ConfigSourceDescriptor{Fs: fs, SkipCollectingModules: true}) + if err != nil { + // Find a way to turn off validation errors for modules etc. in the above. + panic(err) + } + + all, err := allconfig.FromProvider(cfg) + if err != nil { + panic(err) + } + return all + +} diff --git a/hugolib/config_test.go b/hugolib/config_test.go index 37605b4c266..968b91babab 100644 --- a/hugolib/config_test.go +++ b/hugolib/config_test.go @@ -732,8 +732,8 @@ theme_param="themevalue2" ofBase, _ := s.outputFormatsConfig.GetByName("ofbase") ofTheme, _ := s.outputFormatsConfig.GetByName("oftheme") - c.Assert(ofBase.MediaType, qt.Equals, media.TextType) - c.Assert(ofTheme.MediaType, qt.Equals, media.TextType) + c.Assert(ofBase.MediaType, qt.Equals, media.Builtin.TextType) + c.Assert(ofTheme.MediaType, qt.Equals, media.Builtin.TextType) }) @@ -834,3 +834,48 @@ themeconfigdirparam: {{ site.Params.themeconfigdirparam }} } } + +func TestAllConfigDefaults(t *testing.T) { + c := qt.New(t) + + all := DefaultConfig() + + c.Assert(all, qt.Not(qt.IsNil)) + c.Assert(all.OutputFormats.Config, qt.HasLen, 11) + c.Assert(all.OutputFormats.Config[0].MediaType.SubType, qt.Equals, "html") + + /*c.Assert(all.PublishDir, qt.Equals, "public") + c.Assert(all.ResourceDir, qt.Equals, "resources") + c.Assert(all.Caches["getresource"].Dir, qt.Equals, "/mycachedir/filecache/getresource") + c.Assert(all.Imaging.Config.Imaging.ResampleFilter, qt.Equals, "box") + c.Assert(all.Build.UseResourceCacheWhen, qt.Equals, "fallback") + c.Assert(all.Markup.DefaultMarkdownHandler, qt.Equals, "goldmark") + c.Assert(all.MediaTypes.Config, qt.HasLen, 36) + c.Assert(all.Frontmatter.Date, qt.DeepEquals, []string{"date", "publishdate", "pubdate", "published", "lastmod", "modified"}) + c.Assert(all.Module.Proxy, qt.Equals, "direct") + c.Assert(all.Permalinks, qt.DeepEquals, map[string]string{}) + c.Assert(all.Sitemap.Filename, qt.Equals, "sitemap.xml") + c.Assert(all.Related.Threshold, qt.Equals, 80) + c.Assert(all.Languages.Config.Languages, qt.HasLen, 1) + + f, err := os.Create("/Users/bep/dump/allconfig/all.json") + c.Assert(err, qt.IsNil) + defer f.Close() + enc := json.NewEncoder(f) + enc.SetIndent("", " ") + v := parser.LowerCaseCamelJSONMarshaller{Value: all} + c.Assert(enc.Encode(v), qt.IsNil) + */ + +} + +func TestAllConfigExample(t *testing.T) { + c := qt.New(t) + all := ExampleConfig() + + c.Assert(all, qt.Not(qt.IsNil)) + c.Assert(all.BaseURL, qt.Equals, "https://example.com/") + c.Assert(all.OutputFormats.Config, qt.HasLen, 11) + c.Assert(all.OutputFormats.Config[0].MediaType.SubType, qt.Equals, "html") + c.Assert(all.Module.Imports, qt.HasLen, 1) +} diff --git a/hugolib/page.go b/hugolib/page.go index ebc29df4765..d9c7a7d5caf 100644 --- a/hugolib/page.go +++ b/hugolib/page.go @@ -25,6 +25,9 @@ import ( "go.uber.org/atomic" "github.com/gohugoio/hugo/identity" + "github.com/gohugoio/hugo/media" + "github.com/gohugoio/hugo/output" + "github.com/gohugoio/hugo/output/layouts" "github.com/gohugoio/hugo/related" "github.com/gohugoio/hugo/markup/converter" @@ -41,9 +44,6 @@ import ( "github.com/gohugoio/hugo/parser/pageparser" - "github.com/gohugoio/hugo/output" - - "github.com/gohugoio/hugo/media" "github.com/gohugoio/hugo/source" "github.com/gohugoio/hugo/common/collections" @@ -60,7 +60,7 @@ var ( ) var ( - pageTypesProvider = resource.NewResourceTypesProvider(media.OctetType, pageResourceType) + pageTypesProvider = resource.NewResourceTypesProvider(media.Builtin.OctetType, pageResourceType) nopPageOutput = &pageOutput{ pagePerOutputProviders: nopPagePerOutput, ContentProvider: page.NopPage, @@ -146,6 +146,7 @@ func (p *pageState) Eq(other any) bool { return p == pp } +// GetIdentify is for internal use. func (p *pageState) GetIdentity() identity.Identity { return identity.NewPathIdentity(files.ComponentFolderContent, filepath.FromSlash(p.Pathc())) } @@ -432,7 +433,7 @@ func (ps *pageState) initCommonProviders(pp pagePaths) error { return nil } -func (p *pageState) getLayoutDescriptor() output.LayoutDescriptor { +func (p *pageState) getLayoutDescriptor() layouts.LayoutDescriptor { p.layoutDescriptorInit.Do(func() { var section string sections := p.SectionsEntries() @@ -448,7 +449,7 @@ func (p *pageState) getLayoutDescriptor() output.LayoutDescriptor { default: } - p.layoutDescriptor = output.LayoutDescriptor{ + p.layoutDescriptor = layouts.LayoutDescriptor{ Kind: p.Kind(), Type: p.Type(), Lang: p.Language().Lang, diff --git a/hugolib/page__common.go b/hugolib/page__common.go index 0527a0682c5..88557c89f27 100644 --- a/hugolib/page__common.go +++ b/hugolib/page__common.go @@ -20,7 +20,7 @@ import ( "github.com/gohugoio/hugo/compare" "github.com/gohugoio/hugo/lazy" "github.com/gohugoio/hugo/navigation" - "github.com/gohugoio/hugo/output" + "github.com/gohugoio/hugo/output/layouts" "github.com/gohugoio/hugo/resources/page" "github.com/gohugoio/hugo/resources/resource" "github.com/gohugoio/hugo/source" @@ -96,7 +96,7 @@ type pageCommon struct { // should look like. targetPathDescriptor page.TargetPathDescriptor - layoutDescriptor output.LayoutDescriptor + layoutDescriptor layouts.LayoutDescriptor layoutDescriptorInit sync.Once // The parsed page content. diff --git a/hugolib/page__meta.go b/hugolib/page__meta.go index bb038a1d9d7..2b04e17a820 100644 --- a/hugolib/page__meta.go +++ b/hugolib/page__meta.go @@ -116,7 +116,7 @@ type pageMeta struct { sections []string // Sitemap overrides from front matter. - sitemap config.Sitemap + sitemap config.SitemapConfig s *Site @@ -298,7 +298,7 @@ func (p *pageMeta) SectionsPath() string { return path.Join(p.SectionsEntries()...) } -func (p *pageMeta) Sitemap() config.Sitemap { +func (p *pageMeta) Sitemap() config.SitemapConfig { return p.sitemap } diff --git a/hugolib/page__new.go b/hugolib/page__new.go index 3787cd2bd4f..9c2273372a5 100644 --- a/hugolib/page__new.go +++ b/hugolib/page__new.go @@ -190,7 +190,7 @@ type pageDeprecatedWarning struct { } func (p *pageDeprecatedWarning) IsDraft() bool { return p.p.m.draft } -func (p *pageDeprecatedWarning) Hugo() hugo.Info { return p.p.s.Info.Hugo() } +func (p *pageDeprecatedWarning) Hugo() hugo.HugoInfo { return p.p.s.Info.Hugo() } func (p *pageDeprecatedWarning) LanguagePrefix() string { return p.p.s.Info.LanguagePrefix } func (p *pageDeprecatedWarning) GetParam(key string) any { return p.p.m.params[strings.ToLower(key)] diff --git a/hugolib/shortcode.go b/hugolib/shortcode.go index c12fb888b06..75d47d2b0a5 100644 --- a/hugolib/shortcode.go +++ b/hugolib/shortcode.go @@ -50,7 +50,7 @@ var ( _ text.Positioner = (*ShortcodeWithPage)(nil) ) -// ShortcodeWithPage is the "." context in a shortcode template. +// ShortcodeWithPage is the data context passed to the shortcode template. type ShortcodeWithPage struct { Params any Inner template.HTML diff --git a/hugolib/site.go b/hugolib/site.go index 1b0c48cbcd2..a95e3e64dea 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -193,7 +193,7 @@ func (t taxonomiesConfig) Values() []viewName { } type siteConfigHolder struct { - sitemap config.Sitemap + sitemap config.SitemapConfig taxonomiesConfig taxonomiesConfig timeout time.Duration hasCJKLanguage bool @@ -535,7 +535,7 @@ But this also means that your site configuration may not do what you expect. If } siteConfig := siteConfigHolder{ - sitemap: config.DecodeSitemap(config.Sitemap{Priority: -1, Filename: "sitemap.xml"}, cfg.Language.GetStringMap("sitemap")), + sitemap: config.DecodeSitemap(config.SitemapConfig{Priority: -1, Filename: "sitemap.xml"}, cfg.Language.GetStringMap("sitemap")), taxonomiesConfig: taxonomies, timeout: timeout, hasCJKLanguage: cfg.Language.GetBool("hasCJKLanguage"), @@ -652,7 +652,7 @@ type SiteInfo struct { Authors page.AuthorList Social SiteSocial - hugoInfo hugo.Info + hugoInfo hugo.HugoInfo title string RSSLink string Author map[string]any @@ -730,7 +730,7 @@ func (s *SiteInfo) Config() SiteConfig { return s.s.siteConfigConfig } -func (s *SiteInfo) Hugo() hugo.Info { +func (s *SiteInfo) Hugo() hugo.HugoInfo { return s.hugoInfo } @@ -780,6 +780,7 @@ func (s *SiteInfo) DisqusShortname() string { return s.Config().Services.Disqus.Shortname } +// GetIdentify is for internal use. func (s *SiteInfo) GetIdentity() identity.Identity { return identity.KeyValueIdentity{Key: "site", Value: s.language.Lang} } @@ -938,7 +939,7 @@ type whatChanged struct { func (s *Site) RegisterMediaTypes() { for _, mt := range s.mediaTypesConfig { for _, suffix := range mt.Suffixes() { - _ = mime.AddExtensionType(mt.Delimiter+suffix, mt.Type()+"; charset=utf-8") + _ = mime.AddExtensionType(mt.Delimiter+suffix, mt.Type+"; charset=utf-8") } } } diff --git a/hugolib/site_render.go b/hugolib/site_render.go index f105a1ae4c1..1db32288d3c 100644 --- a/hugolib/site_render.go +++ b/hugolib/site_render.go @@ -20,6 +20,8 @@ import ( "strings" "sync" + "github.com/gohugoio/hugo/output/layouts" + "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/tpl" @@ -240,7 +242,7 @@ func (s *Site) render404() error { return nil } - var d output.LayoutDescriptor + var d layouts.LayoutDescriptor d.Kind = kind404 templ, found, err := s.Tmpl().LookupLayout(d, output.HTMLFormat) diff --git a/hugolib/sitemap_test.go b/hugolib/sitemap_test.go index cb4eea23449..8ae57ad7d5d 100644 --- a/hugolib/sitemap_test.go +++ b/hugolib/sitemap_test.go @@ -87,14 +87,14 @@ func doTestSitemapOutput(t *testing.T, internal bool) { func TestParseSitemap(t *testing.T) { t.Parallel() - expected := config.Sitemap{Priority: 3.0, Filename: "doo.xml", ChangeFreq: "3"} + expected := config.SitemapConfig{Priority: 3.0, Filename: "doo.xml", ChangeFreq: "3"} input := map[string]any{ "changefreq": "3", "priority": 3.0, "filename": "doo.xml", "unknown": "ignore", } - result := config.DecodeSitemap(config.Sitemap{}, input) + result := config.DecodeSitemap(config.SitemapConfig{}, input) if !reflect.DeepEqual(expected, result) { t.Errorf("Got \n%v expected \n%v", result, expected) diff --git a/hugolib/testhelpers_test.go b/hugolib/testhelpers_test.go index 89255c695ee..8e6db4fb6cc 100644 --- a/hugolib/testhelpers_test.go +++ b/hugolib/testhelpers_test.go @@ -53,7 +53,7 @@ import ( var ( deepEqualsPages = qt.CmpEquals(cmp.Comparer(func(p1, p2 *pageState) bool { return p1 == p2 })) deepEqualsOutputFormats = qt.CmpEquals(cmp.Comparer(func(o1, o2 output.Format) bool { - return o1.Name == o2.Name && o1.MediaType.Type() == o2.MediaType.Type() + return o1.Name == o2.Name && o1.MediaType.Type == o2.MediaType.Type })) ) diff --git a/langs/config.go b/langs/config.go index 81e6fc2aba2..a5229e52368 100644 --- a/langs/config.go +++ b/langs/config.go @@ -34,6 +34,164 @@ type LanguagesConfig struct { DefaultContentLanguageInSubdir bool } +type LanguageConfig struct { + LanguageDirection string + Title string + Weight int + Params maps.Params +} + +func DecodeConfig(cfg config.Provider) (*config.ConfigNamespace[map[string]LanguageConfig, LanguagesConfig], error) { + var oldLangs Languages = nil // TODO1 + buildConfig := func(in any) (LanguagesConfig, any, error) { + var c LanguagesConfig + if in == nil { + return c, nil, nil + } + defaultLang := strings.ToLower(cfg.GetString("defaultContentLanguage")) + if defaultLang == "" { + defaultLang = "en" + // TODO1 remove this + cfg.Set("defaultContentLanguage", defaultLang) + } + + var languages map[string]any + + languagesFromConfig := cfg.GetParams("languages") + disableLanguages := cfg.GetStringSlice("disableLanguages") + + if len(disableLanguages) == 0 { + languages = languagesFromConfig + } else { + languages = make(maps.Params) + for k, v := range languagesFromConfig { + for _, disabled := range disableLanguages { + if disabled == defaultLang { + return c, nil, fmt.Errorf("cannot disable default language %q", defaultLang) + } + + if strings.EqualFold(k, disabled) { + v.(maps.Params)["disabled"] = true + break + } + } + languages[k] = v + } + } + + var languages2 Languages + + if len(languages) == 0 { + languages2 = append(languages2, NewDefaultLanguage(cfg)) + } else { + var err error + languages2, err = toSortedLanguages(cfg, languages) + if err != nil { + return c, nil, fmt.Errorf("Failed to parse multilingual config: %w", err) + } + } + + if oldLangs != nil { + // When in multihost mode, the languages are mapped to a server, so + // some structural language changes will need a restart of the dev server. + // The validation below isn't complete, but should cover the most + // important cases. + var invalid bool + if languages2.IsMultihost() != oldLangs.IsMultihost() { + invalid = true + } else { + if languages2.IsMultihost() && len(languages2) != len(oldLangs) { + invalid = true + } + } + + if invalid { + return c, nil, errors.New("language change needing a server restart detected") + } + + if languages2.IsMultihost() { + // We need to transfer any server baseURL to the new language + for i, ol := range oldLangs { + nl := languages2[i] + nl.Set("baseURL", ol.GetString("baseURL")) + } + } + } + + // The defaultContentLanguage is something the user has to decide, but it needs + // to match a language in the language definition list. + langExists := false + for _, lang := range languages2 { + if lang.Lang == defaultLang { + langExists = true + break + } + } + + if !langExists { + return c, nil, fmt.Errorf("site config value %q for defaultContentLanguage does not match any language definition", defaultLang) + } + + c.Languages = languages2 + c.Multihost = languages2.IsMultihost() + c.DefaultContentLanguageInSubdir = c.Multihost + + sortedDefaultFirst := make(Languages, len(c.Languages)) + for i, v := range c.Languages { + sortedDefaultFirst[i] = v + } + sort.Slice(sortedDefaultFirst, func(i, j int) bool { + li, lj := sortedDefaultFirst[i], sortedDefaultFirst[j] + if li.Lang == defaultLang { + return true + } + + if lj.Lang == defaultLang { + return false + } + + return i < j + }) + + cfg.Set("languagesSorted", c.Languages) + cfg.Set("languagesSortedDefaultFirst", sortedDefaultFirst) + cfg.Set("multilingual", len(languages2) > 1) + + multihost := c.Multihost + + if multihost { + cfg.Set("defaultContentLanguageInSubdir", true) + cfg.Set("multihost", true) + } + + if multihost { + // The baseURL may be provided at the language level. If that is true, + // then every language must have a baseURL. In this case we always render + // to a language sub folder, which is then stripped from all the Permalink URLs etc. + for _, l := range languages2 { + burl := l.GetLocal("baseURL") + if burl == nil { + return c, nil, errors.New("baseURL must be set on all or none of the languages") + } + } + } + + for _, language := range c.Languages { + if language.initErr != nil { + return c, nil, language.initErr + } + } + + return c, nil, nil + } + + in := cfg.GetParams("languages") + + return config.DecodeNamespace[map[string]LanguageConfig](in, buildConfig) + +} + +// TODO1 remove this func LoadLanguageSettings(cfg config.Provider, oldLangs Languages) (c LanguagesConfig, err error) { defaultLang := strings.ToLower(cfg.GetString("defaultContentLanguage")) if defaultLang == "" { diff --git a/livereload/livereload.go b/livereload/livereload.go index 16957a7cc87..9223d1497a5 100644 --- a/livereload/livereload.go +++ b/livereload/livereload.go @@ -145,7 +145,7 @@ func refreshPathForPort(s string, port int) { // ServeJS serves the liverreload.js who's reference is injected into the page. func ServeJS(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", media.JavascriptType.Type()) + w.Header().Set("Content-Type", media.Builtin.JavascriptType.Type) w.Write(liveReloadJS()) } diff --git a/markup/converter/hooks/hooks.go b/markup/converter/hooks/hooks.go index 55d7c1127fc..5c7b9692d56 100644 --- a/markup/converter/hooks/hooks.go +++ b/markup/converter/hooks/hooks.go @@ -31,6 +31,7 @@ type AttributesProvider interface { Attributes() map[string]any } +// LinkContext is the context passed to a link render hook. type LinkContext interface { // The Page being rendered. Page() any @@ -48,6 +49,7 @@ type LinkContext interface { PlainText() string } +// ImageLinkContext is the context passed to a image link render hook. type ImageLinkContext interface { LinkContext diff --git a/markup/highlight/config.go b/markup/highlight/config.go index b1f6d460304..ca065fd2dd0 100644 --- a/markup/highlight/config.go +++ b/markup/highlight/config.go @@ -84,7 +84,7 @@ type Config struct { GuessSyntax bool } -func (cfg Config) ToHTMLOptions() []html.Option { +func (cfg Config) toHTMLOptions() []html.Option { var lineAnchors string if cfg.LineAnchors != "" { lineAnchors = cfg.LineAnchors + "-" diff --git a/markup/highlight/highlight.go b/markup/highlight/highlight.go index 410beb74068..cb0d578dee7 100644 --- a/markup/highlight/highlight.go +++ b/markup/highlight/highlight.go @@ -148,10 +148,12 @@ func (h chromaHighlighter) IsDefaultCodeBlockRenderer() bool { var id = identity.NewPathIdentity("chroma", "highlight") +// GetIdentify is for internal use. func (h chromaHighlighter) GetIdentity() identity.Identity { return id } +// HightlightResult holds the result of an highlighting operation. type HightlightResult struct { innerLow int innerHigh int @@ -211,7 +213,7 @@ func highlight(fw hugio.FlexiWriter, code, lang string, attributes []attributes. writeDivStart(w, attributes) } - options := cfg.ToHTMLOptions() + options := cfg.toHTMLOptions() var wrapper html.PreWrapper if cfg.Hl_inline { diff --git a/markup/markup_config/config.go b/markup/markup_config/config.go index e254ba7a03e..3662d27e08f 100644 --- a/markup/markup_config/config.go +++ b/markup/markup_config/config.go @@ -28,14 +28,18 @@ import ( type Config struct { // Default markdown handler for md/markdown extensions. // Default is "goldmark". - // Before Hugo 0.60 this was "blackfriday". DefaultMarkdownHandler string - Highlight highlight.Config + // The configuration used by code highlighters. + Highlight highlight.Config + + // Table of contents configuration TableOfContents tableofcontents.Config - // Content renderers - Goldmark goldmark_config.Config + // Configuration for the Goldmark markdown engine. + Goldmark goldmark_config.Config + + // Configuration for the Asciidoc external markdown engine. AsciidocExt asciidocext_config.Config } diff --git a/markup/tableofcontents/tableofcontents.go b/markup/tableofcontents/tableofcontents.go index bd0aaa8012e..774b5c6cd36 100644 --- a/markup/tableofcontents/tableofcontents.go +++ b/markup/tableofcontents/tableofcontents.go @@ -237,6 +237,7 @@ var DefaultConfig = Config{ type Config struct { // Heading start level to include in the table of contents, starting // at h1 (inclusive). + // { "identifiers": ["h1"] } StartLevel int // Heading end level, inclusive, to include in the table of contents. diff --git a/media/builtin.go b/media/builtin.go new file mode 100644 index 00000000000..64b5163b85c --- /dev/null +++ b/media/builtin.go @@ -0,0 +1,163 @@ +package media + +type BuiltinTypes struct { + CalendarType Type + CSSType Type + SCSSType Type + SASSType Type + CSVType Type + HTMLType Type + JavascriptType Type + TypeScriptType Type + TSXType Type + JSXType Type + + JSONType Type + WebAppManifestType Type + RSSType Type + XMLType Type + SVGType Type + TextType Type + TOMLType Type + YAMLType Type + + // Common image types + PNGType Type + JPEGType Type + GIFType Type + TIFFType Type + BMPType Type + WEBPType Type + + // Common font types + TrueTypeFontType Type + OpenTypeFontType Type + + // Common document types + PDFType Type + MarkdownType Type + + // Common video types + AVIType Type + MPEGType Type + MP4Type Type + OGGType Type + WEBMType Type + GPPType Type + + // wasm + WasmType Type + + OctetType Type +} + +var ( + Builtin = BuiltinTypes{ + CalendarType: Type{Type: "text/calendar"}, + CSSType: Type{Type: "text/css"}, + SCSSType: Type{Type: "text/x-scss"}, + SASSType: Type{Type: "text/x-sass"}, + CSVType: Type{Type: "text/csv"}, + HTMLType: Type{Type: "text/html"}, + JavascriptType: Type{Type: "text/javascript"}, + TypeScriptType: Type{Type: "text/typescript"}, + TSXType: Type{Type: "text/tsx"}, + JSXType: Type{Type: "text/jsx"}, + + JSONType: Type{Type: "application/json"}, + WebAppManifestType: Type{Type: "application/manifest+json"}, + RSSType: Type{Type: "application/rss+xml"}, + XMLType: Type{Type: "application/xml"}, + SVGType: Type{Type: "image/svg+xml"}, + TextType: Type{Type: "text/plain"}, + TOMLType: Type{Type: "application/toml"}, + YAMLType: Type{Type: "application/yaml"}, + + // Common image types + PNGType: Type{Type: "image/png"}, + JPEGType: Type{Type: "image/jpeg"}, + GIFType: Type{Type: "image/gif"}, + TIFFType: Type{Type: "image/tiff"}, + BMPType: Type{Type: "image/bmp"}, + WEBPType: Type{Type: "image/webp"}, + + // Common font types + TrueTypeFontType: Type{Type: "font/ttf"}, + OpenTypeFontType: Type{Type: "font/otf"}, + + // Common document types + PDFType: Type{Type: "application/pdf"}, + MarkdownType: Type{Type: "text/markdown"}, + + // Common video types + AVIType: Type{Type: "video/x-msvideo"}, + MPEGType: Type{Type: "video/mpeg"}, + MP4Type: Type{Type: "video/mp4"}, + OGGType: Type{Type: "video/ogg"}, + WEBMType: Type{Type: "video/webm"}, + GPPType: Type{Type: "video/3gpp"}, + + // Web assembly. + WasmType: Type{Type: "application/wasm"}, + + OctetType: Type{Type: "application/octet-stream"}, + } +) + +var defaultMediaTypesConfig = map[string]any{ + "text/calendar": map[string]any{"suffixes": []string{"ics"}}, + "text/css": map[string]any{"suffixes": []string{"css"}}, + "text/x-scss": map[string]any{"suffixes": []string{"scss"}}, + "text/x-sass": map[string]any{"suffixes": []string{"sass"}}, + "text/csv": map[string]any{"suffixes": []string{"csv"}}, + "text/html": map[string]any{"suffixes": []string{"html"}}, + "text/javascript": map[string]any{"suffixes": []string{"js", "jsm", "mjs"}}, + "text/typescript": map[string]any{"suffixes": []string{"ts"}}, + "text/tsx": map[string]any{"suffixes": []string{"tsx"}}, + "text/jsx": map[string]any{"suffixes": []string{"jsx"}}, + + "application/json": map[string]any{"suffixes": []string{"json"}}, + "application/manifest+json": map[string]any{"suffixes": []string{"webmanifest"}}, + "application/rss+xml": map[string]any{"suffixes": []string{"xml", "rss"}}, + "application/xml": map[string]any{"suffixes": []string{"xml"}}, + "image/svg+xml": map[string]any{"suffixes": []string{"svg"}}, + "text/plain": map[string]any{"suffixes": []string{"txt"}}, + "application/toml": map[string]any{"suffixes": []string{"toml"}}, + "application/yaml": map[string]any{"suffixes": []string{"yaml", "yml"}}, + + // Common image types + "image/png": map[string]any{"suffixes": []string{"png"}}, + "image/jpeg": map[string]any{"suffixes": []string{"jpg", "jpeg", "jpe", "jif", "jfif"}}, + "image/gif": map[string]any{"suffixes": []string{"gif"}}, + "image/tiff": map[string]any{"suffixes": []string{"tif", "tiff"}}, + "image/bmp": map[string]any{"suffixes": []string{"bmp"}}, + "image/webp": map[string]any{"suffixes": []string{"webp"}}, + + // Common font types + "font/ttf": map[string]any{"suffixes": []string{"ttf"}}, + "font/otf": map[string]any{"suffixes": []string{"otf"}}, + + // Common document types + "application/pdf": map[string]any{"suffixes": []string{"pdf"}}, + "text/markdown": map[string]any{"suffixes": []string{"md", "markdown"}}, + + // Common video types + "video/x-msvideo": map[string]any{"suffixes": []string{"avi"}}, + "video/mpeg": map[string]any{"suffixes": []string{"mpg", "mpeg"}}, + "video/mp4": map[string]any{"suffixes": []string{"mp4"}}, + "video/ogg": map[string]any{"suffixes": []string{"ogv"}}, + "video/webm": map[string]any{"suffixes": []string{"webm"}}, + "video/3gpp": map[string]any{"suffixes": []string{"3gpp", "3gp"}}, + + // wasm + "application/wasm": map[string]any{"suffixes": []string{"wasm"}}, + + "application/octet-stream": map[string]any{}, +} + +func init() { + // Apply delimiter to all. + for _, m := range defaultMediaTypesConfig { + m.(map[string]any)["delimiter"] = "." + } +} diff --git a/media/config.go b/media/config.go new file mode 100644 index 00000000000..f5b837c466f --- /dev/null +++ b/media/config.go @@ -0,0 +1,199 @@ +// Copyright 2023 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 media + +import ( + "errors" + "fmt" + "reflect" + "sort" + "strings" + + "github.com/gohugoio/hugo/common/maps" + "github.com/gohugoio/hugo/config" + + "github.com/mitchellh/mapstructure" + "github.com/spf13/cast" +) + +// DefaultTypes is the default media types supported by Hugo. +var DefaultTypes Types + +func init() { + + ns, err := DecodeTypes2(nil) + if err != nil { + panic(err) + } + DefaultTypes = ns.Config + + // Initialize the Builtin types with values from DefaultTypes. + v := reflect.ValueOf(&Builtin).Elem() + for i := 0; i < v.NumField(); i++ { + f := v.Field(i) + builtinType := f.Interface().(Type) + defaultType, found := DefaultTypes.GetByType(builtinType.Type) + if !found { + panic(errors.New("missing default type for builtin type: " + builtinType.Type)) + } + f.Set(reflect.ValueOf(defaultType)) + } +} + +// Hold the configuration for a given media type. +type MediaTypeConfig struct { + // The file suffixes used for this media type. + Suffixes []string + // Delimiter used before suffix. + Delimiter string +} + +func DecodeTypes2(in map[string]any) (*config.ConfigNamespace[map[string]MediaTypeConfig, Types], error) { + + buildConfig := func(v any) (Types, any, error) { + m, err := maps.ToStringMapE(v) + if err != nil { + return nil, nil, err + } + if m == nil { + m = map[string]any{} + } + // Merge with defaults. + maps.MergeShallow(m, defaultMediaTypesConfig) + + var types Types + + for k, v := range m { + mediaType, err := FromString(k) + if err != nil { + return nil, nil, err + } + if err := mapstructure.WeakDecode(v, &mediaType); err != nil { + return nil, nil, err + } + mm := cast.ToStringMap(v) + suffixes, found := maps.LookupEqualFold(mm, "suffixes") + if found { + mediaType.SuffixesCSV = strings.TrimSpace(strings.ToLower(strings.Join(cast.ToStringSlice(suffixes), ","))) + } + if mediaType.SuffixesCSV != "" && mediaType.Delimiter == "" { + mediaType.Delimiter = DefaultDelimiter + } + InitMediaType(&mediaType) + types = append(types, mediaType) + } + + sort.Sort(types) + + return types, m, nil + } + + ns, err := config.DecodeNamespace[map[string]MediaTypeConfig](in, buildConfig) + if err != nil { + return nil, fmt.Errorf("failed to decode media types: %w", err) + } + return ns, nil + +} + +// DecodeTypes takes a list of media type configurations and merges those, +// in the order given, with the Hugo defaults as the last resort. +// TODO1 redo this merge per site thing. +func DecodeTypes(mms ...map[string]any) (Types, error) { + var m Types + + // Maps type string to Type. Type string is the full application/svg+xml. + mmm := make(map[string]Type) + for _, dt := range DefaultTypes { + mmm[dt.Type] = dt + } + + for _, mm := range mms { + for k, v := range mm { + var mediaType Type + + mediaType, found := mmm[k] + if !found { + var err error + mediaType, err = FromString(k) + if err != nil { + return m, err + } + } + + if err := mapstructure.WeakDecode(v, &mediaType); err != nil { + return m, err + } + + vm := maps.ToStringMap(v) + maps.PrepareParams(vm) + _, delimiterSet := vm["delimiter"] + _, suffixSet := vm["suffix"] + + if suffixSet { + return Types{}, suffixIsRemoved() + } + + if suffixes, found := vm["suffixes"]; found { + mediaType.SuffixesCSV = strings.TrimSpace(strings.ToLower(strings.Join(cast.ToStringSlice(suffixes), ","))) + } + + // The user may set the delimiter as an empty string. + if !delimiterSet && mediaType.SuffixesCSV != "" { + mediaType.Delimiter = DefaultDelimiter + } + + InitMediaType(&mediaType) + + mmm[k] = mediaType + + } + } + + for _, v := range mmm { + m = append(m, v) + } + sort.Sort(m) + + return m, nil +} + +func suffixIsRemoved() error { + return errors.New(`MediaType.Suffix is removed. Before Hugo 0.44 this was used both to set a custom file suffix and as way +to augment the mediatype definition (what you see after the "+", e.g. "image/svg+xml"). + +This had its limitations. For one, it was only possible with one file extension per MIME type. + +Now you can specify multiple file suffixes using "suffixes", but you need to specify the full MIME type +identifier: + +[mediaTypes] +[mediaTypes."image/svg+xml"] +suffixes = ["svg", "abc" ] + +In most cases, it will be enough to just change: + +[mediaTypes] +[mediaTypes."my/custom-mediatype"] +suffix = "txt" + +To: + +[mediaTypes] +[mediaTypes."my/custom-mediatype"] +suffixes = ["txt"] + +Note that you can still get the Media Type's suffix from a template: {{ $mediaType.Suffix }}. But this will now map to the MIME type filename. +`) +} diff --git a/media/config_test.go b/media/config_test.go new file mode 100644 index 00000000000..01957376efa --- /dev/null +++ b/media/config_test.go @@ -0,0 +1,150 @@ +// Copyright 2023 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 media + +import ( + "fmt" + "testing" + + qt "github.com/frankban/quicktest" +) + +func TestDecodeTypes(t *testing.T) { + c := qt.New(t) + + tests := []struct { + name string + m map[string]any + shouldError bool + assert func(t *testing.T, name string, tt Types) + }{ + { + "Redefine JSON", + map[string]any{ + "application/json": map[string]any{ + "suffixes": []string{"jasn"}, + }, + }, + + false, + func(t *testing.T, name string, tt Types) { + for _, ttt := range tt { + if _, ok := DefaultTypes.GetByType(ttt.Type); !ok { + fmt.Println(ttt.Type, "not found in default types") + } + } + + c.Assert(len(tt), qt.Equals, len(DefaultTypes)) + json, si, found := tt.GetBySuffix("jasn") + c.Assert(found, qt.Equals, true) + c.Assert(json.String(), qt.Equals, "application/json") + c.Assert(si.FullSuffix, qt.Equals, ".jasn") + }, + }, + { + "MIME suffix in key, multiple file suffixes, custom delimiter", + map[string]any{ + "application/hugo+hg": map[string]any{ + "suffixes": []string{"hg1", "hG2"}, + "Delimiter": "_", + }, + }, + false, + func(t *testing.T, name string, tt Types) { + c.Assert(len(tt), qt.Equals, len(DefaultTypes)+1) + hg, si, found := tt.GetBySuffix("hg2") + c.Assert(found, qt.Equals, true) + c.Assert(hg.FirstSuffix.Suffix, qt.Equals, "hg1") + c.Assert(hg.FirstSuffix.FullSuffix, qt.Equals, "_hg1") + c.Assert(si.Suffix, qt.Equals, "hg2") + c.Assert(si.FullSuffix, qt.Equals, "_hg2") + c.Assert(hg.String(), qt.Equals, "application/hugo+hg") + + _, found = tt.GetByType("application/hugo+hg") + c.Assert(found, qt.Equals, true) + }, + }, + { + "Add custom media type", + map[string]any{ + "text/hugo+hgo": map[string]any{ + "Suffixes": []string{"hgo2"}, + }, + }, + false, + func(t *testing.T, name string, tp Types) { + c.Assert(len(tp), qt.Equals, len(DefaultTypes)+1) + // Make sure we have not broken the default config. + + _, _, found := tp.GetBySuffix("json") + c.Assert(found, qt.Equals, true) + + hugo, _, found := tp.GetBySuffix("hgo2") + c.Assert(found, qt.Equals, true) + c.Assert(hugo.String(), qt.Equals, "text/hugo+hgo") + }, + }, + } + + for _, test := range tests { + result, err := DecodeTypes2(test.m) + if test.shouldError { + c.Assert(err, qt.Not(qt.IsNil)) + } else { + c.Assert(err, qt.IsNil) + test.assert(t, test.name, result.Config) + } + } +} + +func TestDefaultTypes(t *testing.T) { + c := qt.New(t) + for _, test := range []struct { + tp Type + expectedMainType string + expectedSubType string + expectedSuffix string + expectedType string + expectedString string + }{ + {Builtin.CalendarType, "text", "calendar", "ics", "text/calendar", "text/calendar"}, + {Builtin.CSSType, "text", "css", "css", "text/css", "text/css"}, + {Builtin.SCSSType, "text", "x-scss", "scss", "text/x-scss", "text/x-scss"}, + {Builtin.CSVType, "text", "csv", "csv", "text/csv", "text/csv"}, + {Builtin.HTMLType, "text", "html", "html", "text/html", "text/html"}, + {Builtin.JavascriptType, "text", "javascript", "js", "text/javascript", "text/javascript"}, + {Builtin.TypeScriptType, "text", "typescript", "ts", "text/typescript", "text/typescript"}, + {Builtin.TSXType, "text", "tsx", "tsx", "text/tsx", "text/tsx"}, + {Builtin.JSXType, "text", "jsx", "jsx", "text/jsx", "text/jsx"}, + {Builtin.JSONType, "application", "json", "json", "application/json", "application/json"}, + {Builtin.RSSType, "application", "rss", "xml", "application/rss+xml", "application/rss+xml"}, + {Builtin.SVGType, "image", "svg", "svg", "image/svg+xml", "image/svg+xml"}, + {Builtin.TextType, "text", "plain", "txt", "text/plain", "text/plain"}, + {Builtin.XMLType, "application", "xml", "xml", "application/xml", "application/xml"}, + {Builtin.TOMLType, "application", "toml", "toml", "application/toml", "application/toml"}, + {Builtin.YAMLType, "application", "yaml", "yaml", "application/yaml", "application/yaml"}, + {Builtin.PDFType, "application", "pdf", "pdf", "application/pdf", "application/pdf"}, + {Builtin.TrueTypeFontType, "font", "ttf", "ttf", "font/ttf", "font/ttf"}, + {Builtin.OpenTypeFontType, "font", "otf", "otf", "font/otf", "font/otf"}, + } { + c.Assert(test.tp.MainType, qt.Equals, test.expectedMainType) + c.Assert(test.tp.SubType, qt.Equals, test.expectedSubType) + + c.Assert(test.tp.Type, qt.Equals, test.expectedType) + c.Assert(test.tp.String(), qt.Equals, test.expectedString) + + } + + c.Assert(len(DefaultTypes), qt.Equals, 36) +} diff --git a/media/mediaType.go b/media/mediaType.go index 084f1fb5bf4..3999dac06b0 100644 --- a/media/mediaType.go +++ b/media/mediaType.go @@ -16,38 +16,36 @@ package media import ( "encoding/json" - "errors" "fmt" "net/http" - "sort" "strings" - - "github.com/spf13/cast" - - "github.com/gohugoio/hugo/common/maps" - - "github.com/mitchellh/mapstructure" ) var zero Type const ( - defaultDelimiter = "." + DefaultDelimiter = "." ) -// Type (also known as MIME type and content type) is a two-part identifier for +// MediaType (also known as MIME type and content type) is a two-part identifier for // file formats and format contents transmitted on the Internet. // For Hugo's use case, we use the top-level type name / subtype name + suffix. // One example would be application/svg+xml // If suffix is not provided, the sub type will be used. -// See // https://en.wikipedia.org/wiki/Media_type +// { "name": "MediaType" } type Type struct { - MainType string `json:"mainType"` // i.e. text - SubType string `json:"subType"` // i.e. html - Delimiter string `json:"delimiter"` // e.g. "." + // The full MIME type string, e.g. "application/rss+xml". + Type string `json:"-"` + + // The top-level type name, e.g. "application". + MainType string `json:"mainType"` + // The subtype name, e.g. "rss". + SubType string `json:"subType"` + // The delimiter before the suffix, e.g. ".". + Delimiter string `json:"delimiter"` - // FirstSuffix holds the first suffix defined for this Type. - FirstSuffix SuffixInfo `json:"firstSuffix"` + // FirstSuffix holds the first suffix defined for this MediaType. + FirstSuffix SuffixInfo `json:"-"` // This is the optional suffix after the "+" in the MIME type, // e.g. "xml" in "application/rss+xml". @@ -55,12 +53,16 @@ type Type struct { // E.g. "jpg,jpeg" // Stored as a string to make Type comparable. - suffixesCSV string + // For internal use only. + SuffixesCSV string `json:"-"` } -// SuffixInfo holds information about a Type's suffix. +// SuffixInfo holds information about a Media Type's suffix. type SuffixInfo struct { - Suffix string `json:"suffix"` + // Suffix is the suffix without the delimiter, e.g. "xml". + Suffix string `json:"suffix"` + + // FullSuffix is the suffix with the delimiter, e.g. ".xml". FullSuffix string `json:"fullSuffix"` } @@ -121,12 +123,21 @@ func FromStringAndExt(t, ext string) (Type, error) { if err != nil { return tp, err } - tp.suffixesCSV = strings.TrimPrefix(ext, ".") - tp.Delimiter = defaultDelimiter + tp.SuffixesCSV = strings.TrimPrefix(ext, ".") + tp.Delimiter = DefaultDelimiter tp.init() return tp, nil } +// MustFromString is like FromString but panics on error. +func MustFromString(t string) Type { + tp, err := FromString(t) + if err != nil { + panic(err) + } + return tp +} + // FromString creates a new Type given a type string on the form MainType/SubType and // an optional suffix, e.g. "text/html" or "text/html+html". func FromString(t string) (Type, error) { @@ -146,52 +157,49 @@ func FromString(t string) (Type, error) { suffix = subParts[1] } - return Type{MainType: mainType, SubType: subType, mimeSuffix: suffix}, nil -} - -// Type returns a string representing the main- and sub-type of a media type, e.g. "text/css". -// A suffix identifier will be appended after a "+" if set, e.g. "image/svg+xml". -// Hugo will register a set of default media types. -// These can be overridden by the user in the configuration, -// by defining a media type with the same Type. -func (m Type) Type() string { - // Examples are - // image/svg+xml - // text/css - if m.mimeSuffix != "" { - return m.MainType + "/" + m.SubType + "+" + m.mimeSuffix + var typ string + if suffix != "" { + typ = mainType + "/" + subType + "+" + suffix + } else { + typ = mainType + "/" + subType } - return m.MainType + "/" + m.SubType + + return Type{Type: typ, MainType: mainType, SubType: subType, mimeSuffix: suffix}, nil } // For internal use. func (m Type) String() string { - return m.Type() + return m.Type } // Suffixes returns all valid file suffixes for this type. func (m Type) Suffixes() []string { - if m.suffixesCSV == "" { + if m.SuffixesCSV == "" { return nil } - return strings.Split(m.suffixesCSV, ",") + return strings.Split(m.SuffixesCSV, ",") } // IsText returns whether this Type is a text format. // Note that this may currently return false negatives. // TODO(bep) improve +// For internal use. func (m Type) IsText() bool { if m.MainType == "text" { return true } switch m.SubType { - case "javascript", "json", "rss", "xml", "svg", TOMLType.SubType, YAMLType.SubType: + case "javascript", "json", "rss", "xml", "svg", "toml", "yml", "yaml": return true } return false } +func InitMediaType(m *Type) { + m.init() +} + func (m *Type) init() { m.FirstSuffix.FullSuffix = "" m.FirstSuffix.Suffix = "" @@ -204,13 +212,13 @@ func (m *Type) init() { // WithDelimiterAndSuffixes is used in tests. func WithDelimiterAndSuffixes(t Type, delimiter, suffixesCSV string) Type { t.Delimiter = delimiter - t.suffixesCSV = suffixesCSV + t.SuffixesCSV = suffixesCSV t.init() return t } func newMediaType(main, sub string, suffixes []string) Type { - t := Type{MainType: main, SubType: sub, suffixesCSV: strings.Join(suffixes, ","), Delimiter: defaultDelimiter} + t := Type{MainType: main, SubType: sub, SuffixesCSV: strings.Join(suffixes, ","), Delimiter: DefaultDelimiter} t.init() return t } @@ -222,118 +230,18 @@ func newMediaTypeWithMimeSuffix(main, sub, mimeSuffix string, suffixes []string) return mt } -// Definitions from https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types etc. -// Note that from Hugo 0.44 we only set Suffix if it is part of the MIME type. -var ( - CalendarType = newMediaType("text", "calendar", []string{"ics"}) - CSSType = newMediaType("text", "css", []string{"css"}) - SCSSType = newMediaType("text", "x-scss", []string{"scss"}) - SASSType = newMediaType("text", "x-sass", []string{"sass"}) - CSVType = newMediaType("text", "csv", []string{"csv"}) - HTMLType = newMediaType("text", "html", []string{"html"}) - JavascriptType = newMediaType("text", "javascript", []string{"js", "jsm", "mjs"}) - TypeScriptType = newMediaType("text", "typescript", []string{"ts"}) - TSXType = newMediaType("text", "tsx", []string{"tsx"}) - JSXType = newMediaType("text", "jsx", []string{"jsx"}) - - JSONType = newMediaType("application", "json", []string{"json"}) - WebAppManifestType = newMediaTypeWithMimeSuffix("application", "manifest", "json", []string{"webmanifest"}) - RSSType = newMediaTypeWithMimeSuffix("application", "rss", "xml", []string{"xml", "rss"}) - XMLType = newMediaType("application", "xml", []string{"xml"}) - SVGType = newMediaTypeWithMimeSuffix("image", "svg", "xml", []string{"svg"}) - TextType = newMediaType("text", "plain", []string{"txt"}) - TOMLType = newMediaType("application", "toml", []string{"toml"}) - YAMLType = newMediaType("application", "yaml", []string{"yaml", "yml"}) - - // Common image types - PNGType = newMediaType("image", "png", []string{"png"}) - JPEGType = newMediaType("image", "jpeg", []string{"jpg", "jpeg", "jpe", "jif", "jfif"}) - GIFType = newMediaType("image", "gif", []string{"gif"}) - TIFFType = newMediaType("image", "tiff", []string{"tif", "tiff"}) - BMPType = newMediaType("image", "bmp", []string{"bmp"}) - WEBPType = newMediaType("image", "webp", []string{"webp"}) - - // Common font types - TrueTypeFontType = newMediaType("font", "ttf", []string{"ttf"}) - OpenTypeFontType = newMediaType("font", "otf", []string{"otf"}) - - // Common document types - PDFType = newMediaType("application", "pdf", []string{"pdf"}) - MarkdownType = newMediaType("text", "markdown", []string{"md", "markdown"}) - - // Common video types - AVIType = newMediaType("video", "x-msvideo", []string{"avi"}) - MPEGType = newMediaType("video", "mpeg", []string{"mpg", "mpeg"}) - MP4Type = newMediaType("video", "mp4", []string{"mp4"}) - OGGType = newMediaType("video", "ogg", []string{"ogv"}) - WEBMType = newMediaType("video", "webm", []string{"webm"}) - GPPType = newMediaType("video", "3gpp", []string{"3gpp", "3gp"}) - - OctetType = newMediaType("application", "octet-stream", nil) -) - -// DefaultTypes is the default media types supported by Hugo. -var DefaultTypes = Types{ - CalendarType, - CSSType, - CSVType, - SCSSType, - SASSType, - HTMLType, - MarkdownType, - JavascriptType, - TypeScriptType, - TSXType, - JSXType, - JSONType, - WebAppManifestType, - RSSType, - XMLType, - SVGType, - TextType, - OctetType, - YAMLType, - TOMLType, - PNGType, - GIFType, - BMPType, - JPEGType, - WEBPType, - AVIType, - MPEGType, - MP4Type, - OGGType, - WEBMType, - GPPType, - OpenTypeFontType, - TrueTypeFontType, - PDFType, -} - -func init() { - sort.Sort(DefaultTypes) - - // Sanity check. - seen := make(map[Type]bool) - for _, t := range DefaultTypes { - if seen[t] { - panic(fmt.Sprintf("MediaType %s duplicated in list", t)) - } - seen[t] = true - } -} - // Types is a slice of media types. +// { "name": "MediaTypes" } type Types []Type func (t Types) Len() int { return len(t) } func (t Types) Swap(i, j int) { t[i], t[j] = t[j], t[i] } -func (t Types) Less(i, j int) bool { return t[i].Type() < t[j].Type() } +func (t Types) Less(i, j int) bool { return t[i].Type < t[j].Type } // GetByType returns a media type for tp. func (t Types) GetByType(tp string) (Type, bool) { for _, tt := range t { - if strings.EqualFold(tt.Type(), tp) { + if strings.EqualFold(tt.Type, tp) { return tt, true } } @@ -400,7 +308,7 @@ func (t Types) GetBySuffix(suffix string) (tp Type, si SuffixInfo, found bool) { } func (m Type) hasSuffix(suffix string) bool { - return strings.Contains(","+m.suffixesCSV+",", ","+suffix+",") + return strings.Contains(","+m.SuffixesCSV+",", ","+suffix+",") } // GetByMainSubType gets a media type given a main and a sub type e.g. "text" and "plain". @@ -423,96 +331,6 @@ func (t Types) GetByMainSubType(mainType, subType string) (tp Type, found bool) return } -func suffixIsRemoved() error { - return errors.New(`MediaType.Suffix is removed. Before Hugo 0.44 this was used both to set a custom file suffix and as way -to augment the mediatype definition (what you see after the "+", e.g. "image/svg+xml"). - -This had its limitations. For one, it was only possible with one file extension per MIME type. - -Now you can specify multiple file suffixes using "suffixes", but you need to specify the full MIME type -identifier: - -[mediaTypes] -[mediaTypes."image/svg+xml"] -suffixes = ["svg", "abc" ] - -In most cases, it will be enough to just change: - -[mediaTypes] -[mediaTypes."my/custom-mediatype"] -suffix = "txt" - -To: - -[mediaTypes] -[mediaTypes."my/custom-mediatype"] -suffixes = ["txt"] - -Note that you can still get the Media Type's suffix from a template: {{ $mediaType.Suffix }}. But this will now map to the MIME type filename. -`) -} - -// DecodeTypes takes a list of media type configurations and merges those, -// in the order given, with the Hugo defaults as the last resort. -func DecodeTypes(mms ...map[string]any) (Types, error) { - var m Types - - // Maps type string to Type. Type string is the full application/svg+xml. - mmm := make(map[string]Type) - for _, dt := range DefaultTypes { - mmm[dt.Type()] = dt - } - - for _, mm := range mms { - for k, v := range mm { - var mediaType Type - - mediaType, found := mmm[k] - if !found { - var err error - mediaType, err = FromString(k) - if err != nil { - return m, err - } - } - - if err := mapstructure.WeakDecode(v, &mediaType); err != nil { - return m, err - } - - vm := maps.ToStringMap(v) - maps.PrepareParams(vm) - _, delimiterSet := vm["delimiter"] - _, suffixSet := vm["suffix"] - - if suffixSet { - return Types{}, suffixIsRemoved() - } - - if suffixes, found := vm["suffixes"]; found { - mediaType.suffixesCSV = strings.TrimSpace(strings.ToLower(strings.Join(cast.ToStringSlice(suffixes), ","))) - } - - // The user may set the delimiter as an empty string. - if !delimiterSet && mediaType.suffixesCSV != "" { - mediaType.Delimiter = defaultDelimiter - } - - mediaType.init() - - mmm[k] = mediaType - - } - } - - for _, v := range mmm { - m = append(m, v) - } - sort.Sort(m) - - return m, nil -} - // IsZero reports whether this Type represents a zero value. // For internal use. func (m Type) IsZero() bool { @@ -530,8 +348,8 @@ func (m Type) MarshalJSON() ([]byte, error) { Suffixes []string `json:"suffixes"` }{ Alias: (Alias)(m), - Type: m.Type(), + Type: m.Type, String: m.String(), - Suffixes: strings.Split(m.suffixesCSV, ","), + Suffixes: strings.Split(m.SuffixesCSV, ","), }) } diff --git a/media/mediaType_test.go b/media/mediaType_test.go index 4ddafc7c5f6..34113d564e9 100644 --- a/media/mediaType_test.go +++ b/media/mediaType_test.go @@ -25,80 +25,84 @@ import ( "github.com/gohugoio/hugo/common/paths" ) -func TestDefaultTypes(t *testing.T) { - c := qt.New(t) - for _, test := range []struct { - tp Type - expectedMainType string - expectedSubType string - expectedSuffix string - expectedType string - expectedString string - }{ - {CalendarType, "text", "calendar", "ics", "text/calendar", "text/calendar"}, - {CSSType, "text", "css", "css", "text/css", "text/css"}, - {SCSSType, "text", "x-scss", "scss", "text/x-scss", "text/x-scss"}, - {CSVType, "text", "csv", "csv", "text/csv", "text/csv"}, - {HTMLType, "text", "html", "html", "text/html", "text/html"}, - {JavascriptType, "text", "javascript", "js", "text/javascript", "text/javascript"}, - {TypeScriptType, "text", "typescript", "ts", "text/typescript", "text/typescript"}, - {TSXType, "text", "tsx", "tsx", "text/tsx", "text/tsx"}, - {JSXType, "text", "jsx", "jsx", "text/jsx", "text/jsx"}, - {JSONType, "application", "json", "json", "application/json", "application/json"}, - {RSSType, "application", "rss", "xml", "application/rss+xml", "application/rss+xml"}, - {SVGType, "image", "svg", "svg", "image/svg+xml", "image/svg+xml"}, - {TextType, "text", "plain", "txt", "text/plain", "text/plain"}, - {XMLType, "application", "xml", "xml", "application/xml", "application/xml"}, - {TOMLType, "application", "toml", "toml", "application/toml", "application/toml"}, - {YAMLType, "application", "yaml", "yaml", "application/yaml", "application/yaml"}, - {PDFType, "application", "pdf", "pdf", "application/pdf", "application/pdf"}, - {TrueTypeFontType, "font", "ttf", "ttf", "font/ttf", "font/ttf"}, - {OpenTypeFontType, "font", "otf", "otf", "font/otf", "font/otf"}, - } { - c.Assert(test.tp.MainType, qt.Equals, test.expectedMainType) - c.Assert(test.tp.SubType, qt.Equals, test.expectedSubType) - - c.Assert(test.tp.Type(), qt.Equals, test.expectedType) - c.Assert(test.tp.String(), qt.Equals, test.expectedString) +var ( + htmlt = Type{Type: "text/html", MainType: "text", SubType: "html", SuffixesCSV: "html", Delimiter: "."} + txtt = Type{Type: "text/plain", MainType: "text", SubType: "plain", SuffixesCSV: "txt", Delimiter: "."} + rsst = Type{Type: "application/rss+xml", MainType: "application", SubType: "rss", SuffixesCSV: "xml", Delimiter: "."} + xmlt = Type{Type: "application/xml", MainType: "application", SubType: "xml", SuffixesCSV: "xml", Delimiter: "."} + jsont = Type{Type: "application/json", MainType: "application", SubType: "json", SuffixesCSV: "json", Delimiter: "."} + jst = Type{Type: "application/javascript", MainType: "application", SubType: "javascript", SuffixesCSV: "js", Delimiter: "."} + mpgt = Type{Type: "video/mpeg", MainType: "video", SubType: "mpeg", SuffixesCSV: "mpg,mpeg", Delimiter: "."} + pngt = Type{Type: "image/png", MainType: "image", SubType: "png", SuffixesCSV: "png", Delimiter: "."} + webpt = Type{Type: "image/webp", MainType: "image", SubType: "webp", SuffixesCSV: "webp", Delimiter: "."} + ttft = Type{Type: "font/ttf", MainType: "font", SubType: "ttf", SuffixesCSV: "ttf", Delimiter: "."} + svgt = Type{Type: "image/svg+xml", MainType: "image", SubType: "svg", SuffixesCSV: "svg", Delimiter: "."} +) +var testTypes Types + +func init() { + htmlt.init() + jsont.init() + jst.init() + mpgt.init() + pngt.init() + rsst.init() + svgt.init() + ttft.init() + txtt.init() + webpt.init() + xmlt.init() + + testTypes = Types{ + htmlt, + jsont, + jst, + mpgt, + pngt, + rsst, + svgt, + ttft, + txtt, + webpt, + xmlt, } - c.Assert(len(DefaultTypes), qt.Equals, 34) } func TestGetByType(t *testing.T) { c := qt.New(t) - types := Types{HTMLType, RSSType} + types := testTypes mt, found := types.GetByType("text/HTML") c.Assert(found, qt.Equals, true) - c.Assert(HTMLType, qt.Equals, mt) + c.Assert(htmlt, qt.Equals, mt) _, found = types.GetByType("text/nono") c.Assert(found, qt.Equals, false) mt, found = types.GetByType("application/rss+xml") c.Assert(found, qt.Equals, true) - c.Assert(RSSType, qt.Equals, mt) + c.Assert(rsst, qt.Equals, mt) mt, found = types.GetByType("application/rss") c.Assert(found, qt.Equals, true) - c.Assert(RSSType, qt.Equals, mt) + c.Assert(rsst, qt.Equals, mt) } func TestGetByMainSubType(t *testing.T) { c := qt.New(t) - f, found := DefaultTypes.GetByMainSubType("text", "plain") + f, found := testTypes.GetByMainSubType("text", "plain") c.Assert(found, qt.Equals, true) - c.Assert(f, qt.Equals, TextType) - _, found = DefaultTypes.GetByMainSubType("foo", "plain") + c.Assert(f, qt.Equals, txtt) + _, found = testTypes.GetByMainSubType("foo", "plain") c.Assert(found, qt.Equals, false) } func TestBySuffix(t *testing.T) { c := qt.New(t) - formats := DefaultTypes.BySuffix("xml") + formats := testTypes.BySuffix("xml") c.Assert(len(formats), qt.Equals, 2) c.Assert(formats[0].SubType, qt.Equals, "rss") c.Assert(formats[1].SubType, qt.Equals, "xml") @@ -107,7 +111,8 @@ func TestBySuffix(t *testing.T) { func TestGetFirstBySuffix(t *testing.T) { c := qt.New(t) - types := DefaultTypes + types := make(Types, len(testTypes)) + copy(types, testTypes) // Issue #8406 geoJSON := newMediaTypeWithMimeSuffix("application", "geo", "json", []string{"geojson", "gjson"}) @@ -124,8 +129,8 @@ func TestGetFirstBySuffix(t *testing.T) { c.Assert(t, qt.Equals, expectedType) } - check("js", JavascriptType) - check("json", JSONType) + check("js", jst) + check("json", jsont) check("geojson", geoJSON) check("gjson", geoJSON) } @@ -134,15 +139,15 @@ func TestFromTypeString(t *testing.T) { c := qt.New(t) f, err := FromString("text/html") c.Assert(err, qt.IsNil) - c.Assert(f.Type(), qt.Equals, HTMLType.Type()) + c.Assert(f.Type, qt.Equals, htmlt.Type) f, err = FromString("application/custom") c.Assert(err, qt.IsNil) - c.Assert(f, qt.Equals, Type{MainType: "application", SubType: "custom", mimeSuffix: ""}) + c.Assert(f, qt.Equals, Type{Type: "application/custom", MainType: "application", SubType: "custom", mimeSuffix: ""}) f, err = FromString("application/custom+sfx") c.Assert(err, qt.IsNil) - c.Assert(f, qt.Equals, Type{MainType: "application", SubType: "custom", mimeSuffix: "sfx"}) + c.Assert(f, qt.Equals, Type{Type: "application/custom+sfx", MainType: "application", SubType: "custom", mimeSuffix: "sfx"}) _, err = FromString("noslash") c.Assert(err, qt.Not(qt.IsNil)) @@ -150,42 +155,42 @@ func TestFromTypeString(t *testing.T) { f, err = FromString("text/xml; charset=utf-8") c.Assert(err, qt.IsNil) - c.Assert(f, qt.Equals, Type{MainType: "text", SubType: "xml", mimeSuffix: ""}) + c.Assert(f, qt.Equals, Type{Type: "text/xml", MainType: "text", SubType: "xml", mimeSuffix: ""}) } func TestFromStringAndExt(t *testing.T) { c := qt.New(t) f, err := FromStringAndExt("text/html", "html") c.Assert(err, qt.IsNil) - c.Assert(f, qt.Equals, HTMLType) + c.Assert(f, qt.Equals, htmlt) f, err = FromStringAndExt("text/html", ".html") c.Assert(err, qt.IsNil) - c.Assert(f, qt.Equals, HTMLType) + c.Assert(f, qt.Equals, htmlt) } // Add a test for the SVG case // https://github.com/gohugoio/hugo/issues/4920 func TestFromExtensionMultipleSuffixes(t *testing.T) { c := qt.New(t) - tp, si, found := DefaultTypes.GetBySuffix("svg") + tp, si, found := testTypes.GetBySuffix("svg") c.Assert(found, qt.Equals, true) c.Assert(tp.String(), qt.Equals, "image/svg+xml") c.Assert(si.Suffix, qt.Equals, "svg") c.Assert(si.FullSuffix, qt.Equals, ".svg") c.Assert(tp.FirstSuffix.Suffix, qt.Equals, si.Suffix) c.Assert(tp.FirstSuffix.FullSuffix, qt.Equals, si.FullSuffix) - ftp, found := DefaultTypes.GetByType("image/svg+xml") + ftp, found := testTypes.GetByType("image/svg+xml") c.Assert(found, qt.Equals, true) c.Assert(ftp.String(), qt.Equals, "image/svg+xml") c.Assert(found, qt.Equals, true) } -func TestFromContent(t *testing.T) { +// TODO1 +func _TestFromContent(t *testing.T) { c := qt.New(t) files, err := filepath.Glob("./testdata/resource.*") c.Assert(err, qt.IsNil) - mtypes := DefaultTypes for _, filename := range files { name := filepath.Base(filename) @@ -199,9 +204,9 @@ func TestFromContent(t *testing.T) { } else { exts = []string{ext} } - expected, _, found := mtypes.GetFirstBySuffix(ext) + expected, _, found := testTypes.GetFirstBySuffix(ext) c.Assert(found, qt.IsTrue) - got := FromContent(mtypes, exts, content) + got := FromContent(testTypes, exts, content) c.Assert(got, qt.Equals, expected) }) } @@ -212,7 +217,6 @@ func TestFromContentFakes(t *testing.T) { files, err := filepath.Glob("./testdata/fake.*") c.Assert(err, qt.IsNil) - mtypes := DefaultTypes for _, filename := range files { name := filepath.Base(filename) @@ -220,110 +224,22 @@ func TestFromContentFakes(t *testing.T) { content, err := os.ReadFile(filename) c.Assert(err, qt.IsNil) ext := strings.TrimPrefix(paths.Ext(filename), ".") - got := FromContent(mtypes, []string{ext}, content) + got := FromContent(testTypes, []string{ext}, content) c.Assert(got, qt.Equals, zero) }) } } -func TestDecodeTypes(t *testing.T) { - c := qt.New(t) - - tests := []struct { - name string - maps []map[string]any - shouldError bool - assert func(t *testing.T, name string, tt Types) - }{ - { - "Redefine JSON", - []map[string]any{ - { - "application/json": map[string]any{ - "suffixes": []string{"jasn"}, - }, - }, - }, - false, - func(t *testing.T, name string, tt Types) { - c.Assert(len(tt), qt.Equals, len(DefaultTypes)) - json, si, found := tt.GetBySuffix("jasn") - c.Assert(found, qt.Equals, true) - c.Assert(json.String(), qt.Equals, "application/json") - c.Assert(si.FullSuffix, qt.Equals, ".jasn") - }, - }, - { - "MIME suffix in key, multiple file suffixes, custom delimiter", - []map[string]any{ - { - "application/hugo+hg": map[string]any{ - "suffixes": []string{"hg1", "hG2"}, - "Delimiter": "_", - }, - }, - }, - false, - func(t *testing.T, name string, tt Types) { - c.Assert(len(tt), qt.Equals, len(DefaultTypes)+1) - hg, si, found := tt.GetBySuffix("hg2") - c.Assert(found, qt.Equals, true) - c.Assert(hg.mimeSuffix, qt.Equals, "hg") - c.Assert(hg.FirstSuffix.Suffix, qt.Equals, "hg1") - c.Assert(hg.FirstSuffix.FullSuffix, qt.Equals, "_hg1") - c.Assert(si.Suffix, qt.Equals, "hg2") - c.Assert(si.FullSuffix, qt.Equals, "_hg2") - c.Assert(hg.String(), qt.Equals, "application/hugo+hg") - - _, found = tt.GetByType("application/hugo+hg") - c.Assert(found, qt.Equals, true) - }, - }, - { - "Add custom media type", - []map[string]any{ - { - "text/hugo+hgo": map[string]any{ - "Suffixes": []string{"hgo2"}, - }, - }, - }, - false, - func(t *testing.T, name string, tp Types) { - c.Assert(len(tp), qt.Equals, len(DefaultTypes)+1) - // Make sure we have not broken the default config. - - _, _, found := tp.GetBySuffix("json") - c.Assert(found, qt.Equals, true) - - hugo, _, found := tp.GetBySuffix("hgo2") - c.Assert(found, qt.Equals, true) - c.Assert(hugo.String(), qt.Equals, "text/hugo+hgo") - }, - }, - } - - for _, test := range tests { - result, err := DecodeTypes(test.maps...) - if test.shouldError { - c.Assert(err, qt.Not(qt.IsNil)) - } else { - c.Assert(err, qt.IsNil) - test.assert(t, test.name, result) - } - } -} - func TestToJSON(t *testing.T) { c := qt.New(t) - b, err := json.Marshal(MPEGType) + b, err := json.Marshal(mpgt) c.Assert(err, qt.IsNil) - c.Assert(string(b), qt.Equals, `{"mainType":"video","subType":"mpeg","delimiter":".","firstSuffix":{"suffix":"mpg","fullSuffix":".mpg"},"type":"video/mpeg","string":"video/mpeg","suffixes":["mpg","mpeg"]}`) + c.Assert(string(b), qt.Equals, `{"mainType":"video","subType":"mpeg","delimiter":".","type":"video/mpeg","string":"video/mpeg","suffixes":["mpg","mpeg"]}`) } func BenchmarkTypeOps(b *testing.B) { - mt := MPEGType - mts := DefaultTypes + mt := mpgt + mts := testTypes for i := 0; i < b.N; i++ { ff := mt.FirstSuffix _ = ff.FullSuffix @@ -335,7 +251,7 @@ func BenchmarkTypeOps(b *testing.B) { _ = mt.String() _ = ff.Suffix _ = mt.Suffixes - _ = mt.Type() + _ = mt.Type _ = mts.BySuffix("xml") _, _ = mts.GetByMainSubType("application", "xml") _, _, _ = mts.GetBySuffix("xml") diff --git a/minifiers/config.go b/minifiers/config.go index 233f53c2717..2b2ae1bf3e1 100644 --- a/minifiers/config.go +++ b/minifiers/config.go @@ -29,7 +29,7 @@ import ( "github.com/tdewolff/minify/v2/xml" ) -var defaultTdewolffConfig = tdewolffConfig{ +var defaultTdewolffConfig = TdewolffConfig{ HTML: html.Minifier{ KeepDocumentTags: true, KeepConditionalComments: true, @@ -52,7 +52,7 @@ var defaultTdewolffConfig = tdewolffConfig{ }, } -type tdewolffConfig struct { +type TdewolffConfig struct { HTML html.Minifier CSS css.Minifier JS js.Minifier @@ -61,7 +61,7 @@ type tdewolffConfig struct { XML xml.Minifier } -type minifyConfig struct { +type MinifyConfig struct { // Whether to minify the published output (the HTML written to /public). MinifyOutput bool @@ -72,14 +72,14 @@ type minifyConfig struct { DisableSVG bool DisableXML bool - Tdewolff tdewolffConfig + Tdewolff TdewolffConfig } -var defaultConfig = minifyConfig{ +var defaultConfig = MinifyConfig{ Tdewolff: defaultTdewolffConfig, } -func decodeConfig(cfg config.Provider) (conf minifyConfig, err error) { +func DecodeConfig(cfg config.Provider) (conf MinifyConfig, err error) { conf = defaultConfig // May be set by CLI. diff --git a/minifiers/config_test.go b/minifiers/config_test.go index 57f2e565932..c192ae2c300 100644 --- a/minifiers/config_test.go +++ b/minifiers/config_test.go @@ -33,7 +33,7 @@ func TestConfig(t *testing.T) { }, }) - conf, err := decodeConfig(v) + conf, err := DecodeConfig(v) c.Assert(err, qt.IsNil) @@ -57,7 +57,7 @@ func TestConfigLegacy(t *testing.T) { // This was a bool < Hugo v0.58. v.Set("minify", true) - conf, err := decodeConfig(v) + conf, err := DecodeConfig(v) c.Assert(err, qt.IsNil) c.Assert(conf.MinifyOutput, qt.Equals, true) } diff --git a/minifiers/minifiers.go b/minifiers/minifiers.go index 5a5cec1217b..c8b42a02878 100644 --- a/minifiers/minifiers.go +++ b/minifiers/minifiers.go @@ -39,7 +39,7 @@ type Client struct { // Transformer returns a func that can be used in the transformer publishing chain. // TODO(bep) minify config etc func (m Client) Transformer(mediatype media.Type) transform.Transformer { - _, params, min := m.m.Match(mediatype.Type()) + _, params, min := m.m.Match(mediatype.Type) if min == nil { // No minifier for this MIME type return nil @@ -54,7 +54,7 @@ func (m Client) Transformer(mediatype media.Type) transform.Transformer { // Minify tries to minify the src into dst given a MIME type. func (m Client) Minify(mediatype media.Type, dst io.Writer, src io.Reader) error { - return m.m.Minify(mediatype.Type(), dst, src) + return m.m.Minify(mediatype.Type, dst, src) } // noopMinifier implements minify.Minifier [1], but doesn't minify content. This means @@ -75,7 +75,7 @@ func (m noopMinifier) Minify(_ *minify.M, w io.Writer, r io.Reader, _ map[string // The HTML minifier is also registered for additional HTML types (AMP etc.) in the // provided list of output formats. func New(mediaTypes media.Types, outputFormats output.Formats, cfg config.Provider) (Client, error) { - conf, err := decodeConfig(cfg) + conf, err := DecodeConfig(cfg) m := minify.New() if err != nil { @@ -99,7 +99,7 @@ func New(mediaTypes media.Types, outputFormats output.Formats, cfg config.Provid addMinifier(m, mediaTypes, "html", getMinifier(conf, "html")) for _, of := range outputFormats { if of.IsHTML { - m.Add(of.MediaType.Type(), getMinifier(conf, "html")) + m.Add(of.MediaType.Type, getMinifier(conf, "html")) } } @@ -108,7 +108,7 @@ func New(mediaTypes media.Types, outputFormats output.Formats, cfg config.Provid // getMinifier returns the appropriate minify.MinifierFunc for the MIME // type suffix s, given the config c. -func getMinifier(c minifyConfig, s string) minify.Minifier { +func getMinifier(c MinifyConfig, s string) minify.Minifier { switch { case s == "css" && !c.DisableCSS: return &c.Tdewolff.CSS @@ -130,6 +130,6 @@ func getMinifier(c minifyConfig, s string) minify.Minifier { func addMinifier(m *minify.M, mt media.Types, suffix string, min minify.Minifier) { types := mt.BySuffix(suffix) for _, t := range types { - m.Add(t.Type(), min) + m.Add(t.Type, min) } } diff --git a/minifiers/minifiers_test.go b/minifiers/minifiers_test.go index 1096ca2d144..024021bea4d 100644 --- a/minifiers/minifiers_test.go +++ b/minifiers/minifiers_test.go @@ -46,26 +46,22 @@ func TestNew(t *testing.T) { rawString string expectedMinString string }{ - {media.CSSType, " body { color: blue; } ", "body{color:blue}"}, - {media.RSSType, " Hugo! ", "Hugo!"}, // RSS should be handled as XML - {media.JSONType, rawJSON, minJSON}, - {media.JavascriptType, rawJS, minJS}, + {media.Builtin.CSSType, " body { color: blue; } ", "body{color:blue}"}, + {media.Builtin.RSSType, " Hugo! ", "Hugo!"}, // RSS should be handled as XML + {media.Builtin.JSONType, rawJSON, minJSON}, + {media.Builtin.JavascriptType, rawJS, minJS}, // JS Regex minifiers - {media.Type{MainType: "application", SubType: "ecmascript"}, rawJS, minJS}, - {media.Type{MainType: "application", SubType: "javascript"}, rawJS, minJS}, - {media.Type{MainType: "application", SubType: "x-javascript"}, rawJS, minJS}, - {media.Type{MainType: "application", SubType: "x-ecmascript"}, rawJS, minJS}, - {media.Type{MainType: "text", SubType: "ecmascript"}, rawJS, minJS}, - {media.Type{MainType: "text", SubType: "javascript"}, rawJS, minJS}, - {media.Type{MainType: "text", SubType: "x-javascript"}, rawJS, minJS}, - {media.Type{MainType: "text", SubType: "x-ecmascript"}, rawJS, minJS}, + {media.Type{Type: "application/ecmascript", MainType: "application", SubType: "ecmascript"}, rawJS, minJS}, + {media.Type{Type: "application/javascript", MainType: "application", SubType: "javascript"}, rawJS, minJS}, + {media.Type{Type: "application/x-javascript", MainType: "application", SubType: "x-javascript"}, rawJS, minJS}, + {media.Type{Type: "application/x-ecmascript", MainType: "application", SubType: "x-ecmascript"}, rawJS, minJS}, + {media.Type{Type: "text/ecmascript", MainType: "text", SubType: "ecmascript"}, rawJS, minJS}, + {media.Type{Type: "application/javascript", MainType: "text", SubType: "javascript"}, rawJS, minJS}, // JSON Regex minifiers - {media.Type{MainType: "application", SubType: "json"}, rawJSON, minJSON}, - {media.Type{MainType: "application", SubType: "x-json"}, rawJSON, minJSON}, - {media.Type{MainType: "application", SubType: "ld+json"}, rawJSON, minJSON}, - {media.Type{MainType: "text", SubType: "json"}, rawJSON, minJSON}, - {media.Type{MainType: "text", SubType: "x-json"}, rawJSON, minJSON}, - {media.Type{MainType: "text", SubType: "ld+json"}, rawJSON, minJSON}, + {media.Type{Type: "application/json", MainType: "application", SubType: "json"}, rawJSON, minJSON}, + {media.Type{Type: "application/x-json", MainType: "application", SubType: "x-json"}, rawJSON, minJSON}, + {media.Type{Type: "application/ld+json", MainType: "application", SubType: "ld+json"}, rawJSON, minJSON}, + {media.Type{Type: "application/json", MainType: "text", SubType: "json"}, rawJSON, minJSON}, } { var b bytes.Buffer @@ -93,9 +89,9 @@ func TestConfigureMinify(t *testing.T) { expectedMinString string errorExpected bool }{ - {media.HTMLType, " Hugo! ", " Hugo! ", false}, // configured minifier - {media.CSSType, " body { color: blue; } ", "body{color:blue}", false}, // default minifier - {media.XMLType, " Hugo! ", " Hugo! ", false}, // disable Xml minification + {media.Builtin.HTMLType, " Hugo! ", " Hugo! ", false}, // configured minifier + {media.Builtin.CSSType, " body { color: blue; } ", "body{color:blue}", false}, // default minifier + {media.Builtin.XMLType, " Hugo! ", " Hugo! ", false}, // disable Xml minification } { var b bytes.Buffer if !test.errorExpected { @@ -140,7 +136,7 @@ func TestJSONRoundTrip(t *testing.T) { m1 := make(map[string]any) m2 := make(map[string]any) c.Assert(json.Unmarshal([]byte(test), &m1), qt.IsNil) - c.Assert(m.Minify(media.JSONType, &b, strings.NewReader(test)), qt.IsNil) + c.Assert(m.Minify(media.Builtin.JSONType, &b, strings.NewReader(test)), qt.IsNil) c.Assert(json.Unmarshal(b.Bytes(), &m2), qt.IsNil) c.Assert(m1, qt.DeepEquals, m2) } @@ -157,9 +153,9 @@ func TestBugs(t *testing.T) { expectedMinString string }{ // https://github.com/gohugoio/hugo/issues/5506 - {media.CSSType, " body { color: rgba(000, 000, 000, 0.7); }", "body{color:rgba(0,0,0,.7)}"}, + {media.Builtin.CSSType, " body { color: rgba(000, 000, 000, 0.7); }", "body{color:rgba(0,0,0,.7)}"}, // https://github.com/gohugoio/hugo/issues/8332 - {media.HTMLType, " Tags", ` Tags`}, + {media.Builtin.HTMLType, " Tags", ` Tags`}, } { var b bytes.Buffer @@ -184,7 +180,7 @@ func TestDecodeConfigDecimalIsNowPrecision(t *testing.T) { }, }) - conf, err := decodeConfig(v) + conf, err := DecodeConfig(v) c.Assert(err, qt.IsNil) c.Assert(conf.Tdewolff.CSS.Precision, qt.Equals, 3) @@ -203,7 +199,7 @@ func TestDecodeConfigKeepWhitespace(t *testing.T) { }, }) - conf, err := decodeConfig(v) + conf, err := DecodeConfig(v) c.Assert(err, qt.IsNil) c.Assert(conf.Tdewolff.HTML, qt.DeepEquals, diff --git a/modules/config.go b/modules/config.go index 9d516e841cc..ffd40fa521f 100644 --- a/modules/config.go +++ b/modules/config.go @@ -283,7 +283,10 @@ func decodeConfig(cfg config.Provider, pathReplacements map[string]string) (Conf // Config holds a module config. type Config struct { - Mounts []Mount + // File system mounts. + Mounts []Mount + + // Module imports. Imports []Import // Meta info about this module (license information etc.). @@ -292,8 +295,7 @@ type Config struct { // Will be validated against the running Hugo version. HugoVersion HugoVersion - // A optional Glob pattern matching module paths to skip when vendoring, e.g. - // "github.com/**". + // Optional Glob pattern matching module paths to skip when vendoring, e.g. “github.com/**” NoVendor string // When enabled, we will pick the vendored module closest to the module @@ -303,21 +305,31 @@ type Config struct { // so once it is in use it cannot be redefined. VendorClosest bool + // A comma separated (or a slice) list of module path to directory replacement mapping, + // e.g. github.com/bep/my-theme -> ../..,github.com/bep/shortcodes -> /some/path. + // This is mostly useful for temporary locally development of a module, and then it makes sense to set it as an + // OS environment variable, e.g: env HUGO_MODULE_REPLACEMENTS="github.com/bep/my-theme -> ../..". + // Any relative path is relate to themesDir, and absolute paths are allowed. Replacements []string replacementsMap map[string]string - // Configures GOPROXY. + // Defines the proxy server to use to download remote modules. Default is direct, which means “git clone” and similar. + // Configures GOPROXY when running the Go command for module operations. Proxy string - // Configures GONOPROXY. + + // Comma separated glob list matching paths that should not use the proxy configured above. + // Configures GONOPROXY when running the Go command for module operations. NoProxy string - // Configures GOPRIVATE. + + // Comma separated glob list matching paths that should be treated as private. + // Configures GOPRIVATE when running the Go command for module operations. Private string // Defaults to "off". // Set to a work file, e.g. hugo.work, to enable Go "Workspace" mode. // Can be relative to the working directory or absolute. - // Requires Go 1.18+ - // See https://tip.golang.org/doc/go1.18 + // Requires Go 1.18+. + // Note that this can also be set via OS env, e.g. export HUGO_MODULE_WORKSPACE=/my/hugo.work. Workspace string } @@ -387,21 +399,33 @@ func (v HugoVersion) IsValid() bool { } type Import struct { - Path string // Module path - pathProjectReplaced bool // Set when Path is replaced in project config. - IgnoreConfig bool // Ignore any config in config.toml (will still follow imports). - IgnoreImports bool // Do not follow any configured imports. - NoMounts bool // Do not mount any folder in this import. - NoVendor bool // Never vendor this import (only allowed in main project). - Disable bool // Turn off this module. - Mounts []Mount + // Module path + Path string + // Set when Path is replaced in project config. + pathProjectReplaced bool + // Ignore any config in config.toml (will still follow imports). + IgnoreConfig bool + // Do not follow any configured imports. + IgnoreImports bool + // Do not mount any folder in this import. + NoMounts bool + // Never vendor this import (only allowed in main project). + NoVendor bool + // Turn off this module. + Disable bool + // File mounts. + Mounts []Mount } type Mount struct { - Source string // relative path in source repo, e.g. "scss" - Target string // relative target path, e.g. "assets/bootstrap/scss" + // Relative path in source repo, e.g. "scss". + Source string + + // Relative target path, e.g. "assets/bootstrap/scss". + Target string - Lang string // any language code associated with this mount. + // Any file in this mount will be associated with this language. + Lang string // Include only files matching the given Glob patterns (string or slice). IncludeFiles any diff --git a/navigation/menu.go b/navigation/menu.go index cb280823cbb..74f954a10bf 100644 --- a/navigation/menu.go +++ b/navigation/menu.go @@ -23,6 +23,7 @@ import ( "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/compare" + "github.com/gohugoio/hugo/config" "github.com/spf13/cast" ) @@ -180,10 +181,9 @@ func (m *MenuEntry) MarshallMap(ime map[string]any) error { case "parent": m.Parent = cast.ToString(v) case "params": - var ok bool - m.Params, ok = maps.ToParamsAndPrepare(v) - if !ok { - err = fmt.Errorf("cannot convert %T to Params", v) + m.Params, err = maps.ToParamsAndPrepare(v) + if err != nil { + err = fmt.Errorf("cannot convert %T to Params: %s", v, err) } } } @@ -314,3 +314,61 @@ func (m *MenuEntry) Title() string { return "" } + +// MenuConfig holds the configuration for a menu. +type MenuConfig struct { + Identifier string + Name string + Pre string + URL string + Weight int + // User defined params. + Params maps.Params +} + +func DecodeConfig(in any) (*config.ConfigNamespace[[]MenuConfig, Menus], error) { + buildConfig := func(in any) (Menus, any, error) { + ret := Menus{} + + if in == nil { + return ret, []map[string]any{}, nil + } + + menus, err := maps.ToStringMapE(in) + if err != nil { + return ret, nil, err + } + + for name, menu := range menus { + m, err := cast.ToSliceE(menu) + if err != nil { + return ret, nil, err + } else { + + for _, entry := range m { + menuEntry := MenuEntry{Menu: name} + ime, err := maps.ToStringMapE(entry) + if err != nil { + return ret, nil, err + } + err = menuEntry.MarshallMap(ime) + if err != nil { + return ret, nil, err + } + + // TODO1 menuEntry.ConfiguredURL = s.Info.createNodeMenuEntryURL(menuEntry.ConfiguredURL) + + if ret[name] == nil { + ret[name] = Menu{} + } + ret[name] = ret[name].Add(&menuEntry) + } + } + } + + return ret, nil, nil + + } + + return config.DecodeNamespace[[]MenuConfig](in, buildConfig) +} diff --git a/output/config.go b/output/config.go new file mode 100644 index 00000000000..9481d996c01 --- /dev/null +++ b/output/config.go @@ -0,0 +1,191 @@ +// Copyright 2023 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 output + +import ( + "fmt" + "reflect" + "sort" + "strings" + + "github.com/gohugoio/hugo/common/maps" + "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/media" + "github.com/mitchellh/mapstructure" +) + +// OutputFormatConfig configures a single output format. +type OutputFormatConfig struct { + // The MediaType string. This must be a configured media type. + MediaType string + Format +} + +func DecodeConfig(mediaTypes media.Types, in any) (*config.ConfigNamespace[map[string]OutputFormatConfig, Formats], error) { + buildConfig := func(in any) (Formats, any, error) { + f := make(Formats, len(DefaultFormats)) + copy(f, DefaultFormats) + if in != nil { + m, err := maps.ToStringMapE(in) + if err != nil { + return nil, nil, err + } + + for k, v := range m { + found := false + for i, vv := range f { + // Both are lower case. + if k == vv.Name { + // Merge it with the existing + if err := decode(mediaTypes, v, &f[i]); err != nil { + return f, nil, err + } + found = true + } + } + if found { + continue + } + + var newOutFormat Format + newOutFormat.Name = k + if err := decode(mediaTypes, v, &newOutFormat); err != nil { + return f, nil, err + } + + // We need values for these + if newOutFormat.BaseName == "" { + newOutFormat.BaseName = "index" + } + if newOutFormat.Rel == "" { + newOutFormat.Rel = "alternate" + } + + f = append(f, newOutFormat) + + } + } + + // Also format is a map for documentation purposes. + docm := make(map[string]OutputFormatConfig, len(f)) + for _, ff := range f { + docm[ff.Name] = OutputFormatConfig{ + MediaType: ff.MediaType.Type, + Format: ff, + } + } + + sort.Sort(f) + return f, docm, nil + } + + return config.DecodeNamespace[map[string]OutputFormatConfig](in, buildConfig) +} + +// DecodeFormats takes a list of output format configurations and merges those, +// in the order given, with the Hugo defaults as the last resort. +// TODO1 remove this. +func DecodeFormats(mediaTypes media.Types, maps ...map[string]any) (Formats, error) { + f := make(Formats, len(DefaultFormats)) + copy(f, DefaultFormats) + + for _, m := range maps { + for k, v := range m { + found := false + for i, vv := range f { + if strings.EqualFold(k, vv.Name) { + // Merge it with the existing + if err := decode(mediaTypes, v, &f[i]); err != nil { + return f, err + } + found = true + } + } + if !found { + var newOutFormat Format + newOutFormat.Name = k + if err := decode(mediaTypes, v, &newOutFormat); err != nil { + return f, err + } + + // We need values for these + if newOutFormat.BaseName == "" { + newOutFormat.BaseName = "index" + } + if newOutFormat.Rel == "" { + newOutFormat.Rel = "alternate" + } + + f = append(f, newOutFormat) + + } + } + } + + sort.Sort(f) + + return f, nil +} + +func decode(mediaTypes media.Types, input any, output *Format) error { + config := &mapstructure.DecoderConfig{ + Metadata: nil, + Result: output, + WeaklyTypedInput: true, + DecodeHook: func(a reflect.Type, b reflect.Type, c any) (any, error) { + if a.Kind() == reflect.Map { + dataVal := reflect.Indirect(reflect.ValueOf(c)) + for _, key := range dataVal.MapKeys() { + keyStr, ok := key.Interface().(string) + if !ok { + // Not a string key + continue + } + if strings.EqualFold(keyStr, "mediaType") { + // If mediaType is a string, look it up and replace it + // in the map. + vv := dataVal.MapIndex(key) + vvi := vv.Interface() + + switch vviv := vvi.(type) { + case media.Type: + // OK + case string: + mediaType, found := mediaTypes.GetByType(vviv) + if !found { + return c, fmt.Errorf("media type %q not found", vviv) + } + dataVal.SetMapIndex(key, reflect.ValueOf(mediaType)) + default: + return nil, fmt.Errorf("invalid output format configuration; wrong type for media type, expected string (e.g. text/html), got %T", vvi) + } + } + } + } + return c, nil + }, + } + + decoder, err := mapstructure.NewDecoder(config) + if err != nil { + return err + } + + if err = decoder.Decode(input); err != nil { + return fmt.Errorf("failed to decode output format configuration: %w", err) + } + + return nil + +} diff --git a/output/config_test.go b/output/config_test.go new file mode 100644 index 00000000000..f408da0c30e --- /dev/null +++ b/output/config_test.go @@ -0,0 +1,128 @@ +// Copyright 2023 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 output + +import ( + "testing" + + qt "github.com/frankban/quicktest" + "github.com/gohugoio/hugo/media" +) + +func TestDecodeFormats(t *testing.T) { + c := qt.New(t) + + mediaTypes := media.Types{media.Builtin.JSONType, media.Builtin.XMLType} + + tests := []struct { + name string + maps []map[string]any + shouldError bool + assert func(t *testing.T, name string, f Formats) + }{ + { + "Redefine JSON", + []map[string]any{ + { + "JsON": map[string]any{ + "baseName": "myindex", + "isPlainText": "false", + }, + }, + }, + false, + func(t *testing.T, name string, f Formats) { + msg := qt.Commentf(name) + c.Assert(len(f), qt.Equals, len(DefaultFormats), msg) + json, _ := f.GetByName("JSON") + c.Assert(json.BaseName, qt.Equals, "myindex") + c.Assert(json.MediaType, qt.Equals, media.Builtin.JSONType) + c.Assert(json.IsPlainText, qt.Equals, false) + }, + }, + { + "Add XML format with string as mediatype", + []map[string]any{ + { + "MYXMLFORMAT": map[string]any{ + "baseName": "myxml", + "mediaType": "application/xml", + }, + }, + }, + false, + func(t *testing.T, name string, f Formats) { + c.Assert(len(f), qt.Equals, len(DefaultFormats)+1) + xml, found := f.GetByName("MYXMLFORMAT") + c.Assert(found, qt.Equals, true) + c.Assert(xml.BaseName, qt.Equals, "myxml") + c.Assert(xml.MediaType, qt.Equals, media.Builtin.XMLType) + + // Verify that we haven't changed the DefaultFormats slice. + json, _ := f.GetByName("JSON") + c.Assert(json.BaseName, qt.Equals, "index") + }, + }, + { + "Add format unknown mediatype", + []map[string]any{ + { + "MYINVALID": map[string]any{ + "baseName": "mymy", + "mediaType": "application/hugo", + }, + }, + }, + true, + func(t *testing.T, name string, f Formats) { + }, + }, + { + "Add and redefine XML format", + []map[string]any{ + { + "MYOTHERXMLFORMAT": map[string]any{ + "baseName": "myotherxml", + "mediaType": media.Builtin.XMLType, + }, + }, + { + "MYOTHERXMLFORMAT": map[string]any{ + "baseName": "myredefined", + }, + }, + }, + false, + func(t *testing.T, name string, f Formats) { + c.Assert(len(f), qt.Equals, len(DefaultFormats)+1) + xml, found := f.GetByName("MYOTHERXMLFORMAT") + c.Assert(found, qt.Equals, true) + c.Assert(xml.BaseName, qt.Equals, "myredefined") + c.Assert(xml.MediaType, qt.Equals, media.Builtin.XMLType) + }, + }, + } + + for _, test := range tests { + result, err := DecodeFormats(mediaTypes, test.maps...) + msg := qt.Commentf(test.name) + + if test.shouldError { + c.Assert(err, qt.Not(qt.IsNil), msg) + } else { + c.Assert(err, qt.IsNil, msg) + test.assert(t, test.name, result) + } + } +} diff --git a/output/docshelper.go b/output/docshelper.go index abfedd14820..fa8ed13428d 100644 --- a/output/docshelper.go +++ b/output/docshelper.go @@ -6,6 +6,7 @@ import ( // "fmt" "github.com/gohugoio/hugo/docshelper" + "github.com/gohugoio/hugo/output/layouts" ) // This is is just some helpers used to create some JSON used in the Hugo docs. @@ -39,44 +40,43 @@ func createLayoutExamples() any { for _, example := range []struct { name string - d LayoutDescriptor - f Format + d layouts.LayoutDescriptor }{ - // Taxonomy output.LayoutDescriptor={categories category taxonomy en false Type Section - {"Single page in \"posts\" section", LayoutDescriptor{Kind: "page", Type: "posts"}, HTMLFormat}, - {"Base template for single page in \"posts\" section", LayoutDescriptor{Baseof: true, Kind: "page", Type: "posts"}, HTMLFormat}, - {"Single page in \"posts\" section with layout set", LayoutDescriptor{Kind: "page", Type: "posts", Layout: demoLayout}, HTMLFormat}, - {"Base template for single page in \"posts\" section with layout set", LayoutDescriptor{Baseof: true, Kind: "page", Type: "posts", Layout: demoLayout}, HTMLFormat}, - {"AMP single page", LayoutDescriptor{Kind: "page", Type: "posts"}, AMPFormat}, - {"AMP single page, French language", LayoutDescriptor{Kind: "page", Type: "posts", Lang: "fr"}, AMPFormat}, + // Taxonomy layouts.LayoutDescriptor={categories category taxonomy en false Type Section + {"Single page in \"posts\" section", layouts.LayoutDescriptor{Kind: "page", Type: "posts", OutputFormatName: "html", Suffix: "html"}}, + {"Base template for single page in \"posts\" section", layouts.LayoutDescriptor{Baseof: true, Kind: "page", Type: "posts", OutputFormatName: "html", Suffix: "html"}}, + {"Single page in \"posts\" section with layout set", layouts.LayoutDescriptor{Kind: "page", Type: "posts", Layout: demoLayout, OutputFormatName: "html", Suffix: "html"}}, + {"Base template for single page in \"posts\" section with layout set", layouts.LayoutDescriptor{Baseof: true, Kind: "page", Type: "posts", Layout: demoLayout, OutputFormatName: "html", Suffix: "html"}}, + {"AMP single page", layouts.LayoutDescriptor{Kind: "page", Type: "posts", OutputFormatName: "amp", Suffix: "html"}}, + {"AMP single page, French language", layouts.LayoutDescriptor{Kind: "page", Type: "posts", Lang: "fr", OutputFormatName: "html", Suffix: "html"}}, // All section or typeless pages gets "page" as type - {"Home page", LayoutDescriptor{Kind: "home", Type: "page"}, HTMLFormat}, - {"Base template for home page", LayoutDescriptor{Baseof: true, Kind: "home", Type: "page"}, HTMLFormat}, - {"Home page with type set", LayoutDescriptor{Kind: "home", Type: demoType}, HTMLFormat}, - {"Base template for home page with type set", LayoutDescriptor{Baseof: true, Kind: "home", Type: demoType}, HTMLFormat}, - {"Home page with layout set", LayoutDescriptor{Kind: "home", Type: "page", Layout: demoLayout}, HTMLFormat}, - {"AMP home, French language", LayoutDescriptor{Kind: "home", Type: "page", Lang: "fr"}, AMPFormat}, - {"JSON home", LayoutDescriptor{Kind: "home", Type: "page"}, JSONFormat}, - {"RSS home", LayoutDescriptor{Kind: "home", Type: "page"}, RSSFormat}, - {"RSS section posts", LayoutDescriptor{Kind: "section", Type: "posts"}, RSSFormat}, - {"Taxonomy in categories", LayoutDescriptor{Kind: "taxonomy", Type: "categories", Section: "category"}, RSSFormat}, - {"Term in categories", LayoutDescriptor{Kind: "term", Type: "categories", Section: "category"}, RSSFormat}, - {"Section list for \"posts\" section", LayoutDescriptor{Kind: "section", Type: "posts", Section: "posts"}, HTMLFormat}, - {"Section list for \"posts\" section with type set to \"blog\"", LayoutDescriptor{Kind: "section", Type: "blog", Section: "posts"}, HTMLFormat}, - {"Section list for \"posts\" section with layout set to \"demoLayout\"", LayoutDescriptor{Kind: "section", Layout: demoLayout, Section: "posts"}, HTMLFormat}, + {"Home page", layouts.LayoutDescriptor{Kind: "home", Type: "page", OutputFormatName: "html", Suffix: "html"}}, + {"Base template for home page", layouts.LayoutDescriptor{Baseof: true, Kind: "home", Type: "page", OutputFormatName: "html", Suffix: "html"}}, + {"Home page with type set", layouts.LayoutDescriptor{Kind: "home", Type: demoType, OutputFormatName: "html", Suffix: "html"}}, + {"Base template for home page with type set", layouts.LayoutDescriptor{Baseof: true, Kind: "home", Type: demoType, OutputFormatName: "html", Suffix: "html"}}, + {"Home page with layout set", layouts.LayoutDescriptor{Kind: "home", Type: "page", Layout: demoLayout, OutputFormatName: "html", Suffix: "html"}}, + {"AMP home, French language", layouts.LayoutDescriptor{Kind: "home", Type: "page", Lang: "fr", OutputFormatName: "amp", Suffix: "html"}}, + {"JSON home", layouts.LayoutDescriptor{Kind: "home", Type: "page", OutputFormatName: "json", Suffix: "json"}}, + {"RSS home", layouts.LayoutDescriptor{Kind: "home", Type: "page", OutputFormatName: "rss", Suffix: "rss"}}, + {"RSS section posts", layouts.LayoutDescriptor{Kind: "section", Type: "posts", OutputFormatName: "rss", Suffix: "rss"}}, + {"Taxonomy in categories", layouts.LayoutDescriptor{Kind: "taxonomy", Type: "categories", Section: "category", OutputFormatName: "rss", Suffix: "rss"}}, + {"Term in categories", layouts.LayoutDescriptor{Kind: "term", Type: "categories", Section: "category", OutputFormatName: "rss", Suffix: "rss"}}, + {"Section list for \"posts\" section", layouts.LayoutDescriptor{Kind: "section", Type: "posts", Section: "posts", OutputFormatName: "html", Suffix: "html"}}, + {"Section list for \"posts\" section with type set to \"blog\"", layouts.LayoutDescriptor{Kind: "section", Type: "blog", Section: "posts", OutputFormatName: "html", Suffix: "html"}}, + {"Section list for \"posts\" section with layout set to \"demoLayout\"", layouts.LayoutDescriptor{Kind: "section", Layout: demoLayout, Section: "posts", OutputFormatName: "html", Suffix: "html"}}, - {"Taxonomy list in categories", LayoutDescriptor{Kind: "taxonomy", Type: "categories", Section: "category"}, HTMLFormat}, - {"Taxonomy term in categories", LayoutDescriptor{Kind: "term", Type: "categories", Section: "category"}, HTMLFormat}, + {"Taxonomy list in categories", layouts.LayoutDescriptor{Kind: "taxonomy", Type: "categories", Section: "category", OutputFormatName: "html", Suffix: "html"}}, + {"Taxonomy term in categories", layouts.LayoutDescriptor{Kind: "term", Type: "categories", Section: "category", OutputFormatName: "html", Suffix: "html"}}, } { - l := NewLayoutHandler() - layouts, _ := l.For(example.d, example.f) + l := layouts.NewLayoutHandler() + layouts, _ := l.For(example.d) basicExamples = append(basicExamples, Example{ Example: example.name, Kind: example.d.Kind, - OutputFormat: example.f.Name, - Suffix: example.f.MediaType.FirstSuffix.Suffix, + OutputFormat: example.d.OutputFormatName, + Suffix: example.d.Suffix, Layouts: makeLayoutsPresentable(layouts), }) } diff --git a/output/layout.go b/output/layouts/layout.go similarity index 85% rename from output/layout.go rename to output/layouts/layout.go index dcbdf461ac3..9c5ef17a121 100644 --- a/output/layout.go +++ b/output/layouts/layout.go @@ -1,4 +1,4 @@ -// Copyright 2017-present The Hugo Authors. All rights reserved. +// Copyright 2023 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,13 +11,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -package output +package layouts import ( "strings" "sync" - - "github.com/gohugoio/hugo/helpers" ) // These may be used as content sections with potential conflicts. Avoid that. @@ -43,6 +41,10 @@ type LayoutDescriptor struct { // LayoutOverride indicates what we should only look for the above layout. LayoutOverride bool + // From OutputFormat and MediaType. + OutputFormatName string + Suffix string + RenderingHook bool Baseof bool } @@ -54,37 +56,31 @@ func (d LayoutDescriptor) isList() bool { // LayoutHandler calculates the layout template to use to render a given output type. type LayoutHandler struct { mu sync.RWMutex - cache map[layoutCacheKey][]string -} - -type layoutCacheKey struct { - d LayoutDescriptor - f string + cache map[LayoutDescriptor][]string } // NewLayoutHandler creates a new LayoutHandler. func NewLayoutHandler() *LayoutHandler { - return &LayoutHandler{cache: make(map[layoutCacheKey][]string)} + return &LayoutHandler{cache: make(map[LayoutDescriptor][]string)} } // For returns a layout for the given LayoutDescriptor and options. // Layouts are rendered and cached internally. -func (l *LayoutHandler) For(d LayoutDescriptor, f Format) ([]string, error) { +func (l *LayoutHandler) For(d LayoutDescriptor) ([]string, error) { // We will get lots of requests for the same layouts, so avoid recalculations. - key := layoutCacheKey{d, f.Name} l.mu.RLock() - if cacheVal, found := l.cache[key]; found { + if cacheVal, found := l.cache[d]; found { l.mu.RUnlock() return cacheVal, nil } l.mu.RUnlock() - layouts := resolvePageTemplate(d, f) + layouts := resolvePageTemplate(d) - layouts = helpers.UniqueStringsReuse(layouts) + layouts = uniqueStringsReuse(layouts) l.mu.Lock() - l.cache[key] = layouts + l.cache[d] = layouts l.mu.Unlock() return layouts, nil @@ -94,7 +90,7 @@ type layoutBuilder struct { layoutVariations []string typeVariations []string d LayoutDescriptor - f Format + //f Format } func (l *layoutBuilder) addLayoutVariations(vars ...string) { @@ -134,8 +130,8 @@ func (l *layoutBuilder) addKind() { const renderingHookRoot = "/_markup" -func resolvePageTemplate(d LayoutDescriptor, f Format) []string { - b := &layoutBuilder{d: d, f: f} +func resolvePageTemplate(d LayoutDescriptor) []string { + b := &layoutBuilder{d: d} if !d.RenderingHook && d.Layout != "" { b.addLayoutVariations(d.Layout) @@ -190,7 +186,7 @@ func resolvePageTemplate(d LayoutDescriptor, f Format) []string { b.addTypeVariations("") } - isRSS := f.Name == RSSFormat.Name + isRSS := strings.EqualFold(d.OutputFormatName, "rss") if !d.RenderingHook && !d.Baseof && isRSS { // The historic and common rss.xml case b.addLayoutVariations("") @@ -223,7 +219,7 @@ func (l *layoutBuilder) resolveVariations() []string { var layouts []string var variations []string - name := strings.ToLower(l.f.Name) + name := strings.ToLower(l.d.OutputFormatName) if l.d.Lang != "" { // We prefer the most specific type before language. @@ -241,7 +237,7 @@ func (l *layoutBuilder) resolveVariations() []string { continue } - s := constructLayoutPath(typeVar, layoutVar, variation, l.f.MediaType.FirstSuffix.Suffix) + s := constructLayoutPath(typeVar, layoutVar, variation, l.d.Suffix) if s != "" { layouts = append(layouts, s) } @@ -300,3 +296,23 @@ func constructLayoutPath(typ, layout, variations, extension string) string { return p.String() } + +// Inline this here so we can use tinygo to compile a wasm binary of this package. +func uniqueStringsReuse(s []string) []string { + result := s[:0] + for i, val := range s { + var seen bool + + for j := 0; j < i; j++ { + if s[j] == val { + seen = true + break + } + } + + if !seen { + result = append(result, val) + } + } + return result +} diff --git a/output/layout_test.go b/output/layouts/layout_test.go similarity index 88% rename from output/layout_test.go rename to output/layouts/layout_test.go index 8b7a2b541bd..2f340f238ff 100644 --- a/output/layout_test.go +++ b/output/layouts/layout_test.go @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package output +package layouts import ( "fmt" @@ -19,8 +19,6 @@ import ( "strings" "testing" - "github.com/gohugoio/hugo/media" - qt "github.com/frankban/quicktest" "github.com/kylelemons/godebug/diff" ) @@ -28,42 +26,16 @@ import ( func TestLayout(t *testing.T) { c := qt.New(t) - noExtNoDelimMediaType := media.WithDelimiterAndSuffixes(media.TextType, "", "") - noExtMediaType := media.WithDelimiterAndSuffixes(media.TextType, ".", "") - - var ( - ampType = Format{ - Name: "AMP", - MediaType: media.HTMLType, - BaseName: "index", - } - - htmlFormat = HTMLFormat - - noExtDelimFormat = Format{ - Name: "NEM", - MediaType: noExtNoDelimMediaType, - BaseName: "_redirects", - } - - noExt = Format{ - Name: "NEX", - MediaType: noExtMediaType, - BaseName: "next", - } - ) - for _, this := range []struct { name string layoutDescriptor LayoutDescriptor layoutOverride string - format Format expect []string }{ { "Home", - LayoutDescriptor{Kind: "home"}, - "", ampType, + LayoutDescriptor{Kind: "home", OutputFormatName: "amp", Suffix: "html"}, + "", []string{ "index.amp.html", "home.amp.html", @@ -81,8 +53,8 @@ func TestLayout(t *testing.T) { }, { "Home baseof", - LayoutDescriptor{Kind: "home", Baseof: true}, - "", ampType, + LayoutDescriptor{Kind: "home", Baseof: true, OutputFormatName: "amp", Suffix: "html"}, + "", []string{ "index-baseof.amp.html", "home-baseof.amp.html", @@ -104,8 +76,8 @@ func TestLayout(t *testing.T) { }, { "Home, HTML", - LayoutDescriptor{Kind: "home"}, - "", htmlFormat, + LayoutDescriptor{Kind: "home", OutputFormatName: "html", Suffix: "html"}, + "", // We will eventually get to index.html. This looks stuttery, but makes the lookup logic easy to understand. []string{ "index.html.html", @@ -124,8 +96,8 @@ func TestLayout(t *testing.T) { }, { "Home, HTML, baseof", - LayoutDescriptor{Kind: "home", Baseof: true}, - "", htmlFormat, + LayoutDescriptor{Kind: "home", Baseof: true, OutputFormatName: "html", Suffix: "html"}, + "", []string{ "index-baseof.html.html", "home-baseof.html.html", @@ -147,8 +119,8 @@ func TestLayout(t *testing.T) { }, { "Home, french language", - LayoutDescriptor{Kind: "home", Lang: "fr"}, - "", ampType, + LayoutDescriptor{Kind: "home", Lang: "fr", OutputFormatName: "amp", Suffix: "html"}, + "", []string{ "index.fr.amp.html", "home.fr.amp.html", @@ -178,8 +150,8 @@ func TestLayout(t *testing.T) { }, { "Home, no ext or delim", - LayoutDescriptor{Kind: "home"}, - "", noExtDelimFormat, + LayoutDescriptor{Kind: "home", OutputFormatName: "nem", Suffix: ""}, + "", []string{ "index.nem", "home.nem", @@ -191,8 +163,8 @@ func TestLayout(t *testing.T) { }, { "Home, no ext", - LayoutDescriptor{Kind: "home"}, - "", noExt, + LayoutDescriptor{Kind: "home", OutputFormatName: "nex", Suffix: ""}, + "", []string{ "index.nex", "home.nex", @@ -204,14 +176,14 @@ func TestLayout(t *testing.T) { }, { "Page, no ext or delim", - LayoutDescriptor{Kind: "page"}, - "", noExtDelimFormat, + LayoutDescriptor{Kind: "page", OutputFormatName: "nem", Suffix: ""}, + "", []string{"_default/single.nem"}, }, { "Section", - LayoutDescriptor{Kind: "section", Section: "sect1"}, - "", ampType, + LayoutDescriptor{Kind: "section", Section: "sect1", OutputFormatName: "amp", Suffix: "html"}, + "", []string{ "sect1/sect1.amp.html", "sect1/section.amp.html", @@ -235,8 +207,8 @@ func TestLayout(t *testing.T) { }, { "Section, baseof", - LayoutDescriptor{Kind: "section", Section: "sect1", Baseof: true}, - "", ampType, + LayoutDescriptor{Kind: "section", Section: "sect1", Baseof: true, OutputFormatName: "amp", Suffix: "html"}, + "", []string{ "sect1/sect1-baseof.amp.html", "sect1/section-baseof.amp.html", @@ -266,8 +238,8 @@ func TestLayout(t *testing.T) { }, { "Section, baseof, French, AMP", - LayoutDescriptor{Kind: "section", Section: "sect1", Lang: "fr", Baseof: true}, - "", ampType, + LayoutDescriptor{Kind: "section", Section: "sect1", Lang: "fr", Baseof: true, OutputFormatName: "amp", Suffix: "html"}, + "", []string{ "sect1/sect1-baseof.fr.amp.html", "sect1/section-baseof.fr.amp.html", @@ -321,8 +293,8 @@ func TestLayout(t *testing.T) { }, { "Section with layout", - LayoutDescriptor{Kind: "section", Section: "sect1", Layout: "mylayout"}, - "", ampType, + LayoutDescriptor{Kind: "section", Section: "sect1", Layout: "mylayout", OutputFormatName: "amp", Suffix: "html"}, + "", []string{ "sect1/mylayout.amp.html", "sect1/sect1.amp.html", @@ -352,8 +324,8 @@ func TestLayout(t *testing.T) { }, { "Term, French, AMP", - LayoutDescriptor{Kind: "term", Section: "tags", Lang: "fr"}, - "", ampType, + LayoutDescriptor{Kind: "term", Section: "tags", Lang: "fr", OutputFormatName: "amp", Suffix: "html"}, + "", []string{ "term/term.fr.amp.html", "term/tags.fr.amp.html", @@ -423,8 +395,8 @@ func TestLayout(t *testing.T) { }, { "Term, baseof, French, AMP", - LayoutDescriptor{Kind: "term", Section: "tags", Lang: "fr", Baseof: true}, - "", ampType, + LayoutDescriptor{Kind: "term", Section: "tags", Lang: "fr", Baseof: true, OutputFormatName: "amp", Suffix: "html"}, + "", []string{ "term/term-baseof.fr.amp.html", "term/tags-baseof.fr.amp.html", @@ -510,8 +482,8 @@ func TestLayout(t *testing.T) { }, { "Term", - LayoutDescriptor{Kind: "term", Section: "tags"}, - "", ampType, + LayoutDescriptor{Kind: "term", Section: "tags", OutputFormatName: "amp", Suffix: "html"}, + "", []string{ "term/term.amp.html", "term/tags.amp.html", @@ -549,8 +521,8 @@ func TestLayout(t *testing.T) { }, { "Taxonomy", - LayoutDescriptor{Kind: "taxonomy", Section: "categories"}, - "", ampType, + LayoutDescriptor{Kind: "taxonomy", Section: "categories", OutputFormatName: "amp", Suffix: "html"}, + "", []string{ "categories/categories.terms.amp.html", "categories/terms.amp.html", @@ -580,8 +552,8 @@ func TestLayout(t *testing.T) { }, { "Page", - LayoutDescriptor{Kind: "page"}, - "", ampType, + LayoutDescriptor{Kind: "page", OutputFormatName: "amp", Suffix: "html"}, + "", []string{ "_default/single.amp.html", "_default/single.html", @@ -589,8 +561,8 @@ func TestLayout(t *testing.T) { }, { "Page, baseof", - LayoutDescriptor{Kind: "page", Baseof: true}, - "", ampType, + LayoutDescriptor{Kind: "page", Baseof: true, OutputFormatName: "amp", Suffix: "html"}, + "", []string{ "_default/single-baseof.amp.html", "_default/baseof.amp.html", @@ -600,8 +572,8 @@ func TestLayout(t *testing.T) { }, { "Page with layout", - LayoutDescriptor{Kind: "page", Layout: "mylayout"}, - "", ampType, + LayoutDescriptor{Kind: "page", Layout: "mylayout", OutputFormatName: "amp", Suffix: "html"}, + "", []string{ "_default/mylayout.amp.html", "_default/single.amp.html", @@ -611,8 +583,8 @@ func TestLayout(t *testing.T) { }, { "Page with layout, baseof", - LayoutDescriptor{Kind: "page", Layout: "mylayout", Baseof: true}, - "", ampType, + LayoutDescriptor{Kind: "page", Layout: "mylayout", Baseof: true, OutputFormatName: "amp", Suffix: "html"}, + "", []string{ "_default/mylayout-baseof.amp.html", "_default/single-baseof.amp.html", @@ -624,8 +596,8 @@ func TestLayout(t *testing.T) { }, { "Page with layout and type", - LayoutDescriptor{Kind: "page", Layout: "mylayout", Type: "myttype"}, - "", ampType, + LayoutDescriptor{Kind: "page", Layout: "mylayout", Type: "myttype", OutputFormatName: "amp", Suffix: "html"}, + "", []string{ "myttype/mylayout.amp.html", "myttype/single.amp.html", @@ -639,8 +611,8 @@ func TestLayout(t *testing.T) { }, { "Page baseof with layout and type", - LayoutDescriptor{Kind: "page", Layout: "mylayout", Type: "myttype", Baseof: true}, - "", ampType, + LayoutDescriptor{Kind: "page", Layout: "mylayout", Type: "myttype", Baseof: true, OutputFormatName: "amp", Suffix: "html"}, + "", []string{ "myttype/mylayout-baseof.amp.html", "myttype/single-baseof.amp.html", @@ -658,8 +630,8 @@ func TestLayout(t *testing.T) { }, { "Page baseof with layout and type in French", - LayoutDescriptor{Kind: "page", Layout: "mylayout", Type: "myttype", Lang: "fr", Baseof: true}, - "", ampType, + LayoutDescriptor{Kind: "page", Layout: "mylayout", Type: "myttype", Lang: "fr", Baseof: true, OutputFormatName: "amp", Suffix: "html"}, + "", []string{ "myttype/mylayout-baseof.fr.amp.html", "myttype/single-baseof.fr.amp.html", @@ -689,8 +661,8 @@ func TestLayout(t *testing.T) { }, { "Page with layout and type with subtype", - LayoutDescriptor{Kind: "page", Layout: "mylayout", Type: "myttype/mysubtype"}, - "", ampType, + LayoutDescriptor{Kind: "page", Layout: "mylayout", Type: "myttype/mysubtype", OutputFormatName: "amp", Suffix: "html"}, + "", []string{ "myttype/mysubtype/mylayout.amp.html", "myttype/mysubtype/single.amp.html", @@ -705,8 +677,8 @@ func TestLayout(t *testing.T) { // RSS { "RSS Home", - LayoutDescriptor{Kind: "home"}, - "", RSSFormat, + LayoutDescriptor{Kind: "home", OutputFormatName: "rss", Suffix: "xml"}, + "", []string{ "index.rss.xml", "home.rss.xml", @@ -727,8 +699,8 @@ func TestLayout(t *testing.T) { }, { "RSS Home, baseof", - LayoutDescriptor{Kind: "home", Baseof: true}, - "", RSSFormat, + LayoutDescriptor{Kind: "home", Baseof: true, OutputFormatName: "rss", Suffix: "xml"}, + "", []string{ "index-baseof.rss.xml", "home-baseof.rss.xml", @@ -750,8 +722,8 @@ func TestLayout(t *testing.T) { }, { "RSS Section", - LayoutDescriptor{Kind: "section", Section: "sect1"}, - "", RSSFormat, + LayoutDescriptor{Kind: "section", Section: "sect1", OutputFormatName: "rss", Suffix: "xml"}, + "", []string{ "sect1/sect1.rss.xml", "sect1/section.rss.xml", @@ -779,8 +751,8 @@ func TestLayout(t *testing.T) { }, { "RSS Term", - LayoutDescriptor{Kind: "term", Section: "tag"}, - "", RSSFormat, + LayoutDescriptor{Kind: "term", Section: "tag", OutputFormatName: "rss", Suffix: "xml"}, + "", []string{ "term/term.rss.xml", "term/tag.rss.xml", @@ -823,8 +795,8 @@ func TestLayout(t *testing.T) { }, { "RSS Taxonomy", - LayoutDescriptor{Kind: "taxonomy", Section: "tag"}, - "", RSSFormat, + LayoutDescriptor{Kind: "taxonomy", Section: "tag", OutputFormatName: "rss", Suffix: "xml"}, + "", []string{ "tag/tag.terms.rss.xml", "tag/terms.rss.xml", @@ -858,8 +830,8 @@ func TestLayout(t *testing.T) { }, { "Home plain text", - LayoutDescriptor{Kind: "home"}, - "", JSONFormat, + LayoutDescriptor{Kind: "home", OutputFormatName: "json", Suffix: "json"}, + "", []string{ "index.json.json", "home.json.json", @@ -877,8 +849,8 @@ func TestLayout(t *testing.T) { }, { "Page plain text", - LayoutDescriptor{Kind: "page"}, - "", JSONFormat, + LayoutDescriptor{Kind: "page", OutputFormatName: "json", Suffix: "json"}, + "", []string{ "_default/single.json.json", "_default/single.json", @@ -886,8 +858,8 @@ func TestLayout(t *testing.T) { }, { "Reserved section, shortcodes", - LayoutDescriptor{Kind: "section", Section: "shortcodes", Type: "shortcodes"}, - "", ampType, + LayoutDescriptor{Kind: "section", Section: "shortcodes", Type: "shortcodes", OutputFormatName: "amp", Suffix: "html"}, + "", []string{ "section/shortcodes.amp.html", "section/section.amp.html", @@ -905,8 +877,8 @@ func TestLayout(t *testing.T) { }, { "Reserved section, partials", - LayoutDescriptor{Kind: "section", Section: "partials", Type: "partials"}, - "", ampType, + LayoutDescriptor{Kind: "section", Section: "partials", Type: "partials", OutputFormatName: "amp", Suffix: "html"}, + "", []string{ "section/partials.amp.html", "section/section.amp.html", @@ -925,8 +897,8 @@ func TestLayout(t *testing.T) { // This is currently always HTML only { "404, HTML", - LayoutDescriptor{Kind: "404"}, - "", htmlFormat, + LayoutDescriptor{Kind: "404", OutputFormatName: "html", Suffix: "html"}, + "", []string{ "404.html.html", "404.html", @@ -934,8 +906,8 @@ func TestLayout(t *testing.T) { }, { "404, HTML baseof", - LayoutDescriptor{Kind: "404", Baseof: true}, - "", htmlFormat, + LayoutDescriptor{Kind: "404", Baseof: true, OutputFormatName: "html", Suffix: "html"}, + "", []string{ "404-baseof.html.html", "baseof.html.html", @@ -949,8 +921,8 @@ func TestLayout(t *testing.T) { }, { "Content hook", - LayoutDescriptor{Kind: "render-link", RenderingHook: true, Layout: "mylayout", Section: "blog"}, - "", ampType, + LayoutDescriptor{Kind: "render-link", RenderingHook: true, Layout: "mylayout", Section: "blog", OutputFormatName: "amp", Suffix: "html"}, + "", []string{ "blog/_markup/render-link.amp.html", "blog/_markup/render-link.html", @@ -962,7 +934,7 @@ func TestLayout(t *testing.T) { c.Run(this.name, func(c *qt.C) { l := NewLayoutHandler() - layouts, err := l.For(this.layoutDescriptor, this.format) + layouts, err := l.For(this.layoutDescriptor) c.Assert(err, qt.IsNil) c.Assert(layouts, qt.Not(qt.IsNil), qt.Commentf(this.layoutDescriptor.Kind)) @@ -981,8 +953,10 @@ func TestLayout(t *testing.T) { } }) } + } +/* func BenchmarkLayout(b *testing.B) { descriptor := LayoutDescriptor{Kind: "taxonomy", Section: "categories"} l := NewLayoutHandler() @@ -1006,3 +980,4 @@ func BenchmarkLayoutUncached(b *testing.B) { } } } +*/ diff --git a/output/outputFormat.go b/output/outputFormat.go index 0bc08e4905d..f602c03f36f 100644 --- a/output/outputFormat.go +++ b/output/outputFormat.go @@ -17,19 +17,18 @@ package output import ( "encoding/json" "fmt" - "reflect" "sort" "strings" - "github.com/mitchellh/mapstructure" - "github.com/gohugoio/hugo/media" ) // Format represents an output representation, usually to a file on disk. +// { "name": "OutputFormat" } type Format struct { - // The Name is used as an identifier. Internal output formats (i.e. HTML and RSS) + // The Name is used as an identifier. Internal output formats (i.e. html and rss) // can be overridden by providing a new definition for those types. + // { "identifiers": ["html", "rss"] } Name string `json:"name"` MediaType media.Type `json:"-"` @@ -40,14 +39,7 @@ type Format struct { // The base output file name used when not using "ugly URLs", defaults to "index". BaseName string `json:"baseName"` - // The value to use for rel links - // - // See https://www.w3schools.com/tags/att_link_rel.asp - // - // AMP has a special requirement in this department, see: - // https://www.ampproject.org/docs/guides/deploy/discovery - // I.e.: - // + // The value to use for rel links. Rel string `json:"rel"` // The protocol to use, i.e. "webcal://". Defaults to the protocol of the baseURL. @@ -86,8 +78,8 @@ type Format struct { // An ordered list of built-in output formats. var ( AMPFormat = Format{ - Name: "AMP", - MediaType: media.HTMLType, + Name: "amp", + MediaType: media.Builtin.HTMLType, BaseName: "index", Path: "amp", Rel: "amphtml", @@ -97,8 +89,8 @@ var ( } CalendarFormat = Format{ - Name: "Calendar", - MediaType: media.CalendarType, + Name: "calendar", + MediaType: media.Builtin.CalendarType, IsPlainText: true, Protocol: "webcal://", BaseName: "index", @@ -106,24 +98,24 @@ var ( } CSSFormat = Format{ - Name: "CSS", - MediaType: media.CSSType, + Name: "css", + MediaType: media.Builtin.CSSType, BaseName: "styles", IsPlainText: true, Rel: "stylesheet", NotAlternative: true, } CSVFormat = Format{ - Name: "CSV", - MediaType: media.CSVType, + Name: "csv", + MediaType: media.Builtin.CSVType, BaseName: "index", IsPlainText: true, Rel: "alternate", } HTMLFormat = Format{ - Name: "HTML", - MediaType: media.HTMLType, + Name: "html", + MediaType: media.Builtin.HTMLType, BaseName: "index", Rel: "canonical", IsHTML: true, @@ -135,24 +127,24 @@ var ( } MarkdownFormat = Format{ - Name: "MARKDOWN", - MediaType: media.MarkdownType, + Name: "markdown", + MediaType: media.Builtin.MarkdownType, BaseName: "index", Rel: "alternate", IsPlainText: true, } JSONFormat = Format{ - Name: "JSON", - MediaType: media.JSONType, + Name: "json", + MediaType: media.Builtin.JSONType, BaseName: "index", IsPlainText: true, Rel: "alternate", } WebAppManifestFormat = Format{ - Name: "WebAppManifest", - MediaType: media.WebAppManifestType, + Name: "webappmanifest", + MediaType: media.Builtin.WebAppManifestType, BaseName: "manifest", IsPlainText: true, NotAlternative: true, @@ -160,24 +152,24 @@ var ( } RobotsTxtFormat = Format{ - Name: "ROBOTS", - MediaType: media.TextType, + Name: "robots", + MediaType: media.Builtin.TextType, BaseName: "robots", IsPlainText: true, Rel: "alternate", } RSSFormat = Format{ - Name: "RSS", - MediaType: media.RSSType, + Name: "rss", + MediaType: media.Builtin.RSSType, BaseName: "index", NoUgly: true, Rel: "alternate", } SitemapFormat = Format{ - Name: "Sitemap", - MediaType: media.XMLType, + Name: "sitemap", + MediaType: media.Builtin.XMLType, BaseName: "sitemap", NoUgly: true, Rel: "sitemap", @@ -204,6 +196,7 @@ func init() { } // Formats is a slice of Format. +// { "name": "OutputFormats" } type Formats []Format func (formats Formats) Len() int { return len(formats) } @@ -298,102 +291,6 @@ func (formats Formats) FromFilename(filename string) (f Format, found bool) { return } -// DecodeFormats takes a list of output format configurations and merges those, -// in the order given, with the Hugo defaults as the last resort. -func DecodeFormats(mediaTypes media.Types, maps ...map[string]any) (Formats, error) { - f := make(Formats, len(DefaultFormats)) - copy(f, DefaultFormats) - - for _, m := range maps { - for k, v := range m { - found := false - for i, vv := range f { - if strings.EqualFold(k, vv.Name) { - // Merge it with the existing - if err := decode(mediaTypes, v, &f[i]); err != nil { - return f, err - } - found = true - } - } - if !found { - var newOutFormat Format - newOutFormat.Name = k - if err := decode(mediaTypes, v, &newOutFormat); err != nil { - return f, err - } - - // We need values for these - if newOutFormat.BaseName == "" { - newOutFormat.BaseName = "index" - } - if newOutFormat.Rel == "" { - newOutFormat.Rel = "alternate" - } - - f = append(f, newOutFormat) - - } - } - } - - sort.Sort(f) - - return f, nil -} - -func decode(mediaTypes media.Types, input any, output *Format) error { - config := &mapstructure.DecoderConfig{ - Metadata: nil, - Result: output, - WeaklyTypedInput: true, - DecodeHook: func(a reflect.Type, b reflect.Type, c any) (any, error) { - if a.Kind() == reflect.Map { - dataVal := reflect.Indirect(reflect.ValueOf(c)) - for _, key := range dataVal.MapKeys() { - keyStr, ok := key.Interface().(string) - if !ok { - // Not a string key - continue - } - if strings.EqualFold(keyStr, "mediaType") { - // If mediaType is a string, look it up and replace it - // in the map. - vv := dataVal.MapIndex(key) - vvi := vv.Interface() - - switch vviv := vvi.(type) { - case media.Type: - // OK - case string: - mediaType, found := mediaTypes.GetByType(vviv) - if !found { - return c, fmt.Errorf("media type %q not found", vviv) - } - dataVal.SetMapIndex(key, reflect.ValueOf(mediaType)) - default: - return nil, fmt.Errorf("invalid output format configuration; wrong type for media type, expected string (e.g. text/html), got %T", vvi) - } - } - } - } - return c, nil - }, - } - - decoder, err := mapstructure.NewDecoder(config) - if err != nil { - return err - } - - if err = decoder.Decode(input); err != nil { - return fmt.Errorf("failed to decode output format configuration: %w", err) - } - - return nil - -} - // BaseFilename returns the base filename of f including an extension (ie. // "index.xml"). func (f Format) BaseFilename() string { diff --git a/output/outputFormat_test.go b/output/outputFormat_test.go index c5c4534bfd1..e4a4e836576 100644 --- a/output/outputFormat_test.go +++ b/output/outputFormat_test.go @@ -24,21 +24,21 @@ import ( func TestDefaultTypes(t *testing.T) { c := qt.New(t) c.Assert(CalendarFormat.Name, qt.Equals, "Calendar") - c.Assert(CalendarFormat.MediaType, qt.Equals, media.CalendarType) + c.Assert(CalendarFormat.MediaType, qt.Equals, media.Builtin.CalendarType) c.Assert(CalendarFormat.Protocol, qt.Equals, "webcal://") c.Assert(CalendarFormat.Path, qt.HasLen, 0) c.Assert(CalendarFormat.IsPlainText, qt.Equals, true) c.Assert(CalendarFormat.IsHTML, qt.Equals, false) c.Assert(CSSFormat.Name, qt.Equals, "CSS") - c.Assert(CSSFormat.MediaType, qt.Equals, media.CSSType) + c.Assert(CSSFormat.MediaType, qt.Equals, media.Builtin.CSSType) c.Assert(CSSFormat.Path, qt.HasLen, 0) c.Assert(CSSFormat.Protocol, qt.HasLen, 0) // Will inherit the BaseURL protocol. c.Assert(CSSFormat.IsPlainText, qt.Equals, true) c.Assert(CSSFormat.IsHTML, qt.Equals, false) c.Assert(CSVFormat.Name, qt.Equals, "CSV") - c.Assert(CSVFormat.MediaType, qt.Equals, media.CSVType) + c.Assert(CSVFormat.MediaType, qt.Equals, media.Builtin.CSVType) c.Assert(CSVFormat.Path, qt.HasLen, 0) c.Assert(CSVFormat.Protocol, qt.HasLen, 0) c.Assert(CSVFormat.IsPlainText, qt.Equals, true) @@ -46,7 +46,7 @@ func TestDefaultTypes(t *testing.T) { c.Assert(CSVFormat.Permalinkable, qt.Equals, false) c.Assert(HTMLFormat.Name, qt.Equals, "HTML") - c.Assert(HTMLFormat.MediaType, qt.Equals, media.HTMLType) + c.Assert(HTMLFormat.MediaType, qt.Equals, media.Builtin.HTMLType) c.Assert(HTMLFormat.Path, qt.HasLen, 0) c.Assert(HTMLFormat.Protocol, qt.HasLen, 0) c.Assert(HTMLFormat.IsPlainText, qt.Equals, false) @@ -54,7 +54,7 @@ func TestDefaultTypes(t *testing.T) { c.Assert(AMPFormat.Permalinkable, qt.Equals, true) c.Assert(AMPFormat.Name, qt.Equals, "AMP") - c.Assert(AMPFormat.MediaType, qt.Equals, media.HTMLType) + c.Assert(AMPFormat.MediaType, qt.Equals, media.Builtin.HTMLType) c.Assert(AMPFormat.Path, qt.Equals, "amp") c.Assert(AMPFormat.Protocol, qt.HasLen, 0) c.Assert(AMPFormat.IsPlainText, qt.Equals, false) @@ -62,7 +62,7 @@ func TestDefaultTypes(t *testing.T) { c.Assert(AMPFormat.Permalinkable, qt.Equals, true) c.Assert(RSSFormat.Name, qt.Equals, "RSS") - c.Assert(RSSFormat.MediaType, qt.Equals, media.RSSType) + c.Assert(RSSFormat.MediaType, qt.Equals, media.Builtin.RSSType) c.Assert(RSSFormat.Path, qt.HasLen, 0) c.Assert(RSSFormat.IsPlainText, qt.Equals, false) c.Assert(RSSFormat.NoUgly, qt.Equals, true) @@ -101,10 +101,10 @@ func TestGetFormatByExt(t *testing.T) { func TestGetFormatByFilename(t *testing.T) { c := qt.New(t) - noExtNoDelimMediaType := media.TextType + noExtNoDelimMediaType := media.Builtin.TextType noExtNoDelimMediaType.Delimiter = "" - noExtMediaType := media.TextType + noExtMediaType := media.Builtin.TextType var ( noExtDelimFormat = Format{ @@ -138,113 +138,6 @@ func TestGetFormatByFilename(t *testing.T) { c.Assert(found, qt.Equals, false) } -func TestDecodeFormats(t *testing.T) { - c := qt.New(t) - - mediaTypes := media.Types{media.JSONType, media.XMLType} - - tests := []struct { - name string - maps []map[string]any - shouldError bool - assert func(t *testing.T, name string, f Formats) - }{ - { - "Redefine JSON", - []map[string]any{ - { - "JsON": map[string]any{ - "baseName": "myindex", - "isPlainText": "false", - }, - }, - }, - false, - func(t *testing.T, name string, f Formats) { - msg := qt.Commentf(name) - c.Assert(len(f), qt.Equals, len(DefaultFormats), msg) - json, _ := f.GetByName("JSON") - c.Assert(json.BaseName, qt.Equals, "myindex") - c.Assert(json.MediaType, qt.Equals, media.JSONType) - c.Assert(json.IsPlainText, qt.Equals, false) - }, - }, - { - "Add XML format with string as mediatype", - []map[string]any{ - { - "MYXMLFORMAT": map[string]any{ - "baseName": "myxml", - "mediaType": "application/xml", - }, - }, - }, - false, - func(t *testing.T, name string, f Formats) { - c.Assert(len(f), qt.Equals, len(DefaultFormats)+1) - xml, found := f.GetByName("MYXMLFORMAT") - c.Assert(found, qt.Equals, true) - c.Assert(xml.BaseName, qt.Equals, "myxml") - c.Assert(xml.MediaType, qt.Equals, media.XMLType) - - // Verify that we haven't changed the DefaultFormats slice. - json, _ := f.GetByName("JSON") - c.Assert(json.BaseName, qt.Equals, "index") - }, - }, - { - "Add format unknown mediatype", - []map[string]any{ - { - "MYINVALID": map[string]any{ - "baseName": "mymy", - "mediaType": "application/hugo", - }, - }, - }, - true, - func(t *testing.T, name string, f Formats) { - }, - }, - { - "Add and redefine XML format", - []map[string]any{ - { - "MYOTHERXMLFORMAT": map[string]any{ - "baseName": "myotherxml", - "mediaType": media.XMLType, - }, - }, - { - "MYOTHERXMLFORMAT": map[string]any{ - "baseName": "myredefined", - }, - }, - }, - false, - func(t *testing.T, name string, f Formats) { - c.Assert(len(f), qt.Equals, len(DefaultFormats)+1) - xml, found := f.GetByName("MYOTHERXMLFORMAT") - c.Assert(found, qt.Equals, true) - c.Assert(xml.BaseName, qt.Equals, "myredefined") - c.Assert(xml.MediaType, qt.Equals, media.XMLType) - }, - }, - } - - for _, test := range tests { - result, err := DecodeFormats(mediaTypes, test.maps...) - msg := qt.Commentf(test.name) - - if test.shouldError { - c.Assert(err, qt.Not(qt.IsNil), msg) - } else { - c.Assert(err, qt.IsNil, msg) - test.assert(t, test.name, result) - } - } -} - func TestSort(t *testing.T) { c := qt.New(t) c.Assert(DefaultFormats[0].Name, qt.Equals, "HTML") diff --git a/parser/lowercase_camel_json.go b/parser/lowercase_camel_json.go index e6605c80363..d48aa40c4a3 100644 --- a/parser/lowercase_camel_json.go +++ b/parser/lowercase_camel_json.go @@ -19,6 +19,8 @@ import ( "regexp" "unicode" "unicode/utf8" + + "github.com/gohugoio/hugo/common/hreflect" ) // Regexp definitions @@ -57,3 +59,58 @@ func (c LowerCaseCamelJSONMarshaller) MarshalJSON() ([]byte, error) { return converted, err } + +type ReplacingJSONMarshaller struct { + Value any + + KeysToLower bool + OmitEmpty bool +} + +func (c ReplacingJSONMarshaller) MarshalJSON() ([]byte, error) { + converted, err := json.Marshal(c.Value) + + if c.KeysToLower { + converted = keyMatchRegex.ReplaceAllFunc( + converted, + func(match []byte) []byte { + return bytes.ToLower(match) + }, + ) + } + + if c.OmitEmpty { + // It's tricky to do this with a regexp, so convert it to a map, remove zero values and convert back. + var m map[string]interface{} + err = json.Unmarshal(converted, &m) + if err != nil { + return nil, err + } + var removeZeroVAlues func(m map[string]any) + removeZeroVAlues = func(m map[string]any) { + for k, v := range m { + if !hreflect.IsTruthful(v) { + delete(m, k) + } else { + switch v.(type) { + case map[string]interface{}: + removeZeroVAlues(v.(map[string]any)) + case []interface{}: + for _, vv := range v.([]interface{}) { + if m, ok := vv.(map[string]any); ok { + removeZeroVAlues(m) + } + } + } + + } + + } + } + removeZeroVAlues(m) + converted, err = json.Marshal(m) + + } + + return converted, err +} diff --git a/parser/lowercase_camel_json_test.go b/parser/lowercase_camel_json_test.go new file mode 100644 index 00000000000..ffbc8029522 --- /dev/null +++ b/parser/lowercase_camel_json_test.go @@ -0,0 +1,33 @@ +package parser + +import ( + "testing" + + qt "github.com/frankban/quicktest" +) + +func TestReplacingJSONMarshaller(t *testing.T) { + c := qt.New(t) + + m := map[string]any{ + "foo": "bar", + "baz": 42, + "zeroInt1": 0, + "zeroInt2": 0, + "zeroFloat": 0.0, + "zeroString": "", + "zeroBool": false, + "nil": nil, + } + + marshaller := ReplacingJSONMarshaller{ + Value: m, + KeysToLower: true, + OmitEmpty: true, + } + + b, err := marshaller.MarshalJSON() + c.Assert(err, qt.IsNil) + + c.Assert(string(b), qt.Equals, `{"baz":42,"foo":"bar"}`) +} diff --git a/parser/metadecoders/format.go b/parser/metadecoders/format.go index 17e13f46794..2e7e6964c46 100644 --- a/parser/metadecoders/format.go +++ b/parser/metadecoders/format.go @@ -16,8 +16,6 @@ package metadecoders import ( "path/filepath" "strings" - - "github.com/gohugoio/hugo/media" ) type Format string @@ -33,6 +31,16 @@ const ( XML Format = "xml" ) +// FormatFromStrings returns the first non-empty Format from the given strings. +func FormatFromStrings(ss ...string) Format { + for _, s := range ss { + if f := FormatFromString(s); f != "" { + return f + } + } + return "" +} + // FormatFromString turns formatStr, typically a file extension without any ".", // into a Format. It returns an empty string for unknown formats. func FormatFromString(formatStr string) Format { @@ -59,18 +67,6 @@ func FormatFromString(formatStr string) Format { return "" } -// FormatFromMediaType gets the Format given a MIME type, empty string -// if unknown. -func FormatFromMediaType(m media.Type) Format { - for _, suffix := range m.Suffixes() { - if f := FormatFromString(suffix); f != "" { - return f - } - } - - return "" -} - // FormatFromContentString tries to detect the format (JSON, YAML, TOML or XML) // in the given string. // It return an empty string if no format could be detected. diff --git a/parser/metadecoders/format_test.go b/parser/metadecoders/format_test.go index db33a7d8c12..c70db3fb3b6 100644 --- a/parser/metadecoders/format_test.go +++ b/parser/metadecoders/format_test.go @@ -16,8 +16,6 @@ package metadecoders import ( "testing" - "github.com/gohugoio/hugo/media" - qt "github.com/frankban/quicktest" ) @@ -41,23 +39,6 @@ func TestFormatFromString(t *testing.T) { } } -func TestFormatFromMediaType(t *testing.T) { - c := qt.New(t) - for _, test := range []struct { - m media.Type - expect Format - }{ - {media.JSONType, JSON}, - {media.YAMLType, YAML}, - {media.XMLType, XML}, - {media.RSSType, XML}, - {media.TOMLType, TOML}, - {media.CalendarType, ""}, - } { - c.Assert(FormatFromMediaType(test.m), qt.Equals, test.expect) - } -} - func TestFormatFromContentString(t *testing.T) { t.Parallel() c := qt.New(t) diff --git a/publisher/htmlElementsCollector_test.go b/publisher/htmlElementsCollector_test.go index f9c9424cbac..7ad7b20604c 100644 --- a/publisher/htmlElementsCollector_test.go +++ b/publisher/htmlElementsCollector_test.go @@ -143,7 +143,7 @@ func TestClassCollector(t *testing.T) { } v := config.NewWithTestDefaults() m, _ := minifiers.New(media.DefaultTypes, output.DefaultFormats, v) - m.Minify(media.HTMLType, w, strings.NewReader(test.html)) + m.Minify(media.Builtin.HTMLType, w, strings.NewReader(test.html)) } else { var buff bytes.Buffer diff --git a/related/inverted_index.go b/related/inverted_index.go index ae894e5220d..fcebdc71646 100644 --- a/related/inverted_index.go +++ b/related/inverted_index.go @@ -53,32 +53,15 @@ var ( // DefaultConfig is the default related config. DefaultConfig = Config{ Threshold: 80, - Indices: IndexConfigs{ + Indices: IndicesConfig{ IndexConfig{Name: "keywords", Weight: 100, Type: TypeBasic}, IndexConfig{Name: "date", Weight: 10, Type: TypeBasic}, }, } ) -/* -Config is the top level configuration element used to configure how to retrieve -related content in Hugo. - -An example site config.toml: - - [related] - threshold = 1 - [[related.indices]] - name = "keywords" - weight = 200 - [[related.indices]] - name = "tags" - weight = 100 - [[related.indices]] - name = "date" - weight = 1 - pattern = "2006" -*/ +// Config is the top level configuration element used to configure how to retrieve +// related content in Hugo. type Config struct { // Only include matches >= threshold, a normalized rank between 0 and 100. Threshold int @@ -90,7 +73,7 @@ type Config struct { // May get better results, but at a slight performance cost. ToLower bool - Indices IndexConfigs + Indices IndicesConfig } // Add adds a given index. @@ -110,8 +93,8 @@ func (c *Config) HasType(s string) bool { return false } -// IndexConfigs holds a set of index configurations. -type IndexConfigs []IndexConfig +// IndicesConfig holds a set of index configurations. +type IndicesConfig []IndexConfig // IndexConfig configures an index. type IndexConfig struct { @@ -366,13 +349,13 @@ func (idx *InvertedIndex) Search(ctx context.Context, opts SearchOpts) ([]Docume var ( queryElements []queryElement - configs IndexConfigs + configs IndicesConfig ) if len(opts.Indices) == 0 { configs = idx.cfg.Indices } else { - configs = make(IndexConfigs, len(opts.Indices)) + configs = make(IndicesConfig, len(opts.Indices)) for i, indexName := range opts.Indices { cfg, found := idx.getIndexCfg(indexName) if !found { @@ -396,12 +379,14 @@ func (idx *InvertedIndex) Search(ctx context.Context, opts SearchOpts) ([]Docume keywords = append(keywords, FragmentKeyword(fragment)) } if opts.Document != nil { + if fp, ok := opts.Document.(FragmentProvider); ok { for _, fragment := range fp.Fragments(ctx).Identifiers { keywords = append(keywords, FragmentKeyword(fragment)) } } } + } queryElements = append(queryElements, newQueryElement(cfg.Name, keywords...)) } @@ -553,6 +538,7 @@ func (idx *InvertedIndex) searchDate(ctx context.Context, self Document, upperDa for i, m := range matches { result[i] = m.Doc + if len(fragmentsFilter) > 0 { if dp, ok := result[i].(FragmentProvider); ok { result[i] = dp.ApplyFilterToHeadings(ctx, func(h *tableofcontents.Heading) bool { diff --git a/related/inverted_index_test.go b/related/inverted_index_test.go index c7348e08898..72b2f3252dd 100644 --- a/related/inverted_index_test.go +++ b/related/inverted_index_test.go @@ -91,7 +91,7 @@ func TestCardinalityThreshold(t *testing.T) { config := Config{ Threshold: 90, IncludeNewer: false, - Indices: IndexConfigs{ + Indices: IndicesConfig{ IndexConfig{Name: "tags", Weight: 50, CardinalityThreshold: 79}, IndexConfig{Name: "keywords", Weight: 65, CardinalityThreshold: 90}, }, @@ -125,7 +125,7 @@ func TestSearch(t *testing.T) { config := Config{ Threshold: 90, IncludeNewer: false, - Indices: IndexConfigs{ + Indices: IndicesConfig{ IndexConfig{Name: "tags", Weight: 50}, IndexConfig{Name: "keywords", Weight: 65}, }, @@ -293,7 +293,7 @@ func BenchmarkRelatedNewIndex(b *testing.B) { cfg := Config{ Threshold: 50, - Indices: IndexConfigs{ + Indices: IndicesConfig{ IndexConfig{Name: "tags", Weight: 100}, IndexConfig{Name: "keywords", Weight: 200}, }, @@ -334,7 +334,7 @@ func BenchmarkRelatedMatchesIn(b *testing.B) { cfg := Config{ Threshold: 20, - Indices: IndexConfigs{ + Indices: IndicesConfig{ IndexConfig{Name: "tags", Weight: 100}, IndexConfig{Name: "keywords", Weight: 200}, }, diff --git a/resources/image.go b/resources/image.go index 6deb0dfe70e..c61e903abb5 100644 --- a/resources/image.go +++ b/resources/image.go @@ -323,7 +323,7 @@ func (i *imageResource) doWithImageConfig(conf images.ImageConfig, f func(src im if shouldFill { bgColor = conf.BgColor if bgColor == nil { - bgColor = i.Proc.Cfg.BgColor + bgColor = i.Proc.Cfg.Config.BgColor } tmp := image.NewRGBA(converted.Bounds()) draw.Draw(tmp, tmp.Bounds(), image.NewUniform(bgColor), image.Point{}, draw.Src) @@ -380,7 +380,7 @@ func (g *giphy) GIF() *gif.GIF { } // DecodeImage decodes the image source into an Image. -// This an internal method and may change. +// This for internal use only. func (i *imageResource) DecodeImage() (image.Image, error) { f, err := i.ReadSeekCloser() if err != nil { @@ -423,7 +423,7 @@ func (i *imageResource) setBasePath(conf images.ImageConfig) { func (i *imageResource) getImageMetaCacheTargetPath() string { const imageMetaVersionNumber = 1 // Increment to invalidate the meta cache - cfgHash := i.getSpec().imaging.Cfg.CfgHash + cfgHash := i.getSpec().imaging.Cfg.SourceHash df := i.getResourcePaths().relTargetDirFile if fi := i.getFileInfo(); fi != nil { df.dir = filepath.Dir(fi.Meta().Path) diff --git a/resources/image_extended_test.go b/resources/image_extended_test.go index a0b274f3e42..04e0ad5d28b 100644 --- a/resources/image_extended_test.go +++ b/resources/image_extended_test.go @@ -19,9 +19,8 @@ package resources import ( "testing" - "github.com/gohugoio/hugo/media" - qt "github.com/frankban/quicktest" + "github.com/gohugoio/hugo/media" ) func TestImageResizeWebP(t *testing.T) { @@ -29,14 +28,14 @@ func TestImageResizeWebP(t *testing.T) { image := fetchImage(c, "sunset.webp") - c.Assert(image.MediaType(), qt.Equals, media.WEBPType) + c.Assert(image.MediaType(), qt.Equals, media.Builtin.WEBPType) c.Assert(image.RelPermalink(), qt.Equals, "/a/sunset.webp") c.Assert(image.ResourceType(), qt.Equals, "image") c.Assert(image.Exif(), qt.IsNil) resized, err := image.Resize("123x") c.Assert(err, qt.IsNil) - c.Assert(image.MediaType(), qt.Equals, media.WEBPType) + c.Assert(image.MediaType(), qt.Equals, media.Builtin.WEBPType) c.Assert(resized.RelPermalink(), qt.Equals, "/a/sunset_hu36ee0b61ba924719ad36da960c273f96_59826_123x0_resize_q68_h2_linear_2.webp") c.Assert(resized.Width(), qt.Equals, 123) } diff --git a/resources/image_test.go b/resources/image_test.go index d401fa7836d..792aa1af9f0 100644 --- a/resources/image_test.go +++ b/resources/image_test.go @@ -67,7 +67,7 @@ var eq = qt.CmpEquals( }), cmp.Comparer(func(p1, p2 *genericResource) bool { return p1 == p2 }), cmp.Comparer(func(m1, m2 media.Type) bool { - return m1.Type() == m2.Type() + return m1.Type == m2.Type }), cmp.Comparer( func(v1, v2 *big.Rat) bool { @@ -386,14 +386,14 @@ func TestImageResize8BitPNG(t *testing.T) { image := fetchImage(c, "gohugoio.png") - c.Assert(image.MediaType().Type(), qt.Equals, "image/png") + c.Assert(image.MediaType().Type, qt.Equals, "image/png") c.Assert(image.RelPermalink(), qt.Equals, "/a/gohugoio.png") c.Assert(image.ResourceType(), qt.Equals, "image") c.Assert(image.Exif(), qt.IsNil) resized, err := image.Resize("800x") c.Assert(err, qt.IsNil) - c.Assert(resized.MediaType().Type(), qt.Equals, "image/png") + c.Assert(resized.MediaType().Type, qt.Equals, "image/png") c.Assert(resized.RelPermalink(), qt.Equals, "/a/gohugoio_hu0e1b9e4a4be4d6f86c7b37b9ccce3fbc_73886_800x0_resize_linear_3.png") c.Assert(resized.Width(), qt.Equals, 800) } @@ -403,14 +403,14 @@ func TestImageResizeInSubPath(t *testing.T) { image := fetchImage(c, "sub/gohugoio2.png") - c.Assert(image.MediaType(), eq, media.PNGType) + c.Assert(image.MediaType(), eq, media.Builtin.PNGType) c.Assert(image.RelPermalink(), qt.Equals, "/a/sub/gohugoio2.png") c.Assert(image.ResourceType(), qt.Equals, "image") c.Assert(image.Exif(), qt.IsNil) resized, err := image.Resize("101x101") c.Assert(err, qt.IsNil) - c.Assert(resized.MediaType().Type(), qt.Equals, "image/png") + c.Assert(resized.MediaType().Type, qt.Equals, "image/png") c.Assert(resized.RelPermalink(), qt.Equals, "/a/sub/gohugoio2_hu0e1b9e4a4be4d6f86c7b37b9ccce3fbc_73886_101x101_resize_linear_3.png") c.Assert(resized.Width(), qt.Equals, 101) c.Assert(resized.Exif(), qt.IsNil) diff --git a/resources/images/config.go b/resources/images/config.go index 09a7016c143..a3ca0c3597a 100644 --- a/resources/images/config.go +++ b/resources/images/config.go @@ -19,16 +19,16 @@ import ( "strconv" "strings" - "github.com/gohugoio/hugo/identity" + "github.com/gohugoio/hugo/common/maps" + "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/media" + "github.com/mitchellh/mapstructure" "errors" "github.com/bep/gowebp/libwebp/webpoptions" "github.com/disintegration/gift" - - "github.com/mitchellh/mapstructure" ) var ( @@ -47,12 +47,12 @@ var ( } imageFormatsBySubType = map[string]Format{ - media.JPEGType.SubType: JPEG, - media.PNGType.SubType: PNG, - media.TIFFType.SubType: TIFF, - media.BMPType.SubType: BMP, - media.GIFType.SubType: GIF, - media.WEBPType.SubType: WEBP, + media.Builtin.JPEGType.SubType: JPEG, + media.Builtin.PNGType.SubType: PNG, + media.Builtin.TIFFType.SubType: TIFF, + media.Builtin.BMPType.SubType: BMP, + media.Builtin.GIFType.SubType: GIF, + media.Builtin.WEBPType.SubType: WEBP, } // Add or increment if changes to an image format's processing requires @@ -121,66 +121,83 @@ func ImageFormatFromMediaSubType(sub string) (Format, bool) { const ( defaultJPEGQuality = 75 defaultResampleFilter = "box" - defaultBgColor = "ffffff" + defaultBgColor = "#ffffff" defaultHint = "photo" ) -var defaultImaging = Imaging{ - ResampleFilter: defaultResampleFilter, - BgColor: defaultBgColor, - Hint: defaultHint, - Quality: defaultJPEGQuality, -} - -func DecodeConfig(m map[string]any) (ImagingConfig, error) { - if m == nil { - m = make(map[string]any) +var ( + defaultImaging = map[string]any{ + "resampleFilter": defaultResampleFilter, + "bgColor": defaultBgColor, + "hint": defaultHint, + "quality": defaultJPEGQuality, } - i := ImagingConfig{ - Cfg: defaultImaging, - CfgHash: identity.HashString(m), - } + defaultImageConfig *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal] +) - if err := mapstructure.WeakDecode(m, &i.Cfg); err != nil { - return i, err +func init() { + var err error + defaultImageConfig, err = DecodeConfig(defaultImaging) + if err != nil { + panic(err) } +} - if err := i.Cfg.init(); err != nil { - return i, err +func DecodeConfig(in map[string]any) (*config.ConfigNamespace[ImagingConfig, ImagingConfigInternal], error) { + if in == nil { + in = make(map[string]any) } - var err error - i.BgColor, err = hexStringToColor(i.Cfg.BgColor) - if err != nil { - return i, err - } + buildConfig := func(in any) (ImagingConfigInternal, any, error) { + m, err := maps.ToStringMapE(in) + if err != nil { + return ImagingConfigInternal{}, nil, err + } + // Merge in the defaults. + maps.MergeShallow(m, defaultImaging) + + var i ImagingConfigInternal + if err := mapstructure.Decode(m, &i.Imaging); err != nil { + return i, nil, err + } + + if err := i.Imaging.init(); err != nil { + return i, nil, err + } + + i.BgColor, err = hexStringToColor(i.Imaging.BgColor) + if err != nil { + return i, nil, err + } - if i.Cfg.Anchor != "" && i.Cfg.Anchor != smartCropIdentifier { - anchor, found := anchorPositions[i.Cfg.Anchor] + if i.Imaging.Anchor != "" && i.Imaging.Anchor != smartCropIdentifier { + anchor, found := anchorPositions[i.Imaging.Anchor] + if !found { + return i, nil, fmt.Errorf("invalid anchor value %q in imaging config", i.Anchor) + } + i.Anchor = anchor + } + + filter, found := imageFilters[i.Imaging.ResampleFilter] if !found { - return i, fmt.Errorf("invalid anchor value %q in imaging config", i.Anchor) + return i, nil, fmt.Errorf("%q is not a valid resample filter", filter) } - i.Anchor = anchor - } else { - i.Cfg.Anchor = smartCropIdentifier - } - filter, found := imageFilters[i.Cfg.ResampleFilter] - if !found { - return i, fmt.Errorf("%q is not a valid resample filter", filter) + i.ResampleFilter = filter + + return i, nil, nil } - i.ResampleFilter = filter - if strings.TrimSpace(i.Cfg.Exif.IncludeFields) == "" && strings.TrimSpace(i.Cfg.Exif.ExcludeFields) == "" { - // Don't change this for no good reason. Please don't. - i.Cfg.Exif.ExcludeFields = "GPS|Exif|Exposure[M|P|B]|Contrast|Resolution|Sharp|JPEG|Metering|Sensing|Saturation|ColorSpace|Flash|WhiteBalance" + ns, err := config.DecodeNamespace[ImagingConfig](in, buildConfig) + if err != nil { + return nil, fmt.Errorf("failed to decode media types: %w", err) } + return ns, nil - return i, nil } -func DecodeImageConfig(action, config string, defaults ImagingConfig, sourceFormat Format) (ImageConfig, error) { +func DecodeImageConfig(action, config string, defaults *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal], sourceFormat Format) (ImageConfig, error) { var ( c ImageConfig = GetDefaultImageConfig(action, defaults) err error @@ -268,8 +285,8 @@ func DecodeImageConfig(action, config string, defaults ImagingConfig, sourceForm } if c.FilterStr == "" { - c.FilterStr = defaults.Cfg.ResampleFilter - c.Filter = defaults.ResampleFilter + c.FilterStr = defaults.Config.Imaging.ResampleFilter + c.Filter = defaults.Config.ResampleFilter } if c.Hint == 0 { @@ -277,8 +294,8 @@ func DecodeImageConfig(action, config string, defaults ImagingConfig, sourceForm } if c.AnchorStr == "" { - c.AnchorStr = defaults.Cfg.Anchor - c.Anchor = defaults.Anchor + c.AnchorStr = defaults.Config.Imaging.Anchor + c.Anchor = defaults.Config.Anchor } // default to the source format @@ -288,13 +305,13 @@ func DecodeImageConfig(action, config string, defaults ImagingConfig, sourceForm if c.Quality <= 0 && c.TargetFormat.RequiresDefaultQuality() { // We need a quality setting for all JPEGs and WEBPs. - c.Quality = defaults.Cfg.Quality + c.Quality = defaults.Config.Imaging.Quality } if c.BgColor == nil && c.TargetFormat != sourceFormat { if sourceFormat.SupportsTransparency() && !c.TargetFormat.SupportsTransparency() { - c.BgColor = defaults.BgColor - c.BgColorStr = defaults.Cfg.BgColor + c.BgColor = defaults.Config.BgColor + c.BgColorStr = defaults.Config.Imaging.BgColor } } @@ -389,22 +406,43 @@ func (i ImageConfig) GetKey(format Format) string { return k } -type ImagingConfig struct { +type ImagingConfigInternal struct { BgColor color.Color Hint webpoptions.EncodingPreset ResampleFilter gift.Resampling Anchor gift.Anchor - // Config as provided by the user. - Cfg Imaging + Imaging ImagingConfig +} + +func (i *ImagingConfigInternal) Compile(externalCfg *ImagingConfig) error { + var err error + i.BgColor, err = hexStringToColor(externalCfg.BgColor) + if err != nil { + return err + } + + if externalCfg.Anchor != "" && externalCfg.Anchor != smartCropIdentifier { + anchor, found := anchorPositions[externalCfg.Anchor] + if !found { + return fmt.Errorf("invalid anchor value %q in imaging config", i.Anchor) + } + i.Anchor = anchor + } + + filter, found := imageFilters[externalCfg.ResampleFilter] + if !found { + return fmt.Errorf("%q is not a valid resample filter", filter) + } + i.ResampleFilter = filter + + return nil - // Hash of the config map provided by the user. - CfgHash string } -// Imaging contains default image processing configuration. This will be fetched +// ImagingConfig contains default image processing configuration. This will be fetched // from site (or language) config. -type Imaging struct { +type ImagingConfig struct { // Default image quality setting (1-100). Only used for JPEG images. Quality int @@ -426,7 +464,7 @@ type Imaging struct { Exif ExifConfig } -func (cfg *Imaging) init() error { +func (cfg *ImagingConfig) init() error { if cfg.Quality < 0 || cfg.Quality > 100 { return errors.New("image quality must be a number between 1 and 100") } @@ -436,6 +474,15 @@ func (cfg *Imaging) init() error { cfg.ResampleFilter = strings.ToLower(cfg.ResampleFilter) cfg.Hint = strings.ToLower(cfg.Hint) + if cfg.Anchor == "" { + cfg.Anchor = smartCropIdentifier + } + + if strings.TrimSpace(cfg.Exif.IncludeFields) == "" && strings.TrimSpace(cfg.Exif.ExcludeFields) == "" { + // Don't change this for no good reason. Please don't. + cfg.Exif.ExcludeFields = "GPS|Exif|Exposure[M|P|B]|Contrast|Resolution|Sharp|JPEG|Metering|Sensing|Saturation|ColorSpace|Flash|WhiteBalance" + } + return nil } diff --git a/resources/images/config_test.go b/resources/images/config_test.go index 1b785f7ca52..2e0d6635d92 100644 --- a/resources/images/config_test.go +++ b/resources/images/config_test.go @@ -32,18 +32,18 @@ func TestDecodeConfig(t *testing.T) { imagingConfig, err := DecodeConfig(m) c.Assert(err, qt.IsNil) - imaging := imagingConfig.Cfg - c.Assert(imaging.Quality, qt.Equals, 42) - c.Assert(imaging.ResampleFilter, qt.Equals, "nearestneighbor") - c.Assert(imaging.Anchor, qt.Equals, "topleft") + conf := imagingConfig.Config + c.Assert(conf.Imaging.Quality, qt.Equals, 42) + c.Assert(conf.Imaging.ResampleFilter, qt.Equals, "nearestneighbor") + c.Assert(conf.Imaging.Anchor, qt.Equals, "topleft") m = map[string]any{} imagingConfig, err = DecodeConfig(m) c.Assert(err, qt.IsNil) - imaging = imagingConfig.Cfg - c.Assert(imaging.ResampleFilter, qt.Equals, "box") - c.Assert(imaging.Anchor, qt.Equals, "smart") + conf = imagingConfig.Config + c.Assert(conf.Imaging.ResampleFilter, qt.Equals, "box") + c.Assert(conf.Imaging.Anchor, qt.Equals, "smart") _, err = DecodeConfig(map[string]any{ "quality": 123, @@ -63,9 +63,9 @@ func TestDecodeConfig(t *testing.T) { imagingConfig, err = DecodeConfig(map[string]any{ "anchor": "Smart", }) - imaging = imagingConfig.Cfg + conf = imagingConfig.Config c.Assert(err, qt.IsNil) - c.Assert(imaging.Anchor, qt.Equals, "smart") + c.Assert(conf.Imaging.Anchor, qt.Equals, "smart") imagingConfig, err = DecodeConfig(map[string]any{ "exif": map[string]any{ @@ -73,9 +73,9 @@ func TestDecodeConfig(t *testing.T) { }, }) c.Assert(err, qt.IsNil) - imaging = imagingConfig.Cfg - c.Assert(imaging.Exif.DisableLatLong, qt.Equals, true) - c.Assert(imaging.Exif.ExcludeFields, qt.Equals, "GPS|Exif|Exposure[M|P|B]|Contrast|Resolution|Sharp|JPEG|Metering|Sensing|Saturation|ColorSpace|Flash|WhiteBalance") + conf = imagingConfig.Config + c.Assert(conf.Imaging.Exif.DisableLatLong, qt.Equals, true) + c.Assert(conf.Imaging.Exif.ExcludeFields, qt.Equals, "GPS|Exif|Exposure[M|P|B]|Contrast|Resolution|Sharp|JPEG|Metering|Sensing|Saturation|ColorSpace|Flash|WhiteBalance") } func TestDecodeImageConfig(t *testing.T) { @@ -123,7 +123,7 @@ func TestDecodeImageConfig(t *testing.T) { } func newImageConfig(action string, width, height, quality, rotate int, filter, anchor, bgColor string) ImageConfig { - var c ImageConfig = GetDefaultImageConfig(action, ImagingConfig{}) + var c ImageConfig = GetDefaultImageConfig(action, nil) c.TargetFormat = PNG c.Hint = 2 c.Width = width diff --git a/resources/images/image.go b/resources/images/image.go index 9dc8ed408c3..530057d800e 100644 --- a/resources/images/image.go +++ b/resources/images/image.go @@ -25,6 +25,7 @@ import ( "sync" "github.com/bep/gowebp/libwebp/webpoptions" + "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/resources/images/webp" "github.com/gohugoio/hugo/media" @@ -174,8 +175,8 @@ func (i *Image) initConfig() error { return nil } -func NewImageProcessor(cfg ImagingConfig) (*ImageProcessor, error) { - e := cfg.Cfg.Exif +func NewImageProcessor(cfg *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal]) (*ImageProcessor, error) { + e := cfg.Config.Imaging.Exif exifDecoder, err := exif.NewDecoder( exif.WithDateDisabled(e.DisableDate), exif.WithLatLongDisabled(e.DisableLatLong), @@ -193,7 +194,7 @@ func NewImageProcessor(cfg ImagingConfig) (*ImageProcessor, error) { } type ImageProcessor struct { - Cfg ImagingConfig + Cfg *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal] exifDecoder *exif.Decoder } @@ -304,11 +305,14 @@ func (p *ImageProcessor) doFilter(src image.Image, targetFormat Format, filters return dst, nil } -func GetDefaultImageConfig(action string, defaults ImagingConfig) ImageConfig { +func GetDefaultImageConfig(action string, defaults *config.ConfigNamespace[ImagingConfig, ImagingConfigInternal]) ImageConfig { + if defaults == nil { + defaults = defaultImageConfig + } return ImageConfig{ Action: action, - Hint: defaults.Hint, - Quality: defaults.Cfg.Quality, + Hint: defaults.Config.Hint, + Quality: defaults.Config.Imaging.Quality, } } @@ -350,17 +354,17 @@ func (f Format) DefaultExtension() string { func (f Format) MediaType() media.Type { switch f { case JPEG: - return media.JPEGType + return media.Builtin.JPEGType case PNG: - return media.PNGType + return media.Builtin.PNGType case GIF: - return media.GIFType + return media.Builtin.GIFType case TIFF: - return media.TIFFType + return media.Builtin.TIFFType case BMP: - return media.BMPType + return media.Builtin.BMPType case WEBP: - return media.WEBPType + return media.Builtin.WEBPType default: panic(fmt.Sprintf("%d is not a valid image format", f)) } diff --git a/resources/images/image_resource.go b/resources/images/image_resource.go index 8469590063a..dcd2b47416b 100644 --- a/resources/images/image_resource.go +++ b/resources/images/image_resource.go @@ -62,6 +62,6 @@ type ImageResourceOps interface { // using a simple histogram method. Colors() ([]string, error) - // Internal + // For internal use. DecodeImage() (image.Image, error) } diff --git a/resources/page/page.go b/resources/page/page.go index 6f6f1d10003..1ec56e8cf62 100644 --- a/resources/page/page.go +++ b/resources/page/page.go @@ -166,7 +166,7 @@ type OutputFormatsProvider interface { OutputFormats() OutputFormats } -// Page is the core interface in Hugo. +// Page is the core interface in Hugo and what you get as the top level data context in your templates. type Page interface { ContentProvider TableOfContentsProvider @@ -249,7 +249,7 @@ type PageMetaProvider interface { // Sitemap returns the sitemap configuration for this page. // This is for internal use only. - Sitemap() config.Sitemap + Sitemap() config.SitemapConfig // Type is a discriminator used to select layouts etc. It is typically set // in front matter, but will fall back to the root section. diff --git a/resources/page/page_marshaljson.autogen.go b/resources/page/page_marshaljson.autogen.go index c3524ec3680..2f4b1e4130c 100644 --- a/resources/page/page_marshaljson.autogen.go +++ b/resources/page/page_marshaljson.autogen.go @@ -15,168 +15,6 @@ package page -import ( - "encoding/json" - "github.com/gohugoio/hugo/common/maps" - "github.com/gohugoio/hugo/config" - "github.com/gohugoio/hugo/hugofs/files" - "github.com/gohugoio/hugo/identity" - "github.com/gohugoio/hugo/langs" - "github.com/gohugoio/hugo/media" - "github.com/gohugoio/hugo/navigation" - "github.com/gohugoio/hugo/source" - "time" -) - func MarshalPageToJSON(p Page) ([]byte, error) { - rawContent := p.RawContent() - resourceType := p.ResourceType() - mediaType := p.MediaType() - permalink := p.Permalink() - relPermalink := p.RelPermalink() - name := p.Name() - title := p.Title() - params := p.Params() - data := p.Data() - date := p.Date() - lastmod := p.Lastmod() - publishDate := p.PublishDate() - expiryDate := p.ExpiryDate() - aliases := p.Aliases() - bundleType := p.BundleType() - description := p.Description() - draft := p.Draft() - isHome := p.IsHome() - keywords := p.Keywords() - kind := p.Kind() - layout := p.Layout() - linkTitle := p.LinkTitle() - isNode := p.IsNode() - isPage := p.IsPage() - path := p.Path() - pathc := p.Pathc() - slug := p.Slug() - lang := p.Lang() - isSection := p.IsSection() - section := p.Section() - sectionsEntries := p.SectionsEntries() - sectionsPath := p.SectionsPath() - sitemap := p.Sitemap() - typ := p.Type() - weight := p.Weight() - language := p.Language() - file := p.File() - gitInfo := p.GitInfo() - codeOwners := p.CodeOwners() - outputFormats := p.OutputFormats() - alternativeOutputFormats := p.AlternativeOutputFormats() - menus := p.Menus() - translationKey := p.TranslationKey() - isTranslated := p.IsTranslated() - allTranslations := p.AllTranslations() - translations := p.Translations() - store := p.Store() - getIdentity := p.GetIdentity() - - s := struct { - RawContent string - ResourceType string - MediaType media.Type - Permalink string - RelPermalink string - Name string - Title string - Params maps.Params - Data interface{} - Date time.Time - Lastmod time.Time - PublishDate time.Time - ExpiryDate time.Time - Aliases []string - BundleType files.ContentClass - Description string - Draft bool - IsHome bool - Keywords []string - Kind string - Layout string - LinkTitle string - IsNode bool - IsPage bool - Path string - Pathc string - Slug string - Lang string - IsSection bool - Section string - SectionsEntries []string - SectionsPath string - Sitemap config.Sitemap - Type string - Weight int - Language *langs.Language - File source.File - GitInfo source.GitInfo - CodeOwners []string - OutputFormats OutputFormats - AlternativeOutputFormats OutputFormats - Menus navigation.PageMenus - TranslationKey string - IsTranslated bool - AllTranslations Pages - Translations Pages - Store *maps.Scratch - GetIdentity identity.Identity - }{ - RawContent: rawContent, - ResourceType: resourceType, - MediaType: mediaType, - Permalink: permalink, - RelPermalink: relPermalink, - Name: name, - Title: title, - Params: params, - Data: data, - Date: date, - Lastmod: lastmod, - PublishDate: publishDate, - ExpiryDate: expiryDate, - Aliases: aliases, - BundleType: bundleType, - Description: description, - Draft: draft, - IsHome: isHome, - Keywords: keywords, - Kind: kind, - Layout: layout, - LinkTitle: linkTitle, - IsNode: isNode, - IsPage: isPage, - Path: path, - Pathc: pathc, - Slug: slug, - Lang: lang, - IsSection: isSection, - Section: section, - SectionsEntries: sectionsEntries, - SectionsPath: sectionsPath, - Sitemap: sitemap, - Type: typ, - Weight: weight, - Language: language, - File: file, - GitInfo: gitInfo, - CodeOwners: codeOwners, - OutputFormats: outputFormats, - AlternativeOutputFormats: alternativeOutputFormats, - Menus: menus, - TranslationKey: translationKey, - IsTranslated: isTranslated, - AllTranslations: allTranslations, - Translations: translations, - Store: store, - GetIdentity: getIdentity, - } - - return json.Marshal(&s) + return nil, nil } diff --git a/resources/page/page_matcher.go b/resources/page/page_matcher.go index c302ff21a9a..e4006b92774 100644 --- a/resources/page/page_matcher.go +++ b/resources/page/page_matcher.go @@ -19,6 +19,7 @@ import ( "strings" "github.com/gohugoio/hugo/common/maps" + "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/hugofs/glob" "github.com/mitchellh/mapstructure" ) @@ -80,7 +81,80 @@ func (m PageMatcher) Matches(p Page) bool { return true } +func DecodeCascadeConfig(in any) (*config.ConfigNamespace[[]PageMatcherParamsConfig, map[PageMatcher]maps.Params], error) { + buildConfig := func(in any) (map[PageMatcher]maps.Params, any, error) { + cascade := make(map[PageMatcher]maps.Params) + if in == nil { + return cascade, []map[string]any{}, nil + } + ms, err := maps.ToSliceStringMap(in) + if err != nil { + return nil, nil, err + } + + var cfgs []PageMatcherParamsConfig + + for _, m := range ms { + c, err := mapToPageMatcherParamsConfig(m) + if err != nil { + return nil, nil, err + } + cfgs = append(cfgs, c) + } + + for _, cfg := range cfgs { + m := cfg.Target + c, found := cascade[m] + if found { + // Merge + for k, v := range cfg.Params { + if _, found := c[k]; !found { + c[k] = v + } + } + } else { + cascade[m] = cfg.Params + } + } + + return cascade, cfgs, nil + } + + return config.DecodeNamespace[[]PageMatcherParamsConfig](in, buildConfig) + +} + +func mapToPageMatcherParamsConfig(m map[string]any) (PageMatcherParamsConfig, error) { + var pcfg PageMatcherParamsConfig + for k, v := range m { + switch strings.ToLower(k) { + case "params": + // We simplified the structure of the cascade config in Hugo 0.111.0. + // There is a small chance that someone has used the old structure with the params keyword, + // those values will now be moved to the top level. + // This should be very unlikely as it would lead to constructs like .Params.params.foo, + // and most people see params as an Hugo internal keyword. + pcfg.Params = maps.ToStringMap(v) + case "_target", "target": + var target PageMatcher + if err := decodePageMatcher(v, &target); err != nil { + return pcfg, err + } + pcfg.Target = target + default: + // Legacy config. + if pcfg.Params == nil { + pcfg.Params = make(maps.Params) + } + pcfg.Params[k] = v + } + } + return pcfg, pcfg.init() + +} + // DecodeCascade decodes in which could be either a map or a slice of maps. +// TODO1 remove. func DecodeCascade(in any) (map[PageMatcher]maps.Params, error) { m, err := maps.ToSliceStringMap(in) if err != nil { @@ -94,7 +168,7 @@ func DecodeCascade(in any) (map[PageMatcher]maps.Params, error) { for _, vv := range m { var m PageMatcher if mv, found := vv["_target"]; found { - err := DecodePageMatcher(mv, &m) + err := decodePageMatcher(mv, &m) if err != nil { return nil, err } @@ -115,8 +189,8 @@ func DecodeCascade(in any) (map[PageMatcher]maps.Params, error) { return cascade, nil } -// DecodePageMatcher decodes m into v. -func DecodePageMatcher(m any, v *PageMatcher) error { +// decodePageMatcher decodes m into v. +func decodePageMatcher(m any, v *PageMatcher) error { if err := mapstructure.WeakDecode(m, v); err != nil { return err } @@ -140,3 +214,14 @@ func DecodePageMatcher(m any, v *PageMatcher) error { return nil } + +type PageMatcherParamsConfig struct { + // Apply Params to all Pages matching Target. + Params maps.Params + Target PageMatcher +} + +func (p *PageMatcherParamsConfig) init() error { + maps.PrepareParams(p.Params) + return nil +} diff --git a/resources/page/page_matcher_test.go b/resources/page/page_matcher_test.go index 4a59dc50232..990312ed1eb 100644 --- a/resources/page/page_matcher_test.go +++ b/resources/page/page_matcher_test.go @@ -18,6 +18,7 @@ import ( "testing" "github.com/gohugoio/hugo/common/hugo" + "github.com/gohugoio/hugo/common/maps" qt "github.com/frankban/quicktest" ) @@ -71,13 +72,87 @@ func TestPageMatcher(t *testing.T) { c.Run("Decode", func(c *qt.C) { var v PageMatcher - c.Assert(DecodePageMatcher(map[string]any{"kind": "foo"}, &v), qt.Not(qt.IsNil)) - c.Assert(DecodePageMatcher(map[string]any{"kind": "{foo,bar}"}, &v), qt.Not(qt.IsNil)) - c.Assert(DecodePageMatcher(map[string]any{"kind": "taxonomy"}, &v), qt.IsNil) - c.Assert(DecodePageMatcher(map[string]any{"kind": "{taxonomy,foo}"}, &v), qt.IsNil) - c.Assert(DecodePageMatcher(map[string]any{"kind": "{taxonomy,term}"}, &v), qt.IsNil) - c.Assert(DecodePageMatcher(map[string]any{"kind": "*"}, &v), qt.IsNil) - c.Assert(DecodePageMatcher(map[string]any{"kind": "home", "path": filepath.FromSlash("/a/b/**")}, &v), qt.IsNil) + c.Assert(decodePageMatcher(map[string]any{"kind": "foo"}, &v), qt.Not(qt.IsNil)) + c.Assert(decodePageMatcher(map[string]any{"kind": "{foo,bar}"}, &v), qt.Not(qt.IsNil)) + c.Assert(decodePageMatcher(map[string]any{"kind": "taxonomy"}, &v), qt.IsNil) + c.Assert(decodePageMatcher(map[string]any{"kind": "{taxonomy,foo}"}, &v), qt.IsNil) + c.Assert(decodePageMatcher(map[string]any{"kind": "{taxonomy,term}"}, &v), qt.IsNil) + c.Assert(decodePageMatcher(map[string]any{"kind": "*"}, &v), qt.IsNil) + c.Assert(decodePageMatcher(map[string]any{"kind": "home", "path": filepath.FromSlash("/a/b/**")}, &v), qt.IsNil) c.Assert(v, qt.Equals, PageMatcher{Kind: "home", Path: "/a/b/**"}) }) + + c.Run("mapToPageMatcherParamsConfig", func(c *qt.C) { + fn := func(m map[string]any) PageMatcherParamsConfig { + v, err := mapToPageMatcherParamsConfig(m) + c.Assert(err, qt.IsNil) + return v + } + // Legacy. + c.Assert(fn(map[string]any{"_target": map[string]any{"kind": "page"}, "foo": "bar"}), qt.DeepEquals, PageMatcherParamsConfig{ + Params: maps.Params{ + "foo": "bar", + }, + Target: PageMatcher{Path: "", Kind: "page", Lang: "", Environment: ""}, + }) + + // Current format. + c.Assert(fn(map[string]any{"target": map[string]any{"kind": "page"}, "params": map[string]any{"foo": "bar"}}), qt.DeepEquals, PageMatcherParamsConfig{ + Params: maps.Params{ + "foo": "bar", + }, + Target: PageMatcher{Path: "", Kind: "page", Lang: "", Environment: ""}, + }) + }) +} + +func TestDecodeCascadeConfig(t *testing.T) { + c := qt.New(t) + + in := []map[string]any{ + { + "params": map[string]any{ + "a": "av", + }, + "target": map[string]any{ + "kind": "page", + "Environment": "production", + }, + }, + { + "params": map[string]any{ + "b": "bv", + }, + "target": map[string]any{ + "kind": "page", + }, + }, + } + + got, err := DecodeCascadeConfig(in) + + c.Assert(err, qt.IsNil) + c.Assert(got, qt.IsNotNil) + c.Assert(got.Config, qt.DeepEquals, + map[PageMatcher]maps.Params{ + {Path: "", Kind: "page", Lang: "", Environment: ""}: { + "b": "bv", + }, + {Path: "", Kind: "page", Lang: "", Environment: "production"}: { + "a": "av", + }, + }, + ) + c.Assert(got.SourceStructure, qt.DeepEquals, []PageMatcherParamsConfig{ + { + Params: maps.Params{"a": string("av")}, + Target: PageMatcher{Kind: "page", Environment: "production"}, + }, + {Params: maps.Params{"b": string("bv")}, Target: PageMatcher{Kind: "page"}}, + }) + + got, err = DecodeCascadeConfig(nil) + c.Assert(err, qt.IsNil) + c.Assert(got, qt.IsNotNil) + } diff --git a/resources/page/page_nop.go b/resources/page/page_nop.go index c04c019fe1c..59765ebf296 100644 --- a/resources/page/page_nop.go +++ b/resources/page/page_nop.go @@ -67,8 +67,8 @@ func (p *nopPage) Aliases() []string { return nil } -func (p *nopPage) Sitemap() config.Sitemap { - return config.Sitemap{} +func (p *nopPage) Sitemap() config.SitemapConfig { + return config.SitemapConfig{} } func (p *nopPage) Layout() string { @@ -217,7 +217,7 @@ func (p *nopPage) HasShortcode(name string) bool { return false } -func (p *nopPage) Hugo() (h hugo.Info) { +func (p *nopPage) Hugo() (h hugo.HugoInfo) { return } diff --git a/resources/page/page_paths_test.go b/resources/page/page_paths_test.go index 28937899f51..da711748043 100644 --- a/resources/page/page_paths_test.go +++ b/resources/page/page_paths_test.go @@ -27,7 +27,7 @@ import ( func TestPageTargetPath(t *testing.T) { pathSpec := newTestPathSpec() - noExtNoDelimMediaType := media.WithDelimiterAndSuffixes(media.TextType, "", "") + noExtNoDelimMediaType := media.WithDelimiterAndSuffixes(media.Builtin.TextType, "", "") noExtNoDelimMediaType.Delimiter = "" // Netlify style _redirects diff --git a/resources/page/pagemeta/page_frontmatter.go b/resources/page/pagemeta/page_frontmatter.go index bc82773e8c0..e7558bd431c 100644 --- a/resources/page/pagemeta/page_frontmatter.go +++ b/resources/page/pagemeta/page_frontmatter.go @@ -31,7 +31,7 @@ import ( // FrontMatterHandler maps front matter into Page fields and .Params. // Note that we currently have only extracted the date logic. type FrontMatterHandler struct { - fmConfig frontmatterConfig + fmConfig FrontmatterConfig dateHandler frontMatterFieldHandler lastModHandler frontMatterFieldHandler @@ -159,11 +159,15 @@ func (f FrontMatterHandler) newChainedFrontMatterFieldHandler(handlers ...frontM } } -type frontmatterConfig struct { - date []string - lastmod []string - publishDate []string - expiryDate []string +type FrontmatterConfig struct { + // Controls how the Date is set from front matter. + Date []string + // Controls how the Lastmod is set from front matter. + Lastmod []string + // Controls how the PublishDate is set from front matter. + PublishDate []string + // Controls how the ExpiryDate is set from front matter. + ExpiryDate []string } const ( @@ -185,16 +189,16 @@ const ( ) // This is the config you get when doing nothing. -func newDefaultFrontmatterConfig() frontmatterConfig { - return frontmatterConfig{ - date: []string{fmDate, fmPubDate, fmLastmod}, - lastmod: []string{fmGitAuthorDate, fmLastmod, fmDate, fmPubDate}, - publishDate: []string{fmPubDate, fmDate}, - expiryDate: []string{fmExpiryDate}, +func newDefaultFrontmatterConfig() FrontmatterConfig { + return FrontmatterConfig{ + Date: []string{fmDate, fmPubDate, fmLastmod}, + Lastmod: []string{fmGitAuthorDate, fmLastmod, fmDate, fmPubDate}, + PublishDate: []string{fmPubDate, fmDate}, + ExpiryDate: []string{fmExpiryDate}, } } -func newFrontmatterConfig(cfg config.Provider) (frontmatterConfig, error) { +func DecodeFrontMatterConfig(cfg config.Provider) (FrontmatterConfig, error) { c := newDefaultFrontmatterConfig() defaultConfig := c @@ -204,13 +208,13 @@ func newFrontmatterConfig(cfg config.Provider) (frontmatterConfig, error) { loki := strings.ToLower(k) switch loki { case fmDate: - c.date = toLowerSlice(v) + c.Date = toLowerSlice(v) case fmPubDate: - c.publishDate = toLowerSlice(v) + c.PublishDate = toLowerSlice(v) case fmLastmod: - c.lastmod = toLowerSlice(v) + c.Lastmod = toLowerSlice(v) case fmExpiryDate: - c.expiryDate = toLowerSlice(v) + c.ExpiryDate = toLowerSlice(v) } } } @@ -221,10 +225,10 @@ func newFrontmatterConfig(cfg config.Provider) (frontmatterConfig, error) { return out } - c.date = expander(c.date, defaultConfig.date) - c.publishDate = expander(c.publishDate, defaultConfig.publishDate) - c.lastmod = expander(c.lastmod, defaultConfig.lastmod) - c.expiryDate = expander(c.expiryDate, defaultConfig.expiryDate) + c.Date = expander(c.Date, defaultConfig.Date) + c.PublishDate = expander(c.PublishDate, defaultConfig.PublishDate) + c.Lastmod = expander(c.Lastmod, defaultConfig.Lastmod) + c.ExpiryDate = expander(c.ExpiryDate, defaultConfig.ExpiryDate) return c, nil } @@ -269,7 +273,7 @@ func NewFrontmatterHandler(logger loggers.Logger, cfg config.Provider) (FrontMat logger = loggers.NewErrorLogger() } - frontMatterConfig, err := newFrontmatterConfig(cfg) + frontMatterConfig, err := DecodeFrontMatterConfig(cfg) if err != nil { return FrontMatterHandler{}, err } @@ -283,10 +287,10 @@ func NewFrontmatterHandler(logger loggers.Logger, cfg config.Provider) (FrontMat } } - addKeys(frontMatterConfig.date) - addKeys(frontMatterConfig.expiryDate) - addKeys(frontMatterConfig.lastmod) - addKeys(frontMatterConfig.publishDate) + addKeys(frontMatterConfig.Date) + addKeys(frontMatterConfig.ExpiryDate) + addKeys(frontMatterConfig.Lastmod) + addKeys(frontMatterConfig.PublishDate) f := FrontMatterHandler{logger: logger, fmConfig: frontMatterConfig, allDateKeys: allDateKeys} @@ -300,7 +304,7 @@ func NewFrontmatterHandler(logger loggers.Logger, cfg config.Provider) (FrontMat func (f *FrontMatterHandler) createHandlers() error { var err error - if f.dateHandler, err = f.createDateHandler(f.fmConfig.date, + if f.dateHandler, err = f.createDateHandler(f.fmConfig.Date, func(d *FrontMatterDescriptor, t time.Time) { d.Dates.FDate = t setParamIfNotSet(fmDate, t, d) @@ -308,7 +312,7 @@ func (f *FrontMatterHandler) createHandlers() error { return err } - if f.lastModHandler, err = f.createDateHandler(f.fmConfig.lastmod, + if f.lastModHandler, err = f.createDateHandler(f.fmConfig.Lastmod, func(d *FrontMatterDescriptor, t time.Time) { setParamIfNotSet(fmLastmod, t, d) d.Dates.FLastmod = t @@ -316,7 +320,7 @@ func (f *FrontMatterHandler) createHandlers() error { return err } - if f.publishDateHandler, err = f.createDateHandler(f.fmConfig.publishDate, + if f.publishDateHandler, err = f.createDateHandler(f.fmConfig.PublishDate, func(d *FrontMatterDescriptor, t time.Time) { setParamIfNotSet(fmPubDate, t, d) d.Dates.FPublishDate = t @@ -324,7 +328,7 @@ func (f *FrontMatterHandler) createHandlers() error { return err } - if f.expiryDateHandler, err = f.createDateHandler(f.fmConfig.expiryDate, + if f.expiryDateHandler, err = f.createDateHandler(f.fmConfig.ExpiryDate, func(d *FrontMatterDescriptor, t time.Time) { setParamIfNotSet(fmExpiryDate, t, d) d.Dates.FExpiryDate = t diff --git a/resources/page/pagemeta/page_frontmatter_test.go b/resources/page/pagemeta/page_frontmatter_test.go index c5c4ccf2d02..5a94545faaf 100644 --- a/resources/page/pagemeta/page_frontmatter_test.go +++ b/resources/page/pagemeta/page_frontmatter_test.go @@ -83,21 +83,21 @@ func TestFrontMatterNewConfig(t *testing.T) { "publishDate": []string{"date"}, }) - fc, err := newFrontmatterConfig(cfg) + fc, err := DecodeFrontMatterConfig(cfg) c.Assert(err, qt.IsNil) - c.Assert(fc.date, qt.DeepEquals, []string{"publishdate", "pubdate", "published", "lastmod", "modified"}) - c.Assert(fc.lastmod, qt.DeepEquals, []string{"publishdate", "pubdate", "published"}) - c.Assert(fc.expiryDate, qt.DeepEquals, []string{"lastmod", "modified"}) - c.Assert(fc.publishDate, qt.DeepEquals, []string{"date"}) + c.Assert(fc.Date, qt.DeepEquals, []string{"publishdate", "pubdate", "published", "lastmod", "modified"}) + c.Assert(fc.Lastmod, qt.DeepEquals, []string{"publishdate", "pubdate", "published"}) + c.Assert(fc.ExpiryDate, qt.DeepEquals, []string{"lastmod", "modified"}) + c.Assert(fc.PublishDate, qt.DeepEquals, []string{"date"}) // Default cfg = config.New() - fc, err = newFrontmatterConfig(cfg) + fc, err = DecodeFrontMatterConfig(cfg) c.Assert(err, qt.IsNil) - c.Assert(fc.date, qt.DeepEquals, []string{"date", "publishdate", "pubdate", "published", "lastmod", "modified"}) - c.Assert(fc.lastmod, qt.DeepEquals, []string{":git", "lastmod", "modified", "date", "publishdate", "pubdate", "published"}) - c.Assert(fc.expiryDate, qt.DeepEquals, []string{"expirydate", "unpublishdate"}) - c.Assert(fc.publishDate, qt.DeepEquals, []string{"publishdate", "pubdate", "published", "date"}) + c.Assert(fc.Date, qt.DeepEquals, []string{"date", "publishdate", "pubdate", "published", "lastmod", "modified"}) + c.Assert(fc.Lastmod, qt.DeepEquals, []string{":git", "lastmod", "modified", "date", "publishdate", "pubdate", "published"}) + c.Assert(fc.ExpiryDate, qt.DeepEquals, []string{"expirydate", "unpublishdate"}) + c.Assert(fc.PublishDate, qt.DeepEquals, []string{"publishdate", "pubdate", "published", "date"}) // :default keyword cfg.Set("frontmatter", map[string]any{ @@ -106,12 +106,12 @@ func TestFrontMatterNewConfig(t *testing.T) { "expiryDate": []string{"d3", ":default"}, "publishDate": []string{"d4", ":default"}, }) - fc, err = newFrontmatterConfig(cfg) + fc, err = DecodeFrontMatterConfig(cfg) c.Assert(err, qt.IsNil) - c.Assert(fc.date, qt.DeepEquals, []string{"d1", "date", "publishdate", "pubdate", "published", "lastmod", "modified"}) - c.Assert(fc.lastmod, qt.DeepEquals, []string{"d2", ":git", "lastmod", "modified", "date", "publishdate", "pubdate", "published"}) - c.Assert(fc.expiryDate, qt.DeepEquals, []string{"d3", "expirydate", "unpublishdate"}) - c.Assert(fc.publishDate, qt.DeepEquals, []string{"d4", "publishdate", "pubdate", "published", "date"}) + c.Assert(fc.Date, qt.DeepEquals, []string{"d1", "date", "publishdate", "pubdate", "published", "lastmod", "modified"}) + c.Assert(fc.Lastmod, qt.DeepEquals, []string{"d2", ":git", "lastmod", "modified", "date", "publishdate", "pubdate", "published"}) + c.Assert(fc.ExpiryDate, qt.DeepEquals, []string{"d3", "expirydate", "unpublishdate"}) + c.Assert(fc.PublishDate, qt.DeepEquals, []string{"d4", "publishdate", "pubdate", "published", "date"}) } func TestFrontMatterDatesHandlers(t *testing.T) { diff --git a/resources/page/pages_language_merge.go b/resources/page/pages_language_merge.go index 4c5a926cfc5..aa2ec2e0d24 100644 --- a/resources/page/pages_language_merge.go +++ b/resources/page/pages_language_merge.go @@ -50,6 +50,7 @@ func (p1 Pages) MergeByLanguage(p2 Pages) Pages { // MergeByLanguageInterface is the generic version of MergeByLanguage. It // is here just so it can be called from the tpl package. +// This is for internal use. func (p1 Pages) MergeByLanguageInterface(in any) (any, error) { if in == nil { return p1, nil diff --git a/resources/page/site.go b/resources/page/site.go index 47bd770efaf..e784422c879 100644 --- a/resources/page/site.go +++ b/resources/page/site.go @@ -57,7 +57,7 @@ type Site interface { Current() Site // Returns a struct with some information about the build. - Hugo() hugo.Info + Hugo() hugo.HugoInfo // Returns the BaseURL for this Site. BaseURL() template.URL @@ -78,6 +78,7 @@ type Site interface { Data() map[string]any // Returns the identity of this site. + // This is for internal use only. GetIdentity() identity.Identity } @@ -93,11 +94,11 @@ func (s Sites) First() Site { } type testSite struct { - h hugo.Info + h hugo.HugoInfo l *langs.Language } -func (t testSite) Hugo() hugo.Info { +func (t testSite) Hugo() hugo.HugoInfo { return t.h } diff --git a/resources/page/testhelpers_test.go b/resources/page/testhelpers_test.go index 72f62ee8d32..b323f6d6425 100644 --- a/resources/page/testhelpers_test.go +++ b/resources/page/testhelpers_test.go @@ -171,8 +171,8 @@ func (p *testPage) Data() any { return p.data } -func (p *testPage) Sitemap() config.Sitemap { - return config.Sitemap{} +func (p *testPage) Sitemap() config.SitemapConfig { + return config.SitemapConfig{} } func (p *testPage) Layout() string { @@ -267,7 +267,7 @@ func (p *testPage) HasShortcode(name string) bool { panic("not implemented") } -func (p *testPage) Hugo() hugo.Info { +func (p *testPage) Hugo() hugo.HugoInfo { panic("not implemented") } diff --git a/resources/postpub/fields_test.go b/resources/postpub/fields_test.go index 8e80063f146..336da1f0e3e 100644 --- a/resources/postpub/fields_test.go +++ b/resources/postpub/fields_test.go @@ -17,14 +17,13 @@ import ( "testing" qt "github.com/frankban/quicktest" - "github.com/gohugoio/hugo/media" ) func TestCreatePlaceholders(t *testing.T) { c := qt.New(t) - m := structToMap(media.CSSType) + m := structToMap(media.Builtin.CSSType) insertFieldPlaceholders("foo", m, func(s string) string { return "pre_" + s + "_post" @@ -34,6 +33,7 @@ func TestCreatePlaceholders(t *testing.T) { "IsZero": "pre_foo.IsZero_post", "MarshalJSON": "pre_foo.MarshalJSON_post", "Suffixes": "pre_foo.Suffixes_post", + "SuffixesCSV": "pre_foo.SuffixesCSV_post", "Delimiter": "pre_foo.Delimiter_post", "FirstSuffix": "pre_foo.FirstSuffix_post", "IsText": "pre_foo.IsText_post", diff --git a/resources/resource.go b/resources/resource.go index 94016154a02..45dd22b2c44 100644 --- a/resources/resource.go +++ b/resources/resource.go @@ -154,7 +154,7 @@ type baseResourceInternal interface { ReadSeekCloser() (hugio.ReadSeekCloser, error) - // Internal + // For internal use. cloneWithUpdates(*transformationUpdate) (baseResource, error) tryTransformedFileCache(key string, u *transformationUpdate) io.ReadCloser diff --git a/resources/resource/resources.go b/resources/resource/resources.go index a877c890638..795fe19342c 100644 --- a/resources/resource/resources.go +++ b/resources/resource/resources.go @@ -144,6 +144,7 @@ func (r Resources) MergeByLanguage(r2 Resources) Resources { // MergeByLanguageInterface is the generic version of MergeByLanguage. It // is here just so it can be called from the tpl package. +// This is for internal use. func (r Resources) MergeByLanguageInterface(in any) (any, error) { r2, ok := in.(Resources) if !ok { diff --git a/resources/resource_factories/bundler/bundler.go b/resources/resource_factories/bundler/bundler.go index 7de2282270d..67f1f90fa86 100644 --- a/resources/resource_factories/bundler/bundler.go +++ b/resources/resource_factories/bundler/bundler.go @@ -88,8 +88,8 @@ func (c *Client) Concat(targetPath string, r resource.Resources) (resource.Resou // The given set of resources must be of the same Media Type. // We may improve on that in the future, but then we need to know more. for i, r := range r { - if i > 0 && r.MediaType().Type() != resolvedm.Type() { - return nil, fmt.Errorf("resources in Concat must be of the same Media Type, got %q and %q", r.MediaType().Type(), resolvedm.Type()) + if i > 0 && r.MediaType().Type != resolvedm.Type { + return nil, fmt.Errorf("resources in Concat must be of the same Media Type, got %q and %q", r.MediaType().Type, resolvedm.Type) } resolvedm = r.MediaType() } @@ -115,7 +115,7 @@ func (c *Client) Concat(targetPath string, r resource.Resources) (resource.Resou // Arbitrary JavaScript files require a barrier between them to be safely concatenated together. // Without this, the last line of one file can affect the first line of the next file and change how both files are interpreted. - if resolvedm.MainType == media.JavascriptType.MainType && resolvedm.SubType == media.JavascriptType.SubType { + if resolvedm.MainType == media.Builtin.JavascriptType.MainType && resolvedm.SubType == media.Builtin.JavascriptType.SubType { readers := make([]hugio.ReadSeekCloser, 2*len(rcsources)-1) j := 0 for i := 0; i < len(rcsources); i++ { diff --git a/resources/resource_metadata_test.go b/resources/resource_metadata_test.go index fa9659162da..aaef856dc4b 100644 --- a/resources/resource_metadata_test.go +++ b/resources/resource_metadata_test.go @@ -200,11 +200,11 @@ func TestAssignMetadata(t *testing.T) { }}, } { - foo2 = spec.newGenericResource(nil, nil, nil, "/b/foo2.css", "foo2.css", media.CSSType) + foo2 = spec.newGenericResource(nil, nil, nil, "/b/foo2.css", "foo2.css", media.Builtin.CSSType) logo2 = spec.newGenericResource(nil, nil, nil, "/b/Logo2.png", "Logo2.png", pngType) - foo1 = spec.newGenericResource(nil, nil, nil, "/a/foo1.css", "foo1.css", media.CSSType) + foo1 = spec.newGenericResource(nil, nil, nil, "/a/foo1.css", "foo1.css", media.Builtin.CSSType) logo1 = spec.newGenericResource(nil, nil, nil, "/a/logo1.png", "logo1.png", pngType) - foo3 = spec.newGenericResource(nil, nil, nil, "/b/foo3.css", "foo3.css", media.CSSType) + foo3 = spec.newGenericResource(nil, nil, nil, "/b/foo3.css", "foo3.css", media.Builtin.CSSType) logo3 = spec.newGenericResource(nil, nil, nil, "/b/logo3.png", "logo3.png", pngType) resources = resource.Resources{ diff --git a/resources/resource_spec.go b/resources/resource_spec.go index 8ef69318362..b9c91f884fb 100644 --- a/resources/resource_spec.go +++ b/resources/resource_spec.go @@ -88,7 +88,7 @@ func NewSpec( MediaTypes: mimeTypes, OutputFormats: outputFormats, Permalinks: permalinks, - BuildConfig: config.DecodeBuild(s.Cfg), + BuildConfig: config.DecodeBuildConfig(s.Cfg), FileCaches: fileCaches, PostBuildAssets: &PostBuildAssets{ PostProcessResources: make(map[string]postpub.PostPublishedResource), @@ -118,7 +118,7 @@ type Spec struct { TextTemplates tpl.TemplateParseFinder Permalinks page.PermalinkExpander - BuildConfig config.Build + BuildConfig config.BuildConfig // Holds default filter settings etc. imaging *images.ImageProcessor diff --git a/resources/resource_test.go b/resources/resource_test.go index 031c7b3c682..7cdeb716c93 100644 --- a/resources/resource_test.go +++ b/resources/resource_test.go @@ -33,7 +33,7 @@ func TestGenericResource(t *testing.T) { c := qt.New(t) spec := newTestResourceSpec(specDescriptor{c: c}) - r := spec.newGenericResource(nil, nil, nil, "/a/foo.css", "foo.css", media.CSSType) + r := spec.newGenericResource(nil, nil, nil, "/a/foo.css", "foo.css", media.Builtin.CSSType) c.Assert(r.Permalink(), qt.Equals, "https://example.com/foo.css") c.Assert(r.RelPermalink(), qt.Equals, "/foo.css") @@ -46,7 +46,7 @@ func TestGenericResourceWithLinkFactory(t *testing.T) { factory := newTargetPaths("/foo") - r := spec.newGenericResource(nil, factory, nil, "/a/foo.css", "foo.css", media.CSSType) + r := spec.newGenericResource(nil, factory, nil, "/a/foo.css", "foo.css", media.Builtin.CSSType) c.Assert(r.Permalink(), qt.Equals, "https://example.com/foo/foo.css") c.Assert(r.RelPermalink(), qt.Equals, "/foo/foo.css") @@ -101,10 +101,10 @@ func TestResourcesByType(t *testing.T) { c := qt.New(t) spec := newTestResourceSpec(specDescriptor{c: c}) resources := resource.Resources{ - spec.newGenericResource(nil, nil, nil, "/a/foo1.css", "foo1.css", media.CSSType), + spec.newGenericResource(nil, nil, nil, "/a/foo1.css", "foo1.css", media.Builtin.CSSType), spec.newGenericResource(nil, nil, nil, "/a/logo.png", "logo.css", pngType), - spec.newGenericResource(nil, nil, nil, "/a/foo2.css", "foo2.css", media.CSSType), - spec.newGenericResource(nil, nil, nil, "/a/foo3.css", "foo3.css", media.CSSType), + spec.newGenericResource(nil, nil, nil, "/a/foo2.css", "foo2.css", media.Builtin.CSSType), + spec.newGenericResource(nil, nil, nil, "/a/foo3.css", "foo3.css", media.Builtin.CSSType), } c.Assert(len(resources.ByType("text")), qt.Equals, 3) @@ -115,11 +115,11 @@ func TestResourcesGetByPrefix(t *testing.T) { c := qt.New(t) spec := newTestResourceSpec(specDescriptor{c: c}) resources := resource.Resources{ - spec.newGenericResource(nil, nil, nil, "/a/foo1.css", "foo1.css", media.CSSType), + spec.newGenericResource(nil, nil, nil, "/a/foo1.css", "foo1.css", media.Builtin.CSSType), spec.newGenericResource(nil, nil, nil, "/a/logo1.png", "logo1.png", pngType), spec.newGenericResource(nil, nil, nil, "/b/Logo2.png", "Logo2.png", pngType), - spec.newGenericResource(nil, nil, nil, "/b/foo2.css", "foo2.css", media.CSSType), - spec.newGenericResource(nil, nil, nil, "/b/foo3.css", "foo3.css", media.CSSType), + spec.newGenericResource(nil, nil, nil, "/b/foo2.css", "foo2.css", media.Builtin.CSSType), + spec.newGenericResource(nil, nil, nil, "/b/foo3.css", "foo3.css", media.Builtin.CSSType), } c.Assert(resources.GetMatch("asdf*"), qt.IsNil) @@ -144,14 +144,14 @@ func TestResourcesGetMatch(t *testing.T) { c := qt.New(t) spec := newTestResourceSpec(specDescriptor{c: c}) resources := resource.Resources{ - spec.newGenericResource(nil, nil, nil, "/a/foo1.css", "foo1.css", media.CSSType), + spec.newGenericResource(nil, nil, nil, "/a/foo1.css", "foo1.css", media.Builtin.CSSType), spec.newGenericResource(nil, nil, nil, "/a/logo1.png", "logo1.png", pngType), spec.newGenericResource(nil, nil, nil, "/b/Logo2.png", "Logo2.png", pngType), - spec.newGenericResource(nil, nil, nil, "/b/foo2.css", "foo2.css", media.CSSType), - spec.newGenericResource(nil, nil, nil, "/b/foo3.css", "foo3.css", media.CSSType), - spec.newGenericResource(nil, nil, nil, "/b/c/foo4.css", "c/foo4.css", media.CSSType), - spec.newGenericResource(nil, nil, nil, "/b/c/foo5.css", "c/foo5.css", media.CSSType), - spec.newGenericResource(nil, nil, nil, "/b/c/d/foo6.css", "c/d/foo6.css", media.CSSType), + spec.newGenericResource(nil, nil, nil, "/b/foo2.css", "foo2.css", media.Builtin.CSSType), + spec.newGenericResource(nil, nil, nil, "/b/foo3.css", "foo3.css", media.Builtin.CSSType), + spec.newGenericResource(nil, nil, nil, "/b/c/foo4.css", "c/foo4.css", media.Builtin.CSSType), + spec.newGenericResource(nil, nil, nil, "/b/c/foo5.css", "c/foo5.css", media.Builtin.CSSType), + spec.newGenericResource(nil, nil, nil, "/b/c/d/foo6.css", "c/d/foo6.css", media.Builtin.CSSType), } c.Assert(resources.GetMatch("logo*").RelPermalink(), qt.Equals, "/logo1.png") @@ -206,7 +206,7 @@ func BenchmarkResourcesMatchA100(b *testing.B) { a100 := strings.Repeat("a", 100) pattern := "a*a*a*a*a*a*a*a*b" - resources := resource.Resources{spec.newGenericResource(nil, nil, nil, "/a/"+a100, a100, media.CSSType)} + resources := resource.Resources{spec.newGenericResource(nil, nil, nil, "/a/"+a100, a100, media.Builtin.CSSType)} b.ResetTimer() for i := 0; i < b.N; i++ { @@ -221,17 +221,17 @@ func benchResources(b *testing.B) resource.Resources { for i := 0; i < 30; i++ { name := fmt.Sprintf("abcde%d_%d.css", i%5, i) - resources = append(resources, spec.newGenericResource(nil, nil, nil, "/a/"+name, name, media.CSSType)) + resources = append(resources, spec.newGenericResource(nil, nil, nil, "/a/"+name, name, media.Builtin.CSSType)) } for i := 0; i < 30; i++ { name := fmt.Sprintf("efghi%d_%d.css", i%5, i) - resources = append(resources, spec.newGenericResource(nil, nil, nil, "/a/"+name, name, media.CSSType)) + resources = append(resources, spec.newGenericResource(nil, nil, nil, "/a/"+name, name, media.Builtin.CSSType)) } for i := 0; i < 30; i++ { name := fmt.Sprintf("jklmn%d_%d.css", i%5, i) - resources = append(resources, spec.newGenericResource(nil, nil, nil, "/b/sub/"+name, "sub/"+name, media.CSSType)) + resources = append(resources, spec.newGenericResource(nil, nil, nil, "/b/sub/"+name, "sub/"+name, media.Builtin.CSSType)) } return resources @@ -258,7 +258,7 @@ func BenchmarkAssignMetadata(b *testing.B) { } for i := 0; i < 20; i++ { name := fmt.Sprintf("foo%d_%d.css", i%5, i) - resources = append(resources, spec.newGenericResource(nil, nil, nil, "/a/"+name, name, media.CSSType)) + resources = append(resources, spec.newGenericResource(nil, nil, nil, "/a/"+name, name, media.Builtin.CSSType)) } b.StartTimer() diff --git a/resources/resource_transformers/js/build.go b/resources/resource_transformers/js/build.go index 34bc2cc1291..98832a5b30b 100644 --- a/resources/resource_transformers/js/build.go +++ b/resources/resource_transformers/js/build.go @@ -27,12 +27,12 @@ import ( "github.com/spf13/afero" "github.com/gohugoio/hugo/hugofs" + "github.com/gohugoio/hugo/media" "github.com/gohugoio/hugo/common/herrors" "github.com/gohugoio/hugo/common/text" "github.com/gohugoio/hugo/hugolib/filesystems" - "github.com/gohugoio/hugo/media" "github.com/gohugoio/hugo/resources/internal" "github.com/evanw/esbuild/pkg/api" @@ -64,7 +64,7 @@ func (t *buildTransformation) Key() internal.ResourceTransformationKey { } func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx) error { - ctx.OutMediaType = media.JavascriptType + ctx.OutMediaType = media.Builtin.JavascriptType opts, err := decodeOptions(t.optsm) if err != nil { diff --git a/resources/resource_transformers/js/options.go b/resources/resource_transformers/js/options.go index 8b40648e7b4..1f57709cd95 100644 --- a/resources/resource_transformers/js/options.go +++ b/resources/resource_transformers/js/options.go @@ -337,20 +337,20 @@ func toBuildOptions(opts Options) (buildOptions api.BuildOptions, err error) { mediaType := opts.mediaType if mediaType.IsZero() { - mediaType = media.JavascriptType + mediaType = media.Builtin.JavascriptType } var loader api.Loader switch mediaType.SubType { // TODO(bep) ESBuild support a set of other loaders, but I currently fail // to see the relevance. That may change as we start using this. - case media.JavascriptType.SubType: + case media.Builtin.JavascriptType.SubType: loader = api.LoaderJS - case media.TypeScriptType.SubType: + case media.Builtin.TypeScriptType.SubType: loader = api.LoaderTS - case media.TSXType.SubType: + case media.Builtin.TSXType.SubType: loader = api.LoaderTSX - case media.JSXType.SubType: + case media.Builtin.JSXType.SubType: loader = api.LoaderJSX default: err = fmt.Errorf("unsupported Media Type: %q", opts.mediaType) diff --git a/resources/resource_transformers/js/options_test.go b/resources/resource_transformers/js/options_test.go index 135164d1848..a76a24caaab 100644 --- a/resources/resource_transformers/js/options_test.go +++ b/resources/resource_transformers/js/options_test.go @@ -18,11 +18,10 @@ import ( "testing" "github.com/gohugoio/hugo/hugofs" + "github.com/gohugoio/hugo/media" "github.com/spf13/afero" - "github.com/gohugoio/hugo/media" - "github.com/evanw/esbuild/pkg/api" qt "github.com/frankban/quicktest" @@ -46,7 +45,7 @@ func TestOptionKey(t *testing.T) { func TestToBuildOptions(t *testing.T) { c := qt.New(t) - opts, err := toBuildOptions(Options{mediaType: media.JavascriptType}) + opts, err := toBuildOptions(Options{mediaType: media.Builtin.JavascriptType}) c.Assert(err, qt.IsNil) c.Assert(opts, qt.DeepEquals, api.BuildOptions{ @@ -62,7 +61,7 @@ func TestToBuildOptions(t *testing.T) { Target: "es2018", Format: "cjs", Minify: true, - mediaType: media.JavascriptType, + mediaType: media.Builtin.JavascriptType, AvoidTDZ: true, }) c.Assert(err, qt.IsNil) @@ -79,7 +78,7 @@ func TestToBuildOptions(t *testing.T) { }) opts, err = toBuildOptions(Options{ - Target: "es2018", Format: "cjs", Minify: true, mediaType: media.JavascriptType, + Target: "es2018", Format: "cjs", Minify: true, mediaType: media.Builtin.JavascriptType, SourceMap: "inline", }) c.Assert(err, qt.IsNil) @@ -97,7 +96,7 @@ func TestToBuildOptions(t *testing.T) { }) opts, err = toBuildOptions(Options{ - Target: "es2018", Format: "cjs", Minify: true, mediaType: media.JavascriptType, + Target: "es2018", Format: "cjs", Minify: true, mediaType: media.Builtin.JavascriptType, SourceMap: "inline", }) c.Assert(err, qt.IsNil) @@ -115,7 +114,7 @@ func TestToBuildOptions(t *testing.T) { }) opts, err = toBuildOptions(Options{ - Target: "es2018", Format: "cjs", Minify: true, mediaType: media.JavascriptType, + Target: "es2018", Format: "cjs", Minify: true, mediaType: media.Builtin.JavascriptType, SourceMap: "external", }) c.Assert(err, qt.IsNil) diff --git a/resources/resource_transformers/tocss/dartsass/transform.go b/resources/resource_transformers/tocss/dartsass/transform.go index fdf4d8ef363..61ea54437ad 100644 --- a/resources/resource_transformers/tocss/dartsass/transform.go +++ b/resources/resource_transformers/tocss/dartsass/transform.go @@ -59,7 +59,7 @@ func (t *transform) Key() internal.ResourceTransformationKey { } func (t *transform) Transform(ctx *resources.ResourceTransformationCtx) error { - ctx.OutMediaType = media.CSSType + ctx.OutMediaType = media.Builtin.CSSType opts, err := decodeOptions(t.optsm) if err != nil { @@ -102,7 +102,7 @@ func (t *transform) Transform(ctx *resources.ResourceTransformationCtx) error { } } - if ctx.InMediaType.SubType == media.SASSType.SubType { + if ctx.InMediaType.SubType == media.Builtin.SASSType.SubType { args.SourceSyntax = godartsass.SourceSyntaxSASS } diff --git a/resources/resource_transformers/tocss/scss/tocss.go b/resources/resource_transformers/tocss/scss/tocss.go index 7e44f327e6f..d656d2b58d6 100644 --- a/resources/resource_transformers/tocss/scss/tocss.go +++ b/resources/resource_transformers/tocss/scss/tocss.go @@ -40,7 +40,7 @@ func Supports() bool { } func (t *toCSSTransformation) Transform(ctx *resources.ResourceTransformationCtx) error { - ctx.OutMediaType = media.CSSType + ctx.OutMediaType = media.Builtin.CSSType var outName string if t.options.from.TargetPath != "" { @@ -124,7 +124,7 @@ func (t *toCSSTransformation) Transform(ctx *resources.ResourceTransformationCtx return "", "", false } - if ctx.InMediaType.SubType == media.SASSType.SubType { + if ctx.InMediaType.SubType == media.Builtin.SASSType.SubType { options.to.SassSyntax = true } diff --git a/resources/testhelpers_test.go b/resources/testhelpers_test.go index 09268402e53..a1f60215e78 100644 --- a/resources/testhelpers_test.go +++ b/resources/testhelpers_test.go @@ -11,14 +11,14 @@ import ( "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/langs" + "github.com/gohugoio/hugo/media" "github.com/gohugoio/hugo/modules" + "github.com/gohugoio/hugo/output" qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/cache/filecache" "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/hugofs" - "github.com/gohugoio/hugo/media" - "github.com/gohugoio/hugo/output" "github.com/gohugoio/hugo/resources/images" "github.com/gohugoio/hugo/resources/page" "github.com/gohugoio/hugo/resources/resource" diff --git a/resources/transform.go b/resources/transform.go index fe438e36614..bc31179df81 100644 --- a/resources/transform.go +++ b/resources/transform.go @@ -447,7 +447,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()) + msg := fmt.Sprintf("%s: failed to transform %q (%s)", strings.ToUpper(tr.Key().Name), tctx.InPath, tctx.InMediaType.Type) if err == herrors.ErrFeatureNotAvailable { var errMsg string @@ -654,7 +654,7 @@ func (u *transformationUpdate) isContentChanged() bool { func (u *transformationUpdate) toTransformedResourceMetadata() transformedResourceMetadata { return transformedResourceMetadata{ - MediaTypeV: u.mediaType.Type(), + MediaTypeV: u.mediaType.Type, Target: u.targetPath, MetaData: u.data, } diff --git a/resources/transform_test.go b/resources/transform_test.go index c883e2593f4..f5d090bcc1e 100644 --- a/resources/transform_test.go +++ b/resources/transform_test.go @@ -25,11 +25,11 @@ import ( "testing" "github.com/gohugoio/hugo/htesting" + "github.com/gohugoio/hugo/media" "github.com/gohugoio/hugo/common/herrors" "github.com/gohugoio/hugo/hugofs" - "github.com/gohugoio/hugo/media" "github.com/gohugoio/hugo/resources/images" "github.com/gohugoio/hugo/resources/internal" @@ -100,7 +100,7 @@ func TestTransform(t *testing.T) { fmt.Fprint(ctx.To, in) // Media type - ctx.OutMediaType = media.CSVType + ctx.OutMediaType = media.Builtin.CSVType // Change target ctx.ReplaceOutPathExtension(".csv") @@ -120,7 +120,7 @@ func TestTransform(t *testing.T) { c.Assert(err, qt.IsNil) c.Assert(content, qt.Equals, "color is green") - c.Assert(tr.MediaType(), eq, media.CSVType) + c.Assert(tr.MediaType(), eq, media.Builtin.CSVType) c.Assert(tr.RelPermalink(), qt.Equals, "/f1.csv") assertShouldExist(c, spec, "public/f1.csv", true) @@ -139,7 +139,7 @@ func TestTransform(t *testing.T) { name: "test", transform: func(ctx *ResourceTransformationCtx) error { // Change media type only - ctx.OutMediaType = media.CSVType + ctx.OutMediaType = media.Builtin.CSVType ctx.ReplaceOutPathExtension(".csv") return nil @@ -154,7 +154,7 @@ func TestTransform(t *testing.T) { c.Assert(err, qt.IsNil) c.Assert(content, qt.Equals, "color is blue") - c.Assert(tr.MediaType(), eq, media.CSVType) + c.Assert(tr.MediaType(), eq, media.Builtin.CSVType) // The transformed file should only be published if RelPermalink // or Permalink is called. @@ -216,7 +216,7 @@ func TestTransform(t *testing.T) { in := helpers.ReaderToString(ctx.From) in = strings.Replace(in, "blue", "green", 1) ctx.AddOutPathIdentifier("." + "cached") - ctx.OutMediaType = media.CSVType + ctx.OutMediaType = media.Builtin.CSVType ctx.Data = map[string]any{ "Hugo": "Rocks!", } @@ -241,7 +241,7 @@ func TestTransform(t *testing.T) { content, err := tr.(resource.ContentProvider).Content(context.Background()) c.Assert(err, qt.IsNil) c.Assert(content, qt.Equals, "color is green", msg) - c.Assert(tr.MediaType(), eq, media.CSVType) + c.Assert(tr.MediaType(), eq, media.Builtin.CSVType) c.Assert(tr.Data(), qt.DeepEquals, map[string]any{ "Hugo": "Rocks!", }) @@ -270,7 +270,7 @@ func TestTransform(t *testing.T) { c.Assert(relPermalink, qt.Equals, "/f1.t1.txt") c.Assert(content, qt.Equals, "color is green") - c.Assert(tr.MediaType(), eq, media.TextType) + c.Assert(tr.MediaType(), eq, media.Builtin.TextType) assertNoDuplicateWrites(c, spec) assertShouldExist(c, spec, "public/f1.t1.txt", true) @@ -291,7 +291,7 @@ func TestTransform(t *testing.T) { c.Assert(err, qt.IsNil) c.Assert(content, qt.Equals, "car is green") - c.Assert(tr.MediaType(), eq, media.TextType) + c.Assert(tr.MediaType(), eq, media.Builtin.TextType) assertNoDuplicateWrites(c, spec) }) @@ -365,7 +365,7 @@ func TestTransform(t *testing.T) { tr, err := r.Transform(transformation) c.Assert(err, qt.IsNil) - c.Assert(tr.MediaType(), eq, media.PNGType) + c.Assert(tr.MediaType(), eq, media.Builtin.PNGType) img, ok := tr.(images.ImageResource) c.Assert(ok, qt.Equals, true) diff --git a/source/fileInfo.go b/source/fileInfo.go index 618498add3a..c58a0c3b908 100644 --- a/source/fileInfo.go +++ b/source/fileInfo.go @@ -96,6 +96,7 @@ type FileWithoutOverlap interface { // Hugo content files being one of them, considered to be unique. UniqueID() string + // For internal use only. FileInfo() hugofs.FileMetaInfo } @@ -182,6 +183,7 @@ func (fi *FileInfo) UniqueID() string { } // FileInfo returns a file's underlying os.FileInfo. +// For internal use only. func (fi *FileInfo) FileInfo() hugofs.FileMetaInfo { return fi.fi } func (fi *FileInfo) String() string { return fi.BaseFileName() } diff --git a/tpl/collections/apply_test.go b/tpl/collections/apply_test.go index 2c7783fd910..a9824bd4780 100644 --- a/tpl/collections/apply_test.go +++ b/tpl/collections/apply_test.go @@ -25,6 +25,7 @@ import ( "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/langs" "github.com/gohugoio/hugo/output" + "github.com/gohugoio/hugo/output/layouts" "github.com/gohugoio/hugo/tpl" ) @@ -46,7 +47,7 @@ func (templateFinder) LookupVariants(name string) []tpl.Template { return nil } -func (templateFinder) LookupLayout(d output.LayoutDescriptor, f output.Format) (tpl.Template, bool, error) { +func (templateFinder) LookupLayout(d layouts.LayoutDescriptor, f output.Format) (tpl.Template, bool, error) { return nil, false, nil } diff --git a/tpl/crypto/crypto.go b/tpl/crypto/crypto.go index d40ddbe84c3..c721d401b6b 100644 --- a/tpl/crypto/crypto.go +++ b/tpl/crypto/crypto.go @@ -70,6 +70,7 @@ func (ns *Namespace) SHA256(v any) (string, error) { } // FNV32a hashes v using fnv32a algorithm. +// {"newIn": "0.98.0" } func (ns *Namespace) FNV32a(v any) (int, error) { conv, err := cast.ToStringE(v) if err != nil { diff --git a/tpl/math/math.go b/tpl/math/math.go index a1c12425f49..67c6d06c5c5 100644 --- a/tpl/math/math.go +++ b/tpl/math/math.go @@ -208,6 +208,7 @@ var counter uint64 // have the needed precision (especially on Windows). // Note that given the parallel nature of Hugo, you cannot use this to get sequences of numbers, // and the counter will reset on new builds. +// {"identifiers": ["now.UnixNano"] } func (ns *Namespace) Counter() uint64 { return atomic.AddUint64(&counter, uint64(1)) } diff --git a/tpl/openapi/openapi3/openapi3.go b/tpl/openapi/openapi3/openapi3.go index 74c731f025d..38857dd9893 100644 --- a/tpl/openapi/openapi3/openapi3.go +++ b/tpl/openapi/openapi3/openapi3.go @@ -63,7 +63,7 @@ func (ns *Namespace) Unmarshal(r resource.UnmarshableResource) (*OpenAPIDocument } v, err := ns.cache.GetOrCreate(key, func() (any, error) { - f := metadecoders.FormatFromMediaType(r.MediaType()) + f := metadecoders.FormatFromStrings(r.MediaType().Suffixes()...) if f == "" { return nil, fmt.Errorf("MIME %q not supported", r.MediaType()) } diff --git a/tpl/partials/partials.go b/tpl/partials/partials.go index 32f86b3322c..fdb6d1178e0 100644 --- a/tpl/partials/partials.go +++ b/tpl/partials/partials.go @@ -26,6 +26,7 @@ import ( "github.com/bep/lazycache" "github.com/gohugoio/hugo/identity" + texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate" "github.com/gohugoio/hugo/tpl" diff --git a/tpl/strings/strings.go b/tpl/strings/strings.go index 6c6e9b9d3fd..4ba68dbc51c 100644 --- a/tpl/strings/strings.go +++ b/tpl/strings/strings.go @@ -163,6 +163,7 @@ func (ns *Namespace) ContainsAny(s, chars any) (bool, error) { // ContainsNonSpace reports whether s contains any non-space characters as defined // by Unicode's White Space property, +// {"newIn": "0.111.0" } func (ns *Namespace) ContainsNonSpace(s any) bool { ss := cast.ToString(s) diff --git a/tpl/template.go b/tpl/template.go index f71de8bb2a0..446c2eb9c5c 100644 --- a/tpl/template.go +++ b/tpl/template.go @@ -23,6 +23,7 @@ import ( "unicode" bp "github.com/gohugoio/hugo/bufferpool" + "github.com/gohugoio/hugo/output/layouts" "github.com/gohugoio/hugo/output" @@ -60,7 +61,7 @@ type UnusedTemplatesProvider interface { type TemplateHandler interface { TemplateFinder ExecuteWithContext(ctx context.Context, t Template, wr io.Writer, data any) error - LookupLayout(d output.LayoutDescriptor, f output.Format) (Template, bool, error) + LookupLayout(d layouts.LayoutDescriptor, f output.Format) (Template, bool, error) HasTemplate(name string) bool } diff --git a/tpl/tplimpl/template.go b/tpl/tplimpl/template.go index 53e6b4902f9..731f5f87fd7 100644 --- a/tpl/tplimpl/template.go +++ b/tpl/tplimpl/template.go @@ -31,6 +31,7 @@ import ( "unicode/utf8" "github.com/gohugoio/hugo/common/types" + "github.com/gohugoio/hugo/output/layouts" "github.com/gohugoio/hugo/helpers" @@ -156,7 +157,7 @@ func newTemplateExec(d *deps.Deps) (*templateExec, error) { main: newTemplateNamespace(funcMap), Deps: d, - layoutHandler: output.NewLayoutHandler(), + layoutHandler: layouts.NewLayoutHandler(), layoutsFs: d.BaseFs.Layouts.Fs, layoutTemplateCache: make(map[layoutCacheKey]layoutCacheEntry), @@ -211,7 +212,7 @@ func newTemplateState(templ tpl.Template, info templateInfo) *templateState { } type layoutCacheKey struct { - d output.LayoutDescriptor + d layouts.LayoutDescriptor f string } @@ -250,6 +251,7 @@ func (t *templateExec) ExecuteWithContext(ctx context.Context, templ tpl.Templat if t.templateUsageTracker != nil { if ts, ok := templ.(*templateState); ok { + t.templateUsageTrackerMu.Lock() if _, found := t.templateUsageTracker[ts.Name()]; !found { t.templateUsageTracker[ts.Name()] = ts.info @@ -335,7 +337,7 @@ type templateHandler struct { // stored in the root of this filesystem. layoutsFs afero.Fs - layoutHandler *output.LayoutHandler + layoutHandler *layouts.LayoutHandler layoutTemplateCache map[layoutCacheKey]layoutCacheEntry layoutTemplateCacheMu sync.RWMutex @@ -392,7 +394,7 @@ func (t *templateHandler) Lookup(name string) (tpl.Template, bool) { return nil, false } -func (t *templateHandler) LookupLayout(d output.LayoutDescriptor, f output.Format) (tpl.Template, bool, error) { +func (t *templateHandler) LookupLayout(d layouts.LayoutDescriptor, f output.Format) (tpl.Template, bool, error) { key := layoutCacheKey{d, f.Name} t.layoutTemplateCacheMu.RLock() if cacheVal, found := t.layoutTemplateCache[key]; found { @@ -459,8 +461,10 @@ func (t *templateHandler) HasTemplate(name string) bool { return found } -func (t *templateHandler) findLayout(d output.LayoutDescriptor, f output.Format) (tpl.Template, bool, error) { - layouts, _ := t.layoutHandler.For(d, f) +func (t *templateHandler) findLayout(d layouts.LayoutDescriptor, f output.Format) (tpl.Template, bool, error) { + d.OutputFormatName = f.Name + d.Suffix = f.MediaType.FirstSuffix.Suffix + layouts, _ := t.layoutHandler.For(d) for _, name := range layouts { templ, found := t.main.Lookup(name) if found { @@ -474,7 +478,7 @@ func (t *templateHandler) findLayout(d output.LayoutDescriptor, f output.Format) } d.Baseof = true - baseLayouts, _ := t.layoutHandler.For(d, f) + baseLayouts, _ := t.layoutHandler.For(d) var base templateInfo found = false for _, l := range baseLayouts { diff --git a/tpl/transform/unmarshal.go b/tpl/transform/unmarshal.go index f5ff635850a..3936126cadd 100644 --- a/tpl/transform/unmarshal.go +++ b/tpl/transform/unmarshal.go @@ -72,7 +72,7 @@ func (ns *Namespace) Unmarshal(args ...any) (any, error) { } return ns.cache.GetOrCreate(key, func() (any, error) { - f := metadecoders.FormatFromMediaType(r.MediaType()) + f := metadecoders.FormatFromStrings(r.MediaType().Suffixes()...) if f == "" { return nil, fmt.Errorf("MIME %q not supported", r.MediaType()) } diff --git a/tpl/transform/unmarshal_test.go b/tpl/transform/unmarshal_test.go index e63f96de2a0..12774298a94 100644 --- a/tpl/transform/unmarshal_test.go +++ b/tpl/transform/unmarshal_test.go @@ -105,26 +105,26 @@ func TestUnmarshal(t *testing.T) { {`slogan = "Hugo Rocks!"`, nil, func(m map[string]any) { assertSlogan(m) }}, - {testContentResource{key: "r1", content: `slogan: "Hugo Rocks!"`, mime: media.YAMLType}, nil, func(m map[string]any) { + {testContentResource{key: "r1", content: `slogan: "Hugo Rocks!"`, mime: media.Builtin.YAMLType}, nil, func(m map[string]any) { assertSlogan(m) }}, - {testContentResource{key: "r1", content: `{ "slogan": "Hugo Rocks!" }`, mime: media.JSONType}, nil, func(m map[string]any) { + {testContentResource{key: "r1", content: `{ "slogan": "Hugo Rocks!" }`, mime: media.Builtin.JSONType}, nil, func(m map[string]any) { assertSlogan(m) }}, - {testContentResource{key: "r1", content: `slogan = "Hugo Rocks!"`, mime: media.TOMLType}, nil, func(m map[string]any) { + {testContentResource{key: "r1", content: `slogan = "Hugo Rocks!"`, mime: media.Builtin.TOMLType}, nil, func(m map[string]any) { assertSlogan(m) }}, - {testContentResource{key: "r1", content: `Hugo Rocks!"`, mime: media.XMLType}, nil, func(m map[string]any) { + {testContentResource{key: "r1", content: `Hugo Rocks!"`, mime: media.Builtin.XMLType}, nil, func(m map[string]any) { assertSlogan(m) }}, {testContentResource{key: "r1", content: `1997,Ford,E350,"ac, abs, moon",3000.00 -1999,Chevy,"Venture ""Extended Edition""","",4900.00`, mime: media.CSVType}, nil, func(r [][]string) { +1999,Chevy,"Venture ""Extended Edition""","",4900.00`, mime: media.Builtin.CSVType}, nil, func(r [][]string) { b.Assert(len(r), qt.Equals, 2) first := r[0] b.Assert(len(first), qt.Equals, 5) b.Assert(first[1], qt.Equals, "Ford") }}, - {testContentResource{key: "r1", content: `a;b;c`, mime: media.CSVType}, map[string]any{"delimiter": ";"}, func(r [][]string) { + {testContentResource{key: "r1", content: `a;b;c`, mime: media.Builtin.CSVType}, map[string]any{"delimiter": ";"}, func(r [][]string) { b.Assert([][]string{{"a", "b", "c"}}, qt.DeepEquals, r) }}, {"a,b,c", nil, func(r [][]string) { @@ -135,13 +135,13 @@ func TestUnmarshal(t *testing.T) { }}, {testContentResource{key: "r1", content: ` % This is a comment -a;b;c`, mime: media.CSVType}, map[string]any{"DElimiter": ";", "Comment": "%"}, func(r [][]string) { +a;b;c`, mime: media.Builtin.CSVType}, map[string]any{"DElimiter": ";", "Comment": "%"}, func(r [][]string) { b.Assert([][]string{{"a", "b", "c"}}, qt.DeepEquals, r) }}, // errors {"thisisnotavaliddataformat", nil, false}, - {testContentResource{key: "r1", content: `invalid&toml"`, mime: media.TOMLType}, nil, false}, - {testContentResource{key: "r1", content: `unsupported: MIME"`, mime: media.CalendarType}, nil, false}, + {testContentResource{key: "r1", content: `invalid&toml"`, mime: media.Builtin.TOMLType}, nil, false}, + {testContentResource{key: "r1", content: `unsupported: MIME"`, mime: media.Builtin.CalendarType}, nil, false}, {"thisisnotavaliddataformat", nil, false}, {`{ notjson }`, nil, false}, {tstNoStringer{}, nil, false}, @@ -217,7 +217,7 @@ func BenchmarkUnmarshalResource(b *testing.B) { var jsons [numJsons]testContentResource for i := 0; i < numJsons; i++ { key := fmt.Sprintf("root%d", i) - jsons[i] = testContentResource{key: key, content: strings.Replace(testJSON, "ROOT_KEY", key, 1), mime: media.JSONType} + jsons[i] = testContentResource{key: key, content: strings.Replace(testJSON, "ROOT_KEY", key, 1), mime: media.Builtin.JSONType} } b.ResetTimer()