Fix auto generated header ids so they don't contain e.g. hyperlink destinations (note)

This makes the header ids match the newly added dt ids.

Also make sure newlines are preserved in hooks' `.PlainText`.

Fixes #13405
Fixes #13410
This commit is contained in:
Bjørn Erik Pedersen 2025-02-16 21:52:46 +01:00
parent a2ca95629a
commit 24cc25552f
6 changed files with 59 additions and 14 deletions

View file

@ -350,3 +350,31 @@ Image: ![alt-"<>&](/destination-"<> 'title-"<>&')
})
}
}
// Issue 13410.
func TestRenderHooksMultilineTitlePlainText(t *testing.T) {
t.Parallel()
files := `
-- hugo.toml --
-- content/p1.md --
---
title: "p1"
---
First line.
Second line.
----------------
-- layouts/_default/_markup/render-heading.html --
Plain text: {{ .PlainText }}|Text: {{ .Text }}|
-- layouts/_default/single.html --
Content: {{ .Content}}|
}
`
b := Test(t, files)
b.AssertFileContent("public/p1/index.html",
"Content: Plain text: First line.\nSecond line.|",
"|Text: First line.\nSecond line.||\n",
)
}

View file

@ -157,7 +157,7 @@ title: "p1"
b := hugolib.Test(t, files)
b.AssertFileContent("public/p1/index.html",
"<h2 id=\"hello-testhttpsexamplecom\">\n Hello <a href=\"https://example.com\">Test</a>\n\n <a class=\"anchor\" href=\"#hello-testhttpsexamplecom\">#</a>\n</h2>",
"<h2 id=\"hello-test\">\n Hello <a href=\"https://example.com\">Test</a>\n\n <a class=\"anchor\" href=\"#hello-test\">#</a>\n</h2>",
)
}

View file

@ -1,6 +1,8 @@
package attributes
import (
"strings"
"github.com/gohugoio/hugo/markup/goldmark/goldmark_config"
"github.com/gohugoio/hugo/markup/goldmark/internal/render"
"github.com/yuin/goldmark"
@ -181,12 +183,17 @@ func (a *transformer) generateAutoID(n ast.Node, reader text.Reader, pc parser.C
}
// Markdown settext headers can have multiple lines, use the last line for the ID.
func textHeadingID(node *ast.Heading, reader text.Reader) []byte {
var line []byte
lastIndex := node.Lines().Len() - 1
if lastIndex > -1 {
lastLine := node.Lines().At(lastIndex)
line = lastLine.Value(reader.Source())
func textHeadingID(n *ast.Heading, reader text.Reader) []byte {
text := render.TextPlain(n, reader.Source())
if n.Lines().Len() > 1 {
// For multiline headings, Goldmark's extension for headings returns the last line.
// We have a slightly different approach, but in most cases the end result should be the same.
// Instead of looking at the text segments in Lines (see #13405 for issues with that),
// we split the text above and use the last line.
parts := strings.Split(text, "\n")
text = parts[len(parts)-1]
}
return line
return []byte(text)
}

View file

@ -47,10 +47,12 @@ foo [something](/a/b/) bar
Ā ā Ă ă Ą ą Ć ć Ĉ ĉ Ċ ċ Č č Ď
: Testing accents.
Mutiline set text header
Multiline set text header
Second line
---------------
## Example [hyperlink](https://example.com/) in a header
-- layouts/_default/single.html --
{{ .Content }}|Identifiers: {{ .Fragments.Identifiers }}|
`
@ -68,7 +70,8 @@ Second line
`<dt id="my-title-1">My Title</dt>`,
`<dt id="term">良善天父</dt>`,
`<dt id="a-a-a-a-a-a-c-c-c-c-c-c-c-c-d">Ā ā Ă ă Ą ą Ć ć Ĉ ĉ Ċ ċ Č č Ď</dt>`,
`<h2 id="second-line">Mutiline set text header`,
"|Identifiers: [a-a-a-a-a-a-c-c-c-c-c-c-c-c-d base-name base-name-1 foo-something-bar foobar my-title my-title-1 second-line term title-with-id title-with-id]|",
`<h2 id="second-line">`,
`<h2 id="example-hyperlink-in-a-header">`,
"|Identifiers: [a-a-a-a-a-a-c-c-c-c-c-c-c-c-d base-name base-name-1 example-hyperlink-in-a-header foo-something-bar foobar my-title my-title-1 second-line term title-with-id title-with-id]|",
)
}

View file

@ -1,4 +1,4 @@
// Copyright 2024 The Hugo Authors. All rights reserved.
// Copyright 2025 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -20,6 +20,7 @@ import (
"sync"
bp "github.com/gohugoio/hugo/bufferpool"
east "github.com/yuin/goldmark-emoji/ast"
htext "github.com/gohugoio/hugo/common/text"
"github.com/gohugoio/hugo/tpl"
@ -282,6 +283,7 @@ func textPlainTo(c ast.Node, source []byte, buf *bytes.Buffer) {
if c == nil {
return
}
switch c := c.(type) {
case *ast.RawHTML:
s := strings.TrimSpace(tpl.StripHTML(string(c.Segments.Value(source))))
@ -290,6 +292,11 @@ func textPlainTo(c ast.Node, source []byte, buf *bytes.Buffer) {
buf.Write(c.Value)
case *ast.Text:
buf.Write(c.Segment.Value(source))
if c.HardLineBreak() || c.SoftLineBreak() {
buf.WriteByte('\n')
}
case *east.Emoji:
buf.WriteString(string(c.ShortName))
default:
textPlainTo(c.FirstChild(), source, buf)
}

View file

@ -239,12 +239,12 @@ title: p7 (emoji)
// image
b.AssertFileContent("public/p3/index.html", `
<li><a href="#an-image-kittenajpg">An image <img src="a.jpg" alt="kitten" /></a></li>
<li><a href="#an-image-kitten">An image <img src="a.jpg" alt="kitten" /></a></li>
`)
// raw html
b.AssertFileContent("public/p4/index.html", `
<li><a href="#some-spanrawspan-html">Some <span>raw</span> HTML</a></li>
<li><a href="#some-raw-html">Some <span>raw</span> HTML</a></li>
`)
// typographer