github.com/gitbundle/modules@v0.0.0-20231025071548-85b91c5c3b01/emoji/emoji.go (about)

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