Compare commits

...

16 commits

Author SHA1 Message Date
Sebastian Höffner d1a4a9d701
Merge c91c64a1aa into 1961327536 2024-04-26 00:05:20 +09:00
hugoreleaser 1961327536 releaser: Prepare repository for 0.126.0-DEV
[ci skip]
2024-04-25 13:40:37 +00:00
hugoreleaser cc3574ef4f releaser: Bump versions for release of 0.125.4
[ci skip]
2024-04-25 13:27:26 +00:00
Peter van Dijk fe84cc218e
commands: Clarify that create or install a theme are two options 2024-04-25 14:38:22 +02:00
Bjørn Erik Pedersen babcb339a8 config: Setups with only one active language can never be multihost
Fixes #12288
2024-04-25 14:35:49 +02:00
Bjørn Erik Pedersen 7203a95a60 Fix rebuilds when running hugo -w
This was partly broken in Hugo 0.123.0.

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

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

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

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

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

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

Closes #12396
2024-04-22 15:57:37 +02:00
Eric Anderson 722c486a34 SECURITY.md: Update link to security model
The security model was moved in https://github.com/gohugoio/hugoDocs/pull/2495
2024-04-22 13:04:53 +02:00
Bjørn Erik Pedersen f40f50ead0 modules: Fix potential infinite loop in module collection
Fixes #12407
2024-04-22 11:34:11 +02:00
Sebastian Höffner c91c64a1aa
markup: add --citeproc to pandoc converter
Adds the citeproc filter to the pandoc converter.

There are several PRs for it this feature already. However, I think
simply adding `--citeproc` is the cleanest way to enable this feature,
with the option to flesh it out later, e.g., in #7529.

Some PRs and issues attempt adding more config options to Hugo which
indirectly configure pandoc, but I think simply configuring Pandoc via
Pandoc itself is simpler, as it is already possible with two YAML
blocks -- one for Hugo, and one for Pandoc:

    ---
    title: This is the Hugo YAML block
    ---
    ---
    bibliography: assets/pandoc-yaml-block-bibliography.bib
    ...
    Document content with @citation!

There are other useful options, e.g., #4800 attempts to use `nocite`,
which works out of the box with this PR:

    ---
    title: This is the Hugo YAML block
    ---
    ---
    bibliography: assets/pandoc-yaml-block-bibliography.bib
    nocite: |
      @*
    ...
    Document content with no citations but a full bibliography:

    ## Bibliography

Other useful options are `csl: ...` and `link-citations: true`, which
set the path to a custom CSL file and create HTML links between the
references and the bibliography.

The following issues and PRs are related:

- Add support for parsing citations and Jupyter notebooks via Pandoc and/or Goldmark extension #6101
  Bundles multiple requests, this PR tackles citation parsing.

- WIP: Bibliography with Pandoc #4800
  Passes the frontmatter to Pandoc and still uses
  `--filter pandoc-citeproc` instead of `--citeproc`.
- Allow configuring Pandoc #7529
  That PR is much more extensive and might eventually supersede this PR,
  but I think --bibliography and --citeproc should be independent
  options (--bibliography should be optional and citeproc can always be
  specified).
- Pandoc - allow citeproc extension to be invoked, with bibliography. #8610
  Similar to #7529, #8610 adds a new config option to Hugo.
  I think passing --citeproc and letting the users decide on the
  metadata they want to pass to pandoc is better, albeit uglier.
2023-08-08 15:45:27 +02:00
30 changed files with 539 additions and 74 deletions

View file

@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2022 The Hugo Authors.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View file

@ -4,4 +4,4 @@
Please report (suspected) security vulnerabilities to **[bjorn.erik.pedersen@gmail.com](mailto:bjorn.erik.pedersen@gmail.com)**. You will receive a response from us within 48 hours. If we can confirm the issue, we will release a patch as soon as possible depending on the complexity of the issue but historically within days.
Also see [Hugo's Security Model](https://gohugo.io/about/security-model/).
Also see [Hugo's Security Model](https://gohugo.io/about/security/).

View file

@ -67,7 +67,7 @@ func New(opts Options) *Cache {
evictedIdentities := collections.NewStack[identity.Identity]()
onEvict := func(k, v any) {
if !opts.Running {
if !opts.Watching {
return
}
identity.WalkIdentitiesShallow(v, func(level int, id identity.Identity) bool {
@ -97,7 +97,7 @@ type Options struct {
CheckInterval time.Duration
MaxSize int
MinMaxSize int
Running bool
Watching bool
}
// Options for a partition.

View file

@ -209,7 +209,7 @@ func (c *newCommand) newSiteNextStepsText(path string, format string) string {
1. Change the current directory to ` + path + `.
2. Create or install a theme:
- Create a new theme with the command "hugo new theme <THEMENAME>"
- Install a theme from https://themes.gohugo.io/
- Or, install a theme from https://themes.gohugo.io/
3. Edit hugo.` + format + `, setting the "theme" property to the theme name.
4. Create new content with the command "hugo new content `)

View file

@ -17,7 +17,7 @@ package hugo
// This should be the only one.
var CurrentVersion = Version{
Major: 0,
Minor: 125,
PatchLevel: 2,
Suffix: "",
Minor: 126,
PatchLevel: 0,
Suffix: "-DEV",
}

View file

@ -71,6 +71,9 @@ func (c ConfigLanguage) Environment() string {
}
func (c ConfigLanguage) IsMultihost() bool {
if len(c.m.Languages)-len(c.config.C.DisabledLanguages) <= 1 {
return false
}
return c.m.IsMultihost
}

2
deps/deps.go vendored
View file

@ -155,7 +155,7 @@ func (d *Deps) Init() error {
}
if d.MemCache == nil {
d.MemCache = dynacache.New(dynacache.Options{Running: d.Conf.Running(), Log: d.Log})
d.MemCache = dynacache.New(dynacache.Options{Watching: d.Conf.Watching(), Log: d.Log})
}
if d.PathSpec == nil {

View file

@ -43,7 +43,7 @@ Hugo passes reasonable default arguments to these external helpers by default:
- `asciidoctor`: `--no-header-footer -`
- `rst2html`: `--leave-comments --initial-header-level=2`
- `pandoc`: `--mathjax`
- `pandoc`: `--mathjax` and, for pandoc >= 2.11, `--citeproc`
{{% note %}}
Because additional formats are external commands, generation performance will rely heavily on the performance of the external tool you are using. As this feature is still in its infancy, feedback is welcome.
@ -63,7 +63,59 @@ Some Asciidoctor parameters can be customized in Hugo. See&nbsp;[details].
[details]: /getting-started/configuration-markup/#asciidoc
## Learn markdown
### External Helper Pandoc
[Pandoc](https://pandoc.org) is a universal document converter and can be used to convert markdown files.
In Hugo, Pandoc can be used for LaTeX-style math (the `--mathjax` command line option is provided):
```
---
title: Math document
---
Some inline math: $a^2 + b^2 = c^2$.
```
This will render in your HTML as:
```
<p>Some inline math: <span class="math inline">\(a^2 + b^2 = c^2\)</span></p>
```
You will have to [add MathJax](https://www.mathjax.org/#gettingstarted) to your template to properly render the math.
For **Pandoc >= 2.11**, you can use [citations](https://pandoc.org/MANUAL.html#extension-citations).
One way is to employ [BibTeX files](https://en.wikibooks.org/wiki/LaTeX/Bibliography_Management#BibTeX) to cite:
```
---
title: Citation document
---
---
bibliography: assets/bibliography.bib
...
This is a citation: @Doe2022
```
Note that Hugo will **not** pass its metadata YAML block to Pandoc; however, it will pass the **second** meta data block, denoted with `---` and `...` to Pandoc.
Thus, all Pandoc settings should go there.
You can also add all elements from a bibliography file (without citing them explicitly) using:
```
---
title: My Publications
---
---
bibliography: assets/bibliography.bib
nocite: |
@*
...
```
It is also possible to provide a custom [CSL style](https://citationstyles.org/authors/) by passing `csl: path-to-style.csl` as a Pandoc option.
## Learn Markdown
Markdown syntax is simple enough to learn in a single sitting. The following are excellent resources to get you up and running:

4
go.mod
View file

@ -64,8 +64,8 @@ require (
github.com/spf13/cobra v1.8.0
github.com/spf13/fsync v0.10.1
github.com/spf13/pflag v1.0.5
github.com/tdewolff/minify/v2 v2.20.19
github.com/tdewolff/parse/v2 v2.7.12
github.com/tdewolff/minify/v2 v2.20.20
github.com/tdewolff/parse/v2 v2.7.13
github.com/yuin/goldmark v1.7.1
github.com/yuin/goldmark-emoji v1.0.2
go.uber.org/automaxprocs v1.5.3

8
go.sum
View file

@ -429,10 +429,10 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tdewolff/minify/v2 v2.20.19 h1:tX0SR0LUrIqGoLjXnkIzRSIbKJ7PaNnSENLD4CyH6Xo=
github.com/tdewolff/minify/v2 v2.20.19/go.mod h1:ulkFoeAVWMLEyjuDz1ZIWOA31g5aWOawCFRp9R/MudM=
github.com/tdewolff/parse/v2 v2.7.12 h1:tgavkHc2ZDEQVKy1oWxwIyh5bP4F5fEh/JmBwPP/3LQ=
github.com/tdewolff/parse/v2 v2.7.12/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA=
github.com/tdewolff/minify/v2 v2.20.20 h1:vhULb+VsW2twkplgsawAoUY957efb+EdiZ7zu5fUhhk=
github.com/tdewolff/minify/v2 v2.20.20/go.mod h1:GYaLXFpIIwsX99apQHXfGdISUdlA98wmaoWxjT9C37k=
github.com/tdewolff/parse/v2 v2.7.13 h1:iSiwOUkCYLNfapHoqdLcqZVgvQ0jrsao8YYKP/UJYTI=
github.com/tdewolff/parse/v2 v2.7.13/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA=
github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo=
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=

View file

@ -14,6 +14,7 @@
package hugolib
import (
"fmt"
"strings"
"testing"
)
@ -241,3 +242,52 @@ iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAA
"p1|<p><a href=\"p2\">P2</a>", "<img src=\"pixel.png\" alt=\"Pixel\">")
})
}
func TestRenderHooksDefaultEscape(t *testing.T) {
files := `
-- hugo.toml --
[markup.goldmark.renderHooks]
[markup.goldmark.renderHooks.image]
enableDefault = ENABLE
[markup.goldmark.renderHooks.link]
enableDefault = ENABLE
[markup.goldmark.parser]
wrapStandAloneImageWithinParagraph = false
[markup.goldmark.parser.attribute]
block = true
title = true
-- content/_index.md --
---
title: "Home"
---
Link: [text-"<>&](/destination-"<> 'title-"<>&')
Image: ![alt-"<>&](/destination-"<> 'title-"<>&')
{class="><script>alert()</script>" id="baz"}
-- layouts/index.html --
{{ .Content }}
`
for _, enabled := range []bool{true, false} {
enabled := enabled
t.Run(fmt.Sprint(enabled), func(t *testing.T) {
t.Parallel()
b := Test(t, strings.ReplaceAll(files, "ENABLE", fmt.Sprint(enabled)))
// The escaping is slightly different between the two.
if enabled {
b.AssertFileContent("public/index.html",
"Link: <a href=\"/destination-%22%3C%3E\" title=\"title-&#34;&lt;&gt;&amp;\">text-&quot;&lt;&gt;&amp;</a>",
"img alt=\"alt-&quot;&lt;&gt;&amp;\" src=\"/destination-%22%3C%3E\" title=\"title-&#34;&lt;&gt;&amp;\">",
"&gt;&lt;script&gt;",
)
} else {
b.AssertFileContent("public/index.html",
"Link: <a href=\"/destination-%22%3C%3E\" title=\"title-&quot;&lt;&gt;&amp;\">text-&quot;&lt;&gt;&amp;</a>",
"Image: <img src=\"/destination-%22%3C%3E\" alt=\"alt-&quot;&lt;&gt;&amp;\" title=\"title-&quot;&lt;&gt;&amp;\">",
)
}
})
}
}

View file

@ -252,3 +252,31 @@ Files: {{ range $files }}{{ .Permalink }}|{{ end }}$
b.AssertFileContent("public/en/enpages/mybundle-en/file2.txt", "File 2 en.")
b.AssertFileContent("public/fr/section/mybundle/file2.txt", "File 2 en.")
}
func TestMultihostAllButOneLanguageDisabledIssue12288(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
defaultContentLanguage = "en"
disableLanguages = ["fr"]
#baseURL = "https://example.com"
[languages]
[languages.en]
baseURL = "https://example.en"
weight = 1
[languages.fr]
baseURL = "https://example.fr"
weight = 2
-- assets/css/main.css --
body { color: red; }
-- layouts/index.html --
{{ $css := resources.Get "css/main.css" | minify }}
CSS: {{ $css.Permalink }}|{{ $css.RelPermalink }}|
`
b := Test(t, files)
b.AssertFileContent("public/css/main.min.css", "body{color:red}")
b.AssertFileContent("public/index.html", "CSS: https://example.en/css/main.min.css|/css/main.min.css|")
}

View file

@ -38,12 +38,20 @@ import (
type TestOpt func(*IntegrationTestConfig)
// TestOptRunning will enable running in integration tests.
func TestOptRunning() TestOpt {
return func(c *IntegrationTestConfig) {
c.Running = true
}
}
// TestOptWatching will enable watching in integration tests.
func TestOptWatching() TestOpt {
return func(c *IntegrationTestConfig) {
c.Watching = true
}
}
// Enable tracing in integration tests.
// THis should only be used during development and not committed to the repo.
func TestOptTrace() TestOpt {
@ -570,6 +578,10 @@ func (s *IntegrationTestBuilder) initBuilder() error {
"running": s.Cfg.Running,
"watch": s.Cfg.Running,
})
} else if s.Cfg.Watching {
flags.Set("internal", maps.Params{
"watch": s.Cfg.Watching,
})
}
if s.Cfg.WorkingDir != "" {
@ -817,6 +829,11 @@ type IntegrationTestConfig struct {
// Whether to simulate server mode.
Running bool
// Watch for changes.
// This is (currently) always set to true when Running is set.
// Note that the CLI for the server does allow for --watch=false, but that is not used in these test.
Watching bool
// Will print the log buffer after the build
Verbose bool

View file

@ -175,7 +175,7 @@ func (pco *pageContentOutput) RenderShortcodes(ctx context.Context) (template.HT
// This content will be parsed and rendered by Goldmark.
// Wrap it in a special Hugo markup to assign the correct Page from
// the stack.
c = hugocontext.Wrap(c, pco.po.p.pid)
return template.HTML(hugocontext.Wrap(c, pco.po.p.pid)), nil
}
return helpers.BytesToHTML(c), nil

View file

@ -121,14 +121,23 @@ func TestRebuildEditTextFileInBranchBundle(t *testing.T) {
b.AssertRenderCountContent(1)
}
func TestRebuildRenameTextFileInLeafBundle(t *testing.T) {
b := TestRunning(t, rebuildFilesSimple)
b.AssertFileContent("public/mysection/mysectionbundle/index.html", "My Section Bundle Text 2 Content.", "Len Resources: 2|")
func testRebuildBothWatchingAndRunning(t *testing.T, files string, withB func(b *IntegrationTestBuilder)) {
t.Helper()
for _, opt := range []TestOpt{TestOptWatching(), TestOptRunning()} {
b := Test(t, files, opt)
withB(b)
}
}
b.RenameFile("content/mysection/mysectionbundle/mysectionbundletext.txt", "content/mysection/mysectionbundle/mysectionbundletext2.txt").Build()
b.AssertFileContent("public/mysection/mysectionbundle/index.html", "mysectionbundletext2", "My Section Bundle Text 2 Content.", "Len Resources: 2|")
b.AssertRenderCountPage(3)
b.AssertRenderCountContent(3)
func TestRebuildRenameTextFileInLeafBundle(t *testing.T) {
testRebuildBothWatchingAndRunning(t, rebuildFilesSimple, func(b *IntegrationTestBuilder) {
b.AssertFileContent("public/mysection/mysectionbundle/index.html", "My Section Bundle Text 2 Content.", "Len Resources: 2|")
b.RenameFile("content/mysection/mysectionbundle/mysectionbundletext.txt", "content/mysection/mysectionbundle/mysectionbundletext2.txt").Build()
b.AssertFileContent("public/mysection/mysectionbundle/index.html", "mysectionbundletext2", "My Section Bundle Text 2 Content.", "Len Resources: 2|")
b.AssertRenderCountPage(3)
b.AssertRenderCountContent(3)
})
}
func TestRebuilEditContentFileInLeafBundle(t *testing.T) {
@ -367,8 +376,6 @@ My short.
}
func TestRebuildBaseof(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
title = "Hugo Site"
@ -383,12 +390,13 @@ Baseof: {{ .Title }}|
Home: {{ .Title }}|{{ .Content }}|
{{ end }}
`
b := Test(t, files, TestOptRunning())
b.AssertFileContent("public/index.html", "Baseof: Hugo Site|", "Home: Hugo Site||")
b.EditFileReplaceFunc("layouts/_default/baseof.html", func(s string) string {
return strings.Replace(s, "Baseof", "Baseof Edited", 1)
}).Build()
b.AssertFileContent("public/index.html", "Baseof Edited: Hugo Site|", "Home: Hugo Site||")
testRebuildBothWatchingAndRunning(t, files, func(b *IntegrationTestBuilder) {
b.AssertFileContent("public/index.html", "Baseof: Hugo Site|", "Home: Hugo Site||")
b.EditFileReplaceFunc("layouts/_default/baseof.html", func(s string) string {
return strings.Replace(s, "Baseof", "Baseof Edited", 1)
}).Build()
b.AssertFileContent("public/index.html", "Baseof Edited: Hugo Site|", "Home: Hugo Site||")
})
}
func TestRebuildSingleWithBaseof(t *testing.T) {

View file

@ -123,14 +123,14 @@ func NewHugoSites(cfg deps.DepsCfg) (*HugoSites, error) {
HandlerPost: logHookLast,
Stdout: cfg.LogOut,
Stderr: cfg.LogOut,
StoreErrors: conf.Running(),
StoreErrors: conf.Watching(),
SuppressStatements: conf.IgnoredLogs(),
}
logger = loggers.New(logOpts)
}
memCache := dynacache.New(dynacache.Options{Running: conf.Running(), Log: logger})
memCache := dynacache.New(dynacache.Options{Watching: conf.Watching(), Log: logger})
firstSiteDeps := &deps.Deps{
Fs: cfg.Fs,

View file

@ -1,7 +1,8 @@
# Release env.
# These will be replaced by script before release.
HUGORELEASER_TAG=v0.125.1
HUGORELEASER_COMMITISH=68c5ad638c2072969e47262926b912e80fd71a77
HUGORELEASER_TAG=v0.125.4
HUGORELEASER_COMMITISH=cc3574ef4f41fccbe88d9443ed066eb10867ada2

View file

@ -34,7 +34,7 @@ func New() goldmark.Extender {
// Wrap wraps the given byte slice in a Hugo context that used to determine the correct Page
// in .RenderShortcodes.
func Wrap(b []byte, pid uint64) []byte {
func Wrap(b []byte, pid uint64) string {
buf := bufferpool.GetBuffer()
defer bufferpool.PutBuffer(buf)
buf.Write(prefix)
@ -45,7 +45,7 @@ func Wrap(b []byte, pid uint64) []byte {
buf.Write(b)
buf.Write(prefix)
buf.Write(closingDelimAndNewline)
return buf.Bytes()
return buf.String()
}
var kindHugoContext = ast.NewNodeKind("HugoContext")

View file

@ -24,7 +24,7 @@ func TestWrap(t *testing.T) {
b := []byte("test")
c.Assert(string(Wrap(b, 42)), qt.Equals, "{{__hugo_ctx pid=42}}\ntest{{__hugo_ctx/}}\n")
c.Assert(Wrap(b, 42), qt.Equals, "{{__hugo_ctx pid=42}}\ntest{{__hugo_ctx/}}\n")
}
func BenchmarkWrap(b *testing.B) {

View file

@ -15,10 +15,14 @@
package pandoc
import (
"bytes"
"strconv"
"strings"
"sync"
"github.com/gohugoio/hugo/common/hexec"
"github.com/gohugoio/hugo/htesting"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/markup/converter"
"github.com/gohugoio/hugo/markup/internal"
)
@ -64,6 +68,9 @@ func (c *pandocConverter) getPandocContent(src []byte, ctx converter.DocumentCon
return src, nil
}
args := []string{"--mathjax"}
if supportsCitations(c.cfg) {
args = append(args[:], "--citeproc")
}
return internal.ExternallyRenderContent(c.cfg, ctx, src, binaryName, args)
}
@ -76,6 +83,69 @@ func getPandocBinaryName() string {
return ""
}
type pandocVersion struct {
major, minor int64
}
func (left pandocVersion) greaterThanOrEqual(right pandocVersion) bool {
return left.major > right.major || (left.major == right.major && left.minor >= right.minor)
}
var versionOnce sync.Once
var foundPandocVersion pandocVersion
// getPandocVersion parses the pandoc version output
func getPandocVersion(cfg converter.ProviderConfig) (pandocVersion, error) {
var err error
versionOnce.Do(func() {
argsv := []any{"--version"}
var out bytes.Buffer
argsv = append(argsv, hexec.WithStdout(&out))
cmd, err := cfg.Exec.New(pandocBinary, argsv...)
if err != nil {
cfg.Logger.Errorf("Could not call pandoc: %v", err)
foundPandocVersion = pandocVersion{0, 0}
return
}
err = cmd.Run()
if err != nil {
cfg.Logger.Errorf("%s --version: %v", pandocBinary, err)
foundPandocVersion = pandocVersion{0, 0}
return
}
outbytes := bytes.Replace(out.Bytes(), []byte("\r"), []byte(""), -1)
output := strings.Split(string(outbytes), "\n")[0]
// Split, e.g., "pandoc 2.5" into 2 and 5 and convert them to integers
versionStrings := strings.Split(strings.Split(output, " ")[1], ".")
majorVersion, err := strconv.ParseInt(versionStrings[0], 10, 64)
if err != nil {
println(err)
}
minorVersion, err := strconv.ParseInt(versionStrings[1], 10, 64)
if err != nil {
println(err)
}
foundPandocVersion = pandocVersion{majorVersion, minorVersion}
})
return foundPandocVersion, err
}
// SupportsCitations returns true for pandoc versions >= 2.11, which include citeproc
func supportsCitations(cfg converter.ProviderConfig) bool {
if Supports() {
foundPandocVersion, err := getPandocVersion(cfg)
supportsCitations := foundPandocVersion.greaterThanOrEqual(pandocVersion{2, 11}) && err == nil
return supportsCitations
}
return false
}
// Supports returns whether Pandoc is installed on this computer.
func Supports() bool {
hasBin := getPandocBinaryName() != ""

View file

@ -25,7 +25,7 @@ import (
qt "github.com/frankban/quicktest"
)
func TestConvert(t *testing.T) {
func setupTestConverter(t *testing.T) (*qt.C, converter.Converter, converter.ProviderConfig) {
if !Supports() {
t.Skip("pandoc not installed")
}
@ -38,7 +38,140 @@ func TestConvert(t *testing.T) {
c.Assert(err, qt.IsNil)
conv, err := p.New(converter.DocumentContext{})
c.Assert(err, qt.IsNil)
b, err := conv.Convert(converter.RenderContext{Src: []byte("testContent")})
c.Assert(err, qt.IsNil)
c.Assert(string(b.Bytes()), qt.Equals, "<p>testContent</p>\n")
return c, conv, cfg
}
func TestConvert(t *testing.T) {
c, conv, _ := setupTestConverter(t)
output, err := conv.Convert(converter.RenderContext{Src: []byte("testContent")})
c.Assert(err, qt.IsNil)
c.Assert(string(output.Bytes()), qt.Equals, "<p>testContent</p>\n")
}
func runCiteprocTest(t *testing.T, content string, expected string) {
c, conv, cfg := setupTestConverter(t)
if !supportsCitations(cfg) {
t.Skip("pandoc does not support citations")
}
output, err := conv.Convert(converter.RenderContext{Src: []byte(content)})
c.Assert(err, qt.IsNil)
c.Assert(string(output.Bytes()), qt.Equals, expected)
}
func TestGetPandocVersionCallTwice(t *testing.T) {
c, _, cfg := setupTestConverter(t)
version1, err1 := getPandocVersion(cfg)
version2, err2 := getPandocVersion(cfg)
c.Assert(version1, qt.Equals, version2)
c.Assert(err1, qt.IsNil)
c.Assert(err2, qt.IsNil)
}
func TestPandocVersionEquality(t *testing.T) {
c := qt.New(t)
v1 := pandocVersion{1, 0}
v2 := pandocVersion{2, 0}
v3 := pandocVersion{2, 2}
v4 := pandocVersion{1, 2}
v5 := pandocVersion{2, 11}
// 1 >= 1 -> true
c.Assert(v1.greaterThanOrEqual(v1), qt.IsTrue)
// 1 >= 2 -> false, 2 >= 1 -> tru
c.Assert(v1.greaterThanOrEqual(v2), qt.IsFalse)
c.Assert(v2.greaterThanOrEqual(v1), qt.IsTrue)
// 2.0 >= 2.2 -> false, 2.2 >= 2.0 -> true
c.Assert(v2.greaterThanOrEqual(v3), qt.IsFalse)
c.Assert(v3.greaterThanOrEqual(v2), qt.IsTrue)
// 2.2 >= 1.2 -> true, 1.2 >= 2.2 -> false
c.Assert(v3.greaterThanOrEqual(v4), qt.IsTrue)
c.Assert(v4.greaterThanOrEqual(v3), qt.IsFalse)
// 2.11 >= 2.2 -> true, 2.2 >= 2.11 -> false
c.Assert(v5.greaterThanOrEqual(v3), qt.IsTrue)
c.Assert(v3.greaterThanOrEqual(v5), qt.IsFalse)
}
func TestCiteprocWithHugoMeta(t *testing.T) {
content := `
---
title: Test
published: 2022-05-30
---
testContent
`
expected := "<p>testContent</p>\n"
runCiteprocTest(t, content, expected)
}
func TestCiteprocWithPandocMeta(t *testing.T) {
content := `
---
---
---
...
testContent
`
expected := "<p>testContent</p>\n"
runCiteprocTest(t, content, expected)
}
func TestCiteprocWithBibliography(t *testing.T) {
content := `
---
---
---
bibliography: testdata/bibliography.bib
...
testContent
`
expected := "<p>testContent</p>\n"
runCiteprocTest(t, content, expected)
}
func TestCiteprocWithExplicitCitation(t *testing.T) {
content := `
---
---
---
bibliography: testdata/bibliography.bib
...
@Doe2022
`
expected := `<p><span class="citation" data-cites="Doe2022">Doe and Mustermann
(2022)</span></p>
<div id="refs" class="references csl-bib-body hanging-indent"
role="doc-bibliography">
<div id="ref-Doe2022" class="csl-entry" role="doc-biblioentry">
Doe, Jane, and Max Mustermann. 2022. <span>A Treatise on Hugo
Tests.</span> <em>Hugo Websites</em>.
</div>
</div>
`
runCiteprocTest(t, content, expected)
}
func TestCiteprocWithNocite(t *testing.T) {
content := `
---
---
---
bibliography: testdata/bibliography.bib
nocite: |
@*
...
`
expected := `<div id="refs" class="references csl-bib-body hanging-indent"
role="doc-bibliography">
<div id="ref-Doe2022" class="csl-entry" role="doc-biblioentry">
Doe, Jane, and Max Mustermann. 2022. <span>A Treatise on Hugo
Tests.</span> <em>Hugo Websites</em>.
</div>
</div>
`
runCiteprocTest(t, content, expected)
}

View file

@ -0,0 +1,6 @@
@article{Doe2022,
author = "Jane Doe and Max Mustermann",
title = "A Treatise on Hugo Tests",
journal = "Hugo Websites",
year = "2022",
}

View file

@ -261,7 +261,10 @@ func (c *collector) add(owner *moduleAdapter, moduleImport Import) (*moduleAdapt
// This will select the latest release-version (not beta etc.).
versionQuery = "upgrade"
}
if err := c.Get(fmt.Sprintf("%s@%s", modulePath, versionQuery)); err != nil {
// Note that we cannot use c.Get for this, as that may
// trigger a new module collection and potentially create a infinite loop.
if err := c.get(fmt.Sprintf("%s@%s", modulePath, versionQuery)); err != nil {
return nil, err
}
if err := c.loadModules(); err != nil {

View file

@ -5,7 +5,7 @@
{{- $src = .RelPermalink -}}
{{- end -}}
{{- end -}}
{{- $attributes := merge .Attributes (dict "alt" .Text "src" $src "title" .Title) -}}
{{- $attributes := merge .Attributes (dict "alt" .Text "src" $src "title" (.Title | transform.HTMLEscape)) -}}
<img
{{- range $k, $v := $attributes -}}
{{- if $v -}}

View file

@ -17,7 +17,7 @@
{{- end -}}
{{- end -}}
{{- end -}}
{{- $attributes := dict "href" $href "title" .Title -}}
{{- $attributes := dict "href" $href "title" (.Title | transform.HTMLEscape) -}}
<a
{{- range $k, $v := $attributes -}}
{{- if $v -}}

View file

@ -48,7 +48,7 @@
<title>{{ if eq .Title .Site.Title }}{{ .Site.Title }}{{ else }}{{ with .Title }}{{ . }} on {{ end }}{{ .Site.Title }}{{ end }}</title>
<link>{{ .Permalink }}</link>
<description>Recent content {{ if ne .Title .Site.Title }}{{ with .Title }}in {{ . }} {{ end }}{{ end }}on {{ .Site.Title }}</description>
<generator>Hugo {{ hugo.Version }}</generator>
<generator>Hugo</generator>
<language>{{ site.Language.LanguageCode }}</language>{{ with $authorEmail }}
<managingEditor>{{.}}{{ with $authorName }} ({{ . }}){{ end }}</managingEditor>{{ end }}{{ with $authorEmail }}
<webMaster>{{ . }}{{ with $authorName }} ({{ . }}){{ end }}</webMaster>{{ end }}{{ with .Site.Copyright }}

View file

@ -4,11 +4,11 @@
<meta property="og:site_name" content="{{ . }}">
{{- end }}
{{- with or .Title site.Title site.Params.title | plainify}}
{{- with or .Title site.Title site.Params.title | plainify }}
<meta property="og:title" content="{{ . }}">
{{- end }}
{{- with or .Description .Summary site.Params.description | plainify }}
{{- with or .Description .Summary site.Params.description | plainify | htmlUnescape | chomp }}
<meta property="og:description" content="{{ . }}">
{{- end }}
@ -18,7 +18,9 @@
{{- if .IsPage }}
<meta property="og:type" content="article">
<meta property="article:section" content="{{ .Section }}">
{{- with .Section }}
<meta property="article:section" content="{{ . }}">
{{- end }}
{{- $ISO8601 := "2006-01-02T15:04:05-07:00" }}
{{- with .PublishDate }}
<meta property="article:published_time" {{ .Format $ISO8601 | printf "content=%q" | safeHTMLAttr }}>

View file

@ -8,10 +8,10 @@ Renders an embedded YouTube video.
@param {int} [end] The time, measured in seconds from the start of the video, when the player should stop playing the video.
@param {string} [id] The video id. Optional if the id is provided as first positional argument.
@param {string} [loading=eager] The loading attribute of the iframe element.
@param {bool} [loop=false] Whether to indefinitely repeat the video.
@param {bool} [loop=false] Whether to indefinitely repeat the video. Ignores the start and end arguments after the first play.
@param {bool} [mute=false] Whether to mute the video. Always true when autoplay is true.
@param {int} [start] The time, measured in seconds from the start of the video, when the player should start playing the video.
@param {string} [title] The title attribute of the iframe element. Defaults to the title returned by YouTube oEmbed API.
@param {string} [title] The title attribute of the iframe element. Defaults to "YouTube video".
@returns {template.HTML}
@ -26,20 +26,6 @@ Renders an embedded YouTube video.
{{- if not $pc.Disable }}
{{- with $id := or (.Get "id") (.Get 0) }}
{{- /* Get data from the YouTube oEmbed API. */}}
{{- $q := querify "url" (printf "https://www.youtube.com/watch?v=%s" $id) "format" "json" }}
{{- $url := printf "https://www.youtube.com/oembed?%s" $q }}
{{- $data := dict }}
{{- with resources.GetRemote $url }}
{{- with .Err }}
{{- erroridf $remoteErrID "The %q shortcode was unable to get remote resource %q. %s. See %s" $.Name $url . $.Position }}
{{- else }}
{{- $data = .Content | transform.Unmarshal }}
{{- end }}
{{- else }}
{{- erroridf $remoteErrID "The %q shortcode was unable to get remote resource %q. See %s" $.Name $url $.Position }}
{{- end }}
{{/* Set defaults. */}}
{{- $allowFullScreen := "allowfullscreen" }}
{{- $autoplay := 0 }}
@ -50,7 +36,7 @@ Renders an embedded YouTube video.
{{- $loop := 0 }}
{{- $mute := 0 }}
{{- $start := 0 }}
{{- $title := $data.title }}
{{- $title := "YouTube video" }}
{{- /* Get arguments. */}}
{{- if in (slice "false" false 0) ($.Get "allowFullScreen") }}

View file

@ -71,7 +71,7 @@ var (
)
type templateExecHelper struct {
running bool // whether we're in server mode.
watching bool // whether we're in server/watch mode.
site reflect.Value
siteParams reflect.Value
funcs map[string]reflect.Value
@ -95,7 +95,7 @@ func (t *templateExecHelper) GetFunc(ctx context.Context, tmpl texttemplate.Prep
}
func (t *templateExecHelper) Init(ctx context.Context, tmpl texttemplate.Preparer) {
if t.running {
if t.watching {
_, ok := tmpl.(identity.IdentityProvider)
if ok {
t.trackDependencies(ctx, tmpl, "", reflect.Value{})
@ -129,7 +129,7 @@ func (t *templateExecHelper) GetMethod(ctx context.Context, tmpl texttemplate.Pr
name = "MainSections"
}
if t.running {
if t.watching {
ctx = t.trackDependencies(ctx, tmpl, name, receiver)
}
@ -151,7 +151,7 @@ func (t *templateExecHelper) GetMethod(ctx context.Context, tmpl texttemplate.Pr
}
func (t *templateExecHelper) OnCalled(ctx context.Context, tmpl texttemplate.Preparer, name string, args []reflect.Value, result reflect.Value) {
if !t.running {
if !t.watching {
return
}
@ -238,7 +238,7 @@ func newTemplateExecuter(d *deps.Deps) (texttemplate.Executer, map[string]reflec
}
exeHelper := &templateExecHelper{
running: d.Conf.Running(),
watching: d.Conf.Watching(),
funcs: funcsv,
site: reflect.ValueOf(d.Site),
siteParams: reflect.ValueOf(d.Site.Params()),

View file

@ -305,3 +305,109 @@ title: p2
"<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\"\n xmlns:xhtml=\"http://www.w3.org/1999/xhtml\">\n <url>\n <loc>/p2/</loc>\n </url>\n</urlset>\n",
)
}
// Issue 12418
func TestOpengraph(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
capitalizeListTitles = false
disableKinds = ['rss','sitemap']
languageCode = 'en-US'
[markup.goldmark.renderer]
unsafe = true
[params]
description = "m <em>n</em> and **o** can't."
[params.social]
facebook_admin = 'foo'
[taxonomies]
series = 'series'
tag = 'tags'
-- layouts/_default/list.html --
{{ template "_internal/opengraph.html" . }}
-- layouts/_default/single.html --
{{ template "_internal/opengraph.html" . }}
-- content/s1/p1.md --
---
title: p1
date: 2024-04-24T08:00:00-07:00
lastmod: 2024-04-24T11:00:00-07:00
images: [a.jpg,b.jpg]
audio: [c.mp3,d.mp3]
videos: [e.mp4,f.mp4]
series: [series-1]
tags: [t1,t2]
---
a <em>b</em> and **c** can't.
-- content/s1/p2.md --
---
title: p2
series: [series-1]
---
d <em>e</em> and **f** can't.
<!--more-->
-- content/s1/p3.md --
---
title: p3
series: [series-1]
summary: g <em>h</em> and **i** can't.
---
-- content/s1/p4.md --
---
title: p4
series: [series-1]
description: j <em>k</em> and **l** can't.
---
-- content/s1/p5.md --
---
title: p5
series: [series-1]
---
`
b := hugolib.Test(t, files)
b.AssertFileContent("public/s1/p1/index.html", `
<meta property="og:url" content="/s1/p1/">
<meta property="og:title" content="p1">
<meta property="og:description" content="a b and c cant.">
<meta property="og:locale" content="en-US">
<meta property="og:type" content="article">
<meta property="article:section" content="s1">
<meta property="article:published_time" content="2024-04-24T08:00:00-07:00">
<meta property="article:modified_time" content="2024-04-24T11:00:00-07:00">
<meta property="article:tag" content="t1">
<meta property="article:tag" content="t2">
<meta property="og:image" content="/a.jpg">
<meta property="og:image" content="/b.jpg">
<meta property="og:audio" content="/c.mp3">
<meta property="og:audio" content="/d.mp3">
<meta property="og:video" content="/e.mp4">
<meta property="og:video" content="/f.mp4">
<meta property="og:see_also" content="/s1/p2/">
<meta property="og:see_also" content="/s1/p3/">
<meta property="og:see_also" content="/s1/p4/">
<meta property="og:see_also" content="/s1/p5/">
<meta property="fb:admins" content="foo">
`,
)
b.AssertFileContent("public/s1/p2/index.html",
`<meta property="og:description" content="d e and f cant.">`,
)
b.AssertFileContent("public/s1/p3/index.html",
`<meta property="og:description" content="g h and i cant.">`,
)
// The markdown is intentionally not rendered to HTML.
b.AssertFileContent("public/s1/p4/index.html",
`<meta property="og:description" content="j k and **l** can&#39;t.">`,
)
// The markdown is intentionally not rendered to HTML.
b.AssertFileContent("public/s1/p5/index.html",
`<meta property="og:description" content="m n and **o** can&#39;t.">`,
)
}