namespace QWERTYkez.ExcelProcessor; internal static class PlaceholderFinder { /// Находит плейсхолдеры $...$ в указанных листах. public static ISet FindInDocument(SpreadsheetDocument doc, params WorksheetPart[] worksheets) { var result = new NormalizedSet(); if (doc.WorkbookPart is null) return result; var sharedStringTable = doc.WorkbookPart.SharedStringTablePart?.SharedStringTable; foreach (var ws in worksheets) ProcessWorksheet(ws, sharedStringTable, result); return result; } /// Находит плейсхолдеры во всех листах документа. public static ISet FindInDocument(SpreadsheetDocument doc) { if (doc.WorkbookPart is null) return new NormalizedSet(); return FindInDocument(doc, [.. doc.WorkbookPart.WorksheetParts]); } private static void ProcessWorksheet(WorksheetPart wsPart, SharedStringTable? sharedStrings, ISet result) { var worksheet = wsPart.Worksheet; if (worksheet is null) return; ProcessCells(worksheet, sharedStrings, result); ProcessHeaderFooter(worksheet, result); } private static void ProcessCells(Worksheet worksheet, SharedStringTable? sharedStrings, ISet result) { var sheetData = worksheet.GetFirstChild(); if (sheetData is null) return; List? tempList = null; // буфер для плейсхолдеров из одного текста foreach (var row in sheetData.Elements()) { foreach (var cell in row.Elements()) { string text = GetCellValue(cell, sharedStrings); if (string.IsNullOrEmpty(text)) continue; #if DEBUG if (text.Contains('$')) Debug.WriteLine($"[PlaceholderFinder] Cell {cell.CellReference}: '{text}'"); #endif if (FindPlaceholdersInText(text, ref tempList)) { foreach (var ph in tempList!) result.Add(ph); tempList.Clear(); } } } } private static void ProcessHeaderFooter(Worksheet worksheet, ISet result) { var hf = worksheet.Descendants().FirstOrDefault(); if (hf is null) return; var texts = new[] { hf.OddHeader?.Text, hf.OddFooter?.Text, hf.EvenHeader?.Text, hf.EvenFooter?.Text, hf.FirstHeader?.Text, hf.FirstFooter?.Text }; List? tempList = null; foreach (var text in texts) { if (string.IsNullOrEmpty(text)) continue; if (FindPlaceholdersInText(text!, ref tempList)) { foreach (var ph in tempList!) result.Add(ph); tempList.Clear(); } } } private static string GetCellValue(Cell cell, SharedStringTable? sharedStrings) { if (cell?.CellValue is null && cell?.InlineString is null) return string.Empty; // Встроенная строка if (cell.InlineString is not null) return string.Concat(cell.InlineString.Descendants().Select(t => t.Text)); // Обычное значение или общая строка string raw = cell.CellValue!.InnerText; if (cell.DataType?.Value == CellValues.SharedString) { if (sharedStrings is not null && int.TryParse(raw, out int idx) && idx >= 0 && idx < sharedStrings.Count!) { var si = sharedStrings.ElementAt(idx); return string.Concat(si.Descendants().Select(t => t.Text)); } return string.Empty; } return raw; } private static unsafe bool FindPlaceholdersInText(string text, ref List? output) { fixed (char* pText = text) { char* start = pText; char* end = pText + text.Length; bool found = false; while (start < end) { // Ищем '$' while (start < end && *start != '$') start++; if (start >= end) break; char* open = start++; if (start >= end) break; // Ищем закрывающий '$' char* contentStart = start; while (start < end && *start != '$') start++; if (start >= end) break; char* close = start++; int len = (int)(close - contentStart); if (len > 0) { found = true; output ??= []; output.Add(new string(contentStart, 0, len)); } } return found; } } }