From da188eaeab0496f4f17760b3638805c50cfa8cbb Mon Sep 17 00:00:00 2001 From: melekhin Date: Thu, 23 Jul 2026 16:42:26 +0700 Subject: [PATCH] 26.07.23 --- QWERTYkez.WordProcessor/Debugger.cs | 8 + QWERTYkez.WordProcessor/MultiReplace.cs | 239 +++++++++++++++++------- QWERTYkez.WordProcessor/ReplaceItem.cs | 3 +- 3 files changed, 185 insertions(+), 65 deletions(-) create mode 100644 QWERTYkez.WordProcessor/Debugger.cs diff --git a/QWERTYkez.WordProcessor/Debugger.cs b/QWERTYkez.WordProcessor/Debugger.cs new file mode 100644 index 0000000..e41254b --- /dev/null +++ b/QWERTYkez.WordProcessor/Debugger.cs @@ -0,0 +1,8 @@ +namespace QWERTYkez.WordProcessor; + +#if DEBUG +public static class Debugger +{ + public static StringBuilder Builder { get; } = new(); +} +#endif \ No newline at end of file diff --git a/QWERTYkez.WordProcessor/MultiReplace.cs b/QWERTYkez.WordProcessor/MultiReplace.cs index f7d5eee..0e4b2bc 100644 --- a/QWERTYkez.WordProcessor/MultiReplace.cs +++ b/QWERTYkez.WordProcessor/MultiReplace.cs @@ -13,7 +13,6 @@ internal static class MultiReplaceExt #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; @@ -21,7 +20,6 @@ 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; @@ -33,7 +31,6 @@ 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; @@ -48,7 +45,6 @@ internal static class MultiReplaceExt } } - /// Заменяет все вхождения из словаря (ключ -> массив ReplaceItem) в теле документа. internal static void Replace(this Body body, IEnumerable>> replacements, StringComparison comparisonType) { if (body is null || replacements is null) return; @@ -67,7 +63,6 @@ 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) @@ -85,7 +80,6 @@ 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) @@ -189,29 +183,28 @@ internal static class MultiReplaceExt return sb.ToString(); } - /// Клонирует свойства параграфа, но не копирует SectionProperties. - private static Paragraph CloneParagraphProperties(Paragraph original) + /// Клонирует параграф, удаляя все SectionProperties. + private static Paragraph CloneParagraphWithoutSection(Paragraph original) { var newPara = new Paragraph(); if (original.ParagraphProperties is not null) { - var props = new ParagraphProperties(); + var newProps = new ParagraphProperties(); foreach (var child in original.ParagraphProperties.ChildElements) { if (child is not SectionProperties) - props.AppendChild(child.CloneNode(true)); + newProps.AppendChild(child.CloneNode(true)); } - newPara.ParagraphProperties = props; + newPara.ParagraphProperties = newProps; } return newPara; } - /// Строит параграф из текстового диапазона [start, end) исходного параграфа. private static Paragraph? BuildRangeParagraph(Paragraph original, ParagraphStructure structure, int start, int end) { if (start >= end) return null; - var newPara = CloneParagraphProperties(original); + var newPara = CloneParagraphWithoutSection(original); foreach (var child in original.ChildElements) { @@ -259,12 +252,11 @@ 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); + var newPara = CloneParagraphWithoutSection(original); var firstTextSeg = structure.Segments.FirstOrDefault(s => s.Start >= position); bool passedFirstText = false; @@ -316,7 +308,6 @@ internal static class MultiReplaceExt return newPara; } - /// Вставляет 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); @@ -329,7 +320,6 @@ internal static class MultiReplaceExt para.AppendChild(textRun); } - /// Добавляет SectionProperties для смены ориентации, включая явный разрыв раздела. private static void AddSectionProperties(Paragraph para, PageBreakType splitValue) { if (para is null) return; @@ -356,15 +346,18 @@ internal static class MultiReplaceExt Height = height, Orient = orientation }, - new SectionType { Val = SectionMarkValues.NextPage } // явный разрыв раздела + new SectionType { Val = SectionMarkValues.NextPage } ); para.ParagraphProperties ??= new ParagraphProperties(); - // Вставляем в начало, чтобы свойства секции были первыми + // Удаляем все существующие SectionProperties перед добавлением новой + var existingSections = para.ParagraphProperties.Elements().ToList(); + foreach (var sec in existingSections) + sec.Remove(); + para.ParagraphProperties.InsertAt(sectionProps, 0); } - /// Сливает содержимое исходного параграфа в целевой (клонирует дочерние элементы). private static void MergeParagraph(Paragraph target, Paragraph source) { foreach (var child in source.ChildElements) @@ -378,7 +371,9 @@ internal static class MultiReplaceExt IEnumerable>>? itemReplacements, StringComparison comparisonType) { - // 1. Сбор определений + // Логируем начало + Log($"=== ProcessMultiReplacements START ==="); + Log($"Original text: '{original.InnerText}'"); var definitions = new List(); if (stringReplacements is not null) { @@ -397,6 +392,11 @@ internal static class MultiReplaceExt definitions.Add(new MatchDefinition(kvp.Key, kvp.Value)); } } + Log($"Definitions count: {definitions.Count}"); + foreach (var def in definitions) + { + Log($" Key: '{def.Key}', Values: {string.Join(", ", def.Values.Select(v => $"'{v.Text}' [{v.SplitValue}]"))}"); + } if (definitions.Count == 0) return null; // 2. Анализ структуры параграфа @@ -404,6 +404,7 @@ internal static class MultiReplaceExt if (runs.Count == 0) return null; var structure = AnalyzeParagraphStructure(runs); string fullText = structure.FullText; + Log($"Full text: '{fullText}'"); if (fullText.Length == 0) return null; // 3. Поиск всех вхождений @@ -424,17 +425,53 @@ internal static class MultiReplaceExt } if (matches.Count == 0) return null; + Log($"Matches found: {matches.Count}"); + foreach (var match in matches) + { + Log($" Match: '{match.Definition.Key}' at {match.Start}-{match.End}"); + } + matches.Sort((a, b) => a.Start.CompareTo(b.Start)); - // 4. Построение результата + // 4. Определяем секцию, которая должна следовать за original + SectionProperties? followingSection = original.ParagraphProperties?.GetFirstChild()?.CloneNode(true) as SectionProperties; + if (followingSection is null) + { + var nextPara = original.NextSibling(); + if (nextPara is not null) + { + followingSection = nextPara.ParagraphProperties?.GetFirstChild()?.CloneNode(true) as SectionProperties; + if (followingSection is not null) + { + var pageSize = followingSection.GetFirstChild(); + if (pageSize is not null) + { + Log($"Following section taken from next paragraph: '{nextPara.InnerText}'"); + Log($"Following section orientation: {(pageSize.Orient == PageOrientationValues.Landscape ? "landscape" : "portrait")}"); + } + } + } + } + bool hasFollowingSection = followingSection is not null; + Log($"Has following section: {hasFollowingSection}"); + + // 5. Построение результата var resultParas = new List(); Paragraph? currentPara = null; int currentPos = 0; - PageBreakType? pendingOrientation = null; // отложенная смена ориентации для следующего параграфа + + // Отложенная ориентация для следующего параграфа + PageBreakType? pendingOrientation = null; + bool pendingApplied = false; + + // Флаг, была ли смена ориентации внутри группы + bool sectionChangeInsideGroup = false; for (int i = 0; i < matches.Count; i++) { var match = matches[i]; + Log($"--- Processing match {i}: '{match.Definition.Key}' at {match.Start}-{match.End} ---"); + Log($" Values count: {match.Definition.Values.Count()}"); // Текст перед совпадением if (currentPos < match.Start) @@ -460,45 +497,38 @@ internal static class MultiReplaceExt for (int vIdx = 0; vIdx < values.Count; vIdx++) { var item = values[vIdx]; + Log($" Processing value {vIdx}: '{item.Text}' [{item.SplitValue}]"); - bool createNew = false; - if (vIdx == 0) + // Всегда создаём новый параграф для каждого элемента замены + var newPara = CloneParagraphWithoutSection(original); + resultParas.Add(newPara); + currentPara = newPara; + Log($" Created new paragraph: '{item.Text}' (placeholder)"); + + // Применяем отложенную ориентацию, если есть и не применена + if (pendingOrientation.HasValue && !pendingApplied) { - // Создаём новый параграф, если у первого элемента есть разрыв/смена ориентации - if (currentPara is null || item.SplitValue != PageBreakType.None) - createNew = true; - } - else - { - createNew = true; + AddSectionProperties(currentPara, pendingOrientation.Value); + Log($" Applied pending orientation: {pendingOrientation.Value}"); + pendingOrientation = null; + pendingApplied = true; } - if (createNew) + // Если это первый созданный параграф и у него есть смена ориентации, + // задаём книжную, чтобы избежать наследования предыдущей секции + if (resultParas.Count == 1 && vIdx == 0 && item.SplitValue != PageBreakType.None) { - var newPara = CloneParagraphProperties(original); - resultParas.Add(newPara); - currentPara = newPara; - - // Если есть отложенная ориентация, применяем её к этому новому параграфу и сбрасываем - if (pendingOrientation.HasValue) + if (currentPara.ParagraphProperties?.GetFirstChild() is null) { - 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); - } + AddSectionProperties(currentPara, PageBreakType.NewPortraitSection); + Log($" Added portrait section to first paragraph (to avoid inheriting landscape)"); } + sectionChangeInsideGroup = true; } // Вставляем текст InsertFormattedRun(currentPara, item, structure, match.Start); + Log($" Inserted text '{item.Text}' into paragraph"); // Обработка разрывов страниц (обычный PageBreak) if (item.SplitValue == PageBreakType.PageBreak) @@ -508,52 +538,118 @@ internal static class MultiReplaceExt if (seg is not null && seg.Run.RunProperties is not null) breakRun.RunProperties = (RunProperties)seg.Run.RunProperties.CloneNode(true); currentPara.AppendChild(breakRun); + Log($" Added page break"); } - // Смена ориентации – откладываем для следующего параграфа + // Смена ориентации – устанавливаем отложенную для следующего параграфа else if (item.SplitValue == PageBreakType.NewLandscapeSection || item.SplitValue == PageBreakType.NewPortraitSection) { pendingOrientation = item.SplitValue; + pendingApplied = false; + sectionChangeInsideGroup = true; + Log($" Set pending orientation: {item.SplitValue} (will be applied to next paragraph)"); } + + // Логируем содержимое параграфа после обработки + Log($" Current paragraph content now: '{currentPara.InnerText}'"); } currentPos = match.End; } // Текст после последнего совпадения + Log($"--- Processing remainder after last match, currentPos={currentPos}, fullText.Length={fullText.Length} ---"); if (currentPos < fullText.Length) { var textPart = BuildAfterParagraph(original, structure, currentPos); if (textPart is not null) { - if (pendingOrientation.HasValue) + // Если есть отложенная ориентация и не применена, применяем к остатку + if (pendingOrientation.HasValue && !pendingApplied) { - // Создаём новый параграф для остатка и применяем ориентацию - var newPara = CloneParagraphProperties(original); + var newPara = CloneParagraphWithoutSection(original); MergeParagraph(newPara, textPart); AddSectionProperties(newPara, pendingOrientation.Value); resultParas.Add(newPara); + Log($" Applied pending orientation to remainder: {pendingOrientation.Value}"); pendingOrientation = null; + pendingApplied = true; } else { - if (currentPara is null) + // Если есть следующая секция (followingSection) и внутри группы была смена, + // применяем следующую секцию к остатку с явным разрывом страницы + if (hasFollowingSection && sectionChangeInsideGroup) { - currentPara = textPart; - resultParas.Add(currentPara); + var newPara = CloneParagraphWithoutSection(original); + // Добавляем явный разрыв страницы перед остатком + var breakRun = new Run(new Break { Type = BreakValues.Page }); + newPara.AppendChild(breakRun); + MergeParagraph(newPara, textPart); + if (newPara.ParagraphProperties is null) + newPara.ParagraphProperties = new ParagraphProperties(); + // Удаляем все существующие секции + var existing = newPara.ParagraphProperties.Elements().ToList(); + foreach (var sec in existing) sec.Remove(); + newPara.ParagraphProperties.InsertAt(followingSection!.CloneNode(true), 0); + resultParas.Add(newPara); + Log($" Applied following section to remainder with page break"); } else { - MergeParagraph(currentPara, textPart); + // Иначе добавляем остаток в текущий параграф (или создаём новый) + if (currentPara is null) + { + var newPara = CloneParagraphWithoutSection(original); + MergeParagraph(newPara, textPart); + if (hasFollowingSection && !sectionChangeInsideGroup) + { + if (newPara.ParagraphProperties is null) + newPara.ParagraphProperties = new ParagraphProperties(); + newPara.ParagraphProperties.InsertAt(followingSection!.CloneNode(true), 0); + Log($" Applied following section to new remainder paragraph (no section change)"); + } + resultParas.Add(newPara); + currentPara = newPara; + } + else + { + if (hasFollowingSection && !sectionChangeInsideGroup) + { + if (currentPara.ParagraphProperties is null) + currentPara.ParagraphProperties = new ParagraphProperties(); + currentPara.ParagraphProperties.InsertAt(followingSection!.CloneNode(true), 0); + Log($" Applied following section to existing remainder paragraph (no section change)"); + } + MergeParagraph(currentPara, textPart); + } } } } } - // Если осталась отложенная ориентация и нет остатка текста, - // это значит, что маркер смены ориентации был последним элементом в документе. - // В этом случае мы НЕ создаём новый параграф, чтобы избежать пустого листа. - // pendingOrientation просто игнорируется. + // Если остатка нет, но была смена и есть следующая секция, + // создаём параграф со следующей секцией + if (currentPos >= fullText.Length && hasFollowingSection && sectionChangeInsideGroup) + { + var newPara = CloneParagraphWithoutSection(original); + if (newPara.ParagraphProperties is null) + newPara.ParagraphProperties = new ParagraphProperties(); + newPara.ParagraphProperties.InsertAt(followingSection!.CloneNode(true), 0); + resultParas.Add(newPara); + Log($" Created empty paragraph with following section (no remainder)"); + } + + // Исправление: если осталась отложенная ориентация, но остатка нет, + // создаём новый параграф с этой ориентацией (чтобы начать новую секцию для последующего текста) + if (pendingOrientation.HasValue && currentPos >= fullText.Length) + { + var newPara = CloneParagraphWithoutSection(original); + AddSectionProperties(newPara, pendingOrientation.Value); + resultParas.Add(newPara); + Log($" Created empty paragraph with pending orientation: {pendingOrientation.Value} (no remainder)"); + pendingOrientation = null; + } // Очистка пустых параграфов for (int i = resultParas.Count - 1; i >= 0; i--) @@ -562,6 +658,23 @@ internal static class MultiReplaceExt resultParas.RemoveAt(i); } + // Логируем результат + Log($"=== ProcessMultiReplacements END, resulting paragraphs: {resultParas.Count} ==="); + for (int i = 0; i < resultParas.Count; i++) + { + var p = resultParas[i]; + var hasSection = p.ParagraphProperties?.GetFirstChild() is not null; + Log($" Paragraph {i}: '{p.InnerText}' - Section: {hasSection}"); + } + return resultParas.Count > 0 ? resultParas : null; } + + // Логирование + private static void Log(string message) + { +#if DEBUG + Debugger.Builder.AppendLine(message); +#endif + } } \ No newline at end of file diff --git a/QWERTYkez.WordProcessor/ReplaceItem.cs b/QWERTYkez.WordProcessor/ReplaceItem.cs index 4383de8..09a8a1a 100644 --- a/QWERTYkez.WordProcessor/ReplaceItem.cs +++ b/QWERTYkez.WordProcessor/ReplaceItem.cs @@ -1,8 +1,7 @@ namespace QWERTYkez.WordProcessor; /// -/// Определяет тип разрыва или смены ориентации страницы, -/// применяемый к элементу замены. +/// Определяет тип разрыва или смены ориентации страницы, применяется к элементам следующим после замены /// public enum PageBreakType {