namespace QWERTYkez.WordProcessor; internal static class SimplyReplaceExt { private readonly struct TextNodeInfo(Text text, int startIndex, int length) { internal readonly Text Text = text; internal readonly int StartIndex = startIndex; internal readonly int Length = length; } private sealed class ParagraphStructure(string fullText, SimplyReplaceExt.TextNodeInfo[] textNodes) { internal readonly string FullText = fullText; internal readonly TextNodeInfo[] TextNodes = textNodes; internal int FindFirstNodeIndexAtPosition(int position) { if (position < 0 || position >= FullText.Length) return -1; int left = 0; int right = TextNodes.Length - 1; int result = -1; while (left <= right) { int mid = left + ((right - left) >> 1); if (TextNodes[mid].StartIndex <= position) { result = mid; left = mid + 1; } else { right = mid - 1; } } return result >= 0 && TextNodes[result].StartIndex + TextNodes[result].Length > position ? result : -1; } } internal static void Replace(this Body body, string oldValue, string newValue, StringComparison comparisonType) { if (body is null || string.IsNullOrEmpty(oldValue)) return; var paragraphs = body.Elements(); foreach (var paragraph in paragraphs) { paragraph?.SimpleReplace(oldValue, newValue, comparisonType); } } internal static void Replace(this Body body, IEnumerable> replacements, StringComparison comparisonType) { if (body is null || replacements is null || replacements.Count() == 0) return; var paragraphs = body.Elements(); foreach (var paragraph in paragraphs) { paragraph?.Replace(replacements, comparisonType); } } internal static void Replace(this Body body, IEnumerable> replacements, StringComparison comparisonType) { if (body is null || replacements is null || replacements.Count() == 0) return; var paragraphs = body.Elements(); foreach (var paragraph in paragraphs) { paragraph?.Replace(replacements, comparisonType); } } internal static bool SimpleReplace(this Paragraph? paragraph, string oldValue, string newValue, StringComparison comparisonType, bool breakPage = false) { if (paragraph is null || string.IsNullOrEmpty(oldValue)) return false; var paragraphText = paragraph.InnerText; if (string.IsNullOrEmpty(paragraphText)) return false; int matchIndex = paragraphText.IndexOf(oldValue, comparisonType); if (matchIndex == -1) return false; var runs = paragraph.Elements(); if (!runs.Any()) return false; var structure = AnalyzeParagraphStructure(runs); if (structure.FullText.Length == 0) return false; int matchEnd = matchIndex + oldValue.Length; var nodesToReplace = FindNodesToReplace(structure, matchIndex, matchEnd); if (nodesToReplace.Count == 0) return false; ExecuteReplacement(nodesToReplace, matchIndex, matchEnd, newValue, breakPage); return true; } internal static void Replace(this Paragraph paragraph, IEnumerable> replacements, StringComparison comparisonType) { if (paragraph is null || replacements is null || replacements.Count() == 0) return; var runs = paragraph.Elements().ToList(); if (runs.Count == 0) return; var structure = AnalyzeParagraphStructure(runs); if (structure.FullText.Length == 0) return; // Используем List с предопределенной емкостью var replacementsInParagraph = new List(replacements.Count() * 2); // Сначала находим все вхождения var fullText = structure.FullText; foreach (var kvp in replacements) { if (string.IsNullOrEmpty(kvp.Key)) continue; int pos = 0; while ((pos = fullText.IndexOf(kvp.Key, pos, comparisonType)) != -1) { replacementsInParagraph.Add(new ReplacementInfo { OldValue = kvp.Key, NewValue = kvp.Value ?? string.Empty, Index = pos }); pos += kvp.Key.Length; } } if (replacementsInParagraph.Count == 0) return; // Сортируем по убыванию позиции replacementsInParagraph.Sort((x, y) => y.Index.CompareTo(x.Index)); // Выполняем замены for (int i = 0; i < replacementsInParagraph.Count; i++) { var replacement = replacementsInParagraph[i]; int matchIndex = replacement.Index; int matchEnd = matchIndex + replacement.OldValue.Length; var nodesToReplace = FindNodesToReplace(structure, matchIndex, matchEnd); if (nodesToReplace.Count > 0) { ExecuteReplacement(nodesToReplace, matchIndex, matchEnd, replacement.NewValue); } } } internal static void Replace(this Paragraph paragraph, IEnumerable> replacements, StringComparison comparisonType) { if (paragraph is null || replacements is null || replacements.Count() == 0) return; var runs = paragraph.Elements().ToList(); if (runs.Count == 0) return; var structure = AnalyzeParagraphStructure(runs); if (structure.FullText.Length == 0) return; // Используем List с предопределенной емкостью var replacementsInParagraph = new List(replacements.Count() * 2); // Сначала находим все вхождения var fullText = structure.FullText; foreach (var kvp in replacements) { if (string.IsNullOrEmpty(kvp.Key)) continue; int pos = 0; while ((pos = fullText.IndexOf(kvp.Key, pos, comparisonType)) != -1) { replacementsInParagraph.Add(new ReplacementInfo { OldValue = kvp.Key, NewValue = kvp.Value.Text ?? string.Empty, BreakPage = kvp.Value.BreakPage, Index = pos }); pos += kvp.Key.Length; } } if (replacementsInParagraph.Count == 0) return; // Сортируем по убыванию позиции replacementsInParagraph.Sort((x, y) => y.Index.CompareTo(x.Index)); // Выполняем замены for (int i = 0; i < replacementsInParagraph.Count; i++) { var replacement = replacementsInParagraph[i]; int matchIndex = replacement.Index; int matchEnd = matchIndex + replacement.OldValue.Length; var nodesToReplace = FindNodesToReplace(structure, matchIndex, matchEnd); if (nodesToReplace.Count > 0) { ExecuteReplacement(nodesToReplace, matchIndex, matchEnd, replacement.NewValue, replacement.BreakPage); } } } private class ReplacementInfo { internal string OldValue { get; set; } = null!; internal string NewValue { get; set; } = null!; internal int Index { get; set; } internal bool BreakPage { get; set; } } private static ParagraphStructure AnalyzeParagraphStructure(IEnumerable runs) { var textNodesList = new List(32); var sb = new StringBuilder(256); int currentIndex = 0; foreach (var run in runs) { var texts = run.Elements(); foreach (var text in texts) { var textValue = text.Text; if (string.IsNullOrEmpty(textValue)) continue; textNodesList.Add(new TextNodeInfo( text, currentIndex, textValue.Length )); sb.Append(textValue); currentIndex += textValue.Length; } } return new ParagraphStructure(sb.ToString(), [.. textNodesList]); } private static List FindNodesToReplace( ParagraphStructure structure, int matchStart, int matchEnd) { var result = new List(4); int firstNodeIndex = structure.FindFirstNodeIndexAtPosition(matchStart); if (firstNodeIndex == -1) return result; var textNodes = structure.TextNodes; for (int i = firstNodeIndex; i < textNodes.Length; i++) { var node = textNodes[i]; if (node.StartIndex >= matchEnd) break; if (node.StartIndex + node.Length > matchStart) { result.Add(node); } } return result; } private static void ExecuteReplacement( List nodesToReplace, int matchStart, int matchEnd, string newValue, bool breakPage = false) { if (nodesToReplace.Count == 0) return; var firstNode = nodesToReplace[0]; string oldText = firstNode.Text.Text ?? string.Empty; int startInFirstNode = matchStart - firstNode.StartIndex; int charsToReplaceInFirstNode = Math.Min( oldText.Length - startInFirstNode, matchEnd - matchStart ); string processedNewValue = ReplaceSpacesWithNonBreaking(newValue); firstNode.Text.Text = ReplaceSubstringOptimized( oldText, startInFirstNode, charsToReplaceInFirstNode, processedNewValue ); // Очищаем остальные текстовые ноды for (int i = 1; i < nodesToReplace.Count; i++) { nodesToReplace[i].Text.Text = string.Empty; } if (breakPage) { if (nodesToReplace[0].Text.Parent is Run run && run.Parent is Paragraph para) { var breakRun = new Run(new Break() { Type = BreakValues.Page }); if (run.RunProperties is not null) breakRun.RunProperties = (RunProperties)run.RunProperties.CloneNode(true); para.AppendChild(breakRun); } } } private static unsafe string ReplaceSpacesWithNonBreaking(string input) { if (!input.Contains(' ')) return input; fixed (char* pInput = input) { char* resultPtr = stackalloc char[input.Length]; for (int i = 0; i < input.Length; i++) { resultPtr[i] = pInput[i] == ' ' ? '\u00A0' : pInput[i]; } return new string(resultPtr, 0, input.Length); } } private static string ReplaceSubstringOptimized(string original, int start, int length, string replacement) { if (string.IsNullOrEmpty(original)) return replacement ?? string.Empty; if (start < 0 || start >= original.Length || length <= 0) return original; if (start == 0 && length == original.Length) return replacement; int end = Math.Min(start + length, original.Length); // Оптимизированная конкатенация var sb = new StringBuilder(original.Length - length + replacement.Length); if (start > 0) { sb.Append(original, 0, start); } sb.Append(replacement); if (end < original.Length) { sb.Append(original, end, original.Length - end); } return sb.ToString(); } }