Skip to content

Commit 816a2f3

Browse files
committed
Create ToolTipUpdater project
1 parent 3124181 commit 816a2f3

3 files changed

Lines changed: 354 additions & 0 deletions

File tree

ToolTipUpdater/Program.cs

Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
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+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
</PropertyGroup>
8+
9+
</Project>

Z2Randomizer.sln

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PrintRooms", "PrintRooms\Pr
4646
EndProject
4747
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpriteEdit", "SpriteEdit\SpriteEdit.csproj", "{8C4C208C-42AA-7A93-C10F-D90FA4C0E6EA}"
4848
EndProject
49+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ToolTipUpdater", "ToolTipUpdater\ToolTipUpdater.csproj", "{C391E88B-2D8B-EED2-2740-B58E6F8EBD24}"
50+
EndProject
4951
Global
5052
GlobalSection(SolutionConfigurationPlatforms) = preSolution
5153
Debug|Any CPU = Debug|Any CPU
@@ -236,6 +238,18 @@ Global
236238
{8C4C208C-42AA-7A93-C10F-D90FA4C0E6EA}.Unsafe Debug|Any CPU.Build.0 = Unsafe Debug|Any CPU
237239
{8C4C208C-42AA-7A93-C10F-D90FA4C0E6EA}.Unsafe Debug|x64.ActiveCfg = Unsafe Debug|x64
238240
{8C4C208C-42AA-7A93-C10F-D90FA4C0E6EA}.Unsafe Debug|x64.Build.0 = Unsafe Debug|x64
241+
{C391E88B-2D8B-EED2-2740-B58E6F8EBD24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
242+
{C391E88B-2D8B-EED2-2740-B58E6F8EBD24}.Debug|Any CPU.Build.0 = Debug|Any CPU
243+
{C391E88B-2D8B-EED2-2740-B58E6F8EBD24}.Debug|x64.ActiveCfg = Debug|Any CPU
244+
{C391E88B-2D8B-EED2-2740-B58E6F8EBD24}.Debug|x64.Build.0 = Debug|Any CPU
245+
{C391E88B-2D8B-EED2-2740-B58E6F8EBD24}.Release|Any CPU.ActiveCfg = Release|Any CPU
246+
{C391E88B-2D8B-EED2-2740-B58E6F8EBD24}.Release|Any CPU.Build.0 = Release|Any CPU
247+
{C391E88B-2D8B-EED2-2740-B58E6F8EBD24}.Release|x64.ActiveCfg = Release|Any CPU
248+
{C391E88B-2D8B-EED2-2740-B58E6F8EBD24}.Release|x64.Build.0 = Release|Any CPU
249+
{C391E88B-2D8B-EED2-2740-B58E6F8EBD24}.Unsafe Debug|Any CPU.ActiveCfg = Release|Any CPU
250+
{C391E88B-2D8B-EED2-2740-B58E6F8EBD24}.Unsafe Debug|Any CPU.Build.0 = Release|Any CPU
251+
{C391E88B-2D8B-EED2-2740-B58E6F8EBD24}.Unsafe Debug|x64.ActiveCfg = Release|Any CPU
252+
{C391E88B-2D8B-EED2-2740-B58E6F8EBD24}.Unsafe Debug|x64.Build.0 = Release|Any CPU
239253
EndGlobalSection
240254
GlobalSection(SolutionProperties) = preSolution
241255
HideSolutionNode = FALSE

0 commit comments

Comments
 (0)