Files
2026-06-05 15:58:03 +07:00

374 lines
12 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<Paragraph>();
foreach (var paragraph in paragraphs)
{
paragraph?.SimpleReplace(oldValue, newValue, comparisonType);
}
}
internal static void Replace(this Body body, IEnumerable<KeyValuePair<string, string>> replacements, StringComparison comparisonType)
{
if (body is null || replacements is null || replacements.Count() == 0)
return;
var paragraphs = body.Elements<Paragraph>();
foreach (var paragraph in paragraphs)
{
paragraph?.Replace(replacements, comparisonType);
}
}
internal static void Replace(this Body body, IEnumerable<KeyValuePair<string, ReplaceItem>> replacements, StringComparison comparisonType)
{
if (body is null || replacements is null || replacements.Count() == 0)
return;
var paragraphs = body.Elements<Paragraph>();
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<Run>();
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<KeyValuePair<string, string>> replacements, StringComparison comparisonType)
{
if (paragraph is null || replacements is null || replacements.Count() == 0)
return;
var runs = paragraph.Elements<Run>().ToList();
if (runs.Count == 0)
return;
var structure = AnalyzeParagraphStructure(runs);
if (structure.FullText.Length == 0)
return;
// Используем List с предопределенной емкостью
var replacementsInParagraph = new List<ReplacementInfo>(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<KeyValuePair<string, ReplaceItem>> replacements, StringComparison comparisonType)
{
if (paragraph is null || replacements is null || replacements.Count() == 0)
return;
var runs = paragraph.Elements<Run>().ToList();
if (runs.Count == 0)
return;
var structure = AnalyzeParagraphStructure(runs);
if (structure.FullText.Length == 0)
return;
// Используем List с предопределенной емкостью
var replacementsInParagraph = new List<ReplacementInfo>(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<Run> runs)
{
var textNodesList = new List<TextNodeInfo>(32);
var sb = new StringBuilder(256);
int currentIndex = 0;
foreach (var run in runs)
{
var texts = run.Elements<Text>();
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<TextNodeInfo> FindNodesToReplace(
ParagraphStructure structure,
int matchStart,
int matchEnd)
{
var result = new List<TextNodeInfo>(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<TextNodeInfo> 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();
}
}