From ab295e62e4c708d87600efa77fca44195bacfa17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nico=20Schl=C3=B6mer?= Date: Wed, 4 Nov 2026 05:51:12 +0000 Subject: [PATCH] fix(markup): make heading anchors match GitHub for whitespace and combining marks (#40195) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Heading anchors now keep the same characters as GitHub: letters, marks, decimal and letter numbers, connector punctuation and alphabetic symbols, with ASCII spaces turned into `-`. The rule follows [github-slugger](https://github.com/Flet/github-slugger), whose fixtures are generated from real GitHub renders. - Other whitespace, including newlines in multi-line headings, is dropped instead of becoming `-`, so `1ab` gets `#1ab` - Combining marks are kept, so `हिन्दी` no longer becomes `हनद` - Other numbers like `²` and `①` are dropped Fixes https://github.com/go-gitea/gitea/issues/27754 This PR was written with Claude Code. --------- Co-authored-by: Claude Opus 5.5 Co-authored-by: silverwind --- modules/markup/common/footnote.go | 19 ++++++++----------- modules/markup/common/footnote_test.go | 5 ++++- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/modules/markup/common/footnote.go b/modules/markup/common/footnote.go index ef89a7046a3..4f1c0ed9363 100644 --- a/modules/markup/common/footnote.go +++ b/modules/markup/common/footnote.go @@ -26,18 +26,15 @@ import ( // and more closely matches the output from the shurcooL sanitizer // In particular Unicode letters and numbers are a lot more than a-zA-Z0-9... func CleanValue(value []byte) []byte { - value = bytes.TrimSpace(value) - rs := bytes.Runes(value) - result := make([]rune, 0, len(rs)) - for _, r := range rs { - if unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_' || r == '-' { - result = append(result, unicode.ToLower(r)) + return bytes.Map(func(r rune) rune { + switch { + case r == ' ': + return '-' + case r == '-' || unicode.In(r, unicode.L, unicode.M, unicode.Nd, unicode.Nl, unicode.Pc, unicode.Other_Alphabetic): + return unicode.ToLower(r) } - if unicode.IsSpace(r) { - result = append(result, '-') - } - } - return []byte(string(result)) + return -1 + }, bytes.TrimSpace(value)) } // Most of what follows is a subtly changed version of github.com/yuin/goldmark/extension/footnote.go diff --git a/modules/markup/common/footnote_test.go b/modules/markup/common/footnote_test.go index 2327a7b14b2..b8e8e7a4f10 100644 --- a/modules/markup/common/footnote_test.go +++ b/modules/markup/common/footnote_test.go @@ -50,9 +50,12 @@ func TestCleanValue(t *testing.T) { {"tes\\a", "tesa"}, {"tes/a", "tesa"}, {"a啊啊b", "a啊啊b"}, - {"c🤔️🤔️d", "cd"}, + {"c🤔️🤔️d", "c\ufe0f\ufe0fd"}, {"a⚡a", "aa"}, {"e.~f", "ef"}, + {"a \u2003 b", "a--b"}, + {"हिन्दी", "हिन्दी"}, + {"m²", "m"}, } for _, test := range tests { assert.Equal(t, []byte(test.expect), CleanValue([]byte(test.param)), test.param)