Files
QWERTYkez.OpenXmlProcessors/QWERTYkez.WordProcessor/PlaceholderFinder.cs

175 lines
5.6 KiB
C#
Raw Normal View History

namespace QWERTYkez.WordProcessor;
internal static class PlaceholderFinder
{
public static ISet<string> FindInDocument(WordprocessingDocument doc, Body body)
{
ISet<string> result = new NormalizedSet();
// 1. Основной текст
FindInParagraphs(body.Elements<Paragraph>(), result);
// 2. Таблицы в основном тексте
foreach (var table in body.Descendants<Table>())
{
FindInParagraphs(table.Descendants<Paragraph>(), result);
}
// 3. Колонтитулы
FindInHeadersAndFooters(doc, result);
return result;
}
private static void FindInParagraphs(IEnumerable<Paragraph> paragraphs, ISet<string> result)
{
// Локальная коллекция для каждого вызова - оптимизация для уменьшения аллокаций
List<string>? 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<string>? 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<string> result)
{
if (doc.MainDocumentPart is null) return;
// Верхние колонтитулы
foreach (var headerPart in doc.MainDocumentPart.HeaderParts)
{
if (headerPart?.Header is not null)
{
FindInParagraphs(headerPart.Header.Descendants<Paragraph>(), result);
}
}
// Нижние колонтитулы
foreach (var footerPart in doc.MainDocumentPart.FooterParts)
{
if (footerPart?.Footer is not null)
{
FindInParagraphs(footerPart.Footer.Descendants<Paragraph>(), result);
}
}
}
private static string GetParagraphText(Paragraph paragraph)
{
if (paragraph is null) return string.Empty;
// Используем Span для минимальных аллокаций
var texts = paragraph.Descendants<Text>();
// Быстрая проверка: если всего один WordText элемент
if (texts is ICollection<Text> 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);
}
}