|
| 1 | +using System; |
| 2 | +using System.Collections.Generic; |
| 3 | +using System.Linq; |
| 4 | +using System.Text.RegularExpressions; |
| 5 | +using TMPro; |
| 6 | +using UnityEngine.Pool; |
| 7 | + |
| 8 | +namespace BP.TMPA |
| 9 | +{ |
| 10 | + internal class TextMeshPreprocessor : ITextPreprocessor, IDisposable |
| 11 | + { |
| 12 | + private const string tagPattern = @"<(\/?)([A-Za-z0-9]+)(=[^>]+)?(\/?)>"; |
| 13 | + |
| 14 | + // Caching mechanisms |
| 15 | + private string lastInputText; |
| 16 | + private string processedText; |
| 17 | + |
| 18 | + // Object pooling for TagData |
| 19 | + private readonly ObjectPool<TextTagData> tagDataPool; |
| 20 | + private readonly ObjectPool<List<TextTagData>> tagListPool; |
| 21 | + |
| 22 | + public Dictionary<int, List<TextTagData>> CharacterTagEffects { get; } = new(); |
| 23 | + private readonly Func<string, string, bool> tagValidator; |
| 24 | + |
| 25 | + // Reusable collections to reduce allocations |
| 26 | + private readonly Dictionary<string, Stack<TextTagData>> activeStacks = new(16); |
| 27 | + |
| 28 | + public TextMeshPreprocessor(Func<string, string, bool> tagValidator) |
| 29 | + { |
| 30 | + this.tagValidator = tagValidator ?? throw new ArgumentNullException(nameof(tagValidator)); |
| 31 | + |
| 32 | + tagDataPool = new ObjectPool<TextTagData>( |
| 33 | + createFunc: () => new TextTagData(), |
| 34 | + actionOnGet: (tag) => tag.Reset(), |
| 35 | + actionOnRelease: (tag) => tag.Clear(), |
| 36 | + defaultCapacity: 64 |
| 37 | + ); |
| 38 | + |
| 39 | + tagListPool = new ObjectPool<List<TextTagData>>( |
| 40 | + createFunc: () => new List<TextTagData>(), |
| 41 | + actionOnGet: (list) => list.Clear(), |
| 42 | + defaultCapacity: 64 |
| 43 | + ); |
| 44 | + } |
| 45 | + |
| 46 | + public string PreprocessText(string inputText) |
| 47 | + { |
| 48 | + // Checks if the last processed text really changed |
| 49 | + if (inputText != lastInputText) |
| 50 | + { |
| 51 | + processedText = ProcessText(inputText); |
| 52 | + lastInputText = inputText; |
| 53 | + } |
| 54 | + |
| 55 | + return processedText; |
| 56 | + } |
| 57 | + |
| 58 | + private string ProcessText(string input) |
| 59 | + { |
| 60 | + ReleaseResources(); |
| 61 | + CharacterTagEffects.Clear(); |
| 62 | + activeStacks.Clear(); |
| 63 | + |
| 64 | + string processedText = input; |
| 65 | + var matches = Regex.Matches(input, tagPattern); |
| 66 | + int charIndex = 0; |
| 67 | + |
| 68 | + foreach (Match match in matches.Cast<Match>()) |
| 69 | + { |
| 70 | + // Tag properties |
| 71 | + string tag = match.Value; |
| 72 | + bool isClosingTag = match.Groups[1].Value == "/"; |
| 73 | + string tagName = match.Groups[2].Value; |
| 74 | + string attributes = match.Groups[3].Value; |
| 75 | + int tagIndex = match.Index - charIndex; |
| 76 | + |
| 77 | + if (tagValidator(tagName, attributes)) |
| 78 | + { |
| 79 | + if (isClosingTag) |
| 80 | + { |
| 81 | + if (!activeStacks.TryGetValue(tagName, out var activeStack)) |
| 82 | + continue; |
| 83 | + |
| 84 | + var matchingOpenTag = activeStack.Pop(); |
| 85 | + if (matchingOpenTag != null) |
| 86 | + { |
| 87 | + matchingOpenTag.Close(tagIndex); |
| 88 | + for (int i = matchingOpenTag.StartIndex; i < matchingOpenTag.EndIndex; i++) |
| 89 | + { |
| 90 | + // Use pooled list for character tag effects |
| 91 | + if (!CharacterTagEffects.TryGetValue(i, out var tagList)) |
| 92 | + { |
| 93 | + tagList = tagListPool.Get(); |
| 94 | + CharacterTagEffects[i] = tagList; |
| 95 | + } |
| 96 | + |
| 97 | + // Only add the innermost tag at each position |
| 98 | + if (!tagList.Any(t => t.Name == matchingOpenTag.Name)) |
| 99 | + tagList.Add(matchingOpenTag); |
| 100 | + } |
| 101 | + } |
| 102 | + } |
| 103 | + else |
| 104 | + { |
| 105 | + var tagData = tagDataPool.Get(); |
| 106 | + tagData.Initialize(tag, tagName, attributes, tagIndex, int.MaxValue); |
| 107 | + AddTagToStack(tagName, tagData); |
| 108 | + } |
| 109 | + |
| 110 | + // Removes tag from display and offsets the range |
| 111 | + processedText = processedText.Replace(tag, string.Empty); |
| 112 | + charIndex += tag.Length; |
| 113 | + } |
| 114 | + } |
| 115 | + |
| 116 | + foreach (var stack in activeStacks) |
| 117 | + { |
| 118 | + foreach (var tag in stack.Value) |
| 119 | + { |
| 120 | + for (int i = tag.StartIndex; i < processedText.Length; i++) |
| 121 | + { |
| 122 | + // Use pooled list for character tag effects |
| 123 | + if (!CharacterTagEffects.TryGetValue(i, out var tagList)) |
| 124 | + { |
| 125 | + tagList = tagListPool.Get(); |
| 126 | + CharacterTagEffects[i] = tagList; |
| 127 | + } |
| 128 | + |
| 129 | + if (!tagList.Any(t => t.Name == tag.Name)) |
| 130 | + tagList.Add(tag); |
| 131 | + } |
| 132 | + } |
| 133 | + } |
| 134 | + |
| 135 | + return processedText; |
| 136 | + } |
| 137 | + |
| 138 | + private void AddTagToStack(string name, TextTagData data) |
| 139 | + { |
| 140 | + if (activeStacks.TryGetValue(name, out var stack)) |
| 141 | + { |
| 142 | + if (stack.Peek().RawTag != data.RawTag) |
| 143 | + stack.Push(data); |
| 144 | + } |
| 145 | + else |
| 146 | + { |
| 147 | + var newStack = new Stack<TextTagData>(); |
| 148 | + newStack.Push(data); |
| 149 | + activeStacks.Add(name, newStack); |
| 150 | + } |
| 151 | + } |
| 152 | + |
| 153 | + /// <summary> |
| 154 | + /// Releases all tag lists back to the pool. |
| 155 | + /// </summary> |
| 156 | + private void ReleaseResources() |
| 157 | + { |
| 158 | + // Release all the tag lists before rebuilding them |
| 159 | + foreach (var tagList in CharacterTagEffects.Values) |
| 160 | + { |
| 161 | + if (tagList != null) |
| 162 | + { |
| 163 | + tagListPool.Release(tagList); |
| 164 | + } |
| 165 | + } |
| 166 | + |
| 167 | + // Clear the dictionary as well |
| 168 | + CharacterTagEffects.Clear(); |
| 169 | + } |
| 170 | + |
| 171 | + public List<TextTagData> GetTagEffectsAtIndex(int index) |
| 172 | + { |
| 173 | + CharacterTagEffects.TryGetValue(index, out var tags); |
| 174 | + return tags; |
| 175 | + } |
| 176 | + |
| 177 | + public void Dispose() |
| 178 | + { |
| 179 | + tagDataPool.Clear(); |
| 180 | + tagListPool.Clear(); |
| 181 | + CharacterTagEffects.Clear(); |
| 182 | + } |
| 183 | + |
| 184 | + public void ClearCache() |
| 185 | + { |
| 186 | + lastInputText = string.Empty; |
| 187 | + processedText = string.Empty; |
| 188 | + } |
| 189 | + } |
| 190 | +} |
0 commit comments