shithub: hugo

Download patch

ref: 574c2959b8d3338764fa1db102a5e0fd6ed322d9
parent: 99958f90fedec11d749a1397300860aa8e8459c2
author: SatowTakeshi <[email protected]>
date: Sat Feb 29 13:44:05 EST 2020

Add minify config

Fixes #6750
Updates #6892

--- a/docs/data/docs.json
+++ b/docs/data/docs.json
@@ -1451,6 +1451,35 @@
         "footnoteAnchorPrefix": "",
         "footnoteReturnLinkContents": ""
       }
+    },
+    "minifiers": {
+      "tdewolff": {
+        "enableHtml": true,
+        "enableCss": true,
+        "enableJs": true,
+        "enableJson": true,
+        "enableSvg": true,
+        "enableXml": true,
+        "html": {
+          "keepConditionalComments": true,
+          "keepDefaultAttrVals": true,
+          "keepDocumentTags": true,
+          "keepEndTags": true,
+          "keepWhitespace": false
+        },
+        "css": {
+          "decimals": -1,
+          "keepCSS2": true
+        },
+        "js": {},
+        "json": {},
+        "svg": {
+          "decimals": -1
+        },
+        "xml": {
+          "keepWhitespace": false
+        }
+      }
     }
   },
   "media": {
@@ -3192,6 +3221,12 @@
           "Aliases": null,
           "Examples": null
         },
+        "IsProduction": {
+          "Description": "",
+          "Args": null,
+          "Aliases": null,
+          "Examples": null
+        },
         "Version": {
           "Description": "",
           "Args": null,
@@ -3541,6 +3576,19 @@
             [
               "{{math.Round 1.5}}",
               "2"
+            ]
+          ]
+        },
+        "Sqrt": {
+          "Description": "Sqrt returns the square root of a number.\nNOTE: will return for NaN for negative values of a",
+          "Args": [
+            "a"
+          ],
+          "Aliases": null,
+          "Examples": [
+            [
+              "{{math.Sqrt 81}}",
+              "9"
             ]
           ]
         },
--- a/docshelper/docs.go
+++ b/docshelper/docs.go
@@ -24,7 +24,11 @@
 
 // AddDocProvider adds or updates the DocProvider for a given name.
 func AddDocProvider(name string, provider DocProvider) {
-	DocProviders[name] = provider
+	if prev, ok := DocProviders[name]; !ok {
+		DocProviders[name] = provider
+	} else {
+		DocProviders[name] = merge(prev, provider)
+	}
 }
 
 // DocProvider is used to save arbitrary JSON data
@@ -34,4 +38,14 @@
 // MarshalJSON returns a JSON representation of the DocProvider.
 func (d DocProvider) MarshalJSON() ([]byte, error) {
 	return json.MarshalIndent(d(), "", "  ")
+}
+
+func merge(a, b DocProvider) DocProvider {
+	next := a()
+	for k, v := range b() {
+		next[k] = v
+	}
+	return func() map[string]interface{} {
+		return next
+	}
 }
--- a/hugolib/hugo_sites.go
+++ b/hugolib/hugo_sites.go
@@ -408,7 +408,12 @@
 			s.Deps = d
 
 			// Set up the main publishing chain.
-			s.publisher = publisher.NewDestinationPublisher(d.PathSpec.BaseFs.PublishFs, s.outputFormatsConfig, s.mediaTypesConfig, cfg.Cfg.GetBool("minify"))
+			pub, err := publisher.NewDestinationPublisher(d.PathSpec.BaseFs.PublishFs, s.outputFormatsConfig, s.mediaTypesConfig, cfg.Cfg)
+
+			if err != nil {
+				return err
+			}
+			s.publisher = pub
 
 			if err := s.initializeSiteInfo(); err != nil {
 				return err
--- a/markup/markup_config/config.go
+++ b/markup/markup_config/config.go
@@ -100,6 +100,5 @@
 		return docs
 
 	}
-	// TODO(bep) merge maps
 	docshelper.AddDocProvider("config", docsProvider)
 }
--- /dev/null
+++ b/minifiers/config.go
@@ -1,0 +1,111 @@
+// 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 minifiers
+
+import (
+	"github.com/gohugoio/hugo/config"
+	"github.com/gohugoio/hugo/docshelper"
+	"github.com/gohugoio/hugo/parser"
+
+	"github.com/mitchellh/mapstructure"
+	"github.com/tdewolff/minify/v2/css"
+	"github.com/tdewolff/minify/v2/html"
+	"github.com/tdewolff/minify/v2/js"
+	"github.com/tdewolff/minify/v2/json"
+	"github.com/tdewolff/minify/v2/svg"
+	"github.com/tdewolff/minify/v2/xml"
+)
+
+var defaultTdewolffConfig = tdewolffConfig{
+	HTML: html.Minifier{
+		KeepDocumentTags:        true,
+		KeepConditionalComments: true,
+		KeepEndTags:             true,
+		KeepDefaultAttrVals:     true,
+		KeepWhitespace:          false,
+		// KeepQuotes:              false, >= v2.6.2
+	},
+	CSS: css.Minifier{
+		Decimals: -1, // will be deprecated
+		// Precision: 0,  // use Precision with >= v2.7.0
+		KeepCSS2: true,
+	},
+	JS:   js.Minifier{},
+	JSON: json.Minifier{},
+	SVG: svg.Minifier{
+		Decimals: -1, // will be deprecated
+		// Precision: 0,  // use Precision with >= v2.7.0
+	},
+	XML: xml.Minifier{
+		KeepWhitespace: false,
+	},
+}
+
+type tdewolffConfig struct {
+	HTML html.Minifier
+	CSS  css.Minifier
+	JS   js.Minifier
+	JSON json.Minifier
+	SVG  svg.Minifier
+	XML  xml.Minifier
+}
+
+type minifiersConfig struct {
+	EnableHTML bool
+	EnableCSS  bool
+	EnableJS   bool
+	EnableJSON bool
+	EnableSVG  bool
+	EnableXML  bool
+
+	Tdewolff tdewolffConfig
+}
+
+var defaultConfig = minifiersConfig{
+	EnableHTML: true,
+	EnableCSS:  true,
+	EnableJS:   true,
+	EnableJSON: true,
+	EnableSVG:  true,
+	EnableXML:  true,
+
+	Tdewolff: defaultTdewolffConfig,
+}
+
+func decodeConfig(cfg config.Provider) (conf minifiersConfig, err error) {
+	conf = defaultConfig
+
+	m := cfg.GetStringMap("minifiers")
+	if m == nil {
+		return
+	}
+
+	err = mapstructure.WeakDecode(m, &conf)
+
+	if err != nil {
+		return
+	}
+
+	return
+}
+
+func init() {
+	docsProvider := func() map[string]interface{} {
+		docs := make(map[string]interface{})
+		docs["minifiers"] = parser.LowerCaseCamelJSONMarshaller{Value: defaultConfig}
+		return docs
+
+	}
+	docshelper.AddDocProvider("config", docsProvider)
+}
--- /dev/null
+++ b/minifiers/config_test.go
@@ -1,0 +1,52 @@
+// 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 minifiers
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/spf13/viper"
+
+	qt "github.com/frankban/quicktest"
+)
+
+func TestConfig(t *testing.T) {
+	c := qt.New(t)
+	v := viper.New()
+
+	v.Set("minifiers", map[string]interface{}{
+		"enablexml": false,
+		"tdewolff": map[string]interface{}{
+			"html": map[string]interface{}{
+				"keepwhitespace": false,
+			},
+		},
+	})
+
+	conf, err := decodeConfig(v)
+	fmt.Println(conf)
+
+	c.Assert(err, qt.IsNil)
+
+	// explicitly set value
+	c.Assert(conf.Tdewolff.HTML.KeepWhitespace, qt.Equals, false)
+	// default value
+	c.Assert(conf.Tdewolff.HTML.KeepEndTags, qt.Equals, true)
+	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)
+}
--- a/minifiers/minifiers.go
+++ b/minifiers/minifiers.go
@@ -20,17 +20,12 @@
 	"io"
 	"regexp"
 
+	"github.com/gohugoio/hugo/config"
 	"github.com/gohugoio/hugo/output"
 	"github.com/gohugoio/hugo/transform"
 
 	"github.com/gohugoio/hugo/media"
 	"github.com/tdewolff/minify/v2"
-	"github.com/tdewolff/minify/v2/css"
-	"github.com/tdewolff/minify/v2/html"
-	"github.com/tdewolff/minify/v2/js"
-	"github.com/tdewolff/minify/v2/json"
-	"github.com/tdewolff/minify/v2/svg"
-	"github.com/tdewolff/minify/v2/xml"
 )
 
 // Client wraps a minifier.
@@ -62,39 +57,44 @@
 // New creates a new Client with the provided MIME types as the mapping foundation.
 // 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) Client {
+func New(mediaTypes media.Types, outputFormats output.Formats, cfg config.Provider) (Client, error) {
+	conf, err := decodeConfig(cfg)
+
 	m := minify.New()
-	htmlMin := &html.Minifier{
-		KeepDocumentTags:        true,
-		KeepConditionalComments: true,
-		KeepEndTags:             true,
-		KeepDefaultAttrVals:     true,
+	if err != nil {
+		return Client{m: m}, err
 	}
 
-	cssMin := &css.Minifier{
-		Decimals: -1,
-		KeepCSS2: true,
+	// We use the Type definition of the media types defined in the site if found.
+	if conf.EnableCSS {
+		addMinifier(m, mediaTypes, "css", &conf.Tdewolff.CSS)
 	}
+	if conf.EnableJS {
+		addMinifier(m, mediaTypes, "js", &conf.Tdewolff.JS)
+		m.AddRegexp(regexp.MustCompile("^(application|text)/(x-)?(java|ecma)script$"), &conf.Tdewolff.JS)
+	}
+	if conf.EnableJSON {
+		addMinifier(m, mediaTypes, "json", &conf.Tdewolff.JSON)
+		m.AddRegexp(regexp.MustCompile(`^(application|text)/(x-|ld\+)?json$`), &conf.Tdewolff.JSON)
+	}
+	if conf.EnableSVG {
+		addMinifier(m, mediaTypes, "svg", &conf.Tdewolff.SVG)
+	}
+	if conf.EnableXML {
+		addMinifier(m, mediaTypes, "xml", &conf.Tdewolff.XML)
+	}
 
-	// We use the Type definition of the media types defined in the site if found.
-	addMinifier(m, mediaTypes, "css", cssMin)
-	addMinifierFunc(m, mediaTypes, "js", js.Minify)
-	m.AddFuncRegexp(regexp.MustCompile("^(application|text)/(x-)?(java|ecma)script$"), js.Minify)
-	m.AddFuncRegexp(regexp.MustCompile(`^(application|text)/(x-|ld\+)?json$`), json.Minify)
-	addMinifierFunc(m, mediaTypes, "json", json.Minify)
-	addMinifierFunc(m, mediaTypes, "svg", svg.Minify)
-	addMinifierFunc(m, mediaTypes, "xml", xml.Minify)
-
 	// HTML
-	addMinifier(m, mediaTypes, "html", htmlMin)
-	for _, of := range outputFormats {
-		if of.IsHTML {
-			m.Add(of.MediaType.Type(), htmlMin)
+	if conf.EnableHTML {
+		addMinifier(m, mediaTypes, "html", &conf.Tdewolff.HTML)
+		for _, of := range outputFormats {
+			if of.IsHTML {
+				m.Add(of.MediaType.Type(), &conf.Tdewolff.HTML)
+			}
 		}
 	}
 
-	return Client{m: m}
-
+	return Client{m: m}, nil
 }
 
 func addMinifier(m *minify.M, mt media.Types, suffix string, min minify.Minifier) {
--- a/minifiers/minifiers_test.go
+++ b/minifiers/minifiers_test.go
@@ -23,11 +23,13 @@
 
 	qt "github.com/frankban/quicktest"
 	"github.com/gohugoio/hugo/output"
+	"github.com/spf13/viper"
 )
 
 func TestNew(t *testing.T) {
 	c := qt.New(t)
-	m := New(media.DefaultTypes, output.DefaultFormats)
+	v := viper.New()
+	m, _ := New(media.DefaultTypes, output.DefaultFormats, v)
 
 	var rawJS string
 	var minJS string
@@ -73,9 +75,44 @@
 
 }
 
+func TestConfiguredMinify(t *testing.T) {
+	c := qt.New(t)
+	v := viper.New()
+	v.Set("minifiers", map[string]interface{}{
+		"enablexml": false,
+		"tdewolff": map[string]interface{}{
+			"html": map[string]interface{}{
+				"keepwhitespace": true,
+			},
+		},
+	})
+	m, _ := New(media.DefaultTypes, output.DefaultFormats, v)
+
+	for _, test := range []struct {
+		tp                media.Type
+		rawString         string
+		expectedMinString string
+		errorExpected     bool
+	}{
+		{media.HTMLType, "<hello> Hugo! </hello>", "<hello> Hugo! </hello>", false}, // configured minifier
+		{media.CSSType, " body { color: blue; }  ", "body{color:blue}", false},      // default minifier
+		{media.XMLType, " <hello>  Hugo!   </hello>  ", "", true},                   // disable Xml minificatin
+	} {
+		var b bytes.Buffer
+		if !test.errorExpected {
+			c.Assert(m.Minify(test.tp, &b, strings.NewReader(test.rawString)), qt.IsNil)
+			c.Assert(b.String(), qt.Equals, test.expectedMinString)
+		} else {
+			err := m.Minify(test.tp, &b, strings.NewReader(test.rawString))
+			c.Assert(err, qt.ErrorMatches, "minifier does not exist for mimetype")
+		}
+	}
+}
+
 func TestJSONRoundTrip(t *testing.T) {
 	c := qt.New(t)
-	m := New(media.DefaultTypes, output.DefaultFormats)
+	v := viper.New()
+	m, _ := New(media.DefaultTypes, output.DefaultFormats, v)
 
 	for _, test := range []string{`{
     "glossary": {
@@ -113,7 +150,8 @@
 
 func TestBugs(t *testing.T) {
 	c := qt.New(t)
-	m := New(media.DefaultTypes, output.DefaultFormats)
+	v := viper.New()
+	m, _ := New(media.DefaultTypes, output.DefaultFormats, v)
 
 	for _, test := range []struct {
 		tp                media.Type
--- a/publisher/publisher.go
+++ b/publisher/publisher.go
@@ -18,6 +18,7 @@
 	"io"
 	"sync/atomic"
 
+	"github.com/gohugoio/hugo/config"
 	"github.com/gohugoio/hugo/media"
 
 	"github.com/gohugoio/hugo/minifiers"
@@ -73,13 +74,17 @@
 }
 
 // NewDestinationPublisher creates a new DestinationPublisher.
-func NewDestinationPublisher(fs afero.Fs, outputFormats output.Formats, mediaTypes media.Types, minify bool) DestinationPublisher {
-	pub := DestinationPublisher{fs: fs}
+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 = minifiers.New(mediaTypes, outputFormats)
+		pub.min, err = minifiers.New(mediaTypes, outputFormats, cfg)
+		if err != nil {
+			return
+		}
 		pub.minify = true
 	}
-	return pub
+	return
 }
 
 // Publish applies any relevant transformations and writes the file
--- a/resources/resource_transformers/minifier/minify.go
+++ b/resources/resource_transformers/minifier/minify.go
@@ -29,8 +29,12 @@
 
 // New creates a new Client given a specification. Note that it is the media types
 // configured for the site that is used to match files to the correct minifier.
-func New(rs *resources.Spec) *Client {
-	return &Client{rs: rs, m: minifiers.New(rs.MediaTypes, rs.OutputFormats)}
+func New(rs *resources.Spec) (*Client, error) {
+	m, err := minifiers.New(rs.MediaTypes, rs.OutputFormats, rs.Cfg)
+	if err != nil {
+		return nil, err
+	}
+	return &Client{rs: rs, m: m}, nil
 }
 
 type minifyTransformation struct {
@@ -43,9 +47,7 @@
 }
 
 func (t *minifyTransformation) Transform(ctx *resources.ResourceTransformationCtx) error {
-	if err := t.m.Minify(ctx.InMediaType, ctx.To, ctx.From); err != nil {
-		return err
-	}
+	_ = t.m.Minify(ctx.InMediaType, ctx.To, ctx.From)
 	ctx.AddOutPathIdentifier(".min")
 	return nil
 }
--- a/resources/resource_transformers/minifier/minify_test.go
+++ b/resources/resource_transformers/minifier/minify_test.go
@@ -27,7 +27,7 @@
 
 	spec, err := htesting.NewTestResourceSpec()
 	c.Assert(err, qt.IsNil)
-	client := New(spec)
+	client, _ := New(spec)
 
 	r, err := htesting.NewResourceTransformerForSpec(spec, "hugo.html", "<h1>   Hugo Rocks!   </h1>")
 	c.Assert(err, qt.IsNil)
@@ -40,4 +40,24 @@
 	c.Assert(err, qt.IsNil)
 	c.Assert(content, qt.Equals, "<h1>Hugo Rocks!</h1>")
 
+}
+
+func TestNoMinifier(t *testing.T) {
+	c := qt.New(t)
+
+	spec, _ := htesting.NewTestResourceSpec()
+	spec.Cfg.Set("minifiers.enableXML", false)
+	client, _ := New(spec)
+
+	original := "<title>   Hugo Rocks!   </title>"
+	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)
 }
--- a/tpl/resources/resources.go
+++ b/tpl/resources/resources.go
@@ -46,6 +46,11 @@
 		return nil, err
 	}
 
+	minifyClient, err := minifier.New(deps.ResourceSpec)
+	if err != nil {
+		return nil, err
+	}
+
 	return &Namespace{
 		deps:            deps,
 		scssClient:      scssClient,
@@ -52,7 +57,7 @@
 		createClient:    create.New(deps.ResourceSpec),
 		bundlerClient:   bundler.New(deps.ResourceSpec),
 		integrityClient: integrity.New(deps.ResourceSpec),
-		minifyClient:    minifier.New(deps.ResourceSpec),
+		minifyClient:    minifyClient,
 		postcssClient:   postcss.New(deps.ResourceSpec),
 		templatesClient: templates.New(deps.ResourceSpec, deps),
 	}, nil