175 lines
5.6 KiB
C#
175 lines
5.6 KiB
C#
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);
|
||
}
|
||
} |