mirror of
https://github.com/go-gitea/gitea.git
synced 2025-10-27 00:23:41 +09:00
189 lines
5.0 KiB
Go
189 lines
5.0 KiB
Go
// Copyright 2020 The Gitea Authors. All rights reserved.
|
|
// Copyright 2015 Kenneth Shaw
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package emoji
|
|
|
|
import (
|
|
"io"
|
|
"sort"
|
|
"strings"
|
|
"sync/atomic"
|
|
|
|
"code.gitea.io/gitea/modules/setting"
|
|
)
|
|
|
|
// Gemoji is a set of emoji data.
|
|
type Gemoji []Emoji
|
|
|
|
// Emoji represents a single emoji and associated data.
|
|
type Emoji struct {
|
|
Emoji string
|
|
Description string
|
|
Aliases []string
|
|
UnicodeVersion string
|
|
SkinTones bool
|
|
}
|
|
|
|
type globalVarsStruct struct {
|
|
codeMap map[string]int // emoji unicode code to its emoji data.
|
|
aliasMap map[string]int // the alias to its emoji data.
|
|
emptyReplacer *strings.Replacer // string replacer for emoji codes, used for finding emoji positions.
|
|
codeReplacer *strings.Replacer // string replacer for emoji codes.
|
|
aliasReplacer *strings.Replacer // string replacer for emoji aliases.
|
|
}
|
|
|
|
var globalVarsStore atomic.Pointer[globalVarsStruct]
|
|
|
|
func globalVars() *globalVarsStruct {
|
|
vars := globalVarsStore.Load()
|
|
if vars != nil {
|
|
return vars
|
|
}
|
|
// although there can be concurrent calls, the result should be the same, and there is no performance problem
|
|
vars = &globalVarsStruct{}
|
|
vars.codeMap = make(map[string]int, len(GemojiData))
|
|
vars.aliasMap = make(map[string]int, len(GemojiData))
|
|
|
|
// process emoji codes and aliases
|
|
codePairs := make([]string, 0)
|
|
emptyPairs := make([]string, 0)
|
|
aliasPairs := make([]string, 0)
|
|
|
|
// sort from largest to small so we match combined emoji first
|
|
sort.Slice(GemojiData, func(i, j int) bool {
|
|
return len(GemojiData[i].Emoji) > len(GemojiData[j].Emoji)
|
|
})
|
|
|
|
for idx, emoji := range GemojiData {
|
|
if emoji.Emoji == "" || len(emoji.Aliases) == 0 {
|
|
continue
|
|
}
|
|
|
|
// process aliases
|
|
firstAlias := ""
|
|
for _, alias := range emoji.Aliases {
|
|
if alias == "" {
|
|
continue
|
|
}
|
|
enabled := len(setting.UI.EnabledEmojisSet) == 0 || setting.UI.EnabledEmojisSet.Contains(alias)
|
|
if !enabled {
|
|
continue
|
|
}
|
|
if firstAlias == "" {
|
|
firstAlias = alias
|
|
}
|
|
vars.aliasMap[alias] = idx
|
|
aliasPairs = append(aliasPairs, ":"+alias+":", emoji.Emoji)
|
|
}
|
|
|
|
// process emoji code
|
|
if firstAlias != "" {
|
|
vars.codeMap[emoji.Emoji] = idx
|
|
codePairs = append(codePairs, emoji.Emoji, ":"+emoji.Aliases[0]+":")
|
|
emptyPairs = append(emptyPairs, emoji.Emoji, emoji.Emoji)
|
|
}
|
|
}
|
|
|
|
// create replacers
|
|
vars.emptyReplacer = strings.NewReplacer(emptyPairs...)
|
|
vars.codeReplacer = strings.NewReplacer(codePairs...)
|
|
vars.aliasReplacer = strings.NewReplacer(aliasPairs...)
|
|
globalVarsStore.Store(vars)
|
|
return vars
|
|
}
|
|
|
|
// FromCode retrieves the emoji data based on the provided unicode code (ie,
|
|
// "\u2618" will return the Gemoji data for "shamrock").
|
|
func FromCode(code string) *Emoji {
|
|
i, ok := globalVars().codeMap[code]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
return &GemojiData[i]
|
|
}
|
|
|
|
// FromAlias retrieves the emoji data based on the provided alias in the form
|
|
// "alias" or ":alias:" (ie, "shamrock" or ":shamrock:" will return the Gemoji
|
|
// data for "shamrock").
|
|
func FromAlias(alias string) *Emoji {
|
|
if strings.HasPrefix(alias, ":") && strings.HasSuffix(alias, ":") {
|
|
alias = alias[1 : len(alias)-1]
|
|
}
|
|
|
|
i, ok := globalVars().aliasMap[alias]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
return &GemojiData[i]
|
|
}
|
|
|
|
// ReplaceCodes replaces all emoji codes with the first corresponding emoji
|
|
// alias (in the form of ":alias:") (ie, "\u2618" will be converted to
|
|
// ":shamrock:").
|
|
func ReplaceCodes(s string) string {
|
|
return globalVars().codeReplacer.Replace(s)
|
|
}
|
|
|
|
// ReplaceAliases replaces all aliases of the form ":alias:" with its
|
|
// corresponding unicode value.
|
|
func ReplaceAliases(s string) string {
|
|
return globalVars().aliasReplacer.Replace(s)
|
|
}
|
|
|
|
type rememberSecondWriteWriter struct {
|
|
pos int
|
|
idx int
|
|
end int
|
|
writecount int
|
|
}
|
|
|
|
func (n *rememberSecondWriteWriter) Write(p []byte) (int, error) {
|
|
n.writecount++
|
|
if n.writecount == 2 {
|
|
n.idx = n.pos
|
|
n.end = n.pos + len(p)
|
|
n.pos += len(p)
|
|
return len(p), io.EOF
|
|
}
|
|
n.pos += len(p)
|
|
return len(p), nil
|
|
}
|
|
|
|
func (n *rememberSecondWriteWriter) WriteString(s string) (int, error) {
|
|
n.writecount++
|
|
if n.writecount == 2 {
|
|
n.idx = n.pos
|
|
n.end = n.pos + len(s)
|
|
n.pos += len(s)
|
|
return len(s), io.EOF
|
|
}
|
|
n.pos += len(s)
|
|
return len(s), nil
|
|
}
|
|
|
|
// FindEmojiSubmatchIndex returns index pair of longest emoji in a string
|
|
func FindEmojiSubmatchIndex(s string) []int {
|
|
secondWriteWriter := rememberSecondWriteWriter{}
|
|
|
|
// A faster and clean implementation would copy the trie tree formation in strings.NewReplacer but
|
|
// we can be lazy here.
|
|
//
|
|
// The implementation of strings.Replacer.WriteString is such that the first index of the emoji
|
|
// submatch is simply the second thing that is written to WriteString in the writer.
|
|
//
|
|
// Therefore we can simply take the index of the second write as our first emoji
|
|
//
|
|
// FIXME: just copy the trie implementation from strings.NewReplacer
|
|
_, _ = globalVars().emptyReplacer.WriteString(&secondWriteWriter, s)
|
|
|
|
// if we wrote less than twice then we never "replaced"
|
|
if secondWriteWriter.writecount < 2 {
|
|
return nil
|
|
}
|
|
|
|
return []int{secondWriteWriter.idx, secondWriteWriter.end}
|
|
}
|