mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-30 10:26:14 +01: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}
 | |
| }
 |