enhance(emoji): update to Unicode 17, unify and lazy-load emoji data (#39363)

Generate emoji data from Unicode 17's `emoji-test.txt`, keeping existing
aliases. `public/assets/emoji.json` is now the single emoji data file,
also loaded by the backend. Rendered emoji drop their `aria-label`, the
dark theme inverts key on a new `data-alias` attribute instead.

Skin tone variants and their Gitea-only aliases are removed, GitHub has
none either.

Emoji autocompletion is now lazy-loaded with the markdown editor,
shrinking the index JS chunk from 653KB to 563KB.

---------

Signed-off-by: silverwind <me@silverwind.io>
Signed-off-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: bircni <bircni@icloud.com>
This commit is contained in:
silverwind
2026-09-24 20:23:15 +00:00
committed by GitHub
co-authored by wxiaoguang bircni
parent 05f049e8bb
commit 64f31d9b70
24 changed files with 2185 additions and 15287 deletions
+4 -7
View File
@@ -14,16 +14,14 @@ import (
"golang.org/x/net/html/atom"
)
func createEmoji(ctx *RenderContext, content, name string) *html.Node {
func createEmoji(ctx *RenderContext, content, alias string) *html.Node {
span := &html.Node{
Type: html.ElementNode,
Data: atom.Span.String(),
Attr: []html.Attribute{},
}
span.Attr = append(span.Attr, ctx.RenderInternal.NodeSafeAttr("class", "emoji"))
if name != "" {
span.Attr = append(span.Attr, html.Attribute{Key: "aria-label", Val: name})
}
span.Attr = append(span.Attr, html.Attribute{Key: "data-alias", Val: alias})
text := &html.Node{
Type: html.TextNode,
@@ -41,7 +39,6 @@ func createCustomEmoji(ctx *RenderContext, alias string) *html.Node {
Attr: []html.Attribute{},
}
span.Attr = append(span.Attr, ctx.RenderInternal.NodeSafeAttr("class", "emoji"))
span.Attr = append(span.Attr, html.Attribute{Key: "aria-label", Val: alias})
img := &html.Node{
Type: html.ElementNode,
@@ -88,7 +85,7 @@ func emojiShortCodeProcessor(ctx *RenderContext, node *html.Node) {
converted := emoji.FromAlias(alias)
if converted != nil {
// standard emoji
replaceContent(node, m[0], m[1], createEmoji(ctx, converted.Emoji, converted.Description))
replaceContent(node, m[0], m[1], createEmoji(ctx, converted.Emoji, converted.Aliases[0]))
node = node.NextSibling.NextSibling
start = 0 // restart searching start since node has changed
} else if _, exist := setting.UI.CustomEmojisMap[alias]; exist {
@@ -116,7 +113,7 @@ func emojiProcessor(ctx *RenderContext, node *html.Node) {
start = m[1]
val := emoji.FromCode(codepoint)
if val != nil {
replaceContent(node, m[0], m[1], createEmoji(ctx, codepoint, val.Description))
replaceContent(node, m[0], m[1], createEmoji(ctx, codepoint, val.Aliases[0]))
node = node.NextSibling.NextSibling
start = 0
}
+26 -21
View File
@@ -9,14 +9,17 @@ import (
"testing"
"gitea.dev/modules/emoji"
"gitea.dev/modules/json"
"gitea.dev/modules/markup"
"gitea.dev/modules/markup/common"
"gitea.dev/modules/markup/markdown"
"gitea.dev/modules/public"
"gitea.dev/modules/setting"
testModule "gitea.dev/modules/test"
"gitea.dev/modules/util"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var (
@@ -334,45 +337,47 @@ func TestRender_emoji(t *testing.T) {
}
// Make sure we can successfully match every emoji in our dataset with regex
for i := range emoji.GemojiData {
test(
emoji.GemojiData[i].Emoji,
`<p><span class="emoji" aria-label="`+emoji.GemojiData[i].Description+`">`+emoji.GemojiData[i].Emoji+`</span></p>`)
}
for i := range emoji.GemojiData {
test(
":"+emoji.GemojiData[i].Aliases[0]+":",
`<p><span class="emoji" aria-label="`+emoji.GemojiData[i].Description+`">`+emoji.GemojiData[i].Emoji+`</span></p>`)
data, err := public.AssetFS().ReadFile("assets", "emoji.json")
require.NoError(t, err)
var emojis []emoji.Emoji
require.NoError(t, json.Unmarshal(data, &emojis))
for _, e := range emojis {
expected := `<p><span class="emoji" data-alias="` + e.Aliases[0] + `">` + e.Emoji + `</span></p>`
test(e.Emoji, expected)
test(":"+e.Aliases[0]+":", expected)
}
// Text that should be turned into or recognized as emoji
test(
":gitea:",
`<p><span class="emoji" aria-label="gitea"><img alt=":gitea:" src="`+setting.StaticURLPrefix+`/assets/img/emoji/gitea.png"/></span></p>`)
`<p><span class="emoji"><img alt=":gitea:" src="`+setting.StaticURLPrefix+`/assets/img/emoji/gitea.png"/></span></p>`)
test(
":custom-emoji:",
`<p>:custom-emoji:</p>`)
setting.UI.CustomEmojisMap["custom-emoji"] = ":custom-emoji:"
test(
":custom-emoji:",
`<p><span class="emoji" aria-label="custom-emoji"><img alt=":custom-emoji:" src="`+setting.StaticURLPrefix+`/assets/img/emoji/custom-emoji.png"/></span></p>`)
`<p><span class="emoji"><img alt=":custom-emoji:" src="`+setting.StaticURLPrefix+`/assets/img/emoji/custom-emoji.png"/></span></p>`)
test(
"这是字符:1::+1: some🐊 \U0001f44d:custom-emoji: :gitea:",
`<p>这是字符:1:<span class="emoji" aria-label="thumbs up">👍</span> some<span class="emoji" aria-label="crocodile">🐊</span> `+
`<span class="emoji" aria-label="thumbs up">👍</span><span class="emoji" aria-label="custom-emoji"><img alt=":custom-emoji:" src="`+setting.StaticURLPrefix+`/assets/img/emoji/custom-emoji.png"/></span> `+
`<span class="emoji" aria-label="gitea"><img alt=":gitea:" src="`+setting.StaticURLPrefix+`/assets/img/emoji/gitea.png"/></span></p>`)
`<p>这是字符:1:<span class="emoji" data-alias="+1">👍</span> some<span class="emoji" data-alias="crocodile">🐊</span> `+
`<span class="emoji" data-alias="+1">👍</span><span class="emoji"><img alt=":custom-emoji:" src="`+setting.StaticURLPrefix+`/assets/img/emoji/custom-emoji.png"/></span> `+
`<span class="emoji"><img alt=":gitea:" src="`+setting.StaticURLPrefix+`/assets/img/emoji/gitea.png"/></span></p>`)
test(
"Some text with 😄 in the middle",
`<p>Some text with <span class="emoji" aria-label="grinning face with smiling eyes">😄</span> in the middle</p>`)
`<p>Some text with <span class="emoji" data-alias="smile">😄</span> in the middle</p>`)
test(
"Some text with :smile: in the middle",
`<p>Some text with <span class="emoji" aria-label="grinning face with smiling eyes">😄</span> in the middle</p>`)
`<p>Some text with <span class="emoji" data-alias="smile">😄</span> in the middle</p>`)
test(
"Some text with 😄😄 2 emoji next to each other",
`<p>Some text with <span class="emoji" aria-label="grinning face with smiling eyes">😄</span><span class="emoji" aria-label="grinning face with smiling eyes">😄</span> 2 emoji next to each other</p>`)
`<p>Some text with <span class="emoji" data-alias="smile">😄</span><span class="emoji" data-alias="smile">😄</span> 2 emoji next to each other</p>`)
test(
"😎🤪🔐🤑❓",
`<p><span class="emoji" aria-label="smiling face with sunglasses">😎</span><span class="emoji" aria-label="zany face">🤪</span><span class="emoji" aria-label="locked with key">🔐</span><span class="emoji" aria-label="money-mouth face">🤑</span><span class="emoji" aria-label="red question mark">❓</span></p>`)
`<p><span class="emoji" data-alias="sunglasses">😎</span><span class="emoji" data-alias="zany_face">🤪</span><span class="emoji" data-alias="closed_lock_with_key">🔐</span><span class="emoji" data-alias="money_mouth_face">🤑</span><span class="emoji" data-alias="question">❓</span></p>`)
test(
"👍🏽🧑🏽‍💻👩🏿‍❤️‍👩🏿",
`<p><span class="emoji" data-alias="+1">👍🏽</span><span class="emoji" data-alias="technologist">🧑🏽‍💻</span><span class="emoji" data-alias="couple_with_heart_woman_woman">👩🏿‍❤️‍👩🏿</span></p>`)
// should match nothing
test(":100:200", `<p>:100:200</p>`)
@@ -380,7 +385,7 @@ func TestRender_emoji(t *testing.T) {
test(":not exist:", `<p>:not exist:</p>`)
test("foo `:smile:", "<p>foo `:smile:</p>")
test("foo `:smile:`", `<p>foo <code>:smile:</code></p>`)
test("foo ` :smile:", "<p>foo ` <span class=\"emoji\" aria-label=\"grinning face with smiling eyes\">😄</span></p>")
test("foo ` :smile:", "<p>foo ` <span class=\"emoji\" data-alias=\"smile\">😄</span></p>")
}
func TestRender_ShortLinks(t *testing.T) {
@@ -532,10 +537,10 @@ func TestPostProcess(t *testing.T) {
// Test that other post-processing still works.
test(
":gitea:",
`<span class="emoji" aria-label="gitea"><img alt=":gitea:" src="`+setting.StaticURLPrefix+`/assets/img/emoji/gitea.png"/></span>`)
`<span class="emoji"><img alt=":gitea:" src="`+setting.StaticURLPrefix+`/assets/img/emoji/gitea.png"/></span>`)
test(
"Some text with 😄 in the middle",
`Some text with <span class="emoji" aria-label="grinning face with smiling eyes">😄</span> in the middle`)
`Some text with <span class="emoji" data-alias="smile">😄</span> in the middle`)
test("http://localhost:3000/person/repo/issues/4#issuecomment-1234",
`<a href="http://localhost:3000/person/repo/issues/4#issuecomment-1234" class="ref-issue">person/repo#4 (comment)</a>`)
+1 -1
View File
@@ -12,7 +12,7 @@ import (
)
func TestMain(m *testing.M) {
setting.IsInTesting = true
setting.SetupGiteaTestEnv()
markup.RenderBehaviorForTesting.DisableAdditionalAttributes = true
markup.RefreshFileNamePatterns()
os.Exit(m.Run())
+2 -3
View File
@@ -4,7 +4,6 @@
package markdown
import (
"os"
"testing"
"gitea.dev/modules/markup"
@@ -12,7 +11,7 @@ import (
)
func TestMain(m *testing.M) {
setting.IsInTesting = true
setting.SetupGiteaTestEnv()
markup.RenderBehaviorForTesting.DisableAdditionalAttributes = true
os.Exit(m.Run())
m.Run()
}
+3 -3
View File
@@ -323,8 +323,8 @@ func TestRenderSiblingImages_Issue12925(t *testing.T) {
}
func TestRenderEmojiInLinks_Issue12331(t *testing.T) {
testcase := `[Link with emoji :moon: in text](https://gitea.io)`
expected := `<p><a href="https://gitea.io" rel="nofollow">Link with emoji <span class="emoji" aria-label="waxing gibbous moon">🌔</span> in text</a></p>
testcase := `[Link with emoji :moon: in text](https://gitea.com)`
expected := `<p><a href="https://gitea.com" rel="nofollow">Link with emoji <span class="emoji" data-alias="moon">🌔</span> in text</a></p>
`
res, err := markdown.RenderString(markup.NewTestRenderContext(), testcase)
assert.NoError(t, err)
@@ -542,7 +542,7 @@ mail@domain.com
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb...12fc37a3c0a4dda553bdcfc80c178a58247f42fb pare
<a href="https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb" rel="nofollow">https://example.com/user/repo/commit/88fc37a3c0a4dda553bdcfc80c178a58247f42fb</a>
com 88fc37a3c0a4dda553bdcfc80c178a58247f42fb mit
<span class="emoji" aria-label="thumbs up">👍</span>
<span class="emoji" data-alias="+1">👍</span>
<a href="mailto:mail@domain.com" rel="nofollow">mail@domain.com</a>
@mention-user test
#123
+3
View File
@@ -34,6 +34,9 @@ func (st *Sanitizer) createDefaultPolicy() *bluemonday.Policy {
// Line numbers on codepreview
policy.AllowAttrs("data-line-number").OnElements("span")
// emoji aliases for dark theme inversion
policy.AllowAttrs("data-alias").OnElements("span")
// HINT: CUSTOM-URL-SCHEMES-ALLOW: setting custom means also allow them besides http/https, no custom means "allow all"
if len(setting.Markdown.CustomURLSchemes) > 0 {
policy.AllowURLSchemes(setting.Markdown.CustomURLSchemes...)