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
{