Make build.writeStats a struct

So you can do

```toml
[build.writeStats]
  tags = true
  classes = true
  ids = false
```

Fixes #11191
This commit is contained in:
Bjørn Erik Pedersen 2023-07-01 10:37:38 +02:00
parent da98724bc8
commit 11ecea6106
7 changed files with 166 additions and 14 deletions

View file

@ -82,7 +82,7 @@ type LoadConfigResult struct {
var defaultBuild = BuildConfig{ var defaultBuild = BuildConfig{
UseResourceCacheWhen: "fallback", UseResourceCacheWhen: "fallback",
WriteStats: false, WriteStats: WriteStats{},
CacheBusters: []CacheBuster{ CacheBusters: []CacheBuster{
{ {
@ -111,7 +111,8 @@ type BuildConfig struct {
// When enabled, will collect and write a hugo_stats.json with some build // When enabled, will collect and write a hugo_stats.json with some build
// related aggregated data (e.g. CSS class names). // related aggregated data (e.g. CSS class names).
WriteStats bool // Note that this was a bool <= v0.115.0.
WriteStats WriteStats
// Can be used to toggle off writing of the IntelliSense /assets/jsconfig.js // Can be used to toggle off writing of the IntelliSense /assets/jsconfig.js
// file. // file.
@ -121,6 +122,17 @@ type BuildConfig struct {
CacheBusters []CacheBuster CacheBusters []CacheBuster
} }
// WriteStats configures what to write to the hugo_stats.json file.
type WriteStats struct {
Tags bool
Classes bool
IDs bool
}
func (w WriteStats) Enabled() bool {
return w.Tags || w.Classes || w.IDs
}
func (b BuildConfig) clone() BuildConfig { func (b BuildConfig) clone() BuildConfig {
b.CacheBusters = append([]CacheBuster{}, b.CacheBusters...) b.CacheBusters = append([]CacheBuster{}, b.CacheBusters...)
return b return b
@ -171,14 +183,26 @@ func (b *BuildConfig) CompileConfig(logger loggers.Logger) error {
func DecodeBuildConfig(cfg Provider) BuildConfig { func DecodeBuildConfig(cfg Provider) BuildConfig {
m := cfg.GetStringMap("build") m := cfg.GetStringMap("build")
b := defaultBuild.clone() b := defaultBuild.clone()
if m == nil { if m == nil {
return b return b
} }
// writeStats was a bool <= v0.115.0.
if writeStats, ok := m["writestats"]; ok {
if bb, ok := writeStats.(bool); ok {
m["writestats"] = WriteStats{
Tags: bb,
Classes: bb,
IDs: bb,
}
}
}
err := mapstructure.WeakDecode(m, &b) err := mapstructure.WeakDecode(m, &b)
if err != nil { if err != nil {
return defaultBuild return b
} }
b.UseResourceCacheWhen = strings.ToLower(b.UseResourceCacheWhen) b.UseResourceCacheWhen = strings.ToLower(b.UseResourceCacheWhen)

View file

@ -475,7 +475,7 @@ func (h *HugoSites) writeBuildStats() error {
if h.ResourceSpec == nil { if h.ResourceSpec == nil {
panic("h.ResourceSpec is nil") panic("h.ResourceSpec is nil")
} }
if !h.ResourceSpec.BuildConfig().WriteStats { if !h.ResourceSpec.BuildConfig().WriteStats.Enabled() {
return nil return nil
} }

View file

@ -147,6 +147,15 @@ func (s *IntegrationTestBuilder) AssertFileContent(filename string, matches ...s
if match == "" || strings.HasPrefix(match, "#") { if match == "" || strings.HasPrefix(match, "#") {
continue continue
} }
var negate bool
if strings.HasPrefix(match, "! ") {
negate = true
match = strings.TrimPrefix(match, "! ")
}
if negate {
s.Assert(content, qt.Not(qt.Contains), match, qt.Commentf(m))
continue
}
s.Assert(content, qt.Contains, match, qt.Commentf(m)) s.Assert(content, qt.Contains, match, qt.Commentf(m))
} }
} }

View file

@ -1162,6 +1162,89 @@ Some text.
} }
} }
func TestClassCollectorConfigWriteStats(t *testing.T) {
r := func(writeStatsConfig string) *IntegrationTestBuilder {
files := `
-- hugo.toml --
WRITE_STATS_CONFIG
-- layouts/_default/list.html --
<div id="myid" class="myclass">Foo</div>
`
files = strings.Replace(files, "WRITE_STATS_CONFIG", writeStatsConfig, 1)
b := NewIntegrationTestBuilder(
IntegrationTestConfig{
T: t,
TxtarString: files,
NeedsOsFS: true,
},
).Build()
return b
}
// Legacy config.
b := r(`
[build]
writeStats = true
`)
b.AssertFileContent("hugo_stats.json", "myclass", "div", "myid")
b = r(`
[build]
writeStats = false
`)
b.AssertDestinationExists("hugo_stats.json", false)
b = r(`
[build.writeStats]
tags = true
classes = true
ids = true
`)
b.AssertFileContent("hugo_stats.json", "myclass", "div", "myid")
b = r(`
[build.writeStats]
tags = true
classes = true
ids = false
`)
b.AssertFileContent("hugo_stats.json", "myclass", "div", "! myid")
b = r(`
[build.writeStats]
tags = true
classes = false
ids = true
`)
b.AssertFileContent("hugo_stats.json", "! myclass", "div", "myid")
b = r(`
[build.writeStats]
tags = false
classes = true
ids = true
`)
b.AssertFileContent("hugo_stats.json", "myclass", "! div", "myid")
b = r(`
[build.writeStats]
tags = false
classes = false
ids = false
`)
b.AssertDestinationExists("hugo_stats.json", false)
}
func TestClassCollectorStress(t *testing.T) { func TestClassCollectorStress(t *testing.T) {
statsFilename := "hugo_stats.json" statsFilename := "hugo_stats.json"
defer os.Remove(statsFilename) defer os.Remove(statsFilename)

View file

@ -24,6 +24,7 @@ import (
"golang.org/x/net/html" "golang.org/x/net/html"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/helpers"
) )
@ -46,8 +47,9 @@ var (
} }
) )
func newHTMLElementsCollector() *htmlElementsCollector { func newHTMLElementsCollector(conf config.WriteStats) *htmlElementsCollector {
return &htmlElementsCollector{ return &htmlElementsCollector{
conf: conf,
elementSet: make(map[string]bool), elementSet: make(map[string]bool),
} }
} }
@ -93,6 +95,8 @@ type htmlElement struct {
} }
type htmlElementsCollector struct { type htmlElementsCollector struct {
conf config.WriteStats
// Contains the raw HTML string. We will get the same element // Contains the raw HTML string. We will get the same element
// several times, and want to avoid costly reparsing when this // several times, and want to avoid costly reparsing when this
// is used for aggregated data only. // is used for aggregated data only.
@ -113,7 +117,9 @@ func (c *htmlElementsCollector) getHTMLElements() HTMLElements {
for _, el := range c.elements { for _, el := range c.elements {
classes = append(classes, el.Classes...) classes = append(classes, el.Classes...)
ids = append(ids, el.IDs...) ids = append(ids, el.IDs...)
tags = append(tags, el.Tag) if c.conf.Tags {
tags = append(tags, el.Tag)
}
} }
classes = helpers.UniqueStringsSorted(classes) classes = helpers.UniqueStringsSorted(classes)
@ -246,7 +252,7 @@ func (w *htmlElementsCollectorWriter) lexElementInside(resolve htmlCollectorStat
} }
// Parse each collected element. // Parse each collected element.
el, err := parseHTMLElement(s) el, err := w.parseHTMLElement(s)
if err != nil { if err != nil {
w.err = err w.err = err
return resolve return resolve
@ -363,7 +369,13 @@ func htmlLexToEndOfComment(w *htmlElementsCollectorWriter) htmlCollectorStateFun
return htmlLexToEndOfComment return htmlLexToEndOfComment
} }
func parseHTMLElement(elStr string) (el htmlElement, err error) { func (w *htmlElementsCollectorWriter) parseHTMLElement(elStr string) (el htmlElement, err error) {
conf := w.collector.conf
if !conf.IDs && !conf.Classes {
// Nothing to do.
return
}
tagName := parseStartTag(elStr) tagName := parseStartTag(elStr)
@ -390,8 +402,13 @@ func parseHTMLElement(elStr string) (el htmlElement, err error) {
switch { switch {
case strings.EqualFold(a.Key, "id"): case strings.EqualFold(a.Key, "id"):
// There should be only one, but one never knows... // There should be only one, but one never knows...
el.IDs = append(el.IDs, a.Val) if conf.IDs {
el.IDs = append(el.IDs, a.Val)
}
default: default:
if !conf.Classes {
continue
}
if classAttrRe.MatchString(a.Key) { if classAttrRe.MatchString(a.Key) {
el.Classes = append(el.Classes, strings.Fields(a.Val)...) el.Classes = append(el.Classes, strings.Fields(a.Val)...)
} else { } else {

View file

@ -22,6 +22,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/config/testconfig" "github.com/gohugoio/hugo/config/testconfig"
"github.com/gohugoio/hugo/media" "github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/minifiers" "github.com/gohugoio/hugo/minifiers"
@ -136,7 +137,13 @@ func TestClassCollector(t *testing.T) {
} { } {
c.Run(fmt.Sprintf("%s--minify-%t", test.name, variant.minify), func(c *qt.C) { c.Run(fmt.Sprintf("%s--minify-%t", test.name, variant.minify), func(c *qt.C) {
w := newHTMLElementsCollectorWriter(newHTMLElementsCollector()) w := newHTMLElementsCollectorWriter(newHTMLElementsCollector(
config.WriteStats{
Tags: true,
Classes: true,
IDs: true,
},
))
if variant.minify { if variant.minify {
if skipMinifyTest[test.name] { if skipMinifyTest[test.name] {
c.Skip("skip minify test") c.Skip("skip minify test")
@ -240,7 +247,13 @@ func BenchmarkElementsCollectorWriter(b *testing.B) {
</html> </html>
` `
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
w := newHTMLElementsCollectorWriter(newHTMLElementsCollector()) w := newHTMLElementsCollectorWriter(newHTMLElementsCollector(
config.WriteStats{
Tags: true,
Classes: true,
IDs: true,
},
))
fmt.Fprint(w, benchHTML) fmt.Fprint(w, benchHTML)
} }
@ -262,7 +275,13 @@ func BenchmarkElementsCollectorWriterPre(b *testing.B) {
<div class="foo"></div> <div class="foo"></div>
` `
w := newHTMLElementsCollectorWriter(newHTMLElementsCollector()) w := newHTMLElementsCollectorWriter(newHTMLElementsCollector(
config.WriteStats{
Tags: true,
Classes: true,
IDs: true,
},
))
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
fmt.Fprint(w, benchHTML) fmt.Fprint(w, benchHTML)

View file

@ -81,8 +81,8 @@ func NewDestinationPublisher(rs *resources.Spec, outputFormats output.Formats, m
fs := rs.BaseFs.PublishFs fs := rs.BaseFs.PublishFs
cfg := rs.Cfg cfg := rs.Cfg
var classCollector *htmlElementsCollector var classCollector *htmlElementsCollector
if rs.BuildConfig().WriteStats { if rs.BuildConfig().WriteStats.Enabled() {
classCollector = newHTMLElementsCollector() classCollector = newHTMLElementsCollector(rs.BuildConfig().WriteStats)
} }
pub = DestinationPublisher{fs: fs, htmlElementsCollector: classCollector} pub = DestinationPublisher{fs: fs, htmlElementsCollector: classCollector}
pub.min, err = minifiers.New(mediaTypes, outputFormats, cfg) pub.min, err = minifiers.New(mediaTypes, outputFormats, cfg)