using System.Diagnostics; using System.Text; namespace RhythmBase.Components.RichText { /// /// Represents a list of rich text strings. /// [DebuggerDisplay($"{{{nameof(GetDebuggerDisplay)}(),nq}}")] public struct RDLine() : IRDRichTextLine where TStyle : IRDRichStringStyle, new() { /// /// Gets or sets the list of rich text strings. /// private RDPhrase[] texts = []; /// /// The length of the string. /// public readonly int Length => texts.Sum(i => i.Length); /// /// Gets or sets the at the specified index. /// /// The index of the to get or set. /// The at the specified index. /// Thrown when the index is out of range. public RDLine this[Index index] { get { int i = index.GetOffset(Length); if (Length <= i) throw new ArgumentOutOfRangeException(nameof(index)); int ti = 0; while (texts[ti].Length < i) { i -= texts[ti].Length; ti++; } RDLine line = new() { texts = [texts[ti][i]] }; return line; } set { int i = index.GetOffset(Length); if (Length <= i) throw new ArgumentOutOfRangeException(nameof(index)); texts = Concat([this[..i], value, this[(i + 1)..]]).texts; } } /// /// Gets or sets the within the specified range. /// /// The range of the to get or set. /// The within the specified range. /// Thrown when the range is out of bounds. public RDLine this[Range range] { get { int start = range.Start.GetOffset(Length); int end = range.End.GetOffset(Length); if (!(start <= end && end <= Length)) throw new ArgumentOutOfRangeException(nameof(range)); int ti = 0, tstart, tend; RDPhrase[] strings = []; while (texts[ti].Length <= start) { start -= texts[ti].Length; end -= texts[ti].Length; ti++; } tstart = ti; while (texts[ti].Length < end) { end -= texts[ti].Length; ti++; } tend = ti; if (tstart == tend) strings = [texts[tstart][start..end]]; else { for (int i = tstart + 1; i < tend; i++) strings = [.. strings, texts[i]]; strings = [texts[tstart][start..], .. strings, texts[tend][..end]]; } RDLine line = new() { texts = strings }; return line; } set { int start = range.Start.GetOffset(Length); int end = range.End.GetOffset(Length); if (!(start < end && end <= Length)) throw new ArgumentOutOfRangeException(nameof(range)); texts = Concat([this[..start], value, this[end..]]).texts; } } /// public static RDLine Concat(params RDLine[] lines) { RDPhrase[] texts = [.. lines[0].texts]; foreach (RDLine line in lines[1..]) { if (texts[^1].Style == line.texts[0].Style) { RDPhrase before = texts[^1], after = line.texts[0]; RDPhrase richString = new(before.Text + after.Text) { Style = before.Style, Events = [.. before.Events, .. after.Events.Select(i => new RDDialogueTone(i.Type, i.Index + before.Length) { Pause = i.Pause })] }; texts = [.. texts[..^1], richString, .. line.texts[1..]]; } else texts = [.. texts, .. line.texts]; } return new() { texts = texts }; } /// /// Implicitly converts a to a . /// /// The to convert. /// A new containing the specified . public static implicit operator RDLine(RDPhrase text) => new() { texts = [text] }; /// /// Implicitly converts a to a . /// /// The to convert. /// A new containing the specified . public static implicit operator RDLine(RDPhrase[] texts) => new() { texts = texts }; /// /// Implicitly converts a to a . /// /// The to convert. /// A new containing the specified . public static implicit operator RDLine(string text) => new() { texts = [new RDPhrase(text)] }; /// /// Deserializes a string into an . /// /// The string to deserialize. /// A new containing the deserialized content. /// Thrown when the input text is null. /// Thrown when the input text has an invalid format. static public RDLine Deserialize(string text) { RDLine line = ""; TStyle style = new(); int start = 0; while (start < text.Length) { TStyle tempStyle = style; int end = text.IndexOf('<', start); if (end == -1) { line += DeserializeStringPart(text[start..], tempStyle); break; } int start2 = text.IndexOf('>', end); int end2 = text.IndexOf('<', end + 1); if (start2 == -1) break; if (end2 != -1 && end2 < start2) { line += DeserializeStringPart(text[start..end2], tempStyle); start = end2; continue; } string textpart = text[start..end]; line += DeserializeStringPart(textpart, tempStyle); string[] keyvalue = text[(end + 1)..start2].Split('=', 2); if (keyvalue[0].StartsWith('/') && style.ResetProperty(keyvalue[0][1..])) start = start2 + 1; else if (style.SetProperty(keyvalue[0], keyvalue.Length == 2 ? keyvalue[1] : "true")) start = start2 + 1; else start = start2 + 1; } return line; } private static RDPhrase DeserializeStringPart(string text, TStyle style) { if (!TStyle.HasPhrase) return new RDPhrase(text) { Style = style }; int pstart = 0; RDDialogueTone[] events = []; while (pstart < text.Length) { int pend = text.IndexOf('[', pstart); if (pend == -1) { break; } int pstart2 = text.IndexOf(']', pend); int pend2 = text.IndexOf('[', pend + 1); if (pstart2 == -1) break; if (pend2 != -1 && pend2 < pstart2) { pstart = pend2 + 1; continue; } string btag = text[(pend + 1)..pstart2]; if (RDDialogueTone.Create(btag, pend, out RDDialogueTone? e) && e is RDDialogueTone ei) events = [.. events, ei]; text = text[..pend] + text[(pstart2 + 1)..]; } return new RDPhrase(text) { Style = style, Events = events }; } /// /// Serializes the current instance to a string. /// /// A string representation of the current instance. /// /// The serialization process converts the rich text line into a string format, including any styling information. /// public readonly string Serialize() { StringBuilder sb = new(); TStyle style = new(); int ci = 0; foreach (RDPhrase str in texts) { sb.Append(TStyle.GetXmlTag(style, str.Style)); string part = str.Text; int offset = 0; foreach (RDDialogueTone e in str.Events) { string serialized = e.Serialize(); part = part.Insert(e.Index + offset, serialized); offset += serialized.Length; } sb.Append(part); ci += str.Length; style = str.Style; } sb.Append(TStyle.GetXmlTag(style, new())); return sb.ToString(); } /// /// Concatenates two instances. /// /// The left . /// The right . /// A new that is the result of concatenating the two specified instances. public static RDLine operator +(RDLine left, RDLine right) => Concat([.. left.texts, .. right.texts]); /// public override readonly string ToString() => string.Join("", texts); /// private readonly string GetDebuggerDisplay() => ToString(); } }