diff --git a/commands/gendocshelper.go b/commands/gendocshelper.go index c243581f..68ac035e 100644 --- a/commands/gendocshelper.go +++ b/commands/gendocshelper.go @@ -64,7 +64,7 @@ func (g *genDocsHelper) generate() error { enc := json.NewEncoder(f) enc.SetIndent("", " ") - if err := enc.Encode(docshelper.DocProviders); err != nil { + if err := enc.Encode(docshelper.GetDocProvider()); err != nil { return err } diff --git a/commands/hugo.go b/commands/hugo.go index 84131868..b7392bc4 100644 --- a/commands/hugo.go +++ b/commands/hugo.go @@ -231,11 +231,6 @@ func initializeFlags(cmd *cobra.Command, cfg config.Provider) { "duplicateTargetPaths", } - // Will set a value even if it is the default. - flagKeysForced := []string{ - "minify", - } - for _, key := range persFlagKeys { setValueFromFlag(cmd.PersistentFlags(), key, cfg, "", false) } @@ -243,9 +238,7 @@ func initializeFlags(cmd *cobra.Command, cfg config.Provider) { setValueFromFlag(cmd.Flags(), key, cfg, "", false) } - for _, key := range flagKeysForced { - setValueFromFlag(cmd.Flags(), key, cfg, "", true) - } + setValueFromFlag(cmd.Flags(), "minify", cfg, "minifyOutput", true) // Set some "config aliases" setValueFromFlag(cmd.Flags(), "destination", cfg, "publishDir", false) diff --git a/docs/content/en/getting-started/configuration.md b/docs/content/en/getting-started/configuration.md index e4a788e2..d23adb44 100644 --- a/docs/content/en/getting-started/configuration.md +++ b/docs/content/en/getting-started/configuration.md @@ -193,6 +193,9 @@ markup menu : See [Add Non-content Entries to a Menu](/content-management/menus/#add-non-content-entries-to-a-menu). +minify +: See [Configure Minify](#configure-minify) + module : Module config see [Module Config](/hugo-modules/configuration/).{{< new-in "0.56.0" >}} @@ -481,6 +484,14 @@ The above will try first to extract the value for `.Date` from the filename, the Hugo v0.20 introduced the ability to render your content to multiple output formats (e.g., to JSON, AMP html, or CSV). See [Output Formats][] for information on how to add these values to your Hugo project's configuration file. +## Configure Minify + +{{< new-in "0.68.0" >}} + +Default configuration: + +{{< code-toggle config="minify" />}} + ## Configure File Caches Since Hugo 0.52 you can configure more than just the `cacheDir`. This is the default configuration: diff --git a/docs/data/docs.json b/docs/data/docs.json index 2af97631..9a5b4767 100644 --- a/docs/data/docs.json +++ b/docs/data/docs.json @@ -1415,7 +1415,7 @@ "goldmark": { "renderer": { "hardWraps": false, - "xHTML": false, + "xhtml": false, "unsafe": false }, "parser": { @@ -1452,14 +1452,15 @@ "footnoteReturnLinkContents": "" } }, - "minifiers": { + "minify": { + "minifyOutput": false, + "disableHTML": false, + "disableCSS": false, + "disableJS": false, + "disableJSON": false, + "disableSVG": false, + "disableXML": false, "tdewolff": { - "enableHtml": true, - "enableCss": true, - "enableJs": true, - "enableJson": true, - "enableSvg": true, - "enableXml": true, "html": { "keepConditionalComments": true, "keepDefaultAttrVals": true, diff --git a/docshelper/docs.go b/docshelper/docs.go index 17e0ccd9..999e14d7 100644 --- a/docshelper/docs.go +++ b/docshelper/docs.go @@ -15,37 +15,37 @@ // is of limited interest for the general Hugo user. package docshelper -import ( - "encoding/json" +type ( + DocProviderFunc = func() DocProvider + DocProvider map[string]map[string]interface{} ) -// DocProviders contains all DocProviders added to the system. -var DocProviders = make(map[string]DocProvider) +var docProviderFuncs []DocProviderFunc -// AddDocProvider adds or updates the DocProvider for a given name. -func AddDocProvider(name string, provider DocProvider) { - if prev, ok := DocProviders[name]; !ok { - DocProviders[name] = provider - } else { - DocProviders[name] = merge(prev, provider) - } +func AddDocProviderFunc(fn DocProviderFunc) { + docProviderFuncs = append(docProviderFuncs, fn) } -// DocProvider is used to save arbitrary JSON data -// used for the generation of the documentation. -type DocProvider func() map[string]interface{} +func GetDocProvider() DocProvider { + provider := make(DocProvider) -// MarshalJSON returns a JSON representation of the DocProvider. -func (d DocProvider) MarshalJSON() ([]byte, error) { - return json.MarshalIndent(d(), "", " ") + for _, fn := range docProviderFuncs { + p := fn() + for k, v := range p { + if prev, found := provider[k]; !found { + provider[k] = v + } else { + merge(prev, v) + } + } + } + + return provider } -func merge(a, b DocProvider) DocProvider { - next := a() - for k, v := range b() { - next[k] = v - } - return func() map[string]interface{} { - return next +// Shallow merge +func merge(dst, src map[string]interface{}) { + for k, v := range src { + dst[k] = v } } diff --git a/helpers/docshelper.go b/helpers/docshelper.go index 66cbfa7d..1397acc5 100644 --- a/helpers/docshelper.go +++ b/helpers/docshelper.go @@ -12,8 +12,7 @@ import ( // This is is just some helpers used to create some JSON used in the Hugo docs. func init() { - docsProvider := func() map[string]interface{} { - docs := make(map[string]interface{}) + docsProvider := func() docshelper.DocProvider { var chromaLexers []interface{} @@ -48,11 +47,11 @@ func init() { chromaLexers = append(chromaLexers, lexerEntry) - docs["lexers"] = chromaLexers } - return docs + + return docshelper.DocProvider{"chroma": map[string]interface{}{"lexers": chromaLexers}} } - docshelper.AddDocProvider("chroma", docsProvider) + docshelper.AddDocProviderFunc(docsProvider) } diff --git a/hugolib/resource_chain_test.go b/hugolib/resource_chain_test.go index 39279b5b..8bca6c7b 100644 --- a/hugolib/resource_chain_test.go +++ b/hugolib/resource_chain_test.go @@ -947,3 +947,33 @@ class-in-b { build("never", true) } + +func TestResourceMinifyDisabled(t *testing.T) { + t.Parallel() + + b := newTestSitesBuilder(t).WithConfigFile("toml", ` +baseURL = "https://example.org" + +[minify] +disableXML=true + + +`) + + b.WithContent("page.md", "") + + b.WithSourceFile( + "assets/xml/data.xml", " asdfasdf ", + ) + + b.WithTemplates("index.html", ` +{{ $xml := resources.Get "xml/data.xml" | minify | fingerprint }} +XML: {{ $xml.Content | safeHTML }}|{{ $xml.RelPermalink }} +`) + + b.Build(BuildCfg{}) + + b.AssertFileContent("public/index.html", ` +XML: asdfasdf |/xml/data.min.3be4fddd19aaebb18c48dd6645215b822df74701957d6d36e59f203f9c30fd9f.xml +`) +} diff --git a/markup/markup_config/config.go b/markup/markup_config/config.go index 34c8807a..a94c11f0 100644 --- a/markup/markup_config/config.go +++ b/markup/markup_config/config.go @@ -94,11 +94,8 @@ var Default = Config{ } func init() { - docsProvider := func() map[string]interface{} { - docs := make(map[string]interface{}) - docs["markup"] = parser.LowerCaseCamelJSONMarshaller{Value: Default} - return docs - + docsProvider := func() docshelper.DocProvider { + return docshelper.DocProvider{"config": map[string]interface{}{"markup": parser.LowerCaseCamelJSONMarshaller{Value: Default}}} } - docshelper.AddDocProvider("config", docsProvider) + docshelper.AddDocProviderFunc(docsProvider) } diff --git a/media/docshelper.go b/media/docshelper.go index f5afb52f..de9dbe59 100644 --- a/media/docshelper.go +++ b/media/docshelper.go @@ -6,12 +6,8 @@ import ( // This is is just some helpers used to create some JSON used in the Hugo docs. func init() { - docsProvider := func() map[string]interface{} { - docs := make(map[string]interface{}) - - docs["types"] = DefaultTypes - return docs + docsProvider := func() docshelper.DocProvider { + return docshelper.DocProvider{"media": map[string]interface{}{"types": DefaultTypes}} } - - docshelper.AddDocProvider("media", docsProvider) + docshelper.AddDocProviderFunc(docsProvider) } diff --git a/minifiers/config.go b/minifiers/config.go index 20d122e9..5ee3aa2f 100644 --- a/minifiers/config.go +++ b/minifiers/config.go @@ -14,6 +14,7 @@ package minifiers import ( + "github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/docshelper" "github.com/gohugoio/hugo/parser" @@ -61,36 +62,43 @@ type tdewolffConfig struct { XML xml.Minifier } -type minifiersConfig struct { - EnableHTML bool - EnableCSS bool - EnableJS bool - EnableJSON bool - EnableSVG bool - EnableXML bool +type minifyConfig struct { + // Whether to minify the published output (the HTML written to /public). + MinifyOutput bool + + DisableHTML bool + DisableCSS bool + DisableJS bool + DisableJSON bool + DisableSVG bool + DisableXML bool Tdewolff tdewolffConfig } -var defaultConfig = minifiersConfig{ - EnableHTML: true, - EnableCSS: true, - EnableJS: true, - EnableJSON: true, - EnableSVG: true, - EnableXML: true, - +var defaultConfig = minifyConfig{ Tdewolff: defaultTdewolffConfig, } -func decodeConfig(cfg config.Provider) (conf minifiersConfig, err error) { +func decodeConfig(cfg config.Provider) (conf minifyConfig, err error) { conf = defaultConfig - m := cfg.GetStringMap("minifiers") - if m == nil { + // May be set by CLI. + conf.MinifyOutput = cfg.GetBool("minifyOutput") + + v := cfg.Get("minify") + if v == nil { return } + // Legacy. + if b, ok := v.(bool); ok { + conf.MinifyOutput = b + return + } + + m := maps.ToStringMap(v) + err = mapstructure.WeakDecode(m, &conf) if err != nil { @@ -101,11 +109,8 @@ func decodeConfig(cfg config.Provider) (conf minifiersConfig, err error) { } func init() { - docsProvider := func() map[string]interface{} { - docs := make(map[string]interface{}) - docs["minifiers"] = parser.LowerCaseCamelJSONMarshaller{Value: defaultConfig} - return docs - + docsProvider := func() docshelper.DocProvider { + return docshelper.DocProvider{"config": map[string]interface{}{"minify": parser.LowerCaseCamelJSONMarshaller{Value: defaultConfig}}} } - docshelper.AddDocProvider("config", docsProvider) + docshelper.AddDocProviderFunc(docsProvider) } diff --git a/minifiers/config_test.go b/minifiers/config_test.go index b4f68f8c..f90bad99 100644 --- a/minifiers/config_test.go +++ b/minifiers/config_test.go @@ -14,7 +14,6 @@ package minifiers import ( - "fmt" "testing" "github.com/spf13/viper" @@ -26,8 +25,8 @@ func TestConfig(t *testing.T) { c := qt.New(t) v := viper.New() - v.Set("minifiers", map[string]interface{}{ - "enablexml": false, + v.Set("minify", map[string]interface{}{ + "disablexml": true, "tdewolff": map[string]interface{}{ "html": map[string]interface{}{ "keepwhitespace": false, @@ -36,10 +35,11 @@ func TestConfig(t *testing.T) { }) conf, err := decodeConfig(v) - fmt.Println(conf) c.Assert(err, qt.IsNil) + c.Assert(conf.MinifyOutput, qt.Equals, false) + // explicitly set value c.Assert(conf.Tdewolff.HTML.KeepWhitespace, qt.Equals, false) // default value @@ -47,6 +47,19 @@ func TestConfig(t *testing.T) { c.Assert(conf.Tdewolff.CSS.KeepCSS2, qt.Equals, true) // `enable` flags - c.Assert(conf.EnableHTML, qt.Equals, true) - c.Assert(conf.EnableXML, qt.Equals, false) + c.Assert(conf.DisableHTML, qt.Equals, false) + c.Assert(conf.DisableXML, qt.Equals, true) +} + +func TestConfigLegacy(t *testing.T) { + c := qt.New(t) + v := viper.New() + + // This was a bool < Hugo v0.58. + v.Set("minify", true) + + 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 3feb8160..e76e56af 100644 --- a/minifiers/minifiers.go +++ b/minifiers/minifiers.go @@ -30,6 +30,9 @@ import ( // Client wraps a minifier. type Client struct { + // Whether output minification is enabled (HTML in /public) + MinifyOutput bool + m *minify.M } @@ -62,30 +65,30 @@ func New(mediaTypes media.Types, outputFormats output.Formats, cfg config.Provid m := minify.New() if err != nil { - return Client{m: m}, err + return Client{}, err } // We use the Type definition of the media types defined in the site if found. - if conf.EnableCSS { + if !conf.DisableCSS { addMinifier(m, mediaTypes, "css", &conf.Tdewolff.CSS) } - if conf.EnableJS { + if !conf.DisableJS { addMinifier(m, mediaTypes, "js", &conf.Tdewolff.JS) m.AddRegexp(regexp.MustCompile("^(application|text)/(x-)?(java|ecma)script$"), &conf.Tdewolff.JS) } - if conf.EnableJSON { + if !conf.DisableJSON { addMinifier(m, mediaTypes, "json", &conf.Tdewolff.JSON) m.AddRegexp(regexp.MustCompile(`^(application|text)/(x-|ld\+)?json$`), &conf.Tdewolff.JSON) } - if conf.EnableSVG { + if !conf.DisableSVG { addMinifier(m, mediaTypes, "svg", &conf.Tdewolff.SVG) } - if conf.EnableXML { + if !conf.DisableXML { addMinifier(m, mediaTypes, "xml", &conf.Tdewolff.XML) } // HTML - if conf.EnableHTML { + if !conf.DisableHTML { addMinifier(m, mediaTypes, "html", &conf.Tdewolff.HTML) for _, of := range outputFormats { if of.IsHTML { @@ -94,7 +97,7 @@ func New(mediaTypes media.Types, outputFormats output.Formats, cfg config.Provid } } - return Client{m: m}, nil + return Client{m: m, MinifyOutput: conf.MinifyOutput}, nil } func addMinifier(m *minify.M, mt media.Types, suffix string, min minify.Minifier) { diff --git a/minifiers/minifiers_test.go b/minifiers/minifiers_test.go index 64588f83..fb222fd6 100644 --- a/minifiers/minifiers_test.go +++ b/minifiers/minifiers_test.go @@ -75,11 +75,11 @@ func TestNew(t *testing.T) { } -func TestConfiguredMinify(t *testing.T) { +func TestConfigureMinify(t *testing.T) { c := qt.New(t) v := viper.New() - v.Set("minifiers", map[string]interface{}{ - "enablexml": false, + v.Set("minify", map[string]interface{}{ + "disablexml": true, "tdewolff": map[string]interface{}{ "html": map[string]interface{}{ "keepwhitespace": true, diff --git a/output/docshelper.go b/output/docshelper.go index f08b20b0..13291ce9 100644 --- a/output/docshelper.go +++ b/output/docshelper.go @@ -10,15 +10,16 @@ import ( // This is is just some helpers used to create some JSON used in the Hugo docs. func init() { - docsProvider := func() map[string]interface{} { - docs := make(map[string]interface{}) - - docs["formats"] = DefaultFormats - docs["layouts"] = createLayoutExamples() - return docs + docsProvider := func() docshelper.DocProvider { + return docshelper.DocProvider{ + "output": map[string]interface{}{ + "formats": DefaultFormats, + "layouts": createLayoutExamples(), + }, + } } - docshelper.AddDocProvider("output", docsProvider) + docshelper.AddDocProviderFunc(docsProvider) } func createLayoutExamples() interface{} { diff --git a/parser/lowercase_camel_json.go b/parser/lowercase_camel_json.go index e7aeb2ab..6994d121 100644 --- a/parser/lowercase_camel_json.go +++ b/parser/lowercase_camel_json.go @@ -14,6 +14,7 @@ package parser import ( + "bytes" "encoding/json" "regexp" "unicode" @@ -35,6 +36,12 @@ func (c LowerCaseCamelJSONMarshaller) MarshalJSON() ([]byte, error) { converted := keyMatchRegex.ReplaceAllFunc( marshalled, func(match []byte) []byte { + + // Attributes on the form XML, JSON etc. + if bytes.Equal(match, bytes.ToUpper(match)) { + return bytes.ToLower(match) + } + // Empty keys are valid JSON, only lowercase if we do not have an // empty key. if len(match) > 2 { diff --git a/publisher/publisher.go b/publisher/publisher.go index e1179572..f30073c0 100644 --- a/publisher/publisher.go +++ b/publisher/publisher.go @@ -68,21 +68,16 @@ type Descriptor struct { // DestinationPublisher is the default and currently only publisher in Hugo. This // publisher prepares and publishes an item to the defined destination, e.g. /public. type DestinationPublisher struct { - fs afero.Fs - minify bool - min minifiers.Client + fs afero.Fs + min minifiers.Client } // NewDestinationPublisher creates a new DestinationPublisher. func NewDestinationPublisher(fs afero.Fs, outputFormats output.Formats, mediaTypes media.Types, cfg config.Provider) (pub DestinationPublisher, err error) { pub = DestinationPublisher{fs: fs} - minify := cfg.GetBool("minify") - if minify { - pub.min, err = minifiers.New(mediaTypes, outputFormats, cfg) - if err != nil { - return - } - pub.minify = true + pub.min, err = minifiers.New(mediaTypes, outputFormats, cfg) + if err != nil { + return } return } @@ -155,7 +150,7 @@ func (p DestinationPublisher) createTransformerChain(f Descriptor) transform.Cha } - if p.minify { + if p.min.MinifyOutput { minifyTransformer := p.min.Transformer(f.OutputFormat.MediaType) if minifyTransformer != nil { transformers = append(transformers, minifyTransformer) diff --git a/resources/resource_transformers/minifier/minify_test.go b/resources/resource_transformers/minifier/minify_test.go index a1b22f10..38828c17 100644 --- a/resources/resource_transformers/minifier/minify_test.go +++ b/resources/resource_transformers/minifier/minify_test.go @@ -41,23 +41,3 @@ func TestTransform(t *testing.T) { c.Assert(content, qt.Equals, "

Hugo Rocks!

") } - -func TestNoMinifier(t *testing.T) { - c := qt.New(t) - - spec, _ := htesting.NewTestResourceSpec() - spec.Cfg.Set("minifiers.enableXML", false) - client, _ := New(spec) - - original := " Hugo Rocks! " - r, err := htesting.NewResourceTransformerForSpec(spec, "hugo.xml", original) - c.Assert(err, qt.IsNil) - - transformed, err := client.Minify(r) - c.Assert(err, qt.IsNil) - - content, err := transformed.(resource.ContentProvider).Content() - // error should be ignored because general users cannot control codes under `theme`s - c.Assert(err, qt.IsNil) - c.Assert(content, qt.Equals, original) -} diff --git a/tpl/cast/docshelper.go b/tpl/cast/docshelper.go index 1ee614b1..a497f6e8 100644 --- a/tpl/cast/docshelper.go +++ b/tpl/cast/docshelper.go @@ -24,8 +24,7 @@ import ( // This file provides documentation support and is randomly put into this package. func init() { - docsProvider := func() map[string]interface{} { - docs := make(map[string]interface{}) + docsProvider := func() docshelper.DocProvider { d := &deps.Deps{ Cfg: viper.New(), Log: loggers.NewErrorLogger(), @@ -41,11 +40,11 @@ func init() { } - docs["funcs"] = namespaces - return docs + return docshelper.DocProvider{"tpl": map[string]interface{}{"funcs": namespaces}} + } - docshelper.AddDocProvider("tpl", docsProvider) + docshelper.AddDocProviderFunc(docsProvider) } func newTestConfig() *viper.Viper {