mirror of
https://github.com/gohugoio/hugo.git
synced 2024-05-09 19:26:41 +00:00
Compare commits
16 commits
b6b8696ebc
...
d1a4a9d701
Author | SHA1 | Date | |
---|---|---|---|
d1a4a9d701 | |||
1961327536 | |||
cc3574ef4f | |||
fe84cc218e | |||
babcb339a8 | |||
7203a95a60 | |||
fb084390cd | |||
fb51b698b3 | |||
6b867972ec | |||
509ab08c1b | |||
2d75f539e1 | |||
15a4b9b337 | |||
10a8448eee | |||
722c486a34 | |||
f40f50ead0 | |||
c91c64a1aa |
2
LICENSE
2
LICENSE
|
@ -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.
|
||||
|
|
|
@ -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/).
|
||||
|
|
4
cache/dynacache/dynacache.go
vendored
4
cache/dynacache/dynacache.go
vendored
|
@ -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.
|
||||
|
|
|
@ -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 `)
|
||||
|
||||
|
|
|
@ -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",
|
||||
}
|
||||
|
|
|
@ -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
2
deps/deps.go
vendored
|
@ -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 {
|
||||
|
|
|
@ -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 [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
4
go.mod
|
@ -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
8
go.sum
|
@ -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=
|
||||
|
|
|
@ -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-"<>&\">text-"<>&</a>",
|
||||
"img alt=\"alt-"<>&\" src=\"/destination-%22%3C%3E\" title=\"title-"<>&\">",
|
||||
"><script>",
|
||||
)
|
||||
} else {
|
||||
b.AssertFileContent("public/index.html",
|
||||
"Link: <a href=\"/destination-%22%3C%3E\" title=\"title-"<>&\">text-"<>&</a>",
|
||||
"Image: <img src=\"/destination-%22%3C%3E\" alt=\"alt-"<>&\" title=\"title-"<>&\">",
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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|")
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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() != ""
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
6
markup/pandoc/testdata/bibliography.bib
vendored
Normal file
6
markup/pandoc/testdata/bibliography.bib
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
@article{Doe2022,
|
||||
author = "Jane Doe and Max Mustermann",
|
||||
title = "A Treatise on Hugo Tests",
|
||||
journal = "Hugo Websites",
|
||||
year = "2022",
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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 -}}
|
||||
|
|
|
@ -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 -}}
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -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 }}>
|
||||
|
|
|
@ -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") }}
|
||||
|
|
|
@ -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()),
|
||||
|
|
|
@ -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 can’t.">
|
||||
<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 can’t.">`,
|
||||
)
|
||||
|
||||
b.AssertFileContent("public/s1/p3/index.html",
|
||||
`<meta property="og:description" content="g h and i can’t.">`,
|
||||
)
|
||||
|
||||
// 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'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't.">`,
|
||||
)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue