Skip to content

Commit

Permalink
Implement --prettify parameter (only supports HTML for now).
Browse files Browse the repository at this point in the history
This is similar to --minify, but has essentially the opposite effect:
instead of making the output less readable it makes it *more* readable.

This uses github.com/yosssi/gohtml to perform the actual transformation.

Fixes gohugoio#7190
  • Loading branch information
fvbommel committed Oct 12, 2020
1 parent fef057b commit 5c0732b
Show file tree
Hide file tree
Showing 10 changed files with 473 additions and 2 deletions.
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -178,6 +178,7 @@ Hugo stands on the shoulder of many great open source libraries, in lexical orde
| [github.com/spf13/viper](https://github.com/spf13/viper) | MIT License |
| [github.com/tdewolff/minify](https://github.com/tdewolff/minify) | MIT License |
| [github.com/tdewolff/parse](https://github.com/tdewolff/parse) | MIT License |
| [github.com/yosssi/gohtml](https://github.com/yosssi/gohtml) | MIT License |
| [github.com/yuin/goldmark](https://github.com/yuin/goldmark) | MIT License |
| [github.com/yuin/goldmark-highlighting](https://github.com/yuin/goldmark-highlighting) | MIT License |
| [go.opencensus.io](https://go.opencensus.io) | Apache License 2.0 |
Expand Down
1 change: 1 addition & 0 deletions commands/commands.go
Expand Up @@ -313,6 +313,7 @@ func (cc *hugoBuilderCommon) handleFlags(cmd *cobra.Command) {
cmd.Flags().StringSlice("disableKinds", []string{}, "disable different kind of pages (home, RSS etc.)")

cmd.Flags().Bool("minify", false, "minify any supported output format (HTML, XML etc.)")
cmd.Flags().Bool("prettify", false, "prettify any supported output format (HTML)")

// Set bash-completion.
// Each flag must first be defined before using the SetAnnotation() call.
Expand Down
1 change: 1 addition & 0 deletions commands/hugo.go
Expand Up @@ -241,6 +241,7 @@ func initializeFlags(cmd *cobra.Command, cfg config.Provider) {
}

setValueFromFlag(cmd.Flags(), "minify", cfg, "minifyOutput", true)
setValueFromFlag(cmd.Flags(), "prettify", cfg, "prettifyOutput", true)

// Set some "config aliases"
setValueFromFlag(cmd.Flags(), "destination", cfg, "publishDir", false)
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Expand Up @@ -52,11 +52,12 @@ require (
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.7.1
github.com/tdewolff/minify/v2 v2.6.2
github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4
github.com/yuin/goldmark v1.2.1
github.com/yuin/goldmark-highlighting v0.0.0-20200307114337-60d527fdb691
gocloud.dev v0.15.0
golang.org/x/image v0.0.0-20191214001246-9130b4cfad52
golang.org/x/net v0.0.0-20200202094626-16171245cfb2
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
golang.org/x/text v0.3.3
google.golang.org/api v0.13.0
Expand Down
10 changes: 10 additions & 0 deletions go.sum
Expand Up @@ -297,6 +297,7 @@ github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2
github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
Expand Down Expand Up @@ -481,6 +482,7 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
Expand All @@ -503,6 +505,8 @@ github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhe
github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4 h1:0sw0nJM544SpsihWx1bkXdYLQDlzRflMgFJQ4Yih9ts=
github.com/yosssi/gohtml v0.0.0-20201013000340-ee4748c638f4/go.mod h1:+ccdNT0xMY1dtc5XBxumbYfOUhmduiGudqaDgD2rVRE=
github.com/yuin/goldmark v1.1.22 h1:0e0f6Zee9SAQ5yOZGNMWaOxqVvcc/9/kUWu/Kl91Jk8=
github.com/yuin/goldmark v1.1.22/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1 h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM=
Expand Down Expand Up @@ -533,6 +537,7 @@ golang.org/x/crypto v0.0.0-20190422183909-d864b10871cd/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
Expand Down Expand Up @@ -578,6 +583,8 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb h1:mUVeFHoDKis5nxCAzoAi7E8Ghb86EXh/RK6wtvJIqRY=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 h1:Wo7BWFiOk0QRFMLYMqJGFMd9CgUAcGx7V+qEg/h5IBI=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
Expand Down Expand Up @@ -623,6 +630,8 @@ golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 h1:opSr2sbRXk5X5/givKrrKj9HX
golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3 h1:5B6i6EAiSYyejWfvc5Rc9BbI3rzIsrrXfAQBWnYfn+w=
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To=
Expand Down Expand Up @@ -724,6 +733,7 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
Expand Down
106 changes: 106 additions & 0 deletions prettifiers/config.go
@@ -0,0 +1,106 @@
// 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 prettifiers

import (
"sort"
"strings"

"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/docshelper"
"github.com/gohugoio/hugo/parser"

"github.com/mitchellh/mapstructure"
"github.com/yosssi/gohtml"
)

type prettifyConfig struct {
// Whether to prettify the published output (the HTML written to /public).
PrettifyOutput bool

DisableHTML bool

HTML htmlConfig
}

type htmlConfig struct {
Condense bool
InlineTags []string
InlineTagMaxLength int
}

var defaultConfig = prettifyConfig{
HTML: htmlConfig{
// Copy the defaults of gohtml
Condense: gohtml.Condense,
InlineTags: boolSetToSlice(gohtml.InlineTags),
InlineTagMaxLength: gohtml.InlineTagMaxLength,
},
}

func decodeConfig(cfg config.Provider) (conf prettifyConfig, err error) {
conf = defaultConfig

// May be set by CLI.
conf.PrettifyOutput = cfg.GetBool("prettifyOutput")

v := cfg.Get("prettify")
if v == nil {
return
}

m := maps.ToStringMap(v)

err = mapstructure.WeakDecode(m, &conf)

if err != nil {
return
}

// Set some global properties for the HTML formatter
gohtml.Condense = conf.HTML.Condense
gohtml.InlineTags = sliceToBoolSet(conf.HTML.InlineTags)
gohtml.InlineTagMaxLength = conf.HTML.InlineTagMaxLength

return
}

// boolSetToSlice converts a map[string]bool to a sorted list of keys.
func boolSetToSlice(set map[string]bool) []string {
slice := make([]string, 0, len(set))
for tag, isShort := range set {
if isShort {
slice = append(slice, tag)
}
}
sort.Strings(slice) // Ensure consistent ordering
return slice
}

// sliceToBoolSet converts a list of strings to a map[string]bool mapping the items in the list to true.
func sliceToBoolSet(items []string) map[string]bool {
set := make(map[string]bool)
for _, tag := range items {
set[strings.ToLower(tag)] = true
}
return set
}

func init() {
docsProvider := func() docshelper.DocProvider {
return docshelper.DocProvider{"config": map[string]interface{}{"prettify": parser.LowerCaseCamelJSONMarshaller{Value: defaultConfig}}}
}
docshelper.AddDocProviderFunc(docsProvider)
}
64 changes: 64 additions & 0 deletions prettifiers/config_test.go
@@ -0,0 +1,64 @@
// 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 prettifiers

import (
"testing"

"github.com/spf13/viper"
"github.com/yosssi/gohtml"

qt "github.com/frankban/quicktest"
)

func TestConfig(t *testing.T) {
c := qt.New(t)
v := viper.New()

v.Set("prettifyOutput", true)
v.Set("prettify", map[string]interface{}{
"disableHTML": true,
})

conf, err := decodeConfig(v)

c.Assert(err, qt.IsNil)

c.Assert(conf.PrettifyOutput, qt.Equals, true)

// `enable` flags
c.Assert(conf.DisableHTML, qt.Equals, true)
}

func TestConfigCondensedHTML(t *testing.T) { testHTMLCondense(t, true) }
func TestConfigUncondensedHTML(t *testing.T) { testHTMLCondense(t, false) }

func testHTMLCondense(t *testing.T, condense bool) {
c := qt.New(t)
v := viper.New()

v.Set("prettify", map[string]interface{}{
"html": map[string]interface{}{
"condense": condense,
},
})

conf, err := decodeConfig(v)

c.Assert(err, qt.IsNil)

c.Assert(conf.HTML.Condense, qt.Equals, condense)
c.Assert(gohtml.Condense, qt.Equals, condense)

}
107 changes: 107 additions & 0 deletions prettifiers/prettifiers.go
@@ -0,0 +1,107 @@
// Copyright 2018 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 prettifiers contains prettifiers mapped to MIME types. This package is used
// in the publishing chain.
package prettifiers

import (
"io"

"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/output"
"github.com/gohugoio/hugo/transform"

"github.com/yosssi/gohtml"
)

// Client wraps a prettifier.
type Client struct {
// Whether output prettifying is enabled (HTML in /public)
PrettifyOutput bool

prettifiers map[string]prettifier
}

type prettifier func(input []byte, dst io.Writer) error

// Transformer returns a func that can be used in the transformer publishing chain.
func (m Client) Transformer(mediatype media.Type) transform.Transformer {
if !m.PrettifyOutput {
return nil
}
prettifier := m.prettifiers[mediatype.Type()]
if prettifier == nil {
return nil
}
return func(ft transform.FromTo) error {
return prettifier(ft.From().Bytes(), ft.To())
}
}

// Prettify tries to prettify the src into dst given a MIME type.
func (m Client) Prettify(mediatype media.Type, dst io.Writer, src io.Reader) error {
prettifier := m.prettifiers[mediatype.Type()]
if prettifier == nil {
// No supported prettifier. Just pass it through.
_, err := io.Copy(dst, src)
return err
}

var w = gohtml.NewWriter(dst)
_, err := io.Copy(w, src)
return err
}

// New creates a new Client with the provided MIME types as the mapping foundation.
// The HTML prettifier 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)

if err != nil {
return Client{}, err
}

client := Client{
PrettifyOutput: conf.PrettifyOutput,
prettifiers: make(map[string]prettifier),
}

// We use the Type definition of the media types defined in the site if found.

// TODO: implement other media types (see ../minifiers/minifiers.go)

// HTML
if !conf.DisableHTML {
for _, of := range outputFormats {
if of.IsHTML {
client.prettifiers[of.MediaType.Type()] = formatHTML
}
}
}

return client, nil
}

func formatHTML(input []byte, w io.Writer) error {
prettified := gohtml.FormatBytes(input)

n, err := w.Write(prettified)
if err == nil && n != len(prettified) {
err = io.ErrShortWrite
}

return err
}

0 comments on commit 5c0732b

Please sign in to comment.