From 1dcb22e1d58467a842777bc82fa98cf589c1f5ef Mon Sep 17 00:00:00 2001 From: melekhin Date: Tue, 21 Jul 2026 16:43:59 +0700 Subject: [PATCH] 26.07.21 --- QWERTYkez.ExcelProcessor/Editors/ExcelBook.cs | 26 +- .../Editors/ExcelSheet.cs | 2 +- QWERTYkez.ExcelProcessor/ExcelWriter.cs | 8 +- QWERTYkez.WordProcessor/MultiReplace.cs | 239 ++++++++++++------ QWERTYkez.WordProcessor/ReplaceItem.cs | 67 ++++- QWERTYkez.WordProcessor/SimplyReplace.cs | 86 +++++-- QWERTYkez.WordProcessor/WordReader.cs | 2 +- QWERTYkez.WordProcessor/WordWriter.cs | 2 +- 8 files changed, 306 insertions(+), 126 deletions(-) diff --git a/QWERTYkez.ExcelProcessor/Editors/ExcelBook.cs b/QWERTYkez.ExcelProcessor/Editors/ExcelBook.cs index 040a6d8..be89644 100644 --- a/QWERTYkez.ExcelProcessor/Editors/ExcelBook.cs +++ b/QWERTYkez.ExcelProcessor/Editors/ExcelBook.cs @@ -21,16 +21,16 @@ internal sealed class ExcelBook : IBook public IReadOnlyList GetSheets() => Writer.GetSheets(); /// - public ISheet? Sheet(string name) => Writer.Sheet(name); + public ISheet? Sheet(string name) => Writer.Sheet(name.EscapeSymbols()); /// - 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); /// - public bool TryAddSheet(string name, Action? edit = null) => Writer.TryAddSheet(name, edit); + public bool TryAddSheet(string name, Action? edit = null) => Writer.TryAddSheet(name.EscapeSymbols(), edit); /// - public bool TryRemoveSheet(string name) => Writer.TryRemoveSheet(name); + public bool TryRemoveSheet(string name) => Writer.TryRemoveSheet(name.EscapeSymbols()); /// public bool TryRemoveSheet(ISheet sheet) => Writer.TryRemoveSheet(sheet); @@ -40,4 +40,22 @@ internal sealed class ExcelBook : IBook /// 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(); + } } \ No newline at end of file diff --git a/QWERTYkez.ExcelProcessor/Editors/ExcelSheet.cs b/QWERTYkez.ExcelProcessor/Editors/ExcelSheet.cs index e2bb4d1..88d0912 100644 --- a/QWERTYkez.ExcelProcessor/Editors/ExcelSheet.cs +++ b/QWERTYkez.ExcelProcessor/Editors/ExcelSheet.cs @@ -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; } diff --git a/QWERTYkez.ExcelProcessor/ExcelWriter.cs b/QWERTYkez.ExcelProcessor/ExcelWriter.cs index 67af58e..f8521a8 100644 --- a/QWERTYkez.ExcelProcessor/ExcelWriter.cs +++ b/QWERTYkez.ExcelProcessor/ExcelWriter.cs @@ -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 /// 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()) { @@ -1168,7 +1172,7 @@ internal sealed class ExcelWriter : ExcelReader, IExcelReader, IExcelWriter /// public bool TryRemoveSheet(string name) { - var sheet = Sheet(name); + var sheet = Sheet(name.EscapeSymbols()); return sheet != null && TryRemoveSheet(sheet); } diff --git a/QWERTYkez.WordProcessor/MultiReplace.cs b/QWERTYkez.WordProcessor/MultiReplace.cs index ef99614..aa1d858 100644 --- a/QWERTYkez.WordProcessor/MultiReplace.cs +++ b/QWERTYkez.WordProcessor/MultiReplace.cs @@ -5,13 +5,15 @@ /// Каждое значение из массива помещается в отдельный параграф, причём первое значение /// остаётся в текущем параграфе, а последующие создают новые. /// Текст между вхождениями и после последнего сохраняется в соответствующих параграфах. +/// Поддерживает разрывы страниц и смену ориентации (альбомная/книжная) через . /// internal static class MultiReplaceExt { - // ---------- ПУБЛИЧНЫЕ МЕТОДЫ (СИГНАТУРЫ НЕИЗМЕННЫ) ---------- + // ---------- ПУБЛИЧНЫЕ МЕТОДЫ (для Body) ---------- #region Body.Replace с одним ключом + /// Заменяет все вхождения oldValue в теле документа на массив строк (каждая строка в отдельном параграфе). internal static void Replace(this Body body, string oldValue, IEnumerable 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); } + /// Заменяет все вхождения oldValue в теле документа на массив ReplaceItem (каждый элемент в отдельном параграфе с учётом разрывов). internal static void Replace(this Body body, string oldValue, IEnumerable 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 со словарём массивов + /// Заменяет все вхождения из словаря (ключ -> массив строк) в теле документа. internal static void Replace(this Body body, IEnumerable>> replacements, StringComparison comparisonType) { if (body is null || replacements is null) return; @@ -44,6 +48,7 @@ internal static class MultiReplaceExt } } + /// Заменяет все вхождения из словаря (ключ -> массив ReplaceItem) в теле документа. internal static void Replace(this Body body, IEnumerable>> replacements, StringComparison comparisonType) { if (body is null || replacements is null) return; @@ -62,6 +67,7 @@ internal static class MultiReplaceExt #region Paragraph.ReplaceWithMultiple (один ключ) + /// Заменяет все вхождения oldValue в параграфе на массив строк (каждая строка в новом параграфе). internal static bool ReplaceWithMultiple(this Paragraph? paragraph, string oldValue, IEnumerable 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; } + /// Заменяет все вхождения oldValue в параграфе на массив ReplaceItem (каждый элемент в новом параграфе с учётом разрывов). internal static bool ReplaceWithMultiple(this Paragraph? paragraph, string oldValue, IEnumerable 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 segments) + private class ParagraphStructure(string fullText, List segments) { public string FullText { get; } = fullText; public List Segments { get; } = segments; @@ -182,18 +189,24 @@ internal static class MultiReplaceExt return sb.ToString(); } + /// Клонирует свойства параграфа, но не копирует SectionProperties. 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; } - /// - /// Строит параграф, содержащий копии всех элементов исходного параграфа, - /// попадающих в текстовый диапазон [start, end). - /// + /// Строит параграф из текстового диапазона [start, end) исходного параграфа. 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().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().Where(r => !r.HasChildren).ToList()) run.Remove(); @@ -251,18 +259,13 @@ internal static class MultiReplaceExt return newPara; } - /// - /// Строит параграф, содержащий все элементы исходного параграфа, - /// которые находятся строго после указанной текстовой позиции, - /// пропуская нетекстовые элементы до первого текстового сегмента. - /// + /// Строит параграф из текста после позиции position, пропуская нетекстовые элементы до первого текстового сегмента. 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().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; } - /// - /// Вставляет в параграф новый Run с текстом из ReplaceItem, - /// копируя форматирование из сегмента, содержащего указанную позицию. - /// Если BreakPage == true, добавляет отдельный Run с разрывом страницы. - /// + /// Вставляет Run с текстом из ReplaceItem, копируя форматирование из сегмента по позиции. 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); - } } - /// - /// Добавляет содержимое одного параграфа в другой (клонируя элементы). - /// + /// Добавляет SectionProperties для смены ориентации, включая явный разрыв раздела. + 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); + } + + /// Сливает содержимое исходного параграфа в целевой (клонирует дочерние элементы). private static void MergeParagraph(Paragraph target, Paragraph source) { foreach (var child in source.ChildElements) target.AppendChild(child.CloneNode(true)); } - /// - /// Основной алгоритм: обрабатывает все вхождения всех ключей из предоставленных словарей. - /// + /// Основной алгоритм множественной замены с поддержкой разрывов и смены ориентации. private static List? ProcessMultiReplacements( Paragraph original, IEnumerable>>? stringReplacements, IEnumerable>>? itemReplacements, StringComparison comparisonType) { - // 1. Собираем определения замен + // 1. Сбор определений var definitions = new List(); 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(); 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? 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() 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().Any() && resultParas[i].ParagraphProperties is null) diff --git a/QWERTYkez.WordProcessor/ReplaceItem.cs b/QWERTYkez.WordProcessor/ReplaceItem.cs index acff729..4383de8 100644 --- a/QWERTYkez.WordProcessor/ReplaceItem.cs +++ b/QWERTYkez.WordProcessor/ReplaceItem.cs @@ -1,24 +1,71 @@ namespace QWERTYkez.WordProcessor; +/// +/// Определяет тип разрыва или смены ориентации страницы, +/// применяемый к элементу замены. +/// +public enum PageBreakType +{ + /// Без разрыва или смены ориентации. + None, + + /// Обычный разрыв страницы (новый лист). + PageBreak, + + /// Начать новую секцию с альбомной ориентацией страницы. + NewLandscapeSection, + + /// Начать новую секцию с книжной ориентацией страницы. + NewPortraitSection, +} + +/// +/// Представляет элемент замены текста, содержащий сам текст и указание +/// на тип разрыва или смены ориентации, который должен быть применён +/// после вставки этого текста. +/// +/// +/// Используется в методах множественной замены, например, +/// . +/// public readonly struct ReplaceItem { + /// + /// Инициализирует новый экземпляр с пустым текстом + /// и типом разрыва . + /// public ReplaceItem() { } - public ReplaceItem(string text) + + /// + /// Инициализирует новый экземпляр с указанным текстом + /// и типом разрыва/смены ориентации. + /// + /// Текст, который будет вставлен вместо плейсхолдера. + /// + /// Тип разрыва или смены ориентации, который будет применён после вставки текста. + /// По умолчанию . + /// + public ReplaceItem(string text, PageBreakType splitValue = PageBreakType.None) { Text = text; - } - public ReplaceItem(string text, bool breakPage) - { - Text = text; - BreakPage = breakPage; + SplitValue = splitValue; } + /// + /// Получает текст, который будет вставлен вместо плейсхолдера. + /// public string Text { get; init; } = string.Empty; - public bool BreakPage { get; init; } = false; + /// + /// Получает тип разрыва или смены ориентации, который будет применён + /// после вставки текста. + /// + public PageBreakType SplitValue { get; init; } = PageBreakType.None; - // Неявное преобразование из ReplaceItem в string - //public static implicit operator string(ReplaceItem item) => item.Text; - // Явное преобразование из string в ReplaceItem + /// + /// Определяет явное преобразование из строки в . + /// + /// Строка текста. + /// Новый экземпляр с указанным текстом и . public static explicit operator ReplaceItem(string text) => new() { Text = text }; } \ No newline at end of file diff --git a/QWERTYkez.WordProcessor/SimplyReplace.cs b/QWERTYkez.WordProcessor/SimplyReplace.cs index 6463036..40b7aa0 100644 --- a/QWERTYkez.WordProcessor/SimplyReplace.cs +++ b/QWERTYkez.WordProcessor/SimplyReplace.cs @@ -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> 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(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(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 runs) @@ -287,11 +283,11 @@ internal static class SimplyReplaceExt } private static void ExecuteReplacement( - List nodesToReplace, - int matchStart, - int matchEnd, - string newValue, - bool breakPage = false) + List 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) { diff --git a/QWERTYkez.WordProcessor/WordReader.cs b/QWERTYkez.WordProcessor/WordReader.cs index 9c9695e..1af0b8e 100644 --- a/QWERTYkez.WordProcessor/WordReader.cs +++ b/QWERTYkez.WordProcessor/WordReader.cs @@ -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}"); diff --git a/QWERTYkez.WordProcessor/WordWriter.cs b/QWERTYkez.WordProcessor/WordWriter.cs index 823043b..0013cc0 100644 --- a/QWERTYkez.WordProcessor/WordWriter.cs +++ b/QWERTYkez.WordProcessor/WordWriter.cs @@ -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