code.gitea.io/gitea@v1.19.3/modules/emoji/emoji.go (about)

     1  // Copyright 2020 The Gitea Authors. All rights reserved.
     2  // Copyright 2015 Kenneth Shaw
     3  // SPDX-License-Identifier: MIT
     4  
     5  package emoji
     6  
     7  import (
     8  	"io"
     9  	"sort"
    10  	"strings"
    11  	"sync"
    12  )
    13  
    14  // Gemoji is a set of emoji data.
    15  type Gemoji []Emoji
    16  
    17  // Emoji represents a single emoji and associated data.
    18  type Emoji struct {
    19  	Emoji          string
    20  	Description    string
    21  	Aliases        []string
    22  	UnicodeVersion string
    23  	SkinTones      bool
    24  }
    25  
    26  var (
    27  	// codeMap provides a map of the emoji unicode code to its emoji data.
    28  	codeMap map[string]int
    29  
    30  	// aliasMap provides a map of the alias to its emoji data.
    31  	aliasMap map[string]int
    32  
    33  	// emptyReplacer is the string replacer for emoji codes.
    34  	emptyReplacer *strings.Replacer
    35  
    36  	// codeReplacer is the string replacer for emoji codes.
    37  	codeReplacer *strings.Replacer
    38  
    39  	// aliasReplacer is the string replacer for emoji aliases.
    40  	aliasReplacer *strings.Replacer
    41  
    42  	once sync.Once
    43  )
    44  
    45  func loadMap() {
    46  	once.Do(func() {
    47  		// initialize
    48  		codeMap = make(map[string]int, len(GemojiData))
    49  		aliasMap = make(map[string]int, len(GemojiData))
    50  
    51  		// process emoji codes and aliases
    52  		codePairs := make([]string, 0)
    53  		emptyPairs := make([]string, 0)
    54  		aliasPairs := make([]string, 0)
    55  
    56  		// sort from largest to small so we match combined emoji first
    57  		sort.Slice(GemojiData, func(i, j int) bool {
    58  			return len(GemojiData[i].Emoji) > len(GemojiData[j].Emoji)
    59  		})
    60  
    61  		for i, e := range GemojiData {
    62  			if e.Emoji == "" || len(e.Aliases) == 0 {
    63  				continue
    64  			}
    65  
    66  			// setup codes
    67  			codeMap[e.Emoji] = i
    68  			codePairs = append(codePairs, e.Emoji, ":"+e.Aliases[0]+":")
    69  			emptyPairs = append(emptyPairs, e.Emoji, e.Emoji)
    70  
    71  			// setup aliases
    72  			for _, a := range e.Aliases {
    73  				if a == "" {
    74  					continue
    75  				}
    76  
    77  				aliasMap[a] = i
    78  				aliasPairs = append(aliasPairs, ":"+a+":", e.Emoji)
    79  			}
    80  		}
    81  
    82  		// create replacers
    83  		emptyReplacer = strings.NewReplacer(emptyPairs...)
    84  		codeReplacer = strings.NewReplacer(codePairs...)
    85  		aliasReplacer = strings.NewReplacer(aliasPairs...)
    86  	})
    87  }
    88  
    89  // FromCode retrieves the emoji data based on the provided unicode code (ie,
    90  // "\u2618" will return the Gemoji data for "shamrock").
    91  func FromCode(code string) *Emoji {
    92  	loadMap()
    93  	i, ok := codeMap[code]
    94  	if !ok {
    95  		return nil
    96  	}
    97  
    98  	return &GemojiData[i]
    99  }
   100  
   101  // FromAlias retrieves the emoji data based on the provided alias in the form
   102  // "alias" or ":alias:" (ie, "shamrock" or ":shamrock:" will return the Gemoji
   103  // data for "shamrock").
   104  func FromAlias(alias string) *Emoji {
   105  	loadMap()
   106  	if strings.HasPrefix(alias, ":") && strings.HasSuffix(alias, ":") {
   107  		alias = alias[1 : len(alias)-1]
   108  	}
   109  
   110  	i, ok := aliasMap[alias]
   111  	if !ok {
   112  		return nil
   113  	}
   114  
   115  	return &GemojiData[i]
   116  }
   117  
   118  // ReplaceCodes replaces all emoji codes with the first corresponding emoji
   119  // alias (in the form of ":alias:") (ie, "\u2618" will be converted to
   120  // ":shamrock:").
   121  func ReplaceCodes(s string) string {
   122  	loadMap()
   123  	return codeReplacer.Replace(s)
   124  }
   125  
   126  // ReplaceAliases replaces all aliases of the form ":alias:" with its
   127  // corresponding unicode value.
   128  func ReplaceAliases(s string) string {
   129  	loadMap()
   130  	return aliasReplacer.Replace(s)
   131  }
   132  
   133  type rememberSecondWriteWriter struct {
   134  	pos        int
   135  	idx        int
   136  	end        int
   137  	writecount int
   138  }
   139  
   140  func (n *rememberSecondWriteWriter) Write(p []byte) (int, error) {
   141  	n.writecount++
   142  	if n.writecount == 2 {
   143  		n.idx = n.pos
   144  		n.end = n.pos + len(p)
   145  		n.pos += len(p)
   146  		return len(p), io.EOF
   147  	}
   148  	n.pos += len(p)
   149  	return len(p), nil
   150  }
   151  
   152  func (n *rememberSecondWriteWriter) WriteString(s string) (int, error) {
   153  	n.writecount++
   154  	if n.writecount == 2 {
   155  		n.idx = n.pos
   156  		n.end = n.pos + len(s)
   157  		n.pos += len(s)
   158  		return len(s), io.EOF
   159  	}
   160  	n.pos += len(s)
   161  	return len(s), nil
   162  }
   163  
   164  // FindEmojiSubmatchIndex returns index pair of longest emoji in a string
   165  func FindEmojiSubmatchIndex(s string) []int {
   166  	loadMap()
   167  	secondWriteWriter := rememberSecondWriteWriter{}
   168  
   169  	// A faster and clean implementation would copy the trie tree formation in strings.NewReplacer but
   170  	// we can be lazy here.
   171  	//
   172  	// The implementation of strings.Replacer.WriteString is such that the first index of the emoji
   173  	// submatch is simply the second thing that is written to WriteString in the writer.
   174  	//
   175  	// Therefore we can simply take the index of the second write as our first emoji
   176  	//
   177  	// FIXME: just copy the trie implementation from strings.NewReplacer
   178  	_, _ = emptyReplacer.WriteString(&secondWriteWriter, s)
   179  
   180  	// if we wrote less than twice then we never "replaced"
   181  	if secondWriteWriter.writecount < 2 {
   182  		return nil
   183  	}
   184  
   185  	return []int{secondWriteWriter.idx, secondWriteWriter.end}
   186  }