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 + + suffix в тексте. // Возвращает список в порядке возрастания позиции. // ============================================================ internal static List FindMatches( string text, string prefix, string suffix, StringComparison comparisonType) { var result = new List(); 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) // ============================================================ internal static void ReplaceStringInBody( Body body, string oldValue, Func factory, StringComparison comparisonType) { if (body is null || factory is null) return; ParsePattern(oldValue, out var prefix, out var suffix); var paragraphs = body.Elements().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 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 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 factory, StringComparison comparisonType) { var paragraphs = element.Descendants().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 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) // ============================================================ internal static void ReplaceItemInBody( Body body, string oldValue, Func factory, StringComparison comparisonType) { if (body is null || factory is null) return; ParsePattern(oldValue, out var prefix, out var suffix); var paragraphs = body.Elements().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 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 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 factory, StringComparison comparisonType) { var paragraphs = element.Descendants().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 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; } }