374 lines
12 KiB
C#
374 lines
12 KiB
C#
|
|
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();
|
|||
|
|
}
|
|||
|
|
}
|