Compare commits

...

25 Commits

Author SHA1 Message Date
hugoreleaser 77df7bbbff releaser: Bump versions for release of 0.134.0
[ci skip]
2024-09-03 09:54:22 +00:00
Aymen Rachdi ae3815898f Update Dockerfile 2024-09-02 21:56:33 +02:00
Bjørn Erik Pedersen 504a23184f markup/goldmark/blockquotes: Improve some tests 2024-09-01 18:25:10 +02:00
Bjørn Erik Pedersen e651d29801 Add support for Obsidian type blockquote alerts
* Make the alert type parsing more flexible to support more types
* Add `AlertTitle` and `AlertSign` (for folding)

Note that GitHub will not render callouts with alert title/sign.

See https://help.obsidian.md/Editing+and+formatting/Callouts

Closes #12805
Closes #12801
2024-09-01 18:12:26 +02:00
Bjørn Erik Pedersen 0c453420e6 Merge commit 'dec8cd4ada29218971743333f8ac662a9c06aad8' 2024-09-01 14:51:15 +02:00
Bjørn Erik Pedersen dec8cd4ada Squashed 'docs/' changes from fcc3ed651..a49697e53
a49697e53 Add private use subtag requirements to multilingual page
a5c6bb4da Add INFINI Pizza search engine
45b732efa Fix template lookup order for AMP pages
30c672d0b netlify: Hugo 0.133.1
7c766e724 Update page resources documentation
ca802fbec Document how to enable AsciiDoc syntax highlighting
c3350f4cf Update definition of falsy to include zero time.Time values
b0e5ab051 Fir typo
60f6cb63b Update migrations.md
ec52c7ba1 Improve formatting of example code
e5681ad01 Improve formatting of example code
bdf3ffc73 Clarify the various next/prev methods
b5505d22a Clarify template lookup order for shortcodes
cf8dd7034 Improve embedded.md
e5dee2651 Update transform.ToMath
4d419a128 Update pagination configuration to use new struct
05d4fd597 Update PrevInSection.md
fd33370ed Add new-in 0.133.0
f9062042f Add the new page config section
205645e97 Remove out-dated  new-in
3ed3673f7 Fix typo
41df91659 Document the 'else with' construct introduced with Go 1.23
9c4697ab3 netlify: Hugo 0.133.0
62506b052 Merge branch 'temp133'
877e1bfcd Add config options page.nextPrevSortOrder/nextPrevInSectionSortOrder
eb159fe62 Update menu.md
efa7795a0 Update theme
dbe8911ad netlify: Hugo 0.132.2
2f793d328 Document passthrough render hooks
a7ce9a5e8 netlify: Hugo 0.132.1
2c137cb48 Update blockquotes.md
e0fa2f0d1 Add new-in badge to blockquote render hook page
bf42bbe6b Update references to render hooks
85a3d9958 Update theme
2dae72128 Document blockquote render hooks
8f5afb55d Update plainify return type
160f22d0e netlify: Hugo 0.132.0
82b5586fb Document transform.ToMath
1efcbcddb tpl/transform: Make Plainify and ToMath return template.HTML
31727be2e docs: Regen docshelper
88a421426 Merge commit 'a6e635ca7d905d9ec3ffd708db2694f680b03aae'

git-subtree-dir: docs
git-subtree-split: a49697e536
2024-09-01 14:51:15 +02:00
Bjørn Erik Pedersen 469124823c Rename hstring.RenderedHTML => hstring.HTML
And add a comment about why it exists.
2024-09-01 10:04:20 +02:00
Bjørn Erik Pedersen 96afea4acc github: Try to fix "no space left on device" on MacOS 2024-08-31 21:05:58 +02:00
Bjørn Erik Pedersen 6d97ee711e Make all renderhook Text methods return template.HTML 2024-08-31 21:05:58 +02:00
Bjørn Erik Pedersen f738669a4d Add Markdown render hooks for tables
Fixes #9316
Fixes #12811
2024-08-31 17:21:31 +02:00
Joe Mooring b63f24adc7 create/skeletons: Clean up lang attribute in base template 2024-08-31 17:02:47 +02:00
Andreas Deininger 8fb933550f Fix deprecation warning for resources.ToCSS 2024-08-30 22:26:29 +02:00
Joe Mooring a3684c8361 tpl/resources: Improve resources.Concat error message
Closes #7428
2024-08-30 08:39:29 +02:00
Patrice Chalin 53a8de21b8 tpl: Trim whitespace from google_analytics.html 2024-08-29 21:32:46 +02:00
Bjørn Erik Pedersen 37609262dc Add Page.Contents with scope support
Note that this also adds a new `.ContentWithoutSummary` method, and to do that we had to unify the different summary types:

Both `auto` and `manual` now returns HTML. Before this commit, `auto` would return plain text. This could be considered to be a slightly breaking change, but for the better: Now you can treat the `.Summary` the same without thinking about where it comes from, and if you want plain text, pipe it into `{{ .Summary | plainify }}`.

Fixes #8680
Fixes #12761
Fixes #12778
Fixes #716
2024-08-29 16:45:21 +02:00
Bjørn Erik Pedersen 2b5c335e93 deps: Upgrade github.com/bep/golibsass v1.1.1 => v1.2.0
Fixes #12649
2024-08-28 17:02:19 +02:00
dependabot[bot] 12a28ef773 build(deps): bump github.com/evanw/esbuild from 0.23.0 to 0.23.1
Bumps [github.com/evanw/esbuild](https://github.com/evanw/esbuild) from 0.23.0 to 0.23.1.
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.23.0...v0.23.1)

---
updated-dependencies:
- dependency-name: github.com/evanw/esbuild
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-28 13:05:57 +02:00
dependabot[bot] 4ded32d077 build(deps): bump github.com/pelletier/go-toml/v2 from 2.2.2 to 2.2.3
Bumps [github.com/pelletier/go-toml/v2](https://github.com/pelletier/go-toml) from 2.2.2 to 2.2.3.
- [Release notes](https://github.com/pelletier/go-toml/releases)
- [Changelog](https://github.com/pelletier/go-toml/blob/v2/.goreleaser.yaml)
- [Commits](https://github.com/pelletier/go-toml/compare/v2.2.2...v2.2.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-28 13:05:42 +02:00
Joe Mooring 1ecd0596a3 output: Fix docshelper template lookup order for AMP pages
Fixes #12797
2024-08-28 13:05:28 +02:00
Bjørn Erik Pedersen 371246de25 hugolib: Add a test for overriding _internal templates 2024-08-28 08:17:16 +02:00
hugoreleaser aa0f66b290 releaser: Prepare repository for 0.134.0-DEV
[ci skip]
2024-08-26 14:13:52 +00:00
hugoreleaser 47d00202e7 releaser: Bump versions for release of 0.133.1
[ci skip]
2024-08-26 13:58:46 +00:00
Bjørn Erik Pedersen 261d7a03ae Revert "Adjust Circleci workflow to make the Docker images build"
This reverts commit 445283a593.
2024-08-26 15:56:39 +02:00
Bjørn Erik Pedersen 445283a593 Adjust Circleci workflow to make the Docker images build
Closes #12753
2024-08-26 14:19:22 +02:00
Bjørn Erik Pedersen 5f667f8796 Fix missing method NameNormalized panic
Closes #12795
2024-08-25 23:17:26 +02:00
99 changed files with 3403 additions and 1663 deletions
+1 -1
View File
@@ -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'
+1 -1
View File
@@ -2,7 +2,7 @@
# Twitter: https://twitter.com/gohugoio
# Website: https://gohugo.io/
FROM golang:1.21-alpine AS build
FROM golang:1.22.6-alpine AS build
# Optionally set HUGO_BUILD_TAGS to "extended" or "nodeploy" when building like so:
# docker build --build-arg HUGO_BUILD_TAGS=extended .
+26 -1
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -19,5 +19,5 @@ var CurrentVersion = Version{
Major: 0,
Minor: 134,
PatchLevel: 0,
Suffix: "-DEV",
Suffix: "",
}
+2 -2
View File
@@ -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
+18 -2
View File
@@ -13,8 +13,24 @@
package hstring
type RenderedString string
import (
"html/template"
func (s RenderedString) String() string {
"github.com/gohugoio/hugo/common/types"
)
var _ types.PrintableValueProvider = HTML("")
// HTML is a string that represents rendered HTML.
// When printed in templates it will be rendered as template.HTML and considered safe so no need to pipe it into `safeHTML`.
// This type was introduced as a wasy to prevent a common case of inifinite recursion in the template rendering
// when the `linkify` option is enabled with a common (wrong) construct like `{{ .Text | .Page.RenderString }}` in a hook template.
type HTML string
func (s HTML) String() string {
return string(s)
}
func (s HTML) PrintableValue() any {
return template.HTML(s)
}
+2 -2
View File
@@ -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(HTML("Hugo")), qt.Equals, "Hugo")
c.Assert(template.HTML(HTML("Hugo")), qt.Equals, template.HTML("Hugo"))
}
+10 -2
View File
@@ -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
+22
View File
@@ -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>
@@ -1,4 +1,4 @@
{{ define "main" }}
{{ $section_to_display := (.Site.Taxonomies.categories.fundamentals).Pages | lang.Merge (.Sites.First.Taxonomies.categories.fundamentals).Pages }}
{{ $section_to_display := (.Site.Taxonomies.categories.fundamentals).Pages | lang.Merge (.Sites.Default.Taxonomies.categories.fundamentals).Pages }}
{{ partial "pagelayout.html" (dict "context" . "section_to_display" $section_to_display ) }}
{{ end }}
@@ -1,5 +1,5 @@
{{ define "main" }}
{{ $paginator := .Paginate (.Pages | lang.Merge (where .Sites.First.RegularPages "Section" .Section)) }}
{{ $paginator := .Paginate (.Pages | lang.Merge (where .Sites.Default.RegularPages "Section" .Section)) }}
{{ $section_to_display := .Sections | default $paginator.Pages }}
{{ partial "pagelayout.html" (dict "context" . "section_to_display" $section_to_display ) }}
{{ end }}
@@ -1,5 +1,6 @@
{{ $text := `
Most of the commands for **Hugo Modules** require a newer version of Go installed (see https://golang.org/dl/) and the relevant VCS client (e.g. Git, see https://git-scm.com/downloads/ ). If you have an "older" site running on Netlify, you may have to set GO_VERSION to 1.12 in your Environment settings.
Most of the commands for **Hugo Modules** require a newer version (>= 1.18) of Go installed (see https://golang.org/dl/) and the relevant VCS client (e.g. Git, see https://git-scm.com/downloads/ ).
If you have an "older" site running on Netlify, you may have to set GO_VERSION to 1.19 or newer in your Environment settings.
For more information about Go Modules, see:
+1 -1
View File
@@ -1 +1 @@
# github.com/gohugoio/gohugoioTheme v0.0.0-20240728210410-d42c342ce472
# github.com/gohugoio/gohugoioTheme v0.0.0-20240815082608-66ccd383a90f
+1 -1
View File
@@ -49,7 +49,7 @@ toc: true
: Leverage the embedded Markdown extensions to create tables, definition lists, footnotes, task lists, inserted text, mark text, subscripts, superscripts, and more.
[Markdown render hooks]
: Override the conversion of Markdown to HTML when rendering fenced code blocks, headings, images, and links. For example, render every standalone image as an HTML `figure` element.
: Override the conversion of Markdown to HTML when rendering blockquotes, fenced code blocks, headings, images, and links. For example, render every standalone image as an HTML `figure` element.
[Diagrams]
: Use fenced code blocks and Markdown render hooks to include diagrams in your content.
@@ -125,7 +125,7 @@ Set any [front matter field] in the map passed to the [`AddPage`](#addpage) meth
This table describes the fields most commonly passed to the `AddPage` method.
Key|Descripion|Required
Key|Description|Required
:--|:--|:-:
`content.mediaType`|The content [media type]. Default is `text/markdown`. See [content formats] for examples.|&nbsp;
`content.value`|The content value as a string.|&nbsp;
@@ -148,7 +148,7 @@ When setting the `path`, Hugo transforms the given string to a logical path. For
Construct the map passed to the [`AddResource`](#addresource) method using the fields below.
Key|Descripion|Required
Key|Description|Required
:--|:--|:-:
`content.mediaType`|The content [media type].|:heavy_check_mark:
`content.value`|The content value as a string or resource.|:heavy_check_mark:
@@ -21,15 +21,22 @@ This is the default language configuration:
In the above, `en` is the language key.
{{% note %}}
Each language key must conform to the syntax described in [RFC 5646]. You must use hyphens to separate subtags. For example:
Language keys must conform to the syntax described in [RFC 5646]. For example:
- `en`
- `en-GB`
- `pt-BR`
- `en-US`
Artificial languages with private use subtags as defined in [RFC 5646 § 2.2.7] are also supported. Omit the `art-x-` prefix from the language key. For example:
- `hugolang`
{{% note %}}
Private use subtags must not exceed 8 alphanumeric characters.
{{% /note %}}
[RFC 5646]: https://datatracker.ietf.org/doc/html/rfc5646#section-2.1
{{% /note %}}
[RFC 5646 § 2.2.7]: https://datatracker.ietf.org/doc/html/rfc5646#section-2.2.7
This is an example of a site configuration for a multilingual project. Any key not defined in a `languages` object will fall back to the global value in the root of your site configuration.
@@ -1,6 +1,6 @@
---
title: Page resources
description: Page resources -- images, other pages, documents, etc. -- have page-relative URLs and their own metadata.
description: Use page resources to logically associate assets with a page.
categories: [content management]
keywords: [bundle,content,resources]
menu:
@@ -37,82 +37,83 @@ content
└── index.md (root of page bundle)
```
## Properties
## Examples
ResourceType
: The main type of the resource's [Media Type](/templates/output-formats/#media-types). For example, a file of MIME type `image/jpeg` has the ResourceType `image`. A `Page` will have `ResourceType` with value `page`.
Use any of these methods on a `Page` object to capture page resources:
Name
: Default value is the file name (relative to the owning page). Can be set in front matter.
- [`Resources.ByType`]
- [`Resources.Get`]
- [`Resources.GetMatch`]
- [`Resources.Match`]
Title
: Default value is the same as `.Name`. Can be set in front matter.
Once you have captured a resource, use any of the applicable [`Resource`] methods to return a value or perform an action.
Permalink
: The absolute URL to the resource. Resources of type `page` will have no value.
[`Resource`]: /methods/resource
[`Resources.ByType`]: /methods/page/resources#bytype
[`Resources.GetMatch`]: /methods/page/resources#getmatch
[`Resources.Get`]: /methods/page/resources#get
[`Resources.Match`]: /methods/page/resources#match
RelPermalink
: The relative URL to the resource. Resources of type `page` will have no value.
The following examples assume this content structure:
Content
: The content of the resource itself. For most resources, this returns a string
with the contents of the file. Use this to create inline resources.
```text
content/
└── example/
├── data/
│ └── books.json <-- page resource
├── images/
│ ├── a.jpg <-- page resource
│ └── b.jpg <-- page resource
├── snippets/
│ └── text.md <-- page resource
└── index.md
```
Render a single image, and throw an error if the file does not exist:
```go-html-template
{{ with .Resources.GetMatch "script.js" }}
<script>{{ .Content | safeJS }}</script>
{{ end }}
{{ with .Resources.GetMatch "style.css" }}
<style>{{ .Content | safeCSS }}</style>
{{ end }}
{{ with .Resources.GetMatch "img.png" }}
<img src="data:{{ .MediaType.Type }};base64,{{ .Content | base64Encode }}">
{{ $path := "images/a.jpg" }}
{{ with .Resources.Get $path }}
<img src="{{ .RelPermalink }}" width="{{ .Width }}" height="{{ .Height }}" alt="">
{{ else }}
{{ errorf "Unable to get page resource %q" $path }}
{{ end }}
```
MediaType.Type
: The media type (formerly known as a MIME type) of the resource (e.g., `image/jpeg`).
MediaType.MainType
: The main type of the resource's media type (e.g., `image`).
MediaType.SubType
: The subtype of the resource's type (e.g., `jpeg`). This may or may not correspond to the file suffix.
MediaType.Suffixes
: A slice of possible file suffixes for the resource's media type (e.g., `[jpg jpeg jpe jif jfif]`).
## Methods
ByType
: Returns the page resources of the given type.
Render all images, resized to 300 px wide:
```go-html-template
{{ .Resources.ByType "image" }}
{{ range .Resources.ByType "image" }}
{{ with .Resize "300x" }}
<img src="{{ .RelPermalink }}" width="{{ .Width }}" height="{{ .Height }}" alt="">
{{ end }}
{{ end }}
```
Match
: Returns all the page resources (as a slice) whose `Name` matches the given Glob pattern ([examples](https://github.com/gobwas/glob/blob/master/readme.md)). The matching is case-insensitive.
Render the markdown snippet:
```go-html-template
{{ .Resources.Match "images/*" }}
{{ with .Resources.Get "snippets/text.md" }}
{{ .Content }}
{{ end }}
```
GetMatch
: Same as `Match` but will return the first match.
List the titles in the data file, and throw an error if the file does not exist.
### Pattern matching
```go
// Using Match/GetMatch to find this images/sunset.jpg ?
.Resources.Match "images/sun*" ✅
.Resources.Match "**/sunset.jpg" ✅
.Resources.Match "images/*.jpg" ✅
.Resources.Match "**.jpg" ✅
.Resources.Match "*" 🚫
.Resources.Match "sunset.jpg" 🚫
.Resources.Match "*sunset.jpg" 🚫
```go-html-template
{{ $path := "data/books.json" }}
{{ with .Resources.Get $path }}
{{ with . | transform.Unmarshal }}
<p>Books:</p>
<ul>
{{ range . }}
<li>{{ .title }}</li>
{{ end }}
</ul>
{{ end }}
{{ else }}
{{ errorf "Unable to get page resource %q" $path }}
{{ end }}
```
## Metadata
@@ -124,21 +125,21 @@ Resources of type `page` get `Title` etc. from their own front matter.
{{% /note %}}
name
: Sets the value returned in `Name`.
: (`string`) Sets the value returned in `Name`.
{{% note %}}
The methods `Match`, `Get` and `GetMatch` use `Name` to match the resources.
{{% /note %}}
title
: Sets the value returned in `Title`
: (`string`) Sets the value returned in `Title`
params
: A map of custom key-value pairs.
: (`map`) A map of custom key-value pairs.
### Resources metadata example
{{< code-toggle >}}
{{< code-toggle file=content/example.md fm=true >}}
title: Application
date : 2018-01-25
resources :
@@ -173,7 +174,7 @@ From the example above:
- Every docx in the bundle will receive the `word` icon.
{{% note %}}
The __order matters__ --- Only the **first set** values of the `title`, `name` and `params`-**keys** will be used. Consecutive parameters will be set only for the ones not already set. In the above example, `.Params.icon` is first set to `"photo"` in `src = "documents/photo_specs.pdf"`. So that would not get overridden to `"pdf"` by the later set `src = "**.pdf"` rule.
The order matters; only the first set values of the `title`, `name` and `params` keys will be used. Consecutive parameters will be set only for the ones not already set. In the above example, `.Params.icon` is first set to `"photo"` in `src = "documents/photo_specs.pdf"`. So that would not get overridden to `"pdf"` by the later set `src = "**.pdf"` rule.
{{% /note %}}
### The `:counter` placeholder in `name` and `title`
+1 -1
View File
@@ -48,7 +48,7 @@ For a complete guide to contributing to Hugo, see the [Contribution Guide].
To build the extended edition of Hugo from source you must:
1. Install [Git]
1. Install [Go] version 1.20 or later
1. Install [Go] version 1.23.0 or later
1. Install a C compiler, either [GCC] or [Clang]
1. Update your `PATH` environment variable as described in the [Go documentation]
@@ -2,4 +2,6 @@
# Do not remove front matter.
---
In Go templates, the falsy values are `false`, `0`, any nil pointer or interface value, and any array, slice, map, or string of length zero. Everything else is truthy.
The falsy values are `false`, `0`, any `nil` pointer or interface value, any array, slice, map, or string of length zero, and zero `time.Time` values.
Everything else is truthy.
+1 -1
View File
@@ -34,7 +34,7 @@ Use with the [`else`] statement:
{{ end }}
```
Use `else if` to check multiple conditions.
Use `else if` to check multiple conditions:
```go-html-template
{{ $var := 12 }}
@@ -36,6 +36,20 @@ Use with the [`else`] statement:
{{ end }}
```
Use `else with` to check multiple conditions:
```go-html-template
{{ $v1 := 0 }}
{{ $v2 := 42 }}
{{ with $v1 }}
{{ . }}
{{ else with $v2 }}
{{ . }} → 42
{{ else }}
{{ print "v1 and v2 are falsy" }}
{{ end }}
```
Initialize a variable, scoped to the current block:
```go-html-template
@@ -0,0 +1,112 @@
---
title: transform.ToMath
description: Renders a math expression using KaTeX.
categories: []
keywords: []
action:
aliases: []
related:
- content-management/mathematics
returnType: types.Result[template.HTML]
signatures: ['transform.ToMath EXPRESSION [OPTIONS]']
aliases: [/functions/tomath]
toc: true
---
{{< new-in "0.132.0" >}}
{{% note %}}
This feature was introduced in Hugo 0.132.0 and is marked as experimental.
This does not mean that it's going to be removed, but this is our first use of WASI/Wasm in Hugo, and we need to see how it [works in the wild](https://github.com/gohugoio/hugo/issues/12736) before we can set it in stone.
{{% /note %}}
## Arguments
EXPRESSION
: The math expression to render using KaTeX.
OPTIONS
: A map of zero or more options.
## Options
These are a subset of the [KaTeX options].
output
: (`string`). Determines the markup language of the output. One of `html`, `mathml`, or `htmlAndMathml`. Default is `mathml`.
<!-- Indent to prevent spliting the description list. -->
With `html` and `htmlAndMathml` you must include KaTeX CSS within the `head` element of your base template. For example:
```html
<head>
...
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css" integrity="sha384-nB0miv6/jRmo5UMMR1wu3Gz6NLsoTkbqJghGIsx//Rlm+ZU03BU6SQNC66uf4l5+" crossorigin="anonymous">
...
</head>
```
displayMode
: (`bool`) If `true` render in display mode, else render in inline mode. Default is `false`.
leqno
: (`bool`) If `true` render with the equation numbers on the left. Default is `false`.
fleqn
: (`bool`) If `true` render flush left with a 2em left margin. Default is `false`.
minRuleThickness
: (`float`) The minimum thickness of the fraction lines in `em`. Default is `0.04`.
macros
: (`map`) A map of macros to be used in the math expression. Default is `{}`.
throwOnError
: (`bool`) If `true` throw a `ParseError` when KaTeX encounters an unsupported command or invalid LaTex. See [error handling]. Default is `true`.
errorColor
: (`string`) The color of the error messages expressed as an RGB [hexadecimal color]. Default is `#cc0000`.
## Examples
### Basic
```go-html-template
{{ transform.ToMath "c = \\pm\\sqrt{a^2 + b^2}" }}
```
### Macros
```go-html-template
{{ $macros := dict
"\\addBar" "\\bar{#1}"
"\\bold" "\\mathbf{#1}"
}}
{{ $opts := dict "macros" $macros }}
{{ transform.ToMath "\\addBar{y} + \\bold{H}" $opts }}
```
## Error handling
There are 3 ways to handle errors from KaTeX:
1. Let KaTeX throw an error and make the build fail. This is the default behavior.
1. Handle the error in your template. See the render hook example below.
1. Set the `throwOnError` option to `false` to make KaTeX render the expression as an error instead of throwing an error. See [options].
{{< code file=layouts/_default/_markup/render-passthrough-inline.html copy=true >}}
{{ with transform.ToMath .Inner }}
{{ with .Err }}
{{ errorf "Failed to render KaTeX: %q. See %s" . $.Position }}
{{ else }}
{{ . }}
{{ end }}
{{ end }}
{{- /* chomp trailing newline */ -}}
{{< /code >}}
[error handling]: #error-handling
[KaTeX options]: https://katex.org/docs/options.html
[hexadecimal color]: https://developer.mozilla.org/en-US/docs/Web/CSS/hex-color
@@ -238,7 +238,7 @@ This is the default configuration for the AsciiDoc renderer:
###### attributes
(`map`) A map of key-value pairs, each a document attributes,See Asciidoctors [attributes].
(`map`) A map of key-value pairs, each a document attributes. See Asciidoctors [attributes].
[attributes]: https://asciidoctor.org/docs/asciidoc-syntax-quick-reference/#attributes-and-substitutions
@@ -302,6 +302,51 @@ To mitigate security risks, entries in the extension array may not contain forwa
my-attribute-name = "my value"
{{< /code-toggle >}}
### AsciiDoc syntax highlighting
Follow the steps below to enable syntax highlighting.
Step 1
: Set the `source-highlighter` attribute in your site configuration. For example:
{{< code-toggle file=hugo >}}
[markup.asciidocExt.attributes]
source-highlighter = 'rouge'
{{< /code-toggle >}}
Step 2
: Generate the highlighter CSS. For example:
```text
rougify style monokai.sublime > assets/css/syntax.css
```
Step 3
: In your base template add a link to the CSS file:
{{< code file=layouts/_default/baseof.html >}}
<head>
...
{{ with resources.Get "css/syntax.css" }}
<link rel="stylesheet" href="{{ .RelPermalink }}" integrity="{{ .Data.Integrity }}" crossorigin="anonymous">
{{ end }}
...
</head>
{{< /code >}}
Then add the code to be highlighted to your markup:
```text
[#hello,ruby]
----
require 'sinatra'
get '/hi' do
"Hello World!"
end
----
```
### AsciiDoc troubleshooting
Run `hugo --logLevel debug` to examine Hugo's call to the Asciidoctor executable:
@@ -378,6 +378,10 @@ Module configuration see [module configuration](/hugo-modules/configuration/).
See [custom output formats].
###### page
See [configure page](#configure-page).
###### pagination
See [configure pagination](/templates/pagination/#configuration).
@@ -496,6 +500,50 @@ enableemoji: true
```
{{% /note %}}
## Configure page
{{< new-in 0.133.0 >}}
These methods on a `Page` object navigate to the next or previous page within a page collection, relative to the current page:
- [Next](/methods/page/next/)
- [NextInSection](/methods/page/nextinsection/)
- [Prev](/methods/page/prev/)
- [PrevInSection](/methods/page/previnsection/)
Hugo determines the _next_ and _previous_ page by sorting a page collection according to this sorting hierarchy:
Field|Precedence|Sort direction
:--|:--|:--
[`weight`]|1|descending
[`date`]|2|descending
[`linkTitle`]|3|descending
[`path`]|4|descending
[`date`]: /methods/page/date/
[`weight`]: /methods/page/weight/
[`linkTitle`]: /methods/page/linktitle/
[`path`]: /methods/page/path/
The sort direction in the table above corresponds to these default site configuration values:
{{< code-toggle config=page />}}
To sort all fields in ascending order:
{{< code-toggle file=hugo >}}
[page]
nextPrevInSectionSortOrder = 'asc'
nextPrevSortOrder = 'asc'
{{< /code-toggle >}}
{{% note %}}
These settings do not apply to the [`Next`] or [`Prev`] methods on a `Pages` object.
[`Next`]: /methods/pages/next
[`Prev`]: /methods/pages/next
{{% /note %}}
## Configure build
The `build` configuration section contains global build-related configuration options.
@@ -609,8 +657,6 @@ Setting `force=true` will make a redirect even if there is existing content in t
## 404 server error page {#_404-server-error-page}
{{< new-in 0.103.0 >}}
Hugo will, by default, render all 404 errors when running `hugo server` with the `404.html` template. Note that if you have already added one or more redirects to your [server configuration](#configure-server), you need to add the 404 redirect explicitly, e.g:
{{< code-toggle file=config/development/server >}}
@@ -924,7 +970,6 @@ output
It is recommended to put coarse grained filters (e.g. for language and output format) in the excludes section, e.g.:
{{< code-toggle file=hugo >}}
[segments.segment1]
[[segments.segment1.excludes]]
-13
View File
@@ -1,13 +0,0 @@
---
cascade:
_build:
list: never
publishResources: false
render: never
---
<!--
Files within this headless branch bundle are Markdown snippets. Each file must contain front matter delimiters, though front matter fields are not required.
Include the rendered content using the "include" shortcode.
-->
@@ -1,37 +0,0 @@
---
# Do not remove front matter.
---
The `Next` and `Prev` methods on a `Pages` object are more flexible than the `Next` and `Prev` methods on a `Page` object.
||Page collection|Custom sort order
:--|:--|:-:
[`PAGES.Next`] and [`PAGES.Prev`]|locally defined|✔️
[`PAGE.Next`] and [`PAGE.Prev`]|globally defined|❌
[`PAGES.Next`]: /methods/pages/next/
[`PAGES.Prev`]: /methods/pages/prev/
[`PAGE.Next`]: /methods/page/next/
[`PAGE.Prev`]: /methods/page/prev/
locally defined
: Build the page collection every time you call `PAGES.Next` and `PAGES.Prev`. Navigation between pages is relative to the current page's position within the local collection, independent of the global collection.
With a local collection, the navigation sort order is the same as the collection sort order.
globally defined
: Build the page collection once, on a list page. Navigation between pages is relative to the current page's position within the global collection.
With a global collection, the navigation sort order is fixed, using Hugo's default sort order. In order of precedence:
1. Page [weight]
2. Page [date] (descending)
3. Page [linkTitle], falling back to page [title]
4. Page file path if the page is backed by a file
For example, with a global collection sorted by title, the navigation sort order will use Hugo's default sort order. This is probably not what you want or expect. For this reason, the `Next` and `Prev` methods on a `Pages` object are generally a better choice.
[date]: /methods/page/date/
[weight]: /methods/page/weight/
[linkTitle]: /methods/page/linktitle/
[title]: /methods/page/title/
+2 -38
View File
@@ -1,6 +1,6 @@
---
title: Next
description: Returns the next page in a global page collection, relative to the given page.
description: Returns the next page in a site's collection of regular pages, relative to the current page.
categories: []
keywords: []
action:
@@ -12,42 +12,6 @@ action:
- methods/pages/Prev
returnType: page.Page
signatures: [PAGE.Next]
toc: true
---
The behavior of the `Prev` and `Next` methods on a `Page` object is probably the reverse of what you expect.
With this content structure:
```text
content/
├── pages/
│ ├── _index.md
│ ├── page-1.md <-- front matter: weight = 10
│ ├── page-2.md <-- front matter: weight = 20
│ └── page-3.md <-- front matter: weight = 30
└── _index.md
```
When you visit page-2:
- The `Prev` method points to page-3
- The `Next` method points to page-1
{{% note %}}
Use the opposite label in your navigation links as shown in the example below.
{{% /note %}}
```go-html-template
{{ with .Next }}
<a href="{{ .RelPermalink }}">Prev</a>
{{ end }}
{{ with .Prev }}
<a href="{{ .RelPermalink }}">Next</a>
{{ end }}
```
## Compare to Pages methods
{{% include "methods/_common/next-prev-on-page-vs-next-prev-on-pages.md" %}}
{{% include "methods/page/_common/next-and-prev.md" %}}
+2 -58
View File
@@ -1,71 +1,15 @@
---
title: NextInSection
description: Returns the next page within a section, relative to the given page.
description: Returns the next regular page in a section, relative to the given page.
categories: []
keywords: []
action:
related:
- methods/page/PrevInSection
- methods/page/Next
- methods/page/Prev
- methods/pages/Next
- methods/pages/Prev
returnType: page.Page
signatures: [PAGE.NextInSection]
---
The behavior of the `PrevInSection` and `NextInSection` methods on a `Page` object is probably the reverse of what you expect.
With this content structure:
```text
content/
├── books/
│ ├── _index.md
│ ├── book-1.md
│ ├── book-2.md
│ └── book-3.md
├── films/
│ ├── _index.md
│ ├── film-1.md
│ ├── film-2.md
│ └── film-3.md
└── _index.md
```
When you visit book-2:
- The `PrevInSection` method points to book-3
- The `NextInSection` method points to book-1
{{% note %}}
Use the opposite label in your navigation links as shown in the example below.
{{% /note %}}
```go-html-template
{{ with .NextInSection }}
<a href="{{ .RelPermalink }}">Previous in section</a>
{{ end }}
{{ with .PrevInSection }}
<a href="{{ .RelPermalink }}">Next in section</a>
{{ end }}
```
{{% note %}}
The navigation sort order may be different than the page collection sort order.
{{% /note %}}
With the `PrevInSection` and `NextInSection` methods, the navigation sort order is fixed, using Hugos default sort order. In order of precedence:
1. Page [weight]
2. Page [date] (descending)
3. Page [linkTitle], falling back to page [title]
4. Page file path if the page is backed by a file
For example, with a page collection sorted by title, the navigation sort order will use Hugos default sort order. This is probably not what you want or expect. For this reason, the Next and Prev methods on a Pages object are generally a better choice.
[date]: /methods/page/date/
[weight]: /methods/page/weight/
[linkTitle]: /methods/page/linktitle/
[title]: /methods/page/title/
{{% include "methods/page/_common/nextinsection-and-previnsection.md" %}}
+4 -40
View File
@@ -1,53 +1,17 @@
---
title: Prev
description: Returns the previous page in a global page collection, relative to the given page.
description: Returns the previous page in a site's collection of regular pages, relative to the current page.
categories: []
keywords: []
action:
related:
- methods/page/Next
- methods/page/PrevInSection
- methods/page/NextInSection
- methods/pages/Prev
- methods/page/PrevInSection
- methods/pages/Next
- methods/pages/Prev
returnType: page.Page
signatures: [PAGE.Prev]
toc: true
---
The behavior of the `Prev` and `Next` methods on a `Page` object is probably the reverse of what you expect.
With this content structure:
```text
content/
├── pages/
│ ├── _index.md
│ ├── page-1.md <-- front matter: weight = 10
│ ├── page-2.md <-- front matter: weight = 20
│ └── page-3.md <-- front matter: weight = 30
└── _index.md
```
When you visit page-2:
- The `Prev` method points to page-3
- The `Next` method points to page-1
{{% note %}}
Use the opposite label in your navigation links as shown in the example below.
{{% /note %}}
```go-html-template
{{ with .Next }}
<a href="{{ .RelPermalink }}">Prev</a>
{{ end }}
{{ with .Prev }}
<a href="{{ .RelPermalink }}">Next</a>
{{ end }}
```
## Compare to Pages methods
{{% include "methods/_common/next-prev-on-page-vs-next-prev-on-pages.md" %}}
{{% include "methods/page/_common/next-and-prev.md" %}}
+2 -59
View File
@@ -1,72 +1,15 @@
---
title: PrevInSection
description: Returns the previous page within a section, relative to the given page.
description: Returns the previous regular page in a section, relative to the given page.
categories: []
keywords: []
action:
related:
- methods/page/NextInSection
- methods/page/Next
- methods/pages/Next
- methods/page/Prev
- methods/pages/Prev
returnType: page.Page
signatures: [PAGE.PrevInSection]
---
The behavior of the `PrevInSection` and `NextInSection` methods on a `Page` object is probably the reverse of what you expect.
With this content structure:
```text
content/
├── books/
│ ├── _index.md
│ ├── book-1.md
│ ├── book-2.md
│ └── book-3.md
├── films/
│ ├── _index.md
│ ├── film-1.md
│ ├── film-2.md
│ └── film-3.md
└── _index.md
```
When you visit book-2:
- The `PrevInSection` method points to book-3
- The `NextInSection` method points to book-1
{{% note %}}
Use the opposite label in your navigation links as shown in the example below.
{{% /note %}}
```go-html-template
{{ with .NextInSection }}
<a href="{{ .RelPermalink }}">Previous in section</a>
{{ end }}
{{ with .PrevInSection }}
<a href="{{ .RelPermalink }}">Next in section</a>
{{ end }}
```
{{% note %}}
The navigation sort order may be different than the page collection sort order.
{{% /note %}}
With the `PrevInSection` and `NextInSection` methods, the navigation sort order is fixed, using Hugos default sort order. In order of precedence:
1. Page [weight]
2. Page [date] (descending)
3. Page [linkTitle], falling back to page [title]
4. Page file path if the page is backed by a file
For example, with a page collection sorted by title, the navigation sort order will use Hugos default sort order. This is probably not what you want or expect. For this reason, the Next and Prev methods on a Pages object are generally a better choice.
[date]: /methods/page/date/
[weight]: /methods/page/weight/
[linkTitle]: /methods/page/linktitle/
[title]: /methods/page/title/
{{% include "methods/page/_common/nextinsection-and-previnsection.md" %}}
@@ -0,0 +1,60 @@
---
# Do not remove front matter.
---
Hugo determines the _next_ and _previous_ page by sorting the site's collection of regular pages according to this sorting hierarchy:
Field|Precedence|Sort direction
:--|:--|:--
[`weight`]|1|descending
[`date`]|2|descending
[`linkTitle`]|3|descending
[`path`]|4|descending
[`date`]: /methods/page/date/
[`weight`]: /methods/page/weight/
[`linkTitle`]: /methods/page/linktitle/
[`path`]: /methods/page/path/
The sorted page collection used to determine the _next_ and _previous_ page is independent of other page collections, which may lead to unexpected behavior.
For example, with this content structure:
```text
content/
├── pages/
│ ├── _index.md
│ ├── page-1.md <-- front matter: weight = 10
│ ├── page-2.md <-- front matter: weight = 20
│ └── page-3.md <-- front matter: weight = 30
└── _index.md
```
And these templates:
{{< code file=layouts/_default/list.html >}}
{{ range .Pages.ByWeight }}
<h2><a href="{{ .RelPermalink }}">{{ .LinkTitle }}</a></h2>
{{ end }}
{{< /code >}}
{{< code file=layouts/_default/single.html >}}
{{ with .Prev }}
<a href="{{ .RelPermalink }}">Previous</a>
{{ end }}
{{ with .Next }}
<a href="{{ .RelPermalink }}">Next</a>
{{ end }}
{{< /code >}}
When you visit page-2:
- The `Prev` method points to page-3
- The `Next` method points to page-1
To reverse the meaning of _next_ and _previous_ you can change the sort direction in your [site configuration], or use the [`Next`] and [`Prev`] methods on a `Pages` object for more flexibility.
[site configuration]: getting-started/configuration/#configure-page
[`Next`]: /methods/pages/prev
[`Prev`]: /methods/pages/prev
@@ -0,0 +1,78 @@
---
# Do not remove front matter.
---
Hugo determines the _next_ and _previous_ page by sorting the current section's regular pages according to this sorting hierarchy:
Field|Precedence|Sort direction
:--|:--|:--
[`weight`]|1|descending
[`date`]|2|descending
[`linkTitle`]|3|descending
[`path`]|4|descending
[`date`]: /methods/page/date/
[`weight`]: /methods/page/weight/
[`linkTitle`]: /methods/page/linktitle/
[`path`]: /methods/page/path/
The sorted page collection used to determine the _next_ and _previous_ page is independent of other page collections, which may lead to unexpected behavior.
For example, with this content structure:
```text
content/
├── pages/
│ ├── _index.md
│ ├── page-1.md <-- front matter: weight = 10
│ ├── page-2.md <-- front matter: weight = 20
│ └── page-3.md <-- front matter: weight = 30
└── _index.md
```
And these templates:
{{< code file=layouts/_default/list.html >}}
{{ range .Pages.ByWeight }}
<h2><a href="{{ .RelPermalink }}">{{ .LinkTitle }}</a></h2>
{{ end }}
{{< /code >}}
{{< code file=layouts/_default/single.html >}}
{{ with .PrevInSection }}
<a href="{{ .RelPermalink }}">Previous</a>
{{ end }}
{{ with .NextInSection }}
<a href="{{ .RelPermalink }}">Next</a>
{{ end }}
{{< /code >}}
When you visit page-2:
- The `PrevInSection` method points to page-3
- The `NextInSection` method points to page-1
To reverse the meaning of _next_ and _previous_ you can change the sort direction in your [site configuration], or use the [`Next`] and [`Prev`] methods on a `Pages` object for more flexibility.
[site configuration]: getting-started/configuration/#configure-page
[`Next`]: /methods/pages/prev
[`Prev`]: /methods/pages/prev
## Example
Code defensively by checking for page existence:
```go-html-template
{{ with .PrevInSection }}
<a href="{{ .RelPermalink }}">Previous</a>
{{ end }}
{{ with .NextInSection }}
<a href="{{ .RelPermalink }}">Next</a>
{{ end }}
```
## Alternative
Use the [`Next`] and [`Prev`] methods on a `Pages` object for more flexibility.
+3 -41
View File
@@ -1,55 +1,17 @@
---
title: Next
description: Returns the next page in a local page collection, relative to the given page.
description: Returns the next page in a page collection, relative to the given page.
categories: []
keywords: []
action:
related:
- methods/pages/Prev
- methods/page/Next
- methods/page/NextInSection
- methods/page/Prev
- methods/page/NextInSection
- methods/page/PrevInSection
returnType: page.Page
signatures: [PAGES.Next PAGE]
toc: true
---
The behavior of the `Prev` and `Next` methods on a `Pages` objects is probably the reverse of what you expect.
With this content structure and the page collection sorted by weight in ascending order:
```text
content/
├── pages/
│ ├── _index.md
│ ├── page-1.md <-- front matter: weight = 10
│ ├── page-2.md <-- front matter: weight = 20
│ └── page-3.md <-- front matter: weight = 30
└── _index.md
```
When you visit page-2:
- The `Prev` method points to page-3
- The `Next` method points to page-1
{{% note %}}
Use the opposite label in your navigation links as shown in the example below.
{{% /note %}}
```go-html-template
{{ $pages := where .Site.RegularPages.ByWeight "Section" "pages" }}
{{ with $pages.Next . }}
<a href="{{ .RelPermalink }}">Previous</a>
{{ end }}
{{ with $pages.Prev . }}
<a href="{{ .RelPermalink }}">Next</a>
{{ end }}
```
## Compare to Page methods
{{% include "methods/_common/next-prev-on-page-vs-next-prev-on-pages.md" %}}
{{% include "methods/pages/_common/next-and-prev.md" %}}
+3 -41
View File
@@ -1,55 +1,17 @@
---
title: Prev
description: Returns the previous page in a local page collection, relative to the given page.
description: Returns the previous page in a page collection, relative to the given page.
categories: []
keywords: []
action:
related:
- methods/pages/Next
- methods/page/Next
- methods/page/NextInSection
- methods/page/Prev
- methods/page/NextInSection
- methods/page/PrevInSection
returnType: page.Pages
signatures: [PAGES.Prev PAGE]
toc: true
---
The behavior of the `Prev` and `Next` methods on a `Pages` objects is probably the reverse of what you expect.
With this content structure and the page collection sorted by weight in ascending order:
```text
content/
├── pages/
│ ├── _index.md
│ ├── page-1.md <-- front matter: weight = 10
│ ├── page-2.md <-- front matter: weight = 20
│ └── page-3.md <-- front matter: weight = 30
└── _index.md
```
When you visit page-2:
- The `Prev` method points to page-3
- The `Next` method points to page-1
{{% note %}}
Use the opposite label in your navigation links as shown in the example below.
{{% /note %}}
```go-html-template
{{ $pages := where .Site.RegularPages.ByWeight "Section" "pages" }}
{{ with $pages.Next . }}
<a href="{{ .RelPermalink }}">Previous</a>
{{ end }}
{{ with $pages.Prev . }}
<a href="{{ .RelPermalink }}">Next</a>
{{ end }}
```
## Compare to Page methods
{{% include "methods/_common/next-prev-on-page-vs-next-prev-on-pages.md" %}}
{{% include "methods/pages/_common/next-and-prev.md" %}}
@@ -0,0 +1,72 @@
---
# Do not remove front matter.
---
Hugo determines the _next_ and _previous_ page by sorting the page collection according to this sorting hierarchy:
Field|Precedence|Sort direction
:--|:--|:--
[`weight`]|1|descending
[`date`]|2|descending
[`linkTitle`]|3|descending
[`path`]|4|descending
[`date`]: /methods/page/date/
[`weight`]: /methods/page/weight/
[`linkTitle`]: /methods/page/linktitle/
[`path`]: /methods/page/path/
The sorted page collection used to determine the _next_ and _previous_ page is independent of other page collections, which may lead to unexpected behavior.
For example, with this content structure:
```text
content/
├── pages/
│ ├── _index.md
│ ├── page-1.md <-- front matter: weight = 10
│ ├── page-2.md <-- front matter: weight = 20
│ └── page-3.md <-- front matter: weight = 30
└── _index.md
```
And these templates:
{{< code file=layouts/_default/list.html >}}
{{ range .Pages.ByWeight}}
<h2><a href="{{ .RelPermalink }}">{{ .LinkTitle }}</a></h2>
{{ end }}
{{< /code >}}
{{< code file=layouts/_default/single.html >}}
{{ $pages := .CurrentSection.Pages.ByWeight }}
{{ with $pages.Prev . }}
<a href="{{ .RelPermalink }}">Previous</a>
{{ end }}
{{ with $pages.Next . }}
<a href="{{ .RelPermalink }}">Next</a>
{{ end }}
{{< /code >}}
When you visit page-2:
- The `Prev` method points to page-3
- The `Next` method points to page-1
To reverse the meaning of _next_ and _previous_ you can chain the [`Reverse`] method to the page collection definition:
{{< code file=layouts/_default/single.html >}}
{{ $pages := .CurrentSection.Pages.ByWeight.Reverse }}
{{ with $pages.Prev . }}
<a href="{{ .RelPermalink }}">Previous</a>
{{ end }}
{{ with $pages.Next . }}
<a href="{{ .RelPermalink }}">Next</a>
{{ end }}
{{< /code >}}
[`Reverse`]: /methods/pages/reverse/
+185
View File
@@ -0,0 +1,185 @@
---
title: Blockquote render hooks
linkTitle: Blockquotes
description: Create a blockquote render hook to override the rendering of Markdown blockquotes to HTML.
categories: [render hooks]
keywords: []
menu:
docs:
parent: render-hooks
weight: 30
weight: 30
toc: true
---
{{< new-in 0.132.0 >}}
## Context
Blockquote render hook templates receive the following [context]:
[context]: /getting-started/glossary/#context
###### AlertType
(`string`) Applicable when [`Type`](#type) is `alert`, this is the alert type converted to lowercase. See the [alerts](#alerts) section below.
###### AlertTitle
{{< new-in 0.134.0 >}}
(`hstring.HTML`) Applicable when [`Type`](#type) is `alert` when using [Obsidian callouts] syntax, this is the alert title converted to HTML.
###### AlertSign
{{< new-in 0.134.0 >}}
(`string`) Applicable when [`Type`](#type) is `alert` when using [Obsidian callouts] syntax, this is one of "+", "-" or "" (empty string) to indicate the presence of a foldable sign.
[Obsidian callouts]: https://help.obsidian.md/Editing+and+formatting/Callouts
###### Attributes
(`map`) The [Markdown attributes], available if you configure your site as follows:
[Markdown attributes]: /content-management/markdown-attributes/
{{< code-toggle file=hugo >}}
[markup.goldmark.parser.attribute]
block = true
{{< /code-toggle >}}
###### Ordinal
(`int`) The zero-based ordinal of the blockquote on the page.
###### Page
(`page`) A reference to the current page.
###### PageInner
(`page`) A reference to a page nested via the [`RenderShortcodes`] method.
[`RenderShortcodes`]: /methods/page/rendershortcodes
###### Position
(`string`) The position of the blockquote within the page content.
###### Text
(`string`) The blockquote text, excluding the alert designator if present. See the [alerts](#alerts) section below.
###### Type
(`bool`) The blockquote type. Returns `alert` if the blockquote has an alert designator, else `regular`. See the [alerts](#alerts) section below.
## Examples
In its default configuration, Hugo renders Markdown blockquotes according to the [CommonMark specification]. To create a render hook that does the same thing:
[CommonMark specification]: https://spec.commonmark.org/current/
{{< code file=layouts/_default/_markup/render-blockquote.html copy=true >}}
<blockquote>
{{ .Text | safeHTML }}
</blockquote>
{{< /code >}}
To render a blockquote as an HTML `figure` element with an optional citation and caption:
{{< code file=layouts/_default/_markup/render-blockquote.html copy=true >}}
<figure>
<blockquote {{ with .Attributes.cite }}cite="{{ . }}"{{ end }}>
{{ .Text | safeHTML }}
</blockquote>
{{ with .Attributes.caption }}
<figcaption class="blockquote-caption">
{{ . | safeHTML }}
</figcaption>
{{ end }}
</figure>
{{< /code >}}
Then in your markdown:
```text
> Some text
{cite="https://gohugo.io" caption="Some caption"}
```
## Alerts
Also known as _callouts_ or _admonitions_, alerts are blockquotes used to emphasize critical information. For example:
{{< code file=content/example.md lang=text >}}
> [!NOTE]
> Useful information that users should know, even when skimming content.
> [!TIP]
> Helpful advice for doing things better or more easily.
> [!IMPORTANT]
> Key information users need to know to achieve their goal.
> [!WARNING]
> Urgent info that needs immediate user attention to avoid problems.
> [!CAUTION]
> Advises about risks or negative outcomes of certain actions.
{{< /code >}}
{{% note %}}
This syntax is compatible with both the GitHub Alert Markdown extension and Obsidian's callout syntax.
But note that GitHub will not recognize callouts with one of Obsidian's extensions (e.g. callout title or the foldable sign).
{{% /note %}}
The first line of each alert is an alert designator consisting of an exclamation point followed by the alert type, wrapped within brackets.
The blockquote render hook below renders a multilingual alert if an alert designator is present, otherwise it renders a blockquote according to the CommonMark specification.
{{< code file=layouts/_default/_markup/render-blockquote.html copy=true >}}
{{ $emojis := dict
"caution" ":exclamation:"
"important" ":information_source:"
"note" ":information_source:"
"tip" ":bulb:"
"warning" ":information_source:"
}}
{{ if eq .Type "alert" }}
<blockquote class="alert alert-{{ .AlertType }}">
<p class="alert-heading">
{{ transform.Emojify (index $emojis .AlertType) }}
{{ or (i18n .AlertType) (title .AlertType) }}
</p>
{{ .Text | safeHTML }}
</blockquote>
{{ else }}
<blockquote>
{{ .Text | safeHTML }}
</blockquote>
{{ end }}
{{< /code >}}
To override the label, create these entries in your i18n files:
{{< code-toggle file=i18n/en.toml >}}
caution = 'Caution'
important = 'Important'
note = 'Note'
tip = 'Tip'
warning = 'Warning'
{{< /code-toggle >}}
Although you can use one template with conditional logic as shown above, you can also create separate templates for each [`Type`](#type) of blockquote:
```text
layouts/
└── _default/
└── _markup/
├── render-blockquote-alert.html
└── render-blockquote-regular.html
```
+2 -2
View File
@@ -7,8 +7,8 @@ keywords: []
menu:
docs:
parent: render-hooks
weight: 30
weight: 30
weight: 40
weight: 40
toc: true
---
+5 -3
View File
@@ -7,8 +7,8 @@ keywords: []
menu:
docs:
parent: render-hooks
weight: 40
weight: 40
weight: 50
weight: 50
toc: true
---
@@ -24,7 +24,9 @@ Heading render hook templates receive the following [context]:
###### Attributes
(`map`) The Markdown attributes, available if you configure your site as follows:
(`map`) The [Markdown attributes], available if you configure your site as follows:
[Markdown attributes]: /content-management/markdown-attributes/
{{< code-toggle file=hugo >}}
[markup.goldmark.parser.attribute]
+5 -3
View File
@@ -7,8 +7,8 @@ keywords: []
menu:
docs:
parent: render-hooks
weight: 50
weight: 50
weight: 60
weight: 60
toc: true
---
@@ -32,7 +32,9 @@ Image render hook templates receive the following context:
###### Attributes
(`map`) The Markdown attributes, available if you configure your site as follows:
(`map`) The [Markdown attributes], available if you configure your site as follows:
[Markdown attributes]: /content-management/markdown-attributes/
{{< code-toggle file=hugo >}}
[markup.goldmark.parser]
@@ -13,10 +13,12 @@ weight: 20
When rendering Markdown to HTML, render hooks override the conversion. Each render hook is a template, with one template for each supported element type:
- [Blockquotes](/render-hooks/blockquotes)
- [Code blocks](/render-hooks/code-blocks)
- [Headings](/render-hooks/headings)
- [Images](/render-hooks/images)
- [Links](/render-hooks/links)
- [Passthrough elements](/render-hooks/passthrough)
{{% note %}}
Hugo supports multiple [content formats] including Markdown, HTML, AsciiDoc, Emacs Org Mode, Pandoc, and reStructuredText.
@@ -54,6 +56,7 @@ Each render hook is a template, with one template for each supported element typ
layouts/
└── _default/
└── _markup/
├── render-blockquote.html
├── render-codeblock.html
├── render-heading.html
├── render-image.html
+2 -2
View File
@@ -7,8 +7,8 @@ keywords: []
menu:
docs:
parent: render-hooks
weight: 60
weight: 60
weight: 70
weight: 70
toc: true
---
+125
View File
@@ -0,0 +1,125 @@
---
title: Passthrough render hooks
linkTitle: Passthrough
description: Create a passthrough render hook to override the rendering of text snippets captured by the Goldmark passthrough extension.
categories: [render hooks]
keywords: []
menu:
docs:
parent: render-hooks
weight: 80
weight: 80
toc: true
---
{{< new-in 0.132.0 >}}
## Overview
Hugo uses [Goldmark] to render Markdown to HTML. Goldmark supports custom extensions to extend its core functionality. The Goldmark [passthrough extension] captures and preserves raw Markdown within delimited snippets of text, including the delimiters themselves. These are known as _passthrough elements_.
[Goldmark]: https://github.com/yuin/goldmark
[passthrough extension]: /getting-started/configuration-markup/#passthrough
Depending on your choice of delimiters, Hugo will classify a passthrough element as either _block_ or _inline_. Consider this contrived example:
{{< code file=content/sample.md >}}
This is a
\[block\]
passthrough element with opening and closing block delimiters.
This is an \(inline\) passthrough element with opening and closing inline delimiters.
{{< /code >}}
Update your site configuration to enable the passthrough extension and define opening and closing delimiters for each passthrough element type, either `block` or `inline`. For example:
{{< code-toggle file=hugo >}}
[markup.goldmark.extensions.passthrough]
enable = true
[markup.goldmark.extensions.passthrough.delimiters]
block = [['\[', '\]'], ['$$', '$$']]
inline = [['\(', '\)']]
{{< /code-toggle >}}
In the example above there are two sets of `block` delimiters. You may use either one in your Markdown.
The Goldmark passthrough extension is often used in conjunction with the MathJax or KaTeX display engine to render [mathematical expressions] written in [LaTeX] or [Tex].
[mathematical expressions]: /content-management/mathematics/
[LaTeX]: https://www.latex-project.org/
[Tex]: https://en.wikipedia.org/wiki/TeX
To enable custom rendering of passthrough elements, create a render hook.
## Context
Passthrough render hook templates receive the following [context]:
[context]: /getting-started/glossary/#context
###### Attributes
(`map`) The [Markdown attributes], available if you configure your site as follows:
[Markdown attributes]: /content-management/markdown-attributes/
{{< code-toggle file=hugo >}}
[markup.goldmark.parser.attribute]
block = true
{{< /code-toggle >}}
Hugo populates the `Attributes` map for _block_ passthrough elements. Markdown attributes are not applicable to _inline_ elements.
###### Inner
(`string`) The inner content of the passthrough element, excluding the delimiters.
###### Ordinal
(`int`) The zero-based ordinal of the passthrough element on the page.
###### Page
(`page`) A reference to the current page.
###### PageInner
(`page`) A reference to a page nested via the [`RenderShortcodes`] method. [See details](#pageinner-details).
[`RenderShortcodes`]: /methods/page/rendershortcodes
###### Position
(`string`) The position of the passthrough element within the page content.
###### Type
(`bool`) The passthrough element type, either `block` or `inline`.
## Example
As an alternative to rendering mathematical expressions with the MathJax or KaTeX display engine, create a passthrough render hook which calls the [`transform.ToMath`] function:
[`transform.ToMath`]: /functions/transform/tomath/
{{< code file=layouts/_default/_markup/render-passthrough.html copy=true >}}
{{ if eq .Type "block" }}
{{ $opts := dict "displayMode" true }}
{{ transform.ToMath .Inner $opts }}
{{ else }}
{{ transform.ToMath .Inner }}
{{ end }}
{{< /code >}}
Although you can use one template with conditional logic as shown above, you can also create separate templates for each [`Type`](#type) of passthrough element:
```text
layouts/
└── _default/
└── _markup/
├── render-passthrough-block.html
└── render-passthrough-inline.html
```
{{% include "/render-hooks/_common/pageinner.md" %}}
+2 -2
View File
@@ -135,7 +135,7 @@ tags = []
Hugo uses the page title and description for the title and description metadata.
The first 6 URLs from the `images` array are used for image metadata.
If [page bundles](/content-management/page-bundles/) are used and the `images` array is empty or undefined, images with file names matching `*feature*` or `*cover*,*thumbnail*` are used for image metadata.
If [page bundles](/content-management/page-bundles/) are used and the `images` array is empty or undefined, images with file names matching `*feature*`, `*cover*`, or `*thumbnail*` are used for image metadata.
Various optional metadata can also be set:
@@ -203,7 +203,7 @@ description = "Text about this post"
images = ["post-cover.png"]
{{</ code-toggle >}}
If `images` aren't specified in the page front-matter, then hugo searches for [image page resources](/content-management/image-processing/) with `feature`, `cover`, or `thumbnail` in their name.
If [page bundles](/content-management/page-bundles/) are used and the `images` array is empty or undefined, images with file names matching `*feature*`, `*cover*`, or `*thumbnail*` are used for image metadata.
If no image resources with those names are found, the images defined in the [site config](/getting-started/configuration/) are used instead.
If no images are found at all, then an image-less Twitter `summary` card is used instead of `summary_large_image`.
+14
View File
@@ -509,6 +509,20 @@ See documentation for [`with`], [`else`], and [`end`].
{{ end }}
```
To test multiple conditions:
```go-html-template
{{ $v1 := 0 }}
{{ $v2 := 42 }}
{{ with $v1 }}
{{ . }}
{{ else with $v2 }}
{{ . }} → 42
{{ else }}
{{ print "v1 and v2 are falsy" }}
{{ end }}
```
### Access site parameters
See documentation for the [`Params`](/methods/site/params/) method on a `Site` object.
+1 -1
View File
@@ -129,5 +129,5 @@ Hugo provides two methods to localize your menu entries. See [multilingual].
[localize the menu entries]: /content-management/multilingual/#menus
[menu entry defined in front matter]: /content-management/menus/#example-front-matter
[menu entry defined in site configuration]: /content-management/menus/#example-site-configuration
[menu and methods]: /methods/menu/
[menu methods]: /methods/menu/
[multilingual]: /content-management/multilingual/#menus
+21 -4
View File
@@ -37,12 +37,29 @@ You can organize your shortcodes in subdirectories, e.g. in `layouts/shortcodes/
Note the forward slash.
### Shortcode template lookup order
### Template lookup order
Shortcode templates have a simple [lookup order]:
Hugo selects shortcode templates based on the shortcode name, the current output format, and the current language. The examples below are sorted by specificity in descending order. The least specific path is at the bottom of the list.
1. `/layouts/shortcodes/<SHORTCODE>.html`
2. `/themes/<THEME>/layouts/shortcodes/<SHORTCODE>.html`
Shortcode name|Output format|Language|Template path
:--|:--|:--|:--
foo|html|en|layouts/shortcodes/foo.en.html
foo|html|en|layouts/shortcodes/foo.html.html
foo|html|en|layouts/shortcodes/foo.html
foo|html|en|layouts/shortcodes/foo.html.en.html
Shortcode name|Output format|Language|Template path
:--|:--|:--|:--
foo|rss|en|layouts/shortcodes/foo.en.xml
foo|rss|en|layouts/shortcodes/foo.rss.xml
foo|rss|en|layouts/shortcodes/foo.en.html
foo|rss|en|layouts/shortcodes/foo.rss.en.xml
foo|rss|en|layouts/shortcodes/foo.xml
foo|rss|en|layouts/shortcodes/foo.html.en.html
foo|rss|en|layouts/shortcodes/foo.html.html
foo|rss|en|layouts/shortcodes/foo.html
Note that templates provided by a theme or module always take precedence.
### Positional vs. named arguments
+7 -7
View File
@@ -13,9 +13,9 @@ toc: true
aliases: [/developer-tools/migrations/, /developer-tools/migrated/]
---
This section highlights some projects around Hugo that are independently developed. These tools try to extend the functionality of our static site generator or help you to get started.
This section highlights some independently developed projects related to Hugo. These tools extend functionality or help you to get started.
Take a look at this list of migration tools if you currently use other blogging tools like Jekyll or WordPress but intend to switch to Hugo instead. They'll take care to export your content into Hugo-friendly formats.
Take a look at this list of migration tools if you currently use other blogging tools like Jekyll or WordPress but intend to switch to Hugo instead. They'll help you export your content into Hugo-friendly formats.
## Jekyll
@@ -35,18 +35,18 @@ Alternatively, you can use the [Jekyll import command](/commands/hugo_import_jek
## DokuWiki
[dokuwiki-to-hugo](https://github.com/wgroeneveld/dokuwiki-to-hugo)
: Migrates your DokuWiki source pages from [DokuWiki syntax](https://www.dokuwiki.org/wiki:syntax) to Hugo Markdown syntax. Includes extra's like the TODO plugin. Written with extensibility in mind using python 3. Also generates a TOML header for each page. Designed to copypaste the wiki directory into your /content directory.
: Migrates your DokuWiki source pages from [DokuWiki syntax](https://www.dokuwiki.org/wiki:syntax) to Hugo Markdown syntax. Includes extras like the TODO plugin. Written with extensibility in mind using Python 3. Also generates a TOML header for each page. Designed to copy-paste the wiki directory into your /content directory.
## WordPress
[wordpress-to-hugo-exporter](https://github.com/SchumacherFM/wordpress-to-hugo-exporter)
: A one-click WordPress plugin that converts all posts, pages, taxonomies, metadata, and settings to Markdown and YAML which can be dropped into Hugo. (Note: If you have trouble using this plugin, you can [export your site for Jekyll](https://wordpress.org/plugins/jekyll-exporter/) and use Hugo's built in Jekyll converter listed above.)
: A one-click WordPress plugin that converts all posts, pages, taxonomies, metadata, and settings to Markdown and YAML which can be dropped into Hugo. (Note: If you have trouble using this plugin, you can [export your site for Jekyll](https://wordpress.org/plugins/jekyll-exporter/) and use Hugo's built-in Jekyll converter listed above.)
[blog2md](https://github.com/palaniraja/blog2md)
: Works with [exported xml](https://en.support.wordpress.com/export/) file of your free YOUR-TLD.wordpress.com website. It also saves approved comments to `YOUR-POST-NAME-comments.md` file along with posts.
[wordhugopress](https://github.com/nantipov/wordhugopress)
: A small utility written in Java, exports the entire WordPress site from the database and resource (e.g. images) files stored locally or remotely. Therefore, migration from the backup files is possible. Supports merging of the multiple WordPress sites into a single Hugo one.
: A small utility written in Java that exports the entire WordPress site from the database and resource (e.g., images) files stored locally or remotely. Therefore, migration from the backup files is possible. Supports merging multiple WordPress sites into a single Hugo site.
[wp2hugo](https://github.com/ashishb/wp2hugo)
: A Go-based CLI tool to migrate WordPress website to Hugo while preserving original URLs, GUIDs (for feeds), image URLs, code highlights, table of contents, YouTube embeds, Google Maps embeds, and original WordPress navigation categories.
@@ -57,7 +57,7 @@ Alternatively, you can use the [Jekyll import command](/commands/hugo_import_jek
: A simple Medium to Hugo exporter able to import stories in one command, including front matter.
[medium-to-hugo](https://github.com/bgadrian/medium-to-hugo)
: CLI tool written in Go to export medium posts into a Hugo compatible Markdown format. Tags and images are included. All images will be downloaded locally and linked appropriately.
: A CLI tool written in Go to export medium posts into a Hugo-compatible Markdown format. Tags and images are included. All images will be downloaded locally and linked appropriately.
## Tumblr
@@ -68,7 +68,7 @@ Alternatively, you can use the [Jekyll import command](/commands/hugo_import_jek
: Export all your Tumblr content to Hugo Markdown files with preserved original formatting.
[Tumblr to Hugo](https://github.com/jipiboily/tumblr-to-hugo)
: A migration tool that converts each of your Tumblr posts to a content file with a proper title and path. Furthermore, "Tumblr to Hugo" creates a CSV file with the original URL and the new path on Hugo, to help you setup the redirections.
: A migration tool that converts each of your Tumblr posts to a content file with a proper title and path. It also generates a CSV file to help you set up URL redirects.
## Drupal
+4
View File
@@ -43,6 +43,10 @@ A static website with a dynamic search function? Yes, Hugo provides an alternati
[Hugo Lyra](https://github.com/paolomainardi/hugo-lyra)
: Hugo-Lyra is a JavaScript module to integrate [Lyra](https://github.com/LyraSearch/lyra) into a Hugo website. It contains the server-side part to generate the index and the client-side library (optional) to bootstrap the search engine easily.
[INFINI Pizza for WebAssembly](https://github.com/infinilabs/pizza-docsearch)
: Pizza is a super-lightweight yet fully featured search engine written in Rust. You can quickly add offline search functionality to your Hugo website in just five minutes with only three lines of code. For a step-by-step guide on integrating it with Hugo, check out [this blog tutorial](https://dev.to/medcl/adding-search-functionality-to-a-hugo-static-site-based-on-infini-pizza-for-webassembly-4h5e).
## Commercial
[Algolia](https://www.algolia.com/)
+7 -7
View File
@@ -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
+1 -1
View File
@@ -2,4 +2,4 @@ module github.com/gohugoio/hugoDocs
go 1.16
require github.com/gohugoio/gohugoioTheme v0.0.0-20240728210410-d42c342ce472 // indirect
require github.com/gohugoio/gohugoioTheme v0.0.0-20240815082608-66ccd383a90f // indirect
+4
View File
@@ -10,3 +10,7 @@ github.com/gohugoio/gohugoioTheme v0.0.0-20240623150114-cc7096eab3fd h1:I8X7c0oB
github.com/gohugoio/gohugoioTheme v0.0.0-20240623150114-cc7096eab3fd/go.mod h1:GOYeAPQJ/ok8z7oz1cjfcSlsFpXrmx6VkzQ5RpnyhZM=
github.com/gohugoio/gohugoioTheme v0.0.0-20240728210410-d42c342ce472 h1:AYZUibKKFRBp2VCQpDHW+JmQKvCvyhX7z7/SOLUSCcw=
github.com/gohugoio/gohugoioTheme v0.0.0-20240728210410-d42c342ce472/go.mod h1:GOYeAPQJ/ok8z7oz1cjfcSlsFpXrmx6VkzQ5RpnyhZM=
github.com/gohugoio/gohugoioTheme v0.0.0-20240812175901-cc0ef8e4a14a h1:E3JbZo69eqFBz6B+meQlKyy/ZBZQ73ldVDw8TADiIrQ=
github.com/gohugoio/gohugoioTheme v0.0.0-20240812175901-cc0ef8e4a14a/go.mod h1:GOYeAPQJ/ok8z7oz1cjfcSlsFpXrmx6VkzQ5RpnyhZM=
github.com/gohugoio/gohugoioTheme v0.0.0-20240815082608-66ccd383a90f h1:Eo5z3uUYfmrtIxQvHm388dFOERZwWGTjLuUO6vobzLc=
github.com/gohugoio/gohugoioTheme v0.0.0-20240815082608-66ccd383a90f/go.mod h1:GOYeAPQJ/ok8z7oz1cjfcSlsFpXrmx6VkzQ5RpnyhZM=
+3 -1
View File
@@ -5,7 +5,6 @@ defaultContentLanguage = "en"
enableEmoji = true
ignoreErrors = ["error-remote-getjson", "error-missing-instagram-accesstoken"]
languageCode = "en-us"
paginate = 100
pluralizeListTitles = false
timeZone = "Europe/Oslo"
title = "Hugo"
@@ -13,6 +12,9 @@ title = "Hugo"
# We do redirects via Netlify's _redirects file, generated by Hugo (see "outputs" below).
disableAliases = true
[pagination]
pagerSize = 100
[services.googleAnalytics]
ID = 'G-MBZGKNMDWC'
+1 -1
View File
@@ -3,7 +3,7 @@
command = "hugo --gc --minify"
[build.environment]
HUGO_VERSION = "0.131.0"
HUGO_VERSION = "0.133.1"
[context.production.environment]
HUGO_ENV = "production"
+3 -3
View File
@@ -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
+6 -10
View File
@@ -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=
-74
View File
@@ -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'
}
-79
View File
@@ -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=\"#")
+1 -1
View File
@@ -560,7 +560,7 @@ func (m *pageMap) getOrCreateResourcesForPage(ps *pageState) resource.Resources
for _, r := range res2 {
var found bool
for _, r2 := range res {
if r2.(resource.NameNormalizedProvider).NameNormalized() == r.(resource.NameNormalizedProvider).NameNormalized() {
if resource.NameNormalizedOrName(r2) == resource.NameNormalizedOrName(r) {
found = true
break
}
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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.HTML); 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
}
+1 -1
View File
@@ -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
+4 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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()
+4
View File
@@ -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
View File
@@ -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 &#34;world&#34;. (string)",
)
+4
View File
@@ -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)
}
+14
View File
@@ -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
View File
@@ -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
View File
@@ -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 {
+50 -16
View File
@@ -41,7 +41,7 @@ type LinkContext interface {
Title() string
// The rendered (HTML) text.
Text() hstring.RenderedString
Text() hstring.HTML
// 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.HTML
/// Returns the blockquote type, one of "regular" and "alert".
// Type "alert" indicates that this is a GitHub type alert.
@@ -98,6 +109,16 @@ type BlockquoteContext interface {
// The GitHub alert type converted to lowercase, e.g. "note".
// Only set if Type is "alert".
AlertType() string
// The alert title.
// Currently only relevant for Obsidian alerts.
// GitHub does not suport alert titles and will not render alerts with titles.
AlertTitle() hstring.HTML
// The alert sign, "+" or "-" or "" used to indicate folding.
// Currently only relevant for Obsidian alerts.
// GitHub does not suport alert signs and will not render alerts with signs.
AlertSign() string
}
type PositionerSourceTargetProvider interface {
@@ -107,18 +128,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 +155,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 +176,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.HTML
// PlainText is the unrendered version of Text.
PlainText() string
@@ -196,6 +217,19 @@ const (
CodeBlockRendererType
PassthroughRendererType
BlockquoteRendererType
TableRendererType
)
type GetRendererFunc func(t RendererType, id any) any
type TableCell struct {
Text hstring.HTML
Alignment string // left, center, or right
}
type TableRow []TableCell
type Table struct {
THead []TableRow
TBody []TableRow
}
+38 -106
View File
@@ -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)
if alertType != "" {
alert := resolveBlockQuoteAlert(string(text))
if alert.typ != "" {
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,
alert: alert,
text: hstring.HTML(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,20 +132,10 @@ func (r *htmlRenderer) renderBlockquoteDefault(
}
type blockquoteContext struct {
page any
pageInner any
text hstring.RenderedString
typ string
sourceRef []byte
alertType 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
text hstring.HTML
typ string
alert blockQuoteAlert
*attributes.AttributesHolder
}
@@ -202,50 +144,40 @@ func (c *blockquoteContext) Type() string {
}
func (c *blockquoteContext) AlertType() string {
return c.alertType
return c.alert.typ
}
func (c *blockquoteContext) Page() any {
return c.page
func (c *blockquoteContext) AlertTitle() hstring.HTML {
return hstring.HTML(c.alert.title)
}
func (c *blockquoteContext) PageInner() any {
return c.pageInner
func (c *blockquoteContext) AlertSign() string {
return c.alert.sign
}
func (c *blockquoteContext) Text() hstring.RenderedString {
func (c *blockquoteContext) Text() hstring.HTML {
return c.text
}
func (c *blockquoteContext) Ordinal() int {
return c.ordinal
}
var blockQuoteAlertRe = regexp.MustCompile(`^<p>\[!([a-zA-Z]+)\](-|\+)?[^\S\r\n]?([^\n]*)\n?`)
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]
// Note that GitHub's implementation is case-insensitive.
var gitHubAlertRe = regexp.MustCompile(`(?i)^<p>\[!(NOTE|TIP|WARNING|IMPORTANT|CAUTION)\]`)
// resolveGitHubAlert returns one of note, tip, warning, important or caution.
// An empty string if no match.
func resolveGitHubAlert(s string) string {
m := gitHubAlertRe.FindStringSubmatch(s)
if len(m) == 2 {
return strings.ToLower(m[1])
func resolveBlockQuoteAlert(s string) blockQuoteAlert {
m := blockQuoteAlertRe.FindStringSubmatch(s)
if len(m) == 4 {
return blockQuoteAlert{
typ: strings.ToLower(m[1]),
sign: m[2],
title: m[3],
}
}
return ""
return blockQuoteAlert{}
}
// Blockquote alert syntax was introduced by GitHub, but is also used
// by Obsidian which also support some extended attributes: More types, alert titles and a +/- sign for folding.
type blockQuoteAlert struct {
typ string
sign string
title string
}
@@ -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 }}|
@@ -76,7 +76,7 @@ title: "p1"
`
b := hugolib.Test(t, files)
b.AssertFileContent("public/p1/index.html",
b.AssertFileContentExact("public/p1/index.html",
"Blockquote Alert: |<p>This is a note with some whitespace after the alert type.</p>\n|alert|",
"Blockquote Alert: |<p>This is a tip.</p>",
"Blockquote Alert: |<p>This is a caution with some whitespace before the alert type.</p>\n|alert|",
@@ -107,5 +107,47 @@ Content: {{ .Content }}
`
b := hugolib.Test(t, files)
b.AssertFileContent("public/p1/index.html", "Content: <blockquote>\n</blockquote>\n")
b.AssertFileContentExact("public/p1/index.html", "Content: <blockquote>\n</blockquote>\n")
}
func TestBlockquObsidianWithTitleAndSign(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
-- content/_index.md --
---
title: "Home"
---
> [!danger]
> Do not approach or handle without protective gear.
> [!tip] Callouts can have custom titles
> Like this one.
> [!tip] Title-only callout
> [!faq]- Foldable negated callout
> Yes! In a foldable callout, the contents are hidden when the callout is collapsed
> [!faq]+ Foldable callout
> Yes! In a foldable callout, the contents are hidden when the callout is collapsed
-- layouts/index.html --
{{ .Content }}
-- layouts/_default/_markup/render-blockquote.html --
AlertType: {{ .AlertType }}|AlertTitle: {{ .AlertTitle }}|AlertSign: {{ .AlertSign | safeHTML }}|Text: {{ .Text }}|
`
b := hugolib.Test(t, files)
b.AssertFileContentExact("public/index.html",
"AlertType: tip|AlertTitle: Callouts can have custom titles|AlertSign: |",
"AlertType: tip|AlertTitle: Title-only callout</p>|AlertSign: |",
"AlertType: faq|AlertTitle: Foldable negated callout|AlertSign: -|Text: <p>Yes!",
"AlertType: faq|AlertTitle: Foldable callout|AlertSign: +|",
"AlertType: danger|AlertTitle: |AlertSign: |Text: <p>Do not approach or handle without protective gear.</p>\n|",
)
}
+23 -15
View File
@@ -19,42 +19,50 @@ import (
qt "github.com/frankban/quicktest"
)
func TestResolveGitHubAlert(t *testing.T) {
func TestResolveBlockQuoteAlert(t *testing.T) {
t.Parallel()
c := qt.New(t)
tests := []struct {
input string
expected string
expected blockQuoteAlert
}{
{
input: "[!NOTE]",
expected: "note",
expected: blockQuoteAlert{typ: "note"},
},
{
input: "[!WARNING]",
expected: "warning",
input: "[!FaQ]",
expected: blockQuoteAlert{typ: "faq"},
},
{
input: "[!TIP]",
expected: "tip",
input: "[!NOTE]+",
expected: blockQuoteAlert{typ: "note", sign: "+"},
},
{
input: "[!IMPORTANT]",
expected: "important",
input: "[!NOTE]-",
expected: blockQuoteAlert{typ: "note", sign: "-"},
},
{
input: "[!CAUTION]",
expected: "caution",
input: "[!NOTE] This is a note",
expected: blockQuoteAlert{typ: "note", title: "This is a note"},
},
{
input: "[!FOO]",
expected: "",
input: "[!NOTE]+ This is a note",
expected: blockQuoteAlert{typ: "note", sign: "+", title: "This is a note"},
},
{
input: "[!NOTE]+ This is a title\nThis is not.",
expected: blockQuoteAlert{typ: "note", sign: "+", title: "This is a title"},
},
{
input: "[!NOTE]\nThis is not.",
expected: blockQuoteAlert{typ: "note"},
},
}
for _, test := range tests {
c.Assert(resolveGitHubAlert("<p>"+test.input), qt.Equals, test.expected)
for i, test := range tests {
c.Assert(resolveBlockQuoteAlert("<p>"+test.input), qt.Equals, test.expected, qt.Commentf("Test %d", i))
}
}
+6 -65
View File
@@ -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, "{")
+2
View File
@@ -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 {
+17 -5
View File
@@ -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 &ldquo;quotes&rdquo; and single &lsquo;quotes&rsquo;`)
@@ -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
}
+6 -6
View File
@@ -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(
+150
View File
@@ -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()
}
+5 -65
View File
@@ -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)
+28 -37
View File
@@ -52,7 +52,7 @@ type linkContext struct {
pageInner any
destination string
title string
text hstring.RenderedString
text hstring.HTML
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.HTML {
return ctx.text
}
@@ -100,7 +100,7 @@ type headingContext struct {
pageInner any
level int
anchor string
text hstring.RenderedString
text hstring.HTML
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.HTML {
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.HTML(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.HTML(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.HTML(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.HTML(text),
plainText: string(n.Text(source)),
AttributesHolder: attributes.New(n.Attributes(), attributes.AttributesOwnerGeneral),
},
+175
View File
@@ -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.HTML(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>")
}
+2 -2
View File
@@ -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"}},
+9 -1
View File
@@ -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)
+344
View File
@@ -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|",
)
}
+151
View File
@@ -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)
}
}
}
+76
View File
@@ -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
}
+8
View File
@@ -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")
}
+8
View File
@@ -298,3 +298,11 @@ func (r resourceTypesHolder) ResourceType() string {
func NewResourceTypesProvider(mediaType media.Type, resourceType string) ResourceTypesProvider {
return resourceTypesHolder{mediaType: mediaType, resourceType: resourceType}
}
// NameNormalizedOrName returns the normalized name if available, otherwise the name.
func NameNormalizedOrName(r Resource) string {
if nn, ok := r.(NameNormalizedProvider); ok {
return nn.NameNormalized()
}
return r.Name()
}
+3 -3
View File
@@ -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 -}}