Compare commits

...

13 Commits

Author SHA1 Message Date
hugoreleaser 0270364a34 releaser: Bump versions for release of 0.143.1
[ci skip]
2025-02-04 08:57:38 +00:00
Bjørn Erik Pedersen 760c13a7ac Fix RSS with baseURL with sub dir when render hooks is enabled
Fixes #13332
2025-02-03 19:00:12 +01:00
Oleksandr Redko 3bd73d262d mage: Simplify magefile 2025-02-03 18:34:16 +01:00
Joe Mooring 7104de83ce common/hugo: Adjust deprecation timing and message
Closes #13333
2025-02-03 18:32:30 +01:00
Bjørn Erik Pedersen 835579b338 Re-introduce the LRU-evicted identities in change set calculation
This is a follow up to db28695ff5 -- that commit dropped the cache items evicted in the LRU process. This was done as performance optimization for large Hugo sites.

That made much sense, but now there's a slight chance that we miss out on a change when rebuilding.

This commit fixes this by applying the same logic to the evicted items as if they were still in the cache. This should preserve the performance gains in db28695ff5 and close the hole for the possible false negatives.
2025-02-02 15:55:11 +01:00
hugoreleaser 05e067ced8 releaser: Prepare repository for 0.144.0-DEV
[ci skip]
2025-02-01 16:09:38 +00:00
hugoreleaser ee48d9692a releaser: Bump versions for release of 0.143.0
[ci skip]
2025-02-01 15:56:04 +00:00
Bjørn Erik Pedersen db28695ff5 Fix some server/watch rebuild issues
Two issues:

1. Fixe potential edit-loop in server/watch mode (see below)
2. Drain the cache eviction stack before we start calculating the change set. This should allow more fine grained rebuilds for bigger sites and/or in low memory situations.

The fix in 6c68142cc1 wasn't really fixing the complete problem.

In Hugo we have some steps that takes more time than others, one example being CSS building with TailwindCSS.

The symptom here is that sometimes when you:

1. Edit content or templates that does not trigger a CSS rebuild => Snappy rebuild.
2. Edit stylesheet or add a CSS class to template that triggers a CSS rebuild => relatively slow rebuild (expected)
3. Then back to content editing or template edits that should not trigger a CSS rebuild => relatively slow rebuild (not expected)

This commit fixes this by pulling the dynacache GC step up and merge it with the cache buster step.

Fixes #13316
2025-02-01 16:29:14 +01:00
Bjørn Erik Pedersen 778f0d9002 Don't re-render aliases on server rebuilds
This can lead to stale aliases when rebuilding, but that's a trade-off we need to take for snappier rebuilds on bigger sites.

Note that it should be possible to detect alias changes, but I'm not sure it's worth it.
2025-02-01 15:23:36 +01:00
Alex Shpak 13b208e2f7 tpl/tplimpl: Remove leading whitespaces produced by Youtube shortcode 2025-01-31 09:29:10 +01:00
Bjørn Erik Pedersen 329b2342f0 Fix "concurrent map iteration and map write" in pages from data
Fixes #13254
2025-01-30 19:57:09 +01:00
Joe Mooring 33b46d8a41 resources: Remove debug statement
Closes #13320
2025-01-30 11:32:57 +01:00
Bjørn Erik Pedersen 6c68142cc1 Fix TailwindCSS related server rebuild issue
Fixes #13316
2025-01-29 18:46:12 +01:00
23 changed files with 239 additions and 123 deletions
+11 -5
View File
@@ -176,11 +176,12 @@ func (c *Cache) ClearMatching(predicatePartition func(k string, p PartitionManag
}
// ClearOnRebuild prepares the cache for a new rebuild taking the given changeset into account.
func (c *Cache) ClearOnRebuild(changeset ...identity.Identity) {
// predicate is optional and will clear any entry for which it returns true.
func (c *Cache) ClearOnRebuild(predicate func(k, v any) bool, changeset ...identity.Identity) {
g := rungroup.Run[PartitionManager](context.Background(), rungroup.Config[PartitionManager]{
NumWorkers: len(c.partitions),
Handle: func(ctx context.Context, partition PartitionManager) error {
partition.clearOnRebuild(changeset...)
partition.clearOnRebuild(predicate, changeset...)
return nil
},
})
@@ -479,7 +480,12 @@ func (p *Partition[K, V]) clearMatching(predicate func(k, v any) bool) {
})
}
func (p *Partition[K, V]) clearOnRebuild(changeset ...identity.Identity) {
func (p *Partition[K, V]) clearOnRebuild(predicate func(k, v any) bool, changeset ...identity.Identity) {
if predicate == nil {
predicate = func(k, v any) bool {
return false
}
}
opts := p.getOptions()
if opts.ClearWhen == ClearNever {
return
@@ -525,7 +531,7 @@ func (p *Partition[K, V]) clearOnRebuild(changeset ...identity.Identity) {
// Second pass needs to be done in a separate loop to catch any
// elements marked as stale in the other partitions.
p.c.DeleteFunc(func(key K, v V) bool {
if shouldDelete(key, v) {
if predicate(key, v) || shouldDelete(key, v) {
p.trace.Log(
logg.StringFunc(
func() string {
@@ -601,7 +607,7 @@ type PartitionManager interface {
adjustMaxSize(addend int) int
getMaxSize() int
getOptions() OptionsPartition
clearOnRebuild(changeset ...identity.Identity)
clearOnRebuild(predicate func(k, v any) bool, changeset ...identity.Identity)
clearMatching(predicate func(k, v any) bool)
clearStale()
}
+2 -2
View File
@@ -147,13 +147,13 @@ func TestClear(t *testing.T) {
c.Assert(cache.Keys(predicateAll), qt.HasLen, 4)
cache.ClearOnRebuild()
cache.ClearOnRebuild(nil)
// Stale items are always cleared.
c.Assert(cache.Keys(predicateAll), qt.HasLen, 2)
cache = newTestCache(t)
cache.ClearOnRebuild(identity.StringIdentity("changed"))
cache.ClearOnRebuild(nil, identity.StringIdentity("changed"))
c.Assert(cache.Keys(nil), qt.HasLen, 1)
+5 -5
View File
@@ -418,7 +418,7 @@ func Deprecate(item, alternative string, version string) {
func DeprecateLevel(item, alternative, version string, level logg.Level) {
var msg string
if level == logg.LevelError {
msg = fmt.Sprintf("%s was deprecated in Hugo %s and will be removed in Hugo %s. %s", item, version, CurrentVersion.Next().ReleaseVersion(), alternative)
msg = fmt.Sprintf("%s was deprecated in Hugo %s and subsequently removed. %s", item, version, alternative)
} else {
msg = fmt.Sprintf("%s was deprecated in Hugo %s and will be removed in a future release. %s", item, version, alternative)
}
@@ -434,11 +434,11 @@ func deprecationLogLevelFromVersion(ver string) logg.Level {
to := CurrentVersion
minorDiff := to.Minor - from.Minor
switch {
case minorDiff >= 12:
// Start failing the build after about a year.
case minorDiff >= 15:
// Start failing the build after about 15 months.
return logg.LevelError
case minorDiff >= 6:
// Start printing warnings after about six months.
case minorDiff >= 3:
// Start printing warnings after about 3 months.
return logg.LevelWarn
default:
return logg.LevelInfo
+4 -4
View File
@@ -57,11 +57,11 @@ func TestDeprecationLogLevelFromVersion(t *testing.T) {
c.Assert(deprecationLogLevelFromVersion("0.55.0"), qt.Equals, logg.LevelError)
ver := CurrentVersion
c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelInfo)
ver.Minor -= 1
c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelInfo)
ver.Minor -= 6
ver.Minor -= 3
c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelWarn)
ver.Minor -= 6
ver.Minor -= 4
c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelWarn)
ver.Minor -= 13
c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelError)
// Added just to find the threshold for where we can remove deprecated items.
+2 -2
View File
@@ -18,6 +18,6 @@ package hugo
var CurrentVersion = Version{
Major: 0,
Minor: 143,
PatchLevel: 0,
Suffix: "-DEV",
PatchLevel: 1,
Suffix: "",
}
+4
View File
@@ -73,10 +73,14 @@ func TestPrepareParams(t *testing.T) {
for i, test := range tests {
t.Run(fmt.Sprint(i), func(t *testing.T) {
// PrepareParams modifies input.
prepareClone := PrepareParamsClone(test.input)
PrepareParams(test.input)
if !reflect.DeepEqual(test.expected, test.input) {
t.Errorf("[%d] Expected\n%#v, got\n%#v\n", i, test.expected, test.input)
}
if !reflect.DeepEqual(test.expected, prepareClone) {
t.Errorf("[%d] Expected\n%#v, got\n%#v\n", i, test.expected, prepareClone)
}
})
}
}
+40 -1
View File
@@ -303,7 +303,7 @@ func toMergeStrategy(v any) ParamsMergeStrategy {
}
// PrepareParams
// * makes all the keys in the given map lower cased and will do so
// * makes all the keys in the given map lower cased and will do so recursively.
// * This will modify the map given.
// * Any nested map[interface{}]interface{}, map[string]interface{},map[string]string will be converted to Params.
// * Any _merge value will be converted to proper type and value.
@@ -343,3 +343,42 @@ func PrepareParams(m Params) {
}
}
}
// PrepareParamsClone is like PrepareParams, but it does not modify the input.
func PrepareParamsClone(m Params) Params {
m2 := make(Params)
for k, v := range m {
var retyped bool
lKey := strings.ToLower(k)
if lKey == MergeStrategyKey {
v = toMergeStrategy(v)
retyped = true
} else {
switch vv := v.(type) {
case map[any]any:
var p Params = cast.ToStringMap(v)
v = PrepareParamsClone(p)
retyped = true
case map[string]any:
var p Params = v.(map[string]any)
v = PrepareParamsClone(p)
retyped = true
case map[string]string:
p := make(Params)
for k, v := range vv {
p[k] = v
}
v = p
PrepareParams(p)
retyped = true
}
}
if retyped || k != lKey {
m2[lKey] = v
} else {
m2[k] = v
}
}
return m2
}
+48 -26
View File
@@ -1123,6 +1123,9 @@ func (h *HugoSites) resolveAndClearStateForIdentities(
l logg.LevelLogger,
cachebuster func(s string) bool, changes []identity.Identity,
) error {
// Drain the cache eviction stack to start fresh.
evictedStart := h.Deps.MemCache.DrainEvictedIdentities()
h.Log.Debug().Log(logg.StringFunc(
func() string {
var sb strings.Builder
@@ -1163,17 +1166,32 @@ func (h *HugoSites) resolveAndClearStateForIdentities(
}
// The order matters here:
// 1. Handle the cache busters first, as those may produce identities for the page reset step.
// 1. Then GC the cache, which may produce changes.
// 2. Then reset the page outputs, which may mark some resources as stale.
// 3. Then GC the cache.
if cachebuster != nil {
if err := loggers.TimeTrackfn(func() (logg.LevelLogger, error) {
ll := l.WithField("substep", "gc dynacache cachebuster")
h.dynacacheGCCacheBuster(cachebuster)
return ll, nil
}); err != nil {
return err
if err := loggers.TimeTrackfn(func() (logg.LevelLogger, error) {
ll := l.WithField("substep", "gc dynacache")
predicate := func(k any, v any) bool {
if cachebuster != nil {
if s, ok := k.(string); ok {
return cachebuster(s)
}
}
return false
}
h.MemCache.ClearOnRebuild(predicate, changes...)
h.Log.Trace(logg.StringFunc(func() string {
var sb strings.Builder
sb.WriteString("dynacache keys:\n")
for _, key := range h.MemCache.Keys(nil) {
sb.WriteString(fmt.Sprintf(" %s\n", key))
}
return sb.String()
}))
return ll, nil
}); err != nil {
return err
}
// Drain the cache eviction stack.
@@ -1182,6 +1200,27 @@ func (h *HugoSites) resolveAndClearStateForIdentities(
for _, c := range evicted {
changes = append(changes, c.Identity)
}
if len(evictedStart) > 0 {
// In low memory situations and/or very big sites, there can be a lot of unrelated evicted items,
// but there's a chance that some of them are related to the changes we are about to process,
// so check.
depsFinder := identity.NewFinder(identity.FinderConfig{})
var addends []identity.Identity
for _, ev := range evictedStart {
for _, id := range changes {
if cachebuster != nil && cachebuster(ev.Key.(string)) {
addends = append(addends, ev.Identity)
break
}
if r := depsFinder.Contains(id, ev.Identity, -1); r > 0 {
addends = append(addends, ev.Identity)
break
}
}
}
changes = append(changes, addends...)
}
} else {
// Mass eviction, we might as well invalidate everything.
changes = []identity.Identity{identity.GenghisKhan}
@@ -1238,23 +1277,6 @@ func (h *HugoSites) resolveAndClearStateForIdentities(
return err
}
if err := loggers.TimeTrackfn(func() (logg.LevelLogger, error) {
ll := l.WithField("substep", "gc dynacache")
h.MemCache.ClearOnRebuild(changes...)
h.Log.Trace(logg.StringFunc(func() string {
var sb strings.Builder
sb.WriteString("dynacache keys:\n")
for _, key := range h.MemCache.Keys(nil) {
sb.WriteString(fmt.Sprintf(" %s\n", key))
}
return sb.String()
}))
return ll, nil
}); err != nil {
return err
}
return nil
}
+12 -5
View File
@@ -827,6 +827,11 @@ func (h *HugoSites) processPartialFileEvents(ctx context.Context, l logg.LevelLo
addedContentPaths []*paths.Path
)
var (
addedOrChangedContent []pathChange
changes []identity.Identity
)
for _, ev := range eventInfos {
cpss := h.BaseFs.ResolvePaths(ev.Name)
pss := make([]*paths.Path, len(cpss))
@@ -853,6 +858,13 @@ func (h *HugoSites) processPartialFileEvents(ctx context.Context, l logg.LevelLo
if err == nil && g != nil {
cacheBusters = append(cacheBusters, g)
}
if ev.added {
changes = append(changes, identity.StructuralChangeAdd)
}
if ev.removed {
changes = append(changes, identity.StructuralChangeRemove)
}
}
if ev.removed {
@@ -864,11 +876,6 @@ func (h *HugoSites) processPartialFileEvents(ctx context.Context, l logg.LevelLo
}
}
var (
addedOrChangedContent []pathChange
changes []identity.Identity
)
// Find the most specific identity possible.
handleChange := func(pathInfo *paths.Path, delete, isDir bool) {
switch pathInfo.Component() {
+48
View File
@@ -96,3 +96,51 @@ Figure:
b.AssertFileContent("public/index.xml", "img src="http://example.com/images/sunset.jpg")
}
// Issue 13332.
func TestRSSCanonifyURLsSubDir(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
baseURL = 'https://example.org/subdir'
disableKinds = ['section','sitemap','taxonomy','term']
[markup.goldmark.renderHooks.image]
enableDefault = true
[markup.goldmark.renderHooks.link]
enableDefault = true
-- layouts/_default/_markup/render-image.html --
{{- $u := urls.Parse .Destination -}}
{{- $src := $u.String | relURL -}}
<img srcset="{{ $src }}" src="{{ $src }} 2x">
<img src="{{ $src }}">
{{- /**/ -}}
-- layouts/_default/home.html --
{{ .Content }}|
-- layouts/_default/single.html --
{{ .Content }}|
-- layouts/_default/rss.xml --
{{ with site.GetPage "/s1/p2" }}
{{ .Content | transform.XMLEscape | safeHTML }}
{{ end }}
-- content/s1/p1.md --
---
title: p1
---
-- content/s1/p2/index.md --
---
title: p2
---
![alt](a.jpg)
[p1](/s1/p1)
-- content/s1/p2/a.jpg --
`
b := Test(t, files)
b.AssertFileContent("public/index.xml", "https://example.org/subdir/s1/p1/")
b.AssertFileContent("public/index.xml",
"img src=&#34;https://example.org/subdir/a.jpg",
"img srcset=&#34;https://example.org/subdir/a.jpg&#34; src=&#34;https://example.org/subdir/a.jpg 2x")
}
+1 -1
View File
@@ -1542,7 +1542,7 @@ func (s *Site) render(ctx *siteRenderContext) (err error) {
return err
}
if ctx.outIdx == 0 {
if ctx.outIdx == 0 && s.h.buildCounter.Load() == 0 {
// Note that even if disableAliases is set, the aliases themselves are
// preserved on page. The motivation with this is to be able to generate
// 301 redirects in a .htaccess file and similar using a custom output format.
+3 -2
View File
@@ -1,7 +1,8 @@
# Release env.
# These will be replaced by script before release.
HUGORELEASER_TAG=v0.142.0
HUGORELEASER_COMMITISH=1f746a872442e66b6afd47c8c04ac42dc92cdb6f
HUGORELEASER_TAG=v0.143.0
HUGORELEASER_COMMITISH=ee48d9692af281180aea00645d86f3231a5231df
+3
View File
@@ -33,6 +33,9 @@ const (
// GenghisKhan is an Identity everyone relates to.
GenghisKhan = StringIdentity("__genghiskhan")
StructuralChangeAdd = StringIdentity("__structural_change_add")
StructuralChangeRemove = StringIdentity("__structural_change_remove")
)
var NopManager = new(nopManager)
+17 -56
View File
@@ -1,11 +1,9 @@
//go:build mage
// +build mage
package main
import (
"bytes"
"errors"
"fmt"
"os"
"path"
@@ -36,10 +34,6 @@ func init() {
if exe := os.Getenv("GOEXE"); exe != "" {
goexe = exe
}
// We want to use Go 1.11 modules even if the source lives inside GOPATH.
// The default is "auto".
os.Setenv("GO111MODULE", "on")
}
func runWith(env map[string]string, cmd string, inArgs ...any) error {
@@ -122,10 +116,10 @@ func HugoNoGitInfo() error {
return Hugo()
}
var docker = sh.RunCmd("docker")
// Build hugo Docker container
func Docker() error {
docker := sh.RunCmd("docker")
if err := docker("build", "-t", "hugo", "."); err != nil {
return err
}
@@ -148,7 +142,7 @@ func Check() {
fmt.Printf("Skip Test386 on %s and/or %s\n", runtime.GOARCH, runtime.GOOS)
}
if isCi() && isDarwin() {
if isCI() && isDarwin() {
// Skip on macOS in CI (disk space issues)
} else {
mg.Deps(Fmt, Vet)
@@ -200,56 +194,19 @@ func Fmt() error {
return nil
}
var (
pkgPrefixLen = len("github.com/gohugoio/hugo")
pkgs []string
pkgsInit sync.Once
)
const pkgPrefixLen = len("github.com/gohugoio/hugo")
func hugoPackages() ([]string, error) {
var err error
pkgsInit.Do(func() {
var s string
s, err = sh.Output(goexe, "list", "./...")
if err != nil {
return
}
pkgs = strings.Split(s, "\n")
for i := range pkgs {
pkgs[i] = "." + pkgs[i][pkgPrefixLen:]
}
})
return pkgs, err
}
// Run golint linter
func Lint() error {
pkgs, err := hugoPackages()
var hugoPackages = sync.OnceValues(func() ([]string, error) {
s, err := sh.Output(goexe, "list", "./...")
if err != nil {
return err
return nil, err
}
failed := false
for _, pkg := range pkgs {
// We don't actually want to fail this target if we find golint errors,
// so we don't pass -set_exit_status, but we still print out any failures.
if _, err := sh.Exec(nil, os.Stderr, nil, "golint", pkg); err != nil {
fmt.Printf("ERROR: running go lint on %q: %v\n", pkg, err)
failed = true
}
pkgs := strings.Split(s, "\n")
for i := range pkgs {
pkgs[i] = "." + pkgs[i][pkgPrefixLen:]
}
if failed {
return errors.New("errors running golint")
}
return nil
}
func isCi() bool {
return os.Getenv("CI") != ""
}
func isDarwin() bool {
return runtime.GOOS == "darwin"
}
return pkgs, nil
})
// Run go vet linter
func Vet() error {
@@ -270,7 +227,7 @@ func TestCoverHTML() error {
return err
}
defer f.Close()
if _, err := f.Write([]byte("mode: count")); err != nil {
if _, err := f.WriteString("mode: count"); err != nil {
return err
}
pkgs, err := hugoPackages()
@@ -320,6 +277,10 @@ func isUnix() bool {
return runtime.GOOS != "windows"
}
func isDarwin() bool {
return runtime.GOOS == "darwin"
}
func isCI() bool {
return os.Getenv("CI") != ""
}
+4 -1
View File
@@ -158,8 +158,11 @@ func (p *PageConfig) Compile(basePath string, pagesFromData bool, ext string, lo
if p.Params == nil {
p.Params = make(maps.Params)
} else if pagesFromData {
p.Params = maps.PrepareParamsClone(p.Params)
} else {
maps.PrepareParams(p.Params)
}
maps.PrepareParams(p.Params)
if p.Content.Markup == "" && p.Content.MediaType == "" {
if ext == "" {
@@ -95,6 +95,10 @@ func (c *Client) Concat(targetPath string, r resource.Resources) (resource.Resou
}
idm := c.rs.Cfg.NewIdentityManager("concat")
// Re-create on structural changes.
idm.AddIdentity(identity.StructuralChangeAdd, identity.StructuralChangeRemove)
// Add the concatenated resources as dependencies to the composite resource
// so that we can track changes to the individual resources.
idm.AddIdentityForEach(identity.ForEeachIdentityProviderFunc(
@@ -36,8 +36,8 @@ func TestTailwindV4Basic(t *testing.T) {
"url": "https://github.com/bep/hugo-starter-tailwind-basic.git"
},
"devDependencies": {
"@tailwindcss/cli": "^4.0.0-alpha.26",
"tailwindcss": "^4.0.0-alpha.26"
"@tailwindcss/cli": "^4.0.1",
"tailwindcss": "^4.0.1"
},
"name": "hugo-starter-tailwind-basic",
"version": "0.1.0"
@@ -68,5 +68,5 @@ CSS: {{ $css.Content | safeCSS }}|
LogLevel: logg.LevelInfo,
}).Build()
b.AssertFileContent("public/index.html", "/*! tailwindcss v4.0.0")
b.AssertFileContent("public/index.html", "/*! tailwindcss v4.")
}
-1
View File
@@ -388,7 +388,6 @@ func (r *resourceAdapter) getImageOps() images.ImageResourceOps {
if r.MediaType().SubType == "svg" {
panic("this method is only available for raster images. To determine if an image is SVG, you can do {{ if eq .MediaType.SubType \"svg\" }}{{ end }}")
}
fmt.Println(r.MediaType().SubType)
panic("this method is only available for image resources")
}
r.init(false, false)
+2 -2
View File
@@ -171,7 +171,7 @@ func (ns *Namespace) TestDeprecationInfo(item, alternative string) string {
// Internal template func, used in tests only.
func (ns *Namespace) TestDeprecationWarn(item, alternative string) string {
v := hugo.CurrentVersion
v.Minor -= 6
v.Minor -= 3
hugo.Deprecate(item, alternative, v.String())
return ""
}
@@ -179,7 +179,7 @@ func (ns *Namespace) TestDeprecationWarn(item, alternative string) string {
// Internal template func, used in tests only.
func (ns *Namespace) TestDeprecationErr(item, alternative string) string {
v := hugo.CurrentVersion
v.Minor -= 12
v.Minor -= 15
hugo.Deprecate(item, alternative, v.String())
return ""
}
@@ -102,7 +102,7 @@ Renders an embedded YouTube video.
{{- $allow := "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" }}
{{- $referrerpolicy := "strict-origin-when-cross-origin" }}
{{- /* Render. */}}
{{- /* Render. */ -}}
<div
{{- with $class }} class="{{ . }}" {{- end }}
{{- with $divStyle }} style="{{ . | safeCSS }}" {{- end -}}
+4 -4
View File
@@ -675,12 +675,12 @@ title: p2
b := hugolib.Test(t, files)
b.AssertFileContent("public/p1/index.html", "515600e76b272f51")
b.AssertFileContent("public/p2/index.html", "b5ceeace7dfa797a")
b.AssertFileContent("public/p1/index.html", "a0a6f5ade9cc3a9f")
b.AssertFileContent("public/p2/index.html", "289c655e727e596c")
files = strings.ReplaceAll(files, "privacy.youtube.privacyEnhanced = false", "privacy.youtube.privacyEnhanced = true")
b = hugolib.Test(t, files)
b.AssertFileContent("public/p1/index.html", "e92c7f4b768d7e23")
b.AssertFileContent("public/p2/index.html", "c384e83e035b71d9")
b.AssertFileContent("public/p1/index.html", "b76d790c20d2bd04")
b.AssertFileContent("public/p2/index.html", "a6db910a9cf54bc1")
}
+3 -1
View File
@@ -13,7 +13,9 @@
package urlreplacers
import "github.com/gohugoio/hugo/transform"
import (
"github.com/gohugoio/hugo/transform"
)
var ar = newAbsURLReplacer()
+18 -1
View File
@@ -16,9 +16,11 @@ package urlreplacers
import (
"bytes"
"io"
"net/url"
"unicode"
"unicode/utf8"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/transform"
)
@@ -31,6 +33,9 @@ type absurllexer struct {
// path may be set to a "." relative path
path []byte
// The root path, without leading slash.
root []byte
pos int // input position
start int // item start position
@@ -119,6 +124,9 @@ func checkCandidateBase(l *absurllexer) {
}
l.pos += relURLPrefixLen
l.w.Write(l.path)
if len(l.root) > 0 && bytes.HasPrefix(l.content[l.pos:], l.root) {
l.pos += len(l.root)
}
l.start = l.pos
}
@@ -174,7 +182,11 @@ func checkCandidateSrcset(l *absurllexer) {
for i, f := range fields {
if f[0] == '/' {
l.w.Write(l.path)
l.w.Write(f[1:])
n := 1
if len(l.root) > 0 && bytes.HasPrefix(f[n:], l.root) {
n += len(l.root)
}
l.w.Write(f[n:])
} else {
l.w.Write(f)
@@ -229,10 +241,15 @@ func (l *absurllexer) replace() {
}
func doReplace(path string, ct transform.FromTo, quotes [][]byte) {
var root string
if u, err := url.Parse(path); err == nil {
root = paths.TrimLeading(u.Path)
}
lexer := &absurllexer{
content: ct.From().Bytes(),
w: ct.To(),
path: []byte(path),
root: []byte(root),
quotes: quotes,
}