namespace QWERTYkez.WordProcessor; internal static class PlaceholderFinder { public static ISet FindInDocument(WordprocessingDocument doc, Body body) { ISet result = new NormalizedSet(); // 1. Основной текст FindInParagraphs(body.Elements(), result); // 2. Таблицы в основном тексте foreach (var table in body.Descendants()) { FindInParagraphs(table.Descendants(), result); } // 3. Колонтитулы FindInHeadersAndFooters(doc, result); return result; } private static void FindInParagraphs(IEnumerable paragraphs, ISet result) { // Локальная коллекция для каждого вызова - оптимизация для уменьшения аллокаций List? tempList = null; foreach (var paragraph in paragraphs) { var text = GetParagraphText(paragraph); if (string.IsNullOrEmpty(text)) continue; // Откладываем создание списка до первого найденного плейсхолдера bool hasPlaceholders = FindPlaceholdersInText(text, ref tempList); if (hasPlaceholders && tempList is not null) { // Добавляем найденные плейсхолдеры с учетом регистра foreach (var placeholder in tempList) { result.Add(placeholder); } tempList.Clear(); } } } private static unsafe bool FindPlaceholdersInText(string text, ref List? output) { fixed (char* pText = text) { char* start = pText; char* end = pText + text.Length; bool foundAny = false; while (start < end) { // Ищем начало плейсхолдера char* dollarStart = null; while (start < end) { if (*start == '$') { dollarStart = start; start++; break; } start++; } if (dollarStart is null || start >= end) break; // Ищем конец плейсхолдера char* dollarEnd = null; char* contentStart = start; // Начало содержимого (после первого $) while (start < end) { if (*start == '$') { dollarEnd = start; start++; break; } start++; } if (dollarEnd is null) break; // Извлекаем содержимое между долларами int contentLength = (int)(dollarEnd - contentStart); if (contentLength > 0) // Игнорируем пустые "$$" { foundAny = true; output ??= []; string content = new(contentStart, 0, contentLength); output.Add(content); } } return foundAny; } } private static void FindInHeadersAndFooters(WordprocessingDocument doc, ISet result) { if (doc.MainDocumentPart is null) return; // Верхние колонтитулы foreach (var headerPart in doc.MainDocumentPart.HeaderParts) { if (headerPart?.Header is not null) { FindInParagraphs(headerPart.Header.Descendants(), result); } } // Нижние колонтитулы foreach (var footerPart in doc.MainDocumentPart.FooterParts) { if (footerPart?.Footer is not null) { FindInParagraphs(footerPart.Footer.Descendants(), result); } } } private static string GetParagraphText(Paragraph paragraph) { if (paragraph is null) return string.Empty; // Используем Span для минимальных аллокаций var texts = paragraph.Descendants(); // Быстрая проверка: если всего один WordText элемент if (texts is ICollection collection && collection.Count == 1) { foreach (var text in collection) { return text.Text ?? string.Empty; } } // Для нескольких WordText элементов int totalLength = 0; // Первый проход: подсчет общей длины foreach (var text in texts) { if (text.Text is not null) { totalLength += text.Text.Length; } } if (totalLength == 0) return string.Empty; // Второй проход: копирование var chars = new char[totalLength]; int position = 0; foreach (var text in texts) { if (text.Text is not null) { text.Text.CopyTo(0, chars, position, text.Text.Length); position += text.Text.Length; } } return new string(chars); } }