Skip to content

Commit ec726b4

Browse files
refactor(emoji): move Trie implementation to modules/util
Relocate the custom slice-based Trie structure and tests from modules/emoji to modules/util as a generic utility Signed-off-by: Sudhanshu Singh <sudhanshuwriterblc@gmail.com>
1 parent 1bd61d7 commit ec726b4

3 files changed

Lines changed: 101 additions & 57 deletions

File tree

modules/emoji/emoji.go

Lines changed: 5 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"sync/atomic"
1111

1212
"gitea.dev/modules/setting"
13+
"gitea.dev/modules/util"
1314
)
1415

1516
// Gemoji is a set of emoji data.
@@ -24,63 +25,10 @@ type Emoji struct {
2425
SkinTones bool
2526
}
2627

27-
type trieEdge struct {
28-
b byte
29-
node *trieNode
30-
}
31-
32-
type trieNode struct {
33-
children []trieEdge
34-
isEnd bool
35-
}
36-
37-
func (t *trieNode) insert(val string) {
38-
curr := t
39-
for i := 0; i < len(val); i++ {
40-
b := val[i]
41-
var next *trieNode
42-
for _, edge := range curr.children {
43-
if edge.b == b {
44-
next = edge.node
45-
break
46-
}
47-
}
48-
if next == nil {
49-
next = &trieNode{}
50-
curr.children = append(curr.children, trieEdge{b: b, node: next})
51-
}
52-
curr = next
53-
}
54-
curr.isEnd = true
55-
}
56-
57-
func (t *trieNode) match(s string, start int) int {
58-
curr := t
59-
matchLen := -1
60-
for j := start; j < len(s); j++ {
61-
b := s[j]
62-
var next *trieNode
63-
for _, edge := range curr.children {
64-
if edge.b == b {
65-
next = edge.node
66-
break
67-
}
68-
}
69-
if next == nil {
70-
break
71-
}
72-
curr = next
73-
if curr.isEnd {
74-
matchLen = j - start + 1
75-
}
76-
}
77-
return matchLen
78-
}
79-
8028
type globalVarsStruct struct {
8129
codeMap map[string]int // emoji unicode code to its emoji data.
8230
aliasMap map[string]int // the alias to its emoji data.
83-
trie *trieNode // trie for finding emoji positions.
31+
trie *util.TrieNode // trie for finding emoji positions.
8432
isStartingByte [256]bool // fast-path skip for starting bytes
8533
codeReplacer *strings.Replacer // string replacer for emoji codes.
8634
aliasReplacer *strings.Replacer // string replacer for emoji aliases.
@@ -97,7 +45,7 @@ func globalVars() *globalVarsStruct {
9745
vars = &globalVarsStruct{}
9846
vars.codeMap = make(map[string]int, len(GemojiData))
9947
vars.aliasMap = make(map[string]int, len(GemojiData))
100-
vars.trie = &trieNode{}
48+
vars.trie = &util.TrieNode{}
10149

10250
// process emoji codes and aliases
10351
codePairs := make([]string, 0)
@@ -134,7 +82,7 @@ func globalVars() *globalVarsStruct {
13482
if firstAlias != "" {
13583
vars.codeMap[emoji.Emoji] = idx
13684
codePairs = append(codePairs, emoji.Emoji, ":"+emoji.Aliases[0]+":")
137-
vars.trie.insert(emoji.Emoji)
85+
vars.trie.Insert(emoji.Emoji)
13886
vars.isStartingByte[emoji.Emoji[0]] = true
13987
}
14088
}
@@ -193,7 +141,7 @@ func FindEmojiSubmatchIndex(s string) []int {
193141
if !vars.isStartingByte[s[i]] {
194142
continue
195143
}
196-
if matchLen := vars.trie.match(s, i); matchLen > 0 {
144+
if matchLen := vars.trie.Match(s, i); matchLen > 0 {
197145
return []int{i, i + matchLen}
198146
}
199147
}

modules/util/trie.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright 2026 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package util
5+
6+
// TrieEdge represents a byte transition edge in the Trie.
7+
type TrieEdge struct {
8+
B byte
9+
Node *TrieNode
10+
}
11+
12+
// TrieNode represents a node in a slice-based Trie for fast byte-sequence prefix matching.
13+
type TrieNode struct {
14+
Children []TrieEdge
15+
IsEnd bool
16+
}
17+
18+
// Insert adds a string (byte sequence) to the Trie.
19+
func (t *TrieNode) Insert(val string) {
20+
curr := t
21+
for i := 0; i < len(val); i++ {
22+
b := val[i]
23+
var next *TrieNode
24+
for _, edge := range curr.Children {
25+
if edge.B == b {
26+
next = edge.Node
27+
break
28+
}
29+
}
30+
if next == nil {
31+
next = &TrieNode{}
32+
curr.Children = append(curr.Children, TrieEdge{B: b, Node: next})
33+
}
34+
curr = next
35+
}
36+
curr.IsEnd = true
37+
}
38+
39+
// Match returns the length of the longest matching prefix starting at index `start` in string `s`.
40+
// It returns -1 if no prefix is matched.
41+
func (t *TrieNode) Match(s string, start int) int {
42+
curr := t
43+
matchLen := -1
44+
for j := start; j < len(s); j++ {
45+
b := s[j]
46+
var next *TrieNode
47+
for _, edge := range curr.Children {
48+
if edge.B == b {
49+
next = edge.Node
50+
break
51+
}
52+
}
53+
if next == nil {
54+
break
55+
}
56+
curr = next
57+
if curr.IsEnd {
58+
matchLen = j - start + 1
59+
}
60+
}
61+
return matchLen
62+
}

modules/util/trie_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Copyright 2026 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package util
5+
6+
import (
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestTrie(t *testing.T) {
13+
trie := &TrieNode{}
14+
trie.Insert("apple")
15+
trie.Insert("apricot")
16+
trie.Insert("banana")
17+
trie.Insert("app")
18+
19+
// Test exact matches
20+
assert.Equal(t, 5, trie.Match("apple", 0))
21+
assert.Equal(t, 7, trie.Match("apricot", 0))
22+
assert.Equal(t, 6, trie.Match("banana", 0))
23+
assert.Equal(t, 3, trie.Match("app", 0))
24+
25+
// Test partial match (longest match priority)
26+
assert.Equal(t, 5, trie.Match("apple-pie", 0))
27+
28+
// Test suffix/nested position match
29+
assert.Equal(t, 5, trie.Match("sweet apple", 6))
30+
31+
// Test no match
32+
assert.Equal(t, -1, trie.Match("orange", 0))
33+
assert.Equal(t, -1, trie.Match("ap", 0)) // prefix exists but is not an end node
34+
}

0 commit comments

Comments
 (0)