256 lines
10 KiB
C#
256 lines
10 KiB
C#
namespace QWERTYkez.WordProcessor;
|
|||
|
|
|
||
|
|
internal static class DynamicReplaceExt
|
||
|
|
{
|
||
|
|
// ============================================================
|
||
|
|
// Разбор шаблона вида "$key|$" на prefix="$key|" и suffix="$"
|
||
|
|
// Параметр помещается между prefix и suffix.
|
||
|
|
// ============================================================
|
||
|
|
internal static void ParsePattern(string oldValue, out string prefix, out string suffix)
|
||
|
|
{
|
||
|
|
if (string.IsNullOrEmpty(oldValue))
|
||
|
|
throw new ArgumentException("Old value cannot be null or empty", nameof(oldValue));
|
||
|
|
|
||
|
|
int lastPipe = oldValue.LastIndexOf('|');
|
||
|
|
if (lastPipe < 0)
|
||
|
|
throw new ArgumentException("Dynamic pattern must contain '|' separator", nameof(oldValue));
|
||
|
|
|
||
|
|
prefix = oldValue.Substring(0, lastPipe + 1);
|
||
|
|
suffix = oldValue.Substring(lastPipe + 1);
|
||
|
|
}
|
||
|
|
|
||
|
|
internal readonly struct MatchInfo(int start, int end, string param)
|
||
|
|
{
|
||
|
|
public readonly int Start = start;
|
||
|
|
public readonly int End = end;
|
||
|
|
public readonly string Param = param;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// Поиск всех вхождений prefix + <param> + suffix в тексте.
|
||
|
|
// Возвращает список в порядке возрастания позиции.
|
||
|
|
// ============================================================
|
||
|
|
internal static List<MatchInfo> FindMatches(
|
||
|
|
string text,
|
||
|
|
string prefix,
|
||
|
|
string suffix,
|
||
|
|
StringComparison comparisonType)
|
||
|
|
{
|
||
|
|
var result = new List<MatchInfo>();
|
||
|
|
if (string.IsNullOrEmpty(text) || string.IsNullOrEmpty(prefix))
|
||
|
|
return result;
|
||
|
|
|
||
|
|
int searchPos = 0;
|
||
|
|
while (searchPos < text.Length)
|
||
|
|
{
|
||
|
|
int pStart = text.IndexOf(prefix, searchPos, comparisonType);
|
||
|
|
if (pStart < 0) break;
|
||
|
|
|
||
|
|
int paramStart = pStart + prefix.Length;
|
||
|
|
int pEnd;
|
||
|
|
if (string.IsNullOrEmpty(suffix))
|
||
|
|
{
|
||
|
|
pEnd = text.Length;
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
pEnd = text.IndexOf(suffix, paramStart, comparisonType);
|
||
|
|
if (pEnd < 0) break;
|
||
|
|
}
|
||
|
|
|
||
|
|
string param = text.Substring(paramStart, pEnd - paramStart);
|
||
|
|
int matchEnd = pEnd + suffix.Length;
|
||
|
|
result.Add(new MatchInfo(pStart, matchEnd, param));
|
||
|
|
|
||
|
|
// следующий поиск — после окончания текущего совпадения
|
||
|
|
searchPos = matchEnd;
|
||
|
|
if (searchPos <= pStart) searchPos = pStart + 1; // защита от зацикливания
|
||
|
|
}
|
||
|
|
return result;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// REPLACE STRING (Func<string,string>)
|
||
|
|
// ============================================================
|
||
|
|
|
||
|
|
internal static void ReplaceStringInBody(
|
||
|
|
Body body, string oldValue, Func<string, string> factory, StringComparison comparisonType)
|
||
|
|
{
|
||
|
|
if (body is null || factory is null) return;
|
||
|
|
ParsePattern(oldValue, out var prefix, out var suffix);
|
||
|
|
|
||
|
|
var paragraphs = body.Elements<Paragraph>().ToList();
|
||
|
|
for (int i = paragraphs.Count - 1; i >= 0; i--)
|
||
|
|
{
|
||
|
|
paragraphs[i].SimpleReplaceDynamic(prefix, suffix, factory, comparisonType);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
internal static void ReplaceStringInTables(
|
||
|
|
OpenXmlElement element, string oldValue, Func<string, string> factory, StringComparison comparisonType)
|
||
|
|
{
|
||
|
|
if (element is null || factory is null) return;
|
||
|
|
ParsePattern(oldValue, out var prefix, out var suffix);
|
||
|
|
|
||
|
|
foreach (var table in TableTextProcessor.GetAllTables(element))
|
||
|
|
{
|
||
|
|
var paragraphs = TableTextProcessor.GetTableParagraphs(table).ToList();
|
||
|
|
for (int i = paragraphs.Count - 1; i >= 0; i--)
|
||
|
|
{
|
||
|
|
paragraphs[i].SimpleReplaceDynamic(prefix, suffix, factory, comparisonType);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
internal static void ReplaceStringInHeadersFooters(
|
||
|
|
WordprocessingDocument doc, string oldValue, Func<string, string> factory, StringComparison comparisonType)
|
||
|
|
{
|
||
|
|
if (doc is null || factory is null) return;
|
||
|
|
ParsePattern(oldValue, out var prefix, out var suffix);
|
||
|
|
|
||
|
|
foreach (var header in HeaderFooterProcessor.GetAllHeaders(doc))
|
||
|
|
ReplaceStringInElement(header, prefix, suffix, factory, comparisonType);
|
||
|
|
|
||
|
|
foreach (var footer in HeaderFooterProcessor.GetAllFooters(doc))
|
||
|
|
ReplaceStringInElement(footer, prefix, suffix, factory, comparisonType);
|
||
|
|
}
|
||
|
|
|
||
|
|
private static void ReplaceStringInElement(
|
||
|
|
OpenXmlElement element, string prefix, string suffix,
|
||
|
|
Func<string, string> factory, StringComparison comparisonType)
|
||
|
|
{
|
||
|
|
var paragraphs = element.Descendants<Paragraph>().ToList();
|
||
|
|
var tables = TableTextProcessor.GetAllTables(element).ToList();
|
||
|
|
var tableParagraphs = tables.SelectMany(t => TableTextProcessor.GetTableParagraphs(t)).ToList();
|
||
|
|
|
||
|
|
var allParagraphs = paragraphs.Concat(tableParagraphs).Distinct().ToList();
|
||
|
|
for (int i = allParagraphs.Count - 1; i >= 0; i--)
|
||
|
|
{
|
||
|
|
allParagraphs[i].SimpleReplaceDynamic(prefix, suffix, factory, comparisonType);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
internal static bool SimpleReplaceDynamic(
|
||
|
|
this Paragraph? paragraph,
|
||
|
|
string prefix,
|
||
|
|
string suffix,
|
||
|
|
Func<string, string> factory,
|
||
|
|
StringComparison comparisonType)
|
||
|
|
{
|
||
|
|
if (paragraph is null || factory is null) return false;
|
||
|
|
|
||
|
|
var text = paragraph.InnerText;
|
||
|
|
if (string.IsNullOrEmpty(text)) return false;
|
||
|
|
|
||
|
|
var matches = FindMatches(text, prefix, suffix, comparisonType);
|
||
|
|
if (matches.Count == 0) return false;
|
||
|
|
|
||
|
|
// Обходим справа налево: правые замены не сдвигают координаты левых.
|
||
|
|
for (int i = matches.Count - 1; i >= 0; i--)
|
||
|
|
{
|
||
|
|
var m = matches[i];
|
||
|
|
string matchedText = text.Substring(m.Start, m.End - m.Start);
|
||
|
|
string replacement = factory(m.Param) ?? string.Empty;
|
||
|
|
|
||
|
|
paragraph.SimpleReplace(matchedText, replacement, comparisonType);
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// REPLACE ITEM (Func<string,ReplaceItem>)
|
||
|
|
// ============================================================
|
||
|
|
|
||
|
|
internal static void ReplaceItemInBody(
|
||
|
|
Body body, string oldValue, Func<string, ReplaceItem> factory, StringComparison comparisonType)
|
||
|
|
{
|
||
|
|
if (body is null || factory is null) return;
|
||
|
|
ParsePattern(oldValue, out var prefix, out var suffix);
|
||
|
|
|
||
|
|
var paragraphs = body.Elements<Paragraph>().ToList();
|
||
|
|
for (int i = paragraphs.Count - 1; i >= 0; i--)
|
||
|
|
{
|
||
|
|
paragraphs[i].SimpleReplaceItemDynamic(prefix, suffix, factory, comparisonType);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
internal static void ReplaceItemInTables(
|
||
|
|
OpenXmlElement element, string oldValue, Func<string, ReplaceItem> factory, StringComparison comparisonType)
|
||
|
|
{
|
||
|
|
if (element is null || factory is null) return;
|
||
|
|
ParsePattern(oldValue, out var prefix, out var suffix);
|
||
|
|
|
||
|
|
foreach (var table in TableTextProcessor.GetAllTables(element))
|
||
|
|
{
|
||
|
|
var paragraphs = TableTextProcessor.GetTableParagraphs(table).ToList();
|
||
|
|
for (int i = paragraphs.Count - 1; i >= 0; i--)
|
||
|
|
{
|
||
|
|
paragraphs[i].SimpleReplaceItemDynamic(prefix, suffix, factory, comparisonType);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
internal static void ReplaceItemInHeadersFooters(
|
||
|
|
WordprocessingDocument doc, string oldValue, Func<string, ReplaceItem> factory, StringComparison comparisonType)
|
||
|
|
{
|
||
|
|
if (doc is null || factory is null) return;
|
||
|
|
ParsePattern(oldValue, out var prefix, out var suffix);
|
||
|
|
|
||
|
|
foreach (var header in HeaderFooterProcessor.GetAllHeaders(doc))
|
||
|
|
ReplaceItemInElement(header, prefix, suffix, factory, comparisonType);
|
||
|
|
|
||
|
|
foreach (var footer in HeaderFooterProcessor.GetAllFooters(doc))
|
||
|
|
ReplaceItemInElement(footer, prefix, suffix, factory, comparisonType);
|
||
|
|
}
|
||
|
|
|
||
|
|
private static void ReplaceItemInElement(
|
||
|
|
OpenXmlElement element, string prefix, string suffix,
|
||
|
|
Func<string, ReplaceItem> factory, StringComparison comparisonType)
|
||
|
|
{
|
||
|
|
var paragraphs = element.Descendants<Paragraph>().ToList();
|
||
|
|
var tables = TableTextProcessor.GetAllTables(element).ToList();
|
||
|
|
var tableParagraphs = tables.SelectMany(t => TableTextProcessor.GetTableParagraphs(t)).ToList();
|
||
|
|
|
||
|
|
var allParagraphs = paragraphs.Concat(tableParagraphs).Distinct().ToList();
|
||
|
|
for (int i = allParagraphs.Count - 1; i >= 0; i--)
|
||
|
|
{
|
||
|
|
allParagraphs[i].SimpleReplaceItemDynamic(prefix, suffix, factory, comparisonType);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
internal static bool SimpleReplaceItemDynamic(
|
||
|
|
this Paragraph? paragraph,
|
||
|
|
string prefix,
|
||
|
|
string suffix,
|
||
|
|
Func<string, ReplaceItem> factory,
|
||
|
|
StringComparison comparisonType)
|
||
|
|
{
|
||
|
|
if (paragraph is null || factory is null) return false;
|
||
|
|
|
||
|
|
var text = paragraph.InnerText;
|
||
|
|
if (string.IsNullOrEmpty(text)) return false;
|
||
|
|
|
||
|
|
var matches = FindMatches(text, prefix, suffix, comparisonType);
|
||
|
|
if (matches.Count == 0) return false;
|
||
|
|
|
||
|
|
for (int i = matches.Count - 1; i >= 0; i--)
|
||
|
|
{
|
||
|
|
var m = matches[i];
|
||
|
|
string matchedText = text.Substring(m.Start, m.End - m.Start);
|
||
|
|
|
||
|
|
ReplaceItem item = factory(m.Param);
|
||
|
|
if (item is null) continue;
|
||
|
|
|
||
|
|
if (item.BreakValue.HasValue)
|
||
|
|
{
|
||
|
|
// Поддерживаются PageBreak / NewLandscapeSection / NewPortraitSection
|
||
|
|
paragraph.SimpleReplace(matchedText, string.Empty, comparisonType, item.BreakValue.Value);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
paragraph.SimpleReplace(matchedText, item.Text ?? string.Empty, comparisonType);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|