217 lines
10 KiB
C#
217 lines
10 KiB
C#
|
|
namespace QWERTYkez.ExcelProcessor;
|
|||
|
|
|
|||
|
|
internal static class ReplaceStringExtensions
|
|||
|
|
{
|
|||
|
|
// --- Публичный метод для одиночной замены ---
|
|||
|
|
internal static void Replace(this SpreadsheetDocument doc, string oldValue, string newValue, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase)
|
|||
|
|
{
|
|||
|
|
if (string.IsNullOrEmpty(oldValue)) return;
|
|||
|
|
WorksheetPart[] worksheets = [.. doc.WorkbookPart?.WorksheetParts!];
|
|||
|
|
if (worksheets.Length < 1) return;
|
|||
|
|
|
|||
|
|
var replacement = new Dictionary<string, string> { [oldValue] = newValue };
|
|||
|
|
ReplaceCore(doc, worksheets, replacement, comparisonType);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- Публичный метод для множественной замены ---
|
|||
|
|
internal static void Replace(this SpreadsheetDocument doc, IEnumerable<KeyValuePair<string, string>> replacements, StringComparison comparisonType = StringComparison.OrdinalIgnoreCase)
|
|||
|
|
{
|
|||
|
|
if (replacements is null) return;
|
|||
|
|
WorksheetPart[] worksheets = [.. doc.WorkbookPart?.WorksheetParts!];
|
|||
|
|
if (worksheets.Length < 1) return;
|
|||
|
|
|
|||
|
|
var comparer = GetComparerForStringComparison(comparisonType);
|
|||
|
|
var replacementDict = new Dictionary<string, string>(comparer);
|
|||
|
|
foreach (var kvp in replacements)
|
|||
|
|
if (!string.IsNullOrEmpty(kvp.Key))
|
|||
|
|
replacementDict[kvp.Key] = kvp.Value;
|
|||
|
|
if (replacementDict.Count == 0) return;
|
|||
|
|
ReplaceCore(doc, worksheets, replacementDict, comparisonType);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- Общий приватный метод, содержащий всю логику замены ---
|
|||
|
|
private static void ReplaceCore(SpreadsheetDocument doc, WorksheetPart[] worksheets, Dictionary<string, string> replacementDict, StringComparison comparisonType)
|
|||
|
|
{
|
|||
|
|
var workbookPart = doc.WorkbookPart!;
|
|||
|
|
|
|||
|
|
// 1. Собрать все изменения для ячеек
|
|||
|
|
var cellChanges = new Dictionary<Cell, string>();
|
|||
|
|
var allSharedStrings = new List<string>();
|
|||
|
|
var sharedStringTable = workbookPart.SharedStringTablePart?.SharedStringTable;
|
|||
|
|
if (sharedStringTable is not null)
|
|||
|
|
{
|
|||
|
|
foreach (var item in sharedStringTable.Elements<SharedStringItem>())
|
|||
|
|
allSharedStrings.Add(string.Concat(item.Descendants<Text>().Select(t => t.Text)));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
foreach (var worksheetPart in worksheets)
|
|||
|
|
CollectCellChanges(worksheetPart, replacementDict, comparisonType, cellChanges, allSharedStrings);
|
|||
|
|
|
|||
|
|
// 2. Применить изменения к ячейкам
|
|||
|
|
ApplyCellChanges(cellChanges, allSharedStrings);
|
|||
|
|
|
|||
|
|
// 3. Обновить SharedStringTable
|
|||
|
|
UpdateSharedStringTable(workbookPart, allSharedStrings);
|
|||
|
|
|
|||
|
|
// 4. Обработать колонтитулы
|
|||
|
|
foreach (var worksheetPart in worksheets)
|
|||
|
|
ReplaceInHeadersFooters(worksheetPart, replacementDict, comparisonType);
|
|||
|
|
|
|||
|
|
// 5. Обработать комментарии
|
|||
|
|
foreach (var worksheetPart in worksheets)
|
|||
|
|
ReplaceInComments(worksheetPart, replacementDict, comparisonType);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- Остальные вспомогательные методы (без изменений) ---
|
|||
|
|
private static IEqualityComparer<string> GetComparerForStringComparison(StringComparison comparisonType) =>
|
|||
|
|
comparisonType switch
|
|||
|
|
{
|
|||
|
|
StringComparison.Ordinal => StringComparer.Ordinal,
|
|||
|
|
StringComparison.OrdinalIgnoreCase => StringComparer.OrdinalIgnoreCase,
|
|||
|
|
StringComparison.InvariantCulture => StringComparer.InvariantCulture,
|
|||
|
|
StringComparison.InvariantCultureIgnoreCase => StringComparer.InvariantCultureIgnoreCase,
|
|||
|
|
StringComparison.CurrentCulture => StringComparer.CurrentCulture,
|
|||
|
|
StringComparison.CurrentCultureIgnoreCase => StringComparer.CurrentCultureIgnoreCase,
|
|||
|
|
_ => StringComparer.OrdinalIgnoreCase,
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
private static void CollectCellChanges(WorksheetPart worksheetPart, Dictionary<string, string> replacementDict, StringComparison comparisonType, Dictionary<Cell, string> cellChanges, List<string> allSharedStrings)
|
|||
|
|
{
|
|||
|
|
var worksheet = worksheetPart.Worksheet;
|
|||
|
|
if (worksheet is null) return;
|
|||
|
|
var sheetData = worksheet.GetFirstChild<SheetData>();
|
|||
|
|
if (sheetData is null) return;
|
|||
|
|
|
|||
|
|
foreach (var row in sheetData.Elements<Row>())
|
|||
|
|
foreach (var cell in row.Elements<Cell>())
|
|||
|
|
{
|
|||
|
|
string originalText = GetCellText(cell, allSharedStrings);
|
|||
|
|
if (string.IsNullOrEmpty(originalText)) continue;
|
|||
|
|
string newText = ProcessReplacements(originalText, replacementDict, comparisonType);
|
|||
|
|
if (newText != originalText)
|
|||
|
|
cellChanges[cell] = newText;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static void ApplyCellChanges(Dictionary<Cell, string> cellChanges, List<string> allSharedStrings)
|
|||
|
|
{
|
|||
|
|
foreach (var kvp in cellChanges)
|
|||
|
|
{
|
|||
|
|
var cell = kvp.Key;
|
|||
|
|
var newText = kvp.Value;
|
|||
|
|
if (cell.InlineString is not null)
|
|||
|
|
{
|
|||
|
|
var texts = cell.InlineString.Descendants<Text>().ToList();
|
|||
|
|
foreach (var t in texts) t.Remove();
|
|||
|
|
cell.InlineString.AppendChild(new Text(newText));
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
int newIndex = AddOrFindStringIndex(allSharedStrings, newText);
|
|||
|
|
cell.DataType = new EnumValue<CellValues>(CellValues.SharedString);
|
|||
|
|
cell.CellValue = new CellValue(newIndex.ToString());
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static void UpdateSharedStringTable(WorkbookPart workbookPart, List<string> allSharedStrings)
|
|||
|
|
{
|
|||
|
|
var ssPart = workbookPart.SharedStringTablePart;
|
|||
|
|
ssPart ??= workbookPart.AddNewPart<SharedStringTablePart>();
|
|||
|
|
var sharedStringTable = ssPart.SharedStringTable ?? new SharedStringTable();
|
|||
|
|
sharedStringTable.RemoveAllChildren<SharedStringItem>();
|
|||
|
|
foreach (var str in allSharedStrings)
|
|||
|
|
sharedStringTable.AppendChild(new SharedStringItem(new Text(str)));
|
|||
|
|
sharedStringTable.Save();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static string ProcessReplacements(string input, Dictionary<string, string> replacementDict, StringComparison comparisonType)
|
|||
|
|
{
|
|||
|
|
if (string.IsNullOrEmpty(input) || replacementDict.Count == 0) return input;
|
|||
|
|
string result = input;
|
|||
|
|
foreach (string key in replacementDict.Keys.OrderByDescending(k => k.Length))
|
|||
|
|
{
|
|||
|
|
string value = replacementDict[key];
|
|||
|
|
result = ReplaceInString(result, key, value, comparisonType);
|
|||
|
|
}
|
|||
|
|
return result;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static string ReplaceInString(string original, string oldValue, string newValue, StringComparison comparisonType)
|
|||
|
|
{
|
|||
|
|
int idx = original.IndexOf(oldValue, comparisonType);
|
|||
|
|
if (idx < 0) return original;
|
|||
|
|
var sb = new StringBuilder(original.Length + newValue.Length - oldValue.Length);
|
|||
|
|
int last = 0;
|
|||
|
|
while (idx >= 0)
|
|||
|
|
{
|
|||
|
|
sb.Append(original, last, idx - last);
|
|||
|
|
sb.Append(newValue);
|
|||
|
|
last = idx + oldValue.Length;
|
|||
|
|
idx = original.IndexOf(oldValue, last, comparisonType);
|
|||
|
|
}
|
|||
|
|
sb.Append(original, last, original.Length - last);
|
|||
|
|
return sb.ToString();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static string GetCellText(Cell cell, List<string> allSharedStrings)
|
|||
|
|
{
|
|||
|
|
if (cell?.CellValue is null) return string.Empty;
|
|||
|
|
if (cell.InlineString is not null)
|
|||
|
|
return string.Concat(cell.InlineString.Descendants<Text>().Select(t => t.Text));
|
|||
|
|
string value = cell.CellValue.InnerText;
|
|||
|
|
if (cell.DataType?.Value == CellValues.SharedString)
|
|||
|
|
{
|
|||
|
|
if (int.TryParse(value, out int idx) && idx >= 0 && idx < allSharedStrings.Count)
|
|||
|
|
return allSharedStrings[idx];
|
|||
|
|
return string.Empty;
|
|||
|
|
}
|
|||
|
|
return value;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static int AddOrFindStringIndex(List<string> allSharedStrings, string text)
|
|||
|
|
{
|
|||
|
|
int idx = allSharedStrings.IndexOf(text);
|
|||
|
|
if (idx >= 0) return idx;
|
|||
|
|
allSharedStrings.Add(text);
|
|||
|
|
return allSharedStrings.Count - 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- Колонтитулы (обобщённый метод) ---
|
|||
|
|
private static void ReplaceInHeadersFooters(WorksheetPart worksheetPart, Dictionary<string, string> replacementDict, StringComparison comparisonType)
|
|||
|
|
{
|
|||
|
|
var worksheet = worksheetPart.Worksheet;
|
|||
|
|
if (worksheet is null) return;
|
|||
|
|
var headerFooter = worksheet.Descendants<HeaderFooter>().FirstOrDefault();
|
|||
|
|
if (headerFooter is null) return;
|
|||
|
|
|
|||
|
|
foreach (var elem in new OpenXmlLeafTextElement?[] { headerFooter.OddHeader, headerFooter.OddFooter, headerFooter.EvenHeader, headerFooter.EvenFooter, headerFooter.FirstHeader, headerFooter.FirstFooter })
|
|||
|
|
ReplaceHeaderFooter(elem, replacementDict, comparisonType);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static void ReplaceHeaderFooter(OpenXmlLeafTextElement? element, Dictionary<string, string> replacementDict, StringComparison comparisonType)
|
|||
|
|
{
|
|||
|
|
if (element?.Text is null) return;
|
|||
|
|
string original = element.Text;
|
|||
|
|
string processed = ProcessReplacements(original, replacementDict, comparisonType);
|
|||
|
|
if (processed != original)
|
|||
|
|
element.Text = processed;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// --- Комментарии ---
|
|||
|
|
private static void ReplaceInComments(WorksheetPart worksheetPart, Dictionary<string, string> replacementDict, StringComparison comparisonType)
|
|||
|
|
{
|
|||
|
|
var commentsPart = worksheetPart.WorksheetCommentsPart;
|
|||
|
|
if (commentsPart?.Comments is null) return;
|
|||
|
|
foreach (var comment in commentsPart.Comments.Elements<Comment>())
|
|||
|
|
{
|
|||
|
|
var textElement = comment.Descendants<CommentText>().FirstOrDefault();
|
|||
|
|
if (textElement?.Text is null) continue;
|
|||
|
|
string original = textElement.Text.Text;
|
|||
|
|
if (string.IsNullOrEmpty(original)) continue;
|
|||
|
|
string processed = ProcessReplacements(original, replacementDict, comparisonType);
|
|||
|
|
if (processed != original)
|
|||
|
|
textElement.Text.Text = processed;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|