Skip to content

Commit 47678d8

Browse files
jmooringbep
authored andcommitted
markup/goldmark: Enhance footnote extension with auto-prefixing option
This commit introduces a new option, enableAutoIDPrefix, to the Goldmark footnote extension. When enabled, it prepends a unique prefix to footnote IDs, preventing clashes when multiple documents are rendered together. This prefix is unique to each logical path, which means that the prefix is not unique across content dimensions such as language. This change also refactors the extension's configuration from a boolean to a struct. Closes #8045
1 parent 18b9b64 commit 47678d8

File tree

7 files changed

+122
-25
lines changed

7 files changed

+122
-25
lines changed

‎docs/data/docs.yaml‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1238,7 +1238,9 @@ config:
12381238
enable: false
12391239
superscript:
12401240
enable: false
1241-
footnote: true
1241+
footnote:
1242+
enable: true
1243+
enableAutoIDPrefix: false
12421244
linkify: true
12431245
linkifyProtocol: https
12441246
passthrough:

‎hugolib/page__meta.go‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -794,11 +794,9 @@ func (p *pageMeta) newContentConverter(ps *pageState, markup string) (converter.
794794
return converter.NopConverter, fmt.Errorf("no content renderer found for markup %q, page: %s", markup, ps.getPageInfoForError())
795795
}
796796

797-
var id string
798797
var filename string
799798
var path string
800799
if p.f != nil {
801-
id = p.f.UniqueID()
802800
filename = p.f.Filename()
803801
path = p.f.Path()
804802
} else {
@@ -822,7 +820,7 @@ func (p *pageMeta) newContentConverter(ps *pageState, markup string) (converter.
822820
converter.DocumentContext{
823821
Document: doc,
824822
DocumentLookup: documentLookup,
825-
DocumentID: id,
823+
DocumentID: hashing.XxHashFromStringHexEncoded(p.Path()),
826824
DocumentName: path,
827825
Filename: filename,
828826
},

‎markup/goldmark/convert.go‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,15 @@ func newMarkdown(pcfg converter.ProviderConfig) goldmark.Markdown {
176176
extensions = append(extensions, extension.DefinitionList)
177177
}
178178

179-
if cfg.Extensions.Footnote {
180-
extensions = append(extensions, extension.Footnote)
179+
if cfg.Extensions.Footnote.Enable {
180+
if cfg.Extensions.Footnote.EnableAutoIDPrefix {
181+
extensions = append(extensions, extension.NewFootnote(extension.WithFootnoteIDPrefixFunction(func(n ast.Node) []byte {
182+
documentID := n.OwnerDocument().Meta()["documentID"].(string)
183+
return []byte("h" + documentID)
184+
})))
185+
} else {
186+
extensions = append(extensions, extension.Footnote)
187+
}
181188
}
182189

183190
if cfg.Extensions.CJK.Enable {
@@ -262,6 +269,7 @@ func (c *goldmarkConverter) Parse(ctx converter.RenderContext) (converter.Result
262269
reader,
263270
parser.WithContext(pctx),
264271
)
272+
doc.OwnerDocument().AddMeta("documentID", c.ctx.DocumentID)
265273

266274
return parserResult{
267275
doc: doc,

‎markup/goldmark/goldmark_config/config.go‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,10 @@ var Default = Config{
4040
RightAngleQuote: "»",
4141
Apostrophe: "’",
4242
},
43-
Footnote: true,
43+
Footnote: Footnote{
44+
Enable: true,
45+
EnableAutoIDPrefix: false,
46+
},
4447
DefinitionList: true,
4548
Table: true,
4649
Strikethrough: true,
@@ -152,7 +155,7 @@ type LinkRenderHook struct {
152155

153156
type Extensions struct {
154157
Typographer Typographer
155-
Footnote bool
158+
Footnote Footnote
156159
DefinitionList bool
157160
Extras Extras
158161
Passthrough Passthrough
@@ -166,6 +169,12 @@ type Extensions struct {
166169
CJK CJK
167170
}
168171

172+
// Footnote holds footnote configuration.
173+
type Footnote struct {
174+
Enable bool
175+
EnableAutoIDPrefix bool
176+
}
177+
169178
// Typographer holds typographer configuration.
170179
type Typographer struct {
171180
// Whether to disable typographer.

‎markup/goldmark/goldmark_integration_test.go‎

Lines changed: 73 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -897,7 +897,7 @@ title: "p1"
897897
---
898898
# HTML comments
899899
900-
## Simple
900+
## Simple
901901
<!-- This is a comment -->
902902
903903
<!-- This is a comment indented -->
@@ -918,7 +918,7 @@ title: "p1"
918918
<img border="0" src="pic_trulli.jpg" alt="Trulli">
919919
-->
920920
921-
## XSS
921+
## XSS
922922
923923
<!-- --><script>alert("I just escaped the HTML comment")</script><!-- -->
924924
@@ -931,10 +931,10 @@ This is a <!-- hidden--> word.
931931
932932
This is a <!-- hidden --> word.
933933
934-
This is a <!--
934+
This is a <!--
935935
hidden --> word.
936936
937-
This is a <!--
937+
This is a <!--
938938
hidden
939939
--> word.
940940
@@ -961,3 +961,72 @@ hidden
961961
)
962962
b.AssertLogContains("! WARN")
963963
}
964+
965+
func TestFootnoteExtension(t *testing.T) {
966+
t.Parallel()
967+
968+
files := `
969+
-- hugo.toml --
970+
disableKinds = ['home','rss','section','sitemap','taxonomy','term']
971+
[markup.goldmark.extensions.footnote]
972+
enable = false
973+
enableAutoIDPrefix = false
974+
-- layouts/all.html --
975+
{{ .Content }}
976+
-- content/p1.md --
977+
---
978+
title: p1
979+
---
980+
foo[^1] and bar[^2]
981+
982+
[^1]: footnote one
983+
[^2]: footnote two
984+
-- content/p2.md --
985+
---
986+
title: p2
987+
---
988+
foo[^1] and bar[^2]
989+
990+
[^1]: footnote one
991+
[^2]: footnote two
992+
-- content/_content.gotmpl --
993+
{{ range slice 3 4 }}
994+
{{ $page := dict
995+
"content" (dict "mediaType" "text/markdown" "value" "foo[^1] and bar[^2]\n\n[^1]: footnote one\n[^2]: footnote two")
996+
"path" (printf "p%d" .)
997+
"title" (printf "p%d" .)
998+
}}
999+
{{ $.AddPage $page }}
1000+
{{ end }}
1001+
`
1002+
1003+
want := "<p>foo[^1] and bar[^2]</p>\n<p>[^1]: footnote one\n[^2]: footnote two</p>"
1004+
b := hugolib.Test(t, files)
1005+
b.AssertFileContent("public/p1/index.html", want)
1006+
b.AssertFileContent("public/p2/index.html", want)
1007+
b.AssertFileContent("public/p3/index.html", want)
1008+
b.AssertFileContent("public/p4/index.html", want)
1009+
1010+
files = strings.ReplaceAll(files, "enable = false", "enable = true")
1011+
want = "<p>foo<sup id=\"fnref:1\"><a href=\"#fn:1\" class=\"footnote-ref\" role=\"doc-noteref\">1</a></sup> and bar<sup id=\"fnref:2\"><a href=\"#fn:2\" class=\"footnote-ref\" role=\"doc-noteref\">2</a></sup></p>\n<div class=\"footnotes\" role=\"doc-endnotes\">\n<hr>\n<ol>\n<li id=\"fn:1\">\n<p>footnote one&#160;<a href=\"#fnref:1\" class=\"footnote-backref\" role=\"doc-backlink\">&#x21a9;&#xfe0e;</a></p>\n</li>\n<li id=\"fn:2\">\n<p>footnote two&#160;<a href=\"#fnref:2\" class=\"footnote-backref\" role=\"doc-backlink\">&#x21a9;&#xfe0e;</a></p>\n</li>\n</ol>\n</div>"
1012+
b = hugolib.Test(t, files)
1013+
b.AssertFileContent("public/p1/index.html", want)
1014+
b.AssertFileContent("public/p2/index.html", want)
1015+
b.AssertFileContent("public/p3/index.html", want)
1016+
b.AssertFileContent("public/p4/index.html", want)
1017+
1018+
files = strings.ReplaceAll(files, "enableAutoIDPrefix = false", "enableAutoIDPrefix = true")
1019+
b = hugolib.Test(t, files)
1020+
b.AssertFileContent("public/p1/index.html",
1021+
"<p>foo<sup id=\"hb5cdcabc9e678612fnref:1\"><a href=\"#hb5cdcabc9e678612fn:1\" class=\"footnote-ref\" role=\"doc-noteref\">1</a></sup> and bar<sup id=\"hb5cdcabc9e678612fnref:2\"><a href=\"#hb5cdcabc9e678612fn:2\" class=\"footnote-ref\" role=\"doc-noteref\">2</a></sup></p>\n<div class=\"footnotes\" role=\"doc-endnotes\">\n<hr>\n<ol>\n<li id=\"hb5cdcabc9e678612fn:1\">\n<p>footnote one&#160;<a href=\"#hb5cdcabc9e678612fnref:1\" class=\"footnote-backref\" role=\"doc-backlink\">&#x21a9;&#xfe0e;</a></p>\n</li>\n<li id=\"hb5cdcabc9e678612fn:2\">\n<p>footnote two&#160;<a href=\"#hb5cdcabc9e678612fnref:2\" class=\"footnote-backref\" role=\"doc-backlink\">&#x21a9;&#xfe0e;</a></p>\n</li>\n</ol>\n</div>",
1022+
)
1023+
b.AssertFileContent("public/p2/index.html",
1024+
"<p>foo<sup id=\"h58e8265a0c07b195fnref:1\"><a href=\"#h58e8265a0c07b195fn:1\" class=\"footnote-ref\" role=\"doc-noteref\">1</a></sup> and bar<sup id=\"h58e8265a0c07b195fnref:2\"><a href=\"#h58e8265a0c07b195fn:2\" class=\"footnote-ref\" role=\"doc-noteref\">2</a></sup></p>\n<div class=\"footnotes\" role=\"doc-endnotes\">\n<hr>\n<ol>\n<li id=\"h58e8265a0c07b195fn:1\">\n<p>footnote one&#160;<a href=\"#h58e8265a0c07b195fnref:1\" class=\"footnote-backref\" role=\"doc-backlink\">&#x21a9;&#xfe0e;</a></p>\n</li>\n<li id=\"h58e8265a0c07b195fn:2\">\n<p>footnote two&#160;<a href=\"#h58e8265a0c07b195fnref:2\" class=\"footnote-backref\" role=\"doc-backlink\">&#x21a9;&#xfe0e;</a></p>\n</li>\n</ol>\n</div>",
1025+
)
1026+
b.AssertFileContent("public/p3/index.html",
1027+
"<p>foo<sup id=\"h0aab769290d7e233fnref:1\"><a href=\"#h0aab769290d7e233fn:1\" class=\"footnote-ref\" role=\"doc-noteref\">1</a></sup> and bar<sup id=\"h0aab769290d7e233fnref:2\"><a href=\"#h0aab769290d7e233fn:2\" class=\"footnote-ref\" role=\"doc-noteref\">2</a></sup></p>\n<div class=\"footnotes\" role=\"doc-endnotes\">\n<hr>\n<ol>\n<li id=\"h0aab769290d7e233fn:1\">\n<p>footnote one&#160;<a href=\"#h0aab769290d7e233fnref:1\" class=\"footnote-backref\" role=\"doc-backlink\">&#x21a9;&#xfe0e;</a></p>\n</li>\n<li id=\"h0aab769290d7e233fn:2\">\n<p>footnote two&#160;<a href=\"#h0aab769290d7e233fnref:2\" class=\"footnote-backref\" role=\"doc-backlink\">&#x21a9;&#xfe0e;</a></p>\n</li>\n</ol>\n</div>",
1028+
)
1029+
b.AssertFileContent("public/p4/index.html",
1030+
"<p>foo<sup id=\"ha35b794ad6e8626cfnref:1\"><a href=\"#ha35b794ad6e8626cfn:1\" class=\"footnote-ref\" role=\"doc-noteref\">1</a></sup> and bar<sup id=\"ha35b794ad6e8626cfnref:2\"><a href=\"#ha35b794ad6e8626cfn:2\" class=\"footnote-ref\" role=\"doc-noteref\">2</a></sup></p>\n<div class=\"footnotes\" role=\"doc-endnotes\">\n<hr>\n<ol>\n<li id=\"ha35b794ad6e8626cfn:1\">\n<p>footnote one&#160;<a href=\"#ha35b794ad6e8626cfnref:1\" class=\"footnote-backref\" role=\"doc-backlink\">&#x21a9;&#xfe0e;</a></p>\n</li>\n<li id=\"ha35b794ad6e8626cfn:2\">\n<p>footnote two&#160;<a href=\"#ha35b794ad6e8626cfnref:2\" class=\"footnote-backref\" role=\"doc-backlink\">&#x21a9;&#xfe0e;</a></p>\n</li>\n</ol>\n</div>",
1031+
)
1032+
}

‎markup/markup_config/config.go‎

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -86,20 +86,26 @@ func normalizeConfig(m map[string]any) {
8686
}
8787
}
8888

89-
// Changed from a bool in 0.112.0.
89+
// Handle changes to the Goldmark configuration.
9090
v, err = maps.GetNestedParam("goldmark.extensions", ".", m)
9191
if err == nil {
9292
vm := maps.ToStringMap(v)
93-
const typographerKey = "typographer"
94-
if vv, found := vm[typographerKey]; found {
95-
if vvb, ok := vv.(bool); ok {
96-
if !vvb {
97-
vm[typographerKey] = goldmark_config.Typographer{
98-
Disable: true,
99-
}
100-
} else {
101-
delete(vm, typographerKey)
102-
}
93+
94+
// We changed the typographer extension config from a bool to a struct in 0.112.0.
95+
migrateGoldmarkConfig(vm, "typographer", goldmark_config.Typographer{Disable: true})
96+
97+
// We changed the footnote extension config from a bool to a struct in 0.151.0.
98+
migrateGoldmarkConfig(vm, "footnote", goldmark_config.Footnote{Enable: false})
99+
}
100+
}
101+
102+
func migrateGoldmarkConfig(vm map[string]any, key string, falseVal any) {
103+
if vv, found := vm[key]; found {
104+
if vvb, ok := vv.(bool); ok {
105+
if !vvb {
106+
vm[key] = falseVal
107+
} else {
108+
delete(vm, key)
103109
}
104110
}
105111
}

‎markup/markup_config/config_test.go‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,16 @@ func TestConfig(t *testing.T) {
5252
c.Assert(conf.AsciidocExt.Extensions[0], qt.Equals, "asciidoctor-html5s")
5353
})
5454

55-
c.Run("Decode legacy typographer", func(c *qt.C) {
55+
// We changed the typographer extension config from a bool to a struct in 0.112.0.
56+
// We changed the footnote extension config from a bool to a struct in 0.151.0.
57+
c.Run("Decode legacy Goldmark configs", func(c *qt.C) {
5658
c.Parallel()
5759
v := config.New()
5860

59-
// typographer was changed from a bool to a struct in 0.112.0.
6061
v.Set("markup", map[string]any{
6162
"goldmark": map[string]any{
6263
"extensions": map[string]any{
64+
"footnote": false,
6365
"typographer": false,
6466
},
6567
},
@@ -68,11 +70,13 @@ func TestConfig(t *testing.T) {
6870
conf, err := Decode(v)
6971

7072
c.Assert(err, qt.IsNil)
73+
c.Assert(conf.Goldmark.Extensions.Footnote.Enable, qt.Equals, false)
7174
c.Assert(conf.Goldmark.Extensions.Typographer.Disable, qt.Equals, true)
7275

7376
v.Set("markup", map[string]any{
7477
"goldmark": map[string]any{
7578
"extensions": map[string]any{
79+
"footnote": true,
7680
"typographer": true,
7781
},
7882
},
@@ -81,6 +85,7 @@ func TestConfig(t *testing.T) {
8185
conf, err = Decode(v)
8286

8387
c.Assert(err, qt.IsNil)
88+
c.Assert(conf.Goldmark.Extensions.Footnote.Enable, qt.Equals, true)
8489
c.Assert(conf.Goldmark.Extensions.Typographer.Disable, qt.Equals, false)
8590
c.Assert(conf.Goldmark.Extensions.Typographer.Ellipsis, qt.Equals, "&hellip;")
8691
})

0 commit comments

Comments
 (0)