From 25207773f7c7915b2c82622dfff0c61774838213 Mon Sep 17 00:00:00 2001 From: melekhin Date: Mon, 3 Aug 2026 16:11:54 +0700 Subject: [PATCH] [ Word ROTATES ] --- QWERTYkez.WordProcessor/Debugger.cs | 8 - QWERTYkez.WordProcessor/Extension.cs | 31 ++ QWERTYkez.WordProcessor/IWordReader.cs | 4 + QWERTYkez.WordProcessor/IeExtension.cs | 106 ----- QWERTYkez.WordProcessor/MultiReplace.cs | 592 +++++++++++------------- 5 files changed, 317 insertions(+), 424 deletions(-) delete mode 100644 QWERTYkez.WordProcessor/Debugger.cs create mode 100644 QWERTYkez.WordProcessor/Extension.cs delete mode 100644 QWERTYkez.WordProcessor/IeExtension.cs diff --git a/QWERTYkez.WordProcessor/Debugger.cs b/QWERTYkez.WordProcessor/Debugger.cs deleted file mode 100644 index e41254b..0000000 --- a/QWERTYkez.WordProcessor/Debugger.cs +++ /dev/null @@ -1,8 +0,0 @@ -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/Extension.cs b/QWERTYkez.WordProcessor/Extension.cs new file mode 100644 index 0000000..229efc7 --- /dev/null +++ b/QWERTYkez.WordProcessor/Extension.cs @@ -0,0 +1,31 @@ +namespace QWERTYkez.WordProcessor; + +internal static class Extension +{ + public static void SwapBottomRight(this PageMargin margin) + { + if (margin.Right is { } r) + { + margin.Right = margin.Bottom is not null ? new((uint)margin.Bottom.Value) : null; + margin.Bottom = new((int)r.Value); + } + else if(margin.Bottom is { } b) + { + margin.Bottom = null; + margin.Right = new((uint)b.Value); + } + } + public static void SwapValues(this PageSize size) + { + if (size.Width is { } w) + { + size.Width = size.Height is not null ? new(size.Height.Value) : null; + size.Height = new(w.Value); + } + else if (size.Height is { } h) + { + size.Height = null; + size.Width = new(h.Value); + } + } +} \ No newline at end of file diff --git a/QWERTYkez.WordProcessor/IWordReader.cs b/QWERTYkez.WordProcessor/IWordReader.cs index d148f65..1e33aed 100644 --- a/QWERTYkez.WordProcessor/IWordReader.cs +++ b/QWERTYkez.WordProcessor/IWordReader.cs @@ -6,6 +6,10 @@ public interface IWordReader string? FilePath { get; } bool IsValid { get; } +#if DEBUG + Body Body { get; } +#endif + ISet FindPlaceholders(); diff --git a/QWERTYkez.WordProcessor/IeExtension.cs b/QWERTYkez.WordProcessor/IeExtension.cs deleted file mode 100644 index 8344851..0000000 --- a/QWERTYkez.WordProcessor/IeExtension.cs +++ /dev/null @@ -1,106 +0,0 @@ -namespace QWERTYkez.WordProcessor; - -internal static class IeExtension -{ - /// - /// Выполняет указанные действия для первого и последующих элементов последовательности. - /// - /// Тип элементов последовательности. - /// Последовательность элементов. - /// Действие над первым элементом (если есть). - /// Действие над каждым последующим элементом, начиная со второго. - /// Возникает, если items или любой из делегатов равен null. - public static void ForFirstNext(this IEnumerable items, Action first, Action next) - { - if (items is null) throw new ArgumentNullException(nameof(items)); - if (first is null) throw new ArgumentNullException(nameof(first)); - if (next is null) throw new ArgumentNullException(nameof(next)); - - using var enumerator = items.GetEnumerator(); - - if (!enumerator.MoveNext()) - return; - - first(enumerator.Current); - - while (enumerator.MoveNext()) - { - next(enumerator.Current); - } - } - - /// - /// Выполняет указанные действия для первого, промежуточных и последнего элементов последовательности. - /// Если последовательность содержит только один элемент, то для него вызываются и first, и last. - /// - /// Тип элементов последовательности. - /// Последовательность элементов. - /// Действие над первым элементом. - /// Действие над элементами, которые не являются ни первыми, ни последними. - /// Действие над последним элементом. - /// Возникает, если items или любой из делегатов равен null. - public static void ForFirstNextLast(this IEnumerable items, Action first, Action next, Action last) - { - if (items is null) throw new ArgumentNullException(nameof(items)); - if (first is null) throw new ArgumentNullException(nameof(first)); - if (next is null) throw new ArgumentNullException(nameof(next)); - if (last is null) throw new ArgumentNullException(nameof(last)); - - using var enumerator = items.GetEnumerator(); - - if (!enumerator.MoveNext()) - return; - - T firstItem = enumerator.Current; - - // Если только один элемент - if (!enumerator.MoveNext()) - { - first(firstItem); - last(firstItem); - return; - } - - // Есть как минимум два элемента - first(firstItem); - - T prev = enumerator.Current; // второй элемент - while (enumerator.MoveNext()) - { - next(prev); // предыдущий элемент точно не последний - prev = enumerator.Current; - } - - last(prev); // последний элемент - } - - /// - /// Выполняет указанные действия для всех элементов, кроме последнего, и для последнего элемента. - /// Если последовательность содержит только один элемент, то вызывается только last. - /// - /// Тип элементов последовательности. - /// Последовательность элементов. - /// Действие над элементами, не являющимися последними. - /// Действие над последним элементом. - /// Возникает, если items или любой из делегатов равен null. - public static void ForNextLast(this IEnumerable items, Action next, Action last) - { - if (items is null) throw new ArgumentNullException(nameof(items)); - if (next is null) throw new ArgumentNullException(nameof(next)); - if (last is null) throw new ArgumentNullException(nameof(last)); - - using var enumerator = items.GetEnumerator(); - - if (!enumerator.MoveNext()) - return; - - T prev = enumerator.Current; - while (enumerator.MoveNext()) - { - next(prev); - prev = enumerator.Current; - } - - last(prev); - } -} \ No newline at end of file diff --git a/QWERTYkez.WordProcessor/MultiReplace.cs b/QWERTYkez.WordProcessor/MultiReplace.cs index 0e4b2bc..c422853 100644 --- a/QWERTYkez.WordProcessor/MultiReplace.cs +++ b/QWERTYkez.WordProcessor/MultiReplace.cs @@ -1,5 +1,12 @@ namespace QWERTYkez.WordProcessor; +#if DEBUG +public static class Debugger +{ + public static StringBuilder Builder { get; } = new(); +} +#endif + /// /// Выполняет замену всех вхождений ключей из словаря на соответствующие массивы значений. /// Каждое значение из массива помещается в отдельный параграф, причём первое значение @@ -183,7 +190,7 @@ internal static class MultiReplaceExt return sb.ToString(); } - /// Клонирует параграф, удаляя все SectionProperties. + /// Клонирует параграф, удаляя все SectionProperties и PageBreakBefore. private static Paragraph CloneParagraphWithoutSection(Paragraph original) { var newPara = new Paragraph(); @@ -192,7 +199,7 @@ internal static class MultiReplaceExt var newProps = new ParagraphProperties(); foreach (var child in original.ParagraphProperties.ChildElements) { - if (child is not SectionProperties) + if (child is not SectionProperties && child is not PageBreakBefore) newProps.AppendChild(child.CloneNode(true)); } newPara.ParagraphProperties = newProps; @@ -200,111 +207,32 @@ internal static class MultiReplaceExt return newPara; } - private static Paragraph? BuildRangeParagraph(Paragraph original, ParagraphStructure structure, int start, int end) - { - if (start >= end) return null; - - var newPara = CloneParagraphWithoutSection(original); - - foreach (var child in original.ChildElements) - { - if (child is Run run) - { - var seg = structure.Segments.FirstOrDefault(s => s.Run == run); - if (seg is null) - { - newPara.AppendChild(run.CloneNode(true)); - continue; - } - - if (seg.End <= start || seg.Start >= end) - continue; - - 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); - } - } - else - { - newPara.AppendChild(child.CloneNode(true)); - } - } - - foreach (var run in newPara.Descendants().Where(r => !r.HasChildren).ToList()) - run.Remove(); - - if (!newPara.ChildElements.OfType().Any() && newPara.ParagraphProperties is null) - return null; - - return newPara; - } - - private static Paragraph? BuildAfterParagraph(Paragraph original, ParagraphStructure structure, int position) + private static Paragraph? BuildRemainderParagraph(Paragraph original, ParagraphStructure structure, int position) { if (position >= structure.FullText.Length) return null; var newPara = CloneParagraphWithoutSection(original); - - var firstTextSeg = structure.Segments.FirstOrDefault(s => s.Start >= position); - bool passedFirstText = false; - - foreach (var child in original.ChildElements) + foreach (var seg in structure.Segments) { - if (child is Run run) + if (seg.End <= position) continue; + if (seg.Start >= position) { - var seg = structure.Segments.FirstOrDefault(s => s.Run == run); - if (seg is null) - { - if (passedFirstText) - newPara.AppendChild(run.CloneNode(true)); - continue; - } - - 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(); - - int offset = position - seg.Start; - string newText = seg.Text.Substring(offset); - runClone.AppendChild(new Text(newText)); - newPara.AppendChild(runClone); - passedFirstText = true; - } + newPara.AppendChild(seg.Run.CloneNode(true)); } - else + else if (seg.End > position) { - if (passedFirstText) - newPara.AppendChild(child.CloneNode(true)); + var runClone = (Run)seg.Run.CloneNode(true); + foreach (var t in runClone.Elements().ToList()) t.Remove(); + int offset = position - seg.Start; + string newText = seg.Text.Substring(offset); + runClone.AppendChild(new Text(newText)); + newPara.AppendChild(runClone); } } - foreach (var run in newPara.Descendants().Where(r => !r.HasChildren).ToList()) run.Remove(); - if (!newPara.ChildElements.OfType().Any() && newPara.ParagraphProperties is null) return null; - return newPara; } @@ -320,42 +248,99 @@ internal static class MultiReplaceExt para.AppendChild(textRun); } - private static void AddSectionProperties(Paragraph para, PageBreakType splitValue) + /// + /// Добавляет SectionProperties к параграфу. Все значения (PageSize, PageMargin) берутся из документа. + /// Для книжных секций (addPageSize=false) Orient не устанавливается (not set). + /// + private static void AddSectionProperties(Paragraph para, PageBreakType splitValue, bool addPageSize, SectionProperties sourceSection, SectionProperties portraitSection) { if (para is null) return; - PageOrientationValues orientation = splitValue == PageBreakType.NewLandscapeSection - ? PageOrientationValues.Landscape - : PageOrientationValues.Portrait; - uint width, height; - if (orientation == PageOrientationValues.Landscape) + // Удаляем существующие секции + if (para.ParagraphProperties is not null) { - width = 16838; // A4 landscape - height = 11906; + var sections = para.ParagraphProperties.Elements().ToList(); + foreach (var sec in sections) sec.Remove(); + var pageBreaks = para.ParagraphProperties.Elements().ToList(); + foreach (var pb in pageBreaks) pb.Remove(); + } + + para.ParagraphProperties ??= new ParagraphProperties(); + var sectionProps = new SectionProperties(); + + // Добавляем PageSize + SectionProperties? sizeSource = addPageSize ? sourceSection : portraitSection; + var sourcePageSize = sizeSource?.GetFirstChild(); + if (sourcePageSize is null && portraitSection is not null) + sourcePageSize = portraitSection.GetFirstChild(); + if (sourcePageSize is null && sourceSection is not null) + sourcePageSize = sourceSection.GetFirstChild(); + + if (sourcePageSize is not null) + { + var pageSizeClone = (PageSize)sourcePageSize.CloneNode(true); + bool sourceIsLandscape = pageSizeClone.Orient?.Value == PageOrientationValues.Landscape || + (pageSizeClone.Width?.Value > pageSizeClone.Height?.Value); + + bool targetIsLandscape = (addPageSize && splitValue == PageBreakType.NewLandscapeSection); + + if (targetIsLandscape && !sourceIsLandscape) + { + pageSizeClone.SwapValues(); + pageSizeClone.Orient = PageOrientationValues.Landscape; + } + else if (!targetIsLandscape && sourceIsLandscape) + { + pageSizeClone.SwapValues(); + if (addPageSize) + pageSizeClone.Orient = PageOrientationValues.Portrait; + else + pageSizeClone.Orient = null; // не задаём Orient (not set) + } + else + { + if (targetIsLandscape) + pageSizeClone.Orient = PageOrientationValues.Landscape; + else if (addPageSize) + pageSizeClone.Orient = PageOrientationValues.Portrait; + else + pageSizeClone.Orient = null; // not set + } + sectionProps.AppendChild(pageSizeClone); + } + + // Копируем PageMargin + PageMargin? marginToUse = null; + if (addPageSize) + { + var sourceMargin = sourceSection?.GetFirstChild() + ?? portraitSection?.GetFirstChild(); + if (sourceMargin is not null) + { + var marginClone = (PageMargin)sourceMargin.CloneNode(true); + var sourceOrient = sourceSection?.GetFirstChild()?.Orient; + bool sourceIsLandscape = sourceOrient?.Value == PageOrientationValues.Landscape || + (sourceSection?.GetFirstChild()?.Width?.Value > sourceSection?.GetFirstChild()?.Height?.Value); + if ((splitValue == PageBreakType.NewLandscapeSection && !sourceIsLandscape) || + (splitValue == PageBreakType.NewPortraitSection && sourceIsLandscape)) + { + marginClone.SwapBottomRight(); + } + marginToUse = marginClone; + } } else { - width = 11906; // A4 portrait - height = 16838; + var portraitMargin = portraitSection?.GetFirstChild(); + if (portraitMargin is not null) + marginToUse = (PageMargin)portraitMargin.CloneNode(true); } - var sectionProps = new SectionProperties( - new PageSize - { - Width = width, - Height = height, - Orient = orientation - }, - new SectionType { Val = SectionMarkValues.NextPage } - ); + if (marginToUse is not null) + sectionProps.AppendChild(marginToUse); - para.ParagraphProperties ??= new ParagraphProperties(); - // Удаляем все существующие SectionProperties перед добавлением новой - var existingSections = para.ParagraphProperties.Elements().ToList(); - foreach (var sec in existingSections) - sec.Remove(); - - para.ParagraphProperties.InsertAt(sectionProps, 0); + sectionProps.AppendChild(new SectionType { Val = SectionMarkValues.NextPage }); + para.ParagraphProperties.AppendChild(sectionProps); } private static void MergeParagraph(Paragraph target, Paragraph source) @@ -364,6 +349,56 @@ internal static class MultiReplaceExt target.AppendChild(child.CloneNode(true)); } + /// Выводит структуру документа: для каждого параграфа показывает текст, наличие секции, ориентацию, размеры и поля. + private static void LogDocumentStructure(Body body, string title) + { +#if DEBUG + Debugger.Builder.AppendLine($"=== {title} ==="); + var paragraphs = body.Descendants().ToList(); + int index = 0; + foreach (var para in paragraphs) + { + var text = para.InnerText.Replace("\n", "\\n").Replace("\r", "\\r"); + var section = para.ParagraphProperties?.GetFirstChild(); + string sectionInfo = "None"; + if (section is not null) + { + var pageSize = section.GetFirstChild(); + string orient = pageSize?.Orient?.ToString() ?? "not set"; + string sizeInfo = ""; + if (pageSize is not null) + { + sizeInfo = $" Size: W={pageSize.Width?.Value}, H={pageSize.Height?.Value}"; + } + var margins = section.GetFirstChild(); + string marginInfo = ""; + if (margins is not null) + { + marginInfo = $" Margins: Top={margins.Top?.Value}, Bottom={margins.Bottom?.Value}, Left={margins.Left?.Value}, Right={margins.Right?.Value}"; + } + sectionInfo = $"Orient={orient}{sizeInfo}{marginInfo}"; + } + Debugger.Builder.AppendLine($" Para {index}: Text='{text}', Section={sectionInfo}"); + index++; + } + + // Логируем секции из Body (если есть) + var bodySections = body.Elements().ToList(); + if (bodySections.Any()) + { + Debugger.Builder.AppendLine(" Body SectionProperties:"); + foreach (var sec in bodySections) + { + var ps = sec.GetFirstChild(); + var pm = sec.GetFirstChild(); + Debugger.Builder.AppendLine($" PageSize: Width={ps?.Width}, Height={ps?.Height}, Orient={ps?.Orient}"); + Debugger.Builder.AppendLine($" PageMargin: Top={pm?.Top}, Bottom={pm?.Bottom}, Left={pm?.Left}, Right={pm?.Right}"); + } + } + Debugger.Builder.AppendLine($"=== END {title} ==="); +#endif + } + /// Основной алгоритм множественной замены с поддержкой разрывов и смены ориентации. private static List? ProcessMultiReplacements( Paragraph original, @@ -371,9 +406,9 @@ internal static class MultiReplaceExt IEnumerable>>? itemReplacements, StringComparison comparisonType) { - // Логируем начало - Log($"=== ProcessMultiReplacements START ==="); - Log($"Original text: '{original.InnerText}'"); + var body = original.Ancestors().FirstOrDefault(); + + // 1. Сбор определений var definitions = new List(); if (stringReplacements is not null) { @@ -392,11 +427,6 @@ 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,7 +434,6 @@ 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,71 +453,63 @@ 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. Определяем секцию, которая должна следовать за original - SectionProperties? followingSection = original.ParagraphProperties?.GetFirstChild()?.CloneNode(true) as SectionProperties; - if (followingSection is null) + // 4. Получаем исходную секцию для копирования (из original или из документа) + SectionProperties? sourceSection = original.ParagraphProperties?.GetFirstChild()?.CloneNode(true) as SectionProperties; + if (sourceSection is null && body is not 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")}"); - } - } - } + sourceSection = body.Elements().LastOrDefault()?.CloneNode(true) as SectionProperties; } - bool hasFollowingSection = followingSection is not null; - Log($"Has following section: {hasFollowingSection}"); + sourceSection ??= new SectionProperties(); + + // Получаем книжную секцию для копирования полей (из документа) + SectionProperties? portraitSection = null; + if (body is not null) + { + portraitSection = body.Elements().FirstOrDefault()?.CloneNode(true) as SectionProperties; + } + portraitSection ??= new SectionProperties(); // 5. Построение результата var resultParas = new List(); Paragraph? currentPara = null; int currentPos = 0; - - // Отложенная ориентация для следующего параграфа - PageBreakType? pendingOrientation = null; - bool pendingApplied = false; - - // Флаг, была ли смена ориентации внутри группы bool sectionChangeInsideGroup = false; + PageBreakType? lastOrientation = null; 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) { - var textPart = BuildRangeParagraph(original, structure, currentPos, match.Start); - if (textPart is not null) + var beforePara = CloneParagraphWithoutSection(original); + foreach (var seg in structure.Segments) { - if (currentPara is null) + if (seg.End <= currentPos) continue; + if (seg.Start >= match.Start) break; + if (seg.Start >= currentPos && seg.End <= match.Start) { - currentPara = textPart; - resultParas.Add(currentPara); + beforePara.AppendChild(seg.Run.CloneNode(true)); } - else + else if (seg.Start < match.Start && seg.End > currentPos) { - MergeParagraph(currentPara, textPart); + int startOffset = Math.Max(0, currentPos - seg.Start); + int endOffset = Math.Min(seg.Text.Length, match.Start - seg.Start); + var runClone = (Run)seg.Run.CloneNode(true); + foreach (var t in runClone.Elements().ToList()) t.Remove(); + string textPart = seg.Text.Substring(startOffset, endOffset - startOffset); + runClone.AppendChild(new Text(textPart)); + beforePara.AppendChild(runClone); } } + if (beforePara.ChildElements.OfType().Any()) + { + resultParas.Add(beforePara); + currentPara = beforePara; + } } var values = match.Definition.Values.ToList(); @@ -497,184 +518,135 @@ internal static class MultiReplaceExt for (int vIdx = 0; vIdx < values.Count; vIdx++) { var item = values[vIdx]; - Log($" Processing value {vIdx}: '{item.Text}' [{item.SplitValue}]"); - // Всегда создаём новый параграф для каждого элемента замены + // Создаём новый параграф для каждого элемента var newPara = CloneParagraphWithoutSection(original); resultParas.Add(newPara); currentPara = newPara; - Log($" Created new paragraph: '{item.Text}' (placeholder)"); - // Применяем отложенную ориентацию, если есть и не применена - if (pendingOrientation.HasValue && !pendingApplied) - { - AddSectionProperties(currentPara, pendingOrientation.Value); - Log($" Applied pending orientation: {pendingOrientation.Value}"); - pendingOrientation = null; - pendingApplied = true; - } + InsertFormattedRun(currentPara, item, structure, match.Start); - // Если это первый созданный параграф и у него есть смена ориентации, - // задаём книжную, чтобы избежать наследования предыдущей секции - if (resultParas.Count == 1 && vIdx == 0 && item.SplitValue != PageBreakType.None) + // Обработка смены ориентации + if (item.SplitValue == PageBreakType.NewLandscapeSection || item.SplitValue == PageBreakType.NewPortraitSection) { - if (currentPara.ParagraphProperties?.GetFirstChild() is null) + bool addPageSize = (vIdx != 0); + PageBreakType orientation = item.SplitValue; + if (vIdx != 0) { - AddSectionProperties(currentPara, PageBreakType.NewPortraitSection); - Log($" Added portrait section to first paragraph (to avoid inheriting landscape)"); + orientation = PageBreakType.NewLandscapeSection; } + AddSectionProperties(currentPara, orientation, addPageSize, sourceSection, portraitSection); + lastOrientation = item.SplitValue; sectionChangeInsideGroup = true; } - - // Вставляем текст - InsertFormattedRun(currentPara, item, structure, match.Start); - Log($" Inserted text '{item.Text}' into paragraph"); - - // Обработка разрывов страниц (обычный PageBreak) - if (item.SplitValue == PageBreakType.PageBreak) + else 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); - 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}'"); + // Закрываем секцию, если была смена и последний параграф не имеет секции + if (sectionChangeInsideGroup && lastOrientation.HasValue && currentPara is not null) + { + if (currentPara.ParagraphProperties?.GetFirstChild() is null) + { + bool addPageSize = (lastOrientation.Value == PageBreakType.NewLandscapeSection); + AddSectionProperties(currentPara, lastOrientation.Value, addPageSize, sourceSection, portraitSection); + } + } + + // Если исходная секция была landscape, внутри группы была смена, и последняя ориентация — книжная, + // добавляем landscape секцию в Body, чтобы остаток остался landscape. + if (body is not null) + { + var sourcePageSize = sourceSection.GetFirstChild(); + if (sourcePageSize is not null) + { + bool sourceIsLandscape = sourcePageSize.Orient?.Value == PageOrientationValues.Landscape || + (sourcePageSize.Width?.Value > sourcePageSize.Height?.Value); + bool lastIsPortrait = lastOrientation.HasValue && lastOrientation.Value == PageBreakType.NewPortraitSection; + if (sourceIsLandscape && sectionChangeInsideGroup && lastIsPortrait) + { + // Проверяем, есть ли уже секция landscape на Body + bool hasBodyLandscape = false; + foreach (var bodySec in body.Elements()) + { + var ps = bodySec.GetFirstChild(); + if (ps is not null && ps.Orient?.Value == PageOrientationValues.Landscape) + { + hasBodyLandscape = true; + break; + } + } + if (!hasBodyLandscape) + { + // Создаём секцию landscape для Body + var bodySection = new SectionProperties(); + var pageSizeClone = (PageSize)sourcePageSize.CloneNode(true); + // Если ориентация не landscape, меняем + if (pageSizeClone.Orient?.Value != PageOrientationValues.Landscape) + { + pageSizeClone.SwapValues(); + pageSizeClone.Orient = PageOrientationValues.Landscape; + } + bodySection.AppendChild(pageSizeClone); + var sourceMargin = sourceSection.GetFirstChild(); + if (sourceMargin is not null) + { + var marginClone = (PageMargin)sourceMargin.CloneNode(true); + // Если исходная секция не landscape, но мы делаем landscape, меняем поля + if (sourceSection.GetFirstChild()?.Orient?.Value != PageOrientationValues.Landscape) + { + marginClone.SwapBottomRight(); + } + bodySection.AppendChild(marginClone); + } + bodySection.AppendChild(new SectionType { Val = SectionMarkValues.NextPage }); + body.AppendChild(bodySection); + } + } + } } currentPos = match.End; } - // Текст после последнего совпадения - Log($"--- Processing remainder after last match, currentPos={currentPos}, fullText.Length={fullText.Length} ---"); + // 6. Обработка остатка текста (если есть) if (currentPos < fullText.Length) { - var textPart = BuildAfterParagraph(original, structure, currentPos); - if (textPart is not null) + var remainderPara = BuildRemainderParagraph(original, structure, currentPos); + if (remainderPara is not null) { - // Если есть отложенная ориентация и не применена, применяем к остатку - if (pendingOrientation.HasValue && !pendingApplied) + // Если внутри группы была смена, применяем последнюю ориентацию к остатку + if (sectionChangeInsideGroup && lastOrientation.HasValue) { - 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 - { - // Если есть следующая секция (followingSection) и внутри группы была смена, - // применяем следующую секцию к остатку с явным разрывом страницы - if (hasFollowingSection && sectionChangeInsideGroup) - { - 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 - { - // Иначе добавляем остаток в текущий параграф (или создаём новый) - 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); - } - } + var breakRun = new Run(new Break { Type = BreakValues.Page }); + remainderPara.InsertAt(breakRun, 0); + bool addPageSize = (lastOrientation.Value == PageBreakType.NewLandscapeSection); + AddSectionProperties(remainderPara, lastOrientation.Value, addPageSize, sourceSection, portraitSection); } + resultParas.Add(remainderPara); } } - // Если остатка нет, но была смена и есть следующая секция, - // создаём параграф со следующей секцией - 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; - } - - // Очистка пустых параграфов + // 7. Очистка пустых параграфов for (int i = resultParas.Count - 1; i >= 0; i--) { - if (!resultParas[i].ChildElements.OfType().Any() && resultParas[i].ParagraphProperties is null) + var p = resultParas[i]; + if (!p.ChildElements.OfType().Any() && p.ParagraphProperties is null) 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}"); - } + // Логирование +#if DEBUG + if (body is not null) + LogDocumentStructure(body, "FINAL DOCUMENT STRUCTURE"); +#endif 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