This commit is contained in:
melekhin
2026-07-21 16:43:59 +07:00
parent 5302edfb8f
commit 1dcb22e1d5
8 changed files with 306 additions and 126 deletions
+22 -4
View File
@@ -21,16 +21,16 @@ internal sealed class ExcelBook : IBook
public IReadOnlyList<ISheet> GetSheets() => Writer.GetSheets();
/// <inheritdoc />
public ISheet? Sheet(string name) => Writer.Sheet(name);
public ISheet? Sheet(string name) => Writer.Sheet(name.EscapeSymbols());
/// <inheritdoc />
public bool TryGetSheet(string name, out ISheet sheet) => Writer.TryGetSheet(name, out sheet);
public bool TryGetSheet(string name, out ISheet sheet) => Writer.TryGetSheet(name.EscapeSymbols(), out sheet);
/// <inheritdoc />
public bool TryAddSheet(string name, Action<ISheet>? edit = null) => Writer.TryAddSheet(name, edit);
public bool TryAddSheet(string name, Action<ISheet>? edit = null) => Writer.TryAddSheet(name.EscapeSymbols(), edit);
/// <inheritdoc />
public bool TryRemoveSheet(string name) => Writer.TryRemoveSheet(name);
public bool TryRemoveSheet(string name) => Writer.TryRemoveSheet(name.EscapeSymbols());
/// <inheritdoc />
public bool TryRemoveSheet(ISheet sheet) => Writer.TryRemoveSheet(sheet);
@@ -40,4 +40,22 @@ internal sealed class ExcelBook : IBook
/// <inheritdoc />
public NumberFormatPattern CreateNumberFormat(string format) => Writer.CreateNumberFormat(format);
}
public static class EscapeExt
{
public static string EscapeSymbols(this string source)
{
var sb = new StringBuilder(source);
sb.Replace('/', '');
sb.Replace('\\', '');
sb.Replace('*', '');
sb.Replace('?', '');
sb.Replace(':', '˸');
sb.Replace('[', '');
sb.Replace(']', '');
return sb.ToString();
}
}
@@ -33,7 +33,7 @@ internal sealed class ExcelSheet : ISheet
{
if (string.IsNullOrEmpty(name)) return false;
Book.ThrowIfDisposed();
SheetElement.Name = name;
SheetElement.Name = name.EscapeSymbols();
return true;
}
+6 -2
View File
@@ -1095,6 +1095,8 @@ internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter
if (string.IsNullOrEmpty(name))
return null;
name = name.EscapeSymbols();
lock (_syncLock)
{
var workbookPart = _doc.WorkbookPart;
@@ -1113,7 +1115,7 @@ internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter
/// <inheritdoc />
public bool TryGetSheet(string name, out ISheet sheet)
{
sheet = Sheet(name)!;
sheet = Sheet(name.EscapeSymbols())!;
return sheet != null;
}
@@ -1130,6 +1132,8 @@ internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter
if (workbookPart?.Workbook?.Sheets == null)
return false;
name = name.EscapeSymbols();
// Проверка уникальности имени
foreach (Sheet s in workbookPart.Workbook.Sheets.Elements<Sheet>())
{
@@ -1168,7 +1172,7 @@ internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter
/// <inheritdoc />
public bool TryRemoveSheet(string name)
{
var sheet = Sheet(name);
var sheet = Sheet(name.EscapeSymbols());
return sheet != null && TryRemoveSheet(sheet);
}
+158 -81
View File
@@ -5,13 +5,15 @@
/// Каждое значение из массива помещается в отдельный параграф, причём первое значение
/// остаётся в текущем параграфе, а последующие создают новые.
/// Текст между вхождениями и после последнего сохраняется в соответствующих параграфах.
/// Поддерживает разрывы страниц и смену ориентации (альбомная/книжная) через <see cref="ReplaceItem.SplitValue"/>.
/// </summary>
internal static class MultiReplaceExt
{
// ---------- ПУБЛИЧНЫЕ МЕТОДЫ (СИГНАТУРЫ НЕИЗМЕННЫ) ----------
// ---------- ПУБЛИЧНЫЕ МЕТОДЫ (для Body) ----------
#region Body.Replace с одним ключом
/// <summary>Заменяет все вхождения oldValue в теле документа на массив строк (каждая строка в отдельном параграфе).</summary>
internal static void Replace(this Body body, string oldValue, IEnumerable<string> newValues, StringComparison comparisonType)
{
if (body is null || string.IsNullOrEmpty(oldValue) || newValues is null) return;
@@ -19,6 +21,7 @@ internal static class MultiReplaceExt
body.Replace(dict, comparisonType);
}
/// <summary>Заменяет все вхождения oldValue в теле документа на массив ReplaceItem (каждый элемент в отдельном параграфе с учётом разрывов).</summary>
internal static void Replace(this Body body, string oldValue, IEnumerable<ReplaceItem> newValues, StringComparison comparisonType)
{
if (body is null || string.IsNullOrEmpty(oldValue) || newValues is null) return;
@@ -30,6 +33,7 @@ internal static class MultiReplaceExt
#region Body.Replace со словарём массивов
/// <summary>Заменяет все вхождения из словаря (ключ -> массив строк) в теле документа.</summary>
internal static void Replace(this Body body, IEnumerable<KeyValuePair<string, IEnumerable<string>>> replacements, StringComparison comparisonType)
{
if (body is null || replacements is null) return;
@@ -44,6 +48,7 @@ internal static class MultiReplaceExt
}
}
/// <summary>Заменяет все вхождения из словаря (ключ -> массив ReplaceItem) в теле документа.</summary>
internal static void Replace(this Body body, IEnumerable<KeyValuePair<string, IEnumerable<ReplaceItem>>> replacements, StringComparison comparisonType)
{
if (body is null || replacements is null) return;
@@ -62,6 +67,7 @@ internal static class MultiReplaceExt
#region Paragraph.ReplaceWithMultiple (один ключ)
/// <summary>Заменяет все вхождения oldValue в параграфе на массив строк (каждая строка в новом параграфе).</summary>
internal static bool ReplaceWithMultiple(this Paragraph? paragraph, string oldValue, IEnumerable<string> newValues, StringComparison comparisonType)
{
if (paragraph is null || string.IsNullOrEmpty(oldValue) || newValues is null || newValues.Count() == 0)
@@ -79,6 +85,7 @@ internal static class MultiReplaceExt
return false;
}
/// <summary>Заменяет все вхождения oldValue в параграфе на массив ReplaceItem (каждый элемент в новом параграфе с учётом разрывов).</summary>
internal static bool ReplaceWithMultiple(this Paragraph? paragraph, string oldValue, IEnumerable<ReplaceItem> newValues, StringComparison comparisonType)
{
if (paragraph is null || string.IsNullOrEmpty(oldValue) || newValues is null || newValues.Count() == 0)
@@ -152,7 +159,7 @@ internal static class MultiReplaceExt
public int End { get; } = end;
}
private class ParagraphStructure(string fullText, List<MultiReplaceExt.RunSegment> segments)
private class ParagraphStructure(string fullText, List<RunSegment> segments)
{
public string FullText { get; } = fullText;
public List<RunSegment> Segments { get; } = segments;
@@ -182,18 +189,24 @@ internal static class MultiReplaceExt
return sb.ToString();
}
/// <summary>Клонирует свойства параграфа, но не копирует SectionProperties.</summary>
private static Paragraph CloneParagraphProperties(Paragraph original)
{
var newPara = new Paragraph();
if (original.ParagraphProperties is not null)
newPara.ParagraphProperties = (ParagraphProperties)original.ParagraphProperties.CloneNode(true);
{
var props = new ParagraphProperties();
foreach (var child in original.ParagraphProperties.ChildElements)
{
if (child is not SectionProperties)
props.AppendChild(child.CloneNode(true));
}
newPara.ParagraphProperties = props;
}
return newPara;
}
/// <summary>
/// Строит параграф, содержащий копии всех элементов исходного параграфа,
/// попадающих в текстовый диапазон [start, end).
/// </summary>
/// <summary>Строит параграф из текстового диапазона [start, end) исходного параграфа.</summary>
private static Paragraph? BuildRangeParagraph(Paragraph original, ParagraphStructure structure, int start, int end)
{
if (start >= end) return null;
@@ -207,7 +220,6 @@ internal static class MultiReplaceExt
var seg = structure.Segments.FirstOrDefault(s => s.Run == run);
if (seg is null)
{
// Run без текста (разрыв, поле) – копируем целиком, т.к. не можем привязать к позиции
newPara.AppendChild(run.CloneNode(true));
continue;
}
@@ -217,31 +229,27 @@ internal static class MultiReplaceExt
if (seg.Start >= start && seg.End <= end)
{
// Полностью внутри диапазона
newPara.AppendChild(run.CloneNode(true));
}
else
{
// Частичное пересечение – обрезаем текст
var runClone = (Run)run.CloneNode(true);
foreach (var t in runClone.Elements<Text>().ToList())
t.Remove();
int cutStart = Math.Max(start, seg.Start) - seg.Start;
int cutEnd = Math.Min(end, seg.End) - seg.Start;
string newText = seg.Text.Substring(cutStart, cutEnd - cutStart);
runClone.AppendChild(new Text(newText));
newPara.AppendChild(runClone);
}
int cutStart = Math.Max(start, seg.Start) - seg.Start;
int cutEnd = Math.Min(end, seg.End) - seg.Start;
string newText = seg.Text.Substring(cutStart, cutEnd - cutStart);
runClone.AppendChild(new Text(newText));
newPara.AppendChild(runClone);
}
}
else
{
// Не Run – копируем всегда (закладки, поля и т.п.), т.к. не можем определить позицию
newPara.AppendChild(child.CloneNode(true));
}
}
// Удаляем пустые Run
foreach (var run in newPara.Descendants<Run>().Where(r => !r.HasChildren).ToList())
run.Remove();
@@ -251,18 +259,13 @@ internal static class MultiReplaceExt
return newPara;
}
/// <summary>
/// Строит параграф, содержащий все элементы исходного параграфа,
/// которые находятся строго после указанной текстовой позиции,
/// пропуская нетекстовые элементы до первого текстового сегмента.
/// </summary>
/// <summary>Строит параграф из текста после позиции position, пропуская нетекстовые элементы до первого текстового сегмента.</summary>
private static Paragraph? BuildAfterParagraph(Paragraph original, ParagraphStructure structure, int position)
{
if (position >= structure.FullText.Length) return null;
var newPara = CloneParagraphProperties(original);
// Находим первый текстовый сегмент, который начинается на или после position
var firstTextSeg = structure.Segments.FirstOrDefault(s => s.Start >= position);
bool passedFirstText = false;
@@ -273,7 +276,6 @@ internal static class MultiReplaceExt
var seg = structure.Segments.FirstOrDefault(s => s.Run == run);
if (seg is null)
{
// Run без текста – добавляем только если уже прошли первый текстовый сегмент
if (passedFirstText)
newPara.AppendChild(run.CloneNode(true));
continue;
@@ -281,14 +283,12 @@ internal static class MultiReplaceExt
if (seg.Start >= position)
{
// Полностью после позиции
newPara.AppendChild(run.CloneNode(true));
if (seg == firstTextSeg)
passedFirstText = true;
}
else if (seg.End > position)
{
// Частично пересекает – обрезаем текст
var runClone = (Run)run.CloneNode(true);
foreach (var t in runClone.Elements<Text>().ToList())
t.Remove();
@@ -299,11 +299,9 @@ internal static class MultiReplaceExt
newPara.AppendChild(runClone);
passedFirstText = true;
}
// seg.End <= position игнорируем
}
else
{
// Не Run – добавляем только если уже прошли первый текстовый сегмент
if (passedFirstText)
newPara.AppendChild(child.CloneNode(true));
}
@@ -318,11 +316,7 @@ internal static class MultiReplaceExt
return newPara;
}
/// <summary>
/// Вставляет в параграф новый Run с текстом из ReplaceItem,
/// копируя форматирование из сегмента, содержащего указанную позицию.
/// Если BreakPage == true, добавляет отдельный Run с разрывом страницы.
/// </summary>
/// <summary>Вставляет Run с текстом из ReplaceItem, копируя форматирование из сегмента по позиции.</summary>
private static void InsertFormattedRun(Paragraph para, ReplaceItem item, ParagraphStructure structure, int position)
{
var seg = structure.Segments.FirstOrDefault(s => position >= s.Start && position < s.End);
@@ -333,42 +327,65 @@ internal static class MultiReplaceExt
textRun.RunProperties = (RunProperties)seg.Run.RunProperties.CloneNode(true);
textRun.AppendChild(new Text(item.Text ?? string.Empty));
para.AppendChild(textRun);
if (item.BreakPage)
{
var breakRun = new Run(new Break() { Type = BreakValues.Page });
if (seg.Run.RunProperties is not null)
breakRun.RunProperties = (RunProperties)seg.Run.RunProperties.CloneNode(true);
para.AppendChild(breakRun);
}
}
/// <summary>
/// Добавляет содержимое одного параграфа в другой (клонируя элементы).
/// </summary>
/// <summary>Добавляет SectionProperties для смены ориентации, включая явный разрыв раздела.</summary>
private static void AddSectionProperties(Paragraph para, PageBreakType splitValue)
{
if (para is null) return;
PageOrientationValues orientation = splitValue == PageBreakType.NewLandscapeSection
? PageOrientationValues.Landscape
: PageOrientationValues.Portrait;
uint width, height;
if (orientation == PageOrientationValues.Landscape)
{
width = 16838; // A4 landscape
height = 11906;
}
else
{
width = 11906; // A4 portrait
height = 16838;
}
var sectionProps = new SectionProperties(
new PageSize
{
Width = width,
Height = height,
Orient = orientation
},
new SectionType { Val = SectionMarkValues.NextPage } // явный разрыв раздела
);
para.ParagraphProperties ??= new ParagraphProperties();
para.ParagraphProperties.AppendChild(sectionProps);
}
/// <summary>Сливает содержимое исходного параграфа в целевой (клонирует дочерние элементы).</summary>
private static void MergeParagraph(Paragraph target, Paragraph source)
{
foreach (var child in source.ChildElements)
target.AppendChild(child.CloneNode(true));
}
/// <summary>
/// Основной алгоритм: обрабатывает все вхождения всех ключей из предоставленных словарей.
/// </summary>
/// <summary>Основной алгоритм множественной замены с поддержкой разрывов и смены ориентации.</summary>
private static List<Paragraph>? ProcessMultiReplacements(
Paragraph original,
IEnumerable<KeyValuePair<string, IEnumerable<string>>>? stringReplacements,
IEnumerable<KeyValuePair<string, IEnumerable<ReplaceItem>>>? itemReplacements,
StringComparison comparisonType)
{
// 1. Собираем определения замен
// 1. Сбор определений
var definitions = new List<MatchDefinition>();
if (stringReplacements is not null)
{
foreach (var kvp in stringReplacements)
{
if (string.IsNullOrEmpty(kvp.Key) || kvp.Value is null || kvp.Value.Count() == 0) continue;
definitions.Add(new MatchDefinition(kvp.Key, [.. kvp.Value.Select(v => new ReplaceItem(v, false))]));
var items = kvp.Value.Select(v => new ReplaceItem(v, PageBreakType.None));
definitions.Add(new MatchDefinition(kvp.Key, items));
}
}
if (itemReplacements is not null)
@@ -379,7 +396,6 @@ internal static class MultiReplaceExt
definitions.Add(new MatchDefinition(kvp.Key, kvp.Value));
}
}
if (definitions.Count == 0) return null;
// 2. Анализ структуры параграфа
@@ -389,7 +405,7 @@ internal static class MultiReplaceExt
string fullText = structure.FullText;
if (fullText.Length == 0) return null;
// 3. Находим все вхождения всех ключей
// 3. Поиск всех вхождений
var matches = new List<Match>();
foreach (var def in definitions)
{
@@ -405,22 +421,21 @@ internal static class MultiReplaceExt
pos += def.Key.Length;
}
}
if (matches.Count == 0) return null;
// 4. Сортируем по позиции
matches.Sort((a, b) => a.Start.CompareTo(b.Start));
// 5. Построение результирующих параграфов
// 4. Построение результата
var resultParas = new List<Paragraph>();
Paragraph? currentPara = null;
int currentPos = 0;
PageBreakType? pendingOrientation = null; // отложенная смена ориентации для следующего параграфа
for (int i = 0; i < matches.Count; i++)
{
var match = matches[i];
// Текст перед текущим совпадением (от currentPos до match.Start)
// Текст перед совпадением
if (currentPos < match.Start)
{
var textPart = BuildRangeParagraph(original, structure, currentPos, match.Start);
@@ -438,51 +453,113 @@ internal static class MultiReplaceExt
}
}
// Обрабатываем значения замены для этого совпадения
var values = match.Definition.Values;
var values = match.Definition.Values.ToList();
if (values.Count == 0) continue;
// Первое значение – в текущий параграф (или создаём новый)
if (currentPara is null)
for (int vIdx = 0; vIdx < values.Count; vIdx++)
{
currentPara = CloneParagraphProperties(original);
resultParas.Add(currentPara);
var item = values[vIdx];
bool createNew = false;
if (vIdx == 0)
{
// Создаём новый параграф, если у первого элемента есть разрыв/смена ориентации
if (currentPara is null || item.SplitValue != PageBreakType.None)
createNew = true;
}
else
{
createNew = true;
}
if (createNew)
{
var newPara = CloneParagraphProperties(original);
resultParas.Add(newPara);
currentPara = newPara;
// Если есть отложенная ориентация, применяем её к этому новому параграфу и сбрасываем
if (pendingOrientation.HasValue)
{
AddSectionProperties(currentPara, pendingOrientation.Value);
pendingOrientation = null;
}
// Если это первый созданный параграф и у него есть разрыв/смена ориентации,
// явно задаём книжную ориентацию, чтобы избежать наследования альбомной.
if (resultParas.Count == 1 && vIdx == 0 && item.SplitValue != PageBreakType.None)
{
if (currentPara.ParagraphProperties?.GetFirstChild<SectionProperties>() is null)
{
AddSectionProperties(currentPara, PageBreakType.NewPortraitSection);
}
}
}
// Вставляем текст
InsertFormattedRun(currentPara, item, structure, match.Start);
// Обработка разрывов страниц (обычный PageBreak)
if (item.SplitValue == PageBreakType.PageBreak)
{
var seg = structure.Segments.FirstOrDefault(s => match.Start >= s.Start && match.Start < s.End);
var breakRun = new Run(new Break { Type = BreakValues.Page });
if (seg is not null && seg.Run.RunProperties is not null)
breakRun.RunProperties = (RunProperties)seg.Run.RunProperties.CloneNode(true);
currentPara.AppendChild(breakRun);
}
// Смена ориентации – откладываем для следующего параграфа
else if (item.SplitValue == PageBreakType.NewLandscapeSection ||
item.SplitValue == PageBreakType.NewPortraitSection)
{
pendingOrientation = item.SplitValue;
}
}
values.ForFirstNext(first =>
{
InsertFormattedRun(currentPara, first, structure, match.Start);
},
next =>
{
// Остальные значения – в новые параграфы
var newPara = CloneParagraphProperties(original);
InsertFormattedRun(newPara, next, structure, match.Start);
resultParas.Add(newPara);
currentPara = newPara; // теперь текущий параграф – последний созданный
});
currentPos = match.End;
}
// Текст после последнего совпадения – используем BuildAfterParagraph, чтобы пропустить лишние разрывы
// Текст после последнего совпадения
if (currentPos < fullText.Length)
{
var textPart = BuildAfterParagraph(original, structure, currentPos);
if (textPart is not null)
{
if (currentPara is null)
if (pendingOrientation.HasValue)
{
currentPara = textPart;
resultParas.Add(currentPara);
// Создаём новый параграф для остатка и применяем ориентацию
var newPara = CloneParagraphProperties(original);
MergeParagraph(newPara, textPart);
AddSectionProperties(newPara, pendingOrientation.Value);
resultParas.Add(newPara);
pendingOrientation = null;
}
else
{
MergeParagraph(currentPara, textPart);
if (currentPara is null)
{
currentPara = textPart;
resultParas.Add(currentPara);
}
else
{
MergeParagraph(currentPara, textPart);
}
}
}
}
// Удаляем пустые параграфы
// Если осталась отложенная ориентация (например, замена в конце документа, и не было создано новых параграфов),
// создаём новый параграф с этой ориентацией (пустой, чтобы начать новую секцию)
if (pendingOrientation.HasValue)
{
var newPara = CloneParagraphProperties(original);
AddSectionProperties(newPara, pendingOrientation.Value);
resultParas.Add(newPara);
pendingOrientation = null;
}
// Очистка пустых параграфов
for (int i = resultParas.Count - 1; i >= 0; i--)
{
if (!resultParas[i].ChildElements.OfType<Run>().Any() && resultParas[i].ParagraphProperties is null)
+57 -10
View File
@@ -1,24 +1,71 @@
namespace QWERTYkez.WordProcessor;
/// <summary>
/// Определяет тип разрыва или смены ориентации страницы,
/// применяемый к элементу замены.
/// </summary>
public enum PageBreakType
{
/// <summary>Без разрыва или смены ориентации.</summary>
None,
/// <summary>Обычный разрыв страницы (новый лист).</summary>
PageBreak,
/// <summary>Начать новую секцию с альбомной ориентацией страницы.</summary>
NewLandscapeSection,
/// <summary>Начать новую секцию с книжной ориентацией страницы.</summary>
NewPortraitSection,
}
/// <summary>
/// Представляет элемент замены текста, содержащий сам текст и указание
/// на тип разрыва или смены ориентации, который должен быть применён
/// после вставки этого текста.
/// </summary>
/// <remarks>
/// Используется в методах множественной замены, например,
/// <see cref="IWordWriter.ReplaceItem(string, IEnumerable{ReplaceItem})"/>.
/// </remarks>
public readonly struct ReplaceItem
{
/// <summary>
/// Инициализирует новый экземпляр <see cref="ReplaceItem"/> с пустым текстом
/// и типом разрыва <see cref="PageBreakType.None"/>.
/// </summary>
public ReplaceItem() { }
public ReplaceItem(string text)
/// <summary>
/// Инициализирует новый экземпляр <see cref="ReplaceItem"/> с указанным текстом
/// и типом разрыва/смены ориентации.
/// </summary>
/// <param name="text">Текст, который будет вставлен вместо плейсхолдера.</param>
/// <param name="splitValue">
/// Тип разрыва или смены ориентации, который будет применён после вставки текста.
/// По умолчанию <see cref="PageBreakType.None"/>.
/// </param>
public ReplaceItem(string text, PageBreakType splitValue = PageBreakType.None)
{
Text = text;
}
public ReplaceItem(string text, bool breakPage)
{
Text = text;
BreakPage = breakPage;
SplitValue = splitValue;
}
/// <summary>
/// Получает текст, который будет вставлен вместо плейсхолдера.
/// </summary>
public string Text { get; init; } = string.Empty;
public bool BreakPage { get; init; } = false;
/// <summary>
/// Получает тип разрыва или смены ориентации, который будет применён
/// после вставки текста.
/// </summary>
public PageBreakType SplitValue { get; init; } = PageBreakType.None;
// Неявное преобразование из ReplaceItem в string
//public static implicit operator string(ReplaceItem item) => item.Text;
// Явное преобразование из string в ReplaceItem
/// <summary>
/// Определяет явное преобразование из строки в <see cref="ReplaceItem"/>.
/// </summary>
/// <param name="text">Строка текста.</param>
/// <returns>Новый экземпляр <see cref="ReplaceItem"/> с указанным текстом и <see cref="PageBreakType.None"/>.</returns>
public static explicit operator ReplaceItem(string text) => new() { Text = text };
}
+60 -26
View File
@@ -9,7 +9,7 @@ internal static class SimplyReplaceExt
internal readonly int Length = length;
}
private sealed class ParagraphStructure(string fullText, SimplyReplaceExt.TextNodeInfo[] textNodes)
private sealed class ParagraphStructure(string fullText, TextNodeInfo[] textNodes)
{
internal readonly string FullText = fullText;
internal readonly TextNodeInfo[] TextNodes = textNodes;
@@ -77,7 +77,7 @@ internal static class SimplyReplaceExt
}
}
internal static bool SimpleReplace(this Paragraph? paragraph, string oldValue, string newValue, StringComparison comparisonType, bool breakPage = false)
internal static bool SimpleReplace(this Paragraph? paragraph, string oldValue, string newValue, StringComparison comparisonType, PageBreakType splitValue = PageBreakType.None)
{
if (paragraph is null || string.IsNullOrEmpty(oldValue))
return false;
@@ -103,10 +103,13 @@ internal static class SimplyReplaceExt
if (nodesToReplace.Count == 0)
return false;
ExecuteReplacement(nodesToReplace, matchIndex, matchEnd, newValue, breakPage);
ExecuteReplacement(nodesToReplace, matchIndex, matchEnd, newValue, splitValue);
return true;
}
internal static bool SimpleReplace(this Paragraph? paragraph, string oldValue, string newValue, StringComparison comparisonType, bool breakPage)
=> SimpleReplace(paragraph, oldValue, newValue, comparisonType, breakPage ? PageBreakType.PageBreak : PageBreakType.None);
internal static void Replace(this Paragraph paragraph, IEnumerable<KeyValuePair<string, string>> replacements, StringComparison comparisonType)
{
if (paragraph is null || replacements is null || replacements.Count() == 0)
@@ -120,10 +123,8 @@ internal static class SimplyReplaceExt
if (structure.FullText.Length == 0)
return;
// Используем List с предопределенной емкостью
var replacementsInParagraph = new List<ReplacementInfo>(replacements.Count() * 2);
// Сначала находим все вхождения
var fullText = structure.FullText;
foreach (var kvp in replacements)
{
@@ -137,7 +138,8 @@ internal static class SimplyReplaceExt
{
OldValue = kvp.Key,
NewValue = kvp.Value ?? string.Empty,
Index = pos
Index = pos,
SplitValue = PageBreakType.None
});
pos += kvp.Key.Length;
}
@@ -146,10 +148,8 @@ internal static class SimplyReplaceExt
if (replacementsInParagraph.Count == 0)
return;
// Сортируем по убыванию позиции
replacementsInParagraph.Sort((x, y) => y.Index.CompareTo(x.Index));
// Выполняем замены
for (int i = 0; i < replacementsInParagraph.Count; i++)
{
var replacement = replacementsInParagraph[i];
@@ -159,7 +159,7 @@ internal static class SimplyReplaceExt
var nodesToReplace = FindNodesToReplace(structure, matchIndex, matchEnd);
if (nodesToReplace.Count > 0)
{
ExecuteReplacement(nodesToReplace, matchIndex, matchEnd, replacement.NewValue);
ExecuteReplacement(nodesToReplace, matchIndex, matchEnd, replacement.NewValue, replacement.SplitValue);
}
}
}
@@ -177,10 +177,8 @@ internal static class SimplyReplaceExt
if (structure.FullText.Length == 0)
return;
// Используем List с предопределенной емкостью
var replacementsInParagraph = new List<ReplacementInfo>(replacements.Count() * 2);
// Сначала находим все вхождения
var fullText = structure.FullText;
foreach (var kvp in replacements)
{
@@ -194,8 +192,8 @@ internal static class SimplyReplaceExt
{
OldValue = kvp.Key,
NewValue = kvp.Value.Text ?? string.Empty,
BreakPage = kvp.Value.BreakPage,
Index = pos
Index = pos,
SplitValue = kvp.Value.SplitValue
});
pos += kvp.Key.Length;
}
@@ -204,10 +202,8 @@ internal static class SimplyReplaceExt
if (replacementsInParagraph.Count == 0)
return;
// Сортируем по убыванию позиции
replacementsInParagraph.Sort((x, y) => y.Index.CompareTo(x.Index));
// Выполняем замены
for (int i = 0; i < replacementsInParagraph.Count; i++)
{
var replacement = replacementsInParagraph[i];
@@ -217,7 +213,7 @@ internal static class SimplyReplaceExt
var nodesToReplace = FindNodesToReplace(structure, matchIndex, matchEnd);
if (nodesToReplace.Count > 0)
{
ExecuteReplacement(nodesToReplace, matchIndex, matchEnd, replacement.NewValue, replacement.BreakPage);
ExecuteReplacement(nodesToReplace, matchIndex, matchEnd, replacement.NewValue, replacement.SplitValue);
}
}
}
@@ -227,7 +223,7 @@ internal static class SimplyReplaceExt
internal string OldValue { get; set; } = null!;
internal string NewValue { get; set; } = null!;
internal int Index { get; set; }
internal bool BreakPage { get; set; }
internal PageBreakType SplitValue { get; set; } = PageBreakType.None;
}
private static ParagraphStructure AnalyzeParagraphStructure(IEnumerable<Run> runs)
@@ -287,11 +283,11 @@ internal static class SimplyReplaceExt
}
private static void ExecuteReplacement(
List<TextNodeInfo> nodesToReplace,
int matchStart,
int matchEnd,
string newValue,
bool breakPage = false)
List<TextNodeInfo> nodesToReplace,
int matchStart,
int matchEnd,
string newValue,
PageBreakType splitValue)
{
if (nodesToReplace.Count == 0) return;
@@ -311,22 +307,61 @@ internal static class SimplyReplaceExt
processedNewValue
);
// Очищаем остальные текстовые ноды
for (int i = 1; i < nodesToReplace.Count; i++)
{
nodesToReplace[i].Text.Text = string.Empty;
}
if (breakPage)
if (splitValue == PageBreakType.PageBreak)
{
if (nodesToReplace[0].Text.Parent is Run run && run.Parent is Paragraph para)
{
var breakRun = new Run(new Break() { Type = BreakValues.Page });
var breakRun = new Run(new Break { Type = BreakValues.Page });
if (run.RunProperties is not null)
breakRun.RunProperties = (RunProperties)run.RunProperties.CloneNode(true);
para.AppendChild(breakRun);
}
}
else if (splitValue == PageBreakType.NewLandscapeSection || splitValue == PageBreakType.NewPortraitSection)
{
var firstText = nodesToReplace[0].Text;
if (firstText.Parent is Run run && run.Parent is Paragraph para)
{
AddSectionProperties(para, splitValue);
}
}
}
private static void AddSectionProperties(Paragraph para, PageBreakType splitValue)
{
if (para is null) return;
PageOrientationValues orientation = splitValue == PageBreakType.NewLandscapeSection
? PageOrientationValues.Landscape
: PageOrientationValues.Portrait;
uint width, height;
if (orientation == PageOrientationValues.Landscape)
{
width = 16838;
height = 11906;
}
else
{
width = 11906;
height = 16838;
}
var sectionProps = new SectionProperties(
new PageSize
{
Width = width,
Height = height,
Orient = orientation
}
);
para.ParagraphProperties ??= new ParagraphProperties();
para.ParagraphProperties.AppendChild(sectionProps);
}
private static unsafe string ReplaceSpacesWithNonBreaking(string input)
@@ -358,7 +393,6 @@ internal static class SimplyReplaceExt
int end = Math.Min(start + length, original.Length);
// Оптимизированная конкатенация
var sb = new StringBuilder(original.Length - length + replacement.Length);
if (start > 0)
{
+1 -1
View File
@@ -21,7 +21,7 @@ internal class WordReader : IDisposable, IWordReader
internal static WordReader? CreateInternal(FileInfo sourceFile)
{
if (sourceFile is null || !sourceFile.Exists)
if (sourceFile is null || !File.Exists(sourceFile.FullName))
{
#if DEBUG
Debug.WriteLine($"[DEBUG] Source file is null or does not exist: {sourceFile?.FullName}");
+1 -1
View File
@@ -81,7 +81,7 @@ internal sealed class WordWriter : WordReader, IWordWriter
internal static WordWriter? CreateInternal(FileInfo sourceFile, string? destinationPath = null!)
{
if (sourceFile is null || !sourceFile.Exists) return null;
if (sourceFile is null || !File.Exists(sourceFile.FullName)) return null;
var ms = new MemoryStream();
try