diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Text/DescriptionTextBlock.cs b/src/Snap.Hutao/Snap.Hutao/Control/Text/DescriptionTextBlock.cs
index 897c014f..42a60d62 100644
--- a/src/Snap.Hutao/Snap.Hutao/Control/Text/DescriptionTextBlock.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Control/Text/DescriptionTextBlock.cs
@@ -1,12 +1,14 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
+using Microsoft.EntityFrameworkCore.Query.Internal;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Documents;
using Microsoft.UI.Xaml.Media;
using Snap.Hutao.Control.Extension;
using Snap.Hutao.Control.Media;
+using Snap.Hutao.Control.Text.Syntax.MiHoYo;
using Snap.Hutao.Control.Theme;
using Snap.Hutao.Metadata;
using Windows.Foundation;
@@ -16,23 +18,12 @@ namespace Snap.Hutao.Control.Text;
///
/// 专用于呈现描述文本的文本块
-/// Some part of this file came from:
-/// https://github.com/xunkong/desktop/tree/main/src/Desktop/Desktop/Pages/CharacterInfoPage.xaml.cs
///
[HighQuality]
[DependencyProperty("Description", typeof(string), "", nameof(OnDescriptionChanged))]
[DependencyProperty("TextStyle", typeof(Style), default(Style), nameof(OnTextStyleChanged))]
internal sealed partial class DescriptionTextBlock : ContentControl
{
- private static readonly int RgbaColorTagFullLength = "".Length;
- private static readonly int RgbaColorTagLeftLength = "".Length;
-
- private static readonly int RgbColorTagFullLength = "".Length;
- private static readonly int RgbColorTagLeftLength = "".Length;
-
- private static readonly int ItalicTagFullLength = "".Length;
- private static readonly int ItalicTagLeftLength = "".Length;
-
private readonly TypedEventHandler actualThemeChangedEventHandler;
///
@@ -59,7 +50,7 @@ internal sealed partial class DescriptionTextBlock : ContentControl
try
{
- UpdateDescription(textBlock, description);
+ UpdateDescription(textBlock, MiHoYoSyntaxTree.Parse(SpecialNameHandler.Handle((string)e.NewValue)));
}
catch (Exception ex)
{
@@ -73,85 +64,62 @@ internal sealed partial class DescriptionTextBlock : ContentControl
textBlock.Style = (Style)e.NewValue;
}
- private static void UpdateDescription(TextBlock textBlock, in ReadOnlySpan description)
+ private static void UpdateDescription(TextBlock textBlock, MiHoYoSyntaxTree syntaxTree)
{
textBlock.Inlines.Clear();
+ AppendNode(textBlock, textBlock.Inlines, syntaxTree.Root);
+ }
- int last = 0;
- for (int i = 0; i < description.Length;)
+ private static void AppendNode(TextBlock textBlock, InlineCollection inlines, MiHoYoSyntaxNode node)
+ {
+ switch (node.Kind)
{
- // newline
- if (description[i..].StartsWith(@"\n"))
- {
- AppendText(textBlock, description[last..i]);
- AppendLineBreak(textBlock);
- i += 2;
- last = i;
- }
-
- // color tag
- else if (description[i..].StartsWith("'))
+ case MiHoYoSyntaxKind.Root:
+ foreach (MiHoYoSyntaxNode child in ((MiHoYoRootSyntax)node).Children)
{
- case 16: // RgbaColorTag
- {
- AppendText(textBlock, description[last..i]);
- Rgba32 color = new(description.Slice(i + 8, 8).ToString());
- int length = description[(i + RgbaColorTagLeftLength)..].IndexOf('<');
- AppendColorText(textBlock, description.Slice(i + RgbaColorTagLeftLength, length), color);
-
- i += length + RgbaColorTagFullLength;
- last = i;
- break;
- }
-
- case 14: // RgbColorTag
- {
- AppendText(textBlock, description[last..i]);
- Rgba32 color = new(description.Slice(i + 8, 6).ToString());
- int length = description[(i + RgbColorTagLeftLength)..].IndexOf('<');
- AppendColorText(textBlock, description.Slice(i + RgbColorTagLeftLength, length), color);
-
- i += length + RgbColorTagFullLength;
- last = i;
- break;
- }
+ AppendNode(textBlock, inlines, child);
}
- }
- // italic
- else if (description[i..].StartsWith(" slice)
+ private static void AppendLine(TextBlock textBlock, InlineCollection inlines, MiHoYoLineSyntax line)
{
- text.Inlines.Add(new Run { Text = slice.ToString() });
+ foreach (MiHoYoSyntaxNode node in line.Children)
+ {
+ AppendNode(textBlock, inlines, node);
+ }
+
+ if (line.HasTailingNewLine)
+ {
+ inlines.Add(new LineBreak());
+ }
}
- private static void AppendColorText(TextBlock text, in ReadOnlySpan slice, Rgba32 color)
+ private static void AppendPlainText(TextBlock textBlock, InlineCollection inlines, MiHoYoPlainTextSyntax plainText)
{
+ // PlainText doesn't have children
+ inlines.Add(new Run { Text = plainText.Span.ToString() });
+ }
+
+ private static void AppendColorText(TextBlock textBlock, InlineCollection inlines, MiHoYoColorTextSyntax colorText)
+ {
+ Rgba32 color = new(colorText.ColorSpan.ToString());
Color targetColor;
- if (ThemeHelper.IsDarkMode(text.ActualTheme))
+ if (ThemeHelper.IsDarkMode(textBlock.ActualTheme))
{
targetColor = color;
}
@@ -163,30 +131,55 @@ internal sealed partial class DescriptionTextBlock : ContentControl
targetColor = Rgba32.FromHsl(hsl);
}
- text.Inlines.Add(new Run
+ if (colorText.Children.Count > 0)
{
- Text = slice.ToString(),
- Foreground = new SolidColorBrush(targetColor),
- });
+ Span span = new()
+ {
+ Foreground = new SolidColorBrush(targetColor),
+ };
+
+ foreach (MiHoYoSyntaxNode child in colorText.Children)
+ {
+ AppendNode(textBlock, span.Inlines, child);
+ }
+ }
+ else
+ {
+ inlines.Add(new Run
+ {
+ Text = colorText.ContentSpan.ToString(),
+ Foreground = new SolidColorBrush(targetColor),
+ });
+ }
}
- private static void AppendItalicText(TextBlock text, in ReadOnlySpan slice)
+ private static void AppendItalicText(TextBlock textBlock, InlineCollection inlines, MiHoYoItalicTextSyntax italicText)
{
- text.Inlines.Add(new Run
+ if (italicText.Children.Count > 0)
{
- Text = slice.ToString(),
- FontStyle = Windows.UI.Text.FontStyle.Italic,
- });
- }
+ Span span = new()
+ {
+ FontStyle = Windows.UI.Text.FontStyle.Italic,
+ };
- private static void AppendLineBreak(TextBlock text)
- {
- text.Inlines.Add(new LineBreak());
+ foreach (MiHoYoSyntaxNode child in italicText.Children)
+ {
+ AppendNode(textBlock, span.Inlines, child);
+ }
+ }
+ else
+ {
+ inlines.Add(new Run
+ {
+ Text = italicText.ContentSpan.ToString(),
+ FontStyle = Windows.UI.Text.FontStyle.Italic,
+ });
+ }
}
private void OnActualThemeChanged(FrameworkElement sender, object args)
{
// Simply re-apply texts
- UpdateDescription((TextBlock)Content, Description);
+ UpdateDescription((TextBlock)Content, MiHoYoSyntaxTree.Parse(SpecialNameHandler.Handle(Description)));
}
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoColorKind.cs b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoColorKind.cs
new file mode 100644
index 00000000..ccaa8b34
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoColorKind.cs
@@ -0,0 +1,11 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Control.Text.Syntax.MiHoYo;
+
+internal enum MiHoYoColorKind
+{
+ None,
+ Rgba,
+ Rgb,
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoColorTextSyntax.cs b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoColorTextSyntax.cs
new file mode 100644
index 00000000..c5662232
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoColorTextSyntax.cs
@@ -0,0 +1,49 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Control.Text.Syntax.MiHoYo;
+
+internal sealed class MiHoYoColorTextSyntax : MiHoYoXmlElementSyntax
+{
+ public MiHoYoColorTextSyntax(MiHoYoColorKind colorKind, string text, int start, int end)
+ : base(MiHoYoSyntaxKind.ColorText, text, start, end)
+ {
+ ColorKind = colorKind;
+ }
+
+ public MiHoYoColorTextSyntax(MiHoYoColorKind colorKind, string text, TextPosition position)
+ : base(MiHoYoSyntaxKind.ColorText, text, position)
+ {
+ ColorKind = colorKind;
+ }
+
+ public MiHoYoColorKind ColorKind { get; }
+
+ public override TextPosition ContentPosition
+ {
+ get
+ {
+ return ColorKind switch
+ {
+ MiHoYoColorKind.Rgba => new(Position.Start + 17, Position.End - 8),
+ MiHoYoColorKind.Rgb => new(Position.Start + 15, Position.End - 8),
+ _ => throw Must.NeverHappen(),
+ };
+ }
+ }
+
+ public TextPosition ColorPosition
+ {
+ get
+ {
+ return ColorKind switch
+ {
+ MiHoYoColorKind.Rgba => new(Position.Start + 8, Position.Start + 16),
+ MiHoYoColorKind.Rgb => new(Position.Start + 8, Position.Start + 14),
+ _ => throw Must.NeverHappen(),
+ };
+ }
+ }
+
+ public ReadOnlySpan ColorSpan { get => Text.AsSpan()[ColorPosition.Start..ColorPosition.End]; }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoItalicTextSyntax.cs b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoItalicTextSyntax.cs
new file mode 100644
index 00000000..e02fdb5c
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoItalicTextSyntax.cs
@@ -0,0 +1,19 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Control.Text.Syntax.MiHoYo;
+
+internal sealed class MiHoYoItalicTextSyntax : MiHoYoXmlElementSyntax
+{
+ public MiHoYoItalicTextSyntax(string text, int start, int end)
+ : base(MiHoYoSyntaxKind.ItalicText, text, start, end)
+ {
+ }
+
+ public MiHoYoItalicTextSyntax(string text, TextPosition position)
+ : base(MiHoYoSyntaxKind.ItalicText, text, position)
+ {
+ }
+
+ public override TextPosition ContentPosition { get => new(Position.Start + 3, Position.End - 4); }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoLineSyntax.cs b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoLineSyntax.cs
new file mode 100644
index 00000000..43711298
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoLineSyntax.cs
@@ -0,0 +1,17 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Control.Text.Syntax.MiHoYo;
+
+internal sealed class MiHoYoLineSyntax : MiHoYoSyntaxNode
+{
+ public MiHoYoLineSyntax(bool hasTailingNewLine, string text, int start, int end)
+ : base(MiHoYoSyntaxKind.Line, text, start, end)
+ {
+ HasTailingNewLine = hasTailingNewLine;
+ }
+
+ public bool HasTailingNewLine { get; }
+
+ public TextPosition TextPosition { get => HasTailingNewLine ? new(Position.Start, Position.Length - 1) : Position; }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoPlainTextSyntax.cs b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoPlainTextSyntax.cs
new file mode 100644
index 00000000..7bf707d8
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoPlainTextSyntax.cs
@@ -0,0 +1,12 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Control.Text.Syntax.MiHoYo;
+
+internal sealed class MiHoYoPlainTextSyntax : MiHoYoSyntaxNode
+{
+ public MiHoYoPlainTextSyntax(string text, int start, int end)
+ : base(MiHoYoSyntaxKind.PlainText, text, start, end)
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoRootSyntax.cs b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoRootSyntax.cs
new file mode 100644
index 00000000..73c003e5
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoRootSyntax.cs
@@ -0,0 +1,12 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Control.Text.Syntax.MiHoYo;
+
+internal sealed class MiHoYoRootSyntax : MiHoYoSyntaxNode
+{
+ public MiHoYoRootSyntax(string text, int start, int end)
+ : base(MiHoYoSyntaxKind.Root, text, start, end)
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoSyntaxKind.cs b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoSyntaxKind.cs
new file mode 100644
index 00000000..d844920c
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoSyntaxKind.cs
@@ -0,0 +1,14 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Control.Text.Syntax.MiHoYo;
+
+internal enum MiHoYoSyntaxKind
+{
+ None,
+ Root,
+ Line,
+ PlainText,
+ ColorText,
+ ItalicText,
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoSyntaxNode.cs b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoSyntaxNode.cs
new file mode 100644
index 00000000..297afd66
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoSyntaxNode.cs
@@ -0,0 +1,17 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Control.Text.Syntax.MiHoYo;
+
+internal abstract class MiHoYoSyntaxNode : SyntaxNode
+{
+ public MiHoYoSyntaxNode(MiHoYoSyntaxKind kind, string text, int start, int end)
+ : base(kind, text, start, end)
+ {
+ }
+
+ public MiHoYoSyntaxNode(MiHoYoSyntaxKind kind, string text, TextPosition position)
+ : base(kind, text, position)
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoSyntaxTree.cs b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoSyntaxTree.cs
new file mode 100644
index 00000000..79cb2296
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoSyntaxTree.cs
@@ -0,0 +1,163 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Control.Text.Syntax.MiHoYo;
+
+internal sealed class MiHoYoSyntaxTree
+{
+ public MiHoYoSyntaxNode Root { get; set; } = default!;
+
+ public string Text { get; set; } = default!;
+
+ public static MiHoYoSyntaxTree Parse(string text)
+ {
+ MiHoYoRootSyntax root = new(text, 0, text.Length);
+ ParseLines(text, root);
+
+ MiHoYoSyntaxTree tree = new()
+ {
+ Text = text,
+ Root = root,
+ };
+
+ return tree;
+ }
+
+ private static void ParseLines(string text, MiHoYoRootSyntax syntax)
+ {
+ ReadOnlySpan textSpan = text.AsSpan();
+ int previousProcessedIndexOfText = 0;
+
+ while (true)
+ {
+ int newLineIndexAtSlicedText = textSpan[previousProcessedIndexOfText..].IndexOf('\n');
+
+ if (newLineIndexAtSlicedText < 0)
+ {
+ MiHoYoLineSyntax line = new(false, text, previousProcessedIndexOfText, textSpan.Length);
+ ParseComponents(text, line);
+ syntax.Children.Add(line);
+ break;
+ }
+
+ MiHoYoLineSyntax lineWithBreaking = new(true, text, previousProcessedIndexOfText, previousProcessedIndexOfText + newLineIndexAtSlicedText + 1);
+ ParseComponents(text, lineWithBreaking);
+ syntax.Children.Add(lineWithBreaking);
+
+ previousProcessedIndexOfText = lineWithBreaking.Position.End;
+ }
+ }
+
+ private static void ParseComponents(string text, MiHoYoSyntaxNode syntax)
+ {
+ TextPosition contentPosition = syntax switch
+ {
+ MiHoYoXmlElementSyntax xmlSyntax => xmlSyntax.ContentPosition,
+ MiHoYoLineSyntax lineSyntax => lineSyntax.TextPosition,
+ _ => syntax.Position,
+ };
+ ReadOnlySpan contentSpan = text.AsSpan().Slice(contentPosition.Start, contentPosition.Length);
+
+ int previousProcessedIndexOfContent = 0;
+ while (true)
+ {
+ int fullXmlOpeningIndexOfContent = contentSpan[previousProcessedIndexOfContent..].IndexOf('<');
+
+ // End of content
+ if (fullXmlOpeningIndexOfContent < 0)
+ {
+ MiHoYoPlainTextSyntax plainText = new(text, contentPosition.Start + previousProcessedIndexOfContent, contentPosition.End);
+ syntax.Children.Add(plainText);
+ break;
+ }
+
+ // We have plain text between xml elements
+ if (previousProcessedIndexOfContent < fullXmlOpeningIndexOfContent)
+ {
+ MiHoYoPlainTextSyntax plainText = new(text, contentPosition.Start + previousProcessedIndexOfContent, contentPosition.End);
+ syntax.Children.Add(plainText);
+ }
+
+ // Peek the next character after '<'
+ switch (contentSpan[previousProcessedIndexOfContent + fullXmlOpeningIndexOfContent + 1])
+ {
+ case 'c':
+ {
+ //
+ //
+ int colorTagClosingEndOfSlicedContent = IndexOfClosingEnd(contentSpan[fullXmlOpeningIndexOfContent..], out int colorTagLeftClosingEndOfSlicedContent);
+
+ MiHoYoColorKind colorKind = colorTagLeftClosingEndOfSlicedContent switch
+ {
+ 17 => MiHoYoColorKind.Rgba,
+ 15 => MiHoYoColorKind.Rgb,
+ _ => throw Must.NeverHappen(),
+ };
+
+ TextPosition positionOfColorElement = new(0, colorTagClosingEndOfSlicedContent);
+ TextPosition positionAtContent = positionOfColorElement.Add(fullXmlOpeningIndexOfContent);
+ TextPosition positionAtText = positionAtContent.Add(contentPosition.Start + previousProcessedIndexOfContent);
+
+ MiHoYoColorTextSyntax colorText = new(colorKind, text, positionAtText);
+ ParseComponents(text, colorText);
+ syntax.Children.Add(colorText);
+ previousProcessedIndexOfContent = positionAtContent.End;
+ break;
+ }
+
+ case 'i':
+ {
+ // sometext 14
+ int italicTagClosingEndOfSlicedContent = IndexOfClosingEnd(contentSpan[fullXmlOpeningIndexOfContent..], out _);
+
+ TextPosition positionOfItalicElement = new(0, italicTagClosingEndOfSlicedContent);
+ TextPosition positionAtContent = positionOfItalicElement.Add(fullXmlOpeningIndexOfContent);
+ TextPosition positionAtText = positionAtContent.Add(contentPosition.Start + previousProcessedIndexOfContent);
+
+ MiHoYoItalicTextSyntax italicText = new(text, positionAtText);
+ ParseComponents(text, italicText);
+ syntax.Children.Add(italicText);
+ previousProcessedIndexOfContent = positionAtContent.End;
+ break;
+ }
+ }
+ }
+ }
+
+ private static int IndexOfClosingEnd(in ReadOnlySpan span, out int leftClosingEnd)
+ {
+ leftClosingEnd = 0;
+
+ int openingCount = 0;
+ int closingCount = 0;
+
+ int current = 0;
+
+ // Considering text1text2text3
+ // Considering text1text2text3
+ while (true)
+ {
+ int leftMarkIndex = span[current..].IndexOf('<');
+ if (span[current..][leftMarkIndex + 1] is '/')
+ {
+ closingCount++;
+ }
+ else
+ {
+ openingCount++;
+ }
+
+ current += span[current..].IndexOf('>') + 1;
+
+ if (openingCount is 1 && closingCount is 0)
+ {
+ leftClosingEnd = current;
+ }
+
+ if (openingCount == closingCount)
+ {
+ return current;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoXmlElementSyntax.cs b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoXmlElementSyntax.cs
new file mode 100644
index 00000000..8907bb49
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/MiHoYo/MiHoYoXmlElementSyntax.cs
@@ -0,0 +1,21 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Control.Text.Syntax.MiHoYo;
+
+internal abstract class MiHoYoXmlElementSyntax : MiHoYoSyntaxNode
+{
+ public MiHoYoXmlElementSyntax(MiHoYoSyntaxKind kind, string text, int start, int end)
+ : base(kind, text, start, end)
+ {
+ }
+
+ public MiHoYoXmlElementSyntax(MiHoYoSyntaxKind kind, string text, TextPosition position)
+ : base(kind, text, position)
+ {
+ }
+
+ public abstract TextPosition ContentPosition { get; }
+
+ public ReadOnlySpan ContentSpan { get => Text.AsSpan(ContentPosition.Start, ContentPosition.Length); }
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/SyntaxNode.cs b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/SyntaxNode.cs
new file mode 100644
index 00000000..0ed91eea
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/SyntaxNode.cs
@@ -0,0 +1,33 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Control.Text.Syntax;
+
+internal abstract class SyntaxNode
+ where TSelf : SyntaxNode
+ where TKind : struct, Enum
+{
+ public SyntaxNode(TKind kind, string text, int start, int end)
+ {
+ Kind = kind;
+ Text = text;
+ Position = new(start, end);
+ }
+
+ public SyntaxNode(TKind kind, string text, TextPosition position)
+ {
+ Kind = kind;
+ Text = text;
+ Position = position;
+ }
+
+ public TKind Kind { get; protected set; }
+
+ public List Children { get; } = [];
+
+ public TextPosition Position { get; protected set; }
+
+ public ReadOnlySpan Span { get => Text.AsSpan().Slice(Position.Start, Position.Length); }
+
+ protected string Text { get; set; } = default!;
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/TextPosition.cs b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/TextPosition.cs
new file mode 100644
index 00000000..89bf97c9
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Control/Text/Syntax/TextPosition.cs
@@ -0,0 +1,29 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using System.Diagnostics;
+
+namespace Snap.Hutao.Control.Text.Syntax;
+
+[DebuggerDisplay("[{Start}..{End}]")]
+internal readonly struct TextPosition
+{
+ public readonly int Start;
+ public readonly int End;
+
+ public TextPosition(int start, int end)
+ {
+ Start = start;
+ End = end;
+ }
+
+ public readonly int Length
+ {
+ get => End - Start;
+ }
+
+ public TextPosition Add(int offset)
+ {
+ return new(Start + offset, End + offset);
+ }
+}
\ No newline at end of file