mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
125 lines
3.3 KiB
Go
125 lines
3.3 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package markup
|
|
|
|
import (
|
|
"strings"
|
|
"unicode"
|
|
|
|
"github.com/hanzoai/git/modules/emoji"
|
|
"github.com/hanzoai/git/modules/setting"
|
|
|
|
"golang.org/x/net/html"
|
|
"golang.org/x/net/html/atom"
|
|
)
|
|
|
|
func createEmoji(ctx *RenderContext, content, name 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})
|
|
}
|
|
|
|
text := &html.Node{
|
|
Type: html.TextNode,
|
|
Data: content,
|
|
}
|
|
|
|
span.AppendChild(text)
|
|
return span
|
|
}
|
|
|
|
func createCustomEmoji(ctx *RenderContext, 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"))
|
|
span.Attr = append(span.Attr, html.Attribute{Key: "aria-label", Val: alias})
|
|
|
|
img := &html.Node{
|
|
Type: html.ElementNode,
|
|
DataAtom: atom.Img,
|
|
Data: "img",
|
|
Attr: []html.Attribute{},
|
|
}
|
|
img.Attr = append(img.Attr, html.Attribute{Key: "alt", Val: ":" + alias + ":"})
|
|
img.Attr = append(img.Attr, html.Attribute{Key: "src", Val: setting.StaticURLPrefix + "/assets/img/emoji/" + alias + ".png"})
|
|
|
|
span.AppendChild(img)
|
|
return span
|
|
}
|
|
|
|
// emojiShortCodeProcessor for rendering text like :smile: into emoji
|
|
func emojiShortCodeProcessor(ctx *RenderContext, node *html.Node) {
|
|
start := 0
|
|
next := node.NextSibling
|
|
for node != nil && node != next && start < len(node.Data) {
|
|
m := globalVars().emojiShortCodeRegex.FindStringSubmatchIndex(node.Data[start:])
|
|
if m == nil {
|
|
return
|
|
}
|
|
m[0] += start
|
|
m[1] += start
|
|
start = m[1]
|
|
|
|
// don't render a shortcode whose ":" directly follows a backtick (an unclosed code span)
|
|
if m[0] > 0 && node.Data[m[0]-1] == '`' {
|
|
continue
|
|
}
|
|
|
|
alias := node.Data[m[0]:m[1]]
|
|
|
|
var nextChar byte
|
|
if m[1] < len(node.Data) {
|
|
nextChar = node.Data[m[1]]
|
|
}
|
|
if nextChar == ':' || unicode.IsLetter(rune(nextChar)) || unicode.IsDigit(rune(nextChar)) {
|
|
continue
|
|
}
|
|
|
|
alias = strings.Trim(alias, ":")
|
|
converted := emoji.FromAlias(alias)
|
|
if converted != nil {
|
|
// standard emoji
|
|
replaceContent(node, m[0], m[1], createEmoji(ctx, converted.Emoji, converted.Description))
|
|
node = node.NextSibling.NextSibling
|
|
start = 0 // restart searching start since node has changed
|
|
} else if _, exist := setting.UI.CustomEmojisMap[alias]; exist {
|
|
// custom reaction
|
|
replaceContent(node, m[0], m[1], createCustomEmoji(ctx, alias))
|
|
node = node.NextSibling.NextSibling
|
|
start = 0 // restart searching start since node has changed
|
|
}
|
|
}
|
|
}
|
|
|
|
// emoji processor to match emoji and add emoji class
|
|
func emojiProcessor(ctx *RenderContext, node *html.Node) {
|
|
start := 0
|
|
next := node.NextSibling
|
|
for node != nil && node != next && start < len(node.Data) {
|
|
m := emoji.FindEmojiSubmatchIndex(node.Data[start:])
|
|
if m == nil {
|
|
return
|
|
}
|
|
m[0] += start
|
|
m[1] += start
|
|
|
|
codepoint := node.Data[m[0]:m[1]]
|
|
start = m[1]
|
|
val := emoji.FromCode(codepoint)
|
|
if val != nil {
|
|
replaceContent(node, m[0], m[1], createEmoji(ctx, codepoint, val.Description))
|
|
node = node.NextSibling.NextSibling
|
|
start = 0
|
|
}
|
|
}
|
|
}
|