Files
QWERTYkez.OpenXmlProcessors/QWERTYkez.WordProcessor/PlaceholderFinder.cs
2026-06-05 15:58:03 +07:00

175 lines
5.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
}