From 9c81a084f1cc5caa87a8320dc9c630b4edf76119 Mon Sep 17 00:00:00 2001 From: melekhin Date: Mon, 10 Aug 2026 13:58:11 +0700 Subject: [PATCH] ROTATES 2 --- QWERTYkez.WordProcessor/MultiReplace.cs | 621 +++++++++++++++-------- QWERTYkez.WordProcessor/ReplaceItem.cs | 75 +-- QWERTYkez.WordProcessor/SimplyReplace.cs | 24 +- 3 files changed, 452 insertions(+), 268 deletions(-) diff --git a/QWERTYkez.WordProcessor/MultiReplace.cs b/QWERTYkez.WordProcessor/MultiReplace.cs index 00bae0e..766e3ed 100644 --- a/QWERTYkez.WordProcessor/MultiReplace.cs +++ b/QWERTYkez.WordProcessor/MultiReplace.cs @@ -2,10 +2,7 @@ /// /// Выполняет замену всех вхождений ключей из словаря на соответствующие массивы значений. -/// Каждое значение из массива помещается в отдельный параграф, причём первое значение -/// остаётся в текущем параграфе, а последующие создают новые. -/// Текст между вхождениями и после последнего сохраняется в соответствующих параграфах. -/// Поддерживает разрывы страниц и смену ориентации (альбомная/книжная) через . +/// Поддерживает разрывы страниц и смену ориентации (альбомная/книжная) через . /// internal static class MultiReplaceExt { @@ -16,13 +13,15 @@ internal static class MultiReplaceExt internal static void Replace(this Body body, string oldValue, IEnumerable newValues, StringComparison comparisonType) { if (body is null || string.IsNullOrEmpty(oldValue) || newValues is null) return; - body.Replace(new Dictionary> { { oldValue, newValues } }, comparisonType); + var dict = new Dictionary> { { oldValue, newValues } }; + body.Replace(dict, comparisonType); } internal static void Replace(this Body body, string oldValue, IEnumerable newValues, StringComparison comparisonType) { if (body is null || string.IsNullOrEmpty(oldValue) || newValues is null) return; - body.Replace(new Dictionary> { { oldValue, newValues } }, comparisonType); + var dict = new Dictionary> { { oldValue, newValues } }; + body.Replace(dict, comparisonType); } #endregion @@ -32,29 +31,27 @@ internal static class MultiReplaceExt internal static void Replace(this Body body, IEnumerable>> replacements, StringComparison comparisonType) { if (body is null || replacements is null) return; - ReplaceInBody(body, replacements, null, comparisonType); - } - - internal static void Replace(this Body body, IEnumerable>> replacements, StringComparison comparisonType) - { - if (body is null || replacements is null) return; - ReplaceInBody(body, null, replacements, comparisonType); - } - - private static void ReplaceInBody( - Body body, - IEnumerable>>? stringReplacements, - IEnumerable>>? itemReplacements, - StringComparison comparisonType) - { var paragraphs = body.Elements().ToList(); for (int i = paragraphs.Count - 1; i >= 0; i--) { var p = paragraphs[i]; if (p?.Parent is null) continue; + var newParas = ProcessMultiReplacements(p, replacements, null, comparisonType); + if (newParas is not null && newParas.Count > 0) + ParagraphReplacer.ReplaceParagraph(p, newParas); + } + } - var newParas = ProcessMultiReplacements(p, stringReplacements, itemReplacements, comparisonType); - if (newParas is { Count: > 0 }) + internal static void Replace(this Body body, IEnumerable>> replacements, StringComparison comparisonType) + { + if (body is null || replacements is null) return; + var paragraphs = body.Elements().ToList(); + for (int i = paragraphs.Count - 1; i >= 0; i--) + { + var p = paragraphs[i]; + if (p?.Parent is null) continue; + var newParas = ProcessMultiReplacements(p, null, replacements, comparisonType); + if (newParas is not null && newParas.Count > 0) ParagraphReplacer.ReplaceParagraph(p, newParas); } } @@ -65,13 +62,14 @@ internal static class MultiReplaceExt internal static bool ReplaceWithMultiple(this Paragraph? paragraph, string oldValue, IEnumerable newValues, StringComparison comparisonType) { - if (paragraph?.Parent is null || string.IsNullOrEmpty(oldValue) || newValues is null || !newValues.Any()) + if (paragraph is null || string.IsNullOrEmpty(oldValue) || newValues is null || newValues.Count() == 0) return false; var dict = new Dictionary> { { oldValue, newValues } }; var newParas = ProcessMultiReplacements(paragraph, dict, null, comparisonType); + if (newParas is null || newParas.Count == 0) return false; - if (newParas is { Count: > 0 }) + if (paragraph.Parent is not null) { ParagraphReplacer.ReplaceParagraph(paragraph, newParas); return true; @@ -81,13 +79,14 @@ internal static class MultiReplaceExt internal static bool ReplaceWithMultiple(this Paragraph? paragraph, string oldValue, IEnumerable newValues, StringComparison comparisonType) { - if (paragraph?.Parent is null || string.IsNullOrEmpty(oldValue) || newValues is null || !newValues.Any()) + if (paragraph is null || string.IsNullOrEmpty(oldValue) || newValues is null || newValues.Count() == 0) return false; var dict = new Dictionary> { { oldValue, newValues } }; var newParas = ProcessMultiReplacements(paragraph, null, dict, comparisonType); + if (newParas is null || newParas.Count == 0) return false; - if (newParas is { Count: > 0 }) + if (paragraph.Parent is not null) { ParagraphReplacer.ReplaceParagraph(paragraph, newParas); return true; @@ -103,23 +102,26 @@ internal static class MultiReplaceExt Paragraph paragraph, IEnumerable>> replacements, StringComparison comparisonType) - => ProcessMultiReplacements(paragraph, replacements, null, comparisonType); + { + return ProcessMultiReplacements(paragraph, replacements, null, comparisonType); + } internal static List? ProcessParagraphWithAllReplacements( Paragraph paragraph, IEnumerable>> replacements, StringComparison comparisonType) - => ProcessMultiReplacements(paragraph, null, replacements, comparisonType); + { + return ProcessMultiReplacements(paragraph, null, replacements, comparisonType); + } internal static List? ProcessParagraphWithAllReplacements( Paragraph paragraph, IEnumerable> replacements, StringComparison comparisonType) { - var dict = replacements + Dictionary> dict = replacements .Where(kvp => !string.IsNullOrEmpty(kvp.Key)) .ToDictionary(kvp => kvp.Key, kvp => (IEnumerable)[kvp.Value]); - return ProcessMultiReplacements(paragraph, dict, null, comparisonType); } @@ -135,9 +137,9 @@ internal static class MultiReplaceExt private class Match { - public MatchDefinition Definition { get; init; } = null!; - public int Start { get; init; } - public int End { get; init; } + public MatchDefinition Definition { get; set; } = null!; + public int Start { get; set; } + public int End { get; set; } } private class RunSegment(Run run, string text, int start, int end) @@ -156,15 +158,13 @@ internal static class MultiReplaceExt private static ParagraphStructure AnalyzeParagraphStructure(List runs) { - var segments = new List(runs.Count); + var segments = new List(); var sb = new StringBuilder(); int pos = 0; - foreach (var run in runs) { string text = GetRunText(run); if (string.IsNullOrEmpty(text)) continue; - segments.Add(new RunSegment(run, text, pos, pos + text.Length)); sb.Append(text); pos += text.Length; @@ -184,18 +184,16 @@ internal static class MultiReplaceExt private static Paragraph CloneParagraphWithoutSection(Paragraph original) { var newPara = new Paragraph(); - if (original.ParagraphProperties is null) return newPara; - - var newProps = new ParagraphProperties(); - foreach (var child in original.ParagraphProperties.ChildElements) + if (original.ParagraphProperties is not null) { - if (child is not SectionProperties and not PageBreakBefore) - newProps.AppendChild(child.CloneNode(true)); - } - - if (newProps.HasChildren) + var newProps = new ParagraphProperties(); + foreach (var child in original.ParagraphProperties.ChildElements) + { + if (child is not SectionProperties && child is not PageBreakBefore) + newProps.AppendChild(child.CloneNode(true)); + } newPara.ParagraphProperties = newProps; - + } return newPara; } @@ -207,29 +205,28 @@ internal static class MultiReplaceExt foreach (var seg in structure.Segments) { if (seg.End <= position) continue; - if (seg.Start >= position) { newPara.AppendChild(seg.Run.CloneNode(true)); } - else + else if (seg.End > position) { var runClone = (Run)seg.Run.CloneNode(true); - runClone.RemoveAllChildren(); // Упрощенная очистка - + foreach (var t in runClone.Elements().ToList()) t.Remove(); int offset = position - seg.Start; - runClone.AppendChild(new Text(seg.Text.Substring(offset))); + string newText = seg.Text.Substring(offset); + runClone.AppendChild(new Text(newText)); newPara.AppendChild(runClone); } } - - // Удаляем пустые Run-ы без вложений - newPara.Elements().Where(r => !r.HasChildren).ToList().ForEach(r => r.Remove()); - - return !newPara.Elements().Any() && newPara.ParagraphProperties is null ? null : newPara; + 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 void InsertFormattedRun(Paragraph para, ReplaceItem item, ParagraphStructure structure, int position) + private static void InsertFormattedRun(Paragraph para, string text, ParagraphStructure structure, int position) { var seg = structure.Segments.FirstOrDefault(s => position >= s.Start && position < s.End); if (seg is null) return; @@ -237,89 +234,178 @@ internal static class MultiReplaceExt var textRun = new Run(); if (seg.Run.RunProperties is not null) textRun.RunProperties = (RunProperties)seg.Run.RunProperties.CloneNode(true); - - textRun.AppendChild(new Text(item.Text ?? string.Empty)); + textRun.AppendChild(new Text(text)); para.AppendChild(textRun); } /// /// Добавляет SectionProperties к параграфу. Все значения (PageSize, PageMargin) берутся из документа. + /// Для книжных секций (addPageSize=false) Orient не устанавливается (not set). /// - private static void AddSectionProperties(Paragraph para, PageBreakType splitValue, bool addPageSize, SectionProperties? sourceSection, SectionProperties? portraitSection) + private static void AddSectionProperties( + Paragraph para, + BreakType splitValue, + bool addPageSize, + SectionProperties sourceSection, + SectionProperties portraitSection) { if (para is null) return; - // Удаляем существующие секции и разрывы if (para.ParagraphProperties is not null) { - para.ParagraphProperties.RemoveAllChildren(); - para.ParagraphProperties.RemoveAllChildren(); + 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 - var sizeSource = addPageSize ? sourceSection : portraitSection; - var sourcePageSize = sizeSource?.GetFirstChild() - ?? portraitSection?.GetFirstChild() - ?? 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(); - pageSizeClone.Orient = addPageSize ? PageOrientationValues.Portrait : null; - } - else - { - pageSizeClone.Orient = targetIsLandscape ? PageOrientationValues.Landscape : (addPageSize ? PageOrientationValues.Portrait : null); - } + var pageSizeClone = CreatePageSizeClone(addPageSize, splitValue, sourceSection, portraitSection); + if (pageSizeClone is not null) 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 sourceIsLndscape = sourceSection?.GetFirstChild()?.Orient?.Value == PageOrientationValues.Landscape || - sourceSection?.GetFirstChild()?.Width?.Value > sourceSection?.GetFirstChild()?.Height?.Value; - - if ((splitValue == PageBreakType.NewLandscapeSection && !sourceIsLndscape) || - (splitValue == PageBreakType.NewPortraitSection && sourceIsLndscape)) - { - marginClone.SwapBottomRight(); - } - marginToUse = marginClone; - } - } - else if (portraitSection?.GetFirstChild() is { } portraitMargin) - { - marginToUse = (PageMargin)portraitMargin.CloneNode(true); - } - - if (marginToUse is not null) - sectionProps.AppendChild(marginToUse); + var marginClone = CreateMarginClone(addPageSize, splitValue, sourceSection, portraitSection); + if (marginClone is not null) + sectionProps.AppendChild(marginClone); sectionProps.AppendChild(new SectionType { Val = SectionMarkValues.NextPage }); para.ParagraphProperties.AppendChild(sectionProps); } + private static PageSize? CreatePageSizeClone( + bool addPageSize, + BreakType splitValue, + SectionProperties sourceSection, + SectionProperties portraitSection) + { + if (!addPageSize && splitValue != BreakType.NewLandscapeSection) + return null; + + SectionProperties? sizeSource = addPageSize ? sourceSection : portraitSection; + var sourcePageSize = sizeSource?.GetFirstChild() + ?? portraitSection.GetFirstChild() + ?? sourceSection.GetFirstChild(); + + if (sourcePageSize is null) + return null; + + var clone = (PageSize)sourcePageSize.CloneNode(true); + bool sourceIsLandscape = clone.Orient?.Value == PageOrientationValues.Landscape || + (clone.Width?.Value > clone.Height?.Value); + + bool targetIsLandscape = addPageSize && splitValue == BreakType.NewLandscapeSection; + + if (targetIsLandscape && !sourceIsLandscape) + { + clone.SwapValues(); + clone.Orient = PageOrientationValues.Landscape; + } + else if (!targetIsLandscape && sourceIsLandscape) + { + clone.SwapValues(); + clone.Orient = addPageSize ? PageOrientationValues.Portrait : null; + } + else + { + clone.Orient = targetIsLandscape ? PageOrientationValues.Landscape : + addPageSize ? PageOrientationValues.Portrait : null; + } + + return clone; + } + + private static PageMargin? CreateMarginClone( + bool addPageSize, + BreakType splitValue, + SectionProperties sourceSection, + SectionProperties portraitSection) + { + PageMargin? marginToUse = null; + + if (addPageSize) + { + var sourceMargin = sourceSection?.GetFirstChild() + ?? portraitSection?.GetFirstChild(); + if (sourceMargin is not null) + { + var clone = (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 == BreakType.NewLandscapeSection && !sourceIsLandscape) || + (splitValue == BreakType.NewPortraitSection && sourceIsLandscape)) + { + clone.SwapBottomRight(); + } + marginToUse = clone; + } + } + else + { + var portraitMargin = portraitSection?.GetFirstChild(); + if (portraitMargin is not null) + marginToUse = (PageMargin)portraitMargin.CloneNode(true); + } + + return marginToUse; + } + + private static void EnsureBodyLandscapeSection( + Body body, + SectionProperties sourceSection, + bool sectionChangeInsideGroup, + BreakType? lastOrientation) + { + if (body is null || sourceSection is null) return; + + var sourcePageSize = sourceSection.GetFirstChild(); + if (sourcePageSize is null) return; + + bool sourceIsLandscape = sourcePageSize.Orient?.Value == PageOrientationValues.Landscape || + (sourcePageSize.Width?.Value > sourcePageSize.Height?.Value); + bool lastIsPortrait = lastOrientation.HasValue && lastOrientation.Value == BreakType.NewPortraitSection; + + if (!(sourceIsLandscape && sectionChangeInsideGroup && lastIsPortrait)) + return; + + foreach (var bodySec in body.Elements()) + { + var ps = bodySec.GetFirstChild(); + if (ps is not null && ps.Orient?.Value == PageOrientationValues.Landscape) + return; + } + + var bodySection = new SectionProperties(); + var pageSizeClone = (PageSize)sourcePageSize.CloneNode(true); + 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); + if (sourceSection.GetFirstChild()?.Orient?.Value != PageOrientationValues.Landscape) + marginClone.SwapBottomRight(); + bodySection.AppendChild(marginClone); + } + + bodySection.AppendChild(new SectionType { Val = SectionMarkValues.NextPage }); + body.AppendChild(bodySection); + } + + 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, @@ -327,61 +413,89 @@ internal static class MultiReplaceExt IEnumerable>>? itemReplacements, StringComparison comparisonType) { - var definitions = new List(); + var body = original.Ancestors().FirstOrDefault(); + // 1. Сбор определений + var definitions = new List(); if (stringReplacements is not null) { - definitions.AddRange(stringReplacements - .Where(kvp => !string.IsNullOrEmpty(kvp.Key) && kvp.Value != null && kvp.Value.Any()) - .Select(kvp => new MatchDefinition(kvp.Key, kvp.Value.Select(v => new ReplaceItem(v, PageBreakType.None))))); + foreach (var kvp in stringReplacements) + { + if (string.IsNullOrEmpty(kvp.Key) || kvp.Value is null || kvp.Value.Count() == 0) continue; + var items = kvp.Value.Select(v => new ReplaceItem(v)); + definitions.Add(new MatchDefinition(kvp.Key, items)); + } } - if (itemReplacements is not null) { - definitions.AddRange(itemReplacements - .Where(kvp => !string.IsNullOrEmpty(kvp.Key) && kvp.Value != null && kvp.Value.Any()) - .Select(kvp => new MatchDefinition(kvp.Key, kvp.Value))); + foreach (var kvp in itemReplacements) + { + if (string.IsNullOrEmpty(kvp.Key) || kvp.Value is null || kvp.Value.Count() == 0) continue; + definitions.Add(new MatchDefinition(kvp.Key, kvp.Value)); + } } - if (definitions.Count == 0) return null; - var runs = original.Elements().ToList(); + // 2. Анализ структуры параграфа + var runs = original.Descendants().ToList(); if (runs.Count == 0) return null; - var structure = AnalyzeParagraphStructure(runs); string fullText = structure.FullText; if (fullText.Length == 0) return null; + // 3. Поиск всех вхождений var matches = new List(); foreach (var def in definitions) { int pos = 0; while ((pos = fullText.IndexOf(def.Key, pos, comparisonType)) != -1) { - matches.Add(new Match { Definition = def, Start = pos, End = pos + def.Key.Length }); + matches.Add(new Match + { + Definition = def, + Start = pos, + End = pos + def.Key.Length + }); pos += def.Key.Length; } } - if (matches.Count == 0) return null; matches.Sort((a, b) => a.Start.CompareTo(b.Start)); - var body = original.Ancestors().FirstOrDefault(); - var sourceSection = original.ParagraphProperties?.GetFirstChild()?.CloneNode(true) as SectionProperties - ?? body?.Elements().LastOrDefault()?.CloneNode(true) as SectionProperties - ?? new SectionProperties(); + // 4. Получаем исходную секцию для копирования (из original или из документа) + SectionProperties? sourceSection = original.ParagraphProperties?.GetFirstChild()?.CloneNode(true) as SectionProperties; + if (sourceSection is null && body is not null) + { + sourceSection = body.Elements().LastOrDefault()?.CloneNode(true) as SectionProperties; + } + sourceSection ??= new SectionProperties(); - var portraitSection = body?.Elements().FirstOrDefault()?.CloneNode(true) as SectionProperties - ?? 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; bool sectionChangeInsideGroup = false; - PageBreakType? lastOrientation = null; + BreakType? lastOrientation = null; - foreach (var match in matches) + // Состояние обработки группы + bool pageBreakBeforeNext = false; + BreakType? pendingOrientation = null; + bool firstSplitMarkerHandled = false; + int textCount = 0; + Paragraph? lastTextPara = null; + + for (int i = 0; i < matches.Count; i++) { + var match = matches[i]; + + // Текст перед совпадением if (currentPos < match.Start) { var beforePara = CloneParagraphWithoutSection(original); @@ -389,7 +503,6 @@ internal static class MultiReplaceExt { if (seg.End <= currentPos) continue; if (seg.Start >= match.Start) break; - if (seg.Start >= currentPos && seg.End <= match.Start) { beforePara.AppendChild(seg.Run.CloneNode(true)); @@ -398,99 +511,201 @@ internal static class MultiReplaceExt { 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); - runClone.RemoveAllChildren(); - runClone.AppendChild(new Text(seg.Text.Substring(startOffset, endOffset - startOffset))); + 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.Elements().Any()) + if (beforePara.ChildElements.OfType().Any()) { resultParas.Add(beforePara); currentPara = beforePara; + lastTextPara = beforePara; } } var values = match.Definition.Values.ToList(); if (values.Count == 0) continue; - for (int vIdx = 0; vIdx < values.Count; vIdx++) + // Сбрасываем флаги для новой группы + pageBreakBeforeNext = false; + pendingOrientation = null; + firstSplitMarkerHandled = false; + textCount = 0; + lastTextPara = null; + + foreach (var item in values) { - var item = values[vIdx]; - currentPara = CloneParagraphWithoutSection(original); - resultParas.Add(currentPara); - - InsertFormattedRun(currentPara, item, structure, match.Start); - - if (item.SplitValue is PageBreakType.NewLandscapeSection or PageBreakType.NewPortraitSection) + if (!string.IsNullOrEmpty(item.Text)) { - bool addPageSize = vIdx != 0; - var orientation = vIdx != 0 ? PageBreakType.NewLandscapeSection : item.SplitValue; - - AddSectionProperties(currentPara, orientation, addPageSize, sourceSection, portraitSection); - lastOrientation = item.SplitValue; - sectionChangeInsideGroup = true; - } - 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?.Run.RunProperties is not null) - breakRun.RunProperties = (RunProperties)seg.Run.RunProperties.CloneNode(true); - currentPara.AppendChild(breakRun); - } - } - - if (sectionChangeInsideGroup && lastOrientation.HasValue && currentPara?.ParagraphProperties?.GetFirstChild() is null) - { - AddSectionProperties(currentPara!, lastOrientation.Value, lastOrientation.Value == PageBreakType.NewLandscapeSection, sourceSection, portraitSection); - } - - if (body is not null && sourceSection.GetFirstChild() is { } sourcePageSize) - { - bool sourceIsLandscape = sourcePageSize.Orient?.Value == PageOrientationValues.Landscape || sourcePageSize.Width?.Value > sourcePageSize.Height?.Value; - bool lastIsPortrait = lastOrientation == PageBreakType.NewPortraitSection; - - if (sourceIsLandscape && sectionChangeInsideGroup && lastIsPortrait && !body.Elements().Any(s => s.GetFirstChild()?.Orient?.Value == PageOrientationValues.Landscape)) - { - var bodySection = new SectionProperties(); - var pageSizeClone = (PageSize)sourcePageSize.CloneNode(true); - - if (pageSizeClone.Orient?.Value != PageOrientationValues.Landscape) + // Если есть отложенная ориентация — применяем её + if (pendingOrientation.HasValue) { - pageSizeClone.SwapValues(); - pageSizeClone.Orient = PageOrientationValues.Landscape; - } - bodySection.AppendChild(pageSizeClone); + var newPara = CloneParagraphWithoutSection(original); + resultParas.Add(newPara); + currentPara = newPara; - if (sourceSection.GetFirstChild() is { } sourceMargin) - { - var marginClone = (PageMargin)sourceMargin.CloneNode(true); - if (sourceSection.GetFirstChild()?.Orient?.Value != PageOrientationValues.Landscape) + bool addPageSize = (pendingOrientation.Value == BreakType.NewLandscapeSection); + AddSectionProperties(currentPara, pendingOrientation.Value, addPageSize, sourceSection, portraitSection); + lastOrientation = pendingOrientation; + sectionChangeInsideGroup = true; + pendingOrientation = null; + textCount++; + lastTextPara = currentPara; + + if (pageBreakBeforeNext) { - marginClone.SwapBottomRight(); + var breakRun = new Run(new Break { Type = BreakValues.Page }); + currentPara.InsertAt(breakRun, 0); + pageBreakBeforeNext = false; + } + + InsertFormattedRun(currentPara, item.Text, structure, match.Start); + } + else + { + var newPara = CloneParagraphWithoutSection(original); + resultParas.Add(newPara); + currentPara = newPara; + textCount++; + lastTextPara = currentPara; + + if (pageBreakBeforeNext) + { + var breakRun = new Run(new Break { Type = BreakValues.Page }); + currentPara.InsertAt(breakRun, 0); + pageBreakBeforeNext = false; + } + + InsertFormattedRun(currentPara, item.Text, structure, match.Start); + } + } + else if (item.BreakValue.HasValue) + { + var breakType = item.BreakValue.Value; + if (breakType == BreakType.PageBreak) + { + if (currentPara is not null) + { + var breakRun = new Run(new Break { Type = BreakValues.Page }); + var lastRun = currentPara.Descendants().LastOrDefault(); + if (lastRun?.RunProperties is not null) + breakRun.RunProperties = (RunProperties)lastRun.RunProperties.CloneNode(true); + currentPara.AppendChild(breakRun); + } + else + { + pageBreakBeforeNext = true; + } + } + else if (breakType == BreakType.NewLandscapeSection || breakType == BreakType.NewPortraitSection) + { + if (currentPara is null) + { + var newPara = CloneParagraphWithoutSection(original); + resultParas.Add(newPara); + currentPara = newPara; + AddSectionProperties(currentPara, breakType, false, sourceSection, portraitSection); + lastOrientation = breakType; + sectionChangeInsideGroup = true; + firstSplitMarkerHandled = true; + pendingOrientation = null; + textCount = 0; + lastTextPara = null; + } + else + { + if (!firstSplitMarkerHandled) + { + if (lastTextPara is not null) + { + if (lastTextPara.ParagraphProperties?.GetFirstChild() is null) + { + lastTextPara.ParagraphProperties ??= new ParagraphProperties(); + + var sourcePageSize = sourceSection.GetFirstChild(); + if (sourcePageSize is not null) + { + lastTextPara.ParagraphProperties.AppendChild(sourceSection.CloneNode(true)); + } + else + { + AddSectionProperties(lastTextPara, breakType, false, sourceSection, portraitSection); + } + lastOrientation = breakType; + sectionChangeInsideGroup = true; + } + firstSplitMarkerHandled = true; + pendingOrientation = null; + } + else + { + AddSectionProperties(currentPara, breakType, false, sourceSection, portraitSection); + lastOrientation = breakType; + sectionChangeInsideGroup = true; + firstSplitMarkerHandled = true; + pendingOrientation = null; + } + } + else + { + pendingOrientation = breakType; + } } - bodySection.AppendChild(marginClone); } - bodySection.AppendChild(new SectionType { Val = SectionMarkValues.NextPage }); - body.AppendChild(bodySection); } } + + // Закрываем секцию, если была смена + if (sectionChangeInsideGroup && lastOrientation.HasValue && currentPara is not null) + { + if (currentPara.ParagraphProperties?.GetFirstChild() is null) + { + bool addPageSize = (lastOrientation.Value == BreakType.NewLandscapeSection); + AddSectionProperties(currentPara, lastOrientation.Value, addPageSize, sourceSection, portraitSection); + } + } + + EnsureBodyLandscapeSection(body!, sourceSection, sectionChangeInsideGroup, lastOrientation); + currentPos = match.End; } - if (currentPos < fullText.Length && BuildRemainderParagraph(original, structure, currentPos) is { } remainderPara) + // 6. Обработка остатка текста + if (currentPos < fullText.Length) { - if (sectionChangeInsideGroup && lastOrientation.HasValue) + var remainderPara = BuildRemainderParagraph(original, structure, currentPos); + if (remainderPara is not null) { - remainderPara.InsertAt(new Run(new Break { Type = BreakValues.Page }), 0); - AddSectionProperties(remainderPara, lastOrientation.Value, lastOrientation.Value == PageBreakType.NewLandscapeSection, sourceSection, portraitSection); + if (pendingOrientation.HasValue) + { + var breakRun = new Run(new Break { Type = BreakValues.Page }); + remainderPara.InsertAt(breakRun, 0); + bool addPageSize = (pendingOrientation.Value == BreakType.NewLandscapeSection); + AddSectionProperties(remainderPara, pendingOrientation.Value, addPageSize, sourceSection, portraitSection); + pendingOrientation = null; + } + else if (sectionChangeInsideGroup && lastOrientation.HasValue) + { + var breakRun = new Run(new Break { Type = BreakValues.Page }); + remainderPara.InsertAt(breakRun, 0); + bool addPageSize = (lastOrientation.Value == BreakType.NewLandscapeSection); + AddSectionProperties(remainderPara, lastOrientation.Value, addPageSize, sourceSection, portraitSection); + } + resultParas.Add(remainderPara); } - resultParas.Add(remainderPara); } - resultParas.RemoveAll(p => !p.Elements().Any() && p.ParagraphProperties is null); + // 7. Очистка пустых параграфов + for (int i = resultParas.Count - 1; i >= 0; i--) + { + var p = resultParas[i]; + if (!p.ChildElements.OfType().Any() && p.ParagraphProperties is null) + resultParas.RemoveAt(i); + } + return resultParas.Count > 0 ? resultParas : null; } } \ No newline at end of file diff --git a/QWERTYkez.WordProcessor/ReplaceItem.cs b/QWERTYkez.WordProcessor/ReplaceItem.cs index 09a8a1a..8451c5a 100644 --- a/QWERTYkez.WordProcessor/ReplaceItem.cs +++ b/QWERTYkez.WordProcessor/ReplaceItem.cs @@ -1,70 +1,39 @@ namespace QWERTYkez.WordProcessor; -/// -/// Определяет тип разрыва или смены ориентации страницы, применяется к элементам следующим после замены -/// -public enum PageBreakType +/// Определяет тип разрыва или смены ориентации страницы +public enum BreakType { - /// Без разрыва или смены ориентации. - None, - /// Обычный разрыв страницы (новый лист). PageBreak, - /// Начать новую секцию с альбомной ориентацией страницы. NewLandscapeSection, - /// Начать новую секцию с книжной ориентацией страницы. NewPortraitSection, } -/// -/// Представляет элемент замены текста, содержащий сам текст и указание -/// на тип разрыва или смены ориентации, который должен быть применён -/// после вставки этого текста. -/// -/// -/// Используется в методах множественной замены, например, -/// . -/// -public readonly struct ReplaceItem +public class ReplaceItem { - /// - /// Инициализирует новый экземпляр с пустым текстом - /// и типом разрыва . - /// - public ReplaceItem() { } + private ReplaceItem() { } + public ReplaceItem(string text) => _Text = text; + public ReplaceItem(BreakType item) => _BreakValue = item; - /// - /// Инициализирует новый экземпляр с указанным текстом - /// и типом разрыва/смены ориентации. - /// - /// Текст, который будет вставлен вместо плейсхолдера. - /// - /// Тип разрыва или смены ориентации, который будет применён после вставки текста. - /// По умолчанию . - /// - public ReplaceItem(string text, PageBreakType splitValue = PageBreakType.None) - { - Text = text; - SplitValue = splitValue; - } - /// - /// Получает текст, который будет вставлен вместо плейсхолдера. - /// - public string Text { get; init; } = string.Empty; + public static implicit operator ReplaceItem(string text) => new(text); + public static implicit operator ReplaceItem(BreakType item) => item switch + { + BreakType.PageBreak => PageBreak, + BreakType.NewLandscapeSection => NewLandscapeSection, + BreakType.NewPortraitSection => NewPortraitSection, + _ => throw new NotImplementedException() + }; - /// - /// Получает тип разрыва или смены ориентации, который будет применён - /// после вставки текста. - /// - public PageBreakType SplitValue { get; init; } = PageBreakType.None; + public static ReplaceItem PageBreak { get; } = new() { _BreakValue = BreakType.PageBreak }; + public static ReplaceItem NewLandscapeSection { get; } = new() { _BreakValue = BreakType.NewLandscapeSection }; + public static ReplaceItem NewPortraitSection { get; } = new() { _BreakValue = BreakType.NewPortraitSection }; - /// - /// Определяет явное преобразование из строки в . - /// - /// Строка текста. - /// Новый экземпляр с указанным текстом и . - public static explicit operator ReplaceItem(string text) => new() { Text = text }; + public string Text => _Text; + public string _Text = string.Empty; + + public BreakType? BreakValue => _BreakValue; + public BreakType? _BreakValue; } \ No newline at end of file diff --git a/QWERTYkez.WordProcessor/SimplyReplace.cs b/QWERTYkez.WordProcessor/SimplyReplace.cs index 40b7aa0..c0790b7 100644 --- a/QWERTYkez.WordProcessor/SimplyReplace.cs +++ b/QWERTYkez.WordProcessor/SimplyReplace.cs @@ -77,7 +77,7 @@ internal static class SimplyReplaceExt } } - internal static bool SimpleReplace(this Paragraph? paragraph, string oldValue, string newValue, StringComparison comparisonType, PageBreakType splitValue = PageBreakType.None) + internal static bool SimpleReplace(this Paragraph? paragraph, string oldValue, string newValue, StringComparison comparisonType, BreakType? splitValue = null) { if (paragraph is null || string.IsNullOrEmpty(oldValue)) return false; @@ -108,7 +108,7 @@ internal static class SimplyReplaceExt } 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); + => SimpleReplace(paragraph, oldValue, newValue, comparisonType, breakPage ? BreakType.PageBreak : null); internal static void Replace(this Paragraph paragraph, IEnumerable> replacements, StringComparison comparisonType) { @@ -139,7 +139,7 @@ internal static class SimplyReplaceExt OldValue = kvp.Key, NewValue = kvp.Value ?? string.Empty, Index = pos, - SplitValue = PageBreakType.None + BreakValue = null }); pos += kvp.Key.Length; } @@ -159,7 +159,7 @@ internal static class SimplyReplaceExt var nodesToReplace = FindNodesToReplace(structure, matchIndex, matchEnd); if (nodesToReplace.Count > 0) { - ExecuteReplacement(nodesToReplace, matchIndex, matchEnd, replacement.NewValue, replacement.SplitValue); + ExecuteReplacement(nodesToReplace, matchIndex, matchEnd, replacement.NewValue, replacement.BreakValue); } } } @@ -193,7 +193,7 @@ internal static class SimplyReplaceExt OldValue = kvp.Key, NewValue = kvp.Value.Text ?? string.Empty, Index = pos, - SplitValue = kvp.Value.SplitValue + BreakValue = kvp.Value.BreakValue }); pos += kvp.Key.Length; } @@ -213,7 +213,7 @@ internal static class SimplyReplaceExt var nodesToReplace = FindNodesToReplace(structure, matchIndex, matchEnd); if (nodesToReplace.Count > 0) { - ExecuteReplacement(nodesToReplace, matchIndex, matchEnd, replacement.NewValue, replacement.SplitValue); + ExecuteReplacement(nodesToReplace, matchIndex, matchEnd, replacement.NewValue, replacement.BreakValue); } } } @@ -223,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 PageBreakType SplitValue { get; set; } = PageBreakType.None; + internal BreakType? BreakValue { get; set; } = null; } private static ParagraphStructure AnalyzeParagraphStructure(IEnumerable runs) @@ -287,7 +287,7 @@ internal static class SimplyReplaceExt int matchStart, int matchEnd, string newValue, - PageBreakType splitValue) + BreakType? splitValue) { if (nodesToReplace.Count == 0) return; @@ -312,7 +312,7 @@ internal static class SimplyReplaceExt nodesToReplace[i].Text.Text = string.Empty; } - if (splitValue == PageBreakType.PageBreak) + if (splitValue == BreakType.PageBreak) { if (nodesToReplace[0].Text.Parent is Run run && run.Parent is Paragraph para) { @@ -322,7 +322,7 @@ internal static class SimplyReplaceExt para.AppendChild(breakRun); } } - else if (splitValue == PageBreakType.NewLandscapeSection || splitValue == PageBreakType.NewPortraitSection) + else if (splitValue == BreakType.NewLandscapeSection || splitValue == BreakType.NewPortraitSection) { var firstText = nodesToReplace[0].Text; if (firstText.Parent is Run run && run.Parent is Paragraph para) @@ -332,10 +332,10 @@ internal static class SimplyReplaceExt } } - private static void AddSectionProperties(Paragraph para, PageBreakType splitValue) + private static void AddSectionProperties(Paragraph para, BreakType? splitValue) { if (para is null) return; - PageOrientationValues orientation = splitValue == PageBreakType.NewLandscapeSection + PageOrientationValues orientation = splitValue == BreakType.NewLandscapeSection ? PageOrientationValues.Landscape : PageOrientationValues.Portrait;