mirror of
https://github.com/AtomUI/AtomUI.git
synced 2026-10-19 23:30:44 +08:00
216 lines
6.8 KiB
C#
216 lines
6.8 KiB
C#
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using System.Xml.Linq;
|
|
using AtomUI.Docs.LLMsGenerator.Catalog;
|
|
|
|
namespace AtomUI.Docs.LLMsGenerator.Reader;
|
|
|
|
public static partial class ControlThemeStructureReader
|
|
{
|
|
private const int MaxTemplateDepth = 7;
|
|
private const int MaxTemplateNodes = 36;
|
|
|
|
public static string Read(
|
|
string repositoryRoot,
|
|
ControlDocumentInfo control,
|
|
string displayName,
|
|
string sourceIndex)
|
|
{
|
|
var candidates = FindThemeCandidates(repositoryRoot, control, displayName, sourceIndex);
|
|
foreach (var candidate in candidates)
|
|
{
|
|
var structure = TryReadControlTemplate(repositoryRoot, candidate, control, displayName);
|
|
if (!string.IsNullOrWhiteSpace(structure))
|
|
{
|
|
return structure;
|
|
}
|
|
}
|
|
|
|
return string.Empty;
|
|
}
|
|
|
|
private static IReadOnlyList<string> FindThemeCandidates(
|
|
string repositoryRoot,
|
|
ControlDocumentInfo control,
|
|
string displayName,
|
|
string sourceIndex)
|
|
{
|
|
var candidates = new List<string>();
|
|
|
|
foreach (Match match in SourceThemePathRegex().Matches(sourceIndex))
|
|
{
|
|
var path = Path.Combine(repositoryRoot, match.Value);
|
|
if (File.Exists(path))
|
|
{
|
|
candidates.Add(path);
|
|
}
|
|
}
|
|
|
|
var expectedNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
$"{displayName}Theme.axaml",
|
|
$"{ToPascalName(control.Name)}Theme.axaml",
|
|
$"{control.Name.Replace("-", string.Empty, StringComparison.Ordinal)}Theme.axaml"
|
|
};
|
|
|
|
foreach (var sourceRoot in control.SourceRoots.Where(Directory.Exists))
|
|
{
|
|
foreach (var file in Directory.GetFiles(sourceRoot, "*Theme.axaml", SearchOption.AllDirectories))
|
|
{
|
|
if (expectedNames.Contains(Path.GetFileName(file)))
|
|
{
|
|
candidates.Add(file);
|
|
}
|
|
}
|
|
}
|
|
|
|
return candidates.Distinct(StringComparer.Ordinal).ToArray();
|
|
}
|
|
|
|
private static string TryReadControlTemplate(
|
|
string repositoryRoot,
|
|
string themePath,
|
|
ControlDocumentInfo control,
|
|
string displayName)
|
|
{
|
|
var text = File.ReadAllText(themePath);
|
|
if (!text.Contains("ControlTheme", StringComparison.Ordinal) ||
|
|
!text.Contains("ControlTemplate", StringComparison.Ordinal))
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
if (!TargetsControl(text, control, displayName))
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
XDocument document;
|
|
try
|
|
{
|
|
document = XDocument.Parse(text, LoadOptions.PreserveWhitespace);
|
|
}
|
|
catch
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
var template = document.Descendants().FirstOrDefault(element => element.Name.LocalName == "ControlTemplate");
|
|
if (template is null)
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
var visualRoots = template.Elements().Where(IsVisualNode).ToArray();
|
|
if (visualRoots.Length == 0)
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
var builder = new StringBuilder();
|
|
builder.AppendLine($"来源:`{Path.GetRelativePath(repositoryRoot, themePath).Replace(Path.DirectorySeparatorChar, '/')}`");
|
|
builder.AppendLine();
|
|
builder.AppendLine("```xml");
|
|
|
|
var remainingNodes = MaxTemplateNodes;
|
|
foreach (var root in visualRoots)
|
|
{
|
|
AppendElement(builder, root, 0, ref remainingNodes);
|
|
if (remainingNodes <= 0)
|
|
{
|
|
builder.AppendLine(" <!-- structure truncated -->");
|
|
break;
|
|
}
|
|
}
|
|
|
|
builder.AppendLine("```");
|
|
return builder.ToString().TrimEnd();
|
|
}
|
|
|
|
private static bool TargetsControl(string text, ControlDocumentInfo control, string displayName)
|
|
{
|
|
var names = new[]
|
|
{
|
|
displayName,
|
|
ToPascalName(control.Name),
|
|
control.Name.Replace("-", string.Empty, StringComparison.Ordinal)
|
|
};
|
|
|
|
return names.Any(name =>
|
|
text.Contains($"TargetType=\"atom:{name}\"", StringComparison.Ordinal) ||
|
|
text.Contains($"TargetType=\"atomc:{name}\"", StringComparison.Ordinal) ||
|
|
text.Contains($"TargetType=\"{name}\"", StringComparison.Ordinal) ||
|
|
text.Contains($":{name}\"", StringComparison.Ordinal));
|
|
}
|
|
|
|
private static void AppendElement(StringBuilder builder, XElement element, int depth, ref int remainingNodes)
|
|
{
|
|
if (remainingNodes <= 0 || depth >= MaxTemplateDepth)
|
|
{
|
|
return;
|
|
}
|
|
|
|
remainingNodes--;
|
|
var indent = new string(' ', depth * 4);
|
|
var name = element.Name.LocalName;
|
|
var nameAttribute = ReadNameAttribute(element);
|
|
var children = element.Elements().Where(IsVisualNode).ToArray();
|
|
|
|
if (children.Length == 0)
|
|
{
|
|
builder.AppendLine(string.IsNullOrWhiteSpace(nameAttribute)
|
|
? $"{indent}<{name} />"
|
|
: $"{indent}<{name} Name=\"{EscapeAttribute(nameAttribute)}\" />");
|
|
return;
|
|
}
|
|
|
|
builder.AppendLine(string.IsNullOrWhiteSpace(nameAttribute)
|
|
? $"{indent}<{name}>"
|
|
: $"{indent}<{name} Name=\"{EscapeAttribute(nameAttribute)}\">");
|
|
|
|
foreach (var child in children)
|
|
{
|
|
AppendElement(builder, child, depth + 1, ref remainingNodes);
|
|
if (remainingNodes <= 0)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
builder.AppendLine($"{indent}</{name}>");
|
|
}
|
|
|
|
private static string ReadNameAttribute(XElement element)
|
|
{
|
|
return element.Attributes().FirstOrDefault(attribute =>
|
|
attribute.Name.LocalName is "Name" or "x:Name")?.Value ??
|
|
string.Empty;
|
|
}
|
|
|
|
private static bool IsVisualNode(XElement element)
|
|
{
|
|
var localName = element.Name.LocalName;
|
|
if (localName.Contains('.', StringComparison.Ordinal))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return localName is not "Binding" and not "MultiBinding" and not "Setter" and not "Style"
|
|
and not "ControlTemplate" and not "ControlTheme" and not "Transitions";
|
|
}
|
|
|
|
private static string EscapeAttribute(string value)
|
|
{
|
|
return value.Replace("\"", """, StringComparison.Ordinal);
|
|
}
|
|
|
|
private static string ToPascalName(string value)
|
|
{
|
|
return string.Concat(value.Split('-', StringSplitOptions.RemoveEmptyEntries)
|
|
.Select(part => char.ToUpperInvariant(part[0]) + part[1..]));
|
|
}
|
|
|
|
[GeneratedRegex(@"src/[^\s`]+Theme\.axaml")]
|
|
private static partial Regex SourceThemePathRegex();
|
|
}
|