mirror of
https://github.com/gohugoio/hugo.git
synced 2024-11-21 20:46:30 -05:00
parent
80595bbe3e
commit
5b7cb258ec
15 changed files with 229 additions and 42 deletions
2
check_gofmt.sh
Executable file
2
check_gofmt.sh
Executable file
|
@ -0,0 +1,2 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
diff <(gofmt -d .) <(printf '')
|
|
@ -387,6 +387,11 @@ func ToSlashTrimLeading(s string) string {
|
||||||
return strings.TrimPrefix(filepath.ToSlash(s), "/")
|
return strings.TrimPrefix(filepath.ToSlash(s), "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ToSlashTrimTrailing is just a filepath.ToSlash with an added / suffix trimmer.
|
||||||
|
func ToSlashTrimTrailing(s string) string {
|
||||||
|
return strings.TrimSuffix(filepath.ToSlash(s), "/")
|
||||||
|
}
|
||||||
|
|
||||||
// ToSlashPreserveLeading converts the path given to a forward slash separated path
|
// ToSlashPreserveLeading converts the path given to a forward slash separated path
|
||||||
// and preserves the leading slash if present trimming any trailing slash.
|
// and preserves the leading slash if present trimming any trailing slash.
|
||||||
func ToSlashPreserveLeading(s string) string {
|
func ToSlashPreserveLeading(s string) string {
|
||||||
|
|
|
@ -107,3 +107,8 @@ type LowHigh struct {
|
||||||
|
|
||||||
// This is only used for debugging purposes.
|
// This is only used for debugging purposes.
|
||||||
var InvocationCounter atomic.Int64
|
var InvocationCounter atomic.Int64
|
||||||
|
|
||||||
|
// NewTrue returns a pointer to b.
|
||||||
|
func NewBool(b bool) *bool {
|
||||||
|
return &b
|
||||||
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ import (
|
||||||
"github.com/gohugoio/hugo/common/loggers"
|
"github.com/gohugoio/hugo/common/loggers"
|
||||||
"github.com/gohugoio/hugo/common/maps"
|
"github.com/gohugoio/hugo/common/maps"
|
||||||
"github.com/gohugoio/hugo/common/paths"
|
"github.com/gohugoio/hugo/common/paths"
|
||||||
|
"github.com/gohugoio/hugo/common/types"
|
||||||
"github.com/gohugoio/hugo/common/urls"
|
"github.com/gohugoio/hugo/common/urls"
|
||||||
"github.com/gohugoio/hugo/config"
|
"github.com/gohugoio/hugo/config"
|
||||||
"github.com/gohugoio/hugo/config/privacy"
|
"github.com/gohugoio/hugo/config/privacy"
|
||||||
|
@ -899,6 +900,18 @@ func fromLoadConfigResult(fs afero.Fs, logger loggers.Logger, res config.LoadCon
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Adjust Goldmark config defaults for multilingual, single-host sites.
|
||||||
|
if len(languagesConfig) > 1 && !isMultiHost && !clone.Markup.Goldmark.DuplicateResourceFiles {
|
||||||
|
if !clone.Markup.Goldmark.DuplicateResourceFiles {
|
||||||
|
if clone.Markup.Goldmark.RenderHooks.Link.EnableDefault == nil {
|
||||||
|
clone.Markup.Goldmark.RenderHooks.Link.EnableDefault = types.NewBool(true)
|
||||||
|
}
|
||||||
|
if clone.Markup.Goldmark.RenderHooks.Image.EnableDefault == nil {
|
||||||
|
clone.Markup.Goldmark.RenderHooks.Image.EnableDefault = types.NewBool(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
langConfigMap[k] = clone
|
langConfigMap[k] = clone
|
||||||
case maps.ParamsMergeStrategy:
|
case maps.ParamsMergeStrategy:
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
package hugolib
|
package hugolib
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -169,3 +170,74 @@ Self Fragments: [d e f]
|
||||||
P1 Fragments: [b c z]
|
P1 Fragments: [b c z]
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDefaultRenderHooksMultilingual(t *testing.T) {
|
||||||
|
files := `
|
||||||
|
-- hugo.toml --
|
||||||
|
baseURL = "https://example.org"
|
||||||
|
disableKinds = ["taxonomy", "term", "RSS", "sitemap", "robotsTXT"]
|
||||||
|
defaultContentLanguage = "nn"
|
||||||
|
defaultContentLanguageInSubdir = true
|
||||||
|
[markup]
|
||||||
|
[markup.goldmark]
|
||||||
|
duplicateResourceFiles = false
|
||||||
|
[markup.goldmark.renderhooks]
|
||||||
|
[markup.goldmark.renderhooks.link]
|
||||||
|
#enableDefault = false
|
||||||
|
[markup.goldmark.renderhooks.image]
|
||||||
|
#enableDefault = false
|
||||||
|
[languages]
|
||||||
|
[languages.en]
|
||||||
|
weight = 1
|
||||||
|
[languages.nn]
|
||||||
|
weight = 2
|
||||||
|
-- content/p1/index.md --
|
||||||
|
---
|
||||||
|
title: "p1"
|
||||||
|
---
|
||||||
|
[P2](p2)
|
||||||
|
![Pixel](pixel.png)
|
||||||
|
-- content/p2/index.md --
|
||||||
|
---
|
||||||
|
title: "p2"
|
||||||
|
---
|
||||||
|
[P1](p1)
|
||||||
|
![Pixel](pixel.jpg)
|
||||||
|
-- content/p1/index.en.md --
|
||||||
|
---
|
||||||
|
title: "p1 en"
|
||||||
|
---
|
||||||
|
[P2](p2)
|
||||||
|
![Pixel](pixel.png)
|
||||||
|
-- content/p2/index.en.md --
|
||||||
|
---
|
||||||
|
title: "p2 en"
|
||||||
|
---
|
||||||
|
[P1](p1)
|
||||||
|
![Pixel](pixel.png)
|
||||||
|
|
||||||
|
-- content/p1/pixel.nn.png --
|
||||||
|
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
|
||||||
|
-- content/p2/pixel.png --
|
||||||
|
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
|
||||||
|
-- layouts/_default/single.html --
|
||||||
|
{{ .Title }}|{{ .Content }}|$
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
t.Run("Default multilingual", func(t *testing.T) {
|
||||||
|
b := Test(t, files)
|
||||||
|
|
||||||
|
b.AssertFileContent("public/nn/p1/index.html",
|
||||||
|
"p1|<p><a href=\"/nn/p2/\">P2</a\n></p>", "<img alt=\"Pixel\" src=\"/nn/p1/pixel.nn.png\">")
|
||||||
|
b.AssertFileContent("public/en/p1/index.html",
|
||||||
|
"p1 en|<p><a href=\"/en/p2/\">P2</a\n></p>", "<img alt=\"Pixel\" src=\"/nn/p1/pixel.nn.png\">")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Disabled", func(t *testing.T) {
|
||||||
|
b := Test(t, strings.ReplaceAll(files, "#enableDefault = false", "enableDefault = false"))
|
||||||
|
|
||||||
|
b.AssertFileContent("public/nn/p1/index.html",
|
||||||
|
"p1|<p><a href=\"p2\">P2</a>", "<img src=\"pixel.png\" alt=\"Pixel\">")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -470,6 +470,21 @@ func (pco *pageContentOutput) initRenderHooks() error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
if found {
|
||||||
|
if isitp, ok := templ.(tpl.IsInternalTemplateProvider); ok && isitp.IsInternalTemplate() {
|
||||||
|
renderHookConfig := pco.po.p.s.conf.Markup.Goldmark.RenderHooks
|
||||||
|
switch templ.Name() {
|
||||||
|
case "_default/_markup/render-link.html":
|
||||||
|
if !renderHookConfig.Link.IsEnableDefault() {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
case "_default/_markup/render-image.html":
|
||||||
|
if !renderHookConfig.Image.IsEnableDefault() {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
return templ, found
|
return templ, found
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -56,7 +56,7 @@ func (c *pageFinder) getPageRef(context page.Page, ref string) (page.Page, error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *pageFinder) getPage(context page.Page, ref string) (page.Page, error) {
|
func (c *pageFinder) getPage(context page.Page, ref string) (page.Page, error) {
|
||||||
n, err := c.getContentNode(context, false, filepath.ToSlash(ref))
|
n, err := c.getContentNode(context, false, paths.ToSlashTrimTrailing(ref))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -413,6 +413,10 @@ title: p2
|
||||||
func TestPageGetPageVariations(t *testing.T) {
|
func TestPageGetPageVariations(t *testing.T) {
|
||||||
files := `
|
files := `
|
||||||
-- hugo.toml --
|
-- hugo.toml --
|
||||||
|
-- content/s1/_index.md --
|
||||||
|
---
|
||||||
|
title: s1 section
|
||||||
|
---
|
||||||
-- content/s1/p1/index.md --
|
-- content/s1/p1/index.md --
|
||||||
---
|
---
|
||||||
title: p1
|
title: p1
|
||||||
|
@ -430,6 +434,8 @@ title: p3
|
||||||
title: p2_root
|
title: p2_root
|
||||||
---
|
---
|
||||||
-- layouts/index.html --
|
-- layouts/index.html --
|
||||||
|
/s1: {{ with .GetPage "/s1" }}{{ .Title }}{{ end }}|
|
||||||
|
/s1/: {{ with .GetPage "/s1/" }}{{ .Title }}{{ end }}|
|
||||||
/s1/p2.md: {{ with .GetPage "/s1/p2.md" }}{{ .Title }}{{ end }}|
|
/s1/p2.md: {{ with .GetPage "/s1/p2.md" }}{{ .Title }}{{ end }}|
|
||||||
/s1/p2: {{ with .GetPage "/s1/p2" }}{{ .Title }}{{ end }}|
|
/s1/p2: {{ with .GetPage "/s1/p2" }}{{ .Title }}{{ end }}|
|
||||||
/s1/p1/index.md: {{ with .GetPage "/s1/p1/index.md" }}{{ .Title }}{{ end }}|
|
/s1/p1/index.md: {{ with .GetPage "/s1/p1/index.md" }}{{ .Title }}{{ end }}|
|
||||||
|
@ -444,6 +450,8 @@ p1/index.md: {{ with .GetPage "p1/index.md" }}{{ .Title }}{{ end }}|
|
||||||
b := Test(t, files)
|
b := Test(t, files)
|
||||||
|
|
||||||
b.AssertFileContent("public/index.html", `
|
b.AssertFileContent("public/index.html", `
|
||||||
|
/s1: s1 section|
|
||||||
|
/s1/: s1 section|
|
||||||
/s1/p2.md: p2|
|
/s1/p2.md: p2|
|
||||||
/s1/p2: p2|
|
/s1/p2: p2|
|
||||||
/s1/p1/index.md: p1|
|
/s1/p1/index.md: p1|
|
||||||
|
|
39
magefile.go
39
magefile.go
|
@ -185,42 +185,15 @@ func TestRace() error {
|
||||||
|
|
||||||
// Run gofmt linter
|
// Run gofmt linter
|
||||||
func Fmt() error {
|
func Fmt() error {
|
||||||
if !isGoLatest() {
|
if !isGoLatest() && !isUnix() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
pkgs, err := hugoPackages()
|
s, err := sh.Output("./check_gofmt.sh")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
|
||||||
}
|
|
||||||
failed := false
|
|
||||||
first := true
|
|
||||||
for _, pkg := range pkgs {
|
|
||||||
files, err := filepath.Glob(filepath.Join(pkg, "*.go"))
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
for _, f := range files {
|
|
||||||
// gofmt doesn't exit with non-zero when it finds unformatted code
|
|
||||||
// so we have to explicitly look for output, and if we find any, we
|
|
||||||
// should fail this target.
|
|
||||||
s, err := sh.Output("gofmt", "-l", f)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("ERROR: running gofmt on %q: %v\n", f, err)
|
|
||||||
failed = true
|
|
||||||
}
|
|
||||||
if s != "" {
|
|
||||||
if first {
|
|
||||||
fmt.Println("The following files are not gofmt'ed:")
|
|
||||||
first = false
|
|
||||||
}
|
|
||||||
failed = true
|
|
||||||
fmt.Println(s)
|
fmt.Println(s)
|
||||||
|
return fmt.Errorf("gofmt needs to be run: %s", err)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
if failed {
|
|
||||||
return errors.New("improperly formatted go files")
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -332,6 +305,10 @@ func isGoLatest() bool {
|
||||||
return strings.Contains(runtime.Version(), "1.21")
|
return strings.Contains(runtime.Version(), "1.21")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isUnix() bool {
|
||||||
|
return runtime.GOOS != "windows"
|
||||||
|
}
|
||||||
|
|
||||||
func isCI() bool {
|
func isCI() bool {
|
||||||
return os.Getenv("CI") != ""
|
return os.Getenv("CI") != ""
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,10 +73,39 @@ var Default = Config{
|
||||||
|
|
||||||
// Config configures Goldmark.
|
// Config configures Goldmark.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
DuplicateResourceFiles bool
|
|
||||||
Renderer Renderer
|
Renderer Renderer
|
||||||
Parser Parser
|
Parser Parser
|
||||||
Extensions Extensions
|
Extensions Extensions
|
||||||
|
DuplicateResourceFiles bool
|
||||||
|
RenderHooks RenderHooks
|
||||||
|
}
|
||||||
|
|
||||||
|
// RenderHooks contains configuration for Goldmark render hooks.
|
||||||
|
type RenderHooks struct {
|
||||||
|
Image ImageRenderHook
|
||||||
|
Link LinkRenderHook
|
||||||
|
}
|
||||||
|
|
||||||
|
// ImageRenderHook contains configuration for the image render hook.
|
||||||
|
type ImageRenderHook struct {
|
||||||
|
// Enable the default image render hook.
|
||||||
|
// We need to know if it is set or not, hence the pointer.
|
||||||
|
EnableDefault *bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h ImageRenderHook) IsEnableDefault() bool {
|
||||||
|
return h.EnableDefault != nil && *h.EnableDefault
|
||||||
|
}
|
||||||
|
|
||||||
|
// LinkRenderHook contains configuration for the link render hook.
|
||||||
|
type LinkRenderHook struct {
|
||||||
|
// Disable the default image render hook.
|
||||||
|
// We need to know if it is set or not, hence the pointer.
|
||||||
|
EnableDefault *bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h LinkRenderHook) IsEnableDefault() bool {
|
||||||
|
return h.EnableDefault != nil && *h.EnableDefault
|
||||||
}
|
}
|
||||||
|
|
||||||
type Extensions struct {
|
type Extensions struct {
|
||||||
|
|
|
@ -25,6 +25,10 @@ type FileInfo interface {
|
||||||
Filename() string
|
Filename() string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type IsInternalTemplateProvider interface {
|
||||||
|
IsInternalTemplate() bool
|
||||||
|
}
|
||||||
|
|
||||||
type ParseInfo struct {
|
type ParseInfo struct {
|
||||||
// Set for shortcode templates with any {{ .Inner }}
|
// Set for shortcode templates with any {{ .Inner }}
|
||||||
IsInner bool
|
IsInner bool
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
{{- $u := urls.Parse .Destination -}}
|
||||||
|
{{- $src := $u.String -}}
|
||||||
|
{{- if not $u.IsAbs -}}
|
||||||
|
{{- with or (.Page.Resources.Get $u.Path) (resources.Get $u.Path) -}}
|
||||||
|
{{- $src = .RelPermalink -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- $attributes := dict "alt" .Text "src" $src "title" .Title -}}
|
||||||
|
<img
|
||||||
|
{{- range $k, $v := $attributes -}}
|
||||||
|
{{- if $v -}}
|
||||||
|
{{- printf " %s=%q" $k $v | safeHTMLAttr -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}>
|
||||||
|
{{- /**/ -}}
|
|
@ -0,0 +1,26 @@
|
||||||
|
{{- $u := urls.Parse .Destination -}}
|
||||||
|
{{- $href := $u.String -}}
|
||||||
|
{{- if not $u.IsAbs -}}
|
||||||
|
{{- with or
|
||||||
|
($.Page.GetPage $u.Path)
|
||||||
|
($.Page.Resources.Get $u.Path)
|
||||||
|
(resources.Get $u.Path)
|
||||||
|
-}}
|
||||||
|
{{- $href = .RelPermalink -}}
|
||||||
|
{{- with $u.RawQuery -}}
|
||||||
|
{{- $href = printf "%s?%s" $href . -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- with $u.Fragment -}}
|
||||||
|
{{- $href = printf "%s#%s" $href . -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- $attributes := dict "href" $href "title" .Title -}}
|
||||||
|
<a
|
||||||
|
{{- range $k, $v := $attributes -}}
|
||||||
|
{{- if $v -}}
|
||||||
|
{{- printf " %s=%q" $k $v | safeHTMLAttr -}}
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
>{{ .Text | safeHTML }}</a>
|
||||||
|
{{- /**/ -}}
|
|
@ -55,6 +55,7 @@ const (
|
||||||
|
|
||||||
shortcodesPathPrefix = "shortcodes/"
|
shortcodesPathPrefix = "shortcodes/"
|
||||||
internalPathPrefix = "_internal/"
|
internalPathPrefix = "_internal/"
|
||||||
|
embeddedPathPrefix = "_embedded/"
|
||||||
baseFileBase = "baseof"
|
baseFileBase = "baseof"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -517,10 +518,18 @@ func (t *templateHandler) findLayout(d layouts.LayoutDescriptor, f output.Format
|
||||||
|
|
||||||
func (t *templateHandler) newTemplateInfo(name, tpl string) templateInfo {
|
func (t *templateHandler) newTemplateInfo(name, tpl string) templateInfo {
|
||||||
var isText bool
|
var isText bool
|
||||||
|
var isEmbedded bool
|
||||||
|
|
||||||
|
if strings.HasPrefix(name, embeddedPathPrefix) {
|
||||||
|
isEmbedded = true
|
||||||
|
name = strings.TrimPrefix(name, embeddedPathPrefix)
|
||||||
|
}
|
||||||
|
|
||||||
name, isText = t.nameIsText(name)
|
name, isText = t.nameIsText(name)
|
||||||
return templateInfo{
|
return templateInfo{
|
||||||
name: name,
|
name: name,
|
||||||
isText: isText,
|
isText: isText,
|
||||||
|
isEmbedded: isEmbedded,
|
||||||
template: tpl,
|
template: tpl,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -772,7 +781,7 @@ func (t *templateHandler) loadEmbedded() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, found := t.Lookup(templateName); !found {
|
if _, found := t.Lookup(templateName); !found {
|
||||||
if err := t.AddTemplate(templateName, templ); err != nil {
|
if err := t.AddTemplate(embeddedPathPrefix+templateName, templ); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -781,7 +790,7 @@ func (t *templateHandler) loadEmbedded() error {
|
||||||
// TODO(bep) avoid reparsing these aliases
|
// TODO(bep) avoid reparsing these aliases
|
||||||
for _, alias := range aliases {
|
for _, alias := range aliases {
|
||||||
alias = internalPathPrefix + alias
|
alias = internalPathPrefix + alias
|
||||||
if err := t.AddTemplate(alias, templ); err != nil {
|
if err := t.AddTemplate(embeddedPathPrefix+alias, templ); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1026,6 +1035,8 @@ func (t *templateNamespace) parse(info templateInfo) (*templateState, error) {
|
||||||
return ts, nil
|
return ts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ tpl.IsInternalTemplateProvider = (*templateState)(nil)
|
||||||
|
|
||||||
type templateState struct {
|
type templateState struct {
|
||||||
tpl.Template
|
tpl.Template
|
||||||
|
|
||||||
|
@ -1037,6 +1048,10 @@ type templateState struct {
|
||||||
baseInfo templateInfo // Set when a base template is used.
|
baseInfo templateInfo // Set when a base template is used.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *templateState) IsInternalTemplate() bool {
|
||||||
|
return t.info.isEmbedded
|
||||||
|
}
|
||||||
|
|
||||||
func (t *templateState) GetIdentity() identity.Identity {
|
func (t *templateState) GetIdentity() identity.Identity {
|
||||||
return t.id
|
return t.id
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,7 @@ type templateInfo struct {
|
||||||
name string
|
name string
|
||||||
template string
|
template string
|
||||||
isText bool // HTML or plain text template.
|
isText bool // HTML or plain text template.
|
||||||
|
isEmbedded bool
|
||||||
|
|
||||||
meta *hugofs.FileMeta
|
meta *hugofs.FileMeta
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue