mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-30 10:12:40 +00:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 56d3114199 | |||
| 6bde2a5f88 | |||
| f738669a4d | |||
| b63f24adc7 | |||
| 8fb933550f | |||
| a3684c8361 | |||
| 53a8de21b8 | |||
| 37609262dc | |||
| 2b5c335e93 | |||
| 12a28ef773 | |||
| 4ded32d077 | |||
| 1ecd0596a3 | |||
| 371246de25 | |||
| aa0f66b290 | |||
| 47d00202e7 | |||
| 261d7a03ae | |||
| 445283a593 | |||
| 5f667f8796 | |||
| e1becf1dfe | |||
| 7792392a6f | |||
| 46484bff84 | |||
| 0e5a26057c | |||
| c8f850ae17 | |||
| 702b1e8b81 | |||
| 73d32e7737 | |||
| 777534b2a4 | |||
| c054cba042 | |||
| c9777473d1 | |||
| 01008ba512 | |||
| 53c0ddfcb4 | |||
| 133eeafeb4 | |||
| 2168c5b125 |
@@ -4,7 +4,7 @@ parameters:
|
||||
defaults: &defaults
|
||||
resource_class: large
|
||||
docker:
|
||||
- image: bepsays/ci-hugoreleaser:1.22200.20501
|
||||
- image: bepsays/ci-hugoreleaser:1.22300.20000
|
||||
environment: &buildenv
|
||||
GOMODCACHE: /root/project/gomodcache
|
||||
version: 2
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
environment:
|
||||
<<: [*buildenv]
|
||||
docker:
|
||||
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22200.20501
|
||||
- image: bepsays/ci-hugoreleaser-linux-arm64:1.22300.20000
|
||||
steps:
|
||||
- *restore-cache
|
||||
- &attach-workspace
|
||||
|
||||
@@ -16,8 +16,8 @@ jobs:
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.21.x, 1.22.x]
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
go-version: [1.22.x, 1.23.x]
|
||||
os: [ubuntu-latest, windows-latest] # macos disabled for now because of disk space issues.
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- if: matrix.os == 'ubuntu-latest'
|
||||
|
||||
+26
-1
@@ -14,6 +14,7 @@
|
||||
package hugo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"os"
|
||||
@@ -29,6 +30,7 @@ import (
|
||||
"github.com/mitchellh/mapstructure"
|
||||
|
||||
"github.com/bep/godartsass/v2"
|
||||
"github.com/gohugoio/hugo/common/hcontext"
|
||||
"github.com/gohugoio/hugo/common/hexec"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/hugofs/files"
|
||||
@@ -69,6 +71,9 @@ type HugoInfo struct {
|
||||
|
||||
conf ConfigProvider
|
||||
deps []*Dependency
|
||||
|
||||
// Context gives access to some of the context scoped variables.
|
||||
Context Context
|
||||
}
|
||||
|
||||
// Version returns the current version as a comparable version string.
|
||||
@@ -127,6 +132,26 @@ func (i HugoInfo) IsMultilingual() bool {
|
||||
return i.conf.IsMultilingual()
|
||||
}
|
||||
|
||||
type contextKey string
|
||||
|
||||
var markupScope = hcontext.NewContextDispatcher[string](contextKey("markupScope"))
|
||||
|
||||
type Context struct{}
|
||||
|
||||
func (c Context) MarkupScope(ctx context.Context) string {
|
||||
return GetMarkupScope(ctx)
|
||||
}
|
||||
|
||||
// SetMarkupScope sets the markup scope in the context.
|
||||
func SetMarkupScope(ctx context.Context, s string) context.Context {
|
||||
return markupScope.Set(ctx, s)
|
||||
}
|
||||
|
||||
// GetMarkupScope gets the markup scope from the context.
|
||||
func GetMarkupScope(ctx context.Context) string {
|
||||
return markupScope.Get(ctx)
|
||||
}
|
||||
|
||||
// ConfigProvider represents the config options that are relevant for HugoInfo.
|
||||
type ConfigProvider interface {
|
||||
Environment() string
|
||||
@@ -276,7 +301,7 @@ func GetDependencyListNonGo() []string {
|
||||
if IsExtended {
|
||||
deps = append(
|
||||
deps,
|
||||
formatDep("github.com/sass/libsass", "3.6.5"),
|
||||
formatDep("github.com/sass/libsass", "3.6.6"),
|
||||
formatDep("github.com/webmproject/libwebp", "v1.3.2"),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
package hugo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
@@ -64,6 +65,19 @@ func TestDeprecationLogLevelFromVersion(t *testing.T) {
|
||||
c.Assert(deprecationLogLevelFromVersion(ver.String()), qt.Equals, logg.LevelError)
|
||||
}
|
||||
|
||||
func TestMarkupScope(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
conf := testConfig{environment: "production", workingDir: "/mywork", running: false}
|
||||
info := NewInfo(conf, nil)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
ctx = SetMarkupScope(ctx, "foo")
|
||||
|
||||
c.Assert(info.Context.MarkupScope(ctx), qt.Equals, "foo")
|
||||
}
|
||||
|
||||
type testConfig struct {
|
||||
environment string
|
||||
running bool
|
||||
|
||||
@@ -17,7 +17,7 @@ package hugo
|
||||
// This should be the only one.
|
||||
var CurrentVersion = Version{
|
||||
Major: 0,
|
||||
Minor: 133,
|
||||
Minor: 134,
|
||||
PatchLevel: 0,
|
||||
Suffix: "-DEV",
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
|
||||
} else {
|
||||
high = len(p.s)
|
||||
}
|
||||
id := types.LowHigh{Low: i + 1, High: high}
|
||||
id := types.LowHigh[string]{Low: i + 1, High: high}
|
||||
if len(p.identifiers) == 0 {
|
||||
p.identifiers = append(p.identifiers, id)
|
||||
} else if len(p.identifiers) == 1 {
|
||||
@@ -260,7 +260,7 @@ type Path struct {
|
||||
component string
|
||||
bundleType PathType
|
||||
|
||||
identifiers []types.LowHigh
|
||||
identifiers []types.LowHigh[string]
|
||||
|
||||
posIdentifierLanguage int
|
||||
disabled bool
|
||||
|
||||
@@ -13,8 +13,22 @@
|
||||
|
||||
package hstring
|
||||
|
||||
type RenderedString string
|
||||
import (
|
||||
"html/template"
|
||||
|
||||
func (s RenderedString) String() string {
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
)
|
||||
|
||||
var _ types.PrintableValueProvider = RenderedHTML("")
|
||||
|
||||
// RenderedHTML is a string that represents rendered HTML.
|
||||
// When printed in templates it will be rendered as template.HTML and considered safe.
|
||||
type RenderedHTML string
|
||||
|
||||
func (s RenderedHTML) String() string {
|
||||
return string(s)
|
||||
}
|
||||
|
||||
func (s RenderedHTML) PrintableValue() any {
|
||||
return template.HTML(s)
|
||||
}
|
||||
|
||||
@@ -25,6 +25,6 @@ func TestRenderedString(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
// Validate that it will behave like a string in Hugo settings.
|
||||
c.Assert(cast.ToString(RenderedString("Hugo")), qt.Equals, "Hugo")
|
||||
c.Assert(template.HTML(RenderedString("Hugo")), qt.Equals, template.HTML("Hugo"))
|
||||
c.Assert(cast.ToString(RenderedHTML("Hugo")), qt.Equals, "Hugo")
|
||||
c.Assert(template.HTML(RenderedHTML("Hugo")), qt.Equals, template.HTML("Hugo"))
|
||||
}
|
||||
|
||||
+10
-2
@@ -107,12 +107,20 @@ func Unwrapv(v any) any {
|
||||
return v
|
||||
}
|
||||
|
||||
// LowHigh is typically used to represent a slice boundary.
|
||||
type LowHigh struct {
|
||||
// LowHigh represents a byte or slice boundary.
|
||||
type LowHigh[S ~[]byte | string] struct {
|
||||
Low int
|
||||
High int
|
||||
}
|
||||
|
||||
func (l LowHigh[S]) IsZero() bool {
|
||||
return l.Low < 0 || (l.Low == 0 && l.High == 0)
|
||||
}
|
||||
|
||||
func (l LowHigh[S]) Value(source S) S {
|
||||
return source[l.Low:l.High]
|
||||
}
|
||||
|
||||
// This is only used for debugging purposes.
|
||||
var InvocationCounter atomic.Int64
|
||||
|
||||
|
||||
@@ -27,3 +27,25 @@ func TestKeyValues(t *testing.T) {
|
||||
c.Assert(kv.KeyString(), qt.Equals, "key")
|
||||
c.Assert(kv.Values, qt.DeepEquals, []any{"a1", "a2"})
|
||||
}
|
||||
|
||||
func TestLowHigh(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
lh := LowHigh[string]{
|
||||
Low: 2,
|
||||
High: 10,
|
||||
}
|
||||
|
||||
s := "abcdefghijklmnopqrstuvwxyz"
|
||||
c.Assert(lh.IsZero(), qt.IsFalse)
|
||||
c.Assert(lh.Value(s), qt.Equals, "cdefghij")
|
||||
|
||||
lhb := LowHigh[[]byte]{
|
||||
Low: 2,
|
||||
High: 10,
|
||||
}
|
||||
|
||||
sb := []byte(s)
|
||||
c.Assert(lhb.IsZero(), qt.IsFalse)
|
||||
c.Assert(lhb.Value(sb), qt.DeepEquals, []byte("cdefghij"))
|
||||
}
|
||||
|
||||
@@ -182,6 +182,9 @@ type Config struct {
|
||||
// Pagination configuration.
|
||||
Pagination config.Pagination `mapstructure:"-"`
|
||||
|
||||
// Page configuration.
|
||||
Page config.PageConfig `mapstructure:"-"`
|
||||
|
||||
// Privacy configuration.
|
||||
Privacy privacy.Config `mapstructure:"-"`
|
||||
|
||||
@@ -378,12 +381,12 @@ func (c *Config) CompileConfig(logger loggers.Logger) error {
|
||||
|
||||
// Legacy paginate values.
|
||||
if c.Paginate != 0 {
|
||||
hugo.Deprecate("site config key paginate", "Use paginator.pagerSize instead.", "v0.128.0")
|
||||
hugo.Deprecate("site config key paginate", "Use pagination.pagerSize instead.", "v0.128.0")
|
||||
c.Pagination.PagerSize = c.Paginate
|
||||
}
|
||||
|
||||
if c.PaginatePath != "" {
|
||||
hugo.Deprecate("site config key paginatePath", "Use paginator.path instead.", "v0.128.0")
|
||||
hugo.Deprecate("site config key paginatePath", "Use pagination.path instead.", "v0.128.0")
|
||||
c.Pagination.Path = c.PaginatePath
|
||||
}
|
||||
|
||||
|
||||
@@ -327,6 +327,25 @@ var allDecoderSetups = map[string]decodeWeight{
|
||||
return err
|
||||
},
|
||||
},
|
||||
"page": {
|
||||
key: "page",
|
||||
decode: func(d decodeWeight, p decodeConfig) error {
|
||||
p.c.Page = config.PageConfig{
|
||||
NextPrevSortOrder: "desc",
|
||||
NextPrevInSectionSortOrder: "desc",
|
||||
}
|
||||
if p.p.IsSet(d.key) {
|
||||
if err := mapstructure.WeakDecode(p.p.Get(d.key), &p.c.Page); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
getCompiler: func(c *Config) configCompiler {
|
||||
return &c.Page
|
||||
},
|
||||
},
|
||||
"pagination": {
|
||||
key: "pagination",
|
||||
decode: func(d decodeWeight, p decodeConfig) error {
|
||||
|
||||
@@ -422,3 +422,18 @@ type Pagination struct {
|
||||
// Whether to disable generation of alias for the first pagination page.
|
||||
DisableAliases bool
|
||||
}
|
||||
|
||||
// PageConfig configures the behavior of pages.
|
||||
type PageConfig struct {
|
||||
// Sort order for Page.Next and Page.Prev. Default "desc" (the default page sort order in Hugo).
|
||||
NextPrevSortOrder string
|
||||
|
||||
// Sort order for Page.NextInSection and Page.PrevInSection. Default "desc".
|
||||
NextPrevInSectionSortOrder string
|
||||
}
|
||||
|
||||
func (c *PageConfig) CompileConfig(loggers.Logger) error {
|
||||
c.NextPrevInSectionSortOrder = strings.ToLower(c.NextPrevInSectionSortOrder)
|
||||
c.NextPrevSortOrder = strings.ToLower(c.NextPrevSortOrder)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ or site.Language.LanguageCode }}" dir="{{ or site.Language.LanguageDirection `ltr` }}">
|
||||
<html lang="{{ site.Language.LanguageCode }}" dir="{{ or site.Language.LanguageDirection `ltr` }}">
|
||||
<head>
|
||||
{{ partial "head.html" . }}
|
||||
</head>
|
||||
|
||||
+12
-7
@@ -1600,6 +1600,9 @@ config:
|
||||
term:
|
||||
- html
|
||||
- rss
|
||||
page:
|
||||
nextPrevInSectionSortOrder: desc
|
||||
nextPrevSortOrder: desc
|
||||
paginate: 0
|
||||
paginatePath: ""
|
||||
pagination:
|
||||
@@ -1784,6 +1787,8 @@ config_helpers:
|
||||
_merge: shallow
|
||||
outputs:
|
||||
_merge: none
|
||||
page:
|
||||
_merge: none
|
||||
pagination:
|
||||
_merge: none
|
||||
params:
|
||||
@@ -1860,7 +1865,7 @@ output:
|
||||
- layouts/_default/demolayout-baseof.html
|
||||
- layouts/_default/single-baseof.html
|
||||
- layouts/_default/baseof.html
|
||||
- Example: AMP single page
|
||||
- Example: AMP single page in "posts" section
|
||||
Kind: page
|
||||
OutputFormat: amp
|
||||
Suffix: html
|
||||
@@ -1869,17 +1874,17 @@ output:
|
||||
- layouts/posts/single.html
|
||||
- layouts/_default/single.amp.html
|
||||
- layouts/_default/single.html
|
||||
- Example: AMP single page, French language
|
||||
- Example: AMP single page in "posts" section, French language
|
||||
Kind: page
|
||||
OutputFormat: html
|
||||
OutputFormat: amp
|
||||
Suffix: html
|
||||
Template Lookup Order:
|
||||
- layouts/posts/single.fr.html.html
|
||||
- layouts/posts/single.html.html
|
||||
- layouts/posts/single.fr.amp.html
|
||||
- layouts/posts/single.amp.html
|
||||
- layouts/posts/single.fr.html
|
||||
- layouts/posts/single.html
|
||||
- layouts/_default/single.fr.html.html
|
||||
- layouts/_default/single.html.html
|
||||
- layouts/_default/single.fr.amp.html
|
||||
- layouts/_default/single.amp.html
|
||||
- layouts/_default/single.fr.html
|
||||
- layouts/_default/single.html
|
||||
- Example: Home page
|
||||
|
||||
@@ -12,10 +12,10 @@ require (
|
||||
github.com/bep/goat v0.5.0
|
||||
github.com/bep/godartsass v1.2.0
|
||||
github.com/bep/godartsass/v2 v2.1.0
|
||||
github.com/bep/golibsass v1.1.1
|
||||
github.com/bep/golibsass v1.2.0
|
||||
github.com/bep/gowebp v0.3.0
|
||||
github.com/bep/helpers v0.4.0
|
||||
github.com/bep/imagemeta v0.8.0
|
||||
github.com/bep/imagemeta v0.8.1
|
||||
github.com/bep/lazycache v0.4.0
|
||||
github.com/bep/logg v0.4.0
|
||||
github.com/bep/mclib v1.20400.20402
|
||||
@@ -27,7 +27,7 @@ require (
|
||||
github.com/cli/safeexec v1.0.1
|
||||
github.com/disintegration/gift v1.2.1
|
||||
github.com/dustin/go-humanize v1.0.1
|
||||
github.com/evanw/esbuild v0.23.0
|
||||
github.com/evanw/esbuild v0.23.1
|
||||
github.com/fatih/color v1.17.0
|
||||
github.com/fortytw2/leaktest v1.3.0
|
||||
github.com/frankban/quicktest v1.14.6
|
||||
@@ -59,7 +59,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.2
|
||||
github.com/pelletier/go-toml/v2 v2.2.3
|
||||
github.com/rogpeppe/go-internal v1.12.0
|
||||
github.com/sanity-io/litter v1.5.5
|
||||
github.com/spf13/afero v1.11.0
|
||||
@@ -69,11 +69,11 @@ require (
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/tdewolff/minify/v2 v2.20.37
|
||||
github.com/tdewolff/parse/v2 v2.7.15
|
||||
github.com/tetratelabs/wazero v1.7.4-0.20240805170331-2b12e189eeec
|
||||
github.com/tetratelabs/wazero v1.8.0
|
||||
github.com/yuin/goldmark v1.7.4
|
||||
github.com/yuin/goldmark-emoji v1.0.3
|
||||
go.uber.org/automaxprocs v1.5.3
|
||||
gocloud.dev v0.38.0
|
||||
gocloud.dev v0.39.0
|
||||
golang.org/x/exp v0.0.0-20221031165847-c99f073a8326
|
||||
golang.org/x/image v0.19.0
|
||||
golang.org/x/mod v0.19.0
|
||||
@@ -81,42 +81,42 @@ require (
|
||||
golang.org/x/sync v0.8.0
|
||||
golang.org/x/text v0.17.0
|
||||
golang.org/x/tools v0.23.0
|
||||
google.golang.org/api v0.189.0
|
||||
google.golang.org/api v0.191.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.115.0 // indirect
|
||||
cloud.google.com/go/auth v0.7.2 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.3 // indirect
|
||||
cloud.google.com/go/auth v0.8.1 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.4 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.5.0 // indirect
|
||||
cloud.google.com/go/iam v1.1.10 // indirect
|
||||
cloud.google.com/go/storage v1.41.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0 // indirect
|
||||
cloud.google.com/go/iam v1.1.13 // indirect
|
||||
cloud.google.com/go/storage v1.43.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.9.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.2 // indirect
|
||||
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
|
||||
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 // indirect
|
||||
github.com/aws/aws-sdk-go v1.51.30 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.15 // indirect
|
||||
github.com/aws/aws-sdk-go v1.55.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.27 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.53.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.20.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.28.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
|
||||
github.com/aws/smithy-go v1.20.3 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.0 // indirect
|
||||
@@ -127,12 +127,11 @@ require (
|
||||
github.com/go-openapi/swag v0.22.8 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/s2a-go v0.1.7 // indirect
|
||||
github.com/google/s2a-go v0.1.8 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/google/wire v0.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.5 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.13.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/invopop/yaml v0.2.0 // indirect
|
||||
@@ -150,24 +149,24 @@ 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
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 // indirect
|
||||
go.opentelemetry.io/otel v1.26.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.26.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.26.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect
|
||||
go.opentelemetry.io/otel v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.28.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.28.0 // indirect
|
||||
golang.org/x/crypto v0.26.0 // indirect
|
||||
golang.org/x/oauth2 v0.21.0 // indirect
|
||||
golang.org/x/sys v0.23.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
|
||||
google.golang.org/genproto v0.0.0-20240722135656-d784300faade // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade // indirect
|
||||
google.golang.org/grpc v1.64.1 // indirect
|
||||
golang.org/x/oauth2 v0.22.0 // indirect
|
||||
golang.org/x/sys v0.24.0 // indirect
|
||||
golang.org/x/time v0.6.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect
|
||||
google.golang.org/genproto v0.0.0-20240812133136-8ffd90a71988 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240812133136-8ffd90a71988 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240812133136-8ffd90a71988 // indirect
|
||||
google.golang.org/grpc v1.65.0 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // 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
|
||||
)
|
||||
|
||||
go 1.21.8
|
||||
go 1.22.6
|
||||
|
||||
@@ -19,10 +19,10 @@ cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmW
|
||||
cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY=
|
||||
cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14=
|
||||
cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU=
|
||||
cloud.google.com/go/auth v0.7.2 h1:uiha352VrCDMXg+yoBtaD0tUF4Kv9vrtrWPYXwutnDE=
|
||||
cloud.google.com/go/auth v0.7.2/go.mod h1:VEc4p5NNxycWQTMQEDQF0bd6aTMb6VgYDXEwiJJQAbs=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.3 h1:MlxF+Pd3OmSudg/b1yZ5lJwoXCEaeedAguodky1PcKI=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.3/go.mod h1:tMQXOfZzFuNuUxOypHlQEXgdfX5cuhwU+ffUuXRJE8I=
|
||||
cloud.google.com/go/auth v0.8.1 h1:QZW9FjC5lZzN864p13YxvAtGUlQ+KgRL+8Sg45Z6vxo=
|
||||
cloud.google.com/go/auth v0.8.1/go.mod h1:qGVp/Y3kDRSDZ5gFD/XPUfYQ9xW1iI7q8RIRoCyBbJc=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.4 h1:0GWE/FUsXhf6C+jAkWgYm7X9tK8cuEIfy19DBn6B6bY=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.4/go.mod h1:jC/jOpwFP6JBxhB3P5Rr0a9HLMC/Pe3eaL4NmdvqPtc=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
@@ -33,8 +33,10 @@ cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJ
|
||||
cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/iam v1.1.10 h1:ZSAr64oEhQSClwBL670MsJAW5/RLiC6kfw3Bqmd5ZDI=
|
||||
cloud.google.com/go/iam v1.1.10/go.mod h1:iEgMq62sg8zx446GCaijmA2Miwg5o3UbO+nI47WHJps=
|
||||
cloud.google.com/go/iam v1.1.13 h1:7zWBXG9ERbMLrzQBRhFliAV+kjcRToDTgQT3CTwYyv4=
|
||||
cloud.google.com/go/iam v1.1.13/go.mod h1:K8mY0uSXwEXS30KrnVb+j54LB/ntfZu1dr+4zFMNbus=
|
||||
cloud.google.com/go/longrunning v0.5.12 h1:5LqSIdERr71CqfUsFlJdBpOkBH8FBCFD7P1nTWy3TYE=
|
||||
cloud.google.com/go/longrunning v0.5.12/go.mod h1:S5hMV8CDJ6r50t2ubVJSKQVv5u0rmik5//KgLO3k4lU=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
@@ -45,15 +47,15 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
|
||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo=
|
||||
cloud.google.com/go/storage v1.41.0 h1:RusiwatSu6lHeEXe3kglxakAmAbfV+rhtPqA6i8RBx0=
|
||||
cloud.google.com/go/storage v1.41.0/go.mod h1:J1WCa/Z2FcgdEDuPUY8DxT5I+d9mFKsCepp5vR6Sq80=
|
||||
cloud.google.com/go/storage v1.43.0 h1:CcxnSohZwizt4LCzQHWvBf1/kvtHUn7gk9QERXPyXFs=
|
||||
cloud.google.com/go/storage v1.43.0/go.mod h1:ajvxEa7WmZS1PxvKRq4bq0tFT3vMd502JwstCcYv0Q0=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0 h1:1nGuui+4POelzDwI7RG56yfQJHCnKvwfMoU7VsEp+Zg=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.12.0/go.mod h1:99EvauvlcJ1U06amZiksfYz/3aFGyIhWGHVyiZXtBAI=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0 h1:nyQWyZvwGTvunIMxi1Y9uXkcyr+I7TeNrr/foo4Kpk8=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.14.0/go.mod h1:l38EPgmsp71HHLq9j7De57JcKOWPyhrsW1Awm1JS6K0=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.9.0 h1:H+U3Gk9zY56G3u872L82bk4thcsy2Gghb9ExT4Zvm1o=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.9.0/go.mod h1:mgrmMSgaLp9hmax62XQTd0N4aAqSE5E0DulSpVYK7vc=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.5.0 h1:AifHbc4mg0x9zW52WOpKbsHaDKuRhlI7TVl47thgQ70=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.5.0/go.mod h1:T5RfihdXtBDxt1Ch2wobif3TvzTdumDy29kahv6AV9A=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.3.2 h1:YUUxeiOWgdAQE3pXt2H7QXzZs0q8UBjgRbl56qo8GYM=
|
||||
@@ -77,46 +79,46 @@ github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc
|
||||
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c h1:651/eoCRnQ7YtSjAnSzRucrJz+3iGEFt+ysraELS81M=
|
||||
github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/aws/aws-sdk-go v1.51.30 h1:RVFkjn9P0JMwnuZCVH0TlV5k9zepHzlbc4943eZMhGw=
|
||||
github.com/aws/aws-sdk-go v1.51.30/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
|
||||
github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
|
||||
github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY=
|
||||
github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 h1:x6xsQXGSmW6frevwDA+vi/wqhp1ct18mVXYN08/93to=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2/go.mod h1:lPprDr1e6cJdyYeGXnRaJoP4Md+cDBvi2eOj00BlGmg=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.11 h1:f47rANd2LQEYHda2ddSCKYId18/8BhSRM4BULGmfgNA=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.11/go.mod h1:SMsV78RIOYdve1vf36z8LmnszlRWkwMQtomCAI0/mIE=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.11 h1:YuIB1dJNf1Re822rriUOTxopaHHvIq0l/pX3fwO+Tzs=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.11/go.mod h1:AQtFPsDH9bI2O+71anW6EKL+NcD7LG3dpKGMV4SShgo=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 h1:FVJ0r5XTHSmIHJV6KuDmdYhEpvlHpiSd38RQWhut5J4=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1/go.mod h1:zusuAeqezXzAB24LGuzuekqMAEgWkVYukBec3kr3jUg=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.15 h1:7Zwtt/lP3KNRkeZre7soMELMGNoBrutx8nobg1jKWmo=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.15/go.mod h1:436h2adoHb57yd+8W+gYPrrA9U/R/SuAuOO42Ushzhw=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3/go.mod h1:UbnqO+zjqk3uIt9yCACHJ9IVNhyhOCnYk8yA19SAWrM=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.10 h1:zeN9UtUlA6FTx0vFSayxSX32HDw73Yb6Hh2izDSFxXY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.10/go.mod h1:3HKuexPDcwLWPaqpW2UR/9n8N/u/3CKcGAzSs8p8u8g=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 h1:SoNJ4RlFEQEbtDcCEt+QG56MY4fm4W8rYirAmq+/DdU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15/go.mod h1:U9ke74k1n2bf+RIgoX1SXFed1HLs51OgUSs+Ph0KJP8=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 h1:C6WHdGnTDIYETAm5iErQUiVNsclNx9qbJVPIt03B6bI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15/go.mod h1:ZQLZqhcu+JhSrA9/NXRm8SkDvsycE+JkV3WGY41e+IM=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.5 h1:81KE7vaZzrl7yHBYHVEzYB8sypz11NMOZ40YlWvPxsU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.5/go.mod h1:LIt2rg7Mcgn09Ygbdh/RdIm0rQ+3BNkbP1gyVMFtRK0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 h1:Z5r7SycxmSllHYmaAZPpmN8GviDrSGhMS6bldqtXZPw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15/go.mod h1:CetW7bDE00QoGEmPUoZuRog07SGVAUVW6LFpNP0YfIg=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudfront v1.38.4 h1:I/sQ9uGOs72/483obb2SPoa9ZEsYGbel6jcTTwD/0zU=
|
||||
github.com/aws/aws-sdk-go-v2/service/cloudfront v1.38.4/go.mod h1:P6ByphKl2oNQZlv4WsCaLSmRncKEcOnbitYLtJPfqZI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.7 h1:ZMeFZ5yk+Ek+jNr1+uwCd2tG89t6oTS5yVWpa6yy2es=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.7/go.mod h1:mxV05U+4JiHqIpGqqYXOHLPKUC6bDXC44bsUhNjOEwY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 h1:ogRAwT1/gxJBcSWDMZlgyFUM962F51A5CRhDLbxLdmo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7/go.mod h1:YCsIZhXfRPLFFCl5xxY+1T9RKzOKjCut+28JSX2DnAk=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.5 h1:f9RyWNtS8oH7cZlbn+/JNPpjUk5+5fLd5lM9M0i49Ys=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.5/go.mod h1:h5CoMZV2VF297/VLhRhO1WF+XYWOzXo+4HsObA4HjBQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.53.1 h1:6cnno47Me9bRykw9AEv9zkXE+5or7jz8TsskTTccbgc=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.53.1/go.mod h1:qmdkIIAC+GCLASF7R2whgNrJADz0QZPX+Seiw/i4S3o=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.20.5 h1:vN8hEbpRnL7+Hopy9dzmRle1xmDc7o8tmY0klsr175w=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.20.5/go.mod h1:qGzynb/msuZIE8I75DVRCUXw3o3ZyBmUvMwQ2t/BrGM=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4 h1:Jux+gDDyi1Lruk+KHF91tK2KCuY61kzoCpvtvJJBtOE=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4/go.mod h1:mUYPBhaF2lGiukDEjJX2BLRRKTmoUSitGDUgM4tRxak=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.28.6 h1:cwIxeBttqPN3qkaAjcEcsh8NYr8n2HZPkcKgPAi1phU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.28.6/go.mod h1:FZf1/nKNEkHdGGJP/cI2MoIMquumuRK6ol3QQJNDxmw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 h1:YPYe6ZmvUfDDDELqEKtAd6bo8zxhkm+XEFEzQisqUIE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17/go.mod h1:oBtcnYua/CgzCWYN7NZ5j7PotFDaFSUjCYVTtfyn7vw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17/go.mod h1:RkZEx4l0EHYDJpWppMJ3nD9wZJAa8/0lq9aVC+r2UII=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 h1:246A4lSTXWJw/rmlQI+TT2OcqeDMKBdyjEQrafMaQdA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15/go.mod h1:haVfg3761/WF7YPuJOER2MP0k4UAXyHaLclKXB6usDg=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3 h1:hT8ZAZRIfqBqHbzKTII+CIiY8G2oC9OpLedkZ51DWl8=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3/go.mod h1:Lcxzg5rojyVPU/0eFwLtcyTaek/6Mtic5B1gJo7e/zE=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 h1:BXx0ZIxvrJdSgSvKTZ+yRBeSqqgPM89VPlulEcl37tM=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.22.4/go.mod h1:ooyCOXjvJEsUw7x+ZDHeISPMhtwI3ZCB7ggFMcFfWLU=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 h1:yiwVzJW2ZxZTurVbYWA7QOrAaCYQR72t0wrSBfoesUE=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4/go.mod h1:0oxfLkpz3rQ/CHlx5hB7H69YUpFiI1tql6Q6Ne+1bCw=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudrvuKpDKgMVRlepGE=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ=
|
||||
github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE=
|
||||
github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
|
||||
github.com/bep/clocks v0.5.0 h1:hhvKVGLPQWRVsBP/UB7ErrHYIO42gINVbvqxvYTPVps=
|
||||
@@ -133,14 +135,14 @@ github.com/bep/godartsass/v2 v2.1.0 h1:fq5Y1xYf4diu4tXABiekZUCA+5l/dmNjGKCeQwdy+
|
||||
github.com/bep/godartsass/v2 v2.1.0/go.mod h1:AcP8QgC+OwOXEq6im0WgDRYK7scDsmZCEW62o1prQLo=
|
||||
github.com/bep/golibsass v1.1.1 h1:xkaet75ygImMYjM+FnHIT3xJn7H0xBA9UxSOJjk8Khw=
|
||||
github.com/bep/golibsass v1.1.1/go.mod h1:DL87K8Un/+pWUS75ggYv41bliGiolxzDKWJAq3eJ1MA=
|
||||
github.com/bep/golibsass v1.2.0 h1:nyZUkKP/0psr8nT6GR2cnmt99xS93Ji82ZD9AgOK6VI=
|
||||
github.com/bep/golibsass v1.2.0/go.mod h1:DL87K8Un/+pWUS75ggYv41bliGiolxzDKWJAq3eJ1MA=
|
||||
github.com/bep/gowebp v0.3.0 h1:MhmMrcf88pUY7/PsEhMgEP0T6fDUnRTMpN8OclDrbrY=
|
||||
github.com/bep/gowebp v0.3.0/go.mod h1:ZhFodwdiFp8ehGJpF4LdPl6unxZm9lLFjxD3z2h2AgI=
|
||||
github.com/bep/helpers v0.4.0 h1:ab9veaAiWY4ST48Oxp5usaqivDmYdB744fz+tcZ3Ifs=
|
||||
github.com/bep/helpers v0.4.0/go.mod h1:/QpHdmcPagDw7+RjkLFCvnlUc8lQ5kg4KDrEkb2Yyco=
|
||||
github.com/bep/imagemeta v0.7.6 h1:No64uhsEgUg/wz19yUC8BmHkFNMGhNu3X5puvsuvi2E=
|
||||
github.com/bep/imagemeta v0.7.6/go.mod h1:5piPAq5Qomh07m/dPPCLN3mDJyFusvUG7VwdRD/vX0s=
|
||||
github.com/bep/imagemeta v0.8.0 h1:4lqI839akl6lR61D7hmvaw2LDOxiXFZ4D0VIyHyGpc4=
|
||||
github.com/bep/imagemeta v0.8.0/go.mod h1:5piPAq5Qomh07m/dPPCLN3mDJyFusvUG7VwdRD/vX0s=
|
||||
github.com/bep/imagemeta v0.8.1 h1:tjZLPRftjxU7PTI87o5e5WKOFQ4S9S0engiP1OTpJTI=
|
||||
github.com/bep/imagemeta v0.8.1/go.mod h1:5piPAq5Qomh07m/dPPCLN3mDJyFusvUG7VwdRD/vX0s=
|
||||
github.com/bep/lazycache v0.4.0 h1:X8yVyWNVupPd4e1jV7efi3zb7ZV/qcjKQgIQ5aPbkYI=
|
||||
github.com/bep/lazycache v0.4.0/go.mod h1:NmRm7Dexh3pmR1EignYR8PjO2cWybFQ68+QgY3VMCSc=
|
||||
github.com/bep/logg v0.4.0 h1:luAo5mO4ZkhA5M1iDVDqDqnBBnlHjmtZF6VAyTp+nCQ=
|
||||
@@ -189,8 +191,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
|
||||
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/evanw/esbuild v0.23.0 h1:PLUwTn2pzQfIBRrMKcD3M0g1ALOKIHMDefdFCk7avwM=
|
||||
github.com/evanw/esbuild v0.23.0/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
|
||||
github.com/evanw/esbuild v0.23.1 h1:ociewhY6arjTarKLdrXfDTgy25oxhTZmzP8pfuBTfTA=
|
||||
github.com/evanw/esbuild v0.23.1/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
|
||||
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
|
||||
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
@@ -291,8 +293,8 @@ github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-replayers/grpcreplay v1.1.0 h1:S5+I3zYyZ+GQz68OfbURDdt/+cSMqCK1wrvNx7WBzTE=
|
||||
github.com/google/go-replayers/grpcreplay v1.1.0/go.mod h1:qzAvJ8/wi57zq7gWqaE6AwLM6miiXUQwP1S+I9icmhk=
|
||||
github.com/google/go-replayers/grpcreplay v1.3.0 h1:1Keyy0m1sIpqstQmgz307zhiJ1pV4uIlFds5weTmxbo=
|
||||
github.com/google/go-replayers/grpcreplay v1.3.0/go.mod h1:v6NgKtkijC0d3e3RW8il6Sy5sqRVUwoQa4mHOGEy8DI=
|
||||
github.com/google/go-replayers/httpreplay v1.2.0 h1:VM1wEyyjaoU53BwrOnaf9VhAyQQEEioJvFYxYcLRKzk=
|
||||
github.com/google/go-replayers/httpreplay v1.2.0/go.mod h1:WahEFFZZ7a1P4VM1qEeHy+tME4bwyqPcwWbNlUI1Mcg=
|
||||
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
|
||||
@@ -312,8 +314,8 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe
|
||||
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
|
||||
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
|
||||
github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM=
|
||||
github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA=
|
||||
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
@@ -324,8 +326,8 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfF
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/googleapis/gax-go/v2 v2.12.5 h1:8gw9KZK8TiVKB6q3zHY3SBzLnrGp6HQjyfYBYGmXdxA=
|
||||
github.com/googleapis/gax-go/v2 v2.12.5/go.mod h1:BUDKcWo+RaKq5SC9vVYL0wLADa3VcfswbOMMRmB9H3E=
|
||||
github.com/googleapis/gax-go/v2 v2.13.0 h1:yitjD5f7jQHhyDsnhKEBU52NdvvdSeGzlAnDPT0hH1s=
|
||||
github.com/googleapis/gax-go/v2 v2.13.0/go.mod h1:Z/fvTZXF8/uw7Xu5GuslPw+bplx6SS338j1Is2S+B7A=
|
||||
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
@@ -400,8 +402,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.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||
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-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
@@ -442,7 +444,6 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
@@ -451,7 +452,6 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
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.37 h1:Q97cx4STXCh1dlWDlNHZniE8BJ2EBL0+2b0n92BJQhw=
|
||||
@@ -461,8 +461,8 @@ github.com/tdewolff/parse/v2 v2.7.15/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W
|
||||
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/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
|
||||
github.com/tetratelabs/wazero v1.7.4-0.20240805170331-2b12e189eeec h1:KeQseLFSWb9qjW4PSWxciTBk1hbG7KsVx3rs1hIQnbQ=
|
||||
github.com/tetratelabs/wazero v1.7.4-0.20240805170331-2b12e189eeec/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y=
|
||||
github.com/tetratelabs/wazero v1.8.0 h1:iEKu0d4c2Pd+QSRieYbnQC9yiFlMS9D+Jr0LsRmcF4g=
|
||||
github.com/tetratelabs/wazero v1.8.0/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
|
||||
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
|
||||
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
@@ -484,22 +484,22 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0 h1:A3SayB3rNyt+1S6qpI9mHPkeHTZbD7XILEqWnYZb2l0=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.51.0/go.mod h1:27iA5uvhuRNmalO+iEUdVn5ZMj2qy10Mm+XRIpRmyuU=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 h1:Xs2Ncz0gNihqu9iosIZ5SkBbWo5T8JhhLJFMQL1qmLI=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0/go.mod h1:vy+2G/6NvVMpwGX/NyLqcC41fxepnuKHk16E6IZUcJc=
|
||||
go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs=
|
||||
go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4=
|
||||
go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30=
|
||||
go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0 h1:9G6E0TXzGFVfTnawRzrPl83iHOAV7L8NJiR8RSGYV1g=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.53.0/go.mod h1:azvtTADFQJA8mX80jIH/akaE7h+dbm/sVuaHqN13w74=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg=
|
||||
go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo=
|
||||
go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4=
|
||||
go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q=
|
||||
go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s=
|
||||
go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
|
||||
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
|
||||
go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA=
|
||||
go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0=
|
||||
go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g=
|
||||
go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI=
|
||||
go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=
|
||||
go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
|
||||
gocloud.dev v0.38.0 h1:SpxfaOc/Fp4PeO8ui7wRcCZV0EgXZ+IWcVSLn6ZMSw0=
|
||||
gocloud.dev v0.38.0/go.mod h1:3XjKvd2E5iVNu/xFImRzjN0d/fkNHe4s0RiKidpEUMQ=
|
||||
gocloud.dev v0.39.0 h1:EYABYGhAalPUaMrbSKOr5lejxoxvXj99nE8XFtsDgds=
|
||||
gocloud.dev v0.39.0/go.mod h1:drz+VyYNBvrMTW0KZiBAYEdl8lbNZx+OQ7oQvdrFmSQ=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
@@ -608,8 +608,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ
|
||||
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs=
|
||||
golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
|
||||
golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -674,8 +674,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
|
||||
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
||||
golang.org/x/sys v0.24.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/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
@@ -699,8 +699,8 @@ golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
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=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
@@ -759,8 +759,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 h1:LLhsEBxRTBLuKlQxFBYUOU8xyFgXv6cOTp2HASDlsDk=
|
||||
golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
@@ -780,8 +780,8 @@ google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz513
|
||||
google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
|
||||
google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
|
||||
google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
|
||||
google.golang.org/api v0.189.0 h1:equMo30LypAkdkLMBqfeIqtyAnlyig1JSZArl4XPwdI=
|
||||
google.golang.org/api v0.189.0/go.mod h1:FLWGJKb0hb+pU2j+rJqwbnsF+ym+fQs73rbJ+KAUgy8=
|
||||
google.golang.org/api v0.191.0 h1:cJcF09Z+4HAB2t5qTQM1ZtfL/PemsLFkcFG67qq2afk=
|
||||
google.golang.org/api v0.191.0/go.mod h1:tD5dsFGxFza0hnQveGfVk9QQYKcfp+VzgRqyXFxE0+E=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
@@ -825,12 +825,12 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D
|
||||
google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
|
||||
google.golang.org/genproto v0.0.0-20240722135656-d784300faade h1:lKFsS7wpngDgSCeFn7MoLy+wBDQZ1UQIJD4UNM1Qvkg=
|
||||
google.golang.org/genproto v0.0.0-20240722135656-d784300faade/go.mod h1:FfBgJBJg9GcpPvKIuHSZ/aE1g2ecGL74upMzGZjiGEY=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade h1:oCRSWfwGXQsqlVdErcyTt4A93Y8fo0/9D4b1gnI++qo=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
|
||||
google.golang.org/genproto v0.0.0-20240812133136-8ffd90a71988 h1:CT2Thj5AuPV9phrYMtzX11k+XkzMGfRAet42PmoTATM=
|
||||
google.golang.org/genproto v0.0.0-20240812133136-8ffd90a71988/go.mod h1:7uvplUBj4RjHAxIZ//98LzOvrQ04JBkaixRmCMI29hc=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240812133136-8ffd90a71988 h1:+/tmTy5zAieooKIXfzDm9KiA3Bv6JBwriRN9LY+yayk=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240812133136-8ffd90a71988/go.mod h1:4+X6GvPs+25wZKbQq9qyAXrwIRExv7w0Ea6MgZLZiDM=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240812133136-8ffd90a71988 h1:V71AcdLZr2p8dC9dbOIMCpqi4EmRl8wUwnJzXXLmbmc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240812133136-8ffd90a71988/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
@@ -847,8 +847,8 @@ google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
|
||||
google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.64.1 h1:LKtvyfbX3UGVPFcGqJ9ItpVWW6oN/2XqTxfAnwRRXiA=
|
||||
google.golang.org/grpc v1.64.1/go.mod h1:hiQF4LFZelK2WKaP6W0L92zGHtiQdZxk8CrSdvyjeP0=
|
||||
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
|
||||
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
"html/template"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hexec"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
@@ -165,75 +164,6 @@ func TotalWords(s string) int {
|
||||
return n
|
||||
}
|
||||
|
||||
// TruncateWordsByRune truncates words by runes.
|
||||
func (c *ContentSpec) TruncateWordsByRune(in []string) (string, bool) {
|
||||
words := make([]string, len(in))
|
||||
copy(words, in)
|
||||
|
||||
count := 0
|
||||
for index, word := range words {
|
||||
if count >= c.Cfg.SummaryLength() {
|
||||
return strings.Join(words[:index], " "), true
|
||||
}
|
||||
runeCount := utf8.RuneCountInString(word)
|
||||
if len(word) == runeCount {
|
||||
count++
|
||||
} else if count+runeCount < c.Cfg.SummaryLength() {
|
||||
count += runeCount
|
||||
} else {
|
||||
for ri := range word {
|
||||
if count >= c.Cfg.SummaryLength() {
|
||||
truncatedWords := append(words[:index], word[:ri])
|
||||
return strings.Join(truncatedWords, " "), true
|
||||
}
|
||||
count++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(words, " "), false
|
||||
}
|
||||
|
||||
// TruncateWordsToWholeSentence takes content and truncates to whole sentence
|
||||
// limited by max number of words. It also returns whether it is truncated.
|
||||
func (c *ContentSpec) TruncateWordsToWholeSentence(s string) (string, bool) {
|
||||
var (
|
||||
wordCount = 0
|
||||
lastWordIndex = -1
|
||||
)
|
||||
|
||||
for i, r := range s {
|
||||
if unicode.IsSpace(r) {
|
||||
wordCount++
|
||||
lastWordIndex = i
|
||||
|
||||
if wordCount >= c.Cfg.SummaryLength() {
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if lastWordIndex == -1 {
|
||||
return s, false
|
||||
}
|
||||
|
||||
endIndex := -1
|
||||
|
||||
for j, r := range s[lastWordIndex:] {
|
||||
if isEndOfSentence(r) {
|
||||
endIndex = j + lastWordIndex + utf8.RuneLen(r)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if endIndex == -1 {
|
||||
return s, false
|
||||
}
|
||||
|
||||
return strings.TrimSpace(s[:endIndex]), endIndex < len(s)
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -256,7 +186,3 @@ func (c *ContentSpec) TrimShortHTML(input []byte, markup string) []byte {
|
||||
}
|
||||
return input
|
||||
}
|
||||
|
||||
func isEndOfSentence(r rune) bool {
|
||||
return r == '.' || r == '?' || r == '!' || r == '"' || r == '\n'
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/gohugoio/hugo/config"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
)
|
||||
|
||||
@@ -66,84 +65,6 @@ func TestBytesToHTML(t *testing.T) {
|
||||
c.Assert(helpers.BytesToHTML([]byte("dobedobedo")), qt.Equals, template.HTML("dobedobedo"))
|
||||
}
|
||||
|
||||
var benchmarkTruncateString = strings.Repeat("This is a sentence about nothing.", 20)
|
||||
|
||||
func BenchmarkTestTruncateWordsToWholeSentence(b *testing.B) {
|
||||
c := newTestContentSpec(nil)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
c.TruncateWordsToWholeSentence(benchmarkTruncateString)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTruncateWordsToWholeSentence(t *testing.T) {
|
||||
type test struct {
|
||||
input, expected string
|
||||
max int
|
||||
truncated bool
|
||||
}
|
||||
data := []test{
|
||||
{"a b c", "a b c", 12, false},
|
||||
{"a b c", "a b c", 3, false},
|
||||
{"a", "a", 1, false},
|
||||
{"This is a sentence.", "This is a sentence.", 5, false},
|
||||
{"This is also a sentence!", "This is also a sentence!", 1, false},
|
||||
{"To be. Or not to be. That's the question.", "To be.", 1, true},
|
||||
{" \nThis is not a sentence\nAnd this is another", "This is not a sentence", 4, true},
|
||||
{"", "", 10, false},
|
||||
{"This... is a more difficult test?", "This... is a more difficult test?", 1, false},
|
||||
}
|
||||
for i, d := range data {
|
||||
cfg := config.New()
|
||||
cfg.Set("summaryLength", d.max)
|
||||
c := newTestContentSpec(cfg)
|
||||
output, truncated := c.TruncateWordsToWholeSentence(d.input)
|
||||
if d.expected != output {
|
||||
t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output)
|
||||
}
|
||||
|
||||
if d.truncated != truncated {
|
||||
t.Errorf("Test %d failed. Expected truncated=%t got %t", i, d.truncated, truncated)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTruncateWordsByRune(t *testing.T) {
|
||||
type test struct {
|
||||
input, expected string
|
||||
max int
|
||||
truncated bool
|
||||
}
|
||||
data := []test{
|
||||
{"", "", 1, false},
|
||||
{"a b c", "a b c", 12, false},
|
||||
{"a b c", "a b c", 3, false},
|
||||
{"a", "a", 1, false},
|
||||
{"Hello 中国", "", 0, true},
|
||||
{"这是中文,全中文。", "这是中文,", 5, true},
|
||||
{"Hello 中国", "Hello 中", 2, true},
|
||||
{"Hello 中国", "Hello 中国", 3, false},
|
||||
{"Hello中国 Good 好的", "Hello中国 Good 好", 9, true},
|
||||
{"This is a sentence.", "This is", 2, true},
|
||||
{"This is also a sentence!", "This", 1, true},
|
||||
{"To be. Or not to be. That's the question.", "To be. Or not", 4, true},
|
||||
{" \nThis is not a sentence\n ", "This is not", 3, true},
|
||||
}
|
||||
for i, d := range data {
|
||||
cfg := config.New()
|
||||
cfg.Set("summaryLength", d.max)
|
||||
c := newTestContentSpec(cfg)
|
||||
output, truncated := c.TruncateWordsByRune(strings.Fields(d.input))
|
||||
if d.expected != output {
|
||||
t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output)
|
||||
}
|
||||
|
||||
if d.truncated != truncated {
|
||||
t.Errorf("Test %d failed. Expected truncated=%t got %t", i, d.truncated, truncated)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractTOCNormalContent(t *testing.T) {
|
||||
content := []byte("<nav>\n<ul>\nTOC<li><a href=\"#")
|
||||
|
||||
|
||||
+2
-1
@@ -147,7 +147,8 @@ func isWrite(flag int) bool {
|
||||
// TODO(bep) move this to a more suitable place.
|
||||
func MakeReadableAndRemoveAllModulePkgDir(fs afero.Fs, dir string) (int, error) {
|
||||
// Safe guard
|
||||
if !strings.Contains(dir, "pkg") {
|
||||
// Note that the base directory changed from pkg to gomod_cache in Go 1.23.
|
||||
if !strings.Contains(dir, "pkg") && !strings.Contains(dir, "gomod") {
|
||||
panic(fmt.Sprint("invalid dir:", dir))
|
||||
}
|
||||
|
||||
|
||||
@@ -560,7 +560,7 @@ func (m *pageMap) getOrCreateResourcesForPage(ps *pageState) resource.Resources
|
||||
for _, r := range res2 {
|
||||
var found bool
|
||||
for _, r2 := range res {
|
||||
if r2.(resource.NameNormalizedProvider).NameNormalized() == r.(resource.NameNormalizedProvider).NameNormalized() {
|
||||
if resource.NameNormalizedOrName(r2) == resource.NameNormalizedOrName(r) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
|
||||
@@ -98,8 +98,8 @@ baseURL="https://example.org"
|
||||
P1: {{ $p.Content }}
|
||||
|
||||
`,
|
||||
"_default/_markup/render-link.html", `html-link: {{ .Destination | safeURL }}|Text: {{ .Text | safeHTML }}|Plain: {{ .PlainText | safeHTML }}`,
|
||||
"_default/_markup/render-image.html", `html-image: {{ .Destination | safeURL }}|Text: {{ .Text | safeHTML }}|Plain: {{ .PlainText | safeHTML }}`,
|
||||
"_default/_markup/render-link.html", `html-link: {{ .Destination | safeURL }}|Text: {{ .Text }}|Plain: {{ .PlainText | safeHTML }}`,
|
||||
"_default/_markup/render-image.html", `html-image: {{ .Destination | safeURL }}|Text: {{ .Text }}|Plain: {{ .PlainText | safeHTML }}`,
|
||||
)
|
||||
|
||||
b.WithContent("p1.md", `---
|
||||
|
||||
+4
-5
@@ -61,6 +61,7 @@ var (
|
||||
pageTypesProvider = resource.NewResourceTypesProvider(media.Builtin.OctetType, pageResourceType)
|
||||
nopPageOutput = &pageOutput{
|
||||
pagePerOutputProviders: nopPagePerOutput,
|
||||
MarkupProvider: page.NopPage,
|
||||
ContentProvider: page.NopPage,
|
||||
}
|
||||
)
|
||||
@@ -213,11 +214,8 @@ func (p *pageHeadingsFiltered) page() page.Page {
|
||||
|
||||
// For internal use by the related content feature.
|
||||
func (p *pageState) ApplyFilterToHeadings(ctx context.Context, fn func(*tableofcontents.Heading) bool) related.Document {
|
||||
r, err := p.m.content.contentToC(ctx, p.pageOutput.pco)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
headings := r.tableOfContents.Headings.FilterBy(fn)
|
||||
fragments := p.pageOutput.pco.c().Fragments(ctx)
|
||||
headings := fragments.Headings.FilterBy(fn)
|
||||
return &pageHeadingsFiltered{
|
||||
pageState: p,
|
||||
headings: headings,
|
||||
@@ -719,6 +717,7 @@ func (p *pageState) shiftToOutputFormat(isRenderingSite bool, idx int) error {
|
||||
})
|
||||
p.pageOutput.contentRenderer = lcp
|
||||
p.pageOutput.ContentProvider = lcp
|
||||
p.pageOutput.MarkupProvider = lcp
|
||||
p.pageOutput.PageRenderProvider = lcp
|
||||
p.pageOutput.TableOfContentsProvider = lcp
|
||||
}
|
||||
|
||||
+469
-117
@@ -14,7 +14,6 @@
|
||||
package hugolib
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -29,15 +28,23 @@ import (
|
||||
"github.com/gohugoio/hugo/common/hcontext"
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
"github.com/gohugoio/hugo/common/hugio"
|
||||
"github.com/gohugoio/hugo/common/hugo"
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"github.com/gohugoio/hugo/common/types/hstring"
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/identity"
|
||||
"github.com/gohugoio/hugo/markup"
|
||||
"github.com/gohugoio/hugo/markup/converter"
|
||||
"github.com/gohugoio/hugo/markup/goldmark/hugocontext"
|
||||
"github.com/gohugoio/hugo/markup/tableofcontents"
|
||||
"github.com/gohugoio/hugo/parser/metadecoders"
|
||||
"github.com/gohugoio/hugo/parser/pageparser"
|
||||
"github.com/gohugoio/hugo/resources"
|
||||
"github.com/gohugoio/hugo/resources/page"
|
||||
"github.com/gohugoio/hugo/resources/resource"
|
||||
"github.com/gohugoio/hugo/tpl"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/spf13/cast"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -45,8 +52,8 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
internalSummaryDividerBaseBytes = []byte(internalSummaryDividerBase)
|
||||
internalSummaryDividerPre = []byte("\n\n" + internalSummaryDividerBase + "\n\n")
|
||||
internalSummaryDividerPreString = "\n\n" + internalSummaryDividerBase + "\n\n"
|
||||
internalSummaryDividerPre = []byte(internalSummaryDividerPreString)
|
||||
)
|
||||
|
||||
type pageContentReplacement struct {
|
||||
@@ -130,6 +137,7 @@ func (m *pageMeta) newCachedContent(h *HugoSites, pi *contentParseInfo) (*cached
|
||||
shortcodeState: newShortcodeHandler(filename, m.s),
|
||||
pi: pi,
|
||||
enableEmoji: m.s.conf.EnableEmoji,
|
||||
scopes: maps.NewCache[string, *cachedContentScope](),
|
||||
}
|
||||
|
||||
source, err := c.pi.contentSource(m)
|
||||
@@ -155,6 +163,20 @@ type cachedContent struct {
|
||||
pi *contentParseInfo
|
||||
|
||||
enableEmoji bool
|
||||
|
||||
scopes *maps.Cache[string, *cachedContentScope]
|
||||
}
|
||||
|
||||
func (c *cachedContent) getOrCreateScope(scope string, pco *pageContentOutput) *cachedContentScope {
|
||||
key := scope + pco.po.f.Name
|
||||
cs, _ := c.scopes.GetOrCreate(key, func() (*cachedContentScope, error) {
|
||||
return &cachedContentScope{
|
||||
cachedContent: c,
|
||||
pco: pco,
|
||||
scope: scope,
|
||||
}, nil
|
||||
})
|
||||
return cs
|
||||
}
|
||||
|
||||
type contentParseInfo struct {
|
||||
@@ -171,9 +193,6 @@ type contentParseInfo struct {
|
||||
// Whether the parsed content contains a summary separator.
|
||||
hasSummaryDivider bool
|
||||
|
||||
// Whether there are more content after the summary divider.
|
||||
summaryTruncated bool
|
||||
|
||||
// Returns the position in bytes after any front matter.
|
||||
posMainContent int
|
||||
|
||||
@@ -368,8 +387,6 @@ Loop:
|
||||
}
|
||||
|
||||
if item.IsNonWhitespace(source) {
|
||||
rn.summaryTruncated = true
|
||||
|
||||
// Done
|
||||
return false
|
||||
}
|
||||
@@ -487,26 +504,28 @@ type contentTableOfContents struct {
|
||||
}
|
||||
|
||||
type contentSummary struct {
|
||||
content template.HTML
|
||||
summary template.HTML
|
||||
summaryTruncated bool
|
||||
content string
|
||||
contentWithoutSummary template.HTML
|
||||
summary page.Summary
|
||||
}
|
||||
|
||||
type contentPlainPlainWords struct {
|
||||
plain string
|
||||
plainWords []string
|
||||
|
||||
summary template.HTML
|
||||
summaryTruncated bool
|
||||
|
||||
wordCount int
|
||||
fuzzyWordCount int
|
||||
readingTime int
|
||||
}
|
||||
|
||||
func (c *cachedContent) contentRendered(ctx context.Context, cp *pageContentOutput) (contentSummary, error) {
|
||||
func (c *cachedContentScope) keyScope(ctx context.Context) string {
|
||||
return hugo.GetMarkupScope(ctx) + c.pco.po.f.Name
|
||||
}
|
||||
|
||||
func (c *cachedContentScope) contentRendered(ctx context.Context) (contentSummary, error) {
|
||||
cp := c.pco
|
||||
ctx = tpl.Context.DependencyScope.Set(ctx, pageDependencyScopeGlobal)
|
||||
key := c.pi.sourceKey + "/" + cp.po.f.Name
|
||||
key := c.pi.sourceKey + "/" + c.keyScope(ctx)
|
||||
versionv := c.version(cp)
|
||||
|
||||
v, err := c.pm.cacheContentRendered.GetOrCreate(key, func(string) (*resources.StaleValue[contentSummary], error) {
|
||||
@@ -515,97 +534,121 @@ func (c *cachedContent) contentRendered(ctx context.Context, cp *pageContentOutp
|
||||
}))
|
||||
|
||||
cp.po.p.s.h.contentRenderCounter.Add(1)
|
||||
cp.contentRendered = true
|
||||
cp.contentRendered.Store(true)
|
||||
po := cp.po
|
||||
|
||||
ct, err := c.contentToC(ctx, cp)
|
||||
ct, err := c.contentToC(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rs := &resources.StaleValue[contentSummary]{
|
||||
StaleVersionFunc: func() uint32 {
|
||||
return c.version(cp) - versionv
|
||||
},
|
||||
}
|
||||
|
||||
if len(c.pi.itemsStep2) == 0 {
|
||||
// Nothing to do.
|
||||
return rs, nil
|
||||
}
|
||||
|
||||
var b []byte
|
||||
|
||||
if ct.astDoc != nil {
|
||||
// The content is parsed, but not rendered.
|
||||
r, ok, err := po.contentRenderer.RenderContent(ctx, ct.contentToRender, ct.astDoc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
rs, err := func() (*resources.StaleValue[contentSummary], error) {
|
||||
rs := &resources.StaleValue[contentSummary]{
|
||||
StaleVersionFunc: func() uint32 {
|
||||
return c.version(cp) - versionv
|
||||
},
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return nil, errors.New("invalid state: astDoc is set but RenderContent returned false")
|
||||
if len(c.pi.itemsStep2) == 0 {
|
||||
// Nothing to do.
|
||||
return rs, nil
|
||||
}
|
||||
|
||||
b = r.Bytes()
|
||||
var b []byte
|
||||
|
||||
} else {
|
||||
// Copy the content to be rendered.
|
||||
b = make([]byte, len(ct.contentToRender))
|
||||
copy(b, ct.contentToRender)
|
||||
}
|
||||
|
||||
// There are one or more replacement tokens to be replaced.
|
||||
var hasShortcodeVariants bool
|
||||
tokenHandler := func(ctx context.Context, token string) ([]byte, error) {
|
||||
if token == tocShortcodePlaceholder {
|
||||
return []byte(ct.tableOfContentsHTML), nil
|
||||
}
|
||||
renderer, found := ct.contentPlaceholders[token]
|
||||
if found {
|
||||
repl, more, err := renderer.renderShortcode(ctx)
|
||||
if ct.astDoc != nil {
|
||||
// The content is parsed, but not rendered.
|
||||
r, ok, err := po.contentRenderer.RenderContent(ctx, ct.contentToRender, ct.astDoc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hasShortcodeVariants = hasShortcodeVariants || more
|
||||
return repl, nil
|
||||
}
|
||||
// This should never happen.
|
||||
panic(fmt.Errorf("unknown shortcode token %q (number of tokens: %d)", token, len(ct.contentPlaceholders)))
|
||||
}
|
||||
|
||||
b, err = expandShortcodeTokens(ctx, b, tokenHandler)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if hasShortcodeVariants {
|
||||
cp.po.p.pageOutputTemplateVariationsState.Add(1)
|
||||
}
|
||||
if !ok {
|
||||
return nil, errors.New("invalid state: astDoc is set but RenderContent returned false")
|
||||
}
|
||||
|
||||
var result contentSummary // hasVariants bool
|
||||
|
||||
if c.pi.hasSummaryDivider {
|
||||
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):]
|
||||
b = r.Bytes()
|
||||
|
||||
} else {
|
||||
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 {
|
||||
b = content
|
||||
result.summary = helpers.BytesToHTML(summary)
|
||||
}
|
||||
// Copy the content to be rendered.
|
||||
b = make([]byte, len(ct.contentToRender))
|
||||
copy(b, ct.contentToRender)
|
||||
}
|
||||
result.summaryTruncated = c.pi.summaryTruncated
|
||||
}
|
||||
result.content = helpers.BytesToHTML(b)
|
||||
rs.Value = result
|
||||
|
||||
return rs, nil
|
||||
// There are one or more replacement tokens to be replaced.
|
||||
var hasShortcodeVariants bool
|
||||
tokenHandler := func(ctx context.Context, token string) ([]byte, error) {
|
||||
if token == tocShortcodePlaceholder {
|
||||
return []byte(ct.tableOfContentsHTML), nil
|
||||
}
|
||||
renderer, found := ct.contentPlaceholders[token]
|
||||
if found {
|
||||
repl, more, err := renderer.renderShortcode(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hasShortcodeVariants = hasShortcodeVariants || more
|
||||
return repl, nil
|
||||
}
|
||||
// This should never happen.
|
||||
panic(fmt.Errorf("unknown shortcode token %q (number of tokens: %d)", token, len(ct.contentPlaceholders)))
|
||||
}
|
||||
|
||||
b, err = expandShortcodeTokens(ctx, b, tokenHandler)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if hasShortcodeVariants {
|
||||
cp.po.p.pageOutputTemplateVariationsState.Add(1)
|
||||
}
|
||||
|
||||
var result contentSummary
|
||||
if c.pi.hasSummaryDivider {
|
||||
s := string(b)
|
||||
summarized := page.ExtractSummaryFromHTMLWithDivider(cp.po.p.m.pageConfig.ContentMediaType, s, internalSummaryDividerBase)
|
||||
result.summary = page.Summary{
|
||||
Text: template.HTML(summarized.Summary()),
|
||||
Type: page.SummaryTypeManual,
|
||||
Truncated: summarized.Truncated(),
|
||||
}
|
||||
result.contentWithoutSummary = template.HTML(summarized.ContentWithoutSummary())
|
||||
result.content = summarized.Content()
|
||||
} else {
|
||||
result.content = string(b)
|
||||
}
|
||||
|
||||
if !c.pi.hasSummaryDivider && cp.po.p.m.pageConfig.Summary == "" {
|
||||
numWords := cp.po.p.s.conf.SummaryLength
|
||||
isCJKLanguage := cp.po.p.m.pageConfig.IsCJKLanguage
|
||||
summary := page.ExtractSummaryFromHTML(cp.po.p.m.pageConfig.ContentMediaType, string(result.content), numWords, isCJKLanguage)
|
||||
result.summary = page.Summary{
|
||||
Text: template.HTML(summary.Summary()),
|
||||
Type: page.SummaryTypeAuto,
|
||||
Truncated: summary.Truncated(),
|
||||
}
|
||||
result.contentWithoutSummary = template.HTML(summary.ContentWithoutSummary())
|
||||
}
|
||||
rs.Value = result
|
||||
|
||||
return rs, nil
|
||||
}()
|
||||
if err != nil {
|
||||
return rs, cp.po.p.wrapError(err)
|
||||
}
|
||||
|
||||
if rs.Value.summary.IsZero() {
|
||||
b, err := cp.po.contentRenderer.ParseAndRenderContent(ctx, []byte(cp.po.p.m.pageConfig.Summary), false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
html := cp.po.p.s.ContentSpec.TrimShortHTML(b.Bytes(), cp.po.p.m.pageConfig.Content.Markup)
|
||||
rs.Value.summary = page.Summary{
|
||||
Text: helpers.BytesToHTML(html),
|
||||
Type: page.SummaryTypeFrontMatter,
|
||||
}
|
||||
}
|
||||
|
||||
return rs, err
|
||||
})
|
||||
if err != nil {
|
||||
return contentSummary{}, cp.po.p.wrapError(err)
|
||||
@@ -614,8 +657,8 @@ func (c *cachedContent) contentRendered(ctx context.Context, cp *pageContentOutp
|
||||
return v.Value, nil
|
||||
}
|
||||
|
||||
func (c *cachedContent) mustContentToC(ctx context.Context, cp *pageContentOutput) contentTableOfContents {
|
||||
ct, err := c.contentToC(ctx, cp)
|
||||
func (c *cachedContentScope) mustContentToC(ctx context.Context) contentTableOfContents {
|
||||
ct, err := c.contentToC(ctx)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -624,8 +667,9 @@ func (c *cachedContent) mustContentToC(ctx context.Context, cp *pageContentOutpu
|
||||
|
||||
var setGetContentCallbackInContext = hcontext.NewContextDispatcher[func(*pageContentOutput, contentTableOfContents)]("contentCallback")
|
||||
|
||||
func (c *cachedContent) contentToC(ctx context.Context, cp *pageContentOutput) (contentTableOfContents, error) {
|
||||
key := c.pi.sourceKey + "/" + cp.po.f.Name
|
||||
func (c *cachedContentScope) contentToC(ctx context.Context) (contentTableOfContents, error) {
|
||||
cp := c.pco
|
||||
key := c.pi.sourceKey + "/" + c.keyScope(ctx)
|
||||
versionv := c.version(cp)
|
||||
|
||||
v, err := c.pm.contentTableOfContents.GetOrCreate(key, func(string) (*resources.StaleValue[contentTableOfContents], error) {
|
||||
@@ -648,7 +692,7 @@ func (c *cachedContent) contentToC(ctx context.Context, cp *pageContentOutput) (
|
||||
|
||||
// Callback called from below (e.g. in .RenderString)
|
||||
ctxCallback := func(cp2 *pageContentOutput, ct2 contentTableOfContents) {
|
||||
cp.otherOutputs[cp2.po.p.pid] = cp2
|
||||
cp.otherOutputs.Set(cp2.po.p.pid, cp2)
|
||||
|
||||
// Merge content placeholders
|
||||
for k, v := range ct2.contentPlaceholders {
|
||||
@@ -749,8 +793,9 @@ func (c *cachedContent) version(cp *pageContentOutput) uint32 {
|
||||
return c.StaleVersion() + cp.contentRenderedVersion
|
||||
}
|
||||
|
||||
func (c *cachedContent) contentPlain(ctx context.Context, cp *pageContentOutput) (contentPlainPlainWords, error) {
|
||||
key := c.pi.sourceKey + "/" + cp.po.f.Name
|
||||
func (c *cachedContentScope) contentPlain(ctx context.Context) (contentPlainPlainWords, error) {
|
||||
cp := c.pco
|
||||
key := c.pi.sourceKey + "/" + c.keyScope(ctx)
|
||||
|
||||
versionv := c.version(cp)
|
||||
|
||||
@@ -762,7 +807,7 @@ func (c *cachedContent) contentPlain(ctx context.Context, cp *pageContentOutput)
|
||||
},
|
||||
}
|
||||
|
||||
rendered, err := c.contentRendered(ctx, cp)
|
||||
rendered, err := c.contentRendered(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -797,28 +842,6 @@ func (c *cachedContent) contentPlain(ctx context.Context, cp *pageContentOutput)
|
||||
result.readingTime = (result.wordCount + 212) / 213
|
||||
}
|
||||
|
||||
if c.pi.hasSummaryDivider || rendered.summary != "" {
|
||||
result.summary = rendered.summary
|
||||
result.summaryTruncated = rendered.summaryTruncated
|
||||
} else if cp.po.p.m.pageConfig.Summary != "" {
|
||||
b, err := cp.po.contentRenderer.ParseAndRenderContent(ctx, []byte(cp.po.p.m.pageConfig.Summary), false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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
|
||||
var truncated bool
|
||||
if isCJKLanguage {
|
||||
summary, truncated = cp.po.p.s.ContentSpec.TruncateWordsByRune(result.plainWords)
|
||||
} else {
|
||||
summary, truncated = cp.po.p.s.ContentSpec.TruncateWordsToWholeSentence(result.plain)
|
||||
}
|
||||
result.summary = template.HTML(summary)
|
||||
result.summaryTruncated = truncated
|
||||
}
|
||||
|
||||
rs.Value = result
|
||||
|
||||
return rs, nil
|
||||
@@ -831,3 +854,332 @@ func (c *cachedContent) contentPlain(ctx context.Context, cp *pageContentOutput)
|
||||
}
|
||||
return v.Value, nil
|
||||
}
|
||||
|
||||
type cachedContentScope struct {
|
||||
*cachedContent
|
||||
pco *pageContentOutput
|
||||
scope string
|
||||
}
|
||||
|
||||
func (c *cachedContentScope) prepareContext(ctx context.Context) context.Context {
|
||||
// The markup scope is recursive, so if already set to a non zero value, preserve that value.
|
||||
if s := hugo.GetMarkupScope(ctx); s != "" || s == c.scope {
|
||||
return ctx
|
||||
}
|
||||
return hugo.SetMarkupScope(ctx, c.scope)
|
||||
}
|
||||
|
||||
func (c *cachedContentScope) Render(ctx context.Context) (page.Content, error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *cachedContentScope) Content(ctx context.Context) (template.HTML, error) {
|
||||
ctx = c.prepareContext(ctx)
|
||||
cr, err := c.contentRendered(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return template.HTML(cr.content), nil
|
||||
}
|
||||
|
||||
func (c *cachedContentScope) ContentWithoutSummary(ctx context.Context) (template.HTML, error) {
|
||||
ctx = c.prepareContext(ctx)
|
||||
cr, err := c.contentRendered(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return cr.contentWithoutSummary, nil
|
||||
}
|
||||
|
||||
func (c *cachedContentScope) Summary(ctx context.Context) (page.Summary, error) {
|
||||
ctx = c.prepareContext(ctx)
|
||||
rendered, err := c.contentRendered(ctx)
|
||||
return rendered.summary, err
|
||||
}
|
||||
|
||||
func (c *cachedContentScope) RenderString(ctx context.Context, args ...any) (template.HTML, error) {
|
||||
ctx = c.prepareContext(ctx)
|
||||
|
||||
if len(args) < 1 || len(args) > 2 {
|
||||
return "", errors.New("want 1 or 2 arguments")
|
||||
}
|
||||
|
||||
pco := c.pco
|
||||
|
||||
var contentToRender string
|
||||
opts := defaultRenderStringOpts
|
||||
sidx := 1
|
||||
|
||||
if len(args) == 1 {
|
||||
sidx = 0
|
||||
} else {
|
||||
m, ok := args[0].(map[string]any)
|
||||
if !ok {
|
||||
return "", errors.New("first argument must be a map")
|
||||
}
|
||||
|
||||
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]
|
||||
|
||||
if _, ok := contentToRenderv.(hstring.RenderedHTML); ok {
|
||||
// This content is already rendered, this is potentially
|
||||
// a infinite recursion.
|
||||
return "", errors.New("text is already rendered, repeating it may cause infinite recursion")
|
||||
}
|
||||
|
||||
var err error
|
||||
contentToRender, err = cast.ToStringE(contentToRenderv)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err = pco.initRenderHooks(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
conv := pco.po.p.getContentConverter()
|
||||
|
||||
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 {
|
||||
return "", pco.po.p.wrapError(err)
|
||||
}
|
||||
}
|
||||
|
||||
var rendered []byte
|
||||
|
||||
parseInfo := &contentParseInfo{
|
||||
h: pco.po.p.s.h,
|
||||
pid: pco.po.p.pid,
|
||||
}
|
||||
|
||||
if pageparser.HasShortcode(contentToRender) {
|
||||
contentToRenderb := []byte(contentToRender)
|
||||
// String contains a shortcode.
|
||||
parseInfo.itemsStep1, err = pageparser.ParseBytes(contentToRenderb, pageparser.Config{
|
||||
NoFrontMatter: true,
|
||||
NoSummaryDivider: true,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
s := newShortcodeHandler(pco.po.p.pathOrTitle(), pco.po.p.s)
|
||||
if err := parseInfo.mapItemsAfterFrontMatter(contentToRenderb, s); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
placeholders, err := s.prepareShortcodesForPage(ctx, pco.po.p, pco.po.f, true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
contentToRender, hasVariants, err := parseInfo.contentToRender(ctx, contentToRenderb, placeholders)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if hasVariants {
|
||||
pco.po.p.pageOutputTemplateVariationsState.Add(1)
|
||||
}
|
||||
b, err := pco.renderContentWithConverter(ctx, conv, contentToRender, false)
|
||||
if err != nil {
|
||||
return "", pco.po.p.wrapError(err)
|
||||
}
|
||||
rendered = b.Bytes()
|
||||
|
||||
if parseInfo.hasNonMarkdownShortcode {
|
||||
var hasShortcodeVariants bool
|
||||
|
||||
tokenHandler := func(ctx context.Context, token string) ([]byte, error) {
|
||||
if token == tocShortcodePlaceholder {
|
||||
toc, err := c.contentToC(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// The Page's TableOfContents was accessed in a shortcode.
|
||||
return []byte(toc.tableOfContentsHTML), nil
|
||||
}
|
||||
renderer, found := placeholders[token]
|
||||
if found {
|
||||
repl, more, err := renderer.renderShortcode(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hasShortcodeVariants = hasShortcodeVariants || more
|
||||
return repl, nil
|
||||
}
|
||||
// This should not happen.
|
||||
return nil, fmt.Errorf("unknown shortcode token %q", token)
|
||||
}
|
||||
|
||||
rendered, err = expandShortcodeTokens(ctx, rendered, tokenHandler)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if hasShortcodeVariants {
|
||||
pco.po.p.pageOutputTemplateVariationsState.Add(1)
|
||||
}
|
||||
}
|
||||
|
||||
// We need a consolidated view in $page.HasShortcode
|
||||
pco.po.p.m.content.shortcodeState.transferNames(s)
|
||||
|
||||
} else {
|
||||
c, err := pco.renderContentWithConverter(ctx, conv, []byte(contentToRender), false)
|
||||
if err != nil {
|
||||
return "", pco.po.p.wrapError(err)
|
||||
}
|
||||
|
||||
rendered = c.Bytes()
|
||||
}
|
||||
|
||||
if opts.Display == "inline" {
|
||||
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
|
||||
}
|
||||
|
||||
func (c *cachedContentScope) RenderShortcodes(ctx context.Context) (template.HTML, error) {
|
||||
ctx = c.prepareContext(ctx)
|
||||
|
||||
pco := c.pco
|
||||
content := pco.po.p.m.content
|
||||
|
||||
source, err := content.pi.contentSource(content)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ct, err := c.contentToC(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var insertPlaceholders bool
|
||||
var hasVariants bool
|
||||
cb := setGetContentCallbackInContext.Get(ctx)
|
||||
if cb != nil {
|
||||
insertPlaceholders = true
|
||||
}
|
||||
cc := make([]byte, 0, len(source)+(len(source)/10))
|
||||
for _, it := range content.pi.itemsStep2 {
|
||||
switch v := it.(type) {
|
||||
case pageparser.Item:
|
||||
cc = append(cc, source[v.Pos():v.Pos()+len(v.Val(source))]...)
|
||||
case pageContentReplacement:
|
||||
// Ignore.
|
||||
case *shortcode:
|
||||
if !insertPlaceholders || !v.insertPlaceholder() {
|
||||
// Insert the rendered shortcode.
|
||||
renderedShortcode, found := ct.contentPlaceholders[v.placeholder]
|
||||
if !found {
|
||||
// This should never happen.
|
||||
panic(fmt.Sprintf("rendered shortcode %q not found", v.placeholder))
|
||||
}
|
||||
|
||||
b, more, err := renderedShortcode.renderShortcode(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to render shortcode: %w", err)
|
||||
}
|
||||
hasVariants = hasVariants || more
|
||||
cc = append(cc, []byte(b)...)
|
||||
|
||||
} else {
|
||||
// Insert the placeholder so we can insert the content after
|
||||
// markdown processing.
|
||||
cc = append(cc, []byte(v.placeholder)...)
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown item type %T", it))
|
||||
}
|
||||
}
|
||||
|
||||
if hasVariants {
|
||||
pco.po.p.pageOutputTemplateVariationsState.Add(1)
|
||||
}
|
||||
|
||||
if cb != nil {
|
||||
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(cc, pco.po.p.pid)), nil
|
||||
}
|
||||
|
||||
return helpers.BytesToHTML(cc), nil
|
||||
}
|
||||
|
||||
func (c *cachedContentScope) Plain(ctx context.Context) string {
|
||||
ctx = c.prepareContext(ctx)
|
||||
return c.mustContentPlain(ctx).plain
|
||||
}
|
||||
|
||||
func (c *cachedContentScope) PlainWords(ctx context.Context) []string {
|
||||
ctx = c.prepareContext(ctx)
|
||||
return c.mustContentPlain(ctx).plainWords
|
||||
}
|
||||
|
||||
func (c *cachedContentScope) WordCount(ctx context.Context) int {
|
||||
ctx = c.prepareContext(ctx)
|
||||
return c.mustContentPlain(ctx).wordCount
|
||||
}
|
||||
|
||||
func (c *cachedContentScope) FuzzyWordCount(ctx context.Context) int {
|
||||
ctx = c.prepareContext(ctx)
|
||||
return c.mustContentPlain(ctx).fuzzyWordCount
|
||||
}
|
||||
|
||||
func (c *cachedContentScope) ReadingTime(ctx context.Context) int {
|
||||
ctx = c.prepareContext(ctx)
|
||||
return c.mustContentPlain(ctx).readingTime
|
||||
}
|
||||
|
||||
func (c *cachedContentScope) Len(ctx context.Context) int {
|
||||
ctx = c.prepareContext(ctx)
|
||||
return len(c.mustContentRendered(ctx).content)
|
||||
}
|
||||
|
||||
func (c *cachedContentScope) Fragments(ctx context.Context) *tableofcontents.Fragments {
|
||||
ctx = c.prepareContext(ctx)
|
||||
toc := c.mustContentToC(ctx).tableOfContents
|
||||
if toc == nil {
|
||||
return nil
|
||||
}
|
||||
return toc
|
||||
}
|
||||
|
||||
func (c *cachedContentScope) fragmentsHTML(ctx context.Context) template.HTML {
|
||||
ctx = c.prepareContext(ctx)
|
||||
return c.mustContentToC(ctx).tableOfContentsHTML
|
||||
}
|
||||
|
||||
func (c *cachedContentScope) mustContentPlain(ctx context.Context) contentPlainPlainWords {
|
||||
r, err := c.contentPlain(ctx)
|
||||
if err != nil {
|
||||
c.pco.fail(err)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (c *cachedContentScope) mustContentRendered(ctx context.Context) contentSummary {
|
||||
r, err := c.contentRendered(ctx)
|
||||
if err != nil {
|
||||
c.pco.fail(err)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -821,7 +821,7 @@ func (p *pageMeta) newContentConverter(ps *pageState, markup string) (converter.
|
||||
// This prevents infinite recursion in some cases.
|
||||
return doc
|
||||
}
|
||||
if v, ok := ps.pageOutput.pco.otherOutputs[id]; ok {
|
||||
if v, ok := ps.pageOutput.pco.otherOutputs.Get(id); ok {
|
||||
return v.po.p
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -65,6 +65,7 @@ func newPageOutput(
|
||||
p: ps,
|
||||
f: f,
|
||||
pagePerOutputProviders: providers,
|
||||
MarkupProvider: page.NopPage,
|
||||
ContentProvider: page.NopPage,
|
||||
PageRenderProvider: page.NopPage,
|
||||
TableOfContentsProvider: page.NopPage,
|
||||
@@ -95,6 +96,7 @@ type pageOutput struct {
|
||||
// output format.
|
||||
contentRenderer page.ContentRenderer
|
||||
pagePerOutputProviders
|
||||
page.MarkupProvider
|
||||
page.ContentProvider
|
||||
page.PageRenderProvider
|
||||
page.TableOfContentsProvider
|
||||
@@ -119,7 +121,7 @@ func (po *pageOutput) isRendered() bool {
|
||||
if po.renderState > 0 {
|
||||
return true
|
||||
}
|
||||
if po.pco != nil && po.pco.contentRendered {
|
||||
if po.pco != nil && po.pco.contentRendered.Load() {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -139,6 +141,7 @@ func (p *pageOutput) setContentProvider(cp *pageContentOutput) {
|
||||
}
|
||||
p.contentRenderer = cp
|
||||
p.ContentProvider = cp
|
||||
p.MarkupProvider = cp
|
||||
p.PageRenderProvider = cp
|
||||
p.TableOfContentsProvider = cp
|
||||
p.RenderShortcodesProvider = cp
|
||||
|
||||
+120
-360
@@ -21,18 +21,14 @@ import (
|
||||
"html/template"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
"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"
|
||||
|
||||
@@ -41,7 +37,6 @@ import (
|
||||
bp "github.com/gohugoio/hugo/bufferpool"
|
||||
"github.com/gohugoio/hugo/tpl"
|
||||
|
||||
"github.com/gohugoio/hugo/helpers"
|
||||
"github.com/gohugoio/hugo/output"
|
||||
"github.com/gohugoio/hugo/resources/page"
|
||||
"github.com/gohugoio/hugo/resources/resource"
|
||||
@@ -73,7 +68,7 @@ func newPageContentOutput(po *pageOutput) (*pageContentOutput, error) {
|
||||
cp := &pageContentOutput{
|
||||
po: po,
|
||||
renderHooks: &renderHooks{},
|
||||
otherOutputs: make(map[uint64]*pageContentOutput),
|
||||
otherOutputs: maps.NewCache[uint64, *pageContentOutput](),
|
||||
}
|
||||
return cp, nil
|
||||
}
|
||||
@@ -89,10 +84,10 @@ type pageContentOutput struct {
|
||||
|
||||
// Other pages involved in rendering of this page,
|
||||
// typically included with .RenderShortcodes.
|
||||
otherOutputs map[uint64]*pageContentOutput
|
||||
otherOutputs *maps.Cache[uint64, *pageContentOutput]
|
||||
|
||||
contentRenderedVersion uint32 // Incremented on reset.
|
||||
contentRendered bool // Set on content render.
|
||||
contentRenderedVersion uint32 // Incremented on reset.
|
||||
contentRendered atomic.Bool // Set on content render.
|
||||
|
||||
// Renders Markdown hooks.
|
||||
renderHooks *renderHooks
|
||||
@@ -107,294 +102,10 @@ func (pco *pageContentOutput) Reset() {
|
||||
return
|
||||
}
|
||||
pco.contentRenderedVersion++
|
||||
pco.contentRendered = false
|
||||
pco.contentRendered.Store(false)
|
||||
pco.renderHooks = &renderHooks{}
|
||||
}
|
||||
|
||||
func (pco *pageContentOutput) Fragments(ctx context.Context) *tableofcontents.Fragments {
|
||||
return pco.po.p.m.content.mustContentToC(ctx, pco).tableOfContents
|
||||
}
|
||||
|
||||
func (pco *pageContentOutput) RenderShortcodes(ctx context.Context) (template.HTML, error) {
|
||||
content := pco.po.p.m.content
|
||||
source, err := content.pi.contentSource(content)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ct, err := content.contentToC(ctx, pco)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var insertPlaceholders bool
|
||||
var hasVariants bool
|
||||
cb := setGetContentCallbackInContext.Get(ctx)
|
||||
if cb != nil {
|
||||
insertPlaceholders = true
|
||||
}
|
||||
c := make([]byte, 0, len(source)+(len(source)/10))
|
||||
for _, it := range content.pi.itemsStep2 {
|
||||
switch v := it.(type) {
|
||||
case pageparser.Item:
|
||||
c = append(c, source[v.Pos():v.Pos()+len(v.Val(source))]...)
|
||||
case pageContentReplacement:
|
||||
// Ignore.
|
||||
case *shortcode:
|
||||
if !insertPlaceholders || !v.insertPlaceholder() {
|
||||
// Insert the rendered shortcode.
|
||||
renderedShortcode, found := ct.contentPlaceholders[v.placeholder]
|
||||
if !found {
|
||||
// This should never happen.
|
||||
panic(fmt.Sprintf("rendered shortcode %q not found", v.placeholder))
|
||||
}
|
||||
|
||||
b, more, err := renderedShortcode.renderShortcode(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to render shortcode: %w", err)
|
||||
}
|
||||
hasVariants = hasVariants || more
|
||||
c = append(c, []byte(b)...)
|
||||
|
||||
} else {
|
||||
// Insert the placeholder so we can insert the content after
|
||||
// markdown processing.
|
||||
c = append(c, []byte(v.placeholder)...)
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown item type %T", it))
|
||||
}
|
||||
}
|
||||
|
||||
if hasVariants {
|
||||
pco.po.p.pageOutputTemplateVariationsState.Add(1)
|
||||
}
|
||||
|
||||
if cb != nil {
|
||||
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
|
||||
}
|
||||
|
||||
func (pco *pageContentOutput) Content(ctx context.Context) (any, error) {
|
||||
r, err := pco.po.p.m.content.contentRendered(ctx, pco)
|
||||
return r.content, err
|
||||
}
|
||||
|
||||
func (pco *pageContentOutput) TableOfContents(ctx context.Context) template.HTML {
|
||||
return pco.po.p.m.content.mustContentToC(ctx, pco).tableOfContentsHTML
|
||||
}
|
||||
|
||||
func (p *pageContentOutput) Len(ctx context.Context) int {
|
||||
return len(p.mustContentRendered(ctx).content)
|
||||
}
|
||||
|
||||
func (pco *pageContentOutput) mustContentRendered(ctx context.Context) contentSummary {
|
||||
r, err := pco.po.p.m.content.contentRendered(ctx, pco)
|
||||
if err != nil {
|
||||
pco.fail(err)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (pco *pageContentOutput) mustContentPlain(ctx context.Context) contentPlainPlainWords {
|
||||
r, err := pco.po.p.m.content.contentPlain(ctx, pco)
|
||||
if err != nil {
|
||||
pco.fail(err)
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (pco *pageContentOutput) fail(err error) {
|
||||
pco.po.p.s.h.FatalError(pco.po.p.wrapError(err))
|
||||
}
|
||||
|
||||
func (pco *pageContentOutput) Plain(ctx context.Context) string {
|
||||
return pco.mustContentPlain(ctx).plain
|
||||
}
|
||||
|
||||
func (pco *pageContentOutput) PlainWords(ctx context.Context) []string {
|
||||
return pco.mustContentPlain(ctx).plainWords
|
||||
}
|
||||
|
||||
func (pco *pageContentOutput) ReadingTime(ctx context.Context) int {
|
||||
return pco.mustContentPlain(ctx).readingTime
|
||||
}
|
||||
|
||||
func (pco *pageContentOutput) WordCount(ctx context.Context) int {
|
||||
return pco.mustContentPlain(ctx).wordCount
|
||||
}
|
||||
|
||||
func (pco *pageContentOutput) FuzzyWordCount(ctx context.Context) int {
|
||||
return pco.mustContentPlain(ctx).fuzzyWordCount
|
||||
}
|
||||
|
||||
func (pco *pageContentOutput) Summary(ctx context.Context) template.HTML {
|
||||
return pco.mustContentPlain(ctx).summary
|
||||
}
|
||||
|
||||
func (pco *pageContentOutput) Truncated(ctx context.Context) bool {
|
||||
return pco.mustContentPlain(ctx).summaryTruncated
|
||||
}
|
||||
|
||||
func (pco *pageContentOutput) RenderString(ctx context.Context, args ...any) (template.HTML, error) {
|
||||
if len(args) < 1 || len(args) > 2 {
|
||||
return "", errors.New("want 1 or 2 arguments")
|
||||
}
|
||||
|
||||
var contentToRender string
|
||||
opts := defaultRenderStringOpts
|
||||
sidx := 1
|
||||
|
||||
if len(args) == 1 {
|
||||
sidx = 0
|
||||
} else {
|
||||
m, ok := args[0].(map[string]any)
|
||||
if !ok {
|
||||
return "", errors.New("first argument must be a map")
|
||||
}
|
||||
|
||||
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]
|
||||
|
||||
if _, ok := contentToRenderv.(hstring.RenderedString); ok {
|
||||
// This content is already rendered, this is potentially
|
||||
// a infinite recursion.
|
||||
return "", errors.New("text is already rendered, repeating it may cause infinite recursion")
|
||||
}
|
||||
|
||||
var err error
|
||||
contentToRender, err = cast.ToStringE(contentToRenderv)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err = pco.initRenderHooks(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
conv := pco.po.p.getContentConverter()
|
||||
|
||||
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 {
|
||||
return "", pco.po.p.wrapError(err)
|
||||
}
|
||||
}
|
||||
|
||||
var rendered []byte
|
||||
|
||||
parseInfo := &contentParseInfo{
|
||||
h: pco.po.p.s.h,
|
||||
pid: pco.po.p.pid,
|
||||
}
|
||||
|
||||
if pageparser.HasShortcode(contentToRender) {
|
||||
contentToRenderb := []byte(contentToRender)
|
||||
// String contains a shortcode.
|
||||
parseInfo.itemsStep1, err = pageparser.ParseBytes(contentToRenderb, pageparser.Config{
|
||||
NoFrontMatter: true,
|
||||
NoSummaryDivider: true,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
s := newShortcodeHandler(pco.po.p.pathOrTitle(), pco.po.p.s)
|
||||
if err := parseInfo.mapItemsAfterFrontMatter(contentToRenderb, s); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
placeholders, err := s.prepareShortcodesForPage(ctx, pco.po.p, pco.po.f, true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
contentToRender, hasVariants, err := parseInfo.contentToRender(ctx, contentToRenderb, placeholders)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if hasVariants {
|
||||
pco.po.p.pageOutputTemplateVariationsState.Add(1)
|
||||
}
|
||||
b, err := pco.renderContentWithConverter(ctx, conv, contentToRender, false)
|
||||
if err != nil {
|
||||
return "", pco.po.p.wrapError(err)
|
||||
}
|
||||
rendered = b.Bytes()
|
||||
|
||||
if parseInfo.hasNonMarkdownShortcode {
|
||||
var hasShortcodeVariants bool
|
||||
|
||||
tokenHandler := func(ctx context.Context, token string) ([]byte, error) {
|
||||
if token == tocShortcodePlaceholder {
|
||||
toc, err := pco.po.p.m.content.contentToC(ctx, pco)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// The Page's TableOfContents was accessed in a shortcode.
|
||||
return []byte(toc.tableOfContentsHTML), nil
|
||||
}
|
||||
renderer, found := placeholders[token]
|
||||
if found {
|
||||
repl, more, err := renderer.renderShortcode(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hasShortcodeVariants = hasShortcodeVariants || more
|
||||
return repl, nil
|
||||
}
|
||||
// This should not happen.
|
||||
return nil, fmt.Errorf("unknown shortcode token %q", token)
|
||||
}
|
||||
|
||||
rendered, err = expandShortcodeTokens(ctx, rendered, tokenHandler)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if hasShortcodeVariants {
|
||||
pco.po.p.pageOutputTemplateVariationsState.Add(1)
|
||||
}
|
||||
}
|
||||
|
||||
// We need a consolidated view in $page.HasShortcode
|
||||
pco.po.p.m.content.shortcodeState.transferNames(s)
|
||||
|
||||
} else {
|
||||
c, err := pco.renderContentWithConverter(ctx, conv, []byte(contentToRender), false)
|
||||
if err != nil {
|
||||
return "", pco.po.p.wrapError(err)
|
||||
}
|
||||
|
||||
rendered = c.Bytes()
|
||||
}
|
||||
|
||||
if opts.Display == "inline" {
|
||||
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
|
||||
}
|
||||
|
||||
func (pco *pageContentOutput) Render(ctx context.Context, layout ...string) (template.HTML, error) {
|
||||
if len(layout) == 0 {
|
||||
return "", errors.New("no layout given")
|
||||
@@ -416,6 +127,105 @@ func (pco *pageContentOutput) Render(ctx context.Context, layout ...string) (tem
|
||||
return template.HTML(res), nil
|
||||
}
|
||||
|
||||
func (pco *pageContentOutput) Fragments(ctx context.Context) *tableofcontents.Fragments {
|
||||
return pco.c().Fragments(ctx)
|
||||
}
|
||||
|
||||
func (pco *pageContentOutput) RenderShortcodes(ctx context.Context) (template.HTML, error) {
|
||||
return pco.c().RenderShortcodes(ctx)
|
||||
}
|
||||
|
||||
func (pco *pageContentOutput) Markup(opts ...any) page.Markup {
|
||||
if len(opts) > 1 {
|
||||
panic("too many arguments, expected 0 or 1")
|
||||
}
|
||||
var scope string
|
||||
if len(opts) == 1 {
|
||||
scope = cast.ToString(opts[0])
|
||||
}
|
||||
return pco.po.p.m.content.getOrCreateScope(scope, pco)
|
||||
}
|
||||
|
||||
func (pco *pageContentOutput) c() page.Markup {
|
||||
return pco.po.p.m.content.getOrCreateScope("", pco)
|
||||
}
|
||||
|
||||
func (pco *pageContentOutput) Content(ctx context.Context) (any, error) {
|
||||
r, err := pco.c().Render(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.Content(ctx)
|
||||
}
|
||||
|
||||
func (pco *pageContentOutput) ContentWithoutSummary(ctx context.Context) (template.HTML, error) {
|
||||
r, err := pco.c().Render(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return r.ContentWithoutSummary(ctx)
|
||||
}
|
||||
|
||||
func (pco *pageContentOutput) TableOfContents(ctx context.Context) template.HTML {
|
||||
return pco.c().(*cachedContentScope).fragmentsHTML(ctx)
|
||||
}
|
||||
|
||||
func (pco *pageContentOutput) Len(ctx context.Context) int {
|
||||
return pco.mustRender(ctx).Len(ctx)
|
||||
}
|
||||
|
||||
func (pco *pageContentOutput) mustRender(ctx context.Context) page.Content {
|
||||
c, err := pco.c().Render(ctx)
|
||||
if err != nil {
|
||||
pco.fail(err)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
func (pco *pageContentOutput) fail(err error) {
|
||||
pco.po.p.s.h.FatalError(pco.po.p.wrapError(err))
|
||||
}
|
||||
|
||||
func (pco *pageContentOutput) Plain(ctx context.Context) string {
|
||||
return pco.mustRender(ctx).Plain(ctx)
|
||||
}
|
||||
|
||||
func (pco *pageContentOutput) PlainWords(ctx context.Context) []string {
|
||||
return pco.mustRender(ctx).PlainWords(ctx)
|
||||
}
|
||||
|
||||
func (pco *pageContentOutput) ReadingTime(ctx context.Context) int {
|
||||
return pco.mustRender(ctx).ReadingTime(ctx)
|
||||
}
|
||||
|
||||
func (pco *pageContentOutput) WordCount(ctx context.Context) int {
|
||||
return pco.mustRender(ctx).WordCount(ctx)
|
||||
}
|
||||
|
||||
func (pco *pageContentOutput) FuzzyWordCount(ctx context.Context) int {
|
||||
return pco.mustRender(ctx).FuzzyWordCount(ctx)
|
||||
}
|
||||
|
||||
func (pco *pageContentOutput) Summary(ctx context.Context) template.HTML {
|
||||
summary, err := pco.mustRender(ctx).Summary(ctx)
|
||||
if err != nil {
|
||||
pco.fail(err)
|
||||
}
|
||||
return summary.Text
|
||||
}
|
||||
|
||||
func (pco *pageContentOutput) Truncated(ctx context.Context) bool {
|
||||
summary, err := pco.mustRender(ctx).Summary(ctx)
|
||||
if err != nil {
|
||||
pco.fail(err)
|
||||
}
|
||||
return summary.Truncated
|
||||
}
|
||||
|
||||
func (pco *pageContentOutput) RenderString(ctx context.Context, args ...any) (template.HTML, error) {
|
||||
return pco.c().RenderString(ctx, args...)
|
||||
}
|
||||
|
||||
func (pco *pageContentOutput) initRenderHooks() error {
|
||||
if pco == nil {
|
||||
return nil
|
||||
@@ -486,6 +296,8 @@ func (pco *pageContentOutput) initRenderHooks() error {
|
||||
if id != nil {
|
||||
layoutDescriptor.KindVariants = id.(string)
|
||||
}
|
||||
case hooks.TableRendererType:
|
||||
layoutDescriptor.Kind = "render-table"
|
||||
case hooks.CodeBlockRendererType:
|
||||
layoutDescriptor.Kind = "render-codeblock"
|
||||
if id != nil {
|
||||
@@ -524,13 +336,23 @@ func (pco *pageContentOutput) initRenderHooks() error {
|
||||
|
||||
templ, found1 := getHookTemplate(pco.po.f)
|
||||
|
||||
if pco.po.p.reusePageOutputContent() {
|
||||
if !found1 || pco.po.p.reusePageOutputContent() {
|
||||
// Some hooks may only be available in HTML, and if
|
||||
// this site is configured to not have HTML output, we need to
|
||||
// make sure we have a fallback. This should be very rare.
|
||||
candidates := pco.po.p.s.renderFormats
|
||||
if pco.po.f.MediaType.FirstSuffix.Suffix != "html" {
|
||||
if _, found := candidates.GetBySuffix("html"); !found {
|
||||
candidates = append(candidates, output.HTMLFormat)
|
||||
}
|
||||
}
|
||||
// Check if some of the other output formats would give a different template.
|
||||
for _, f := range pco.po.p.s.renderFormats {
|
||||
for _, f := range candidates {
|
||||
if f.Name == pco.po.f.Name {
|
||||
continue
|
||||
}
|
||||
templ2, found2 := getHookTemplate(f)
|
||||
|
||||
if found2 {
|
||||
if !found1 {
|
||||
templ = templ2
|
||||
@@ -660,65 +482,3 @@ func executeToString(ctx context.Context, h tpl.TemplateHandler, templ tpl.Templ
|
||||
}
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
func splitUserDefinedSummaryAndContent(markup string, c []byte) (summary []byte, content []byte, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = fmt.Errorf("summary split failed: %s", r)
|
||||
}
|
||||
}()
|
||||
|
||||
startDivider := bytes.Index(c, internalSummaryDividerBaseBytes)
|
||||
|
||||
if startDivider == -1 {
|
||||
return
|
||||
}
|
||||
|
||||
startTag := "p"
|
||||
switch markup {
|
||||
case media.DefaultContentTypes.AsciiDoc.SubType:
|
||||
startTag = "div"
|
||||
}
|
||||
|
||||
// Walk back and forward to the surrounding tags.
|
||||
start := bytes.LastIndex(c[:startDivider], []byte("<"+startTag))
|
||||
end := bytes.Index(c[startDivider:], []byte("</"+startTag))
|
||||
|
||||
if start == -1 {
|
||||
start = startDivider
|
||||
} else {
|
||||
start = startDivider - (startDivider - start)
|
||||
}
|
||||
|
||||
if end == -1 {
|
||||
end = startDivider + len(internalSummaryDividerBase)
|
||||
} else {
|
||||
end = startDivider + end + len(startTag) + 3
|
||||
}
|
||||
|
||||
var addDiv bool
|
||||
|
||||
switch markup {
|
||||
case "rst":
|
||||
addDiv = true
|
||||
}
|
||||
|
||||
withoutDivider := append(c[:start], bytes.Trim(c[end:], "\n")...)
|
||||
|
||||
if len(withoutDivider) > 0 {
|
||||
summary = bytes.TrimSpace(withoutDivider[:start])
|
||||
}
|
||||
|
||||
if addDiv {
|
||||
// For the rst
|
||||
summary = append(append([]byte(nil), summary...), []byte("</div>")...)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
content = bytes.TrimSpace(withoutDivider)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
+4
-200
@@ -63,15 +63,6 @@ Summary Next Line
|
||||
|
||||
<!--more-->
|
||||
Some more text
|
||||
`
|
||||
|
||||
simplePageWithBlankSummary = `---
|
||||
title: SimpleWithBlankSummary
|
||||
---
|
||||
|
||||
<!--more-->
|
||||
|
||||
Some text.
|
||||
`
|
||||
|
||||
simplePageWithSummaryParameter = `---
|
||||
@@ -322,7 +313,8 @@ func checkPageTOC(t *testing.T, page page.Page, toc string) {
|
||||
}
|
||||
|
||||
func checkPageSummary(t *testing.T, page page.Page, summary string, msg ...any) {
|
||||
a := normalizeContent(string(page.Summary(context.Background())))
|
||||
s := string(page.Summary(context.Background()))
|
||||
a := normalizeContent(s)
|
||||
b := normalizeContent(summary)
|
||||
if a != b {
|
||||
t.Fatalf("Page summary is:\n%q.\nExpected\n%q (%q)", a, b, msg)
|
||||
@@ -593,26 +585,6 @@ date: 2012-01-12
|
||||
b.Assert(s.Site().Lastmod().Year(), qt.Equals, 2018)
|
||||
}
|
||||
|
||||
func TestCreateNewPage(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
assertFunc := func(t *testing.T, ext string, pages page.Pages) {
|
||||
p := pages[0]
|
||||
|
||||
// issue #2290: Path is relative to the content dir and will continue to be so.
|
||||
c.Assert(p.File().Path(), qt.Equals, fmt.Sprintf("p0.%s", ext))
|
||||
c.Assert(p.IsHome(), qt.Equals, false)
|
||||
checkPageTitle(t, p, "Simple")
|
||||
checkPageContent(t, p, normalizeExpected(ext, "<p>Simple Page</p>\n"))
|
||||
checkPageSummary(t, p, "Simple Page")
|
||||
checkPageType(t, p, "page")
|
||||
}
|
||||
|
||||
settings := map[string]any{}
|
||||
|
||||
testAllMarkdownEnginesForPages(t, assertFunc, settings, simplePage)
|
||||
}
|
||||
|
||||
func TestPageSummary(t *testing.T) {
|
||||
t.Parallel()
|
||||
assertFunc := func(t *testing.T, ext string, pages page.Pages) {
|
||||
@@ -621,7 +593,7 @@ func TestPageSummary(t *testing.T) {
|
||||
// Source is not Asciidoctor- or RST-compatible so don't test them
|
||||
if ext != "ad" && ext != "rst" {
|
||||
checkPageContent(t, p, normalizeExpected(ext, "<p><a href=\"https://lipsum.com/\">Lorem ipsum</a> dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>\n\n<p>Additional text.</p>\n\n<p>Further text.</p>\n"), ext)
|
||||
checkPageSummary(t, p, normalizeExpected(ext, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Additional text."), ext)
|
||||
checkPageSummary(t, p, normalizeExpected(ext, "<p><a href=\"https://lipsum.com/\">Lorem ipsum</a> dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>"), ext)
|
||||
}
|
||||
checkPageType(t, p, "page")
|
||||
}
|
||||
@@ -642,19 +614,6 @@ func TestPageWithDelimiter(t *testing.T) {
|
||||
testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithSummaryDelimiter)
|
||||
}
|
||||
|
||||
func TestPageWithBlankSummary(t *testing.T) {
|
||||
t.Parallel()
|
||||
assertFunc := func(t *testing.T, ext string, pages page.Pages) {
|
||||
p := pages[0]
|
||||
checkPageTitle(t, p, "SimpleWithBlankSummary")
|
||||
checkPageContent(t, p, normalizeExpected(ext, "<p>Some text.</p>\n"), ext)
|
||||
checkPageSummary(t, p, normalizeExpected(ext, ""), ext)
|
||||
checkPageType(t, p, "page")
|
||||
}
|
||||
|
||||
testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithBlankSummary)
|
||||
}
|
||||
|
||||
func TestPageWithSummaryParameter(t *testing.T) {
|
||||
t.Parallel()
|
||||
assertFunc := func(t *testing.T, ext string, pages page.Pages) {
|
||||
@@ -729,19 +688,6 @@ title: "empty"
|
||||
b.AssertFileContent("public/empty/index.html", "! title")
|
||||
}
|
||||
|
||||
func TestPageWithShortCodeInSummary(t *testing.T) {
|
||||
t.Parallel()
|
||||
assertFunc := func(t *testing.T, ext string, pages page.Pages) {
|
||||
p := pages[0]
|
||||
checkPageTitle(t, p, "Simple")
|
||||
checkPageContent(t, p, normalizeExpected(ext, "<p>Summary Next Line. <figure><img src=\"/not/real\"> </figure> . More text here.</p><p>Some more text</p>"))
|
||||
checkPageSummary(t, p, "Summary Next Line. . More text here. Some more text")
|
||||
checkPageType(t, p, "page")
|
||||
}
|
||||
|
||||
testAllMarkdownEnginesForPages(t, assertFunc, nil, simplePageWithShortcodeInSummary)
|
||||
}
|
||||
|
||||
func TestTableOfContents(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
cfg, fs := newTestCfg()
|
||||
@@ -853,7 +799,7 @@ Summary: {{ .Summary }}|Truncated: {{ .Truncated }}|
|
||||
Content: {{ .Content }}|
|
||||
|
||||
`).AssertFileContent("public/simple/index.html",
|
||||
"Summary: This is summary. This is more summary. This is even more summary*.|",
|
||||
"Summary: <p>This is <strong>summary</strong>.\nThis is <strong>more summary</strong>.\nThis is <em>even more summary</em>*.\nThis is <strong>more summary</strong>.</p>|",
|
||||
"Truncated: true|",
|
||||
"Content: <p>This is <strong>summary</strong>.")
|
||||
}
|
||||
@@ -1242,11 +1188,6 @@ func TestWordCountWithMainEnglishWithCJKRunes(t *testing.T) {
|
||||
if p.WordCount(context.Background()) != 74 {
|
||||
t.Fatalf("[%s] incorrect word count, expected %v, got %v", ext, 74, p.WordCount(context.Background()))
|
||||
}
|
||||
|
||||
if p.Summary(context.Background()) != simplePageWithMainEnglishWithCJKRunesSummary {
|
||||
t.Fatalf("[%s] incorrect Summary for content '%s'. expected\n%v, got\n%v", ext, p.Plain(context.Background()),
|
||||
simplePageWithMainEnglishWithCJKRunesSummary, p.Summary(context.Background()))
|
||||
}
|
||||
}
|
||||
|
||||
testAllMarkdownEnginesForPages(t, assertFunc, settings, simplePageWithMainEnglishWithCJKRunes)
|
||||
@@ -1263,11 +1204,6 @@ func TestWordCountWithIsCJKLanguageFalse(t *testing.T) {
|
||||
if p.WordCount(context.Background()) != 75 {
|
||||
t.Fatalf("[%s] incorrect word count for content '%s'. expected %v, got %v", ext, p.Plain(context.Background()), 74, p.WordCount(context.Background()))
|
||||
}
|
||||
|
||||
if p.Summary(context.Background()) != simplePageWithIsCJKLanguageFalseSummary {
|
||||
t.Fatalf("[%s] incorrect Summary for content '%s'. expected %v, got %v", ext, p.Plain(context.Background()),
|
||||
simplePageWithIsCJKLanguageFalseSummary, p.Summary(context.Background()))
|
||||
}
|
||||
}
|
||||
|
||||
testAllMarkdownEnginesForPages(t, assertFunc, settings, simplePageWithIsCJKLanguageFalse)
|
||||
@@ -1485,42 +1421,6 @@ func TestChompBOM(t *testing.T) {
|
||||
checkPageTitle(t, p, "Simple")
|
||||
}
|
||||
|
||||
func TestPageHTMLContent(t *testing.T) {
|
||||
b := newTestSitesBuilder(t)
|
||||
b.WithSimpleConfigFile()
|
||||
|
||||
frontmatter := `---
|
||||
title: "HTML Content"
|
||||
---
|
||||
`
|
||||
b.WithContent("regular.html", frontmatter+`<h1>Hugo</h1>`)
|
||||
b.WithContent("nomarkdownforyou.html", frontmatter+`**Hugo!**`)
|
||||
b.WithContent("manualsummary.html", frontmatter+`
|
||||
<p>This is summary</p>
|
||||
<!--more-->
|
||||
<p>This is the main content.</p>`)
|
||||
|
||||
b.Build(BuildCfg{})
|
||||
|
||||
b.AssertFileContent(
|
||||
"public/regular/index.html",
|
||||
"Single: HTML Content|Hello|en|RelPermalink: /regular/|",
|
||||
"Summary: Hugo|Truncated: false")
|
||||
|
||||
b.AssertFileContent(
|
||||
"public/nomarkdownforyou/index.html",
|
||||
"Permalink: http://example.com/nomarkdownforyou/|**Hugo!**|",
|
||||
)
|
||||
|
||||
// https://github.com/gohugoio/hugo/issues/5723
|
||||
b.AssertFileContent(
|
||||
"public/manualsummary/index.html",
|
||||
"Single: HTML Content|Hello|en|RelPermalink: /manualsummary/|",
|
||||
"Summary: \n<p>This is summary</p>\n|Truncated: true",
|
||||
"|<p>This is the main content.</p>|",
|
||||
)
|
||||
}
|
||||
|
||||
// https://github.com/gohugoio/hugo/issues/5381
|
||||
func TestPageManualSummary(t *testing.T) {
|
||||
b := newTestSitesBuilder(t)
|
||||
@@ -1761,102 +1661,6 @@ Single: {{ .Title}}|{{ .RelPermalink }}|{{ .Path }}|
|
||||
b.AssertFileContent("public/sect3/Pag.E4/index.html", "Single: Pag.E4|/sect3/Pag.E4/|/sect3/p4|")
|
||||
}
|
||||
|
||||
// https://github.com/gohugoio/hugo/issues/4675
|
||||
func TestWordCountAndSimilarVsSummary(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
single := []string{"_default/single.html", `
|
||||
WordCount: {{ .WordCount }}
|
||||
FuzzyWordCount: {{ .FuzzyWordCount }}
|
||||
ReadingTime: {{ .ReadingTime }}
|
||||
Len Plain: {{ len .Plain }}
|
||||
Len PlainWords: {{ len .PlainWords }}
|
||||
Truncated: {{ .Truncated }}
|
||||
Len Summary: {{ len .Summary }}
|
||||
Len Content: {{ len .Content }}
|
||||
|
||||
SUMMARY:{{ .Summary }}:{{ len .Summary }}:END
|
||||
|
||||
`}
|
||||
|
||||
b := newTestSitesBuilder(t)
|
||||
b.WithSimpleConfigFile().WithTemplatesAdded(single...).WithContent("p1.md", fmt.Sprintf(`---
|
||||
title: p1
|
||||
---
|
||||
|
||||
%s
|
||||
|
||||
`, strings.Repeat("word ", 510)),
|
||||
|
||||
"p2.md", fmt.Sprintf(`---
|
||||
title: p2
|
||||
---
|
||||
This is a summary.
|
||||
|
||||
<!--more-->
|
||||
|
||||
%s
|
||||
|
||||
`, strings.Repeat("word ", 310)),
|
||||
"p3.md", fmt.Sprintf(`---
|
||||
title: p3
|
||||
isCJKLanguage: true
|
||||
---
|
||||
Summary: In Chinese, 好 means good.
|
||||
|
||||
<!--more-->
|
||||
|
||||
%s
|
||||
|
||||
`, strings.Repeat("好", 200)),
|
||||
"p4.md", fmt.Sprintf(`---
|
||||
title: p4
|
||||
isCJKLanguage: false
|
||||
---
|
||||
Summary: In Chinese, 好 means good.
|
||||
|
||||
<!--more-->
|
||||
|
||||
%s
|
||||
|
||||
`, strings.Repeat("好", 200)),
|
||||
|
||||
"p5.md", fmt.Sprintf(`---
|
||||
title: p4
|
||||
isCJKLanguage: true
|
||||
---
|
||||
Summary: In Chinese, 好 means good.
|
||||
|
||||
%s
|
||||
|
||||
`, strings.Repeat("好", 200)),
|
||||
"p6.md", fmt.Sprintf(`---
|
||||
title: p4
|
||||
isCJKLanguage: false
|
||||
---
|
||||
Summary: In Chinese, 好 means good.
|
||||
|
||||
%s
|
||||
|
||||
`, strings.Repeat("好", 200)),
|
||||
)
|
||||
|
||||
b.CreateSites().Build(BuildCfg{})
|
||||
|
||||
c.Assert(len(b.H.Sites), qt.Equals, 1)
|
||||
c.Assert(len(b.H.Sites[0].RegularPages()), qt.Equals, 6)
|
||||
|
||||
b.AssertFileContent("public/p1/index.html", "WordCount: 510\nFuzzyWordCount: 600\nReadingTime: 3\nLen Plain: 2550\nLen PlainWords: 510\nTruncated: false\nLen Summary: 2549\nLen Content: 2557")
|
||||
|
||||
b.AssertFileContent("public/p2/index.html", "WordCount: 314\nFuzzyWordCount: 400\nReadingTime: 2\nLen Plain: 1569\nLen PlainWords: 314\nTruncated: true\nLen Summary: 25\nLen Content: 1582")
|
||||
|
||||
b.AssertFileContent("public/p3/index.html", "WordCount: 206\nFuzzyWordCount: 300\nReadingTime: 1\nLen Plain: 638\nLen PlainWords: 7\nTruncated: true\nLen Summary: 43\nLen Content: 651")
|
||||
b.AssertFileContent("public/p4/index.html", "WordCount: 7\nFuzzyWordCount: 100\nReadingTime: 1\nLen Plain: 638\nLen PlainWords: 7\nTruncated: true\nLen Summary: 43\nLen Content: 651")
|
||||
b.AssertFileContent("public/p5/index.html", "WordCount: 206\nFuzzyWordCount: 300\nReadingTime: 1\nLen Plain: 638\nLen PlainWords: 7\nTruncated: true\nLen Summary: 229\nLen Content: 652")
|
||||
b.AssertFileContent("public/p6/index.html", "WordCount: 7\nFuzzyWordCount: 100\nReadingTime: 1\nLen Plain: 638\nLen PlainWords: 7\nTruncated: false\nLen Summary: 637\nLen Content: 652")
|
||||
}
|
||||
|
||||
func TestScratch(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
@@ -65,6 +65,7 @@ var zeroShortcode = prerenderedShortcode{}
|
||||
type pageForShortcode struct {
|
||||
page.PageWithoutContent
|
||||
page.TableOfContentsProvider
|
||||
page.MarkupProvider
|
||||
page.ContentProvider
|
||||
|
||||
// We need to replace it after we have rendered it, so provide a
|
||||
@@ -80,6 +81,7 @@ func newPageForShortcode(p *pageState) page.Page {
|
||||
return &pageForShortcode{
|
||||
PageWithoutContent: p,
|
||||
TableOfContentsProvider: p,
|
||||
MarkupProvider: page.NopPage,
|
||||
ContentProvider: page.NopPage,
|
||||
toc: template.HTML(tocShortcodePlaceholder),
|
||||
p: p,
|
||||
@@ -105,6 +107,7 @@ var _ types.Unwrapper = (*pageForRenderHooks)(nil)
|
||||
type pageForRenderHooks struct {
|
||||
page.PageWithoutContent
|
||||
page.TableOfContentsProvider
|
||||
page.MarkupProvider
|
||||
page.ContentProvider
|
||||
p *pageState
|
||||
}
|
||||
@@ -112,6 +115,7 @@ type pageForRenderHooks struct {
|
||||
func newPageForRenderHook(p *pageState) page.Page {
|
||||
return &pageForRenderHooks{
|
||||
PageWithoutContent: p,
|
||||
MarkupProvider: page.NopPage,
|
||||
ContentProvider: page.NopPage,
|
||||
TableOfContentsProvider: p,
|
||||
p: p,
|
||||
|
||||
+13
-18
@@ -756,12 +756,15 @@ title: "Hugo Rocks!"
|
||||
|
||||
func TestShortcodeParams(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := qt.New(t)
|
||||
|
||||
builder := newTestSitesBuilder(t).WithSimpleConfigFile()
|
||||
|
||||
builder.WithContent("page.md", `---
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
baseURL = "https://example.org"
|
||||
-- layouts/shortcodes/hello.html --
|
||||
{{ range $i, $v := .Params }}{{ printf "- %v: %v (%T) " $i $v $v -}}{{ end }}
|
||||
-- content/page.md --
|
||||
title: "Hugo Rocks!"
|
||||
summary: "Foo"
|
||||
---
|
||||
|
||||
# doc
|
||||
@@ -770,23 +773,15 @@ types positional: {{< hello true false 33 3.14 >}}
|
||||
types named: {{< hello b1=true b2=false i1=33 f1=3.14 >}}
|
||||
types string: {{< hello "true" trues "33" "3.14" >}}
|
||||
escaped quoute: {{< hello "hello \"world\"." >}}
|
||||
-- layouts/_default/single.html --
|
||||
Content: {{ .Content }}|
|
||||
`
|
||||
|
||||
b := Test(t, files)
|
||||
|
||||
`).WithTemplatesAdded(
|
||||
"layouts/shortcodes/hello.html",
|
||||
`{{ range $i, $v := .Params }}
|
||||
- {{ printf "%v: %v (%T)" $i $v $v }}
|
||||
{{ end }}
|
||||
{{ $b1 := .Get "b1" }}
|
||||
Get: {{ printf "%v (%T)" $b1 $b1 | safeHTML }}
|
||||
`).Build(BuildCfg{})
|
||||
|
||||
s := builder.H.Sites[0]
|
||||
c.Assert(len(s.RegularPages()), qt.Equals, 1)
|
||||
|
||||
builder.AssertFileContent("public/page/index.html",
|
||||
b.AssertFileContent("public/page/index.html",
|
||||
"types positional: - 0: true (bool) - 1: false (bool) - 2: 33 (int) - 3: 3.14 (float64)",
|
||||
"types named: - b1: true (bool) - b2: false (bool) - f1: 3.14 (float64) - i1: 33 (int) Get: true (bool) ",
|
||||
"types named: - b1: true (bool) - b2: false (bool) - f1: 3.14 (float64) - i1: 33 (int)",
|
||||
"types string: - 0: true (string) - 1: trues (string) - 2: 33 (string) - 3: 3.14 (string) ",
|
||||
"hello "world". (string)",
|
||||
)
|
||||
|
||||
+12
-1
@@ -117,6 +117,9 @@ func (s *Site) prepareInits() {
|
||||
|
||||
s.init.prevNext = init.Branch(func(context.Context) (any, error) {
|
||||
regularPages := s.RegularPages()
|
||||
if s.conf.Page.NextPrevSortOrder == "asc" {
|
||||
regularPages = regularPages.Reverse()
|
||||
}
|
||||
for i, p := range regularPages {
|
||||
np, ok := p.(nextPrevProvider)
|
||||
if !ok {
|
||||
@@ -181,7 +184,11 @@ func (s *Site) prepareInits() {
|
||||
)
|
||||
|
||||
for _, section := range sections {
|
||||
setNextPrev(section.RegularPages())
|
||||
ps := section.RegularPages()
|
||||
if s.conf.Page.NextPrevInSectionSortOrder == "asc" {
|
||||
ps = ps.Reverse()
|
||||
}
|
||||
setNextPrev(ps)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
@@ -923,6 +930,10 @@ func (hr hookRendererTemplate) RenderBlockquote(cctx context.Context, w hugio.Fl
|
||||
return hr.templateHandler.ExecuteWithContext(cctx, hr.templ, w, ctx)
|
||||
}
|
||||
|
||||
func (hr hookRendererTemplate) RenderTable(cctx context.Context, w hugio.FlexiWriter, ctx hooks.TableContext) error {
|
||||
return hr.templateHandler.ExecuteWithContext(cctx, hr.templ, w, ctx)
|
||||
}
|
||||
|
||||
func (hr hookRendererTemplate) ResolvePosition(ctx any) text.Position {
|
||||
return hr.resolvePosition(ctx)
|
||||
}
|
||||
|
||||
@@ -706,3 +706,17 @@ a: {{ $a }}
|
||||
|
||||
b.AssertFileContent("public/index.html", `a: [a b c]`)
|
||||
}
|
||||
|
||||
func TestOverrideInternalTemplate(t *testing.T) {
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
baseURL = "https://example.org"
|
||||
-- layouts/index.html --
|
||||
{{ template "_internal/google_analytics_async.html" . }}
|
||||
-- layouts/_internal/google_analytics_async.html --
|
||||
Overridden.
|
||||
`
|
||||
b := Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/index.html", "Overridden.")
|
||||
}
|
||||
|
||||
+4
-2
@@ -1,7 +1,9 @@
|
||||
# Release env.
|
||||
# These will be replaced by script before release.
|
||||
HUGORELEASER_TAG=v0.132.2
|
||||
HUGORELEASER_COMMITISH=3fd26c70dff5934ec1802b9563530130ed1bca75
|
||||
HUGORELEASER_TAG=v0.133.1
|
||||
HUGORELEASER_COMMITISH=47d00202e7e61769ce4d14691e43b27852c9cce4
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+13
-1
@@ -148,7 +148,11 @@ func Check() {
|
||||
fmt.Printf("Skip Test386 on %s and/or %s\n", runtime.GOARCH, runtime.GOOS)
|
||||
}
|
||||
|
||||
mg.Deps(Fmt, Vet)
|
||||
if isCi() && isDarwin() {
|
||||
// Skip on macOS in CI (disk space issues)
|
||||
} else {
|
||||
mg.Deps(Fmt, Vet)
|
||||
}
|
||||
|
||||
// don't run two tests in parallel, they saturate the CPUs anyway, and running two
|
||||
// causes memory issues in CI.
|
||||
@@ -239,6 +243,14 @@ func Lint() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func isCi() bool {
|
||||
return os.Getenv("CI") != ""
|
||||
}
|
||||
|
||||
func isDarwin() bool {
|
||||
return runtime.GOOS == "darwin"
|
||||
}
|
||||
|
||||
// Run go vet linter
|
||||
func Vet() error {
|
||||
if err := sh.Run(goexe, "vet", "./..."); err != nil {
|
||||
|
||||
@@ -41,7 +41,7 @@ type LinkContext interface {
|
||||
Title() string
|
||||
|
||||
// The rendered (HTML) text.
|
||||
Text() hstring.RenderedString
|
||||
Text() hstring.RenderedHTML
|
||||
|
||||
// The plain variant of Text.
|
||||
PlainText() string
|
||||
@@ -61,9 +61,8 @@ type ImageLinkContext interface {
|
||||
|
||||
// CodeblockContext is the context passed to a code block render hook.
|
||||
type CodeblockContext interface {
|
||||
BaseContext
|
||||
AttributesProvider
|
||||
text.Positioner
|
||||
PageProvider
|
||||
|
||||
// Chroma highlighting processing options. This will only be filled if Type is a known Chroma Lexer.
|
||||
Options() map[string]any
|
||||
@@ -73,23 +72,35 @@ type CodeblockContext interface {
|
||||
|
||||
// The text between the code fences.
|
||||
Inner() string
|
||||
}
|
||||
|
||||
// Zero-based ordinal for all code blocks in the current document.
|
||||
// TableContext is the context passed to a table render hook.
|
||||
type TableContext interface {
|
||||
BaseContext
|
||||
AttributesProvider
|
||||
|
||||
THead() []TableRow
|
||||
TBody() []TableRow
|
||||
}
|
||||
|
||||
// BaseContext is the base context used in most render hooks.
|
||||
type BaseContext interface {
|
||||
text.Positioner
|
||||
PageProvider
|
||||
|
||||
// Zero-based ordinal for all elements of this kind in the current document.
|
||||
Ordinal() int
|
||||
}
|
||||
|
||||
// BlockquoteContext is the context passed to a blockquote render hook.
|
||||
type BlockquoteContext interface {
|
||||
AttributesProvider
|
||||
text.Positioner
|
||||
PageProvider
|
||||
BaseContext
|
||||
|
||||
// Zero-based ordinal for all block quotes in the current document.
|
||||
Ordinal() int
|
||||
AttributesProvider
|
||||
|
||||
// The blockquote text.
|
||||
// If type is "alert", this will be the alert text.
|
||||
Text() hstring.RenderedString
|
||||
Text() hstring.RenderedHTML
|
||||
|
||||
/// Returns the blockquote type, one of "regular" and "alert".
|
||||
// Type "alert" indicates that this is a GitHub type alert.
|
||||
@@ -107,18 +118,14 @@ type PositionerSourceTargetProvider interface {
|
||||
|
||||
// PassThroughContext is the context passed to a passthrough render hook.
|
||||
type PassthroughContext interface {
|
||||
BaseContext
|
||||
AttributesProvider
|
||||
text.Positioner
|
||||
PageProvider
|
||||
|
||||
// Currently one of "inline" or "block".
|
||||
Type() string
|
||||
|
||||
// The inner content of the passthrough element, excluding the delimiters.
|
||||
Inner() string
|
||||
|
||||
// Zero-based ordinal for all passthrough elements in the document.
|
||||
Ordinal() int
|
||||
}
|
||||
|
||||
type AttributesOptionsSliceProvider interface {
|
||||
@@ -138,6 +145,10 @@ type BlockquoteRenderer interface {
|
||||
RenderBlockquote(cctx context.Context, w hugio.FlexiWriter, ctx BlockquoteContext) error
|
||||
}
|
||||
|
||||
type TableRenderer interface {
|
||||
RenderTable(cctx context.Context, w hugio.FlexiWriter, ctx TableContext) error
|
||||
}
|
||||
|
||||
type PassthroughRenderer interface {
|
||||
RenderPassthrough(cctx context.Context, w io.Writer, ctx PassthroughContext) error
|
||||
}
|
||||
@@ -155,7 +166,7 @@ type HeadingContext interface {
|
||||
// Anchor is the HTML id assigned to the heading.
|
||||
Anchor() string
|
||||
// Text is the rendered (HTML) heading text, excluding the heading marker.
|
||||
Text() hstring.RenderedString
|
||||
Text() hstring.RenderedHTML
|
||||
// PlainText is the unrendered version of Text.
|
||||
PlainText() string
|
||||
|
||||
@@ -196,6 +207,19 @@ const (
|
||||
CodeBlockRendererType
|
||||
PassthroughRendererType
|
||||
BlockquoteRendererType
|
||||
TableRendererType
|
||||
)
|
||||
|
||||
type GetRendererFunc func(t RendererType, id any) any
|
||||
|
||||
type TableCell struct {
|
||||
Text hstring.RenderedHTML
|
||||
Alignment string // left, center, or right
|
||||
}
|
||||
|
||||
type TableRow []TableCell
|
||||
|
||||
type Table struct {
|
||||
THead []TableRow
|
||||
TBody []TableRow
|
||||
}
|
||||
|
||||
@@ -16,10 +16,8 @@ package blockquotes
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
htext "github.com/gohugoio/hugo/common/text"
|
||||
"github.com/gohugoio/hugo/common/types/hstring"
|
||||
"github.com/gohugoio/hugo/markup/converter/hooks"
|
||||
"github.com/gohugoio/hugo/markup/goldmark/internal/render"
|
||||
@@ -71,70 +69,36 @@ func (r *htmlRenderer) renderBlockquote(w util.BufWriter, src []byte, node ast.N
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
pos := ctx.PopPos()
|
||||
text := ctx.Buffer.Bytes()[pos:]
|
||||
ctx.Buffer.Truncate(pos)
|
||||
text := ctx.PopRenderedString()
|
||||
|
||||
ordinal := ctx.GetAndIncrementOrdinal(ast.KindBlockquote)
|
||||
|
||||
texts := string(text)
|
||||
typ := typeRegular
|
||||
alertType := resolveGitHubAlert(texts)
|
||||
alertType := resolveGitHubAlert(string(text))
|
||||
if alertType != "" {
|
||||
typ = typeAlert
|
||||
}
|
||||
|
||||
renderer := ctx.RenderContext().GetRenderer(hooks.BlockquoteRendererType, typ)
|
||||
if renderer == nil {
|
||||
return r.renderBlockquoteDefault(w, n, texts)
|
||||
return r.renderBlockquoteDefault(w, n, text)
|
||||
}
|
||||
|
||||
if typ == typeAlert {
|
||||
// Trim preamble: <p>[!NOTE]<br>\n but preserve leading paragraph.
|
||||
// We could possibly complicate this by moving this to the parser, but
|
||||
// keep it simple for now.
|
||||
texts = "<p>" + texts[strings.Index(texts, "\n")+1:]
|
||||
}
|
||||
|
||||
var sourceRef []byte
|
||||
|
||||
// Extract a source sample to use for position information.
|
||||
if nn := n.FirstChild(); nn != nil {
|
||||
var start, stop int
|
||||
for i := 0; i < nn.Lines().Len() && i < 2; i++ {
|
||||
line := nn.Lines().At(i)
|
||||
if i == 0 {
|
||||
start = line.Start
|
||||
}
|
||||
stop = line.Stop
|
||||
}
|
||||
// We do not mutate the source, so this is safe.
|
||||
sourceRef = src[start:stop]
|
||||
text = "<p>" + text[strings.Index(text, "\n")+1:]
|
||||
}
|
||||
|
||||
bqctx := &blockquoteContext{
|
||||
page: ctx.DocumentContext().Document,
|
||||
pageInner: r.getPageInner(ctx),
|
||||
BaseContext: render.NewBaseContext(ctx, renderer, n, src, nil, ordinal),
|
||||
typ: typ,
|
||||
alertType: alertType,
|
||||
text: hstring.RenderedString(texts),
|
||||
sourceRef: sourceRef,
|
||||
ordinal: ordinal,
|
||||
text: hstring.RenderedHTML(text),
|
||||
AttributesHolder: attributes.New(n.Attributes(), attributes.AttributesOwnerGeneral),
|
||||
}
|
||||
|
||||
bqctx.createPos = func() htext.Position {
|
||||
if resolver, ok := renderer.(hooks.ElementPositionResolver); ok {
|
||||
return resolver.ResolvePosition(bqctx)
|
||||
}
|
||||
|
||||
return htext.Position{
|
||||
Filename: ctx.DocumentContext().Filename,
|
||||
LineNumber: 1,
|
||||
ColumnNumber: 1,
|
||||
}
|
||||
}
|
||||
|
||||
cr := renderer.(hooks.BlockquoteRenderer)
|
||||
|
||||
err := cr.RenderBlockquote(
|
||||
@@ -143,24 +107,12 @@ func (r *htmlRenderer) renderBlockquote(w util.BufWriter, src []byte, node ast.N
|
||||
bqctx,
|
||||
)
|
||||
if err != nil {
|
||||
return ast.WalkContinue, herrors.NewFileErrorFromPos(err, bqctx.createPos())
|
||||
return ast.WalkContinue, herrors.NewFileErrorFromPos(err, bqctx.Position())
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Code borrowed from goldmark's html renderer.
|
||||
func (r *htmlRenderer) renderBlockquoteDefault(
|
||||
w util.BufWriter, n ast.Node, text string,
|
||||
@@ -180,19 +132,11 @@ func (r *htmlRenderer) renderBlockquoteDefault(
|
||||
}
|
||||
|
||||
type blockquoteContext struct {
|
||||
page any
|
||||
pageInner any
|
||||
text hstring.RenderedString
|
||||
typ string
|
||||
sourceRef []byte
|
||||
alertType string
|
||||
ordinal int
|
||||
hooks.BaseContext
|
||||
|
||||
// This is only used in error situations and is expensive to create,
|
||||
// so delay creation until needed.
|
||||
pos htext.Position
|
||||
posInit sync.Once
|
||||
createPos func() htext.Position
|
||||
text hstring.RenderedHTML
|
||||
alertType string
|
||||
typ string
|
||||
|
||||
*attributes.AttributesHolder
|
||||
}
|
||||
@@ -205,35 +149,10 @@ func (c *blockquoteContext) AlertType() string {
|
||||
return c.alertType
|
||||
}
|
||||
|
||||
func (c *blockquoteContext) Page() any {
|
||||
return c.page
|
||||
}
|
||||
|
||||
func (c *blockquoteContext) PageInner() any {
|
||||
return c.pageInner
|
||||
}
|
||||
|
||||
func (c *blockquoteContext) Text() hstring.RenderedString {
|
||||
func (c *blockquoteContext) Text() hstring.RenderedHTML {
|
||||
return c.text
|
||||
}
|
||||
|
||||
func (c *blockquoteContext) Ordinal() int {
|
||||
return c.ordinal
|
||||
}
|
||||
|
||||
func (c *blockquoteContext) Position() htext.Position {
|
||||
c.posInit.Do(func() {
|
||||
c.pos = c.createPos()
|
||||
})
|
||||
return c.pos
|
||||
}
|
||||
|
||||
func (c *blockquoteContext) PositionerSourceTarget() []byte {
|
||||
return c.sourceRef
|
||||
}
|
||||
|
||||
var _ hooks.PositionerSourceTargetProvider = (*blockquoteContext)(nil)
|
||||
|
||||
// https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts
|
||||
// Five types:
|
||||
// [!NOTE], [!TIP], [!WARNING], [!IMPORTANT], [!CAUTION]
|
||||
|
||||
@@ -32,9 +32,9 @@ func TestBlockquoteHook(t *testing.T) {
|
||||
block = true
|
||||
title = true
|
||||
-- layouts/_default/_markup/render-blockquote.html --
|
||||
Blockquote: |{{ .Text | safeHTML }}|{{ .Type }}|
|
||||
Blockquote: |{{ .Text }}|{{ .Type }}|
|
||||
-- layouts/_default/_markup/render-blockquote-alert.html --
|
||||
{{ $text := .Text | safeHTML }}
|
||||
{{ $text := .Text }}
|
||||
Blockquote Alert: |{{ $text }}|{{ .Type }}|
|
||||
Blockquote Alert Attributes: |{{ $text }}|{{ .Attributes }}|
|
||||
Blockquote Alert Page: |{{ $text }}|{{ .Page.Title }}|{{ .PageInner.Title }}|
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
htext "github.com/gohugoio/hugo/common/text"
|
||||
@@ -101,26 +100,14 @@ func (r *htmlRenderer) renderCodeBlock(w util.BufWriter, src []byte, node ast.No
|
||||
if err != nil {
|
||||
return ast.WalkStop, &herrors.TextSegmentError{Err: err, Segment: attrStr}
|
||||
}
|
||||
|
||||
cbctx := &codeBlockContext{
|
||||
page: ctx.DocumentContext().Document,
|
||||
pageInner: r.getPageInner(ctx),
|
||||
BaseContext: render.NewBaseContext(ctx, renderer, node, src, func() []byte { return []byte(s) }, ordinal),
|
||||
lang: lang,
|
||||
code: s,
|
||||
ordinal: ordinal,
|
||||
AttributesHolder: attributes.New(attrs, attrtp),
|
||||
}
|
||||
|
||||
cbctx.createPos = func() htext.Position {
|
||||
if resolver, ok := renderer.(hooks.ElementPositionResolver); ok {
|
||||
return resolver.ResolvePosition(cbctx)
|
||||
}
|
||||
return htext.Position{
|
||||
Filename: ctx.DocumentContext().Filename,
|
||||
LineNumber: 1,
|
||||
ColumnNumber: 1,
|
||||
}
|
||||
}
|
||||
|
||||
cr := renderer.(hooks.CodeBlockRenderer)
|
||||
|
||||
err = cr.RenderCodeblock(
|
||||
@@ -129,50 +116,20 @@ func (r *htmlRenderer) renderCodeBlock(w util.BufWriter, src []byte, node ast.No
|
||||
cbctx,
|
||||
)
|
||||
if err != nil {
|
||||
return ast.WalkContinue, herrors.NewFileErrorFromPos(err, cbctx.createPos())
|
||||
return ast.WalkContinue, herrors.NewFileErrorFromPos(err, cbctx.Position())
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
var _ hooks.PositionerSourceTargetProvider = (*codeBlockContext)(nil)
|
||||
|
||||
type codeBlockContext struct {
|
||||
page any
|
||||
pageInner any
|
||||
lang string
|
||||
code string
|
||||
ordinal int
|
||||
|
||||
// This is only used in error situations and is expensive to create,
|
||||
// so delay creation until needed.
|
||||
pos htext.Position
|
||||
posInit sync.Once
|
||||
createPos func() htext.Position
|
||||
hooks.BaseContext
|
||||
lang string
|
||||
code string
|
||||
|
||||
*attributes.AttributesHolder
|
||||
}
|
||||
|
||||
func (c *codeBlockContext) Page() any {
|
||||
return c.page
|
||||
}
|
||||
|
||||
func (c *codeBlockContext) PageInner() any {
|
||||
return c.pageInner
|
||||
}
|
||||
|
||||
func (c *codeBlockContext) Type() string {
|
||||
return c.lang
|
||||
}
|
||||
@@ -181,22 +138,6 @@ func (c *codeBlockContext) Inner() string {
|
||||
return c.code
|
||||
}
|
||||
|
||||
func (c *codeBlockContext) Ordinal() int {
|
||||
return c.ordinal
|
||||
}
|
||||
|
||||
func (c *codeBlockContext) Position() htext.Position {
|
||||
c.posInit.Do(func() {
|
||||
c.pos = c.createPos()
|
||||
})
|
||||
return c.pos
|
||||
}
|
||||
|
||||
// For internal use.
|
||||
func (c *codeBlockContext) PositionerSourceTarget() []byte {
|
||||
return []byte(c.code)
|
||||
}
|
||||
|
||||
func getLang(node *ast.FencedCodeBlock, src []byte) string {
|
||||
langWithAttributes := string(node.Language(src))
|
||||
lang, _, _ := strings.Cut(langWithAttributes, "{")
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"github.com/gohugoio/hugo/markup/goldmark/internal/extensions/attributes"
|
||||
"github.com/gohugoio/hugo/markup/goldmark/internal/render"
|
||||
"github.com/gohugoio/hugo/markup/goldmark/passthrough"
|
||||
"github.com/gohugoio/hugo/markup/goldmark/tables"
|
||||
"github.com/yuin/goldmark/util"
|
||||
|
||||
"github.com/yuin/goldmark"
|
||||
@@ -131,6 +132,7 @@ func newMarkdown(pcfg converter.ProviderConfig) goldmark.Markdown {
|
||||
|
||||
if cfg.Extensions.Table {
|
||||
extensions = append(extensions, extension.Table)
|
||||
extensions = append(extensions, tables.New())
|
||||
}
|
||||
|
||||
if cfg.Extensions.Strikethrough {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
package goldmark_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -30,6 +31,7 @@ import (
|
||||
|
||||
"github.com/gohugoio/hugo/markup/markup_config"
|
||||
|
||||
"github.com/gohugoio/hugo/common/hugio"
|
||||
"github.com/gohugoio/hugo/common/loggers"
|
||||
"github.com/gohugoio/hugo/common/maps"
|
||||
|
||||
@@ -60,9 +62,13 @@ func convert(c *qt.C, conf config.AllProvider, content string) converter.ResultR
|
||||
h := highlight.New(mconf.Highlight)
|
||||
|
||||
getRenderer := func(t hooks.RendererType, id any) any {
|
||||
if t == hooks.CodeBlockRendererType {
|
||||
switch t {
|
||||
case hooks.CodeBlockRendererType:
|
||||
return h
|
||||
case hooks.TableRendererType:
|
||||
return tableRenderer(0)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -168,8 +174,6 @@ unsafe = true
|
||||
b := convert(c, testconfig.GetTestConfig(nil, cfg), content)
|
||||
got := string(b.Bytes())
|
||||
|
||||
fmt.Println(got)
|
||||
|
||||
// Links
|
||||
c.Assert(got, qt.Contains, `<a href="https://docuapi.netlify.com/">Live Demo here!</a>`)
|
||||
c.Assert(got, qt.Contains, `<a href="https://foo.bar/">https://foo.bar/</a>`)
|
||||
@@ -191,7 +195,7 @@ unsafe = true
|
||||
// Extensions
|
||||
c.Assert(got, qt.Contains, `Autolink: <a href="https://gohugo.io/">https://gohugo.io/</a>`)
|
||||
c.Assert(got, qt.Contains, `Strikethrough:<del>Hi</del> Hello, world`)
|
||||
c.Assert(got, qt.Contains, `<th>foo</th>`)
|
||||
c.Assert(got, qt.Contains, `Table`)
|
||||
c.Assert(got, qt.Contains, `<li><input disabled="" type="checkbox"> Push my commits to GitHub</li>`)
|
||||
|
||||
c.Assert(got, qt.Contains, `Straight double “quotes” and single ‘quotes’`)
|
||||
@@ -378,7 +382,7 @@ func TestConvertAttributes(t *testing.T) {
|
||||
| ------------- |:-------------:| -----:|
|
||||
| AV | BV |
|
||||
{.myclass }`,
|
||||
"<table class=\"myclass\">\n<thead>",
|
||||
"Table",
|
||||
},
|
||||
{
|
||||
"Title and Blockquote",
|
||||
@@ -741,3 +745,11 @@ escapedSpace=true
|
||||
|
||||
c.Assert(got, qt.Contains, "<p>私は太郎です。\nプログラミングが好きです。運動が苦手です。</p>\n")
|
||||
}
|
||||
|
||||
type tableRenderer int
|
||||
|
||||
func (hr tableRenderer) RenderTable(cctx context.Context, w hugio.FlexiWriter, ctx hooks.TableContext) error {
|
||||
// This is set up with a render hook in the hugolib package, make it simple here.
|
||||
fmt.Fprintln(w, "Table")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ title: "p1"
|
||||
{{- range $k, $v := .Attributes -}}
|
||||
{{- printf " %s=%q" $k $v | safeHTMLAttr -}}
|
||||
{{- end -}}
|
||||
>{{ .Text | safeHTML }}</h{{ .Level }}>
|
||||
>{{ .Text }}</h{{ .Level }}>
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files)
|
||||
@@ -146,11 +146,11 @@ title: "p1"
|
||||
{{ .Content }}
|
||||
-- layouts/_default/_markup/render-heading.html --
|
||||
<h{{ .Level }} id="{{ .Anchor | safeURL }}">
|
||||
{{ .Text | safeHTML }}
|
||||
{{ .Text }}
|
||||
<a class="anchor" href="#{{ .Anchor | safeURL }}">#</a>
|
||||
</h{{ .Level }}>
|
||||
-- layouts/_default/_markup/render-link.html --
|
||||
<a href="{{ .Destination | safeURL }}"{{ with .Title}} title="{{ . }}"{{ end }}>{{ .Text | safeHTML }}</a>
|
||||
<a href="{{ .Destination | safeURL }}"{{ with .Title}} title="{{ . }}"{{ end }}>{{ .Text }}</a>
|
||||
|
||||
`
|
||||
|
||||
@@ -236,11 +236,11 @@ func BenchmarkRenderHooks(b *testing.B) {
|
||||
-- config.toml --
|
||||
-- layouts/_default/_markup/render-heading.html --
|
||||
<h{{ .Level }} id="{{ .Anchor | safeURL }}">
|
||||
{{ .Text | safeHTML }}
|
||||
{{ .Text }}
|
||||
<a class="anchor" href="#{{ .Anchor | safeURL }}">#</a>
|
||||
</h{{ .Level }}>
|
||||
-- layouts/_default/_markup/render-link.html --
|
||||
<a href="{{ .Destination | safeURL }}"{{ with .Title}} title="{{ . }}"{{ end }}>{{ .Text | safeHTML }}</a>
|
||||
<a href="{{ .Destination | safeURL }}"{{ with .Title}} title="{{ . }}"{{ end }}>{{ .Text }}</a>
|
||||
-- layouts/_default/single.html --
|
||||
{{ .Content }}
|
||||
`
|
||||
@@ -452,7 +452,7 @@ Link https procol: https://www.example.org
|
||||
|
||||
if withHook {
|
||||
files += `-- layouts/_default/_markup/render-link.html --
|
||||
<a href="{{ .Destination | safeURL }}">{{ .Text | safeHTML }}</a>`
|
||||
<a href="{{ .Destination | safeURL }}">{{ .Text }}</a>`
|
||||
}
|
||||
|
||||
return hugolib.NewIntegrationTestBuilder(
|
||||
|
||||
@@ -16,8 +16,12 @@ package render
|
||||
import (
|
||||
"bytes"
|
||||
"math/bits"
|
||||
"sync"
|
||||
|
||||
htext "github.com/gohugoio/hugo/common/text"
|
||||
|
||||
"github.com/gohugoio/hugo/markup/converter"
|
||||
"github.com/gohugoio/hugo/markup/converter/hooks"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
)
|
||||
|
||||
@@ -45,6 +49,7 @@ type Context struct {
|
||||
positions []int
|
||||
pids []uint64
|
||||
ordinals map[ast.NodeKind]int
|
||||
values map[ast.NodeKind][]any
|
||||
}
|
||||
|
||||
func (ctx *Context) GetAndIncrementOrdinal(kind ast.NodeKind) int {
|
||||
@@ -67,6 +72,13 @@ func (ctx *Context) PopPos() int {
|
||||
return p
|
||||
}
|
||||
|
||||
func (ctx *Context) PopRenderedString() string {
|
||||
pos := ctx.PopPos()
|
||||
text := string(ctx.Bytes()[pos:])
|
||||
ctx.Truncate(pos)
|
||||
return text
|
||||
}
|
||||
|
||||
// PushPid pushes a new page ID to the stack.
|
||||
func (ctx *Context) PushPid(pid uint64) {
|
||||
ctx.pids = append(ctx.pids, pid)
|
||||
@@ -91,6 +103,38 @@ func (ctx *Context) PopPid() uint64 {
|
||||
return p
|
||||
}
|
||||
|
||||
func (ctx *Context) PushValue(k ast.NodeKind, v any) {
|
||||
if ctx.values == nil {
|
||||
ctx.values = make(map[ast.NodeKind][]any)
|
||||
}
|
||||
ctx.values[k] = append(ctx.values[k], v)
|
||||
}
|
||||
|
||||
func (ctx *Context) PopValue(k ast.NodeKind) any {
|
||||
if ctx.values == nil {
|
||||
return nil
|
||||
}
|
||||
v := ctx.values[k]
|
||||
if len(v) == 0 {
|
||||
return nil
|
||||
}
|
||||
i := len(v) - 1
|
||||
r := v[i]
|
||||
ctx.values[k] = v[:i]
|
||||
return r
|
||||
}
|
||||
|
||||
func (ctx *Context) PeekValue(k ast.NodeKind) any {
|
||||
if ctx.values == nil {
|
||||
return nil
|
||||
}
|
||||
v := ctx.values[k]
|
||||
if len(v) == 0 {
|
||||
return nil
|
||||
}
|
||||
return v[len(v)-1]
|
||||
}
|
||||
|
||||
type ContextData interface {
|
||||
RenderContext() converter.RenderContext
|
||||
DocumentContext() converter.DocumentContext
|
||||
@@ -108,3 +152,109 @@ func (ctx *RenderContextDataHolder) RenderContext() converter.RenderContext {
|
||||
func (ctx *RenderContextDataHolder) DocumentContext() converter.DocumentContext {
|
||||
return ctx.Dctx
|
||||
}
|
||||
|
||||
// extractSourceSample returns a sample of the source for the given node.
|
||||
// Note that this is not a copy of the source, but a slice of it,
|
||||
// so it assumes that the source is not mutated.
|
||||
func extractSourceSample(n ast.Node, src []byte) []byte {
|
||||
var sample []byte
|
||||
|
||||
// Extract a source sample to use for position information.
|
||||
if nn := n.FirstChild(); nn != nil {
|
||||
var start, stop int
|
||||
for i := 0; i < nn.Lines().Len() && i < 2; i++ {
|
||||
line := nn.Lines().At(i)
|
||||
if i == 0 {
|
||||
start = line.Start
|
||||
}
|
||||
stop = line.Stop
|
||||
}
|
||||
// We do not mutate the source, so this is safe.
|
||||
sample = src[start:stop]
|
||||
}
|
||||
return sample
|
||||
}
|
||||
|
||||
// GetPageAndPageInner returns the current page and the inner page for the given context.
|
||||
func GetPageAndPageInner(rctx *Context) (any, any) {
|
||||
p := rctx.DocumentContext().Document
|
||||
pid := rctx.PeekPid()
|
||||
if pid > 0 {
|
||||
if lookup := rctx.DocumentContext().DocumentLookup; lookup != nil {
|
||||
if v := rctx.DocumentContext().DocumentLookup(pid); v != nil {
|
||||
return p, v
|
||||
}
|
||||
}
|
||||
}
|
||||
return p, p
|
||||
}
|
||||
|
||||
// NewBaseContext creates a new BaseContext.
|
||||
func NewBaseContext(rctx *Context, renderer any, n ast.Node, src []byte, getSourceSample func() []byte, ordinal int) hooks.BaseContext {
|
||||
if getSourceSample == nil {
|
||||
getSourceSample = func() []byte {
|
||||
return extractSourceSample(n, src)
|
||||
}
|
||||
}
|
||||
page, pageInner := GetPageAndPageInner(rctx)
|
||||
b := &hookBase{
|
||||
page: page,
|
||||
pageInner: pageInner,
|
||||
|
||||
getSourceSample: getSourceSample,
|
||||
ordinal: ordinal,
|
||||
}
|
||||
|
||||
b.createPos = func() htext.Position {
|
||||
if resolver, ok := renderer.(hooks.ElementPositionResolver); ok {
|
||||
return resolver.ResolvePosition(b)
|
||||
}
|
||||
|
||||
return htext.Position{
|
||||
Filename: rctx.DocumentContext().Filename,
|
||||
LineNumber: 1,
|
||||
ColumnNumber: 1,
|
||||
}
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
var _ hooks.PositionerSourceTargetProvider = (*hookBase)(nil)
|
||||
|
||||
type hookBase struct {
|
||||
page any
|
||||
pageInner any
|
||||
ordinal int
|
||||
|
||||
// This is only used in error situations and is expensive to create,
|
||||
// so delay creation until needed.
|
||||
pos htext.Position
|
||||
posInit sync.Once
|
||||
createPos func() htext.Position
|
||||
getSourceSample func() []byte
|
||||
}
|
||||
|
||||
func (c *hookBase) Page() any {
|
||||
return c.page
|
||||
}
|
||||
|
||||
func (c *hookBase) PageInner() any {
|
||||
return c.pageInner
|
||||
}
|
||||
|
||||
func (c *hookBase) Ordinal() int {
|
||||
return c.ordinal
|
||||
}
|
||||
|
||||
func (c *hookBase) Position() htext.Position {
|
||||
c.posInit.Do(func() {
|
||||
c.pos = c.createPos()
|
||||
})
|
||||
return c.pos
|
||||
}
|
||||
|
||||
// For internal use.
|
||||
func (c *hookBase) PositionerSourceTarget() []byte {
|
||||
return c.getSourceSample()
|
||||
}
|
||||
|
||||
@@ -15,9 +15,6 @@ package passthrough
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"sync"
|
||||
|
||||
htext "github.com/gohugoio/hugo/common/text"
|
||||
|
||||
"github.com/gohugoio/hugo-goldmark-extensions/passthrough"
|
||||
"github.com/gohugoio/hugo/markup/converter/hooks"
|
||||
@@ -136,25 +133,12 @@ func (r *htmlRenderer) renderPassthroughBlock(w util.BufWriter, src []byte, node
|
||||
s = s[len(delims.Open) : len(s)-len(delims.Close)]
|
||||
|
||||
pctx := &passthroughContext{
|
||||
ordinal: ordinal,
|
||||
page: ctx.DocumentContext().Document,
|
||||
pageInner: r.getPageInner(ctx),
|
||||
BaseContext: render.NewBaseContext(ctx, renderer, node, src, nil, ordinal),
|
||||
inner: s,
|
||||
typ: typ,
|
||||
AttributesHolder: attributes.New(node.Attributes(), attributes.AttributesOwnerGeneral),
|
||||
}
|
||||
|
||||
pctx.createPos = func() htext.Position {
|
||||
if resolver, ok := renderer.(hooks.ElementPositionResolver); ok {
|
||||
return resolver.ResolvePosition(pctx)
|
||||
}
|
||||
return htext.Position{
|
||||
Filename: ctx.DocumentContext().Filename,
|
||||
LineNumber: 1,
|
||||
ColumnNumber: 1,
|
||||
}
|
||||
}
|
||||
|
||||
pr := renderer.(hooks.PassthroughRenderer)
|
||||
|
||||
if err := pr.RenderPassthrough(ctx.RenderContext().Ctx, w, pctx); err != nil {
|
||||
@@ -164,41 +148,15 @@ func (r *htmlRenderer) renderPassthroughBlock(w util.BufWriter, src []byte, node
|
||||
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 passthroughContext struct {
|
||||
page any
|
||||
pageInner any
|
||||
typ string // inner or block
|
||||
inner string
|
||||
ordinal int
|
||||
hooks.BaseContext
|
||||
|
||||
typ string // inner or block
|
||||
inner string
|
||||
|
||||
// This is only used in error situations and is expensive to create,
|
||||
// so delay creation until needed.
|
||||
pos htext.Position
|
||||
posInit sync.Once
|
||||
createPos func() htext.Position
|
||||
*attributes.AttributesHolder
|
||||
}
|
||||
|
||||
func (p *passthroughContext) Page() any {
|
||||
return p.page
|
||||
}
|
||||
|
||||
func (p *passthroughContext) PageInner() any {
|
||||
return p.pageInner
|
||||
}
|
||||
|
||||
func (p *passthroughContext) Type() string {
|
||||
return p.typ
|
||||
}
|
||||
@@ -206,21 +164,3 @@ func (p *passthroughContext) Type() string {
|
||||
func (p *passthroughContext) Inner() string {
|
||||
return p.inner
|
||||
}
|
||||
|
||||
func (p *passthroughContext) Ordinal() int {
|
||||
return p.ordinal
|
||||
}
|
||||
|
||||
func (p *passthroughContext) Position() htext.Position {
|
||||
p.posInit.Do(func() {
|
||||
p.pos = p.createPos()
|
||||
})
|
||||
return p.pos
|
||||
}
|
||||
|
||||
// For internal use.
|
||||
func (p *passthroughContext) PositionerSourceTarget() []byte {
|
||||
return []byte(p.inner)
|
||||
}
|
||||
|
||||
var _ hooks.PositionerSourceTargetProvider = (*passthroughContext)(nil)
|
||||
|
||||
@@ -52,7 +52,7 @@ type linkContext struct {
|
||||
pageInner any
|
||||
destination string
|
||||
title string
|
||||
text hstring.RenderedString
|
||||
text hstring.RenderedHTML
|
||||
plainText string
|
||||
*attributes.AttributesHolder
|
||||
}
|
||||
@@ -69,7 +69,7 @@ func (ctx linkContext) PageInner() any {
|
||||
return ctx.pageInner
|
||||
}
|
||||
|
||||
func (ctx linkContext) Text() hstring.RenderedString {
|
||||
func (ctx linkContext) Text() hstring.RenderedHTML {
|
||||
return ctx.text
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ type headingContext struct {
|
||||
pageInner any
|
||||
level int
|
||||
anchor string
|
||||
text hstring.RenderedString
|
||||
text hstring.RenderedHTML
|
||||
plainText string
|
||||
*attributes.AttributesHolder
|
||||
}
|
||||
@@ -121,7 +121,7 @@ func (ctx headingContext) Anchor() string {
|
||||
return ctx.anchor
|
||||
}
|
||||
|
||||
func (ctx headingContext) Text() hstring.RenderedString {
|
||||
func (ctx headingContext) Text() hstring.RenderedHTML {
|
||||
return ctx.text
|
||||
}
|
||||
|
||||
@@ -169,9 +169,7 @@ func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.N
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
pos := ctx.PopPos()
|
||||
text := ctx.Buffer.Bytes()[pos:]
|
||||
ctx.Buffer.Truncate(pos)
|
||||
text := ctx.PopRenderedString()
|
||||
|
||||
var (
|
||||
isBlock bool
|
||||
@@ -190,16 +188,18 @@ func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.N
|
||||
// internal attributes before rendering.
|
||||
attrs := r.filterInternalAttributes(n.Attributes())
|
||||
|
||||
page, pageInner := render.GetPageAndPageInner(ctx)
|
||||
|
||||
err := lr.RenderLink(
|
||||
ctx.RenderContext().Ctx,
|
||||
w,
|
||||
imageLinkContext{
|
||||
linkContext: linkContext{
|
||||
page: ctx.DocumentContext().Document,
|
||||
pageInner: r.getPageInner(ctx),
|
||||
page: page,
|
||||
pageInner: pageInner,
|
||||
destination: string(n.Destination),
|
||||
title: string(n.Title),
|
||||
text: hstring.RenderedString(text),
|
||||
text: hstring.RenderedHTML(text),
|
||||
plainText: string(n.Text(source)),
|
||||
AttributesHolder: attributes.New(attrs, attributes.AttributesOwnerGeneral),
|
||||
},
|
||||
@@ -211,18 +211,6 @@ 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 {
|
||||
@@ -288,19 +276,19 @@ func (r *hookedRenderer) renderLink(w util.BufWriter, source []byte, node ast.No
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
pos := ctx.PopPos()
|
||||
text := ctx.Buffer.Bytes()[pos:]
|
||||
ctx.Buffer.Truncate(pos)
|
||||
text := ctx.PopRenderedString()
|
||||
|
||||
page, pageInner := render.GetPageAndPageInner(ctx)
|
||||
|
||||
err := lr.RenderLink(
|
||||
ctx.RenderContext().Ctx,
|
||||
w,
|
||||
linkContext{
|
||||
page: ctx.DocumentContext().Document,
|
||||
pageInner: r.getPageInner(ctx),
|
||||
page: page,
|
||||
pageInner: pageInner,
|
||||
destination: string(n.Destination),
|
||||
title: string(n.Title),
|
||||
text: hstring.RenderedString(text),
|
||||
text: hstring.RenderedHTML(text),
|
||||
plainText: string(n.Text(source)),
|
||||
AttributesHolder: attributes.Empty,
|
||||
},
|
||||
@@ -358,14 +346,16 @@ func (r *hookedRenderer) renderAutoLink(w util.BufWriter, source []byte, node as
|
||||
url = "mailto:" + url
|
||||
}
|
||||
|
||||
page, pageInner := render.GetPageAndPageInner(ctx)
|
||||
|
||||
err := lr.RenderLink(
|
||||
ctx.RenderContext().Ctx,
|
||||
w,
|
||||
linkContext{
|
||||
page: ctx.DocumentContext().Document,
|
||||
pageInner: r.getPageInner(ctx),
|
||||
page: page,
|
||||
pageInner: pageInner,
|
||||
destination: url,
|
||||
text: hstring.RenderedString(label),
|
||||
text: hstring.RenderedHTML(label),
|
||||
plainText: label,
|
||||
AttributesHolder: attributes.Empty,
|
||||
},
|
||||
@@ -435,23 +425,24 @@ func (r *hookedRenderer) renderHeading(w util.BufWriter, source []byte, node ast
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
pos := ctx.PopPos()
|
||||
text := ctx.Buffer.Bytes()[pos:]
|
||||
ctx.Buffer.Truncate(pos)
|
||||
text := ctx.PopRenderedString()
|
||||
|
||||
// All ast.Heading nodes are guaranteed to have an attribute called "id"
|
||||
// that is an array of bytes that encode a valid string.
|
||||
anchori, _ := n.AttributeString("id")
|
||||
anchor := anchori.([]byte)
|
||||
|
||||
page, pageInner := render.GetPageAndPageInner(ctx)
|
||||
|
||||
err := hr.RenderHeading(
|
||||
ctx.RenderContext().Ctx,
|
||||
w,
|
||||
headingContext{
|
||||
page: ctx.DocumentContext().Document,
|
||||
pageInner: r.getPageInner(ctx),
|
||||
page: page,
|
||||
pageInner: pageInner,
|
||||
level: n.Level,
|
||||
anchor: string(anchor),
|
||||
text: hstring.RenderedString(text),
|
||||
text: hstring.RenderedHTML(text),
|
||||
plainText: string(n.Text(source)),
|
||||
AttributesHolder: attributes.New(n.Attributes(), attributes.AttributesOwnerGeneral),
|
||||
},
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
// 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 tables
|
||||
|
||||
import (
|
||||
"github.com/gohugoio/hugo/common/herrors"
|
||||
"github.com/gohugoio/hugo/common/types/hstring"
|
||||
"github.com/gohugoio/hugo/markup/converter/hooks"
|
||||
"github.com/gohugoio/hugo/markup/goldmark/internal/render"
|
||||
"github.com/gohugoio/hugo/markup/internal/attributes"
|
||||
"github.com/yuin/goldmark"
|
||||
"github.com/yuin/goldmark/ast"
|
||||
gast "github.com/yuin/goldmark/extension/ast"
|
||||
"github.com/yuin/goldmark/renderer"
|
||||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
||||
type (
|
||||
ext struct{}
|
||||
htmlRenderer struct{}
|
||||
)
|
||||
|
||||
func New() goldmark.Extender {
|
||||
return &ext{}
|
||||
}
|
||||
|
||||
func (e *ext) Extend(m goldmark.Markdown) {
|
||||
m.Renderer().AddOptions(renderer.WithNodeRenderers(
|
||||
util.Prioritized(newHTMLRenderer(), 100),
|
||||
))
|
||||
}
|
||||
|
||||
func newHTMLRenderer() renderer.NodeRenderer {
|
||||
r := &htmlRenderer{}
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *htmlRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
||||
reg.Register(gast.KindTable, r.renderTable)
|
||||
reg.Register(gast.KindTableHeader, r.renderHeaderOrRow)
|
||||
reg.Register(gast.KindTableRow, r.renderHeaderOrRow)
|
||||
reg.Register(gast.KindTableCell, r.renderCell)
|
||||
}
|
||||
|
||||
func (r *htmlRenderer) renderTable(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
ctx := w.(*render.Context)
|
||||
if entering {
|
||||
// This will be modified below.
|
||||
table := &hooks.Table{}
|
||||
ctx.PushValue(gast.KindTable, table)
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
v := ctx.PopValue(gast.KindTable)
|
||||
if v == nil {
|
||||
panic("table not found")
|
||||
}
|
||||
|
||||
table := v.(*hooks.Table)
|
||||
|
||||
renderer := ctx.RenderContext().GetRenderer(hooks.TableRendererType, nil)
|
||||
if renderer == nil {
|
||||
panic("table hook renderer not found")
|
||||
}
|
||||
|
||||
ordinal := ctx.GetAndIncrementOrdinal(gast.KindTable)
|
||||
|
||||
tctx := &tableContext{
|
||||
BaseContext: render.NewBaseContext(ctx, renderer, n, source, nil, ordinal),
|
||||
AttributesHolder: attributes.New(n.Attributes(), attributes.AttributesOwnerGeneral),
|
||||
tHead: table.THead,
|
||||
tBody: table.TBody,
|
||||
}
|
||||
|
||||
cr := renderer.(hooks.TableRenderer)
|
||||
|
||||
err := cr.RenderTable(
|
||||
ctx.RenderContext().Ctx,
|
||||
w,
|
||||
tctx,
|
||||
)
|
||||
if err != nil {
|
||||
return ast.WalkContinue, herrors.NewFileErrorFromPos(err, tctx.Position())
|
||||
}
|
||||
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *htmlRenderer) peekTable(ctx *render.Context) *hooks.Table {
|
||||
v := ctx.PeekValue(gast.KindTable)
|
||||
if v == nil {
|
||||
panic("table not found")
|
||||
}
|
||||
return v.(*hooks.Table)
|
||||
}
|
||||
|
||||
func (r *htmlRenderer) renderCell(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
ctx := w.(*render.Context)
|
||||
|
||||
if entering {
|
||||
// Store the current pos so we can capture the rendered text.
|
||||
ctx.PushPos(ctx.Buffer.Len())
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
n := node.(*gast.TableCell)
|
||||
|
||||
text := ctx.PopRenderedString()
|
||||
|
||||
table := r.peekTable(ctx)
|
||||
|
||||
var alignment string
|
||||
switch n.Alignment {
|
||||
case gast.AlignLeft:
|
||||
alignment = "left"
|
||||
case gast.AlignRight:
|
||||
alignment = "right"
|
||||
case gast.AlignCenter:
|
||||
alignment = "center"
|
||||
default:
|
||||
alignment = "left"
|
||||
}
|
||||
|
||||
cell := hooks.TableCell{Text: hstring.RenderedHTML(text), Alignment: alignment}
|
||||
|
||||
if node.Parent().Kind() == gast.KindTableHeader {
|
||||
table.THead[len(table.THead)-1] = append(table.THead[len(table.THead)-1], cell)
|
||||
} else {
|
||||
table.TBody[len(table.TBody)-1] = append(table.TBody[len(table.TBody)-1], cell)
|
||||
}
|
||||
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *htmlRenderer) renderHeaderOrRow(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
ctx := w.(*render.Context)
|
||||
table := r.peekTable(ctx)
|
||||
if entering {
|
||||
if n.Kind() == gast.KindTableHeader {
|
||||
table.THead = append(table.THead, hooks.TableRow{})
|
||||
} else {
|
||||
table.TBody = append(table.TBody, hooks.TableRow{})
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
type tableContext struct {
|
||||
hooks.BaseContext
|
||||
*attributes.AttributesHolder
|
||||
|
||||
tHead []hooks.TableRow
|
||||
tBody []hooks.TableRow
|
||||
}
|
||||
|
||||
func (c *tableContext) THead() []hooks.TableRow {
|
||||
return c.tHead
|
||||
}
|
||||
|
||||
func (c *tableContext) TBody() []hooks.TableRow {
|
||||
return c.tBody
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
// 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 tables_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
)
|
||||
|
||||
func TestTableHook(t *testing.T) {
|
||||
t.Parallel()
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
[markup.goldmark.parser.attribute]
|
||||
block = true
|
||||
title = true
|
||||
-- content/p1.md --
|
||||
## Table 1
|
||||
|
||||
| Item | In Stock | Price |
|
||||
| :---------------- | :------: | ----: |
|
||||
| Python Hat | True | 23.99 |
|
||||
| SQL **Hat** | True | 23.99 |
|
||||
| Codecademy Tee | False | 19.99 |
|
||||
| Codecademy Hoodie | False | 42.99 |
|
||||
{.foo foo="bar"}
|
||||
|
||||
## Table 2
|
||||
|
||||
| Month | Savings |
|
||||
| -------- | ------- |
|
||||
| January | $250 |
|
||||
| February | $80 |
|
||||
| March | $420 |
|
||||
|
||||
-- layouts/_default/single.html --
|
||||
{{ .Content }}
|
||||
-- layouts/_default/_markup/render-table.html --
|
||||
Attributes: {{ .Attributes }}|
|
||||
{{ template "print" (dict "what" (printf "table-%d-thead" $.Ordinal) "rows" .THead) }}
|
||||
{{ template "print" (dict "what" (printf "table-%d-tbody" $.Ordinal) "rows" .TBody) }}
|
||||
{{ define "print" }}
|
||||
{{ .what }}:{{ range $i, $a := .rows }} {{ $i }}:{{ range $j, $b := . }} {{ $j }}: {{ .Alignment }}: {{ .Text }}|{{ end }}{{ end }}$
|
||||
{{ end }}
|
||||
|
||||
`
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/p1/index.html",
|
||||
"Attributes: map[class:foo foo:bar]|",
|
||||
"table-0-thead: 0: 0: left: Item| 1: center: In Stock| 2: right: Price|$",
|
||||
"table-0-tbody: 0: 0: left: Python Hat| 1: center: True| 2: right: 23.99| 1: 0: left: SQL <strong>Hat</strong>| 1: center: True| 2: right: 23.99| 2: 0: left: Codecademy Tee| 1: center: False| 2: right: 19.99| 3: 0: left: Codecademy Hoodie| 1: center: False| 2: right: 42.99|$",
|
||||
)
|
||||
|
||||
b.AssertFileContent("public/p1/index.html",
|
||||
"table-1-thead: 0: 0: left: Month| 1: left: Savings|$",
|
||||
"table-1-tbody: 0: 0: left: January| 1: left: $250| 1: 0: left: February| 1: left: $80| 2: 0: left: March| 1: left: $420|$",
|
||||
)
|
||||
}
|
||||
|
||||
func TestTableDefault(t *testing.T) {
|
||||
t.Parallel()
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
[markup.goldmark.parser.attribute]
|
||||
block = true
|
||||
title = true
|
||||
-- content/p1.md --
|
||||
|
||||
## Table 1
|
||||
|
||||
| Item | In Stock | Price |
|
||||
| :---------------- | :------: | ----: |
|
||||
| Python Hat | True | 23.99 |
|
||||
| SQL Hat | True | 23.99 |
|
||||
| Codecademy Tee | False | 19.99 |
|
||||
| Codecademy Hoodie | False | 42.99 |
|
||||
{.foo}
|
||||
|
||||
|
||||
-- layouts/_default/single.html --
|
||||
Summary: {{ .Summary }}
|
||||
Content: {{ .Content }}
|
||||
|
||||
`
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/p1/index.html", "<table class=\"foo\">")
|
||||
}
|
||||
|
||||
// Issue 12811.
|
||||
func TestTableDefaultRSSAndHTML(t *testing.T) {
|
||||
t.Parallel()
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
[outputFormats]
|
||||
[outputFormats.rss]
|
||||
weight = 30
|
||||
[outputFormats.html]
|
||||
weight = 20
|
||||
-- content/_index.md --
|
||||
---
|
||||
title: "Home"
|
||||
output: ["rss", "html"]
|
||||
---
|
||||
|
||||
| Item | In Stock | Price |
|
||||
| :---------------- | :------: | ----: |
|
||||
| Python Hat | True | 23.99 |
|
||||
| SQL Hat | True | 23.99 |
|
||||
| Codecademy Tee | False | 19.99 |
|
||||
| Codecademy Hoodie | False | 42.99 |
|
||||
|
||||
{{< foo >}}
|
||||
|
||||
-- layouts/index.html --
|
||||
Content: {{ .Content }}
|
||||
-- layouts/index.xml --
|
||||
Content: {{ .Content }}
|
||||
-- layouts/shortcodes/foo.xml --
|
||||
foo xml
|
||||
-- layouts/shortcodes/foo.html --
|
||||
foo html
|
||||
|
||||
`
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/index.xml", "<table>")
|
||||
b.AssertFileContent("public/index.html", "<table>")
|
||||
}
|
||||
|
||||
func TestTableDefaultRSSOnly(t *testing.T) {
|
||||
t.Parallel()
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
[outputs]
|
||||
home = ['rss']
|
||||
section = ['rss']
|
||||
taxonomy = ['rss']
|
||||
term = ['rss']
|
||||
page = ['rss']
|
||||
disableKinds = ["taxonomy", "term", "page", "section"]
|
||||
-- content/_index.md --
|
||||
---
|
||||
title: "Home"
|
||||
---
|
||||
|
||||
## Table 1
|
||||
|
||||
| Item | In Stock | Price |
|
||||
| :---------------- | :------: | ----: |
|
||||
| Python Hat | True | 23.99 |
|
||||
| SQL Hat | True | 23.99 |
|
||||
| Codecademy Tee | False | 19.99 |
|
||||
| Codecademy Hoodie | False | 42.99 |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
-- layouts/index.xml --
|
||||
Content: {{ .Content }}
|
||||
|
||||
|
||||
`
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/index.xml", "<table>")
|
||||
}
|
||||
@@ -365,18 +365,6 @@ func (c *Client) Get(args ...string) error {
|
||||
}
|
||||
|
||||
func (c *Client) get(args ...string) error {
|
||||
var hasD bool
|
||||
for _, arg := range args {
|
||||
if arg == "-d" {
|
||||
hasD = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasD {
|
||||
// go get without the -d flag does not make sense to us, as
|
||||
// it will try to build and install go packages.
|
||||
args = append([]string{"-d"}, args...)
|
||||
}
|
||||
if err := c.runGo(context.Background(), c.logger.Out(), append([]string{"get"}, args...)...); err != nil {
|
||||
return fmt.Errorf("failed to get %q: %w", args, err)
|
||||
}
|
||||
|
||||
@@ -46,8 +46,8 @@ func createLayoutExamples() any {
|
||||
{"Base template for single page in \"posts\" section", layouts.LayoutDescriptor{Baseof: true, Kind: "page", Type: "posts", OutputFormatName: "html", Suffix: "html"}},
|
||||
{"Single page in \"posts\" section with layout set to \"demolayout\"", layouts.LayoutDescriptor{Kind: "page", Type: "posts", Layout: demoLayout, OutputFormatName: "html", Suffix: "html"}},
|
||||
{"Base template for single page in \"posts\" section with layout set to \"demolayout\"", layouts.LayoutDescriptor{Baseof: true, Kind: "page", Type: "posts", Layout: demoLayout, OutputFormatName: "html", Suffix: "html"}},
|
||||
{"AMP single page", layouts.LayoutDescriptor{Kind: "page", Type: "posts", OutputFormatName: "amp", Suffix: "html"}},
|
||||
{"AMP single page, French language", layouts.LayoutDescriptor{Kind: "page", Type: "posts", Lang: "fr", OutputFormatName: "html", Suffix: "html"}},
|
||||
{"AMP single page in \"posts\" section", layouts.LayoutDescriptor{Kind: "page", Type: "posts", OutputFormatName: "amp", Suffix: "html"}},
|
||||
{"AMP single page in \"posts\" section, French language", layouts.LayoutDescriptor{Kind: "page", Type: "posts", Lang: "fr", OutputFormatName: "amp", Suffix: "html"}},
|
||||
// Typeless pages get "page" as type
|
||||
{"Home page", layouts.LayoutDescriptor{Kind: "home", Type: "page", OutputFormatName: "html", Suffix: "html"}},
|
||||
{"Base template for home page", layouts.LayoutDescriptor{Baseof: true, Kind: "home", Type: "page", OutputFormatName: "html", Suffix: "html"}},
|
||||
|
||||
@@ -74,10 +74,17 @@ type ChildCareProvider interface {
|
||||
Resources() resource.Resources
|
||||
}
|
||||
|
||||
type MarkupProvider interface {
|
||||
Markup(opts ...any) Markup
|
||||
}
|
||||
|
||||
// ContentProvider provides the content related values for a Page.
|
||||
type ContentProvider interface {
|
||||
Content(context.Context) (any, error)
|
||||
|
||||
// ContentWithoutSummary returns the Page Content stripped of the summary.
|
||||
ContentWithoutSummary(ctx context.Context) (template.HTML, error)
|
||||
|
||||
// Plain returns the Page Content stripped of HTML markup.
|
||||
Plain(context.Context) string
|
||||
|
||||
@@ -169,6 +176,7 @@ type PageProvider interface {
|
||||
|
||||
// Page is the core interface in Hugo and what you get as the top level data context in your templates.
|
||||
type Page interface {
|
||||
MarkupProvider
|
||||
ContentProvider
|
||||
TableOfContentsProvider
|
||||
PageWithoutContent
|
||||
@@ -260,7 +268,7 @@ type PageMetaInternalProvider interface {
|
||||
type PageRenderProvider interface {
|
||||
// Render renders the given layout with this Page as context.
|
||||
Render(ctx context.Context, layout ...string) (template.HTML, error)
|
||||
// RenderString renders the first value in args with tPaginatorhe content renderer defined
|
||||
// RenderString renders the first value in args with the content renderer defined
|
||||
// for this Page.
|
||||
// It takes an optional map as a second argument:
|
||||
//
|
||||
|
||||
@@ -35,6 +35,7 @@ type OutputFormatContentProvider interface {
|
||||
|
||||
// OutputFormatPageContentProvider holds the exported methods from Page that are "outputFormat aware".
|
||||
type OutputFormatPageContentProvider interface {
|
||||
MarkupProvider
|
||||
ContentProvider
|
||||
TableOfContentsProvider
|
||||
PageRenderProvider
|
||||
@@ -74,6 +75,11 @@ func (lcp *LazyContentProvider) Reset() {
|
||||
lcp.init.Reset()
|
||||
}
|
||||
|
||||
func (lcp *LazyContentProvider) Markup(opts ...any) Markup {
|
||||
lcp.init.Do(context.Background())
|
||||
return lcp.cp.Markup(opts...)
|
||||
}
|
||||
|
||||
func (lcp *LazyContentProvider) TableOfContents(ctx context.Context) template.HTML {
|
||||
lcp.init.Do(ctx)
|
||||
return lcp.cp.TableOfContents(ctx)
|
||||
@@ -89,6 +95,11 @@ func (lcp *LazyContentProvider) Content(ctx context.Context) (any, error) {
|
||||
return lcp.cp.Content(ctx)
|
||||
}
|
||||
|
||||
func (lcp *LazyContentProvider) ContentWithoutSummary(ctx context.Context) (template.HTML, error) {
|
||||
lcp.init.Do(ctx)
|
||||
return lcp.cp.ContentWithoutSummary(ctx)
|
||||
}
|
||||
|
||||
func (lcp *LazyContentProvider) Plain(ctx context.Context) string {
|
||||
lcp.init.Do(ctx)
|
||||
return lcp.cp.Plain(ctx)
|
||||
|
||||
@@ -0,0 +1,344 @@
|
||||
// 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 page
|
||||
|
||||
import (
|
||||
"context"
|
||||
"html/template"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
"github.com/gohugoio/hugo/markup/tableofcontents"
|
||||
"github.com/gohugoio/hugo/media"
|
||||
"github.com/gohugoio/hugo/tpl"
|
||||
)
|
||||
|
||||
type Content interface {
|
||||
Content(context.Context) (template.HTML, error)
|
||||
ContentWithoutSummary(context.Context) (template.HTML, error)
|
||||
Summary(context.Context) (Summary, error)
|
||||
Plain(context.Context) string
|
||||
PlainWords(context.Context) []string
|
||||
WordCount(context.Context) int
|
||||
FuzzyWordCount(context.Context) int
|
||||
ReadingTime(context.Context) int
|
||||
Len(context.Context) int
|
||||
}
|
||||
|
||||
type Markup interface {
|
||||
Render(context.Context) (Content, error)
|
||||
RenderString(ctx context.Context, args ...any) (template.HTML, error)
|
||||
RenderShortcodes(context.Context) (template.HTML, error)
|
||||
Fragments(context.Context) *tableofcontents.Fragments
|
||||
}
|
||||
|
||||
var _ types.PrintableValueProvider = Summary{}
|
||||
|
||||
const (
|
||||
SummaryTypeAuto = "auto"
|
||||
SummaryTypeManual = "manual"
|
||||
SummaryTypeFrontMatter = "frontmatter"
|
||||
)
|
||||
|
||||
type Summary struct {
|
||||
Text template.HTML
|
||||
Type string // "auto", "manual" or "frontmatter"
|
||||
Truncated bool
|
||||
}
|
||||
|
||||
func (s Summary) IsZero() bool {
|
||||
return s.Text == ""
|
||||
}
|
||||
|
||||
func (s Summary) PrintableValue() any {
|
||||
return s.Text
|
||||
}
|
||||
|
||||
var _ types.PrintableValueProvider = (*Summary)(nil)
|
||||
|
||||
type HtmlSummary struct {
|
||||
source string
|
||||
SummaryLowHigh types.LowHigh[string]
|
||||
SummaryEndTag types.LowHigh[string]
|
||||
WrapperStart types.LowHigh[string]
|
||||
WrapperEnd types.LowHigh[string]
|
||||
Divider types.LowHigh[string]
|
||||
}
|
||||
|
||||
func (s HtmlSummary) wrap(ss string) string {
|
||||
if s.WrapperStart.IsZero() {
|
||||
return ss
|
||||
}
|
||||
return s.source[s.WrapperStart.Low:s.WrapperStart.High] + ss + s.source[s.WrapperEnd.Low:s.WrapperEnd.High]
|
||||
}
|
||||
|
||||
func (s HtmlSummary) wrapLeft(ss string) string {
|
||||
if s.WrapperStart.IsZero() {
|
||||
return ss
|
||||
}
|
||||
|
||||
return s.source[s.WrapperStart.Low:s.WrapperStart.High] + ss
|
||||
}
|
||||
|
||||
func (s HtmlSummary) Value(l types.LowHigh[string]) string {
|
||||
return s.source[l.Low:l.High]
|
||||
}
|
||||
|
||||
func (s HtmlSummary) trimSpace(ss string) string {
|
||||
return strings.TrimSpace(ss)
|
||||
}
|
||||
|
||||
func (s HtmlSummary) Content() string {
|
||||
if s.Divider.IsZero() {
|
||||
return s.source
|
||||
}
|
||||
ss := s.source[:s.Divider.Low]
|
||||
ss += s.source[s.Divider.High:]
|
||||
return s.trimSpace(ss)
|
||||
}
|
||||
|
||||
func (s HtmlSummary) Summary() string {
|
||||
if s.Divider.IsZero() {
|
||||
return s.trimSpace(s.wrap(s.Value(s.SummaryLowHigh)))
|
||||
}
|
||||
ss := s.source[s.SummaryLowHigh.Low:s.Divider.Low]
|
||||
if s.SummaryLowHigh.High > s.Divider.High {
|
||||
ss += s.source[s.Divider.High:s.SummaryLowHigh.High]
|
||||
}
|
||||
if !s.SummaryEndTag.IsZero() {
|
||||
ss += s.Value(s.SummaryEndTag)
|
||||
}
|
||||
return s.trimSpace(s.wrap(ss))
|
||||
}
|
||||
|
||||
func (s HtmlSummary) ContentWithoutSummary() string {
|
||||
if s.Divider.IsZero() {
|
||||
if s.SummaryLowHigh.Low == s.WrapperStart.High && s.SummaryLowHigh.High == s.WrapperEnd.Low {
|
||||
return ""
|
||||
}
|
||||
return s.trimSpace(s.wrapLeft(s.source[s.SummaryLowHigh.High:]))
|
||||
}
|
||||
if s.SummaryEndTag.IsZero() {
|
||||
return s.trimSpace(s.wrapLeft(s.source[s.Divider.High:]))
|
||||
}
|
||||
return s.trimSpace(s.wrapLeft(s.source[s.SummaryEndTag.High:]))
|
||||
}
|
||||
|
||||
func (s HtmlSummary) Truncated() bool {
|
||||
return s.SummaryLowHigh.High < len(s.source)
|
||||
}
|
||||
|
||||
func (s *HtmlSummary) resolveParagraphTagAndSetWrapper(mt media.Type) tagReStartEnd {
|
||||
ptag := startEndP
|
||||
|
||||
switch mt.SubType {
|
||||
case media.DefaultContentTypes.AsciiDoc.SubType:
|
||||
ptag = startEndDiv
|
||||
case media.DefaultContentTypes.ReStructuredText.SubType:
|
||||
const markerStart = "<div class=\"document\">"
|
||||
const markerEnd = "</div>"
|
||||
i1 := strings.Index(s.source, markerStart)
|
||||
i2 := strings.LastIndex(s.source, markerEnd)
|
||||
if i1 > -1 && i2 > -1 {
|
||||
s.WrapperStart = types.LowHigh[string]{Low: 0, High: i1 + len(markerStart)}
|
||||
s.WrapperEnd = types.LowHigh[string]{Low: i2, High: len(s.source)}
|
||||
}
|
||||
}
|
||||
return ptag
|
||||
}
|
||||
|
||||
// ExtractSummaryFromHTML extracts a summary from the given HTML content.
|
||||
func ExtractSummaryFromHTML(mt media.Type, input string, numWords int, isCJK bool) (result HtmlSummary) {
|
||||
result.source = input
|
||||
ptag := result.resolveParagraphTagAndSetWrapper(mt)
|
||||
|
||||
if numWords <= 0 {
|
||||
return result
|
||||
}
|
||||
|
||||
var count int
|
||||
|
||||
countWord := func(word string) int {
|
||||
if isCJK {
|
||||
word = tpl.StripHTML(word)
|
||||
runeCount := utf8.RuneCountInString(word)
|
||||
if len(word) == runeCount {
|
||||
return 1
|
||||
} else {
|
||||
return runeCount
|
||||
}
|
||||
}
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
high := len(input)
|
||||
if result.WrapperEnd.Low > 0 {
|
||||
high = result.WrapperEnd.Low
|
||||
}
|
||||
|
||||
for j := result.WrapperStart.High; j < high; {
|
||||
s := input[j:]
|
||||
closingIndex := strings.Index(s, "</"+ptag.tagName)
|
||||
|
||||
if closingIndex == -1 {
|
||||
break
|
||||
}
|
||||
|
||||
s = s[:closingIndex]
|
||||
|
||||
// Count the words in the current paragraph.
|
||||
var wi int
|
||||
|
||||
for i, r := range s {
|
||||
if unicode.IsSpace(r) || (i+utf8.RuneLen(r) == len(s)) {
|
||||
word := s[wi:i]
|
||||
count += countWord(word)
|
||||
wi = i
|
||||
if count >= numWords {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if count >= numWords {
|
||||
result.SummaryLowHigh = types.LowHigh[string]{
|
||||
Low: result.WrapperStart.High,
|
||||
High: j + closingIndex + len(ptag.tagName) + 3,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
j += closingIndex + len(ptag.tagName) + 2
|
||||
|
||||
}
|
||||
|
||||
result.SummaryLowHigh = types.LowHigh[string]{
|
||||
Low: result.WrapperStart.High,
|
||||
High: high,
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ExtractSummaryFromHTMLWithDivider extracts a summary from the given HTML content with
|
||||
// a manual summary divider.
|
||||
func ExtractSummaryFromHTMLWithDivider(mt media.Type, input, divider string) (result HtmlSummary) {
|
||||
result.source = input
|
||||
result.Divider.Low = strings.Index(input, divider)
|
||||
result.Divider.High = result.Divider.Low + len(divider)
|
||||
|
||||
if result.Divider.Low == -1 {
|
||||
// No summary.
|
||||
return
|
||||
}
|
||||
|
||||
ptag := result.resolveParagraphTagAndSetWrapper(mt)
|
||||
|
||||
if !mt.IsHTML() {
|
||||
result.Divider, result.SummaryEndTag = expandSummaryDivider(result.source, ptag, result.Divider)
|
||||
}
|
||||
|
||||
result.SummaryLowHigh = types.LowHigh[string]{
|
||||
Low: result.WrapperStart.High,
|
||||
High: result.Divider.Low,
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
pOrDiv = regexp.MustCompile(`<p[^>]?>|<div[^>]?>$`)
|
||||
|
||||
startEndDiv = tagReStartEnd{
|
||||
startEndOfString: regexp.MustCompile(`<div[^>]*?>$`),
|
||||
endEndOfString: regexp.MustCompile(`</div>$`),
|
||||
tagName: "div",
|
||||
}
|
||||
|
||||
startEndP = tagReStartEnd{
|
||||
startEndOfString: regexp.MustCompile(`<p[^>]*?>$`),
|
||||
endEndOfString: regexp.MustCompile(`</p>$`),
|
||||
tagName: "p",
|
||||
}
|
||||
)
|
||||
|
||||
type tagReStartEnd struct {
|
||||
startEndOfString *regexp.Regexp
|
||||
endEndOfString *regexp.Regexp
|
||||
tagName string
|
||||
}
|
||||
|
||||
func expandSummaryDivider(s string, re tagReStartEnd, divider types.LowHigh[string]) (types.LowHigh[string], types.LowHigh[string]) {
|
||||
var endMarkup types.LowHigh[string]
|
||||
|
||||
if divider.IsZero() {
|
||||
return divider, endMarkup
|
||||
}
|
||||
|
||||
lo, hi := divider.Low, divider.High
|
||||
|
||||
var preserveEndMarkup bool
|
||||
|
||||
// Find the start of the paragraph.
|
||||
|
||||
for i := lo - 1; i >= 0; i-- {
|
||||
if s[i] == '>' {
|
||||
if match := re.startEndOfString.FindString(s[:i+1]); match != "" {
|
||||
lo = i - len(match) + 1
|
||||
break
|
||||
}
|
||||
if match := pOrDiv.FindString(s[:i+1]); match != "" {
|
||||
i -= len(match) - 1
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
r, _ := utf8.DecodeRuneInString(s[i:])
|
||||
if !unicode.IsSpace(r) {
|
||||
preserveEndMarkup = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
divider.Low = lo
|
||||
|
||||
// Now walk forward to the end of the paragraph.
|
||||
for ; hi < len(s); hi++ {
|
||||
if s[hi] != '>' {
|
||||
continue
|
||||
}
|
||||
if match := re.endEndOfString.FindString(s[:hi+1]); match != "" {
|
||||
hi++
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if preserveEndMarkup {
|
||||
endMarkup.Low = divider.High
|
||||
endMarkup.High = hi
|
||||
} else {
|
||||
divider.High = hi
|
||||
}
|
||||
|
||||
// Consume trailing newline if any.
|
||||
if divider.High < len(s) && s[divider.High] == '\n' {
|
||||
divider.High++
|
||||
}
|
||||
|
||||
return divider, endMarkup
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
// 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 page_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
"github.com/gohugoio/hugo/markup/asciidocext"
|
||||
"github.com/gohugoio/hugo/markup/rst"
|
||||
)
|
||||
|
||||
func TestPageMarkupMethods(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
summaryLength=2
|
||||
-- content/p1.md --
|
||||
---
|
||||
title: "Post 1"
|
||||
date: "2020-01-01"
|
||||
---
|
||||
{{% foo %}}
|
||||
-- layouts/shortcodes/foo.html --
|
||||
Two *words*.
|
||||
{{/* Test that markup scope is set in all relevant constructs. */}}
|
||||
{{ if eq hugo.Context.MarkupScope "foo" }}
|
||||
|
||||
## Heading 1
|
||||
Sint ad mollit qui Lorem ut occaecat culpa officia. Et consectetur aute voluptate non sit ullamco adipisicing occaecat. Sunt deserunt amet sit ad. Deserunt enim voluptate proident ipsum dolore dolor ut sit velit esse est mollit irure esse. Mollit incididunt veniam laboris magna et excepteur sit duis. Magna adipisicing reprehenderit tempor irure.
|
||||
### Heading 2
|
||||
Exercitation quis est consectetur occaecat nostrud. Ullamco aute mollit aliqua est amet. Exercitation ullamco consectetur dolor labore et non irure eu cillum Lorem.
|
||||
{{ end }}
|
||||
-- layouts/index.html --
|
||||
Home.
|
||||
{{ .Content }}
|
||||
-- layouts/_default/single.html --
|
||||
Single.
|
||||
Page.ContentWithoutSummmary: {{ .ContentWithoutSummary }}|
|
||||
{{ template "render-scope" (dict "page" . "scope" "main") }}
|
||||
{{ template "render-scope" (dict "page" . "scope" "foo") }}
|
||||
{{ define "render-scope" }}
|
||||
{{ $c := .page.Markup .scope }}
|
||||
{{ with $c.Render }}
|
||||
{{ $.scope }}: Content: {{ .Content }}|
|
||||
{{ $.scope }}: ContentWithoutSummary: {{ .ContentWithoutSummary }}|
|
||||
{{ $.scope }}: Plain: {{ .Plain }}|
|
||||
{{ $.scope }}: PlainWords: {{ .PlainWords }}|
|
||||
{{ $.scope }}: WordCount: {{ .WordCount }}|
|
||||
{{ $.scope }}: FuzzyWordCount: {{ .FuzzyWordCount }}|
|
||||
{{ $.scope }}: ReadingTime: {{ .ReadingTime }}|
|
||||
{{ $.scope }}: Len: {{ .Len }}|
|
||||
{{ $.scope }}: Summary: {{ with .Summary }}{{ . }}{{ else }}nil{{ end }}|
|
||||
{{ end }}
|
||||
{{ $.scope }}: Fragments: {{ $c.Fragments.Identifiers }}|
|
||||
{{ end }}
|
||||
|
||||
|
||||
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
// Main scope.
|
||||
b.AssertFileContent("public/p1/index.html",
|
||||
"Page.ContentWithoutSummmary: |",
|
||||
"main: Content: <p>Two <em>words</em>.</p>\n|",
|
||||
"main: ContentWithoutSummary: |",
|
||||
"main: Plain: Two words.\n|",
|
||||
"PlainWords: [Two words.]|\nmain: WordCount: 2|\nmain: FuzzyWordCount: 100|\nmain: ReadingTime: 1|",
|
||||
"main: Summary: <p>Two <em>words</em>.</p>|\n\nmain: Fragments: []|",
|
||||
"main: Len: 27|",
|
||||
)
|
||||
|
||||
// Foo scope (has more content).
|
||||
b.AssertFileContent("public/p1/index.html",
|
||||
"foo: Content: <p>Two <em>words</em>.</p>\n<h2",
|
||||
"foo: ContentWithoutSummary: <h2",
|
||||
"Plain: Two words.\nHeading 1",
|
||||
"PlainWords: [Two words. Heading 1",
|
||||
"foo: WordCount: 81|\nfoo: FuzzyWordCount: 100|\nfoo: ReadingTime: 1|\nfoo: Len: 622|",
|
||||
"foo: Summary: <p>Two <em>words</em>.</p>|",
|
||||
"foo: Fragments: [heading-1 heading-2]|",
|
||||
)
|
||||
}
|
||||
|
||||
func TestPageMarkupScope(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
disableKinds = ["taxonomy", "term", "rss", "section"]
|
||||
-- content/p1.md --
|
||||
---
|
||||
title: "Post 1"
|
||||
date: "2020-01-01"
|
||||
---
|
||||
|
||||
# P1
|
||||
|
||||
{{< foo >}}
|
||||
|
||||
Begin:{{% includerendershortcodes "p2" %}}:End
|
||||
Begin:{{< includecontent "p3" >}}:End
|
||||
|
||||
-- content/p2.md --
|
||||
---
|
||||
title: "Post 2"
|
||||
date: "2020-01-02"
|
||||
---
|
||||
|
||||
# P2
|
||||
-- content/p3.md --
|
||||
---
|
||||
title: "Post 3"
|
||||
date: "2020-01-03"
|
||||
---
|
||||
|
||||
# P3
|
||||
|
||||
{{< foo >}}
|
||||
|
||||
-- layouts/index.html --
|
||||
Home.
|
||||
{{ with site.GetPage "p1" }}
|
||||
{{ with .Markup "home" }}
|
||||
{{ .Render.Content }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
-- layouts/_default/single.html --
|
||||
Single.
|
||||
{{ with .Markup }}
|
||||
{{ with .Render }}
|
||||
{{ .Content }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
-- layouts/_default/_markup/render-heading.html --
|
||||
Render heading: title: {{ .Text}} scope: {{ hugo.Context.MarkupScope }}|
|
||||
-- layouts/shortcodes/foo.html --
|
||||
Foo scope: {{ hugo.Context.MarkupScope }}|
|
||||
-- layouts/shortcodes/includerendershortcodes.html --
|
||||
{{ $p := site.GetPage (.Get 0) }}
|
||||
includerendershortcodes: {{ hugo.Context.MarkupScope }}|{{ $p.Markup.RenderShortcodes }}|
|
||||
-- layouts/shortcodes/includecontent.html --
|
||||
{{ $p := site.GetPage (.Get 0) }}
|
||||
includecontent: {{ hugo.Context.MarkupScope }}|{{ $p.Markup.Render.Content }}|
|
||||
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/p1/index.html", "Render heading: title: P1 scope: |", "Foo scope: |")
|
||||
|
||||
b.AssertFileContent("public/index.html",
|
||||
"Render heading: title: P1 scope: home|",
|
||||
"Foo scope: home|",
|
||||
"Begin:\nincluderendershortcodes: home|</p>\nRender heading: title: P2 scope: home|<p>|:End",
|
||||
"Begin:\nincludecontent: home|Render heading: title: P3 scope: home|Foo scope: home|\n|\n:End",
|
||||
)
|
||||
}
|
||||
|
||||
func TestPageMarkupWithoutSummary(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
summaryLength=5
|
||||
-- content/p1.md --
|
||||
---
|
||||
title: "Post 1"
|
||||
date: "2020-01-01"
|
||||
---
|
||||
This is summary.
|
||||
<!--more-->
|
||||
This is content.
|
||||
-- content/p2.md --
|
||||
---
|
||||
title: "Post 2"
|
||||
date: "2020-01-01"
|
||||
---
|
||||
This is some content about a summary and more.
|
||||
|
||||
Another paragraph.
|
||||
|
||||
Third paragraph.
|
||||
-- layouts/_default/single.html --
|
||||
Single.
|
||||
Page.Summary: {{ .Summary }}|
|
||||
{{ with .Markup.Render }}
|
||||
Content: {{ .Content }}|
|
||||
ContentWithoutSummary: {{ .ContentWithoutSummary }}|
|
||||
WordCount: {{ .WordCount }}|
|
||||
FuzzyWordCount: {{ .FuzzyWordCount }}|
|
||||
{{ with .Summary }}
|
||||
Summary: {{ . }}|
|
||||
Summary Type: {{ .Type }}|
|
||||
Summary Truncated: {{ .Truncated }}|
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
`
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
b.AssertFileContentExact("public/p1/index.html",
|
||||
"Content: <p>This is summary.</p>\n<p>This is content.</p>",
|
||||
"ContentWithoutSummary: <p>This is content.</p>|",
|
||||
"WordCount: 6|",
|
||||
"FuzzyWordCount: 100|",
|
||||
"Summary: <p>This is summary.</p>|",
|
||||
"Summary Type: manual|",
|
||||
"Summary Truncated: true|",
|
||||
)
|
||||
b.AssertFileContent("public/p2/index.html",
|
||||
"Summary: <p>This is some content about a summary and more.</p>|",
|
||||
"WordCount: 13|",
|
||||
"FuzzyWordCount: 100|",
|
||||
"Summary Type: auto",
|
||||
"Summary Truncated: true",
|
||||
)
|
||||
}
|
||||
|
||||
func TestPageMarkupWithoutSummaryRST(t *testing.T) {
|
||||
t.Parallel()
|
||||
if !rst.Supports() {
|
||||
t.Skip("Skip RST test as not supported")
|
||||
}
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
summaryLength=5
|
||||
[security.exec]
|
||||
allow = ["rst", "python"]
|
||||
|
||||
-- content/p1.rst --
|
||||
This is a story about a summary and more.
|
||||
|
||||
Another paragraph.
|
||||
-- content/p2.rst --
|
||||
This is summary.
|
||||
<!--more-->
|
||||
This is content.
|
||||
-- layouts/_default/single.html --
|
||||
Single.
|
||||
Page.Summary: {{ .Summary }}|
|
||||
{{ with .Markup.Render }}
|
||||
Content: {{ .Content }}|
|
||||
ContentWithoutSummary: {{ .ContentWithoutSummary }}|
|
||||
{{ with .Summary }}
|
||||
Summary: {{ . }}|
|
||||
Summary Type: {{ .Type }}|
|
||||
Summary Truncated: {{ .Truncated }}|
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
// Auto summary.
|
||||
b.AssertFileContentExact("public/p1/index.html",
|
||||
"Content: <div class=\"document\">\n\n\n<p>This is a story about a summary and more.</p>\n<p>Another paragraph.</p>\n</div>|",
|
||||
"Summary: <div class=\"document\">\n\n\n<p>This is a story about a summary and more.</p></div>|\nSummary Type: auto|\nSummary Truncated: true|",
|
||||
"ContentWithoutSummary: <div class=\"document\">\n<p>Another paragraph.</p>\n</div>|",
|
||||
)
|
||||
|
||||
// Manual summary.
|
||||
b.AssertFileContentExact("public/p2/index.html",
|
||||
"Content: <div class=\"document\">\n\n\n<p>This is summary.</p>\n<p>This is content.</p>\n</div>|",
|
||||
"ContentWithoutSummary: <div class=\"document\"><p>This is content.</p>\n</div>|",
|
||||
"Summary: <div class=\"document\">\n\n\n<p>This is summary.</p>\n</div>|\nSummary Type: manual|\nSummary Truncated: true|",
|
||||
)
|
||||
}
|
||||
|
||||
func TestPageMarkupWithoutSummaryAsciidoc(t *testing.T) {
|
||||
t.Parallel()
|
||||
if !asciidocext.Supports() {
|
||||
t.Skip("Skip asiidoc test as not supported")
|
||||
}
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
summaryLength=5
|
||||
[security.exec]
|
||||
allow = ["asciidoc", "python"]
|
||||
|
||||
-- content/p1.ad --
|
||||
This is a story about a summary and more.
|
||||
|
||||
Another paragraph.
|
||||
-- content/p2.ad --
|
||||
This is summary.
|
||||
<!--more-->
|
||||
This is content.
|
||||
-- layouts/_default/single.html --
|
||||
Single.
|
||||
Page.Summary: {{ .Summary }}|
|
||||
{{ with .Markup.Render }}
|
||||
Content: {{ .Content }}|
|
||||
ContentWithoutSummary: {{ .ContentWithoutSummary }}|
|
||||
{{ with .Summary }}
|
||||
Summary: {{ . }}|
|
||||
Summary Type: {{ .Type }}|
|
||||
Summary Truncated: {{ .Truncated }}|
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
`
|
||||
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
// Auto summary.
|
||||
b.AssertFileContentExact("public/p1/index.html",
|
||||
"Content: <div class=\"paragraph\">\n<p>This is a story about a summary and more.</p>\n</div>\n<div class=\"paragraph\">\n<p>Another paragraph.</p>\n</div>\n|",
|
||||
"Summary: <div class=\"paragraph\">\n<p>This is a story about a summary and more.</p>\n</div>|",
|
||||
"Summary Type: auto|\nSummary Truncated: true|",
|
||||
"ContentWithoutSummary: <div class=\"paragraph\">\n<p>Another paragraph.</p>\n</div>|",
|
||||
)
|
||||
|
||||
// Manual summary.
|
||||
b.AssertFileContentExact("public/p2/index.html",
|
||||
"Content: <div class=\"paragraph\">\n<p>This is summary.</p>\n</div>\n<div class=\"paragraph\">\n<p>This is content.</p>\n</div>|",
|
||||
"ContentWithoutSummary: <div class=\"paragraph\">\n<p>This is content.</p>\n</div>|",
|
||||
"Summary: <div class=\"paragraph\">\n<p>This is summary.</p>\n</div>|\nSummary Type: manual|\nSummary Truncated: true|",
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
// 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 page
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"github.com/gohugoio/hugo/common/types"
|
||||
"github.com/gohugoio/hugo/media"
|
||||
)
|
||||
|
||||
func TestExtractSummaryFromHTML(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
tests := []struct {
|
||||
mt media.Type
|
||||
input string
|
||||
isCJK bool
|
||||
numWords int
|
||||
expectSummary string
|
||||
expectContentWithoutSummary string
|
||||
}{
|
||||
{media.Builtin.ReStructuredTextType, "<div class=\"document\">\n\n\n<p>Simple Page</p>\n</div>", false, 70, "<div class=\"document\">\n\n\n<p>Simple Page</p>\n</div>", ""},
|
||||
{media.Builtin.ReStructuredTextType, "<div class=\"document\"><p>First paragraph</p><p>Second paragraph</p></div>", false, 2, `<div class="document"><p>First paragraph</p></div>`, "<div class=\"document\"><p>Second paragraph</p></div>"},
|
||||
{media.Builtin.MarkdownType, "<p>First paragraph</p>", false, 10, "<p>First paragraph</p>", ""},
|
||||
{media.Builtin.MarkdownType, "<p>First paragraph</p><p>Second paragraph</p>", false, 2, "<p>First paragraph</p>", "<p>Second paragraph</p>"},
|
||||
{media.Builtin.MarkdownType, "<p>First paragraph</p><p>Second paragraph</p><p>Third paragraph</p>", false, 3, "<p>First paragraph</p><p>Second paragraph</p>", "<p>Third paragraph</p>"},
|
||||
{media.Builtin.AsciiDocType, "<div><p>First paragraph</p></div><div><p>Second paragraph</p></div>", false, 2, "<div><p>First paragraph</p></div>", "<div><p>Second paragraph</p></div>"},
|
||||
{media.Builtin.MarkdownType, "<p>这是中文,全中文</p><p>a这是中文,全中文</p>", true, 5, "<p>这是中文,全中文</p>", "<p>a这是中文,全中文</p>"},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
summary := ExtractSummaryFromHTML(test.mt, test.input, test.numWords, test.isCJK)
|
||||
c.Assert(summary.Summary(), qt.Equals, test.expectSummary, qt.Commentf("Summary %d", i))
|
||||
c.Assert(summary.ContentWithoutSummary(), qt.Equals, test.expectContentWithoutSummary, qt.Commentf("ContentWithoutSummary %d", i))
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractSummaryFromHTMLWithDivider(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
const divider = "FOOO"
|
||||
|
||||
tests := []struct {
|
||||
mt media.Type
|
||||
input string
|
||||
expectSummary string
|
||||
expectContentWithoutSummary string
|
||||
expectContent string
|
||||
}{
|
||||
{media.Builtin.MarkdownType, "<p>First paragraph</p><p>FOOO</p><p>Second paragraph</p>", "<p>First paragraph</p>", "<p>Second paragraph</p>", "<p>First paragraph</p><p>Second paragraph</p>"},
|
||||
{media.Builtin.MarkdownType, "<p>First paragraph</p>\n<p>FOOO</p>\n<p>Second paragraph</p>", "<p>First paragraph</p>", "<p>Second paragraph</p>", "<p>First paragraph</p>\n<p>Second paragraph</p>"},
|
||||
{media.Builtin.MarkdownType, "<p>FOOO</p>\n<p>First paragraph</p>", "", "<p>First paragraph</p>", "<p>First paragraph</p>"},
|
||||
{media.Builtin.MarkdownType, "<p>First paragraph</p><p>Second paragraphFOOO</p><p>Third paragraph</p>", "<p>First paragraph</p><p>Second paragraph</p>", "<p>Third paragraph</p>", "<p>First paragraph</p><p>Second paragraph</p><p>Third paragraph</p>"},
|
||||
{media.Builtin.MarkdownType, "<p>这是中文,全中文FOOO</p><p>a这是中文,全中文</p>", "<p>这是中文,全中文</p>", "<p>a这是中文,全中文</p>", "<p>这是中文,全中文</p><p>a这是中文,全中文</p>"},
|
||||
{media.Builtin.MarkdownType, `<p>a <strong>b</strong>` + "\v" + ` c</p>` + "\n<p>FOOO</p>", "<p>a <strong>b</strong>\v c</p>", "", "<p>a <strong>b</strong>\v c</p>"},
|
||||
|
||||
{media.Builtin.HTMLType, "<p>First paragraph</p>FOOO<p>Second paragraph</p>", "<p>First paragraph</p>", "<p>Second paragraph</p>", "<p>First paragraph</p><p>Second paragraph</p>"},
|
||||
|
||||
{media.Builtin.ReStructuredTextType, "<div class=\"document\">\n\n\n<p>This is summary.</p>\n<p>FOOO</p>\n<p>This is content.</p>\n</div>", "<div class=\"document\">\n\n\n<p>This is summary.</p>\n</div>", "<div class=\"document\"><p>This is content.</p>\n</div>", "<div class=\"document\">\n\n\n<p>This is summary.</p>\n<p>This is content.</p>\n</div>"},
|
||||
{media.Builtin.ReStructuredTextType, "<div class=\"document\"><p>First paragraphFOOO</p><p>Second paragraph</p></div>", "<div class=\"document\"><p>First paragraph</p></div>", "<div class=\"document\"><p>Second paragraph</p></div>", `<div class="document"><p>First paragraph</p><p>Second paragraph</p></div>`},
|
||||
|
||||
{media.Builtin.AsciiDocType, "<div class=\"paragraph\"><p>Summary Next Line</p></div><div class=\"paragraph\"><p>FOOO</p></div><div class=\"paragraph\"><p>Some more text</p></div>", "<div class=\"paragraph\"><p>Summary Next Line</p></div>", "<div class=\"paragraph\"><p>Some more text</p></div>", "<div class=\"paragraph\"><p>Summary Next Line</p></div><div class=\"paragraph\"><p>Some more text</p></div>"},
|
||||
{media.Builtin.AsciiDocType, "<div class=\"paragraph\">\n<p>Summary Next Line</p>\n</div>\n<div class=\"paragraph\">\n<p>FOOO</p>\n</div>\n<div class=\"paragraph\">\n<p>Some more text</p>\n</div>\n", "<div class=\"paragraph\">\n<p>Summary Next Line</p>\n</div>", "<div class=\"paragraph\">\n<p>Some more text</p>\n</div>", "<div class=\"paragraph\">\n<p>Summary Next Line</p>\n</div>\n<div class=\"paragraph\">\n<p>Some more text</p>\n</div>"},
|
||||
{media.Builtin.AsciiDocType, "<div><p>FOOO</p></div><div><p>First paragraph</p></div>", "", "<div><p>First paragraph</p></div>", "<div><p>First paragraph</p></div>"},
|
||||
{media.Builtin.AsciiDocType, "<div><p>First paragraphFOOO</p></div><div><p>Second paragraph</p></div>", "<div><p>First paragraph</p></div>", "<div><p>Second paragraph</p></div>", "<div><p>First paragraph</p></div><div><p>Second paragraph</p></div>"},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
summary := ExtractSummaryFromHTMLWithDivider(test.mt, test.input, divider)
|
||||
c.Assert(summary.Summary(), qt.Equals, test.expectSummary, qt.Commentf("Summary %d", i))
|
||||
c.Assert(summary.ContentWithoutSummary(), qt.Equals, test.expectContentWithoutSummary, qt.Commentf("ContentWithoutSummary %d", i))
|
||||
c.Assert(summary.Content(), qt.Equals, test.expectContent, qt.Commentf("Content %d", i))
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandDivider(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
for i, test := range []struct {
|
||||
input string
|
||||
divider string
|
||||
ptag tagReStartEnd
|
||||
expect string
|
||||
expectEndMarkup string
|
||||
}{
|
||||
{"<p>First paragraph</p>\n<p>FOOO</p>\n<p>Second paragraph</p>", "FOOO", startEndP, "<p>FOOO</p>\n", ""},
|
||||
{"<div class=\"paragraph\">\n<p>FOOO</p>\n</div>", "FOOO", startEndDiv, "<div class=\"paragraph\">\n<p>FOOO</p>\n</div>", ""},
|
||||
{"<div><p>FOOO</p></div><div><p>Second paragraph</p></div>", "FOOO", startEndDiv, "<div><p>FOOO</p></div>", ""},
|
||||
{"<div><p>First paragraphFOOO</p></div><div><p>Second paragraph</p></div>", "FOOO", startEndDiv, "FOOO", "</p></div>"},
|
||||
{" <p> abc FOOO </p> ", "FOOO", startEndP, "FOOO", " </p>"},
|
||||
{" <p> FOOO </p> ", "FOOO", startEndP, "<p> FOOO </p>", ""},
|
||||
{" <p>\n \nFOOO </p> ", "FOOO", startEndP, "<p>\n \nFOOO </p>", ""},
|
||||
{" <div> FOOO </div> ", "FOOO", startEndDiv, "<div> FOOO </div>", ""},
|
||||
} {
|
||||
|
||||
l := types.LowHigh[string]{Low: strings.Index(test.input, test.divider), High: strings.Index(test.input, test.divider) + len(test.divider)}
|
||||
e, t := expandSummaryDivider(test.input, test.ptag, l)
|
||||
c.Assert(test.input[e.Low:e.High], qt.Equals, test.expect, qt.Commentf("[%d] Test.expect %q", i, test.input))
|
||||
c.Assert(test.input[t.Low:t.High], qt.Equals, test.expectEndMarkup, qt.Commentf("[%d] Test.expectEndMarkup %q", i, test.input))
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSummaryFromHTML(b *testing.B) {
|
||||
b.StopTimer()
|
||||
input := "<p>First paragraph</p><p>Second paragraph</p>"
|
||||
b.StartTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
summary := ExtractSummaryFromHTML(media.Builtin.MarkdownType, input, 2, false)
|
||||
if s := summary.Content(); s != input {
|
||||
b.Fatalf("unexpected content: %q", s)
|
||||
}
|
||||
if s := summary.ContentWithoutSummary(); s != "<p>Second paragraph</p>" {
|
||||
b.Fatalf("unexpected content without summary: %q", s)
|
||||
}
|
||||
if s := summary.Summary(); s != "<p>First paragraph</p>" {
|
||||
b.Fatalf("unexpected summary: %q", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSummaryFromHTMLWithDivider(b *testing.B) {
|
||||
b.StopTimer()
|
||||
input := "<p>First paragraph</p><p>FOOO</p><p>Second paragraph</p>"
|
||||
b.StartTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
summary := ExtractSummaryFromHTMLWithDivider(media.Builtin.MarkdownType, input, "FOOO")
|
||||
if s := summary.Content(); s != "<p>First paragraph</p><p>Second paragraph</p>" {
|
||||
b.Fatalf("unexpected content: %q", s)
|
||||
}
|
||||
if s := summary.ContentWithoutSummary(); s != "<p>Second paragraph</p>" {
|
||||
b.Fatalf("unexpected content without summary: %q", s)
|
||||
}
|
||||
if s := summary.Summary(); s != "<p>First paragraph</p>" {
|
||||
b.Fatalf("unexpected summary: %q", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,8 @@ import (
|
||||
var (
|
||||
NopPage Page = new(nopPage)
|
||||
NopContentRenderer ContentRenderer = new(nopContentRenderer)
|
||||
NopMarkup Markup = new(nopMarkup)
|
||||
NopContent Content = new(nopContent)
|
||||
NopCPageContentRenderer = struct {
|
||||
OutputFormatPageContentProvider
|
||||
ContentRenderer
|
||||
@@ -109,10 +111,18 @@ func (p *nopPage) BundleType() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *nopPage) Markup(...any) Markup {
|
||||
return NopMarkup
|
||||
}
|
||||
|
||||
func (p *nopPage) Content(context.Context) (any, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (p *nopPage) ContentWithoutSummary(ctx context.Context) (template.HTML, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (p *nopPage) ContentBaseName() string {
|
||||
return ""
|
||||
}
|
||||
@@ -547,3 +557,69 @@ func (r *nopContentRenderer) ParseContent(ctx context.Context, content []byte) (
|
||||
func (r *nopContentRenderer) RenderContent(ctx context.Context, content []byte, doc any) (converter.ResultRender, bool, error) {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
type (
|
||||
nopMarkup int
|
||||
nopContent int
|
||||
)
|
||||
|
||||
var (
|
||||
_ Markup = (*nopMarkup)(nil)
|
||||
_ Content = (*nopContent)(nil)
|
||||
)
|
||||
|
||||
func (c *nopMarkup) Render(context.Context) (Content, error) {
|
||||
return NopContent, nil
|
||||
}
|
||||
|
||||
func (c *nopMarkup) RenderString(ctx context.Context, args ...any) (template.HTML, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (c *nopMarkup) RenderShortcodes(context.Context) (template.HTML, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (c *nopContent) Plain(context.Context) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *nopContent) PlainWords(context.Context) []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *nopContent) WordCount(context.Context) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *nopContent) FuzzyWordCount(context.Context) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *nopContent) ReadingTime(context.Context) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *nopContent) Len(context.Context) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *nopContent) Content(context.Context) (template.HTML, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (c *nopContent) ContentWithoutSummary(context.Context) (template.HTML, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (c *nopMarkup) Fragments(context.Context) *tableofcontents.Fragments {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *nopMarkup) FragmentsHTML(context.Context) template.HTML {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *nopContent) Summary(context.Context) (Summary, error) {
|
||||
return Summary{}, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// 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 page_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/hugolib"
|
||||
)
|
||||
|
||||
func TestNextPrevConfig(t *testing.T) {
|
||||
filesTemplate := `
|
||||
-- hugo.toml --
|
||||
-- content/mysection/_index.md --
|
||||
-- content/mysection/p1.md --
|
||||
---
|
||||
title: "Page 1"
|
||||
weight: 10
|
||||
---
|
||||
-- content/mysection/p2.md --
|
||||
---
|
||||
title: "Page 2"
|
||||
weight: 20
|
||||
---
|
||||
-- content/mysection/p3.md --
|
||||
---
|
||||
title: "Page 3"
|
||||
weight: 30
|
||||
---
|
||||
-- layouts/_default/single.html --
|
||||
{{ .Title }}|Next: {{ with .Next}}{{ .Title}}{{ end }}|Prev: {{ with .Prev}}{{ .Title}}{{ end }}|NextInSection: {{ with .NextInSection}}{{ .Title}}{{ end }}|PrevInSection: {{ with .PrevInSection}}{{ .Title}}{{ end }}|
|
||||
|
||||
`
|
||||
b := hugolib.Test(t, filesTemplate)
|
||||
|
||||
b.AssertFileContent("public/mysection/p1/index.html", "Page 1|Next: |Prev: Page 2|NextInSection: |PrevInSection: Page 2|")
|
||||
b.AssertFileContent("public/mysection/p2/index.html", "Page 2|Next: Page 1|Prev: Page 3|NextInSection: Page 1|PrevInSection: Page 3|")
|
||||
b.AssertFileContent("public/mysection/p3/index.html", "Page 3|Next: Page 2|Prev: |NextInSection: Page 2|PrevInSection: |")
|
||||
|
||||
files := strings.ReplaceAll(filesTemplate, "-- hugo.toml --", `-- hugo.toml --
|
||||
[page]
|
||||
nextPrevSortOrder="aSc"
|
||||
nextPrevInSectionSortOrder="asC"
|
||||
`)
|
||||
|
||||
b = hugolib.Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/mysection/p1/index.html", "Page 1|Next: Page 2|Prev: |NextInSection: Page 2|PrevInSection: |")
|
||||
b.AssertFileContent("public/mysection/p2/index.html", "Page 2|Next: Page 3|Prev: Page 1|NextInSection: Page 3|PrevInSection: Page 1|")
|
||||
b.AssertFileContent("public/mysection/p3/index.html", "Page 3|Next: |Prev: Page 2|NextInSection: |PrevInSection: Page 2|")
|
||||
|
||||
files = strings.ReplaceAll(filesTemplate, "-- hugo.toml --", `-- hugo.toml --
|
||||
[page]
|
||||
nextPrevSortOrder="aSc"
|
||||
`)
|
||||
|
||||
b = hugolib.Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/mysection/p1/index.html", "Page 1|Next: Page 2|Prev: |NextInSection: |PrevInSection: Page 2|")
|
||||
b.AssertFileContent("public/mysection/p2/index.html", "Page 2|Next: Page 3|Prev: Page 1|NextInSection: Page 1|PrevInSection: Page 3|")
|
||||
b.AssertFileContent("public/mysection/p3/index.html", "Page 3|Next: |Prev: Page 2|NextInSection: Page 2|PrevInSection: |")
|
||||
|
||||
files = strings.ReplaceAll(filesTemplate, "-- hugo.toml --", `-- hugo.toml --
|
||||
[page]
|
||||
nextPrevInSectionSortOrder="aSc"
|
||||
`)
|
||||
|
||||
b = hugolib.Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/mysection/p1/index.html", "Page 1|Next: |Prev: Page 2|NextInSection: Page 2|PrevInSection: |")
|
||||
}
|
||||
@@ -149,6 +149,10 @@ func (p *testPage) Content(context.Context) (any, error) {
|
||||
panic("testpage: not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) Markup(...any) Markup {
|
||||
panic("testpage: not implemented")
|
||||
}
|
||||
|
||||
func (p *testPage) ContentBaseName() string {
|
||||
panic("testpage: not implemented")
|
||||
}
|
||||
@@ -177,6 +181,10 @@ func (p *testPage) Description() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (p *testPage) ContentWithoutSummary(ctx context.Context) (template.HTML, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (p *testPage) Dir() string {
|
||||
panic("testpage: not implemented")
|
||||
}
|
||||
|
||||
@@ -298,3 +298,11 @@ func (r resourceTypesHolder) ResourceType() string {
|
||||
func NewResourceTypesProvider(mediaType media.Type, resourceType string) ResourceTypesProvider {
|
||||
return resourceTypesHolder{mediaType: mediaType, resourceType: resourceType}
|
||||
}
|
||||
|
||||
// NameNormalizedOrName returns the normalized name if available, otherwise the name.
|
||||
func NameNormalizedOrName(r Resource) string {
|
||||
if nn, ok := r.(NameNormalizedProvider); ok {
|
||||
return nn.NameNormalized()
|
||||
}
|
||||
return r.Name()
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
// The current is built with 8e1fdea8316d840fd07e9d6e026048e53290948b go1.22.5
|
||||
// The current is built with 6885bad7dd86880be6929c02085e5c7a67ff2887 go1.23.0
|
||||
// TODO(bep) preserve the staticcheck.conf file.
|
||||
fmt.Println("Forking ...")
|
||||
defer fmt.Println("Done ...")
|
||||
|
||||
@@ -18,7 +18,8 @@ hugo mod clean
|
||||
! stderr .
|
||||
stdout 'hugo: removed 1 dirs in module cache for \"github.com/bep/empty-hugo-module\"'
|
||||
hugo mod clean --all
|
||||
stdout 'Deleted 2\d{2} files from module cache\.'
|
||||
# Currently this is 299 on MacOS and 301 on Linux.
|
||||
stdout 'Deleted (2|3)\d{2} files from module cache\.'
|
||||
cd submod
|
||||
hugo mod init testsubmod
|
||||
cmpenv go.mod $WORK/golden/go.mod.testsubmod
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
dostounix golden/package.json
|
||||
|
||||
|
||||
hugo mod npm pack
|
||||
cmp package.json golden/package.json
|
||||
|
||||
@@ -41,3 +42,4 @@ path="github.com/gohugoio/hugoTestModule2"
|
||||
}
|
||||
-- go.mod --
|
||||
module github.com/gohugoio/hugoTestModule
|
||||
go 1.20
|
||||
|
||||
@@ -55,3 +55,4 @@ path="github.com/gohugoio/hugoTestModule2"
|
||||
}
|
||||
-- go.mod --
|
||||
module github.com/gohugoio/hugoTestModule
|
||||
go 1.20
|
||||
|
||||
@@ -36,6 +36,7 @@ const KnownEnv = `
|
||||
GOAMD64
|
||||
GOARCH
|
||||
GOARM
|
||||
GOARM64
|
||||
GOBIN
|
||||
GOCACHE
|
||||
GOCACHEPROG
|
||||
@@ -57,6 +58,7 @@ const KnownEnv = `
|
||||
GOPPC64
|
||||
GOPRIVATE
|
||||
GOPROXY
|
||||
GORISCV64
|
||||
GOROOT
|
||||
GOSUMDB
|
||||
GOTMPDIR
|
||||
|
||||
@@ -9,25 +9,23 @@
|
||||
package fmtsort
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"reflect"
|
||||
"sort"
|
||||
"slices"
|
||||
)
|
||||
|
||||
// Note: Throughout this package we avoid calling reflect.Value.Interface as
|
||||
// it is not always legal to do so and it's easier to avoid the issue than to face it.
|
||||
|
||||
// SortedMap represents a map's keys and values. The keys and values are
|
||||
// aligned in index order: Value[i] is the value in the map corresponding to Key[i].
|
||||
type SortedMap struct {
|
||||
Key []reflect.Value
|
||||
Value []reflect.Value
|
||||
}
|
||||
// SortedMap is a slice of KeyValue pairs that simplifies sorting
|
||||
// and iterating over map entries.
|
||||
//
|
||||
// Each KeyValue pair contains a map key and its corresponding value.
|
||||
type SortedMap []KeyValue
|
||||
|
||||
func (o *SortedMap) Len() int { return len(o.Key) }
|
||||
func (o *SortedMap) Less(i, j int) bool { return compare(o.Key[i], o.Key[j]) < 0 }
|
||||
func (o *SortedMap) Swap(i, j int) {
|
||||
o.Key[i], o.Key[j] = o.Key[j], o.Key[i]
|
||||
o.Value[i], o.Value[j] = o.Value[j], o.Value[i]
|
||||
// KeyValue holds a single key and value pair found in a map.
|
||||
type KeyValue struct {
|
||||
Key, Value reflect.Value
|
||||
}
|
||||
|
||||
// Sort accepts a map and returns a SortedMap that has the same keys and
|
||||
@@ -48,7 +46,7 @@ func (o *SortedMap) Swap(i, j int) {
|
||||
// Otherwise identical arrays compare by length.
|
||||
// - interface values compare first by reflect.Type describing the concrete type
|
||||
// and then by concrete value as described in the previous rules.
|
||||
func Sort(mapValue reflect.Value) *SortedMap {
|
||||
func Sort(mapValue reflect.Value) SortedMap {
|
||||
if mapValue.Type().Kind() != reflect.Map {
|
||||
return nil
|
||||
}
|
||||
@@ -56,18 +54,14 @@ func Sort(mapValue reflect.Value) *SortedMap {
|
||||
// of a concurrent map update. The runtime is responsible for
|
||||
// yelling loudly if that happens. See issue 33275.
|
||||
n := mapValue.Len()
|
||||
key := make([]reflect.Value, 0, n)
|
||||
value := make([]reflect.Value, 0, n)
|
||||
sorted := make(SortedMap, 0, n)
|
||||
iter := mapValue.MapRange()
|
||||
for iter.Next() {
|
||||
key = append(key, iter.Key())
|
||||
value = append(value, iter.Value())
|
||||
sorted = append(sorted, KeyValue{iter.Key(), iter.Value()})
|
||||
}
|
||||
sorted := &SortedMap{
|
||||
Key: key,
|
||||
Value: value,
|
||||
}
|
||||
sort.Stable(sorted)
|
||||
slices.SortStableFunc(sorted, func(a, b KeyValue) int {
|
||||
return compare(a.Key, b.Key)
|
||||
})
|
||||
return sorted
|
||||
}
|
||||
|
||||
@@ -82,43 +76,19 @@ func compare(aVal, bVal reflect.Value) int {
|
||||
}
|
||||
switch aVal.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
a, b := aVal.Int(), bVal.Int()
|
||||
switch {
|
||||
case a < b:
|
||||
return -1
|
||||
case a > b:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
return cmp.Compare(aVal.Int(), bVal.Int())
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
a, b := aVal.Uint(), bVal.Uint()
|
||||
switch {
|
||||
case a < b:
|
||||
return -1
|
||||
case a > b:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
return cmp.Compare(aVal.Uint(), bVal.Uint())
|
||||
case reflect.String:
|
||||
a, b := aVal.String(), bVal.String()
|
||||
switch {
|
||||
case a < b:
|
||||
return -1
|
||||
case a > b:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
return cmp.Compare(aVal.String(), bVal.String())
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return floatCompare(aVal.Float(), bVal.Float())
|
||||
return cmp.Compare(aVal.Float(), bVal.Float())
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
a, b := aVal.Complex(), bVal.Complex()
|
||||
if c := floatCompare(real(a), real(b)); c != 0 {
|
||||
if c := cmp.Compare(real(a), real(b)); c != 0 {
|
||||
return c
|
||||
}
|
||||
return floatCompare(imag(a), imag(b))
|
||||
return cmp.Compare(imag(a), imag(b))
|
||||
case reflect.Bool:
|
||||
a, b := aVal.Bool(), bVal.Bool()
|
||||
switch {
|
||||
@@ -130,28 +100,12 @@ func compare(aVal, bVal reflect.Value) int {
|
||||
return -1
|
||||
}
|
||||
case reflect.Pointer, reflect.UnsafePointer:
|
||||
a, b := aVal.Pointer(), bVal.Pointer()
|
||||
switch {
|
||||
case a < b:
|
||||
return -1
|
||||
case a > b:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
return cmp.Compare(aVal.Pointer(), bVal.Pointer())
|
||||
case reflect.Chan:
|
||||
if c, ok := nilCompare(aVal, bVal); ok {
|
||||
return c
|
||||
}
|
||||
ap, bp := aVal.Pointer(), bVal.Pointer()
|
||||
switch {
|
||||
case ap < bp:
|
||||
return -1
|
||||
case ap > bp:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
return cmp.Compare(aVal.Pointer(), bVal.Pointer())
|
||||
case reflect.Struct:
|
||||
for i := 0; i < aVal.NumField(); i++ {
|
||||
if c := compare(aVal.Field(i), bVal.Field(i)); c != 0 {
|
||||
@@ -198,22 +152,3 @@ func nilCompare(aVal, bVal reflect.Value) (int, bool) {
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// floatCompare compares two floating-point values. NaNs compare low.
|
||||
func floatCompare(a, b float64) int {
|
||||
switch {
|
||||
case isNaN(a):
|
||||
return -1 // No good answer if b is a NaN so don't bother checking.
|
||||
case isNaN(b):
|
||||
return 1
|
||||
case a < b:
|
||||
return -1
|
||||
case a > b:
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func isNaN(a float64) bool {
|
||||
return a != a
|
||||
}
|
||||
|
||||
@@ -5,12 +5,13 @@
|
||||
package fmtsort_test
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"
|
||||
"math"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sort"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"unsafe"
|
||||
@@ -67,10 +68,6 @@ func TestCompare(t *testing.T) {
|
||||
switch {
|
||||
case i == j:
|
||||
expect = 0
|
||||
// NaNs are tricky.
|
||||
if typ := v0.Type(); (typ.Kind() == reflect.Float32 || typ.Kind() == reflect.Float64) && math.IsNaN(v0.Float()) {
|
||||
expect = -1
|
||||
}
|
||||
case i < j:
|
||||
expect = -1
|
||||
case i > j:
|
||||
@@ -142,13 +139,13 @@ func sprint(data any) string {
|
||||
return "nil"
|
||||
}
|
||||
b := new(strings.Builder)
|
||||
for i, key := range om.Key {
|
||||
for i, m := range om {
|
||||
if i > 0 {
|
||||
b.WriteRune(' ')
|
||||
}
|
||||
b.WriteString(sprintKey(key))
|
||||
b.WriteString(sprintKey(m.Key))
|
||||
b.WriteRune(':')
|
||||
fmt.Fprint(b, om.Value[i])
|
||||
fmt.Fprint(b, m.Value)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
@@ -200,8 +197,8 @@ func makeChans() []chan int {
|
||||
for i := range cs {
|
||||
pin.Pin(reflect.ValueOf(cs[i]).UnsafePointer())
|
||||
}
|
||||
sort.Slice(cs, func(i, j int) bool {
|
||||
return uintptr(reflect.ValueOf(cs[i]).UnsafePointer()) < uintptr(reflect.ValueOf(cs[j]).UnsafePointer())
|
||||
slices.SortFunc(cs, func(a, b chan int) int {
|
||||
return cmp.Compare(reflect.ValueOf(a).Pointer(), reflect.ValueOf(b).Pointer())
|
||||
})
|
||||
return cs
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ const (
|
||||
|
||||
// indirect returns the value, after dereferencing as many times
|
||||
// as necessary to reach the base type (or nil).
|
||||
// Signature modified by Hugo. TODO(bep) script this.
|
||||
func doIndirect(a any) any {
|
||||
if a == nil {
|
||||
return nil
|
||||
@@ -46,8 +45,8 @@ func doIndirect(a any) any {
|
||||
}
|
||||
|
||||
var (
|
||||
errorType = reflect.TypeOf((*error)(nil)).Elem()
|
||||
fmtStringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
|
||||
errorType = reflect.TypeFor[error]()
|
||||
fmtStringerType = reflect.TypeFor[fmt.Stringer]()
|
||||
)
|
||||
|
||||
// indirectToStringerOrError returns the value, after dereferencing as many times
|
||||
|
||||
@@ -232,11 +232,9 @@ Least Surprise Property:
|
||||
knows that contextual autoescaping happens should be able to look at a {{.}}
|
||||
and correctly infer what sanitization happens."
|
||||
|
||||
As a consequence of the Least Surprise Property, template actions within an
|
||||
ECMAScript 6 template literal are disabled by default.
|
||||
Handling string interpolation within these literals is rather complex resulting
|
||||
in no clear safe way to support it.
|
||||
To re-enable template actions within ECMAScript 6 template literals, use the
|
||||
GODEBUG=jstmpllitinterp=1 environment variable.
|
||||
Previously, ECMAScript 6 template literal were disabled by default, and could be
|
||||
enabled with the GODEBUG=jstmpllitinterp=1 environment variable. Template
|
||||
literals are now supported by default, and setting jstmpllitinterp has no
|
||||
effect.
|
||||
*/
|
||||
package template
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.13
|
||||
// +build go1.13
|
||||
|
||||
package template_test
|
||||
|
||||
import (
|
||||
|
||||
@@ -273,8 +273,8 @@ type execTest struct {
|
||||
// of the max int boundary.
|
||||
// We do it this way so the test doesn't depend on ints being 32 bits.
|
||||
var (
|
||||
bigInt = fmt.Sprintf("0x%x", int(1<<uint(reflect.TypeOf(0).Bits()-1)-1))
|
||||
bigUint = fmt.Sprintf("0x%x", uint(1<<uint(reflect.TypeOf(0).Bits()-1)))
|
||||
bigInt = fmt.Sprintf("0x%x", int(1<<uint(reflect.TypeFor[int]().Bits()-1)-1))
|
||||
bigUint = fmt.Sprintf("0x%x", uint(1<<uint(reflect.TypeFor[int]().Bits()-1)))
|
||||
)
|
||||
|
||||
var execTests = []execTest{
|
||||
@@ -580,6 +580,8 @@ var execTests = []execTest{
|
||||
{"with $x struct.U.V", "{{with $x := $}}{{$x.U.V}}{{end}}", "v", tVal, true},
|
||||
{"with variable and action", "{{with $x := $}}{{$y := $.U.V}}{{$y}}{{end}}", "v", tVal, true},
|
||||
{"with on typed nil interface value", "{{with .NonEmptyInterfaceTypedNil}}TRUE{{ end }}", "", tVal, true},
|
||||
{"with else with", "{{with 0}}{{.}}{{else with true}}{{.}}{{end}}", "true", tVal, true},
|
||||
{"with else with chain", "{{with 0}}{{.}}{{else with false}}{{.}}{{else with `notempty`}}{{.}}{{end}}", "notempty", tVal, true},
|
||||
|
||||
// Range.
|
||||
{"range []int", "{{range .SI}}-{{.}}-{{end}}", "-3--4--5-", tVal, true},
|
||||
|
||||
@@ -125,7 +125,7 @@ var regexpPrecederKeywords = map[string]bool{
|
||||
"void": true,
|
||||
}
|
||||
|
||||
var jsonMarshalType = reflect.TypeOf((*json.Marshaler)(nil)).Elem()
|
||||
var jsonMarshalType = reflect.TypeFor[json.Marshaler]()
|
||||
|
||||
// indirectToJSONMarshaler returns the value, after dereferencing as many times
|
||||
// as necessary to reach the base type (or nil) or an implementation of json.Marshal.
|
||||
@@ -172,7 +172,7 @@ func jsValEscaper(args ...any) string {
|
||||
// cyclic data. This may be an unacceptable DoS risk.
|
||||
b, err := json.Marshal(a)
|
||||
if err != nil {
|
||||
// While the standard JSON marshaller does not include user controlled
|
||||
// While the standard JSON marshaler does not include user controlled
|
||||
// information in the error message, if a type has a MarshalJSON method,
|
||||
// the content of the error message is not guaranteed. Since we insert
|
||||
// the error into the template, as part of a comment, we attempt to
|
||||
@@ -393,7 +393,6 @@ var jsStrNormReplacementTable = []string{
|
||||
'<': `\u003c`,
|
||||
'>': `\u003e`,
|
||||
}
|
||||
|
||||
var jsRegexpReplacementTable = []string{
|
||||
0: `\u0000`,
|
||||
'\t': `\t`,
|
||||
|
||||
@@ -179,7 +179,7 @@ func (t *Template) DefinedTemplates() string {
|
||||
// definition of t itself.
|
||||
//
|
||||
// Templates can be redefined in successive calls to Parse,
|
||||
// before the first use of Execute on t or any associated template.
|
||||
// before the first use of [Template.Execute] on t or any associated template.
|
||||
// A template definition with a body containing only white space and comments
|
||||
// is considered empty and will not replace an existing template's body.
|
||||
// This allows using Parse to add new named template definitions without
|
||||
@@ -238,8 +238,8 @@ func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error
|
||||
|
||||
// Clone returns a duplicate of the template, including all associated
|
||||
// templates. The actual representation is not copied, but the name space of
|
||||
// associated templates is, so further calls to Parse in the copy will add
|
||||
// templates to the copy but not to the original. Clone can be used to prepare
|
||||
// associated templates is, so further calls to [Template.Parse] in the copy will add
|
||||
// templates to the copy but not to the original. [Template.Clone] can be used to prepare
|
||||
// common templates and use them with variant definitions for other templates
|
||||
// by adding the variants after the clone is made.
|
||||
//
|
||||
@@ -342,7 +342,7 @@ func (t *Template) Funcs(funcMap FuncMap) *Template {
|
||||
}
|
||||
|
||||
// Delims sets the action delimiters to the specified strings, to be used in
|
||||
// subsequent calls to Parse, ParseFiles, or ParseGlob. Nested template
|
||||
// subsequent calls to [Template.Parse], [ParseFiles], or [ParseGlob]. Nested template
|
||||
// definitions will inherit the settings. An empty delimiter stands for the
|
||||
// corresponding default: {{ or }}.
|
||||
// The return value is the template, so calls can be chained.
|
||||
@@ -359,7 +359,7 @@ func (t *Template) Lookup(name string) *Template {
|
||||
return t.set[name]
|
||||
}
|
||||
|
||||
// Must is a helper that wraps a call to a function returning (*Template, error)
|
||||
// Must is a helper that wraps a call to a function returning ([*Template], error)
|
||||
// and panics if the error is non-nil. It is intended for use in variable initializations
|
||||
// such as
|
||||
//
|
||||
@@ -371,10 +371,10 @@ func Must(t *Template, err error) *Template {
|
||||
return t
|
||||
}
|
||||
|
||||
// ParseFiles creates a new Template and parses the template definitions from
|
||||
// ParseFiles creates a new [Template] and parses the template definitions from
|
||||
// the named files. The returned template's name will have the (base) name and
|
||||
// (parsed) contents of the first file. There must be at least one file.
|
||||
// If an error occurs, parsing stops and the returned *Template is nil.
|
||||
// If an error occurs, parsing stops and the returned [*Template] is nil.
|
||||
//
|
||||
// When parsing multiple files with the same name in different directories,
|
||||
// the last one mentioned will be the one that results.
|
||||
@@ -436,12 +436,12 @@ func parseFiles(t *Template, readFile func(string) (string, []byte, error), file
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// ParseGlob creates a new Template and parses the template definitions from
|
||||
// ParseGlob creates a new [Template] and parses the template definitions from
|
||||
// the files identified by the pattern. The files are matched according to the
|
||||
// semantics of filepath.Match, and the pattern must match at least one file.
|
||||
// The returned template will have the (base) name and (parsed) contents of the
|
||||
// first file matched by the pattern. ParseGlob is equivalent to calling
|
||||
// ParseFiles with the list of files matched by the pattern.
|
||||
// [ParseFiles] with the list of files matched by the pattern.
|
||||
//
|
||||
// When parsing multiple files with the same name in different directories,
|
||||
// the last one mentioned will be the one that results.
|
||||
@@ -485,7 +485,7 @@ func IsTrue(val any) (truth, ok bool) {
|
||||
return template.IsTrue(val)
|
||||
}
|
||||
|
||||
// ParseFS is like ParseFiles or ParseGlob but reads from the file system fs
|
||||
// ParseFS is like [ParseFiles] or [ParseGlob] but reads from the file system fs
|
||||
// instead of the host operating system's file system.
|
||||
// It accepts a list of glob patterns.
|
||||
// (Note that most file names serve as glob patterns matching only themselves.)
|
||||
@@ -493,7 +493,7 @@ func ParseFS(fs fs.FS, patterns ...string) (*Template, error) {
|
||||
return parseFS(nil, fs, patterns)
|
||||
}
|
||||
|
||||
// ParseFS is like ParseFiles or ParseGlob but reads from the file system fs
|
||||
// ParseFS is like [Template.ParseFiles] or [Template.ParseGlob] but reads from the file system fs
|
||||
// instead of the host operating system's file system.
|
||||
// It accepts a list of glob patterns.
|
||||
// (Note that most file names serve as glob patterns matching only themselves.)
|
||||
|
||||
@@ -414,7 +414,7 @@ func tJSDelimited(c context, s []byte) (context, int) {
|
||||
// If "</script" appears in a regex literal, the '/' should not
|
||||
// close the regex literal, and it will later be escaped to
|
||||
// "\x3C/script" in escapeText.
|
||||
if i > 0 && i+7 <= len(s) && bytes.Compare(bytes.ToLower(s[i-1:i+7]), []byte("</script")) == 0 {
|
||||
if i > 0 && i+7 <= len(s) && bytes.Equal(bytes.ToLower(s[i-1:i+7]), []byte("</script")) {
|
||||
i++
|
||||
} else if !inCharset {
|
||||
c.state, c.jsCtx = stateJS, jsCtxDivOp
|
||||
|
||||
@@ -132,15 +132,13 @@ func findGOROOT() (string, error) {
|
||||
// If runtime.GOROOT() is non-empty, assume that it is valid.
|
||||
//
|
||||
// (It might not be: for example, the user may have explicitly set GOROOT
|
||||
// to the wrong directory, or explicitly set GOROOT_FINAL but not GOROOT
|
||||
// and hasn't moved the tree to GOROOT_FINAL yet. But those cases are
|
||||
// to the wrong directory. But this case is
|
||||
// rare, and if that happens the user can fix what they broke.)
|
||||
return
|
||||
}
|
||||
|
||||
// runtime.GOROOT doesn't know where GOROOT is (perhaps because the test
|
||||
// binary was built with -trimpath, or perhaps because GOROOT_FINAL was set
|
||||
// without GOROOT and the tree hasn't been moved there yet).
|
||||
// binary was built with -trimpath).
|
||||
//
|
||||
// Since this is internal/testenv, we can cheat and assume that the caller
|
||||
// is a test of some package in a subdirectory of GOROOT/src. ('go test'
|
||||
@@ -315,12 +313,18 @@ func MustInternalLink(t testing.TB, withCgo bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// MustInternalLinkPIE checks whether the current system can link PIE binary using
|
||||
// internal linking.
|
||||
// If not, MustInternalLinkPIE calls t.Skip with an explanation.
|
||||
// Modified by Hugo (not needed)
|
||||
func MustInternalLinkPIE(t testing.TB) {
|
||||
}
|
||||
|
||||
// MustHaveBuildMode reports whether the current system can build programs in
|
||||
// the given build mode.
|
||||
// If not, MustHaveBuildMode calls t.Skip with an explanation.
|
||||
// Modified by Hugo (not needed)
|
||||
func MustHaveBuildMode(t testing.TB, buildmode string) {
|
||||
return
|
||||
}
|
||||
|
||||
// HasSymlink reports whether the current system can use os.Symlink.
|
||||
@@ -447,3 +451,10 @@ func WriteImportcfg(t testing.TB, dstPath string, packageFiles map[string]string
|
||||
func SyscallIsNotSupported(err error) bool {
|
||||
return syscallIsNotSupported(err)
|
||||
}
|
||||
|
||||
// ParallelOn64Bit calls t.Parallel() unless there is a case that cannot be parallel.
|
||||
// This function should be used when it is necessary to avoid t.Parallel on
|
||||
// 32-bit machines, typically because the test uses lots of memory.
|
||||
// Disabled by Hugo.
|
||||
func ParallelOn64Bit(t *testing.T) {
|
||||
}
|
||||
|
||||
@@ -144,6 +144,13 @@ data, defined in detail in the corresponding sections that follow.
|
||||
is executed; otherwise, dot is set to the value of the pipeline
|
||||
and T1 is executed.
|
||||
|
||||
{{with pipeline}} T1 {{else with pipeline}} T0 {{end}}
|
||||
To simplify the appearance of with-else chains, the else action
|
||||
of a with may include another with directly; the effect is exactly
|
||||
the same as writing
|
||||
{{with pipeline}} T1 {{else}}{{with pipeline}} T0 {{end}}{{end}}
|
||||
|
||||
|
||||
Arguments
|
||||
|
||||
An argument is a simple value, denoted by one of the following.
|
||||
|
||||
@@ -35,7 +35,7 @@ Josie
|
||||
Name, Gift string
|
||||
Attended bool
|
||||
}
|
||||
var recipients = []Recipient{
|
||||
recipients := []Recipient{
|
||||
{"Aunt Mildred", "bone china tea set", true},
|
||||
{"Uncle John", "moleskin pants", false},
|
||||
{"Cousin Rodney", "", false},
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.13
|
||||
// +build go1.13
|
||||
|
||||
package template_test
|
||||
|
||||
import (
|
||||
|
||||
@@ -7,13 +7,12 @@ package template
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"
|
||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
|
||||
"io"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/fmtsort"
|
||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate/parse"
|
||||
)
|
||||
|
||||
// maxExecDepth specifies the maximum stack depth of templates within
|
||||
@@ -95,7 +94,7 @@ type missingValType struct{}
|
||||
|
||||
var missingVal = reflect.ValueOf(missingValType{})
|
||||
|
||||
var missingValReflectType = reflect.TypeOf(missingValType{})
|
||||
var missingValReflectType = reflect.TypeFor[missingValType]()
|
||||
|
||||
func isMissing(v reflect.Value) bool {
|
||||
return v.IsValid() && v.Type() == missingValReflectType
|
||||
@@ -202,8 +201,8 @@ func (t *Template) ExecuteTemplate(wr io.Writer, name string, data any) error {
|
||||
// A template may be executed safely in parallel, although if parallel
|
||||
// executions share a Writer the output may be interleaved.
|
||||
//
|
||||
// If data is a reflect.Value, the template applies to the concrete
|
||||
// value that the reflect.Value holds, as in fmt.Print.
|
||||
// If data is a [reflect.Value], the template applies to the concrete
|
||||
// value that the reflect.Value holds, as in [fmt.Print].
|
||||
func (t *Template) Execute(wr io.Writer, data any) error {
|
||||
return t.execute(wr, data)
|
||||
}
|
||||
@@ -229,7 +228,7 @@ func (t *Template) execute(wr io.Writer, data any) (err error) {
|
||||
// DefinedTemplates returns a string listing the defined templates,
|
||||
// prefixed by the string "; defined templates are: ". If there are none,
|
||||
// it returns the empty string. For generating an error message here
|
||||
// and in html/template.
|
||||
// and in [html/template].
|
||||
func (t *Template) DefinedTemplates() string {
|
||||
if t.common == nil {
|
||||
return ""
|
||||
@@ -409,8 +408,8 @@ func (s *state) walkRange(dot reflect.Value, r *parse.RangeNode) {
|
||||
break
|
||||
}
|
||||
om := fmtsort.Sort(val)
|
||||
for i, key := range om.Key {
|
||||
oneIteration(key, om.Value[i])
|
||||
for _, m := range om {
|
||||
oneIteration(m.Key, m.Value)
|
||||
}
|
||||
return
|
||||
case reflect.Chan:
|
||||
@@ -480,7 +479,7 @@ func (s *state) evalPipeline(dot reflect.Value, pipe *parse.PipeNode) (value ref
|
||||
value = s.evalCommand(dot, cmd, value) // previous value is this one's final arg.
|
||||
// If the object has type interface{}, dig down one level to the thing inside.
|
||||
if value.Kind() == reflect.Interface && value.Type().NumMethod() == 0 {
|
||||
value = reflect.ValueOf(value.Interface()) // lovely!
|
||||
value = value.Elem()
|
||||
}
|
||||
}
|
||||
for _, variable := range pipe.Decl {
|
||||
@@ -709,9 +708,9 @@ func (s *state) evalFieldOld(dot reflect.Value, fieldName string, node parse.Nod
|
||||
}
|
||||
|
||||
var (
|
||||
errorType = reflect.TypeOf((*error)(nil)).Elem()
|
||||
fmtStringerType = reflect.TypeOf((*fmt.Stringer)(nil)).Elem()
|
||||
reflectValueType = reflect.TypeOf((*reflect.Value)(nil)).Elem()
|
||||
errorType = reflect.TypeFor[error]()
|
||||
fmtStringerType = reflect.TypeFor[fmt.Stringer]()
|
||||
reflectValueType = reflect.TypeFor[reflect.Value]()
|
||||
)
|
||||
|
||||
// evalCall executes a function or method call. If it's a method, fun already has the receiver bound, so
|
||||
@@ -735,9 +734,8 @@ func (s *state) evalCallOld(dot, fun reflect.Value, isBuiltin bool, node parse.N
|
||||
} else if numIn != typ.NumIn() {
|
||||
s.errorf("wrong number of args for %s: want %d got %d", name, typ.NumIn(), numIn)
|
||||
}
|
||||
if !goodFunc(typ) {
|
||||
// TODO: This could still be a confusing error; maybe goodFunc should provide info.
|
||||
s.errorf("can't call method/function %q with %d results", name, typ.NumOut())
|
||||
if err := goodFunc(name, typ); err != nil {
|
||||
s.errorf("%v", err)
|
||||
}
|
||||
|
||||
unwrap := func(v reflect.Value) reflect.Value {
|
||||
@@ -801,6 +799,15 @@ func (s *state) evalCallOld(dot, fun reflect.Value, isBuiltin bool, node parse.N
|
||||
}
|
||||
argv[i] = s.validateType(final, t)
|
||||
}
|
||||
|
||||
// Special case for the "call" builtin.
|
||||
// Insert the name of the callee function as the first argument.
|
||||
if isBuiltin && name == "call" {
|
||||
calleeName := args[0].String()
|
||||
argv = append([]reflect.Value{reflect.ValueOf(calleeName)}, argv...)
|
||||
fun = reflect.ValueOf(call)
|
||||
}
|
||||
|
||||
v, err := safeCall(fun, argv)
|
||||
// If we have an error that is not nil, stop execution and return that
|
||||
// error to the caller.
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package template
|
||||
|
||||
import (
|
||||
@@ -75,12 +78,15 @@ type T struct {
|
||||
PSI *[]int
|
||||
NIL *int
|
||||
// Function (not method)
|
||||
BinaryFunc func(string, string) string
|
||||
VariadicFunc func(...string) string
|
||||
VariadicFuncInt func(int, ...string) string
|
||||
NilOKFunc func(*int) bool
|
||||
ErrFunc func() (string, error)
|
||||
PanicFunc func() string
|
||||
BinaryFunc func(string, string) string
|
||||
VariadicFunc func(...string) string
|
||||
VariadicFuncInt func(int, ...string) string
|
||||
NilOKFunc func(*int) bool
|
||||
ErrFunc func() (string, error)
|
||||
PanicFunc func() string
|
||||
TooFewReturnCountFunc func()
|
||||
TooManyReturnCountFunc func() (string, error, int)
|
||||
InvalidReturnTypeFunc func() (string, bool)
|
||||
// Template to test evaluation of templates.
|
||||
Tmpl *Template
|
||||
// Unexported field; cannot be accessed by template.
|
||||
@@ -168,6 +174,9 @@ var tVal = &T{
|
||||
NilOKFunc: func(s *int) bool { return s == nil },
|
||||
ErrFunc: func() (string, error) { return "bla", nil },
|
||||
PanicFunc: func() string { panic("test panic") },
|
||||
TooFewReturnCountFunc: func() {},
|
||||
TooManyReturnCountFunc: func() (string, error, int) { return "", nil, 0 },
|
||||
InvalidReturnTypeFunc: func() (string, bool) { return "", false },
|
||||
Tmpl: Must(New("x").Parse("test template")), // "x" is the value of .X
|
||||
}
|
||||
|
||||
@@ -265,8 +274,8 @@ type execTest struct {
|
||||
// of the max int boundary.
|
||||
// We do it this way so the test doesn't depend on ints being 32 bits.
|
||||
var (
|
||||
bigInt = fmt.Sprintf("0x%x", int(1<<uint(reflect.TypeOf(0).Bits()-1)-1))
|
||||
bigUint = fmt.Sprintf("0x%x", uint(1<<uint(reflect.TypeOf(0).Bits()-1)))
|
||||
bigInt = fmt.Sprintf("0x%x", int(1<<uint(reflect.TypeFor[int]().Bits()-1)-1))
|
||||
bigUint = fmt.Sprintf("0x%x", uint(1<<uint(reflect.TypeFor[int]().Bits()-1)))
|
||||
)
|
||||
|
||||
var execTests = []execTest{
|
||||
@@ -583,6 +592,8 @@ var execTests = []execTest{
|
||||
{"with $x struct.U.V", "{{with $x := $}}{{$x.U.V}}{{end}}", "v", tVal, true},
|
||||
{"with variable and action", "{{with $x := $}}{{$y := $.U.V}}{{$y}}{{end}}", "v", tVal, true},
|
||||
{"with on typed nil interface value", "{{with .NonEmptyInterfaceTypedNil}}TRUE{{ end }}", "", tVal, true},
|
||||
{"with else with", "{{with 0}}{{.}}{{else with true}}{{.}}{{end}}", "true", tVal, true},
|
||||
{"with else with chain", "{{with 0}}{{.}}{{else with false}}{{.}}{{else with `notempty`}}{{.}}{{end}}", "notempty", tVal, true},
|
||||
|
||||
// Range.
|
||||
{"range []int", "{{range .SI}}-{{.}}-{{end}}", "-3--4--5-", tVal, true},
|
||||
@@ -1723,6 +1734,81 @@ func TestExecutePanicDuringCall(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFunctionCheckDuringCall(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
data any
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "call nothing",
|
||||
input: `{{call}}`,
|
||||
data: tVal,
|
||||
wantErr: "wrong number of args for call: want at least 1 got 0",
|
||||
},
|
||||
{
|
||||
name: "call non-function",
|
||||
input: "{{call .True}}",
|
||||
data: tVal,
|
||||
wantErr: "error calling call: non-function .True of type bool",
|
||||
},
|
||||
{
|
||||
name: "call func with wrong argument",
|
||||
input: "{{call .BinaryFunc 1}}",
|
||||
data: tVal,
|
||||
wantErr: "error calling call: wrong number of args for .BinaryFunc: got 1 want 2",
|
||||
},
|
||||
{
|
||||
name: "call variadic func with wrong argument",
|
||||
input: `{{call .VariadicFuncInt}}`,
|
||||
data: tVal,
|
||||
wantErr: "error calling call: wrong number of args for .VariadicFuncInt: got 0 want at least 1",
|
||||
},
|
||||
{
|
||||
name: "call too few return number func",
|
||||
input: `{{call .TooFewReturnCountFunc}}`,
|
||||
data: tVal,
|
||||
wantErr: "error calling call: function .TooFewReturnCountFunc has 0 return values; should be 1 or 2",
|
||||
},
|
||||
{
|
||||
name: "call too many return number func",
|
||||
input: `{{call .TooManyReturnCountFunc}}`,
|
||||
data: tVal,
|
||||
wantErr: "error calling call: function .TooManyReturnCountFunc has 3 return values; should be 1 or 2",
|
||||
},
|
||||
{
|
||||
name: "call invalid return type func",
|
||||
input: `{{call .InvalidReturnTypeFunc}}`,
|
||||
data: tVal,
|
||||
wantErr: "error calling call: invalid function signature for .InvalidReturnTypeFunc: second return value should be error; is bool",
|
||||
},
|
||||
{
|
||||
name: "call pipeline",
|
||||
input: `{{call (len "test")}}`,
|
||||
data: nil,
|
||||
wantErr: "error calling call: non-function len \"test\" of type int",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
b := new(bytes.Buffer)
|
||||
tmpl, err := New("t").Parse(tc.input)
|
||||
if err != nil {
|
||||
t.Fatalf("parse error: %s", err)
|
||||
}
|
||||
err = tmpl.Execute(b, tc.data)
|
||||
if err == nil {
|
||||
t.Errorf("%s: expected error; got none", tc.name)
|
||||
} else if tc.wantErr == "" || !strings.Contains(err.Error(), tc.wantErr) {
|
||||
if *debug {
|
||||
fmt.Printf("%s: test execute error: %s\n", tc.name, err)
|
||||
}
|
||||
t.Errorf("%s: expected error:\n%s\ngot:\n%s", tc.name, tc.wantErr, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Issue 31810. Check that a parenthesized first argument behaves properly.
|
||||
func TestIssue31810(t *testing.T) {
|
||||
// A simple value with no arguments is fine.
|
||||
|
||||
@@ -22,14 +22,14 @@ import (
|
||||
// return value evaluates to non-nil during execution, execution terminates and
|
||||
// Execute returns that error.
|
||||
//
|
||||
// Errors returned by Execute wrap the underlying error; call errors.As to
|
||||
// Errors returned by Execute wrap the underlying error; call [errors.As] to
|
||||
// unwrap them.
|
||||
//
|
||||
// When template execution invokes a function with an argument list, that list
|
||||
// must be assignable to the function's parameter types. Functions meant to
|
||||
// apply to arguments of arbitrary type can use parameters of type interface{} or
|
||||
// of type reflect.Value. Similarly, functions meant to return a result of arbitrary
|
||||
// type can return interface{} or reflect.Value.
|
||||
// of type [reflect.Value]. Similarly, functions meant to return a result of arbitrary
|
||||
// type can return interface{} or [reflect.Value].
|
||||
type FuncMap map[string]any
|
||||
|
||||
// builtins returns the FuncMap.
|
||||
@@ -39,7 +39,7 @@ type FuncMap map[string]any
|
||||
func builtins() FuncMap {
|
||||
return FuncMap{
|
||||
"and": and,
|
||||
"call": call,
|
||||
"call": emptyCall,
|
||||
"html": HTMLEscaper,
|
||||
"index": index,
|
||||
"slice": slice,
|
||||
@@ -93,8 +93,8 @@ func addValueFuncs(out map[string]reflect.Value, in FuncMap) {
|
||||
if v.Kind() != reflect.Func {
|
||||
panic("value for " + name + " not a function")
|
||||
}
|
||||
if !goodFunc(v.Type()) {
|
||||
panic(fmt.Errorf("can't install method/function %q with %d results", name, v.Type().NumOut()))
|
||||
if err := goodFunc(name, v.Type()); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
out[name] = v
|
||||
}
|
||||
@@ -109,15 +109,18 @@ func addFuncs(out, in FuncMap) {
|
||||
}
|
||||
|
||||
// goodFunc reports whether the function or method has the right result signature.
|
||||
func goodFunc(typ reflect.Type) bool {
|
||||
func goodFunc(name string, typ reflect.Type) error {
|
||||
// We allow functions with 1 result or 2 results where the second is an error.
|
||||
switch {
|
||||
case typ.NumOut() == 1:
|
||||
return true
|
||||
case typ.NumOut() == 2 && typ.Out(1) == errorType:
|
||||
return true
|
||||
switch numOut := typ.NumOut(); {
|
||||
case numOut == 1:
|
||||
return nil
|
||||
case numOut == 2 && typ.Out(1) == errorType:
|
||||
return nil
|
||||
case numOut == 2:
|
||||
return fmt.Errorf("invalid function signature for %s: second return value should be error; is %s", name, typ.Out(1))
|
||||
default:
|
||||
return fmt.Errorf("function %s has %d return values; should be 1 or 2", name, typ.NumOut())
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// goodName reports whether the function name is a valid identifier.
|
||||
@@ -309,30 +312,35 @@ func length(item reflect.Value) (int, error) {
|
||||
|
||||
// Function invocation
|
||||
|
||||
func emptyCall(fn reflect.Value, args ...reflect.Value) reflect.Value {
|
||||
panic("unreachable") // implemented as a special case in evalCall
|
||||
}
|
||||
|
||||
// call returns the result of evaluating the first argument as a function.
|
||||
// The function must return 1 result, or 2 results, the second of which is an error.
|
||||
func call(fn reflect.Value, args ...reflect.Value) (reflect.Value, error) {
|
||||
func call(name string, fn reflect.Value, args ...reflect.Value) (reflect.Value, error) {
|
||||
fn = indirectInterface(fn)
|
||||
if !fn.IsValid() {
|
||||
return reflect.Value{}, fmt.Errorf("call of nil")
|
||||
}
|
||||
typ := fn.Type()
|
||||
if typ.Kind() != reflect.Func {
|
||||
return reflect.Value{}, fmt.Errorf("non-function of type %s", typ)
|
||||
return reflect.Value{}, fmt.Errorf("non-function %s of type %s", name, typ)
|
||||
}
|
||||
if !goodFunc(typ) {
|
||||
return reflect.Value{}, fmt.Errorf("function called with %d args; should be 1 or 2", typ.NumOut())
|
||||
|
||||
if err := goodFunc(name, typ); err != nil {
|
||||
return reflect.Value{}, err
|
||||
}
|
||||
numIn := typ.NumIn()
|
||||
var dddType reflect.Type
|
||||
if typ.IsVariadic() {
|
||||
if len(args) < numIn-1 {
|
||||
return reflect.Value{}, fmt.Errorf("wrong number of args: got %d want at least %d", len(args), numIn-1)
|
||||
return reflect.Value{}, fmt.Errorf("wrong number of args for %s: got %d want at least %d", name, len(args), numIn-1)
|
||||
}
|
||||
dddType = typ.In(numIn - 1).Elem()
|
||||
} else {
|
||||
if len(args) != numIn {
|
||||
return reflect.Value{}, fmt.Errorf("wrong number of args: got %d want %d", len(args), numIn)
|
||||
return reflect.Value{}, fmt.Errorf("wrong number of args for %s: got %d want %d", name, len(args), numIn)
|
||||
}
|
||||
}
|
||||
argv := make([]reflect.Value, len(args))
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
|
||||
// Functions and methods to parse templates.
|
||||
|
||||
// Must is a helper that wraps a call to a function returning (*Template, error)
|
||||
// Must is a helper that wraps a call to a function returning ([*Template], error)
|
||||
// and panics if the error is non-nil. It is intended for use in variable
|
||||
// initializations such as
|
||||
//
|
||||
@@ -28,7 +28,7 @@ func Must(t *Template, err error) *Template {
|
||||
return t
|
||||
}
|
||||
|
||||
// ParseFiles creates a new Template and parses the template definitions from
|
||||
// ParseFiles creates a new [Template] and parses the template definitions from
|
||||
// the named files. The returned template's name will have the base name and
|
||||
// parsed contents of the first file. There must be at least one file.
|
||||
// If an error occurs, parsing stops and the returned *Template is nil.
|
||||
@@ -45,9 +45,9 @@ func ParseFiles(filenames ...string) (*Template, error) {
|
||||
// t. If an error occurs, parsing stops and the returned template is nil;
|
||||
// otherwise it is t. There must be at least one file.
|
||||
// Since the templates created by ParseFiles are named by the base
|
||||
// names of the argument files, t should usually have the name of one
|
||||
// of the (base) names of the files. If it does not, depending on t's
|
||||
// contents before calling ParseFiles, t.Execute may fail. In that
|
||||
// (see [filepath.Base]) names of the argument files, t should usually have the
|
||||
// name of one of the (base) names of the files. If it does not, depending on
|
||||
// t's contents before calling ParseFiles, t.Execute may fail. In that
|
||||
// case use t.ExecuteTemplate to execute a valid template.
|
||||
//
|
||||
// When parsing multiple files with the same name in different directories,
|
||||
@@ -93,12 +93,12 @@ func parseFiles(t *Template, readFile func(string) (string, []byte, error), file
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// ParseGlob creates a new Template and parses the template definitions from
|
||||
// ParseGlob creates a new [Template] and parses the template definitions from
|
||||
// the files identified by the pattern. The files are matched according to the
|
||||
// semantics of filepath.Match, and the pattern must match at least one file.
|
||||
// The returned template will have the (base) name and (parsed) contents of the
|
||||
// first file matched by the pattern. ParseGlob is equivalent to calling
|
||||
// ParseFiles with the list of files matched by the pattern.
|
||||
// semantics of [filepath.Match], and the pattern must match at least one file.
|
||||
// The returned template will have the [filepath.Base] name and (parsed)
|
||||
// contents of the first file matched by the pattern. ParseGlob is equivalent to
|
||||
// calling [ParseFiles] with the list of files matched by the pattern.
|
||||
//
|
||||
// When parsing multiple files with the same name in different directories,
|
||||
// the last one mentioned will be the one that results.
|
||||
@@ -108,9 +108,9 @@ func ParseGlob(pattern string) (*Template, error) {
|
||||
|
||||
// ParseGlob parses the template definitions in the files identified by the
|
||||
// pattern and associates the resulting templates with t. The files are matched
|
||||
// according to the semantics of filepath.Match, and the pattern must match at
|
||||
// least one file. ParseGlob is equivalent to calling t.ParseFiles with the
|
||||
// list of files matched by the pattern.
|
||||
// according to the semantics of [filepath.Match], and the pattern must match at
|
||||
// least one file. ParseGlob is equivalent to calling [Template.ParseFiles] with
|
||||
// the list of files matched by the pattern.
|
||||
//
|
||||
// When parsing multiple files with the same name in different directories,
|
||||
// the last one mentioned will be the one that results.
|
||||
@@ -131,17 +131,17 @@ func parseGlob(t *Template, pattern string) (*Template, error) {
|
||||
return parseFiles(t, readFileOS, filenames...)
|
||||
}
|
||||
|
||||
// ParseFS is like ParseFiles or ParseGlob but reads from the file system fsys
|
||||
// ParseFS is like [Template.ParseFiles] or [Template.ParseGlob] but reads from the file system fsys
|
||||
// instead of the host operating system's file system.
|
||||
// It accepts a list of glob patterns.
|
||||
// It accepts a list of glob patterns (see [path.Match]).
|
||||
// (Note that most file names serve as glob patterns matching only themselves.)
|
||||
func ParseFS(fsys fs.FS, patterns ...string) (*Template, error) {
|
||||
return parseFS(nil, fsys, patterns)
|
||||
}
|
||||
|
||||
// ParseFS is like ParseFiles or ParseGlob but reads from the file system fsys
|
||||
// ParseFS is like [Template.ParseFiles] or [Template.ParseGlob] but reads from the file system fsys
|
||||
// instead of the host operating system's file system.
|
||||
// It accepts a list of glob patterns.
|
||||
// It accepts a list of glob patterns (see [path.Match]).
|
||||
// (Note that most file names serve as glob patterns matching only themselves.)
|
||||
func (t *Template) ParseFS(fsys fs.FS, patterns ...string) (*Template, error) {
|
||||
t.init()
|
||||
|
||||
@@ -278,9 +278,8 @@ func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node
|
||||
} else if numIn != typ.NumIn() {
|
||||
s.errorf("wrong number of args for %s: want %d got %d", name, typ.NumIn(), numIn)
|
||||
}
|
||||
if !goodFunc(typ) {
|
||||
// TODO: This could still be a confusing error; maybe goodFunc should provide info.
|
||||
s.errorf("can't call method/function %q with %d results", name, typ.NumOut())
|
||||
if err := goodFunc(name, typ); err != nil {
|
||||
s.errorf("%v", err)
|
||||
}
|
||||
|
||||
unwrap := func(v reflect.Value) reflect.Value {
|
||||
@@ -345,6 +344,14 @@ func (s *state) evalCall(dot, fun reflect.Value, isBuiltin bool, node parse.Node
|
||||
argv[i] = s.validateType(final, t)
|
||||
}
|
||||
|
||||
// Special case for the "call" builtin.
|
||||
// Insert the name of the callee function as the first argument.
|
||||
if isBuiltin && name == "call" {
|
||||
calleeName := args[0].String()
|
||||
argv = append([]reflect.Value{reflect.ValueOf(calleeName)}, argv...)
|
||||
fun = reflect.ValueOf(call)
|
||||
}
|
||||
|
||||
// Added for Hugo
|
||||
for i := 0; i < len(first); i++ {
|
||||
argv[i] = s.validateType(first[i], typ.In(i))
|
||||
|
||||
@@ -2,18 +2,16 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.13
|
||||
// +build go1.13
|
||||
|
||||
package template_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/testenv"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/gohugoio/hugo/tpl/internal/go_templates/testenv"
|
||||
)
|
||||
|
||||
// Issue 36021: verify that text/template doesn't prevent the linker from removing
|
||||
@@ -44,7 +42,7 @@ func main() {
|
||||
`
|
||||
td := t.TempDir()
|
||||
|
||||
if err := os.WriteFile(filepath.Join(td, "x.go"), []byte(prog), 0644); err != nil {
|
||||
if err := os.WriteFile(filepath.Join(td, "x.go"), []byte(prog), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", "x.exe", "x.go")
|
||||
|
||||
@@ -217,7 +217,11 @@ func (p *PipeNode) writeTo(sb *strings.Builder) {
|
||||
}
|
||||
v.writeTo(sb)
|
||||
}
|
||||
sb.WriteString(" := ")
|
||||
if p.IsAssign {
|
||||
sb.WriteString(" = ")
|
||||
} else {
|
||||
sb.WriteString(" := ")
|
||||
}
|
||||
}
|
||||
for i, c := range p.Cmds {
|
||||
if i > 0 {
|
||||
@@ -346,12 +350,12 @@ type IdentifierNode struct {
|
||||
Ident string // The identifier's name.
|
||||
}
|
||||
|
||||
// NewIdentifier returns a new IdentifierNode with the given identifier name.
|
||||
// NewIdentifier returns a new [IdentifierNode] with the given identifier name.
|
||||
func NewIdentifier(ident string) *IdentifierNode {
|
||||
return &IdentifierNode{NodeType: NodeIdentifier, Ident: ident}
|
||||
}
|
||||
|
||||
// SetPos sets the position. NewIdentifier is a public method so we can't modify its signature.
|
||||
// SetPos sets the position. [NewIdentifier] is a public method so we can't modify its signature.
|
||||
// Chained for convenience.
|
||||
// TODO: fix one day?
|
||||
func (i *IdentifierNode) SetPos(pos Pos) *IdentifierNode {
|
||||
@@ -359,7 +363,7 @@ func (i *IdentifierNode) SetPos(pos Pos) *IdentifierNode {
|
||||
return i
|
||||
}
|
||||
|
||||
// SetTree sets the parent tree for the node. NewIdentifier is a public method so we can't modify its signature.
|
||||
// SetTree sets the parent tree for the node. [NewIdentifier] is a public method so we can't modify its signature.
|
||||
// Chained for convenience.
|
||||
// TODO: fix one day?
|
||||
func (i *IdentifierNode) SetTree(t *Tree) *IdentifierNode {
|
||||
|
||||
@@ -42,7 +42,7 @@ const (
|
||||
SkipFuncCheck // do not check that functions are defined
|
||||
)
|
||||
|
||||
// Copy returns a copy of the Tree. Any parsing state is discarded.
|
||||
// Copy returns a copy of the [Tree]. Any parsing state is discarded.
|
||||
func (t *Tree) Copy() *Tree {
|
||||
if t == nil {
|
||||
return nil
|
||||
@@ -55,7 +55,7 @@ func (t *Tree) Copy() *Tree {
|
||||
}
|
||||
}
|
||||
|
||||
// Parse returns a map from template name to parse.Tree, created by parsing the
|
||||
// Parse returns a map from template name to [Tree], created by parsing the
|
||||
// templates described in the argument string. The top-level template will be
|
||||
// given the specified name. If an error is encountered, parsing stops and an
|
||||
// empty map is returned with the error.
|
||||
@@ -521,7 +521,7 @@ func (t *Tree) checkPipeline(pipe *PipeNode, context string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tree) parseControl(allowElseIf bool, context string) (pos Pos, line int, pipe *PipeNode, list, elseList *ListNode) {
|
||||
func (t *Tree) parseControl(context string) (pos Pos, line int, pipe *PipeNode, list, elseList *ListNode) {
|
||||
defer t.popVars(len(t.vars))
|
||||
pipe = t.pipeline(context, itemRightDelim)
|
||||
if context == "range" {
|
||||
@@ -535,27 +535,30 @@ func (t *Tree) parseControl(allowElseIf bool, context string) (pos Pos, line int
|
||||
switch next.Type() {
|
||||
case nodeEnd: //done
|
||||
case nodeElse:
|
||||
if allowElseIf {
|
||||
// Special case for "else if". If the "else" is followed immediately by an "if",
|
||||
// the elseControl will have left the "if" token pending. Treat
|
||||
// {{if a}}_{{else if b}}_{{end}}
|
||||
// as
|
||||
// {{if a}}_{{else}}{{if b}}_{{end}}{{end}}.
|
||||
// To do this, parse the if as usual and stop at it {{end}}; the subsequent{{end}}
|
||||
// is assumed. This technique works even for long if-else-if chains.
|
||||
// TODO: Should we allow else-if in with and range?
|
||||
if t.peek().typ == itemIf {
|
||||
t.next() // Consume the "if" token.
|
||||
elseList = t.newList(next.Position())
|
||||
elseList.append(t.ifControl())
|
||||
// Do not consume the next item - only one {{end}} required.
|
||||
break
|
||||
// Special case for "else if" and "else with".
|
||||
// If the "else" is followed immediately by an "if" or "with",
|
||||
// the elseControl will have left the "if" or "with" token pending. Treat
|
||||
// {{if a}}_{{else if b}}_{{end}}
|
||||
// {{with a}}_{{else with b}}_{{end}}
|
||||
// as
|
||||
// {{if a}}_{{else}}{{if b}}_{{end}}{{end}}
|
||||
// {{with a}}_{{else}}{{with b}}_{{end}}{{end}}.
|
||||
// To do this, parse the "if" or "with" as usual and stop at it {{end}};
|
||||
// the subsequent{{end}} is assumed. This technique works even for long if-else-if chains.
|
||||
if context == "if" && t.peek().typ == itemIf {
|
||||
t.next() // Consume the "if" token.
|
||||
elseList = t.newList(next.Position())
|
||||
elseList.append(t.ifControl())
|
||||
} else if context == "with" && t.peek().typ == itemWith {
|
||||
t.next()
|
||||
elseList = t.newList(next.Position())
|
||||
elseList.append(t.withControl())
|
||||
} else {
|
||||
elseList, next = t.itemList()
|
||||
if next.Type() != nodeEnd {
|
||||
t.errorf("expected end; found %s", next)
|
||||
}
|
||||
}
|
||||
elseList, next = t.itemList()
|
||||
if next.Type() != nodeEnd {
|
||||
t.errorf("expected end; found %s", next)
|
||||
}
|
||||
}
|
||||
return pipe.Position(), pipe.Line, pipe, list, elseList
|
||||
}
|
||||
@@ -567,7 +570,7 @@ func (t *Tree) parseControl(allowElseIf bool, context string) (pos Pos, line int
|
||||
//
|
||||
// If keyword is past.
|
||||
func (t *Tree) ifControl() Node {
|
||||
return t.newIf(t.parseControl(true, "if"))
|
||||
return t.newIf(t.parseControl("if"))
|
||||
}
|
||||
|
||||
// Range:
|
||||
@@ -577,7 +580,7 @@ func (t *Tree) ifControl() Node {
|
||||
//
|
||||
// Range keyword is past.
|
||||
func (t *Tree) rangeControl() Node {
|
||||
r := t.newRange(t.parseControl(false, "range"))
|
||||
r := t.newRange(t.parseControl("range"))
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -588,7 +591,7 @@ func (t *Tree) rangeControl() Node {
|
||||
//
|
||||
// If keyword is past.
|
||||
func (t *Tree) withControl() Node {
|
||||
return t.newWith(t.parseControl(false, "with"))
|
||||
return t.newWith(t.parseControl("with"))
|
||||
}
|
||||
|
||||
// End:
|
||||
@@ -606,10 +609,11 @@ func (t *Tree) endControl() Node {
|
||||
//
|
||||
// Else keyword is past.
|
||||
func (t *Tree) elseControl() Node {
|
||||
// Special case for "else if".
|
||||
peek := t.peekNonSpace()
|
||||
if peek.typ == itemIf {
|
||||
// We see "{{else if ... " but in effect rewrite it to {{else}}{{if ... ".
|
||||
// The "{{else if ... " and "{{else with ..." will be
|
||||
// treated as "{{else}}{{if ..." and "{{else}}{{with ...".
|
||||
// So return the else node here.
|
||||
if peek.typ == itemIf || peek.typ == itemWith {
|
||||
return t.newElse(peek.pos, peek.line)
|
||||
}
|
||||
token := t.expect(itemRightDelim, "else")
|
||||
|
||||
@@ -33,9 +33,9 @@ var numberTests = []numberTest{
|
||||
{"7_3", true, true, true, false, 73, 73, 73, 0},
|
||||
{"0b10_010_01", true, true, true, false, 73, 73, 73, 0},
|
||||
{"0B10_010_01", true, true, true, false, 73, 73, 73, 0},
|
||||
{"073", true, true, true, false, 073, 073, 073, 0},
|
||||
{"0o73", true, true, true, false, 073, 073, 073, 0},
|
||||
{"0O73", true, true, true, false, 073, 073, 073, 0},
|
||||
{"073", true, true, true, false, 0o73, 0o73, 0o73, 0},
|
||||
{"0o73", true, true, true, false, 0o73, 0o73, 0o73, 0},
|
||||
{"0O73", true, true, true, false, 0o73, 0o73, 0o73, 0},
|
||||
{"0x73", true, true, true, false, 0x73, 0x73, 0x73, 0},
|
||||
{"0X73", true, true, true, false, 0x73, 0x73, 0x73, 0},
|
||||
{"0x7_3", true, true, true, false, 0x73, 0x73, 0x73, 0},
|
||||
@@ -61,7 +61,7 @@ var numberTests = []numberTest{
|
||||
{"-12+0i", true, false, true, true, -12, 0, -12, -12},
|
||||
{"13+0i", true, true, true, true, 13, 13, 13, 13},
|
||||
// funny bases
|
||||
{"0123", true, true, true, false, 0123, 0123, 0123, 0},
|
||||
{"0123", true, true, true, false, 0o123, 0o123, 0o123, 0},
|
||||
{"-0x0", true, true, true, false, 0, 0, 0, 0},
|
||||
{"0xdeadbeef", true, true, true, false, 0xdeadbeef, 0xdeadbeef, 0xdeadbeef, 0},
|
||||
// character constants
|
||||
@@ -176,74 +176,150 @@ const (
|
||||
)
|
||||
|
||||
var parseTests = []parseTest{
|
||||
{"empty", "", noError,
|
||||
``},
|
||||
{"comment", "{{/*\n\n\n*/}}", noError,
|
||||
``},
|
||||
{"spaces", " \t\n", noError,
|
||||
`" \t\n"`},
|
||||
{"text", "some text", noError,
|
||||
`"some text"`},
|
||||
{"emptyAction", "{{}}", hasError,
|
||||
`{{}}`},
|
||||
{"field", "{{.X}}", noError,
|
||||
`{{.X}}`},
|
||||
{"simple command", "{{printf}}", noError,
|
||||
`{{printf}}`},
|
||||
{"$ invocation", "{{$}}", noError,
|
||||
"{{$}}"},
|
||||
{"variable invocation", "{{with $x := 3}}{{$x 23}}{{end}}", noError,
|
||||
"{{with $x := 3}}{{$x 23}}{{end}}"},
|
||||
{"variable with fields", "{{$.I}}", noError,
|
||||
"{{$.I}}"},
|
||||
{"multi-word command", "{{printf `%d` 23}}", noError,
|
||||
"{{printf `%d` 23}}"},
|
||||
{"pipeline", "{{.X|.Y}}", noError,
|
||||
`{{.X | .Y}}`},
|
||||
{"pipeline with decl", "{{$x := .X|.Y}}", noError,
|
||||
`{{$x := .X | .Y}}`},
|
||||
{"nested pipeline", "{{.X (.Y .Z) (.A | .B .C) (.E)}}", noError,
|
||||
`{{.X (.Y .Z) (.A | .B .C) (.E)}}`},
|
||||
{"field applied to parentheses", "{{(.Y .Z).Field}}", noError,
|
||||
`{{(.Y .Z).Field}}`},
|
||||
{"simple if", "{{if .X}}hello{{end}}", noError,
|
||||
`{{if .X}}"hello"{{end}}`},
|
||||
{"if with else", "{{if .X}}true{{else}}false{{end}}", noError,
|
||||
`{{if .X}}"true"{{else}}"false"{{end}}`},
|
||||
{"if with else if", "{{if .X}}true{{else if .Y}}false{{end}}", noError,
|
||||
`{{if .X}}"true"{{else}}{{if .Y}}"false"{{end}}{{end}}`},
|
||||
{"if else chain", "+{{if .X}}X{{else if .Y}}Y{{else if .Z}}Z{{end}}+", noError,
|
||||
`"+"{{if .X}}"X"{{else}}{{if .Y}}"Y"{{else}}{{if .Z}}"Z"{{end}}{{end}}{{end}}"+"`},
|
||||
{"simple range", "{{range .X}}hello{{end}}", noError,
|
||||
`{{range .X}}"hello"{{end}}`},
|
||||
{"chained field range", "{{range .X.Y.Z}}hello{{end}}", noError,
|
||||
`{{range .X.Y.Z}}"hello"{{end}}`},
|
||||
{"nested range", "{{range .X}}hello{{range .Y}}goodbye{{end}}{{end}}", noError,
|
||||
`{{range .X}}"hello"{{range .Y}}"goodbye"{{end}}{{end}}`},
|
||||
{"range with else", "{{range .X}}true{{else}}false{{end}}", noError,
|
||||
`{{range .X}}"true"{{else}}"false"{{end}}`},
|
||||
{"range over pipeline", "{{range .X|.M}}true{{else}}false{{end}}", noError,
|
||||
`{{range .X | .M}}"true"{{else}}"false"{{end}}`},
|
||||
{"range []int", "{{range .SI}}{{.}}{{end}}", noError,
|
||||
`{{range .SI}}{{.}}{{end}}`},
|
||||
{"range 1 var", "{{range $x := .SI}}{{.}}{{end}}", noError,
|
||||
`{{range $x := .SI}}{{.}}{{end}}`},
|
||||
{"range 2 vars", "{{range $x, $y := .SI}}{{.}}{{end}}", noError,
|
||||
`{{range $x, $y := .SI}}{{.}}{{end}}`},
|
||||
{"range with break", "{{range .SI}}{{.}}{{break}}{{end}}", noError,
|
||||
`{{range .SI}}{{.}}{{break}}{{end}}`},
|
||||
{"range with continue", "{{range .SI}}{{.}}{{continue}}{{end}}", noError,
|
||||
`{{range .SI}}{{.}}{{continue}}{{end}}`},
|
||||
{"constants", "{{range .SI 1 -3.2i true false 'a' nil}}{{end}}", noError,
|
||||
`{{range .SI 1 -3.2i true false 'a' nil}}{{end}}`},
|
||||
{"template", "{{template `x`}}", noError,
|
||||
`{{template "x"}}`},
|
||||
{"template with arg", "{{template `x` .Y}}", noError,
|
||||
`{{template "x" .Y}}`},
|
||||
{"with", "{{with .X}}hello{{end}}", noError,
|
||||
`{{with .X}}"hello"{{end}}`},
|
||||
{"with with else", "{{with .X}}hello{{else}}goodbye{{end}}", noError,
|
||||
`{{with .X}}"hello"{{else}}"goodbye"{{end}}`},
|
||||
{
|
||||
"empty", "", noError,
|
||||
``,
|
||||
},
|
||||
{
|
||||
"comment", "{{/*\n\n\n*/}}", noError,
|
||||
``,
|
||||
},
|
||||
{
|
||||
"spaces", " \t\n", noError,
|
||||
`" \t\n"`,
|
||||
},
|
||||
{
|
||||
"text", "some text", noError,
|
||||
`"some text"`,
|
||||
},
|
||||
{
|
||||
"emptyAction", "{{}}", hasError,
|
||||
`{{}}`,
|
||||
},
|
||||
{
|
||||
"field", "{{.X}}", noError,
|
||||
`{{.X}}`,
|
||||
},
|
||||
{
|
||||
"simple command", "{{printf}}", noError,
|
||||
`{{printf}}`,
|
||||
},
|
||||
{
|
||||
"$ invocation", "{{$}}", noError,
|
||||
"{{$}}",
|
||||
},
|
||||
{
|
||||
"variable invocation", "{{with $x := 3}}{{$x 23}}{{end}}", noError,
|
||||
"{{with $x := 3}}{{$x 23}}{{end}}",
|
||||
},
|
||||
{
|
||||
"variable with fields", "{{$.I}}", noError,
|
||||
"{{$.I}}",
|
||||
},
|
||||
{
|
||||
"multi-word command", "{{printf `%d` 23}}", noError,
|
||||
"{{printf `%d` 23}}",
|
||||
},
|
||||
{
|
||||
"pipeline", "{{.X|.Y}}", noError,
|
||||
`{{.X | .Y}}`,
|
||||
},
|
||||
{
|
||||
"pipeline with decl", "{{$x := .X|.Y}}", noError,
|
||||
`{{$x := .X | .Y}}`,
|
||||
},
|
||||
{
|
||||
"nested pipeline", "{{.X (.Y .Z) (.A | .B .C) (.E)}}", noError,
|
||||
`{{.X (.Y .Z) (.A | .B .C) (.E)}}`,
|
||||
},
|
||||
{
|
||||
"field applied to parentheses", "{{(.Y .Z).Field}}", noError,
|
||||
`{{(.Y .Z).Field}}`,
|
||||
},
|
||||
{
|
||||
"simple if", "{{if .X}}hello{{end}}", noError,
|
||||
`{{if .X}}"hello"{{end}}`,
|
||||
},
|
||||
{
|
||||
"if with else", "{{if .X}}true{{else}}false{{end}}", noError,
|
||||
`{{if .X}}"true"{{else}}"false"{{end}}`,
|
||||
},
|
||||
{
|
||||
"if with else if", "{{if .X}}true{{else if .Y}}false{{end}}", noError,
|
||||
`{{if .X}}"true"{{else}}{{if .Y}}"false"{{end}}{{end}}`,
|
||||
},
|
||||
{
|
||||
"if else chain", "+{{if .X}}X{{else if .Y}}Y{{else if .Z}}Z{{end}}+", noError,
|
||||
`"+"{{if .X}}"X"{{else}}{{if .Y}}"Y"{{else}}{{if .Z}}"Z"{{end}}{{end}}{{end}}"+"`,
|
||||
},
|
||||
{
|
||||
"simple range", "{{range .X}}hello{{end}}", noError,
|
||||
`{{range .X}}"hello"{{end}}`,
|
||||
},
|
||||
{
|
||||
"chained field range", "{{range .X.Y.Z}}hello{{end}}", noError,
|
||||
`{{range .X.Y.Z}}"hello"{{end}}`,
|
||||
},
|
||||
{
|
||||
"nested range", "{{range .X}}hello{{range .Y}}goodbye{{end}}{{end}}", noError,
|
||||
`{{range .X}}"hello"{{range .Y}}"goodbye"{{end}}{{end}}`,
|
||||
},
|
||||
{
|
||||
"range with else", "{{range .X}}true{{else}}false{{end}}", noError,
|
||||
`{{range .X}}"true"{{else}}"false"{{end}}`,
|
||||
},
|
||||
{
|
||||
"range over pipeline", "{{range .X|.M}}true{{else}}false{{end}}", noError,
|
||||
`{{range .X | .M}}"true"{{else}}"false"{{end}}`,
|
||||
},
|
||||
{
|
||||
"range []int", "{{range .SI}}{{.}}{{end}}", noError,
|
||||
`{{range .SI}}{{.}}{{end}}`,
|
||||
},
|
||||
{
|
||||
"range 1 var", "{{range $x := .SI}}{{.}}{{end}}", noError,
|
||||
`{{range $x := .SI}}{{.}}{{end}}`,
|
||||
},
|
||||
{
|
||||
"range 2 vars", "{{range $x, $y := .SI}}{{.}}{{end}}", noError,
|
||||
`{{range $x, $y := .SI}}{{.}}{{end}}`,
|
||||
},
|
||||
{
|
||||
"range with break", "{{range .SI}}{{.}}{{break}}{{end}}", noError,
|
||||
`{{range .SI}}{{.}}{{break}}{{end}}`,
|
||||
},
|
||||
{
|
||||
"range with continue", "{{range .SI}}{{.}}{{continue}}{{end}}", noError,
|
||||
`{{range .SI}}{{.}}{{continue}}{{end}}`,
|
||||
},
|
||||
{
|
||||
"constants", "{{range .SI 1 -3.2i true false 'a' nil}}{{end}}", noError,
|
||||
`{{range .SI 1 -3.2i true false 'a' nil}}{{end}}`,
|
||||
},
|
||||
{
|
||||
"template", "{{template `x`}}", noError,
|
||||
`{{template "x"}}`,
|
||||
},
|
||||
{
|
||||
"template with arg", "{{template `x` .Y}}", noError,
|
||||
`{{template "x" .Y}}`,
|
||||
},
|
||||
{
|
||||
"with", "{{with .X}}hello{{end}}", noError,
|
||||
`{{with .X}}"hello"{{end}}`,
|
||||
},
|
||||
{
|
||||
"with with else", "{{with .X}}hello{{else}}goodbye{{end}}", noError,
|
||||
`{{with .X}}"hello"{{else}}"goodbye"{{end}}`,
|
||||
},
|
||||
{
|
||||
"with with else with", "{{with .X}}hello{{else with .Y}}goodbye{{end}}", noError,
|
||||
`{{with .X}}"hello"{{else}}{{with .Y}}"goodbye"{{end}}{{end}}`,
|
||||
},
|
||||
{
|
||||
"with else chain", "{{with .X}}X{{else with .Y}}Y{{else with .Z}}Z{{end}}", noError,
|
||||
`{{with .X}}"X"{{else}}{{with .Y}}"Y"{{else}}{{with .Z}}"Z"{{end}}{{end}}{{end}}`,
|
||||
},
|
||||
// Trimming spaces.
|
||||
{"trim left", "x \r\n\t{{- 3}}", noError, `"x"{{3}}`},
|
||||
{"trim right", "{{3 -}}\n\n\ty", noError, `{{3}}"y"`},
|
||||
@@ -252,18 +328,24 @@ var parseTests = []parseTest{
|
||||
{"comment trim left", "x \r\n\t{{- /* hi */}}", noError, `"x"`},
|
||||
{"comment trim right", "{{/* hi */ -}}\n\n\ty", noError, `"y"`},
|
||||
{"comment trim left and right", "x \r\n\t{{- /* */ -}}\n\n\ty", noError, `"x""y"`},
|
||||
{"block definition", `{{block "foo" .}}hello{{end}}`, noError,
|
||||
`{{template "foo" .}}`},
|
||||
{
|
||||
"block definition", `{{block "foo" .}}hello{{end}}`, noError,
|
||||
`{{template "foo" .}}`,
|
||||
},
|
||||
|
||||
{"newline in assignment", "{{ $x \n := \n 1 \n }}", noError, "{{$x := 1}}"},
|
||||
{"newline in empty action", "{{\n}}", hasError, "{{\n}}"},
|
||||
{"newline in pipeline", "{{\n\"x\"\n|\nprintf\n}}", noError, `{{"x" | printf}}`},
|
||||
{"newline in comment", "{{/*\nhello\n*/}}", noError, ""},
|
||||
{"newline in comment", "{{-\n/*\nhello\n*/\n-}}", noError, ""},
|
||||
{"spaces around continue", "{{range .SI}}{{.}}{{ continue }}{{end}}", noError,
|
||||
`{{range .SI}}{{.}}{{continue}}{{end}}`},
|
||||
{"spaces around break", "{{range .SI}}{{.}}{{ break }}{{end}}", noError,
|
||||
`{{range .SI}}{{.}}{{break}}{{end}}`},
|
||||
{
|
||||
"spaces around continue", "{{range .SI}}{{.}}{{ continue }}{{end}}", noError,
|
||||
`{{range .SI}}{{.}}{{continue}}{{end}}`,
|
||||
},
|
||||
{
|
||||
"spaces around break", "{{range .SI}}{{.}}{{ break }}{{end}}", noError,
|
||||
`{{range .SI}}{{.}}{{break}}{{end}}`,
|
||||
},
|
||||
|
||||
// Errors.
|
||||
{"unclosed action", "hello{{range", hasError, ""},
|
||||
@@ -302,6 +384,9 @@ var parseTests = []parseTest{
|
||||
{"bug1a", "{{$x:=.}}{{$x!2}}", hasError, ""}, // ! is just illegal here.
|
||||
{"bug1b", "{{$x:=.}}{{$x+2}}", hasError, ""}, // $x+2 should not parse as ($x) (+2).
|
||||
{"bug1c", "{{$x:=.}}{{$x +2}}", noError, "{{$x := .}}{{$x +2}}"}, // It's OK with a space.
|
||||
// Check the range handles assignment vs. declaration properly.
|
||||
{"bug2a", "{{range $x := 0}}{{$x}}{{end}}", noError, "{{range $x := 0}}{{$x}}{{end}}"},
|
||||
{"bug2b", "{{range $x = 0}}{{$x}}{{end}}", noError, "{{range $x = 0}}{{$x}}{{end}}"},
|
||||
// dot following a literal value
|
||||
{"dot after integer", "{{1.E}}", hasError, ""},
|
||||
{"dot after float", "{{0.1.E}}", hasError, ""},
|
||||
@@ -402,7 +487,7 @@ func TestKeywordsAndFuncs(t *testing.T) {
|
||||
{
|
||||
// 'break' is a defined function, don't treat it as a keyword: it should
|
||||
// accept an argument successfully.
|
||||
var funcsWithKeywordFunc = map[string]any{
|
||||
funcsWithKeywordFunc := map[string]any{
|
||||
"break": func(in any) any { return in },
|
||||
}
|
||||
tmpl, err := New("").Parse(inp, "", "", make(map[string]*Tree), funcsWithKeywordFunc)
|
||||
@@ -489,104 +574,168 @@ func TestErrorContextWithTreeCopy(t *testing.T) {
|
||||
// All failures, and the result is a string that must appear in the error message.
|
||||
var errorTests = []parseTest{
|
||||
// Check line numbers are accurate.
|
||||
{"unclosed1",
|
||||
{
|
||||
"unclosed1",
|
||||
"line1\n{{",
|
||||
hasError, `unclosed1:2: unclosed action`},
|
||||
{"unclosed2",
|
||||
hasError, `unclosed1:2: unclosed action`,
|
||||
},
|
||||
{
|
||||
"unclosed2",
|
||||
"line1\n{{define `x`}}line2\n{{",
|
||||
hasError, `unclosed2:3: unclosed action`},
|
||||
{"unclosed3",
|
||||
hasError, `unclosed2:3: unclosed action`,
|
||||
},
|
||||
{
|
||||
"unclosed3",
|
||||
"line1\n{{\"x\"\n\"y\"\n",
|
||||
hasError, `unclosed3:4: unclosed action started at unclosed3:2`},
|
||||
{"unclosed4",
|
||||
hasError, `unclosed3:4: unclosed action started at unclosed3:2`,
|
||||
},
|
||||
{
|
||||
"unclosed4",
|
||||
"{{\n\n\n\n\n",
|
||||
hasError, `unclosed4:6: unclosed action started at unclosed4:1`},
|
||||
{"var1",
|
||||
hasError, `unclosed4:6: unclosed action started at unclosed4:1`,
|
||||
},
|
||||
{
|
||||
"var1",
|
||||
"line1\n{{\nx\n}}",
|
||||
hasError, `var1:3: function "x" not defined`},
|
||||
hasError, `var1:3: function "x" not defined`,
|
||||
},
|
||||
// Specific errors.
|
||||
{"function",
|
||||
{
|
||||
"function",
|
||||
"{{foo}}",
|
||||
hasError, `function "foo" not defined`},
|
||||
{"comment1",
|
||||
hasError, `function "foo" not defined`,
|
||||
},
|
||||
{
|
||||
"comment1",
|
||||
"{{/*}}",
|
||||
hasError, `comment1:1: unclosed comment`},
|
||||
{"comment2",
|
||||
hasError, `comment1:1: unclosed comment`,
|
||||
},
|
||||
{
|
||||
"comment2",
|
||||
"{{/*\nhello\n}}",
|
||||
hasError, `comment2:1: unclosed comment`},
|
||||
{"lparen",
|
||||
hasError, `comment2:1: unclosed comment`,
|
||||
},
|
||||
{
|
||||
"lparen",
|
||||
"{{.X (1 2 3}}",
|
||||
hasError, `unclosed left paren`},
|
||||
{"rparen",
|
||||
hasError, `unclosed left paren`,
|
||||
},
|
||||
{
|
||||
"rparen",
|
||||
"{{.X 1 2 3 ) }}",
|
||||
hasError, "unexpected right paren"},
|
||||
{"rparen2",
|
||||
hasError, "unexpected right paren",
|
||||
},
|
||||
{
|
||||
"rparen2",
|
||||
"{{(.X 1 2 3",
|
||||
hasError, `unclosed action`},
|
||||
{"space",
|
||||
hasError, `unclosed action`,
|
||||
},
|
||||
{
|
||||
"space",
|
||||
"{{`x`3}}",
|
||||
hasError, `in operand`},
|
||||
{"idchar",
|
||||
hasError, `in operand`,
|
||||
},
|
||||
{
|
||||
"idchar",
|
||||
"{{a#}}",
|
||||
hasError, `'#'`},
|
||||
{"charconst",
|
||||
hasError, `'#'`,
|
||||
},
|
||||
{
|
||||
"charconst",
|
||||
"{{'a}}",
|
||||
hasError, `unterminated character constant`},
|
||||
{"stringconst",
|
||||
hasError, `unterminated character constant`,
|
||||
},
|
||||
{
|
||||
"stringconst",
|
||||
`{{"a}}`,
|
||||
hasError, `unterminated quoted string`},
|
||||
{"rawstringconst",
|
||||
hasError, `unterminated quoted string`,
|
||||
},
|
||||
{
|
||||
"rawstringconst",
|
||||
"{{`a}}",
|
||||
hasError, `unterminated raw quoted string`},
|
||||
{"number",
|
||||
hasError, `unterminated raw quoted string`,
|
||||
},
|
||||
{
|
||||
"number",
|
||||
"{{0xi}}",
|
||||
hasError, `number syntax`},
|
||||
{"multidefine",
|
||||
hasError, `number syntax`,
|
||||
},
|
||||
{
|
||||
"multidefine",
|
||||
"{{define `a`}}a{{end}}{{define `a`}}b{{end}}",
|
||||
hasError, `multiple definition of template`},
|
||||
{"eof",
|
||||
hasError, `multiple definition of template`,
|
||||
},
|
||||
{
|
||||
"eof",
|
||||
"{{range .X}}",
|
||||
hasError, `unexpected EOF`},
|
||||
{"variable",
|
||||
hasError, `unexpected EOF`,
|
||||
},
|
||||
{
|
||||
"variable",
|
||||
// Declare $x so it's defined, to avoid that error, and then check we don't parse a declaration.
|
||||
"{{$x := 23}}{{with $x.y := 3}}{{$x 23}}{{end}}",
|
||||
hasError, `unexpected ":="`},
|
||||
{"multidecl",
|
||||
hasError, `unexpected ":="`,
|
||||
},
|
||||
{
|
||||
"multidecl",
|
||||
"{{$a,$b,$c := 23}}",
|
||||
hasError, `too many declarations`},
|
||||
{"undefvar",
|
||||
hasError, `too many declarations`,
|
||||
},
|
||||
{
|
||||
"undefvar",
|
||||
"{{$a}}",
|
||||
hasError, `undefined variable`},
|
||||
{"wrongdot",
|
||||
hasError, `undefined variable`,
|
||||
},
|
||||
{
|
||||
"wrongdot",
|
||||
"{{true.any}}",
|
||||
hasError, `unexpected . after term`},
|
||||
{"wrongpipeline",
|
||||
hasError, `unexpected . after term`,
|
||||
},
|
||||
{
|
||||
"wrongpipeline",
|
||||
"{{12|false}}",
|
||||
hasError, `non executable command in pipeline`},
|
||||
{"emptypipeline",
|
||||
hasError, `non executable command in pipeline`,
|
||||
},
|
||||
{
|
||||
"emptypipeline",
|
||||
`{{ ( ) }}`,
|
||||
hasError, `missing value for parenthesized pipeline`},
|
||||
{"multilinerawstring",
|
||||
hasError, `missing value for parenthesized pipeline`,
|
||||
},
|
||||
{
|
||||
"multilinerawstring",
|
||||
"{{ $v := `\n` }} {{",
|
||||
hasError, `multilinerawstring:2: unclosed action`},
|
||||
{"rangeundefvar",
|
||||
hasError, `multilinerawstring:2: unclosed action`,
|
||||
},
|
||||
{
|
||||
"rangeundefvar",
|
||||
"{{range $k}}{{end}}",
|
||||
hasError, `undefined variable`},
|
||||
{"rangeundefvars",
|
||||
hasError, `undefined variable`,
|
||||
},
|
||||
{
|
||||
"rangeundefvars",
|
||||
"{{range $k, $v}}{{end}}",
|
||||
hasError, `undefined variable`},
|
||||
{"rangemissingvalue1",
|
||||
hasError, `undefined variable`,
|
||||
},
|
||||
{
|
||||
"rangemissingvalue1",
|
||||
"{{range $k,}}{{end}}",
|
||||
hasError, `missing value for range`},
|
||||
{"rangemissingvalue2",
|
||||
hasError, `missing value for range`,
|
||||
},
|
||||
{
|
||||
"rangemissingvalue2",
|
||||
"{{range $k, $v := }}{{end}}",
|
||||
hasError, `missing value for range`},
|
||||
{"rangenotvariable1",
|
||||
hasError, `missing value for range`,
|
||||
},
|
||||
{
|
||||
"rangenotvariable1",
|
||||
"{{range $k, .}}{{end}}",
|
||||
hasError, `range can only initialize variables`},
|
||||
{"rangenotvariable2",
|
||||
hasError, `range can only initialize variables`,
|
||||
},
|
||||
{
|
||||
"rangenotvariable2",
|
||||
"{{range $k, 123 := .}}{{end}}",
|
||||
hasError, `range can only initialize variables`},
|
||||
hasError, `range can only initialize variables`,
|
||||
},
|
||||
}
|
||||
|
||||
func TestErrors(t *testing.T) {
|
||||
|
||||
@@ -24,7 +24,7 @@ type common struct {
|
||||
}
|
||||
|
||||
// Template is the representation of a parsed template. The *parse.Tree
|
||||
// field is exported only for use by html/template and should be treated
|
||||
// field is exported only for use by [html/template] and should be treated
|
||||
// as unexported by all other clients.
|
||||
type Template struct {
|
||||
name string
|
||||
@@ -79,7 +79,7 @@ func (t *Template) init() {
|
||||
|
||||
// Clone returns a duplicate of the template, including all associated
|
||||
// templates. The actual representation is not copied, but the name space of
|
||||
// associated templates is, so further calls to Parse in the copy will add
|
||||
// associated templates is, so further calls to [Template.Parse] in the copy will add
|
||||
// templates to the copy but not to the original. Clone can be used to prepare
|
||||
// common templates and use them with variant definitions for other templates
|
||||
// by adding the variants after the clone is made.
|
||||
@@ -157,7 +157,7 @@ func (t *Template) Templates() []*Template {
|
||||
}
|
||||
|
||||
// Delims sets the action delimiters to the specified strings, to be used in
|
||||
// subsequent calls to Parse, ParseFiles, or ParseGlob. Nested template
|
||||
// subsequent calls to [Template.Parse], [Template.ParseFiles], or [Template.ParseGlob]. Nested template
|
||||
// definitions will inherit the settings. An empty delimiter stands for the
|
||||
// corresponding default: {{ or }}.
|
||||
// The return value is the template, so calls can be chained.
|
||||
|
||||
@@ -224,7 +224,7 @@ func (ns *Namespace) Concat(targetPathIn any, r any) (resource.Resource, error)
|
||||
case resource.ResourcesConverter:
|
||||
rr = v.ToResources()
|
||||
default:
|
||||
return nil, fmt.Errorf("slice %T not supported in concat", r)
|
||||
return nil, fmt.Errorf("expected slice of Resource objects, received %T instead", r)
|
||||
}
|
||||
|
||||
if len(rr) == 0 {
|
||||
@@ -310,14 +310,14 @@ func (ns *Namespace) Minify(r resources.ResourceTransformer) (resource.Resource,
|
||||
// for the converted CSS resource.
|
||||
// Deprecated: Moved to the css namespace in Hugo 0.128.0.
|
||||
func (ns *Namespace) ToCSS(args ...any) (resource.Resource, error) {
|
||||
hugo.Deprecate("resources.ToCSS", "Use css.SASS.", "v0.128.0")
|
||||
hugo.Deprecate("resources.ToCSS", "Use css.Sass instead.", "v0.128.0")
|
||||
return ns.cssNs.Sass(args...)
|
||||
}
|
||||
|
||||
// PostCSS processes the given Resource with PostCSS.
|
||||
// Deprecated: Moved to the css namespace in Hugo 0.128.0.
|
||||
func (ns *Namespace) PostCSS(args ...any) (resource.Resource, error) {
|
||||
hugo.Deprecate("resources.PostCSS", "Use css.PostCSS.", "v0.128.0")
|
||||
hugo.Deprecate("resources.PostCSS", "Use css.PostCSS instead.", "v0.128.0")
|
||||
return ns.cssNs.PostCSS(args...)
|
||||
}
|
||||
|
||||
|
||||
@@ -25,5 +25,5 @@
|
||||
{{- printf " %s=%q" $k $v | safeHTMLAttr -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
>{{ .Text | safeHTML }}</a>
|
||||
>{{ .Text }}</a>
|
||||
{{- /**/ -}}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<table
|
||||
{{- range $k, $v := .Attributes }}
|
||||
{{- if $v }}
|
||||
{{- printf " %s=%q" $k $v | safeHTMLAttr }}
|
||||
{{- end }}
|
||||
{{- end }}>
|
||||
<thead>
|
||||
{{- range .THead }}
|
||||
<tr>
|
||||
{{- range . }}
|
||||
<th {{ printf "style=%q" (printf "text-align: %s" .Alignment) | safeHTMLAttr }}>
|
||||
{{- .Text -}}
|
||||
</th>
|
||||
{{- end }}
|
||||
</tr>
|
||||
{{- end }}
|
||||
</thead>
|
||||
<tbody>
|
||||
{{- range .TBody }}
|
||||
<tr>
|
||||
{{- range . }}
|
||||
<td {{ printf "style=%q" (printf "text-align: %s" .Alignment) | safeHTMLAttr }}>
|
||||
{{- .Text -}}
|
||||
</td>
|
||||
{{- end }}
|
||||
</tr>
|
||||
{{- end }}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -1,8 +1,8 @@
|
||||
{{ if not site.Config.Privacy.GoogleAnalytics.Disable }}
|
||||
{{ with site.Config.Services.GoogleAnalytics.ID }}
|
||||
{{ if strings.HasPrefix (lower .) "ua-" }}
|
||||
{{ warnf "Google Analytics 4 (GA4) replaced Google Universal Analytics (UA) effective 1 July 2023. See https://support.google.com/analytics/answer/11583528. Create a GA4 property and data stream, then replace the Google Analytics ID in your site configuration with the new value." }}
|
||||
{{ else }}
|
||||
{{- with site.Config.Services.GoogleAnalytics.ID }}
|
||||
{{- if strings.HasPrefix (lower .) "ua-" }}
|
||||
{{- warnf "Google Analytics 4 (GA4) replaced Google Universal Analytics (UA) effective 1 July 2023. See https://support.google.com/analytics/answer/11583528. Create a GA4 property and data stream, then replace the Google Analytics ID in your site configuration with the new value." }}
|
||||
{{- else }}
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id={{ . }}"></script>
|
||||
<script>
|
||||
var doNotTrack = false;
|
||||
@@ -17,6 +17,6 @@
|
||||
gtag('config', '{{ . }}');
|
||||
}
|
||||
</script>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end -}}
|
||||
|
||||
@@ -116,6 +116,20 @@ counter2: 3
|
||||
`)
|
||||
}
|
||||
|
||||
func TestGo23ElseWith(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
files := `
|
||||
-- hugo.toml --
|
||||
title = "Hugo"
|
||||
-- layouts/index.html --
|
||||
{{ with false }}{{ else with .Site }}{{ .Title }}{{ end }}|
|
||||
`
|
||||
b := hugolib.Test(t, files)
|
||||
|
||||
b.AssertFileContent("public/index.html", "Hugo|")
|
||||
}
|
||||
|
||||
// Issue 10495
|
||||
func TestCommentsBeforeBlockDefinition(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
Reference in New Issue
Block a user