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);
|
|||
|
|
}
|
|||
|
|
}
|