summaryrefslogtreecommitdiffstats
path: root/modules/markup/html.go
diff options
context:
space:
mode:
Diffstat (limited to 'modules/markup/html.go')
-rw-r--r--modules/markup/html.go123
1 files changed, 123 insertions, 0 deletions
diff --git a/modules/markup/html.go b/modules/markup/html.go
index 294b870d8c..c5bb4d847b 100644
--- a/modules/markup/html.go
+++ b/modules/markup/html.go
@@ -6,6 +6,7 @@ package markup
import (
"bytes"
+ "fmt"
"net/url"
"path"
"path/filepath"
@@ -13,6 +14,7 @@ import (
"strings"
"code.gitea.io/gitea/modules/base"
+ "code.gitea.io/gitea/modules/emoji"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup/common"
@@ -60,6 +62,13 @@ var (
// blackfriday extensions create IDs like fn:user-content-footnote
blackfridayExtRegex = regexp.MustCompile(`[^:]*:user-content-`)
+
+ // EmojiShortCodeRegex find emoji by alias like :smile:
+ EmojiShortCodeRegex = regexp.MustCompile(`\:[\w\+\-]+\:{1}`)
+
+ // find emoji literal: search all emoji hex range as many times as they appear as
+ // some emojis (skin color etc..) are just two or more chained together
+ emojiRegex = regexp.MustCompile(`[\x{1F000}-\x{1FFFF}|\x{2000}-\x{32ff}|\x{fe4e5}-\x{fe4ee}|\x{200D}|\x{FE0F}|\x{e0000}-\x{e007f}]+`)
)
// CSS class for action keywords (e.g. "closes: #1")
@@ -154,6 +163,8 @@ var defaultProcessors = []processor{
issueIndexPatternProcessor,
sha1CurrentPatternProcessor,
emailAddressProcessor,
+ emojiProcessor,
+ emojiShortCodeProcessor,
}
type postProcessCtx struct {
@@ -194,6 +205,8 @@ var commitMessageProcessors = []processor{
issueIndexPatternProcessor,
sha1CurrentPatternProcessor,
emailAddressProcessor,
+ emojiProcessor,
+ emojiShortCodeProcessor,
}
// RenderCommitMessage will use the same logic as PostProcess, but will disable
@@ -226,6 +239,13 @@ var commitMessageSubjectProcessors = []processor{
mentionProcessor,
issueIndexPatternProcessor,
sha1CurrentPatternProcessor,
+ emojiShortCodeProcessor,
+ emojiProcessor,
+}
+
+var emojiProcessors = []processor{
+ emojiShortCodeProcessor,
+ emojiProcessor,
}
// RenderCommitMessageSubject will use the same logic as PostProcess and
@@ -269,6 +289,17 @@ func RenderDescriptionHTML(
return ctx.postProcess(rawHTML)
}
+// RenderEmoji for when we want to just process emoji and shortcodes
+// in various places it isn't already run through the normal markdown procesor
+func RenderEmoji(
+ rawHTML []byte,
+) ([]byte, error) {
+ ctx := &postProcessCtx{
+ procs: emojiProcessors,
+ }
+ return ctx.postProcess(rawHTML)
+}
+
var byteBodyTag = []byte("<body>")
var byteBodyTagClosing = []byte("</body>")
@@ -319,7 +350,12 @@ func (ctx *postProcessCtx) visitNode(node *html.Node, visitText bool) {
if attr.Key == "id" && !(strings.HasPrefix(attr.Val, "user-content-") || blackfridayExtRegex.MatchString(attr.Val)) {
node.Attr[idx].Val = "user-content-" + attr.Val
}
+
+ if attr.Key == "class" && attr.Val == "emoji" {
+ visitText = false
+ }
}
+
// We ignore code, pre and already generated links.
switch node.Type {
case html.TextNode:
@@ -406,6 +442,54 @@ func createKeyword(content string) *html.Node {
return span
}
+func createEmoji(content, class, name string) *html.Node {
+ span := &html.Node{
+ Type: html.ElementNode,
+ Data: atom.Span.String(),
+ Attr: []html.Attribute{},
+ }
+ if class != "" {
+ span.Attr = append(span.Attr, html.Attribute{Key: "class", Val: class})
+ }
+ 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(alias, class string) *html.Node {
+
+ span := &html.Node{
+ Type: html.ElementNode,
+ Data: atom.Span.String(),
+ Attr: []html.Attribute{},
+ }
+ if class != "" {
+ span.Attr = append(span.Attr, html.Attribute{Key: "class", Val: class})
+ 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{},
+ }
+ if class != "" {
+ img.Attr = append(img.Attr, html.Attribute{Key: "src", Val: fmt.Sprintf(`%s/img/emoji/%s.png`, setting.StaticURLPrefix, alias)})
+ }
+
+ span.AppendChild(img)
+ return span
+}
+
func createLink(href, content, class string) *html.Node {
a := &html.Node{
Type: html.ElementNode,
@@ -810,6 +894,45 @@ func fullSha1PatternProcessor(ctx *postProcessCtx, node *html.Node) {
replaceContent(node, start, end, createCodeLink(urlFull, text, "commit"))
}
+// emojiShortCodeProcessor for rendering text like :smile: into emoji
+func emojiShortCodeProcessor(ctx *postProcessCtx, node *html.Node) {
+
+ m := EmojiShortCodeRegex.FindStringSubmatchIndex(node.Data)
+ if m == nil {
+ return
+ }
+
+ alias := node.Data[m[0]:m[1]]
+ alias = strings.Replace(alias, ":", "", -1)
+ converted := emoji.FromAlias(alias)
+ if converted == nil {
+ // check if this is a custom reaction
+ s := strings.Join(setting.UI.Reactions, " ") + "gitea"
+ if strings.Contains(s, alias) {
+ replaceContent(node, m[0], m[1], createCustomEmoji(alias, "emoji"))
+ return
+ }
+ return
+ }
+
+ replaceContent(node, m[0], m[1], createEmoji(converted.Emoji, "emoji", converted.Description))
+}
+
+// emoji processor to match emoji and add emoji class
+func emojiProcessor(ctx *postProcessCtx, node *html.Node) {
+ m := emojiRegex.FindStringSubmatchIndex(node.Data)
+
+ if m == nil {
+ return
+ }
+
+ codepoint := node.Data[m[0]:m[1]]
+ val := emoji.FromCode(codepoint)
+ if val != nil {
+ replaceContent(node, m[0], m[1], createEmoji(codepoint, "emoji", val.Description))
+ }
+}
+
// sha1CurrentPatternProcessor renders SHA1 strings to corresponding links that
// are assumed to be in the same repository.
func sha1CurrentPatternProcessor(ctx *postProcessCtx, node *html.Node) {