Files
QWERTYkez.OpenXmlProcessors/QWERTYkez.WordProcessor/SimplyReplace.cs
T

408 lines
13 KiB
C#
Raw Normal View History

2026-06-05 15:58:03 +07:00
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;
}
2026-07-21 16:43:59 +07:00
private sealed class ParagraphStructure(string fullText, TextNodeInfo[] textNodes)
2026-06-05 15:58:03 +07:00
{
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);
}
}
2026-07-21 16:43:59 +07:00
internal static bool SimpleReplace(this Paragraph? paragraph, string oldValue, string newValue, StringComparison comparisonType, PageBreakType splitValue = PageBreakType.None)
2026-06-05 15:58:03 +07:00
{
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;
2026-07-21 16:43:59 +07:00
ExecuteReplacement(nodesToReplace, matchIndex, matchEnd, newValue, splitValue);
2026-06-05 15:58:03 +07:00
return true;
}
2026-07-21 16:43:59 +07:00
internal static bool SimpleReplace(this Paragraph? paragraph, string oldValue, string newValue, StringComparison comparisonType, bool breakPage)
=> SimpleReplace(paragraph, oldValue, newValue, comparisonType, breakPage ? PageBreakType.PageBreak : PageBreakType.None);
2026-06-05 15:58:03 +07:00
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;
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,
2026-07-21 16:43:59 +07:00
Index = pos,
SplitValue = PageBreakType.None
2026-06-05 15:58:03 +07:00
});
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)
{
2026-07-21 16:43:59 +07:00
ExecuteReplacement(nodesToReplace, matchIndex, matchEnd, replacement.NewValue, replacement.SplitValue);
2026-06-05 15:58:03 +07:00
}
}
}
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;
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,
2026-07-21 16:43:59 +07:00
Index = pos,
SplitValue = kvp.Value.SplitValue
2026-06-05 15:58:03 +07:00
});
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)
{
2026-07-21 16:43:59 +07:00
ExecuteReplacement(nodesToReplace, matchIndex, matchEnd, replacement.NewValue, replacement.SplitValue);
2026-06-05 15:58:03 +07:00
}
}
}
private class ReplacementInfo
{
internal string OldValue { get; set; } = null!;
internal string NewValue { get; set; } = null!;
internal int Index { get; set; }
2026-07-21 16:43:59 +07:00
internal PageBreakType SplitValue { get; set; } = PageBreakType.None;
2026-06-05 15:58:03 +07:00
}
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(
2026-07-21 16:43:59 +07:00
List<TextNodeInfo> nodesToReplace,
int matchStart,
int matchEnd,
string newValue,
PageBreakType splitValue)
2026-06-05 15:58:03 +07:00
{
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;
}
2026-07-21 16:43:59 +07:00
if (splitValue == PageBreakType.PageBreak)
2026-06-05 15:58:03 +07:00
{
if (nodesToReplace[0].Text.Parent is Run run && run.Parent is Paragraph para)
{
2026-07-21 16:43:59 +07:00
var breakRun = new Run(new Break { Type = BreakValues.Page });
2026-06-05 15:58:03 +07:00
if (run.RunProperties is not null)
breakRun.RunProperties = (RunProperties)run.RunProperties.CloneNode(true);
para.AppendChild(breakRun);
}
}
2026-07-21 16:43:59 +07:00
else if (splitValue == PageBreakType.NewLandscapeSection || splitValue == PageBreakType.NewPortraitSection)
{
var firstText = nodesToReplace[0].Text;
if (firstText.Parent is Run run && run.Parent is Paragraph para)
{
AddSectionProperties(para, splitValue);
}
}
}
private static void AddSectionProperties(Paragraph para, PageBreakType splitValue)
{
if (para is null) return;
PageOrientationValues orientation = splitValue == PageBreakType.NewLandscapeSection
? PageOrientationValues.Landscape
: PageOrientationValues.Portrait;
uint width, height;
if (orientation == PageOrientationValues.Landscape)
{
width = 16838;
height = 11906;
}
else
{
width = 11906;
height = 16838;
}
var sectionProps = new SectionProperties(
new PageSize
{
Width = width,
Height = height,
Orient = orientation
}
);
para.ParagraphProperties ??= new ParagraphProperties();
para.ParagraphProperties.AppendChild(sectionProps);
2026-06-05 15:58:03 +07:00
}
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();
}
}