Compare commits

...

64 Commits

Author SHA1 Message Date
hugoreleaser 32c967551b releaser: Bump versions for release of 0.126.0
[ci skip]
2024-05-14 13:24:11 +00:00
Bjørn Erik Pedersen 266140251f docs: Regen docshelper 2024-05-14 14:46:16 +02:00
Joe Mooring 74ab839ccb tpl/tplimpl: Plainify title and description in twitter_cards.html
Closes #12433
Improves #10900
2024-05-14 14:45:04 +02:00
Joe Mooring 92290aa892 tpl/tplimpl: Plainify title and description in schema.html
Closes #12432
2024-05-14 14:18:49 +02:00
Joe Mooring 6dbbe6dd3a resources/images: Handle NaN EXIF latitude and longitude
Fixes #12490
2024-05-14 14:16:58 +02:00
Bjørn Erik Pedersen e2d66e3218 Create pages from _content.gotmpl
Closes #12427
Closes #12485
Closes #6310
Closes #5074
2024-05-14 13:12:08 +02:00
Joe Mooring 55dea41c1a create/skeletons: Remove superfluous language code fallback
Closes #12479
2024-05-14 10:24:53 +02:00
Joe Mooring 87ab7f7ffc tpl/tplimpl: Improve locale value in opengraph.html
Closes #12480
2024-05-14 10:24:17 +02:00
dependabot[bot] ee26e69ce3 build(deps): bump golang.org/x/net from 0.24.0 to 0.25.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.24.0 to 0.25.0.
- [Commits](https://github.com/golang/net/compare/v0.24.0...v0.25.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-10 22:33:46 +02:00
dependabot[bot] 6e83d00a83 build(deps): bump golang.org/x/image from 0.15.0 to 0.16.0
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.15.0 to 0.16.0.
- [Commits](https://github.com/golang/image/compare/v0.15.0...v0.16.0)

---
updated-dependencies:
- dependency-name: golang.org/x/image
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-10 22:33:33 +02:00
Bjørn Erik Pedersen d02f0622b4 livereload: Improve the livereload script build and update to v4.0.2
This script has very infrequent updates, but just copy pasting the minified source creates some potential trust issues.

This JS will now be pulled from a Git version and both the unminified and minified version gets written to disk.

This way it should be easier to reason about changes in the future.

To upgrade, change the commit hash and run `mage generate`.

Closes #12451
Closes #6290
2024-05-10 22:33:08 +02:00
Joe Mooring 6dfeb9f038 tpl/tplimpl: Retain query string and fragment in render-image.html
Closes #12468
2024-05-10 22:32:48 +02:00
Joe Mooring ca9a77ef92 markup/goldmark: Support extras extension
Enables inclusion of these HTML elements in Markdown:

- Inserted Text (++inserted++)
- Mark Text (==marked==)
- Subscript (H~2~O)
- Superscript (1^st^)
2024-05-10 22:32:02 +02:00
hugoreleaser b1bf0bff2c releaser: Prepare repository for 0.126.0-DEV
[ci skip]
2024-05-08 15:00:23 +00:00
hugoreleaser b1d808bc37 releaser: Bump versions for release of 0.125.7
[ci skip]
2024-05-08 14:46:24 +00:00
Bjørn Erik Pedersen 3c6260f046 deps: Downgrade github.com/getkin/kin-openapi v0.124.0 => v0.123.0
We have received reports from users of Hugo's `openapi3.Unmarshal` about breaking behavior in the mentioned `kin-openapi` version, so revert for now, pending further investigation.
2024-05-08 16:42:48 +02:00
Bjørn Erik Pedersen 70c13f444e readme: Update Sponsors 2024-05-08 11:37:56 +02:00
hugoreleaser c46d603a02 releaser: Prepare repository for 0.126.0-DEV
[ci skip]
2024-05-05 11:05:28 +00:00
hugoreleaser 69ede10edc releaser: Bump versions for release of 0.125.6
[ci skip]
2024-05-05 10:52:52 +00:00
Bjørn Erik Pedersen bb59a7ed97 Fix one more resource change eviction logic issue
This is how we should have fixed #1239.

Fixes #12456
2024-05-05 12:41:51 +02:00
Bjørn Erik Pedersen 503d20954f Make the cache eviction logic for stale entities more robust
Fixes #12458
2024-05-04 19:45:43 +02:00
dependabot[bot] 68e95327f7 build(deps): bump github.com/pelletier/go-toml/v2 from 2.2.1 to 2.2.2
Bumps [github.com/pelletier/go-toml/v2](https://github.com/pelletier/go-toml) from 2.2.1 to 2.2.2.
- [Release notes](https://github.com/pelletier/go-toml/releases)
- [Changelog](https://github.com/pelletier/go-toml/blob/v2/.goreleaser.yaml)
- [Commits](https://github.com/pelletier/go-toml/compare/v2.2.1...v2.2.2)

---
updated-dependencies:
- dependency-name: github.com/pelletier/go-toml/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-02 11:29:07 +02:00
Bjørn Erik Pedersen 9cd7db61d3 Run mage generate 2024-05-02 10:49:09 +02:00
Bjørn Erik Pedersen c892e75fbc resources/page: Pull internal Page methods into its own interface
So it doesn't get visible when passing it to `jsonify`.
2024-05-02 10:49:02 +02:00
hugoreleaser 4255d13d3e releaser: Prepare repository for 0.126.0-DEV
[ci skip]
2024-05-01 15:36:00 +00:00
hugoreleaser c8b9f9f81c releaser: Bump versions for release of 0.125.5
[ci skip]
2024-05-01 15:22:11 +00:00
Bjørn Erik Pedersen 7be7f89bf6 Fix rebuilds on cascade deletes/renames
And also avoid reading sub directories on simple changes to branch `_index.md` files.

Fixes #12449
2024-05-01 17:18:30 +02:00
Bjørn Erik Pedersen c8e400b621 commands: Print "Webserver is ..." right before "Total ..."
Also fix it so

* It's not printed when running `hugo -w`
* It'd printed for all kinds of rebuilds

Fixes #12384
2024-05-01 10:52:40 +02:00
Bjørn Erik Pedersen 9dd687027f Make sure replaced pages gets marked as stale
Fixes #12436
2024-04-30 20:57:44 +02:00
hugoreleaser 1961327536 releaser: Prepare repository for 0.126.0-DEV
[ci skip]
2024-04-25 13:40:37 +00:00
hugoreleaser cc3574ef4f releaser: Bump versions for release of 0.125.4
[ci skip]
2024-04-25 13:27:26 +00:00
Peter van Dijk fe84cc218e commands: Clarify that create or install a theme are two options 2024-04-25 14:38:22 +02:00
Bjørn Erik Pedersen babcb339a8 config: Setups with only one active language can never be multihost
Fixes #12288
2024-04-25 14:35:49 +02:00
Bjørn Erik Pedersen 7203a95a60 Fix rebuilds when running hugo -w
This was partly broken in Hugo 0.123.0.

We have two internal config options that gets set from the CLI:

* Running; a web server is running
* Watching; either set via `hugo -w`  or `hugo server --watch=false`

Part of the change detection code wrongly used the `Running` as a flag when `Watching` would be the correct.

Fixes #12296
2024-04-25 14:35:49 +02:00
dependabot[bot] fb084390cd build(deps): bump github.com/tdewolff/minify/v2 from 2.20.19 to 2.20.20
Bumps [github.com/tdewolff/minify/v2](https://github.com/tdewolff/minify) from 2.20.19 to 2.20.20.
- [Release notes](https://github.com/tdewolff/minify/releases)
- [Commits](https://github.com/tdewolff/minify/compare/v2.20.19...v2.20.20)

---
updated-dependencies:
- dependency-name: github.com/tdewolff/minify/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-25 11:44:14 +02:00
Joe Mooring fb51b698b3 tpl/tplimpl: Fix double-escaping in opengraph template
Closes #12418
2024-04-25 10:03:17 +02:00
Bjørn Erik Pedersen 6b867972ec Use Apache License without modification
Closes #12415
2024-04-23 09:15:29 +02:00
Bjørn Erik Pedersen 509ab08c1b markup/goldmark: Fix data race in the hugocontext wrapper
The window for this to happen is very small, but it has been reported by Go's race detector (-race flag) in a tests once.
2024-04-22 19:10:15 +02:00
Bjørn Erik Pedersen 2d75f539e1 Delete .hugo_build.lock
Added to Git by accident.
2024-04-22 16:57:48 +02:00
Bjørn Erik Pedersen 15a4b9b337 tpl: Escape .Title in built-in image and link render hooks
Co-authored-by: Joe Mooring <joe@mooring.com>
2024-04-22 16:54:24 +02:00
Joe Mooring 10a8448eee tpl/tplimpl: Improve embedded templates
- Do not call the YouTube oEmbed API
- Do not include the Hugo version in RSS feeds

Closes #12396
2024-04-22 15:57:37 +02:00
Eric Anderson 722c486a34 SECURITY.md: Update link to security model
The security model was moved in https://github.com/gohugoio/hugoDocs/pull/2495
2024-04-22 13:04:53 +02:00
Bjørn Erik Pedersen f40f50ead0 modules: Fix potential infinite loop in module collection
Fixes #12407
2024-04-22 11:34:11 +02:00
hugoreleaser 4e483f5d4a releaser: Bump versions for release of 0.125.2
[ci skip]
2024-04-20 15:29:44 +00:00
Bjørn Erik Pedersen 06d248910c Only add root sections to the section pages menu
Fixes #12399
2024-04-20 17:23:33 +02:00
Bjørn Erik Pedersen 004b694390 Fix partial rebuilds for SCSS fetched with GetMatch and similar
Fixes #12395
2024-04-20 15:09:12 +02:00
Joe Mooring da6112fc65 commands: Add gen chromastyles --lineNumbersTableStyle flag
For symmetry, also rename --linesStyle to --lineNumbersInlineStyle.

Closes #12393
2024-04-20 12:25:28 +02:00
Bjørn Erik Pedersen faf9fedc3d resources/images: Fix TestColorLuminance on s390x 2024-04-19 11:21:50 +02:00
Joe Mooring 11aa893198 commands: Provide examples for chromastyles flags
Closes #12387
2024-04-18 12:16:36 -07:00
hugoreleaser d88cb5269a releaser: Prepare repository for 0.126.0-DEV
[ci skip]
2024-04-18 08:34:20 +00:00
hugoreleaser 68c5ad638c releaser: Bump versions for release of 0.125.1
[ci skip]
2024-04-18 08:21:19 +00:00
Bjørn Erik Pedersen 0c188fda24 tpl: Use erroridf for remote YouTube errors
So they can be silenced.

Fixes #12383
2024-04-18 10:02:36 +02:00
Bjørn Erik Pedersen bbc6888d02 build: Fix `GLIBC_2.29' not found issue
Closes #12381
2024-04-17 12:04:00 +02:00
hugoreleaser 8c14d1edc3 releaser: Prepare repository for 0.126.0-DEV
[ci skip]
2024-04-16 15:21:02 +00:00
hugoreleaser a32400b5f4 releaser: Bump versions for release of 0.125.0
[ci skip]
2024-04-16 15:04:41 +00:00
Bjørn Erik Pedersen df9f2fb617 docs: Regen docshelper 2024-04-16 12:08:28 +02:00
Bjørn Erik Pedersen fa60a2fbc3 Fix server rebuilds when adding a content file on Linux
Fixes #12362
2024-04-16 12:06:37 +02:00
dependabot[bot] fe63de3a83 build(deps): bump github.com/pelletier/go-toml/v2 from 2.2.0 to 2.2.1
Bumps [github.com/pelletier/go-toml/v2](https://github.com/pelletier/go-toml) from 2.2.0 to 2.2.1.
- [Release notes](https://github.com/pelletier/go-toml/releases)
- [Changelog](https://github.com/pelletier/go-toml/blob/v2/.goreleaser.yaml)
- [Commits](https://github.com/pelletier/go-toml/compare/v2.2.0...v2.2.1)

---
updated-dependencies:
- dependency-name: github.com/pelletier/go-toml/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-16 10:43:50 +02:00
Bjørn Erik Pedersen e197c7b29d Add Luminance to Color
To sort an image's colors from darkest to lightest, you can then do:

```handlebars
{{ {{ $colorsByLuminance := sort $image.Colors "Luminance" }}
```

This uses the formula defined here: https://www.w3.org/TR/WCAG21/#dfn-relative-luminance

Fixes #10450
2024-04-16 10:02:46 +02:00
Bjørn Erik Pedersen 74e9129568 hugolib: Add an asciidoc rebuild test case
See #12375
2024-04-15 15:57:11 +02:00
Bjørn Erik Pedersen df11327ba9 Pass .RenderShortcodes' Page to render hooks as .PageInner
The main use case for this is to resolve links and resources (e.g. images) relative to the included `Page`.

A typical `include` would similar to this:

```handlebars
{{ with site.GetPage (.Get 0) }}
  {{ .RenderShortcodes }}
{{ end }}
```

And when used in a Markdown file:

```markdown
{{% include "/posts/p1" %}}
```

Any render hook triggered while rendering `/posts/p1` will get `/posts/p1` when calling `.PageInner`.

Note that

* This is only relevant for shortcodes included with `{{%` that calls `.RenderShortcodes`.
* `.PageInner` is available in all render hooks that, before this commit, received `.Page`.
* `.PageInner` will fall back to the value of `.Page` if not relevant and will always have a value.

Fixes #12356
2024-04-15 09:49:57 +02:00
dependabot[bot] a18e2bcb9a build(deps): bump google.golang.org/protobuf from 1.31.0 to 1.33.0
Bumps google.golang.org/protobuf from 1.31.0 to 1.33.0.

---
updated-dependencies:
- dependency-name: google.golang.org/protobuf
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-14 17:53:18 +02:00
Joe Mooring 6049ba99f0 helpers: Fix TrimShortHTML when used with AsciiDoc content
Fixes #12369
2024-04-14 17:53:05 +02:00
Bjørn Erik Pedersen 8e50ccfae7 github: Add a "free space" step on Ubuntu 2024-04-14 16:42:04 +02:00
132 changed files with 8261 additions and 842 deletions
+2 -2
View File
@@ -4,7 +4,7 @@ parameters:
defaults: &defaults
resource_class: large
docker:
- image: bepsays/ci-hugoreleaser:1.22200.20200
- image: bepsays/ci-hugoreleaser:1.22200.20201
environment: &buildenv
GOMODCACHE: /root/project/gomodcache
version: 2
@@ -60,7 +60,7 @@ jobs:
environment:
<<: [*buildenv]
docker:
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22200.20200
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22200.20201
steps:
- *restore-cache
- &attach-workspace
+13
View File
@@ -18,6 +18,19 @@ jobs:
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- if: matrix.os == 'ubuntu-latest'
name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: false
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: true
swap-storage: true
- name: Checkout code
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
- name: Install Go
+13
View File
@@ -20,6 +20,19 @@ jobs:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- if: matrix.os == 'ubuntu-latest'
name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be
with:
# this might remove tools that are actually needed,
# if set to "true" but frees about 6 GB
tool-cache: false
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: true
swap-storage: true
- name: Checkout code
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
- name: Install Go
+1 -1
View File
@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2022 The Hugo Authors.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+1 -2
View File
@@ -62,8 +62,7 @@ And with [Hugo Modules], you can share content, assets, data, translations, them
<p>&nbsp;</p>
<p float="left">
<a href="https://www.linode.com/?utm_campaign=hugosponsor&utm_medium=banner&utm_source=hugogithub" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/gohugoioTheme/master/assets/images/sponsors/linode-logo_standard_light_medium.png" width="200" alt="Linode"></a>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<a href="https://cloudcannon.com/hugo-cms/?utm_campaign=HugoSponsorship&utm_source=sponsor&utm_content=gohugo" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/gohugoioTheme/master/assets/images/sponsors/cloudcannon-blue.svg" width="220" alt="CloudCannon"></a>
<a href="https://www.linode.com/?utm_campaign=hugosponsor&utm_medium=banner&utm_source=hugogithub" target="_blank"><img src="https://raw.githubusercontent.com/gohugoio/gohugoioTheme/master/assets/images/sponsors/linode-logo_standard_light_medium.png" width="200" alt="Linode"></a>
<p>&nbsp;</p>
## Installation
+1 -1
View File
@@ -4,4 +4,4 @@
Please report (suspected) security vulnerabilities to **[bjorn.erik.pedersen@gmail.com](mailto:bjorn.erik.pedersen@gmail.com)**. You will receive a response from us within 48 hours. If we can confirm the issue, we will release a patch as soon as possible depending on the complexity of the issue but historically within days.
Also see [Hugo's Security Model](https://gohugo.io/about/security-model/).
Also see [Hugo's Security Model](https://gohugo.io/about/security/).
+44 -10
View File
@@ -67,7 +67,7 @@ func New(opts Options) *Cache {
evictedIdentities := collections.NewStack[identity.Identity]()
onEvict := func(k, v any) {
if !opts.Running {
if !opts.Watching {
return
}
identity.WalkIdentitiesShallow(v, func(level int, id identity.Identity) bool {
@@ -97,7 +97,7 @@ type Options struct {
CheckInterval time.Duration
MaxSize int
MinMaxSize int
Running bool
Watching bool
}
// Options for a partition.
@@ -140,16 +140,25 @@ func (c *Cache) DrainEvictedIdentities() []identity.Identity {
}
// ClearMatching clears all partition for which the predicate returns true.
func (c *Cache) ClearMatching(predicate func(k, v any) bool) {
func (c *Cache) ClearMatching(predicatePartition func(k string, p PartitionManager) bool, predicateValue func(k, v any) bool) {
if predicatePartition == nil {
predicatePartition = func(k string, p PartitionManager) bool { return true }
}
if predicateValue == nil {
panic("nil predicateValue")
}
g := rungroup.Run[PartitionManager](context.Background(), rungroup.Config[PartitionManager]{
NumWorkers: len(c.partitions),
Handle: func(ctx context.Context, partition PartitionManager) error {
partition.clearMatching(predicate)
partition.clearMatching(predicateValue)
return nil
},
})
for _, p := range c.partitions {
for k, p := range c.partitions {
if !predicatePartition(k, p) {
continue
}
g.Enqueue(p)
}
@@ -356,6 +365,7 @@ func GetOrCreatePartition[K comparable, V any](c *Cache, name string, opts Optio
trace: c.opts.Log.Logger().WithLevel(logg.LevelTrace).WithField("partition", name),
opts: opts,
}
c.partitions[name] = partition
return partition
@@ -375,13 +385,37 @@ type Partition[K comparable, V any] struct {
// GetOrCreate gets or creates a value for the given key.
func (p *Partition[K, V]) GetOrCreate(key K, create func(key K) (V, error)) (V, error) {
v, err := p.doGetOrCreate(key, create)
if err != nil {
return p.zero, err
}
if resource.StaleVersion(v) > 0 {
p.c.Delete(key)
return p.doGetOrCreate(key, create)
}
return v, err
}
func (p *Partition[K, V]) doGetOrCreate(key K, create func(key K) (V, error)) (V, error) {
v, _, err := p.c.GetOrCreate(key, create)
return v, err
}
func (p *Partition[K, V]) GetOrCreateWitTimeout(key K, duration time.Duration, create func(key K) (V, error)) (V, error) {
v, err := p.doGetOrCreateWitTimeout(key, duration, create)
if err != nil {
return p.zero, err
}
if resource.StaleVersion(v) > 0 {
p.c.Delete(key)
return p.doGetOrCreateWitTimeout(key, duration, create)
}
return v, err
}
// GetOrCreateWitTimeout gets or creates a value for the given key and times out if the create function
// takes too long.
func (p *Partition[K, V]) GetOrCreateWitTimeout(key K, duration time.Duration, create func(key K) (V, error)) (V, error) {
func (p *Partition[K, V]) doGetOrCreateWitTimeout(key K, duration time.Duration, create func(key K) (V, error)) (V, error) {
resultch := make(chan V, 1)
errch := make(chan error, 1)
@@ -438,7 +472,7 @@ func (p *Partition[K, V]) clearOnRebuild(changeset ...identity.Identity) {
shouldDelete := func(key K, v V) bool {
// We always clear elements marked as stale.
if resource.IsStaleAny(v) {
if resource.StaleVersion(v) > 0 {
return true
}
@@ -493,8 +527,8 @@ func (p *Partition[K, V]) Keys() []K {
func (p *Partition[K, V]) clearStale() {
p.c.DeleteFunc(func(key K, v V) bool {
isStale := resource.IsStaleAny(v)
if isStale {
staleVersion := resource.StaleVersion(v)
if staleVersion > 0 {
p.trace.Log(
logg.StringFunc(
func() string {
@@ -504,7 +538,7 @@ func (p *Partition[K, V]) clearStale() {
)
}
return isStale
return staleVersion > 0
})
}
+7 -7
View File
@@ -29,12 +29,12 @@ var (
)
type testItem struct {
name string
isStale bool
name string
staleVersion uint32
}
func (t testItem) IsStale() bool {
return t.isStale
func (t testItem) StaleVersion() uint32 {
return t.staleVersion
}
func (t testItem) IdentifierBase() string {
@@ -109,7 +109,7 @@ func newTestCache(t *testing.T) *Cache {
p2.GetOrCreate("clearBecauseStale", func(string) (testItem, error) {
return testItem{
isStale: true,
staleVersion: 32,
}, nil
})
@@ -121,7 +121,7 @@ func newTestCache(t *testing.T) *Cache {
p2.GetOrCreate("clearNever", func(string) (testItem, error) {
return testItem{
isStale: false,
staleVersion: 0,
}, nil
})
@@ -156,7 +156,7 @@ func TestClear(t *testing.T) {
cache = newTestCache(t)
cache.ClearMatching(func(k, v any) bool {
cache.ClearMatching(nil, func(k, v any) bool {
return k.(string) == "clearOnRebuild"
})
+33 -25
View File
@@ -128,6 +128,7 @@ type rootCommand struct {
verbose bool
debug bool
quiet bool
devMode bool // Hidden flag.
renderToMemory bool
@@ -326,12 +327,12 @@ func (r *rootCommand) Name() string {
}
func (r *rootCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
if !r.buildWatch {
defer r.timeTrack(time.Now(), "Total")
}
b := newHugoBuilder(r, nil)
if !r.buildWatch {
defer b.postBuild("Total", time.Now())
}
if err := b.loadConfig(cd, false); err != nil {
return err
}
@@ -423,29 +424,33 @@ func (r *rootCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
func (r *rootCommand) createLogger(running bool) (loggers.Logger, error) {
level := logg.LevelWarn
if r.logLevel != "" {
switch strings.ToLower(r.logLevel) {
case "debug":
level = logg.LevelDebug
case "info":
level = logg.LevelInfo
case "warn", "warning":
level = logg.LevelWarn
case "error":
level = logg.LevelError
default:
return nil, fmt.Errorf("invalid log level: %q, must be one of debug, warn, info or error", r.logLevel)
}
if r.devMode {
level = logg.LevelTrace
} else {
if r.verbose {
hugo.Deprecate("--verbose", "use --logLevel info", "v0.114.0")
hugo.Deprecate("--verbose", "use --logLevel info", "v0.114.0")
level = logg.LevelInfo
}
if r.logLevel != "" {
switch strings.ToLower(r.logLevel) {
case "debug":
level = logg.LevelDebug
case "info":
level = logg.LevelInfo
case "warn", "warning":
level = logg.LevelWarn
case "error":
level = logg.LevelError
default:
return nil, fmt.Errorf("invalid log level: %q, must be one of debug, warn, info or error", r.logLevel)
}
} else {
if r.verbose {
hugo.Deprecate("--verbose", "use --logLevel info", "v0.114.0")
hugo.Deprecate("--verbose", "use --logLevel info", "v0.114.0")
level = logg.LevelInfo
}
if r.debug {
hugo.Deprecate("--debug", "use --logLevel debug", "v0.114.0")
level = logg.LevelDebug
if r.debug {
hugo.Deprecate("--debug", "use --logLevel debug", "v0.114.0")
level = logg.LevelDebug
}
}
}
@@ -505,10 +510,13 @@ Complete documentation is available at https://gohugo.io/.`
cmd.PersistentFlags().BoolVarP(&r.verbose, "verbose", "v", false, "verbose output")
cmd.PersistentFlags().BoolVarP(&r.debug, "debug", "", false, "debug output")
cmd.PersistentFlags().BoolVarP(&r.devMode, "devMode", "", false, "only used for internal testing, flag hidden.")
cmd.PersistentFlags().StringVar(&r.logLevel, "logLevel", "", "log level (debug|info|warn|error)")
_ = cmd.RegisterFlagCompletionFunc("logLevel", cobra.FixedCompletions([]string{"debug", "info", "warn", "error"}, cobra.ShellCompDirectiveNoFileComp))
cmd.Flags().BoolVarP(&r.buildWatch, "watch", "w", false, "watch filesystem for changes and recreate as needed")
cmd.PersistentFlags().MarkHidden("devMode")
// Configure local flags
applyLocalFlagsBuild(cmd, r)
+14 -8
View File
@@ -45,9 +45,10 @@ func newGenCommand() *genCommand {
genmandir string
// Chroma flags.
style string
highlightStyle string
linesStyle string
style string
highlightStyle string
lineNumbersInlineStyle string
lineNumbersTableStyle string
)
newChromaStyles := func() simplecobra.Commander {
@@ -63,8 +64,11 @@ See https://xyproto.github.io/splash/docs/all.html for a preview of the availabl
if highlightStyle != "" {
builder.Add(chroma.LineHighlight, highlightStyle)
}
if linesStyle != "" {
builder.Add(chroma.LineNumbers, linesStyle)
if lineNumbersInlineStyle != "" {
builder.Add(chroma.LineNumbers, lineNumbersInlineStyle)
}
if lineNumbersTableStyle != "" {
builder.Add(chroma.LineNumbersTable, lineNumbersTableStyle)
}
style, err := builder.Build()
if err != nil {
@@ -78,10 +82,12 @@ See https://xyproto.github.io/splash/docs/all.html for a preview of the availabl
cmd.ValidArgsFunction = cobra.NoFileCompletions
cmd.PersistentFlags().StringVar(&style, "style", "friendly", "highlighter style (see https://xyproto.github.io/splash/docs/)")
_ = cmd.RegisterFlagCompletionFunc("style", cobra.NoFileCompletions)
cmd.PersistentFlags().StringVar(&highlightStyle, "highlightStyle", "", "style used for highlighting lines (see https://github.com/alecthomas/chroma)")
cmd.PersistentFlags().StringVar(&highlightStyle, "highlightStyle", "", `foreground and background colors for highlighted lines, e.g. --highlightStyle "#fff000 bg:#000fff"`)
_ = cmd.RegisterFlagCompletionFunc("highlightStyle", cobra.NoFileCompletions)
cmd.PersistentFlags().StringVar(&linesStyle, "linesStyle", "", "style used for line numbers (see https://github.com/alecthomas/chroma)")
_ = cmd.RegisterFlagCompletionFunc("linesStyle", cobra.NoFileCompletions)
cmd.PersistentFlags().StringVar(&lineNumbersInlineStyle, "lineNumbersInlineStyle", "", `foreground and background colors for inline line numbers, e.g. --lineNumbersInlineStyle "#fff000 bg:#000fff"`)
_ = cmd.RegisterFlagCompletionFunc("lineNumbersInlineStyle", cobra.NoFileCompletions)
cmd.PersistentFlags().StringVar(&lineNumbersTableStyle, "lineNumbersTableStyle", "", `foreground and background colors for table line numbers, e.g. --lineNumbersTableStyle "#fff000 bg:#000fff"`)
_ = cmd.RegisterFlagCompletionFunc("lineNumbersTableStyle", cobra.NoFileCompletions)
},
}
}
+15 -3
View File
@@ -75,9 +75,14 @@ type hugoBuilder struct {
errState hugoBuilderErrState
}
var errConfigNotSet = errors.New("config not set")
func (c *hugoBuilder) withConfE(fn func(conf *commonConfig) error) error {
c.confmu.Lock()
defer c.confmu.Unlock()
if c.conf == nil {
return errConfigNotSet
}
return fn(c.conf)
}
@@ -585,7 +590,7 @@ func (c *hugoBuilder) fullRebuild(changeType string) {
time.Sleep(2 * time.Second)
}()
defer c.r.timeTrack(time.Now(), "Rebuilt")
defer c.postBuild("Rebuilt", time.Now())
err := c.reloadConfig()
if err != nil {
@@ -849,13 +854,13 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
h.BaseFs.SourceFilesystems,
dynamicEvents)
onePageName := pickOneWriteOrCreatePath(partitionedEvents.ContentEvents)
onePageName := pickOneWriteOrCreatePath(h.Conf.ContentTypes(), partitionedEvents.ContentEvents)
c.printChangeDetected("")
c.changeDetector.PrepareNew()
func() {
defer c.r.timeTrack(time.Now(), "Total")
defer c.postBuild("Total", time.Now())
if err := c.rebuildSites(dynamicEvents); err != nil {
c.handleBuildErr(err, "Rebuild failed")
}
@@ -901,6 +906,13 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher,
}
}
func (c *hugoBuilder) postBuild(what string, start time.Time) {
if h, err := c.hugo(); err == nil && h.Conf.Running() {
h.LogServerAddresses()
}
c.r.timeTrack(start, what)
}
func (c *hugoBuilder) hugo() (*hugolib.HugoSites, error) {
var h *hugolib.HugoSites
if err := c.withConfE(func(conf *commonConfig) error {
+1 -1
View File
@@ -209,7 +209,7 @@ func (c *newCommand) newSiteNextStepsText(path string, format string) string {
1. Change the current directory to ` + path + `.
2. Create or install a theme:
- Create a new theme with the command "hugo new theme <THEMENAME>"
- Install a theme from https://themes.gohugo.io/
- Or, install a theme from https://themes.gohugo.io/
3. Edit hugo.` + format + `, setting the "theme" property to the theme name.
4. Create new content with the command "hugo new content `)
+4 -4
View File
@@ -46,12 +46,12 @@ import (
"github.com/fsnotify/fsnotify"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/common/urls"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/hugolib/filesystems"
"github.com/gohugoio/hugo/livereload"
@@ -1188,16 +1188,16 @@ func partitionDynamicEvents(sourceFs *filesystems.SourceFilesystems, events []fs
return
}
func pickOneWriteOrCreatePath(events []fsnotify.Event) string {
func pickOneWriteOrCreatePath(contentTypes config.ContentTypesProvider, events []fsnotify.Event) string {
name := ""
for _, ev := range events {
if ev.Op&fsnotify.Write == fsnotify.Write || ev.Op&fsnotify.Create == fsnotify.Create {
if files.IsIndexContentFile(ev.Name) {
if contentTypes.IsIndexContentFile(ev.Name) {
return ev.Name
}
if files.IsContentFile(ev.Name) {
if contentTypes.IsContentFile(ev.Name) {
name = ev.Name
}
+14
View File
@@ -123,6 +123,20 @@ func InSlicEqualFold(arr []string, el string) bool {
return false
}
// ToString converts the given value to a string.
// Note that this is a more strict version compared to cast.ToString,
// as it will not try to convert numeric values to strings,
// but only accept strings or fmt.Stringer.
func ToString(v any) (string, bool) {
switch vv := v.(type) {
case string:
return vv, true
case fmt.Stringer:
return vv.String(), true
}
return "", false
}
type Tuple struct {
First string
Second string
+2 -2
View File
@@ -17,7 +17,7 @@ package hugo
// This should be the only one.
var CurrentVersion = Version{
Major: 0,
Minor: 125,
Minor: 126,
PatchLevel: 0,
Suffix: "-DEV",
Suffix: "",
}
+14
View File
@@ -27,7 +27,12 @@ func NewCache[K comparable, T any]() *Cache[K, T] {
}
// Delete deletes the given key from the cache.
// If c is nil, this method is a no-op.
func (c *Cache[K, T]) Get(key K) (T, bool) {
if c == nil {
var zero T
return zero, false
}
c.RLock()
v, found := c.m[key]
c.RUnlock()
@@ -60,6 +65,15 @@ func (c *Cache[K, T]) Set(key K, value T) {
c.Unlock()
}
// ForEeach calls the given function for each key/value pair in the cache.
func (c *Cache[K, T]) ForEeach(f func(K, T)) {
c.RLock()
defer c.RUnlock()
for k, v := range c.m {
f(k, v)
}
}
// SliceCache is a simple thread safe cache backed by a map.
type SliceCache[T any] struct {
m map[string][]T
+26 -25
View File
@@ -25,8 +25,6 @@ import (
"github.com/gohugoio/hugo/identity"
)
var defaultPathParser PathParser
// PathParser parses a path into a Path.
type PathParser struct {
// Maps the language code to its index in the languages/sites slice.
@@ -34,11 +32,9 @@ type PathParser struct {
// Reports whether the given language is disabled.
IsLangDisabled func(string) bool
}
// Parse parses component c with path s into Path using the default path parser.
func Parse(c, s string) *Path {
return defaultPathParser.Parse(c, s)
// Reports whether the given ext is a content file.
IsContentExt func(string) bool
}
// NormalizePathString returns a normalized path string using the very basic Hugo rules.
@@ -108,7 +104,6 @@ func (pp *PathParser) parse(component, s string) (*Path, error) {
var err error
// Preserve the original case for titles etc.
p.unnormalized, err = pp.doParse(component, s, pp.newPath(component))
if err != nil {
return nil, err
}
@@ -195,23 +190,26 @@ func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
}
}
isContentComponent := p.component == files.ComponentFolderContent || p.component == files.ComponentFolderArchetypes
isContent := isContentComponent && files.IsContentExt(p.Ext())
if isContent {
if len(p.identifiers) > 0 {
isContentComponent := p.component == files.ComponentFolderContent || p.component == files.ComponentFolderArchetypes
isContent := isContentComponent && pp.IsContentExt(p.Ext())
id := p.identifiers[len(p.identifiers)-1]
b := p.s[p.posContainerHigh : id.Low-1]
switch b {
case "index":
p.bundleType = PathTypeLeaf
case "_index":
p.bundleType = PathTypeBranch
default:
p.bundleType = PathTypeContentSingle
}
if isContent {
switch b {
case "index":
p.bundleType = PathTypeLeaf
case "_index":
p.bundleType = PathTypeBranch
default:
p.bundleType = PathTypeContentSingle
}
if slashCount == 2 && p.IsLeafBundle() {
p.posSectionHigh = 0
if slashCount == 2 && p.IsLeafBundle() {
p.posSectionHigh = 0
}
} else if b == files.NameContentData && files.IsContentDataExt(p.Ext()) {
p.bundleType = PathTypeContentData
}
}
@@ -246,6 +244,9 @@ const (
// Branch bundles, e.g. /blog/_index.md
PathTypeBranch
// Content data file, _content.gotmpl.
PathTypeContentData
)
type Path struct {
@@ -521,10 +522,6 @@ func (p *Path) Identifiers() []string {
return ids
}
func (p *Path) IsHTML() bool {
return files.IsHTML(p.Ext())
}
func (p *Path) BundleType() PathType {
return p.bundleType
}
@@ -541,6 +538,10 @@ func (p *Path) IsLeafBundle() bool {
return p.bundleType == PathTypeLeaf
}
func (p *Path) IsContentData() bool {
return p.bundleType == PathTypeContentData
}
func (p Path) ForBundleType(t PathType) *Path {
p.bundleType = t
return &p
+19
View File
@@ -27,6 +27,9 @@ var testParser = &PathParser{
"no": 0,
"en": 1,
},
IsContentExt: func(ext string) bool {
return ext == "md"
},
}
func TestParse(t *testing.T) {
@@ -333,6 +336,22 @@ func TestParse(t *testing.T) {
c.Assert(p.Path(), qt.Equals, "/a/b/c.txt")
},
},
{
"Content data file gotmpl",
"/a/b/_content.gotmpl",
func(c *qt.C, p *Path) {
c.Assert(p.Path(), qt.Equals, "/a/b/_content.gotmpl")
c.Assert(p.Ext(), qt.Equals, "gotmpl")
c.Assert(p.IsContentData(), qt.IsTrue)
},
},
{
"Content data file yaml",
"/a/b/_content.yaml",
func(c *qt.C, p *Path) {
c.Assert(p.IsContentData(), qt.IsFalse)
},
},
}
for _, test := range tests {
c.Run(test.name, func(c *qt.C) {
+3 -1
View File
@@ -367,6 +367,7 @@ func (c *Config) CompileConfig(logger loggers.Logger) error {
DisabledLanguages: disabledLangs,
IgnoredLogs: ignoredLogIDs,
KindOutputFormats: kindOutputFormats,
ContentTypes: media.DefaultContentTypes.FromTypes(c.MediaTypes.Config),
CreateTitle: helpers.GetTitleFunc(c.TitleCaseStyle),
IsUglyURLSection: isUglyURL,
IgnoreFile: ignoreFile,
@@ -402,6 +403,7 @@ type ConfigCompiled struct {
BaseURLLiveReload urls.BaseURL
ServerInterface string
KindOutputFormats map[string]output.Formats
ContentTypes media.ContentTypes
DisabledKinds map[string]bool
DisabledLanguages map[string]bool
IgnoredLogs map[string]bool
@@ -759,7 +761,7 @@ func (c *Configs) Init() error {
c.Languages = languages
c.LanguagesDefaultFirst = languagesDefaultFirst
c.ContentPathParser = &paths.PathParser{LanguageIndex: languagesDefaultFirst.AsIndexSet(), IsLangDisabled: c.Base.IsLangDisabled}
c.ContentPathParser = &paths.PathParser{LanguageIndex: languagesDefaultFirst.AsIndexSet(), IsLangDisabled: c.Base.IsLangDisabled, IsContentExt: c.Base.C.ContentTypes.IsContentSuffix}
c.configLangs = make([]config.AllProvider, len(c.Languages))
for i, l := range c.LanguagesDefaultFirst {
@@ -84,3 +84,21 @@ logPathWarnings = true
b.Assert(conf.PrintI18nWarnings, qt.Equals, true)
b.Assert(conf.PrintPathWarnings, qt.Equals, true)
}
func TestRedefineContentTypes(t *testing.T) {
files := `
-- hugo.toml --
baseURL = "https://example.com"
[mediaTypes]
[mediaTypes."text/html"]
suffixes = ["html", "xhtml"]
`
b := hugolib.Test(t, files)
conf := b.H.Configs.Base
contentTypes := conf.C.ContentTypes
b.Assert(contentTypes.HTML.Suffixes(), qt.DeepEquals, []string{"html", "xhtml"})
b.Assert(contentTypes.Markdown.Suffixes(), qt.DeepEquals, []string{"md", "mdown", "markdown"})
}
+7
View File
@@ -71,6 +71,9 @@ func (c ConfigLanguage) Environment() string {
}
func (c ConfigLanguage) IsMultihost() bool {
if len(c.m.Languages)-len(c.config.C.DisabledLanguages) <= 1 {
return false
}
return c.m.IsMultihost
}
@@ -141,6 +144,10 @@ func (c ConfigLanguage) NewIdentityManager(name string) identity.Manager {
return identity.NewManager(name)
}
func (c ConfigLanguage) ContentTypes() config.ContentTypesProvider {
return c.config.C.ContentTypes
}
// GetConfigSection is mostly used in tests. The switch statement isn't complete, but what's in use.
func (c ConfigLanguage) GetConfigSection(s string) any {
switch s {
+10
View File
@@ -41,6 +41,7 @@ type AllProvider interface {
Dirs() CommonDirs
Quiet() bool
DirsBase() CommonDirs
ContentTypes() ContentTypesProvider
GetConfigSection(string) any
GetConfig() any
CanonifyURLs() bool
@@ -75,6 +76,15 @@ type AllProvider interface {
EnableEmoji() bool
}
// We cannot import the media package as that would create a circular dependency.
// This interface defineds a sub set of what media.ContentTypes provides.
type ContentTypesProvider interface {
IsContentSuffix(suffix string) bool
IsContentFile(filename string) bool
IsIndexContentFile(filename string) bool
IsHTMLSuffix(suffix string) bool
}
// Provider provides the configuration settings for Hugo.
type Provider interface {
GetString(key string) string
+1 -3
View File
@@ -29,8 +29,6 @@ import (
"github.com/gohugoio/hugo/common/hstrings"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/helpers"
@@ -98,7 +96,7 @@ func NewContent(h *hugolib.HugoSites, kind, targetPath string, force bool) error
return "", fmt.Errorf("failed to resolve %q to an archetype template", targetPath)
}
if !files.IsContentFile(b.targetPath) {
if !h.Conf.ContentTypes().IsContentFile(b.targetPath) {
return "", fmt.Errorf("target path %q is not a known content format", b.targetPath)
}
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="{{ or site.Language.LanguageCode site.Language.Lang }}" dir="{{ or site.Language.LanguageDirection `ltr` }}">
<html lang="{{ or site.Language.LanguageCode }}" dir="{{ or site.Language.LanguageDirection `ltr` }}">
<head>
{{ partial "head.html" . }}
</head>
+1 -1
View File
@@ -155,7 +155,7 @@ func (d *Deps) Init() error {
}
if d.MemCache == nil {
d.MemCache = dynacache.New(dynacache.Options{Running: d.Conf.Running(), Log: d.Log})
d.MemCache = dynacache.New(dynacache.Options{Watching: d.Conf.Watching(), Log: d.Log})
}
if d.PathSpec == nil {
@@ -20,10 +20,11 @@ hugo gen chromastyles [flags] [args]
### Options
```
-h, --help help for chromastyles
--highlightStyle string style used for highlighting lines (see https://github.com/alecthomas/chroma)
--linesStyle string style used for line numbers (see https://github.com/alecthomas/chroma)
--style string highlighter style (see https://xyproto.github.io/splash/docs/) (default "friendly")
-h, --help help for chromastyles
--highlightStyle string foreground and background colors for highlighted lines, e.g. --highlightStyle "#fff000 bg:#000fff"
--lineNumbersInlineStyle string foreground and background colors for inline line numbers, e.g. --lineNumbersInlineStyle "#fff000 bg:#000fff"
--lineNumbersTableStyle string foreground and background colors for table line numbers, e.g. --lineNumbersTableStyle "#fff000 bg:#000fff"
--style string highlighter style (see https://xyproto.github.io/splash/docs/) (default "friendly")
```
### Options inherited from parent commands
+35
View File
@@ -1065,6 +1065,15 @@ config:
enable: false
escapedSpace: false
definitionList: true
extras:
insert:
enable: false
mark:
enable: false
subscript:
enable: false
superscript:
enable: false
footnote: true
linkify: true
linkifyProtocol: https
@@ -1200,6 +1209,12 @@ config:
delimiter: .
suffixes:
- webp
text/asciidoc:
delimiter: .
suffixes:
- adoc
- asciidoc
- ad
text/calendar:
delimiter: .
suffixes:
@@ -1216,6 +1231,7 @@ config:
delimiter: .
suffixes:
- html
- htm
text/javascript:
delimiter: .
suffixes:
@@ -1230,11 +1246,25 @@ config:
delimiter: .
suffixes:
- md
- mdown
- markdown
text/org:
delimiter: .
suffixes:
- org
text/pandoc:
delimiter: .
suffixes:
- pandoc
- pdc
text/plain:
delimiter: .
suffixes:
- txt
text/rst:
delimiter: .
suffixes:
- rst
text/tsx:
delimiter: .
suffixes:
@@ -4017,6 +4047,11 @@ tpl:
- s
Description: CountWords returns the approximate word count in s.
Examples: []
Diff:
Aliases: null
Args: null
Description: ""
Examples: null
FindRE:
Aliases:
- findRE
+11 -10
View File
@@ -30,11 +30,12 @@ require (
github.com/fortytw2/leaktest v1.3.0
github.com/frankban/quicktest v1.14.6
github.com/fsnotify/fsnotify v1.7.0
github.com/getkin/kin-openapi v0.124.0
github.com/getkin/kin-openapi v0.123.0
github.com/ghodss/yaml v1.0.0
github.com/gobuffalo/flect v1.0.2
github.com/gobwas/glob v0.2.3
github.com/gohugoio/go-i18n/v2 v2.1.3-0.20230805085216-e63c13218d0e
github.com/gohugoio/hugo-goldmark-extensions/extras v0.1.0
github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.2.0
github.com/gohugoio/locales v0.14.0
github.com/gohugoio/localescompressed v1.0.1
@@ -55,7 +56,7 @@ require (
github.com/niklasfasching/go-org v1.7.0
github.com/olekukonko/tablewriter v0.0.5
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58
github.com/pelletier/go-toml/v2 v2.2.0
github.com/pelletier/go-toml/v2 v2.2.2
github.com/rogpeppe/go-internal v1.12.0
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd
github.com/sanity-io/litter v1.5.5
@@ -64,18 +65,18 @@ require (
github.com/spf13/cobra v1.8.0
github.com/spf13/fsync v0.10.1
github.com/spf13/pflag v1.0.5
github.com/tdewolff/minify/v2 v2.20.19
github.com/tdewolff/parse/v2 v2.7.12
github.com/tdewolff/minify/v2 v2.20.20
github.com/tdewolff/parse/v2 v2.7.13
github.com/yuin/goldmark v1.7.1
github.com/yuin/goldmark-emoji v1.0.2
go.uber.org/automaxprocs v1.5.3
gocloud.dev v0.36.0
golang.org/x/exp v0.0.0-20221031165847-c99f073a8326
golang.org/x/image v0.15.0
golang.org/x/image v0.16.0
golang.org/x/mod v0.17.0
golang.org/x/net v0.24.0
golang.org/x/net v0.25.0
golang.org/x/sync v0.7.0
golang.org/x/text v0.14.0
golang.org/x/text v0.15.0
golang.org/x/tools v0.20.0
google.golang.org/api v0.152.0
gopkg.in/yaml.v2 v2.4.0
@@ -142,9 +143,9 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/oauth2 v0.15.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
google.golang.org/appengine v1.6.8 // indirect
@@ -152,7 +153,7 @@ require (
google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect
google.golang.org/grpc v1.59.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
howett.net/plist v1.0.0 // indirect
software.sslmate.com/src/go-pkcs12 v0.2.0 // indirect
+22 -18
View File
@@ -193,6 +193,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/getkin/kin-openapi v0.123.0 h1:zIik0mRwFNLyvtXK274Q6ut+dPh6nlxBp0x7mNrPhs8=
github.com/getkin/kin-openapi v0.123.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM=
github.com/getkin/kin-openapi v0.124.0 h1:VSFNMB9C9rTKBnQ/fpyDU8ytMTr4dWI9QovSKj9kz/M=
github.com/getkin/kin-openapi v0.124.0/go.mod h1:wb1aSZA/iWmorQP9KTAS/phLj/t17B5jT7+fS8ed9NM=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
@@ -211,6 +213,8 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/gohugoio/go-i18n/v2 v2.1.3-0.20230805085216-e63c13218d0e h1:QArsSubW7eDh8APMXkByjQWvuljwPGAGQpJEFn0F0wY=
github.com/gohugoio/go-i18n/v2 v2.1.3-0.20230805085216-e63c13218d0e/go.mod h1:3Ltoo9Banwq0gOtcOwxuHG6omk+AwsQPADyw2vQYOJQ=
github.com/gohugoio/hugo-goldmark-extensions/extras v0.1.0 h1:YhxZNU8y2vxV6Ibr7QJzzUlpr8oHHWX/l+Q1R/a5Zao=
github.com/gohugoio/hugo-goldmark-extensions/extras v0.1.0/go.mod h1:0cuvOnGKW7WeXA3i7qK6IS07FH1bgJ2XzOjQ7BMJYH4=
github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.2.0 h1:PCtO5l++psZf48yen2LxQ3JiOXxaRC6v0594NeHvGZg=
github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.2.0/go.mod h1:g9CCh+Ci2IMbPUrVJuXbBTrA+rIIx5+hDQ4EXYaQDoM=
github.com/gohugoio/locales v0.14.0 h1:Q0gpsZwfv7ATHMbcTNepFd59H7GoykzWJIxi113XGDc=
@@ -376,8 +380,8 @@ github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo=
github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU=
@@ -429,10 +433,10 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tdewolff/minify/v2 v2.20.19 h1:tX0SR0LUrIqGoLjXnkIzRSIbKJ7PaNnSENLD4CyH6Xo=
github.com/tdewolff/minify/v2 v2.20.19/go.mod h1:ulkFoeAVWMLEyjuDz1ZIWOA31g5aWOawCFRp9R/MudM=
github.com/tdewolff/parse/v2 v2.7.12 h1:tgavkHc2ZDEQVKy1oWxwIyh5bP4F5fEh/JmBwPP/3LQ=
github.com/tdewolff/parse/v2 v2.7.12/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA=
github.com/tdewolff/minify/v2 v2.20.20 h1:vhULb+VsW2twkplgsawAoUY957efb+EdiZ7zu5fUhhk=
github.com/tdewolff/minify/v2 v2.20.20/go.mod h1:GYaLXFpIIwsX99apQHXfGdISUdlA98wmaoWxjT9C37k=
github.com/tdewolff/parse/v2 v2.7.13 h1:iSiwOUkCYLNfapHoqdLcqZVgvQ0jrsao8YYKP/UJYTI=
github.com/tdewolff/parse/v2 v2.7.13/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA=
github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo=
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
@@ -468,8 +472,8 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
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=
@@ -485,8 +489,8 @@ golang.org/x/exp v0.0.0-20221031165847-c99f073a8326/go.mod h1:CxIveKay+FTh1D0yPZ
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8=
golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
golang.org/x/image v0.16.0 h1:9kloLAKhUufZhA12l5fwnx2NZW39/we1UhBesW433jw=
golang.org/x/image v0.16.0/go.mod h1:ugSZItdV4nOxyqp56HmXwH0Ry0nBCpjnZdpDaIHdoPs=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -547,8 +551,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -617,8 +621,8 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -630,8 +634,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -797,8 +801,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+25 -26
View File
@@ -26,6 +26,7 @@ import (
"github.com/gohugoio/hugo/common/hexec"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/media"
"github.com/spf13/afero"
@@ -36,11 +37,6 @@ import (
"github.com/gohugoio/hugo/config"
)
var (
openingPTag = []byte("<p>")
closingPTag = []byte("</p>")
)
// ContentSpec provides functionality to render markdown content.
type ContentSpec struct {
Converters markup.ConverterProvider
@@ -140,20 +136,16 @@ func (c *ContentSpec) SanitizeAnchorName(s string) string {
}
func (c *ContentSpec) ResolveMarkup(in string) string {
if c == nil {
panic("nil ContentSpec")
}
in = strings.ToLower(in)
switch in {
case "md", "markdown", "mdown":
return "markdown"
case "html", "htm":
return "html"
default:
if conv := c.Converters.Get(in); conv != nil {
return conv.Name()
}
if mediaType, found := c.Cfg.ContentTypes().(media.ContentTypes).Types().GetBestMatch(markup.ResolveMarkup(in)); found {
return mediaType.SubType
}
if conv := c.Converters.Get(in); conv != nil {
return markup.ResolveMarkup(conv.Name())
}
return ""
}
@@ -242,19 +234,26 @@ func (c *ContentSpec) TruncateWordsToWholeSentence(s string) (string, bool) {
return strings.TrimSpace(s[:endIndex]), endIndex < len(s)
}
// TrimShortHTML removes the <p>/</p> tags from HTML input in the situation
// where said tags are the only <p> tags in the input and enclose the content
// of the input (whitespace excluded).
func (c *ContentSpec) TrimShortHTML(input []byte) []byte {
if bytes.Count(input, openingPTag) == 1 {
// TrimShortHTML removes the outer tags from HTML input where (a) the opening
// tag is present only once with the input, and (b) the opening and closing
// tags wrap the input after white space removal.
func (c *ContentSpec) TrimShortHTML(input []byte, markup string) []byte {
openingTag := []byte("<p>")
closingTag := []byte("</p>")
if markup == media.DefaultContentTypes.AsciiDoc.SubType {
openingTag = []byte("<div class=\"paragraph\">\n<p>")
closingTag = []byte("</p>\n</div>")
}
if bytes.Count(input, openingTag) == 1 {
input = bytes.TrimSpace(input)
if bytes.HasPrefix(input, openingPTag) && bytes.HasSuffix(input, closingPTag) {
input = bytes.TrimPrefix(input, openingPTag)
input = bytes.TrimSuffix(input, closingPTag)
if bytes.HasPrefix(input, openingTag) && bytes.HasSuffix(input, closingTag) {
input = bytes.TrimPrefix(input, openingTag)
input = bytes.TrimSuffix(input, closingTag)
input = bytes.TrimSpace(input)
}
}
return input
}
+17 -14
View File
@@ -26,24 +26,27 @@ import (
func TestTrimShortHTML(t *testing.T) {
tests := []struct {
input, output []byte
markup string
input []byte
output []byte
}{
{[]byte(""), []byte("")},
{[]byte("Plain text"), []byte("Plain text")},
// This seems wrong. Why touch it if it doesn't have p tag?
// {[]byte(" \t\n Whitespace text\n\n"), []byte("Whitespace text")},
{[]byte("<p>Simple paragraph</p>"), []byte("Simple paragraph")},
{[]byte("\n \n \t <p> \t Whitespace\nHTML \n\t </p>\n\t"), []byte("Whitespace\nHTML")},
{[]byte("<p>Multiple</p><p>paragraphs</p>"), []byte("<p>Multiple</p><p>paragraphs</p>")},
{[]byte("<p>Nested<p>paragraphs</p></p>"), []byte("<p>Nested<p>paragraphs</p></p>")},
{[]byte("<p>Hello</p>\n<ul>\n<li>list1</li>\n<li>list2</li>\n</ul>"), []byte("<p>Hello</p>\n<ul>\n<li>list1</li>\n<li>list2</li>\n</ul>")},
// Issue #11698
{[]byte("<h2 id=`a`>b</h2>\n\n<p>c</p>"), []byte("<h2 id=`a`>b</h2>\n\n<p>c</p>")},
{"markdown", []byte(""), []byte("")},
{"markdown", []byte("Plain text"), []byte("Plain text")},
{"markdown", []byte("<p>Simple paragraph</p>"), []byte("Simple paragraph")},
{"markdown", []byte("\n \n \t <p> \t Whitespace\nHTML \n\t </p>\n\t"), []byte("Whitespace\nHTML")},
{"markdown", []byte("<p>Multiple</p><p>paragraphs</p>"), []byte("<p>Multiple</p><p>paragraphs</p>")},
{"markdown", []byte("<p>Nested<p>paragraphs</p></p>"), []byte("<p>Nested<p>paragraphs</p></p>")},
{"markdown", []byte("<p>Hello</p>\n<ul>\n<li>list1</li>\n<li>list2</li>\n</ul>"), []byte("<p>Hello</p>\n<ul>\n<li>list1</li>\n<li>list2</li>\n</ul>")},
// Issue 11698
{"markdown", []byte("<h2 id=`a`>b</h2>\n\n<p>c</p>"), []byte("<h2 id=`a`>b</h2>\n\n<p>c</p>")},
// Issue 12369
{"markdown", []byte("<div class=\"paragraph\">\n<p>foo</p>\n</div>"), []byte("<div class=\"paragraph\">\n<p>foo</p>\n</div>")},
{"asciidoc", []byte("<div class=\"paragraph\">\n<p>foo</p>\n</div>"), []byte("foo")},
}
c := newTestContentSpec(nil)
for i, test := range tests {
output := c.TrimShortHTML(test.input)
output := c.TrimShortHTML(test.input, test.markup)
if !bytes.Equal(test.output, output) {
t.Errorf("Test %d failed. Expected %q got %q", i, test.output, output)
}
@@ -54,7 +57,7 @@ func BenchmarkTrimShortHTML(b *testing.B) {
c := newTestContentSpec(nil)
b.ResetTimer()
for i := 0; i < b.N; i++ {
c.TrimShortHTML([]byte("<p>Simple paragraph</p>"))
c.TrimShortHTML([]byte("<p>Simple paragraph</p>"), "markdown")
}
}
+3 -3
View File
@@ -35,9 +35,9 @@ func TestResolveMarkup(t *testing.T) {
{"md", "markdown"},
{"markdown", "markdown"},
{"mdown", "markdown"},
{"asciidocext", "asciidocext"},
{"adoc", "asciidocext"},
{"ad", "asciidocext"},
{"asciidocext", "asciidoc"},
{"adoc", "asciidoc"},
{"ad", "asciidoc"},
{"rst", "rst"},
{"pandoc", "pandoc"},
{"pdc", "pandoc"},
+6
View File
@@ -16,6 +16,7 @@ package hqt
import (
"errors"
"fmt"
"math"
"reflect"
"strings"
@@ -38,6 +39,11 @@ var IsSameType qt.Checker = &typeChecker{
argNames: []string{"got", "want"},
}
// IsSameFloat64 asserts that two float64 values are equal within a small delta.
var IsSameFloat64 = qt.CmpEquals(cmp.Comparer(func(a, b float64) bool {
return math.Abs(a-b) < 0.0001
}))
type argNames []string
func (a argNames) ArgNames() []string {
+7 -49
View File
@@ -29,57 +29,13 @@ const (
FilenameHugoStatsJSON = "hugo_stats.json"
)
var (
// This should be the only list of valid extensions for content files.
contentFileExtensions = []string{
"html", "htm",
"mdown", "markdown", "md",
"asciidoc", "adoc", "ad",
"rest", "rst",
"org",
"pandoc", "pdc",
}
contentFileExtensionsSet map[string]bool
htmlFileExtensions = []string{
"html", "htm",
}
htmlFileExtensionsSet map[string]bool
)
func init() {
contentFileExtensionsSet = make(map[string]bool)
for _, ext := range contentFileExtensions {
contentFileExtensionsSet[ext] = true
}
htmlFileExtensionsSet = make(map[string]bool)
for _, ext := range htmlFileExtensions {
htmlFileExtensionsSet[ext] = true
}
func IsGoTmplExt(ext string) bool {
return ext == "gotmpl"
}
func IsContentFile(filename string) bool {
return contentFileExtensionsSet[strings.TrimPrefix(filepath.Ext(filename), ".")]
}
func IsIndexContentFile(filename string) bool {
if !IsContentFile(filename) {
return false
}
base := filepath.Base(filename)
return strings.HasPrefix(base, "index.") || strings.HasPrefix(base, "_index.")
}
func IsHTML(ext string) bool {
return htmlFileExtensionsSet[ext]
}
func IsContentExt(ext string) bool {
return contentFileExtensionsSet[ext]
// Supported data file extensions for _content.* files.
func IsContentDataExt(ext string) bool {
return IsGoTmplExt(ext)
}
const (
@@ -93,6 +49,8 @@ const (
FolderResources = "resources"
FolderJSConfig = "_jsconfig" // Mounted below /assets with postcss.config.js etc.
NameContentData = "_content"
)
var (
-11
View File
@@ -14,22 +14,11 @@
package files
import (
"path/filepath"
"testing"
qt "github.com/frankban/quicktest"
)
func TestIsContentFile(t *testing.T) {
c := qt.New(t)
c.Assert(IsContentFile(filepath.FromSlash("my/file.md")), qt.Equals, true)
c.Assert(IsContentFile(filepath.FromSlash("my/file.ad")), qt.Equals, true)
c.Assert(IsContentFile(filepath.FromSlash("textfile.txt")), qt.Equals, false)
c.Assert(IsContentExt("md"), qt.Equals, true)
c.Assert(IsContentExt("json"), qt.Equals, false)
}
func TestComponentFolders(t *testing.T) {
c := qt.New(t)
+8 -2
View File
@@ -23,6 +23,7 @@ import (
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/media"
"github.com/spf13/afero"
)
@@ -50,7 +51,8 @@ type WalkwayConfig struct {
Root string
// The logger to use.
Logger loggers.Logger
Logger loggers.Logger
PathParser *paths.PathParser
// One or both of these may be pre-set.
Info FileMetaInfo // The start info.
@@ -72,6 +74,10 @@ func NewWalkway(cfg WalkwayConfig) *Walkway {
panic("fs must be set")
}
if cfg.PathParser == nil {
cfg.PathParser = media.DefaultPathParser
}
logger := cfg.Logger
if logger == nil {
logger = loggers.NewDefault()
@@ -161,7 +167,7 @@ func (w *Walkway) walk(path string, info FileMetaInfo, dirEntries []FileMetaInfo
dirEntries = DirEntriesToFileMetaInfos(fis)
for _, fi := range dirEntries {
if fi.Meta().PathInfo == nil {
fi.Meta().PathInfo = paths.Parse("", filepath.Join(pathRel, fi.Name()))
fi.Meta().PathInfo = w.cfg.PathParser.Parse("", filepath.Join(pathRel, fi.Name()))
}
}
+50 -1
View File
@@ -329,7 +329,7 @@ cascade:
counters := &buildCounters{}
b.Build(BuildCfg{testCounters: counters})
b.Assert(int(counters.contentRenderCounter.Load()), qt.Equals, 2)
b.Assert(int(counters.contentRenderCounter.Load()), qt.Equals, 1)
b.AssertFileContent("public/post/index.html", `Banner: post.jpg|Layout: postlayout|Type: posttype|Content: <p>content edit</p>`)
b.AssertFileContent("public/post/dir/p1/index.html", `Banner: post.jpg|Layout: postlayout|`)
@@ -672,6 +672,55 @@ S1|p1:|p2:p2|
})
}
func TestCascadeEditIssue12449(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
baseURL = "https://example.com"
disableKinds = ['sitemap','rss', 'home', 'taxonomy','term']
disableLiveReload = true
-- layouts/_default/list.html --
Title: {{ .Title }}|{{ .Content }}|cascadeparam: {{ .Params.cascadeparam }}|
-- layouts/_default/single.html --
Title: {{ .Title }}|{{ .Content }}|cascadeparam: {{ .Params.cascadeparam }}|
-- content/mysect/_index.md --
---
title: mysect
cascade:
description: descriptionvalue
params:
cascadeparam: cascadeparamvalue
---
mysect-content|
-- content/mysect/p1/index.md --
---
slug: p1
---
p1-content|
-- content/mysect/subsect/_index.md --
---
slug: subsect
---
subsect-content|
`
b := TestRunning(t, files)
// Make the cascade set the title.
b.EditFileReplaceAll("content/mysect/_index.md", "description: descriptionvalue", "title: cascadetitle").Build()
b.AssertFileContent("public/mysect/subsect/index.html", "Title: cascadetitle|")
// Edit cascade title.
b.EditFileReplaceAll("content/mysect/_index.md", "title: cascadetitle", "title: cascadetitle-edit").Build()
b.AssertFileContent("public/mysect/subsect/index.html", "Title: cascadetitle-edit|")
// Revert title change.
// The step below failed in #12449.
b.EditFileReplaceAll("content/mysect/_index.md", "title: cascadetitle-edit", "description: descriptionvalue").Build()
b.AssertFileContent("public/mysect/subsect/index.html", "Title: |")
}
// Issue 11977.
func TestCascadeExtensionInPath(t *testing.T) {
t.Parallel()
+1 -1
View File
@@ -1144,7 +1144,7 @@ Home.
enConfig := b.H.Sites[0].conf
m, _ := enConfig.MediaTypes.Config.GetByType("text/html")
b.Assert(m.Suffixes(), qt.DeepEquals, []string{"html"})
b.Assert(m.Suffixes(), qt.DeepEquals, []string{"html", "htm"})
svConfig := b.H.Sites[1].conf
f, _ := svConfig.OutputFormats.Config.GetByName("html")
+180 -17
View File
@@ -14,6 +14,7 @@
package hugolib
import (
"context"
"fmt"
"path"
"path/filepath"
@@ -23,10 +24,13 @@ import (
"github.com/bep/logg"
"github.com/gohugoio/hugo/common/hugio"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/hugolib/pagesfromdata"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/source"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/page/pagemeta"
"github.com/gohugoio/hugo/resources/resource"
"github.com/gohugoio/hugo/hugofs"
@@ -51,9 +55,11 @@ type contentMapConfig struct {
var _ contentNodeI = (*resourceSource)(nil)
type resourceSource struct {
path *paths.Path
opener hugio.OpenReadSeekCloser
fi hugofs.FileMetaInfo
langIndex int
path *paths.Path
opener hugio.OpenReadSeekCloser
fi hugofs.FileMetaInfo
rc *pagemeta.ResourceConfig
r resource.Resource
}
@@ -64,11 +70,7 @@ func (r resourceSource) clone() *resourceSource {
}
func (r *resourceSource) LangIndex() int {
if r.r != nil && r.isPage() {
return r.r.(*pageState).s.languagei
}
return r.fi.Meta().LangIndex
return r.langIndex
}
func (r *resourceSource) MarkStale() {
@@ -162,12 +164,13 @@ func (cfg contentMapConfig) getTaxonomyConfig(s string) (v viewName) {
return
}
func (m *pageMap) AddFi(fi hugofs.FileMetaInfo) error {
func (m *pageMap) AddFi(fi hugofs.FileMetaInfo, buildConfig *BuildCfg) (pageCount uint64, resourceCount uint64, addErr error) {
if fi.IsDir() {
return nil
return
}
insertResource := func(fim hugofs.FileMetaInfo) error {
resourceCount++
pi := fi.Meta().PathInfo
key := pi.Base()
tree := m.treeResources
@@ -199,9 +202,9 @@ func (m *pageMap) AddFi(fi hugofs.FileMetaInfo) error {
}
key = pi.Base()
rs = &resourceSource{r: pageResource}
rs = &resourceSource{r: pageResource, langIndex: pageResource.s.languagei}
} else {
rs = &resourceSource{path: pi, opener: r, fi: fim}
rs = &resourceSource{path: pi, opener: r, fi: fim, langIndex: fim.Meta().LangIndex}
}
tree.InsertIntoValuesDimension(key, rs)
@@ -220,14 +223,27 @@ func (m *pageMap) AddFi(fi hugofs.FileMetaInfo) error {
},
))
if err := insertResource(fi); err != nil {
return err
addErr = err
return
}
case paths.PathTypeContentData:
pc, rc, err := m.addPagesFromGoTmplFi(fi, buildConfig)
pageCount += pc
resourceCount += rc
if err != nil {
addErr = err
return
}
default:
m.s.Log.Trace(logg.StringFunc(
func() string {
return fmt.Sprintf("insert bundle: %q", fi.Meta().Filename)
},
))
pageCount++
// A content file.
p, pi, err := m.s.h.newPage(
&pageMeta{
@@ -237,17 +253,164 @@ func (m *pageMap) AddFi(fi hugofs.FileMetaInfo) error {
},
)
if err != nil {
return err
addErr = err
return
}
if p == nil {
// Disabled page.
return nil
return
}
m.treePages.InsertWithLock(pi.Base(), p)
m.treePages.InsertIntoValuesDimensionWithLock(pi.Base(), p)
}
return nil
return
}
func (m *pageMap) addPagesFromGoTmplFi(fi hugofs.FileMetaInfo, buildConfig *BuildCfg) (pageCount uint64, resourceCount uint64, addErr error) {
meta := fi.Meta()
pi := meta.PathInfo
m.s.Log.Trace(logg.StringFunc(
func() string {
return fmt.Sprintf("insert pages from data file: %q", fi.Meta().Filename)
},
))
if !files.IsGoTmplExt(pi.Ext()) {
addErr = fmt.Errorf("unsupported data file extension %q", pi.Ext())
return
}
s := m.s.h.resolveSite(fi.Meta().Lang)
f := source.NewFileInfo(fi)
h := s.h
// Make sure the layouts are initialized.
if _, err := h.init.layouts.Do(context.Background()); err != nil {
addErr = err
return
}
contentAdapter := s.pageMap.treePagesFromTemplateAdapters.Get(pi.Base())
var rebuild bool
if contentAdapter != nil {
// Rebuild
contentAdapter = contentAdapter.CloneForGoTmpl(fi)
rebuild = true
} else {
contentAdapter = pagesfromdata.NewPagesFromTemplate(
pagesfromdata.PagesFromTemplateOptions{
GoTmplFi: fi,
Site: s,
DepsFromSite: func(s page.Site) pagesfromdata.PagesFromTemplateDeps {
ss := s.(*Site)
return pagesfromdata.PagesFromTemplateDeps{
TmplFinder: ss.TextTmpl(),
TmplExec: ss.Tmpl(),
}
},
DependencyManager: s.Conf.NewIdentityManager("pagesfromdata"),
Watching: s.Conf.Watching(),
HandlePage: func(pt *pagesfromdata.PagesFromTemplate, pc *pagemeta.PageConfig) error {
s := pt.Site.(*Site)
if err := pc.Compile(pt.GoTmplFi.Meta().PathInfo.Base(), true, "", s.Log, s.conf.MediaTypes.Config); err != nil {
return err
}
ps, pi, err := h.newPage(
&pageMeta{
f: f,
s: s,
pageMetaParams: &pageMetaParams{
pageConfig: pc,
},
},
)
if err != nil {
return err
}
if ps == nil {
// Disabled page.
return nil
}
u, n, replaced := s.pageMap.treePages.InsertIntoValuesDimensionWithLock(pi.Base(), ps)
if h.isRebuild() {
if replaced {
pt.AddChange(n.GetIdentity())
} else {
pt.AddChange(u.GetIdentity())
}
}
return nil
},
HandleResource: func(pt *pagesfromdata.PagesFromTemplate, rc *pagemeta.ResourceConfig) error {
s := pt.Site.(*Site)
if err := rc.Compile(
pt.GoTmplFi.Meta().PathInfo.Base(),
s.Conf.PathParser(),
s.conf.MediaTypes.Config,
); err != nil {
return err
}
rs := &resourceSource{path: rc.PathInfo, rc: rc, opener: nil, fi: nil, langIndex: s.languagei}
_, n, replaced := s.pageMap.treeResources.InsertIntoValuesDimensionWithLock(rc.PathInfo.Base(), rs)
if h.isRebuild() && replaced {
pt.AddChange(n.GetIdentity())
}
return nil
},
},
)
s.pageMap.treePagesFromTemplateAdapters.Insert(pi.Base(), contentAdapter)
}
handleBuildInfo := func(s *Site, bi pagesfromdata.BuildInfo) {
resourceCount += bi.NumResourcesAdded
pageCount += bi.NumPagesAdded
s.handleContentAdapterChanges(bi, buildConfig)
}
bi, err := contentAdapter.Execute(context.Background())
if err != nil {
addErr = err
return
}
handleBuildInfo(s, bi)
if !rebuild && bi.EnableAllLanguages {
// Clone and insert the adapter for the other sites.
for _, ss := range s.h.Sites {
if s == ss {
continue
}
clone := contentAdapter.CloneForSite(ss)
// Make sure it gets executed for the first time.
bi, err := clone.Execute(context.Background())
if err != nil {
addErr = err
return
}
handleBuildInfo(ss, bi)
// Insert into the correct language tree so it get rebuilt on changes.
ss.pageMap.treePagesFromTemplateAdapters.Insert(pi.Base(), clone)
}
}
return
}
// The home page is represented with the zero string.
+146 -58
View File
@@ -34,7 +34,9 @@ import (
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/hugolib/doctree"
"github.com/gohugoio/hugo/hugolib/pagesfromdata"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/output"
"github.com/gohugoio/hugo/resources"
"github.com/spf13/cast"
@@ -100,6 +102,8 @@ type pageMap struct {
cacheContentPlain *dynacache.Partition[string, *resources.StaleValue[contentPlainPlainWords]]
contentTableOfContents *dynacache.Partition[string, *resources.StaleValue[contentTableOfContents]]
contentDataFileSeenItems *maps.Cache[string, map[uint64]bool]
cfg contentMapConfig
}
@@ -122,6 +126,10 @@ type pageTrees struct {
// This tree contains all taxonomy entries, e.g "/tags/blue/page1"
treeTaxonomyEntries *doctree.TreeShiftTree[*weightedContentNode]
// Stores the state for _content.gotmpl files.
// Mostly releveant for rebuilds.
treePagesFromTemplateAdapters *doctree.TreeShiftTree[*pagesfromdata.PagesFromTemplate]
// A slice of the resource trees.
resourceTrees doctree.MutableTrees
}
@@ -222,6 +230,7 @@ func (t pageTrees) Shape(d, v int) *pageTrees {
t.treePages = t.treePages.Shape(d, v)
t.treeResources = t.treeResources.Shape(d, v)
t.treeTaxonomyEntries = t.treeTaxonomyEntries.Shape(d, v)
t.treePagesFromTemplateAdapters = t.treePagesFromTemplateAdapters.Shape(d, v)
t.createMutableTrees()
return &t
@@ -587,9 +596,9 @@ func (m *pageMap) getOrCreateResourcesForPage(ps *pageState) resource.Resources
sort.SliceStable(res, lessFunc)
if len(ps.m.pageConfig.Resources) > 0 {
if len(ps.m.pageConfig.ResourcesMeta) > 0 {
for i, r := range res {
res[i] = resources.CloneWithMetadataIfNeeded(ps.m.pageConfig.Resources, r)
res[i] = resources.CloneWithMetadataFromMapIfNeeded(ps.m.pageConfig.ResourcesMeta, r)
}
sort.SliceStable(res, lessFunc)
}
@@ -667,12 +676,13 @@ type contentNodeShifter struct {
numLanguages int
}
func (s *contentNodeShifter) Delete(n contentNodeI, dimension doctree.Dimension) (bool, bool) {
func (s *contentNodeShifter) Delete(n contentNodeI, dimension doctree.Dimension) (contentNodeI, bool, bool) {
lidx := dimension[0]
switch v := n.(type) {
case contentNodeIs:
resource.MarkStale(v[lidx])
wasDeleted := v[lidx] != nil
deleted := v[lidx]
resource.MarkStale(deleted)
wasDeleted := deleted != nil
v[lidx] = nil
isEmpty := true
for _, vv := range v {
@@ -681,10 +691,11 @@ func (s *contentNodeShifter) Delete(n contentNodeI, dimension doctree.Dimension)
break
}
}
return wasDeleted, isEmpty
return deleted, wasDeleted, isEmpty
case resourceSources:
resource.MarkStale(v[lidx])
wasDeleted := v[lidx] != nil
deleted := v[lidx]
resource.MarkStale(deleted)
wasDeleted := deleted != nil
v[lidx] = nil
isEmpty := true
for _, vv := range v {
@@ -693,19 +704,19 @@ func (s *contentNodeShifter) Delete(n contentNodeI, dimension doctree.Dimension)
break
}
}
return wasDeleted, isEmpty
return deleted, wasDeleted, isEmpty
case *resourceSource:
if lidx != v.LangIndex() {
return false, false
return nil, false, false
}
resource.MarkStale(v)
return true, true
return v, true, true
case *pageState:
if lidx != v.s.languagei {
return false, false
return nil, false, false
}
resource.MarkStale(v)
return true, true
return v, true, true
default:
panic(fmt.Sprintf("unknown type %T", n))
}
@@ -778,7 +789,7 @@ func (s *contentNodeShifter) ForEeachInDimension(n contentNodeI, d int, f func(c
}
}
func (s *contentNodeShifter) InsertInto(old, new contentNodeI, dimension doctree.Dimension) contentNodeI {
func (s *contentNodeShifter) InsertInto(old, new contentNodeI, dimension doctree.Dimension) (contentNodeI, contentNodeI, bool) {
langi := dimension[doctree.DimensionLanguage.Index()]
switch vv := old.(type) {
case *pageState:
@@ -787,37 +798,39 @@ func (s *contentNodeShifter) InsertInto(old, new contentNodeI, dimension doctree
panic(fmt.Sprintf("unknown type %T", new))
}
if vv.s.languagei == newp.s.languagei && newp.s.languagei == langi {
return new
return new, vv, true
}
is := make(contentNodeIs, s.numLanguages)
is[vv.s.languagei] = old
is[langi] = new
return is
return is, old, false
case contentNodeIs:
oldv := vv[langi]
vv[langi] = new
return vv
return vv, oldv, oldv != nil
case resourceSources:
oldv := vv[langi]
vv[langi] = new.(*resourceSource)
return vv
return vv, oldv, oldv != nil
case *resourceSource:
newp, ok := new.(*resourceSource)
if !ok {
panic(fmt.Sprintf("unknown type %T", new))
}
if vv.LangIndex() == newp.LangIndex() && newp.LangIndex() == langi {
return new
return new, vv, true
}
rs := make(resourceSources, s.numLanguages)
rs[vv.LangIndex()] = vv
rs[langi] = newp
return rs
return rs, vv, false
default:
panic(fmt.Sprintf("unknown type %T", old))
}
}
func (s *contentNodeShifter) Insert(old, new contentNodeI) contentNodeI {
func (s *contentNodeShifter) Insert(old, new contentNodeI) (contentNodeI, contentNodeI, bool) {
switch vv := old.(type) {
case *pageState:
newp, ok := new.(*pageState)
@@ -825,40 +838,52 @@ func (s *contentNodeShifter) Insert(old, new contentNodeI) contentNodeI {
panic(fmt.Sprintf("unknown type %T", new))
}
if vv.s.languagei == newp.s.languagei {
return new
if newp != old {
resource.MarkStale(old)
}
return new, vv, true
}
is := make(contentNodeIs, s.numLanguages)
is[newp.s.languagei] = new
is[vv.s.languagei] = old
return is
return is, old, false
case contentNodeIs:
newp, ok := new.(*pageState)
if !ok {
panic(fmt.Sprintf("unknown type %T", new))
}
resource.MarkStale(vv[newp.s.languagei])
oldp := vv[newp.s.languagei]
if oldp != newp {
resource.MarkStale(oldp)
}
vv[newp.s.languagei] = new
return vv
return vv, oldp, oldp != nil
case *resourceSource:
newp, ok := new.(*resourceSource)
if !ok {
panic(fmt.Sprintf("unknown type %T", new))
}
if vv.LangIndex() == newp.LangIndex() {
return new
if vv != newp {
resource.MarkStale(vv)
}
return new, vv, true
}
rs := make(resourceSources, s.numLanguages)
rs[newp.LangIndex()] = newp
rs[vv.LangIndex()] = vv
return rs
return rs, vv, false
case resourceSources:
newp, ok := new.(*resourceSource)
if !ok {
panic(fmt.Sprintf("unknown type %T", new))
}
resource.MarkStale(vv[newp.LangIndex()])
oldp := vv[newp.LangIndex()]
if oldp != newp {
resource.MarkStale(oldp)
}
vv[newp.LangIndex()] = newp
return vv
return vv, oldp, oldp != nil
default:
panic(fmt.Sprintf("unknown type %T", old))
}
@@ -878,6 +903,8 @@ func newPageMap(i int, s *Site, mcache *dynacache.Cache, pageTrees *pageTrees) *
cacheContentPlain: dynacache.GetOrCreatePartition[string, *resources.StaleValue[contentPlainPlainWords]](mcache, fmt.Sprintf("/cont/pla/%d", i), dynacache.OptionsPartition{Weight: 70, ClearWhen: dynacache.ClearOnChange}),
contentTableOfContents: dynacache.GetOrCreatePartition[string, *resources.StaleValue[contentTableOfContents]](mcache, fmt.Sprintf("/cont/toc/%d", i), dynacache.OptionsPartition{Weight: 70, ClearWhen: dynacache.ClearOnChange}),
contentDataFileSeenItems: maps.NewCache[string, map[uint64]bool](),
cfg: contentMapConfig{
lang: s.Lang(),
taxonomyConfig: taxonomiesConfig.Values(),
@@ -948,8 +975,6 @@ type contentTreeReverseIndexMap struct {
type sitePagesAssembler struct {
*Site
watching bool
incomingChanges *whatChanged
assembleChanges *whatChanged
ctx context.Context
}
@@ -1054,7 +1079,7 @@ func (h *HugoSites) resolveAndClearStateForIdentities(
)
for _, id := range changes {
if staler, ok := id.(resource.Staler); ok && !staler.IsStale() {
if staler, ok := id.(resource.Staler); ok {
var msgDetail string
if p, ok := id.(*pageState); ok && p.File() != nil {
msgDetail = fmt.Sprintf(" (%s)", p.File().Filename())
@@ -1068,6 +1093,7 @@ func (h *HugoSites) resolveAndClearStateForIdentities(
// 1. Handle the cache busters first, as those may produce identities for the page reset step.
// 2. Then reset the page outputs, which may mark some resources as stale.
// 3. Then GC the cache.
// TOOD1
if cachebuster != nil {
if err := loggers.TimeTrackfn(func() (logg.LevelLogger, error) {
ll := l.WithField("substep", "gc dynacache cachebuster")
@@ -1084,7 +1110,7 @@ func (h *HugoSites) resolveAndClearStateForIdentities(
return b
}
h.MemCache.ClearMatching(shouldDelete)
h.MemCache.ClearMatching(nil, shouldDelete)
return ll, nil
}); err != nil {
@@ -1113,6 +1139,33 @@ func (h *HugoSites) resolveAndClearStateForIdentities(
}
changes = changes[:n]
if h.pageTrees.treePagesFromTemplateAdapters.LenRaw() > 0 {
if err := loggers.TimeTrackfn(func() (logg.LevelLogger, error) {
ll := l.WithField("substep", "resolve content adapter change set").WithField("changes", len(changes))
checkedCount := 0
matchCount := 0
depsFinder := identity.NewFinder(identity.FinderConfig{})
h.pageTrees.treePagesFromTemplateAdapters.WalkPrefixRaw(doctree.LockTypeRead, "",
func(s string, n *pagesfromdata.PagesFromTemplate) (bool, error) {
for _, id := range changes {
checkedCount++
if r := depsFinder.Contains(id, n.DependencyManager, 2); r > identity.FinderNotFound {
n.MarkStale()
matchCount++
break
}
}
return false, nil
})
ll = ll.WithField("checked", checkedCount).WithField("matches", matchCount)
return ll, nil
}); err != nil {
return err
}
}
if err := loggers.TimeTrackfn(func() (logg.LevelLogger, error) {
// changesLeft: The IDs that the pages is dependent on.
// changesRight: The IDs that the pages depend on.
@@ -1257,6 +1310,7 @@ func (sa *sitePagesAssembler) applyAggregates() error {
rw := pw.Extend()
rw.Tree = sa.pageMap.treeResources
sa.lastmod = time.Time{}
rebuild := sa.s.h.isRebuild()
pw.Handle = func(keyPage string, n contentNodeI, match doctree.DimensionFlag) (bool, error) {
pageBundle := n.(*pageState)
@@ -1277,7 +1331,7 @@ func (sa *sitePagesAssembler) applyAggregates() error {
// Home page gets it's cascade from the site config.
cascade = sa.conf.Cascade.Config
if pageBundle.m.pageConfig.Cascade == nil {
if pageBundle.m.pageConfig.CascadeCompiled == nil {
// Pass the site cascade downwards.
pw.WalkContext.Data().Insert(keyPage, cascade)
}
@@ -1288,18 +1342,20 @@ func (sa *sitePagesAssembler) applyAggregates() error {
}
}
if (pageBundle.IsHome() || pageBundle.IsSection()) && pageBundle.m.setMetaPostCount > 0 {
oldDates := pageBundle.m.pageConfig.Dates
if rebuild {
if (pageBundle.IsHome() || pageBundle.IsSection()) && pageBundle.m.setMetaPostCount > 0 {
oldDates := pageBundle.m.pageConfig.Dates
// We need to wait until after the walk to determine if any of the dates have changed.
pw.WalkContext.AddPostHook(
func() error {
if oldDates != pageBundle.m.pageConfig.Dates {
sa.assembleChanges.Add(pageBundle)
}
return nil
},
)
// We need to wait until after the walk to determine if any of the dates have changed.
pw.WalkContext.AddPostHook(
func() error {
if oldDates != pageBundle.m.pageConfig.Dates {
sa.assembleChanges.Add(pageBundle)
}
return nil
},
)
}
}
// Combine the cascade map with front matter.
@@ -1309,15 +1365,15 @@ func (sa *sitePagesAssembler) applyAggregates() error {
// We receive cascade values from above. If this leads to a change compared
// to the previous value, we need to mark the page and its dependencies as changed.
if pageBundle.m.setMetaPostCascadeChanged {
if rebuild && pageBundle.m.setMetaPostCascadeChanged {
sa.assembleChanges.Add(pageBundle)
}
const eventName = "dates"
if n.isContentNodeBranch() {
if pageBundle.m.pageConfig.Cascade != nil {
if pageBundle.m.pageConfig.CascadeCompiled != nil {
// Pass it down.
pw.WalkContext.Data().Insert(keyPage, pageBundle.m.pageConfig.Cascade)
pw.WalkContext.Data().Insert(keyPage, pageBundle.m.pageConfig.CascadeCompiled)
}
wasZeroDates := pageBundle.m.pageConfig.Dates.IsAllDatesZero()
@@ -1418,9 +1474,9 @@ func (sa *sitePagesAssembler) applyAggregatesToTaxonomiesAndTerms() error {
p := n.(*pageState)
if p.Kind() != kinds.KindTerm {
// The other kinds were handled in applyAggregates.
if p.m.pageConfig.Cascade != nil {
if p.m.pageConfig.CascadeCompiled != nil {
// Pass it down.
pw.WalkContext.Data().Insert(s, p.m.pageConfig.Cascade)
pw.WalkContext.Data().Insert(s, p.m.pageConfig.CascadeCompiled)
}
}
@@ -1541,7 +1597,7 @@ func (sa *sitePagesAssembler) assembleTermsAndTranslations() error {
singular: viewName.singular,
s: sa.Site,
pathInfo: pi,
pageMetaParams: pageMetaParams{
pageMetaParams: &pageMetaParams{
pageConfig: &pagemeta.PageConfig{
Kind: kinds.KindTerm,
},
@@ -1603,13 +1659,13 @@ func (sa *sitePagesAssembler) assembleResources() error {
targetPaths := ps.targetPaths()
baseTarget := targetPaths.SubResourceBaseTarget
duplicateResourceFiles := true
if ps.s.ContentSpec.Converters.IsGoldmark(ps.m.pageConfig.Markup) {
if ps.m.pageConfig.ContentMediaType.IsMarkdown() {
duplicateResourceFiles = ps.s.ContentSpec.Converters.GetMarkupConfig().Goldmark.DuplicateResourceFiles
}
duplicateResourceFiles = duplicateResourceFiles || ps.s.Conf.IsMultihost()
sa.pageMap.forEachResourceInPage(
err := sa.pageMap.forEachResourceInPage(
ps, lockType,
!duplicateResourceFiles,
func(resourceKey string, n contentNodeI, match doctree.DimensionFlag) (bool, error) {
@@ -1635,6 +1691,23 @@ func (sa *sitePagesAssembler) assembleResources() error {
}
if rs.rc != nil && rs.rc.Content.IsResourceValue() {
if rs.rc.Name == "" {
rs.rc.Name = relPathOriginal
}
r, err := ps.m.s.ResourceSpec.NewResourceWrapperFromResourceConfig(rs.rc)
if err != nil {
return false, err
}
rs.r = r
return false, nil
}
var mt media.Type
if rs.rc != nil {
mt = rs.rc.ContentMediaType
}
rd := resources.ResourceSourceDescriptor{
OpenReadSeekCloser: rs.opener,
Path: rs.path,
@@ -1645,8 +1718,23 @@ func (sa *sitePagesAssembler) assembleResources() error {
BasePathTargetPath: baseTarget,
NameNormalized: relPath,
NameOriginal: relPathOriginal,
MediaType: mt,
LazyPublish: !ps.m.pageConfig.Build.PublishResources,
}
if rs.rc != nil {
rc := rs.rc
rd.OpenReadSeekCloser = rc.Content.ValueAsOpenReadSeekCloser()
if rc.Name != "" {
rd.NameNormalized = rc.Name
rd.NameOriginal = rc.Name
}
if rc.Title != "" {
rd.Title = rc.Title
}
rd.Params = rc.Params
}
r, err := ps.m.s.ResourceSpec.NewResource(rd)
if err != nil {
return false, err
@@ -1656,7 +1744,7 @@ func (sa *sitePagesAssembler) assembleResources() error {
},
)
return false, nil
return false, err
},
}
@@ -1763,7 +1851,7 @@ func (sa *sitePagesAssembler) addStandalonePages() error {
m := &pageMeta{
s: s,
pathInfo: s.Conf.PathParser().Parse(files.ComponentFolderContent, key+f.MediaType.FirstSuffix.FullSuffix),
pageMetaParams: pageMetaParams{
pageMetaParams: &pageMetaParams{
pageConfig: &pagemeta.PageConfig{
Kind: kind,
},
@@ -1881,7 +1969,7 @@ func (sa *sitePagesAssembler) addMissingRootSections() error {
m := &pageMeta{
s: sa.Site,
pathInfo: p,
pageMetaParams: pageMetaParams{
pageMetaParams: &pageMetaParams{
pageConfig: &pagemeta.PageConfig{
Kind: kinds.KindHome,
},
@@ -1891,7 +1979,7 @@ func (sa *sitePagesAssembler) addMissingRootSections() error {
if err != nil {
return err
}
w.Tree.InsertWithLock(p.Base(), n)
w.Tree.InsertIntoValuesDimensionWithLock(p.Base(), n)
sa.home = n
}
@@ -1914,7 +2002,7 @@ func (sa *sitePagesAssembler) addMissingTaxonomies() error {
m := &pageMeta{
s: sa.Site,
pathInfo: sa.Conf.PathParser().Parse(files.ComponentFolderContent, key+"/_index.md"),
pageMetaParams: pageMetaParams{
pageMetaParams: &pageMetaParams{
pageConfig: &pagemeta.PageConfig{
Kind: kinds.KindTaxonomy,
},
+50
View File
@@ -14,6 +14,7 @@
package hugolib
import (
"fmt"
"strings"
"testing"
)
@@ -241,3 +242,52 @@ iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAA
"p1|<p><a href=\"p2\">P2</a>", "<img src=\"pixel.png\" alt=\"Pixel\">")
})
}
func TestRenderHooksDefaultEscape(t *testing.T) {
files := `
-- hugo.toml --
[markup.goldmark.renderHooks]
[markup.goldmark.renderHooks.image]
enableDefault = ENABLE
[markup.goldmark.renderHooks.link]
enableDefault = ENABLE
[markup.goldmark.parser]
wrapStandAloneImageWithinParagraph = false
[markup.goldmark.parser.attribute]
block = true
title = true
-- content/_index.md --
---
title: "Home"
---
Link: [text-"<>&](/destination-"<> 'title-"<>&')
Image: ![alt-"<>&](/destination-"<> 'title-"<>&')
{class="><script>alert()</script>" id="baz"}
-- layouts/index.html --
{{ .Content }}
`
for _, enabled := range []bool{true, false} {
enabled := enabled
t.Run(fmt.Sprint(enabled), func(t *testing.T) {
t.Parallel()
b := Test(t, strings.ReplaceAll(files, "ENABLE", fmt.Sprint(enabled)))
// The escaping is slightly different between the two.
if enabled {
b.AssertFileContent("public/index.html",
"Link: <a href=\"/destination-%22%3C%3E\" title=\"title-&#34;&lt;&gt;&amp;\">text-&quot;&lt;&gt;&amp;</a>",
"img alt=\"alt-&quot;&lt;&gt;&amp;\" src=\"/destination-%22%3C%3E\" title=\"title-&#34;&lt;&gt;&amp;\">",
"&gt;&lt;script&gt;",
)
} else {
b.AssertFileContent("public/index.html",
"Link: <a href=\"/destination-%22%3C%3E\" title=\"title-&quot;&lt;&gt;&amp;\">text-&quot;&lt;&gt;&amp;</a>",
"Image: <img src=\"/destination-%22%3C%3E\" alt=\"alt-&quot;&lt;&gt;&amp;\" title=\"title-&quot;&lt;&gt;&amp;\">",
)
}
})
}
}
+7 -7
View File
@@ -173,7 +173,7 @@ func TestTreeInsert(t *testing.T) {
c.Assert(tree.Get("/notfound"), qt.IsNil)
ab2 := &testValue{ID: "/a/b", Lang: 0}
v, ok := tree.InsertIntoValuesDimension("/a/b", ab2)
v, _, ok := tree.InsertIntoValuesDimension("/a/b", ab2)
c.Assert(ok, qt.IsTrue)
c.Assert(v, qt.DeepEquals, ab2)
@@ -239,16 +239,16 @@ func (s *testShifter) ForEeachInDimension(n *testValue, d int, f func(n *testVal
f(n)
}
func (s *testShifter) Insert(old, new *testValue) *testValue {
return new
func (s *testShifter) Insert(old, new *testValue) (*testValue, *testValue, bool) {
return new, old, true
}
func (s *testShifter) InsertInto(old, new *testValue, dimension doctree.Dimension) *testValue {
return new
func (s *testShifter) InsertInto(old, new *testValue, dimension doctree.Dimension) (*testValue, *testValue, bool) {
return new, old, true
}
func (s *testShifter) Delete(n *testValue, dimension doctree.Dimension) (bool, bool) {
return true, true
func (s *testShifter) Delete(n *testValue, dimension doctree.Dimension) (*testValue, bool, bool) {
return nil, true, true
}
func (s *testShifter) Shift(n *testValue, dimension doctree.Dimension, exact bool) (*testValue, bool, doctree.DimensionFlag) {
+37 -18
View File
@@ -38,16 +38,18 @@ type (
// Insert inserts new into the tree into the dimension it provides.
// It may replace old.
// It returns a T (can be the same as old).
Insert(old, new T) T
// It returns the updated and existing T
// and a bool indicating if an existing record is updated.
Insert(old, new T) (T, T, bool)
// Insert inserts new into the given dimension.
// It may replace old.
// It returns a T (can be the same as old).
InsertInto(old, new T, dimension Dimension) T
// It returns the updated and existing T
// and a bool indicating if an existing record is updated.
InsertInto(old, new T, dimension Dimension) (T, T, bool)
// Delete deletes T from the given dimension and returns whether the dimension was deleted and if it's empty after the delete.
Delete(v T, dimension Dimension) (bool, bool)
// Delete deletes T from the given dimension and returns the deleted T and whether the dimension was deleted and if it's empty after the delete.
Delete(v T, dimension Dimension) (T, bool, bool)
// Shift shifts T into the given dimension
// and returns the shifted T and a bool indicating if the shift was successful and
@@ -81,7 +83,11 @@ func New[T any](cfg Config[T]) *NodeShiftTree[T] {
}
}
func (r *NodeShiftTree[T]) Delete(key string) {
func (r *NodeShiftTree[T]) Delete(key string) (T, bool) {
return r.delete(key)
}
func (r *NodeShiftTree[T]) DeleteRaw(key string) {
r.delete(key)
}
@@ -103,23 +109,24 @@ func (r *NodeShiftTree[T]) DeletePrefix(prefix string) int {
return false
})
for _, key := range keys {
if ok := r.delete(key); ok {
if _, ok := r.delete(key); ok {
count++
}
}
return count
}
func (r *NodeShiftTree[T]) delete(key string) bool {
func (r *NodeShiftTree[T]) delete(key string) (T, bool) {
var wasDeleted bool
var deleted T
if v, ok := r.tree.Get(key); ok {
var isEmpty bool
wasDeleted, isEmpty = r.shifter.Delete(v.(T), r.dims)
deleted, wasDeleted, isEmpty = r.shifter.Delete(v.(T), r.dims)
if isEmpty {
r.tree.Delete(key)
}
}
return wasDeleted
return deleted, wasDeleted
}
func (t *NodeShiftTree[T]) DeletePrefixAll(prefix string) int {
@@ -141,22 +148,33 @@ func (t *NodeShiftTree[T]) Increment(d int) *NodeShiftTree[T] {
return t.Shape(d, t.dims[d]+1)
}
func (r *NodeShiftTree[T]) InsertIntoCurrentDimension(s string, v T) (T, bool) {
func (r *NodeShiftTree[T]) InsertIntoCurrentDimension(s string, v T) (T, T, bool) {
s = mustValidateKey(cleanKey(s))
var (
updated bool
existing T
)
if vv, ok := r.tree.Get(s); ok {
v = r.shifter.InsertInto(vv.(T), v, r.dims)
v, existing, updated = r.shifter.InsertInto(vv.(T), v, r.dims)
}
r.tree.Insert(s, v)
return v, true
return v, existing, updated
}
func (r *NodeShiftTree[T]) InsertIntoValuesDimension(s string, v T) (T, bool) {
// InsertIntoValuesDimension inserts v into the tree at the given key and the
// dimension defined by the value.
// It returns the updated and existing T and a bool indicating if an existing record is updated.
func (r *NodeShiftTree[T]) InsertIntoValuesDimension(s string, v T) (T, T, bool) {
s = mustValidateKey(cleanKey(s))
var (
updated bool
existing T
)
if vv, ok := r.tree.Get(s); ok {
v = r.shifter.Insert(vv.(T), v)
v, existing, updated = r.shifter.Insert(vv.(T), v)
}
r.tree.Insert(s, v)
return v, true
return v, existing, updated
}
func (r *NodeShiftTree[T]) InsertRawWithLock(s string, v any) (any, bool) {
@@ -165,7 +183,8 @@ func (r *NodeShiftTree[T]) InsertRawWithLock(s string, v any) (any, bool) {
return r.tree.Insert(s, v)
}
func (r *NodeShiftTree[T]) InsertWithLock(s string, v T) (T, bool) {
// It returns the updated and existing T and a bool indicating if an existing record is updated.
func (r *NodeShiftTree[T]) InsertIntoValuesDimensionWithLock(s string, v T) (T, T, bool) {
r.mu.Lock()
defer r.mu.Unlock()
return r.InsertIntoValuesDimension(s, v)
+12 -5
View File
@@ -28,12 +28,12 @@ type Tree[T any] interface {
}
// NewSimpleTree creates a new SimpleTree.
func NewSimpleTree[T any]() *SimpleTree[T] {
func NewSimpleTree[T comparable]() *SimpleTree[T] {
return &SimpleTree[T]{tree: radix.New()}
}
// SimpleTree is a thread safe radix tree that holds T.
type SimpleTree[T any] struct {
type SimpleTree[T comparable] struct {
mu sync.RWMutex
tree *radix.Tree
zero T
@@ -67,16 +67,23 @@ func (tree *SimpleTree[T]) Insert(s string, v T) T {
return v
}
func (tree *SimpleTree[T]) WalkPrefix(lockType LockType, s string, f func(s string, v T) (bool, error)) error {
func (tree *SimpleTree[T]) Lock(lockType LockType) func() {
switch lockType {
case LockTypeNone:
return func() {}
case LockTypeRead:
tree.mu.RLock()
defer tree.mu.RUnlock()
return tree.mu.RUnlock
case LockTypeWrite:
tree.mu.Lock()
defer tree.mu.Unlock()
return tree.mu.Unlock
}
return func() {}
}
func (tree *SimpleTree[T]) WalkPrefix(lockType LockType, s string, f func(s string, v T) (bool, error)) error {
commit := tree.Lock(lockType)
defer commit()
var err error
tree.tree.WalkPrefix(s, func(s string, v any) bool {
var b bool
+3 -3
View File
@@ -113,7 +113,7 @@ type LockType int
// MutableTree is a tree that can be modified.
type MutableTree interface {
Delete(key string)
DeleteRaw(key string)
DeleteAll(key string)
DeletePrefix(prefix string) int
DeletePrefixAll(prefix string) int
@@ -140,9 +140,9 @@ var _ MutableTree = MutableTrees(nil)
type MutableTrees []MutableTree
func (t MutableTrees) Delete(key string) {
func (t MutableTrees) DeleteRaw(key string) {
for _, tree := range t {
tree.Delete(key)
tree.DeleteRaw(key)
}
}
+37 -24
View File
@@ -15,18 +15,21 @@ package doctree
var _ Tree[string] = (*TreeShiftTree[string])(nil)
type TreeShiftTree[T any] struct {
type TreeShiftTree[T comparable] struct {
// This tree is shiftable in one dimension.
d int
// The value of the current dimension.
v int
// The zero value of T.
zero T
// Will be of length equal to the length of the dimension.
trees []*SimpleTree[T]
}
func NewTreeShiftTree[T any](d, length int) *TreeShiftTree[T] {
func NewTreeShiftTree[T comparable](d, length int) *TreeShiftTree[T] {
if length <= 0 {
panic("length must be > 0")
}
@@ -52,6 +55,17 @@ func (t *TreeShiftTree[T]) Get(s string) T {
return t.trees[t.v].Get(s)
}
func (t *TreeShiftTree[T]) DeleteAllFunc(s string, f func(s string, v T) bool) {
for _, tt := range t.trees {
if v := tt.Get(s); v != t.zero {
if f(s, v) {
// Delete.
tt.tree.Delete(s)
}
}
}
}
func (t *TreeShiftTree[T]) LongestPrefix(s string) (string, T) {
return t.trees[t.v].LongestPrefix(s)
}
@@ -60,10 +74,31 @@ func (t *TreeShiftTree[T]) Insert(s string, v T) T {
return t.trees[t.v].Insert(s, v)
}
func (t *TreeShiftTree[T]) Lock(lockType LockType) func() {
return t.trees[t.v].Lock(lockType)
}
func (t *TreeShiftTree[T]) WalkPrefix(lockType LockType, s string, f func(s string, v T) (bool, error)) error {
return t.trees[t.v].WalkPrefix(lockType, s, f)
}
func (t *TreeShiftTree[T]) WalkPrefixRaw(lockType LockType, s string, f func(s string, v T) (bool, error)) error {
for _, tt := range t.trees {
if err := tt.WalkPrefix(lockType, s, f); err != nil {
return err
}
}
return nil
}
func (t *TreeShiftTree[T]) LenRaw() int {
var count int
for _, tt := range t.trees {
count += tt.tree.Len()
}
return count
}
func (t *TreeShiftTree[T]) Delete(key string) {
for _, tt := range t.trees {
tt.tree.Delete(key)
@@ -77,25 +112,3 @@ func (t *TreeShiftTree[T]) DeletePrefix(prefix string) int {
}
return count
}
func (t *TreeShiftTree[T]) Lock(writable bool) (commit func()) {
if writable {
for _, tt := range t.trees {
tt.mu.Lock()
}
return func() {
for _, tt := range t.trees {
tt.mu.Unlock()
}
}
}
for _, tt := range t.trees {
tt.mu.RLock()
}
return func() {
for _, tt := range t.trees {
tt.mu.RUnlock()
}
}
}
+19
View File
@@ -111,6 +111,24 @@ func (h *HugoSites) ShouldSkipFileChangeEvent(ev fsnotify.Event) bool {
return h.skipRebuildForFilenames[ev.Name]
}
func (h *HugoSites) isRebuild() bool {
return h.buildCounter.Load() > 0
}
func (h *HugoSites) resolveSite(lang string) *Site {
if lang == "" {
lang = h.Conf.DefaultContentLanguage()
}
for _, s := range h.Sites {
if s.Lang() == lang {
return s
}
}
return nil
}
// Only used in tests.
type buildCounters struct {
contentRenderCounter atomic.Uint64
@@ -479,6 +497,7 @@ func (h *HugoSites) loadData() error {
hugofs.WalkwayConfig{
Fs: h.PathSpec.BaseFs.Data.Fs,
IgnoreFile: h.SourceSpec.IgnoreFile,
PathParser: h.Conf.PathParser(),
WalkFn: func(path string, fi hugofs.FileMetaInfo) error {
if fi.IsDir() {
return nil
+116 -45
View File
@@ -26,11 +26,12 @@ import (
"time"
"github.com/bep/logg"
"github.com/gohugoio/hugo/cache/dynacache"
"github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/hugofs/glob"
"github.com/gohugoio/hugo/hugolib/doctree"
"github.com/gohugoio/hugo/hugolib/pagesfromdata"
"github.com/gohugoio/hugo/hugolib/segments"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/output"
@@ -42,6 +43,7 @@ import (
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/para"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/common/rungroup"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/page/siteidentities"
@@ -97,6 +99,10 @@ func (h *HugoSites) Build(config BuildCfg, events ...fsnotify.Event) error {
close(to)
}(errCollector, errs)
for _, s := range h.Sites {
s.state = siteStateInit
}
if h.Metrics != nil {
h.Metrics.Reset()
}
@@ -110,7 +116,7 @@ func (h *HugoSites) Build(config BuildCfg, events ...fsnotify.Event) error {
conf := &config
if conf.whatChanged == nil {
// Assume everything has changed
conf.whatChanged = &whatChanged{contentChanged: true}
conf.whatChanged = &whatChanged{needsPagesAssembly: true}
}
var prepareErr error
@@ -154,6 +160,10 @@ func (h *HugoSites) Build(config BuildCfg, events ...fsnotify.Event) error {
}
}
for _, s := range h.Sites {
s.state = siteStateReady
}
if prepareErr == nil {
if err := h.render(infol, conf); err != nil {
h.SendError(fmt.Errorf("render: %w", err))
@@ -214,7 +224,7 @@ func (h *HugoSites) initRebuild(config *BuildCfg) error {
})
for _, s := range h.Sites {
s.resetBuildState(config.whatChanged.contentChanged)
s.resetBuildState(config.whatChanged.needsPagesAssembly)
}
h.reset(config)
@@ -233,7 +243,7 @@ func (h *HugoSites) process(ctx context.Context, l logg.LevelLogger, config *Bui
// This is a rebuild
return h.processPartial(ctx, l, config, init, events)
}
return h.processFull(ctx, l, *config)
return h.processFull(ctx, l, config)
}
// assemble creates missing sections, applies aggregate values (e.g. dates, cascading params),
@@ -242,22 +252,24 @@ func (h *HugoSites) assemble(ctx context.Context, l logg.LevelLogger, bcfg *Buil
l = l.WithField("step", "assemble")
defer loggers.TimeTrackf(l, time.Now(), nil, "")
if !bcfg.whatChanged.contentChanged {
if !bcfg.whatChanged.needsPagesAssembly {
changes := bcfg.whatChanged.Drain()
if len(changes) > 0 {
if err := h.resolveAndClearStateForIdentities(ctx, l, nil, changes); err != nil {
return err
}
}
return nil
}
h.translationKeyPages.Reset()
assemblers := make([]*sitePagesAssembler, len(h.Sites))
// Changes detected during assembly (e.g. aggregate date changes)
assembleChanges := &whatChanged{
identitySet: make(map[identity.Identity]bool),
}
for i, s := range h.Sites {
assemblers[i] = &sitePagesAssembler{
Site: s,
watching: s.watching(),
incomingChanges: bcfg.whatChanged,
assembleChanges: assembleChanges,
assembleChanges: bcfg.whatChanged,
ctx: ctx,
}
}
@@ -273,9 +285,9 @@ func (h *HugoSites) assemble(ctx context.Context, l logg.LevelLogger, bcfg *Buil
return err
}
changes := assembleChanges.Changes()
changes := bcfg.whatChanged.Drain()
// Changes from the assemble step (e.g. lastMod, cascase) needs a re-calculation
// Changes from the assemble step (e.g. lastMod, cascade) needs a re-calculation
// of what needs to be re-built.
if len(changes) > 0 {
if err := h.resolveAndClearStateForIdentities(ctx, l, nil, changes); err != nil {
@@ -596,6 +608,10 @@ type pathChange struct {
isDir bool
}
func (p pathChange) isStructuralChange() bool {
return p.delete || p.isDir
}
// processPartial prepares the Sites' sources for a partial rebuild.
func (h *HugoSites) processPartial(ctx context.Context, l logg.LevelLogger, config *BuildCfg, init func(config *BuildCfg) error, events []fsnotify.Event) error {
h.Log.Trace(logg.StringFunc(func() string {
@@ -610,16 +626,16 @@ func (h *HugoSites) processPartial(ctx context.Context, l logg.LevelLogger, conf
// For a list of events for the different OSes, see the test output in https://github.com/bep/fsnotifyeventlister/.
events = h.fileEventsFilter(events)
events = h.fileEventsTranslate(events)
events = h.fileEventsTrim(events)
eventInfos := h.fileEventsApplyInfo(events)
logger := h.Log
var (
tmplAdded bool
tmplChanged bool
i18nChanged bool
contentChanged bool
tmplAdded bool
tmplChanged bool
i18nChanged bool
needsPagesAssemble bool
)
changedPaths := struct {
@@ -693,11 +709,33 @@ func (h *HugoSites) processPartial(ctx context.Context, l logg.LevelLogger, conf
switch pathInfo.Component() {
case files.ComponentFolderContent:
logger.Println("Source changed", pathInfo.Path())
if ids := h.pageTrees.collectAndMarkStaleIdentities(pathInfo); len(ids) > 0 {
changes = append(changes, ids...)
isContentDataFile := pathInfo.IsContentData()
if !isContentDataFile {
if ids := h.pageTrees.collectAndMarkStaleIdentities(pathInfo); len(ids) > 0 {
changes = append(changes, ids...)
}
} else {
h.pageTrees.treePagesFromTemplateAdapters.DeleteAllFunc(pathInfo.Base(),
func(s string, n *pagesfromdata.PagesFromTemplate) bool {
changes = append(changes, n.DependencyManager)
// Try to open the file to see if has been deleted.
f, err := n.GoTmplFi.Meta().Open()
if err == nil {
f.Close()
}
if err != nil {
// Remove all pages and resources below.
prefix := pathInfo.Base() + "/"
h.pageTrees.treePages.DeletePrefixAll(prefix)
h.pageTrees.resourceTrees.DeletePrefixAll(prefix)
changes = append(changes, identity.NewGlobIdentity(prefix+"*"))
}
return err != nil
})
}
contentChanged = true
needsPagesAssemble = true
if config.RecentlyVisited != nil {
// Fast render mode. Adding them to the visited queue
@@ -711,7 +749,7 @@ func (h *HugoSites) processPartial(ctx context.Context, l logg.LevelLogger, conf
h.pageTrees.treeTaxonomyEntries.DeletePrefix("")
if delete {
if delete && !isContentDataFile {
_, ok := h.pageTrees.treePages.LongestPrefixAll(pathInfo.Base())
if ok {
h.pageTrees.treePages.DeleteAll(pathInfo.Base())
@@ -759,17 +797,7 @@ func (h *HugoSites) processPartial(ctx context.Context, l logg.LevelLogger, conf
}
case files.ComponentFolderAssets:
logger.Println("Asset changed", pathInfo.Path())
var hasID bool
r, _ := h.ResourceSpec.ResourceCache.Get(context.Background(), dynacache.CleanKey(pathInfo.Base()))
identity.WalkIdentitiesShallow(r, func(level int, rid identity.Identity) bool {
hasID = true
changes = append(changes, rid)
return false
})
if !hasID {
changes = append(changes, pathInfo)
}
changes = append(changes, pathInfo)
case files.ComponentFolderData:
logger.Println("Data changed", pathInfo.Path())
@@ -860,8 +888,8 @@ func (h *HugoSites) processPartial(ctx context.Context, l logg.LevelLogger, conf
resourceFiles := h.fileEventsContentPaths(addedOrChangedContent)
changed := &whatChanged{
contentChanged: contentChanged,
identitySet: make(identity.Identities),
needsPagesAssembly: needsPagesAssemble,
identitySet: make(identity.Identities),
}
changed.Add(changes...)
@@ -883,10 +911,7 @@ func (h *HugoSites) processPartial(ctx context.Context, l logg.LevelLogger, conf
}
}
// Removes duplicates.
changes = changed.identitySet.AsSlice()
if err := h.resolveAndClearStateForIdentities(ctx, l, cacheBusterOr, changes); err != nil {
if err := h.resolveAndClearStateForIdentities(ctx, l, cacheBusterOr, changed.Drain()); err != nil {
return err
}
@@ -914,17 +939,21 @@ func (h *HugoSites) processPartial(ctx context.Context, l logg.LevelLogger, conf
}
if resourceFiles != nil {
if err := h.processFiles(ctx, l, *config, resourceFiles...); err != nil {
if err := h.processFiles(ctx, l, config, resourceFiles...); err != nil {
return err
}
}
h.logServerAddresses()
if h.isRebuild() {
if err := h.processContentAdaptersOnRebuild(ctx, config); err != nil {
return err
}
}
return nil
}
func (h *HugoSites) logServerAddresses() {
func (h *HugoSites) LogServerAddresses() {
if h.hugoInfo.IsMultihost() {
for _, s := range h.Sites {
h.Log.Printf("Web Server is available at %s (bind address %s) %s\n", s.conf.C.BaseURL, s.conf.C.ServerInterface, s.Language().Lang)
@@ -935,7 +964,7 @@ func (h *HugoSites) logServerAddresses() {
}
}
func (h *HugoSites) processFull(ctx context.Context, l logg.LevelLogger, config BuildCfg) (err error) {
func (h *HugoSites) processFull(ctx context.Context, l logg.LevelLogger, config *BuildCfg) (err error) {
if err = h.processFiles(ctx, l, config); err != nil {
err = fmt.Errorf("readAndProcessContent: %w", err)
return
@@ -943,7 +972,49 @@ func (h *HugoSites) processFull(ctx context.Context, l logg.LevelLogger, config
return err
}
func (s *HugoSites) processFiles(ctx context.Context, l logg.LevelLogger, buildConfig BuildCfg, filenames ...pathChange) error {
func (s *Site) handleContentAdapterChanges(bi pagesfromdata.BuildInfo, buildConfig *BuildCfg) {
if !s.h.isRebuild() {
return
}
if len(bi.ChangedIdentities) > 0 {
buildConfig.whatChanged.Add(bi.ChangedIdentities...)
buildConfig.whatChanged.needsPagesAssembly = true
}
for _, p := range bi.DeletedPaths {
pp := path.Join(bi.Path.Base(), p)
if v, ok := s.pageMap.treePages.Delete(pp); ok {
buildConfig.whatChanged.Add(v.GetIdentity())
}
}
}
func (h *HugoSites) processContentAdaptersOnRebuild(ctx context.Context, buildConfig *BuildCfg) error {
g := rungroup.Run[*pagesfromdata.PagesFromTemplate](ctx, rungroup.Config[*pagesfromdata.PagesFromTemplate]{
NumWorkers: h.numWorkers,
Handle: func(ctx context.Context, p *pagesfromdata.PagesFromTemplate) error {
bi, err := p.Execute(ctx)
if err != nil {
return err
}
s := p.Site.(*Site)
s.handleContentAdapterChanges(bi, buildConfig)
return nil
},
})
h.pageTrees.treePagesFromTemplateAdapters.WalkPrefixRaw(doctree.LockTypeRead, "", func(key string, p *pagesfromdata.PagesFromTemplate) (bool, error) {
if p.StaleVersion() > 0 {
g.Enqueue(p)
}
return false, nil
})
return g.Wait()
}
func (s *HugoSites) processFiles(ctx context.Context, l logg.LevelLogger, buildConfig *BuildCfg, filenames ...pathChange) error {
if s.Deps == nil {
panic("nil deps on site")
}
@@ -953,7 +1024,7 @@ func (s *HugoSites) processFiles(ctx context.Context, l logg.LevelLogger, buildC
// For inserts, we can pick an arbitrary pageMap.
pageMap := s.Sites[0].pageMap
c := newPagesCollector(ctx, s.h, sourceSpec, s.Log, l, pageMap, filenames)
c := newPagesCollector(ctx, s.h, sourceSpec, s.Log, l, pageMap, buildConfig, filenames)
if err := c.Collect(); err != nil {
return err
+28
View File
@@ -252,3 +252,31 @@ Files: {{ range $files }}{{ .Permalink }}|{{ end }}$
b.AssertFileContent("public/en/enpages/mybundle-en/file2.txt", "File 2 en.")
b.AssertFileContent("public/fr/section/mybundle/file2.txt", "File 2 en.")
}
func TestMultihostAllButOneLanguageDisabledIssue12288(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
defaultContentLanguage = "en"
disableLanguages = ["fr"]
#baseURL = "https://example.com"
[languages]
[languages.en]
baseURL = "https://example.en"
weight = 1
[languages.fr]
baseURL = "https://example.fr"
weight = 2
-- assets/css/main.css --
body { color: red; }
-- layouts/index.html --
{{ $css := resources.Get "css/main.css" | minify }}
CSS: {{ $css.Permalink }}|{{ $css.RelPermalink }}|
`
b := Test(t, files)
b.AssertFileContent("public/css/main.min.css", "body{color:red}")
b.AssertFileContent("public/index.html", "CSS: https://example.en/css/main.min.css|/css/main.min.css|")
}
+1 -1
View File
@@ -41,7 +41,7 @@ Home: {{ .Title }}
IntegrationTestConfig{
T: t,
TxtarString: files,
LogLevel: logg.LevelTrace,
// LogLevel: logg.LevelTrace,
},
).Build()
+48 -1
View File
@@ -10,6 +10,7 @@ import (
"os"
"path/filepath"
"regexp"
"runtime"
"sort"
"strings"
"sync"
@@ -37,12 +38,20 @@ import (
type TestOpt func(*IntegrationTestConfig)
// TestOptRunning will enable running in integration tests.
func TestOptRunning() TestOpt {
return func(c *IntegrationTestConfig) {
c.Running = true
}
}
// TestOptWatching will enable watching in integration tests.
func TestOptWatching() TestOpt {
return func(c *IntegrationTestConfig) {
c.Watching = true
}
}
// Enable tracing in integration tests.
// THis should only be used during development and not committed to the repo.
func TestOptTrace() TestOpt {
@@ -569,6 +578,10 @@ func (s *IntegrationTestBuilder) initBuilder() error {
"running": s.Cfg.Running,
"watch": s.Cfg.Running,
})
} else if s.Cfg.Watching {
flags.Set("internal", maps.Params{
"watch": s.Cfg.Watching,
})
}
if s.Cfg.WorkingDir != "" {
@@ -685,8 +698,17 @@ func (s *IntegrationTestBuilder) build(cfg BuildCfg) error {
return nil
}
// We simulate the fsnotify events.
// See the test output in https://github.com/bep/fsnotifyeventlister for what events gets produced
// by the different OSes.
func (s *IntegrationTestBuilder) changeEvents() []fsnotify.Event {
var events []fsnotify.Event
var (
events []fsnotify.Event
isLinux = runtime.GOOS == "linux"
isMacOs = runtime.GOOS == "darwin"
isWindows = runtime.GOOS == "windows"
)
for _, v := range s.removedFiles {
events = append(events, fsnotify.Event{
Name: v,
@@ -713,12 +735,32 @@ func (s *IntegrationTestBuilder) changeEvents() []fsnotify.Event {
Name: v,
Op: fsnotify.Write,
})
if isLinux || isWindows {
// Duplicate write events, for some reason.
events = append(events, fsnotify.Event{
Name: v,
Op: fsnotify.Write,
})
}
if isMacOs {
events = append(events, fsnotify.Event{
Name: v,
Op: fsnotify.Chmod,
})
}
}
for _, v := range s.createdFiles {
events = append(events, fsnotify.Event{
Name: v,
Op: fsnotify.Create,
})
if isLinux || isWindows {
events = append(events, fsnotify.Event{
Name: v,
Op: fsnotify.Write,
})
}
}
// Shuffle events.
@@ -787,6 +829,11 @@ type IntegrationTestConfig struct {
// Whether to simulate server mode.
Running bool
// Watch for changes.
// This is (currently) always set to true when Running is set.
// Note that the CLI for the server does allow for --watch=false, but that is not used in these test.
Watching bool
// Will print the log buffer after the build
Verbose bool
+34
View File
@@ -676,3 +676,37 @@ menu: main
b.AssertFileContent("public/fr/index.html", `<a href="/fr/p1/">p1</a>`)
b.AssertLogNotContains("WARN")
}
func TestSectionPagesIssue12399(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ['rss','sitemap','taxonomy','term']
capitalizeListTitles = false
pluralizeListTitles = false
sectionPagesMenu = 'main'
-- content/p1.md --
---
title: p1
---
-- content/s1/p2.md --
---
title: p2
menus: main
---
-- content/s1/p3.md --
---
title: p3
---
-- layouts/_default/list.html --
{{ range site.Menus.main }}<a href="{{ .URL }}">{{ .Name }}</a>{{ end }}
-- layouts/_default/single.html --
{{ .Title }}
`
b := Test(t, files)
b.AssertFileExists("public/index.html", true)
b.AssertFileContent("public/index.html", `<a href="/s1/p2/">p2</a><a href="/s1/">s1</a>`)
}
+12 -2
View File
@@ -510,9 +510,15 @@ func (p *pageState) renderResources() error {
continue
}
if _, isWrapper := r.(resource.ResourceWrapper); isWrapper {
// Skip resources that are wrapped.
// These gets published on its own.
continue
}
src, ok := r.(resource.Source)
if !ok {
initErr = fmt.Errorf("resource %T does not support resource.Source", src)
initErr = fmt.Errorf("resource %T does not support resource.Source", r)
return
}
@@ -581,7 +587,11 @@ func (p *pageState) getPageInfoForError() string {
func (p *pageState) getContentConverter() converter.Converter {
var err error
p.contentConverterInit.Do(func() {
markup := p.m.pageConfig.Markup
if p.m.pageConfig.ContentMediaType.IsZero() {
panic("ContentMediaType not set")
}
markup := p.m.pageConfig.ContentMediaType.SubType
if markup == "html" {
// Only used for shortcode inner content.
markup = "markdown"
+1
View File
@@ -67,6 +67,7 @@ type pageCommon struct {
page.InSectionPositioner
page.OutputFormatsProvider
page.PageMetaProvider
page.PageMetaInternalProvider
page.Positioner
page.RawContentProvider
page.RelatedKeywordsProvider
+52 -27
View File
@@ -20,6 +20,7 @@ import (
"fmt"
"html/template"
"io"
"path/filepath"
"strconv"
"strings"
"unicode/utf8"
@@ -54,21 +55,31 @@ type pageContentReplacement struct {
source pageparser.Item
}
func (m *pageMeta) parseFrontMatter(h *HugoSites, pid uint64, sourceKey string) (*contentParseInfo, error) {
var openSource hugio.OpenReadSeekCloser
if m.f != nil {
meta := m.f.FileInfo().Meta()
openSource = func() (hugio.ReadSeekCloser, error) {
r, err := meta.Open()
if err != nil {
return nil, fmt.Errorf("failed to open file %q: %w", meta.Filename, err)
func (m *pageMeta) parseFrontMatter(h *HugoSites, pid uint64) (*contentParseInfo, error) {
var (
sourceKey string
openSource hugio.OpenReadSeekCloser
hasContent = m.pageConfig.IsFromContentAdapter
)
if m.f != nil && !hasContent {
sourceKey = filepath.ToSlash(m.f.Filename())
if !hasContent {
meta := m.f.FileInfo().Meta()
openSource = func() (hugio.ReadSeekCloser, error) {
r, err := meta.Open()
if err != nil {
return nil, fmt.Errorf("failed to open file %q: %w", meta.Filename, err)
}
return r, nil
}
return r, nil
}
} else if hasContent {
openSource = m.pageConfig.Content.ValueAsOpenReadSeekCloser()
}
if sourceKey == "" {
sourceKey = strconv.Itoa(int(pid))
sourceKey = strconv.FormatUint(pid, 10)
}
pi := &contentParseInfo{
@@ -93,6 +104,11 @@ func (m *pageMeta) parseFrontMatter(h *HugoSites, pid uint64, sourceKey string)
pi.itemsStep1 = items
if hasContent {
// No front matter.
return pi, nil
}
if err := pi.mapFrontMatter(source); err != nil {
return nil, err
}
@@ -418,6 +434,8 @@ func (c *cachedContent) mustSource() []byte {
func (c *contentParseInfo) contentSource(s resource.StaleInfo) ([]byte, error) {
key := c.sourceKey
versionv := s.StaleVersion()
v, err := c.h.cacheContentSource.GetOrCreate(key, func(string) (*resources.StaleValue[[]byte], error) {
b, err := c.readSourceAll()
if err != nil {
@@ -426,8 +444,8 @@ func (c *contentParseInfo) contentSource(s resource.StaleInfo) ([]byte, error) {
return &resources.StaleValue[[]byte]{
Value: b,
IsStaleFunc: func() bool {
return s.IsStale()
StaleVersionFunc: func() uint32 {
return s.StaleVersion() - versionv
},
}, nil
})
@@ -487,7 +505,7 @@ type contentPlainPlainWords struct {
func (c *cachedContent) contentRendered(ctx context.Context, cp *pageContentOutput) (contentSummary, error) {
ctx = tpl.Context.DependencyScope.Set(ctx, pageDependencyScopeGlobal)
key := c.pi.sourceKey + "/" + cp.po.f.Name
versionv := cp.contentRenderedVersion
versionv := c.version(cp)
v, err := c.pm.cacheContentRendered.GetOrCreate(key, func(string) (*resources.StaleValue[contentSummary], error) {
cp.po.p.s.Log.Trace(logg.StringFunc(func() string {
@@ -504,8 +522,8 @@ func (c *cachedContent) contentRendered(ctx context.Context, cp *pageContentOutp
}
rs := &resources.StaleValue[contentSummary]{
IsStaleFunc: func() bool {
return c.IsStale() || cp.contentRenderedVersion != versionv
StaleVersionFunc: func() uint32 {
return c.version(cp) - versionv
},
}
@@ -522,6 +540,7 @@ func (c *cachedContent) contentRendered(ctx context.Context, cp *pageContentOutp
if err != nil {
return nil, err
}
if !ok {
return nil, errors.New("invalid state: astDoc is set but RenderContent returned false")
}
@@ -564,15 +583,14 @@ func (c *cachedContent) contentRendered(ctx context.Context, cp *pageContentOutp
var result contentSummary // hasVariants bool
if c.pi.hasSummaryDivider {
isHTML := cp.po.p.m.pageConfig.Markup == "html"
if isHTML {
if cp.po.p.m.pageConfig.ContentMediaType.IsHTML() {
// Use the summary sections as provided by the user.
i := bytes.Index(b, internalSummaryDividerPre)
result.summary = helpers.BytesToHTML(b[:i])
b = b[i+len(internalSummaryDividerPre):]
} else {
summary, content, err := splitUserDefinedSummaryAndContent(cp.po.p.m.pageConfig.Markup, b)
summary, content, err := splitUserDefinedSummaryAndContent(cp.po.p.m.pageConfig.Content.Markup, b)
if err != nil {
cp.po.p.s.Log.Errorf("Failed to set user defined summary for page %q: %s", cp.po.p.pathOrTitle(), err)
} else {
@@ -606,7 +624,7 @@ var setGetContentCallbackInContext = hcontext.NewContextDispatcher[func(*pageCon
func (c *cachedContent) contentToC(ctx context.Context, cp *pageContentOutput) (contentTableOfContents, error) {
key := c.pi.sourceKey + "/" + cp.po.f.Name
versionv := cp.contentRenderedVersion
versionv := c.version(cp)
v, err := c.pm.contentTableOfContents.GetOrCreate(key, func(string) (*resources.StaleValue[contentTableOfContents], error) {
source, err := c.pi.contentSource(c)
@@ -626,8 +644,10 @@ func (c *cachedContent) contentToC(ctx context.Context, cp *pageContentOutput) (
return nil, err
}
// Callback called from above (e.g. in .RenderString)
// Callback called from below (e.g. in .RenderString)
ctxCallback := func(cp2 *pageContentOutput, ct2 contentTableOfContents) {
cp.otherOutputs[cp2.po.p.pid] = cp2
// Merge content placeholders
for k, v := range ct2.contentPlaceholders {
ct.contentPlaceholders[k] = v
@@ -660,7 +680,7 @@ func (c *cachedContent) contentToC(ctx context.Context, cp *pageContentOutput) (
p.pageOutputTemplateVariationsState.Add(1)
}
isHTML := cp.po.p.m.pageConfig.Markup == "html"
isHTML := cp.po.p.m.pageConfig.ContentMediaType.IsHTML()
if !isHTML {
createAndSetToC := func(tocProvider converter.TableOfContentsProvider) {
@@ -710,8 +730,8 @@ func (c *cachedContent) contentToC(ctx context.Context, cp *pageContentOutput) (
return &resources.StaleValue[contentTableOfContents]{
Value: ct,
IsStaleFunc: func() bool {
return c.IsStale() || cp.contentRenderedVersion != versionv
StaleVersionFunc: func() uint32 {
return c.version(cp) - versionv
},
}, nil
})
@@ -722,16 +742,21 @@ func (c *cachedContent) contentToC(ctx context.Context, cp *pageContentOutput) (
return v.Value, nil
}
func (c *cachedContent) version(cp *pageContentOutput) uint32 {
// Both of these gets incremented on change.
return c.StaleVersion() + cp.contentRenderedVersion
}
func (c *cachedContent) contentPlain(ctx context.Context, cp *pageContentOutput) (contentPlainPlainWords, error) {
key := c.pi.sourceKey + "/" + cp.po.f.Name
versionv := cp.contentRenderedVersion
versionv := c.version(cp)
v, err := c.pm.cacheContentPlain.GetOrCreateWitTimeout(key, cp.po.p.s.Conf.Timeout(), func(string) (*resources.StaleValue[contentPlainPlainWords], error) {
var result contentPlainPlainWords
rs := &resources.StaleValue[contentPlainPlainWords]{
IsStaleFunc: func() bool {
return c.IsStale() || cp.contentRenderedVersion != versionv
StaleVersionFunc: func() uint32 {
return c.version(cp) - versionv
},
}
@@ -778,7 +803,7 @@ func (c *cachedContent) contentPlain(ctx context.Context, cp *pageContentOutput)
if err != nil {
return nil, err
}
html := cp.po.p.s.ContentSpec.TrimShortHTML(b.Bytes())
html := cp.po.p.s.ContentSpec.TrimShortHTML(b.Bytes(), cp.po.p.m.pageConfig.Content.Markup)
result.summary = helpers.BytesToHTML(html)
} else {
var summary string
+68 -34
View File
@@ -54,7 +54,7 @@ type pageMeta struct {
singular string // Set for kind == KindTerm and kind == KindTaxonomy.
resource.Staler
pageMetaParams
*pageMetaParams
pageMetaFrontMatter
// Set for standalone pages, e.g. robotsTXT.
@@ -74,7 +74,9 @@ type pageMeta struct {
// Prepare for a rebuild of the data passed in from front matter.
func (m *pageMeta) setMetaPostPrepareRebuild() {
params := xmaps.Clone[map[string]any](m.paramsOriginal)
m.pageMetaParams.pageConfig.Params = params
m.pageMetaParams.pageConfig = &pagemeta.PageConfig{
Params: params,
}
m.pageMetaFrontMatter = pageMetaFrontMatter{}
}
@@ -98,7 +100,7 @@ type pageMetaFrontMatter struct {
func (m *pageMetaParams) init(preserveOringal bool) {
if preserveOringal {
m.paramsOriginal = xmaps.Clone[maps.Params](m.pageConfig.Params)
m.cascadeOriginal = xmaps.Clone[map[page.PageMatcher]maps.Params](m.pageConfig.Cascade)
m.cascadeOriginal = xmaps.Clone[map[page.PageMatcher]maps.Params](m.pageConfig.CascadeCompiled)
}
}
@@ -135,19 +137,19 @@ func (p *pageMeta) BundleType() string {
}
func (p *pageMeta) Date() time.Time {
return p.pageConfig.Date
return p.pageConfig.Dates.Date
}
func (p *pageMeta) PublishDate() time.Time {
return p.pageConfig.PublishDate
return p.pageConfig.Dates.PublishDate
}
func (p *pageMeta) Lastmod() time.Time {
return p.pageConfig.Lastmod
return p.pageConfig.Dates.Lastmod
}
func (p *pageMeta) ExpiryDate() time.Time {
return p.pageConfig.ExpiryDate
return p.pageConfig.Dates.ExpiryDate
}
func (p *pageMeta) Description() string {
@@ -275,11 +277,9 @@ func (p *pageMeta) Weight() int {
func (p *pageMeta) setMetaPre(pi *contentParseInfo, logger loggers.Logger, conf config.AllProvider) error {
frontmatter := pi.frontMatter
if frontmatter != nil {
pcfg := p.pageConfig
if pcfg == nil {
panic("pageConfig not set")
}
// Needed for case insensitive fetching of params values
maps.PrepareParams(frontmatter)
pcfg.Params = frontmatter
@@ -290,7 +290,7 @@ func (p *pageMeta) setMetaPre(pi *contentParseInfo, logger loggers.Logger, conf
if err != nil {
return err
}
pcfg.Cascade = cascade
pcfg.CascadeCompiled = cascade
}
// Look for path, lang and kind, all of which values we need early on.
@@ -328,18 +328,18 @@ func (ps *pageState) setMetaPost(cascade map[page.PageMatcher]maps.Params) error
ps.m.setMetaPostCount++
var cascadeHashPre uint64
if ps.m.setMetaPostCount > 1 {
cascadeHashPre = identity.HashUint64(ps.m.pageConfig.Cascade)
ps.m.pageConfig.Cascade = xmaps.Clone[map[page.PageMatcher]maps.Params](ps.m.cascadeOriginal)
cascadeHashPre = identity.HashUint64(ps.m.pageConfig.CascadeCompiled)
ps.m.pageConfig.CascadeCompiled = xmaps.Clone[map[page.PageMatcher]maps.Params](ps.m.cascadeOriginal)
}
// Apply cascades first so they can be overridden later.
if cascade != nil {
if ps.m.pageConfig.Cascade != nil {
if ps.m.pageConfig.CascadeCompiled != nil {
for k, v := range cascade {
vv, found := ps.m.pageConfig.Cascade[k]
vv, found := ps.m.pageConfig.CascadeCompiled[k]
if !found {
ps.m.pageConfig.Cascade[k] = v
ps.m.pageConfig.CascadeCompiled[k] = v
} else {
// Merge
for ck, cv := range v {
@@ -349,19 +349,20 @@ func (ps *pageState) setMetaPost(cascade map[page.PageMatcher]maps.Params) error
}
}
}
cascade = ps.m.pageConfig.Cascade
cascade = ps.m.pageConfig.CascadeCompiled
} else {
ps.m.pageConfig.Cascade = cascade
ps.m.pageConfig.CascadeCompiled = cascade
}
}
if cascade == nil {
cascade = ps.m.pageConfig.Cascade
cascade = ps.m.pageConfig.CascadeCompiled
}
if ps.m.setMetaPostCount > 1 {
ps.m.setMetaPostCascadeChanged = cascadeHashPre != identity.HashUint64(ps.m.pageConfig.Cascade)
ps.m.setMetaPostCascadeChanged = cascadeHashPre != identity.HashUint64(ps.m.pageConfig.CascadeCompiled)
if !ps.m.setMetaPostCascadeChanged {
// No changes, restore any value that may be changed by aggregation.
ps.m.pageConfig.Dates = ps.m.datesOriginal
return nil
@@ -400,11 +401,17 @@ func (p *pageState) setMetaPostParams() error {
pm := p.m
var mtime time.Time
var contentBaseName string
var ext string
var isContentAdapter bool
if p.File() != nil {
isContentAdapter = p.File().IsContentAdapter()
contentBaseName = p.File().ContentBaseName()
if p.File().FileInfo() != nil {
mtime = p.File().FileInfo().ModTime()
}
if !isContentAdapter {
ext = p.File().Ext()
}
}
var gitAuthorDate time.Time
@@ -428,6 +435,11 @@ func (p *pageState) setMetaPostParams() error {
p.s.Log.Errorf("Failed to handle dates for page %q: %s", p.pathOrTitle(), err)
}
if isContentAdapter {
// Done.
return nil
}
var buildConfig any
var isNewBuildKeyword bool
if v, ok := pm.pageConfig.Params["_build"]; ok {
@@ -456,8 +468,10 @@ params:
var sitemapSet bool
pcfg := pm.pageConfig
params := pcfg.Params
if params == nil {
panic("params not set for " + p.Title())
}
var draft, published, isCJKLanguage *bool
var userParams map[string]any
@@ -550,8 +564,8 @@ params:
pcfg.Layout = cast.ToString(v)
params[loki] = pcfg.Layout
case "markup":
pcfg.Markup = cast.ToString(v)
params[loki] = pcfg.Markup
pcfg.Content.Markup = cast.ToString(v)
params[loki] = pcfg.Content.Markup
case "weight":
pcfg.Weight = cast.ToInt(v)
params[loki] = pcfg.Weight
@@ -601,7 +615,7 @@ params:
}
if handled {
pcfg.Resources = resources
pcfg.ResourcesMeta = resources
break
}
fallthrough
@@ -648,8 +662,6 @@ params:
pcfg.Sitemap = p.s.conf.Sitemap
}
pcfg.Markup = p.s.ContentSpec.ResolveMarkup(pcfg.Markup)
if draft != nil && published != nil {
pcfg.Draft = *draft
p.m.s.Log.Warnf("page %q has both draft and published settings in its frontmatter. Using draft.", p.File().Filename())
@@ -672,6 +684,14 @@ params:
params["iscjklanguage"] = pcfg.IsCJKLanguage
if err := pcfg.Validate(false); err != nil {
return err
}
if err := pcfg.Compile("", false, ext, p.s.Log, p.s.conf.MediaTypes.Config); err != nil {
return err
}
return nil
}
@@ -727,13 +747,13 @@ func (p *pageMeta) applyDefaultValues() error {
(&p.pageConfig.Build).Disable()
}
if p.pageConfig.Markup == "" {
if p.pageConfig.Content.Markup == "" {
if p.File() != nil {
// Fall back to file extension
p.pageConfig.Markup = p.s.ContentSpec.ResolveMarkup(p.File().Ext())
p.pageConfig.Content.Markup = p.s.ContentSpec.ResolveMarkup(p.File().Ext())
}
if p.pageConfig.Markup == "" {
p.pageConfig.Markup = "markdown"
if p.pageConfig.Content.Markup == "" {
p.pageConfig.Content.Markup = "markdown"
}
}
@@ -794,12 +814,26 @@ func (p *pageMeta) newContentConverter(ps *pageState, markup string) (converter.
path = p.Path()
}
doc := newPageForRenderHook(ps)
documentLookup := func(id uint64) any {
if id == ps.pid {
// This prevents infinite recursion in some cases.
return doc
}
if v, ok := ps.pageOutput.pco.otherOutputs[id]; ok {
return v.po.p
}
return nil
}
cpp, err := cp.New(
converter.DocumentContext{
Document: newPageForRenderHook(ps),
DocumentID: id,
DocumentName: path,
Filename: filename,
Document: doc,
DocumentLookup: documentLookup,
DocumentID: id,
DocumentName: path,
Filename: filename,
},
)
if err != nil {
+34 -36
View File
@@ -15,7 +15,6 @@ package hugolib
import (
"fmt"
"path/filepath"
"sync"
"sync/atomic"
@@ -36,21 +35,17 @@ var pageIDCounter atomic.Uint64
func (h *HugoSites) newPage(m *pageMeta) (*pageState, *paths.Path, error) {
m.Staler = &resources.AtomicStaler{}
if m.pageConfig == nil {
m.pageMetaParams = pageMetaParams{
pageConfig: &pagemeta.PageConfig{
Params: maps.Params{},
},
if m.pageMetaParams == nil {
m.pageMetaParams = &pageMetaParams{
pageConfig: &pagemeta.PageConfig{},
}
}
var sourceKey string
if m.f != nil {
sourceKey = filepath.ToSlash(m.f.Filename())
if m.pageConfig.Params == nil {
m.pageConfig.Params = maps.Params{}
}
pid := pageIDCounter.Add(1)
pi, err := m.parseFrontMatter(h, pid, sourceKey)
pi, err := m.parseFrontMatter(h, pid)
if err != nil {
return nil, nil, err
}
@@ -69,28 +64,40 @@ func (h *HugoSites) newPage(m *pageMeta) (*pageState, *paths.Path, error) {
s := m.pageConfig.Path
if !paths.HasExt(s) {
var (
isBranch bool
ext string = "md"
isBranch bool
isBranchSet bool
ext string = m.pageConfig.ContentMediaType.FirstSuffix.Suffix
)
if pcfg.Kind != "" {
isBranch = kinds.IsBranch(pcfg.Kind)
} else if m.pathInfo != nil {
isBranch = m.pathInfo.IsBranchBundle()
if m.pathInfo.Ext() != "" {
ext = m.pathInfo.Ext()
}
} else if m.f != nil {
pi := m.f.FileInfo().Meta().PathInfo
isBranch = pi.IsBranchBundle()
if pi.Ext() != "" {
ext = pi.Ext()
isBranchSet = true
}
if !pcfg.IsFromContentAdapter {
if m.pathInfo != nil {
if !isBranchSet {
isBranch = m.pathInfo.IsBranchBundle()
}
if m.pathInfo.Ext() != "" {
ext = m.pathInfo.Ext()
}
} else if m.f != nil {
pi := m.f.FileInfo().Meta().PathInfo
if !isBranchSet {
isBranch = pi.IsBranchBundle()
}
if pi.Ext() != "" {
ext = pi.Ext()
}
}
}
if isBranch {
s += "/_index." + ext
} else {
s += "/index." + ext
}
}
m.pathInfo = h.Conf.PathParser().Parse(files.ComponentFolderContent, s)
} else if m.pathInfo == nil {
@@ -112,23 +119,13 @@ func (h *HugoSites) newPage(m *pageMeta) (*pageState, *paths.Path, error) {
} else if m.f != nil {
meta := m.f.FileInfo().Meta()
lang = meta.Lang
m.s = h.Sites[meta.LangIndex]
} else {
lang = m.pathInfo.Lang()
}
if lang == "" {
lang = h.Conf.DefaultContentLanguage()
}
var found bool
for _, ss := range h.Sites {
if ss.Lang() == lang {
m.s = ss
found = true
break
}
}
if !found {
m.s = h.resolveSite(lang)
if m.s == nil {
return nil, fmt.Errorf("no site found for language %q", lang)
}
}
@@ -184,6 +181,7 @@ func (h *HugoSites) newPage(m *pageMeta) (*pageState, *paths.Path, error) {
ResourceNameTitleProvider: m,
ResourceParamsProvider: m,
PageMetaProvider: m,
PageMetaInternalProvider: m,
RelatedKeywordsProvider: m,
OutputFormatsProvider: page.NopPage,
ResourceTypeProvider: pageTypesProvider,
+30 -9
View File
@@ -25,11 +25,14 @@ import (
"github.com/gohugoio/hugo/common/text"
"github.com/gohugoio/hugo/common/types/hstring"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/markup"
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/parser/pageparser"
"github.com/mitchellh/mapstructure"
"github.com/spf13/cast"
"github.com/gohugoio/hugo/markup/converter/hooks"
"github.com/gohugoio/hugo/markup/goldmark/hugocontext"
"github.com/gohugoio/hugo/markup/highlight/chromalexers"
"github.com/gohugoio/hugo/markup/tableofcontents"
@@ -68,8 +71,9 @@ var (
func newPageContentOutput(po *pageOutput) (*pageContentOutput, error) {
cp := &pageContentOutput{
po: po,
renderHooks: &renderHooks{},
po: po,
renderHooks: &renderHooks{},
otherOutputs: make(map[uint64]*pageContentOutput),
}
return cp, nil
}
@@ -83,8 +87,12 @@ type renderHooks struct {
type pageContentOutput struct {
po *pageOutput
contentRenderedVersion int // Incremented on reset.
contentRendered bool // Set on content render.
// Other pages involved in rendering of this page,
// typically included with .RenderShortcodes.
otherOutputs map[uint64]*pageContentOutput
contentRenderedVersion uint32 // Incremented on reset.
contentRendered bool // Set on content render.
// Renders Markdown hooks.
renderHooks *renderHooks
@@ -165,6 +173,13 @@ func (pco *pageContentOutput) RenderShortcodes(ctx context.Context) (template.HT
cb(pco, ct)
}
if tpl.Context.IsInGoldmark.Get(ctx) {
// This content will be parsed and rendered by Goldmark.
// Wrap it in a special Hugo markup to assign the correct Page from
// the stack.
return template.HTML(hugocontext.Wrap(c, pco.po.p.pid)), nil
}
return helpers.BytesToHTML(c), nil
}
@@ -249,6 +264,9 @@ func (pco *pageContentOutput) RenderString(ctx context.Context, args ...any) (te
if err := mapstructure.WeakDecode(m, &opts); err != nil {
return "", fmt.Errorf("failed to decode options: %w", err)
}
if opts.Markup != "" {
opts.Markup = markup.ResolveMarkup(opts.Markup)
}
}
contentToRenderv := args[sidx]
@@ -270,7 +288,8 @@ func (pco *pageContentOutput) RenderString(ctx context.Context, args ...any) (te
}
conv := pco.po.p.getContentConverter()
if opts.Markup != "" && opts.Markup != pco.po.p.m.pageConfig.Markup {
if opts.Markup != "" && opts.Markup != pco.po.p.m.pageConfig.ContentMediaType.SubType {
var err error
conv, err = pco.po.p.m.newContentConverter(pco.po.p, opts.Markup)
if err != nil {
@@ -363,9 +382,11 @@ func (pco *pageContentOutput) RenderString(ctx context.Context, args ...any) (te
}
if opts.Display == "inline" {
// We may have to rethink this in the future when we get other
// renderers.
rendered = pco.po.p.s.ContentSpec.TrimShortHTML(rendered)
markup := pco.po.p.m.pageConfig.Content.Markup
if opts.Markup != "" {
markup = pco.po.p.s.ContentSpec.ResolveMarkup(opts.Markup)
}
rendered = pco.po.p.s.ContentSpec.TrimShortHTML(rendered, markup)
}
return template.HTML(string(rendered)), nil
@@ -642,7 +663,7 @@ func splitUserDefinedSummaryAndContent(markup string, c []byte) (summary []byte,
startTag := "p"
switch markup {
case "asciidocext":
case media.DefaultContentTypes.AsciiDoc.SubType:
startTag = "div"
}
+41 -12
View File
@@ -42,18 +42,20 @@ func newPagesCollector(
logger loggers.Logger,
infoLogger logg.LevelLogger,
m *pageMap,
buildConfig *BuildCfg,
ids []pathChange,
) *pagesCollector {
return &pagesCollector{
ctx: ctx,
h: h,
fs: sp.BaseFs.Content.Fs,
m: m,
sp: sp,
logger: logger,
infoLogger: infoLogger,
ids: ids,
seenDirs: make(map[string]bool),
ctx: ctx,
h: h,
fs: sp.BaseFs.Content.Fs,
m: m,
sp: sp,
logger: logger,
infoLogger: infoLogger,
buildConfig: buildConfig,
ids: ids,
seenDirs: make(map[string]bool),
}
}
@@ -68,6 +70,8 @@ type pagesCollector struct {
fs afero.Fs
buildConfig *BuildCfg
// List of paths that have changed. Used in partial builds.
ids []pathChange
seenDirs map[string]bool
@@ -82,6 +86,8 @@ func (c *pagesCollector) Collect() (collectErr error) {
var (
numWorkers = c.h.numWorkers
numFilesProcessedTotal atomic.Uint64
numPagesProcessedTotal atomic.Uint64
numResourcesProcessed atomic.Uint64
numFilesProcessedLast uint64
fileBatchTimer = time.Now()
fileBatchTimerMu sync.Mutex
@@ -98,6 +104,8 @@ func (c *pagesCollector) Collect() (collectErr error) {
logg.Fields{
logg.Field{Name: "files", Value: numFilesProcessedBatch},
logg.Field{Name: "files_total", Value: numFilesProcessedTotal.Load()},
logg.Field{Name: "pages_total", Value: numPagesProcessedTotal.Load()},
logg.Field{Name: "resources_total", Value: numResourcesProcessed.Load()},
},
"",
)
@@ -113,10 +121,13 @@ func (c *pagesCollector) Collect() (collectErr error) {
c.g = rungroup.Run[hugofs.FileMetaInfo](c.ctx, rungroup.Config[hugofs.FileMetaInfo]{
NumWorkers: numWorkers,
Handle: func(ctx context.Context, fi hugofs.FileMetaInfo) error {
if err := c.m.AddFi(fi); err != nil {
numPages, numResources, err := c.m.AddFi(fi, c.buildConfig)
if err != nil {
return hugofs.AddFileInfoToError(err, fi, c.fs)
}
numFilesProcessedTotal.Add(1)
numPagesProcessedTotal.Add(numPages)
numResourcesProcessed.Add(numResources)
if numFilesProcessedTotal.Load()%1000 == 0 {
logFilesProcessed(false)
}
@@ -147,7 +158,7 @@ func (c *pagesCollector) Collect() (collectErr error) {
false,
func(fim hugofs.FileMetaInfo) bool {
if fim.IsDir() {
return true
return id.isStructuralChange()
}
fimp := fim.Meta().PathInfo
if fimp == nil {
@@ -160,7 +171,7 @@ func (c *pagesCollector) Collect() (collectErr error) {
} else {
// We always start from a directory.
collectErr = c.collectDir(id.p, id.isDir, func(fim hugofs.FileMetaInfo) bool {
if id.delete || id.isDir {
if id.isStructuralChange() {
if id.isDir && fim.Meta().PathInfo.IsLeafBundle() {
return strings.HasPrefix(fim.Meta().PathInfo.Path(), paths.AddTrailingSlash(id.p.Path()))
}
@@ -243,6 +254,21 @@ func (c *pagesCollector) collectDirDir(path string, root hugofs.FileMetaInfo, in
return nil, nil
}
n := 0
for _, fi := range readdir {
if fi.Meta().PathInfo.IsContentData() {
// _content.json
// These are not part of any bundle, so just add them directly and remove them from the readdir slice.
if err := c.g.Enqueue(fi); err != nil {
return nil, err
}
} else {
readdir[n] = fi
n++
}
}
readdir = readdir[:n]
// Pick the first regular file.
var first hugofs.FileMetaInfo
for _, fi := range readdir {
@@ -260,6 +286,7 @@ func (c *pagesCollector) collectDirDir(path string, root hugofs.FileMetaInfo, in
// Any bundle file will always be first.
firstPi := first.Meta().PathInfo
if firstPi == nil {
panic(fmt.Sprintf("collectDirDir: no path info for %q", first.Meta().Filename))
}
@@ -320,6 +347,7 @@ func (c *pagesCollector) collectDirDir(path string, root hugofs.FileMetaInfo, in
Info: root,
Fs: c.fs,
IgnoreFile: c.h.SourceSpec.IgnoreFile,
PathParser: c.h.Conf.PathParser(),
HookPre: preHook,
HookPost: postHook,
WalkFn: wfn,
@@ -370,6 +398,7 @@ func (c *pagesCollector) handleBundleLeaf(dir, bundle hugofs.FileMetaInfo, inPat
Info: dir,
DirEntries: readdir,
IgnoreFile: c.h.SourceSpec.IgnoreFile,
PathParser: c.h.Conf.PathParser(),
WalkFn: walk,
})
+331
View File
@@ -0,0 +1,331 @@
// Copyright 2024 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 pagesfromdata
import (
"context"
"fmt"
"io"
"path/filepath"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/page/pagemeta"
"github.com/gohugoio/hugo/resources/resource"
"github.com/gohugoio/hugo/tpl"
"github.com/mitchellh/mapstructure"
"github.com/spf13/cast"
)
type PagesFromDataTemplateContext interface {
// AddPage adds a new page to the site.
// The first return value will always be an empty string.
AddPage(any) (string, error)
// AddResource adds a new resource to the site.
// The first return value will always be an empty string.
AddResource(any) (string, error)
// The site to which the pages will be added.
Site() page.Site
// The same template may be executed multiple times for multiple languages.
// The Store can be used to store state between these invocations.
Store() *maps.Scratch
// By default, the template will be executed for the language
// defined by the _content.gotmpl file (e.g. its mount definition).
// This method can be used to activate the template for all languages.
// The return value will always be an empty string.
EnableAllLanguages() string
}
var _ PagesFromDataTemplateContext = (*pagesFromDataTemplateContext)(nil)
type pagesFromDataTemplateContext struct {
p *PagesFromTemplate
}
func (p *pagesFromDataTemplateContext) toPathMap(v any) (string, map[string]any, error) {
m, err := maps.ToStringMapE(v)
if err != nil {
return "", nil, err
}
pathv, ok := m["path"]
if !ok {
return "", nil, fmt.Errorf("path not set")
}
path, err := cast.ToStringE(pathv)
if err != nil || path == "" {
return "", nil, fmt.Errorf("invalid path %q", path)
}
return path, m, nil
}
func (p *pagesFromDataTemplateContext) AddPage(v any) (string, error) {
path, m, err := p.toPathMap(v)
if err != nil {
return "", err
}
if !p.p.buildState.checkHasChangedAndSetSourceInfo(path, m) {
return "", nil
}
pd := pagemeta.DefaultPageConfig
pd.IsFromContentAdapter = true
if err := mapstructure.WeakDecode(m, &pd); err != nil {
return "", fmt.Errorf("failed to decode page map: %w", err)
}
p.p.buildState.NumPagesAdded++
if err := pd.Validate(true); err != nil {
return "", err
}
return "", p.p.HandlePage(p.p, &pd)
}
func (p *pagesFromDataTemplateContext) AddResource(v any) (string, error) {
path, m, err := p.toPathMap(v)
if err != nil {
return "", err
}
if !p.p.buildState.checkHasChangedAndSetSourceInfo(path, m) {
return "", nil
}
var rd pagemeta.ResourceConfig
if err := mapstructure.WeakDecode(m, &rd); err != nil {
return "", err
}
p.p.buildState.NumResourcesAdded++
if err := rd.Validate(); err != nil {
return "", err
}
return "", p.p.HandleResource(p.p, &rd)
}
func (p *pagesFromDataTemplateContext) Site() page.Site {
return p.p.Site
}
func (p *pagesFromDataTemplateContext) Store() *maps.Scratch {
return p.p.store
}
func (p *pagesFromDataTemplateContext) EnableAllLanguages() string {
p.p.buildState.EnableAllLanguages = true
return ""
}
func NewPagesFromTemplate(opts PagesFromTemplateOptions) *PagesFromTemplate {
return &PagesFromTemplate{
PagesFromTemplateOptions: opts,
PagesFromTemplateDeps: opts.DepsFromSite(opts.Site),
buildState: &BuildState{
sourceInfosCurrent: maps.NewCache[string, *sourceInfo](),
},
store: maps.NewScratch(),
}
}
type PagesFromTemplateOptions struct {
Site page.Site
DepsFromSite func(page.Site) PagesFromTemplateDeps
DependencyManager identity.Manager
Watching bool
HandlePage func(pt *PagesFromTemplate, p *pagemeta.PageConfig) error
HandleResource func(pt *PagesFromTemplate, p *pagemeta.ResourceConfig) error
GoTmplFi hugofs.FileMetaInfo
}
type PagesFromTemplateDeps struct {
TmplFinder tpl.TemplateParseFinder
TmplExec tpl.TemplateExecutor
}
var _ resource.Staler = (*PagesFromTemplate)(nil)
type PagesFromTemplate struct {
PagesFromTemplateOptions
PagesFromTemplateDeps
buildState *BuildState
store *maps.Scratch
}
func (b *PagesFromTemplate) AddChange(id identity.Identity) {
b.buildState.ChangedIdentities = append(b.buildState.ChangedIdentities, id)
}
func (b *PagesFromTemplate) MarkStale() {
b.buildState.StaleVersion++
}
func (b *PagesFromTemplate) StaleVersion() uint32 {
return b.buildState.StaleVersion
}
type BuildInfo struct {
NumPagesAdded uint64
NumResourcesAdded uint64
EnableAllLanguages bool
ChangedIdentities []identity.Identity
DeletedPaths []string
Path *paths.Path
}
type BuildState struct {
StaleVersion uint32
EnableAllLanguages bool
// Paths deleted in the current build.
DeletedPaths []string
// Changed identities in the current build.
ChangedIdentities []identity.Identity
NumPagesAdded uint64
NumResourcesAdded uint64
sourceInfosCurrent *maps.Cache[string, *sourceInfo]
sourceInfosPrevious *maps.Cache[string, *sourceInfo]
}
func (b *BuildState) hash(v any) uint64 {
return identity.HashUint64(v)
}
func (b *BuildState) checkHasChangedAndSetSourceInfo(changedPath string, v any) bool {
h := b.hash(v)
si, found := b.sourceInfosPrevious.Get(changedPath)
if found {
b.sourceInfosCurrent.Set(changedPath, si)
if si.hash == h {
return false
}
} else {
si = &sourceInfo{}
b.sourceInfosCurrent.Set(changedPath, si)
}
si.hash = h
return true
}
func (b *BuildState) resolveDeletedPaths() {
if b.sourceInfosPrevious == nil {
b.DeletedPaths = nil
return
}
var paths []string
b.sourceInfosPrevious.ForEeach(func(k string, _ *sourceInfo) {
if _, found := b.sourceInfosCurrent.Get(k); !found {
paths = append(paths, k)
}
})
b.DeletedPaths = paths
}
func (b *BuildState) PrepareNextBuild() {
b.sourceInfosPrevious = b.sourceInfosCurrent
b.sourceInfosCurrent = maps.NewCache[string, *sourceInfo]()
b.StaleVersion = 0
b.DeletedPaths = nil
b.ChangedIdentities = nil
b.NumPagesAdded = 0
b.NumResourcesAdded = 0
}
type sourceInfo struct {
hash uint64
}
func (p PagesFromTemplate) CloneForSite(s page.Site) *PagesFromTemplate {
// We deliberately make them share the same DepenencyManager and Store.
p.PagesFromTemplateOptions.Site = s
p.PagesFromTemplateDeps = p.PagesFromTemplateOptions.DepsFromSite(s)
p.buildState = &BuildState{
sourceInfosCurrent: maps.NewCache[string, *sourceInfo](),
}
return &p
}
func (p PagesFromTemplate) CloneForGoTmpl(fi hugofs.FileMetaInfo) *PagesFromTemplate {
p.PagesFromTemplateOptions.GoTmplFi = fi
return &p
}
func (p *PagesFromTemplate) GetDependencyManagerForScope(scope int) identity.Manager {
return p.DependencyManager
}
func (p *PagesFromTemplate) Execute(ctx context.Context) (BuildInfo, error) {
defer func() {
p.buildState.PrepareNextBuild()
}()
f, err := p.GoTmplFi.Meta().Open()
if err != nil {
return BuildInfo{}, err
}
defer f.Close()
tmpl, err := p.TmplFinder.Parse(filepath.ToSlash(p.GoTmplFi.Meta().Filename), helpers.ReaderToString(f))
if err != nil {
return BuildInfo{}, err
}
data := &pagesFromDataTemplateContext{
p: p,
}
ctx = tpl.Context.DependencyManagerScopedProvider.Set(ctx, p)
if err := p.TmplExec.ExecuteWithContext(ctx, tmpl, io.Discard, data); err != nil {
return BuildInfo{}, err
}
if p.Watching {
p.buildState.resolveDeletedPaths()
}
bi := BuildInfo{
NumPagesAdded: p.buildState.NumPagesAdded,
NumResourcesAdded: p.buildState.NumResourcesAdded,
EnableAllLanguages: p.buildState.EnableAllLanguages,
ChangedIdentities: p.buildState.ChangedIdentities,
DeletedPaths: p.buildState.DeletedPaths,
Path: p.GoTmplFi.Meta().PathInfo,
}
return bi, nil
}
//////////////
@@ -0,0 +1,479 @@
// Copyright 2024 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 pagesfromdata_test
import (
"fmt"
"strings"
"testing"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/markup/asciidocext"
"github.com/gohugoio/hugo/markup/pandoc"
"github.com/gohugoio/hugo/markup/rst"
)
const filesPagesFromDataTempleBasic = `
-- hugo.toml --
disableKinds = ["taxonomy", "term", "rss", "sitemap"]
baseURL = "https://example.com"
disableLiveReload = true
-- assets/a/pixel.png --
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
-- assets/mydata.yaml --
p1: "p1"
draft: false
-- layouts/partials/get-value.html --
{{ $val := "p1" }}
{{ return $val }}
-- layouts/_default/single.html --
Single: {{ .Title }}|{{ .Content }}|Params: {{ .Params.param1 }}|Path: {{ .Path }}|
Dates: Date: {{ .Date.Format "2006-01-02" }}|Lastmod: {{ .Lastmod.Format "2006-01-02" }}|PublishDate: {{ .PublishDate.Format "2006-01-02" }}|ExpiryDate: {{ .ExpiryDate.Format "2006-01-02" }}|
Len Resources: {{ .Resources | len }}
Resources: {{ range .Resources }}RelPermalink: {{ .RelPermalink }}|Name: {{ .Name }}|Title: {{ .Title }}|Params: {{ .Params }}|{{ end }}$
{{ with .Resources.Get "featured.png" }}
Featured Image: {{ .RelPermalink }}|{{ .Name }}|
{{ with .Resize "10x10" }}
Resized Featured Image: {{ .RelPermalink }}|{{ .Width }}|
{{ end}}
{{ end }}
-- layouts/_default/list.html --
List: {{ .Title }}|{{ .Content }}|
RegularPagesRecursive: {{ range .RegularPagesRecursive }}{{ .Title }}:{{ .Path }}|{{ end }}$
Sections: {{ range .Sections }}{{ .Title }}:{{ .Path }}|{{ end }}$
-- content/docs/pfile.md --
---
title: "pfile"
date: 2023-03-01
---
Pfile Content
-- content/docs/_content.gotmpl --
{{ $pixel := resources.Get "a/pixel.png" }}
{{ $dataResource := resources.Get "mydata.yaml" }}
{{ $data := $dataResource | transform.Unmarshal }}
{{ $pd := $data.p1 }}
{{ $pp := partial "get-value.html" }}
{{ $title := printf "%s:%s" $pd $pp }}
{{ $date := "2023-03-01" | time.AsTime }}
{{ $dates := dict "date" $date }}
{{ $contentMarkdown := dict "value" "**Hello World**" "mediaType" "text/markdown" }}
{{ $contentMarkdownDefault := dict "value" "**Hello World Default**" }}
{{ $contentHTML := dict "value" "<b>Hello World!</b> No **markdown** here." "mediaType" "text/html" }}
{{ $.AddPage (dict "kind" "page" "path" "P1" "title" $title "dates" $dates "content" $contentMarkdown "params" (dict "param1" "param1v" ) ) }}
{{ $.AddPage (dict "kind" "page" "path" "p2" "title" "p2title" "dates" $dates "content" $contentHTML ) }}
{{ $.AddPage (dict "kind" "page" "path" "p3" "title" "p3title" "dates" $dates "content" $contentMarkdownDefault "draft" false ) }}
{{ $.AddPage (dict "kind" "page" "path" "p4" "title" "p4title" "dates" $dates "content" $contentMarkdownDefault "draft" $data.draft ) }}
{{ $resourceContent := dict "value" $dataResource }}
{{ $.AddResource (dict "path" "p1/data1.yaml" "content" $resourceContent) }}
{{ $.AddResource (dict "path" "p1/mytext.txt" "content" (dict "value" "some text") "name" "textresource" "title" "My Text Resource" "params" (dict "param1" "param1v") )}}
{{ $.AddResource (dict "path" "p1/sub/mytex2.txt" "content" (dict "value" "some text") "title" "My Text Sub Resource" ) }}
{{ $.AddResource (dict "path" "P1/Sub/MyMixCaseText2.txt" "content" (dict "value" "some text") "title" "My Text Sub Mixed Case Path Resource" ) }}
{{ $.AddResource (dict "path" "p1/sub/data1.yaml" "content" $resourceContent "title" "Sub data") }}
{{ $resourceParams := dict "data2ParaM1" "data2Param1v" }}
{{ $.AddResource (dict "path" "p1/data2.yaml" "name" "data2.yaml" "title" "My data 2" "params" $resourceParams "content" $resourceContent) }}
{{ $.AddResource (dict "path" "p1/featuredimage.png" "name" "featured.png" "title" "My Featured Image" "params" $resourceParams "content" (dict "value" $pixel ))}}
`
func TestPagesFromGoTmplMisc(t *testing.T) {
t.Parallel()
b := hugolib.Test(t, filesPagesFromDataTempleBasic)
b.AssertPublishDir(`
docs/p1/mytext.txt
docs/p1/sub/mytex2.tx
docs/p1/sub/mymixcasetext2.txt
`)
// Page from markdown file.
b.AssertFileContent("public/docs/pfile/index.html", "Dates: Date: 2023-03-01|Lastmod: 2023-03-01|PublishDate: 2023-03-01|ExpiryDate: 0001-01-01|")
// Pages from gotmpl.
b.AssertFileContent("public/docs/p1/index.html",
"Single: p1:p1|",
"Path: /docs/p1|",
"<strong>Hello World</strong>",
"Params: param1v|",
"Len Resources: 7",
"RelPermalink: /mydata.yaml|Name: data1.yaml|Title: data1.yaml|Params: map[]|",
"RelPermalink: /mydata.yaml|Name: data2.yaml|Title: My data 2|Params: map[data2param1:data2Param1v]|",
"RelPermalink: /a/pixel.png|Name: featured.png|Title: My Featured Image|Params: map[data2param1:data2Param1v]|",
"RelPermalink: /docs/p1/sub/mytex2.txt|Name: sub/mytex2.txt|",
"RelPermalink: /docs/p1/sub/mymixcasetext2.txt|Name: sub/mymixcasetext2.txt|",
"RelPermalink: /mydata.yaml|Name: sub/data1.yaml|Title: Sub data|Params: map[]|",
"Featured Image: /a/pixel.png|featured.png|",
"Resized Featured Image: /a/pixel_hu8aa3346827e49d756ff4e630147c42b5_70_10x10_resize_box_3.png|10|",
// Resource from string
"RelPermalink: /docs/p1/mytext.txt|Name: textresource|Title: My Text Resource|Params: map[param1:param1v]|",
// Dates
"Dates: Date: 2023-03-01|Lastmod: 2023-03-01|PublishDate: 2023-03-01|ExpiryDate: 0001-01-01|",
)
b.AssertFileContent("public/docs/p2/index.html", "Single: p2title|", "<b>Hello World!</b> No **markdown** here.")
b.AssertFileContent("public/docs/p3/index.html", "<strong>Hello World Default</strong>")
}
func TestPagesFromGoTmplAsciidocAndSimilar(t *testing.T) {
files := `
-- hugo.toml --
disableKinds = ["taxonomy", "term", "rss", "sitemap"]
baseURL = "https://example.com"
[security]
[security.exec]
allow = ['asciidoctor', 'pandoc','rst2html', 'python']
-- layouts/_default/single.html --
|Content: {{ .Content }}|Title: {{ .Title }}|Path: {{ .Path }}|
-- content/docs/_content.gotmpl --
{{ $.AddPage (dict "path" "asciidoc" "content" (dict "value" "Mark my words, #automation is essential#." "mediaType" "text/asciidoc" )) }}
{{ $.AddPage (dict "path" "pandoc" "content" (dict "value" "This ~~is deleted text.~~" "mediaType" "text/pandoc" )) }}
{{ $.AddPage (dict "path" "rst" "content" (dict "value" "This is *bold*." "mediaType" "text/rst" )) }}
{{ $.AddPage (dict "path" "org" "content" (dict "value" "the ability to use +strikethrough+ is a plus" "mediaType" "text/org" )) }}
{{ $.AddPage (dict "path" "nocontent" "title" "No Content" ) }}
`
b := hugolib.Test(t, files)
if asciidocext.Supports() {
b.AssertFileContent("public/docs/asciidoc/index.html",
"Mark my words, <mark>automation is essential</mark>",
"Path: /docs/asciidoc|",
)
}
if pandoc.Supports() {
b.AssertFileContent("public/docs/pandoc/index.html",
"This <del>is deleted text.</del>",
"Path: /docs/pandoc|",
)
}
if rst.Supports() {
b.AssertFileContent("public/docs/rst/index.html",
"This is <em>bold</em>",
"Path: /docs/rst|",
)
}
b.AssertFileContent("public/docs/org/index.html",
"the ability to use <del>strikethrough</del> is a plus",
"Path: /docs/org|",
)
b.AssertFileContent("public/docs/nocontent/index.html", "|Content: |Title: No Content|Path: /docs/nocontent|")
}
func TestPagesFromGoTmplAddPageErrors(t *testing.T) {
filesTemplate := `
-- hugo.toml --
disableKinds = ["taxonomy", "term", "rss", "sitemap"]
baseURL = "https://example.com"
-- content/docs/_content.gotmpl --
{{ $.AddPage DICT }}
`
t.Run("AddPage, missing Path", func(t *testing.T) {
files := strings.ReplaceAll(filesTemplate, "DICT", `(dict "kind" "page" "title" "p1")`)
b, err := hugolib.TestE(t, files)
b.Assert(err, qt.IsNotNil)
b.Assert(err.Error(), qt.Contains, "_content.gotmpl:1:4")
b.Assert(err.Error(), qt.Contains, "error calling AddPage: path not set")
})
t.Run("AddPage, path starting with slash", func(t *testing.T) {
files := strings.ReplaceAll(filesTemplate, "DICT", `(dict "kind" "page" "title" "p1" "path" "/foo")`)
b, err := hugolib.TestE(t, files)
b.Assert(err, qt.IsNotNil)
b.Assert(err.Error(), qt.Contains, `path "/foo" must not start with a /`)
})
t.Run("AddPage, lang set", func(t *testing.T) {
files := strings.ReplaceAll(filesTemplate, "DICT", `(dict "kind" "page" "path" "p1" "lang" "en")`)
b, err := hugolib.TestE(t, files)
b.Assert(err, qt.IsNotNil)
b.Assert(err.Error(), qt.Contains, "_content.gotmpl:1:4")
b.Assert(err.Error(), qt.Contains, "error calling AddPage: lang must not be set")
})
t.Run("Site methods not ready", func(t *testing.T) {
filesTemplate := `
-- hugo.toml --
disableKinds = ["taxonomy", "term", "rss", "sitemap"]
baseURL = "https://example.com"
-- content/docs/_content.gotmpl --
{{ .Site.METHOD }}
`
for _, method := range []string{"RegularPages", "Pages", "AllPages", "AllRegularPages", "Home", "Sections", "GetPage", "Menus", "MainSections", "Taxonomies"} {
t.Run(method, func(t *testing.T) {
files := strings.ReplaceAll(filesTemplate, "METHOD", method)
b, err := hugolib.TestE(t, files)
b.Assert(err, qt.IsNotNil)
b.Assert(err.Error(), qt.Contains, fmt.Sprintf("error calling %s: this method cannot be called before the site is fully initialized", method))
})
}
})
}
func TestPagesFromGoTmplAddResourceErrors(t *testing.T) {
filesTemplate := `
-- hugo.toml --
disableKinds = ["taxonomy", "term", "rss", "sitemap"]
baseURL = "https://example.com"
-- content/docs/_content.gotmpl --
{{ $.AddResource DICT }}
`
t.Run("missing Path", func(t *testing.T) {
files := strings.ReplaceAll(filesTemplate, "DICT", `(dict "name" "r1")`)
b, err := hugolib.TestE(t, files)
b.Assert(err, qt.IsNotNil)
b.Assert(err.Error(), qt.Contains, "error calling AddResource: path not set")
})
}
func TestPagesFromGoTmplEditGoTmpl(t *testing.T) {
t.Parallel()
b := hugolib.TestRunning(t, filesPagesFromDataTempleBasic)
b.EditFileReplaceAll("content/docs/_content.gotmpl", `"title" "p2title"`, `"title" "p2titleedited"`).Build()
b.AssertFileContent("public/docs/p2/index.html", "Single: p2titleedited|")
b.AssertFileContent("public/docs/index.html", "p2titleedited")
}
func TestPagesFromGoTmplEditDataResource(t *testing.T) {
t.Parallel()
b := hugolib.TestRunning(t, filesPagesFromDataTempleBasic)
b.AssertRenderCountPage(7)
b.EditFileReplaceAll("assets/mydata.yaml", "p1: \"p1\"", "p1: \"p1edited\"").Build()
b.AssertFileContent("public/docs/p1/index.html", "Single: p1edited:p1|")
b.AssertFileContent("public/docs/index.html", "p1edited")
b.AssertRenderCountPage(3)
}
func TestPagesFromGoTmplEditPartial(t *testing.T) {
t.Parallel()
b := hugolib.TestRunning(t, filesPagesFromDataTempleBasic)
b.EditFileReplaceAll("layouts/partials/get-value.html", "p1", "p1edited").Build()
b.AssertFileContent("public/docs/p1/index.html", "Single: p1:p1edited|")
b.AssertFileContent("public/docs/index.html", "p1edited")
}
func TestPagesFromGoTmplRemovePage(t *testing.T) {
t.Parallel()
b := hugolib.TestRunning(t, filesPagesFromDataTempleBasic)
b.EditFileReplaceAll("content/docs/_content.gotmpl", `{{ $.AddPage (dict "kind" "page" "path" "p2" "title" "p2title" "dates" $dates "content" $contentHTML ) }}`, "").Build()
b.AssertFileContent("public/index.html", "RegularPagesRecursive: p1:p1:/docs/p1|p3title:/docs/p3|p4title:/docs/p4|pfile:/docs/pfile|$")
}
func TestPagesFromGoTmplDraftPage(t *testing.T) {
t.Parallel()
b := hugolib.TestRunning(t, filesPagesFromDataTempleBasic)
b.EditFileReplaceAll("content/docs/_content.gotmpl", `"draft" false`, `"draft" true`).Build()
b.AssertFileContent("public/index.html", "RegularPagesRecursive: p1:p1:/docs/p1|p2title:/docs/p2|p4title:/docs/p4|pfile:/docs/pfile|$")
}
func TestPagesFromGoTmplDraftFlagFromResource(t *testing.T) {
t.Parallel()
b := hugolib.TestRunning(t, filesPagesFromDataTempleBasic)
b.EditFileReplaceAll("assets/mydata.yaml", `draft: false`, `draft: true`).Build()
b.AssertFileContent("public/index.html", "RegularPagesRecursive: p1:p1:/docs/p1|p2title:/docs/p2|p3title:/docs/p3|pfile:/docs/pfile|$")
b.EditFileReplaceAll("assets/mydata.yaml", `draft: true`, `draft: false`).Build()
b.AssertFileContent("public/index.html", "RegularPagesRecursive: p1:p1:/docs/p1|p2title:/docs/p2|p3title:/docs/p3|p4title:/docs/p4|pfile:/docs/pfile|$")
}
func TestPagesFromGoTmplMovePage(t *testing.T) {
t.Parallel()
b := hugolib.TestRunning(t, filesPagesFromDataTempleBasic)
b.AssertFileContent("public/index.html", "RegularPagesRecursive: p1:p1:/docs/p1|p2title:/docs/p2|p3title:/docs/p3|p4title:/docs/p4|pfile:/docs/pfile|$")
b.EditFileReplaceAll("content/docs/_content.gotmpl", `"path" "p2"`, `"path" "p2moved"`).Build()
b.AssertFileContent("public/index.html", "RegularPagesRecursive: p1:p1:/docs/p1|p2title:/docs/p2moved|p3title:/docs/p3|p4title:/docs/p4|pfile:/docs/pfile|$")
}
func TestPagesFromGoTmplRemoveGoTmpl(t *testing.T) {
t.Parallel()
b := hugolib.TestRunning(t, filesPagesFromDataTempleBasic)
b.AssertFileContent("public/index.html",
"RegularPagesRecursive: p1:p1:/docs/p1|p2title:/docs/p2|p3title:/docs/p3|p4title:/docs/p4|pfile:/docs/pfile|$",
"Sections: Docs:/docs|",
)
b.AssertFileContent("public/docs/index.html", "RegularPagesRecursive: p1:p1:/docs/p1|p2title:/docs/p2|p3title:/docs/p3|p4title:/docs/p4|pfile:/docs/pfile|$")
b.RemoveFiles("content/docs/_content.gotmpl").Build()
// One regular page left.
b.AssertFileContent("public/index.html",
"RegularPagesRecursive: pfile:/docs/pfile|$",
"Sections: Docs:/docs|",
)
b.AssertFileContent("public/docs/index.html", "RegularPagesRecursive: pfile:/docs/pfile|$")
}
func TestPagesFromGoTmplLanguagePerFile(t *testing.T) {
filesTemplate := `
-- hugo.toml --
defaultContentLanguage = "en"
defaultContentLanguageInSubdir = true
[languages]
[languages.en]
weight = 1
title = "Title"
[languages.fr]
weight = 2
title = "Titre"
disabled = DISABLE
-- layouts/_default/single.html --
Single: {{ .Title }}|{{ .Content }}|
-- content/docs/_content.gotmpl --
{{ $.AddPage (dict "kind" "page" "path" "p1" "title" "Title" ) }}
-- content/docs/_content.fr.gotmpl --
{{ $.AddPage (dict "kind" "page" "path" "p1" "title" "Titre" ) }}
`
for _, disable := range []bool{false, true} {
t.Run(fmt.Sprintf("disable=%t", disable), func(t *testing.T) {
b := hugolib.Test(t, strings.ReplaceAll(filesTemplate, "DISABLE", fmt.Sprintf("%t", disable)))
b.AssertFileContent("public/en/docs/p1/index.html", "Single: Title||")
b.AssertFileExists("public/fr/docs/p1/index.html", !disable)
if !disable {
b.AssertFileContent("public/fr/docs/p1/index.html", "Single: Titre||")
}
})
}
}
func TestPagesFromGoTmplEnableAllLanguages(t *testing.T) {
t.Parallel()
filesTemplate := `
-- hugo.toml --
defaultContentLanguage = "en"
defaultContentLanguageInSubdir = true
[languages]
[languages.en]
weight = 1
title = "Title"
[languages.fr]
title = "Titre"
weight = 2
disabled = DISABLE
-- i18n/en.yaml --
title: Title
-- i18n/fr.yaml --
title: Titre
-- content/docs/_content.gotmpl --
{{ .EnableAllLanguages }}
{{ $titleFromStore := .Store.Get "title" }}
{{ if not $titleFromStore }}
{{ $titleFromStore = "notfound"}}
{{ .Store.Set "title" site.Title }}
{{ end }}
{{ $title := printf "%s:%s:%s" site.Title (i18n "title") $titleFromStore }}
{{ $.AddPage (dict "kind" "page" "path" "p1" "title" $title ) }}
-- layouts/_default/single.html --
Single: {{ .Title }}|{{ .Content }}|
`
for _, disable := range []bool{false, true} {
t.Run(fmt.Sprintf("disable=%t", disable), func(t *testing.T) {
b := hugolib.Test(t, strings.ReplaceAll(filesTemplate, "DISABLE", fmt.Sprintf("%t", disable)))
b.AssertFileExists("public/fr/docs/p1/index.html", !disable)
if !disable {
b.AssertFileContent("public/en/docs/p1/index.html", "Single: Title:Title:notfound||")
b.AssertFileContent("public/fr/docs/p1/index.html", "Single: Titre:Titre:Title||")
}
})
}
}
func TestPagesFromGoTmplMarkdownify(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ["taxonomy", "term", "rss", "sitemap"]
baseURL = "https://example.com"
-- layouts/_default/single.html --
|Content: {{ .Content }}|Title: {{ .Title }}|Path: {{ .Path }}|
-- content/docs/_content.gotmpl --
{{ $content := "**Hello World**" | markdownify }}
{{ $.AddPage (dict "path" "p1" "content" (dict "value" $content "mediaType" "text/html" )) }}
`
b, err := hugolib.TestE(t, files)
// This currently fails. We should fix this, but that is not a trivial task, so do it later.
b.Assert(err, qt.IsNotNil)
b.Assert(err.Error(), qt.Contains, "error calling markdownify: this method cannot be called before the site is fully initialized")
}
func TestPagesFromGoTmplResourceWithoutExtensionWithMediaTypeProvided(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ["taxonomy", "term", "rss", "sitemap"]
baseURL = "https://example.com"
-- layouts/_default/single.html --
|Content: {{ .Content }}|Title: {{ .Title }}|Path: {{ .Path }}|
{{ range .Resources }}
|RelPermalink: {{ .RelPermalink }}|Name: {{ .Name }}|Title: {{ .Title }}|Params: {{ .Params }}|MediaType: {{ .MediaType }}|
{{ end }}
-- content/docs/_content.gotmpl --
{{ $.AddPage (dict "path" "p1" "content" (dict "value" "**Hello World**" "mediaType" "text/markdown" )) }}
{{ $.AddResource (dict "path" "p1/myresource" "content" (dict "value" "abcde" "mediaType" "text/plain" )) }}
`
b := hugolib.Test(t, files)
b.AssertFileContent("public/docs/p1/index.html", "RelPermalink: /docs/p1/myresource|Name: myresource|Title: myresource|Params: map[]|MediaType: text/plain|")
}
func TestPagesFromGoTmplCascade(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ["taxonomy", "term", "rss", "sitemap"]
baseURL = "https://example.com"
-- layouts/_default/single.html --
|Content: {{ .Content }}|Title: {{ .Title }}|Path: {{ .Path }}|Params: {{ .Params }}|
-- content/_content.gotmpl --
{{ $cascade := dict "params" (dict "cascadeparam1" "cascadeparam1value" ) }}
{{ $.AddPage (dict "path" "docs" "kind" "section" "cascade" $cascade ) }}
{{ $.AddPage (dict "path" "docs/p1" "content" (dict "value" "**Hello World**" "mediaType" "text/markdown" )) }}
`
b := hugolib.Test(t, files)
b.AssertFileContent("public/docs/p1/index.html", "|Path: /docs/p1|Params: map[cascadeparam1:cascadeparam1value")
}
func TestPagesFromGoBuildOptions(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ["taxonomy", "term", "rss", "sitemap"]
baseURL = "https://example.com"
-- layouts/_default/single.html --
|Content: {{ .Content }}|Title: {{ .Title }}|Path: {{ .Path }}|Params: {{ .Params }}|
-- content/_content.gotmpl --
{{ $.AddPage (dict "path" "docs/p1" "content" (dict "value" "**Hello World**" "mediaType" "text/markdown" )) }}
{{ $never := dict "list" "never" "publishResources" false "render" "never" }}
{{ $.AddPage (dict "path" "docs/p2" "content" (dict "value" "**Hello World**" "mediaType" "text/markdown" ) "build" $never ) }}
`
b := hugolib.Test(t, files)
b.AssertFileExists("public/docs/p1/index.html", true)
b.AssertFileExists("public/docs/p2/index.html", false)
}
@@ -0,0 +1,32 @@
// Copyright 2024 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 pagesfromdata
import "testing"
func BenchmarkHash(b *testing.B) {
m := map[string]any{
"foo": "bar",
"bar": "foo",
"stringSlice": []any{"a", "b", "c"},
"intSlice": []any{1, 2, 3},
"largeText": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec a diam lectus. Sed sit amet ipsum mauris. Maecenas congue ligula ac quam viverra nec consectetur ante hendrerit.",
}
bs := BuildState{}
for i := 0; i < b.N; i++ {
bs.hash(m)
}
}
+145 -18
View File
@@ -11,6 +11,7 @@ import (
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/htesting"
"github.com/gohugoio/hugo/markup/asciidocext"
"github.com/gohugoio/hugo/resources/resource_transformers/tocss/dartsass"
"github.com/gohugoio/hugo/resources/resource_transformers/tocss/scss"
)
@@ -52,6 +53,11 @@ title: "Home"
Home Content.
-- content/hometext.txt --
Home Text Content.
-- content/myothersection/myothersectionpage.md --
---
title: "myothersectionpage"
---
myothersectionpage Content.
-- layouts/_default/single.html --
Single: {{ .Title }}|{{ .Content }}$
Resources: {{ range $i, $e := .Resources }}{{ $i }}:{{ .RelPermalink }}|{{ .Content }}|{{ end }}$
@@ -120,14 +126,23 @@ func TestRebuildEditTextFileInBranchBundle(t *testing.T) {
b.AssertRenderCountContent(1)
}
func TestRebuildRenameTextFileInLeafBundle(t *testing.T) {
b := TestRunning(t, rebuildFilesSimple)
b.AssertFileContent("public/mysection/mysectionbundle/index.html", "My Section Bundle Text 2 Content.", "Len Resources: 2|")
func testRebuildBothWatchingAndRunning(t *testing.T, files string, withB func(b *IntegrationTestBuilder)) {
t.Helper()
for _, opt := range []TestOpt{TestOptWatching(), TestOptRunning()} {
b := Test(t, files, opt)
withB(b)
}
}
b.RenameFile("content/mysection/mysectionbundle/mysectionbundletext.txt", "content/mysection/mysectionbundle/mysectionbundletext2.txt").Build()
b.AssertFileContent("public/mysection/mysectionbundle/index.html", "mysectionbundletext2", "My Section Bundle Text 2 Content.", "Len Resources: 2|")
b.AssertRenderCountPage(3)
b.AssertRenderCountContent(3)
func TestRebuildRenameTextFileInLeafBundle(t *testing.T) {
testRebuildBothWatchingAndRunning(t, rebuildFilesSimple, func(b *IntegrationTestBuilder) {
b.AssertFileContent("public/mysection/mysectionbundle/index.html", "My Section Bundle Text 2 Content.", "Len Resources: 2|")
b.RenameFile("content/mysection/mysectionbundle/mysectionbundletext.txt", "content/mysection/mysectionbundle/mysectionbundletext2.txt").Build()
b.AssertFileContent("public/mysection/mysectionbundle/index.html", "mysectionbundletext2", "My Section Bundle Text 2 Content.", "Len Resources: 2|")
b.AssertRenderCountPage(5)
b.AssertRenderCountContent(6)
})
}
func TestRebuilEditContentFileInLeafBundle(t *testing.T) {
@@ -137,6 +152,19 @@ func TestRebuilEditContentFileInLeafBundle(t *testing.T) {
b.AssertFileContent("public/mysection/mysectionbundle/index.html", "My Section Bundle Content Content Edited.")
}
func TestRebuilEditContentFileThenAnother(t *testing.T) {
b := TestRunning(t, rebuildFilesSimple)
b.EditFileReplaceAll("content/mysection/mysectionbundle/mysectionbundlecontent.md", "Content Content.", "Content Content Edited.").Build()
b.AssertFileContent("public/mysection/mysectionbundle/index.html", "My Section Bundle Content Content Edited.")
b.AssertRenderCountPage(1)
b.AssertRenderCountContent(2)
b.EditFileReplaceAll("content/myothersection/myothersectionpage.md", "myothersectionpage Content.", "myothersectionpage Content Edited.").Build()
b.AssertFileContent("public/myothersection/myothersectionpage/index.html", "myothersectionpage Content Edited")
b.AssertRenderCountPage(1)
b.AssertRenderCountContent(1)
}
func TestRebuildRenameTextFileInBranchBundle(t *testing.T) {
b := TestRunning(t, rebuildFilesSimple)
b.AssertFileContent("public/mysection/index.html", "My Section")
@@ -153,7 +181,7 @@ func TestRebuildRenameTextFileInHomeBundle(t *testing.T) {
b.RenameFile("content/hometext.txt", "content/hometext2.txt").Build()
b.AssertFileContent("public/index.html", "hometext2", "Home Text Content.")
b.AssertRenderCountPage(2)
b.AssertRenderCountPage(3)
}
func TestRebuildRenameDirectoryWithLeafBundle(t *testing.T) {
@@ -169,7 +197,7 @@ func TestRebuildRenameDirectoryWithBranchBundle(t *testing.T) {
b.AssertFileContent("public/mysectionrenamed/index.html", "My Section")
b.AssertFileContent("public/mysectionrenamed/mysectionbundle/index.html", "My Section Bundle")
b.AssertFileContent("public/mysectionrenamed/mysectionbundle/mysectionbundletext.txt", "My Section Bundle Text 2 Content.")
b.AssertRenderCountPage(2)
b.AssertRenderCountPage(3)
}
func TestRebuildRenameDirectoryWithRegularPageUsedInHome(t *testing.T) {
@@ -268,7 +296,7 @@ func TestRebuildRenameDirectoryWithBranchBundleFastRender(t *testing.T) {
b.AssertFileContent("public/mysectionrenamed/index.html", "My Section")
b.AssertFileContent("public/mysectionrenamed/mysectionbundle/index.html", "My Section Bundle")
b.AssertFileContent("public/mysectionrenamed/mysectionbundle/mysectionbundletext.txt", "My Section Bundle Text 2 Content.")
b.AssertRenderCountPage(2)
b.AssertRenderCountPage(3)
}
func TestRebuilErrorRecovery(t *testing.T) {
@@ -366,8 +394,6 @@ My short.
}
func TestRebuildBaseof(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
title = "Hugo Site"
@@ -382,12 +408,13 @@ Baseof: {{ .Title }}|
Home: {{ .Title }}|{{ .Content }}|
{{ end }}
`
b := Test(t, files, TestOptRunning())
b.AssertFileContent("public/index.html", "Baseof: Hugo Site|", "Home: Hugo Site||")
b.EditFileReplaceFunc("layouts/_default/baseof.html", func(s string) string {
return strings.Replace(s, "Baseof", "Baseof Edited", 1)
}).Build()
b.AssertFileContent("public/index.html", "Baseof Edited: Hugo Site|", "Home: Hugo Site||")
testRebuildBothWatchingAndRunning(t, files, func(b *IntegrationTestBuilder) {
b.AssertFileContent("public/index.html", "Baseof: Hugo Site|", "Home: Hugo Site||")
b.EditFileReplaceFunc("layouts/_default/baseof.html", func(s string) string {
return strings.Replace(s, "Baseof", "Baseof Edited", 1)
}).Build()
b.AssertFileContent("public/index.html", "Baseof Edited: Hugo Site|", "Home: Hugo Site||")
})
}
func TestRebuildSingleWithBaseof(t *testing.T) {
@@ -1514,3 +1541,103 @@ MyTemplate: {{ partial "MyTemplate.html" . }}|
b.AssertFileContent("public/index.html", "MyTemplate: MyTemplate Edited")
}
func TestRebuildEditAsciidocContentFile(t *testing.T) {
if !asciidocext.Supports() {
t.Skip("skip asciidoc")
}
files := `
-- hugo.toml --
baseURL = "https://example.com"
disableLiveReload = true
disableKinds = ["taxonomy", "term", "sitemap", "robotsTXT", "404", "rss", "home", "section"]
[security]
[security.exec]
allow = ['^python$', '^rst2html.*', '^asciidoctor$']
-- content/posts/p1.adoc --
---
title: "P1"
---
P1 Content.
-- content/posts/p2.adoc --
---
title: "P2"
---
P2 Content.
-- layouts/_default/single.html --
Single: {{ .Title }}|{{ .Content }}|
`
b := TestRunning(t, files)
b.AssertFileContent("public/posts/p1/index.html",
"Single: P1|<div class=\"paragraph\">\n<p>P1 Content.</p>\n</div>\n|")
b.AssertRenderCountPage(2)
b.AssertRenderCountContent(2)
b.EditFileReplaceAll("content/posts/p1.adoc", "P1 Content.", "P1 Content Edited.").Build()
b.AssertFileContent("public/posts/p1/index.html", "Single: P1|<div class=\"paragraph\">\n<p>P1 Content Edited.</p>\n</div>\n|")
b.AssertRenderCountPage(1)
b.AssertRenderCountContent(1)
}
func TestRebuildEditSingleListChangeUbuntuIssue12362(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ['rss','section','sitemap','taxonomy','term']
disableLiveReload = true
-- layouts/_default/list.html --
{{ range .Pages }}{{ .Title }}|{{ end }}
-- layouts/_default/single.html --
{{ .Title }}
-- content/p1.md --
---
title: p1
---
`
b := TestRunning(t, files)
b.AssertFileContent("public/index.html", "p1|")
b.AddFiles("content/p2.md", "---\ntitle: p2\n---").Build()
b.AssertFileContent("public/index.html", "p1|p2|") // this test passes, which doesn't match reality
}
func TestRebuildHomeThenPageIssue12436(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
baseURL = "https://example.com"
disableKinds = ['sitemap','taxonomy','term']
disableLiveReload = true
-- layouts/_default/list.html --
{{ .Content }}
-- layouts/_default/single.html --
{{ .Content }}
-- content/_index.md --
---
title: home
---
home-content|
-- content/p1/index.md --
---
title: p1
---
p1-content|
`
b := TestRunning(t, files)
b.AssertFileContent("public/index.html", "home-content|")
b.AssertFileContent("public/p1/index.html", "p1-content|")
b.AssertRenderCountPage(3)
b.EditFileReplaceAll("content/_index.md", "home-content", "home-content-foo").Build()
b.AssertFileContent("public/index.html", "home-content-foo")
b.AssertRenderCountPage(2) // Home page rss + html
b.EditFileReplaceAll("content/p1/index.md", "p1-content", "p1-content-foo").Build()
b.AssertFileContent("public/p1/index.html", "p1-content-foo")
}
+133
View File
@@ -200,3 +200,136 @@ Myshort Original.
b.Build()
b.AssertFileContent("public/p1/index.html", "Edited")
}
func TestRenderShortcodesEditSectionContentWithShortcodeInIncludedPageIssue12458(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableLiveReload = true
disableKinds = ["home", "taxonomy", "term", "rss", "sitemap", "robotsTXT", "404"]
-- content/mysection/_index.md --
---
title: "My Section"
---
## p1-h1
{{% include "p2" %}}
-- content/mysection/p2.md --
---
title: "p2"
---
### Original
{{% myshort %}}
-- layouts/shortcodes/include.html --
{{ $p := .Page.GetPage (.Get 0) }}
{{ $p.RenderShortcodes }}
-- layouts/shortcodes/myshort.html --
Myshort Original.
-- layouts/_default/list.html --
{{ .Content }}
`
b := TestRunning(t, files)
b.AssertFileContent("public/mysection/index.html", "p1-h1")
b.EditFileReplaceAll("content/mysection/_index.md", "p1-h1", "p1-h1 Edited").Build()
b.AssertFileContent("public/mysection/index.html", "p1-h1 Edited")
}
func TestRenderShortcodesNestedPageContextIssue12356(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ["taxonomy", "term", "rss", "sitemap", "robotsTXT", "404"]
-- layouts/_default/_markup/render-image.html --
{{- with .PageInner.Resources.Get .Destination -}}Image: {{ .RelPermalink }}|{{- end -}}
-- layouts/_default/_markup/render-link.html --
{{- with .PageInner.GetPage .Destination -}}Link: {{ .RelPermalink }}|{{- end -}}
-- layouts/_default/_markup/render-heading.html --
Heading: {{ .PageInner.Title }}: {{ .PlainText }}|
-- layouts/_default/_markup/render-codeblock.html --
CodeBlock: {{ .PageInner.Title }}: {{ .Type }}|
-- layouts/_default/list.html --
Content:{{ .Content }}|
Fragments: {{ with .Fragments }}{{.Identifiers }}{{ end }}|
-- layouts/_default/single.html --
Content:{{ .Content }}|
-- layouts/shortcodes/include.html --
{{ with site.GetPage (.Get 0) }}
{{ .RenderShortcodes }}
{{ end }}
-- content/markdown/_index.md --
---
title: "Markdown"
---
# H1
|{{% include "/posts/p1" %}}|
![kitten](pixel3.png "Pixel 3")
§§§go
fmt.Println("Hello")
§§§
-- content/markdown2/_index.md --
---
title: "Markdown 2"
---
|{{< include "/posts/p1" >}}|
-- content/html/_index.html --
---
title: "HTML"
---
|{{% include "/posts/p1" %}}|
-- content/posts/p1/index.md --
---
title: "p1"
---
## H2-p1
![kitten](pixel1.png "Pixel 1")
![kitten](pixel2.png "Pixel 2")
[p2](p2)
§§§bash
echo "Hello"
§§§
-- content/posts/p2/index.md --
---
title: "p2"
---
-- content/posts/p1/pixel1.png --
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
-- content/posts/p1/pixel2.png --
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
-- content/markdown/pixel3.png --
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
-- content/html/pixel4.png --
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
`
b := Test(t, files)
b.AssertFileContent("public/markdown/index.html",
// Images.
"Image: /posts/p1/pixel1.png|\nImage: /posts/p1/pixel2.png|\n|\nImage: /markdown/pixel3.png|</p>\n|",
// Links.
"Link: /posts/p2/|",
// Code blocks
"CodeBlock: p1: bash|", "CodeBlock: Markdown: go|",
// Headings.
"Heading: Markdown: H1|", "Heading: p1: H2-p1|",
// Fragments.
"Fragments: [h1 h2-p1]|",
// Check that the special context markup is not rendered.
"! hugo_ctx",
)
b.AssertFileContent("public/markdown2/index.html", "! hugo_ctx", "Content:<p>|\n ![kitten](pixel1.png \"Pixel 1\")\n![kitten](pixel2.png \"Pixel 2\")\n|</p>\n|")
b.AssertFileContent("public/html/index.html", "! hugo_ctx")
}
+7 -1
View File
@@ -321,10 +321,16 @@ func prepareShortcode(
// Allow the caller to delay the rendering of the shortcode if needed.
var fn shortcodeRenderFunc = func(ctx context.Context) ([]byte, bool, error) {
if p.m.pageConfig.ContentMediaType.IsMarkdown() && sc.doMarkup {
// Signal downwards that the content rendered will be
// parsed and rendered by Goldmark.
ctx = tpl.Context.IsInGoldmark.Set(ctx, true)
}
r, err := doRenderShortcode(ctx, level, s, tplVariants, sc, parent, p, isRenderString)
if err != nil {
return nil, false, toParseErr(err)
}
b, hasVariants, err := r.renderShortcode(ctx)
if err != nil {
return nil, false, toParseErr(err)
@@ -443,7 +449,7 @@ func doRenderShortcode(
// unchanged.
// 2 If inner does not have a newline, strip the wrapping <p> block and
// the newline.
switch p.m.pageConfig.Markup {
switch p.m.pageConfig.Content.Markup {
case "", "markdown":
if match, _ := regexp.MatchString(innerNewlineRegexp, inner); !match {
cleaner, err := regexp.Compile(innerCleanupRegexp)
+63 -34
View File
@@ -62,6 +62,7 @@ import (
)
func (s *Site) Taxonomies() page.TaxonomyList {
s.checkReady()
s.init.taxonomies.Do(context.Background())
return s.taxonomies
}
@@ -204,6 +205,7 @@ type siteRenderingContext struct {
}
func (s *Site) Menus() navigation.Menus {
s.checkReady()
s.init.menus.Do(context.Background())
return s.menus
}
@@ -372,19 +374,33 @@ func (s *Site) watching() bool {
type whatChanged struct {
mu sync.Mutex
contentChanged bool
identitySet identity.Identities
needsPagesAssembly bool
identitySet identity.Identities
}
func (w *whatChanged) Add(ids ...identity.Identity) {
w.mu.Lock()
defer w.mu.Unlock()
if w.identitySet == nil {
w.identitySet = make(identity.Identities)
}
for _, id := range ids {
w.identitySet[id] = true
}
}
func (w *whatChanged) Clear() {
w.mu.Lock()
defer w.mu.Unlock()
w.clear()
}
func (w *whatChanged) clear() {
w.identitySet = identity.Identities{}
}
func (w *whatChanged) Changes() []identity.Identity {
if w == nil || w.identitySet == nil {
return nil
@@ -392,6 +408,14 @@ func (w *whatChanged) Changes() []identity.Identity {
return w.identitySet.AsSlice()
}
func (w *whatChanged) Drain() []identity.Identity {
w.mu.Lock()
defer w.mu.Unlock()
ids := w.identitySet.AsSlice()
w.clear()
return ids
}
// RegisterMediaTypes will register the Site's media types in the mime
// package, so it will behave correctly with Hugo's built-in server.
func (s *Site) RegisterMediaTypes() {
@@ -424,7 +448,35 @@ func (h *HugoSites) fileEventsFilter(events []fsnotify.Event) []fsnotify.Event {
events[n] = ev
n++
}
return events[:n]
events = events[:n]
eventOrdinal := func(e fsnotify.Event) int {
// Pull the structural changes to the top.
if e.Op.Has(fsnotify.Create) {
return 1
}
if e.Op.Has(fsnotify.Remove) {
return 2
}
if e.Op.Has(fsnotify.Rename) {
return 3
}
if e.Op.Has(fsnotify.Write) {
return 4
}
return 5
}
sort.Slice(events, func(i, j int) bool {
// First sort by event type.
if eventOrdinal(events[i]) != eventOrdinal(events[j]) {
return eventOrdinal(events[i]) < eventOrdinal(events[j])
}
// Then sort by name.
return events[i].Name < events[j].Name
})
return events
}
type fileEventInfo struct {
@@ -494,41 +546,17 @@ func (h *HugoSites) fileEventsApplyInfo(events []fsnotify.Event) []fileEventInfo
return infos
}
func (h *HugoSites) fileEventsTranslate(events []fsnotify.Event) []fsnotify.Event {
eventMap := make(map[string][]fsnotify.Event)
// We often get a Remove etc. followed by a Create, a Create followed by a Write.
// Remove the superfluous events to make the update logic simpler.
for _, ev := range events {
eventMap[ev.Name] = append(eventMap[ev.Name], ev)
}
func (h *HugoSites) fileEventsTrim(events []fsnotify.Event) []fsnotify.Event {
seen := make(map[string]bool)
n := 0
for _, ev := range events {
mapped := eventMap[ev.Name]
// Keep one
found := false
var kept fsnotify.Event
for i, ev2 := range mapped {
if i == 0 {
kept = ev2
}
if ev2.Op&fsnotify.Write == fsnotify.Write {
kept = ev2
found = true
}
if !found && ev2.Op&fsnotify.Create == fsnotify.Create {
kept = ev2
}
if seen[ev.Name] {
continue
}
events[n] = kept
seen[ev.Name] = true
events[n] = ev
n++
}
return events
}
@@ -655,7 +683,7 @@ func (s *Site) assembleMenus() error {
if sectionPagesMenu != "" {
if err := s.pageMap.forEachPage(pagePredicates.ShouldListGlobal, func(p *pageState) (bool, error) {
if p.IsHome() || !p.m.shouldBeCheckedForMenuDefinitions() {
if p.Kind() != kinds.KindSection || !p.m.shouldBeCheckedForMenuDefinitions() {
return false, nil
}
@@ -782,6 +810,7 @@ func (s *Site) errorCollator(results <-chan error, errs chan<- error) {
// as possible for existing sites. Most sites will use {{ .Site.GetPage "section" "my/section" }},
// i.e. 2 arguments, so we test for that.
func (s *Site) GetPage(ref ...string) (page.Page, error) {
s.checkReady()
p, err := s.s.getPageForRefs(ref...)
if p == nil {
+25 -3
View File
@@ -32,6 +32,7 @@ import (
"github.com/gohugoio/hugo/config/allconfig"
"github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/hugolib/doctree"
"github.com/gohugoio/hugo/hugolib/pagesfromdata"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/langs"
"github.com/gohugoio/hugo/langs/i18n"
@@ -51,7 +52,15 @@ import (
var _ page.Site = (*Site)(nil)
type siteState int
const (
siteStateInit siteState = iota
siteStateReady
)
type Site struct {
state siteState
conf *allconfig.Config
language *langs.Language
languagei int
@@ -123,14 +132,14 @@ func NewHugoSites(cfg deps.DepsCfg) (*HugoSites, error) {
HandlerPost: logHookLast,
Stdout: cfg.LogOut,
Stderr: cfg.LogOut,
StoreErrors: conf.Running(),
StoreErrors: conf.Watching(),
SuppressStatements: conf.IgnoredLogs(),
}
logger = loggers.New(logOpts)
}
memCache := dynacache.New(dynacache.Options{Running: conf.Running(), Log: logger})
memCache := dynacache.New(dynacache.Options{Watching: conf.Watching(), Log: logger})
firstSiteDeps := &deps.Deps{
Fs: cfg.Fs,
@@ -166,7 +175,8 @@ func NewHugoSites(cfg deps.DepsCfg) (*HugoSites, error) {
treeResources: doctree.New(
treeConfig,
),
treeTaxonomyEntries: doctree.NewTreeShiftTree[*weightedContentNode](doctree.DimensionLanguage.Index(), len(confm.Languages)),
treeTaxonomyEntries: doctree.NewTreeShiftTree[*weightedContentNode](doctree.DimensionLanguage.Index(), len(confm.Languages)),
treePagesFromTemplateAdapters: doctree.NewTreeShiftTree[*pagesfromdata.PagesFromTemplate](doctree.DimensionLanguage.Index(), len(confm.Languages)),
}
pageTrees.createMutableTrees()
@@ -415,6 +425,7 @@ func (s *Site) Current() page.Site {
// MainSections returns the list of main sections.
func (s *Site) MainSections() []string {
s.checkReady()
return s.conf.C.MainSections
}
@@ -433,6 +444,7 @@ func (s *Site) BaseURL() string {
// Deprecated: Use .Site.Lastmod instead.
func (s *Site) LastChange() time.Time {
s.checkReady()
hugo.Deprecate(".Site.LastChange", "Use .Site.Lastmod instead.", "v0.123.0")
return s.lastmod
}
@@ -521,6 +533,7 @@ func (s *Site) ForEeachIdentityByName(name string, f func(identity.Identity) boo
// Pages returns all pages.
// This is for the current language only.
func (s *Site) Pages() page.Pages {
s.checkReady()
return s.pageMap.getPagesInSection(
pageMapQueryPagesInSection{
pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{
@@ -537,6 +550,7 @@ func (s *Site) Pages() page.Pages {
// RegularPages returns all the regular pages.
// This is for the current language only.
func (s *Site) RegularPages() page.Pages {
s.checkReady()
return s.pageMap.getPagesInSection(
pageMapQueryPagesInSection{
pageMapQueryPagesBelowPath: pageMapQueryPagesBelowPath{
@@ -551,10 +565,18 @@ func (s *Site) RegularPages() page.Pages {
// AllPages returns all pages for all sites.
func (s *Site) AllPages() page.Pages {
s.checkReady()
return s.h.Pages()
}
// AllRegularPages returns all regular pages for all sites.
func (s *Site) AllRegularPages() page.Pages {
s.checkReady()
return s.h.RegularPages()
}
func (s *Site) checkReady() {
if s.state != siteStateReady {
panic("this method cannot be called before the site is fully initialized")
}
}
+2
View File
@@ -19,10 +19,12 @@ import (
// Sections returns the top level sections.
func (s *Site) Sections() page.Pages {
s.checkReady()
return s.Home().Sections()
}
// Home is a shortcut to the home page, equivalent to .Site.GetPage "home".
func (s *Site) Home() page.Page {
s.checkReady()
return s.s.home
}
+8 -2
View File
@@ -1,7 +1,13 @@
# Release env.
# These will be replaced by script before release.
HUGORELEASER_TAG=v0.124.1
HUGORELEASER_COMMITISH=db083b05f16c945fec04f745f0ca8640560cf1ec
HUGORELEASER_TAG=v0.125.7
HUGORELEASER_COMMITISH=b1d808bc373f53ad37c8966bb02a6aea095db5f8
+1
View File
@@ -61,6 +61,7 @@ func (tp *TranslationProvider) NewResource(dst *deps.Deps) error {
hugofs.WalkwayConfig{
Fs: dst.BaseFs.I18n.Fs,
IgnoreFile: dst.SourceSpec.IgnoreFile,
PathParser: dst.SourceSpec.Cfg.PathParser(),
WalkFn: func(path string, info hugofs.FileMetaInfo) error {
if info.IsDir() {
return nil
+34
View File
@@ -0,0 +1,34 @@
/*
Hugo adds a specific prefix, "__hugo_navigate", to the path in certain situations to signal
navigation to another content page.
*/
function HugoReload() {}
HugoReload.identifier = 'hugoReloader';
HugoReload.version = '0.9';
HugoReload.prototype.reload = function (path, options) {
var prefix = '__hugo_navigate';
if (path.lastIndexOf(prefix, 0) !== 0) {
return false;
}
path = path.substring(prefix.length);
var portChanged = options.overrideURL && options.overrideURL != window.location.port;
if (!portChanged && window.location.pathname === path) {
window.location.reload();
} else {
if (portChanged) {
window.location = location.protocol + '//' + location.hostname + ':' + options.overrideURL + path;
} else {
window.location.pathname = path;
}
}
return true;
};
LiveReload.addPlugin(HugoReload);
+61
View File
@@ -0,0 +1,61 @@
//go:generate go run main.go
package main
import (
_ "embed"
"fmt"
"io"
"log"
"net/http"
"os"
"github.com/evanw/esbuild/pkg/api"
)
//go:embed livereload-hugo-plugin.js
var livereloadHugoPluginJS string
func main() {
// 4.0.2
// To upgrade to a new version, change to the commit hash of the version you want to upgrade to
// then run mage generate from the root.
const liveReloadCommit = "d803a41804d2d71e0814c4e9e3233e78991024d9"
liveReloadSourceURL := fmt.Sprintf("https://raw.githubusercontent.com/livereload/livereload-js/%s/dist/livereload.js", liveReloadCommit)
func() {
resp, err := http.Get(liveReloadSourceURL)
must(err)
defer resp.Body.Close()
b, err := io.ReadAll(resp.Body)
must(err)
// Write the unminified livereload.js file.
err = os.WriteFile("../livereload.js", b, 0o644)
must(err)
// Bundle and minify with ESBuild.
result := api.Build(api.BuildOptions{
Stdin: &api.StdinOptions{
Contents: string(b) + livereloadHugoPluginJS,
},
Outfile: "../livereload.min.js",
Bundle: true,
Target: api.ES2015,
Write: true,
MinifyWhitespace: true,
MinifyIdentifiers: true,
MinifySyntax: true,
})
if len(result.Errors) > 0 {
log.Fatal(result.Errors)
}
}()
}
func must(err error) {
if err != nil {
log.Fatal(err)
}
}
+5 -44
View File
@@ -1,4 +1,4 @@
// Copyright 2015 The Hugo Authors. All rights reserved.
// Copyright 2024 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.
@@ -50,6 +50,7 @@ import (
)
// Prefix to signal to LiveReload that we need to navigate to another path.
// Do not change this.
const hugoNavigatePrefix = "__hugo_navigate"
var upgrader = &websocket.Upgrader{
@@ -144,48 +145,8 @@ func ServeJS(w http.ResponseWriter, r *http.Request) {
}
func liveReloadJS() []byte {
return []byte(livereloadJS + hugoLiveReloadPlugin)
return []byte(livereloadJS)
}
var (
// This is a patched version, see https://github.com/livereload/livereload-js/pull/84
//go:embed livereload.js
livereloadJS string
hugoLiveReloadPlugin = fmt.Sprintf(`
/*
Hugo adds a specific prefix, "__hugo_navigate", to the path in certain situations to signal
navigation to another content page.
*/
function HugoReload() {}
HugoReload.identifier = 'hugoReloader';
HugoReload.version = '0.9';
HugoReload.prototype.reload = function(path, options) {
var prefix = %q;
if (path.lastIndexOf(prefix, 0) !== 0) {
return false
}
path = path.substring(prefix.length);
var portChanged = options.overrideURL && options.overrideURL != window.location.port
if (!portChanged && window.location.pathname === path) {
window.location.reload();
} else {
if (portChanged) {
window.location = location.protocol + "//" + location.hostname + ":" + options.overrideURL + path;
} else {
window.location.pathname = path;
}
}
return true;
};
LiveReload.addPlugin(HugoReload)
`, hugoNavigatePrefix)
)
//go:embed livereload.min.js
var livereloadJS string
+3795 -1
View File
File diff suppressed because one or more lines are too long
+7
View File
File diff suppressed because one or more lines are too long
+1 -2
View File
@@ -79,8 +79,7 @@ func flagEnv() map[string]string {
// Generate autogen packages
func Generate() error {
generatorPackages := []string{
//"tpl/tplimpl/embedded/generate",
//"resources/page/generate",
"livereload/gen",
}
for _, pkg := range generatorPackages {
+5 -4
View File
@@ -135,10 +135,11 @@ func (b Bytes) Bytes() []byte {
// DocumentContext holds contextual information about the document to convert.
type DocumentContext struct {
Document any // May be nil. Usually a page.Page
DocumentID string
DocumentName string
Filename string
Document any // May be nil. Usually a page.Page
DocumentLookup func(uint64) any // May be nil.
DocumentID string
DocumentName string
Filename string
}
// RenderContext holds contextual information about the content to render.
+13 -7
View File
@@ -32,8 +32,7 @@ type AttributesProvider interface {
// LinkContext is the context passed to a link render hook.
type LinkContext interface {
// The Page being rendered.
Page() any
PageProvider
// The link URL.
Destination() string
@@ -64,6 +63,7 @@ type ImageLinkContext interface {
type CodeblockContext interface {
AttributesProvider
text.Positioner
PageProvider
// Chroma highlighting processing options. This will only be filled if Type is a known Chroma Lexer.
Options() map[string]any
@@ -76,9 +76,6 @@ type CodeblockContext interface {
// Zero-based ordinal for all code blocks in the current document.
Ordinal() int
// The owning Page.
Page() any
}
type AttributesOptionsSliceProvider interface {
@@ -101,8 +98,7 @@ type IsDefaultCodeBlockRendererProvider interface {
// HeadingContext contains accessors to all attributes that a HeadingRenderer
// can use to render a heading.
type HeadingContext interface {
// Page is the page containing the heading.
Page() any
PageProvider
// Level is the level of the header (i.e. 1 for top-level, 2 for sub-level, etc.).
Level() int
// Anchor is the HTML id assigned to the heading.
@@ -116,6 +112,16 @@ type HeadingContext interface {
AttributesProvider
}
type PageProvider interface {
// Page is the page being rendered.
Page() any
// PageInner may be different than Page when .RenderShortcodes is in play.
// The main use case for this is to include other pages' markdown into the current page
// but resolve resources and pages relative to the original.
PageInner() any
}
// HeadingRenderer describes a uniquely identifiable rendering hook.
type HeadingRenderer interface {
// RenderHeading writes the rendered content to w using the data in w.
+22 -5
View File
@@ -108,6 +108,7 @@ func (r *htmlRenderer) renderCodeBlock(w util.BufWriter, src []byte, node ast.No
}
cbctx := &codeBlockContext{
page: ctx.DocumentContext().Document,
pageInner: r.getPageInner(ctx),
lang: lang,
code: s,
ordinal: ordinal,
@@ -132,7 +133,6 @@ func (r *htmlRenderer) renderCodeBlock(w util.BufWriter, src []byte, node ast.No
w,
cbctx,
)
if err != nil {
return ast.WalkContinue, herrors.NewFileErrorFromPos(err, cbctx.createPos())
}
@@ -140,11 +140,24 @@ func (r *htmlRenderer) renderCodeBlock(w util.BufWriter, src []byte, node ast.No
return ast.WalkContinue, nil
}
func (r *htmlRenderer) getPageInner(rctx *render.Context) any {
pid := rctx.PeekPid()
if pid > 0 {
if lookup := rctx.DocumentContext().DocumentLookup; lookup != nil {
if v := rctx.DocumentContext().DocumentLookup(pid); v != nil {
return v
}
}
}
return rctx.DocumentContext().Document
}
type codeBlockContext struct {
page any
lang string
code string
ordinal int
page any
pageInner any
lang string
code string
ordinal int
// This is only used in error situations and is expensive to create,
// to delay creation until needed.
@@ -159,6 +172,10 @@ func (c *codeBlockContext) Page() any {
return c.page
}
func (c *codeBlockContext) PageInner() any {
return c.pageInner
}
func (c *codeBlockContext) Type() string {
return c.lang
}
+12
View File
@@ -17,7 +17,9 @@ package goldmark
import (
"bytes"
"github.com/gohugoio/hugo-goldmark-extensions/extras"
"github.com/gohugoio/hugo-goldmark-extensions/passthrough"
"github.com/gohugoio/hugo/markup/goldmark/hugocontext"
"github.com/yuin/goldmark/util"
"github.com/gohugoio/hugo/markup/goldmark/codeblocks"
@@ -103,6 +105,7 @@ func newMarkdown(pcfg converter.ProviderConfig) goldmark.Markdown {
renderer.WithNodeRenderers(util.Prioritized(emoji.NewHTMLRenderer(), 200)))
var (
extensions = []goldmark.Extender{
hugocontext.New(),
newLinks(cfg),
newTocExtension(tocRendererOptions),
}
@@ -111,6 +114,15 @@ func newMarkdown(pcfg converter.ProviderConfig) goldmark.Markdown {
extensions = append(extensions, images.New(cfg.Parser.WrapStandAloneImageWithinParagraph))
extensions = append(extensions, extras.New(
extras.Config{
Insert: extras.InsertConfig{Enable: cfg.Extensions.Extras.Insert.Enable},
Mark: extras.MarkConfig{Enable: cfg.Extensions.Extras.Mark.Enable},
Subscript: extras.SubscriptConfig{Enable: cfg.Extensions.Extras.Subscript.Enable},
Superscript: extras.SuperscriptConfig{Enable: cfg.Extensions.Extras.Superscript.Enable},
},
))
if mcfg.Highlight.CodeFences {
extensions = append(extensions, codeblocks.New())
}
+41 -1
View File
@@ -49,6 +49,20 @@ var Default = Config{
EastAsianLineBreaksStyle: "simple",
EscapedSpace: false,
},
Extras: Extras{
Superscript: Superscript{
Enable: false,
},
Subscript: Subscript{
Enable: false,
},
Insert: Insert{
Enable: false,
},
Mark: Mark{
Enable: false,
},
},
Passthrough: Passthrough{
Enable: false,
Delimiters: DelimitersConfig{
@@ -112,6 +126,7 @@ type Extensions struct {
Typographer Typographer
Footnote bool
DefinitionList bool
Extras Extras
Passthrough Passthrough
// GitHub flavored markdown
@@ -150,7 +165,32 @@ type Typographer struct {
Apostrophe string
}
// Passthrough hold passthrough configuration.
// Extras holds extras configuration.
// github.com/hugoio/hugo-goldmark-extensions/extras
type Extras struct {
Insert Insert
Mark Mark
Subscript Subscript
Superscript Superscript
}
type Insert struct {
Enable bool
}
type Mark struct {
Enable bool
}
type Subscript struct {
Enable bool
}
type Superscript struct {
Enable bool
}
// Passthrough holds passthrough configuration.
// github.com/hugoio/hugo-goldmark-extensions/passthrough
type Passthrough struct {
// Whether to enable the extension
@@ -744,3 +744,53 @@ a^*=x-b^*
%!%
`)
}
func TestExtrasExtension(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
disableKinds = ['page','rss','section','sitemap','taxonomy','term']
[markup.goldmark.extensions.extras.insert]
enable = false
[markup.goldmark.extensions.extras.mark]
enable = false
[markup.goldmark.extensions.extras.subscript]
enable = false
[markup.goldmark.extensions.extras.superscript]
enable = false
-- layouts/index.html --
{{ .Content }}
-- content/_index.md --
---
title: home
---
++insert++
==mark==
H~2~0
1^st^
`
b := hugolib.Test(t, files)
b.AssertFileContent("public/index.html",
"<p>++insert++</p>",
"<p>==mark==</p>",
"<p>H~2~0</p>",
"<p>1^st^</p>",
)
files = strings.ReplaceAll(files, "enable = false", "enable = true")
b = hugolib.Test(t, files)
b.AssertFileContent("public/index.html",
"<p><ins>insert</ins></p>",
"<p><mark>mark</mark></p>",
"<p>H<sub>2</sub>0</p>",
"<p>1<sup>st</sup></p>",
)
}
+165
View File
@@ -0,0 +1,165 @@
// Copyright 2024 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 hugocontext
import (
"bytes"
"fmt"
"strconv"
"github.com/gohugoio/hugo/bufferpool"
"github.com/gohugoio/hugo/markup/goldmark/internal/render"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/text"
"github.com/yuin/goldmark/util"
)
func New() goldmark.Extender {
return &hugoContextExtension{}
}
// Wrap wraps the given byte slice in a Hugo context that used to determine the correct Page
// in .RenderShortcodes.
func Wrap(b []byte, pid uint64) string {
buf := bufferpool.GetBuffer()
defer bufferpool.PutBuffer(buf)
buf.Write(prefix)
buf.WriteString(" pid=")
buf.WriteString(strconv.FormatUint(pid, 10))
buf.Write(endDelim)
buf.WriteByte('\n')
buf.Write(b)
buf.Write(prefix)
buf.Write(closingDelimAndNewline)
return buf.String()
}
var kindHugoContext = ast.NewNodeKind("HugoContext")
// HugoContext is a node that represents a Hugo context.
type HugoContext struct {
ast.BaseInline
Closing bool
// Internal page ID. Not persisted.
Pid uint64
}
// Dump implements Node.Dump.
func (n *HugoContext) Dump(source []byte, level int) {
m := map[string]string{}
m["Pid"] = fmt.Sprintf("%v", n.Pid)
ast.DumpHelper(n, source, level, m, nil)
}
func (n *HugoContext) parseAttrs(attrBytes []byte) {
keyPairs := bytes.Split(attrBytes, []byte(" "))
for _, keyPair := range keyPairs {
kv := bytes.Split(keyPair, []byte("="))
if len(kv) != 2 {
continue
}
key := string(kv[0])
val := string(kv[1])
switch key {
case "pid":
pid, _ := strconv.ParseUint(val, 10, 64)
n.Pid = pid
}
}
}
func (h *HugoContext) Kind() ast.NodeKind {
return kindHugoContext
}
var (
prefix = []byte("{{__hugo_ctx")
endDelim = []byte("}}")
closingDelimAndNewline = []byte("/}}\n")
)
var _ parser.InlineParser = (*hugoContextParser)(nil)
type hugoContextParser struct{}
func (s *hugoContextParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node {
line, _ := block.PeekLine()
if !bytes.HasPrefix(line, prefix) {
return nil
}
end := bytes.Index(line, endDelim)
if end == -1 {
return nil
}
block.Advance(end + len(endDelim) + 1) // +1 for the newline
if line[end-1] == '/' {
return &HugoContext{Closing: true}
}
attrBytes := line[len(prefix)+1 : end]
h := &HugoContext{}
h.parseAttrs(attrBytes)
return h
}
func (a *hugoContextParser) Trigger() []byte {
return []byte{'{'}
}
type hugoContextRenderer struct{}
func (r *hugoContextRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(kindHugoContext, r.handleHugoContext)
}
func (r *hugoContextRenderer) handleHugoContext(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
hctx := node.(*HugoContext)
ctx, ok := w.(*render.Context)
if !ok {
return ast.WalkContinue, nil
}
if hctx.Closing {
_ = ctx.PopPid()
} else {
ctx.PushPid(hctx.Pid)
}
return ast.WalkContinue, nil
}
type hugoContextExtension struct{}
func (a *hugoContextExtension) Extend(m goldmark.Markdown) {
m.Parser().AddOptions(
parser.WithInlineParsers(
util.Prioritized(&hugoContextParser{}, 50),
),
)
m.Renderer().AddOptions(
renderer.WithNodeRenderers(
util.Prioritized(&hugoContextRenderer{}, 50),
),
)
}
@@ -0,0 +1,34 @@
// Copyright 2024 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 hugocontext
import (
"testing"
qt "github.com/frankban/quicktest"
)
func TestWrap(t *testing.T) {
c := qt.New(t)
b := []byte("test")
c.Assert(Wrap(b, 42), qt.Equals, "{{__hugo_ctx pid=42}}\ntest{{__hugo_ctx/}}\n")
}
func BenchmarkWrap(b *testing.B) {
for i := 0; i < b.N; i++ {
Wrap([]byte("test"), 42)
}
}
@@ -41,6 +41,7 @@ func (b *BufWriter) Flush() error {
type Context struct {
*BufWriter
positions []int
pids []uint64
ContextData
}
@@ -55,6 +56,30 @@ func (ctx *Context) PopPos() int {
return p
}
// PushPid pushes a new page ID to the stack.
func (ctx *Context) PushPid(pid uint64) {
ctx.pids = append(ctx.pids, pid)
}
// PeekPid returns the current page ID without removing it from the stack.
func (ctx *Context) PeekPid() uint64 {
if len(ctx.pids) == 0 {
return 0
}
return ctx.pids[len(ctx.pids)-1]
}
// PopPid pops the last page ID from the stack.
func (ctx *Context) PopPid() uint64 {
if len(ctx.pids) == 0 {
return 0
}
i := len(ctx.pids) - 1
p := ctx.pids[i]
ctx.pids = ctx.pids[:i]
return p
}
type ContextData interface {
RenderContext() converter.RenderContext
DocumentContext() converter.DocumentContext
+26
View File
@@ -49,6 +49,7 @@ func newLinks(cfg goldmark_config.Config) goldmark.Extender {
type linkContext struct {
page any
pageInner any
destination string
title string
text hstring.RenderedString
@@ -64,6 +65,10 @@ func (ctx linkContext) Page() any {
return ctx.page
}
func (ctx linkContext) PageInner() any {
return ctx.pageInner
}
func (ctx linkContext) Text() hstring.RenderedString {
return ctx.text
}
@@ -92,6 +97,7 @@ func (ctx imageLinkContext) Ordinal() int {
type headingContext struct {
page any
pageInner any
level int
anchor string
text hstring.RenderedString
@@ -103,6 +109,10 @@ func (ctx headingContext) Page() any {
return ctx.page
}
func (ctx headingContext) PageInner() any {
return ctx.pageInner
}
func (ctx headingContext) Level() int {
return ctx.level
}
@@ -186,6 +196,7 @@ func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.N
imageLinkContext{
linkContext: linkContext{
page: ctx.DocumentContext().Document,
pageInner: r.getPageInner(ctx),
destination: string(n.Destination),
title: string(n.Title),
text: hstring.RenderedString(text),
@@ -200,6 +211,18 @@ func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.N
return ast.WalkContinue, err
}
func (r *hookedRenderer) getPageInner(rctx *render.Context) any {
pid := rctx.PeekPid()
if pid > 0 {
if lookup := rctx.DocumentContext().DocumentLookup; lookup != nil {
if v := rctx.DocumentContext().DocumentLookup(pid); v != nil {
return v
}
}
}
return rctx.DocumentContext().Document
}
func (r *hookedRenderer) filterInternalAttributes(attrs []ast.Attribute) []ast.Attribute {
n := 0
for _, x := range attrs {
@@ -274,6 +297,7 @@ func (r *hookedRenderer) renderLink(w util.BufWriter, source []byte, node ast.No
w,
linkContext{
page: ctx.DocumentContext().Document,
pageInner: r.getPageInner(ctx),
destination: string(n.Destination),
title: string(n.Title),
text: hstring.RenderedString(text),
@@ -339,6 +363,7 @@ func (r *hookedRenderer) renderAutoLink(w util.BufWriter, source []byte, node as
w,
linkContext{
page: ctx.DocumentContext().Document,
pageInner: r.getPageInner(ctx),
destination: url,
text: hstring.RenderedString(label),
plainText: label,
@@ -423,6 +448,7 @@ func (r *hookedRenderer) renderHeading(w util.BufWriter, source []byte, node ast
w,
headingContext{
page: ctx.DocumentContext().Document,
pageInner: r.getPageInner(ctx),
level: n.Level,
anchor: string(anchor),
text: hstring.RenderedString(text),
+23 -6
View File
@@ -19,6 +19,7 @@ import (
"strings"
"github.com/gohugoio/hugo/markup/highlight"
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/markup/markup_config"
@@ -44,7 +45,7 @@ func NewConverterProvider(cfg converter.ProviderConfig) (ConverterProvider, erro
defaultHandler := mcfg.DefaultMarkdownHandler
var defaultFound bool
add := func(p converter.ProviderProvider, aliases ...string) error {
add := func(p converter.ProviderProvider, subType string, aliases ...string) error {
c, err := p.New(cfg)
if err != nil {
return err
@@ -53,6 +54,7 @@ func NewConverterProvider(cfg converter.ProviderConfig) (ConverterProvider, erro
name := c.Name()
aliases = append(aliases, name)
aliases = append(aliases, subType)
if strings.EqualFold(name, defaultHandler) {
aliases = append(aliases, "markdown")
@@ -63,19 +65,21 @@ func NewConverterProvider(cfg converter.ProviderConfig) (ConverterProvider, erro
return nil
}
if err := add(goldmark.Provider); err != nil {
contentTypes := cfg.Conf.ContentTypes().(media.ContentTypes)
if err := add(goldmark.Provider, contentTypes.Markdown.SubType, contentTypes.Markdown.Suffixes()...); err != nil {
return nil, err
}
if err := add(asciidocext.Provider, "ad", "adoc"); err != nil {
if err := add(asciidocext.Provider, contentTypes.AsciiDoc.SubType, contentTypes.AsciiDoc.Suffixes()...); err != nil {
return nil, err
}
if err := add(rst.Provider); err != nil {
if err := add(rst.Provider, contentTypes.ReStructuredText.SubType, contentTypes.ReStructuredText.Suffixes()...); err != nil {
return nil, err
}
if err := add(pandoc.Provider, "pdc"); err != nil {
if err := add(pandoc.Provider, contentTypes.Pandoc.SubType, contentTypes.Pandoc.Suffixes()...); err != nil {
return nil, err
}
if err := add(org.Provider); err != nil {
if err := add(org.Provider, contentTypes.EmacsOrgMode.SubType, contentTypes.EmacsOrgMode.Suffixes()...); err != nil {
return nil, err
}
@@ -133,3 +137,16 @@ func addConverter(m map[string]converter.Provider, c converter.Provider, aliases
m[alias] = c
}
}
// ResolveMarkup returns the markup type.
func ResolveMarkup(s string) string {
s = strings.ToLower(s)
switch s {
case "goldmark":
return media.DefaultContentTypes.Markdown.SubType
case "asciidocext":
return media.DefaultContentTypes.AsciiDoc.SubType
default:
return s
}
}
+18 -13
View File
@@ -34,8 +34,12 @@ type BuiltinTypes struct {
OpenTypeFontType Type
// Common document types
PDFType Type
MarkdownType Type
PDFType Type
MarkdownType Type
EmacsOrgModeType Type
AsciiDocType Type
PandocType Type
ReStructuredTextType Type
// Common video types
AVIType Type
@@ -85,8 +89,12 @@ var Builtin = BuiltinTypes{
OpenTypeFontType: Type{Type: "font/otf"},
// Common document types
PDFType: Type{Type: "application/pdf"},
MarkdownType: Type{Type: "text/markdown"},
PDFType: Type{Type: "application/pdf"},
MarkdownType: Type{Type: "text/markdown"},
AsciiDocType: Type{Type: "text/asciidoc"}, // https://github.com/asciidoctor/asciidoctor/issues/2502
PandocType: Type{Type: "text/pandoc"},
ReStructuredTextType: Type{Type: "text/rst"}, // https://docutils.sourceforge.io/FAQ.html#what-s-the-official-mime-type-for-restructuredtext-data
EmacsOrgModeType: Type{Type: "text/org"},
// Common video types
AVIType: Type{Type: "video/x-msvideo"},
@@ -108,7 +116,7 @@ var defaultMediaTypesConfig = map[string]any{
"text/x-scss": map[string]any{"suffixes": []string{"scss"}},
"text/x-sass": map[string]any{"suffixes": []string{"sass"}},
"text/csv": map[string]any{"suffixes": []string{"csv"}},
"text/html": map[string]any{"suffixes": []string{"html"}},
"text/html": map[string]any{"suffixes": []string{"html", "htm"}},
"text/javascript": map[string]any{"suffixes": []string{"js", "jsm", "mjs"}},
"text/typescript": map[string]any{"suffixes": []string{"ts"}},
"text/tsx": map[string]any{"suffixes": []string{"tsx"}},
@@ -137,7 +145,11 @@ var defaultMediaTypesConfig = map[string]any{
// Common document types
"application/pdf": map[string]any{"suffixes": []string{"pdf"}},
"text/markdown": map[string]any{"suffixes": []string{"md", "markdown"}},
"text/markdown": map[string]any{"suffixes": []string{"md", "mdown", "markdown"}},
"text/asciidoc": map[string]any{"suffixes": []string{"adoc", "asciidoc", "ad"}},
"text/pandoc": map[string]any{"suffixes": []string{"pandoc", "pdc"}},
"text/rst": map[string]any{"suffixes": []string{"rst"}},
"text/org": map[string]any{"suffixes": []string{"org"}},
// Common video types
"video/x-msvideo": map[string]any{"suffixes": []string{"avi"}},
@@ -152,10 +164,3 @@ var defaultMediaTypesConfig = map[string]any{
"application/octet-stream": map[string]any{},
}
func init() {
// Apply delimiter to all.
for _, m := range defaultMediaTypesConfig {
m.(map[string]any)["delimiter"] = "."
}
}
+120 -2
View File
@@ -14,13 +14,14 @@
package media
import (
"errors"
"fmt"
"path/filepath"
"reflect"
"sort"
"strings"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/config"
"github.com/mitchellh/mapstructure"
@@ -31,6 +32,11 @@ import (
var DefaultTypes Types
func init() {
// Apply delimiter to all.
for _, m := range defaultMediaTypesConfig {
m.(map[string]any)["delimiter"] = "."
}
ns, err := DecodeTypes(nil)
if err != nil {
panic(err)
@@ -39,17 +45,122 @@ func init() {
// Initialize the Builtin types with values from DefaultTypes.
v := reflect.ValueOf(&Builtin).Elem()
for i := 0; i < v.NumField(); i++ {
f := v.Field(i)
fieldName := v.Type().Field(i).Name
builtinType := f.Interface().(Type)
if builtinType.Type == "" {
panic(fmt.Errorf("builtin type %q is empty", fieldName))
}
defaultType, found := DefaultTypes.GetByType(builtinType.Type)
if !found {
panic(errors.New("missing default type for builtin type: " + builtinType.Type))
panic(fmt.Errorf("missing default type for field builtin type: %q", fieldName))
}
f.Set(reflect.ValueOf(defaultType))
}
}
func init() {
DefaultContentTypes = ContentTypes{
HTML: Builtin.HTMLType,
Markdown: Builtin.MarkdownType,
AsciiDoc: Builtin.AsciiDocType,
Pandoc: Builtin.PandocType,
ReStructuredText: Builtin.ReStructuredTextType,
EmacsOrgMode: Builtin.EmacsOrgModeType,
}
DefaultContentTypes.init()
}
var DefaultContentTypes ContentTypes
// ContentTypes holds the media types that are considered content in Hugo.
type ContentTypes struct {
HTML Type
Markdown Type
AsciiDoc Type
Pandoc Type
ReStructuredText Type
EmacsOrgMode Type
// Created in init().
types Types
extensionSet map[string]bool
}
func (t *ContentTypes) init() {
t.types = Types{t.HTML, t.Markdown, t.AsciiDoc, t.Pandoc, t.ReStructuredText, t.EmacsOrgMode}
t.extensionSet = make(map[string]bool)
for _, mt := range t.types {
for _, suffix := range mt.Suffixes() {
t.extensionSet[suffix] = true
}
}
}
func (t ContentTypes) IsContentSuffix(suffix string) bool {
return t.extensionSet[suffix]
}
// IsContentFile returns whether the given filename is a content file.
func (t ContentTypes) IsContentFile(filename string) bool {
return t.IsContentSuffix(strings.TrimPrefix(filepath.Ext(filename), "."))
}
// IsIndexContentFile returns whether the given filename is an index content file.
func (t ContentTypes) IsIndexContentFile(filename string) bool {
if !t.IsContentFile(filename) {
return false
}
base := filepath.Base(filename)
return strings.HasPrefix(base, "index.") || strings.HasPrefix(base, "_index.")
}
// IsHTMLSuffix returns whether the given suffix is a HTML media type.
func (t ContentTypes) IsHTMLSuffix(suffix string) bool {
for _, s := range t.HTML.Suffixes() {
if s == suffix {
return true
}
}
return false
}
// Types is a slice of media types.
func (t ContentTypes) Types() Types {
return t.types
}
// FromTypes creates a new ContentTypes updated with the values from the given Types.
func (t ContentTypes) FromTypes(types Types) ContentTypes {
if tt, ok := types.GetByType(t.HTML.Type); ok {
t.HTML = tt
}
if tt, ok := types.GetByType(t.Markdown.Type); ok {
t.Markdown = tt
}
if tt, ok := types.GetByType(t.AsciiDoc.Type); ok {
t.AsciiDoc = tt
}
if tt, ok := types.GetByType(t.Pandoc.Type); ok {
t.Pandoc = tt
}
if tt, ok := types.GetByType(t.ReStructuredText.Type); ok {
t.ReStructuredText = tt
}
if tt, ok := types.GetByType(t.EmacsOrgMode.Type); ok {
t.EmacsOrgMode = tt
}
t.init()
return t
}
// Hold the configuration for a given media type.
type MediaTypeConfig struct {
// The file suffixes used for this media type.
@@ -105,3 +216,10 @@ func DecodeTypes(in map[string]any) (*config.ConfigNamespace[map[string]MediaTyp
}
return ns, nil
}
// TODO(bep) get rid of this.
var DefaultPathParser = &paths.PathParser{
IsContentExt: func(ext string) bool {
return DefaultContentTypes.IsContentSuffix(ext)
},
}
+12 -7
View File
@@ -114,7 +114,7 @@ func TestDefaultTypes(t *testing.T) {
tp Type
expectedMainType string
expectedSubType string
expectedSuffix string
expectedSuffixes string
expectedType string
expectedString string
}{
@@ -122,29 +122,34 @@ func TestDefaultTypes(t *testing.T) {
{Builtin.CSSType, "text", "css", "css", "text/css", "text/css"},
{Builtin.SCSSType, "text", "x-scss", "scss", "text/x-scss", "text/x-scss"},
{Builtin.CSVType, "text", "csv", "csv", "text/csv", "text/csv"},
{Builtin.HTMLType, "text", "html", "html", "text/html", "text/html"},
{Builtin.JavascriptType, "text", "javascript", "js", "text/javascript", "text/javascript"},
{Builtin.HTMLType, "text", "html", "html,htm", "text/html", "text/html"},
{Builtin.MarkdownType, "text", "markdown", "md,mdown,markdown", "text/markdown", "text/markdown"},
{Builtin.EmacsOrgModeType, "text", "org", "org", "text/org", "text/org"},
{Builtin.PandocType, "text", "pandoc", "pandoc,pdc", "text/pandoc", "text/pandoc"},
{Builtin.ReStructuredTextType, "text", "rst", "rst", "text/rst", "text/rst"},
{Builtin.AsciiDocType, "text", "asciidoc", "adoc,asciidoc,ad", "text/asciidoc", "text/asciidoc"},
{Builtin.JavascriptType, "text", "javascript", "js,jsm,mjs", "text/javascript", "text/javascript"},
{Builtin.TypeScriptType, "text", "typescript", "ts", "text/typescript", "text/typescript"},
{Builtin.TSXType, "text", "tsx", "tsx", "text/tsx", "text/tsx"},
{Builtin.JSXType, "text", "jsx", "jsx", "text/jsx", "text/jsx"},
{Builtin.JSONType, "application", "json", "json", "application/json", "application/json"},
{Builtin.RSSType, "application", "rss", "xml", "application/rss+xml", "application/rss+xml"},
{Builtin.RSSType, "application", "rss", "xml,rss", "application/rss+xml", "application/rss+xml"},
{Builtin.SVGType, "image", "svg", "svg", "image/svg+xml", "image/svg+xml"},
{Builtin.TextType, "text", "plain", "txt", "text/plain", "text/plain"},
{Builtin.XMLType, "application", "xml", "xml", "application/xml", "application/xml"},
{Builtin.TOMLType, "application", "toml", "toml", "application/toml", "application/toml"},
{Builtin.YAMLType, "application", "yaml", "yaml", "application/yaml", "application/yaml"},
{Builtin.YAMLType, "application", "yaml", "yaml,yml", "application/yaml", "application/yaml"},
{Builtin.PDFType, "application", "pdf", "pdf", "application/pdf", "application/pdf"},
{Builtin.TrueTypeFontType, "font", "ttf", "ttf", "font/ttf", "font/ttf"},
{Builtin.OpenTypeFontType, "font", "otf", "otf", "font/otf", "font/otf"},
} {
c.Assert(test.tp.MainType, qt.Equals, test.expectedMainType)
c.Assert(test.tp.SubType, qt.Equals, test.expectedSubType)
c.Assert(test.tp.SuffixesCSV, qt.Equals, test.expectedSuffixes)
c.Assert(test.tp.Type, qt.Equals, test.expectedType)
c.Assert(test.tp.String(), qt.Equals, test.expectedString)
}
c.Assert(len(DefaultTypes), qt.Equals, 36)
c.Assert(len(DefaultTypes), qt.Equals, 40)
}
+52 -3
View File
@@ -117,13 +117,16 @@ func FromContent(types Types, extensionHints []string, content []byte) Type {
return m
}
// FromStringAndExt creates a Type from a MIME string and a given extension.
func FromStringAndExt(t, ext string) (Type, error) {
// FromStringAndExt creates a Type from a MIME string and a given extensions
func FromStringAndExt(t string, ext ...string) (Type, error) {
tp, err := FromString(t)
if err != nil {
return tp, err
}
tp.SuffixesCSV = strings.TrimPrefix(ext, ".")
for i, e := range ext {
ext[i] = strings.TrimPrefix(e, ".")
}
tp.SuffixesCSV = strings.Join(ext, ",")
tp.Delimiter = DefaultDelimiter
tp.init()
return tp, nil
@@ -187,6 +190,16 @@ func (m Type) IsText() bool {
return false
}
// For internal use.
func (m Type) IsHTML() bool {
return m.SubType == Builtin.HTMLType.SubType
}
// For internal use.
func (m Type) IsMarkdown() bool {
return m.SubType == Builtin.MarkdownType.SubType
}
func InitMediaType(m *Type) {
m.init()
}
@@ -221,6 +234,26 @@ func (t Types) Len() int { return len(t) }
func (t Types) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
func (t Types) Less(i, j int) bool { return t[i].Type < t[j].Type }
// GetBestMatch returns the best match for the given media type string.
func (t Types) GetBestMatch(s string) (Type, bool) {
// First try an exact match.
if mt, found := t.GetByType(s); found {
return mt, true
}
// Try main type.
if mt, found := t.GetBySubType(s); found {
return mt, true
}
// Try extension.
if mt, _, found := t.GetFirstBySuffix(s); found {
return mt, true
}
return Type{}, false
}
// GetByType returns a media type for tp.
func (t Types) GetByType(tp string) (Type, bool) {
for _, tt := range t {
@@ -324,6 +357,22 @@ func (t Types) GetByMainSubType(mainType, subType string) (tp Type, found bool)
return
}
// GetBySubType gets a media type given a sub type e.g. "plain".
func (t Types) GetBySubType(subType string) (tp Type, found bool) {
for _, tt := range t {
if strings.EqualFold(subType, tt.SubType) {
if found {
// ambiguous
found = false
return
}
tp = tt
found = true
}
}
return
}
// IsZero reports whether this Type represents a zero value.
// For internal use.
func (m Type) IsZero() bool {
+10 -2
View File
@@ -115,10 +115,10 @@ func TestFromTypeString(t *testing.T) {
func TestFromStringAndExt(t *testing.T) {
c := qt.New(t)
f, err := FromStringAndExt("text/html", "html")
f, err := FromStringAndExt("text/html", "html", "htm")
c.Assert(err, qt.IsNil)
c.Assert(f, qt.Equals, Builtin.HTMLType)
f, err = FromStringAndExt("text/html", ".html")
f, err = FromStringAndExt("text/html", ".html", ".htm")
c.Assert(err, qt.IsNil)
c.Assert(f, qt.Equals, Builtin.HTMLType)
}
@@ -214,3 +214,11 @@ func BenchmarkTypeOps(b *testing.B) {
}
}
func TestIsContentFile(t *testing.T) {
c := qt.New(t)
c.Assert(DefaultContentTypes.IsContentFile(filepath.FromSlash("my/file.md")), qt.Equals, true)
c.Assert(DefaultContentTypes.IsContentFile(filepath.FromSlash("my/file.ad")), qt.Equals, true)
c.Assert(DefaultContentTypes.IsContentFile(filepath.FromSlash("textfile.txt")), qt.Equals, false)
}
+4 -1
View File
@@ -261,7 +261,10 @@ func (c *collector) add(owner *moduleAdapter, moduleImport Import) (*moduleAdapt
// This will select the latest release-version (not beta etc.).
versionQuery = "upgrade"
}
if err := c.Get(fmt.Sprintf("%s@%s", modulePath, versionQuery)); err != nil {
// Note that we cannot use c.Get for this, as that may
// trigger a new module collection and potentially create a infinite loop.
if err := c.get(fmt.Sprintf("%s@%s", modulePath, versionQuery)); err != nil {
return nil, err
}
if err := c.loadModules(); err != nil {
-1
View File
@@ -104,7 +104,6 @@ func InterfaceToFrontMatter(in any, format metadecoders.Format, w io.Writer) err
}
err = InterfaceToConfig(in, format, w)
if err != nil {
return err
}
+1 -1
View File
@@ -128,7 +128,7 @@ func (e *errorResource) Exif() *exif.ExifInfo {
panic(e.ResourceError)
}
func (e *errorResource) Colors() ([]string, error) {
func (e *errorResource) Colors() ([]images.Color, error) {
panic(e.ResourceError)
}
+3 -3
View File
@@ -67,7 +67,7 @@ type imageResource struct {
meta *imageMeta
dominantColorInit sync.Once
dominantColors []string
dominantColors []images.Color
baseResource
}
@@ -143,7 +143,7 @@ func (i *imageResource) getExif() *exif.ExifInfo {
// Colors returns a slice of the most dominant colors in an image
// using a simple histogram method.
func (i *imageResource) Colors() ([]string, error) {
func (i *imageResource) Colors() ([]images.Color, error) {
var err error
i.dominantColorInit.Do(func() {
var img image.Image
@@ -153,7 +153,7 @@ func (i *imageResource) Colors() ([]string, error) {
}
colors := color_extractor.ExtractColors(img)
for _, c := range colors {
i.dominantColors = append(i.dominantColors, images.ColorToHexString(c))
i.dominantColors = append(i.dominantColors, images.ColorGoToColor(c))
}
})
return i.dominantColors, nil
+27 -2
View File
@@ -85,9 +85,16 @@ func TestImageTransformBasic(t *testing.T) {
assertWidthHeight(c, img, w, h)
}
colors, err := image.Colors()
gotColors, err := image.Colors()
c.Assert(err, qt.IsNil)
c.Assert(colors, qt.DeepEquals, []string{"#2d2f33", "#a49e93", "#d39e59", "#a76936", "#737a84", "#7c838b"})
expectedColors := images.HexStringsToColors("#2d2f33", "#a49e93", "#d39e59", "#a76936", "#737a84", "#7c838b")
c.Assert(len(gotColors), qt.Equals, len(expectedColors))
for i := range gotColors {
c1, c2 := gotColors[i], expectedColors[i]
c.Assert(c1.ColorHex(), qt.Equals, c2.ColorHex())
c.Assert(c1.ColorGo(), qt.DeepEquals, c2.ColorGo())
c.Assert(c1.Luminance(), qt.Equals, c2.Luminance())
}
c.Assert(image.RelPermalink(), qt.Equals, "/a/sunset.jpg")
c.Assert(image.ResourceType(), qt.Equals, "image")
@@ -445,6 +452,24 @@ func TestImageExif(t *testing.T) {
getAndCheckExif(c, image)
}
func TestImageColorsLuminance(t *testing.T) {
c := qt.New(t)
_, image := fetchSunset(c)
c.Assert(image, qt.Not(qt.IsNil))
colors, err := image.Colors()
c.Assert(err, qt.IsNil)
c.Assert(len(colors), qt.Equals, 6)
var prevLuminance float64
for i, color := range colors {
luminance := color.Luminance()
c.Assert(err, qt.IsNil)
c.Assert(luminance > 0, qt.IsTrue)
c.Assert(luminance, qt.Not(qt.Equals), prevLuminance, qt.Commentf("i=%d", i))
prevLuminance = luminance
}
}
func BenchmarkImageExif(b *testing.B) {
getImages := func(c *qt.C, b *testing.B, fs afero.Fs) []images.ImageResource {
spec := newTestResourceSpec(specDescriptor{fs: fs, c: c})
+116 -4
View File
@@ -16,10 +16,76 @@ package images
import (
"encoding/hex"
"fmt"
"hash/fnv"
"image/color"
"math"
"strings"
"github.com/gohugoio/hugo/common/hstrings"
)
type colorGoProvider interface {
ColorGo() color.Color
}
type Color struct {
// The color.
color color.Color
// The color prefixed with a #.
hex string
// The relative luminance of the color.
luminance float64
}
// Luminance as defined by w3.org.
// See https://www.w3.org/TR/WCAG21/#dfn-relative-luminance
func (c Color) Luminance() float64 {
return c.luminance
}
// ColorGo returns the color as a color.Color.
// For internal use only.
func (c Color) ColorGo() color.Color {
return c.color
}
// ColorHex returns the color as a hex string prefixed with a #.
func (c Color) ColorHex() string {
return c.hex
}
// String returns the color as a hex string prefixed with a #.
func (c Color) String() string {
return c.hex
}
// For hashstructure. This struct is used in template func options
// that needs to be able to hash a Color.
// For internal use only.
func (c Color) Hash() (uint64, error) {
h := fnv.New64a()
h.Write([]byte(c.hex))
return h.Sum64(), nil
}
func (c *Color) init() error {
c.hex = ColorGoToHexString(c.color)
r, g, b, _ := c.color.RGBA()
c.luminance = 0.2126*c.toSRGB(uint8(r)) + 0.7152*c.toSRGB(uint8(g)) + 0.0722*c.toSRGB(uint8(b))
return nil
}
func (c Color) toSRGB(i uint8) float64 {
v := float64(i) / 255
if v <= 0.04045 {
return v / 12.92
} else {
return math.Pow((v+0.055)/1.055, 2.4)
}
}
// AddColorToPalette adds c as the first color in p if not already there.
// Note that it does no additional checks, so callers must make sure
// that the palette is valid for the relevant format.
@@ -45,14 +111,60 @@ func ReplaceColorInPalette(c color.Color, p color.Palette) {
p[p.Index(c)] = c
}
// ColorToHexString converts a color to a hex string.
func ColorToHexString(c color.Color) string {
// ColorGoToHexString converts a color.Color to a hex string.
func ColorGoToHexString(c color.Color) string {
r, g, b, a := c.RGBA()
rgba := color.RGBA{uint8(r), uint8(g), uint8(b), uint8(a)}
return fmt.Sprintf("#%.2x%.2x%.2x", rgba.R, rgba.G, rgba.B)
if rgba.A == 0xff {
return fmt.Sprintf("#%.2x%.2x%.2x", rgba.R, rgba.G, rgba.B)
}
return fmt.Sprintf("#%.2x%.2x%.2x%.2x", rgba.R, rgba.G, rgba.B, rgba.A)
}
func hexStringToColor(s string) (color.Color, error) {
// ColorGoToColor converts a color.Color to a Color.
func ColorGoToColor(c color.Color) Color {
cc := Color{color: c}
if err := cc.init(); err != nil {
panic(err)
}
return cc
}
func hexStringToColor(s string) Color {
c, err := hexStringToColorGo(s)
if err != nil {
panic(err)
}
return ColorGoToColor(c)
}
// HexStringsToColors converts a slice of hex strings to a slice of Colors.
func HexStringsToColors(s ...string) []Color {
var colors []Color
for _, v := range s {
colors = append(colors, hexStringToColor(v))
}
return colors
}
func toColorGo(v any) (color.Color, bool, error) {
switch vv := v.(type) {
case colorGoProvider:
return vv.ColorGo(), true, nil
default:
s, ok := hstrings.ToString(v)
if !ok {
return nil, false, nil
}
c, err := hexStringToColorGo(s)
if err != nil {
return nil, false, err
}
return c, true, nil
}
}
func hexStringToColorGo(s string) (color.Color, error) {
s = strings.TrimPrefix(s, "#")
if len(s) != 3 && len(s) != 4 && len(s) != 6 && len(s) != 8 {
+20 -6
View File
@@ -18,6 +18,7 @@ import (
"testing"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/htesting/hqt"
)
func TestHexStringToColor(t *testing.T) {
@@ -46,7 +47,7 @@ func TestHexStringToColor(t *testing.T) {
c.Run(test.arg, func(c *qt.C) {
c.Parallel()
result, err := hexStringToColor(test.arg)
result, err := hexStringToColorGo(test.arg)
if b, ok := test.expect.(bool); ok && !b {
c.Assert(err, qt.Not(qt.IsNil))
@@ -70,13 +71,18 @@ func TestColorToHexString(t *testing.T) {
{color.White, "#ffffff"},
{color.Black, "#000000"},
{color.RGBA{R: 0x42, G: 0x87, B: 0xf5, A: 0xff}, "#4287f5"},
// 50% opacity.
// Note that the .Colors (dominant colors) received from the Image resource
// will always have an alpha value of 0xff.
{color.RGBA{R: 0x42, G: 0x87, B: 0xf5, A: 0x80}, "#4287f580"},
} {
test := test
c.Run(test.expect, func(c *qt.C) {
c.Parallel()
result := ColorToHexString(test.arg)
result := ColorGoToHexString(test.arg)
c.Assert(result, qt.Equals, test.expect)
})
@@ -91,9 +97,9 @@ func TestAddColorToPalette(t *testing.T) {
c.Assert(AddColorToPalette(color.White, palette), qt.HasLen, 2)
blue1, _ := hexStringToColor("34c3eb")
blue2, _ := hexStringToColor("34c3eb")
white, _ := hexStringToColor("fff")
blue1, _ := hexStringToColorGo("34c3eb")
blue2, _ := hexStringToColorGo("34c3eb")
white, _ := hexStringToColorGo("fff")
c.Assert(AddColorToPalette(white, palette), qt.HasLen, 2)
c.Assert(AddColorToPalette(blue1, palette), qt.HasLen, 3)
@@ -104,10 +110,18 @@ func TestReplaceColorInPalette(t *testing.T) {
c := qt.New(t)
palette := color.Palette{color.White, color.Black}
offWhite, _ := hexStringToColor("fcfcfc")
offWhite, _ := hexStringToColorGo("fcfcfc")
ReplaceColorInPalette(offWhite, palette)
c.Assert(palette, qt.HasLen, 2)
c.Assert(palette[0], qt.Equals, offWhite)
}
func TestColorLuminance(t *testing.T) {
c := qt.New(t)
c.Assert(hexStringToColor("#000000").Luminance(), hqt.IsSameFloat64, 0.0)
c.Assert(hexStringToColor("#768a9a").Luminance(), hqt.IsSameFloat64, 0.24361603589088263)
c.Assert(hexStringToColor("#d5bc9f").Luminance(), hqt.IsSameFloat64, 0.5261577672685374)
c.Assert(hexStringToColor("#ffffff").Luminance(), hqt.IsSameFloat64, 1.0)
}
+3 -3
View File
@@ -171,7 +171,7 @@ func DecodeConfig(in map[string]any) (*config.ConfigNamespace[ImagingConfig, Ima
return i, nil, err
}
i.BgColor, err = hexStringToColor(i.Imaging.BgColor)
i.BgColor, err = hexStringToColorGo(i.Imaging.BgColor)
if err != nil {
return i, nil, err
}
@@ -230,7 +230,7 @@ func DecodeImageConfig(action string, options []string, defaults *config.ConfigN
c.Hint = hint
} else if part[0] == '#' {
c.BgColorStr = part[1:]
c.BgColor, err = hexStringToColor(c.BgColorStr)
c.BgColor, err = hexStringToColorGo(c.BgColorStr)
if err != nil {
return c, err
}
@@ -424,7 +424,7 @@ type ImagingConfigInternal struct {
func (i *ImagingConfigInternal) Compile(externalCfg *ImagingConfig) error {
var err error
i.BgColor, err = hexStringToColor(externalCfg.BgColor)
i.BgColor, err = hexStringToColorGo(externalCfg.BgColor)
if err != nil {
return err
}

Some files were not shown because too many files have changed in this diff Show More