|
| 1 | +// See https://aka.ms/new-console-template for more information |
| 2 | +using System.Security; |
| 3 | +using System.Text; |
| 4 | +using System.Text.RegularExpressions; |
| 5 | + |
| 6 | + |
| 7 | +string viewDirectory = @"O:\source\Z2Randomizer\CrossPlatformUI\Views\Tabs"; |
| 8 | +string[] files = Directory.GetFiles(viewDirectory, "*.axaml"); |
| 9 | +string resourceFile = @"O:\source\Z2Randomizer\CrossPlatformUI\Lang\Resources.resx"; |
| 10 | +string wikiDirectory = @"O:\source\Z2Randomizer\ToolTipUpdater\Z2Randomizer.wiki"; |
| 11 | + |
| 12 | +// (?<=\\|/) ← positive look-behind: ensure we start just after a slash or backslash |
| 13 | +// ([^\\\/]+?) ← capture one or more chars that are not slash/backslash, as few as possible |
| 14 | +// (?=View ← positive look-ahead: next must be "View" |
| 15 | +// (?:\..+)?$ ← optionally followed by a dot+ext, then end of string |
| 16 | +// ) |
| 17 | +Regex tabNameRegex = new Regex(@"(?<=\\|/)([^\\\/]+?)(?=View(?:\..+)?$)"); |
| 18 | + |
| 19 | +Regex markdownHeaderRegex = new Regex(@"^(#{1,6})\s+(.*?)\s*$", RegexOptions.Multiline | RegexOptions.IgnoreCase); |
| 20 | + |
| 21 | +foreach (string file in files) |
| 22 | +{ |
| 23 | + Console.WriteLine($"Processing: {file}"); |
| 24 | + |
| 25 | + var m = tabNameRegex.Match(file); |
| 26 | + if (m.Success) |
| 27 | + { |
| 28 | + string viewName = m.Groups[1].Value; |
| 29 | + string wikiFile = $"{wikiDirectory}\\{viewName}-Configuration-Reference.md"; |
| 30 | + Console.WriteLine($"Input: {file} Tab: \"{viewName}\" Wiki: \"{wikiFile}\""); |
| 31 | + UpdateXmlTooltips(file, resourceFile, wikiFile); |
| 32 | + } |
| 33 | + else |
| 34 | + { |
| 35 | + Console.WriteLine($"Unrecognized view axaml {file}"); |
| 36 | + } |
| 37 | +} |
| 38 | +UpdateXmlTooltips(@$"{viewDirectory}\SpritePreviewView.axaml", resourceFile, $@"{wikiDirectory}\Customize-Configuration-Reference.md"); |
| 39 | + |
| 40 | +static string WordWrapAndStyle(string text, int maxLineLength) |
| 41 | +{ |
| 42 | + var outputLines = new List<string>(); |
| 43 | + var inputLines = text.Split(["\r\n", "\n"], StringSplitOptions.None); |
| 44 | + |
| 45 | + foreach (var inputLine in inputLines) |
| 46 | + { |
| 47 | + // Detect leading whitespace (indentation) |
| 48 | + var indentMatch = Regex.Match(inputLine, @"^([\s-]*)"); |
| 49 | + bool bullet = indentMatch.Value.Contains("-"); |
| 50 | + string indent = new string(' ', indentMatch.Length + (bullet ? 2 : 0)); |
| 51 | + |
| 52 | + // Trim leading whitespace for word splitting |
| 53 | + var trimmedLine = inputLine.TrimStart(['-', ' ', '\t']); |
| 54 | + |
| 55 | + // If the trimmed line is empty, preserve the blank line |
| 56 | + if (trimmedLine.Length == 0) |
| 57 | + { |
| 58 | + outputLines.Add(string.Empty); |
| 59 | + continue; |
| 60 | + } |
| 61 | + |
| 62 | + var words = trimmedLine.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); |
| 63 | + var line = new StringBuilder(indent); |
| 64 | + if (bullet && indent.Length > 1) |
| 65 | + { |
| 66 | + var bulletChar = indent.Length % 4 == 0 ? '•' : '◦'; |
| 67 | + line[indent.Length - 2] = bulletChar; |
| 68 | + } |
| 69 | + int currentLength = indent.Length; |
| 70 | + |
| 71 | + foreach (var word in words) |
| 72 | + { |
| 73 | + if (currentLength + word.Length + 1 > maxLineLength) |
| 74 | + { |
| 75 | + outputLines.Add(line.ToString()); |
| 76 | + line.Clear(); |
| 77 | + line.Append(indent); |
| 78 | + currentLength = indent.Length; |
| 79 | + } |
| 80 | + |
| 81 | + if (currentLength > indent.Length) |
| 82 | + { |
| 83 | + line.Append(' '); |
| 84 | + currentLength++; |
| 85 | + } |
| 86 | + |
| 87 | + line.Append(word); |
| 88 | + currentLength += word.Length; |
| 89 | + } |
| 90 | + |
| 91 | + if (line.Length > 0) |
| 92 | + outputLines.Add(line.ToString()); |
| 93 | + } |
| 94 | + |
| 95 | + return string.Join(Environment.NewLine, outputLines); |
| 96 | +} |
| 97 | + |
| 98 | +string? GetMarkdownSectionText(string markdownContent, string headerToFind) |
| 99 | +{ |
| 100 | + var headerPattern = markdownHeaderRegex; |
| 101 | + var matches = headerPattern.Matches(markdownContent); |
| 102 | + |
| 103 | + int startIndex = -1; |
| 104 | + int endIndex = markdownContent.Length; |
| 105 | + |
| 106 | + for (int i = 0; i < matches.Count; i++) |
| 107 | + { |
| 108 | + string headerText = matches[i].Groups[2].Value.Trim(); |
| 109 | + |
| 110 | + /* No sub headers: |
| 111 | + if (string.Equals(headerText, headerToFind)) |
| 112 | + { |
| 113 | + startIndex = matches[i].Index + matches[i].Length; |
| 114 | + if (i + 1 < matches.Count) |
| 115 | + { |
| 116 | + endIndex = matches[i + 1].Index; |
| 117 | + } |
| 118 | + break; |
| 119 | + }*/ |
| 120 | + if (string.Equals(headerText, headerToFind, StringComparison.OrdinalIgnoreCase)) |
| 121 | + { |
| 122 | + startIndex = matches[i].Index + matches[i].Length; |
| 123 | + |
| 124 | + // Find the next header of same or higher level |
| 125 | + for (int j = i + 1; j < matches.Count; j++) |
| 126 | + { |
| 127 | + if (matches[j].Groups[1].Length <= matches[i].Groups[1].Length) |
| 128 | + { |
| 129 | + endIndex = matches[j].Index; |
| 130 | + break; |
| 131 | + } |
| 132 | + } |
| 133 | + |
| 134 | + break; |
| 135 | + } |
| 136 | + } |
| 137 | + |
| 138 | + if (startIndex == -1) |
| 139 | + { |
| 140 | + return null; // Header not found |
| 141 | + } |
| 142 | + |
| 143 | + return markdownContent.Substring(startIndex, endIndex - startIndex).Trim(); |
| 144 | +} |
| 145 | + |
| 146 | +void UpdateXmlTooltips(string xmlFilePath, string resourceFilePath, string wikiFilePath) |
| 147 | +{ |
| 148 | + string viewXml = File.ReadAllText(xmlFilePath); |
| 149 | + string resourceXml = File.ReadAllText(resourceFilePath); |
| 150 | + string wikiMarkdown; |
| 151 | + try |
| 152 | + { |
| 153 | + wikiMarkdown = File.ReadAllText(wikiFilePath); |
| 154 | + } |
| 155 | + catch(FileNotFoundException) |
| 156 | + { |
| 157 | + Console.WriteLine($"No wiki file found {wikiFilePath}"); |
| 158 | + return; |
| 159 | + } |
| 160 | + |
| 161 | + // Match multi-line CheckBox tags (opening tag only) |
| 162 | + //string checkboxPattern1 = @"<CheckBox\b[^>]*?(\/>)"; // match up to the end of the start tag |
| 163 | + //var checkboxRegex1 = new Regex(checkboxPattern1, RegexOptions.Singleline | RegexOptions.IgnoreCase); |
| 164 | + |
| 165 | + var elementRegex = new Regex(@"<(CheckBox|ComboBox|StackPanel)\b[^>]*>(.*?)</\1>", RegexOptions.Singleline | RegexOptions.IgnoreCase); |
| 166 | + var elementLabelRegex = new Regex(@"^[^>]*(?:Content|assists:ComboBoxAssist.Label)\s*=\s*""([^""]+)""", RegexOptions.Singleline); |
| 167 | + var textBlockRegex = new Regex(@"[^<]*<TextBlock\b.*?>(.*?)</TextBlock>", RegexOptions.Singleline); |
| 168 | + |
| 169 | + bool modified = false; |
| 170 | + string updatedViewXml = viewXml; |
| 171 | + string updatedResourceXml = resourceXml; |
| 172 | + |
| 173 | + int resourceInsertPos = GetXmlDataInsertPos(updatedResourceXml); |
| 174 | + |
| 175 | + int position = 0; |
| 176 | + while (position < updatedViewXml.Length) |
| 177 | + { |
| 178 | + var elementMatch = elementRegex.Match(updatedViewXml, position); |
| 179 | + if (!elementMatch.Success) { break; } |
| 180 | + position = elementMatch.Index + 1; |
| 181 | + |
| 182 | + string elementBlock = elementMatch.Value; |
| 183 | + string elementInner = elementMatch.Groups[2].Value; |
| 184 | + |
| 185 | + string elementLabel; |
| 186 | + var elementLabelMatch = elementLabelRegex.Match(elementBlock); |
| 187 | + if (elementLabelMatch.Success) |
| 188 | + { |
| 189 | + elementLabel = elementLabelMatch.Groups[1].Value; |
| 190 | + } |
| 191 | + else |
| 192 | + { |
| 193 | + var textBlockMatch = textBlockRegex.Match(elementInner); |
| 194 | + if (!textBlockMatch.Success) |
| 195 | + { |
| 196 | + Console.WriteLine("CheckBox with no content?"); |
| 197 | + Console.WriteLine($"Block: {elementBlock}"); |
| 198 | + continue; |
| 199 | + } |
| 200 | + elementLabel = textBlockMatch.Groups[1].Value; |
| 201 | + } |
| 202 | + // Clean up label |
| 203 | + elementLabel = Regex.Replace(elementLabel, @"<[^>]*>", " "); |
| 204 | + elementLabel = elementLabel.Trim(); |
| 205 | + |
| 206 | + var stringId = Regex.Replace(elementLabel, @"[\s()/\.'+-]", "") + "ToolTip"; |
| 207 | + |
| 208 | + string? descUnescaped = GetMarkdownSectionText(wikiMarkdown, elementLabel); |
| 209 | + if (descUnescaped == null) |
| 210 | + { |
| 211 | + Console.WriteLine($"No ToolTip found for {elementLabel}"); |
| 212 | + continue; |
| 213 | + } |
| 214 | + |
| 215 | + // Ignore options line from Wiki (as you can see the options in the UI) |
| 216 | + descUnescaped = Regex.Replace(descUnescaped, @"^Options:.+", ""); |
| 217 | + /* |
| 218 | + var wikiLinkRegex = new Regex(@"(?<=(?:^|\n|[.?!]\s))[^.\n]*?\[\[([^[\]]+)\]\].*$", RegexOptions.Multiline); |
| 219 | + descUnescaped = wikiLinkRegex.Replace(descUnescaped, match => |
| 220 | + { |
| 221 | + var wikiPage = match.Groups[1].Value.Replace(" ", "-"); |
| 222 | + string embedWikiFile = $"{wikiDirectory}\\{wikiPage}.md"; |
| 223 | + string embedWikiMarkdown; |
| 224 | + try |
| 225 | + { |
| 226 | + embedWikiMarkdown = File.ReadAllText(embedWikiFile); |
| 227 | + } |
| 228 | + catch (FileNotFoundException) |
| 229 | + { |
| 230 | + Console.WriteLine($"No Wiki file found {embedWikiFile}"); |
| 231 | + return "(Error including reference)"; |
| 232 | + } |
| 233 | + return "\n" + embedWikiMarkdown; |
| 234 | + });*/ |
| 235 | + |
| 236 | + // Replace wiki links |
| 237 | + descUnescaped = Regex.Replace(descUnescaped, @"\[[^\]]+\]+", "the Wiki"); |
| 238 | + descUnescaped = descUnescaped.Trim(); |
| 239 | + |
| 240 | + // Line wrap so we don't get really wide tooltips |
| 241 | + string wrappedDesc = WordWrapAndStyle(descUnescaped, 90); |
| 242 | + // Make string XML-safe |
| 243 | + string escapedDesc = SecurityElement.Escape(wrappedDesc); |
| 244 | + // Style headers |
| 245 | + //escapedDesc = markdownHeaderRegex.Replace(escapedDesc, "<Run FontWeight=\"Bold\" Text=\"$2\" />"); |
| 246 | + escapedDesc = markdownHeaderRegex.Replace(escapedDesc, "$2:"); |
| 247 | + // Handle line breaks |
| 248 | + //escapedDesc = escapedDesc.Replace("\n", "<LineBreak/>"); |
| 249 | + //escapedDesc = escapedDesc.Replace("\r", ""); |
| 250 | + |
| 251 | + var newResourceStr = $"\t<data name=\"{stringId}\" xml:space=\"preserve\">\r\n<value>{escapedDesc}</value>\r\n\t</data>"; |
| 252 | + // If entry already exists, replace its content |
| 253 | + var resStartTag = $"[\t ]*<\\s*data\\s+name=\"{Regex.Escape(stringId)}\"[^>]*>"; |
| 254 | + var xmlMatch = Regex.Match(updatedResourceXml, resStartTag, RegexOptions.Singleline | RegexOptions.IgnoreCase); |
| 255 | + if (xmlMatch.Success) |
| 256 | + { |
| 257 | + var regex = new Regex(resStartTag + @".*?<\s*\/\s*data\s*>", RegexOptions.Singleline | RegexOptions.IgnoreCase); |
| 258 | + var maybeUpdatedString = regex.Replace(updatedResourceXml, newResourceStr, 1, xmlMatch.Index); |
| 259 | + if (maybeUpdatedString != updatedResourceXml) |
| 260 | + { |
| 261 | + updatedResourceXml = maybeUpdatedString; |
| 262 | + modified = true; |
| 263 | + Console.WriteLine("Updating resource file tooltip"); |
| 264 | + } |
| 265 | + resourceInsertPos = xmlMatch.Index + newResourceStr.Length; // set new insertion point |
| 266 | + } |
| 267 | + else |
| 268 | + { |
| 269 | + // Inject new resource entry |
| 270 | + string insertion = $"{newResourceStr}\n"; |
| 271 | + updatedResourceXml = updatedResourceXml.Insert(resourceInsertPos, insertion); |
| 272 | + modified = true; |
| 273 | + Console.WriteLine("Inserting tooltip"); |
| 274 | + resourceInsertPos += insertion.Length; |
| 275 | + } |
| 276 | + |
| 277 | + var newViewStr = $"<ToolTip.Tip><TextBlock Text=\"{{x:Static lang:Resources.{stringId}}}\"/></ToolTip.Tip>"; |
| 278 | + // If <ToolTip.Tip> already exists, replace its content |
| 279 | + string updatedBlock; |
| 280 | + if (Regex.IsMatch(elementInner, @"<\s*ToolTip\.Tip\s*>", RegexOptions.Singleline | RegexOptions.IgnoreCase)) |
| 281 | + { |
| 282 | + string updatedInner = Regex.Replace(elementInner, |
| 283 | + @"<\s*ToolTip\.Tip\s*>.*?<\s*\/\s*ToolTip\.Tip\s*>", |
| 284 | + newViewStr, |
| 285 | + RegexOptions.Singleline | RegexOptions.IgnoreCase); |
| 286 | + |
| 287 | + updatedBlock = elementBlock.Replace(elementInner, updatedInner); |
| 288 | + modified = true; |
| 289 | + Console.WriteLine("Updating tooltip"); |
| 290 | + } |
| 291 | + else |
| 292 | + { |
| 293 | + // Inject new <ToolTip.Tip> element after the opening tag |
| 294 | + string insertion = $"{newViewStr}\n"; |
| 295 | + int insertPos = elementBlock.IndexOf('>') + 1; |
| 296 | + updatedBlock = elementBlock.Insert(insertPos, insertion); |
| 297 | + modified = true; |
| 298 | + Console.WriteLine("Inserting tooltip"); |
| 299 | + } |
| 300 | + updatedViewXml = updatedViewXml.Replace(elementBlock, updatedBlock); |
| 301 | + } |
| 302 | + |
| 303 | + if (modified) |
| 304 | + { |
| 305 | + File.WriteAllText(xmlFilePath, updatedViewXml); |
| 306 | + Console.WriteLine("File updated: " + xmlFilePath); |
| 307 | + File.WriteAllText(resourceFilePath, updatedResourceXml); |
| 308 | + Console.WriteLine("File updated: " + resourceFilePath); |
| 309 | + } |
| 310 | + else |
| 311 | + { |
| 312 | + Console.WriteLine("No changes made: " + xmlFilePath); |
| 313 | + } |
| 314 | +} |
| 315 | + |
| 316 | +int GetXmlDataInsertPos(string data) |
| 317 | +{ |
| 318 | + var regex = new Regex(@"</data>(?![\s\S]*</data>)"); |
| 319 | + var match = regex.Match(data); |
| 320 | + if (match.Success) |
| 321 | + { |
| 322 | + return match.Index + match.Length; |
| 323 | + } |
| 324 | + var regex2 = new Regex(@"</root>(?![\s\S]*</root>)"); |
| 325 | + var match2 = regex.Match(data); |
| 326 | + if (match2.Success) |
| 327 | + { |
| 328 | + return match2.Index + match2.Length; |
| 329 | + } |
| 330 | + return data.Length; |
| 331 | +} |
0 commit comments