mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-26 16:28:52 +00:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 56d3114199 | |||
| 6bde2a5f88 | |||
| f738669a4d | |||
| b63f24adc7 | |||
| 8fb933550f | |||
| a3684c8361 | |||
| 53a8de21b8 | |||
| 37609262dc | |||
| 2b5c335e93 | |||
| 12a28ef773 | |||
| 4ded32d077 | |||
| 1ecd0596a3 | |||
| 371246de25 | |||
| aa0f66b290 |
@@ -17,7 +17,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.22.x, 1.23.x]
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
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,
|
||||
PatchLevel: 1,
|
||||
Suffix: "",
|
||||
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"))
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
+7
-7
@@ -1865,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
|
||||
@@ -1874,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,7 +12,7 @@ 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.1
|
||||
@@ -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
|
||||
|
||||
@@ -135,12 +135,12 @@ 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.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=
|
||||
@@ -191,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=
|
||||
@@ -402,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=
|
||||
@@ -444,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=
|
||||
@@ -453,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=
|
||||
@@ -463,8 +461,6 @@ 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=
|
||||
|
||||
@@ -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=\"#")
|
||||
|
||||
|
||||
@@ -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)",
|
||||
)
|
||||
|
||||
@@ -930,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.")
|
||||
}
|
||||
|
||||
+3
-2
@@ -1,7 +1,8 @@
|
||||
# Release env.
|
||||
# These will be replaced by script before release.
|
||||
HUGORELEASER_TAG=v0.133.0
|
||||
HUGORELEASER_COMMITISH=c9777473d1369f812d727a6c07dc57ad7be7bf62
|
||||
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>")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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 -}}
|
||||
|
||||
Reference in New Issue
Block a user